mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
d
This commit is contained in:
43 files changed
+1308
-279
No files matched your search
@@ -30,6 +30,8 @@ import lombok.Data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Data
|
||||
public class IrisSettings {
|
||||
@@ -136,6 +138,46 @@ public class IrisSettings {
|
||||
}
|
||||
}
|
||||
|
||||
public static IrisSettings installHotloadSnapshot(String rawJson) {
|
||||
IrisSettings parsed = parseHotloadSnapshot(rawJson);
|
||||
synchronized (SETTINGS_LOCK) {
|
||||
settings = parsed;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public static boolean applyHotloadSnapshot(String rawJson, Predicate<IrisSettings> candidateActivation) {
|
||||
Objects.requireNonNull(candidateActivation, "candidateActivation");
|
||||
IrisSettings parsed = parseHotloadSnapshot(rawJson);
|
||||
if (!candidateActivation.test(parsed)) {
|
||||
return false;
|
||||
}
|
||||
synchronized (SETTINGS_LOCK) {
|
||||
settings = parsed;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static IrisSettings parseHotloadSnapshot(String rawJson) {
|
||||
if (rawJson == null || rawJson.isBlank()) {
|
||||
throw new IllegalArgumentException("Iris settings snapshot is empty");
|
||||
}
|
||||
|
||||
IrisSettings parsed;
|
||||
try {
|
||||
parsed = new Gson().fromJson(rawJson, IrisSettings.class);
|
||||
if (parsed == null) {
|
||||
throw new IllegalArgumentException("Iris settings snapshot did not contain an object");
|
||||
}
|
||||
migrateLegacyKeys(parsed, rawJson);
|
||||
} catch (RuntimeException failure) {
|
||||
throw new IllegalArgumentException("Iris settings snapshot is invalid", failure);
|
||||
}
|
||||
|
||||
parsed.fillMissingSections();
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public void forceSave() {
|
||||
File s = IrisPlatforms.get().dataFile("settings.json");
|
||||
|
||||
@@ -147,6 +189,20 @@ public class IrisSettings {
|
||||
}
|
||||
}
|
||||
|
||||
private void fillMissingSections() {
|
||||
general = general == null ? new IrisSettingsGeneral() : general;
|
||||
world = world == null ? new IrisSettingsWorld() : world;
|
||||
gui = gui == null ? new IrisSettingsGUI() : gui;
|
||||
autoConfiguration = autoConfiguration == null ? new IrisSettingsAutoconfiguration() : autoConfiguration;
|
||||
generator = generator == null ? new IrisSettingsGenerator() : generator;
|
||||
concurrency = concurrency == null ? new IrisSettingsConcurrency() : concurrency;
|
||||
studio = studio == null ? new IrisSettingsStudio() : studio;
|
||||
performance = performance == null ? new IrisSettingsPerformance() : performance;
|
||||
pregen = pregen == null ? new IrisSettingsPregen() : pregen;
|
||||
sentry = sentry == null ? new IrisSettingsSentry() : sentry;
|
||||
treeFeller = treeFeller == null ? new IrisSettingsTreeFeller() : treeFeller;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IrisSettingsAutoconfiguration {
|
||||
public boolean configureSpigotTimeoutTime = true;
|
||||
@@ -273,6 +329,11 @@ public class IrisSettings {
|
||||
public boolean splashLogoStartup = true;
|
||||
public boolean useConsoleCustomColors = true;
|
||||
public boolean useCustomColorsIngame = true;
|
||||
/**
|
||||
* Boss bar progress loaders for jobs, studio opens, world creation, chunk jobs and pack
|
||||
* downloads. Turning this off keeps the action bar progress line; only the bar goes away.
|
||||
*/
|
||||
public boolean progressBossBar = true;
|
||||
public boolean adjustVanillaHeight = false;
|
||||
public boolean autoIngestDatapacks = true;
|
||||
/**
|
||||
|
||||
@@ -28,19 +28,32 @@ import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class IrisLanguage {
|
||||
private static final long MAX_LOCALE_BYTES = 2L * 1024L * 1024L;
|
||||
private static final long HOTLOAD_CONTENT_STABILITY_NANOS = TimeUnit.MILLISECONDS.toNanos(250L);
|
||||
private static final long HOTLOAD_DELETION_GRACE_NANOS = TimeUnit.SECONDS.toNanos(3L);
|
||||
private static final long HOTLOAD_COOLDOWN_NANOS = TimeUnit.SECONDS.toNanos(3L);
|
||||
private static final int MAX_REPORTED_ISSUES = 12;
|
||||
private static final Pattern LOCALE_NAME = Pattern.compile("[A-Za-z0-9_-]+");
|
||||
private static final Pattern LEGACY_COLOR = Pattern.compile("(?i)\\u00a7[0-9A-FK-ORX]");
|
||||
@@ -55,10 +68,17 @@ public final class IrisLanguage {
|
||||
* new snapshot and the whole memo is discarded. Bounded by the catalog size.
|
||||
*/
|
||||
private static final AtomicReference<PlainMemo> PLAIN_MEMO = new AtomicReference<>(null);
|
||||
private static final LocaleHotloadGate HOTLOAD_GATE = new LocaleHotloadGate(
|
||||
new LocaleHotloadGate.Timing(
|
||||
HOTLOAD_CONTENT_STABILITY_NANOS,
|
||||
HOTLOAD_DELETION_GRACE_NANOS,
|
||||
HOTLOAD_COOLDOWN_NANOS
|
||||
)
|
||||
);
|
||||
|
||||
private static volatile File dataFolder;
|
||||
private static volatile File watchedFile;
|
||||
private static volatile long watchedSignature = Long.MIN_VALUE;
|
||||
private static volatile String lastCaptureFailureKey;
|
||||
private static volatile String lastInvalidHotloadLocale;
|
||||
private static volatile String activeLocale = CATALOG.englishLocale();
|
||||
|
||||
private IrisLanguage() {
|
||||
@@ -82,6 +102,20 @@ public final class IrisLanguage {
|
||||
return reload(root, configuredLocale());
|
||||
}
|
||||
|
||||
public static synchronized boolean reload(IrisSettings candidate) {
|
||||
IrisSettings resolvedCandidate = Objects.requireNonNull(candidate, "Candidate settings cannot be null");
|
||||
File root = dataFolder;
|
||||
if (root == null && IrisPlatforms.isBound()) {
|
||||
root = IrisPlatforms.get().dataFolder();
|
||||
}
|
||||
if (root == null) {
|
||||
return false;
|
||||
}
|
||||
IrisSettings.IrisSettingsGeneral general = resolvedCandidate.getGeneral();
|
||||
String locale = general == null ? CATALOG.englishLocale() : general.getLanguage();
|
||||
return reload(root, locale);
|
||||
}
|
||||
|
||||
public static synchronized boolean reload(File root, String locale) {
|
||||
File resolvedRoot = root == null ? null : root.getAbsoluteFile();
|
||||
if (resolvedRoot == null) {
|
||||
@@ -92,16 +126,26 @@ public final class IrisLanguage {
|
||||
requestedLocale = normalizeLocale(locale);
|
||||
} catch (RuntimeException exception) {
|
||||
dataFolder = resolvedRoot;
|
||||
HOTLOAD_GATE.reset(null);
|
||||
IrisLogging.error("Rejected locale setting '" + locale + "'; continuing with " + activeLocale + ".");
|
||||
IrisLogging.reportError(exception);
|
||||
exception.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
LocalizationReloadResult result = MANAGER.reload(() -> loadCandidate(resolvedRoot, requestedLocale));
|
||||
dataFolder = resolvedRoot;
|
||||
|
||||
File override = overrideFile(resolvedRoot, requestedLocale);
|
||||
watchedFile = override;
|
||||
watchedSignature = signature(override);
|
||||
SnapshotCapture capture = captureForReload(override, requestedLocale);
|
||||
LocalizationReloadResult result;
|
||||
if (capture.failure() == null) {
|
||||
result = MANAGER.reload(() -> loadCandidate(resolvedRoot, requestedLocale, capture.snapshot()));
|
||||
} else {
|
||||
result = MANAGER.reload(() -> {
|
||||
throw capture.failure();
|
||||
});
|
||||
}
|
||||
dataFolder = resolvedRoot;
|
||||
HOTLOAD_GATE.reset(capture.snapshot());
|
||||
lastCaptureFailureKey = null;
|
||||
if (!result.applied()) {
|
||||
reportRejectedReload(requestedLocale, result);
|
||||
return false;
|
||||
@@ -123,14 +167,47 @@ public final class IrisLanguage {
|
||||
try {
|
||||
locale = normalizeLocale(configuredLocale());
|
||||
} catch (RuntimeException exception) {
|
||||
return reload(root, configuredLocale());
|
||||
String invalidLocale = configuredLocale();
|
||||
if (Objects.equals(lastInvalidHotloadLocale, invalidLocale)) {
|
||||
return false;
|
||||
}
|
||||
lastInvalidHotloadLocale = invalidLocale;
|
||||
return reload(root, invalidLocale);
|
||||
}
|
||||
lastInvalidHotloadLocale = null;
|
||||
File expected = overrideFile(root, locale);
|
||||
long signature = signature(expected);
|
||||
if (expected.equals(watchedFile) && signature == watchedSignature) {
|
||||
LocaleHotloadSnapshot snapshot;
|
||||
try {
|
||||
snapshot = captureHotloadSnapshot(expected, locale);
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
HOTLOAD_GATE.unavailable();
|
||||
reportCaptureFailure(expected, failure);
|
||||
return false;
|
||||
}
|
||||
if (snapshot == null) {
|
||||
HOTLOAD_GATE.unavailable();
|
||||
return true;
|
||||
}
|
||||
return reload(root, locale);
|
||||
lastCaptureFailureKey = null;
|
||||
|
||||
LocaleHotloadGate.Attempt attempt = HOTLOAD_GATE.observe(snapshot, System.nanoTime());
|
||||
if (attempt == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LocalizationReloadResult result = MANAGER.reload(() -> loadCandidate(root, locale, attempt.snapshot()));
|
||||
boolean applied = result.applied();
|
||||
HOTLOAD_GATE.complete(attempt, System.nanoTime(), applied);
|
||||
if (!applied) {
|
||||
reportRejectedReload(locale, result);
|
||||
return false;
|
||||
}
|
||||
|
||||
activeLocale = locale;
|
||||
int warnings = result.validation().warnings().size();
|
||||
IrisLogging.info("Loaded locale " + locale + " with " + warnings + " fallback "
|
||||
+ (warnings == 1 ? "entry" : "entries") + ".");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String activeLocale() {
|
||||
@@ -229,13 +306,16 @@ public final class IrisLanguage {
|
||||
throw new IllegalArgumentException("Unsupported Iris message key: " + key.id());
|
||||
}
|
||||
|
||||
private static LocalizationCandidate loadCandidate(File root, String locale) throws Exception {
|
||||
private static LocalizationCandidate loadCandidate(
|
||||
File root,
|
||||
String locale,
|
||||
LocaleHotloadSnapshot snapshot
|
||||
) throws Exception {
|
||||
File folder = new File(root, "languages/overrides");
|
||||
Files.createDirectories(folder.toPath());
|
||||
List<LocaleOverlay> overlays = new ArrayList<>(2);
|
||||
File override = overrideFile(root, locale);
|
||||
if (override.exists()) {
|
||||
overlays.add(loadFileOverlay(override, locale));
|
||||
if (!snapshot.missing()) {
|
||||
overlays.add(parseOverlay(snapshot.file().getPath(), locale, snapshot.content()));
|
||||
}
|
||||
|
||||
if (!CATALOG.englishLocale().equals(locale)) {
|
||||
@@ -269,17 +349,6 @@ public final class IrisLanguage {
|
||||
}
|
||||
}
|
||||
|
||||
private static LocaleOverlay loadFileOverlay(File override, String locale) throws Exception {
|
||||
if (!override.isFile()) {
|
||||
throw new IllegalArgumentException("Locale override is not a regular file: " + override.getPath());
|
||||
}
|
||||
if (override.length() > MAX_LOCALE_BYTES) {
|
||||
throw new IllegalArgumentException("Locale override is too large: " + override.getPath());
|
||||
}
|
||||
String raw = Files.readString(override.toPath(), StandardCharsets.UTF_8);
|
||||
return parseOverlay(override.getPath(), locale, raw);
|
||||
}
|
||||
|
||||
private static LocaleOverlay parseOverlay(String source, String locale, String raw) {
|
||||
JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw);
|
||||
if (!parsed.isJsonObject()) {
|
||||
@@ -461,11 +530,92 @@ public final class IrisLanguage {
|
||||
return new File(new File(root, "languages/overrides"), normalizeLocale(locale) + ".json").getAbsoluteFile();
|
||||
}
|
||||
|
||||
private static long signature(File file) {
|
||||
if (file == null || !file.exists()) {
|
||||
return 0L;
|
||||
static LocaleHotloadSnapshot captureHotloadSnapshot(File file, String locale) throws IOException {
|
||||
File resolvedFile = Objects.requireNonNull(file, "Locale override file cannot be null").getAbsoluteFile();
|
||||
String resolvedLocale = normalizeLocale(locale);
|
||||
BasicFileAttributes before;
|
||||
try {
|
||||
before = Files.readAttributes(resolvedFile.toPath(), BasicFileAttributes.class);
|
||||
} catch (NoSuchFileException failure) {
|
||||
return LocaleHotloadSnapshot.missing(resolvedFile, resolvedLocale);
|
||||
}
|
||||
return file.lastModified() * 31L + file.length();
|
||||
if (!before.isRegularFile()) {
|
||||
throw new IllegalArgumentException("Locale override is not a regular file: " + resolvedFile.getPath());
|
||||
}
|
||||
if (before.size() > MAX_LOCALE_BYTES) {
|
||||
throw new IllegalArgumentException("Locale override is too large: " + resolvedFile.getPath());
|
||||
}
|
||||
|
||||
byte[] bytes;
|
||||
try (InputStream input = Files.newInputStream(resolvedFile.toPath())) {
|
||||
bytes = input.readNBytes((int) MAX_LOCALE_BYTES + 1);
|
||||
} catch (NoSuchFileException failure) {
|
||||
return null;
|
||||
}
|
||||
if (bytes.length > MAX_LOCALE_BYTES) {
|
||||
throw new IllegalArgumentException("Locale override is too large: " + resolvedFile.getPath());
|
||||
}
|
||||
|
||||
BasicFileAttributes after;
|
||||
try {
|
||||
after = Files.readAttributes(resolvedFile.toPath(), BasicFileAttributes.class);
|
||||
} catch (NoSuchFileException failure) {
|
||||
return null;
|
||||
}
|
||||
if (!sameIdentity(before, after) || bytes.length != after.size()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String content;
|
||||
try {
|
||||
content = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes)).toString();
|
||||
} catch (CharacterCodingException failure) {
|
||||
throw new IOException("Locale override is not valid UTF-8: " + resolvedFile.getPath(), failure);
|
||||
}
|
||||
return LocaleHotloadSnapshot.present(resolvedFile, resolvedLocale, content, sha256(bytes));
|
||||
}
|
||||
|
||||
private static SnapshotCapture captureForReload(File file, String locale) {
|
||||
try {
|
||||
LocaleHotloadSnapshot snapshot = captureHotloadSnapshot(file, locale);
|
||||
if (snapshot == null) {
|
||||
return new SnapshotCapture(
|
||||
null,
|
||||
new IOException("Locale override changed while being read: " + file.getPath())
|
||||
);
|
||||
}
|
||||
return new SnapshotCapture(snapshot, null);
|
||||
} catch (Exception failure) {
|
||||
return new SnapshotCapture(null, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sameIdentity(BasicFileAttributes before, BasicFileAttributes after) {
|
||||
return before.isRegularFile() == after.isRegularFile()
|
||||
&& before.size() == after.size()
|
||||
&& before.lastModifiedTime().equals(after.lastModifiedTime())
|
||||
&& Objects.equals(before.fileKey(), after.fileKey());
|
||||
}
|
||||
|
||||
private static String sha256(byte[] content) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
return HexFormat.of().formatHex(digest.digest(content));
|
||||
} catch (NoSuchAlgorithmException failure) {
|
||||
throw new IllegalStateException("SHA-256 is unavailable", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static void reportCaptureFailure(File file, Exception failure) {
|
||||
String detail = failure.getMessage() == null ? failure.getClass().getSimpleName() : failure.getMessage();
|
||||
String failureKey = file.getAbsolutePath() + ":" + failure.getClass().getName() + ":" + detail;
|
||||
if (Objects.equals(failureKey, lastCaptureFailureKey)) {
|
||||
return;
|
||||
}
|
||||
lastCaptureFailureKey = failureKey;
|
||||
IrisLogging.error("Failed to capture stable locale override " + file.getPath() + ": " + detail);
|
||||
IrisLogging.reportError(failure);
|
||||
failure.printStackTrace();
|
||||
}
|
||||
|
||||
private static void reportRejectedReload(String locale, LocalizationReloadResult result) {
|
||||
@@ -489,4 +639,7 @@ public final class IrisLanguage {
|
||||
|
||||
private record PlainMemo(LocalizationSnapshot snapshot, Map<String, String> values) {
|
||||
}
|
||||
|
||||
private record SnapshotCapture(LocaleHotloadSnapshot snapshot, Exception failure) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package art.arcane.iris.core.localization;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class LocaleHotloadGate {
|
||||
private final Timing timing;
|
||||
|
||||
private long generation;
|
||||
private LocaleHotloadSnapshot baseline;
|
||||
private LocaleHotloadSnapshot observed;
|
||||
private long observedSinceNanos;
|
||||
private LocaleHotloadSnapshot pending;
|
||||
private Attempt inFlight;
|
||||
private long lastCompletedAtNanos;
|
||||
private boolean hasCompleted;
|
||||
|
||||
LocaleHotloadGate(Timing timing) {
|
||||
this.timing = Objects.requireNonNull(timing, "Locale hotload timing cannot be null");
|
||||
}
|
||||
|
||||
synchronized Attempt observe(LocaleHotloadSnapshot snapshot, long nowNanos) {
|
||||
LocaleHotloadSnapshot current = Objects.requireNonNull(snapshot, "Locale hotload snapshot cannot be null");
|
||||
if (!current.equals(observed)) {
|
||||
observed = current;
|
||||
observedSinceNanos = nowNanos;
|
||||
return null;
|
||||
}
|
||||
|
||||
long stabilityNanos = current.missing()
|
||||
? timing.deletionGraceNanos()
|
||||
: timing.contentStabilityNanos();
|
||||
if (!elapsed(nowNanos, observedSinceNanos, stabilityNanos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
pending = current.equals(baseline) ? null : current;
|
||||
if (pending == null || inFlight != null || !pending.equals(observed)) {
|
||||
return null;
|
||||
}
|
||||
if (hasCompleted && !elapsed(nowNanos, lastCompletedAtNanos, timing.cooldownNanos())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Attempt attempt = new Attempt(generation, pending);
|
||||
inFlight = attempt;
|
||||
return attempt;
|
||||
}
|
||||
|
||||
synchronized void unavailable() {
|
||||
observed = null;
|
||||
observedSinceNanos = 0L;
|
||||
}
|
||||
|
||||
synchronized void complete(Attempt attempt, long nowNanos, boolean applied) {
|
||||
if (attempt == null || inFlight == null || !inFlight.equals(attempt) || attempt.generation() != generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
inFlight = null;
|
||||
lastCompletedAtNanos = nowNanos;
|
||||
hasCompleted = true;
|
||||
if (!applied) {
|
||||
return;
|
||||
}
|
||||
|
||||
baseline = attempt.snapshot();
|
||||
if (attempt.snapshot().equals(pending)) {
|
||||
pending = null;
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void reset(LocaleHotloadSnapshot snapshot) {
|
||||
generation++;
|
||||
baseline = snapshot;
|
||||
observed = snapshot;
|
||||
observedSinceNanos = 0L;
|
||||
pending = null;
|
||||
inFlight = null;
|
||||
lastCompletedAtNanos = 0L;
|
||||
hasCompleted = false;
|
||||
}
|
||||
|
||||
private boolean elapsed(long nowNanos, long startNanos, long durationNanos) {
|
||||
return nowNanos - startNanos >= durationNanos;
|
||||
}
|
||||
|
||||
record Timing(long contentStabilityNanos, long deletionGraceNanos, long cooldownNanos) {
|
||||
Timing {
|
||||
if (contentStabilityNanos < 0L || deletionGraceNanos < 0L || cooldownNanos < 0L) {
|
||||
throw new IllegalArgumentException("Locale hotload timing cannot be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Attempt(long generation, LocaleHotloadSnapshot snapshot) {
|
||||
Attempt {
|
||||
snapshot = Objects.requireNonNull(snapshot, "Locale hotload attempt snapshot cannot be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.core.localization;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Objects;
|
||||
|
||||
record LocaleHotloadSnapshot(File file, String locale, String content, String sha256) {
|
||||
LocaleHotloadSnapshot {
|
||||
file = Objects.requireNonNull(file, "Locale override file cannot be null").getAbsoluteFile();
|
||||
locale = Objects.requireNonNull(locale, "Locale cannot be null");
|
||||
sha256 = Objects.requireNonNull(sha256, "Locale content hash cannot be null");
|
||||
}
|
||||
|
||||
static LocaleHotloadSnapshot missing(File file, String locale) {
|
||||
return new LocaleHotloadSnapshot(file, locale, null, "missing");
|
||||
}
|
||||
|
||||
static LocaleHotloadSnapshot present(File file, String locale, String content, String sha256) {
|
||||
return new LocaleHotloadSnapshot(
|
||||
file,
|
||||
locale,
|
||||
Objects.requireNonNull(content, "Locale content cannot be null"),
|
||||
sha256
|
||||
);
|
||||
}
|
||||
|
||||
boolean missing() {
|
||||
return content == null;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.core.localization.BukkitRuntimeMessages;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeProgressMessages;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
@@ -46,7 +47,8 @@ final class StudioOpenProgressReporter {
|
||||
AtomicInteger taskId = new AtomicInteger(-1);
|
||||
org.bukkit.boss.BossBar bossBar;
|
||||
|
||||
if (sender.isPlayer() && sender.player() != null) {
|
||||
if (sender.isPlayer() && sender.player() != null
|
||||
&& IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
bossBar = Bukkit.createBossBar(
|
||||
IrisLanguage.text(RuntimeProgressMessages.STUDIO_OPENING),
|
||||
org.bukkit.boss.BarColor.BLUE,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeProgressMessages;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -109,8 +110,9 @@ public final class ChunkJobReporter {
|
||||
}
|
||||
|
||||
private void startReporter() {
|
||||
boolean player = sender.isPlayer() && sender.player() != null;
|
||||
BossBar bossBar = player
|
||||
boolean showBossBar = sender.isPlayer() && sender.player() != null
|
||||
&& IrisSettings.get().getGeneral().isProgressBossBar();
|
||||
BossBar bossBar = showBossBar
|
||||
? Bukkit.createBossBar(IrisLanguage.text(
|
||||
RuntimeProgressMessages.CHUNK_BOSSBAR_WORKING,
|
||||
MessageArgument.trusted("title", title)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.PackDownloadMessages;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
@@ -246,15 +247,17 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
|
||||
|
||||
private void renderHudPulse(HudSnapshot snapshot) {
|
||||
try {
|
||||
BukkitPlatform.hudLanes().show(
|
||||
player,
|
||||
hudLaneId,
|
||||
snapshot.line(),
|
||||
snapshot.progress(),
|
||||
BarColor.BLUE,
|
||||
BarStyle.SEGMENTED_20,
|
||||
1_500L
|
||||
);
|
||||
if (IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
BukkitPlatform.hudLanes().show(
|
||||
player,
|
||||
hudLaneId,
|
||||
snapshot.line(),
|
||||
snapshot.progress(),
|
||||
BarColor.BLUE,
|
||||
BarStyle.SEGMENTED_20,
|
||||
1_500L
|
||||
);
|
||||
}
|
||||
sender.sendAction(snapshot.line());
|
||||
} catch (RuntimeException failure) {
|
||||
disableHud(failure);
|
||||
@@ -310,15 +313,17 @@ final class PackDownloadProgressReporter implements PackDownloader.DownloadProgr
|
||||
Runnable retiredCleanup = () -> retireHudLane(cleaned);
|
||||
Runnable display = () -> {
|
||||
try {
|
||||
BukkitPlatform.hudLanes().show(
|
||||
player,
|
||||
hudLaneId,
|
||||
message,
|
||||
progress,
|
||||
color,
|
||||
BarStyle.SOLID,
|
||||
4_000L
|
||||
);
|
||||
if (IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
BukkitPlatform.hudLanes().show(
|
||||
player,
|
||||
hudLaneId,
|
||||
message,
|
||||
progress,
|
||||
color,
|
||||
BarStyle.SOLID,
|
||||
4_000L
|
||||
);
|
||||
}
|
||||
sender.sendAction(message);
|
||||
} finally {
|
||||
if (!J.runEntity(player, cleanup, HUD_TERMINAL_TICKS, retiredCleanup)) {
|
||||
|
||||
@@ -488,33 +488,50 @@ public class StudioSVC implements IrisService {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean blockIfPackBroken(VolmitSender sender, String dimm) {
|
||||
private static BrokenPackException reportPackAdmissionFailure(
|
||||
VolmitSender sender, String dimm) {
|
||||
Optional<String> startupDenial = IrisStartupValidation.denialReason();
|
||||
if (startupDenial.isPresent()) {
|
||||
sender.sendMessage(startupDenial.get());
|
||||
return true;
|
||||
}
|
||||
IrisDimension dimension = IrisToolbelt.getDimension(dimm);
|
||||
String packName = dimension == null || dimension.getLoader() == null
|
||||
? dimm
|
||||
: dimension.getLoader().getDataFolder().getName();
|
||||
PackValidationResult validation = PackValidationRegistry.get(packName);
|
||||
if (validation != null && validation.isLoadable()) {
|
||||
return false;
|
||||
BrokenPackException failure = resolvePackAdmissionFailure(
|
||||
packName, startupDenial, validation);
|
||||
if (failure == null) {
|
||||
return null;
|
||||
}
|
||||
if (startupDenial.isPresent()) {
|
||||
sender.sendMessage(startupDenial.get());
|
||||
return failure;
|
||||
}
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_CANNOT_OPEN_STUDIO_PACK_HAS_BLOCKING_ERRORS, MessageArgument.untrusted("dimm", String.valueOf(dimm))));
|
||||
for (String reason : failure.getReasons()) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_MESSAGE, MessageArgument.untrusted("reason", String.valueOf(reason))));
|
||||
}
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("dimm", String.valueOf(packName))));
|
||||
return failure;
|
||||
}
|
||||
|
||||
static BrokenPackException resolvePackAdmissionFailure(
|
||||
String packName,
|
||||
Optional<String> startupDenial,
|
||||
PackValidationResult validation
|
||||
) {
|
||||
if (startupDenial.isPresent()) {
|
||||
return new BrokenPackException(packName, List.of(startupDenial.get()));
|
||||
}
|
||||
if (validation != null && validation.isLoadable()) {
|
||||
return null;
|
||||
}
|
||||
List<String> failures = validation == null
|
||||
? List.of("Required pack validation has not completed. Studio creation fails closed until validation succeeds.")
|
||||
: validation.getBlockingErrors();
|
||||
for (String reason : failures) {
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_MESSAGE, MessageArgument.untrusted("reason", String.valueOf(reason))));
|
||||
}
|
||||
sender.sendMessage(IrisLanguage.text(BukkitRuntimeMessages.STUDIO_S_V_C_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("dimm", String.valueOf(dimm))));
|
||||
return true;
|
||||
return new BrokenPackException(packName, failures);
|
||||
}
|
||||
|
||||
public void open(VolmitSender sender, long seed, String dimm, Consumer<World> onDone) throws IrisException {
|
||||
if (blockIfPackBroken(sender, dimm)) {
|
||||
if (reportPackAdmissionFailure(sender, dimm) != null) {
|
||||
return;
|
||||
}
|
||||
studioTransitions.submit(() -> replaceActiveProject(sender, seed, dimm, onDone))
|
||||
@@ -537,9 +554,9 @@ public class StudioSVC implements IrisService {
|
||||
Runnable beforeOpen,
|
||||
Consumer<World> onDone
|
||||
) {
|
||||
if (blockIfPackBroken(sender, dimension)) {
|
||||
return CompletableFuture.failedFuture(
|
||||
new IllegalStateException("Studio pack '" + dimension + "' has blocking validation errors."));
|
||||
BrokenPackException failure = reportPackAdmissionFailure(sender, dimension);
|
||||
if (failure != null) {
|
||||
return CompletableFuture.failedFuture(failure);
|
||||
}
|
||||
return studioTransitions.submit(() -> replaceActiveProjectTracked(
|
||||
sender,
|
||||
|
||||
@@ -426,7 +426,7 @@ public final class VillageImporter {
|
||||
emittedPools.size(), emittedPieces.size(), losses, true);
|
||||
}
|
||||
|
||||
String msg = "Imported village " + structureKey + " as '" + name + "': " + emittedPieces.size() + " pieces, " + emittedPools.size() + " pools, " + pieceBlocks + " blocks";
|
||||
String msg = "Imported jigsaw structure " + structureKey + " as '" + name + "': " + emittedPieces.size() + " pieces, " + emittedPools.size() + " pools, " + pieceBlocks + " blocks";
|
||||
if (!losses.isEmpty()) {
|
||||
msg += " (" + losses.size() + " fidelity warning(s) recorded)";
|
||||
}
|
||||
|
||||
@@ -624,11 +624,13 @@ public class IrisCreator {
|
||||
return;
|
||||
}
|
||||
if (showLoaderHud) {
|
||||
BukkitPlatform.hudLanes().show(sender.player(), "iris:pregen", IrisLanguage.text(
|
||||
RuntimeProgressMessages.WORLD_PREGEN_ACTION,
|
||||
MessageArgument.trusted("bar", ""),
|
||||
MessageArgument.trusted("percent", percent)
|
||||
), p, BarColor.GREEN, BarStyle.SOLID, 4000L);
|
||||
if (IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
BukkitPlatform.hudLanes().show(sender.player(), "iris:pregen", IrisLanguage.text(
|
||||
RuntimeProgressMessages.WORLD_PREGEN_ACTION,
|
||||
MessageArgument.trusted("bar", ""),
|
||||
MessageArgument.trusted("percent", percent)
|
||||
), p, BarColor.GREEN, BarStyle.SOLID, 4000L);
|
||||
}
|
||||
int barWidth = 44;
|
||||
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth);
|
||||
StringBuilder bar = new StringBuilder(barWidth * 3 + 4);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.core.tools;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeProgressMessages;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -78,7 +79,8 @@ final class WorldCreationProgressReporter {
|
||||
|
||||
static WorldCreationProgressReporter start(VolmitSender sender, String worldName) {
|
||||
WorldCreationProgressReporter reporter = new WorldCreationProgressReporter(sender, worldName);
|
||||
if (sender.isPlayer() && sender.player() != null) {
|
||||
if (sender.isPlayer() && sender.player() != null
|
||||
&& IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
try {
|
||||
J.sfut(reporter::initializePlayerHud).get(5L, TimeUnit.SECONDS);
|
||||
} catch (Throwable failure) {
|
||||
|
||||
@@ -281,8 +281,8 @@ public final class IrisObjectIO {
|
||||
AtomicReference<IOException> ref = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
new Job() {
|
||||
private int total = self.blocks.size() * 3 + self.states.size();
|
||||
private int c = 0;
|
||||
private volatile int total = self.blocks.size() * 3 + self.states.size();
|
||||
private volatile int c = 0;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
|
||||
@@ -147,7 +147,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.folder = new ReactiveFolder(
|
||||
dataLocation,
|
||||
(_a, _b, _c) -> hotload(),
|
||||
(_a, _b, _c) -> hotloadFromWatcher(),
|
||||
new KList<>(".iob", ".json"),
|
||||
new KList<>(".iris"),
|
||||
new KList<>()
|
||||
@@ -542,6 +542,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
withExclusiveControl(() -> getEngine().hotload());
|
||||
}
|
||||
|
||||
private void hotloadFromWatcher() {
|
||||
if (!shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
|
||||
return;
|
||||
}
|
||||
withExclusiveControlFuture(() -> getEngine().hotload(), 30L, TimeUnit.SECONDS).join();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> hotloadComplexAsync(long acquisitionTimeout, TimeUnit unit) {
|
||||
Engine activeEngine = getEngine();
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.MinecraftVersion;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
@@ -161,6 +162,9 @@ public final class BukkitPlatform implements IrisPlatform {
|
||||
}
|
||||
|
||||
public static void showProgressLane(Player player, String laneId, String title, double progress, long staleMillis) {
|
||||
if (!IrisSettings.get().getGeneral().isProgressBossBar()) {
|
||||
return;
|
||||
}
|
||||
hudLanes().show(player, laneId, title, progress, BarColor.BLUE, BarStyle.SOLID, staleMillis);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ import java.net.URI;
|
||||
|
||||
public class DownloadJob implements Job {
|
||||
private final DL.Download download;
|
||||
private int tw;
|
||||
private int cw;
|
||||
private volatile int tw;
|
||||
private volatile int cw;
|
||||
|
||||
public DownloadJob(String url, File destination) throws MalformedURLException {
|
||||
tw = 1;
|
||||
|
||||
@@ -23,7 +23,7 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
public class JobCollection implements Job {
|
||||
private final String name;
|
||||
private final KList<Job> jobs;
|
||||
private String status;
|
||||
private volatile String status;
|
||||
|
||||
public JobCollection(String name, Job... jobs) {
|
||||
this(name, new KList<>(jobs));
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
public abstract class QueueJob<T> implements Job {
|
||||
final KList<T> queue;
|
||||
private final AtomicInteger completed;
|
||||
protected int totalWork;
|
||||
protected volatile int totalWork;
|
||||
|
||||
public QueueJob() {
|
||||
totalWork = 0;
|
||||
|
||||
@@ -21,7 +21,7 @@ package art.arcane.iris.util.common.scheduling.jobs;
|
||||
public class SingleJob implements Job {
|
||||
private final String name;
|
||||
private final Runnable runnable;
|
||||
private boolean done;
|
||||
private volatile boolean done;
|
||||
|
||||
public SingleJob(String name, Runnable runnable) {
|
||||
this.name = name;
|
||||
|
||||
Reference in new issue
Block a user