d
This commit is contained in:
Brian Neumann-Fopiano committed 2026-06-13 17:07:27 -04:00
1 parent f482b8ef7e
commit ad6732812e
19 files changed
+1050 -337

No files matched your search

+4
View File
@@ -19,3 +19,7 @@ dist/
core/plugins/ core/plugins/
adapters/bukkit/plugin/plugins/ adapters/bukkit/plugin/plugins/
adapters/*/run/ adapters/*/run/
.codegraph/
plans/
@@ -21,10 +21,7 @@ package art.arcane.iris;
import art.arcane.iris.engine.IrisWorldManager; import art.arcane.iris.engine.IrisWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider; import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.core.splash.IrisSplashPackScanner;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.ServerConfigurator;
@@ -58,8 +55,10 @@ import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.core.safeguard.IrisSafeguard; import art.arcane.iris.core.safeguard.IrisSafeguard;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.LogLevel;
import art.arcane.volmlib.integration.ReloadAware; import art.arcane.volmlib.integration.ReloadAware;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
@@ -235,7 +234,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
getSender().sendMessage(string); getSender().sendMessage(string);
} catch (Throwable e) { } catch (Throwable e) {
try { try {
instance.getLogger().info(instance.getTag() + string.replaceAll("(<([^>]+)>)", "")); instance.getLogger().info(instance.getTag() + IrisLogging.clean(string));
} catch (Throwable inner) { } catch (Throwable inner) {
System.err.println("[Iris] Failed to emit log message: " + inner.getMessage()); System.err.println("[Iris] Failed to emit log message: " + inner.getMessage());
inner.printStackTrace(System.err); inner.printStackTrace(System.err);
@@ -377,19 +376,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
} }
private static String safeFormat(String format, Object... args) { private static String safeFormat(String format, Object... args) {
if (format == null) { return IrisLogging.format(format, args);
return "null";
}
if (args == null || args.length == 0) {
return format;
}
try {
return String.format(format, args);
} catch (IllegalFormatException ignored) {
return format;
}
} }
public static void later(NastyRunnable object) { public static void later(NastyRunnable object) {
@@ -555,14 +542,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
BukkitPlatform.hostPlugin(this); BukkitPlatform.hostPlugin(this);
BukkitPlatform.hostConsoleSender(Iris::getSender); BukkitPlatform.hostConsoleSender(Iris::getSender);
BukkitPlatform.hostBridge(new BukkitPlatform.HostBridge( BukkitPlatform.hostBridge(new BukkitPlatform.HostBridge(
(level, message) -> { Iris::bridgeLog,
switch (level) {
case DEBUG -> Iris.debug(message);
case INFO -> Iris.info(message);
case WARN -> Iris.warn(message);
case ERROR -> Iris.error(message);
}
},
Iris::msg, Iris::msg,
Iris::reportError, Iris::reportError,
(event) -> Iris.callEvent((org.bukkit.event.Event) event), (event) -> Iris.callEvent((org.bukkit.event.Event) event),
@@ -574,6 +554,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
SlimJar.load(); SlimJar.load();
} }
private static void bridgeLog(LogLevel level, String message) {
LogLevel target = level == null ? LogLevel.INFO : level;
switch (target) {
case DEBUG -> Iris.debug(message);
case INFO -> Iris.info(message);
case WARN -> Iris.warn(message);
case ERROR -> Iris.error(message);
}
}
private void enable() { private void enable() {
PaperLibBootstrap.install(); PaperLibBootstrap.install();
SimdSupport.install(); SimdSupport.install();
@@ -1279,49 +1269,24 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
} }
static List<SplashPackMetadata> collectSplashPacks(File packFolder) { static List<SplashPackMetadata> collectSplashPacks(File packFolder) {
if (packFolder == null || !packFolder.isDirectory()) { List<IrisSplashPackScanner.SplashPackMetadata> scanned = IrisSplashPackScanner.collect(packFolder, Iris::reportError);
if (scanned.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
File[] folders = packFolder.listFiles(File::isDirectory); List<SplashPackMetadata> packs = new ArrayList<>(scanned.size());
if (folders == null || folders.length == 0) { for (IrisSplashPackScanner.SplashPackMetadata metadata : scanned) {
return Collections.emptyList(); packs.add(new SplashPackMetadata(metadata.name(), metadata.version()));
} }
List<SplashPackMetadata> packs = new ArrayList<>();
for (File folder : folders) {
SplashPackMetadata metadata = readSplashPack(folder);
if (metadata != null) {
packs.add(metadata);
}
}
packs.sort(Comparator.comparing(SplashPackMetadata::name));
return packs; return packs;
} }
static SplashPackMetadata readSplashPack(File pack) { static SplashPackMetadata readSplashPack(File pack) {
if (pack == null || !pack.isDirectory()) { IrisSplashPackScanner.SplashPackMetadata metadata = IrisSplashPackScanner.read(pack, Iris::reportError);
return null; if (metadata == null) {
}
String dimName = pack.getName();
File dimensionFile = new File(pack, "dimensions/" + dimName + ".json");
if (!dimensionFile.isFile()) {
return null;
}
try (FileReader r = new FileReader(dimensionFile)) {
JsonObject json = JsonParser.parseReader(r).getAsJsonObject();
if (!json.has("version")) {
return null;
}
return new SplashPackMetadata(dimName, json.get("version").getAsString());
} catch (IOException | JsonParseException ex) {
reportError("Failed to read splash metadata for dimension pack \"" + dimName + "\".", ex);
return null; return null;
} }
return new SplashPackMetadata(metadata.name(), metadata.version());
} }
private void printPack(SplashPackMetadata pack) { private void printPack(SplashPackMetadata pack) {
@@ -20,6 +20,7 @@ package art.arcane.iris.fabric;
import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap; import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedParityProbe; import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck; import art.arcane.iris.modded.ModdedWorldCheck;
import art.arcane.iris.modded.ModdedWorldEngines; import art.arcane.iris.modded.ModdedWorldEngines;
@@ -49,12 +50,8 @@ import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.BlockHitResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class IrisFabricBootstrap implements ModInitializer { public final class IrisFabricBootstrap implements ModInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
@Override @Override
public void onInitialize() { public void onInitialize() {
ModdedEngineBootstrap.initialize(new FabricModdedLoader()); ModdedEngineBootstrap.initialize(new FabricModdedLoader());
@@ -65,12 +62,12 @@ public final class IrisFabricBootstrap implements ModInitializer {
String minecraftVersion = loader.getModContainer("minecraft") String minecraftVersion = loader.getModContainer("minecraft")
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString()) .map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
.orElse("unknown"); .orElse("unknown");
LOGGER.info("Iris {} bootstrapping on Minecraft {} (Fabric)", modVersion, minecraftVersion); ModdedIrisLog.info("Iris " + modVersion + " bootstrapping on Minecraft " + minecraftVersion + " (Fabric)");
ModdedEngineBootstrap.selfTest(IrisFabricBootstrap.class.getClassLoader()); ModdedEngineBootstrap.selfTest(IrisFabricBootstrap.class.getClassLoader());
ModdedEngineBootstrap.bind(); ModdedEngineBootstrap.bind();
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC); Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC);
LOGGER.info("Iris chunk generator registered as irisworldgen:iris"); ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> { ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> {
ModdedObjectUndo.clearAll(); ModdedObjectUndo.clearAll();
ModdedWandService.clearAll(); ModdedWandService.clearAll();
@@ -85,13 +82,13 @@ public final class IrisFabricBootstrap implements ModInitializer {
String parity = System.getProperty("iris.parity"); String parity = System.getProperty("iris.parity");
if (parity != null) { if (parity != null) {
LOGGER.info("Iris parity probe armed: {}", parity); ModdedIrisLog.info("Iris parity probe armed: " + parity);
ModdedParityProbe.schedule(parity); ModdedParityProbe.schedule(parity);
} }
String worldCheck = System.getProperty("iris.worldcheck"); String worldCheck = System.getProperty("iris.worldcheck");
if (worldCheck != null) { if (worldCheck != null) {
LOGGER.info("Iris world check armed"); ModdedIrisLog.info("Iris world check armed");
ModdedWorldCheck.schedule(); ModdedWorldCheck.schedule();
} }
} }
@@ -20,6 +20,7 @@ package art.arcane.iris.forge;
import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap; import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedParityProbe; import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck; import art.arcane.iris.modded.ModdedWorldCheck;
import art.arcane.iris.modded.ModdedWorldEngines; import art.arcane.iris.modded.ModdedWorldEngines;
@@ -42,20 +43,16 @@ import net.minecraftforge.fml.loading.VersionInfo;
import net.minecraftforge.registries.DeferredRegister; import net.minecraftforge.registries.DeferredRegister;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Mod("irisworldgen") @Mod("irisworldgen")
public final class IrisForgeBootstrap { public final class IrisForgeBootstrap {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
public IrisForgeBootstrap(FMLJavaModLoadingContext context) { public IrisForgeBootstrap(FMLJavaModLoadingContext context) {
ModdedEngineBootstrap.initialize(new ForgeModdedLoader()); ModdedEngineBootstrap.initialize(new ForgeModdedLoader());
String modVersion = ModList.getModContainerById("irisworldgen") String modVersion = ModList.getModContainerById("irisworldgen")
.map((ModContainer container) -> container.getModInfo().getVersion().toString()) .map((ModContainer container) -> container.getModInfo().getVersion().toString())
.orElse("unknown"); .orElse("unknown");
VersionInfo versionInfo = FMLLoader.versionInfo(); VersionInfo versionInfo = FMLLoader.versionInfo();
LOGGER.info("Iris {} bootstrapping on Minecraft {} (Forge {})", modVersion, versionInfo.mcVersion(), versionInfo.forgeVersion()); ModdedIrisLog.info("Iris " + modVersion + " bootstrapping on Minecraft " + versionInfo.mcVersion() + " (Forge " + versionInfo.forgeVersion() + ")");
ModdedEngineBootstrap.selfTest(IrisForgeBootstrap.class.getClassLoader()); ModdedEngineBootstrap.selfTest(IrisForgeBootstrap.class.getClassLoader());
ModdedEngineBootstrap.bind(); ModdedEngineBootstrap.bind();
@@ -63,7 +60,7 @@ public final class IrisForgeBootstrap {
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen"); DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC); chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
chunkGenerators.register(context.getModBusGroup()); chunkGenerators.register(context.getModBusGroup());
LOGGER.info("Iris chunk generator registered as irisworldgen:iris"); ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> { ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> {
ModdedObjectUndo.clearAll(); ModdedObjectUndo.clearAll();
@@ -79,13 +76,13 @@ public final class IrisForgeBootstrap {
String parity = System.getProperty("iris.parity"); String parity = System.getProperty("iris.parity");
if (parity != null) { if (parity != null) {
LOGGER.info("Iris parity probe armed: {}", parity); ModdedIrisLog.info("Iris parity probe armed: " + parity);
ModdedParityProbe.schedule(parity); ModdedParityProbe.schedule(parity);
} }
String worldCheck = System.getProperty("iris.worldcheck"); String worldCheck = System.getProperty("iris.worldcheck");
if (worldCheck != null) { if (worldCheck != null) {
LOGGER.info("Iris world check armed"); ModdedIrisLog.info("Iris world check armed");
ModdedWorldCheck.schedule(); ModdedWorldCheck.schedule();
} }
} }
@@ -82,7 +82,7 @@ public final class ModdedEngineBootstrap {
throw new IllegalStateException("Iris core self-test failed: only " + loadedClasses + " of " + CORE_SELF_TEST_CLASSES.length + " engine classes initialized"); throw new IllegalStateException("Iris core self-test failed: only " + loadedClasses + " of " + CORE_SELF_TEST_CLASSES.length + " engine classes initialized");
} }
LOGGER.info("Iris core loaded ({} classes ok)", loadedClasses); ModdedIrisLog.info("Iris core loaded (" + loadedClasses + " classes ok)");
} }
public static ModdedPlatform bind() { public static ModdedPlatform bind() {
@@ -105,6 +105,7 @@ public final class ModdedEngineBootstrap {
IrisServices.register(PreservationRegistry.class, new InertPreservation()); IrisServices.register(PreservationRegistry.class, new InertPreservation());
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager()); IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
platform = created; platform = created;
ModdedIrisSplash.print(boundLoader);
return created; return created;
} }
} }
@@ -0,0 +1,95 @@
/*
* 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.spi.IrisLogging;
import art.arcane.iris.spi.LogLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ModdedIrisLog {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static volatile boolean DEBUG_SETTING_WARNING_LOGGED;
private ModdedIrisLog() {
}
public static void log(LogLevel level, String message) {
LogLevel target = level == null ? LogLevel.INFO : level;
switch (target) {
case DEBUG -> debug(message);
case INFO -> info(message);
case WARN -> warn(message);
case ERROR -> error(message);
}
}
public static void debug(String message) {
if (!debugEnabled()) {
return;
}
LOGGER.debug(clean(message));
}
public static void info(String message) {
LOGGER.info(clean(message));
}
public static void warn(String message) {
LOGGER.warn(clean(message));
}
public static void error(String message) {
LOGGER.error(clean(message));
}
public static void error(String message, Throwable error) {
if (error == null) {
error(message);
return;
}
LOGGER.error(clean(message), error);
}
public static String clean(String message) {
return IrisLogging.clean(message);
}
private static boolean debugEnabled() {
try {
IrisSettings settings = IrisSettings.settings != null ? IrisSettings.settings : IrisSettings.get();
return settings != null && settings.getGeneral() != null && settings.getGeneral().isDebug();
} catch (Throwable error) {
warnDebugSetting(error);
return false;
}
}
private static void warnDebugSetting(Throwable error) {
if (DEBUG_SETTING_WARNING_LOGGED) {
return;
}
DEBUG_SETTING_WARNING_LOGGED = true;
LOGGER.warn("Iris debug logging setting could not be read", error);
}
}
@@ -0,0 +1,121 @@
/*
* 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.core.splash.IrisSplashPackScanner;
import art.arcane.iris.core.splash.IrisSplashPackScanner.SplashPackMetadata;
import art.arcane.iris.core.splash.IrisSplashRenderer;
import art.arcane.iris.spi.IrisLogging;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
public final class ModdedIrisSplash {
private ModdedIrisSplash() {
}
public static void print(ModdedLoader loader) {
printPacks(loader);
if (isLogoEnabled()) {
printLogo(loader);
}
}
private static void printPacks(ModdedLoader loader) {
File packFolder = loader.configDir().resolve("irisworldgen").resolve("packs").toFile();
List<SplashPackMetadata> packs = IrisSplashPackScanner.collect(packFolder, IrisLogging::reportError);
if (packs.isEmpty()) {
return;
}
IrisLogging.info("Custom Dimensions: " + packs.size());
for (SplashPackMetadata pack : packs) {
IrisLogging.info(" " + pack.name() + " v" + pack.version());
}
}
private static void printLogo(ModdedLoader loader) {
String padding = " ".repeat(4);
String version = loader.modVersion();
String releaseTrain = getReleaseTrain(version);
String startupDate = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
int javaVersion = getJavaVersion();
String[] splash = IrisSplashRenderer.renderPlain();
String[] info = new String[]{
"",
" Iris, Dimension Engine [" + releaseTrain + " RC.1.1.6]",
" Version: " + version,
" By: Volmit Software (Arcane Arts)",
" Server: " + loader.platformName() + " / Minecraft " + loader.minecraftVersion(),
" Java: " + javaVersion + " | Date: " + startupDate,
" Commit: " + BuildConstants.COMMIT + "/" + BuildConstants.ENVIRONMENT,
"",
"",
"",
""
};
StringBuilder builder = new StringBuilder("\n\n");
for (int i = 0; i < splash.length; i++) {
builder.append(padding).append(splash[i]).append(info[i]).append('\n');
}
IrisLogging.info(builder.toString());
}
private static boolean isLogoEnabled() {
try {
return IrisSettings.get().getGeneral().isSplashLogoStartup();
} catch (Throwable error) {
IrisLogging.warn("Iris splash setting could not be read: " + error.getClass().getSimpleName());
return true;
}
}
private static int getJavaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf('.');
if (dot != -1) {
version = version.substring(0, dot);
}
}
return Integer.parseInt(version);
}
private static String getReleaseTrain(String version) {
String value = version == null ? "unknown" : version;
int suffixIndex = value.indexOf('-');
if (suffixIndex >= 0) {
value = value.substring(0, suffixIndex);
}
String[] split = value.split("\\.");
if (split.length >= 2) {
return split[0] + "." + split[1];
}
return value;
}
}
@@ -26,14 +26,11 @@ import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler; import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks; import art.arcane.iris.spi.PlatformStructureHooks;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.util.function.Consumer; import java.util.function.Consumer;
public final class ModdedPlatform implements IrisPlatform { public final class ModdedPlatform implements IrisPlatform {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static volatile Consumer<Throwable> ERROR_SINK = null; private static volatile Consumer<Throwable> ERROR_SINK = null;
private final ModdedLoader loader; private final ModdedLoader loader;
@@ -135,17 +132,12 @@ public final class ModdedPlatform implements IrisPlatform {
@Override @Override
public void log(LogLevel level, String message) { public void log(LogLevel level, String message) {
switch (level) { ModdedIrisLog.log(level, message);
case DEBUG -> LOGGER.debug(message);
case INFO -> LOGGER.info(message);
case WARN -> LOGGER.warn(message);
case ERROR -> LOGGER.error(message);
}
} }
@Override @Override
public void msg(String message) { public void msg(String message) {
LOGGER.info(message); ModdedIrisLog.info(message);
} }
@Override @Override
@@ -156,7 +148,7 @@ public final class ModdedPlatform implements IrisPlatform {
return; return;
} }
if (error != null) { if (error != null) {
LOGGER.error("Iris reported error", error); ModdedIrisLog.error("Iris reported error", error);
} }
} }
@@ -28,6 +28,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap; import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedLoader; import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.ModdedPackInstaller; import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.math.Position2;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
@@ -42,6 +43,7 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands; import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.network.chat.Component;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
@@ -84,7 +86,7 @@ public final class IrisModdedCommands {
LiteralCommandNode<CommandSourceStack> root = dispatcher.register(rootTree()); LiteralCommandNode<CommandSourceStack> root = dispatcher.register(rootTree());
dispatcher.register(Commands.literal("ir").redirect(root)); dispatcher.register(Commands.literal("ir").redirect(root));
dispatcher.register(Commands.literal("irs").redirect(root)); dispatcher.register(Commands.literal("irs").redirect(root));
LOGGER.info("Iris /iris command tree registered"); IrisLogging.info("Iris /iris command tree registered");
} }
private static LiteralArgumentBuilder<CommandSourceStack> rootTree() { private static LiteralArgumentBuilder<CommandSourceStack> rootTree() {
@@ -322,7 +324,7 @@ public final class IrisModdedCommands {
} }
private static int pregenStatus(CommandSourceStack source) { private static int pregenStatus(CommandSourceStack source) {
String status = ModdedPregenJob.status(); Component status = ModdedPregenJob.statusComponent();
if (status == null) { if (status == null) {
fail(source, "No active pregeneration task."); fail(source, "No active pregeneration task.");
return 0; return 0;
@@ -747,6 +749,10 @@ public final class IrisModdedCommands {
ModdedCommandFeedback.ok(source, message); ModdedCommandFeedback.ok(source, message);
} }
static void ok(CommandSourceStack source, Component component) {
ModdedCommandFeedback.ok(source, component);
}
static void fail(CommandSourceStack source, String message) { static void fail(CommandSourceStack source, String message) {
ModdedCommandFeedback.fail(source, message); ModdedCommandFeedback.fail(source, message);
} }
@@ -20,7 +20,12 @@ package art.arcane.iris.modded.command;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.HoverEvent;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.Style;
import net.minecraft.network.chat.TextColor;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundEvents;
@@ -31,6 +36,25 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
final class ModdedCommandFeedback { final class ModdedCommandFeedback {
static final int IRIS = 0x1BB19E;
static final int HEADER_A = 0x34EB6B;
static final int HEADER_B = 0x32BFAD;
static final int BACK = 0x6FE98F;
static final int DARK_GREEN = 0x46826A;
static final int DESCRIPTION_ICON = 0x3FE05A;
static final int DESCRIPTION = 0x6AD97D;
static final int USAGE_ICON = 0xBBE03F;
static final int USAGE = 0xA8E0A2;
static final int EXAMPLE_ICON = 0xC2F7D2;
static final int PARAMETER = 0x5EF288;
static final int PARAMETER_ALT = 0x32BFAD;
static final int OPTIONAL = 0x4F4F4F;
static final int CATEGORY = 0x9DE5B6;
static final int REQUIRED = 0xDB4321;
static final int REQUIRED_TEXT = 0xFAA796;
static final int HOVER_TYPE = 0x8AD9AF;
static final int VALUE = 0xC2F7D2;
static final int PAGE_LINE_LENGTH = 75;
private static final long MESSAGE_SOUND_COOLDOWN_MS = 650L; private static final long MESSAGE_SOUND_COOLDOWN_MS = 650L;
private static final long TAB_SOUND_COOLDOWN_MS = 175L; private static final long TAB_SOUND_COOLDOWN_MS = 175L;
private static final Map<UUID, Long> MESSAGE_SOUNDS = new ConcurrentHashMap<>(); private static final Map<UUID, Long> MESSAGE_SOUNDS = new ConcurrentHashMap<>();
@@ -44,6 +68,11 @@ final class ModdedCommandFeedback {
playSuccess(source); playSuccess(source);
} }
static void ok(CommandSourceStack source, Component component) {
source.sendSuccess(() -> component, false);
playSuccess(source);
}
static void fail(CommandSourceStack source, String message) { static void fail(CommandSourceStack source, String message) {
source.sendFailure(Component.literal(message).withStyle(ChatFormatting.RED)); source.sendFailure(Component.literal(message).withStyle(ChatFormatting.RED));
playFailure(source); playFailure(source);
@@ -53,6 +82,61 @@ final class ModdedCommandFeedback {
source.sendSuccess(() -> component, false); source.sendSuccess(() -> component, false);
} }
static void clear(CommandSourceStack source) {
if (source.getPlayer() != null) {
send(source, Component.literal("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"));
}
}
static MutableComponent header(String title) {
MutableComponent header = Component.empty();
header.append(text(" ".repeat(18), HEADER_A, false, true));
header.append(text(" " + title + " ", IRIS, true, false));
header.append(text(" ".repeat(18), HEADER_B, false, true));
return header;
}
static MutableComponent footer() {
return text(" ".repeat(PAGE_LINE_LENGTH), HEADER_B, false, true);
}
static MutableComponent text(String value, int color) {
return text(value, color, false, false);
}
static MutableComponent text(String value, int color, boolean bold, boolean strikethrough) {
return Component.literal(value).withStyle((Style style) -> {
Style next = style.withColor(TextColor.fromRgb(color));
if (bold) {
next = next.withBold(true);
}
if (strikethrough) {
next = next.withStrikethrough(true);
}
return next;
});
}
static MutableComponent button(String label, String command, String hover, boolean runCommand) {
ClickEvent clickEvent = runCommand ? new ClickEvent.RunCommand(command) : new ClickEvent.SuggestCommand(command);
MutableComponent hoverText = text(hover, DESCRIPTION);
return text(label, PARAMETER_ALT, true, false).withStyle((Style style) -> style
.withClickEvent(clickEvent)
.withHoverEvent(new HoverEvent.ShowText(hoverText)));
}
static MutableComponent progressBar(double percent, int width) {
double clamped = Math.max(0D, Math.min(100D, percent));
int filled = (int) Math.round((clamped / 100D) * width);
MutableComponent bar = Component.empty();
bar.append(text("[", DARK_GREEN));
for (int i = 0; i < width; i++) {
bar.append(text(i < filled ? "|" : "·", i < filled ? PARAMETER : OPTIONAL));
}
bar.append(text("]", DARK_GREEN));
return bar;
}
static void tab(CommandSourceStack source) { static void tab(CommandSourceStack source) {
ServerPlayer player = source.getPlayer(); ServerPlayer player = source.getPlayer();
if (player == null || !claim(TAB_SOUNDS, player.getUUID(), TAB_SOUND_COOLDOWN_MS)) { if (player == null || !claim(TAB_SOUNDS, player.getUUID(), TAB_SOUND_COOLDOWN_MS)) {
@@ -18,17 +18,32 @@
package art.arcane.iris.modded.command; package art.arcane.iris.modded.command;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.HoverEvent;
import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.MutableComponent;
import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.BACK;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.CATEGORY;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.DARK_GREEN;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.DESCRIPTION;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.DESCRIPTION_ICON;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.EXAMPLE_ICON;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.HOVER_TYPE;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.OPTIONAL;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.PARAMETER;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.PARAMETER_ALT;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.REQUIRED;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.REQUIRED_TEXT;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.USAGE;
import static art.arcane.iris.modded.command.ModdedCommandFeedback.USAGE_ICON;
final class ModdedCommandHelp { final class ModdedCommandHelp {
private static final Map<String, List<Entry>> SECTIONS = new LinkedHashMap<>(); private static final Map<String, List<Entry>> SECTIONS = new LinkedHashMap<>();
@@ -89,7 +104,7 @@ final class ModdedCommandHelp {
Entry.command("package", "[pack]", "Package a dimension into a compressed format"), Entry.command("package", "[pack]", "Package a dimension into a compressed format"),
Entry.command("version", "[pack]", "Print a pack version"), Entry.command("version", "[pack]", "Print a pack version"),
Entry.command("regions", "[radius]", "Calculate nearby region distribution"), Entry.command("regions", "[radius]", "Calculate nearby region distribution"),
Entry.command("open", "", "Explain modded studio workflow"), Entry.command("open", "<pack> [seed]", "Open or prepare a dimension pack studio workflow"),
Entry.command("close", "", "Explain modded studio workflow"), Entry.command("close", "", "Explain modded studio workflow"),
Entry.command("vscode", "", "Explain editor workflow"), Entry.command("vscode", "", "Explain editor workflow"),
Entry.command("update", "", "Explain workspace regeneration workflow"), Entry.command("update", "", "Explain workspace regeneration workflow"),
@@ -143,53 +158,187 @@ final class ModdedCommandHelp {
return 0; return 0;
} }
sendHeader(source, normalized.isEmpty() ? "Iris Commands" : "/iris " + normalized); ModdedCommandFeedback.clear(source);
sendHeader(source, normalized);
if (!normalized.isEmpty()) { if (!normalized.isEmpty()) {
ModdedCommandFeedback.send(source, clickable(" < Back", "/iris", "/iris", "Back to Iris command groups", true)); ModdedCommandFeedback.send(source, backButton(normalized));
} }
for (Entry entry : entries) { for (Entry entry : entries) {
ModdedCommandFeedback.send(source, line(normalized, entry)); ModdedCommandFeedback.send(source, line(normalized, entry));
} }
ModdedCommandFeedback.ok(source, "Click a group to open it, or click a command to place it in chat."); ModdedCommandFeedback.ok(source, footer());
return 1; return 1;
} }
private static void sendHeader(CommandSourceStack source, String title) { private static void sendHeader(CommandSourceStack source, String path) {
MutableComponent header = Component.literal("========== ").withStyle(ChatFormatting.DARK_GREEN, ChatFormatting.STRIKETHROUGH) String title = path.isEmpty() ? "/iris" : "/iris " + path;
.append(Component.literal(" " + title + " ").withStyle(ChatFormatting.GREEN, ChatFormatting.BOLD)) ModdedCommandFeedback.send(source, ModdedCommandFeedback.header(title));
.append(Component.literal(" ==========").withStyle(ChatFormatting.DARK_GREEN, ChatFormatting.STRIKETHROUGH)); }
ModdedCommandFeedback.send(source, header);
private static MutableComponent backButton(String path) {
String parent = parentPath(path);
String command = parent.isEmpty() ? "/iris" : "/iris help " + parent;
MutableComponent hover = Component.empty()
.append(text("Click to go back to ", DARK_GREEN))
.append(text(parent.isEmpty() ? "Iris" : parent, PARAMETER_ALT));
return text("〈 Back", BACK).withStyle((style) -> style
.withClickEvent(new ClickEvent.RunCommand(command))
.withHoverEvent(new HoverEvent.ShowText(hover)));
} }
private static MutableComponent line(String path, Entry entry) { private static MutableComponent line(String path, Entry entry) {
String basePath = path.isEmpty() ? "" : " " + path; MutableComponent row = Component.empty();
String command = "/iris" + basePath + " " + entry.name(); row.append(clickableCommand(path, entry));
String suggestion = entry.usage().isBlank() ? command : command + " " + entry.usage(); row.append(nodes(entry));
String hover = entry.description() + (entry.usage().isBlank() ? "" : "\nUsage: " + suggestion);
MutableComponent row = Component.literal(" ");
row.append(clickable(entry.name(), command, suggestion, hover, entry.group()));
if (entry.aliases().length > 0) {
row.append(Component.literal(" " + String.join(", ", entry.aliases())).withStyle(ChatFormatting.DARK_GREEN));
}
row.append(Component.literal(" - ").withStyle(ChatFormatting.DARK_GRAY));
row.append(Component.literal(entry.description()).withStyle(ChatFormatting.GRAY));
return row; return row;
} }
private static MutableComponent clickable(String label, String command, String suggestion, String hover, boolean run) { private static MutableComponent clickableCommand(String path, Entry entry) {
Component hoverText = Component.literal(hover).withStyle(ChatFormatting.GREEN); String parent = path.isEmpty() ? "/iris" : "/iris " + path;
if (run) { String command = parent + " " + entry.name();
return Component.literal(label).withStyle((style) -> style String suggestion = entry.usage().isBlank() ? command : command + " " + entry.usage();
.withColor(ChatFormatting.AQUA) ClickEvent clickEvent = entry.group() ? new ClickEvent.RunCommand(command) : new ClickEvent.SuggestCommand(suggestion);
.withBold(true) MutableComponent hover = entryHover(path, entry, suggestion);
.withClickEvent(new ClickEvent.RunCommand(command)) MutableComponent display = Component.empty();
.withHoverEvent(new HoverEvent.ShowText(hoverText))); display.append(text(parent + " >", 0xFFFFFF));
display.append(text("⇀", DARK_GREEN));
display.append(text(" " + entry.name(), PARAMETER_ALT));
return display.withStyle((style) -> style
.withClickEvent(clickEvent)
.withHoverEvent(new HoverEvent.ShowText(hover)));
}
private static MutableComponent nodes(Entry entry) {
if (entry.group()) {
return text(" - Category of Commands", CATEGORY);
} }
return Component.literal(label).withStyle((style) -> style List<String> tokens = usageTokens(entry.usage());
.withColor(ChatFormatting.GREEN) if (tokens.isEmpty()) {
.withClickEvent(new ClickEvent.SuggestCommand(suggestion)) return Component.empty();
.withHoverEvent(new HoverEvent.ShowText(hoverText))); }
MutableComponent nodes = Component.empty();
for (String token : tokens) {
nodes.append(Component.literal(" "));
nodes.append(parameter(token));
}
return nodes;
}
private static MutableComponent parameter(String token) {
String name = parameterName(token);
boolean required = token.startsWith("<");
MutableComponent title = Component.empty();
if (required) {
title.append(text("[", REQUIRED, true, false));
title.append(text(name, PARAMETER, false, false));
title.append(text("]", REQUIRED, true, false));
} else {
title.append(text("⊰", OPTIONAL));
title.append(text(name, PARAMETER));
title.append(text("⊱", OPTIONAL));
}
MutableComponent hover = Component.empty();
hover.append(text(name, PARAMETER));
hover.append(Component.literal("\n"));
hover.append(text("✎ ", DESCRIPTION_ICON));
hover.append(text("Command parameter", DESCRIPTION));
hover.append(Component.literal("\n"));
if (required) {
hover.append(text("⚠ ", REQUIRED));
hover.append(text("This parameter is required.", REQUIRED_TEXT));
} else {
hover.append(text("✔ ", DESCRIPTION_ICON));
hover.append(text("This parameter is optional.", USAGE));
}
hover.append(Component.literal("\n"));
hover.append(text("✢ ", DARK_GREEN));
hover.append(text("This parameter is read as text by Brigadier.", HOVER_TYPE));
return title.withStyle((style) -> style.withHoverEvent(new HoverEvent.ShowText(hover)));
}
private static MutableComponent entryHover(String path, Entry entry, String suggestion) {
MutableComponent hover = Component.empty();
hover.append(text(names(entry), PARAMETER));
hover.append(Component.literal("\n"));
hover.append(text("✎ ", DESCRIPTION_ICON));
hover.append(text(entry.description(), DESCRIPTION));
hover.append(Component.literal("\n"));
hover.append(text("✒ ", USAGE_ICON));
if (entry.group()) {
hover.append(text("This is a command category. Click to run.", USAGE));
} else if (entry.usage().isBlank()) {
hover.append(text("There are no parameters. Click to type command.", USAGE));
} else {
hover.append(text("Hover over all of the parameters to learn more.", USAGE));
hover.append(Component.literal("\n"));
hover.append(text("✦ ", EXAMPLE_ICON));
hover.append(text(suggestion, PARAMETER));
}
String parent = path.isEmpty() ? "/iris" : "/iris " + path;
if (entry.aliases().length > 0) {
hover.append(Component.literal("\n"));
hover.append(text("Aliases: ", DARK_GREEN));
List<String> aliases = new ArrayList<>(entry.aliases().length);
for (String alias : entry.aliases()) {
aliases.add(parent + " " + alias);
}
hover.append(text(String.join(", ", aliases), PARAMETER_ALT));
}
return hover;
}
private static MutableComponent footer() {
return ModdedCommandFeedback.footer();
}
private static MutableComponent text(String value, int color) {
return ModdedCommandFeedback.text(value, color, false, false);
}
private static MutableComponent text(String value, int color, boolean bold, boolean strikethrough) {
return ModdedCommandFeedback.text(value, color, bold, strikethrough);
}
private static List<String> usageTokens(String usage) {
if (usage == null || usage.isBlank()) {
return List.of();
}
return List.of(usage.trim().split("\\s+"));
}
private static String names(Entry entry) {
if (entry.aliases().length == 0) {
return entry.name();
}
List<String> names = new ArrayList<>(entry.aliases().length + 1);
names.add(entry.name());
for (String alias : entry.aliases()) {
names.add(alias);
}
return String.join(", ", names);
}
private static String parameterName(String token) {
String name = token;
if ((name.startsWith("<") && name.endsWith(">")) || (name.startsWith("[") && name.endsWith("]"))) {
name = name.substring(1, name.length() - 1);
}
return name;
}
private static String parentPath(String path) {
int lastSpace = path.lastIndexOf(' ');
if (lastSpace <= 0) {
return "";
}
return path.substring(0, lastSpace);
} }
private static String normalize(String path) { private static String normalize(String path) {
@@ -24,6 +24,8 @@ import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.math.Position2;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -103,16 +105,63 @@ public final class ModdedPregenJob implements PregenListener {
if (job == null) { if (job == null) {
return null; return null;
} }
long total = Math.max(1L, job.totalChunks); return job.statusText();
double percent = ((double) job.generated / (double) total) * 100D; }
return "Pregen " + job.dimension + ": "
+ Form.f(job.generated) + "/" + Form.f(job.totalChunks) public static Component statusComponent() {
ModdedPregenJob job = ACTIVE.get();
if (job == null) {
return null;
}
double percent = job.percent();
MutableComponent status = Component.empty();
status.append(ModdedCommandFeedback.header("Iris Pregen"));
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.text("Dimension ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(job.dimension, ModdedCommandFeedback.PARAMETER_ALT));
status.append(ModdedCommandFeedback.text(" · Method ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(job.method, ModdedCommandFeedback.PARAMETER));
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.progressBar(percent, 32));
status.append(ModdedCommandFeedback.text(" " + String.format("%.1f", percent) + "%", ModdedCommandFeedback.USAGE));
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.text("Chunks ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(Form.f(job.generated) + "/" + Form.f(job.totalChunks), ModdedCommandFeedback.VALUE));
status.append(ModdedCommandFeedback.text(" · Speed ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(Form.f((int) job.chunksPerSecond) + "/s", ModdedCommandFeedback.VALUE));
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.text("ETA ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(Form.duration(job.eta, 2), ModdedCommandFeedback.VALUE));
status.append(ModdedCommandFeedback.text(" · Elapsed ", ModdedCommandFeedback.DARK_GREEN));
status.append(ModdedCommandFeedback.text(Form.duration(job.elapsed, 2), ModdedCommandFeedback.VALUE));
if (job.pregenerator.paused()) {
status.append(ModdedCommandFeedback.text(" · PAUSED", ModdedCommandFeedback.REQUIRED, true, false));
}
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.button("Pause/Resume", "/iris pregen pause", "Toggle pregeneration pause state", true));
status.append(ModdedCommandFeedback.text(" ", ModdedCommandFeedback.OPTIONAL));
status.append(ModdedCommandFeedback.button("Stop", "/iris pregen stop", "Finish the current region and stop pregeneration", true));
status.append(Component.literal("\n"));
status.append(ModdedCommandFeedback.footer());
return status;
}
private String statusText() {
double percent = percent();
return "Pregen " + dimension + ": "
+ Form.f(generated) + "/" + Form.f(totalChunks)
+ " (" + String.format("%.1f", percent) + "%), " + " (" + String.format("%.1f", percent) + "%), "
+ Form.f((int) job.chunksPerSecond) + "/s" + Form.f((int) chunksPerSecond) + "/s"
+ ", ETA " + Form.duration(job.eta, 2) + ", ETA " + Form.duration(eta, 2)
+ ", elapsed " + Form.duration(job.elapsed, 2) + ", elapsed " + Form.duration(elapsed, 2)
+ ", method " + job.method + ", method " + method
+ (job.pregenerator.paused() ? ", PAUSED" : ""); + (pregenerator.paused() ? ", PAUSED" : "");
}
private double percent() {
long total = Math.max(1L, totalChunks);
return ((double) generated / (double) total) * 100D;
} }
@Override @Override
@@ -38,11 +38,14 @@ import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Spiraler; import art.arcane.volmlib.util.math.Spiraler;
import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands; import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -102,12 +105,12 @@ public final class ModdedStudioCommands {
.then(Commands.argument("radius", IntegerArgumentType.integer(8, 1000)) .then(Commands.argument("radius", IntegerArgumentType.integer(8, 1000))
.executes((CommandContext<CommandSourceStack> context) -> regions(context.getSource(), IntegerArgumentType.getInteger(context, "radius"))))); .executes((CommandContext<CommandSourceStack> context) -> regions(context.getSource(), IntegerArgumentType.getInteger(context, "radius")))));
root.then(message("open", "Studio worlds are temporary Bukkit worlds opened by the Bukkit studio toolchain; modded servers cannot open ad-hoc runtime dimensions. Edit the pack under config/irisworldgen/packs/<pack> and create a fresh world (or /iris regen) to see changes.")); root.then(openTree("open"));
root.then(message("o", "Studio worlds are temporary Bukkit worlds opened by the Bukkit studio toolchain; modded servers cannot open ad-hoc runtime dimensions. Edit the pack under config/irisworldgen/packs/<pack> and create a fresh world (or /iris regen) to see changes.")); root.then(openTree("o"));
root.then(message("close", "There are no studio worlds on modded servers (/iris studio open is Bukkit-only), so there is nothing to close.")); root.then(message("close", "There are no studio worlds on modded servers (/iris studio open prepares the pack workflow instead), so there is nothing to close."));
root.then(message("x", "There are no studio worlds on modded servers (/iris studio open is Bukkit-only), so there is nothing to close.")); root.then(message("x", "There are no studio worlds on modded servers (/iris studio open prepares the pack workflow instead), so there is nothing to close."));
root.then(message("tpstudio", "There are no studio worlds on modded servers to teleport to; /iris studio open is Bukkit-only.")); root.then(message("tpstudio", "There are no temporary Bukkit studio worlds on modded servers to teleport to; use /iris studio open <pack> to prepare the pack workflow instead."));
root.then(message("stp", "There are no studio worlds on modded servers to teleport to; /iris studio open is Bukkit-only.")); root.then(message("stp", "There are no temporary Bukkit studio worlds on modded servers to teleport to; use /iris studio open <pack> to prepare the pack workflow instead."));
root.then(message("vscode", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor.")); root.then(message("vscode", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor."));
root.then(message("vsc", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor.")); root.then(message("vsc", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor."));
root.then(message("update", "Workspace regeneration (.code-workspace + JSON schemas) reads Bukkit registries (SchemaBuilder); run /iris studio update on a Bukkit server against this pack.")); root.then(message("update", "Workspace regeneration (.code-workspace + JSON schemas) reads Bukkit registries (SchemaBuilder); run /iris studio update on a Bukkit server against this pack."));
@@ -128,6 +131,20 @@ public final class ModdedStudioCommands {
return root; return root;
} }
private static LiteralArgumentBuilder<CommandSourceStack> openTree(String name) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> openHelp(context.getSource()))
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> open(context.getSource(), StringArgumentType.getString(context, "pack"), 1337L))
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes((CommandContext<CommandSourceStack> context) -> open(context.getSource(), StringArgumentType.getString(context, "pack"), LongArgumentType.getLong(context, "seed")))));
}
private static int openHelp(CommandSourceStack source) {
IrisModdedCommands.fail(source, "Provide a dimension pack: /iris studio open <pack> [seed]");
return 0;
}
private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) { private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) {
return Commands.literal(name) return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> { .executes((CommandContext<CommandSourceStack> context) -> {
@@ -159,6 +176,52 @@ public final class ModdedStudioCommands {
return folder; return folder;
} }
private static int open(CommandSourceStack source, String pack, long seed) {
File folder = resolvePack(source, pack);
if (folder == null) {
return 0;
}
IrisData data = IrisData.get(folder);
IrisDimension dimension = data.getDimensionLoader().load(folder.getName());
if (dimension == null) {
IrisModdedCommands.fail(source, "Pack '" + folder.getName() + "' has no dimensions/" + folder.getName() + ".json");
return 0;
}
IrisModdedCommands.ok(source, studioOpenComponent(dimension, folder, seed));
return 1;
}
private static MutableComponent studioOpenComponent(IrisDimension dimension, File folder, long seed) {
String dimensionKey = dimension.getLoadKey() == null ? folder.getName() : dimension.getLoadKey();
MutableComponent message = Component.empty();
message.append(ModdedCommandFeedback.header("Iris Studio"));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.text("Opening studio for the \"", ModdedCommandFeedback.DARK_GREEN));
message.append(ModdedCommandFeedback.text(dimension.getName(), ModdedCommandFeedback.PARAMETER_ALT));
message.append(ModdedCommandFeedback.text("\" pack", ModdedCommandFeedback.DARK_GREEN));
message.append(ModdedCommandFeedback.text(" (seed: " + seed + ")", ModdedCommandFeedback.VALUE));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.text("Pack ", ModdedCommandFeedback.DARK_GREEN));
message.append(ModdedCommandFeedback.text(dimensionKey, ModdedCommandFeedback.PARAMETER));
message.append(ModdedCommandFeedback.text(" is ready at ", ModdedCommandFeedback.DESCRIPTION));
message.append(ModdedCommandFeedback.text(folder.getAbsolutePath(), ModdedCommandFeedback.VALUE));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.text("Modded servers cannot create Bukkit's temporary studio world at runtime; edit this pack directly, then use the matching world/datapack workflow or ", ModdedCommandFeedback.DESCRIPTION));
message.append(ModdedCommandFeedback.button("/iris regen", "/iris regen", "Regenerate nearby chunks after editing this pack", false));
message.append(ModdedCommandFeedback.text(" in an Iris dimension.", ModdedCommandFeedback.DESCRIPTION));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.button("Validate", "/iris pack validate " + dimensionKey, "Validate this pack before loading it", true));
message.append(ModdedCommandFeedback.text(" ", ModdedCommandFeedback.OPTIONAL));
message.append(ModdedCommandFeedback.button("World Help", "/iris world", "Open Iris world command help", true));
message.append(ModdedCommandFeedback.text(" ", ModdedCommandFeedback.OPTIONAL));
message.append(ModdedCommandFeedback.button("Package", "/iris studio package " + dimensionKey, "Package this dimension", false));
message.append(Component.literal("\n"));
message.append(ModdedCommandFeedback.footer());
return message;
}
private static int version(CommandSourceStack source, String pack) { private static int version(CommandSourceStack source, String pack) {
File folder = resolvePack(source, pack); File folder = resolvePack(source, pack);
if (folder == null) { if (folder == null) {
@@ -20,6 +20,7 @@ package art.arcane.iris.neoforge;
import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap; import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.modded.ModdedParityProbe; import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck; import art.arcane.iris.modded.ModdedWorldCheck;
import art.arcane.iris.modded.ModdedWorldEngines; import art.arcane.iris.modded.ModdedWorldEngines;
@@ -42,20 +43,16 @@ import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent; import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.neoforged.neoforge.event.tick.ServerTickEvent; import net.neoforged.neoforge.event.tick.ServerTickEvent;
import net.neoforged.neoforge.registries.DeferredRegister; import net.neoforged.neoforge.registries.DeferredRegister;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Mod("irisworldgen") @Mod("irisworldgen")
public final class IrisNeoForgeBootstrap { public final class IrisNeoForgeBootstrap {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
public IrisNeoForgeBootstrap(IEventBus modBus) { public IrisNeoForgeBootstrap(IEventBus modBus) {
ModdedEngineBootstrap.initialize(new NeoForgeModdedLoader()); ModdedEngineBootstrap.initialize(new NeoForgeModdedLoader());
String modVersion = ModList.get().getModContainerById("irisworldgen") String modVersion = ModList.get().getModContainerById("irisworldgen")
.map((ModContainer container) -> container.getModInfo().getVersion().toString()) .map((ModContainer container) -> container.getModInfo().getVersion().toString())
.orElse("unknown"); .orElse("unknown");
VersionInfo versionInfo = FMLLoader.getCurrent().getVersionInfo(); VersionInfo versionInfo = FMLLoader.getCurrent().getVersionInfo();
LOGGER.info("Iris {} bootstrapping on Minecraft {} (NeoForge {})", modVersion, versionInfo.mcVersion(), versionInfo.neoForgeVersion()); ModdedIrisLog.info("Iris " + modVersion + " bootstrapping on Minecraft " + versionInfo.mcVersion() + " (NeoForge " + versionInfo.neoForgeVersion() + ")");
ModdedEngineBootstrap.selfTest(IrisNeoForgeBootstrap.class.getClassLoader()); ModdedEngineBootstrap.selfTest(IrisNeoForgeBootstrap.class.getClassLoader());
ModdedEngineBootstrap.bind(); ModdedEngineBootstrap.bind();
@@ -63,7 +60,7 @@ public final class IrisNeoForgeBootstrap {
DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen"); DeferredRegister<MapCodec<? extends ChunkGenerator>> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen");
chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC); chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC);
chunkGenerators.register(modBus); chunkGenerators.register(modBus);
LOGGER.info("Iris chunk generator registered as irisworldgen:iris"); ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> { NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> {
ModdedObjectUndo.clearAll(); ModdedObjectUndo.clearAll();
@@ -86,13 +83,13 @@ public final class IrisNeoForgeBootstrap {
String parity = System.getProperty("iris.parity"); String parity = System.getProperty("iris.parity");
if (parity != null) { if (parity != null) {
LOGGER.info("Iris parity probe armed: {}", parity); ModdedIrisLog.info("Iris parity probe armed: " + parity);
ModdedParityProbe.schedule(parity); ModdedParityProbe.schedule(parity);
} }
String worldCheck = System.getProperty("iris.worldcheck"); String worldCheck = System.getProperty("iris.worldcheck");
if (worldCheck != null) { if (worldCheck != null) {
LOGGER.info("Iris world check armed"); ModdedIrisLog.info("Iris world check armed");
ModdedWorldCheck.schedule(); ModdedWorldCheck.schedule();
} }
} }
+90 -8
View File
@@ -70,6 +70,9 @@ String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${lo
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}") String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}") String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
extensions.extraProperties.set('bukkitArtifactName', bukkitArtifactName) extensions.extraProperties.set('bukkitArtifactName', bukkitArtifactName)
extensions.extraProperties.set('fabricArtifactName', fabricArtifactName)
extensions.extraProperties.set('forgeArtifactName', forgeArtifactName)
extensions.extraProperties.set('neoForgeArtifactName', neoForgeArtifactName)
apply plugin: ApiGenerator apply plugin: ApiGenerator
@@ -86,7 +89,13 @@ registerCustomOutputTask('PixelFury', 'C://Users/repix/workplace/Iris/1.21.3 - D
registerCustomOutputTask('PixelFuryDev', 'C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins') registerCustomOutputTask('PixelFuryDev', 'C://Users/repix/workplace/Iris/1.21 - Development-v3/plugins')
// ========================== UNIX ============================== // ========================== UNIX ==============================
registerCustomOutputTaskUnix('CyberpwnLT', '/Users/danielmills/development/server/plugins') registerCustomOutputTaskUnix('CyberpwnLT', '/Users/danielmills/development/server/plugins')
registerCustomOutputTaskUnix('PsychoLT', '/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/dropins/plugins') registerCustomOutputTaskUnix(
'PsychoLT',
'/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/dropins/plugins',
'/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/fabric-mod-consumers/dropins/mods',
'/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/forge-mod-consumers/dropins/mods',
'/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/neoforge-mod-consumers/dropins/mods'
)
registerCustomOutputTaskUnix('PixelMac', '/Users/test/Desktop/mcserver/plugins') registerCustomOutputTaskUnix('PixelMac', '/Users/test/Desktop/mcserver/plugins')
registerCustomOutputTaskUnix('CrazyDev22LT', '/home/julian/Desktop/server/plugins') registerCustomOutputTaskUnix('CrazyDev22LT', '/home/julian/Desktop/server/plugins')
// ============================================================== // ==============================================================
@@ -232,6 +241,14 @@ tasks.register('buildNeoforge', Copy) {
} }
} }
tasks.named('forgeJar').configure {
mustRunAfter(tasks.named('fabricJar'))
}
tasks.named('neoforgeJar').configure {
mustRunAfter(tasks.named('forgeJar'))
}
tasks.register('buildAll') { tasks.register('buildAll') {
group = 'iris' group = 'iris'
dependsOn('buildBukkit', 'buildFabric', 'buildForge', 'buildNeoforge', ':spi:jar') dependsOn('buildBukkit', 'buildFabric', 'buildForge', 'buildNeoforge', ':spi:jar')
@@ -375,14 +392,15 @@ void registerCustomOutputTask(String name, String path) {
return return
} }
tasks.register("build${name}", Copy) { createCustomOutputTasks(name, path)
group = 'development' }
outputs.upToDateWhen { false }
dependsOn('iris') void registerCustomOutputTask(String name, String path, String fabricPath, String forgePath, String neoForgePath) {
from(layout.buildDirectory.file((String) project.ext.bukkitArtifactName)) if (!System.getProperty('os.name').toLowerCase().contains('windows')) {
into(file(path)) return
rename { String ignored -> 'Iris.jar' }
} }
createCustomOutputTasks(name, path, fabricPath, forgePath, neoForgePath)
} }
void registerCustomOutputTaskUnix(String name, String path) { void registerCustomOutputTaskUnix(String name, String path) {
@@ -390,6 +408,44 @@ void registerCustomOutputTaskUnix(String name, String path) {
return return
} }
createCustomOutputTasks(name, path)
}
void registerCustomOutputTaskUnix(String name, String path, String fabricPath, String forgePath, String neoForgePath) {
if (System.getProperty('os.name').toLowerCase().contains('windows')) {
return
}
createCustomOutputTasks(name, path, fabricPath, forgePath, neoForgePath)
}
void createCustomOutputTasks(String name, String path) {
createBukkitOutputTask(name, path)
tasks.register("buildAll${name}") {
group = 'development'
dependsOn("build${name}")
}
}
void createCustomOutputTasks(String name, String path, String fabricPath, String forgePath, String neoForgePath) {
createBukkitOutputTask(name, path)
createPlatformOutputTask(name, fabricPath, 'Fabric', 'buildFabric', "adapters/fabric/build/libs/${String.valueOf(project.ext.fabricArtifactName)}")
createPlatformOutputTask(name, forgePath, 'Forge', 'buildForge', "adapters/forge/build/libs/${String.valueOf(project.ext.forgeArtifactName)}")
createPlatformOutputTask(name, neoForgePath, 'Neoforge', 'buildNeoforge', "adapters/neoforge/build/libs/${String.valueOf(project.ext.neoForgeArtifactName)}")
tasks.register("buildAll${name}") {
group = 'development'
dependsOn(
"build${name}",
"build${name}Fabric",
"build${name}Forge",
"build${name}Neoforge"
)
}
}
void createBukkitOutputTask(String name, String path) {
tasks.register("build${name}", Copy) { tasks.register("build${name}", Copy) {
group = 'development' group = 'development'
outputs.upToDateWhen { false } outputs.upToDateWhen { false }
@@ -397,5 +453,31 @@ void registerCustomOutputTaskUnix(String name, String path) {
from(layout.buildDirectory.file((String) project.ext.bukkitArtifactName)) from(layout.buildDirectory.file((String) project.ext.bukkitArtifactName))
into(file(path)) into(file(path))
rename { String ignored -> 'Iris.jar' } rename { String ignored -> 'Iris.jar' }
doFirst {
delete(
file("${path}/Iris-Fabric.jar"),
file("${path}/Iris-Forge.jar"),
file("${path}/Iris-Neoforge.jar")
)
}
}
}
void createPlatformOutputTask(String name, String path, String platform, String buildTask, String sourcePath) {
tasks.register("build${name}${platform}", Copy) {
group = 'development'
outputs.upToDateWhen { false }
dependsOn(buildTask)
from(layout.projectDirectory.file(sourcePath))
into(file(path))
rename { String ignored -> "Iris-${platform}.jar" }
doFirst {
delete(
file("${path}/Iris.jar"),
file("${path}/Iris-Fabric.jar"),
file("${path}/Iris-Forge.jar"),
file("${path}/Iris-Neoforge.jar")
)
}
} }
} }
@@ -2,6 +2,7 @@ package art.arcane.iris.core.safeguard;
import art.arcane.iris.BuildConstants; import art.arcane.iris.BuildConstants;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.splash.IrisSplashRenderer;
import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
@@ -11,21 +12,12 @@ import org.bukkit.Bukkit;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.ThreadLocalRandom;
public enum Mode { public enum Mode {
STABLE(C.IRIS, C.AQUA), STABLE(C.IRIS, C.AQUA),
WARNING(C.GOLD, C.YELLOW), WARNING(C.GOLD, C.YELLOW),
UNSTABLE(C.RED, C.GOLD); UNSTABLE(C.RED, C.GOLD);
private static final int SPLASH_WIDTH = 53;
private static final int SPLASH_HEIGHT = 11;
private static final double SPLASH_CENTER_X = 26.0;
private static final double SPLASH_CENTER_Y = 5.0;
private static final double SPLASH_ASPECT = 2.15;
private static final double SPLASH_EYE_A = 5.6;
private static final double SPLASH_EYE_B = 4.6;
private final C color; private final C color;
private final C glow; private final C glow;
private final String id; private final String id;
@@ -70,7 +62,7 @@ public enum Mode {
String startupDate = getStartupDate(); String startupDate = getStartupDate();
int javaVersion = getJavaVersion(); int javaVersion = getJavaVersion();
String[] splash = renderSplashLogo(); String[] splash = IrisSplashRenderer.render(this::splashTone);
String[] info = new String[]{ String[] info = new String[]{
"", "",
@@ -94,38 +86,6 @@ public enum Mode {
IrisLogging.info(builder.toString()); IrisLogging.info(builder.toString());
} }
private String[] renderSplashLogo() {
ThreadLocalRandom random = ThreadLocalRandom.current();
double headThickness = 3.7 + random.nextDouble() * 0.6;
double tipReach = 19.0 + random.nextDouble() * 4.0;
double notchX = 3.5 + random.nextDouble() * 1.5;
double[][][] spines = splashFinSpines(tipReach);
double[][][] voids = splashNotchVoids(notchX);
String[] lines = new String[SPLASH_HEIGHT];
for (int y = 0; y < SPLASH_HEIGHT; y++) {
StringBuilder line = new StringBuilder();
String activeTone = null;
for (int x = 0; x < SPLASH_WIDTH; x++) {
char glyph = splashEyeGlyph(x, y);
if (glyph == 0) {
glyph = splashFinGlyph(x, y, spines, voids, headThickness);
}
if (glyph == 0 || glyph == ' ') {
line.append(' ');
continue;
}
String tone = splashTone(glyph);
if (!tone.equals(activeTone)) {
line.append(tone);
activeTone = tone;
}
line.append(glyph);
}
lines[y] = line.toString();
}
return lines;
}
private String splashTone(char glyph) { private String splashTone(char glyph) {
return switch (glyph) { return switch (glyph) {
case '=' -> C.GRAY.toString(); case '=' -> C.GRAY.toString();
@@ -136,129 +96,6 @@ public enum Mode {
}; };
} }
private char splashEyeGlyph(int x, int y) {
if (y == 5 && Math.abs(x - 26) <= 1) {
if (x == 25) {
return '(';
}
if (x == 27) {
return ')';
}
return ' ';
}
double radius = splashEyeRadius(x, y);
if (radius <= 0.50) {
return '@';
}
if (radius <= 0.72) {
return '%';
}
if (radius <= 0.88) {
return '*';
}
return 0;
}
private char splashFinGlyph(int x, int y, double[][][] spines, double[][][] voids, double headThickness) {
if (splashEyeRadius(x, y) <= 1.26) {
return 0;
}
double voidDistance = Double.MAX_VALUE;
for (double[][] notch : voids) {
for (double[] sample : notch) {
double voidRadius = Math.max(0.25, 0.95 - 0.65 * sample[2]);
double dx = (x - sample[0]) / SPLASH_ASPECT;
double dy = y - sample[1];
double distance = Math.sqrt(dx * dx + dy * dy) / voidRadius;
if (distance < voidDistance) {
voidDistance = distance;
}
}
}
if (voidDistance < 1.0) {
return 0;
}
double best = Double.MAX_VALUE;
double bestT = 0.0;
for (double[][] spine : spines) {
for (int i = 0; i < spine.length; i++) {
double t = i / (double) (spine.length - 1);
double thickness = headThickness * Math.pow(1.0 - t, 1.3) + 0.6;
double dx = (x - spine[i][0]) / SPLASH_ASPECT;
double dy = y - spine[i][1];
double distance = Math.sqrt(dx * dx + dy * dy) / thickness;
if (distance < best) {
best = distance;
bestT = t;
}
}
}
double shade = best
+ Math.max(0.0, 3.0 - Math.min(x, SPLASH_WIDTH - 1 - x)) * 0.12
+ Math.max(0.0, 0.10 - bestT) * 1.5
+ Math.max(0.0, 1.30 - voidDistance) * 0.15;
if (shade > 1.0) {
return 0;
}
int level = shade <= 0.70 ? 0 : shade <= 0.90 ? 1 : 2;
if (bestT > 0.97) {
level += 2;
} else if (bestT > 0.88) {
level += 1;
}
return switch (Math.min(level, 2)) {
case 0 -> '#';
case 1 -> '=';
default -> '-';
};
}
private double splashEyeRadius(int x, int y) {
double dx = (x - SPLASH_CENTER_X) / SPLASH_ASPECT;
double dy = y - SPLASH_CENTER_Y;
double nx = dx / SPLASH_EYE_A;
double ny = dy / SPLASH_EYE_B;
return Math.sqrt(nx * nx + ny * ny);
}
private double[][][] splashNotchVoids(double notchX) {
double[][] control = {{notchX + 0.8, 12.0}, {notchX, 9.6}, {notchX - 1.4, 7.8}};
int samples = 31;
double[][] left = new double[samples][3];
double[][] right = new double[samples][3];
for (int i = 0; i < samples; i++) {
double t = i / (double) (samples - 1);
double u = 1.0 - t;
double bx = u * u * control[0][0] + 2.0 * u * t * control[1][0] + t * t * control[2][0];
double by = u * u * control[0][1] + 2.0 * u * t * control[1][1] + t * t * control[2][1];
left[i][0] = bx;
left[i][1] = by;
left[i][2] = t;
right[i][0] = (SPLASH_WIDTH - 1.0) - bx;
right[i][1] = (SPLASH_HEIGHT - 1.0) - by;
right[i][2] = t;
}
return new double[][][]{left, right};
}
private double[][][] splashFinSpines(double tipReach) {
double[][] control = {{-1.5, 9.8}, {0.4, 4.4}, {5.2, 1.0}, {tipReach, 0.4}};
int samples = 61;
double[][] left = new double[samples][2];
double[][] right = new double[samples][2];
for (int i = 0; i < samples; i++) {
double t = i / (double) (samples - 1);
double u = 1.0 - t;
double bx = u * u * u * control[0][0] + 3.0 * u * u * t * control[1][0] + 3.0 * u * t * t * control[2][0] + t * t * t * control[3][0];
double by = u * u * u * control[0][1] + 3.0 * u * u * t * control[1][1] + 3.0 * u * t * t * control[2][1] + t * t * t * control[3][1];
left[i][0] = bx;
left[i][1] = by;
right[i][0] = (SPLASH_WIDTH - 1.0) - bx;
right[i][1] = (SPLASH_HEIGHT - 1.0) - by;
}
return new double[][][]{left, right};
}
private String wrap(String tag) { private String wrap(String tag) {
return C.BOLD.toString() + C.DARK_GRAY + "[" + C.BOLD + color + tag + C.BOLD + C.DARK_GRAY + "]" + C.RESET; return C.BOLD.toString() + C.DARK_GRAY + "[" + C.BOLD + color + tag + C.BOLD + C.DARK_GRAY + "]" + C.RESET;
} }
@@ -0,0 +1,78 @@
package art.arcane.iris.core.splash;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public final class IrisSplashPackScanner {
private IrisSplashPackScanner() {
}
public static List<SplashPackMetadata> collect(File packFolder, SplashPackErrorReporter reporter) {
if (packFolder == null || !packFolder.isDirectory()) {
return List.of();
}
File[] folders = packFolder.listFiles(File::isDirectory);
if (folders == null || folders.length == 0) {
return List.of();
}
List<SplashPackMetadata> packs = new ArrayList<>(folders.length);
for (File folder : folders) {
SplashPackMetadata metadata = read(folder, reporter);
if (metadata != null) {
packs.add(metadata);
}
}
packs.sort(Comparator.comparing(SplashPackMetadata::name));
return packs;
}
public static SplashPackMetadata read(File pack, SplashPackErrorReporter reporter) {
if (pack == null || !pack.isDirectory()) {
return null;
}
String dimName = pack.getName();
File dimensionFile = new File(pack, "dimensions/" + dimName + ".json");
if (!dimensionFile.isFile()) {
return null;
}
try (FileReader reader = new FileReader(dimensionFile)) {
JsonObject json = JsonParser.parseReader(reader).getAsJsonObject();
if (!json.has("version")) {
return null;
}
return new SplashPackMetadata(dimName, json.get("version").getAsString());
} catch (IOException | JsonParseException | IllegalStateException error) {
report(reporter, "Failed to read splash metadata for dimension pack \"" + dimName + "\".", error);
return null;
}
}
private static void report(SplashPackErrorReporter reporter, String message, Throwable error) {
if (reporter == null) {
return;
}
reporter.report(message, error);
}
public record SplashPackMetadata(String name, String version) {
}
public interface SplashPackErrorReporter {
void report(String message, Throwable error);
}
}
@@ -0,0 +1,181 @@
package art.arcane.iris.core.splash;
import java.util.concurrent.ThreadLocalRandom;
public final class IrisSplashRenderer {
private static final int SPLASH_WIDTH = 53;
private static final int SPLASH_HEIGHT = 11;
private static final double SPLASH_CENTER_X = 26.0;
private static final double SPLASH_CENTER_Y = 5.0;
private static final double SPLASH_ASPECT = 2.15;
private static final double SPLASH_EYE_A = 5.6;
private static final double SPLASH_EYE_B = 4.6;
private static final SplashTone PLAIN_TONE = (glyph) -> "";
private IrisSplashRenderer() {
}
public static String[] renderPlain() {
return render(PLAIN_TONE);
}
public static String[] render(SplashTone tone) {
SplashTone splashTone = tone == null ? PLAIN_TONE : tone;
ThreadLocalRandom random = ThreadLocalRandom.current();
double headThickness = 3.7 + random.nextDouble() * 0.6;
double tipReach = 19.0 + random.nextDouble() * 4.0;
double notchX = 3.5 + random.nextDouble() * 1.5;
double[][][] spines = splashFinSpines(tipReach);
double[][][] voids = splashNotchVoids(notchX);
String[] lines = new String[SPLASH_HEIGHT];
for (int y = 0; y < SPLASH_HEIGHT; y++) {
StringBuilder line = new StringBuilder();
String activeTone = null;
for (int x = 0; x < SPLASH_WIDTH; x++) {
char glyph = splashEyeGlyph(x, y);
if (glyph == 0) {
glyph = splashFinGlyph(x, y, spines, voids, headThickness);
}
if (glyph == 0 || glyph == ' ') {
line.append(' ');
continue;
}
String glyphTone = splashTone.tone(glyph);
if (!glyphTone.isEmpty() && !glyphTone.equals(activeTone)) {
line.append(glyphTone);
activeTone = glyphTone;
}
line.append(glyph);
}
lines[y] = line.toString();
}
return lines;
}
private static char splashEyeGlyph(int x, int y) {
if (y == 5 && Math.abs(x - 26) <= 1) {
if (x == 25) {
return '(';
}
if (x == 27) {
return ')';
}
return ' ';
}
double radius = splashEyeRadius(x, y);
if (radius <= 0.50) {
return '@';
}
if (radius <= 0.72) {
return '%';
}
if (radius <= 0.88) {
return '*';
}
return 0;
}
private static char splashFinGlyph(int x, int y, double[][][] spines, double[][][] voids, double headThickness) {
if (splashEyeRadius(x, y) <= 1.26) {
return 0;
}
double voidDistance = Double.MAX_VALUE;
for (double[][] notch : voids) {
for (double[] sample : notch) {
double voidRadius = Math.max(0.25, 0.95 - 0.65 * sample[2]);
double dx = (x - sample[0]) / SPLASH_ASPECT;
double dy = y - sample[1];
double distance = Math.sqrt(dx * dx + dy * dy) / voidRadius;
if (distance < voidDistance) {
voidDistance = distance;
}
}
}
if (voidDistance < 1.0) {
return 0;
}
double best = Double.MAX_VALUE;
double bestT = 0.0;
for (double[][] spine : spines) {
for (int i = 0; i < spine.length; i++) {
double t = i / (double) (spine.length - 1);
double thickness = headThickness * Math.pow(1.0 - t, 1.3) + 0.6;
double dx = (x - spine[i][0]) / SPLASH_ASPECT;
double dy = y - spine[i][1];
double distance = Math.sqrt(dx * dx + dy * dy) / thickness;
if (distance < best) {
best = distance;
bestT = t;
}
}
}
double shade = best
+ Math.max(0.0, 3.0 - Math.min(x, SPLASH_WIDTH - 1 - x)) * 0.12
+ Math.max(0.0, 0.10 - bestT) * 1.5
+ Math.max(0.0, 1.30 - voidDistance) * 0.15;
if (shade > 1.0) {
return 0;
}
int level = shade <= 0.70 ? 0 : shade <= 0.90 ? 1 : 2;
if (bestT > 0.97) {
level += 2;
} else if (bestT > 0.88) {
level += 1;
}
return switch (Math.min(level, 2)) {
case 0 -> '#';
case 1 -> '=';
default -> '-';
};
}
private static double splashEyeRadius(int x, int y) {
double dx = (x - SPLASH_CENTER_X) / SPLASH_ASPECT;
double dy = y - SPLASH_CENTER_Y;
double nx = dx / SPLASH_EYE_A;
double ny = dy / SPLASH_EYE_B;
return Math.sqrt(nx * nx + ny * ny);
}
private static double[][][] splashNotchVoids(double notchX) {
double[][] control = {{notchX + 0.8, 12.0}, {notchX, 9.6}, {notchX - 1.4, 7.8}};
int samples = 31;
double[][] left = new double[samples][3];
double[][] right = new double[samples][3];
for (int i = 0; i < samples; i++) {
double t = i / (double) (samples - 1);
double u = 1.0 - t;
double bx = u * u * control[0][0] + 2.0 * u * t * control[1][0] + t * t * control[2][0];
double by = u * u * control[0][1] + 2.0 * u * t * control[1][1] + t * t * control[2][1];
left[i][0] = bx;
left[i][1] = by;
left[i][2] = t;
right[i][0] = (SPLASH_WIDTH - 1.0) - bx;
right[i][1] = (SPLASH_HEIGHT - 1.0) - by;
right[i][2] = t;
}
return new double[][][]{left, right};
}
private static double[][][] splashFinSpines(double tipReach) {
double[][] control = {{-1.5, 9.8}, {0.4, 4.4}, {5.2, 1.0}, {tipReach, 0.4}};
int samples = 61;
double[][] left = new double[samples][2];
double[][] right = new double[samples][2];
for (int i = 0; i < samples; i++) {
double t = i / (double) (samples - 1);
double u = 1.0 - t;
double bx = u * u * u * control[0][0] + 3.0 * u * u * t * control[1][0] + 3.0 * u * t * t * control[2][0] + t * t * t * control[3][0];
double by = u * u * u * control[0][1] + 3.0 * u * u * t * control[1][1] + 3.0 * u * t * t * control[2][1] + t * t * t * control[3][1];
left[i][0] = bx;
left[i][1] = by;
right[i][0] = (SPLASH_WIDTH - 1.0) - bx;
right[i][1] = (SPLASH_HEIGHT - 1.0) - by;
}
return new double[][][]{left, right};
}
public interface SplashTone {
String tone(char glyph);
}
}
@@ -19,13 +19,17 @@
package art.arcane.iris.spi; package art.arcane.iris.spi;
import java.util.IllegalFormatException; import java.util.IllegalFormatException;
import java.util.regex.Pattern;
public final class IrisLogging { public final class IrisLogging {
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
private static final Pattern MINI_MESSAGE_TAG = Pattern.compile("(?i)</?(?:reset|bold|b|italic|i|underlined?|u|strikethrough|st|obfuscated?|obf|black|dark_blue|dark_green|dark_aqua|dark_red|dark_purple|gold|gray|dark_gray|blue|green|aqua|red|light_purple|yellow|white|gradient|font|hover|click|rainbow)(?::[^>\\n]{0,96})?>|<#[0-9a-f]{6}>");
private IrisLogging() { private IrisLogging() {
} }
public static void info(String format, Object... args) { public static void info(String format, Object... args) {
emit(LogLevel.INFO, safeFormat(format, args)); emit(LogLevel.INFO, format(format, args));
} }
public static void debug(String message) { public static void debug(String message) {
@@ -33,11 +37,11 @@ public final class IrisLogging {
} }
public static void warn(String format, Object... args) { public static void warn(String format, Object... args) {
emit(LogLevel.WARN, safeFormat(format, args)); emit(LogLevel.WARN, format(format, args));
} }
public static void error(String format, Object... args) { public static void error(String format, Object... args) {
emit(LogLevel.ERROR, safeFormat(format, args)); emit(LogLevel.ERROR, format(format, args));
} }
public static void msg(String message) { public static void msg(String message) {
@@ -46,7 +50,7 @@ public final class IrisLogging {
return; return;
} }
System.out.println("[Iris] " + message); System.out.println("[Iris] " + clean(message));
} }
public static void reportError(String context, Throwable error) { public static void reportError(String context, Throwable error) {
@@ -76,21 +80,7 @@ public final class IrisLogging {
} }
} }
private static void emit(LogLevel level, String message) { public static String format(String format, Object... args) {
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().log(level, message);
return;
}
if (level == LogLevel.WARN || level == LogLevel.ERROR) {
System.err.println("[Iris/" + level + "] " + message);
return;
}
System.out.println("[Iris/" + level + "] " + message);
}
private static String safeFormat(String format, Object... args) {
if (format == null) { if (format == null) {
return "null"; return "null";
} }
@@ -105,4 +95,29 @@ public final class IrisLogging {
return format; return format;
} }
} }
public static String clean(String message) {
if (message == null) {
return "null";
}
String legacyStripped = LEGACY_COLOR.matcher(message).replaceAll("");
return MINI_MESSAGE_TAG.matcher(legacyStripped).replaceAll("");
}
private static void emit(LogLevel level, String message) {
LogLevel target = level == null ? LogLevel.INFO : level;
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().log(target, message);
return;
}
String output = clean(message);
if (target == LogLevel.WARN || target == LogLevel.ERROR) {
System.err.println("[Iris/" + target + "] " + output);
return;
}
System.out.println("[Iris/" + target + "] " + output);
}
} }