d
This commit is contained in:
Brian Neumann-Fopiano
2026-06-13 17:07:27 -04:00
parent f482b8ef7e
commit ad6732812e
19 changed files with 1050 additions and 337 deletions
@@ -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;
}
}
@@ -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.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<Throwable> 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);
}
}
@@ -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<CommandSourceStack> 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<CommandSourceStack> 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);
}
@@ -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<UUID, Long> 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)) {
@@ -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<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("version", "[pack]", "Print a pack version"),
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("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<String> 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<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) {
@@ -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
@@ -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<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(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(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 <pack> to prepare the pack workflow instead."));
root.then(message("stp", "There are no temporary Bukkit studio worlds on modded servers to teleport to; use /iris studio open <pack> to prepare the pack workflow instead."));
root.then(message("vscode", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor."));
root.then(message("vsc", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/<pack> directly in your editor."));
root.then(message("update", "Workspace regeneration (.code-workspace + JSON schemas) reads Bukkit registries (SchemaBuilder); run /iris studio update on a Bukkit server against this pack."));
@@ -128,6 +131,20 @@ public final class ModdedStudioCommands {
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) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> 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) {