diff --git a/.gitignore b/.gitignore index f12ad69c5..0ec004470 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ dist/ core/plugins/ adapters/bukkit/plugin/plugins/ adapters/*/run/ + +.codegraph/ + +plans/ diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java index f780ace3f..614abc6b6 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -21,10 +21,7 @@ package art.arcane.iris; import art.arcane.iris.engine.IrisWorldManager; import art.arcane.iris.engine.framework.EngineWorldManagerProvider; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; +import art.arcane.iris.core.splash.IrisSplashPackScanner; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisWorlds; 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.engine.platform.PlatformChunkGenerator; import art.arcane.iris.platform.bukkit.BukkitPlatform; +import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.LogLevel; import art.arcane.volmlib.integration.ReloadAware; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -235,7 +234,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { getSender().sendMessage(string); } catch (Throwable e) { try { - instance.getLogger().info(instance.getTag() + string.replaceAll("(<([^>]+)>)", "")); + instance.getLogger().info(instance.getTag() + IrisLogging.clean(string)); } catch (Throwable inner) { System.err.println("[Iris] Failed to emit log message: " + inner.getMessage()); inner.printStackTrace(System.err); @@ -377,19 +376,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } private static String safeFormat(String format, Object... args) { - if (format == null) { - return "null"; - } - - if (args == null || args.length == 0) { - return format; - } - - try { - return String.format(format, args); - } catch (IllegalFormatException ignored) { - return format; - } + return IrisLogging.format(format, args); } public static void later(NastyRunnable object) { @@ -555,14 +542,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { BukkitPlatform.hostPlugin(this); BukkitPlatform.hostConsoleSender(Iris::getSender); BukkitPlatform.hostBridge(new BukkitPlatform.HostBridge( - (level, message) -> { - switch (level) { - case DEBUG -> Iris.debug(message); - case INFO -> Iris.info(message); - case WARN -> Iris.warn(message); - case ERROR -> Iris.error(message); - } - }, + Iris::bridgeLog, Iris::msg, Iris::reportError, (event) -> Iris.callEvent((org.bukkit.event.Event) event), @@ -574,6 +554,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { 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() { PaperLibBootstrap.install(); SimdSupport.install(); @@ -1279,49 +1269,24 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { } static List collectSplashPacks(File packFolder) { - if (packFolder == null || !packFolder.isDirectory()) { + List scanned = IrisSplashPackScanner.collect(packFolder, Iris::reportError); + if (scanned.isEmpty()) { return Collections.emptyList(); } - File[] folders = packFolder.listFiles(File::isDirectory); - if (folders == null || folders.length == 0) { - return Collections.emptyList(); + List packs = new ArrayList<>(scanned.size()); + for (IrisSplashPackScanner.SplashPackMetadata metadata : scanned) { + packs.add(new SplashPackMetadata(metadata.name(), metadata.version())); } - - List 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; } static SplashPackMetadata readSplashPack(File pack) { - 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 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); + IrisSplashPackScanner.SplashPackMetadata metadata = IrisSplashPackScanner.read(pack, Iris::reportError); + if (metadata == null) { return null; } + return new SplashPackMetadata(metadata.name(), metadata.version()); } private void printPack(SplashPackMetadata pack) { diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java index ba34383d2..918f1a4a8 100644 --- a/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java @@ -20,6 +20,7 @@ package art.arcane.iris.fabric; import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedIrisLog; import art.arcane.iris.modded.ModdedParityProbe; import art.arcane.iris.modded.ModdedWorldCheck; 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.level.Level; import net.minecraft.world.phys.BlockHitResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public final class IrisFabricBootstrap implements ModInitializer { - private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); - @Override public void onInitialize() { ModdedEngineBootstrap.initialize(new FabricModdedLoader()); @@ -65,12 +62,12 @@ public final class IrisFabricBootstrap implements ModInitializer { String minecraftVersion = loader.getModContainer("minecraft") .map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString()) .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.bind(); 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) -> { ModdedObjectUndo.clearAll(); ModdedWandService.clearAll(); @@ -85,13 +82,13 @@ public final class IrisFabricBootstrap implements ModInitializer { String parity = System.getProperty("iris.parity"); if (parity != null) { - LOGGER.info("Iris parity probe armed: {}", parity); + ModdedIrisLog.info("Iris parity probe armed: " + parity); ModdedParityProbe.schedule(parity); } String worldCheck = System.getProperty("iris.worldcheck"); if (worldCheck != null) { - LOGGER.info("Iris world check armed"); + ModdedIrisLog.info("Iris world check armed"); ModdedWorldCheck.schedule(); } } diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java index fbf382e67..9a6430df1 100644 --- a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java @@ -20,6 +20,7 @@ package art.arcane.iris.forge; import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedIrisLog; import art.arcane.iris.modded.ModdedParityProbe; import art.arcane.iris.modded.ModdedWorldCheck; import art.arcane.iris.modded.ModdedWorldEngines; @@ -42,20 +43,16 @@ import net.minecraftforge.fml.loading.VersionInfo; import net.minecraftforge.registries.DeferredRegister; import java.util.function.Predicate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Mod("irisworldgen") public final class IrisForgeBootstrap { - private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); - public IrisForgeBootstrap(FMLJavaModLoadingContext context) { ModdedEngineBootstrap.initialize(new ForgeModdedLoader()); String modVersion = ModList.getModContainerById("irisworldgen") .map((ModContainer container) -> container.getModInfo().getVersion().toString()) .orElse("unknown"); 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.bind(); @@ -63,7 +60,7 @@ public final class IrisForgeBootstrap { DeferredRegister> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen"); chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC); 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) -> { ModdedObjectUndo.clearAll(); @@ -79,13 +76,13 @@ public final class IrisForgeBootstrap { String parity = System.getProperty("iris.parity"); if (parity != null) { - LOGGER.info("Iris parity probe armed: {}", parity); + ModdedIrisLog.info("Iris parity probe armed: " + parity); ModdedParityProbe.schedule(parity); } String worldCheck = System.getProperty("iris.worldcheck"); if (worldCheck != null) { - LOGGER.info("Iris world check armed"); + ModdedIrisLog.info("Iris world check armed"); ModdedWorldCheck.schedule(); } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java index 8b4804893..97727e69d 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedEngineBootstrap.java @@ -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"); } - LOGGER.info("Iris core loaded ({} classes ok)", loadedClasses); + ModdedIrisLog.info("Iris core loaded (" + loadedClasses + " classes ok)"); } public static ModdedPlatform bind() { @@ -105,6 +105,7 @@ public final class ModdedEngineBootstrap { IrisServices.register(PreservationRegistry.class, new InertPreservation()); IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager()); platform = created; + ModdedIrisSplash.print(boundLoader); return created; } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java new file mode 100644 index 000000000..1dc7017fd --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisLog.java @@ -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 . + */ + +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); + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java new file mode 100644 index 000000000..fe0ca422a --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedIrisSplash.java @@ -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 . + */ + +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 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; + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java index c7600abd7..521c39614 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java @@ -26,14 +26,11 @@ import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.spi.PlatformScheduler; import art.arcane.iris.spi.PlatformStructureHooks; import net.minecraft.server.MinecraftServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.File; import java.util.function.Consumer; public final class ModdedPlatform implements IrisPlatform { - private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static volatile Consumer ERROR_SINK = null; private final ModdedLoader loader; @@ -135,17 +132,12 @@ public final class ModdedPlatform implements IrisPlatform { @Override public void log(LogLevel level, String message) { - switch (level) { - case DEBUG -> LOGGER.debug(message); - case INFO -> LOGGER.info(message); - case WARN -> LOGGER.warn(message); - case ERROR -> LOGGER.error(message); - } + ModdedIrisLog.log(level, message); } @Override public void msg(String message) { - LOGGER.info(message); + ModdedIrisLog.info(message); } @Override @@ -156,7 +148,7 @@ public final class ModdedPlatform implements IrisPlatform { return; } if (error != null) { - LOGGER.error("Iris reported error", error); + ModdedIrisLog.error("Iris reported error", error); } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java index 0a77a6801..9b6a0bd5f 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java @@ -28,6 +28,7 @@ import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.ModdedEngineBootstrap; import art.arcane.iris.modded.ModdedLoader; import art.arcane.iris.modded.ModdedPackInstaller; +import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.math.Position2; import com.mojang.brigadier.CommandDispatcher; @@ -42,6 +43,7 @@ import com.mojang.brigadier.tree.LiteralCommandNode; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; import net.minecraft.core.BlockPos; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.MinecraftServer; @@ -84,7 +86,7 @@ public final class IrisModdedCommands { LiteralCommandNode root = dispatcher.register(rootTree()); dispatcher.register(Commands.literal("ir").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 rootTree() { @@ -322,7 +324,7 @@ public final class IrisModdedCommands { } private static int pregenStatus(CommandSourceStack source) { - String status = ModdedPregenJob.status(); + Component status = ModdedPregenJob.statusComponent(); if (status == null) { fail(source, "No active pregeneration task."); return 0; @@ -747,6 +749,10 @@ public final class IrisModdedCommands { ModdedCommandFeedback.ok(source, message); } + static void ok(CommandSourceStack source, Component component) { + ModdedCommandFeedback.ok(source, component); + } + static void fail(CommandSourceStack source, String message) { ModdedCommandFeedback.fail(source, message); } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java index c5a39cad1..22491ab75 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java @@ -20,7 +20,12 @@ package art.arcane.iris.modded.command; import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.ClickEvent; 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.ServerPlayer; import net.minecraft.sounds.SoundEvents; @@ -31,6 +36,25 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; 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 TAB_SOUND_COOLDOWN_MS = 175L; private static final Map MESSAGE_SOUNDS = new ConcurrentHashMap<>(); @@ -44,6 +68,11 @@ final class ModdedCommandFeedback { playSuccess(source); } + static void ok(CommandSourceStack source, Component component) { + source.sendSuccess(() -> component, false); + playSuccess(source); + } + static void fail(CommandSourceStack source, String message) { source.sendFailure(Component.literal(message).withStyle(ChatFormatting.RED)); playFailure(source); @@ -53,6 +82,61 @@ final class ModdedCommandFeedback { 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) { ServerPlayer player = source.getPlayer(); if (player == null || !claim(TAB_SOUNDS, player.getUUID(), TAB_SOUND_COOLDOWN_MS)) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java index 9915e765c..ad720ba14 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java @@ -18,17 +18,32 @@ package art.arcane.iris.modded.command; -import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; 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 { private static final Map> SECTIONS = new LinkedHashMap<>(); @@ -89,7 +104,7 @@ final class ModdedCommandHelp { Entry.command("package", "[pack]", "Package a dimension into a compressed format"), Entry.command("version", "[pack]", "Print a pack version"), Entry.command("regions", "[radius]", "Calculate nearby region distribution"), - Entry.command("open", "", "Explain modded studio workflow"), + Entry.command("open", " [seed]", "Open or prepare a dimension pack studio workflow"), Entry.command("close", "", "Explain modded studio workflow"), Entry.command("vscode", "", "Explain editor workflow"), Entry.command("update", "", "Explain workspace regeneration workflow"), @@ -143,53 +158,187 @@ final class ModdedCommandHelp { return 0; } - sendHeader(source, normalized.isEmpty() ? "Iris Commands" : "/iris " + normalized); + ModdedCommandFeedback.clear(source); + + sendHeader(source, normalized); 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) { 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; } - private static void sendHeader(CommandSourceStack source, String title) { - MutableComponent header = Component.literal("========== ").withStyle(ChatFormatting.DARK_GREEN, ChatFormatting.STRIKETHROUGH) - .append(Component.literal(" " + title + " ").withStyle(ChatFormatting.GREEN, ChatFormatting.BOLD)) - .append(Component.literal(" ==========").withStyle(ChatFormatting.DARK_GREEN, ChatFormatting.STRIKETHROUGH)); - ModdedCommandFeedback.send(source, header); + private static void sendHeader(CommandSourceStack source, String path) { + String title = path.isEmpty() ? "/iris" : "/iris " + path; + ModdedCommandFeedback.send(source, ModdedCommandFeedback.header(title)); + } + + 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) { - String basePath = path.isEmpty() ? "" : " " + path; - String command = "/iris" + basePath + " " + entry.name(); - String suggestion = entry.usage().isBlank() ? command : command + " " + entry.usage(); - 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)); + MutableComponent row = Component.empty(); + row.append(clickableCommand(path, entry)); + row.append(nodes(entry)); return row; } - private static MutableComponent clickable(String label, String command, String suggestion, String hover, boolean run) { - Component hoverText = Component.literal(hover).withStyle(ChatFormatting.GREEN); - if (run) { - return Component.literal(label).withStyle((style) -> style - .withColor(ChatFormatting.AQUA) - .withBold(true) - .withClickEvent(new ClickEvent.RunCommand(command)) - .withHoverEvent(new HoverEvent.ShowText(hoverText))); + private static MutableComponent clickableCommand(String path, Entry entry) { + String parent = path.isEmpty() ? "/iris" : "/iris " + path; + String command = parent + " " + entry.name(); + String suggestion = entry.usage().isBlank() ? command : command + " " + entry.usage(); + ClickEvent clickEvent = entry.group() ? new ClickEvent.RunCommand(command) : new ClickEvent.SuggestCommand(suggestion); + MutableComponent hover = entryHover(path, entry, suggestion); + MutableComponent display = Component.empty(); + 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 - .withColor(ChatFormatting.GREEN) - .withClickEvent(new ClickEvent.SuggestCommand(suggestion)) - .withHoverEvent(new HoverEvent.ShowText(hoverText))); + List tokens = usageTokens(entry.usage()); + if (tokens.isEmpty()) { + return Component.empty(); + } + + 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 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 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 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) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java index 0c4f5d5bf..96faf9b21 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenJob.java @@ -24,6 +24,8 @@ import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.engine.framework.Engine; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.math.Position2; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; import net.minecraft.server.level.ServerLevel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -103,16 +105,63 @@ public final class ModdedPregenJob implements PregenListener { if (job == null) { return null; } - long total = Math.max(1L, job.totalChunks); - double percent = ((double) job.generated / (double) total) * 100D; - return "Pregen " + job.dimension + ": " - + Form.f(job.generated) + "/" + Form.f(job.totalChunks) + return job.statusText(); + } + + 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) + "%), " - + Form.f((int) job.chunksPerSecond) + "/s" - + ", ETA " + Form.duration(job.eta, 2) - + ", elapsed " + Form.duration(job.elapsed, 2) - + ", method " + job.method - + (job.pregenerator.paused() ? ", PAUSED" : ""); + + Form.f((int) chunksPerSecond) + "/s" + + ", ETA " + Form.duration(eta, 2) + + ", elapsed " + Form.duration(elapsed, 2) + + ", method " + method + + (pregenerator.paused() ? ", PAUSED" : ""); + } + + private double percent() { + long total = Math.max(1L, totalChunks); + return ((double) generated / (double) total) * 100D; } @Override diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java index 2fd0124f8..f40c08d01 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java @@ -38,11 +38,14 @@ import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.Spiraler; import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.LongArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import net.minecraft.commands.CommandSourceStack; 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.level.ServerPlayer; import org.slf4j.Logger; @@ -102,12 +105,12 @@ public final class ModdedStudioCommands { .then(Commands.argument("radius", IntegerArgumentType.integer(8, 1000)) .executes((CommandContext 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/ and create a fresh world (or /iris regen) to see changes.")); - 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/ and create a fresh world (or /iris regen) to see changes.")); - 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("x", "There are no studio worlds on modded servers (/iris studio open is Bukkit-only), 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("stp", "There are no studio worlds on modded servers to teleport to; /iris studio open is Bukkit-only.")); + root.then(openTree("open")); + root.then(openTree("o")); + root.then(message("close", "There are no studio worlds on modded servers (/iris studio open prepares the pack workflow instead), so there is nothing to close.")); + root.then(message("x", "There are no studio worlds on modded servers (/iris studio open prepares the pack workflow instead), so there is nothing to close.")); + root.then(message("tpstudio", "There are no temporary Bukkit studio worlds on modded servers to teleport to; use /iris studio open to prepare the pack workflow instead.")); + root.then(message("stp", "There are no temporary Bukkit studio worlds on modded servers to teleport to; use /iris studio open 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/ 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/ 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.")); @@ -128,6 +131,20 @@ public final class ModdedStudioCommands { return root; } + private static LiteralArgumentBuilder openTree(String name) { + return Commands.literal(name) + .executes((CommandContext context) -> openHelp(context.getSource())) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> open(context.getSource(), StringArgumentType.getString(context, "pack"), 1337L)) + .then(Commands.argument("seed", LongArgumentType.longArg()) + .executes((CommandContext 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 [seed]"); + return 0; + } + private static LiteralArgumentBuilder message(String name, String text) { return Commands.literal(name) .executes((CommandContext context) -> { @@ -159,6 +176,52 @@ public final class ModdedStudioCommands { 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) { File folder = resolvePack(source, pack); if (folder == null) { diff --git a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java index 458b4f6b7..391361423 100644 --- a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java +++ b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java @@ -20,6 +20,7 @@ package art.arcane.iris.neoforge; import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.ModdedEngineBootstrap; +import art.arcane.iris.modded.ModdedIrisLog; import art.arcane.iris.modded.ModdedParityProbe; import art.arcane.iris.modded.ModdedWorldCheck; 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.tick.ServerTickEvent; import net.neoforged.neoforge.registries.DeferredRegister; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Mod("irisworldgen") public final class IrisNeoForgeBootstrap { - private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); - public IrisNeoForgeBootstrap(IEventBus modBus) { ModdedEngineBootstrap.initialize(new NeoForgeModdedLoader()); String modVersion = ModList.get().getModContainerById("irisworldgen") .map((ModContainer container) -> container.getModInfo().getVersion().toString()) .orElse("unknown"); 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.bind(); @@ -63,7 +60,7 @@ public final class IrisNeoForgeBootstrap { DeferredRegister> chunkGenerators = DeferredRegister.create(Registries.CHUNK_GENERATOR, "irisworldgen"); chunkGenerators.register("iris", () -> IrisModdedChunkGenerator.CODEC); 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) -> { ModdedObjectUndo.clearAll(); @@ -86,13 +83,13 @@ public final class IrisNeoForgeBootstrap { String parity = System.getProperty("iris.parity"); if (parity != null) { - LOGGER.info("Iris parity probe armed: {}", parity); + ModdedIrisLog.info("Iris parity probe armed: " + parity); ModdedParityProbe.schedule(parity); } String worldCheck = System.getProperty("iris.worldcheck"); if (worldCheck != null) { - LOGGER.info("Iris world check armed"); + ModdedIrisLog.info("Iris world check armed"); ModdedWorldCheck.schedule(); } } diff --git a/build.gradle b/build.gradle index 0c14210b1..5e496edc7 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,9 @@ String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${lo String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}") String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}") extensions.extraProperties.set('bukkitArtifactName', bukkitArtifactName) +extensions.extraProperties.set('fabricArtifactName', fabricArtifactName) +extensions.extraProperties.set('forgeArtifactName', forgeArtifactName) +extensions.extraProperties.set('neoForgeArtifactName', neoForgeArtifactName) 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') // ========================== UNIX ============================== 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('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') { group = 'iris' dependsOn('buildBukkit', 'buildFabric', 'buildForge', 'buildNeoforge', ':spi:jar') @@ -375,14 +392,15 @@ void registerCustomOutputTask(String name, String path) { return } - tasks.register("build${name}", Copy) { - group = 'development' - outputs.upToDateWhen { false } - dependsOn('iris') - from(layout.buildDirectory.file((String) project.ext.bukkitArtifactName)) - into(file(path)) - rename { String ignored -> 'Iris.jar' } + createCustomOutputTasks(name, path) +} + +void registerCustomOutputTask(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 registerCustomOutputTaskUnix(String name, String path) { @@ -390,6 +408,44 @@ void registerCustomOutputTaskUnix(String name, String path) { 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) { group = 'development' outputs.upToDateWhen { false } @@ -397,5 +453,31 @@ void registerCustomOutputTaskUnix(String name, String path) { from(layout.buildDirectory.file((String) project.ext.bukkitArtifactName)) into(file(path)) 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") + ) + } } } diff --git a/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java b/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java index 8a198d47b..fd398c015 100644 --- a/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java +++ b/core/src/main/java/art/arcane/iris/core/safeguard/Mode.java @@ -2,6 +2,7 @@ package art.arcane.iris.core.safeguard; import art.arcane.iris.BuildConstants; import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.splash.IrisSplashRenderer; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.util.common.format.C; @@ -11,21 +12,12 @@ import org.bukkit.Bukkit; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Locale; -import java.util.concurrent.ThreadLocalRandom; public enum Mode { STABLE(C.IRIS, C.AQUA), WARNING(C.GOLD, C.YELLOW), 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 glow; private final String id; @@ -70,7 +62,7 @@ public enum Mode { String startupDate = getStartupDate(); int javaVersion = getJavaVersion(); - String[] splash = renderSplashLogo(); + String[] splash = IrisSplashRenderer.render(this::splashTone); String[] info = new String[]{ "", @@ -94,38 +86,6 @@ public enum Mode { 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) { return switch (glyph) { 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) { return C.BOLD.toString() + C.DARK_GRAY + "[" + C.BOLD + color + tag + C.BOLD + C.DARK_GRAY + "]" + C.RESET; } diff --git a/core/src/main/java/art/arcane/iris/core/splash/IrisSplashPackScanner.java b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashPackScanner.java new file mode 100644 index 000000000..eec101de8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashPackScanner.java @@ -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 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 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); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/splash/IrisSplashRenderer.java b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashRenderer.java new file mode 100644 index 000000000..6a01a8428 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/splash/IrisSplashRenderer.java @@ -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); + } +} diff --git a/spi/src/main/java/art/arcane/iris/spi/IrisLogging.java b/spi/src/main/java/art/arcane/iris/spi/IrisLogging.java index f32327d37..5925e7033 100644 --- a/spi/src/main/java/art/arcane/iris/spi/IrisLogging.java +++ b/spi/src/main/java/art/arcane/iris/spi/IrisLogging.java @@ -19,13 +19,17 @@ package art.arcane.iris.spi; import java.util.IllegalFormatException; +import java.util.regex.Pattern; 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)\\n]{0,96})?>|<#[0-9a-f]{6}>"); + private IrisLogging() { } 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) { @@ -33,11 +37,11 @@ public final class IrisLogging { } 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) { - emit(LogLevel.ERROR, safeFormat(format, args)); + emit(LogLevel.ERROR, format(format, args)); } public static void msg(String message) { @@ -46,7 +50,7 @@ public final class IrisLogging { return; } - System.out.println("[Iris] " + message); + System.out.println("[Iris] " + clean(message)); } public static void reportError(String context, Throwable error) { @@ -76,21 +80,7 @@ public final class IrisLogging { } } - private static void emit(LogLevel level, String message) { - 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) { + public static String format(String format, Object... args) { if (format == null) { return "null"; } @@ -105,4 +95,29 @@ public final class IrisLogging { 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); + } }