mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-12 18:04:01 +00:00
few more changes
This commit is contained in:
-82
@@ -859,88 +859,6 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeChunkNbtDirect(NBTWorld nbtWorld, int chunkX, int chunkZ, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes) {
|
||||
try {
|
||||
art.arcane.iris.util.nbt.common.mca.MCAFile mca = nbtWorld.getMCA(chunkX >> 5, chunkZ >> 5);
|
||||
if (mca == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
art.arcane.iris.util.nbt.common.mca.Chunk chunk = art.arcane.iris.util.nbt.common.mca.Chunk.newChunk();
|
||||
art.arcane.iris.util.nbt.common.mca.Chunk.injectIrisData(chunk);
|
||||
|
||||
int blockHeight = blocks.getHeight();
|
||||
java.util.IdentityHashMap<BlockData, CompoundTag> encodeCache = new java.util.IdentityHashMap<>(64);
|
||||
for (int y = 0; y < blockHeight; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
PlatformBlockState platformState = blocks.getRaw(x, y, z);
|
||||
if (platformState == null) {
|
||||
continue;
|
||||
}
|
||||
Object nativeHandle = platformState.nativeHandle();
|
||||
if (!(nativeHandle instanceof BlockData blockData)) {
|
||||
continue;
|
||||
}
|
||||
if (blockData instanceof IrisCustomData customData) {
|
||||
blockData = customData.getBase();
|
||||
}
|
||||
if (blockData.getMaterial().isAir()) {
|
||||
continue;
|
||||
}
|
||||
CompoundTag nbtState = encodeCache.get(blockData);
|
||||
if (nbtState == null) {
|
||||
nbtState = NBTWorld.getCompound(blockData);
|
||||
if (nbtState == null) {
|
||||
continue;
|
||||
}
|
||||
encodeCache.put(blockData, nbtState);
|
||||
}
|
||||
chunk.setBlockStateAt(x, y, z, nbtState, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int biomeHeight = biomes.getHeight();
|
||||
for (int by = 0; by < biomeHeight; by += 4) {
|
||||
int quartY = by >> 2;
|
||||
for (int bz = 0; bz < 16; bz += 4) {
|
||||
int quartZ = bz >> 2;
|
||||
for (int bx = 0; bx < 16; bx += 4) {
|
||||
int quartX = bx >> 2;
|
||||
PlatformBiome platformBiome = biomes.getRaw(bx, by, bz);
|
||||
if (platformBiome == null) {
|
||||
continue;
|
||||
}
|
||||
String biomeKey = platformBiome.key();
|
||||
if (biomeKey == null || biomeKey.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int biomeId = getBiomeBaseIdForKey(biomeKey);
|
||||
if (biomeId < 0) {
|
||||
continue;
|
||||
}
|
||||
chunk.setBiomeAt(quartX, quartY, quartZ, biomeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chunk.cleanupPalettesAndBlockStates();
|
||||
|
||||
synchronized (mca) {
|
||||
mca.setChunk(chunkX & 31, chunkZ & 31, chunk);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.warn("writeChunkNbtDirect failed at " + chunkX + "," + chunkZ + ": " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||
e.printStackTrace(System.err);
|
||||
IrisLogging.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceEvictChunk(World world, int chunkX, int chunkZ) {
|
||||
try {
|
||||
|
||||
@@ -39,6 +39,7 @@ import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.core.gui.BukkitGuiHost;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.service.EditSVC;
|
||||
import art.arcane.iris.core.service.PreservationSVC;
|
||||
@@ -947,6 +948,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
public void onEnable() {
|
||||
IrisPlatforms.bind(new BukkitPlatform());
|
||||
enable();
|
||||
BukkitGuiHost.install();
|
||||
super.onEnable();
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
}
|
||||
|
||||
+1
-1
@@ -323,7 +323,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), 0);
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine());
|
||||
sender().sendMessage(C.GREEN + "Opening map!");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class BukkitGuiHost implements GuiHost.Provider {
|
||||
private final Map<Runnable, Listener> hotloadHooks = new ConcurrentHashMap<>();
|
||||
|
||||
public static void install() {
|
||||
GuiHost.set(new BukkitGuiHost());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine findActiveEngine() {
|
||||
try {
|
||||
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
|
||||
try {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
||||
return access.getEngine();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerHotloadHook(Runnable onHotload) {
|
||||
Listener listener = new HotloadListener(onHotload);
|
||||
hotloadHooks.put(onHotload, listener);
|
||||
Iris.instance.registerListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterHotloadHook(Runnable onHotload) {
|
||||
Listener listener = hotloadHooks.remove(onHotload);
|
||||
if (listener != null) {
|
||||
Iris.instance.unregisterListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiOverlay overlayFor(Engine engine) {
|
||||
return engine == null ? null : new BukkitVisionOverlay(engine);
|
||||
}
|
||||
|
||||
private static final class HotloadListener implements Listener {
|
||||
private final Runnable onHotload;
|
||||
|
||||
private HotloadListener(Runnable onHotload) {
|
||||
this.onHotload = onHotload;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(IrisEngineHotloadEvent event) {
|
||||
onHotload.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
|
||||
|
||||
public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
private final Engine engine;
|
||||
|
||||
public BukkitVisionOverlay(Engine engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuiMarker> players() {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (Player player : world.getPlayers()) {
|
||||
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||
J.s(() -> {
|
||||
IrisWorld world = engine.getWorld();
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (LivingEntity entity : world.getEntitiesByClass(LivingEntity.class)) {
|
||||
if (entity instanceof Player) {
|
||||
continue;
|
||||
}
|
||||
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
|
||||
double maxHealth = 0;
|
||||
try {
|
||||
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
|
||||
entity.getHealth(), maxHealth));
|
||||
}
|
||||
sink.accept(markers);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleport(double worldX, double worldZ) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
if (!world.hasRealWorld()) {
|
||||
return;
|
||||
}
|
||||
J.s(() -> {
|
||||
List<Player> players = world.getPlayers();
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Player player = players.get(0);
|
||||
int xx = (int) worldX;
|
||||
int zz = (int) worldZ;
|
||||
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
|
||||
player.teleport(new Location(player.getWorld(), xx, yy, zz));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File file = switch (type) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
|
||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
default -> null;
|
||||
};
|
||||
return file == null ? null : file.getName();
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,7 @@ tasks.named('compileJava', JavaCompile).configure {
|
||||
}
|
||||
|
||||
loom {
|
||||
accessWidenerPath = file('src/main/resources/irisworldgen.accesswidener')
|
||||
runs {
|
||||
server {
|
||||
String parity = providers.gradleProperty('irisParity').getOrNull()
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.fabric;
|
||||
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import net.minecraft.server.packs.repository.PackRepository;
|
||||
import net.minecraft.server.packs.repository.RepositorySource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class FabricForcedDatapackSources {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private FabricForcedDatapackSources() {
|
||||
}
|
||||
|
||||
public static void attach(PackRepository repository) {
|
||||
if (repository == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Set<RepositorySource> merged = new LinkedHashSet<>(repository.sources);
|
||||
merged.add(ModdedForcedDatapack.repositorySource());
|
||||
repository.sources = merged;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to attach the forced startup datapack source to the server data pack repository", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,13 +72,17 @@ public final class IrisFabricBootstrap implements ModInitializer {
|
||||
ModdedObjectUndo.clearAll();
|
||||
ModdedWandService.clearAll();
|
||||
ModdedWorldEngines.shutdown();
|
||||
ModdedEngineBootstrap.stop();
|
||||
});
|
||||
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
|
||||
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
|
||||
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
|
||||
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
|
||||
ModdedWandService.useBlock(player, level, hand, hit.getBlockPos()) ? InteractionResult.SUCCESS : InteractionResult.PASS);
|
||||
ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> ModdedWandService.serverTick(server));
|
||||
ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> {
|
||||
ModdedEngineBootstrap.tick(server);
|
||||
ModdedWandService.serverTick(server);
|
||||
});
|
||||
|
||||
String parity = System.getProperty("iris.parity");
|
||||
if (parity != null) {
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.fabric.mixin;
|
||||
|
||||
import art.arcane.iris.fabric.FabricForcedDatapackSources;
|
||||
import net.minecraft.server.packs.repository.PackRepository;
|
||||
import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(ServerPacksSource.class)
|
||||
public class ServerPacksSourceMixin {
|
||||
@Inject(method = "createPackRepository(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;)Lnet/minecraft/server/packs/repository/PackRepository;", at = @At("RETURN"))
|
||||
private static void iris$addForcedDatapackSource(LevelStorageSource.LevelStorageAccess storage, CallbackInfoReturnable<PackRepository> info) {
|
||||
FabricForcedDatapackSources.attach(info.getReturnValue());
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"environment": "*",
|
||||
"accessWidener": "irisworldgen.accesswidener",
|
||||
"mixins": ["irisworldgen.mixins.json"],
|
||||
"entrypoints": {
|
||||
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
accessWidener v2 official
|
||||
accessible field net/minecraft/server/MinecraftServer levels Ljava/util/Map;
|
||||
accessible field net/minecraft/server/MinecraftServer executor Ljava/util/concurrent/Executor;
|
||||
accessible field net/minecraft/server/MinecraftServer storageSource Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;
|
||||
accessible field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
|
||||
mutable field net/minecraft/server/packs/repository/PackRepository sources Ljava/util/Set;
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "art.arcane.iris.fabric.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"ServerPacksSourceMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,7 @@ dependencies {
|
||||
}
|
||||
|
||||
minecraft {
|
||||
accessTransformer.from(file('src/main/resources/META-INF/accesstransformer.cfg'))
|
||||
runs {
|
||||
register('server') {
|
||||
workingDir = layout.projectDirectory.dir('run')
|
||||
@@ -214,6 +215,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
}
|
||||
archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}"))
|
||||
configurations = [project.configurations.named('bundle').get()]
|
||||
from(sourceSets.main.output)
|
||||
mergeServiceFiles()
|
||||
exclude('META-INF/maven/**')
|
||||
exclude('META-INF/proguard/**')
|
||||
@@ -223,7 +225,16 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
exclude('org/apache/commons/lang/enum/**')
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
||||
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
||||
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
||||
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
||||
exclude('oshi.properties')
|
||||
exclude('org/jspecify/**')
|
||||
exclude('com/google/errorprone/**')
|
||||
exclude('com/google/j2objc/**')
|
||||
exclude('org/checkerframework/**')
|
||||
exclude('org/jetbrains/annotations/**')
|
||||
exclude('org/intellij/lang/**')
|
||||
}
|
||||
|
||||
tasks.named('assemble').configure {
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.forge;
|
||||
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.modded.ModdedParityProbe;
|
||||
import art.arcane.iris.modded.ModdedWorldCheck;
|
||||
@@ -29,7 +30,9 @@ import art.arcane.iris.modded.command.ModdedObjectUndo;
|
||||
import art.arcane.iris.modded.command.ModdedWandService;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraftforge.event.AddPackFindersEvent;
|
||||
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
@@ -66,13 +69,22 @@ public final class IrisForgeBootstrap {
|
||||
ModdedObjectUndo.clearAll();
|
||||
ModdedWandService.clearAll();
|
||||
ModdedWorldEngines.shutdown();
|
||||
ModdedEngineBootstrap.stop();
|
||||
});
|
||||
AddPackFindersEvent.BUS.addListener((AddPackFindersEvent event) -> {
|
||||
if (event.getPackType() == PackType.SERVER_DATA) {
|
||||
event.addRepositorySource(ModdedForcedDatapack.repositorySource());
|
||||
}
|
||||
});
|
||||
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
|
||||
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
|
||||
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
|
||||
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
|
||||
ModdedWandService.useBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
|
||||
TickEvent.ServerTickEvent.Post.BUS.addListener((TickEvent.ServerTickEvent.Post event) -> ModdedWandService.serverTick(event.server()));
|
||||
TickEvent.ServerTickEvent.Post.BUS.addListener((TickEvent.ServerTickEvent.Post event) -> {
|
||||
ModdedEngineBootstrap.tick(event.server());
|
||||
ModdedWandService.serverTick(event.server());
|
||||
});
|
||||
|
||||
String parity = System.getProperty("iris.parity");
|
||||
if (parity != null) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
public net.minecraft.server.MinecraftServer levels
|
||||
public net.minecraft.server.MinecraftServer executor
|
||||
public net.minecraft.server.MinecraftServer storageSource
|
||||
+34
-1
@@ -79,10 +79,43 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
|
||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||
private volatile Engine engine;
|
||||
private volatile String activePackKey;
|
||||
private volatile long seedOverride = Long.MIN_VALUE;
|
||||
|
||||
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
|
||||
super(biomeSource);
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.activePackKey = dimensionKey;
|
||||
}
|
||||
|
||||
public synchronized void repoint(String packKey, long seed) {
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
ModdedWorldEngines.evict(level);
|
||||
}
|
||||
this.activePackKey = packKey;
|
||||
this.seedOverride = seed;
|
||||
this.engine = null;
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
}
|
||||
|
||||
public synchronized void unbindEngine() {
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
ModdedWorldEngines.evict(level);
|
||||
}
|
||||
this.engine = null;
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
}
|
||||
|
||||
public synchronized void resetToDefault() {
|
||||
repoint(dimensionKey, Long.MIN_VALUE);
|
||||
}
|
||||
|
||||
public String activePackKey() {
|
||||
return activePackKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,7 +149,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
if (level == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
|
||||
}
|
||||
Engine created = ModdedWorldEngines.get(level, dimensionKey);
|
||||
Engine created = ModdedWorldEngines.get(level, activePackKey, seedOverride);
|
||||
engine = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import net.minecraft.core.Registry;
|
||||
@@ -25,12 +28,17 @@ import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String VANILLA_FALLBACK_KEY = "minecraft:plains";
|
||||
|
||||
private final Supplier<MinecraftServer> server;
|
||||
|
||||
public ModdedBiomeWriter(Supplier<MinecraftServer> server) {
|
||||
@@ -40,12 +48,18 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
||||
@Override
|
||||
public int biomeIdFor(String key) {
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (registry == null || identifier == null) {
|
||||
if (registry == null) {
|
||||
return 0;
|
||||
}
|
||||
Biome biome = registry.getValue(identifier);
|
||||
return biome == null ? 0 : registry.getId(biome);
|
||||
int direct = idForKey(registry, key);
|
||||
if (direct >= 0) {
|
||||
return direct;
|
||||
}
|
||||
int derivative = idForDerivative(registry, key);
|
||||
if (derivative >= 0) {
|
||||
return derivative;
|
||||
}
|
||||
return fallbackId(registry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,6 +78,60 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
||||
return biomes;
|
||||
}
|
||||
|
||||
private int idForKey(Registry<Biome> registry, String key) {
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
if (identifier == null) {
|
||||
return -1;
|
||||
}
|
||||
Biome biome = registry.getValue(identifier);
|
||||
return biome == null ? -1 : registry.getId(biome);
|
||||
}
|
||||
|
||||
private int idForDerivative(Registry<Biome> registry, String key) {
|
||||
int colon = key.indexOf(':');
|
||||
if (colon <= 0 || colon >= key.length() - 1) {
|
||||
return -1;
|
||||
}
|
||||
String dimensionLoadKey = key.substring(0, colon);
|
||||
String customBiomeId = key.substring(colon + 1);
|
||||
IrisBiome owner = findCustomBiomeOwner(dimensionLoadKey, customBiomeId);
|
||||
if (owner == null) {
|
||||
return -1;
|
||||
}
|
||||
org.bukkit.block.Biome derivative = owner.getVanillaDerivative();
|
||||
if (derivative == null || derivative.getKey() == null) {
|
||||
return -1;
|
||||
}
|
||||
return idForKey(registry, derivative.getKey().toString());
|
||||
}
|
||||
|
||||
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
|
||||
for (Engine engine : ModdedWorldEngines.activeEngines()) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
continue;
|
||||
}
|
||||
if (!dimensionLoadKey.equalsIgnoreCase(engine.getDimension().getLoadKey())) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiome biome : engine.getDimension().getAllBiomes(engine)) {
|
||||
if (!biome.isCustom()) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
|
||||
if (customBiomeId.equals(custom.getId())) {
|
||||
return biome;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int fallbackId(Registry<Biome> registry) {
|
||||
int plains = idForKey(registry, VANILLA_FALLBACK_KEY);
|
||||
return plains >= 0 ? plains : 0;
|
||||
}
|
||||
|
||||
private Registry<Biome> biomeRegistry() {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
@@ -217,6 +218,10 @@ public final class ModdedBlockResolution {
|
||||
Parsed bdx = parseBlockData(bd, warn);
|
||||
|
||||
if (bdx == null) {
|
||||
BlockState provided = ModdedCustomContentRegistry.resolveBlock(bd);
|
||||
if (provided != null) {
|
||||
return new Parsed(provided, null);
|
||||
}
|
||||
if (warn && shouldWarn()) {
|
||||
IrisLogging.warn("Unknown Block Data '" + bd + "'");
|
||||
}
|
||||
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Relative;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import net.minecraft.world.level.storage.ServerLevelData;
|
||||
import net.minecraft.world.level.storage.WorldData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public final class ModdedDimensionManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Object LOCK = new Object();
|
||||
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
|
||||
private static volatile ModdedServerAccess access;
|
||||
|
||||
private ModdedDimensionManager() {
|
||||
}
|
||||
|
||||
public static void bindAccess(ModdedServerAccess serverAccess) {
|
||||
access = serverAccess;
|
||||
}
|
||||
|
||||
public static Handle handle(String dimensionId) {
|
||||
return HANDLES.get(dimensionId);
|
||||
}
|
||||
|
||||
public static List<Handle> handles() {
|
||||
return new ArrayList<>(HANDLES.values());
|
||||
}
|
||||
|
||||
public static ServerLevel level(MinecraftServer server, String dimensionId) {
|
||||
Handle handle = HANDLES.get(dimensionId);
|
||||
if (handle != null && handle.level().getServer() == server) {
|
||||
return handle.level();
|
||||
}
|
||||
ResourceKey<Level> key = levelKey(dimensionId);
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (level.dimension().equals(key)) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Engine engine(MinecraftServer server, String dimensionId) {
|
||||
ServerLevel level = level(server, dimensionId);
|
||||
if (level == null) {
|
||||
return null;
|
||||
}
|
||||
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
|
||||
return null;
|
||||
}
|
||||
return generator.commandEngine();
|
||||
}
|
||||
|
||||
public static Handle create(MinecraftServer server, String dimensionId, String packKey, long seed) {
|
||||
ModdedServerAccess serverAccess = requireAccess();
|
||||
synchronized (LOCK) {
|
||||
ResourceKey<Level> key = levelKey(dimensionId);
|
||||
Handle existing = HANDLES.get(dimensionId);
|
||||
if (existing != null && serverAccess.hasLevel(server, key)) {
|
||||
existing.generator().repoint(packKey, seed);
|
||||
return existing;
|
||||
}
|
||||
if (serverAccess.hasLevel(server, key)) {
|
||||
ServerLevel present = level(server, dimensionId);
|
||||
if (present == null || !(present.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
|
||||
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
|
||||
}
|
||||
LOGGER.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
|
||||
generator.repoint(packKey, seed);
|
||||
Handle handle = new Handle(dimensionId, packKey, seed, present, generator);
|
||||
HANDLES.put(dimensionId, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
try {
|
||||
Handle handle = inject(server, serverAccess, dimensionId, key, packKey, seed);
|
||||
HANDLES.put(dimensionId, handle);
|
||||
LOGGER.info("Iris injected runtime dimension '{}' (pack={} seed={})", dimensionId, packKey, seed);
|
||||
return handle;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to inject runtime dimension '{}' (pack={} seed={})", dimensionId, packKey, seed, e);
|
||||
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Handle createPersistent(MinecraftServer server, String dimensionId, String packKey, long seed) {
|
||||
Handle handle = create(server, dimensionId, packKey, seed);
|
||||
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, packKey, seed));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public static boolean removePersistent(MinecraftServer server, String dimensionId) {
|
||||
boolean removed = remove(server, dimensionId);
|
||||
ModdedDimensionRegistryStore.remove(server, dimensionId);
|
||||
return removed;
|
||||
}
|
||||
|
||||
public static boolean remove(MinecraftServer server, String dimensionId) {
|
||||
return remove(server, dimensionId, false);
|
||||
}
|
||||
|
||||
public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage) {
|
||||
ModdedServerAccess serverAccess = requireAccess();
|
||||
synchronized (LOCK) {
|
||||
ResourceKey<Level> key = levelKey(dimensionId);
|
||||
ServerLevel level = level(server, dimensionId);
|
||||
if (level == null) {
|
||||
HANDLES.remove(dimensionId);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
evacuate(server, level);
|
||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
|
||||
generator.unbindEngine();
|
||||
}
|
||||
ModdedWorldEngines.evict(level);
|
||||
level.save(null, true, false);
|
||||
serverAccess.removeLevel(server, key);
|
||||
level.close();
|
||||
HANDLES.remove(dimensionId);
|
||||
if (wipeStorage) {
|
||||
ModdedDimensionStorage.wipe(server, key);
|
||||
}
|
||||
LOGGER.info("Iris removed runtime dimension '{}'", dimensionId);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to remove runtime dimension '{}'", dimensionId, e);
|
||||
throw new IllegalStateException("Iris runtime dimension removal failed for " + dimensionId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean teleport(ServerPlayer player, MinecraftServer server, String dimensionId, double x, double y, double z) {
|
||||
ServerLevel level = level(server, dimensionId);
|
||||
if (level == null) {
|
||||
return false;
|
||||
}
|
||||
int blockX = (int) Math.floor(x);
|
||||
int blockZ = (int) Math.floor(z);
|
||||
double targetY = y;
|
||||
if (y == Double.MIN_VALUE) {
|
||||
targetY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ);
|
||||
}
|
||||
player.teleportTo(level, x, targetY, z, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess) {
|
||||
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
|
||||
ResourceKey<DimensionType> studioPool = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse("irisworldgen:studio_pool"));
|
||||
return registry.get(studioPool)
|
||||
.map(reference -> (Holder<DimensionType>) reference)
|
||||
.orElseGet(() -> registry.getOrThrow(BuiltinDimensionTypes.OVERWORLD));
|
||||
}
|
||||
|
||||
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String packKey, long seed) {
|
||||
RegistryAccess registryAccess = server.registryAccess();
|
||||
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess);
|
||||
Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
|
||||
FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
|
||||
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, packKey);
|
||||
generator.repoint(packKey, seed);
|
||||
LevelStem stem = new LevelStem(dimensionType, generator);
|
||||
|
||||
WorldData worldData = server.getWorldData();
|
||||
ServerLevelData overworldData = worldData.overworldData();
|
||||
DerivedLevelData derivedLevelData = new DerivedLevelData(worldData, overworldData);
|
||||
|
||||
Executor executor = serverAccess.levelExecutor(server);
|
||||
LevelStorageSource.LevelStorageAccess storage = serverAccess.levelStorage(server);
|
||||
long obfuscatedSeed = BiomeManager.obfuscateSeed(seed);
|
||||
|
||||
ServerLevel level = new ServerLevel(
|
||||
server,
|
||||
executor,
|
||||
storage,
|
||||
derivedLevelData,
|
||||
key,
|
||||
stem,
|
||||
false,
|
||||
obfuscatedSeed,
|
||||
List.of(),
|
||||
false);
|
||||
|
||||
serverAccess.putLevel(server, key, level);
|
||||
server.getPlayerList().addWorldborderListener(level);
|
||||
return new Handle(dimensionId, packKey, seed, level, generator);
|
||||
}
|
||||
|
||||
private static void evacuate(MinecraftServer server, ServerLevel from) {
|
||||
ServerLevel fallback = server.overworld();
|
||||
if (fallback == from) {
|
||||
return;
|
||||
}
|
||||
int spawnX = 0;
|
||||
int spawnZ = 0;
|
||||
int spawnY = fallback.getHeight(Heightmap.Types.MOTION_BLOCKING, spawnX, spawnZ);
|
||||
for (ServerPlayer player : new ArrayList<>(from.players())) {
|
||||
player.teleportTo(fallback, spawnX + 0.5D, spawnY, spawnZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
}
|
||||
}
|
||||
|
||||
private static ResourceKey<Level> levelKey(String dimensionId) {
|
||||
Identifier identifier = Identifier.parse(dimensionId);
|
||||
return ResourceKey.create(Registries.DIMENSION, identifier);
|
||||
}
|
||||
|
||||
private static ModdedServerAccess requireAccess() {
|
||||
ModdedServerAccess bound = access;
|
||||
if (bound == null) {
|
||||
throw new IllegalStateException("Iris modded server access is not bound; the loader bootstrap must bind ModdedServerAccess before runtime dimension injection");
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
|
||||
public record Handle(String dimensionId, String packKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ModdedDimensionRegistryStore {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String FILE_NAME = "iris-dimensions.json";
|
||||
|
||||
private ModdedDimensionRegistryStore() {
|
||||
}
|
||||
|
||||
public static List<PersistentDimension> load(MinecraftServer server) {
|
||||
Path file = storeFile(server);
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
JSONObject root = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
|
||||
JSONArray entries = root.optJSONArray("dimensions");
|
||||
if (entries == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Map<String, PersistentDimension> deduplicated = new LinkedHashMap<>();
|
||||
for (int index = 0; index < entries.length(); index++) {
|
||||
JSONObject entry = entries.getJSONObject(index);
|
||||
String id = entry.optString("id", null);
|
||||
String packKey = entry.optString("packKey", null);
|
||||
if (id == null || packKey == null) {
|
||||
continue;
|
||||
}
|
||||
long seed = entry.optLong("seed", 0L);
|
||||
deduplicated.put(id, new PersistentDimension(id, packKey, seed));
|
||||
}
|
||||
return new ArrayList<>(deduplicated.values());
|
||||
} catch (RuntimeException | IOException e) {
|
||||
LOGGER.error("Iris persistent dimension registry at {} is invalid; ignoring it", file, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void put(MinecraftServer server, PersistentDimension dimension) {
|
||||
Map<String, PersistentDimension> current = index(load(server));
|
||||
current.put(dimension.id(), dimension);
|
||||
write(server, new ArrayList<>(current.values()));
|
||||
}
|
||||
|
||||
public static synchronized void remove(MinecraftServer server, String id) {
|
||||
Map<String, PersistentDimension> current = index(load(server));
|
||||
if (current.remove(id) != null) {
|
||||
write(server, new ArrayList<>(current.values()));
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, PersistentDimension> index(List<PersistentDimension> dimensions) {
|
||||
Map<String, PersistentDimension> map = new LinkedHashMap<>();
|
||||
for (PersistentDimension dimension : dimensions) {
|
||||
map.put(dimension.id(), dimension);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void write(MinecraftServer server, List<PersistentDimension> dimensions) {
|
||||
Path file = storeFile(server);
|
||||
JSONArray entries = new JSONArray();
|
||||
for (PersistentDimension dimension : dimensions) {
|
||||
JSONObject entry = new JSONObject();
|
||||
entry.put("id", dimension.id());
|
||||
entry.put("packKey", dimension.packKey());
|
||||
entry.put("seed", dimension.seed());
|
||||
entries.put(entry);
|
||||
}
|
||||
JSONObject root = new JSONObject();
|
||||
root.put("dimensions", entries);
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
Path temp = file.resolveSibling(FILE_NAME + ".tmp");
|
||||
Files.writeString(temp, root.toString(2), StandardCharsets.UTF_8);
|
||||
Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris failed to write persistent dimension registry at {}", file, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path storeFile(MinecraftServer server) {
|
||||
return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME);
|
||||
}
|
||||
|
||||
public record PersistentDimension(String id, String packKey, long seed) {
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ModdedDimensionStorage {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final List<String> CHUNK_DATA_FOLDERS = List.of("region", "entities", "poi", "mantle");
|
||||
|
||||
private ModdedDimensionStorage() {
|
||||
}
|
||||
|
||||
public static File storageFolder(MinecraftServer server, ResourceKey<Level> dimension) {
|
||||
return DimensionType.getStorageFolder(dimension, server.getWorldPath(LevelResource.ROOT)).toFile();
|
||||
}
|
||||
|
||||
public static void wipe(MinecraftServer server, ResourceKey<Level> dimension) {
|
||||
File storageFolder = storageFolder(server, dimension);
|
||||
for (String folder : CHUNK_DATA_FOLDERS) {
|
||||
deleteRecursively(new File(storageFolder, folder).toPath());
|
||||
}
|
||||
LOGGER.info("Iris wiped dimension storage at {}", storageFolder.getAbsolutePath());
|
||||
}
|
||||
|
||||
private static void deleteRecursively(Path root) {
|
||||
if (!Files.exists(root)) {
|
||||
return;
|
||||
}
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
|
||||
Files.deleteIfExists(path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris failed to wipe dimension storage at {}", root, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
-16
@@ -22,11 +22,14 @@ import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.object.BlockDataMergeSupport;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.modded.command.ModdedGuiHost;
|
||||
import art.arcane.iris.modded.service.ModdedLogFilterService;
|
||||
import art.arcane.iris.modded.service.ModdedPreservationService;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
@@ -45,12 +48,38 @@ public final class ModdedEngineBootstrap {
|
||||
"art.arcane.iris.core.loader.IrisData"
|
||||
};
|
||||
private static final Object LOCK = new Object();
|
||||
private static final ModdedServiceManager SERVICE_MANAGER = new ModdedServiceManager();
|
||||
private static volatile ModdedLoader loader;
|
||||
private static volatile ModdedPlatform platform;
|
||||
|
||||
private ModdedEngineBootstrap() {
|
||||
}
|
||||
|
||||
public static ModdedServiceManager services() {
|
||||
return SERVICE_MANAGER;
|
||||
}
|
||||
|
||||
public static ModdedScheduler schedulerOrNull() {
|
||||
ModdedPlatform bound = platform;
|
||||
return bound == null ? null : bound.moddedScheduler();
|
||||
}
|
||||
|
||||
public static void tick(MinecraftServer server) {
|
||||
ModdedScheduler.tick(server);
|
||||
ModdedStartup.runOnce(server);
|
||||
ModdedPrimaryWorldRouter.tick(server);
|
||||
SERVICE_MANAGER.tick(server);
|
||||
}
|
||||
|
||||
public static void stop() {
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
SERVICE_MANAGER.disableAll();
|
||||
ModdedScheduler scheduler = schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize(ModdedLoader moddedLoader) {
|
||||
loader = moddedLoader;
|
||||
}
|
||||
@@ -97,33 +126,25 @@ public final class ModdedEngineBootstrap {
|
||||
ModdedLoader boundLoader = loader();
|
||||
ModdedPlatform created = new ModdedPlatform(boundLoader);
|
||||
IrisPlatforms.bind(created);
|
||||
ModdedDimensionManager.bindAccess(new ModdedServerLevels());
|
||||
IrisObjectRotation.bindFallbackRotator(new ModdedStateRotator());
|
||||
BlockDataMergeSupport.bindFallbackMerger(new ModdedStateMerger());
|
||||
TileData.bindFallbackReader(new ModdedTileReader(boundLoader::currentServer));
|
||||
ModdedGuiHost.install();
|
||||
ModdedDecoratorHooks decoratorHooks = new ModdedDecoratorHooks();
|
||||
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
|
||||
IrisServices.register(PreservationRegistry.class, new InertPreservation());
|
||||
ModdedPreservationService preservation = SERVICE_MANAGER.register(ModdedPreservationService.class, new ModdedPreservationService());
|
||||
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
|
||||
IrisServices.register(PreservationRegistry.class, preservation);
|
||||
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
|
||||
ModdedCustomContentRegistry.discover();
|
||||
platform = created;
|
||||
SERVICE_MANAGER.enableAll();
|
||||
ModdedIrisSplash.print(boundLoader);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InertPreservation implements PreservationRegistry {
|
||||
@Override
|
||||
public void register(Thread thread) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCache(MeteredCache cache) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dereference() {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InertWorldManager implements EngineWorldManager {
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionType;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.packs.PackLocationInfo;
|
||||
import net.minecraft.server.packs.PackSelectionConfig;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.server.packs.PathPackResources;
|
||||
import net.minecraft.server.packs.repository.Pack;
|
||||
import net.minecraft.server.packs.repository.PackSource;
|
||||
import net.minecraft.server.packs.repository.RepositorySource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ModdedForcedDatapack {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String PACK_ID = "iris_worldgen";
|
||||
private static final String PACK_FOLDER = "iris";
|
||||
private static final String STUDIO_POOL_TYPE_KEY = "studio_pool";
|
||||
private static final String STUDIO_POOL_TYPE_RESOURCE = "/data/irisworldgen/dimension_type/overworld.json";
|
||||
private static final int STUDIO_POOL_MIN_Y = -256;
|
||||
private static final int STUDIO_POOL_MAX_Y = 512;
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private ModdedForcedDatapack() {
|
||||
}
|
||||
|
||||
public static RepositorySource repositorySource() {
|
||||
return (Consumer<Pack> consumer) -> {
|
||||
Pack pack = buildPack();
|
||||
if (pack != null) {
|
||||
consumer.accept(pack);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Path datapackRoot() {
|
||||
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("generated").resolve("datapack");
|
||||
}
|
||||
|
||||
private static Path packDirectory() {
|
||||
return datapackRoot().resolve(PACK_FOLDER);
|
||||
}
|
||||
|
||||
private static Pack buildPack() {
|
||||
Path directory = generate();
|
||||
if (directory == null) {
|
||||
return null;
|
||||
}
|
||||
PackLocationInfo location = new PackLocationInfo(
|
||||
PACK_ID,
|
||||
Component.literal("Iris World Generation"),
|
||||
PackSource.BUILT_IN,
|
||||
Optional.empty());
|
||||
PackSelectionConfig selection = new PackSelectionConfig(true, Pack.Position.TOP, true);
|
||||
PathPackResources.PathResourcesSupplier supplier = new PathPackResources.PathResourcesSupplier(directory);
|
||||
Pack pack = Pack.readMetaAndCreate(location, supplier, PackType.SERVER_DATA, selection);
|
||||
if (pack == null) {
|
||||
LOGGER.error("Iris forced datapack at {} produced no readable pack metadata", directory);
|
||||
}
|
||||
return pack;
|
||||
}
|
||||
|
||||
private static Path generate() {
|
||||
synchronized (LOCK) {
|
||||
try {
|
||||
return write();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris failed to generate the forced startup datapack", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Path write() throws IOException {
|
||||
Path packDirectory = packDirectory();
|
||||
clean(packDirectory);
|
||||
Files.createDirectories(packDirectory);
|
||||
|
||||
File packFolder = packDirectory.toFile();
|
||||
KList<File> folders = new KList<>();
|
||||
folders.add(packFolder.getParentFile());
|
||||
KSet<String> seenBiomes = new KSet<>();
|
||||
IDataFixer fixer = DataVersion.getLatest().get();
|
||||
|
||||
int packCount = 0;
|
||||
File[] packs = packsRoot().toFile().listFiles(File::isDirectory);
|
||||
if (packs != null) {
|
||||
for (File pack : packs) {
|
||||
if (installPack(pack, fixer, folders, seenBiomes)) {
|
||||
packCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writePackMeta(packDirectory);
|
||||
writeStudioPoolType(packDirectory);
|
||||
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} custom biome(s) at {}", packCount, seenBiomes.size(), packDirectory);
|
||||
return packDirectory;
|
||||
}
|
||||
|
||||
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, KSet<String> seenBiomes) {
|
||||
String packKey = packFolder.getName();
|
||||
if (!new File(packFolder, "dimensions/" + packKey + ".json").isFile()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(packKey);
|
||||
if (dimension == null) {
|
||||
return false;
|
||||
}
|
||||
dimension.installBiomes(fixer, () -> data, folders, seenBiomes);
|
||||
writeDimensionType(folders, fixer, dimension);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to install forced datapack content for pack '{}'", packKey, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension) throws IOException {
|
||||
if (fitsStudioPool(dimension)) {
|
||||
return;
|
||||
}
|
||||
IrisDimensionType type = dimension.getDimensionType();
|
||||
String json = type.toJson(fixer);
|
||||
String typeKey = dimension.getDimensionTypeKey();
|
||||
for (File parent : folders) {
|
||||
Path output = parent.toPath().resolve(PACK_FOLDER).resolve("data").resolve("irisworldgen").resolve("dimension_type").resolve(typeKey + ".json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, json, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean fitsStudioPool(IrisDimension dimension) {
|
||||
return dimension.getMinHeight() >= STUDIO_POOL_MIN_Y && dimension.getMaxHeight() <= STUDIO_POOL_MAX_Y;
|
||||
}
|
||||
|
||||
private static void writeStudioPoolType(Path packDirectory) throws IOException {
|
||||
Path output = packDirectory.resolve("data").resolve("irisworldgen").resolve("dimension_type").resolve(STUDIO_POOL_TYPE_KEY + ".json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, readStudioPoolType(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String readStudioPoolType() throws IOException {
|
||||
try (InputStream stream = ModdedForcedDatapack.class.getResourceAsStream(STUDIO_POOL_TYPE_RESOURCE)) {
|
||||
if (stream == null) {
|
||||
throw new IOException("Bundled studio pool dimension type resource is missing: " + STUDIO_POOL_TYPE_RESOURCE);
|
||||
}
|
||||
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writePackMeta(Path packDirectory) throws IOException {
|
||||
int packFormat = DataVersion.getLatest().getPackFormat();
|
||||
String json = "{\n"
|
||||
+ " \"pack\": {\n"
|
||||
+ " \"description\": \"Iris world generation biomes and dimension types for installed packs.\",\n"
|
||||
+ " \"pack_format\": " + packFormat + ",\n"
|
||||
+ " \"min_format\": " + packFormat + ",\n"
|
||||
+ " \"max_format\": " + packFormat + "\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
Files.writeString(packDirectory.resolve("pack.mcmeta"), json, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void clean(Path packDirectory) throws IOException {
|
||||
if (!Files.exists(packDirectory)) {
|
||||
return;
|
||||
}
|
||||
List<Path> entries = new ArrayList<>();
|
||||
try (Stream<Path> walk = Files.walk(packDirectory)) {
|
||||
walk.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).forEach(entries::add);
|
||||
}
|
||||
for (Path entry : entries) {
|
||||
Files.deleteIfExists(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path packsRoot() {
|
||||
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class ModdedModConfig {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Object LOCK = new Object();
|
||||
private static volatile ModdedModConfig instance;
|
||||
|
||||
private final String defaultPack;
|
||||
private final boolean autoDownloadDefaultPack;
|
||||
private final String primaryWorld;
|
||||
private final boolean routePlayersToPrimaryWorld;
|
||||
|
||||
private ModdedModConfig(String defaultPack, boolean autoDownloadDefaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld) {
|
||||
this.defaultPack = defaultPack;
|
||||
this.autoDownloadDefaultPack = autoDownloadDefaultPack;
|
||||
this.primaryWorld = primaryWorld == null ? "" : primaryWorld.trim();
|
||||
this.routePlayersToPrimaryWorld = routePlayersToPrimaryWorld;
|
||||
}
|
||||
|
||||
public static ModdedModConfig get() {
|
||||
ModdedModConfig bound = instance;
|
||||
if (bound != null) {
|
||||
return bound;
|
||||
}
|
||||
synchronized (LOCK) {
|
||||
if (instance != null) {
|
||||
return instance;
|
||||
}
|
||||
instance = load();
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setPrimaryWorld(String dimensionId) {
|
||||
synchronized (LOCK) {
|
||||
ModdedModConfig current = get();
|
||||
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, dimensionId, current.routePlayersToPrimaryWorld);
|
||||
instance = updated;
|
||||
write(configFile(), updated);
|
||||
}
|
||||
}
|
||||
|
||||
public String defaultPack() {
|
||||
return defaultPack;
|
||||
}
|
||||
|
||||
public boolean autoDownloadDefaultPack() {
|
||||
return autoDownloadDefaultPack;
|
||||
}
|
||||
|
||||
public String primaryWorld() {
|
||||
return primaryWorld;
|
||||
}
|
||||
|
||||
public boolean routePlayersToPrimaryWorld() {
|
||||
return routePlayersToPrimaryWorld;
|
||||
}
|
||||
|
||||
private static Path configFile() {
|
||||
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("modded.json");
|
||||
}
|
||||
|
||||
private static ModdedModConfig load() {
|
||||
Path file = configFile();
|
||||
ModdedModConfig defaults = new ModdedModConfig("overworld", true, "", true);
|
||||
if (!Files.isRegularFile(file)) {
|
||||
write(file, defaults);
|
||||
return defaults;
|
||||
}
|
||||
try {
|
||||
JSONObject json = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
|
||||
return new ModdedModConfig(
|
||||
json.optString("defaultPack", defaults.defaultPack),
|
||||
json.optBoolean("autoDownloadDefaultPack", defaults.autoDownloadDefaultPack),
|
||||
json.optString("primaryWorld", defaults.primaryWorld),
|
||||
json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld));
|
||||
} catch (RuntimeException | IOException e) {
|
||||
LOGGER.error("Iris modded config at {} is invalid; using defaults", file, e);
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(Path file, ModdedModConfig config) {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("defaultPack", config.defaultPack);
|
||||
json.put("autoDownloadDefaultPack", config.autoDownloadDefaultPack);
|
||||
json.put("primaryWorld", config.primaryWorld);
|
||||
json.put("routePlayersToPrimaryWorld", config.routePlayersToPrimaryWorld);
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, json.toString(4), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris failed to write modded config at {}", file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,20 @@ import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.LogLevel;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import art.arcane.iris.spi.PlatformCapabilities;
|
||||
import art.arcane.iris.spi.PlatformEntityType;
|
||||
import art.arcane.iris.spi.PlatformItem;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.function.Consumer;
|
||||
@@ -57,6 +67,10 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
return loader.currentServer();
|
||||
}
|
||||
|
||||
public ModdedScheduler moddedScheduler() {
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String platformName() {
|
||||
return loader.platformName();
|
||||
@@ -130,6 +144,35 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
public void dispatchConsoleCommand(String command) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
|
||||
if (!(world instanceof ServerLevel level) || entityKey == null) {
|
||||
return false;
|
||||
}
|
||||
PlatformEntityType resolved = registries.entity(entityKey);
|
||||
if (resolved == null) {
|
||||
return false;
|
||||
}
|
||||
EntityType<?> type = (EntityType<?>) resolved.nativeHandle();
|
||||
BlockPos pos = BlockPos.containing(x, y, z);
|
||||
Entity entity = type.spawn(level, pos, EntitySpawnReason.COMMAND);
|
||||
return entity != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean giveItem(Object player, String itemKey, int amount) {
|
||||
if (!(player instanceof ServerPlayer serverPlayer) || itemKey == null || amount <= 0) {
|
||||
return false;
|
||||
}
|
||||
PlatformItem resolved = registries.item(itemKey);
|
||||
if (resolved == null) {
|
||||
return false;
|
||||
}
|
||||
Item item = (Item) resolved.nativeHandle();
|
||||
ItemStack stack = new ItemStack(item, amount);
|
||||
return serverPlayer.getInventory().add(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(LogLevel level, String message) {
|
||||
ModdedIrisLog.log(level, message);
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedPrimaryWorldRouter {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final int TICK_INTERVAL = 20;
|
||||
|
||||
private static final Set<UUID> routed = ConcurrentHashMap.newKeySet();
|
||||
private static int tickCounter = 0;
|
||||
|
||||
private ModdedPrimaryWorldRouter() {
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
routed.clear();
|
||||
}
|
||||
|
||||
public static void tick(MinecraftServer server) {
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
tickCounter++;
|
||||
if (tickCounter < TICK_INTERVAL) {
|
||||
return;
|
||||
}
|
||||
tickCounter = 0;
|
||||
|
||||
ModdedModConfig config = ModdedModConfig.get();
|
||||
if (!config.routePlayersToPrimaryWorld()) {
|
||||
return;
|
||||
}
|
||||
String primary = config.primaryWorld();
|
||||
if (primary.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ServerLevel target = ModdedDimensionManager.level(server, primary);
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
ServerLevel overworld = server.overworld();
|
||||
if (target == overworld) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ServerPlayer> players = new ArrayList<>(server.getPlayerList().getPlayers());
|
||||
for (ServerPlayer player : players) {
|
||||
UUID id = player.getUUID();
|
||||
if (routed.contains(id)) {
|
||||
continue;
|
||||
}
|
||||
if (player.level() != overworld) {
|
||||
routed.add(id);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
ModdedDimensionManager.teleport(player, server, primary, player.getX(), Double.MIN_VALUE, player.getZ());
|
||||
routed.add(id);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to route player {} to primary world '{}'", id, primary, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,28 +20,170 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class ModdedScheduler implements PlatformScheduler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final int ASYNC_CORE_THREADS = 2;
|
||||
private static final int ASYNC_MAX_THREADS = Math.max(4, Runtime.getRuntime().availableProcessors());
|
||||
private static final int ASYNC_QUEUE_CAPACITY = 4096;
|
||||
private static final long ASYNC_KEEP_ALIVE_SECONDS = 30L;
|
||||
|
||||
private static volatile Thread mainThread;
|
||||
|
||||
private final ThreadPoolExecutor asyncExecutor;
|
||||
private final ConcurrentLinkedQueue<Runnable> mainQueue = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentLinkedQueue<DelayedTask> delayedQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public ModdedScheduler() {
|
||||
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(ASYNC_QUEUE_CAPACITY);
|
||||
this.asyncExecutor = new ThreadPoolExecutor(
|
||||
ASYNC_CORE_THREADS,
|
||||
ASYNC_MAX_THREADS,
|
||||
ASYNC_KEEP_ALIVE_SECONDS,
|
||||
TimeUnit.SECONDS,
|
||||
workQueue,
|
||||
new AsyncThreadFactory(),
|
||||
new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
this.asyncExecutor.allowCoreThreadTimeOut(true);
|
||||
}
|
||||
|
||||
public static void tick(MinecraftServer server) {
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
mainThread = server.getRunningThread();
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
return;
|
||||
}
|
||||
scheduler.drain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void global(Runnable task) {
|
||||
task.run();
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
if (onMainThread()) {
|
||||
runGuarded(task);
|
||||
return;
|
||||
}
|
||||
mainQueue.add(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void region(PlatformWorld world, int chunkX, int chunkZ, Runnable task) {
|
||||
task.run();
|
||||
global(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void async(Runnable task) {
|
||||
task.run();
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
asyncExecutor.execute(() -> runGuarded(task));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void laterGlobal(Runnable task, int ticks) {
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
if (ticks <= 0) {
|
||||
global(task);
|
||||
return;
|
||||
}
|
||||
delayedQueue.add(new DelayedTask(task, ticks));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void laterRegion(PlatformWorld world, int chunkX, int chunkZ, Runnable task, int ticks) {
|
||||
laterGlobal(task, ticks);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
asyncExecutor.shutdownNow();
|
||||
mainQueue.clear();
|
||||
delayedQueue.clear();
|
||||
}
|
||||
|
||||
private void drain() {
|
||||
promoteDelayed();
|
||||
Runnable task;
|
||||
while ((task = mainQueue.poll()) != null) {
|
||||
runGuarded(task);
|
||||
}
|
||||
}
|
||||
|
||||
private void promoteDelayed() {
|
||||
if (delayedQueue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<DelayedTask> retained = new ArrayList<>();
|
||||
DelayedTask delayed;
|
||||
while ((delayed = delayedQueue.poll()) != null) {
|
||||
if (delayed.tick()) {
|
||||
mainQueue.add(delayed.task());
|
||||
} else {
|
||||
retained.add(delayed);
|
||||
}
|
||||
}
|
||||
delayedQueue.addAll(retained);
|
||||
}
|
||||
|
||||
private boolean onMainThread() {
|
||||
Thread main = mainThread;
|
||||
return main != null && Thread.currentThread() == main;
|
||||
}
|
||||
|
||||
private void runGuarded(Runnable task) {
|
||||
try {
|
||||
task.run();
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris scheduled task failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DelayedTask {
|
||||
private final Runnable task;
|
||||
private int remaining;
|
||||
|
||||
private DelayedTask(Runnable task, int remaining) {
|
||||
this.task = task;
|
||||
this.remaining = remaining;
|
||||
}
|
||||
|
||||
private boolean tick() {
|
||||
remaining--;
|
||||
return remaining <= 0;
|
||||
}
|
||||
|
||||
private Runnable task() {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncThreadFactory implements ThreadFactory {
|
||||
private final AtomicInteger counter = new AtomicInteger(1);
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = new Thread(runnable, "iris-modded-async-" + counter.getAndIncrement());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public interface ModdedServerAccess {
|
||||
Executor levelExecutor(MinecraftServer server);
|
||||
|
||||
LevelStorageSource.LevelStorageAccess levelStorage(MinecraftServer server);
|
||||
|
||||
ServerLevel putLevel(MinecraftServer server, ResourceKey<Level> key, ServerLevel level);
|
||||
|
||||
ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key);
|
||||
|
||||
boolean hasLevel(MinecraftServer server, ResourceKey<Level> key);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public final class ModdedServerLevels implements ModdedServerAccess {
|
||||
@Override
|
||||
public Executor levelExecutor(MinecraftServer server) {
|
||||
return server.executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LevelStorageSource.LevelStorageAccess levelStorage(MinecraftServer server) {
|
||||
return server.storageSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerLevel putLevel(MinecraftServer server, ResourceKey<Level> key, ServerLevel level) {
|
||||
return server.levels.put(key, level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerLevel removeLevel(MinecraftServer server, ResourceKey<Level> key) {
|
||||
return server.levels.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLevel(MinecraftServer server, ResourceKey<Level> key) {
|
||||
return server.levels.containsKey(key);
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.modded.service.ModdedService;
|
||||
import art.arcane.iris.modded.service.ModdedTickableService;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class ModdedServiceManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private final Map<Class<? extends ModdedService>, ModdedService> services = new LinkedHashMap<>();
|
||||
private boolean enabled = false;
|
||||
|
||||
public synchronized <T extends ModdedService> T register(Class<T> type, T service) {
|
||||
if (services.containsKey(type)) {
|
||||
return type.cast(services.get(type));
|
||||
}
|
||||
services.put(type, service);
|
||||
if (enabled) {
|
||||
enableService(service);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
public synchronized <T extends ModdedService> T service(Class<T> type) {
|
||||
ModdedService service = services.get(type);
|
||||
return service == null ? null : type.cast(service);
|
||||
}
|
||||
|
||||
public synchronized void enableAll() {
|
||||
if (enabled) {
|
||||
return;
|
||||
}
|
||||
enabled = true;
|
||||
for (ModdedService service : services.values()) {
|
||||
enableService(service);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void tick(MinecraftServer server) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
for (ModdedService service : services.values()) {
|
||||
if (service instanceof ModdedTickableService tickable) {
|
||||
tickService(tickable, server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void disableAll() {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
enabled = false;
|
||||
forEachReversed(this::disableService);
|
||||
services.clear();
|
||||
}
|
||||
|
||||
private void enableService(ModdedService service) {
|
||||
try {
|
||||
service.onEnable();
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris service onEnable failed for {}", service.getClass().getName(), error);
|
||||
}
|
||||
}
|
||||
|
||||
private void disableService(ModdedService service) {
|
||||
try {
|
||||
service.onDisable();
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris service onDisable failed for {}", service.getClass().getName(), error);
|
||||
}
|
||||
}
|
||||
|
||||
private void tickService(ModdedTickableService service, MinecraftServer server) {
|
||||
try {
|
||||
service.onServerTick(server);
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris service tick failed for {}", service.getClass().getName(), error);
|
||||
}
|
||||
}
|
||||
|
||||
private void forEachReversed(Consumer<ModdedService> action) {
|
||||
ModdedService[] ordered = services.values().toArray(new ModdedService[0]);
|
||||
for (int i = ordered.length - 1; i >= 0; i--) {
|
||||
action.accept(ordered[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedStartup {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
||||
|
||||
private ModdedStartup() {
|
||||
}
|
||||
|
||||
public static void runOnce(MinecraftServer server) {
|
||||
if (server == null || !STARTED.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
reinjectPersistentDimensions(server);
|
||||
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
ensureDefaultPack();
|
||||
return;
|
||||
}
|
||||
scheduler.async(ModdedStartup::ensureDefaultPack);
|
||||
}
|
||||
|
||||
private static void reinjectPersistentDimensions(MinecraftServer server) {
|
||||
List<ModdedDimensionRegistryStore.PersistentDimension> dimensions = ModdedDimensionRegistryStore.load(server);
|
||||
if (dimensions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int injected = 0;
|
||||
for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) {
|
||||
try {
|
||||
ModdedDimensionManager.create(server, dimension.id(), dimension.packKey(), dimension.seed());
|
||||
injected++;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to re-inject persistent dimension '{}' (pack={} seed={})", dimension.id(), dimension.packKey(), dimension.seed(), e);
|
||||
}
|
||||
}
|
||||
LOGGER.info("Iris re-injected {} persistent dimension(s) at startup", injected);
|
||||
}
|
||||
|
||||
private static void ensureDefaultPack() {
|
||||
ModdedModConfig config = ModdedModConfig.get();
|
||||
if (!config.autoDownloadDefaultPack()) {
|
||||
return;
|
||||
}
|
||||
String pack = config.defaultPack();
|
||||
Path configDir = ModdedEngineBootstrap.loader().configDir();
|
||||
File packFolder = configDir.resolve("irisworldgen").resolve("packs").resolve(pack).toFile();
|
||||
if (new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
||||
return;
|
||||
}
|
||||
LOGGER.info("Iris default pack '{}' missing; downloading IrisDimensions/{} (master)", pack, pack);
|
||||
boolean installed = ModdedPackInstaller.install(configDir, pack, "master", (String line) -> LOGGER.info("Iris: {}", line));
|
||||
if (!installed) {
|
||||
LOGGER.warn("Iris default pack '{}' could not be downloaded; install it with /iris download {}", pack, pack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,18 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.Strictness;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public final class ModdedTileData extends TileData {
|
||||
public static final String NBT_PROPERTY = "nbt";
|
||||
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
|
||||
|
||||
private final byte[] raw;
|
||||
private final KMap<String, Object> tileProperties;
|
||||
|
||||
@@ -34,6 +41,22 @@ public final class ModdedTileData extends TileData {
|
||||
this.tileProperties = tileProperties == null ? new KMap<>() : tileProperties;
|
||||
}
|
||||
|
||||
public static ModdedTileData capture(String blockKey, String snbt) throws IOException {
|
||||
KMap<String, Object> properties = new KMap<>();
|
||||
properties.put(NBT_PROPERTY, snbt);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream out = new DataOutputStream(bytes)) {
|
||||
out.writeUTF(blockKey);
|
||||
out.writeUTF(GSON.toJson(properties));
|
||||
}
|
||||
return new ModdedTileData(bytes.toByteArray(), properties);
|
||||
}
|
||||
|
||||
public String snbt() {
|
||||
Object value = tileProperties.get(NBT_PROPERTY);
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMap<String, Object> getProperties() {
|
||||
return tileProperties;
|
||||
|
||||
+25
-4
@@ -31,6 +31,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -41,15 +43,34 @@ public final class ModdedWorldEngines {
|
||||
private ModdedWorldEngines() {
|
||||
}
|
||||
|
||||
public static Engine get(ServerLevel level, String dimensionKey) {
|
||||
public static Engine get(ServerLevel level, String dimensionKey, long seedOverride) {
|
||||
Engine existing = ENGINES.get(level);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, dimensionKey));
|
||||
return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, dimensionKey, seedOverride));
|
||||
}
|
||||
|
||||
private static Engine create(ServerLevel level, String dimensionKey) {
|
||||
public static Collection<Engine> activeEngines() {
|
||||
return new ArrayList<>(ENGINES.values());
|
||||
}
|
||||
|
||||
public static void evict(ServerLevel level) {
|
||||
Engine removed = ENGINES.remove(level);
|
||||
if (removed == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!removed.isClosed()) {
|
||||
removed.close();
|
||||
}
|
||||
LOGGER.info("Iris engine evicted for {}", level.dimension().identifier());
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris engine evict close failed for {}", level.dimension().identifier(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Engine create(ServerLevel level, String dimensionKey, long seedOverride) {
|
||||
ModdedEngineBootstrap.bind();
|
||||
File pack = resolvePack(dimensionKey);
|
||||
IrisData data = IrisData.get(pack);
|
||||
@@ -60,7 +81,7 @@ public final class ModdedWorldEngines {
|
||||
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + pack.getAbsolutePath());
|
||||
}
|
||||
|
||||
long seed = level.getSeed();
|
||||
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
|
||||
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
|
||||
IrisWorld world = IrisWorld.builder()
|
||||
.name(level.dimension().identifier().toString().replace(':', '_'))
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.command.ModdedPregenJob;
|
||||
import art.arcane.iris.modded.command.ModdedPregenMode;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class IrisModdedAPI {
|
||||
private static final Map<ServerLevel, AtomicInteger> WORLD_MAINTENANCE_DEPTH = new ConcurrentHashMap<>();
|
||||
|
||||
private IrisModdedAPI() {
|
||||
}
|
||||
|
||||
public static boolean isIrisLevel(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return false;
|
||||
}
|
||||
return level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator;
|
||||
}
|
||||
|
||||
public static boolean isStudioLevel(ServerLevel level) {
|
||||
Engine engine = getEngine(level);
|
||||
return engine != null && engine.isStudio();
|
||||
}
|
||||
|
||||
public static Engine getEngine(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return null;
|
||||
}
|
||||
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||
if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return irisGenerator.commandEngine();
|
||||
} catch (Throwable error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean pregenerate(ServerLevel level, int radiusBlocks) {
|
||||
return pregenerate(level, radiusBlocks, 0, 0, ModdedPregenMode.ASYNC, false);
|
||||
}
|
||||
|
||||
public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, ModdedPregenMode mode, boolean cached) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null) {
|
||||
return false;
|
||||
}
|
||||
return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, mode, cached);
|
||||
}
|
||||
|
||||
public static <T> T getMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null) {
|
||||
return null;
|
||||
}
|
||||
return engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type);
|
||||
}
|
||||
|
||||
public static <T> void setMantleData(ServerLevel level, int x, int y, int z, T data) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null || data == null) {
|
||||
return;
|
||||
}
|
||||
engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data);
|
||||
}
|
||||
|
||||
public static <T> void deleteMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type);
|
||||
}
|
||||
|
||||
public static void retainMantleDataForSlice(Class<?> sliceType) {
|
||||
if (sliceType == null) {
|
||||
return;
|
||||
}
|
||||
IrisToolbelt.retainMantleDataForSlice(sliceType.getCanonicalName());
|
||||
}
|
||||
|
||||
public static void registerProvider(ModdedDataProvider provider) {
|
||||
ModdedCustomContentRegistry.register(provider);
|
||||
}
|
||||
|
||||
public static void registerCustomBlockData(String namespace, String key, String state) {
|
||||
ModdedCustomContentRegistry.registerCustomBlockData(namespace, key, state);
|
||||
}
|
||||
|
||||
public static void beginWorldMaintenance(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return;
|
||||
}
|
||||
WORLD_MAINTENANCE_DEPTH.computeIfAbsent(level, (ServerLevel l) -> new AtomicInteger()).incrementAndGet();
|
||||
}
|
||||
|
||||
public static void endWorldMaintenance(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return;
|
||||
}
|
||||
AtomicInteger counter = WORLD_MAINTENANCE_DEPTH.get(level);
|
||||
if (counter == null) {
|
||||
return;
|
||||
}
|
||||
if (counter.decrementAndGet() <= 0) {
|
||||
WORLD_MAINTENANCE_DEPTH.remove(level);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isWorldMaintenanceActive(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return false;
|
||||
}
|
||||
AtomicInteger counter = WORLD_MAINTENANCE_DEPTH.get(level);
|
||||
return counter != null && counter.get() > 0;
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public final class ModdedCustomContentRegistry {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final List<ModdedDataProvider> PROVIDERS = new CopyOnWriteArrayList<>();
|
||||
private static final Map<String, BlockState> CUSTOM_BLOCKS = new ConcurrentHashMap<>();
|
||||
private static volatile boolean scanned = false;
|
||||
|
||||
private ModdedCustomContentRegistry() {
|
||||
}
|
||||
|
||||
public static void registerCustomBlockData(String namespace, String key, String state) {
|
||||
if (namespace == null || key == null || state == null) {
|
||||
return;
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(namespace + ":" + key);
|
||||
if (identifier == null) {
|
||||
LOGGER.warn("Iris custom block data registration rejected invalid id {}:{}", namespace, key);
|
||||
return;
|
||||
}
|
||||
BlockState parsed;
|
||||
try {
|
||||
parsed = ModdedBlockResolution.strictParse(state).handle();
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris custom block data '{}:{}' has unparseable state '{}'", namespace, key, state, error);
|
||||
return;
|
||||
}
|
||||
CUSTOM_BLOCKS.put(identifier.toString(), parsed);
|
||||
LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state);
|
||||
}
|
||||
|
||||
public static void register(ModdedDataProvider provider) {
|
||||
if (provider == null) {
|
||||
return;
|
||||
}
|
||||
for (ModdedDataProvider existing : PROVIDERS) {
|
||||
if (existing.modId().equals(provider.modId())) {
|
||||
LOGGER.warn("Iris custom content provider for '{}' already registered; ignoring duplicate", provider.modId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
PROVIDERS.add(provider);
|
||||
try {
|
||||
provider.init();
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris custom content provider '{}' failed to initialize", provider.modId(), error);
|
||||
}
|
||||
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
|
||||
}
|
||||
|
||||
public static void discover() {
|
||||
if (scanned) {
|
||||
return;
|
||||
}
|
||||
scanned = true;
|
||||
ServiceLoader<ModdedDataProvider> loader = ServiceLoader.load(ModdedDataProvider.class, ModdedCustomContentRegistry.class.getClassLoader());
|
||||
for (ModdedDataProvider provider : loader) {
|
||||
register(provider);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasProviders() {
|
||||
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
|
||||
}
|
||||
|
||||
public static BlockState resolveBlock(String key) {
|
||||
if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) {
|
||||
return null;
|
||||
}
|
||||
Identifier base = parseIdentifier(key);
|
||||
if (base == null) {
|
||||
return null;
|
||||
}
|
||||
BlockState custom = CUSTOM_BLOCKS.get(base.toString());
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
if (PROVIDERS.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Map<String, String> state = parseState(key);
|
||||
for (ModdedDataProvider provider : PROVIDERS) {
|
||||
if (!provider.isReady() || !provider.isValidProvider(base, ModdedDataType.BLOCK)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
BlockState resolved = provider.getBlockData(base, state);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris custom content provider '{}' failed resolving block {}", provider.modId(), key, error);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) {
|
||||
if (PROVIDERS.isEmpty() || level == null || key == null) {
|
||||
return null;
|
||||
}
|
||||
Identifier base = parseIdentifier(key);
|
||||
if (base == null) {
|
||||
return null;
|
||||
}
|
||||
for (ModdedDataProvider provider : PROVIDERS) {
|
||||
if (!provider.isReady() || !provider.isValidProvider(base, ModdedDataType.ENTITY)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Entity entity = provider.spawnMob(level, x, y, z, base);
|
||||
if (entity != null) {
|
||||
return entity;
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris custom content provider '{}' failed spawning mob {}", provider.modId(), key, error);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Identifier parseIdentifier(String key) {
|
||||
String trimmed = key.trim();
|
||||
int bracket = trimmed.indexOf('[');
|
||||
String head = bracket < 0 ? trimmed : trimmed.substring(0, bracket);
|
||||
return Identifier.tryParse(head);
|
||||
}
|
||||
|
||||
private static Map<String, String> parseState(String key) {
|
||||
Map<String, String> state = new LinkedHashMap<>();
|
||||
int open = key.indexOf('[');
|
||||
int close = key.indexOf(']');
|
||||
if (open < 0 || close < open) {
|
||||
return state;
|
||||
}
|
||||
String body = key.substring(open + 1, close);
|
||||
if (body.isEmpty()) {
|
||||
return state;
|
||||
}
|
||||
for (String pair : body.split(",")) {
|
||||
int eq = pair.indexOf('=');
|
||||
if (eq <= 0) {
|
||||
continue;
|
||||
}
|
||||
state.put(pair.substring(0, eq).trim(), pair.substring(eq + 1).trim());
|
||||
}
|
||||
return state;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
public interface ModdedDataProvider {
|
||||
String modId();
|
||||
|
||||
default boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
Collection<Identifier> getTypes(ModdedDataType type);
|
||||
|
||||
boolean isValidProvider(Identifier id, ModdedDataType type);
|
||||
|
||||
default BlockState getBlockData(Identifier blockId, Map<String, String> state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default void init() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
public enum ModdedDataType {
|
||||
BLOCK,
|
||||
ITEM,
|
||||
ENTITY
|
||||
}
|
||||
+164
-6
@@ -23,16 +23,21 @@ import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.Locator;
|
||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockState;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedLoader;
|
||||
import art.arcane.iris.modded.ModdedPackInstaller;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
@@ -45,6 +50,7 @@ import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.particles.DustParticleOptions;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -53,6 +59,8 @@ import net.minecraft.world.entity.Relative;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -76,6 +84,8 @@ public final class IrisModdedCommands {
|
||||
private static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
|
||||
private static final SuggestionProvider<CommandSourceStack> STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
|
||||
private static final SuggestionProvider<CommandSourceStack> POI_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder);
|
||||
private static final SuggestionProvider<CommandSourceStack> MARKER_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("cave_floor", "cave_ceiling", "object"), builder);
|
||||
private static final DustParticleOptions MARKER_DUST = new DustParticleOptions(0x5A8CFF, 1.2F);
|
||||
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
|
||||
private static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
|
||||
|
||||
@@ -104,7 +114,12 @@ public final class IrisModdedCommands {
|
||||
.executes((CommandContext<CommandSourceStack> context) -> info(context.getSource(), StringArgumentType.getString(context, "dimension")))));
|
||||
|
||||
root.then(Commands.literal("what").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> what(context.getSource())));
|
||||
.executes((CommandContext<CommandSourceStack> context) -> what(context.getSource()))
|
||||
.then(Commands.literal("block")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> whatBlock(context.getSource())))
|
||||
.then(Commands.literal("markers")
|
||||
.then(Commands.argument("marker", StringArgumentType.greedyString()).suggests(MARKER_TYPES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> whatMarkers(context.getSource(), StringArgumentType.getString(context, "marker"))))));
|
||||
|
||||
root.then(gotoTree("goto"));
|
||||
root.then(gotoTree("find"));
|
||||
@@ -133,6 +148,8 @@ public final class IrisModdedCommands {
|
||||
root.then(ModdedObjectCommands.tree("o"));
|
||||
root.then(editTree());
|
||||
|
||||
root.then(createTree());
|
||||
|
||||
root.then(ModdedStudioCommands.tree("studio"));
|
||||
root.then(ModdedStudioCommands.tree("std"));
|
||||
root.then(ModdedStudioCommands.tree("s"));
|
||||
@@ -150,6 +167,21 @@ public final class IrisModdedCommands {
|
||||
return root;
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> createTree() {
|
||||
return Commands.literal("create").requires(GATE)
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
|
||||
StringArgumentType.getString(context, "name"),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
1337L))
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
|
||||
StringArgumentType.getString(context, "name"),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
LongArgumentType.getLong(context, "seed"))))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> helpTree() {
|
||||
return Commands.literal("help")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), ""))
|
||||
@@ -202,13 +234,19 @@ public final class IrisModdedCommands {
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), name))
|
||||
.then(Commands.literal("start")
|
||||
.then(Commands.argument("radius", IntegerArgumentType.integer(1, 100000))
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 0, 0))
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 0, 0, false, ModdedPregenMode.ASYNC, false))
|
||||
.then(pregenStartFlag("gui", false, true, ModdedPregenMode.ASYNC, false))
|
||||
.then(pregenStartFlag("sync", false, false, ModdedPregenMode.SYNC, false))
|
||||
.then(pregenStartFlag("cached", false, false, ModdedPregenMode.SYNC, true))
|
||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||
.then(Commands.argument("z", IntegerArgumentType.integer())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(),
|
||||
IntegerArgumentType.getInteger(context, "radius"),
|
||||
IntegerArgumentType.getInteger(context, "x"),
|
||||
IntegerArgumentType.getInteger(context, "z")))))))
|
||||
IntegerArgumentType.getInteger(context, "z"), false, ModdedPregenMode.ASYNC, false))
|
||||
.then(pregenStartFlag("gui", true, true, ModdedPregenMode.ASYNC, false))
|
||||
.then(pregenStartFlag("sync", true, false, ModdedPregenMode.SYNC, false))
|
||||
.then(pregenStartFlag("cached", true, false, ModdedPregenMode.SYNC, true))))))
|
||||
.then(Commands.literal("stop")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStop(context.getSource())))
|
||||
.then(Commands.literal("x")
|
||||
@@ -221,6 +259,14 @@ public final class IrisModdedCommands {
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStatus(context.getSource())));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> pregenStartFlag(String name, boolean withCenter, boolean gui, ModdedPregenMode mode, boolean cached) {
|
||||
return Commands.literal(name).executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(),
|
||||
IntegerArgumentType.getInteger(context, "radius"),
|
||||
withCenter ? IntegerArgumentType.getInteger(context, "x") : 0,
|
||||
withCenter ? IntegerArgumentType.getInteger(context, "z") : 0,
|
||||
gui, mode, cached));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> goldenhashTree(String name) {
|
||||
LiteralArgumentBuilder<CommandSourceStack> radiusAndThreads = Commands.literal(name).requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO));
|
||||
@@ -288,19 +334,29 @@ public final class IrisModdedCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int pregenStart(CommandSourceStack source, int radius, int centerX, int centerZ) {
|
||||
private static int pregenStart(CommandSourceStack source, int radius, int centerX, int centerZ, boolean gui, ModdedPregenMode mode, boolean cached) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null) {
|
||||
fail(source, "This dimension is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
if (!ModdedPregenJob.start(level, engine, radius, centerX, centerZ)) {
|
||||
boolean showGui = gui && ModdedGuiHost.isGuiLaunchable();
|
||||
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, mode, cached)) {
|
||||
fail(source, "A pregeneration task is already running. Stop it first with /iris pregen stop.");
|
||||
return 0;
|
||||
}
|
||||
String guiNote;
|
||||
if (!gui) {
|
||||
guiNote = "";
|
||||
} else if (showGui) {
|
||||
guiNote = " A progress map window is opening on the server display.";
|
||||
} else {
|
||||
guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")";
|
||||
}
|
||||
String modeNote = mode == ModdedPregenMode.SYNC ? " Mode: sync" + (cached ? " (checkpoint cache resumable)." : ".") : "";
|
||||
ok(source, "Pregen started in " + level.dimension().identifier() + " of " + (radius * 2) + " by " + (radius * 2)
|
||||
+ " blocks from " + centerX + "," + centerZ + ". Progress logs to console; see /iris pregen status.");
|
||||
+ " blocks from " + centerX + "," + centerZ + "." + modeNote + " Progress logs to console; see /iris pregen status." + guiNote);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -418,6 +474,108 @@ public final class IrisModdedCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int whatBlock(CommandSourceStack source) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
fail(source, "This command can only be used by players (it inspects the block you are looking at).");
|
||||
return 0;
|
||||
}
|
||||
HitResult hit = player.pick(128.0D, 1.0F, false);
|
||||
if (hit.getType() != HitResult.Type.BLOCK || !(hit instanceof BlockHitResult blockHit)) {
|
||||
fail(source, "Look at a block, not the sky.");
|
||||
return 0;
|
||||
}
|
||||
ServerLevel level = source.getLevel();
|
||||
BlockPos pos = blockHit.getBlockPos();
|
||||
BlockState state = level.getBlockState(pos);
|
||||
PlatformBlockState platform = ModdedBlockState.of(state, null);
|
||||
ok(source, "Block: " + platform.key() + " (y=" + pos.getY() + ")");
|
||||
List<String> flags = new ArrayList<>();
|
||||
if (platform.isSolid()) {
|
||||
flags.add("solid");
|
||||
}
|
||||
if (platform.isFluid()) {
|
||||
flags.add("fluid");
|
||||
}
|
||||
if (platform.isWater()) {
|
||||
flags.add("water");
|
||||
}
|
||||
if (platform.isWaterLogged()) {
|
||||
flags.add("waterlogged");
|
||||
}
|
||||
if (platform.isStorage()) {
|
||||
flags.add("storage (loot capable)");
|
||||
}
|
||||
if (platform.isLit()) {
|
||||
flags.add("lit");
|
||||
}
|
||||
if (platform.isFoliage()) {
|
||||
flags.add("foliage");
|
||||
}
|
||||
if (platform.isFoliagePlantable()) {
|
||||
flags.add("plantable foliage");
|
||||
}
|
||||
if (platform.isDecorant()) {
|
||||
flags.add("decorant");
|
||||
}
|
||||
if (platform.isOre()) {
|
||||
flags.add("ore");
|
||||
}
|
||||
if (platform.hasTileEntity()) {
|
||||
flags.add("tile entity");
|
||||
}
|
||||
ok(source, flags.isEmpty() ? "Properties: (none)" : "Properties: " + String.join(", ", flags));
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int whatMarkers(CommandSourceStack source, String markerRaw) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
fail(source, "This command can only be used by players (markers render as particles around you).");
|
||||
return 0;
|
||||
}
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null) {
|
||||
fail(source, "This dimension is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
String marker = markerRaw.trim();
|
||||
BlockPos origin = player.blockPosition();
|
||||
int chunkX = origin.getX() >> 4;
|
||||
int chunkZ = origin.getZ() >> 4;
|
||||
MinecraftServer server = source.getServer();
|
||||
ok(source, "Scanning for '" + marker + "' markers around you...");
|
||||
Thread thread = new Thread(() -> {
|
||||
List<int[]> hits = new ArrayList<>();
|
||||
MatterMarker matterMarker = new MatterMarker(marker);
|
||||
try {
|
||||
for (int cx = chunkX - 4; cx <= chunkX + 4; cx++) {
|
||||
for (int cz = chunkZ - 4; cz <= chunkZ + 4; cz++) {
|
||||
for (IrisPosition position : engine.getMantle().findMarkers(cx, cz, matterMarker)) {
|
||||
hits.add(new int[]{position.getX(), position.getY(), position.getZ()});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris marker scan failed for {}", marker, e);
|
||||
server.execute(() -> fail(source, "Marker scan failed: " + e.getClass().getSimpleName()));
|
||||
return;
|
||||
}
|
||||
server.execute(() -> {
|
||||
for (int[] hit : hits) {
|
||||
level.sendParticles(player, MARKER_DUST, true, true,
|
||||
hit[0] + 0.5D, hit[1] + 1.0D, hit[2] + 0.5D,
|
||||
3, 0.2D, 0.2D, 0.2D, 0.0D);
|
||||
}
|
||||
ok(source, "Found " + hits.size() + " nearby marker(s) (" + marker + ")");
|
||||
});
|
||||
}, "Iris Marker Scan");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int gotoBiome(CommandSourceStack source, String key) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
|
||||
+15
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.ClickEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.HoverEvent;
|
||||
@@ -161,6 +162,9 @@ final class ModdedCommandHelp {
|
||||
ModdedCommandFeedback.clear(source);
|
||||
|
||||
sendHeader(source, normalized);
|
||||
if (!Commands.hasPermission(Commands.LEVEL_GAMEMASTERS).test(source)) {
|
||||
ModdedCommandFeedback.send(source, opNotice());
|
||||
}
|
||||
if (!normalized.isEmpty()) {
|
||||
ModdedCommandFeedback.send(source, backButton(normalized));
|
||||
}
|
||||
@@ -293,6 +297,17 @@ final class ModdedCommandHelp {
|
||||
return hover;
|
||||
}
|
||||
|
||||
private static MutableComponent opNotice() {
|
||||
MutableComponent notice = Component.empty();
|
||||
notice.append(text("⚠ ", REQUIRED));
|
||||
notice.append(text("Iris commands need operator permission (level 2). ", REQUIRED_TEXT));
|
||||
notice.append(text("Run ", DESCRIPTION));
|
||||
notice.append(text("/op <you>", PARAMETER_ALT));
|
||||
notice.append(text(" from the console (or enable cheats in singleplayer); ", DESCRIPTION));
|
||||
notice.append(text("until then these commands will not run or tab-complete.", USAGE));
|
||||
return notice;
|
||||
}
|
||||
|
||||
private static MutableComponent footer() {
|
||||
return ModdedCommandFeedback.footer();
|
||||
}
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ import java.util.function.Predicate;
|
||||
public final class ModdedDatapackCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||
private static final String WORLD_PACK_NAME = ModdedWorldDatapackWriter.WORLD_PACK_NAME;
|
||||
private static final String WORLD_PACK_NAME = "iris";
|
||||
|
||||
private ModdedDatapackCommands() {
|
||||
}
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.core.gui.GuiOverlay;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedGuiHost implements GuiHost.Provider {
|
||||
private static final ModdedGuiHost INSTANCE = new ModdedGuiHost();
|
||||
|
||||
private final Map<Engine, ServerLevel> levels = new ConcurrentHashMap<>();
|
||||
private volatile Engine active;
|
||||
private volatile MinecraftServer server;
|
||||
|
||||
private ModdedGuiHost() {
|
||||
}
|
||||
|
||||
public static void install() {
|
||||
GuiHost.set(INSTANCE);
|
||||
}
|
||||
|
||||
public static void bindContext(MinecraftServer server, ServerLevel level, Engine engine) {
|
||||
INSTANCE.server = server;
|
||||
INSTANCE.active = engine;
|
||||
INSTANCE.levels.put(engine, level);
|
||||
}
|
||||
|
||||
public static boolean isGuiLaunchable() {
|
||||
return GuiHost.isAvailable() && IrisSettings.get().getGui().isUseServerLaunchedGuis();
|
||||
}
|
||||
|
||||
public static String guiUnavailableReason() {
|
||||
if (!GuiHost.isAvailable()) {
|
||||
return "headless JVM (no display)";
|
||||
}
|
||||
return "gui.useServerLaunchedGuis=false in Iris settings";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine findActiveEngine() {
|
||||
Engine current = active;
|
||||
if (current != null && !current.isClosed()) {
|
||||
return current;
|
||||
}
|
||||
for (Map.Entry<Engine, ServerLevel> entry : levels.entrySet()) {
|
||||
if (!entry.getKey().isClosed()) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiOverlay overlayFor(Engine engine) {
|
||||
if (engine == null) {
|
||||
return null;
|
||||
}
|
||||
ServerLevel level = levels.get(engine);
|
||||
if (level == null || server == null) {
|
||||
return null;
|
||||
}
|
||||
return new ModdedVisionOverlay(server, level, engine);
|
||||
}
|
||||
}
|
||||
+63
-9
@@ -23,7 +23,9 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.modded.ModdedBlockState;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
@@ -37,9 +39,14 @@ import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
@@ -272,7 +279,8 @@ public final class ModdedObjectCommands {
|
||||
return 0;
|
||||
}
|
||||
int[] tilesSkipped = {0};
|
||||
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped);
|
||||
int[] tilesSaved = {0};
|
||||
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped, tilesSaved);
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null) {
|
||||
parent.mkdirs();
|
||||
@@ -284,15 +292,25 @@ public final class ModdedObjectCommands {
|
||||
IrisModdedCommands.fail(source, "Failed to save object: " + e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
String tileNote = tilesSkipped[0] > 0 ? " (" + tilesSkipped[0] + " tile state(s) not captured; tile capture is Bukkit-only for now)" : "";
|
||||
StringBuilder tileNote = new StringBuilder();
|
||||
if (tilesSaved[0] > 0) {
|
||||
tileNote.append(" (").append(tilesSaved[0]).append(" tile entity state(s) captured");
|
||||
if (tilesSkipped[0] > 0) {
|
||||
tileNote.append(", ").append(tilesSkipped[0]).append(" failed");
|
||||
}
|
||||
tileNote.append(")");
|
||||
} else if (tilesSkipped[0] > 0) {
|
||||
tileNote.append(" (").append(tilesSkipped[0]).append(" tile state(s) could not be captured)");
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Saved " + engine.getData().getDataFolder().getName() + "/objects/" + name + ".iob: "
|
||||
+ w + "x" + h + "x" + d + ", " + object.getBlocks().size() + " block(s)" + tileNote);
|
||||
LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSkipped[0], file.getAbsolutePath());
|
||||
LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSaved={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSaved[0], tilesSkipped[0], file.getAbsolutePath());
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static IrisObject capture(ServerLevel level, BlockPos min, BlockPos max, int w, int h, int d, int[] tilesSkipped) {
|
||||
private static IrisObject capture(ServerLevel level, BlockPos min, BlockPos max, int w, int h, int d, int[] tilesSkipped, int[] tilesSaved) {
|
||||
IrisObject object = new IrisObject(w, h, d);
|
||||
HolderLookup.Provider provider = level.registryAccess();
|
||||
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
|
||||
for (int x = min.getX(); x <= max.getX(); x++) {
|
||||
for (int y = min.getY(); y <= max.getY(); y++) {
|
||||
@@ -301,16 +319,41 @@ public final class ModdedObjectCommands {
|
||||
if (state.is(Blocks.AIR)) {
|
||||
continue;
|
||||
}
|
||||
int ox = x - min.getX();
|
||||
int oy = y - min.getY();
|
||||
int oz = z - min.getZ();
|
||||
object.setUnsigned(ox, oy, oz, ModdedBlockState.of(state, null));
|
||||
if (state.hasBlockEntity()) {
|
||||
tilesSkipped[0]++;
|
||||
TileData tile = captureTile(level, provider, cursor.immutable(), state);
|
||||
if (tile != null) {
|
||||
object.setUnsignedTile(ox, oy, oz, tile);
|
||||
tilesSaved[0]++;
|
||||
} else {
|
||||
tilesSkipped[0]++;
|
||||
}
|
||||
}
|
||||
object.setUnsigned(x - min.getX(), y - min.getY(), z - min.getZ(), ModdedBlockState.of(state, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
private static TileData captureTile(ServerLevel level, HolderLookup.Provider provider, BlockPos pos, BlockState state) {
|
||||
BlockEntity blockEntity = level.getBlockEntity(pos);
|
||||
if (blockEntity == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
CompoundTag tag = blockEntity.saveWithFullMetadata(provider);
|
||||
String snbt = NbtUtils.structureToSnbt(tag);
|
||||
String blockKey = BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
|
||||
return ModdedTileData.capture(blockKey, snbt);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris tile capture failed at {} {} {}", pos.getX(), pos.getY(), pos.getZ(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int paste(CommandSourceStack source, String keyRaw, int rotation, BlockPos at) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
@@ -354,11 +397,11 @@ public final class ModdedObjectCommands {
|
||||
}
|
||||
UUID owner = player == null ? ModdedObjectUndo.CONSOLE : player.getUUID();
|
||||
ModdedObjectUndo.record(owner, level, placer.undoSnapshot());
|
||||
String tileNote = placer.skippedTiles() > 0 ? ", " + placer.skippedTiles() + " tile state(s) skipped" : "";
|
||||
String tileNote = tileNote(placer);
|
||||
IrisModdedCommands.ok(source, "Placed " + key + " at " + target.getX() + " " + target.getY() + " " + target.getZ()
|
||||
+ " rot=" + rotation + " (" + placer.writes() + " write(s), " + placer.nonAirWrites() + " non-air" + tileNote + ")");
|
||||
LOGGER.info("Iris paste: {} at {},{},{} rot={} writes={} nonAir={} tilesSkipped={}",
|
||||
key, target.getX(), target.getY(), target.getZ(), rotation, placer.writes(), placer.nonAirWrites(), placer.skippedTiles());
|
||||
LOGGER.info("Iris paste: {} at {},{},{} rot={} writes={} nonAir={} tilesRestored={} tilesSkipped={}",
|
||||
key, target.getX(), target.getY(), target.getZ(), rotation, placer.writes(), placer.nonAirWrites(), placer.restoredTiles(), placer.skippedTiles());
|
||||
return placer.writes() > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -619,4 +662,15 @@ public final class ModdedObjectCommands {
|
||||
return "(" + first.getX() + "," + first.getY() + "," + first.getZ() + ") -> ("
|
||||
+ second.getX() + "," + second.getY() + "," + second.getZ() + ")";
|
||||
}
|
||||
|
||||
static String tileNote(ModdedObjectPlacer placer) {
|
||||
StringBuilder note = new StringBuilder();
|
||||
if (placer.restoredTiles() > 0) {
|
||||
note.append(", ").append(placer.restoredTiles()).append(" tile entity state(s) restored");
|
||||
}
|
||||
if (placer.skippedTiles() > 0) {
|
||||
note.append(", ").append(placer.skippedTiles()).append(" tile state(s) skipped");
|
||||
}
|
||||
return note.toString();
|
||||
}
|
||||
}
|
||||
|
||||
+42
-1
@@ -24,23 +24,32 @@ import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||
import art.arcane.iris.modded.ModdedBlockState;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class ModdedObjectPlacer implements IObjectPlacer {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private final ServerLevel level;
|
||||
private final Map<BlockPos, BlockState> undo = new HashMap<>();
|
||||
private int writes = 0;
|
||||
private int nonAirWrites = 0;
|
||||
private int skippedTiles = 0;
|
||||
private int restoredTiles = 0;
|
||||
|
||||
ModdedObjectPlacer(ServerLevel level) {
|
||||
this.level = level;
|
||||
@@ -62,6 +71,10 @@ final class ModdedObjectPlacer implements IObjectPlacer {
|
||||
return skippedTiles;
|
||||
}
|
||||
|
||||
int restoredTiles() {
|
||||
return restoredTiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
return level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
|
||||
@@ -128,7 +141,35 @@ final class ModdedObjectPlacer implements IObjectPlacer {
|
||||
|
||||
@Override
|
||||
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||
skippedTiles++;
|
||||
if (!(tile instanceof ModdedTileData moddedTile)) {
|
||||
skippedTiles++;
|
||||
return;
|
||||
}
|
||||
String snbt = moddedTile.snbt();
|
||||
if (snbt == null || snbt.isBlank()) {
|
||||
skippedTiles++;
|
||||
return;
|
||||
}
|
||||
BlockPos pos = new BlockPos(xx, yy, zz);
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (!state.hasBlockEntity()) {
|
||||
skippedTiles++;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CompoundTag tag = NbtUtils.snbtToStructure(snbt);
|
||||
BlockEntity restored = BlockEntity.loadStatic(pos, state, tag, level.registryAccess());
|
||||
if (restored == null) {
|
||||
skippedTiles++;
|
||||
return;
|
||||
}
|
||||
level.setBlockEntity(restored);
|
||||
restored.setChanged();
|
||||
restoredTiles++;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris tile restore failed at {} {} {}", xx, yy, zz, e);
|
||||
skippedTiles++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+109
-5
@@ -18,26 +18,42 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.gui.PregenRenderSource;
|
||||
import art.arcane.iris.core.gui.PregenRenderer;
|
||||
import art.arcane.iris.core.pregenerator.IrisPregenerator;
|
||||
import art.arcane.iris.core.pregenerator.PregenListener;
|
||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||
import art.arcane.iris.core.pregenerator.cache.PregenCache;
|
||||
import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class ModdedPregenJob implements PregenListener {
|
||||
public final class ModdedPregenJob implements PregenListener, PregenRenderSource {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final AtomicReference<ModdedPregenJob> ACTIVE = new AtomicReference<>();
|
||||
private static final Color COLOR_GENERATING = new Color(0x66967f);
|
||||
private static final Color COLOR_GENERATED = new Color(0x65c295);
|
||||
|
||||
private final String dimension;
|
||||
private final IrisPregenerator pregenerator;
|
||||
private final Engine engine;
|
||||
private final Position2 min;
|
||||
private final Position2 max;
|
||||
private final PregenRenderer renderer;
|
||||
private volatile double chunksPerSecond;
|
||||
private volatile long generated;
|
||||
private volatile long totalChunks;
|
||||
@@ -45,12 +61,46 @@ public final class ModdedPregenJob implements PregenListener {
|
||||
private volatile long elapsed;
|
||||
private volatile String method = "Modded";
|
||||
|
||||
private ModdedPregenJob(ServerLevel level, Engine engine, PregenTask task) {
|
||||
private ModdedPregenJob(MinecraftServer server, ServerLevel level, Engine engine, PregenTask task, boolean gui, ModdedPregenMode mode, boolean cached) {
|
||||
this.dimension = level.dimension().identifier().toString();
|
||||
this.pregenerator = new IrisPregenerator(task, new ModdedPregenMethod(level, engine), this);
|
||||
this.engine = engine;
|
||||
this.min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
this.max = new Position2(Integer.MIN_VALUE, Integer.MIN_VALUE);
|
||||
task.iterateAllChunks((int chunkX, int chunkZ) -> {
|
||||
min.setX(Math.min(chunkX, min.getX()));
|
||||
min.setZ(Math.min(chunkZ, min.getZ()));
|
||||
max.setX(Math.max(chunkX, max.getX()));
|
||||
max.setZ(Math.max(chunkZ, max.getZ()));
|
||||
});
|
||||
PregeneratorMethod baseMethod = new ModdedPregenMethod(level, engine, mode);
|
||||
PregeneratorMethod resolvedMethod = baseMethod;
|
||||
if (cached) {
|
||||
PregenCache cache = PregenCache.create(cacheDirectory(level)).sync();
|
||||
resolvedMethod = new CachedPregenMethod(baseMethod, cache);
|
||||
}
|
||||
this.method = mode == ModdedPregenMode.SYNC ? "Modded Sync" : "Modded";
|
||||
this.pregenerator = new IrisPregenerator(task, resolvedMethod, this);
|
||||
PregenRenderer openedRenderer = null;
|
||||
if (gui) {
|
||||
try {
|
||||
openedRenderer = PregenRenderer.open("Iris Pregen: " + dimension, this, ModdedPregenJob::pauseResume);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris pregen GUI failed to open for {}", dimension, e);
|
||||
}
|
||||
}
|
||||
this.renderer = openedRenderer;
|
||||
}
|
||||
|
||||
public static boolean start(ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ) {
|
||||
private static File cacheDirectory(ServerLevel level) {
|
||||
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
|
||||
return new File(worldFolder, "iris" + File.separator + "pregen");
|
||||
}
|
||||
|
||||
public static boolean start(MinecraftServer server, ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean gui) {
|
||||
return start(server, level, engine, radiusBlocks, centerBlockX, centerBlockZ, gui, ModdedPregenMode.ASYNC, false);
|
||||
}
|
||||
|
||||
public static boolean start(MinecraftServer server, ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean gui, ModdedPregenMode mode, boolean cached) {
|
||||
if (ACTIVE.get() != null) {
|
||||
return false;
|
||||
}
|
||||
@@ -60,8 +110,9 @@ public final class ModdedPregenJob implements PregenListener {
|
||||
.radiusX(radiusBlocks)
|
||||
.radiusZ(radiusBlocks)
|
||||
.build();
|
||||
ModdedPregenJob job = new ModdedPregenJob(level, engine, task);
|
||||
ModdedPregenJob job = new ModdedPregenJob(server, level, engine, task, gui, mode, cached);
|
||||
if (!ACTIVE.compareAndSet(null, job)) {
|
||||
job.closeRenderer();
|
||||
return false;
|
||||
}
|
||||
Thread thread = new Thread(() -> {
|
||||
@@ -70,6 +121,7 @@ public final class ModdedPregenJob implements PregenListener {
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris pregen failed for {}", job.dimension, e);
|
||||
} finally {
|
||||
job.closeRenderer();
|
||||
ACTIVE.compareAndSet(job, null);
|
||||
}
|
||||
}, "Iris Pregen");
|
||||
@@ -78,6 +130,27 @@ public final class ModdedPregenJob implements PregenListener {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void closeRenderer() {
|
||||
if (renderer != null) {
|
||||
renderer.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void draw(int chunkX, int chunkZ, Color color) {
|
||||
if (renderer == null || !renderer.isVisibleFrame()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Color resolved = color;
|
||||
if (engine != null) {
|
||||
resolved = engine.draw((chunkX << 4) + 8, (chunkZ << 4) + 8);
|
||||
}
|
||||
renderer.submit(chunkX, chunkZ, resolved);
|
||||
} catch (Throwable e) {
|
||||
renderer.submit(chunkX, chunkZ, color);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean stop() {
|
||||
ModdedPregenJob job = ACTIVE.get();
|
||||
if (job == null) {
|
||||
@@ -176,10 +249,12 @@ public final class ModdedPregenJob implements PregenListener {
|
||||
|
||||
@Override
|
||||
public void onChunkGenerating(int x, int z) {
|
||||
draw(x, z, COLOR_GENERATING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerated(int x, int z, boolean cached) {
|
||||
draw(x, z, COLOR_GENERATED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -228,5 +303,34 @@ public final class ModdedPregenJob implements PregenListener {
|
||||
|
||||
@Override
|
||||
public void onChunkExistsInRegionGen(int x, int z) {
|
||||
draw(x, z, COLOR_GENERATED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position2 min() {
|
||||
return min;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position2 max() {
|
||||
return max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] progress() {
|
||||
double percent = percent();
|
||||
return new String[]{
|
||||
(pregenerator.paused() ? "PAUSED " : "Generating ") + Form.f(generated) + " of " + Form.f(totalChunks)
|
||||
+ " (" + String.format("%.1f", percent) + "%)",
|
||||
"Speed: " + Form.f((int) chunksPerSecond) + " Chunks/s",
|
||||
Form.duration(eta, 2) + " Remaining (" + Form.duration(elapsed, 2) + " Elapsed)",
|
||||
"Dimension: " + dimension,
|
||||
"Method: " + method
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean paused() {
|
||||
return pregenerator.paused();
|
||||
}
|
||||
}
|
||||
|
||||
+46
-4
@@ -31,8 +31,10 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
@@ -40,13 +42,19 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
private final ServerLevel level;
|
||||
private final Engine engine;
|
||||
private final ModdedPregenMode mode;
|
||||
private final Semaphore semaphore;
|
||||
private final int permits;
|
||||
private final int timeoutSeconds;
|
||||
|
||||
public ModdedPregenMethod(ServerLevel level, Engine engine) {
|
||||
this(level, engine, ModdedPregenMode.ASYNC);
|
||||
}
|
||||
|
||||
public ModdedPregenMethod(ServerLevel level, Engine engine, ModdedPregenMode mode) {
|
||||
this.level = level;
|
||||
this.engine = engine;
|
||||
this.mode = mode;
|
||||
this.permits = Math.min(96, Math.max(8, Runtime.getRuntime().availableProcessors() * 2));
|
||||
this.semaphore = new Semaphore(permits, true);
|
||||
this.timeoutSeconds = Math.max(120, IrisSettings.get().getPregen().getChunkLoadTimeoutSeconds());
|
||||
@@ -54,13 +62,15 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
LOGGER.info("Iris modded pregen init: dim={} inFlightCap={} timeout={}s",
|
||||
level.dimension().identifier(), permits, timeoutSeconds);
|
||||
LOGGER.info("Iris modded pregen init: dim={} mode={} inFlightCap={} timeout={}s",
|
||||
level.dimension().identifier(), mode, mode == ModdedPregenMode.ASYNC ? permits : 1, timeoutSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
semaphore.acquireUninterruptibly(permits);
|
||||
if (mode == ModdedPregenMode.ASYNC) {
|
||||
semaphore.acquireUninterruptibly(permits);
|
||||
}
|
||||
saveLevel();
|
||||
}
|
||||
|
||||
@@ -85,7 +95,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
@Override
|
||||
public boolean isAsyncChunkMode() {
|
||||
return true;
|
||||
return mode == ModdedPregenMode.ASYNC;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,6 +105,38 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
@Override
|
||||
public void generateChunk(int x, int z, PregenListener listener) {
|
||||
if (mode == ModdedPregenMode.SYNC) {
|
||||
generateChunkSync(x, z, listener);
|
||||
return;
|
||||
}
|
||||
generateChunkAsync(x, z, listener);
|
||||
}
|
||||
|
||||
private void generateChunkSync(int x, int z, PregenListener listener) {
|
||||
listener.onChunkGenerating(x, z);
|
||||
ChunkPos pos = new ChunkPos(x, z);
|
||||
CompletableFuture<?> loadFuture = CompletableFuture
|
||||
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(PREGEN_TICKET, pos, 0), level.getServer())
|
||||
.thenCompose((CompletableFuture<?> inner) -> inner);
|
||||
try {
|
||||
Object result = loadFuture.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||
if (result instanceof ChunkResult<?> chunkResult && !chunkResult.isSuccess()) {
|
||||
LOGGER.warn("Iris pregen chunk {},{} returned no chunk: {}", x, z, chunkResult.getError());
|
||||
return;
|
||||
}
|
||||
listener.onChunkGenerated(x, z);
|
||||
cleanupMantleChunk(x, z);
|
||||
listener.onChunkCleaned(x, z);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, e.toString());
|
||||
} finally {
|
||||
level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0));
|
||||
}
|
||||
}
|
||||
|
||||
private void generateChunkAsync(int x, int z, PregenListener listener) {
|
||||
listener.onChunkGenerating(x, z);
|
||||
try {
|
||||
semaphore.acquire();
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
public enum ModdedPregenMode {
|
||||
ASYNC,
|
||||
SYNC
|
||||
}
|
||||
+1
-1
@@ -201,7 +201,7 @@ public final class ModdedStructureCommands {
|
||||
return 0;
|
||||
}
|
||||
ModdedObjectUndo.record(owner, level, placer.undoSnapshot());
|
||||
String tileNote = placer.skippedTiles() > 0 ? ", " + placer.skippedTiles() + " tile state(s) skipped" : "";
|
||||
String tileNote = ModdedObjectCommands.tileNote(placer);
|
||||
IrisModdedCommands.ok(source, "Placed '" + key + "' (" + pieces.size() + " pieces, " + placer.writes() + " write(s)" + tileNote + ") at your location. /iris object undo to revert.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
+280
-46
@@ -18,36 +18,47 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
||||
import art.arcane.iris.core.gui.VisionGUI;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisEntitySpawn;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisSpawner;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedPackInstaller;
|
||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.math.Spiraler;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Relative;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
@@ -57,11 +68,17 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -69,7 +86,22 @@ public final class ModdedStudioCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
|
||||
private static final Pattern STUDIO_ID_SANITIZER = Pattern.compile("[^a-z0-9_-]");
|
||||
private static final String STUDIO_NAMESPACE = "irisworldgen";
|
||||
private static final String STUDIO_PREFIX = "studio_";
|
||||
private static final String DEFAULT_TEMPLATE = "example";
|
||||
private static final Map<UUID, String> STUDIOS = new ConcurrentHashMap<>();
|
||||
private static final SuggestionProvider<CommandSourceStack> GENERATOR_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getData().getGeneratorLoader().getPossibleKeys(), builder);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return builder.buildFuture();
|
||||
};
|
||||
|
||||
private ModdedStudioCommands() {
|
||||
}
|
||||
@@ -107,20 +139,26 @@ public final class ModdedStudioCommands {
|
||||
|
||||
root.then(openTree("open"));
|
||||
root.then(openTree("o"));
|
||||
root.then(message("close", "There are no studio worlds on modded servers (/iris studio open prepares the pack workflow instead), so there is nothing to close."));
|
||||
root.then(message("x", "There are no studio worlds on modded servers (/iris studio open prepares the pack workflow instead), so there is nothing to close."));
|
||||
root.then(message("tpstudio", "There are no temporary Bukkit studio worlds on modded servers to teleport to; use /iris studio open <pack> to prepare the pack workflow instead."));
|
||||
root.then(message("stp", "There are no temporary Bukkit studio worlds on modded servers to teleport to; use /iris studio open <pack> to prepare the pack workflow instead."));
|
||||
root.then(Commands.literal("close")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> close(context.getSource())));
|
||||
root.then(Commands.literal("x")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> close(context.getSource())));
|
||||
root.then(Commands.literal("tpstudio")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> tpStudio(context.getSource())));
|
||||
root.then(Commands.literal("stp")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> tpStudio(context.getSource())));
|
||||
root.then(Commands.literal("status")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> status(context.getSource())));
|
||||
root.then(message("vscode", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor."));
|
||||
root.then(message("vsc", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor."));
|
||||
root.then(message("update", "Workspace regeneration (.code-workspace + JSON schemas) reads Bukkit registries (SchemaBuilder); run /iris studio update on a Bukkit server against this pack."));
|
||||
root.then(message("importvanilla", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over."));
|
||||
root.then(message("importv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over."));
|
||||
root.then(message("iv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over."));
|
||||
root.then(message("noise", "The noise explorer is a desktop GUI launched from the Bukkit plugin."));
|
||||
root.then(message("nmap", "The noise explorer is a desktop GUI launched from the Bukkit plugin."));
|
||||
root.then(message("map", "The world map renderer is a desktop GUI launched from the Bukkit plugin."));
|
||||
root.then(message("render", "The world map renderer is a desktop GUI launched from the Bukkit plugin."));
|
||||
root.then(noiseTree("noise"));
|
||||
root.then(noiseTree("nmap"));
|
||||
root.then(mapTree("map"));
|
||||
root.then(mapTree("render"));
|
||||
root.then(message("loot", "Loot simulation opens a Bukkit chest inventory GUI; it is not available on modded servers."));
|
||||
root.then(message("profile", "Pack performance profiling is part of the Bukkit studio toolchain and is not ported to modded servers."));
|
||||
root.then(message("spawn", "Iris entity spawning uses the Bukkit entity pipeline and is not ported to modded servers."));
|
||||
@@ -145,6 +183,75 @@ public final class ModdedStudioCommands {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> noiseTree(String name) {
|
||||
return Commands.literal(name)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> noise(context.getSource(), null, 12345L))
|
||||
.then(Commands.argument("generator", StringArgumentType.word()).suggests(GENERATOR_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> noise(context.getSource(), StringArgumentType.getString(context, "generator"), 12345L))
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> noise(context.getSource(), StringArgumentType.getString(context, "generator"), LongArgumentType.getLong(context, "seed")))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> mapTree(String name) {
|
||||
return Commands.literal(name)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> map(context.getSource()));
|
||||
}
|
||||
|
||||
private static int noise(CommandSourceStack source, String generatorKey, long seed) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
||||
return 0;
|
||||
}
|
||||
if (engine != null) {
|
||||
ModdedGuiHost.bindContext(source.getServer(), level, engine);
|
||||
}
|
||||
if (generatorKey == null || generatorKey.isBlank()) {
|
||||
NoiseExplorerGUI.launch();
|
||||
IrisModdedCommands.ok(source, "Opening the Noise Explorer on the server display.");
|
||||
return 1;
|
||||
}
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, "This dimension is not generated by Iris; run /iris studio noise from an Iris dimension to resolve generators, or omit the generator name.");
|
||||
return 0;
|
||||
}
|
||||
IrisGenerator generator = engine.getData().getGeneratorLoader().load(generatorKey.trim());
|
||||
if (generator == null) {
|
||||
IrisModdedCommands.fail(source, "Unknown generator '" + generatorKey + "' in pack " + engine.getDimension().getLoadKey() + ".");
|
||||
return 0;
|
||||
}
|
||||
long mixedSeed = new RNG(seed).nextParallelRNG(3245).lmax();
|
||||
Supplier<Function2<Double, Double, Double>> supplier = () -> (Double x, Double z) -> generator.getHeight(x, z, mixedSeed);
|
||||
NoiseExplorerGUI.launch(supplier, generatorKey.trim());
|
||||
IrisModdedCommands.ok(source, "Opening the Noise Explorer for generator '" + generatorKey.trim() + "' (seed " + seed + ").");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int map(CommandSourceStack source) {
|
||||
ServerLevel level = source.getLevel();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
if (engine == null) {
|
||||
IrisModdedCommands.fail(source, "This dimension is not generated by Iris; stand in an Iris (or studio) dimension and run /iris studio map.");
|
||||
return 0;
|
||||
}
|
||||
if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
||||
return 0;
|
||||
}
|
||||
ModdedGuiHost.bindContext(source.getServer(), level, engine);
|
||||
VisionGUI.launch(engine);
|
||||
IrisModdedCommands.ok(source, "Opening the Vision map for " + level.dimension().identifier() + " on the server display.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static String guiUnavailableMessage() {
|
||||
if (!GuiHost.isAvailable()) {
|
||||
return "This server has no display (headless JVM); the Iris desktop GUIs need an AWT-capable session.";
|
||||
}
|
||||
return "Server-launched GUIs are disabled (gui.useServerLaunchedGuis=false in Iris settings).";
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) {
|
||||
return Commands.literal(name)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> {
|
||||
@@ -176,50 +283,177 @@ public final class ModdedStudioCommands {
|
||||
return folder;
|
||||
}
|
||||
|
||||
private static String studioDimensionId(ServerPlayer player) {
|
||||
String base = STUDIO_ID_SANITIZER.matcher(player.getScoreboardName().toLowerCase(Locale.ROOT)).replaceAll("_");
|
||||
if (base.isBlank()) {
|
||||
base = player.getUUID().toString().replace("-", "");
|
||||
}
|
||||
return STUDIO_NAMESPACE + ":" + STUDIO_PREFIX + base;
|
||||
}
|
||||
|
||||
private static int open(CommandSourceStack source, String pack, long seed) {
|
||||
File folder = resolvePack(source, pack);
|
||||
if (folder == null) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, "This command can only be used by players (a studio teleports you into the dimension).");
|
||||
return 0;
|
||||
}
|
||||
|
||||
IrisData data = IrisData.get(folder);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(folder.getName());
|
||||
if (dimension == null) {
|
||||
IrisModdedCommands.fail(source, "Pack '" + folder.getName() + "' has no dimensions/" + folder.getName() + ".json");
|
||||
if (pack == null || pack.isBlank()) {
|
||||
IrisModdedCommands.fail(source, "Provide a dimension pack: /iris studio open <pack> [seed]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
IrisModdedCommands.ok(source, studioOpenComponent(dimension, folder, seed));
|
||||
MinecraftServer server = source.getServer();
|
||||
UUID owner = player.getUUID();
|
||||
String dimensionId = studioDimensionId(player);
|
||||
IrisModdedCommands.ok(source, "Opening studio for '" + pack + "' (seed " + seed + ")...");
|
||||
Thread thread = new Thread(() -> openAsync(source, server, owner, dimensionId, pack, seed), "Iris Studio Open");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static MutableComponent studioOpenComponent(IrisDimension dimension, File folder, long seed) {
|
||||
String dimensionKey = dimension.getLoadKey() == null ? folder.getName() : dimension.getLoadKey();
|
||||
MutableComponent message = Component.empty();
|
||||
message.append(ModdedCommandFeedback.header("Iris Studio"));
|
||||
message.append(Component.literal("\n"));
|
||||
message.append(ModdedCommandFeedback.text("Opening studio for the \"", ModdedCommandFeedback.DARK_GREEN));
|
||||
message.append(ModdedCommandFeedback.text(dimension.getName(), ModdedCommandFeedback.PARAMETER_ALT));
|
||||
message.append(ModdedCommandFeedback.text("\" pack", ModdedCommandFeedback.DARK_GREEN));
|
||||
message.append(ModdedCommandFeedback.text(" (seed: " + seed + ")", ModdedCommandFeedback.VALUE));
|
||||
message.append(Component.literal("\n"));
|
||||
message.append(ModdedCommandFeedback.text("Pack ", ModdedCommandFeedback.DARK_GREEN));
|
||||
message.append(ModdedCommandFeedback.text(dimensionKey, ModdedCommandFeedback.PARAMETER));
|
||||
message.append(ModdedCommandFeedback.text(" is ready at ", ModdedCommandFeedback.DESCRIPTION));
|
||||
message.append(ModdedCommandFeedback.text(folder.getAbsolutePath(), ModdedCommandFeedback.VALUE));
|
||||
message.append(Component.literal("\n"));
|
||||
message.append(ModdedCommandFeedback.text("Modded servers cannot create Bukkit's temporary studio world at runtime; edit this pack directly, then use the matching world/datapack workflow or ", ModdedCommandFeedback.DESCRIPTION));
|
||||
message.append(ModdedCommandFeedback.button("/iris regen", "/iris regen", "Regenerate nearby chunks after editing this pack", false));
|
||||
message.append(ModdedCommandFeedback.text(" in an Iris dimension.", ModdedCommandFeedback.DESCRIPTION));
|
||||
message.append(Component.literal("\n"));
|
||||
message.append(ModdedCommandFeedback.button("Validate", "/iris pack validate " + dimensionKey, "Validate this pack before loading it", true));
|
||||
message.append(ModdedCommandFeedback.text(" ", ModdedCommandFeedback.OPTIONAL));
|
||||
message.append(ModdedCommandFeedback.button("World Help", "/iris world", "Open Iris world command help", true));
|
||||
message.append(ModdedCommandFeedback.text(" ", ModdedCommandFeedback.OPTIONAL));
|
||||
message.append(ModdedCommandFeedback.button("Package", "/iris studio package " + dimensionKey, "Package this dimension", false));
|
||||
message.append(Component.literal("\n"));
|
||||
message.append(ModdedCommandFeedback.footer());
|
||||
return message;
|
||||
private static void openAsync(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) {
|
||||
try {
|
||||
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
|
||||
if (!new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
||||
server.execute(() -> IrisModdedCommands.ok(source, "Pack '" + pack + "' missing; downloading IrisDimensions/" + pack + "..."));
|
||||
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master",
|
||||
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
|
||||
if (!installed || !new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
||||
server.execute(() -> IrisModdedCommands.fail(source, "Pack '" + pack + "' could not be downloaded; check the name and try /iris download " + pack + "."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(pack);
|
||||
if (dimension == null) {
|
||||
server.execute(() -> IrisModdedCommands.fail(source, "Pack '" + pack + "' has no dimensions/" + pack + ".json"));
|
||||
return;
|
||||
}
|
||||
server.execute(() -> injectAndTeleport(source, server, owner, dimensionId, pack, seed));
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio open failed for {}", pack, e);
|
||||
server.execute(() -> IrisModdedCommands.fail(source, "Studio open failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage())));
|
||||
}
|
||||
}
|
||||
|
||||
private static void injectAndTeleport(CommandSourceStack source, MinecraftServer server, UUID owner, String dimensionId, String pack, long seed) {
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(owner);
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
ModdedDimensionManager.Handle handle;
|
||||
try {
|
||||
handle = ModdedDimensionManager.create(server, dimensionId, pack, seed);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio injection failed for {} ({})", dimensionId, pack, e);
|
||||
IrisModdedCommands.fail(source, "Studio injection failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
return;
|
||||
}
|
||||
STUDIOS.put(owner, dimensionId);
|
||||
ServerLevel studio = handle.level();
|
||||
int surface = studio.getMaxY();
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(studio);
|
||||
if (engine != null) {
|
||||
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio surface probe failed for {}", dimensionId, e);
|
||||
}
|
||||
player.teleportTo(studio, 8.5D, surface, 8.5D, java.util.Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
IrisModdedCommands.ok(source, "Studio open: " + dimensionId + " now runs '" + pack + "' seed " + seed + ". Use /iris studio close when done.");
|
||||
}
|
||||
|
||||
private static int close(CommandSourceStack source) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, "This command can only be used by players.");
|
||||
return 0;
|
||||
}
|
||||
MinecraftServer server = source.getServer();
|
||||
UUID owner = player.getUUID();
|
||||
String dimensionId = STUDIOS.remove(owner);
|
||||
if (dimensionId == null) {
|
||||
IrisModdedCommands.fail(source, "You do not have an open studio. Use /iris studio open <pack> first.");
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
ModdedDimensionManager.remove(server, dimensionId, true);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio close failed for {}", dimensionId, e);
|
||||
IrisModdedCommands.fail(source, "Studio close failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Studio closed: " + dimensionId + " was evacuated, unloaded and its region data deleted.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int status(CommandSourceStack source) {
|
||||
MinecraftServer server = source.getServer();
|
||||
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
|
||||
List<ModdedDimensionManager.Handle> studios = new ArrayList<>();
|
||||
for (ModdedDimensionManager.Handle handle : handles) {
|
||||
if (handle.dimensionId().startsWith(STUDIO_NAMESPACE + ":" + STUDIO_PREFIX) && ModdedDimensionManager.level(server, handle.dimensionId()) != null) {
|
||||
studios.add(handle);
|
||||
}
|
||||
}
|
||||
if (studios.isEmpty()) {
|
||||
IrisModdedCommands.ok(source, "No studio dimensions are currently open.");
|
||||
return 1;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Active studio dimension(s): " + studios.size());
|
||||
for (ModdedDimensionManager.Handle handle : studios) {
|
||||
UUID owner = ownerOf(handle.dimensionId());
|
||||
String ownerName = owner == null ? "unclaimed" : ownerName(server, owner);
|
||||
IrisModdedCommands.ok(source, " " + handle.dimensionId() + ": pack '" + handle.packKey() + "' seed " + handle.seed() + " owner " + ownerName);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static UUID ownerOf(String dimensionId) {
|
||||
for (Map.Entry<UUID, String> entry : STUDIOS.entrySet()) {
|
||||
if (entry.getValue().equals(dimensionId)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String ownerName(MinecraftServer server, UUID owner) {
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(owner);
|
||||
return player == null ? owner.toString() : player.getScoreboardName();
|
||||
}
|
||||
|
||||
private static int tpStudio(CommandSourceStack source) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
IrisModdedCommands.fail(source, "This command can only be used by players.");
|
||||
return 0;
|
||||
}
|
||||
MinecraftServer server = source.getServer();
|
||||
String dimensionId = STUDIOS.get(player.getUUID());
|
||||
if (dimensionId == null) {
|
||||
IrisModdedCommands.fail(source, "You do not have an open studio. Use /iris studio open <pack> first.");
|
||||
return 0;
|
||||
}
|
||||
ServerLevel studio = ModdedDimensionManager.level(server, dimensionId);
|
||||
if (studio == null) {
|
||||
STUDIOS.remove(player.getUUID());
|
||||
IrisModdedCommands.fail(source, "Your studio dimension is no longer loaded. Use /iris studio open <pack> again.");
|
||||
return 0;
|
||||
}
|
||||
int surface = studio.getMaxY();
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(studio);
|
||||
if (engine != null) {
|
||||
surface = engine.getMinHeight() + engine.getHeight(8, 8, false) + 2;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris tpstudio surface probe failed", e);
|
||||
}
|
||||
player.teleportTo(studio, 8.5D, surface, 8.5D, java.util.Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
IrisModdedCommands.ok(source, "Teleported to your studio (" + dimensionId + ").");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int version(CommandSourceStack source, String pack) {
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.gui.GuiMarker;
|
||||
import art.arcane.iris.core.gui.GuiOverlay;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
private final MinecraftServer server;
|
||||
private final ServerLevel level;
|
||||
private final Engine engine;
|
||||
|
||||
public ModdedVisionOverlay(MinecraftServer server, ServerLevel level, Engine engine) {
|
||||
this.server = server;
|
||||
this.level = level;
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GuiMarker> players() {
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (ServerPlayer player : level.players()) {
|
||||
Vec3 position = player.position();
|
||||
markers.add(GuiMarker.player(player.getScoreboardName(), position.x(), position.z()));
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||
server.execute(() -> {
|
||||
List<GuiMarker> markers = new ArrayList<>();
|
||||
for (Entity entity : level.getAllEntities()) {
|
||||
if (entity instanceof ServerPlayer || !(entity instanceof LivingEntity living)) {
|
||||
continue;
|
||||
}
|
||||
Vec3 position = living.position();
|
||||
markers.add(GuiMarker.entity(living.getType().toShortString(), position.x(), position.y(), position.z(),
|
||||
living.getHealth(), living.getMaxHealth()));
|
||||
}
|
||||
sink.accept(markers);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleport(double worldX, double worldZ) {
|
||||
int blockX = (int) worldX;
|
||||
int blockZ = (int) worldZ;
|
||||
server.execute(() -> {
|
||||
List<ServerPlayer> players = level.players();
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ServerPlayer player = players.get(0);
|
||||
int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
||||
int safeY = Math.max(surfaceY, level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ) + 1);
|
||||
player.teleportTo(level, blockX + 0.5D, safeY, blockZ + 0.5D, java.util.Set.of(), player.getYRot(), player.getXRot(), false);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+142
-65
@@ -18,12 +18,16 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedModConfig;
|
||||
import art.arcane.iris.modded.ModdedPrimaryWorldRouter;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
@@ -34,15 +38,16 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class ModdedWorldCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||
private static final SuggestionProvider<CommandSourceStack> ENABLED_DIMENSIONS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestEnabledDimensions(context, builder);
|
||||
private static final String DEFAULT_NAMESPACE = "irisworldgen";
|
||||
private static final SuggestionProvider<CommandSourceStack> LOADED_DIMENSIONS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(loadedIrisDimensions(context.getSource().getServer()), builder);
|
||||
|
||||
private ModdedWorldCommands() {
|
||||
}
|
||||
@@ -63,15 +68,9 @@ public final class ModdedWorldCommands {
|
||||
root.then(enableTree("create"));
|
||||
root.then(replaceOverworldTree());
|
||||
|
||||
root.then(Commands.literal("disable")
|
||||
.then(Commands.argument("dimension", StringArgumentType.word()).suggests(ENABLED_DIMENSIONS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension")))));
|
||||
root.then(Commands.literal("remove")
|
||||
.then(Commands.argument("dimension", StringArgumentType.word()).suggests(ENABLED_DIMENSIONS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension")))));
|
||||
root.then(Commands.literal("rm")
|
||||
.then(Commands.argument("dimension", StringArgumentType.word()).suggests(ENABLED_DIMENSIONS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension")))));
|
||||
root.then(disableTree("disable"));
|
||||
root.then(disableTree("remove"));
|
||||
root.then(disableTree("rm"));
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -83,105 +82,183 @@ public final class ModdedWorldCommands {
|
||||
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
|
||||
StringArgumentType.getString(context, "dimension"),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "pack")))
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
1337L))
|
||||
.then(Commands.argument("packDimension", StringArgumentType.word())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
|
||||
StringArgumentType.getString(context, "dimension"),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "packDimension"))))));
|
||||
StringArgumentType.getString(context, "packDimension"),
|
||||
1337L)))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> disableTree(String name) {
|
||||
return Commands.literal(name)
|
||||
.then(Commands.argument("dimension", StringArgumentType.word()).suggests(LOADED_DIMENSIONS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension"))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> replaceOverworldTree() {
|
||||
return Commands.literal("replace-overworld")
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
|
||||
"minecraft:overworld",
|
||||
.executes((CommandContext<CommandSourceStack> context) -> replaceOverworld(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "pack")))
|
||||
.then(Commands.argument("packDimension", StringArgumentType.word())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
|
||||
"minecraft:overworld",
|
||||
.executes((CommandContext<CommandSourceStack> context) -> replaceOverworld(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "packDimension")))));
|
||||
}
|
||||
|
||||
private static int enable(CommandSourceStack source, String targetDimension, String packName, String packDimension) {
|
||||
public static int createWorld(CommandSourceStack source, String name, String pack, long seed) {
|
||||
return enable(source, name, pack, pack, seed);
|
||||
}
|
||||
|
||||
private static int enable(CommandSourceStack source, String targetDimension, String packName, String packDimension, long seed) {
|
||||
MinecraftServer server = source.getServer();
|
||||
String dimensionId;
|
||||
try {
|
||||
ModdedWorldDatapackWriter.WriteResult result = ModdedWorldDatapackWriter.enable(source.getServer(), targetDimension, packName, packDimension);
|
||||
IrisModdedCommands.ok(source, "Enabled Iris world " + result.targetDimension() + " using pack '" + result.packName() + "' dimension '" + result.packDimension() + "'.");
|
||||
IrisModdedCommands.ok(source, "Wrote " + result.dimensionFile().getPath());
|
||||
IrisModdedCommands.ok(source, "Wrote " + result.typeFile().getPath());
|
||||
IrisModdedCommands.ok(source, "Wrote " + result.packMetaFile().getPath());
|
||||
IrisModdedCommands.ok(source, "Restart the server, then use /execute in " + result.targetDimension() + " run tp <player> 0 100 0 or a portal/modded dimension tool.");
|
||||
if ("minecraft:overworld".equals(result.targetDimension())) {
|
||||
IrisModdedCommands.ok(source, "minecraft:overworld replacement was explicitly requested.");
|
||||
}
|
||||
return 1;
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris world datapack write failed for {}", targetDimension, e);
|
||||
IrisModdedCommands.fail(source, "Failed to write Iris world datapack: " + e.getMessage());
|
||||
return 0;
|
||||
dimensionId = normalizeDimensionId(targetDimension);
|
||||
} catch (IllegalArgumentException e) {
|
||||
IrisModdedCommands.fail(source, e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
if (!loadPackDimension(source, packName, packDimension)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
ModdedDimensionManager.createPersistent(server, dimensionId, packDimension, seed);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris world injection failed for {} (pack={} dim={})", dimensionId, packName, packDimension, e);
|
||||
IrisModdedCommands.fail(source, "Failed to inject Iris world '" + dimensionId + "': " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Created Iris world " + dimensionId + " from pack '" + packName + "' dimension '" + packDimension + "' (seed " + seed + ").");
|
||||
IrisModdedCommands.ok(source, "It is live now and re-injected on every startup. Teleport in with /iris world status or a portal; no restart required.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int replaceOverworld(CommandSourceStack source, String packName, String packDimension) {
|
||||
MinecraftServer server = source.getServer();
|
||||
String dimensionId = DEFAULT_NAMESPACE + ":primary";
|
||||
if (!loadPackDimension(source, packName, packDimension)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
ModdedDimensionManager.createPersistent(server, dimensionId, packDimension, 1337L);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris primary world injection failed for {} (pack={} dim={})", dimensionId, packName, packDimension, e);
|
||||
IrisModdedCommands.fail(source, "Failed to inject Iris primary world: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
ModdedModConfig.setPrimaryWorld(dimensionId);
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
IrisModdedCommands.ok(source, "Iris primary world set to " + dimensionId + " (pack '" + packName + "' dimension '" + packDimension + "').");
|
||||
IrisModdedCommands.ok(source, "The vanilla overworld generator cannot be hot-swapped, so this does NOT regenerate the existing overworld.");
|
||||
IrisModdedCommands.ok(source, "Instead, " + dimensionId + " is now the configured primary world: players in the vanilla overworld are routed there on join, and it re-injects on every startup.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static boolean loadPackDimension(CommandSourceStack source, String packName, String packDimension) {
|
||||
File packFolder = new File(ModdedPackCommands.packsRoot(), packName);
|
||||
if (!packFolder.isDirectory()) {
|
||||
IrisModdedCommands.fail(source, "Pack '" + packName + "' was not found under " + ModdedPackCommands.packsRoot().getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(packDimension);
|
||||
if (dimension == null) {
|
||||
IrisModdedCommands.fail(source, "Pack '" + packName + "' does not contain dimensions/" + packDimension + ".json");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int disable(CommandSourceStack source, String targetDimension) {
|
||||
MinecraftServer server = source.getServer();
|
||||
String dimensionId;
|
||||
try {
|
||||
File removed = ModdedWorldDatapackWriter.disable(source.getServer(), targetDimension);
|
||||
IrisModdedCommands.ok(source, "Removed " + removed.getPath());
|
||||
IrisModdedCommands.ok(source, "Restart the server for the dimension removal to apply.");
|
||||
return 1;
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris world datapack removal failed for {}", targetDimension, e);
|
||||
IrisModdedCommands.fail(source, "Failed to remove Iris world datapack entry: " + e.getMessage());
|
||||
return 0;
|
||||
dimensionId = normalizeDimensionId(targetDimension);
|
||||
} catch (IllegalArgumentException e) {
|
||||
IrisModdedCommands.fail(source, e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
boolean removed;
|
||||
try {
|
||||
removed = ModdedDimensionManager.removePersistent(server, dimensionId);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris world removal failed for {}", dimensionId, e);
|
||||
IrisModdedCommands.fail(source, "Failed to remove Iris world '" + dimensionId + "': " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
if (dimensionId.equals(ModdedModConfig.get().primaryWorld())) {
|
||||
ModdedModConfig.setPrimaryWorld("");
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
}
|
||||
if (!removed) {
|
||||
IrisModdedCommands.ok(source, "Iris world '" + dimensionId + "' was not loaded; cleared its persistent registry entry.");
|
||||
return 1;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Removed Iris world '" + dimensionId + "': evacuated, unloaded, region data deleted, and dropped from the startup registry.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int status(CommandSourceStack source) {
|
||||
list(source);
|
||||
int loaded = 0;
|
||||
MinecraftServer server = source.getServer();
|
||||
int loaded = 0;
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
|
||||
loaded++;
|
||||
IrisModdedCommands.ok(source, "Loaded Iris level: " + level.dimension().identifier() + " -> pack dimension '" + generator.dimensionKey() + "'");
|
||||
IrisModdedCommands.ok(source, "Loaded Iris level: " + level.dimension().identifier() + " -> pack dimension '" + generator.activePackKey() + "'");
|
||||
}
|
||||
}
|
||||
String primary = ModdedModConfig.get().primaryWorld();
|
||||
if (!primary.isBlank()) {
|
||||
IrisModdedCommands.ok(source, "Primary world: " + primary + (ModdedModConfig.get().routePlayersToPrimaryWorld() ? " (players routed there)" : " (routing disabled)"));
|
||||
}
|
||||
if (loaded == 0) {
|
||||
IrisModdedCommands.fail(source, "No Iris dimensions are currently loaded. Enabled dimensions require a server restart before Minecraft loads them.");
|
||||
IrisModdedCommands.fail(source, "No Iris dimensions are currently loaded. Create one with /iris world create <name> <pack>.");
|
||||
}
|
||||
return loaded > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
private static int list(CommandSourceStack source) {
|
||||
try {
|
||||
List<String> dimensions = ModdedWorldDatapackWriter.enabledDimensions(source.getServer());
|
||||
IrisModdedCommands.ok(source, "Enabled Iris world datapack dimensions: " + dimensions.size());
|
||||
for (String dimension : dimensions) {
|
||||
IrisModdedCommands.ok(source, " - " + dimension);
|
||||
}
|
||||
if (dimensions.isEmpty()) {
|
||||
IrisModdedCommands.ok(source, "Use /iris world enable <dimension> <pack> to create one without replacing the main world.");
|
||||
}
|
||||
return 1;
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris world datapack listing failed", e);
|
||||
IrisModdedCommands.fail(source, "Failed to list Iris world datapack dimensions: " + e.getMessage());
|
||||
return 0;
|
||||
List<String> dimensions = loadedIrisDimensions(source.getServer());
|
||||
IrisModdedCommands.ok(source, "Loaded Iris dimensions: " + dimensions.size());
|
||||
for (String dimension : dimensions) {
|
||||
IrisModdedCommands.ok(source, " - " + dimension);
|
||||
}
|
||||
if (dimensions.isEmpty()) {
|
||||
IrisModdedCommands.ok(source, "Use /iris world create <name> <pack> to inject one without restarting.");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestEnabledDimensions(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
try {
|
||||
return SharedSuggestionProvider.suggest(ModdedWorldDatapackWriter.enabledDimensions(context.getSource().getServer()), builder);
|
||||
} catch (IOException e) {
|
||||
return Suggestions.empty();
|
||||
private static List<String> loadedIrisDimensions(MinecraftServer server) {
|
||||
List<String> dimensions = new ArrayList<>();
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) {
|
||||
dimensions.add(level.dimension().identifier().toString());
|
||||
}
|
||||
}
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
private static String normalizeDimensionId(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing dimension id.");
|
||||
}
|
||||
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||
String namespace = DEFAULT_NAMESPACE;
|
||||
String path = normalized;
|
||||
int colon = normalized.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
namespace = normalized.substring(0, colon);
|
||||
path = normalized.substring(colon + 1);
|
||||
}
|
||||
if (!namespace.matches("[a-z0-9_.-]+") || !path.matches("[a-z0-9_./-]+") || path.startsWith("/") || path.endsWith("/") || path.contains("..")) {
|
||||
throw new IllegalArgumentException("Invalid dimension id '" + value + "'. Use name or namespace:path.");
|
||||
}
|
||||
return namespace + ":" + path;
|
||||
}
|
||||
}
|
||||
|
||||
-217
@@ -1,217 +0,0 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
final class ModdedWorldDatapackWriter {
|
||||
static final String WORLD_PACK_NAME = "iris";
|
||||
private static final String DEFAULT_NAMESPACE = "irisworldgen";
|
||||
private static final String TYPE_NAMESPACE = "irisworldgen";
|
||||
|
||||
private ModdedWorldDatapackWriter() {
|
||||
}
|
||||
|
||||
static WriteResult enable(MinecraftServer server, String targetDimension, String packName, String packDimension) throws IOException {
|
||||
ResourceTarget target = ResourceTarget.parse(targetDimension);
|
||||
String cleanPackName = cleanPackSegment(packName, "pack");
|
||||
String cleanPackDimension = cleanPackSegment(packDimension, "pack dimension");
|
||||
File packFolder = new File(ModdedPackCommands.packsRoot(), cleanPackName);
|
||||
if (!packFolder.isDirectory()) {
|
||||
throw new IllegalArgumentException("Pack '" + cleanPackName + "' was not found under " + ModdedPackCommands.packsRoot().getAbsolutePath());
|
||||
}
|
||||
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(cleanPackDimension);
|
||||
if (dimension == null) {
|
||||
throw new IllegalArgumentException("Pack '" + cleanPackName + "' does not contain dimensions/" + cleanPackDimension + ".json");
|
||||
}
|
||||
|
||||
String typeKey = IrisDimension.sanitizeDimensionTypeKeyValue(cleanPackDimension);
|
||||
File typeFile = dimensionTypeFile(server, typeKey);
|
||||
File dimensionFile = dimensionFile(server, target);
|
||||
File mcmeta = packMetaFile(server);
|
||||
|
||||
writeParented(typeFile, dimension.getDimensionType().toJson(DataVersion.getLatest().get()));
|
||||
writeParented(dimensionFile, dimensionJson(cleanPackDimension, typeKey));
|
||||
writePackMeta(mcmeta);
|
||||
|
||||
return new WriteResult(target.id(), cleanPackName, cleanPackDimension, dimensionFile, typeFile, mcmeta);
|
||||
}
|
||||
|
||||
static File disable(MinecraftServer server, String targetDimension) throws IOException {
|
||||
ResourceTarget target = ResourceTarget.parse(targetDimension);
|
||||
File dimensionFile = dimensionFile(server, target);
|
||||
if (!dimensionFile.isFile()) {
|
||||
throw new IllegalArgumentException("No Iris world datapack dimension exists for " + target.id());
|
||||
}
|
||||
Files.delete(dimensionFile.toPath());
|
||||
return dimensionFile;
|
||||
}
|
||||
|
||||
static List<String> enabledDimensions(MinecraftServer server) throws IOException {
|
||||
File dataRoot = new File(irisPackFolder(server), "data");
|
||||
if (!dataRoot.isDirectory()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> dimensions = new ArrayList<>();
|
||||
File[] namespaces = dataRoot.listFiles(File::isDirectory);
|
||||
if (namespaces == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
for (File namespace : namespaces) {
|
||||
File dimensionRoot = new File(namespace, "dimension");
|
||||
if (!dimensionRoot.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
collectDimensionFiles(namespace.getName(), dimensionRoot, dimensions);
|
||||
}
|
||||
Collections.sort(dimensions);
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
static File packMetaFile(MinecraftServer server) {
|
||||
return new File(irisPackFolder(server), "pack.mcmeta");
|
||||
}
|
||||
|
||||
private static void collectDimensionFiles(String namespace, File dimensionRoot, List<String> dimensions) throws IOException {
|
||||
Path root = dimensionRoot.toPath();
|
||||
try (Stream<Path> stream = Files.walk(root)) {
|
||||
stream.filter(Files::isRegularFile)
|
||||
.filter((Path path) -> path.getFileName().toString().endsWith(".json"))
|
||||
.forEach((Path path) -> {
|
||||
String relative = root.relativize(path).toString().replace(File.separatorChar, '/');
|
||||
String idPath = relative.substring(0, relative.length() - ".json".length());
|
||||
dimensions.add(namespace + ":" + idPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static File worldDatapacksFolder(MinecraftServer server) {
|
||||
return server.getWorldPath(LevelResource.DATAPACK_DIR).toFile();
|
||||
}
|
||||
|
||||
private static File irisPackFolder(MinecraftServer server) {
|
||||
return new File(worldDatapacksFolder(server), WORLD_PACK_NAME);
|
||||
}
|
||||
|
||||
private static File dimensionTypeFile(MinecraftServer server, String typeKey) {
|
||||
return new File(irisPackFolder(server), "data/" + TYPE_NAMESPACE + "/dimension_type/" + typeKey + ".json");
|
||||
}
|
||||
|
||||
private static File dimensionFile(MinecraftServer server, ResourceTarget target) {
|
||||
return new File(new File(irisPackFolder(server), "data/" + target.namespace() + "/dimension"), target.path() + ".json");
|
||||
}
|
||||
|
||||
private static void writePackMeta(File output) throws IOException {
|
||||
int packFormat = DataVersion.getLatest().getPackFormat();
|
||||
String json = "{\n"
|
||||
+ " \"pack\": {\n"
|
||||
+ " \"description\": \"Iris world and dimension type definitions.\",\n"
|
||||
+ " \"pack_format\": " + packFormat + ",\n"
|
||||
+ " \"min_format\": " + packFormat + ",\n"
|
||||
+ " \"max_format\": " + packFormat + "\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
writeParented(output, json);
|
||||
}
|
||||
|
||||
private static void writeParented(File output, String text) throws IOException {
|
||||
File parent = output.getParentFile();
|
||||
if (parent != null) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
Files.writeString(output.toPath(), text, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String dimensionJson(String packDimension, String typeKey) {
|
||||
return "{\n"
|
||||
+ " \"type\": \"" + TYPE_NAMESPACE + ":" + typeKey + "\",\n"
|
||||
+ " \"generator\": {\n"
|
||||
+ " \"type\": \"irisworldgen:iris\",\n"
|
||||
+ " \"dimension\": \"" + escape(packDimension) + "\",\n"
|
||||
+ " \"biome_source\": {\n"
|
||||
+ " \"type\": \"minecraft:fixed\",\n"
|
||||
+ " \"biome\": \"minecraft:plains\"\n"
|
||||
+ " }\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
}
|
||||
|
||||
private static String cleanPackSegment(String value, String label) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing " + label + ".");
|
||||
}
|
||||
String clean = value.trim();
|
||||
if (clean.contains("/") || clean.contains("..") || clean.contains("\\") || clean.contains(":")) {
|
||||
throw new IllegalArgumentException("Invalid " + label + " '" + value + "'.");
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
private static String escape(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
record WriteResult(String targetDimension, String packName, String packDimension, File dimensionFile, File typeFile, File packMetaFile) {
|
||||
}
|
||||
|
||||
private record ResourceTarget(String namespace, String path) {
|
||||
static ResourceTarget parse(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing dimension id.");
|
||||
}
|
||||
|
||||
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||
String namespace = DEFAULT_NAMESPACE;
|
||||
String path = normalized;
|
||||
int colon = normalized.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
namespace = normalized.substring(0, colon);
|
||||
path = normalized.substring(colon + 1);
|
||||
}
|
||||
|
||||
if (!namespace.matches("[a-z0-9_.-]+") || !path.matches("[a-z0-9_./-]+") || path.startsWith("/") || path.endsWith("/") || path.contains("..")) {
|
||||
throw new IllegalArgumentException("Invalid dimension id '" + value + "'. Use name or namespace:path.");
|
||||
}
|
||||
return new ResourceTarget(namespace, path);
|
||||
}
|
||||
|
||||
String id() {
|
||||
return namespace + ":" + path;
|
||||
}
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Marker;
|
||||
import org.apache.logging.log4j.core.Filter;
|
||||
import org.apache.logging.log4j.core.LogEvent;
|
||||
import org.apache.logging.log4j.core.Logger;
|
||||
import org.apache.logging.log4j.message.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class ModdedLogFilterService implements ModdedService, Filter {
|
||||
private static final List<String> FILTERS = List.of(
|
||||
"Ignoring heightmap data for chunk",
|
||||
"Could not save data net.minecraft.world.entity.raid.PersistentRaid",
|
||||
"UUID of added entity already exists");
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
((Logger) LogManager.getRootLogger()).addFilter(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStarted() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStopped() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState() {
|
||||
return State.STARTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter.Result getOnMatch() {
|
||||
return Result.NEUTRAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter.Result getOnMismatch() {
|
||||
return Result.NEUTRAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(LogEvent event) {
|
||||
return check(event.getMessage().getFormattedMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
|
||||
return check(String.valueOf(msg));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
|
||||
return check(msg.getFormattedMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object... params) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8, Object p9) {
|
||||
return check(message);
|
||||
}
|
||||
|
||||
private Result check(String message) {
|
||||
if (message == null) {
|
||||
return Result.NEUTRAL;
|
||||
}
|
||||
for (String filter : FILTERS) {
|
||||
if (message.contains(filter)) {
|
||||
return Result.DENY;
|
||||
}
|
||||
}
|
||||
return Result.NEUTRAL;
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedPreservationService implements ModdedService, PreservationRegistry {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final long DEREFERENCE_INTERVAL_MILLIS = 60000L;
|
||||
|
||||
private final List<Thread> threads = new CopyOnWriteArrayList<>();
|
||||
private final List<ExecutorService> services = new CopyOnWriteArrayList<>();
|
||||
private final List<WeakReference<MeteredCache>> caches = new CopyOnWriteArrayList<>();
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
private Thread dereferencer;
|
||||
|
||||
@Override
|
||||
public void register(Thread thread) {
|
||||
threads.add(thread);
|
||||
}
|
||||
|
||||
public void register(ExecutorService service) {
|
||||
services.add(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCache(MeteredCache cache) {
|
||||
caches.add(new WeakReference<>(cache));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dereference() {
|
||||
threads.removeIf((Thread thread) -> !thread.isAlive());
|
||||
services.removeIf(ExecutorService::isShutdown);
|
||||
caches.removeIf((WeakReference<MeteredCache> ref) -> {
|
||||
MeteredCache cache = ref.get();
|
||||
return cache == null || cache.isClosed();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
dereferencer = new Thread(this::loop, "iris-modded-preservation");
|
||||
dereferencer.setDaemon(true);
|
||||
dereferencer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (!running.compareAndSet(true, false)) {
|
||||
return;
|
||||
}
|
||||
if (dereferencer != null) {
|
||||
dereferencer.interrupt();
|
||||
dereferencer = null;
|
||||
}
|
||||
dereference();
|
||||
shutdownTrackedResources();
|
||||
}
|
||||
|
||||
private void loop() {
|
||||
while (running.get()) {
|
||||
try {
|
||||
Thread.sleep(DEREFERENCE_INTERVAL_MILLIS);
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
dereference();
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdownTrackedResources() {
|
||||
for (Thread thread : threads) {
|
||||
if (!thread.isAlive()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
thread.interrupt();
|
||||
LOGGER.info("Iris preservation interrupted thread {}", thread.getName());
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris preservation failed to interrupt thread {}", thread.getName(), error);
|
||||
}
|
||||
}
|
||||
for (ExecutorService service : services) {
|
||||
try {
|
||||
service.shutdownNow();
|
||||
LOGGER.info("Iris preservation shut down executor {}", service);
|
||||
} catch (Throwable error) {
|
||||
LOGGER.error("Iris preservation failed to shut down executor {}", service, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
public interface ModdedService {
|
||||
void onEnable();
|
||||
|
||||
void onDisable();
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
public interface ModdedTickableService extends ModdedService {
|
||||
void onServerTick(MinecraftServer server);
|
||||
}
|
||||
@@ -126,6 +126,8 @@ dependencies {
|
||||
neoForge {
|
||||
version = neoForgeVersion
|
||||
|
||||
accessTransformers.from('src/main/resources/META-INF/accesstransformer.cfg')
|
||||
|
||||
runs {
|
||||
server {
|
||||
server()
|
||||
@@ -176,6 +178,7 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
}
|
||||
archiveFileName.set(irisArtifactName('NeoForge', "${minecraftVersion}+${neoForgeVersion}"))
|
||||
configurations = [project.configurations.named('bundle').get()]
|
||||
from(sourceSets.main.output)
|
||||
mergeServiceFiles()
|
||||
exclude('META-INF/maven/**')
|
||||
exclude('META-INF/proguard/**')
|
||||
@@ -184,7 +187,16 @@ tasks.named('shadowJar', ShadowJar).configure {
|
||||
exclude('META-INF/*.RSA')
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
|
||||
relocate('com.sun.jna', 'art.arcane.iris.shadow.jna')
|
||||
relocate('oshi', 'art.arcane.iris.shadow.oshi')
|
||||
relocate('net.jpountz', 'art.arcane.iris.shadow.jpountz')
|
||||
exclude('oshi.properties')
|
||||
exclude('org/jspecify/**')
|
||||
exclude('com/google/errorprone/**')
|
||||
exclude('com/google/j2objc/**')
|
||||
exclude('org/checkerframework/**')
|
||||
exclude('org/jetbrains/annotations/**')
|
||||
exclude('org/intellij/lang/**')
|
||||
}
|
||||
|
||||
tasks.named('assemble').configure {
|
||||
|
||||
+14
-1
@@ -20,6 +20,7 @@ package art.arcane.iris.neoforge;
|
||||
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedForcedDatapack;
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.modded.ModdedParityProbe;
|
||||
import art.arcane.iris.modded.ModdedWorldCheck;
|
||||
@@ -29,6 +30,7 @@ import art.arcane.iris.modded.command.ModdedObjectUndo;
|
||||
import art.arcane.iris.modded.command.ModdedWandService;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
@@ -38,6 +40,7 @@ import net.neoforged.fml.common.Mod;
|
||||
import net.neoforged.fml.loading.FMLLoader;
|
||||
import net.neoforged.fml.loading.VersionInfo;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import net.neoforged.neoforge.event.AddPackFindersEvent;
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
||||
@@ -62,10 +65,17 @@ public final class IrisNeoForgeBootstrap {
|
||||
chunkGenerators.register(modBus);
|
||||
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
|
||||
|
||||
modBus.addListener((AddPackFindersEvent event) -> {
|
||||
if (event.getPackType() == PackType.SERVER_DATA) {
|
||||
event.addRepositorySource(ModdedForcedDatapack.repositorySource());
|
||||
}
|
||||
});
|
||||
|
||||
NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> {
|
||||
ModdedObjectUndo.clearAll();
|
||||
ModdedWandService.clearAll();
|
||||
ModdedWorldEngines.shutdown();
|
||||
ModdedEngineBootstrap.stop();
|
||||
});
|
||||
NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
|
||||
NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.LeftClickBlock event) -> {
|
||||
@@ -79,7 +89,10 @@ public final class IrisNeoForgeBootstrap {
|
||||
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||
}
|
||||
});
|
||||
NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> ModdedWandService.serverTick(event.getServer()));
|
||||
NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> {
|
||||
ModdedEngineBootstrap.tick(event.getServer());
|
||||
ModdedWandService.serverTick(event.getServer());
|
||||
});
|
||||
|
||||
String parity = System.getProperty("iris.parity");
|
||||
if (parity != null) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
public net.minecraft.server.MinecraftServer levels
|
||||
public net.minecraft.server.MinecraftServer executor
|
||||
public net.minecraft.server.MinecraftServer storageSource
|
||||
@@ -150,11 +150,6 @@ public class IrisSettings {
|
||||
public int maxResidentTectonicPlates = 96;
|
||||
public int mantleBackpressureWaitMs = 25;
|
||||
public int mantleBackpressureTimeoutMs = 60_000;
|
||||
public boolean enableSpigotDirectPregen = false;
|
||||
|
||||
public boolean isEnableSpigotDirectPregen() {
|
||||
return enableSpigotDirectPregen;
|
||||
}
|
||||
|
||||
public int getChunkLoadTimeoutSeconds() {
|
||||
return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120));
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
|
||||
import java.awt.GraphicsEnvironment;
|
||||
|
||||
public final class GuiHost {
|
||||
private static volatile Provider provider = new Provider() {
|
||||
};
|
||||
|
||||
private GuiHost() {
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
default Engine findActiveEngine() {
|
||||
return null;
|
||||
}
|
||||
|
||||
default void registerHotloadHook(Runnable onHotload) {
|
||||
}
|
||||
|
||||
default void unregisterHotloadHook(Runnable onHotload) {
|
||||
}
|
||||
|
||||
default GuiOverlay overlayFor(Engine engine) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void set(Provider boundProvider) {
|
||||
if (boundProvider != null) {
|
||||
provider = boundProvider;
|
||||
}
|
||||
}
|
||||
|
||||
public static Provider get() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static boolean isAvailable() {
|
||||
return !GraphicsEnvironment.isHeadless();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
public record GuiMarker(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
|
||||
public static GuiMarker player(String name, double worldX, double worldZ) {
|
||||
return new GuiMarker(name, worldX, 0, worldZ, 0, 0);
|
||||
}
|
||||
|
||||
public static GuiMarker entity(String label, double worldX, double worldY, double worldZ, double health, double maxHealth) {
|
||||
return new GuiMarker(label, worldX, worldY, worldZ, health, maxHealth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public interface GuiOverlay {
|
||||
List<GuiMarker> players();
|
||||
|
||||
void requestEntities(Consumer<List<GuiMarker>> sink);
|
||||
|
||||
void teleport(double worldX, double worldZ);
|
||||
|
||||
String openInEditor(double worldX, double worldZ, RenderType type);
|
||||
}
|
||||
+38
-35
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -18,15 +18,11 @@
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
@@ -36,23 +32,44 @@ import art.arcane.iris.util.common.parallel.BurstExecutor;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.DefaultListModel;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
import java.awt.event.MouseWheelEvent;
|
||||
import java.awt.event.MouseWheelListener;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, Listener {
|
||||
public class NoiseExplorerGUI extends JPanel implements MouseWheelListener {
|
||||
|
||||
private static final long serialVersionUID = 2094606939770332040L;
|
||||
private static final Color BG = new Color(24, 24, 28);
|
||||
@@ -88,6 +105,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
|
||||
private final RollingSequence fpsHistory = new RollingSequence(60);
|
||||
private final boolean colorMode = IrisSettings.get().getGui().colorMode;
|
||||
private final MultiBurst gx = MultiBurst.burst;
|
||||
private final Runnable hotloadHook = this::refreshGenerator;
|
||||
private double scale = 1;
|
||||
private double animScale = 10;
|
||||
private double ox = 0;
|
||||
@@ -107,7 +125,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
|
||||
private String currentName = "STATIC";
|
||||
|
||||
public NoiseExplorerGUI() {
|
||||
Iris.instance.registerListener(this);
|
||||
GuiHost.get().registerHotloadHook(hotloadHook);
|
||||
setBackground(BG);
|
||||
addMouseWheelListener(this);
|
||||
addMouseMotionListener(new MouseMotionListener() {
|
||||
@@ -130,7 +148,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
|
||||
}
|
||||
|
||||
public static void launch() {
|
||||
Engine engine = findActiveEngine();
|
||||
Engine engine = GuiHost.get().findActiveEngine();
|
||||
EventQueue.invokeLater(() -> {
|
||||
NoiseExplorerGUI nv = new NoiseExplorerGUI();
|
||||
buildFrame("Noise Explorer", nv, engine, null, null);
|
||||
@@ -138,7 +156,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
|
||||
}
|
||||
|
||||
public static void launch(Supplier<Function2<Double, Double, Double>> gen, String genName) {
|
||||
Engine engine = findActiveEngine();
|
||||
Engine engine = GuiHost.get().findActiveEngine();
|
||||
EventQueue.invokeLater(() -> {
|
||||
NoiseExplorerGUI nv = new NoiseExplorerGUI();
|
||||
nv.loader = gen;
|
||||
@@ -148,20 +166,6 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
|
||||
});
|
||||
}
|
||||
|
||||
private static Engine findActiveEngine() {
|
||||
try {
|
||||
for (World w : new ArrayList<>(Bukkit.getWorlds())) {
|
||||
try {
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(w);
|
||||
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
|
||||
return access.getEngine();
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JFrame buildFrame(String title, NoiseExplorerGUI nv, Engine engine,
|
||||
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
|
||||
JFrame frame = new JFrame(title);
|
||||
@@ -180,7 +184,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
Iris.instance.unregisterListener(nv);
|
||||
GuiHost.get().unregisterHotloadHook(nv.hotloadHook);
|
||||
}
|
||||
});
|
||||
return frame;
|
||||
@@ -364,8 +368,7 @@ public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, List
|
||||
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void on(IrisEngineHotloadEvent e) {
|
||||
private void refreshGenerator() {
|
||||
if (generator != null && loader != null) {
|
||||
generator = loader.get();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
|
||||
public interface PregenRenderSource {
|
||||
Position2 min();
|
||||
|
||||
Position2 max();
|
||||
|
||||
String[] progress();
|
||||
|
||||
boolean paused();
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.function.Consumer2;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public final class PregenRenderer extends JPanel implements KeyListener {
|
||||
private static final long serialVersionUID = 2094606939770332040L;
|
||||
|
||||
private final KList<Runnable> order = new KList<>();
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final int res = 512;
|
||||
private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB);
|
||||
private final PregenRenderSource source;
|
||||
private final Runnable onPause;
|
||||
private Graphics2D bg;
|
||||
private JFrame frame;
|
||||
|
||||
private PregenRenderer(PregenRenderSource source, Runnable onPause) {
|
||||
this.source = source;
|
||||
this.onPause = onPause;
|
||||
}
|
||||
|
||||
public static PregenRenderer open(String title, PregenRenderSource source, Runnable onPause) {
|
||||
PregenRenderer renderer = new PregenRenderer(source, onPause);
|
||||
JFrame frame = new JFrame(title);
|
||||
renderer.frame = frame;
|
||||
frame.addKeyListener(renderer);
|
||||
frame.add(renderer);
|
||||
frame.setSize(1000, 1000);
|
||||
frame.setVisible(true);
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public Consumer2<Position2, Color> drawFunction() {
|
||||
return (Position2 c, Color color) -> {
|
||||
lock.lock();
|
||||
try {
|
||||
order.add(() -> draw(c, color, bg));
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void submit(int x, int z, Color color) {
|
||||
drawFunction().accept(new Position2(x, z), color);
|
||||
}
|
||||
|
||||
public boolean isVisibleFrame() {
|
||||
return frame != null && frame.isVisible();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (frame != null) {
|
||||
frame.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Graphics gx) {
|
||||
Graphics2D g = (Graphics2D) gx;
|
||||
bg = (Graphics2D) image.getGraphics();
|
||||
lock.lock();
|
||||
try {
|
||||
while (order.isNotEmpty()) {
|
||||
try {
|
||||
order.pop().run();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true);
|
||||
g.setColor(Color.WHITE);
|
||||
g.setFont(new Font("Hevetica", Font.BOLD, 13));
|
||||
String[] prog = source.progress();
|
||||
int h = g.getFontMetrics().getHeight() + 5;
|
||||
int hh = 20;
|
||||
|
||||
if (source.paused()) {
|
||||
g.drawString("PAUSED", 20, hh += h);
|
||||
g.drawString("Press P to Resume", 20, hh += h);
|
||||
} else {
|
||||
for (String i : prog) {
|
||||
g.drawString(i, 20, hh += h);
|
||||
}
|
||||
g.drawString("Press P to Pause", 20, hh += h);
|
||||
}
|
||||
|
||||
J.sleep(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250);
|
||||
repaint();
|
||||
}
|
||||
|
||||
private void draw(Position2 p, Color c, Graphics2D bg) {
|
||||
double pw = M.lerpInverse(source.min().getX(), source.max().getX(), p.getX());
|
||||
double ph = M.lerpInverse(source.min().getZ(), source.max().getZ(), p.getZ());
|
||||
double pwa = M.lerpInverse(source.min().getX(), source.max().getX(), p.getX() + 1);
|
||||
double pha = M.lerpInverse(source.min().getZ(), source.max().getZ(), p.getZ() + 1);
|
||||
int x = (int) M.lerp(0, res, pw);
|
||||
int z = (int) M.lerp(0, res, ph);
|
||||
int xa = (int) M.lerp(0, res, pwa);
|
||||
int za = (int) M.lerp(0, res, pha);
|
||||
bg.setColor(c);
|
||||
bg.fillRect(x, z, xa - x, za - z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyTyped(KeyEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_P && onPause != null) {
|
||||
onPause.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,25 +30,19 @@ import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.format.MemoryMonitor;
|
||||
import art.arcane.volmlib.util.function.Consumer2;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import org.bukkit.World;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.Color;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class PregeneratorJob implements PregenListener {
|
||||
public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
||||
private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
|
||||
private static final Color COLOR_BLACK = parseColor("#4d7d5b");
|
||||
private static final Color COLOR_MANTLE = parseColor("#3c2773");
|
||||
@@ -69,8 +63,8 @@ public class PregeneratorJob implements PregenListener {
|
||||
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
|
||||
private final Engine engine;
|
||||
private final ExecutorService service;
|
||||
private JFrame frame;
|
||||
private PregenRenderer renderer;
|
||||
private Consumer2<Position2, Color> drawFunction;
|
||||
private int rgc = 0;
|
||||
private String[] info;
|
||||
private volatile double lastChunksPerSecond = 0D;
|
||||
@@ -212,8 +206,8 @@ public class PregeneratorJob implements PregenListener {
|
||||
|
||||
public void draw(int x, int z, Color color) {
|
||||
try {
|
||||
if (renderer != null && frame != null && frame.isVisible()) {
|
||||
renderer.func.accept(new Position2(x, z), color);
|
||||
if (renderer != null && drawFunction != null && renderer.isVisibleFrame()) {
|
||||
drawFunction.accept(new Position2(x, z), color);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
IrisLogging.error("Failed to draw pregen");
|
||||
@@ -232,11 +226,11 @@ public class PregeneratorJob implements PregenListener {
|
||||
J.a(() -> {
|
||||
try {
|
||||
monitor.close();
|
||||
if (frame == null) {
|
||||
if (renderer == null) {
|
||||
return;
|
||||
}
|
||||
J.sleep(3000);
|
||||
frame.setVisible(false);
|
||||
renderer.close();
|
||||
} catch (Throwable ignored) {
|
||||
IrisLogging.error("Error closing pregen gui");
|
||||
}
|
||||
@@ -246,20 +240,8 @@ public class PregeneratorJob implements PregenListener {
|
||||
public void open() {
|
||||
J.a(() -> {
|
||||
try {
|
||||
frame = new JFrame("Pregen View");
|
||||
renderer = new PregenRenderer();
|
||||
frame.addKeyListener(renderer);
|
||||
renderer.l = new ReentrantLock();
|
||||
renderer.frame = frame;
|
||||
renderer.job = this;
|
||||
renderer.func = (c, b) -> {
|
||||
renderer.l.lock();
|
||||
renderer.order.add(() -> renderer.draw(c, b, renderer.bg));
|
||||
renderer.l.unlock();
|
||||
};
|
||||
frame.add(renderer);
|
||||
frame.setSize(1000, 1000);
|
||||
frame.setVisible(true);
|
||||
renderer = PregenRenderer.open("Pregen View", this, PregeneratorJob::pauseResume);
|
||||
drawFunction = renderer.drawFunction();
|
||||
} catch (Throwable ignored) {
|
||||
IrisLogging.error("Error opening pregen gui");
|
||||
}
|
||||
@@ -292,7 +274,7 @@ public class PregeneratorJob implements PregenListener {
|
||||
|
||||
@Override
|
||||
public void onChunkGenerated(int x, int z, boolean cached) {
|
||||
if (renderer == null || frame == null || !frame.isVisible()) return;
|
||||
if (renderer == null || !renderer.isVisibleFrame()) return;
|
||||
service.submit(() -> {
|
||||
if (engine != null) {
|
||||
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
||||
@@ -378,112 +360,23 @@ public class PregeneratorJob implements PregenListener {
|
||||
draw(x, z, COLOR_EXISTS);
|
||||
}
|
||||
|
||||
private Position2 getMax() {
|
||||
@Override
|
||||
public Position2 max() {
|
||||
return max;
|
||||
}
|
||||
|
||||
private Position2 getMin() {
|
||||
@Override
|
||||
public Position2 min() {
|
||||
return min;
|
||||
}
|
||||
|
||||
private boolean paused() {
|
||||
@Override
|
||||
public boolean paused() {
|
||||
return pregenerator.paused();
|
||||
}
|
||||
|
||||
private String[] getProgress() {
|
||||
@Override
|
||||
public String[] progress() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public static class PregenRenderer extends JPanel implements KeyListener {
|
||||
private static final long serialVersionUID = 2094606939770332040L;
|
||||
private final KList<Runnable> order = new KList<>();
|
||||
private final int res = 512;
|
||||
private final BufferedImage image = new BufferedImage(res, res, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D bg;
|
||||
private PregeneratorJob job;
|
||||
private ReentrantLock l;
|
||||
private Consumer2<Position2, Color> func;
|
||||
private JFrame frame;
|
||||
|
||||
public PregenRenderer() {
|
||||
|
||||
}
|
||||
|
||||
public void paint(int x, int z, Color c) {
|
||||
func.accept(new Position2(x, z), c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(Graphics gx) {
|
||||
Graphics2D g = (Graphics2D) gx;
|
||||
bg = (Graphics2D) image.getGraphics();
|
||||
l.lock();
|
||||
|
||||
while (order.isNotEmpty()) {
|
||||
try {
|
||||
order.pop().run();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
l.unlock();
|
||||
g.drawImage(image, 0, 0, getParent().getWidth(), getParent().getHeight(), (img, infoflags, x, y, width, height) -> true);
|
||||
g.setColor(Color.WHITE);
|
||||
g.setFont(new Font("Hevetica", Font.BOLD, 13));
|
||||
String[] prog = job.getProgress();
|
||||
int h = g.getFontMetrics().getHeight() + 5;
|
||||
int hh = 20;
|
||||
|
||||
if (job.paused()) {
|
||||
g.drawString("PAUSED", 20, hh += h);
|
||||
|
||||
g.drawString("Press P to Resume", 20, hh += h);
|
||||
} else {
|
||||
for (String i : prog) {
|
||||
g.drawString(i, 20, hh += h);
|
||||
}
|
||||
|
||||
g.drawString("Press P to Pause", 20, hh += h);
|
||||
}
|
||||
|
||||
J.sleep(IrisSettings.get().getGui().isMaximumPregenGuiFPS() ? 4 : 250);
|
||||
repaint();
|
||||
}
|
||||
|
||||
private void draw(Position2 p, Color c, Graphics2D bg) {
|
||||
double pw = M.lerpInverse(job.getMin().getX(), job.getMax().getX(), p.getX());
|
||||
double ph = M.lerpInverse(job.getMin().getZ(), job.getMax().getZ(), p.getZ());
|
||||
double pwa = M.lerpInverse(job.getMin().getX(), job.getMax().getX(), p.getX() + 1);
|
||||
double pha = M.lerpInverse(job.getMin().getZ(), job.getMax().getZ(), p.getZ() + 1);
|
||||
int x = (int) M.lerp(0, res, pw);
|
||||
int z = (int) M.lerp(0, res, ph);
|
||||
int xa = (int) M.lerp(0, res, pwa);
|
||||
int za = (int) M.lerp(0, res, pha);
|
||||
bg.setColor(c);
|
||||
bg.fillRect(x, z, xa - x, za - z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyTyped(KeyEvent e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_P) {
|
||||
PregeneratorJob.pauseResume();
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
frame.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+92
-90
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -18,15 +18,11 @@
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.engine.framework.render.IrisRenderer;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
@@ -38,25 +34,43 @@ import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.O;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.ButtonGroup;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSeparator;
|
||||
import javax.swing.JToggleButton;
|
||||
import javax.swing.SwingConstants;
|
||||
import javax.swing.event.MouseInputListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Point;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
import java.awt.event.MouseWheelEvent;
|
||||
import java.awt.event.MouseWheelListener;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
|
||||
|
||||
public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener {
|
||||
private static final long serialVersionUID = 2094606939770332040L;
|
||||
|
||||
@@ -65,9 +79,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
private static final Color CARD_BORDER = new Color(60, 60, 75, 180);
|
||||
private static final Color TEXT_PRIMARY = new Color(220, 220, 230);
|
||||
private static final Color TEXT_SECONDARY = new Color(140, 140, 155);
|
||||
private static final Color TEXT_DIM = new Color(90, 90, 105);
|
||||
private static final Color ACCENT = new Color(90, 140, 255);
|
||||
private static final Color ACCENT_DIM = new Color(60, 100, 200, 100);
|
||||
private static final Color PLAYER_COLOR = new Color(80, 200, 120);
|
||||
private static final Color MOB_COLOR = new Color(220, 80, 80);
|
||||
private static final Color STATUS_BG = new Color(24, 24, 30, 240);
|
||||
@@ -81,7 +93,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
private static final int CARD_PAD = 12;
|
||||
private static final int STATUS_HEIGHT = 26;
|
||||
|
||||
private final KList<LivingEntity> lastEntities = new KList<>();
|
||||
private final KList<GuiMarker> lastEntities = new KList<>();
|
||||
private final KMap<String, Long> notifications = new KMap<>();
|
||||
private final ChronoLatch centities = new ChronoLatch(1000);
|
||||
private final RollingSequence rs = new RollingSequence(512);
|
||||
@@ -95,7 +107,6 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
private boolean help = true;
|
||||
private boolean helpIgnored = false;
|
||||
private boolean shift = false;
|
||||
private Player player = null;
|
||||
private boolean debug = false;
|
||||
private boolean control = false;
|
||||
private boolean eco = false;
|
||||
@@ -103,8 +114,9 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
private boolean follow = false;
|
||||
private boolean alt = false;
|
||||
private boolean grid = false;
|
||||
private GuiMarker followMarker = null;
|
||||
private IrisRenderer renderer;
|
||||
private IrisWorld world;
|
||||
private GuiOverlay overlay;
|
||||
private double velocity = 0;
|
||||
private int lowq = 12;
|
||||
private double scale = 128;
|
||||
@@ -128,10 +140,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
Thread t = new Thread(r);
|
||||
t.setName("Iris HD Renderer " + tid);
|
||||
t.setPriority(Thread.MIN_PRIORITY);
|
||||
t.setUncaughtExceptionHandler((et, ex) -> {
|
||||
Iris.info("Exception encountered in " + et.getName());
|
||||
ex.printStackTrace();
|
||||
});
|
||||
t.setUncaughtExceptionHandler((et, ex) -> ex.printStackTrace());
|
||||
return t;
|
||||
});
|
||||
|
||||
@@ -140,10 +149,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
Thread t = new Thread(r);
|
||||
t.setName("Iris Renderer " + tid);
|
||||
t.setPriority(Thread.NORM_PRIORITY);
|
||||
t.setUncaughtExceptionHandler((et, ex) -> {
|
||||
Iris.info("Exception encountered in " + et.getName());
|
||||
ex.printStackTrace();
|
||||
});
|
||||
t.setUncaughtExceptionHandler((et, ex) -> ex.printStackTrace());
|
||||
return t;
|
||||
});
|
||||
|
||||
@@ -170,11 +176,15 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
});
|
||||
}
|
||||
|
||||
private static void createAndShowGUI(Engine r, int s, IrisWorld world) {
|
||||
public static void launch(Engine g) {
|
||||
J.a(() -> createAndShowGUI(g));
|
||||
}
|
||||
|
||||
private static void createAndShowGUI(Engine r) {
|
||||
JFrame frame = new JFrame("Iris Vision");
|
||||
VisionGUI nv = new VisionGUI(frame);
|
||||
nv.world = world;
|
||||
nv.engine = r;
|
||||
nv.overlay = GuiHost.get().overlayFor(r);
|
||||
nv.renderer = new IrisRenderer(r);
|
||||
frame.getContentPane().setBackground(BG);
|
||||
frame.setLayout(new BorderLayout());
|
||||
@@ -261,18 +271,17 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
return sep;
|
||||
}
|
||||
|
||||
public static void launch(Engine g, int i) {
|
||||
J.a(() -> createAndShowGUI(g, i, g.getWorld()));
|
||||
}
|
||||
|
||||
public boolean updateEngine() {
|
||||
if (engine.isClosed()) {
|
||||
if (world.hasRealWorld()) {
|
||||
try {
|
||||
engine = IrisToolbelt.access(world.realWorld()).getEngine();
|
||||
return !engine.isClosed();
|
||||
} catch (Throwable ignored) {
|
||||
try {
|
||||
Engine reacquired = GuiHost.get().findActiveEngine();
|
||||
if (reacquired != null && !reacquired.isClosed()) {
|
||||
engine = reacquired;
|
||||
overlay = GuiHost.get().overlayFor(reacquired);
|
||||
renderer = new IrisRenderer(reacquired);
|
||||
return true;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -378,8 +387,8 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
|
||||
void toggleFollow() {
|
||||
follow = !follow;
|
||||
if (player != null && follow) {
|
||||
notify("Following " + player.getName());
|
||||
if (followMarker != null && follow) {
|
||||
notify("Following " + followMarker.label());
|
||||
} else if (follow) {
|
||||
notify("No player in world");
|
||||
follow = false;
|
||||
@@ -478,28 +487,26 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
|
||||
@Override
|
||||
public void paint(Graphics gx) {
|
||||
if (engine.isClosed()) {
|
||||
if (engine.isClosed() && !updateEngine()) {
|
||||
EventQueue.invokeLater(() -> {
|
||||
try { setVisible(false); } catch (Throwable ignored) { }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateEngine()) {
|
||||
dump();
|
||||
}
|
||||
|
||||
velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36;
|
||||
oxp = lerp(oxp, ox, 0.36);
|
||||
ozp = lerp(ozp, oz, 0.36);
|
||||
hx = lerp(hx, lx, 0.36);
|
||||
hz = lerp(hz, lz, 0.36);
|
||||
|
||||
if (centities.flip()) {
|
||||
J.s(() -> {
|
||||
if (centities.flip() && overlay != null) {
|
||||
overlay.requestEntities((List<GuiMarker> next) -> {
|
||||
synchronized (lastEntities) {
|
||||
lastEntities.clear();
|
||||
lastEntities.addAll(world.getEntitiesByClass(LivingEntity.class));
|
||||
if (next != null) {
|
||||
lastEntities.addAll(next);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -588,8 +595,8 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
}
|
||||
|
||||
private void handleFollow() {
|
||||
if (follow && player != null) {
|
||||
animateTo(player.getLocation().getX(), player.getLocation().getZ());
|
||||
if (follow && followMarker != null) {
|
||||
animateTo(followMarker.worldX(), followMarker.worldZ());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,28 +642,30 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
g.drawString(right, w - rw - 8, y + 17);
|
||||
|
||||
g.setColor(ACCENT);
|
||||
int modeW = g.getFontMetrics().stringWidth(" " + modeName(currentType));
|
||||
g.fillRect(0, y + 1, 3, STATUS_HEIGHT - 1);
|
||||
}
|
||||
|
||||
private void renderEntities(Graphics2D g) {
|
||||
Player b = null;
|
||||
GuiMarker firstPlayer = null;
|
||||
|
||||
for (Player i : world.getPlayers()) {
|
||||
b = i;
|
||||
renderPlayerMarker(g, i.getLocation().getX(), i.getLocation().getZ(), i.getName());
|
||||
List<GuiMarker> players = overlay == null ? List.of() : overlay.players();
|
||||
if (players != null) {
|
||||
for (GuiMarker i : players) {
|
||||
firstPlayer = i;
|
||||
renderPlayerMarker(g, i.worldX(), i.worldZ(), i.label());
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (lastEntities) {
|
||||
double dist = Double.MAX_VALUE;
|
||||
LivingEntity nearest = null;
|
||||
GuiMarker nearest = null;
|
||||
|
||||
for (LivingEntity i : lastEntities) {
|
||||
if (i instanceof Player) continue;
|
||||
renderMobMarker(g, i.getLocation().getX(), i.getLocation().getZ());
|
||||
for (GuiMarker i : lastEntities) {
|
||||
renderMobMarker(g, i.worldX(), i.worldZ());
|
||||
if (shift) {
|
||||
double d = i.getLocation().distanceSquared(
|
||||
new Location(i.getWorld(), getWorldX(hx), i.getLocation().getY(), getWorldZ(hz)));
|
||||
double dx = i.worldX() - getWorldX(hx);
|
||||
double dz = i.worldZ() - getWorldZ(hz);
|
||||
double d = dx * dx + dz * dz;
|
||||
if (d < dist) {
|
||||
dist = d;
|
||||
nearest = i;
|
||||
@@ -665,22 +674,24 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
}
|
||||
|
||||
if (nearest != null && shift) {
|
||||
double sx = getScreenX(nearest.getLocation().getX());
|
||||
double sz = getScreenZ(nearest.getLocation().getZ());
|
||||
double sx = getScreenX(nearest.worldX());
|
||||
double sz = getScreenZ(nearest.worldZ());
|
||||
g.setColor(MOB_COLOR);
|
||||
g.fillOval((int) sx - 6, (int) sz - 6, 12, 12);
|
||||
g.setColor(new Color(220, 80, 80, 60));
|
||||
g.fillOval((int) sx - 10, (int) sz - 10, 20, 20);
|
||||
|
||||
KList<String> k = new KList<>();
|
||||
k.add(Form.capitalizeWords(nearest.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")));
|
||||
k.add("Pos: " + nearest.getLocation().getBlockX() + ", " + nearest.getLocation().getBlockY() + ", " + nearest.getLocation().getBlockZ());
|
||||
k.add("HP: " + Form.f(nearest.getHealth(), 1) + " / " + Form.f(nearest.getAttribute(MAX_HEALTH).getValue(), 1));
|
||||
k.add(nearest.label());
|
||||
k.add("Pos: " + (int) nearest.worldX() + ", " + (int) nearest.worldY() + ", " + (int) nearest.worldZ());
|
||||
if (nearest.maxHealth() > 0) {
|
||||
k.add("HP: " + Form.f(nearest.health(), 1) + " / " + Form.f(nearest.maxHealth(), 1));
|
||||
}
|
||||
drawCard(w - CARD_PAD, CARD_PAD, 1, 0, g, k);
|
||||
}
|
||||
}
|
||||
|
||||
player = b;
|
||||
followMarker = firstPlayer;
|
||||
}
|
||||
|
||||
private void renderPlayerMarker(Graphics2D g, double x, double z, String name) {
|
||||
@@ -884,32 +895,23 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener
|
||||
@Override public void mouseExited(MouseEvent e) { }
|
||||
|
||||
private void open() {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File r = null;
|
||||
switch (currentType) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
|
||||
r = complex.getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
case BIOME_LAND -> r = complex.getLandBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
case BIOME_SEA -> r = complex.getSeaBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
case REGION -> r = complex.getRegionStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
case CAVE_LAND -> r = complex.getCaveBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode();
|
||||
if (overlay == null) {
|
||||
return;
|
||||
}
|
||||
if (r != null) {
|
||||
notify("Opened " + r.getName());
|
||||
String opened = overlay.openInEditor(getWorldX(hx), getWorldZ(hz), currentType);
|
||||
if (opened != null) {
|
||||
notify("Opened " + opened);
|
||||
}
|
||||
}
|
||||
|
||||
private void teleport() {
|
||||
J.s(() -> {
|
||||
if (player != null) {
|
||||
int xx = (int) getWorldX(hx);
|
||||
int zz = (int) getWorldZ(hz);
|
||||
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
|
||||
player.teleport(new Location(player.getWorld(), xx, yy, zz));
|
||||
notify("Teleported to " + xx + ", " + yy + ", " + zz);
|
||||
} else {
|
||||
notify("No player in world");
|
||||
}
|
||||
});
|
||||
if (overlay == null) {
|
||||
notify("No player in world");
|
||||
return;
|
||||
}
|
||||
int xx = (int) getWorldX(hx);
|
||||
int zz = (int) getWorldZ(hz);
|
||||
overlay.teleport(xx, zz);
|
||||
notify("Teleporting to " + xx + ", " + zz);
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,7 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -182,10 +180,6 @@ public interface INMSBinding {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean writeChunkNbtDirect(NBTWorld nbtWorld, int chunkX, int chunkZ, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean forceEvictChunk(World world, int chunkX, int chunkZ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@ import art.arcane.iris.core.nms.datapack.v1192.DataFixerV1192;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustomSpawn;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustomSpawnType;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -23,7 +22,7 @@ public class DataFixerV1206 extends DataFixerV1192 {
|
||||
json.remove("creature_spawn_probability");
|
||||
}
|
||||
|
||||
var spawns = biome.getSpawns();
|
||||
KList<IrisBiomeCustomSpawn> spawns = biome.getSpawns();
|
||||
if (spawns != null && spawns.isNotEmpty()) {
|
||||
JSONObject spawners = new JSONObject();
|
||||
KMap<IrisBiomeCustomSpawnType, JSONArray> groups = new KMap<>();
|
||||
@@ -32,20 +31,15 @@ public class DataFixerV1206 extends DataFixerV1192 {
|
||||
if (i == null) {
|
||||
continue;
|
||||
}
|
||||
EntityType type = i.getType();
|
||||
if (type == null) {
|
||||
String key = i.getTypeKey();
|
||||
if (key == null) {
|
||||
IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + biome.getId());
|
||||
continue;
|
||||
}
|
||||
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
|
||||
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
|
||||
JSONObject o = new JSONObject();
|
||||
NamespacedKey key = type.getKey();
|
||||
if (key == null) {
|
||||
IrisLogging.warn("Skipping custom biome spawn with unresolved entity key in biome " + biome.getId());
|
||||
continue;
|
||||
}
|
||||
o.put("type", key.toString());
|
||||
o.put("type", key);
|
||||
o.put("weight", i.getWeight());
|
||||
o.put("minCount", i.getMinCount());
|
||||
o.put("maxCount", i.getMaxCount());
|
||||
|
||||
-28
@@ -18,13 +18,10 @@
|
||||
|
||||
package art.arcane.iris.core.pregenerator.methods;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.pregenerator.PregenListener;
|
||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
|
||||
public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
|
||||
@@ -33,36 +30,11 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
|
||||
public AsyncOrMedievalPregenMethod(World world, int threads) {
|
||||
if (PaperLib.isPaper()) {
|
||||
method = new AsyncPregenMethod(world, threads);
|
||||
} else if (shouldUseSpigotDirect(world)) {
|
||||
method = new SpigotDirectPregenMethod(world, threads);
|
||||
} else {
|
||||
method = new MedievalPregenMethod(world);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldUseSpigotDirect(World world) {
|
||||
if (!IrisSettings.get().getPregen().isEnableSpigotDirectPregen()) {
|
||||
return false;
|
||||
}
|
||||
if (IrisToolbelt.isIrisStudioWorld(world)) {
|
||||
return false;
|
||||
}
|
||||
if (!world.isAutoSave()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
String name = Bukkit.getServer().getName();
|
||||
if (name != null) {
|
||||
String lower = name.toLowerCase();
|
||||
if (lower.contains("spigot") || lower.contains("craftbukkit")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
method.init();
|
||||
|
||||
-586
@@ -1,586 +0,0 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.pregenerator.methods;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.pregenerator.PregenListener;
|
||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.hunk.storage.AtomicHunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public class SpigotDirectPregenMethod implements PregeneratorMethod {
|
||||
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
|
||||
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
|
||||
private static final long PERMIT_WAIT_NOTIFY_MS = 500L;
|
||||
|
||||
private final World world;
|
||||
private final int threads;
|
||||
private final int timeoutSeconds;
|
||||
private final int timeoutWarnIntervalMs;
|
||||
private final int maxResidentTectonicPlates;
|
||||
private final int mantleBackpressureWaitMs;
|
||||
private final int mantleBackpressureTimeoutMs;
|
||||
|
||||
private final Semaphore semaphore;
|
||||
private final AtomicInteger adaptiveInFlightLimit;
|
||||
private final int adaptiveMinInFlightLimit;
|
||||
private final AtomicInteger timeoutStreak = new AtomicInteger();
|
||||
private final AtomicLong lastTimeoutLogAt = new AtomicLong(0L);
|
||||
private final AtomicInteger suppressedTimeoutLogs = new AtomicInteger();
|
||||
private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L);
|
||||
private final AtomicInteger inFlight = new AtomicInteger();
|
||||
private final AtomicLong submitted = new AtomicLong();
|
||||
private final AtomicLong completed = new AtomicLong();
|
||||
private final AtomicLong failed = new AtomicLong();
|
||||
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
|
||||
private final Object permitMonitor = new Object();
|
||||
|
||||
private final Set<Long> liveLoadedChunkKeys;
|
||||
private final ConcurrentHashMap<Long, Boolean> chunksWrittenDirect;
|
||||
private final AtomicBoolean writePathDisabled;
|
||||
private volatile Engine cachedEngine;
|
||||
private volatile Mantle cachedMantle;
|
||||
private volatile NBTWorld nbtWorld;
|
||||
private volatile MedievalPregenMethod fallback;
|
||||
|
||||
public SpigotDirectPregenMethod(World world, int threads) {
|
||||
this.world = world;
|
||||
int configured = Math.max(1, threads);
|
||||
this.threads = Math.max(8, Math.min(32, configured));
|
||||
this.semaphore = new Semaphore(this.threads, true);
|
||||
this.adaptiveInFlightLimit = new AtomicInteger(this.threads);
|
||||
this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4)));
|
||||
|
||||
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
||||
this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds();
|
||||
this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
|
||||
this.maxResidentTectonicPlates = pregen.getMaxResidentTectonicPlates();
|
||||
this.mantleBackpressureWaitMs = pregen.getMantleBackpressureWaitMs();
|
||||
this.mantleBackpressureTimeoutMs = pregen.getMantleBackpressureTimeoutMs();
|
||||
|
||||
this.liveLoadedChunkKeys = ConcurrentHashMap.newKeySet();
|
||||
this.chunksWrittenDirect = new ConcurrentHashMap<>();
|
||||
this.writePathDisabled = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
try {
|
||||
this.nbtWorld = new NBTWorld(world.getWorldFolder());
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error("SpigotDirect pregen could not open NBTWorld for " + world.getName() + "; disabling direct write path.");
|
||||
IrisLogging.reportError(e);
|
||||
writePathDisabled.set(true);
|
||||
}
|
||||
|
||||
snapshotLoadedChunks();
|
||||
|
||||
IrisLogging.info("SpigotDirect pregen init: world=" + world.getName()
|
||||
+ ", threads=" + threads
|
||||
+ ", adaptiveLimit=" + adaptiveInFlightLimit.get()
|
||||
+ ", initialLoadedChunks=" + liveLoadedChunkKeys.size()
|
||||
+ ", maxResidentTectonicPlates=" + maxResidentTectonicPlates
|
||||
+ ", timeout=" + timeoutSeconds + "s");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
semaphore.acquireUninterruptibly(threads);
|
||||
if (nbtWorld != null) {
|
||||
try {
|
||||
nbtWorld.flushNow();
|
||||
nbtWorld.close();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
evictWrittenChunksFromServer();
|
||||
if (fallback != null) {
|
||||
try {
|
||||
fallback.close();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
if (nbtWorld != null) {
|
||||
try {
|
||||
nbtWorld.save();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
if (fallback != null) {
|
||||
try {
|
||||
fallback.save();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRegions(int x, int z, PregenListener listener) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateRegion(int x, int z, PregenListener listener) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod(int x, int z) {
|
||||
return "SpigotDirect";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mantle getMantle() {
|
||||
if (IrisToolbelt.isIrisWorld(world)) {
|
||||
return IrisToolbelt.access(world).getEngine().getMantle().getMantle();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateChunk(int x, int z, PregenListener listener) {
|
||||
listener.onChunkGenerating(x, z);
|
||||
enforceMantleBudget();
|
||||
|
||||
long key = chunkKey(x, z);
|
||||
if (writePathDisabled.get() || nbtWorld == null || liveLoadedChunkKeys.contains(key) || world.isChunkLoaded(x, z)) {
|
||||
ensureFallback().generateChunk(x, z, listener);
|
||||
return;
|
||||
}
|
||||
|
||||
Engine engine = resolveEngine();
|
||||
if (engine == null || !engine.getDimension().isUseMantle()) {
|
||||
ensureFallback().generateChunk(x, z, listener);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
synchronized (permitMonitor) {
|
||||
while (inFlight.get() >= adaptiveInFlightLimit.get()) {
|
||||
permitMonitor.wait(PERMIT_WAIT_NOTIFY_MS);
|
||||
}
|
||||
}
|
||||
while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
markSubmitted();
|
||||
MultiBurst.burst.lazy(() -> runDirect(engine, x, z, listener));
|
||||
}
|
||||
|
||||
private void runDirect(Engine engine, int x, int z, PregenListener listener) {
|
||||
boolean success = false;
|
||||
try {
|
||||
int worldMinY = world.getMinHeight();
|
||||
int worldMaxY = world.getMaxHeight();
|
||||
int height = worldMaxY - worldMinY;
|
||||
|
||||
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
||||
Hunk<PlatformBlockState> blocks = new AirDefaultAtomicHunk(16, height, 16, air);
|
||||
Hunk<PlatformBiome> biomes = Hunk.newSynchronizedArrayHunk(16, height, 16);
|
||||
|
||||
try {
|
||||
engine.generate(x << 4, z << 4, blocks, biomes, false);
|
||||
} catch (Throwable e) {
|
||||
handleFailure(x, z, e);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean wrote;
|
||||
try {
|
||||
wrote = INMS.get().writeChunkNbtDirect(nbtWorld, x, z, blocks, biomes);
|
||||
} catch (Throwable e) {
|
||||
handleFailure(x, z, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wrote) {
|
||||
if (writePathDisabled.compareAndSet(false, true)) {
|
||||
IrisLogging.warn("SpigotDirect NBT write returned false at chunk " + x + "," + z
|
||||
+ "; disabling direct path. Subsequent chunks will route through MedievalPregenMethod on the iterator thread.");
|
||||
}
|
||||
listener.onChunkGenerated(x, z);
|
||||
listener.onChunkCleaned(x, z);
|
||||
success = true;
|
||||
return;
|
||||
}
|
||||
|
||||
chunksWrittenDirect.put(chunkKey(x, z), Boolean.TRUE);
|
||||
|
||||
try {
|
||||
cleanupMantleChunk(x, z);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
listener.onChunkGenerated(x, z);
|
||||
listener.onChunkCleaned(x, z);
|
||||
success = true;
|
||||
} catch (Throwable e) {
|
||||
handleFailure(x, z, e);
|
||||
} finally {
|
||||
markFinished(success);
|
||||
semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFailure(int x, int z, Throwable error) {
|
||||
IrisLogging.warn("SpigotDirect pregen failure at chunk " + x + "," + z + ". " + metricsSnapshot());
|
||||
IrisLogging.reportError(error);
|
||||
if (error != null) {
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
if (writePathDisabled.compareAndSet(false, true)) {
|
||||
IrisLogging.warn("SpigotDirect: disabling direct write path after first failure. Subsequent chunks route through MedievalPregenMethod on the iterator thread.");
|
||||
}
|
||||
onTimeout(x, z);
|
||||
}
|
||||
|
||||
private MedievalPregenMethod ensureFallback() {
|
||||
MedievalPregenMethod existing = fallback;
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
synchronized (this) {
|
||||
existing = fallback;
|
||||
if (existing == null) {
|
||||
existing = new MedievalPregenMethod(world);
|
||||
existing.init();
|
||||
fallback = existing;
|
||||
}
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
private void snapshotLoadedChunks() {
|
||||
try {
|
||||
Set<Long> loaded = J.sfut(() -> {
|
||||
Set<Long> keys = new HashSet<>();
|
||||
if (world == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Chunk loadedChunk : world.getLoadedChunks()) {
|
||||
keys.add(chunkKey(loadedChunk.getX(), loadedChunk.getZ()));
|
||||
}
|
||||
return keys;
|
||||
}).get();
|
||||
if (loaded != null) {
|
||||
liveLoadedChunkKeys.addAll(loaded);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void evictWrittenChunksFromServer() {
|
||||
if (chunksWrittenDirect.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
J.sfut(() -> {
|
||||
int evicted = 0;
|
||||
for (Long key : chunksWrittenDirect.keySet()) {
|
||||
int x = keyX(key);
|
||||
int z = keyZ(key);
|
||||
if (!world.isChunkLoaded(x, z)) {
|
||||
continue;
|
||||
}
|
||||
if (INMS.get().forceEvictChunk(world, x, z)) {
|
||||
evicted++;
|
||||
}
|
||||
}
|
||||
if (evicted > 0) {
|
||||
IrisLogging.info("SpigotDirect: force-evicted " + evicted + " chunks loaded mid-pregen so the server reloads them from disk.");
|
||||
}
|
||||
}).get();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static long chunkKey(int x, int z) {
|
||||
return (((long) x) << 32) | (z & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static int keyX(long key) {
|
||||
return (int) (key >> 32);
|
||||
}
|
||||
|
||||
private static int keyZ(long key) {
|
||||
return (int) (key & 0xffffffffL);
|
||||
}
|
||||
|
||||
private void onTimeout(int x, int z) {
|
||||
int streak = timeoutStreak.incrementAndGet();
|
||||
if (streak % ADAPTIVE_TIMEOUT_STEP == 0) {
|
||||
lowerAdaptiveInFlightLimit();
|
||||
}
|
||||
|
||||
long now = M.ms();
|
||||
long last = lastTimeoutLogAt.get();
|
||||
if (now - last < timeoutWarnIntervalMs || !lastTimeoutLogAt.compareAndSet(last, now)) {
|
||||
suppressedTimeoutLogs.incrementAndGet();
|
||||
return;
|
||||
}
|
||||
|
||||
int suppressed = suppressedTimeoutLogs.getAndSet(0);
|
||||
String suppressedText = suppressed <= 0 ? "" : " suppressed=" + suppressed;
|
||||
IrisLogging.warn("SpigotDirect pregen failure cluster at " + x + "," + z
|
||||
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
|
||||
+ suppressedText + " " + metricsSnapshot());
|
||||
}
|
||||
|
||||
private void onSuccess() {
|
||||
int streak = timeoutStreak.get();
|
||||
if (streak > 0) {
|
||||
int newStreak = Math.max(0, streak - 2);
|
||||
timeoutStreak.compareAndSet(streak, newStreak);
|
||||
if (newStreak > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ((completed.get() & (ADAPTIVE_RECOVERY_INTERVAL - 1)) == 0L) {
|
||||
raiseAdaptiveInFlightLimit();
|
||||
}
|
||||
}
|
||||
|
||||
private void lowerAdaptiveInFlightLimit() {
|
||||
while (true) {
|
||||
int current = adaptiveInFlightLimit.get();
|
||||
if (current <= adaptiveMinInFlightLimit) {
|
||||
return;
|
||||
}
|
||||
int next = Math.max(adaptiveMinInFlightLimit, current - 1);
|
||||
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
|
||||
logAdaptiveLimit("decrease", next);
|
||||
notifyPermitWaiters();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void raiseAdaptiveInFlightLimit() {
|
||||
while (true) {
|
||||
int current = adaptiveInFlightLimit.get();
|
||||
if (current >= threads) {
|
||||
return;
|
||||
}
|
||||
int deficit = threads - current;
|
||||
int step = deficit > (threads / 2) ? Math.max(2, threads / 8) : 1;
|
||||
int next = Math.min(threads, current + step);
|
||||
if (adaptiveInFlightLimit.compareAndSet(current, next)) {
|
||||
logAdaptiveLimit("increase", next);
|
||||
notifyPermitWaiters();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void logAdaptiveLimit(String mode, int value) {
|
||||
long now = M.ms();
|
||||
long last = lastAdaptiveLogAt.get();
|
||||
if (now - last < 5000L) {
|
||||
return;
|
||||
}
|
||||
if (lastAdaptiveLogAt.compareAndSet(last, now)) {
|
||||
IrisLogging.info("SpigotDirect pregen adaptive limit " + mode + " -> " + value + " (" + metricsSnapshot() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private String metricsSnapshot() {
|
||||
long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get());
|
||||
return "world=" + world.getName()
|
||||
+ " permits=" + semaphore.availablePermits() + "/" + threads
|
||||
+ " adaptiveLimit=" + adaptiveInFlightLimit.get()
|
||||
+ " inFlight=" + inFlight.get()
|
||||
+ " submitted=" + submitted.get()
|
||||
+ " completed=" + completed.get()
|
||||
+ " failed=" + failed.get()
|
||||
+ " stalledForMs=" + stalledFor;
|
||||
}
|
||||
|
||||
private void markSubmitted() {
|
||||
submitted.incrementAndGet();
|
||||
inFlight.incrementAndGet();
|
||||
}
|
||||
|
||||
private void markFinished(boolean success) {
|
||||
if (success) {
|
||||
completed.incrementAndGet();
|
||||
onSuccess();
|
||||
} else {
|
||||
failed.incrementAndGet();
|
||||
}
|
||||
|
||||
lastProgressAt.set(M.ms());
|
||||
int after = inFlight.decrementAndGet();
|
||||
if (after < 0) {
|
||||
inFlight.compareAndSet(after, 0);
|
||||
}
|
||||
notifyPermitWaiters();
|
||||
}
|
||||
|
||||
private void notifyPermitWaiters() {
|
||||
synchronized (permitMonitor) {
|
||||
permitMonitor.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupMantleChunk(int x, int z) {
|
||||
Engine engine = resolveEngine();
|
||||
if (engine != null) {
|
||||
try {
|
||||
engine.getMantle().forceCleanupChunk(x, z);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Engine resolveEngine() {
|
||||
Engine cached = cachedEngine;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Engine resolved = IrisToolbelt.access(world).getEngine();
|
||||
if (resolved != null) {
|
||||
cachedEngine = resolved;
|
||||
}
|
||||
return resolved;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Mantle resolveMantle() {
|
||||
Mantle cached = cachedMantle;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Mantle resolved = getMantle();
|
||||
if (resolved != null) {
|
||||
cachedMantle = resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void enforceMantleBudget() {
|
||||
int cap = maxResidentTectonicPlates;
|
||||
if (cap <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mantle mantle = resolveMantle();
|
||||
if (mantle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int hardCap = cap * 2;
|
||||
if (mantle.getLoadedRegionCount() <= hardCap) {
|
||||
return;
|
||||
}
|
||||
|
||||
long waitStart = M.ms();
|
||||
long lastLog = 0L;
|
||||
while (mantle.getLoadedRegionCount() > hardCap) {
|
||||
mantle.trim(0L, 0);
|
||||
int freed = mantle.unloadTectonicPlate(0);
|
||||
int resident = mantle.getLoadedRegionCount();
|
||||
if (resident <= hardCap) {
|
||||
break;
|
||||
}
|
||||
|
||||
long elapsed = M.ms() - waitStart;
|
||||
if (elapsed >= mantleBackpressureTimeoutMs) {
|
||||
IrisLogging.warn("SpigotDirect mantle backpressure exceeded " + mantleBackpressureTimeoutMs + "ms with " + resident
|
||||
+ " tectonic plates resident (hard cap " + hardCap + "); proceeding to avoid deadlock. "
|
||||
+ "Raise pregen.maxResidentTectonicPlates if this persists. " + metricsSnapshot());
|
||||
return;
|
||||
}
|
||||
|
||||
long logNow = M.ms();
|
||||
if (logNow - lastLog >= 5_000L) {
|
||||
lastLog = logNow;
|
||||
IrisLogging.warn("SpigotDirect mantle backpressure: " + resident + " tectonic plates resident (hard cap " + hardCap
|
||||
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(mantleBackpressureWaitMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AirDefaultAtomicHunk extends AtomicHunk<PlatformBlockState> {
|
||||
private final PlatformBlockState airDefault;
|
||||
|
||||
AirDefaultAtomicHunk(int w, int h, int d, PlatformBlockState airDefault) {
|
||||
super(w, h, d);
|
||||
this.airDefault = airDefault;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState getRaw(int x, int y, int z) {
|
||||
PlatformBlockState v = super.getRaw(x, y, z);
|
||||
return v != null ? v : airDefault;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,6 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Locale;
|
||||
@@ -147,20 +145,15 @@ public class IrisBiomeCustom {
|
||||
if (i == null) {
|
||||
continue;
|
||||
}
|
||||
EntityType type = i.getType();
|
||||
if (type == null) {
|
||||
String key = i.getTypeKey();
|
||||
if (key == null) {
|
||||
IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + getId());
|
||||
continue;
|
||||
}
|
||||
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
|
||||
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
|
||||
JSONObject o = new JSONObject();
|
||||
NamespacedKey key = type.getKey();
|
||||
if (key == null) {
|
||||
IrisLogging.warn("Skipping custom biome spawn with unresolved entity key in biome " + getId());
|
||||
continue;
|
||||
}
|
||||
o.put("type", key.toString());
|
||||
o.put("type", key);
|
||||
o.put("weight", i.getWeight());
|
||||
o.put("minCount", i.getMinCount());
|
||||
o.put("maxCount", i.getMaxCount());
|
||||
|
||||
@@ -50,6 +50,13 @@ public class IrisBiomeCustomSpawn {
|
||||
});
|
||||
}
|
||||
|
||||
public String getTypeKey() {
|
||||
if (type == null || type.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return type.contains(":") ? type : "minecraft:" + type;
|
||||
}
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("The min to spawn")
|
||||
private int minCount = 2;
|
||||
|
||||
@@ -38,11 +38,14 @@ import art.arcane.volmlib.util.math.Vector3d;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
@@ -291,6 +294,35 @@ public final class BukkitPlatform implements IrisPlatform {
|
||||
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
|
||||
if (!(world instanceof World bukkitWorld) || entityKey == null) {
|
||||
return false;
|
||||
}
|
||||
NamespacedKey namespacedKey = NamespacedKey.fromString(entityKey);
|
||||
if (namespacedKey == null) {
|
||||
return false;
|
||||
}
|
||||
EntityType type = Registry.ENTITY_TYPE.get(namespacedKey);
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
Entity entity = spawnEntity(new Location(bukkitWorld, x, y, z), type, CreatureSpawnEvent.SpawnReason.COMMAND);
|
||||
return entity != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean giveItem(Object player, String itemKey, int amount) {
|
||||
if (!(player instanceof Player bukkitPlayer) || itemKey == null || amount <= 0) {
|
||||
return false;
|
||||
}
|
||||
Material material = Material.matchMaterial(itemKey);
|
||||
if (material == null) {
|
||||
return false;
|
||||
}
|
||||
return bukkitPlayer.getInventory().addItem(new ItemStack(material, amount)).isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(LogLevel level, String message) {
|
||||
bridge().logSink().accept(level, message);
|
||||
|
||||
+4
-4
@@ -8,8 +8,8 @@ public class AsyncPregenMethodConcurrencyCapTest {
|
||||
@Test
|
||||
public void paperLikeRecommendedCapTracksWorkerThreads() {
|
||||
assertEquals(16, AsyncPregenMethod.computePaperLikeRecommendedCap(1));
|
||||
assertEquals(16, AsyncPregenMethod.computePaperLikeRecommendedCap(4));
|
||||
assertEquals(48, AsyncPregenMethod.computePaperLikeRecommendedCap(12));
|
||||
assertEquals(32, AsyncPregenMethod.computePaperLikeRecommendedCap(4));
|
||||
assertEquals(96, AsyncPregenMethod.computePaperLikeRecommendedCap(12));
|
||||
assertEquals(128, AsyncPregenMethod.computePaperLikeRecommendedCap(80));
|
||||
assertEquals(128, AsyncPregenMethod.computePaperLikeRecommendedCap(128));
|
||||
}
|
||||
@@ -17,8 +17,8 @@ public class AsyncPregenMethodConcurrencyCapTest {
|
||||
@Test
|
||||
public void foliaRecommendedCapTracksWorkerThreads() {
|
||||
assertEquals(64, AsyncPregenMethod.computeFoliaRecommendedCap(1));
|
||||
assertEquals(64, AsyncPregenMethod.computeFoliaRecommendedCap(12));
|
||||
assertEquals(80, AsyncPregenMethod.computeFoliaRecommendedCap(20));
|
||||
assertEquals(96, AsyncPregenMethod.computeFoliaRecommendedCap(12));
|
||||
assertEquals(160, AsyncPregenMethod.computeFoliaRecommendedCap(20));
|
||||
assertEquals(192, AsyncPregenMethod.computeFoliaRecommendedCap(80));
|
||||
}
|
||||
|
||||
|
||||
@@ -427,6 +427,16 @@ public final class StubPlatform implements IrisPlatform {
|
||||
public void dispatchConsoleCommand(String command) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean giveItem(Object player, String itemKey, int amount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(LogLevel level, String message) {
|
||||
if (VERBOSE) {
|
||||
|
||||
@@ -70,6 +70,10 @@ public interface IrisPlatform {
|
||||
|
||||
void dispatchConsoleCommand(String command);
|
||||
|
||||
boolean spawnEntity(Object world, String entityKey, double x, double y, double z);
|
||||
|
||||
boolean giveItem(Object player, String itemKey, int amount);
|
||||
|
||||
void log(LogLevel level, String message);
|
||||
|
||||
void msg(String message);
|
||||
|
||||
Reference in New Issue
Block a user