mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-24 07:40:56 +00:00
Dont ask
This commit is contained in:
+17
-2
@@ -84,7 +84,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
|
||||
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
|
||||
private static final ExecutorService GEN_POOL = createGenPool();
|
||||
private static volatile ExecutorService genPool = createGenPool();
|
||||
|
||||
public static void startGenPool() {
|
||||
ExecutorService pool = genPool;
|
||||
if (pool == null || pool.isShutdown()) {
|
||||
genPool = createGenPool();
|
||||
}
|
||||
}
|
||||
|
||||
public static void shutdownGenPool() {
|
||||
ExecutorService pool = genPool;
|
||||
if (pool != null) {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean detectParallelChunkSystem() {
|
||||
String[] markers = {
|
||||
@@ -269,7 +283,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> generateTerrain(chunk, generationEngine, pos, dimMinY, height, air, biomeRegistry, randomState),
|
||||
GEN_POOL);
|
||||
genPool);
|
||||
}
|
||||
|
||||
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
||||
@@ -294,6 +308,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
writeBlocks(chunk, blocks, dimMinY, height);
|
||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(Heightmap.Types.WORLD_SURFACE_WG, Heightmap.Types.OCEAN_FLOOR_WG));
|
||||
chunk.fillBiomesFromNoise(new HunkBiomeResolver(this, biomes, biomeRegistry, pos, dimMinY, height), randomState.sampler());
|
||||
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
|
||||
+16
-14
@@ -21,6 +21,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 art.arcane.volmlib.util.data.UnresolvedKeyLog;
|
||||
import com.mojang.brigadier.StringReader;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.minecraft.commands.arguments.blocks.BlockStateParser;
|
||||
@@ -83,7 +84,7 @@ public final class ModdedBlockResolution {
|
||||
private static final Set<Block> PLACE_ONTO_LEAVES = blockSet(
|
||||
"acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves");
|
||||
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||
private static long lastWarnMs;
|
||||
private static final UnresolvedKeyLog UNRESOLVED = new UnresolvedKeyLog("Iris modded block resolution", 30_000L);
|
||||
|
||||
private ModdedBlockResolution() {
|
||||
}
|
||||
@@ -150,13 +151,14 @@ public final class ModdedBlockResolution {
|
||||
return map;
|
||||
}
|
||||
|
||||
private static boolean shouldWarn() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastWarnMs >= 1000) {
|
||||
lastWarnMs = now;
|
||||
return true;
|
||||
private static void warnUnresolved(String key, String message) {
|
||||
if (UNRESOLVED.firstOccurrence(key)) {
|
||||
IrisLogging.warn(message);
|
||||
}
|
||||
String summary = UNRESOLVED.pollSummary();
|
||||
if (summary != null) {
|
||||
IrisLogging.warn(summary);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static BlockState airState() {
|
||||
@@ -192,7 +194,7 @@ public final class ModdedBlockResolution {
|
||||
return parsed;
|
||||
}
|
||||
IrisLogging.error("Can't find block data for " + bdxf);
|
||||
return resolveNoCompat("STONE");
|
||||
return new Parsed(AIR, null);
|
||||
}
|
||||
|
||||
static Parsed resolveNoCompat(String bdxf) {
|
||||
@@ -222,8 +224,8 @@ public final class ModdedBlockResolution {
|
||||
if (provided != null) {
|
||||
return new Parsed(provided, null);
|
||||
}
|
||||
if (warn && shouldWarn()) {
|
||||
IrisLogging.warn("Unknown Block Data '" + bd + "'");
|
||||
if (warn) {
|
||||
warnUnresolved(bd, "Unknown Block Data '" + bd + "'");
|
||||
}
|
||||
return new Parsed(AIR, null);
|
||||
}
|
||||
@@ -231,8 +233,8 @@ public final class ModdedBlockResolution {
|
||||
return bdx;
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
if (warn && shouldWarn()) {
|
||||
IrisLogging.warn("Unknown Block Data '" + bdxf + "'");
|
||||
if (warn) {
|
||||
warnUnresolved(bdxf, "Unknown Block Data '" + bdxf + "'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,8 +299,8 @@ public final class ModdedBlockResolution {
|
||||
}
|
||||
|
||||
if (bx == null) {
|
||||
if (warn && shouldWarn()) {
|
||||
IrisLogging.warn("Unknown Block Data: " + ix);
|
||||
if (warn) {
|
||||
warnUnresolved(ix, "Unknown Block Data: " + ix);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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 art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedDeathLoot {
|
||||
private static final String TAG_PREFIX = "iris_loot|";
|
||||
private static final int LOOT_RNG_SALT = 345911;
|
||||
private static final Set<String> WARNED_TABLES = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private ModdedDeathLoot() {
|
||||
}
|
||||
|
||||
public static void tag(LivingEntity entity, KList<String> tableKeys, int spawnX, int spawnY, int spawnZ, RNG rng) {
|
||||
if (entity == null || tableKeys == null || tableKeys.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
entity.addTag(TAG_PREFIX + rng.getSeed() + "|" + spawnX + "|" + spawnY + "|" + spawnZ + "|" + String.join(",", tableKeys));
|
||||
}
|
||||
|
||||
public static void handle(LivingEntity entity) {
|
||||
if (entity == null || !(entity.level() instanceof ServerLevel level)) {
|
||||
return;
|
||||
}
|
||||
String encoded = findTag(entity);
|
||||
if (encoded == null) {
|
||||
return;
|
||||
}
|
||||
String[] parts = encoded.substring(TAG_PREFIX.length()).split("\\|", 5);
|
||||
if (parts.length < 5) {
|
||||
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
|
||||
return;
|
||||
}
|
||||
long seed;
|
||||
int spawnX;
|
||||
int spawnY;
|
||||
int spawnZ;
|
||||
try {
|
||||
seed = Long.parseLong(parts[0]);
|
||||
spawnX = Integer.parseInt(parts[1]);
|
||||
spawnY = Integer.parseInt(parts[2]);
|
||||
spawnZ = Integer.parseInt(parts[3]);
|
||||
} catch (NumberFormatException error) {
|
||||
IrisLogging.debug("Iris death loot: malformed loot tag '" + encoded + "'");
|
||||
return;
|
||||
}
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
RNG lootRng = new RNG(seed).nextParallelRNG(LOOT_RNG_SALT);
|
||||
KList<ItemStack> drops = new KList<>();
|
||||
for (String key : parts[4].split(",")) {
|
||||
String trimmed = key.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
IrisLootTable table = engine.getData().getLootLoader().load(trimmed);
|
||||
if (table == null) {
|
||||
if (WARNED_TABLES.add(trimmed)) {
|
||||
IrisLogging.warn("Iris death loot: unknown loot table '" + trimmed + "'");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
drops.addAll(ModdedItemTranslator.loot(table, lootRng, InventorySlotType.STORAGE, level, spawnX, spawnY, spawnZ));
|
||||
}
|
||||
emit(level, entity, drops);
|
||||
}
|
||||
|
||||
private static void emit(ServerLevel level, LivingEntity entity, KList<ItemStack> drops) {
|
||||
double x = entity.getX();
|
||||
double y = entity.getY() + 0.5D;
|
||||
double z = entity.getZ();
|
||||
for (ItemStack stack : drops) {
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ItemEntity itemEntity = new ItemEntity(level, x, y, z, stack);
|
||||
itemEntity.setDefaultPickUpDelay();
|
||||
level.addFreshEntity(itemEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private static String findTag(LivingEntity entity) {
|
||||
for (String tag : entity.entityTags()) {
|
||||
if (tag.startsWith(TAG_PREFIX)) {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Engine engineFor(ServerLevel level) {
|
||||
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||
if (generator instanceof IrisModdedChunkGenerator irisGenerator) {
|
||||
return irisGenerator.engineIfBound();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+11
-56
@@ -21,7 +21,6 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.iris.core.gui.GuiHost;
|
||||
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.PreservationRegistry;
|
||||
import art.arcane.iris.engine.object.BlockDataMergeSupport;
|
||||
@@ -36,6 +35,7 @@ import art.arcane.iris.modded.command.ModdedStudioCommands;
|
||||
import art.arcane.iris.modded.command.ModdedWandService;
|
||||
import art.arcane.iris.modded.service.ModdedChunkUpdateService;
|
||||
import art.arcane.iris.modded.service.ModdedEngineMaintenanceService;
|
||||
import art.arcane.iris.modded.service.ModdedEntitySpawnService;
|
||||
import art.arcane.iris.modded.service.ModdedLogFilterService;
|
||||
import art.arcane.iris.modded.service.ModdedPreservationService;
|
||||
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
||||
@@ -43,10 +43,6 @@ import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -81,6 +77,7 @@ public final class ModdedEngineBootstrap {
|
||||
ModdedPrimaryWorldRouter.tick(server);
|
||||
SERVICE_MANAGER.tick(server);
|
||||
ModdedPregenBossBar.tick(server);
|
||||
ModdedProtocolHandler.tickDimensionSync(server);
|
||||
}
|
||||
|
||||
public static void start(MinecraftServer server) {
|
||||
@@ -91,10 +88,14 @@ public final class ModdedEngineBootstrap {
|
||||
if (scheduler != null) {
|
||||
scheduler.reset();
|
||||
}
|
||||
IrisModdedChunkGenerator.startGenPool();
|
||||
SERVICE_MANAGER.enableAll();
|
||||
ModdedProtocolHandler.start(server);
|
||||
ModdedSentry.start(loader());
|
||||
}
|
||||
|
||||
public static void stop() {
|
||||
ModdedProtocolHandler.stop();
|
||||
ModdedPregenJob.shutdown();
|
||||
ModdedObjectUndo.clearAll();
|
||||
ModdedWandService.clearAll();
|
||||
@@ -105,8 +106,10 @@ public final class ModdedEngineBootstrap {
|
||||
ModdedDimensionManager.clear();
|
||||
ModdedScheduler scheduler = schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
scheduler.reset();
|
||||
scheduler.shutdown();
|
||||
}
|
||||
IrisModdedChunkGenerator.shutdownGenPool();
|
||||
ModdedSentry.flush();
|
||||
ModdedStartup.reset();
|
||||
currentServer = null;
|
||||
}
|
||||
@@ -197,8 +200,9 @@ public final class ModdedEngineBootstrap {
|
||||
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
|
||||
SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||
SERVICE_MANAGER.register(ModdedEntitySpawnService.class, new ModdedEntitySpawnService());
|
||||
IrisServices.register(PreservationRegistry.class, preservation);
|
||||
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
|
||||
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new ModdedWorldManager(engine));
|
||||
ModdedCustomContentRegistry.discover();
|
||||
platform = created;
|
||||
SERVICE_MANAGER.enableAll();
|
||||
@@ -209,53 +213,4 @@ public final class ModdedEngineBootstrap {
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InertWorldManager implements EngineWorldManager {
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEntityCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChunkCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getEntitySaturation() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSave() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockBreak(BlockBreakEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockPlace(BlockPlaceEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkLoad(Chunk e, boolean generated) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkUnload(Chunk e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleportAsync(PlayerTeleportEvent e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* 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.IrisSettings;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisAttributeModifier;
|
||||
import art.arcane.iris.engine.object.IrisCommand;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisLoot;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.AgeableMob;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.ai.attributes.Attribute;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.entity.animal.panda.Panda;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
import net.minecraft.world.entity.npc.villager.Villager;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedEntitySpawner {
|
||||
private static final int PASSENGER_RNG_BASE = 234858;
|
||||
private static final int LEASH_RNG_SEED = 234548;
|
||||
private static final String COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx";
|
||||
private static final Set<String> WARNED_TYPES = ConcurrentHashMap.newKeySet();
|
||||
private static final Set<String> WARNED_ATTRIBUTES = ConcurrentHashMap.newKeySet();
|
||||
private static boolean warnedSpawnEffect = false;
|
||||
|
||||
private ModdedEntitySpawner() {
|
||||
}
|
||||
|
||||
public static Entity spawn(Engine engine, IrisEntity irisEntity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
if (engine == null || irisEntity == null || level == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
double x = blockX + 0.5;
|
||||
double y = blockY + 0.5;
|
||||
double z = blockZ + 0.5;
|
||||
|
||||
Entity created = create(irisEntity, level, x, y, z);
|
||||
if (created == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (irisEntity.isSpecialType() && !irisEntity.isApplySettingsToCustomMobAnyways()) {
|
||||
return created;
|
||||
}
|
||||
|
||||
applyConfig(engine, irisEntity, created, level, blockX, blockY, blockZ, rng);
|
||||
return created;
|
||||
}
|
||||
|
||||
private static Entity create(IrisEntity irisEntity, ServerLevel level, double x, double y, double z) {
|
||||
if (irisEntity.isSpecialType()) {
|
||||
return ModdedCustomContentRegistry.spawnMob(level, x, y, z, irisEntity.getSpecialType());
|
||||
}
|
||||
|
||||
EntityType<?> type = resolveType(irisEntity.getType());
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return type.spawn(level, BlockPos.containing(x, y, z), reasonFor(irisEntity.getReason()));
|
||||
}
|
||||
|
||||
private static void applyConfig(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
String customName = irisEntity.getCustomName();
|
||||
if (customName != null && !customName.isBlank()) {
|
||||
entity.setCustomName(Component.literal(colorize(customName)));
|
||||
}
|
||||
entity.setCustomNameVisible(irisEntity.isCustomNameVisible());
|
||||
entity.setGlowingTag(irisEntity.isGlowing());
|
||||
entity.setNoGravity(!irisEntity.isGravity());
|
||||
entity.setInvulnerable(irisEntity.isInvulnerable());
|
||||
entity.setSilent(irisEntity.isSilent());
|
||||
|
||||
boolean persistent = irisEntity.isKeepEntity() || forcePersist();
|
||||
if (persistent && entity instanceof Mob persistentMob) {
|
||||
persistentMob.setPersistenceRequired();
|
||||
}
|
||||
|
||||
applyPassengers(engine, irisEntity, entity, level, blockX, blockY, blockZ, rng);
|
||||
|
||||
if (entity instanceof LivingEntity living) {
|
||||
applyAttributes(irisEntity, living, level, rng);
|
||||
}
|
||||
|
||||
if (entity instanceof LivingEntity lootHolder && !irisEntity.getLoot().getTables().isEmpty()) {
|
||||
ModdedDeathLoot.tag(lootHolder, irisEntity.getLoot().getTables(), blockX, blockY, blockZ, rng);
|
||||
}
|
||||
|
||||
if (entity instanceof Mob mob) {
|
||||
mob.setNoAi(!irisEntity.isAi());
|
||||
mob.setCanPickUpLoot(irisEntity.isPickupItems());
|
||||
if (!irisEntity.isRemovable()) {
|
||||
mob.setPersistenceRequired();
|
||||
}
|
||||
if (irisEntity.getLeashHolder() != null) {
|
||||
Entity holder = spawn(engine, irisEntity.getLeashHolder(), level, blockX, blockY, blockZ, rng.nextParallelRNG(LEASH_RNG_SEED));
|
||||
if (holder != null) {
|
||||
mob.setLeashedTo(holder, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entity instanceof LivingEntity equippable) {
|
||||
applyEquipment(irisEntity, equippable, level, rng);
|
||||
}
|
||||
|
||||
if (irisEntity.isBaby()) {
|
||||
applyBaby(entity);
|
||||
}
|
||||
|
||||
if (entity instanceof Panda panda) {
|
||||
panda.setMainGene(gene(irisEntity.getPandaMainGene()));
|
||||
panda.setHiddenGene(gene(irisEntity.getPandaHiddenGene()));
|
||||
}
|
||||
|
||||
if (entity instanceof Villager villager) {
|
||||
villager.setPersistenceRequired();
|
||||
}
|
||||
|
||||
if (irisEntity.getSpawnEffect() != null || irisEntity.isSpawnEffectRiseOutOfGround()) {
|
||||
noteSpawnEffectGap();
|
||||
}
|
||||
|
||||
applyRawCommands(irisEntity, level, blockX, blockY, blockZ);
|
||||
}
|
||||
|
||||
private static void applyPassengers(Engine engine, IrisEntity irisEntity, Entity entity, ServerLevel level, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
int index = 0;
|
||||
for (IrisEntity passengerEntity : irisEntity.getPassengers()) {
|
||||
Entity passenger = spawn(engine, passengerEntity, level, blockX, blockY, blockZ, rng.nextParallelRNG(PASSENGER_RNG_BASE + index++));
|
||||
if (passenger != null) {
|
||||
passenger.startRiding(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyAttributes(IrisEntity irisEntity, LivingEntity living, ServerLevel level, RNG rng) {
|
||||
KList<IrisAttributeModifier> modifiers = irisEntity.getAttributes();
|
||||
if (modifiers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Registry<Attribute> registry = level.registryAccess().lookupOrThrow(Registries.ATTRIBUTE);
|
||||
int index = 0;
|
||||
for (IrisAttributeModifier modifier : modifiers) {
|
||||
index++;
|
||||
if (rng.nextDouble() >= modifier.getChance()) {
|
||||
continue;
|
||||
}
|
||||
Holder<Attribute> holder = ModdedItemTranslator.resolveAttribute(registry, modifier.getAttribute());
|
||||
if (holder == null) {
|
||||
if (WARNED_ATTRIBUTES.add(modifier.getAttribute())) {
|
||||
IrisLogging.warn("Iris entity: unknown attribute '" + modifier.getAttribute() + "'");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
AttributeInstance instance = living.getAttributes().getInstance(holder);
|
||||
if (instance == null) {
|
||||
continue;
|
||||
}
|
||||
Identifier id = Identifier.tryParse("iris:" + normalizeName(modifier.getName()) + "_" + index);
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
instance.addOrReplacePermanentModifier(new AttributeModifier(id, modifier.getAmount(rng), ModdedItemTranslator.operationFor(modifier.getOperation())));
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyEquipment(IrisEntity irisEntity, LivingEntity living, ServerLevel level, RNG rng) {
|
||||
setSlot(living, EquipmentSlot.HEAD, irisEntity.getHelmet(), level, rng);
|
||||
setSlot(living, EquipmentSlot.CHEST, irisEntity.getChestplate(), level, rng);
|
||||
setSlot(living, EquipmentSlot.LEGS, irisEntity.getLeggings(), level, rng);
|
||||
setSlot(living, EquipmentSlot.FEET, irisEntity.getBoots(), level, rng);
|
||||
setSlot(living, EquipmentSlot.MAINHAND, irisEntity.getMainHand(), level, rng);
|
||||
setSlot(living, EquipmentSlot.OFFHAND, irisEntity.getOffHand(), level, rng);
|
||||
}
|
||||
|
||||
private static void setSlot(LivingEntity living, EquipmentSlot slot, IrisLoot loot, ServerLevel level, RNG rng) {
|
||||
if (loot == null || rng.i(1, loot.getRarity()) != 1) {
|
||||
return;
|
||||
}
|
||||
ItemStack stack = ModdedItemTranslator.stack(loot, rng, level);
|
||||
if (stack != null && !stack.isEmpty()) {
|
||||
living.setItemSlot(slot, stack);
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyBaby(Entity entity) {
|
||||
if (entity instanceof AgeableMob ageable) {
|
||||
ageable.setBaby(true);
|
||||
return;
|
||||
}
|
||||
if (entity instanceof Zombie zombie) {
|
||||
zombie.setBaby(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyRawCommands(IrisEntity irisEntity, ServerLevel level, int blockX, int blockY, int blockZ) {
|
||||
KList<IrisCommand> rawCommands = irisEntity.getRawCommands();
|
||||
if (rawCommands.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
MinecraftServer server = level.getServer();
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (IrisCommand command : rawCommands) {
|
||||
for (String raw : command.getCommands()) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String prepared = (raw.startsWith("/") ? raw.substring(1) : raw)
|
||||
.replace("{x}", String.valueOf(blockX))
|
||||
.replace("{y}", String.valueOf(blockY))
|
||||
.replace("{z}", String.valueOf(blockZ));
|
||||
ModdedServerCommands.dispatch(server, prepared);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static EntityType<?> resolveType(String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String normalized = key.trim().toLowerCase(Locale.ROOT);
|
||||
Identifier id = Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized);
|
||||
if (id == null || !BuiltInRegistries.ENTITY_TYPE.containsKey(id)) {
|
||||
if (WARNED_TYPES.add(normalized)) {
|
||||
IrisLogging.warn("Iris entity: unknown entity type '" + key + "'; skipping spawn");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return BuiltInRegistries.ENTITY_TYPE.getValue(id);
|
||||
}
|
||||
|
||||
private static EntitySpawnReason reasonFor(String reason) {
|
||||
if (reason == null || reason.isBlank()) {
|
||||
return EntitySpawnReason.NATURAL;
|
||||
}
|
||||
String value = reason.trim().toUpperCase(Locale.ROOT);
|
||||
String mapped = switch (value) {
|
||||
case "CHUNK_GEN" -> "CHUNK_GENERATION";
|
||||
case "SPAWNER_EGG" -> "SPAWN_EGG";
|
||||
case "DISPENSE_EGG" -> "DISPENSER";
|
||||
case "BUILD_SNOWMAN", "BUILD_IRONGOLEM", "BUILD_WITHER", "CUSTOM", "DEFAULT" -> "MOB_SUMMONED";
|
||||
default -> value;
|
||||
};
|
||||
try {
|
||||
return EntitySpawnReason.valueOf(mapped);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return EntitySpawnReason.NATURAL;
|
||||
}
|
||||
}
|
||||
|
||||
private static Panda.Gene gene(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return Panda.Gene.NORMAL;
|
||||
}
|
||||
try {
|
||||
return Panda.Gene.valueOf(value.trim().toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return Panda.Gene.NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean forcePersist() {
|
||||
return IrisSettings.get().getWorld().isForcePersistEntities();
|
||||
}
|
||||
|
||||
private static void noteSpawnEffectGap() {
|
||||
if (!warnedSpawnEffect) {
|
||||
warnedSpawnEffect = true;
|
||||
IrisLogging.debug("Iris entity spawnEffect / spawnEffectRiseOutOfGround are Bukkit-only visual paths and are skipped on modded.");
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeName(String name) {
|
||||
String source = name == null || name.isBlank() ? "modifier" : name;
|
||||
String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_");
|
||||
return normalized.isBlank() ? "modifier" : normalized;
|
||||
}
|
||||
|
||||
private static String colorize(String text) {
|
||||
char[] chars = text.toCharArray();
|
||||
for (int i = 0; i < chars.length - 1; i++) {
|
||||
if (chars[i] == '&' && COLOR_CODES.indexOf(chars[i + 1]) > -1) {
|
||||
chars[i] = '§';
|
||||
chars[i + 1] = Character.toLowerCase(chars[i + 1]);
|
||||
}
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.spi.protocol.IrisProtocol;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
public record ModdedIrisPayload(byte[] data) implements CustomPacketPayload {
|
||||
public static final CustomPacketPayload.Type<ModdedIrisPayload> TYPE = new CustomPacketPayload.Type<>(Identifier.parse(IrisProtocol.CHANNEL));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ModdedIrisPayload> STREAM_CODEC = CustomPacketPayload.codec(ModdedIrisPayload::write, ModdedIrisPayload::read);
|
||||
|
||||
@Override
|
||||
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
private static ModdedIrisPayload read(RegistryFriendlyByteBuf buffer) {
|
||||
return new ModdedIrisPayload(buffer.readByteArray(IrisProtocol.MAX_FRAME_BYTES));
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buffer) {
|
||||
buffer.writeByteArray(data);
|
||||
}
|
||||
}
|
||||
+140
-7
@@ -43,13 +43,19 @@ import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.Unit;
|
||||
import net.minecraft.world.entity.EquipmentSlotGroup;
|
||||
import net.minecraft.world.entity.ai.attributes.Attribute;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.item.component.CustomModelData;
|
||||
import net.minecraft.world.item.component.DyedItemColor;
|
||||
import net.minecraft.world.item.component.ItemAttributeModifiers;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.item.enchantment.Enchantment;
|
||||
import net.minecraft.world.item.enchantment.ItemEnchantments;
|
||||
|
||||
@@ -96,6 +102,21 @@ public final class ModdedItemTranslator {
|
||||
return out;
|
||||
}
|
||||
|
||||
public static ItemStack stack(IrisLoot loot, RNG rng, ServerLevel level) {
|
||||
try {
|
||||
ItemStack stack = baseStack(loot, rng);
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
applyComponents(loot, stack, rng, level);
|
||||
applyCustomNbt(stack, loot.getCustomNbt());
|
||||
return stack;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, ServerLevel level, int x, int y, int z) {
|
||||
if (loot.getChance().aquire(() -> NoiseStyle.STATIC.create(rng)).fit(1, loot.getRarity() * table.getRarity(), x, y, z) != 1) {
|
||||
return null;
|
||||
@@ -134,14 +155,14 @@ public final class ModdedItemTranslator {
|
||||
|
||||
private static void applyComponents(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
|
||||
applyEnchantments(loot, stack, rng, level);
|
||||
consumeAttributeDraws(loot, rng);
|
||||
applyAttributes(loot, stack, rng, level);
|
||||
|
||||
if (loot.isUnbreakable()) {
|
||||
stack.set(DataComponents.UNBREAKABLE, Unit.INSTANCE);
|
||||
}
|
||||
|
||||
if (loot.getItemFlags().isNotEmpty()) {
|
||||
warnOnce("itemFlags", "Iris loot: itemFlags are not supported on modded; skipping flags");
|
||||
applyItemFlags(loot, stack);
|
||||
}
|
||||
|
||||
if (loot.getCustomModel() != null) {
|
||||
@@ -165,7 +186,7 @@ public final class ModdedItemTranslator {
|
||||
|
||||
String dye = readString(DYE_COLOR_FIELD, loot);
|
||||
if (dye != null) {
|
||||
warnOnce("dyeColor", "Iris loot: dyeColor is not supported on modded; skipping");
|
||||
applyDyeColor(stack, dye);
|
||||
}
|
||||
|
||||
if (loot.getDisplayName() != null) {
|
||||
@@ -208,16 +229,128 @@ public final class ModdedItemTranslator {
|
||||
}
|
||||
}
|
||||
|
||||
private static void consumeAttributeDraws(IrisLoot loot, RNG rng) {
|
||||
private static void applyAttributes(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
|
||||
if (loot.getAttributes().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
warnOnce("attributes", "Iris loot: attribute modifiers are not supported on modded; skipping");
|
||||
|
||||
Registry<Attribute> registry = level.registryAccess().lookupOrThrow(Registries.ATTRIBUTE);
|
||||
ItemAttributeModifiers.Builder builder = ItemAttributeModifiers.builder();
|
||||
boolean changed = false;
|
||||
|
||||
for (IrisAttributeModifier attribute : loot.getAttributes()) {
|
||||
if (rng.nextDouble() < attribute.getChance()) {
|
||||
attribute.getAmount(rng);
|
||||
if (rng.nextDouble() >= attribute.getChance()) {
|
||||
continue;
|
||||
}
|
||||
double amount = attribute.getAmount(rng);
|
||||
Holder<Attribute> holder = resolveAttribute(registry, attribute.getAttribute());
|
||||
if (holder == null) {
|
||||
warnOnce("attribute:" + attribute.getAttribute(), "Iris loot: unknown attribute '" + attribute.getAttribute() + "'");
|
||||
continue;
|
||||
}
|
||||
Identifier modifierId = attributeModifierId(attribute.getName());
|
||||
if (modifierId == null) {
|
||||
continue;
|
||||
}
|
||||
AttributeModifier modifier = new AttributeModifier(modifierId, amount, operationFor(attribute.getOperation()));
|
||||
builder.add(holder, modifier, EquipmentSlotGroup.ANY);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
stack.set(DataComponents.ATTRIBUTE_MODIFIERS, builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
public static Holder<Attribute> resolveAttribute(Registry<Attribute> registry, String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String value = key.trim().toLowerCase(Locale.ROOT);
|
||||
Holder<Attribute> direct = lookupAttribute(registry, Identifier.tryParse(value.indexOf(':') >= 0 ? value : "minecraft:" + value));
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
if (value.startsWith("generic_")) {
|
||||
return lookupAttribute(registry, Identifier.tryParse("minecraft:" + value.substring("generic_".length())));
|
||||
}
|
||||
if (value.startsWith("generic.")) {
|
||||
return lookupAttribute(registry, Identifier.tryParse("minecraft:" + value.substring("generic.".length())));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Holder<Attribute> lookupAttribute(Registry<Attribute> registry, Identifier id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
Optional<Holder.Reference<Attribute>> holder = registry.get(id);
|
||||
return holder.isPresent() ? holder.get() : null;
|
||||
}
|
||||
|
||||
public static AttributeModifier.Operation operationFor(String operation) {
|
||||
if (operation == null || operation.isBlank()) {
|
||||
return AttributeModifier.Operation.ADD_VALUE;
|
||||
}
|
||||
return switch (operation.trim().toUpperCase(Locale.ROOT)) {
|
||||
case "ADD_SCALAR", "ADD_MULTIPLIED_BASE" -> AttributeModifier.Operation.ADD_MULTIPLIED_BASE;
|
||||
case "MULTIPLY_SCALAR_1", "ADD_MULTIPLIED_TOTAL" -> AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL;
|
||||
default -> AttributeModifier.Operation.ADD_VALUE;
|
||||
};
|
||||
}
|
||||
|
||||
private static Identifier attributeModifierId(String name) {
|
||||
String source = name == null || name.isBlank() ? "modifier" : name;
|
||||
String normalized = source.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/._-]", "_");
|
||||
if (normalized.isBlank()) {
|
||||
normalized = "modifier";
|
||||
}
|
||||
return Identifier.tryParse("iris:" + normalized);
|
||||
}
|
||||
|
||||
private static void applyItemFlags(IrisLoot loot, ItemStack stack) {
|
||||
TooltipDisplay display = stack.getOrDefault(DataComponents.TOOLTIP_DISPLAY, TooltipDisplay.DEFAULT);
|
||||
boolean changed = false;
|
||||
|
||||
for (String flag : loot.getItemFlags()) {
|
||||
if (flag == null || flag.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
DataComponentType<?> hidden = tooltipComponentFor(flag.trim().toUpperCase(Locale.ROOT));
|
||||
if (hidden == null) {
|
||||
warnOnce("itemFlag:" + flag, "Iris loot: item flag '" + flag + "' has no modded tooltip equivalent; skipping");
|
||||
continue;
|
||||
}
|
||||
display = display.withHidden(hidden, true);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
stack.set(DataComponents.TOOLTIP_DISPLAY, display);
|
||||
}
|
||||
}
|
||||
|
||||
private static DataComponentType<?> tooltipComponentFor(String flag) {
|
||||
return switch (flag) {
|
||||
case "HIDE_ENCHANTS" -> DataComponents.ENCHANTMENTS;
|
||||
case "HIDE_STORED_ENCHANTS" -> DataComponents.STORED_ENCHANTMENTS;
|
||||
case "HIDE_ATTRIBUTES" -> DataComponents.ATTRIBUTE_MODIFIERS;
|
||||
case "HIDE_UNBREAKABLE" -> DataComponents.UNBREAKABLE;
|
||||
case "HIDE_DESTROYS" -> DataComponents.CAN_BREAK;
|
||||
case "HIDE_PLACED_ON" -> DataComponents.CAN_PLACE_ON;
|
||||
case "HIDE_DYE" -> DataComponents.DYED_COLOR;
|
||||
case "HIDE_ARMOR_TRIM" -> DataComponents.TRIM;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private static void applyDyeColor(ItemStack stack, String dye) {
|
||||
DyeColor color = DyeColor.byName(dye.trim().toLowerCase(Locale.ROOT), null);
|
||||
if (color == null) {
|
||||
warnOnce("dyeColor:" + dye, "Iris loot: unknown dyeColor '" + dye + "'");
|
||||
return;
|
||||
}
|
||||
stack.set(DataComponents.BASE_COLOR, color);
|
||||
}
|
||||
|
||||
private static void applyLore(IrisLoot loot, ItemStack stack) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.BuildConstants;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.LogLevel;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
@@ -37,6 +38,7 @@ import java.util.function.Consumer;
|
||||
|
||||
public final class ModdedPlatform implements IrisPlatform {
|
||||
private static volatile Consumer<Throwable> ERROR_SINK = null;
|
||||
private static volatile Consumer<Throwable> CAPTURE_SINK = null;
|
||||
|
||||
private final ModdedLoader loader;
|
||||
private final ModdedRegistries registries;
|
||||
@@ -56,6 +58,10 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
ERROR_SINK = sink;
|
||||
}
|
||||
|
||||
public static void captureSink(Consumer<Throwable> sink) {
|
||||
CAPTURE_SINK = sink;
|
||||
}
|
||||
|
||||
public MinecraftServer server() {
|
||||
return loader.currentServer();
|
||||
}
|
||||
@@ -116,12 +122,13 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
|
||||
@Override
|
||||
public int irisVersionNumber() {
|
||||
return 0;
|
||||
return parseVersion(loader.modVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minecraftVersionNumber() {
|
||||
return 0;
|
||||
int fromLoader = parseVersion(loader.minecraftVersion());
|
||||
return fromLoader > 0 ? fromLoader : parseVersion(BuildConstants.MINECRAFT_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,6 +137,7 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
|
||||
@Override
|
||||
public void dispatchConsoleCommand(String command) {
|
||||
ModdedServerCommands.dispatch(loader.currentServer(), command);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,13 +167,48 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
|
||||
@Override
|
||||
public void reportError(Throwable error) {
|
||||
if (error == null) {
|
||||
return;
|
||||
}
|
||||
Consumer<Throwable> sink = ERROR_SINK;
|
||||
if (sink != null && error != null) {
|
||||
if (sink != null) {
|
||||
sink.accept(error);
|
||||
return;
|
||||
}
|
||||
if (error != null) {
|
||||
ModdedIrisLog.error("Iris reported error", error);
|
||||
ModdedIrisLog.error("Iris reported error", error);
|
||||
Consumer<Throwable> capture = CAPTURE_SINK;
|
||||
if (capture != null) {
|
||||
try {
|
||||
capture.accept(error);
|
||||
} catch (Throwable captureFailure) {
|
||||
ModdedIrisLog.error("Iris error-reporting sink failed", captureFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseVersion(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return -1;
|
||||
}
|
||||
int hyphen = raw.indexOf('-');
|
||||
String head = hyphen >= 0 ? raw.substring(0, hyphen) : raw;
|
||||
StringBuilder digits = new StringBuilder(head.length());
|
||||
for (int i = 0; i < head.length(); i++) {
|
||||
char ch = head.charAt(i);
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
digits.append(ch);
|
||||
} else if (ch != '.') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (digits.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
long value = Long.parseLong(digits.toString());
|
||||
return value > Integer.MAX_VALUE ? -1 : (int) value;
|
||||
} catch (NumberFormatException error) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.level.ServerPlayer;
|
||||
|
||||
public interface ModdedProtocolChannel {
|
||||
boolean canReceive(ServerPlayer player);
|
||||
|
||||
void send(ServerPlayer player, ModdedIrisPayload payload);
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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.protocol.EngineResolver;
|
||||
import art.arcane.iris.core.protocol.IrisProtocolServer;
|
||||
import art.arcane.iris.core.protocol.IrisSession;
|
||||
import art.arcane.iris.core.protocol.IrisSessionRegistry;
|
||||
import art.arcane.iris.core.protocol.IrisVisionRequestService;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedProtocolHandler {
|
||||
private static final long SERVER_CAPABILITIES = IrisProtocol.CAPABILITY_PREGEN | IrisProtocol.CAPABILITY_VISION | IrisProtocol.CAPABILITY_CURSOR;
|
||||
private static final int DIMENSION_SYNC_INTERVAL_TICKS = 5;
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
|
||||
private static final ConcurrentHashMap<String, Engine> SESSION_ENGINES = new ConcurrentHashMap<>();
|
||||
private static final ConcurrentHashMap<String, String> SESSION_LEVELS = new ConcurrentHashMap<>();
|
||||
|
||||
private static volatile ModdedProtocolChannel channel;
|
||||
private static volatile IrisSessionRegistry registry;
|
||||
private static volatile IrisProtocolServer protocolServer;
|
||||
private static volatile ModdedProtocolTransport transport;
|
||||
private static int dimensionSyncTicks;
|
||||
|
||||
private ModdedProtocolHandler() {
|
||||
}
|
||||
|
||||
public static void bindChannel(ModdedProtocolChannel boundChannel) {
|
||||
channel = Objects.requireNonNull(boundChannel, "protocol channel");
|
||||
}
|
||||
|
||||
public static void start(MinecraftServer server) {
|
||||
ModdedProtocolChannel boundChannel = channel;
|
||||
if (server == null || boundChannel == null) {
|
||||
return;
|
||||
}
|
||||
SESSION_ENGINES.clear();
|
||||
SESSION_LEVELS.clear();
|
||||
IrisSessionRegistry sessionRegistry = new IrisSessionRegistry();
|
||||
ModdedProtocolTransport serverTransport = new ModdedProtocolTransport(server, boundChannel);
|
||||
IrisProtocolServer protocol = new IrisProtocolServer(sessionRegistry, SERVER_CAPABILITIES, brand(), true);
|
||||
EngineResolver engineResolver = (String sessionId) -> {
|
||||
Engine engine = SESSION_ENGINES.get(sessionId);
|
||||
return engine == null || engine.isClosed() ? null : engine;
|
||||
};
|
||||
protocol.setEngineResolver(engineResolver);
|
||||
protocol.setVisionTileHandler(IrisVisionRequestService.create(engineResolver, sessionRegistry));
|
||||
registry = sessionRegistry;
|
||||
transport = serverTransport;
|
||||
protocolServer = protocol;
|
||||
IrisServices.register(IrisProtocolServer.class, protocol);
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
sessionRegistry.register(new IrisSession(player.getUUID().toString(), serverTransport));
|
||||
}
|
||||
}
|
||||
|
||||
public static void stop() {
|
||||
IrisServices.remove(IrisProtocolServer.class);
|
||||
IrisSessionRegistry current = registry;
|
||||
if (current != null) {
|
||||
for (IrisSession session : current.all()) {
|
||||
current.unregister(session.id());
|
||||
}
|
||||
}
|
||||
SESSION_ENGINES.clear();
|
||||
SESSION_LEVELS.clear();
|
||||
registry = null;
|
||||
protocolServer = null;
|
||||
transport = null;
|
||||
}
|
||||
|
||||
public static void onPlayerJoin(ServerPlayer player) {
|
||||
IrisSessionRegistry current = registry;
|
||||
ModdedProtocolTransport currentTransport = transport;
|
||||
if (player == null || current == null || currentTransport == null) {
|
||||
return;
|
||||
}
|
||||
current.register(new IrisSession(player.getUUID().toString(), currentTransport));
|
||||
}
|
||||
|
||||
public static void onPlayerDisconnect(ServerPlayer player) {
|
||||
IrisSessionRegistry current = registry;
|
||||
if (player == null || current == null) {
|
||||
return;
|
||||
}
|
||||
String sessionId = player.getUUID().toString();
|
||||
current.unregister(sessionId);
|
||||
SESSION_ENGINES.remove(sessionId);
|
||||
SESSION_LEVELS.remove(sessionId);
|
||||
}
|
||||
|
||||
public static void onInbound(ServerPlayer player, byte[] frame) {
|
||||
IrisProtocolServer current = protocolServer;
|
||||
if (player == null || frame == null || current == null) {
|
||||
return;
|
||||
}
|
||||
String sessionId = player.getUUID().toString();
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
current.onClientFrame(sessionId, frame);
|
||||
return;
|
||||
}
|
||||
scheduler.global(() -> current.onClientFrame(sessionId, frame));
|
||||
}
|
||||
|
||||
public static void tickDimensionSync(MinecraftServer server) {
|
||||
IrisSessionRegistry current = registry;
|
||||
IrisProtocolServer protocol = protocolServer;
|
||||
if (server == null || current == null || protocol == null) {
|
||||
return;
|
||||
}
|
||||
dimensionSyncTicks++;
|
||||
if (dimensionSyncTicks < DIMENSION_SYNC_INTERVAL_TICKS) {
|
||||
return;
|
||||
}
|
||||
dimensionSyncTicks = 0;
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
String sessionId = player.getUUID().toString();
|
||||
IrisSession session = current.get(sessionId);
|
||||
if (session == null || !session.isReady()) {
|
||||
continue;
|
||||
}
|
||||
ServerLevel level = player.level();
|
||||
String levelId = level.dimension().identifier().toString();
|
||||
Engine cached = SESSION_ENGINES.get(sessionId);
|
||||
if (levelId.equals(SESSION_LEVELS.get(sessionId)) && (cached == null || !cached.isClosed())) {
|
||||
continue;
|
||||
}
|
||||
if (syncDimension(protocol, sessionId, level, levelId)) {
|
||||
SESSION_LEVELS.put(sessionId, levelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean syncDimension(IrisProtocolServer protocol, String sessionId, ServerLevel level, String levelId) {
|
||||
ChunkGenerator generator = level.getChunkSource().getGenerator();
|
||||
Engine engine = generator instanceof IrisModdedChunkGenerator irisGenerator ? resolveEngine(level, irisGenerator) : null;
|
||||
if (generator instanceof IrisModdedChunkGenerator && engine == null) {
|
||||
return false;
|
||||
}
|
||||
long seed = level.getSeed();
|
||||
if (engine != null) {
|
||||
SESSION_ENGINES.put(sessionId, engine);
|
||||
protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(),
|
||||
seed, engine.getMinHeight(), engine.getMaxHeight(), true);
|
||||
return true;
|
||||
}
|
||||
SESSION_ENGINES.remove(sessionId);
|
||||
protocol.sendDimensionStatus(sessionId, levelId, "", seed, level.getMinY(), level.getMinY() + level.getHeight(), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Engine resolveEngine(ServerLevel level, IrisModdedChunkGenerator generator) {
|
||||
try {
|
||||
return generator.commandEngine();
|
||||
} catch (Throwable failure) {
|
||||
LOGGER.error("Iris dimension status engine lookup failed for {}", level.dimension().identifier(), failure);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String brand() {
|
||||
return "Iris " + ModdedEngineBootstrap.loader().modVersion() + " (" + ModdedEngineBootstrap.loader().platformName() + ")";
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.protocol.IrisServerTransport;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class ModdedProtocolTransport implements IrisServerTransport {
|
||||
private final MinecraftServer server;
|
||||
private final ModdedProtocolChannel channel;
|
||||
|
||||
public ModdedProtocolTransport(MinecraftServer server, ModdedProtocolChannel channel) {
|
||||
this.server = Objects.requireNonNull(server, "server");
|
||||
this.channel = Objects.requireNonNull(channel, "protocol channel");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToClient(String sessionId, byte[] frame) {
|
||||
if (sessionId == null || frame == null) {
|
||||
return;
|
||||
}
|
||||
UUID playerId = parseUuid(sessionId);
|
||||
if (playerId == null) {
|
||||
return;
|
||||
}
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
return;
|
||||
}
|
||||
ModdedIrisPayload payload = new ModdedIrisPayload(frame);
|
||||
scheduler.global(() -> deliver(playerId, payload));
|
||||
}
|
||||
|
||||
private void deliver(UUID playerId, ModdedIrisPayload payload) {
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
|
||||
if (player == null || !channel.canReceive(player)) {
|
||||
return;
|
||||
}
|
||||
channel.send(player, payload);
|
||||
}
|
||||
|
||||
private static UUID parseUuid(String sessionId) {
|
||||
try {
|
||||
return UUID.fromString(sessionId);
|
||||
} catch (IllegalArgumentException invalid) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformEntityType;
|
||||
import art.arcane.iris.spi.PlatformItem;
|
||||
@@ -30,12 +31,17 @@ import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.enchantment.Enchantment;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class ModdedRegistries implements PlatformRegistries {
|
||||
@@ -88,10 +94,7 @@ public final class ModdedRegistries implements PlatformRegistries {
|
||||
@Override
|
||||
public PlatformItem item(String key) {
|
||||
String normalized = key.trim().toLowerCase(Locale.ROOT).replace(' ', '_');
|
||||
if (normalized.startsWith("minecraft:")) {
|
||||
normalized = normalized.substring("minecraft:".length());
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse("minecraft:" + normalized);
|
||||
Identifier identifier = Identifier.tryParse(normalized.indexOf(':') >= 0 ? normalized : "minecraft:" + normalized);
|
||||
if (identifier == null || !BuiltInRegistries.ITEM.containsKey(identifier)) {
|
||||
return null;
|
||||
}
|
||||
@@ -144,6 +147,69 @@ public final class ModdedRegistries implements PlatformRegistries {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> itemKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (Identifier identifier : BuiltInRegistries.ITEM.keySet()) {
|
||||
keys.add(identifier.toString());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> entityKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (Identifier identifier : BuiltInRegistries.ENTITY_TYPE.keySet()) {
|
||||
keys.add(identifier.toString());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> blockTypeKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (Identifier identifier : BuiltInRegistries.BLOCK.keySet()) {
|
||||
keys.add(identifier.toString());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> enchantmentKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
Registry<Enchantment> registry = enchantmentRegistry();
|
||||
if (registry == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Identifier identifier : registry.keySet()) {
|
||||
keys.add(identifier.toString());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> potionEffectKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (Identifier identifier : BuiltInRegistries.MOB_EFFECT.keySet()) {
|
||||
keys.add(identifier.toString());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
|
||||
Map<String, List<PlatformBlockProperty>> properties = new LinkedHashMap<>();
|
||||
for (Block block : BuiltInRegistries.BLOCK) {
|
||||
BlockState defaultState = block.defaultBlockState();
|
||||
List<PlatformBlockProperty> converted = new ArrayList<>();
|
||||
for (Property<?> property : block.getStateDefinition().getProperties()) {
|
||||
converted.add(convertProperty(property, defaultState));
|
||||
}
|
||||
properties.put(BuiltInRegistries.BLOCK.getKey(block).toString(), List.copyOf(converted));
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
private Registry<Biome> biomeRegistry() {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
@@ -151,4 +217,29 @@ public final class ModdedRegistries implements PlatformRegistries {
|
||||
}
|
||||
return instance.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
}
|
||||
|
||||
private Registry<Enchantment> enchantmentRegistry() {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
return null;
|
||||
}
|
||||
return instance.registryAccess().lookupOrThrow(Registries.ENCHANTMENT);
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>> PlatformBlockProperty convertProperty(Property<T> property, BlockState defaultState) {
|
||||
T defaultValue = defaultState.getValue(property);
|
||||
Class<T> valueClass = property.getValueClass();
|
||||
List<Object> allowedValues = new ArrayList<>();
|
||||
if (valueClass == Boolean.class || valueClass == Integer.class) {
|
||||
for (T value : property.getPossibleValues()) {
|
||||
allowedValues.add(value);
|
||||
}
|
||||
String jsonType = valueClass == Boolean.class ? "boolean" : "integer";
|
||||
return new PlatformBlockProperty(property.getName(), jsonType, defaultValue, List.copyOf(allowedValues), null);
|
||||
}
|
||||
for (T value : property.getPossibleValues()) {
|
||||
allowedValues.add(property.getName(value));
|
||||
}
|
||||
return new PlatformBlockProperty(property.getName(), "string", property.getName(defaultValue), List.copyOf(allowedValues), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,17 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
|
||||
private static volatile Thread mainThread;
|
||||
|
||||
private final ThreadPoolExecutor asyncExecutor;
|
||||
private volatile ThreadPoolExecutor asyncExecutor;
|
||||
private final ConcurrentLinkedQueue<Runnable> mainQueue = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentLinkedQueue<DelayedTask> delayedQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public ModdedScheduler() {
|
||||
this.asyncExecutor = createAsyncExecutor();
|
||||
}
|
||||
|
||||
private static ThreadPoolExecutor createAsyncExecutor() {
|
||||
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(ASYNC_QUEUE_CAPACITY);
|
||||
this.asyncExecutor = new ThreadPoolExecutor(
|
||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||
ASYNC_CORE_THREADS,
|
||||
ASYNC_MAX_THREADS,
|
||||
ASYNC_KEEP_ALIVE_SECONDS,
|
||||
@@ -57,7 +61,8 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
workQueue,
|
||||
new AsyncThreadFactory(),
|
||||
new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
this.asyncExecutor.allowCoreThreadTimeOut(true);
|
||||
executor.allowCoreThreadTimeOut(true);
|
||||
return executor;
|
||||
}
|
||||
|
||||
public static void tick(MinecraftServer server) {
|
||||
@@ -115,7 +120,18 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
asyncExecutor.getQueue().clear();
|
||||
if (asyncExecutor.isShutdown()) {
|
||||
asyncExecutor = createAsyncExecutor();
|
||||
} else {
|
||||
asyncExecutor.getQueue().clear();
|
||||
}
|
||||
mainQueue.clear();
|
||||
delayedQueue.clear();
|
||||
mainThread = null;
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
asyncExecutor.shutdownNow();
|
||||
mainQueue.clear();
|
||||
delayedQueue.clear();
|
||||
mainThread = null;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.BuildConstants;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.json.JSONException;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import io.sentry.Sentry;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedSentry {
|
||||
private static final String DSN = "http://4cdbb9ac953306529947f4ca1e8e6b26@sentry.volmit.com:8080/2";
|
||||
private static final long FLUSH_TIMEOUT_MS = 2000L;
|
||||
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
||||
|
||||
private ModdedSentry() {
|
||||
}
|
||||
|
||||
public static void start(ModdedLoader loader) {
|
||||
IrisSettings.IrisSettingsSentry settings = IrisSettings.get().getSentry();
|
||||
if (settings.disableAutoReporting || Sentry.isEnabled() || Boolean.getBoolean("iris.suppressReporting")) {
|
||||
return;
|
||||
}
|
||||
if (!STARTED.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.info("Enabling Sentry for anonymous error reporting. You can disable this in the settings.");
|
||||
Sentry.init(options -> {
|
||||
options.setDsn(DSN);
|
||||
if (settings.debug) {
|
||||
options.setDebug(true);
|
||||
}
|
||||
options.setAttachServerName(false);
|
||||
options.setEnableUncaughtExceptionHandler(false);
|
||||
options.setRelease(loader.modVersion());
|
||||
options.setEnvironment(BuildConstants.ENVIRONMENT);
|
||||
options.setBeforeSend((event, hint) -> suppress(event.getThrowable()) ? null : event);
|
||||
});
|
||||
Sentry.configureScope(scope -> {
|
||||
scope.setTag("server", loader.platformName());
|
||||
scope.setTag("server.mc", loader.minecraftVersion());
|
||||
scope.setTag("iris.loader", loader.platformName());
|
||||
scope.setTag("iris.commit", BuildConstants.COMMIT);
|
||||
});
|
||||
ModdedPlatform.captureSink(Sentry::captureException);
|
||||
}
|
||||
|
||||
public static void flush() {
|
||||
if (Sentry.isEnabled()) {
|
||||
Sentry.flush(FLUSH_TIMEOUT_MS);
|
||||
Sentry.close();
|
||||
}
|
||||
ModdedPlatform.captureSink(null);
|
||||
STARTED.set(false);
|
||||
}
|
||||
|
||||
private static boolean suppress(Throwable error) {
|
||||
return (error instanceof IllegalStateException state && "zip file closed".equals(state.getMessage()))
|
||||
|| error instanceof JSONException
|
||||
|| error instanceof JsonSyntaxException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.spi.IrisLogging;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
public final class ModdedServerCommands {
|
||||
private ModdedServerCommands() {
|
||||
}
|
||||
|
||||
public static boolean dispatch(MinecraftServer server, String command) {
|
||||
if (server == null || command == null || command.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String prepared = command.startsWith("/") ? command.substring(1) : command;
|
||||
try {
|
||||
server.getCommands().performPrefixedCommand(server.createCommandSourceStack(), prepared);
|
||||
return true;
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.reportError(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.project.SchemaBuilder;
|
||||
import art.arcane.iris.engine.object.annotations.Snippet;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public final class ModdedWorkspaceGenerator {
|
||||
private ModdedWorkspaceGenerator() {
|
||||
}
|
||||
|
||||
public static File writeWorkspace(IrisData data, File folder) throws IOException {
|
||||
JSONObject workspaceConfig = buildWorkspace(data);
|
||||
File workspace = new File(folder, folder.getName() + ".code-workspace");
|
||||
IO.writeAll(workspace, workspaceConfig.toString(4));
|
||||
return workspace;
|
||||
}
|
||||
|
||||
public static JSONObject buildWorkspace(IrisData data) {
|
||||
JSONObject ws = new JSONObject();
|
||||
JSONArray folders = new JSONArray();
|
||||
JSONObject folder = new JSONObject();
|
||||
folder.put("path", ".");
|
||||
folders.put(folder);
|
||||
ws.put("folders", folders);
|
||||
JSONObject settings = new JSONObject();
|
||||
settings.put("workbench.colorTheme", "Monokai");
|
||||
settings.put("workbench.preferredDarkColorTheme", "Solarized Dark");
|
||||
settings.put("workbench.tips.enabled", false);
|
||||
settings.put("workbench.tree.indent", 24);
|
||||
settings.put("files.autoSave", "onFocusChange");
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("editor.autoIndent", "brackets");
|
||||
json.put("editor.acceptSuggestionOnEnter", "smart");
|
||||
json.put("editor.cursorSmoothCaretAnimation", true);
|
||||
json.put("editor.dragAndDrop", false);
|
||||
json.put("files.trimTrailingWhitespace", true);
|
||||
json.put("diffEditor.ignoreTrimWhitespace", true);
|
||||
json.put("files.trimFinalNewlines", true);
|
||||
json.put("editor.suggest.showKeywords", false);
|
||||
json.put("editor.suggest.showSnippets", false);
|
||||
json.put("editor.suggest.showWords", false);
|
||||
JSONObject quick = new JSONObject();
|
||||
quick.put("strings", true);
|
||||
json.put("editor.quickSuggestions", quick);
|
||||
json.put("editor.suggest.insertMode", "replace");
|
||||
settings.put("[json]", json);
|
||||
settings.put("json.maxItemsComputed", 30000);
|
||||
settings.put("json.schemas", buildSchemas(data));
|
||||
ws.put("settings", settings);
|
||||
return ws;
|
||||
}
|
||||
|
||||
private static JSONArray buildSchemas(IrisData data) {
|
||||
JSONArray schemas = new JSONArray();
|
||||
|
||||
for (ResourceLoader<?> loader : data.getLoaders().v()) {
|
||||
if (loader.supportsSchemas()) {
|
||||
schemas.put(loader.buildSchema());
|
||||
}
|
||||
}
|
||||
|
||||
for (Class<?> snippetClass : data.resolveSnippets()) {
|
||||
try {
|
||||
String snipType = snippetClass.getDeclaredAnnotation(Snippet.class).value();
|
||||
JSONObject entry = new JSONObject();
|
||||
KList<String> fileMatch = new KList<>();
|
||||
|
||||
for (int depth = 1; depth < 8; depth++) {
|
||||
fileMatch.add("/snippet/" + snipType + Form.repeat("/*", depth) + ".json");
|
||||
}
|
||||
|
||||
entry.put("fileMatch", new JSONArray(fileMatch.toArray()));
|
||||
entry.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json");
|
||||
schemas.put(entry);
|
||||
File schemaFile = new File(data.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json");
|
||||
J.attemptAsync(() -> {
|
||||
try {
|
||||
IO.writeAll(schemaFile, new SchemaBuilder(snippetClass, data).construct().toString(4));
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return schemas;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
/*
|
||||
* 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.IrisSettings;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||
import art.arcane.iris.engine.object.IRare;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisEntitySpawn;
|
||||
import art.arcane.iris.engine.object.IrisMarker;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRange;
|
||||
import art.arcane.iris.engine.object.IrisRate;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisSpawnGroup;
|
||||
import art.arcane.iris.engine.object.IrisSpawner;
|
||||
import art.arcane.iris.engine.object.IrisSurface;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public final class ModdedWorldManager implements EngineWorldManager {
|
||||
private static final ConcurrentHashMap<Engine, Queue<Long>> INITIAL_QUEUES = new ConcurrentHashMap<>();
|
||||
private static final int MAX_INITIAL_QUEUE = 8192;
|
||||
private static final int MAX_INITIAL_DRAIN_PER_TICK = 8;
|
||||
private static final long COUNT_INTERVAL_MS = 3_000L;
|
||||
private static final int ENTITY_SCAN_RADIUS = 64;
|
||||
private static final int PLAYER_CHUNK_RADIUS = 4;
|
||||
private static final int MIN_TICK_INTERVAL_MS = 1_000;
|
||||
|
||||
private final Engine engine;
|
||||
private long lastAmbientAt;
|
||||
private long lastCountAt;
|
||||
private volatile int cachedEntityCount;
|
||||
private volatile int cachedConsideredChunks;
|
||||
private volatile double cachedSaturation;
|
||||
|
||||
public ModdedWorldManager(Engine engine) {
|
||||
this.engine = engine;
|
||||
INITIAL_QUEUES.putIfAbsent(engine, new ConcurrentLinkedQueue<>());
|
||||
}
|
||||
|
||||
public static void enqueueGenerated(Engine engine, int chunkX, int chunkZ) {
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
Queue<Long> queue = INITIAL_QUEUES.get(engine);
|
||||
if (queue == null || queue.size() >= MAX_INITIAL_QUEUE) {
|
||||
return;
|
||||
}
|
||||
queue.add(pack(chunkX, chunkZ));
|
||||
}
|
||||
|
||||
public void serverTick(ServerLevel level) {
|
||||
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
return;
|
||||
}
|
||||
if (isPregenActive()) {
|
||||
return;
|
||||
}
|
||||
drainInitialSpawns(level);
|
||||
ambientTick(level);
|
||||
}
|
||||
|
||||
private void drainInitialSpawns(ServerLevel level) {
|
||||
Queue<Long> queue = INITIAL_QUEUES.get(engine);
|
||||
if (queue == null || queue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
||||
queue.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
int budget = MAX_INITIAL_DRAIN_PER_TICK;
|
||||
while (budget-- > 0) {
|
||||
Long key = queue.poll();
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
int chunkX = unpackX(key);
|
||||
int chunkZ = unpackZ(key);
|
||||
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
initialSpawnChunk(level, chunkX, chunkZ);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isChunkLoaded(chunkX, chunkZ) || mantle.hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
|
||||
try {
|
||||
chunk.raiseFlagUnchecked(MantleFlag.INITIAL_SPAWNED, () -> {
|
||||
if (markerSystemEnabled()) {
|
||||
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, true);
|
||||
}
|
||||
if (ambientSystemEnabled()) {
|
||||
spawnAmbient(level, chunkX, chunkZ, true);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void ambientTick(ServerLevel level) {
|
||||
long now = System.currentTimeMillis();
|
||||
long interval = Math.max((long) MIN_TICK_INTERVAL_MS, IrisSettings.get().getWorld().getAsyncTickIntervalMS());
|
||||
if (now - lastAmbientAt < interval) {
|
||||
return;
|
||||
}
|
||||
lastAmbientAt = now;
|
||||
|
||||
if (level.players().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshEntityCount(level, now);
|
||||
if (cachedSaturation > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long[] candidates = loadedChunksNearPlayers(level);
|
||||
if (candidates.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int spawnBuffer = RNG.r.i(2, 12);
|
||||
while (spawnBuffer-- > 0) {
|
||||
long key = candidates[RNG.r.nextInt(candidates.length)];
|
||||
try {
|
||||
ambientSpawnChunk(level, unpackX(key), unpackZ(key));
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ambientSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
|
||||
try {
|
||||
if (markerSystemEnabled()) {
|
||||
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, false);
|
||||
}
|
||||
if (ambientSystemEnabled()) {
|
||||
spawnAmbient(level, chunkX, chunkZ, false);
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnMarkerSpawners(ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk, boolean initial) {
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
chunk.iterate(MatterMarker.class, (Integer x, Integer yf, Integer z, MatterMarker marker) -> {
|
||||
String tag = marker.getTag();
|
||||
if (tag.equals("cave_floor") || tag.equals("cave_ceiling")) {
|
||||
return;
|
||||
}
|
||||
IrisMarker resolved = engine.getData().getMarkerLoader().load(tag);
|
||||
if (resolved == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int worldX = (chunkX << 4) + (x & 15);
|
||||
int worldZ = (chunkZ << 4) + (z & 15);
|
||||
int worldY = yf + minHeight;
|
||||
|
||||
if (resolved.isEmptyAbove() && aboveObstructed(level, worldX, worldY, worldZ)) {
|
||||
return;
|
||||
}
|
||||
|
||||
KList<IrisSpawner> spawners = resolveMarkerSpawners(resolved);
|
||||
if (spawners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
IrisSpawner chosen = spawners.getRandom();
|
||||
if (chosen == null) {
|
||||
return;
|
||||
}
|
||||
spawnFromSpawner(level, new IrisPosition(worldX, worldY, worldZ), chosen, initial);
|
||||
});
|
||||
}
|
||||
|
||||
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker) {
|
||||
KList<IrisSpawner> spawners = new KList<>();
|
||||
for (String key : marker.getSpawners()) {
|
||||
IrisSpawner spawner = engine.getData().getSpawnerLoader().load(key);
|
||||
if (spawner == null) {
|
||||
IrisLogging.error("Cannot load spawner: " + key + " for marker on " + engine.getName());
|
||||
continue;
|
||||
}
|
||||
spawner.setReferenceMarker(marker);
|
||||
spawners.add(spawner);
|
||||
}
|
||||
return spawners;
|
||||
}
|
||||
|
||||
private void spawnFromSpawner(ServerLevel level, IrisPosition position, IrisSpawner spawner, boolean initial) {
|
||||
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
||||
if (spawns.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (IrisEntitySpawn entry : spawns) {
|
||||
entry.setReferenceSpawner(spawner);
|
||||
entry.setReferenceMarker(spawner.getReferenceMarker());
|
||||
}
|
||||
IrisEntitySpawn chosen = rarityPick(spawns);
|
||||
if (chosen == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int chunkX = position.getX() >> 4;
|
||||
int chunkZ = position.getZ() >> 4;
|
||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
int spawned = spawnEntryAt(level, chosen, spawner, position);
|
||||
if (spawned > 0) {
|
||||
spawner.spawn(engine, chunkX, chunkZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnAmbient(ServerLevel level, int chunkX, int chunkZ, boolean initial) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
if (complex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int blockX = chunkX << 4;
|
||||
int blockZ = chunkZ << 4;
|
||||
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
|
||||
IrisRegion region = engine.getRegion(blockX, blockZ);
|
||||
int chunkMobs = countChunkMobs(level, chunkX, chunkZ);
|
||||
|
||||
KList<IrisEntitySpawn> pool = new KList<>();
|
||||
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(engine.getDimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
|
||||
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(region.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
|
||||
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
|
||||
if (pool.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisEntitySpawn chosen = rarityPick(pool);
|
||||
if (chosen == null || chosen.getReferenceSpawner() == null) {
|
||||
return;
|
||||
}
|
||||
IrisSpawner spawner = chosen.getReferenceSpawner();
|
||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
int spawned = spawnEntry(level, chosen, spawner, chunkX, chunkZ);
|
||||
if (spawned > 0) {
|
||||
spawner.spawn(engine, chunkX, chunkZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectSpawns(KList<IrisEntitySpawn> pool, KList<IrisSpawner> spawners, IrisBiome biomeFilter, int chunkX, int chunkZ, int chunkMobs, boolean initial) {
|
||||
for (IrisSpawner spawner : spawners) {
|
||||
if (spawner == null) {
|
||||
continue;
|
||||
}
|
||||
if (spawner.getMaxEntitiesPerChunk() <= chunkMobs) {
|
||||
continue;
|
||||
}
|
||||
if (biomeFilter != null && !spawner.isValid(biomeFilter)) {
|
||||
continue;
|
||||
}
|
||||
if (!canSpawn(spawner, chunkX, chunkZ)) {
|
||||
continue;
|
||||
}
|
||||
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
|
||||
for (IrisEntitySpawn entry : spawns) {
|
||||
entry.setReferenceSpawner(spawner);
|
||||
entry.setReferenceMarker(spawner.getReferenceMarker());
|
||||
pool.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int spawnEntry(ServerLevel level, IrisEntitySpawn entry, IrisSpawner spawner, int chunkX, int chunkZ) {
|
||||
IrisEntity irisEntity = entry.getRealEntity(engine);
|
||||
if (irisEntity == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int min = entry.getMinSpawns();
|
||||
int max = entry.getMaxSpawns();
|
||||
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
|
||||
if (count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
RNG entityRng = new RNG(engine.getSeedManager().getEntity());
|
||||
IrisSpawnGroup group = spawner.getGroup();
|
||||
int spawned = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int worldX = (chunkX << 4) + RNG.r.i(15);
|
||||
int worldZ = (chunkZ << 4) + RNG.r.i(15);
|
||||
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false);
|
||||
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true);
|
||||
int worldY = switch (group) {
|
||||
case NORMAL -> surfaceY + 1;
|
||||
case CAVE -> solidY + 1;
|
||||
case UNDERWATER, BEACH -> surfaceY > solidY + 1 ? RNG.r.i(solidY + 1, surfaceY) : surfaceY;
|
||||
};
|
||||
if (worldY <= level.getMinY() || worldY >= level.getMaxY()) {
|
||||
continue;
|
||||
}
|
||||
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
||||
continue;
|
||||
}
|
||||
if (!surfaceMatches(irisEntity.getSurface(), level, worldX, worldY, worldZ)) {
|
||||
continue;
|
||||
}
|
||||
if (!clearForSpawn(level, worldX, worldY, worldZ)) {
|
||||
continue;
|
||||
}
|
||||
if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) {
|
||||
spawned++;
|
||||
}
|
||||
}
|
||||
return spawned;
|
||||
}
|
||||
|
||||
private int spawnEntryAt(ServerLevel level, IrisEntitySpawn entry, IrisSpawner spawner, IrisPosition position) {
|
||||
IrisEntity irisEntity = entry.getRealEntity(engine);
|
||||
if (irisEntity == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int min = entry.getMinSpawns();
|
||||
int max = entry.getMaxSpawns();
|
||||
int count = min == max ? min : RNG.r.i(Math.min(min, max), Math.max(min, max));
|
||||
if (count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
exhaustMarker(spawner, position);
|
||||
|
||||
RNG entityRng = new RNG(engine.getSeedManager().getEntity());
|
||||
int worldX = position.getX();
|
||||
int worldY = position.getY() + 1;
|
||||
int worldZ = position.getZ();
|
||||
int spawned = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (!lightAllowed(spawner, level, worldX, worldY, worldZ)) {
|
||||
continue;
|
||||
}
|
||||
if (ModdedEntitySpawner.spawn(engine, irisEntity, level, worldX, worldY, worldZ, entityRng) != null) {
|
||||
spawned++;
|
||||
}
|
||||
}
|
||||
return spawned;
|
||||
}
|
||||
|
||||
private void exhaustMarker(IrisSpawner spawner, IrisPosition position) {
|
||||
IrisMarker marker = spawner.getReferenceMarker();
|
||||
if (marker == null || !marker.shouldExhaust()) {
|
||||
return;
|
||||
}
|
||||
engine.getMantle().getMantle().remove(position.getX(), position.getY() - engine.getWorld().minHeight(), position.getZ(), MatterMarker.class);
|
||||
}
|
||||
|
||||
private boolean canSpawn(IrisSpawner spawner, int chunkX, int chunkZ) {
|
||||
IrisRate rate = spawner.getMaximumRate();
|
||||
if (!rate.isInfinite() && !engine.getEngineData().getCooldown(spawner).canSpawn(rate)) {
|
||||
return false;
|
||||
}
|
||||
IrisRate chunkRate = spawner.getMaximumRatePerChunk();
|
||||
return chunkRate.isInfinite() || engine.getEngineData().getChunk(chunkX, chunkZ).getCooldown(spawner).canSpawn(chunkRate);
|
||||
}
|
||||
|
||||
private boolean lightAllowed(IrisSpawner spawner, ServerLevel level, int worldX, int worldY, int worldZ) {
|
||||
IrisRange range = spawner.getAllowedLightLevels();
|
||||
if (range.getMin() > 0 || range.getMax() < 15) {
|
||||
return range.contains(level.getMaxLocalRawBrightness(new BlockPos(worldX, worldY, worldZ)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean surfaceMatches(IrisSurface surface, ServerLevel level, int worldX, int worldY, int worldZ) {
|
||||
BlockState below = level.getBlockState(new BlockPos(worldX, worldY - 1, worldZ));
|
||||
return matchesSurface(surface, below);
|
||||
}
|
||||
|
||||
private static boolean matchesSurface(IrisSurface surface, BlockState below) {
|
||||
if (ModdedBlockResolution.isSolid(below)) {
|
||||
return surface == IrisSurface.LAND || surface == IrisSurface.OVERWORLD
|
||||
|| (surface == IrisSurface.ANIMAL && isAnimalGround(below));
|
||||
}
|
||||
if (below.is(Blocks.LAVA)) {
|
||||
return surface == IrisSurface.LAVA;
|
||||
}
|
||||
if (ModdedBlockResolution.isWater(below) || ModdedBlockResolution.isWaterLogged(below) || isAquaticFoliage(below)) {
|
||||
return surface == IrisSurface.WATER || surface == IrisSurface.OVERWORLD;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isAnimalGround(BlockState state) {
|
||||
return state.is(Blocks.GRASS_BLOCK) || state.is(Blocks.DIRT) || state.is(Blocks.DIRT_PATH)
|
||||
|| state.is(Blocks.COARSE_DIRT) || state.is(Blocks.ROOTED_DIRT) || state.is(Blocks.PODZOL)
|
||||
|| state.is(Blocks.MYCELIUM) || state.is(Blocks.SNOW_BLOCK);
|
||||
}
|
||||
|
||||
private static boolean isAquaticFoliage(BlockState state) {
|
||||
return state.is(Blocks.SEAGRASS) || state.is(Blocks.TALL_SEAGRASS)
|
||||
|| state.is(Blocks.KELP) || state.is(Blocks.KELP_PLANT);
|
||||
}
|
||||
|
||||
private boolean clearForSpawn(ServerLevel level, int worldX, int worldY, int worldZ) {
|
||||
BlockState at = level.getBlockState(new BlockPos(worldX, worldY, worldZ));
|
||||
BlockState above = level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ));
|
||||
return !ModdedBlockResolution.isSolid(at) && !ModdedBlockResolution.isSolid(above);
|
||||
}
|
||||
|
||||
private boolean aboveObstructed(ServerLevel level, int worldX, int worldY, int worldZ) {
|
||||
return ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 1, worldZ)))
|
||||
|| ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(worldX, worldY + 2, worldZ)));
|
||||
}
|
||||
|
||||
private int countChunkMobs(ServerLevel level, int chunkX, int chunkZ) {
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
AABB box = new AABB(baseX, level.getMinY(), baseZ, baseX + 16, level.getMaxY(), baseZ + 16);
|
||||
return level.getEntities((Entity) null, box, (Entity e) -> e instanceof Mob && e.isAlive()).size();
|
||||
}
|
||||
|
||||
private void refreshEntityCount(ServerLevel level, long now) {
|
||||
if (now - lastCountAt < COUNT_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
lastCountAt = now;
|
||||
|
||||
int mobs = 0;
|
||||
int chunks = 0;
|
||||
for (ServerPlayer player : level.players()) {
|
||||
int px = player.blockPosition().getX();
|
||||
int pz = player.blockPosition().getZ();
|
||||
AABB box = new AABB(px - ENTITY_SCAN_RADIUS, level.getMinY(), pz - ENTITY_SCAN_RADIUS,
|
||||
px + ENTITY_SCAN_RADIUS, level.getMaxY(), pz + ENTITY_SCAN_RADIUS);
|
||||
mobs += level.getEntities((Entity) null, box, (Entity e) -> e instanceof Mob && e.isAlive()).size();
|
||||
chunks += (PLAYER_CHUNK_RADIUS * 2 + 1) * (PLAYER_CHUNK_RADIUS * 2 + 1);
|
||||
}
|
||||
|
||||
cachedEntityCount = mobs;
|
||||
cachedConsideredChunks = chunks;
|
||||
cachedSaturation = mobs / (chunks + 1.0) * 1.28;
|
||||
}
|
||||
|
||||
private long[] loadedChunksNearPlayers(ServerLevel level) {
|
||||
Set<Long> keys = new HashSet<>();
|
||||
for (ServerPlayer player : level.players()) {
|
||||
int centerX = player.blockPosition().getX() >> 4;
|
||||
int centerZ = player.blockPosition().getZ() >> 4;
|
||||
for (int dx = -PLAYER_CHUNK_RADIUS; dx <= PLAYER_CHUNK_RADIUS; dx++) {
|
||||
for (int dz = -PLAYER_CHUNK_RADIUS; dz <= PLAYER_CHUNK_RADIUS; dz++) {
|
||||
int chunkX = centerX + dx;
|
||||
int chunkZ = centerZ + dz;
|
||||
if (level.getChunkSource().getChunkNow(chunkX, chunkZ) != null) {
|
||||
keys.add(pack(chunkX, chunkZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long[] result = new long[keys.size()];
|
||||
int index = 0;
|
||||
for (long key : keys) {
|
||||
result[index++] = key;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isPregenActive() {
|
||||
PregeneratorJob job = PregeneratorJob.getInstance();
|
||||
return job != null && job.targetsWorldName(engine.getWorld().name());
|
||||
}
|
||||
|
||||
private static boolean markerSystemEnabled() {
|
||||
return IrisSettings.get().getWorld().isMarkerEntitySpawningSystem();
|
||||
}
|
||||
|
||||
private static boolean ambientSystemEnabled() {
|
||||
return IrisSettings.get().getWorld().isAmbientEntitySpawningSystem();
|
||||
}
|
||||
|
||||
private IrisEntitySpawn rarityPick(KList<IrisEntitySpawn> entries) {
|
||||
int totalRarity = 0;
|
||||
for (IrisEntitySpawn entry : entries) {
|
||||
totalRarity += IRare.get(entry);
|
||||
}
|
||||
if (totalRarity <= 0) {
|
||||
return entries.getRandom();
|
||||
}
|
||||
KList<IrisEntitySpawn> weighted = new KList<>();
|
||||
for (IrisEntitySpawn entry : entries) {
|
||||
weighted.addMultiple(entry, totalRarity / IRare.get(entry));
|
||||
}
|
||||
return weighted.getRandom();
|
||||
}
|
||||
|
||||
private static long pack(int x, int z) {
|
||||
return (((long) x) & 0xFFFFFFFFL) | ((((long) z) & 0xFFFFFFFFL) << 32);
|
||||
}
|
||||
|
||||
private static int unpackX(long key) {
|
||||
return (int) (key & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private static int unpackZ(long key) {
|
||||
return (int) ((key >> 32) & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
INITIAL_QUEUES.remove(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEntityCount() {
|
||||
return cachedEntityCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChunkCount() {
|
||||
return cachedConsideredChunks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getEntitySaturation() {
|
||||
return cachedSaturation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSave() {
|
||||
engine.getMantle().save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockBreak(BlockBreakEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockPlace(BlockPlaceEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkLoad(Chunk e, boolean generated) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkUnload(Chunk e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleportAsync(PlayerTeleportEvent e) {
|
||||
}
|
||||
}
|
||||
+2
@@ -194,6 +194,8 @@ public final class IrisModdedCommands {
|
||||
root.then(ModdedStructureCommands.tree("structure"));
|
||||
root.then(ModdedStructureCommands.tree("struct"));
|
||||
root.then(ModdedStructureCommands.tree("str"));
|
||||
root.then(ModdedDeveloperCommands.tree("developer"));
|
||||
root.then(ModdedDeveloperCommands.tree("dev"));
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
+7
-1
@@ -72,7 +72,8 @@ final class ModdedCommandHelp {
|
||||
Entry.group("world", "Runtime Iris dimension creation, removal and status", "w"),
|
||||
Entry.group("datapack", "World datapack install and status helpers", "datapacks", "dp"),
|
||||
Entry.group("structure", "Iris structure index, info and placement tools", "struct", "str"),
|
||||
Entry.command("goldenhash", "[radius] [threads] [capture|verify]", "Generate deterministic block hashes for parity testing", "gold")
|
||||
Entry.command("goldenhash", "[radius] [threads] [capture|verify]", "Generate deterministic block hashes for parity testing", "gold"),
|
||||
Entry.group("developer", "Developer diagnostics: Sentry test, network interfaces, region-file scan", "dev")
|
||||
));
|
||||
SECTIONS.put("find", List.of(
|
||||
Entry.command("biome", "<key>", "Find an Iris biome"),
|
||||
@@ -162,6 +163,11 @@ final class ModdedCommandHelp {
|
||||
));
|
||||
SECTIONS.put("struct", SECTIONS.get("structure"));
|
||||
SECTIONS.put("str", SECTIONS.get("structure"));
|
||||
SECTIONS.put("developer", List.of(
|
||||
Entry.command("sentry", "", "Send a test exception to the Iris error reporter"),
|
||||
Entry.command("network", "", "List network interfaces and their addresses", "ip")
|
||||
));
|
||||
SECTIONS.put("dev", SECTIONS.get("developer"));
|
||||
}
|
||||
|
||||
private ModdedCommandHelp() {
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.spi.IrisPlatforms;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
final class ModdedDeveloperCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||
|
||||
private ModdedDeveloperCommands() {
|
||||
}
|
||||
|
||||
static LiteralArgumentBuilder<CommandSourceStack> tree(String name) {
|
||||
return Commands.literal(name).requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), "developer"))
|
||||
.then(Commands.literal("sentry")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> sentry(context.getSource())))
|
||||
.then(Commands.literal("network")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> network(context.getSource())))
|
||||
.then(Commands.literal("ip")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> network(context.getSource())));
|
||||
}
|
||||
|
||||
private static int sentry(CommandSourceStack source) {
|
||||
IrisPlatforms.get().reportError(new Exception("This is an Iris Sentry test exception"));
|
||||
ModdedCommandFeedback.ok(source, "Dispatched a test exception to the Iris error reporter (Sentry if enabled).");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int network(CommandSourceStack source) {
|
||||
try {
|
||||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||
for (NetworkInterface networkInterface : Collections.list(interfaces)) {
|
||||
ModdedCommandFeedback.ok(source, networkInterface.getDisplayName());
|
||||
for (InetAddress address : Collections.list(networkInterface.getInetAddresses())) {
|
||||
ModdedCommandFeedback.ok(source, " " + address.getHostAddress());
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
} catch (SocketException error) {
|
||||
LOGGER.error("Iris developer network dump failed", error);
|
||||
ModdedCommandFeedback.fail(source, "Network scan failed: " + error.getClass().getSimpleName());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -1,6 +1,10 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.protocol.IrisProtocolServer;
|
||||
import art.arcane.iris.core.protocol.IrisSession;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.protocol.IrisProtocol;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
@@ -27,6 +31,9 @@ public final class ModdedPregenBossBar {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
if (hasClientPregenHud(player)) {
|
||||
return;
|
||||
}
|
||||
viewer = player.getUUID();
|
||||
bar = new ServerBossEvent(BAR_ID, Component.literal("Iris Pregen starting..."), BossEvent.BossBarColor.GREEN, BossEvent.BossBarOverlay.PROGRESS);
|
||||
bar.setProgress(0.0F);
|
||||
@@ -84,6 +91,15 @@ public final class ModdedPregenBossBar {
|
||||
return name;
|
||||
}
|
||||
|
||||
private static boolean hasClientPregenHud(ServerPlayer player) {
|
||||
IrisProtocolServer protocol = IrisServices.getOrNull(IrisProtocolServer.class);
|
||||
if (protocol == null) {
|
||||
return false;
|
||||
}
|
||||
IrisSession session = protocol.registry().get(player.getUUID().toString());
|
||||
return session != null && session.isReady() && session.hasCapability(IrisProtocol.CAPABILITY_PREGEN);
|
||||
}
|
||||
|
||||
private static double clamp01(double value) {
|
||||
if (value < 0.0D) {
|
||||
return 0.0D;
|
||||
|
||||
+12
-42
@@ -36,13 +36,13 @@ 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.modded.ModdedWorkspaceGenerator;
|
||||
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.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
@@ -262,15 +262,15 @@ public final class ModdedStudioCommands {
|
||||
if (folder == null) {
|
||||
return 0;
|
||||
}
|
||||
File workspace = new File(folder, folder.getName() + ".code-workspace");
|
||||
File workspace;
|
||||
try {
|
||||
IO.writeAll(workspace, workspaceConfig().toString(4));
|
||||
workspace = ModdedWorkspaceGenerator.writeWorkspace(IrisData.get(folder), folder);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris workspace write failed for {}", workspace, e);
|
||||
IrisModdedCommands.fail(source, "Failed to write " + workspace.getAbsolutePath() + ": " + e.getMessage());
|
||||
LOGGER.error("Iris workspace write failed for {}", folder, e);
|
||||
IrisModdedCommands.fail(source, "Failed to write workspace for " + folder.getAbsolutePath() + ": " + e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Workspace regenerated: " + workspace.getAbsolutePath() + " (JSON schemas require the Bukkit studio toolchain).");
|
||||
IrisModdedCommands.ok(source, "Workspace regenerated: " + workspace.getAbsolutePath() + " with JSON schemas for autocomplete.");
|
||||
if (!open) {
|
||||
return 1;
|
||||
}
|
||||
@@ -289,41 +289,6 @@ public final class ModdedStudioCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static JSONObject workspaceConfig() {
|
||||
JSONObject ws = new JSONObject();
|
||||
JSONArray folders = new JSONArray();
|
||||
JSONObject folder = new JSONObject();
|
||||
folder.put("path", ".");
|
||||
folders.put(folder);
|
||||
ws.put("folders", folders);
|
||||
JSONObject settings = new JSONObject();
|
||||
settings.put("workbench.colorTheme", "Monokai");
|
||||
settings.put("workbench.preferredDarkColorTheme", "Solarized Dark");
|
||||
settings.put("workbench.tips.enabled", false);
|
||||
settings.put("workbench.tree.indent", 24);
|
||||
settings.put("files.autoSave", "onFocusChange");
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("editor.autoIndent", "brackets");
|
||||
json.put("editor.acceptSuggestionOnEnter", "smart");
|
||||
json.put("editor.cursorSmoothCaretAnimation", true);
|
||||
json.put("editor.dragAndDrop", false);
|
||||
json.put("files.trimTrailingWhitespace", true);
|
||||
json.put("diffEditor.ignoreTrimWhitespace", true);
|
||||
json.put("files.trimFinalNewlines", true);
|
||||
json.put("editor.suggest.showKeywords", false);
|
||||
json.put("editor.suggest.showSnippets", false);
|
||||
json.put("editor.suggest.showWords", false);
|
||||
JSONObject quick = new JSONObject();
|
||||
quick.put("strings", true);
|
||||
json.put("editor.quickSuggestions", quick);
|
||||
json.put("editor.suggest.insertMode", "replace");
|
||||
settings.put("[json]", json);
|
||||
settings.put("json.maxItemsComputed", 30000);
|
||||
settings.put("json.schemas", new JSONArray());
|
||||
ws.put("settings", settings);
|
||||
return ws;
|
||||
}
|
||||
|
||||
private static String guiUnavailableMessage() {
|
||||
if (GuiHost.isDesktopSuppressed()) {
|
||||
return "Iris desktop GUIs are disabled in singleplayer/client to avoid crashing the game client; use the in-game map and chat output instead.";
|
||||
@@ -611,9 +576,14 @@ public final class ModdedStudioCommands {
|
||||
}
|
||||
}
|
||||
IrisProjectCopier.copyProject(templateFolder, target, template, name);
|
||||
try {
|
||||
ModdedWorkspaceGenerator.writeWorkspace(IrisData.get(target), target);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris studio create workspace generation failed for {}", name, e);
|
||||
}
|
||||
server.execute(() -> {
|
||||
IrisModdedCommands.ok(source, "Created project '" + name + "' at " + target.getAbsolutePath());
|
||||
IrisModdedCommands.ok(source, "Edit dimensions/" + name + ".json and the rest of the pack, then create a world with it. VSCode workspaces are generated by the Bukkit studio toolchain only.");
|
||||
IrisModdedCommands.ok(source, "Edit dimensions/" + name + ".json and the rest of the pack. A VSCode workspace with JSON schema autocomplete was generated; open it or rerun /iris studio vscode.");
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio create failed for {}", name, e);
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedWorldManager;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
public final class ModdedEntitySpawnService implements ModdedTickableService {
|
||||
@Override
|
||||
public void onEnable() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(MinecraftServer server) {
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
|
||||
continue;
|
||||
}
|
||||
Engine engine = generator.engineIfBound();
|
||||
if (engine == null || engine.isClosed()) {
|
||||
continue;
|
||||
}
|
||||
EngineWorldManager worldManager = engine.getWorldManager();
|
||||
if (!(worldManager instanceof ModdedWorldManager moddedWorldManager)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
moddedWorldManager.serverTick(level);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -19,9 +19,11 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
@@ -169,12 +171,22 @@ public final class ModdedStudioHotloadService implements ModdedTickableService {
|
||||
try {
|
||||
engine.hotloadSilently();
|
||||
generator.onHotload();
|
||||
regenerateSchemas(dimensionId, engine);
|
||||
LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio hotload failed for {}", dimensionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void regenerateSchemas(String dimensionId, Engine engine) {
|
||||
try {
|
||||
IrisData data = engine.getData();
|
||||
ModdedWorkspaceGenerator.writeWorkspace(data, data.getDataFolder());
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio schema regeneration failed for {}", dimensionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Watch {
|
||||
private final ChronoLatch latch = new ChronoLatch(CHECK_LATCH_MILLIS, false);
|
||||
private final AtomicBoolean busy = new AtomicBoolean(false);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"key.categories.irisworldgen.iris": "Iris",
|
||||
"key.irisworldgen.toggle_pregen_hud": "Toggle Pregen HUD"
|
||||
}
|
||||
Reference in New Issue
Block a user