mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Version content gate core
- Add the ContentGate, CompatWalker, PackCompatReport and finding types under core.compat - Resolve block keys through the live registry, the legacy rename table, dimension blockFallbacks and the entry backup on every platform - Move the legacy rename table into LegacyBlockRenames so Bukkit and the mod loaders apply it identically - Evaluate every registrant on load from IrisData.preprocessObject and store the verdict on IrisRegistrant - Add IrisDimension.blockFallbacks as a plain object property - Keep missing palette keys as MissingBlockState placeholders so a chance-1 edit rule can still rewrite them - Guard the modded rotator, merger and face fixer against non-modded states - Source the legacy potion effect aliases from the gate so the Bukkit-typed table stays unloaded on modded
This commit is contained in:
@@ -71,3 +71,6 @@ AGENTS.md
|
||||
build-lt.sh
|
||||
BUILDS/
|
||||
.run/
|
||||
|
||||
# Version-content compat lane worktree. Generated, never source.
|
||||
.compatwt/
|
||||
|
||||
+3
-1
@@ -39,7 +39,9 @@ public final class ModdedDecoratorHooks implements DecoratorPlatformHooks.FaceFi
|
||||
|
||||
@Override
|
||||
public PlatformBlockState fixFaces(PlatformBlockState state, Hunk<PlatformBlockState> hunk, int rX, int rZ, int x, int y, int z, EngineMantle mantle) {
|
||||
ModdedBlockState fabric = (ModdedBlockState) state;
|
||||
if (!(state instanceof ModdedBlockState fabric)) {
|
||||
return state;
|
||||
}
|
||||
BlockState cloned = fabric.handle();
|
||||
Map<String, BooleanProperty> allowed = faceProperties(cloned);
|
||||
|
||||
|
||||
@@ -28,8 +28,9 @@ import java.util.Map;
|
||||
public final class ModdedStateMerger implements BlockDataMergeSupport.StateMerger {
|
||||
@Override
|
||||
public PlatformBlockState merge(PlatformBlockState base, PlatformBlockState update) {
|
||||
ModdedBlockState fabricBase = (ModdedBlockState) base;
|
||||
ModdedBlockState fabricUpdate = (ModdedBlockState) update;
|
||||
if (!(base instanceof ModdedBlockState fabricBase) || !(update instanceof ModdedBlockState fabricUpdate)) {
|
||||
return update;
|
||||
}
|
||||
ModdedBlockState metadataSource = fabricUpdate.isCustom() ? fabricUpdate : fabricBase;
|
||||
|
||||
try {
|
||||
|
||||
@@ -47,7 +47,9 @@ public final class ModdedStateRotator implements IrisObjectRotation.StateRotator
|
||||
|
||||
@Override
|
||||
public PlatformBlockState rotate(IrisObjectRotation rotation, PlatformBlockState state, int spinxx, int spinyy, int spinzz) {
|
||||
ModdedBlockState fabric = (ModdedBlockState) state;
|
||||
if (!(state instanceof ModdedBlockState fabric)) {
|
||||
return state;
|
||||
}
|
||||
BlockState d = fabric.handle();
|
||||
boolean deleted = false;
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* What the gate did about a key that is missing on the running Minecraft version.
|
||||
* <ul>
|
||||
* <li>{@code EXCLUDED}: a compat unit (registrant or object placement) composes the missing content and was removed from
|
||||
* every pool that could pick it.</li>
|
||||
* <li>{@code DROPPED}: a single entry or reference was removed from its container; the container keeps generating.</li>
|
||||
* <li>{@code SUBSTITUTED}: a declared fallback (dimension {@code blockFallbacks} or a block {@code backup}) replaced the
|
||||
* missing key; the content still generates.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum CompatAction {
|
||||
EXCLUDED,
|
||||
DROPPED,
|
||||
SUBSTITUTED;
|
||||
|
||||
public String label() {
|
||||
return name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* One gating decision: {@code key} in {@code registry} is missing on this server, and the gate took {@code action} on
|
||||
* {@code subjectType}/{@code subjectKey} (a registrant type name and load key, or an object path) at {@code detail}
|
||||
* (a field path such as {@code layers[1].palette[2]}, or the fallback that was applied). Findings are deduplicated on
|
||||
* every field except {@code detail}.
|
||||
*/
|
||||
public record CompatFinding(CompatRegistry registry,
|
||||
String key,
|
||||
CompatAction action,
|
||||
String subjectType,
|
||||
String subjectKey,
|
||||
String detail) {
|
||||
public CompatFinding {
|
||||
Objects.requireNonNull(registry, "registry");
|
||||
Objects.requireNonNull(key, "key");
|
||||
Objects.requireNonNull(action, "action");
|
||||
subjectType = subjectType == null ? "" : subjectType;
|
||||
subjectKey = subjectKey == null ? "" : subjectKey;
|
||||
detail = detail == null ? "" : detail;
|
||||
}
|
||||
|
||||
/** Grouping key shared by every finding about the same registry entry. */
|
||||
public String groupKey() {
|
||||
return registry.name() + ':' + key;
|
||||
}
|
||||
|
||||
/** Deduplication key: one finding per registry, key, action and subject. */
|
||||
public String dedupKey() {
|
||||
return groupKey() + '|' + action.name() + '|' + subjectType + '|' + subjectKey;
|
||||
}
|
||||
|
||||
/** {@code <key> (<registry>): <action> <subjectType> <subjectKey> at <detail>} */
|
||||
public String line() {
|
||||
StringBuilder sb = new StringBuilder(96);
|
||||
sb.append(key).append(" (").append(registry.label()).append("): ").append(action.label());
|
||||
if (!subjectType.isEmpty()) {
|
||||
sb.append(' ').append(subjectType);
|
||||
}
|
||||
if (!subjectKey.isEmpty()) {
|
||||
sb.append(' ').append(subjectKey);
|
||||
}
|
||||
if (!detail.isEmpty()) {
|
||||
sb.append(" at ").append(detail);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** The part after the key, for lines already grouped under the key. */
|
||||
public String subjectLine() {
|
||||
StringBuilder sb = new StringBuilder(64);
|
||||
sb.append(action.label());
|
||||
if (!subjectType.isEmpty()) {
|
||||
sb.append(' ').append(subjectType);
|
||||
}
|
||||
if (!subjectKey.isEmpty()) {
|
||||
sb.append(' ').append(subjectKey);
|
||||
}
|
||||
if (!detail.isEmpty()) {
|
||||
sb.append(" at ").append(detail);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Registry a pack key is checked against by the {@link ContentGate}. Sounds and particles are absent on purpose: the
|
||||
* platform SPI exposes no registry for them and an unknown effect already plays nothing.
|
||||
*/
|
||||
public enum CompatRegistry {
|
||||
BLOCK,
|
||||
ITEM,
|
||||
ENTITY,
|
||||
BIOME,
|
||||
STRUCTURE,
|
||||
ENCHANTMENT,
|
||||
POTION_EFFECT;
|
||||
|
||||
public String label() {
|
||||
return name().toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Gate verdict for one compat unit. {@code excluded} units must be filtered out of every pool that could pick them;
|
||||
* {@code reasons} lists every finding recorded while evaluating the unit (excluded or not).
|
||||
*/
|
||||
public record CompatStatus(boolean excluded, List<CompatFinding> reasons) {
|
||||
public static final CompatStatus OK = new CompatStatus(false, List.of());
|
||||
|
||||
public CompatStatus {
|
||||
reasons = reasons == null ? List.of() : List.copyOf(reasons);
|
||||
}
|
||||
|
||||
public static CompatStatus excludedBy(List<CompatFinding> reasons) {
|
||||
return new CompatStatus(true, reasons);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListBiome;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListEnchantment;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListEntityType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListItemType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryMapBlockState;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* One evaluation of one registrant against the policy table: walks the registrant graph through a cached per-class
|
||||
* field plan, checks every annotated key, resolves every {@link IrisBlockData} through the gate chain, drops entries
|
||||
* the table says to drop, and records findings on the gate's report.
|
||||
* <p>
|
||||
* Boundaries: {@link IrisObjectPlacement} is never descended (placements evaluate their own pools), match-side block
|
||||
* lists are ignored (a missing block that is only matched is harmless), registrant references
|
||||
* ({@code @RegistryListResource}) belong to the pool cascade, and identity cycles are cut.
|
||||
*/
|
||||
final class CompatWalker {
|
||||
private static final String IRIS_PACKAGE = "art.arcane.iris.";
|
||||
private static final String OBJECT_PACKAGE = "art.arcane.iris.engine.object.";
|
||||
private static final int MAX_BACKUP_DEPTH = 8;
|
||||
private static final Map<Class<?>, ClassPlan> PLANS = new ConcurrentHashMap<>();
|
||||
// Annotated fields whose key defines the registrant itself: missing means the registrant is excluded.
|
||||
private static final Set<String> EXCLUSION_SITES = Set.of(
|
||||
OBJECT_PACKAGE + "IrisEntity#type",
|
||||
OBJECT_PACKAGE + "IrisBiome#derivative");
|
||||
// Block lists that only select which existing blocks to act on; a missing key there matches nothing and is harmless.
|
||||
private static final Set<String> MATCH_ONLY = Set.of(
|
||||
OBJECT_PACKAGE + "IrisObjectReplace#find",
|
||||
OBJECT_PACKAGE + "IrisObjectLoot#filter",
|
||||
OBJECT_PACKAGE + "IrisObjectVanillaLoot#filter",
|
||||
OBJECT_PACKAGE + "IrisObjectMarker#mark",
|
||||
OBJECT_PACKAGE + "IrisBlockDrops#blocks",
|
||||
OBJECT_PACKAGE + "IrisDecorator#whitelist",
|
||||
OBJECT_PACKAGE + "IrisDecorator#blacklist");
|
||||
|
||||
private final ContentGate gate;
|
||||
private final IrisRegistrant registrant;
|
||||
private final String subjectType;
|
||||
private final String subjectKey;
|
||||
private final boolean customLookup;
|
||||
private final List<CompatFinding> reasons = new ArrayList<>();
|
||||
private final Set<Object> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
private boolean excluded;
|
||||
|
||||
CompatWalker(ContentGate gate, IrisRegistrant registrant) {
|
||||
this.gate = gate;
|
||||
this.registrant = registrant;
|
||||
// Lowercase so walker findings read like the pool-cascade findings ("excluded biome x", "dropped region y").
|
||||
this.subjectType = registrant.getTypeName().toLowerCase(Locale.ROOT);
|
||||
this.subjectKey = registrant.getLoadKey();
|
||||
// A blocks/ registrant must not re-enter its own loader cache while it is being loaded.
|
||||
this.customLookup = !(registrant instanceof IrisBlockData);
|
||||
}
|
||||
|
||||
CompatStatus run() {
|
||||
if (registrant instanceof IrisBlockData block) {
|
||||
resolveEntry(block, "block");
|
||||
} else {
|
||||
walkObject(registrant, "");
|
||||
}
|
||||
if (excluded) {
|
||||
return CompatStatus.excludedBy(reasons);
|
||||
}
|
||||
return reasons.isEmpty() ? CompatStatus.OK : new CompatStatus(false, reasons);
|
||||
}
|
||||
|
||||
/** Walks one object; true when its owner must drop it (an annotated key on it is missing and it is not the registrant). */
|
||||
private boolean walkObject(Object node, String path) {
|
||||
if (!visited.add(node)) {
|
||||
return false;
|
||||
}
|
||||
ClassPlan plan = plan(node.getClass());
|
||||
boolean drop = false;
|
||||
for (KeyField key : plan.keys()) {
|
||||
Object value = read(key.field(), node);
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
String fieldPath = child(path, key.field().getName());
|
||||
if (key.list()) {
|
||||
if (value instanceof Collection<?> values) {
|
||||
walkKeyList(key, values, fieldPath);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!(value instanceof String raw) || raw.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (key.registry() == CompatRegistry.BLOCK) {
|
||||
resolveBlockKey(raw, fieldPath);
|
||||
continue;
|
||||
}
|
||||
if (check(key.registry(), raw) != KeyStatus.MISSING) {
|
||||
continue;
|
||||
}
|
||||
String missing = normalizedKey(raw);
|
||||
if (node != registrant) {
|
||||
dropped(key.registry(), missing, fieldPath);
|
||||
drop = true;
|
||||
} else if (key.exclusionSite()) {
|
||||
exclude(key.registry(), missing, fieldPath);
|
||||
} else {
|
||||
dropped(key.registry(), missing, fieldPath);
|
||||
write(key.field(), node, plan.defaultOf(key.field()));
|
||||
}
|
||||
}
|
||||
for (Field field : plan.descents()) {
|
||||
Object value = read(field, node);
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
String fieldPath = child(path, field.getName());
|
||||
if (value instanceof IrisBlockData block) {
|
||||
resolveEntry(block, fieldPath);
|
||||
} else if (value instanceof Collection<?> values) {
|
||||
walkCollection(values, fieldPath);
|
||||
} else if (value instanceof Map<?, ?> map) {
|
||||
walkMap(map, fieldPath);
|
||||
} else if (isIrisObject(value.getClass()) && walkObject(value, fieldPath)) {
|
||||
write(field, node, null);
|
||||
}
|
||||
}
|
||||
return drop;
|
||||
}
|
||||
|
||||
private void walkKeyList(KeyField key, Collection<?> values, String path) {
|
||||
Iterator<?> iterator = values.iterator();
|
||||
int index = 0;
|
||||
while (iterator.hasNext()) {
|
||||
Object element = iterator.next();
|
||||
String elementPath = index(path, index++);
|
||||
if (!(element instanceof String raw) || raw.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (key.registry() == CompatRegistry.BLOCK) {
|
||||
resolveBlockKey(raw, elementPath);
|
||||
continue;
|
||||
}
|
||||
if (check(key.registry(), raw) == KeyStatus.MISSING) {
|
||||
dropped(key.registry(), normalizedKey(raw), elementPath);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void walkCollection(Collection<?> values, String path) {
|
||||
Iterator<?> iterator = values.iterator();
|
||||
int index = 0;
|
||||
while (iterator.hasNext()) {
|
||||
Object element = iterator.next();
|
||||
if (walkValue(element, index(path, index++))) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void walkMap(Map<?, ?> map, String path) {
|
||||
Iterator<? extends Map.Entry<?, ?>> iterator = map.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<?, ?> entry = iterator.next();
|
||||
if (walkValue(entry.getValue(), path + '[' + entry.getKey() + ']')) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean walkValue(Object value, String path) {
|
||||
if (value == null || value instanceof IrisObjectPlacement) {
|
||||
return false;
|
||||
}
|
||||
if (value instanceof IrisBlockData block) {
|
||||
resolveEntry(block, path);
|
||||
return false;
|
||||
}
|
||||
if (value instanceof Collection<?> values) {
|
||||
walkCollection(values, path);
|
||||
return false;
|
||||
}
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
walkMap(map, path);
|
||||
return false;
|
||||
}
|
||||
return isIrisObject(value.getClass()) && walkObject(value, path);
|
||||
}
|
||||
|
||||
/** An annotated block key that generates (procedural trees, formations, ...): the chain without a backup. */
|
||||
private void resolveBlockKey(String raw, String path) {
|
||||
ContentGate.BlockResolution resolution = gate.resolveBlock(raw);
|
||||
String key = normalizedKey(raw);
|
||||
if (resolution == null) {
|
||||
exclude(CompatRegistry.BLOCK, key, path);
|
||||
} else if (resolution.source() == ContentGate.BlockResolution.Source.FALLBACK) {
|
||||
substituted(key, path, "fallback " + resolution.resolvedKey());
|
||||
}
|
||||
}
|
||||
|
||||
/** A block entry the way {@code IrisBlockData.getBlockData} resolves it: custom block, chain, then backups. */
|
||||
private void resolveEntry(IrisBlockData entry, String path) {
|
||||
if (!visited.add(entry)) {
|
||||
return;
|
||||
}
|
||||
String block = entry.getBlock();
|
||||
if (block == null || block.isBlank()) {
|
||||
return;
|
||||
}
|
||||
IrisBlockData custom = customBlock(block);
|
||||
if (custom != null) {
|
||||
if (custom.isCompatExcluded()) {
|
||||
exclude(CompatRegistry.BLOCK, causeKey(custom, block), path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
String state = entry.stateKey();
|
||||
ContentGate.BlockResolution resolution = gate.resolveBlock(state);
|
||||
String key = normalizedKey(state);
|
||||
if (resolution != null) {
|
||||
if (resolution.source() == ContentGate.BlockResolution.Source.FALLBACK) {
|
||||
substituted(key, path, "fallback " + resolution.resolvedKey());
|
||||
}
|
||||
return;
|
||||
}
|
||||
String revived = resolveBackup(entry.getBackup(), 0);
|
||||
if (revived != null) {
|
||||
substituted(key, path, "backup " + revived);
|
||||
return;
|
||||
}
|
||||
exclude(CompatRegistry.BLOCK, key, path);
|
||||
}
|
||||
|
||||
/** The key a backup chain generates, or null when every link is missing. */
|
||||
private String resolveBackup(IrisBlockData backup, int depth) {
|
||||
if (backup == null || depth > MAX_BACKUP_DEPTH || !visited.add(backup)) {
|
||||
return null;
|
||||
}
|
||||
String block = backup.getBlock();
|
||||
if (block == null || block.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
IrisBlockData custom = customBlock(block);
|
||||
if (custom != null) {
|
||||
return custom.isCompatExcluded() ? null : ContentGate.normalizeState(block);
|
||||
}
|
||||
ContentGate.BlockResolution resolution = gate.resolveBlock(backup.stateKey());
|
||||
if (resolution != null) {
|
||||
return resolution.resolvedKey();
|
||||
}
|
||||
return resolveBackup(backup.getBackup(), depth + 1);
|
||||
}
|
||||
|
||||
private IrisBlockData customBlock(String block) {
|
||||
return customLookup ? gate.customBlock(block) : null;
|
||||
}
|
||||
|
||||
private static String causeKey(IrisBlockData custom, String block) {
|
||||
CompatStatus status = custom.getCompat();
|
||||
if (status != null && !status.reasons().isEmpty()) {
|
||||
return status.reasons().getFirst().key();
|
||||
}
|
||||
return normalizedKey(block);
|
||||
}
|
||||
|
||||
private KeyStatus check(CompatRegistry registry, String raw) {
|
||||
KeyStatus status = gate.status(registry, raw);
|
||||
if (status == KeyStatus.UNKNOWN) {
|
||||
gate.report().markIncomplete(registry.label() + " registry not ready");
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private void exclude(CompatRegistry registry, String key, String detail) {
|
||||
excluded = true;
|
||||
record(new CompatFinding(registry, key, CompatAction.EXCLUDED, subjectType, subjectKey, detail));
|
||||
}
|
||||
|
||||
private void dropped(CompatRegistry registry, String key, String detail) {
|
||||
record(new CompatFinding(registry, key, CompatAction.DROPPED, subjectType, subjectKey, detail));
|
||||
}
|
||||
|
||||
private void substituted(String key, String path, String how) {
|
||||
record(new CompatFinding(CompatRegistry.BLOCK, key, CompatAction.SUBSTITUTED, subjectType, subjectKey, path + " (" + how + ")"));
|
||||
}
|
||||
|
||||
private void record(CompatFinding finding) {
|
||||
reasons.add(finding);
|
||||
gate.report().record(finding);
|
||||
}
|
||||
|
||||
private static String normalizedKey(String raw) {
|
||||
return ContentGate.baseKey(ContentGate.normalizeState(raw));
|
||||
}
|
||||
|
||||
private static String child(String path, String name) {
|
||||
return path.isEmpty() ? name : path + '.' + name;
|
||||
}
|
||||
|
||||
private static String index(String path, int index) {
|
||||
return path + '[' + index + ']';
|
||||
}
|
||||
|
||||
private static Object read(Field field, Object node) {
|
||||
try {
|
||||
return field.get(node);
|
||||
} catch (IllegalAccessException | RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(Field field, Object node, Object value) {
|
||||
try {
|
||||
field.set(node, value);
|
||||
} catch (IllegalAccessException | RuntimeException e) {
|
||||
IrisLogging.debug("Compat gate could not reset " + field.getDeclaringClass().getSimpleName() + "." + field.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static ClassPlan plan(Class<?> type) {
|
||||
ClassPlan plan = PLANS.get(type);
|
||||
if (plan == null) {
|
||||
plan = ClassPlan.build(type);
|
||||
PLANS.putIfAbsent(type, plan);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
static boolean isIrisObject(Class<?> type) {
|
||||
return type.getName().startsWith(IRIS_PACKAGE)
|
||||
&& !type.isEnum()
|
||||
&& !type.isRecord()
|
||||
&& !type.isAnnotation()
|
||||
&& !IrisObjectPlacement.class.isAssignableFrom(type);
|
||||
}
|
||||
|
||||
private static boolean descendable(Type type) {
|
||||
if (type instanceof Class<?> clazz) {
|
||||
return isIrisObject(clazz);
|
||||
}
|
||||
if (type instanceof ParameterizedType parameterized && parameterized.getRawType() instanceof Class<?> raw) {
|
||||
Type[] arguments = parameterized.getActualTypeArguments();
|
||||
if (Collection.class.isAssignableFrom(raw)) {
|
||||
return arguments.length == 1 && descendable(arguments[0]);
|
||||
}
|
||||
if (Map.class.isAssignableFrom(raw)) {
|
||||
return arguments.length == 2 && descendable(arguments[1]);
|
||||
}
|
||||
return isIrisObject(raw);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isStringList(Field field) {
|
||||
if (!Collection.class.isAssignableFrom(field.getType())) {
|
||||
return false;
|
||||
}
|
||||
return field.getGenericType() instanceof ParameterizedType parameterized
|
||||
&& parameterized.getActualTypeArguments().length == 1
|
||||
&& parameterized.getActualTypeArguments()[0] == String.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* The registry an annotated field is checked against, or null when the field carries no checkable key: registrant
|
||||
* references, prefix-matching vanilla structure filters, Iris structure resources ({@code @RegistryListStructure}),
|
||||
* and the structure-set and native jigsaw pool keys the platform SPI exposes no registry for.
|
||||
*/
|
||||
private static CompatRegistry registryOf(Field field) {
|
||||
if (field.isAnnotationPresent(RegistryListBlockType.class)) {
|
||||
return CompatRegistry.BLOCK;
|
||||
}
|
||||
if (field.isAnnotationPresent(RegistryListEntityType.class)) {
|
||||
return CompatRegistry.ENTITY;
|
||||
}
|
||||
if (field.isAnnotationPresent(RegistryListItemType.class)) {
|
||||
return CompatRegistry.ITEM;
|
||||
}
|
||||
if (field.isAnnotationPresent(RegistryListEnchantment.class)) {
|
||||
return CompatRegistry.ENCHANTMENT;
|
||||
}
|
||||
if (field.isAnnotationPresent(RegistryListPotionEffect.class)) {
|
||||
return CompatRegistry.POTION_EFFECT;
|
||||
}
|
||||
if (field.isAnnotationPresent(RegistryListBiome.class)) {
|
||||
return CompatRegistry.BIOME;
|
||||
}
|
||||
RegistryListVanillaStructure vanilla = field.getAnnotation(RegistryListVanillaStructure.class);
|
||||
if (vanilla != null && !vanilla.prefixes()) {
|
||||
return CompatRegistry.STRUCTURE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean open(Field field) {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
return true;
|
||||
} catch (RuntimeException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
record KeyField(Field field, CompatRegistry registry, boolean list, boolean exclusionSite) {
|
||||
}
|
||||
|
||||
/** Per-class field plan: annotated key fields and the fields worth descending into. Immutable once built. */
|
||||
static final class ClassPlan {
|
||||
private final Class<?> type;
|
||||
private final List<KeyField> keys;
|
||||
private final List<Field> descents;
|
||||
private volatile Object defaults;
|
||||
private volatile boolean defaultsBuilt;
|
||||
|
||||
private ClassPlan(Class<?> type, List<KeyField> keys, List<Field> descents) {
|
||||
this.type = type;
|
||||
this.keys = keys;
|
||||
this.descents = descents;
|
||||
}
|
||||
|
||||
static ClassPlan build(Class<?> type) {
|
||||
List<KeyField> keys = new ArrayList<>();
|
||||
List<Field> descents = new ArrayList<>();
|
||||
for (Class<?> current = type; current != null && current != Object.class; current = current.getSuperclass()) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
int modifiers = field.getModifiers();
|
||||
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers) || field.isSynthetic()) {
|
||||
continue;
|
||||
}
|
||||
if (field.isAnnotationPresent(RegistryListResource.class) || field.isAnnotationPresent(RegistryMapBlockState.class)) {
|
||||
continue;
|
||||
}
|
||||
String site = current.getName() + '#' + field.getName();
|
||||
if (MATCH_ONLY.contains(site)) {
|
||||
continue;
|
||||
}
|
||||
CompatRegistry registry = registryOf(field);
|
||||
if (registry != null) {
|
||||
boolean list = isStringList(field);
|
||||
if ((field.getType() == String.class || list) && open(field)) {
|
||||
keys.add(new KeyField(field, registry, list, EXCLUSION_SITES.contains(site)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (descendable(field.getGenericType()) && open(field)) {
|
||||
descents.add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ClassPlan(type, List.copyOf(keys), List.copyOf(descents));
|
||||
}
|
||||
|
||||
List<KeyField> keys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
List<Field> descents() {
|
||||
return descents;
|
||||
}
|
||||
|
||||
/** The field's value on a fresh instance (what a dropped top-level key resets to), or null. */
|
||||
Object defaultOf(Field field) {
|
||||
if (!defaultsBuilt) {
|
||||
defaults = newInstance();
|
||||
defaultsBuilt = true;
|
||||
}
|
||||
Object instance = defaults;
|
||||
return instance == null ? null : read(field, instance);
|
||||
}
|
||||
|
||||
private Object newInstance() {
|
||||
try {
|
||||
Constructor<?> constructor = type.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance();
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Answers "does this pack key exist on the running server, and if not what do we generate instead" for one pack
|
||||
* instance, and records every decision in the pack's {@link PackCompatReport}.
|
||||
* <p>
|
||||
* Existence is checked against the live {@link PlatformRegistries} only - never a version number - so the same pack
|
||||
* gates identically on Bukkit and on the modded loaders for the same Minecraft version, and mods that add or remove
|
||||
* content are covered for free. {@link KeyStatus#UNKNOWN} (registry not bound or not ready) never produces a finding.
|
||||
* <p>
|
||||
* Block resolution chain: strict registry lookup, legacy rename table (not reported), dimension {@code blockFallbacks}
|
||||
* (reported), then {@code null} (missing). The per-entry {@code backup} of an {@code IrisBlockData} is applied by that
|
||||
* class and by the walker around this chain. Resolutions are cached per normalized state.
|
||||
*/
|
||||
public final class ContentGate {
|
||||
private static final String DEFAULT_NAMESPACE = "minecraft";
|
||||
private static final String DIMENSIONS_FOLDER = "dimensions";
|
||||
private static final String BLOCK_FALLBACKS_FIELD = "blockFallbacks";
|
||||
private static final Object MISSING = new Object();
|
||||
/** Legacy Bukkit potion effect enum names (lowercase) to the current key path; the Bukkit-typed
|
||||
* {@code PotionEffectTypes} reads this table so it stays unloaded on the modded loaders. */
|
||||
public static final Map<String, String> POTION_EFFECT_ALIASES = Map.of(
|
||||
"slow", "slowness",
|
||||
"fast_digging", "haste",
|
||||
"slow_digging", "mining_fatigue",
|
||||
"increase_damage", "strength",
|
||||
"heal", "instant_health",
|
||||
"harm", "instant_damage",
|
||||
"jump", "jump_boost",
|
||||
"confusion", "nausea",
|
||||
"damage_resistance", "resistance");
|
||||
|
||||
private final PlatformRegistries registries;
|
||||
private final Supplier<Map<String, String>> blockFallbacks;
|
||||
private final PackCompatReport report;
|
||||
private final Function<String, IrisBlockData> customBlocks;
|
||||
private final Map<CompatRegistry, Set<String>> keySets = new ConcurrentHashMap<>();
|
||||
private final Map<String, Object> resolutions = new ConcurrentHashMap<>();
|
||||
private final Map<String, MissingBlockState> placeholders = new ConcurrentHashMap<>();
|
||||
// Keys with no blocks/ registrant: the loader lists the folder on every miss, and every walked entry asks.
|
||||
private final Set<String> customMisses = ConcurrentHashMap.newKeySet();
|
||||
private volatile boolean readyKnown;
|
||||
private volatile Map<String, String> fallbackCache;
|
||||
|
||||
public ContentGate(PlatformRegistries registries, Supplier<Map<String, String>> blockFallbacks, PackCompatReport report) {
|
||||
this(registries, blockFallbacks, report, null);
|
||||
}
|
||||
|
||||
public ContentGate(PlatformRegistries registries, Map<String, String> blockFallbacks, PackCompatReport report) {
|
||||
this(registries, () -> blockFallbacks == null ? Map.of() : blockFallbacks, report, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param customBlocks resolves a pack block key to its {@code blocks/} registrant, or null; consulted before the
|
||||
* registry the way {@code IrisBlockData.getBlockData} does. May be null.
|
||||
*/
|
||||
public ContentGate(PlatformRegistries registries, Supplier<Map<String, String>> blockFallbacks, PackCompatReport report,
|
||||
Function<String, IrisBlockData> customBlocks) {
|
||||
this.registries = registries;
|
||||
this.blockFallbacks = blockFallbacks == null ? Map::of : blockFallbacks;
|
||||
this.report = report == null ? new PackCompatReport() : report;
|
||||
this.customBlocks = customBlocks;
|
||||
}
|
||||
|
||||
/** Gate for a pack folder: live platform registries, fallbacks read from the pack's dimension JSON. */
|
||||
public static ContentGate forData(IrisData data) {
|
||||
PlatformRegistries registries = IrisPlatforms.isBound() ? IrisPlatforms.get().registries() : null;
|
||||
File folder = data == null ? null : data.getDataFolder();
|
||||
PackCompatReport report = data == null ? new PackCompatReport() : data.getCompatReport();
|
||||
Function<String, IrisBlockData> customBlocks = data == null ? null : key -> {
|
||||
ResourceLoader<IrisBlockData> loader = data.getBlockLoader();
|
||||
return loader == null ? null : loader.load(key, false);
|
||||
};
|
||||
return new ContentGate(registries, () -> readBlockFallbacks(folder), report, customBlocks);
|
||||
}
|
||||
|
||||
public PackCompatReport report() {
|
||||
return report;
|
||||
}
|
||||
|
||||
/** True when the registry can be consulted; false means every status is {@link KeyStatus#UNKNOWN}. */
|
||||
public boolean ready() {
|
||||
if (readyKnown) {
|
||||
return true;
|
||||
}
|
||||
if (registries == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
List<String> blocks = registries.blockKeys();
|
||||
boolean ready = blocks != null && !blocks.isEmpty();
|
||||
if (ready) {
|
||||
readyKnown = true;
|
||||
}
|
||||
return ready;
|
||||
} catch (Throwable e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public KeyStatus block(String state) {
|
||||
String normalized = normalizeState(state);
|
||||
if (normalized == null) {
|
||||
return KeyStatus.MISSING;
|
||||
}
|
||||
if (!ready()) {
|
||||
return KeyStatus.UNKNOWN;
|
||||
}
|
||||
return lookup(normalized) != null ? KeyStatus.PRESENT : KeyStatus.MISSING;
|
||||
}
|
||||
|
||||
public KeyStatus item(String key) {
|
||||
return statusOf(CompatRegistry.ITEM, key);
|
||||
}
|
||||
|
||||
public KeyStatus entity(String key) {
|
||||
return statusOf(CompatRegistry.ENTITY, key);
|
||||
}
|
||||
|
||||
public KeyStatus biome(String key) {
|
||||
return statusOf(CompatRegistry.BIOME, key);
|
||||
}
|
||||
|
||||
public KeyStatus structure(String key) {
|
||||
return statusOf(CompatRegistry.STRUCTURE, key);
|
||||
}
|
||||
|
||||
public KeyStatus enchantment(String key) {
|
||||
return statusOf(CompatRegistry.ENCHANTMENT, key);
|
||||
}
|
||||
|
||||
/** Potion effects match on the key path only and accept the legacy Bukkit enum names, as {@code IrisEffect} does. */
|
||||
public KeyStatus potionEffect(String key) {
|
||||
String normalized = normalizeState(key);
|
||||
if (normalized == null) {
|
||||
return KeyStatus.MISSING;
|
||||
}
|
||||
if (!ready()) {
|
||||
return KeyStatus.UNKNOWN;
|
||||
}
|
||||
Set<String> paths = keySet(CompatRegistry.POTION_EFFECT);
|
||||
if (paths == null) {
|
||||
return KeyStatus.UNKNOWN;
|
||||
}
|
||||
String path = pathOf(baseKey(normalized));
|
||||
return paths.contains(POTION_EFFECT_ALIASES.getOrDefault(path, path)) ? KeyStatus.PRESENT : KeyStatus.MISSING;
|
||||
}
|
||||
|
||||
/** Status of {@code key} in {@code registry}; {@link CompatRegistry#BLOCK} goes through {@link #block(String)}. */
|
||||
public KeyStatus status(CompatRegistry registry, String key) {
|
||||
return switch (registry) {
|
||||
case BLOCK -> block(key);
|
||||
case ITEM -> item(key);
|
||||
case ENTITY -> entity(key);
|
||||
case BIOME -> biome(key);
|
||||
case STRUCTURE -> structure(key);
|
||||
case ENCHANTMENT -> enchantment(key);
|
||||
case POTION_EFFECT -> potionEffect(key);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved block for a pack state string, or {@code null} when it is missing on this server after the whole chain
|
||||
* (or the registry is not ready). {@link BlockResolution#substituted()} is true only for a dimension fallback; a
|
||||
* legacy rename is visible through {@link BlockResolution#source()} and is never reported.
|
||||
*/
|
||||
public BlockResolution resolveBlock(String state) {
|
||||
String normalized = normalizeState(state);
|
||||
if (normalized == null || !ready()) {
|
||||
return null;
|
||||
}
|
||||
Object cached = resolutions.get(normalized);
|
||||
if (cached == null) {
|
||||
BlockResolution resolved = resolveUncached(normalized);
|
||||
cached = resolved == null ? MISSING : resolved;
|
||||
resolutions.putIfAbsent(normalized, cached);
|
||||
}
|
||||
return cached == MISSING ? null : (BlockResolution) cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #resolveBlock(String)}, or a {@link MissingBlockState} carrying the key when the whole chain misses, so
|
||||
* object palettes and {@code edit} find lists keep the key an edit rule can still rewrite. Null only when the
|
||||
* registry is not ready (callers keep their plain lookup) or the state is blank.
|
||||
*/
|
||||
public PlatformBlockState resolveBlockOrPlaceholder(String state) {
|
||||
String normalized = normalizeState(state);
|
||||
if (normalized == null || !ready()) {
|
||||
return null;
|
||||
}
|
||||
BlockResolution resolution = resolveBlock(normalized);
|
||||
if (resolution != null) {
|
||||
return resolution.state();
|
||||
}
|
||||
MissingBlockState placeholder = placeholders.get(normalized);
|
||||
if (placeholder == null) {
|
||||
PlatformBlockState air;
|
||||
try {
|
||||
air = registries.air();
|
||||
} catch (Throwable e) {
|
||||
air = null;
|
||||
}
|
||||
if (air == null) {
|
||||
return null;
|
||||
}
|
||||
placeholder = MissingBlockState.of(normalized, air);
|
||||
MissingBlockState raced = placeholders.putIfAbsent(normalized, placeholder);
|
||||
if (raced != null) {
|
||||
placeholder = raced;
|
||||
}
|
||||
}
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
private BlockResolution resolveUncached(String normalized) {
|
||||
PlatformBlockState direct = lookup(normalized);
|
||||
if (direct != null) {
|
||||
return new BlockResolution(direct, normalized, false, null, BlockResolution.Source.REGISTRY);
|
||||
}
|
||||
BlockResolution renamed = resolveRename(normalized);
|
||||
if (renamed != null) {
|
||||
return renamed;
|
||||
}
|
||||
String fallback = fallbacks().get(baseKey(normalized));
|
||||
if (fallback != null) {
|
||||
String normalizedFallback = normalizeState(fallback);
|
||||
PlatformBlockState substitute = normalizedFallback == null ? null : lookup(normalizedFallback);
|
||||
if (substitute != null) {
|
||||
return new BlockResolution(substitute, normalizedFallback, true, normalized, BlockResolution.Source.FALLBACK);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private BlockResolution resolveRename(String normalized) {
|
||||
String candidate = normalized;
|
||||
for (int hop = 0; hop < LegacyBlockRenames.MAX_HOPS; hop++) {
|
||||
String supplement = normalizeState(LegacyBlockRenames.supplementFor(candidate));
|
||||
if (supplement == null) {
|
||||
return null;
|
||||
}
|
||||
PlatformBlockState state = lookup(supplement);
|
||||
if (state != null) {
|
||||
return new BlockResolution(state, supplement, false, null, BlockResolution.Source.RENAME);
|
||||
}
|
||||
candidate = supplement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The {@code blocks/} registrant for a pack block key, or null when there is none or no loader is attached. */
|
||||
public IrisBlockData customBlock(String key) {
|
||||
if (customBlocks == null || key == null || key.isBlank() || customMisses.contains(key)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
IrisBlockData custom = customBlocks.apply(key);
|
||||
if (custom == null) {
|
||||
customMisses.add(key);
|
||||
}
|
||||
return custom;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The dimension {@code blockFallbacks} map, base key to state, normalized. */
|
||||
public Map<String, String> fallbacks() {
|
||||
Map<String, String> cached = fallbackCache;
|
||||
if (cached == null) {
|
||||
Map<String, String> raw = blockFallbacks.get();
|
||||
Map<String, String> normalized = new LinkedHashMap<>();
|
||||
if (raw != null) {
|
||||
for (Map.Entry<String, String> entry : raw.entrySet()) {
|
||||
String key = normalizeState(entry.getKey());
|
||||
if (key != null && entry.getValue() != null) {
|
||||
normalized.put(baseKey(key), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
cached = Collections.unmodifiableMap(normalized);
|
||||
fallbackCache = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
/** Forget cached fallbacks, key sets, resolutions, placeholders and the readiness latch (studio hotload). */
|
||||
public void invalidate() {
|
||||
fallbackCache = null;
|
||||
readyKnown = false;
|
||||
keySets.clear();
|
||||
resolutions.clear();
|
||||
placeholders.clear();
|
||||
customMisses.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a freshly loaded registrant against the policy table, records findings, drops entries the table says
|
||||
* to drop, and returns the status the caller stores on the registrant. A registry that is not ready produces no
|
||||
* findings and no mutation; the report is marked incomplete instead.
|
||||
*/
|
||||
public CompatStatus evaluate(IrisRegistrant registrant) {
|
||||
if (registrant == null) {
|
||||
return CompatStatus.OK;
|
||||
}
|
||||
if (!ready()) {
|
||||
report.markIncomplete("registry not ready");
|
||||
return CompatStatus.OK;
|
||||
}
|
||||
return new CompatWalker(this, registrant).run();
|
||||
}
|
||||
|
||||
/** Reads {@code dimensions/*.json} {@code blockFallbacks} objects straight from disk, without the loader. */
|
||||
public static Map<String, String> readBlockFallbacks(File packFolder) {
|
||||
if (packFolder == null) {
|
||||
return Map.of();
|
||||
}
|
||||
File[] dimensions = new File(packFolder, DIMENSIONS_FOLDER).listFiles((dir, name) -> name.endsWith(".json"));
|
||||
if (dimensions == null || dimensions.length == 0) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, String> merged = new LinkedHashMap<>();
|
||||
for (File file : dimensions) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(IO.readAll(file));
|
||||
if (!json.has(BLOCK_FALLBACKS_FIELD)) {
|
||||
continue;
|
||||
}
|
||||
JSONObject fallbacks = json.getJSONObject(BLOCK_FALLBACKS_FIELD);
|
||||
for (String key : fallbacks.keySet()) {
|
||||
Object value = fallbacks.get(key);
|
||||
if (value instanceof String state && !state.isBlank()) {
|
||||
merged.putIfAbsent(key, state);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.warn("Could not read blockFallbacks from " + file.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return merged.isEmpty() ? Map.of() : Collections.unmodifiableMap(merged);
|
||||
}
|
||||
|
||||
/** {@code namespace:name[props]} lowercased with the default namespace applied; null for blank input. */
|
||||
public static String normalizeState(String state) {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = state.trim().toLowerCase(Locale.ROOT);
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int props = trimmed.indexOf('[');
|
||||
String base = props < 0 ? trimmed : trimmed.substring(0, props);
|
||||
String rest = props < 0 ? "" : trimmed.substring(props);
|
||||
if (base.indexOf(':') < 0) {
|
||||
base = DEFAULT_NAMESPACE + ':' + base;
|
||||
}
|
||||
return base + rest;
|
||||
}
|
||||
|
||||
/** Strips the {@code [props]} section. */
|
||||
public static String baseKey(String normalizedState) {
|
||||
if (normalizedState == null) {
|
||||
return null;
|
||||
}
|
||||
int props = normalizedState.indexOf('[');
|
||||
return props < 0 ? normalizedState : normalizedState.substring(0, props);
|
||||
}
|
||||
|
||||
private static String pathOf(String baseKey) {
|
||||
int namespace = baseKey.indexOf(':');
|
||||
return namespace < 0 ? baseKey : baseKey.substring(namespace + 1);
|
||||
}
|
||||
|
||||
private PlatformBlockState lookup(String normalized) {
|
||||
try {
|
||||
return registries.blockOrNull(normalized, false);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private KeyStatus statusOf(CompatRegistry registry, String key) {
|
||||
String normalized = normalizeState(key);
|
||||
if (normalized == null) {
|
||||
return KeyStatus.MISSING;
|
||||
}
|
||||
if (!ready()) {
|
||||
return KeyStatus.UNKNOWN;
|
||||
}
|
||||
Set<String> keys = keySet(registry);
|
||||
if (keys == null) {
|
||||
return KeyStatus.UNKNOWN;
|
||||
}
|
||||
return keys.contains(baseKey(normalized)) ? KeyStatus.PRESENT : KeyStatus.MISSING;
|
||||
}
|
||||
|
||||
/** Normalized keys of a registry (paths only for potion effects), or null while that registry is empty. */
|
||||
private Set<String> keySet(CompatRegistry registry) {
|
||||
Set<String> cached = keySets.get(registry);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
List<String> keys;
|
||||
try {
|
||||
keys = switch (registry) {
|
||||
case BLOCK -> registries.blockKeys();
|
||||
case ITEM -> registries.itemKeys();
|
||||
case ENTITY -> registries.entityKeys();
|
||||
case BIOME -> registries.biomeKeys();
|
||||
case STRUCTURE -> registries.structureKeys();
|
||||
case ENCHANTMENT -> registries.enchantmentKeys();
|
||||
case POTION_EFFECT -> registries.potionEffectKeys();
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Set<String> built = new HashSet<>(keys.size() * 2);
|
||||
for (String key : keys) {
|
||||
String normalized = normalizeState(key);
|
||||
if (normalized != null) {
|
||||
String base = baseKey(normalized);
|
||||
built.add(registry == CompatRegistry.POTION_EFFECT ? pathOf(base) : base);
|
||||
}
|
||||
}
|
||||
Set<String> frozen = Collections.unmodifiableSet(built);
|
||||
keySets.putIfAbsent(registry, frozen);
|
||||
return frozen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param state the state to generate
|
||||
* @param resolvedKey the normalized key that produced {@code state}
|
||||
* @param substituted true when a dimension fallback produced {@code state} (reported)
|
||||
* @param substitutedFrom the missing key the fallback replaced, or null
|
||||
* @param source which step of the chain answered
|
||||
*/
|
||||
public record BlockResolution(PlatformBlockState state, String resolvedKey, boolean substituted, String substitutedFrom,
|
||||
Source source) {
|
||||
public enum Source {
|
||||
REGISTRY,
|
||||
RENAME,
|
||||
FALLBACK
|
||||
}
|
||||
|
||||
public BlockResolution {
|
||||
Objects.requireNonNull(state, "state");
|
||||
Objects.requireNonNull(source, "source");
|
||||
}
|
||||
|
||||
public BlockResolution(PlatformBlockState state, String resolvedKey, boolean substituted, String substitutedFrom) {
|
||||
this(state, resolvedKey, substituted, substitutedFrom, substituted ? Source.FALLBACK : Source.REGISTRY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
/**
|
||||
* Existence of a key in the live platform registry. {@code UNKNOWN} means the registry could not be consulted (not
|
||||
* bound, or empty because the server is not ready yet); it must never be treated as {@code MISSING}.
|
||||
*/
|
||||
public enum KeyStatus {
|
||||
PRESENT,
|
||||
MISSING,
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The legacy block rename table: keys Minecraft renamed or removed, and the key to try instead. Pure data with no
|
||||
* platform types so the {@link ContentGate} applies it identically on Bukkit and the modded loaders; the Bukkit
|
||||
* {@code IrisCompat} filter list is built from the same entries.
|
||||
* <p>
|
||||
* A {@code when} without {@code exact} matches the bare block name (namespace and properties stripped, case
|
||||
* insensitive); an exact entry matches the whole normalized state. Supplements can chain (the first entry whose
|
||||
* supplement exists wins) and the table contains cycles, so callers bound the number of hops.
|
||||
*/
|
||||
public final class LegacyBlockRenames {
|
||||
public static final int MAX_HOPS = 16;
|
||||
|
||||
public record Rename(String when, String supplement, boolean exact) {
|
||||
public Rename(String when, String supplement) {
|
||||
this(when, supplement, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static final List<Rename> DEFAULTS = defaults0();
|
||||
private static final Map<String, String> BY_NAME = byName(DEFAULTS);
|
||||
private static final Map<String, String> BY_STATE = byState(DEFAULTS);
|
||||
|
||||
private LegacyBlockRenames() {
|
||||
}
|
||||
|
||||
/** Every entry in table order. */
|
||||
public static List<Rename> defaults() {
|
||||
return DEFAULTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supplement for a normalized state ({@code namespace:name[props]}, lowercase), or null when no entry applies.
|
||||
* Exact entries are consulted first, then the bare name.
|
||||
*/
|
||||
public static String supplementFor(String normalizedState) {
|
||||
if (normalizedState == null) {
|
||||
return null;
|
||||
}
|
||||
String exact = BY_STATE.get(normalizedState);
|
||||
if (exact != null) {
|
||||
return exact;
|
||||
}
|
||||
String base = ContentGate.baseKey(normalizedState);
|
||||
int namespace = base.indexOf(':');
|
||||
return BY_NAME.get(namespace < 0 ? base : base.substring(namespace + 1));
|
||||
}
|
||||
|
||||
private static Map<String, String> byName(List<Rename> renames) {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
for (Rename rename : renames) {
|
||||
if (!rename.exact()) {
|
||||
map.putIfAbsent(rename.when().toLowerCase(Locale.ROOT), rename.supplement());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private static Map<String, String> byState(List<Rename> renames) {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
for (Rename rename : renames) {
|
||||
if (rename.exact()) {
|
||||
String state = ContentGate.normalizeState(rename.when());
|
||||
if (state != null) {
|
||||
map.putIfAbsent(state, rename.supplement());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private static List<Rename> defaults0() {
|
||||
List<Rename> filters = new ArrayList<>();
|
||||
|
||||
filters.add(new Rename("CHAIN", "IRON_CHAIN"));
|
||||
filters.add(new Rename("GRASS", "SHORT_GRASS"));
|
||||
filters.add(new Rename("SHORT_GRASS", "GRASS"));
|
||||
|
||||
// Below 1.16
|
||||
filters.add(new Rename("WEEPING_VINES", "NETHER_FENCE"));
|
||||
filters.add(new Rename("WEEPING_VINES_PLANT", "NETHER_FENCE"));
|
||||
filters.add(new Rename("WARPED_WART_BLOCK", "NETHER_WART_BLOCK"));
|
||||
filters.add(new Rename("TWISTING_VINES", "BAMBOO"));
|
||||
filters.add(new Rename("TWISTING_VINES_PLANT", "BAMBOO"));
|
||||
filters.add(new Rename("TARGET", "COBBLESTONE"));
|
||||
filters.add(new Rename("SOUL_SOIL", "SOULSAND"));
|
||||
filters.add(new Rename("SOUL_TORCH", "TORCH"));
|
||||
filters.add(new Rename("SOUL_LANTERN", "LANTERN"));
|
||||
filters.add(new Rename("SOUL_FIRE", "FIRE"));
|
||||
filters.add(new Rename("SOUL_CAMPFIRE", "CAMPFIRE"));
|
||||
filters.add(new Rename("SHROOMLIGHT", "GLOWSTONE"));
|
||||
filters.add(new Rename("RESPAWN_ANCHOR", "OBSIDIAN"));
|
||||
filters.add(new Rename("NETHER_SPROUTS", "RED_MUSHROOM"));
|
||||
filters.add(new Rename("NETHER_GOLD_ORE", "GOLD_ORE"));
|
||||
filters.add(new Rename("LODESTONE", "STONE"));
|
||||
filters.add(new Rename("STRIPPED_WARPED_HYPHAE", "BROWN_MUSHROOM_BLOCK"));
|
||||
filters.add(new Rename("STRIPPED_CRIMSON_HYPHAE", "RED_MUSHROOM_BLOCK"));
|
||||
filters.add(new Rename("WARPED_HYPHAE", "MUSHROOM_STEM"));
|
||||
filters.add(new Rename("CRIMSON_HYPHAE", "RED_MUSHROOM_BLOCK"));
|
||||
filters.add(new Rename("GILDED_BLACKSTONE", "COBBLESTONE"));
|
||||
filters.add(new Rename("CRYING_OBSIDIAN", "OBSIDIAN"));
|
||||
filters.add(new Rename("STRIPPED_WARPED_STEM", "MUSHROOM_STEM"));
|
||||
filters.add(new Rename("STRIPPED_CRIMSON_STEM", "MUSHROOM_STEM"));
|
||||
filters.add(new Rename("WARPED_STEM", "MUSHROOM_STEM"));
|
||||
filters.add(new Rename("CRIMSON_STEM", "MUSHROOM_STEM"));
|
||||
filters.add(new Rename("CRIMSON_ROOTS", "RED_MUSHROOM"));
|
||||
filters.add(new Rename("WARPED_ROOTS", "BROWN_MUSHROOM"));
|
||||
filters.add(new Rename("CRIMSON_PLANKS", "OAK_PLANKS"));
|
||||
filters.add(new Rename("WARPED_PLANKS", "OAK_PLANKS"));
|
||||
filters.add(new Rename("WARPED_NYLIUM", "MYCELIUM"));
|
||||
filters.add(new Rename("CRIMSON_NYLIUM", "MYCELIUM"));
|
||||
filters.add(new Rename("WARPED_FUNGUS", "BROWN_MUSHROOM"));
|
||||
filters.add(new Rename("CRIMSON_FUNGUS", "RED_MUSHROOM"));
|
||||
filters.add(new Rename("CRACKED_NETHER_BRICKS", "NETHER_BRICKS"));
|
||||
filters.add(new Rename("CHISELED_NETHER_BRICKS", "NETHER_BRICKS"));
|
||||
filters.add(new Rename("NETHER_FENCE", "LEGACY_NETHER_FENCE"));
|
||||
filters.add(new Rename("IRON_CHAIN", "IRON_BARS"));
|
||||
filters.add(new Rename("NETHERITE_BLOCK", "QUARTZ_BLOCK"));
|
||||
filters.add(new Rename("BLACKSTONE", "COBBLESTONE"));
|
||||
filters.add(new Rename("BASALT", "STONE"));
|
||||
filters.add(new Rename("ANCIENT_DEBRIS", "NETHERRACK"));
|
||||
filters.add(new Rename("NETHERRACK", "LEGACY_NETHERRACK"));
|
||||
|
||||
// Below 1.15
|
||||
filters.add(new Rename("HONEY_BLOCK", "OAK_LEAVES"));
|
||||
filters.add(new Rename("BEEHIVE", "OAK_LEAVES"));
|
||||
filters.add(new Rename("BEE_NEST", "OAK_LEAVES"));
|
||||
|
||||
// Below 1.14
|
||||
filters.add(new Rename("GRANITE_WALL", "COBBLESTONE_WALL"));
|
||||
filters.add(new Rename("BLUE_ICE", "PACKED_ICE"));
|
||||
filters.add(new Rename("DIORITE_WALL", "COBBLESTONE_WALL"));
|
||||
filters.add(new Rename("ANDESITE_WALL", "COBBLESTONE_WALL"));
|
||||
filters.add(new Rename("SWEET_BERRY_BUSH", "GRASS"));
|
||||
filters.add(new Rename("STONECUTTER", "CRAFTING_TABLE"));
|
||||
filters.add(new Rename("SANDSTONE_STAIRS", "LEGACY_SANDSTONE_STAIRS"));
|
||||
filters.add(new Rename("SMOOTH_SANDSTONE_STAIRS", "LEGACY_SANDSTONE_STAIRS"));
|
||||
filters.add(new Rename("MOSSY_COBBLESTONE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new Rename("MOSSY_STONE_BRICK_STAIRS", "STONE_BRICK_STAIRS"));
|
||||
filters.add(new Rename("POLISHED_GRANITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new Rename("GRANITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new Rename("POLISHED_DIORITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new Rename("DIORITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new Rename("POLISHED_ANDESITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new Rename("ANDESITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new Rename("STONE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new Rename("END_STONE_BRICK_STAIRS", "LEGACY_SANDSTONE_STAIRS"));
|
||||
filters.add(new Rename("NETHER_BRICK_STAIRS", "LEGACY_NETHER_BRICK_STAIRS"));
|
||||
filters.add(new Rename("RED_NETHER_BRICK_STAIRS", "NETHER_BRICK_STAIRS"));
|
||||
filters.add(new Rename("SMOOTH_QUARTZ_STAIRS", "LEGACY_QUARTZ_STAIRS"));
|
||||
filters.add(new Rename("QUARTZ_STAIRS", "LEGACY_QUARTZ_STAIRS"));
|
||||
filters.add(new Rename("RED_SANDSTONE_STAIRS", "LEGACY_RED_SANDSTONE_STAIRS"));
|
||||
filters.add(new Rename("SMOOTH_RED_SANDSTONE_STAIRS", "LEGACY_RED_SANDSTONE_STAIRS"));
|
||||
filters.add(new Rename("STONE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new Rename("SMOKER", "FURNACE"));
|
||||
filters.add(new Rename("SMITHING_TABLE", "CRAFTING_TABLE"));
|
||||
filters.add(new Rename("END_STONE_BRICK_SLAB", "SANDSTONE_SLAB"));
|
||||
filters.add(new Rename("RED_NETHER_BRICK_SLAB", "NETHER_BRICK_SLAB"));
|
||||
filters.add(new Rename("SMOOTH_QUARTZ_SLAB", "QUARTZ_SLAB"));
|
||||
filters.add(new Rename("CUT_SANDSTONE_SLAB", "SANDSTONE_SLAB"));
|
||||
filters.add(new Rename("CUT_RED_SANDSTONE_SLAB", "RED_SANDSTONE_SLAB"));
|
||||
filters.add(new Rename("SMOOTH_RED_SANDSTONE_SLAB", "RED_SANDSTONE_SLAB"));
|
||||
filters.add(new Rename("SMOOTH_SANDSTONE_SLAB", "SANDSTONE_SLAB"));
|
||||
filters.add(new Rename("MOSSY_COBBLESTONE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new Rename("MOSSY_STONE_BRICK_SLAB", "STONE_BRICK_SLAB"));
|
||||
filters.add(new Rename("STONE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new Rename("ANDESITE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new Rename("ANDESITE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new Rename("DIORITE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new Rename("GRANITE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new Rename("POLISHED_ANDESITE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new Rename("POLISHED_DIORITE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new Rename("POLISHED_GRANITE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new Rename("WARPED_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new Rename("WARPED_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new Rename("SPRUCE_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new Rename("SPRUCE_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new Rename("OAK_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new Rename("OAK_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new Rename("JUNGLE_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new Rename("JUNGLE_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new Rename("DARK_OAK_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new Rename("DARK_OAK_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new Rename("CRIMSON_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new Rename("CRIMSON_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new Rename("BIRCH_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new Rename("BIRCH_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new Rename("ACACIA_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new Rename("ACACIA_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new Rename("SCAFFOLDING", "BIRCH_FENCE"));
|
||||
filters.add(new Rename("LECTERN", "BOOKSHELF"));
|
||||
filters.add(new Rename("LANTERN", "REDSTONE_LAMP"));
|
||||
filters.add(new Rename("JIGSAW", "AIR"));
|
||||
filters.add(new Rename("GRINDSTONE", "COBBLESTONE"));
|
||||
filters.add(new Rename("FLETCHING_TABLE", "CRAFTING_TABLE"));
|
||||
filters.add(new Rename("COMPOSTER", "CHEST"));
|
||||
filters.add(new Rename("CARTOGRAPHY_TABLE", "CRAFTING_TABLE"));
|
||||
filters.add(new Rename("CAMPFIRE", "DARK_OAK_SLAB"));
|
||||
filters.add(new Rename("BLAST_FURNACE", "FURNACE"));
|
||||
filters.add(new Rename("BELL", "REDSTONE_LAMP"));
|
||||
filters.add(new Rename("minecraft:barrel[facing=south]", "minecraft:hay_bale[axis=z]", true));
|
||||
filters.add(new Rename("minecraft:barrel[facing=north]", "minecraft:hay_bale[axis=z]", true));
|
||||
filters.add(new Rename("minecraft:barrel[facing=east]", "minecraft:hay_bale[axis=x]", true));
|
||||
filters.add(new Rename("minecraft:barrel[facing=west]", "minecraft:hay_bale[axis=x]", true));
|
||||
filters.add(new Rename("minecraft:barrel[facing=up]", "minecraft:hay_bale[axis=y]", true));
|
||||
filters.add(new Rename("minecraft:barrel[facing=down]", "minecraft:hay_bale[axis=y]", true));
|
||||
filters.add(new Rename("BAMBOO", "BIRCH_FENCE"));
|
||||
filters.add(new Rename("BAMBOO_SAPLING", "BIRCH_SAPLING"));
|
||||
filters.add(new Rename("POTTED_BAMBOO", "POTTED_BIRCH_SAPLING"));
|
||||
|
||||
return Collections.unmodifiableList(filters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Placeholder for a block key the running server does not have. It keeps the missing key so an object placement's
|
||||
* {@code edit} rule can still match and rewrite it (the type-replace rescue), and behaves as the platform's air
|
||||
* everywhere else: every physical predicate and {@link #nativeHandle()} come from the real air state, so a placeholder
|
||||
* that does reach the world places air and never throws.
|
||||
*/
|
||||
public final class MissingBlockState implements PlatformBlockState {
|
||||
private final String key;
|
||||
private final String materialKey;
|
||||
private final String namespace;
|
||||
private final Map<String, String> properties;
|
||||
private final PlatformBlockState air;
|
||||
|
||||
private MissingBlockState(String key, PlatformBlockState air) {
|
||||
this.key = key;
|
||||
this.materialKey = ContentGate.baseKey(key);
|
||||
int colon = materialKey.indexOf(':');
|
||||
this.namespace = colon < 0 ? "minecraft" : materialKey.substring(0, colon);
|
||||
this.properties = properties(key);
|
||||
this.air = air;
|
||||
}
|
||||
|
||||
/** @param normalizedKey the missing key as {@link ContentGate#normalizeState(String)} returns it */
|
||||
public static MissingBlockState of(String normalizedKey, PlatformBlockState air) {
|
||||
return new MissingBlockState(Objects.requireNonNull(normalizedKey, "key"), Objects.requireNonNull(air, "air"));
|
||||
}
|
||||
|
||||
public static boolean isPlaceholder(PlatformBlockState state) {
|
||||
return state instanceof MissingBlockState;
|
||||
}
|
||||
|
||||
/** The missing key, without properties. */
|
||||
public String missingKey() {
|
||||
return materialKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String namespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String materialKey() {
|
||||
return materialKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAir() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOccluding() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCustom() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFluid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWater() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWaterLogged() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdatable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTreeBlock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliagePlantable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDecorant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorageChest() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOre() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeepSlate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVineBlock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlaceOnto(PlatformBlockState onto) {
|
||||
return air.canPlaceOnto(onto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same missing block, and every property this placeholder names is present with the same value on {@code state} -
|
||||
* the partial-match contract the adapters use for exact {@code find} entries.
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(PlatformBlockState state) {
|
||||
if (!(state instanceof MissingBlockState other) || !materialKey.equals(other.materialKey)) {
|
||||
return false;
|
||||
}
|
||||
for (Map.Entry<String, String> required : properties.entrySet()) {
|
||||
if (!required.getValue().equals(other.properties.get(required.getKey()))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTileEntity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState withProperty(String name, String value) {
|
||||
Map<String, String> merged = new LinkedHashMap<>(properties);
|
||||
merged.put(name, value);
|
||||
StringBuilder rebuilt = new StringBuilder(materialKey).append('[');
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String> entry : merged.entrySet()) {
|
||||
if (!first) {
|
||||
rebuilt.append(',');
|
||||
}
|
||||
rebuilt.append(entry.getKey()).append('=').append(entry.getValue());
|
||||
first = false;
|
||||
}
|
||||
return new MissingBlockState(rebuilt.append(']').toString(), air);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nativeHandle() {
|
||||
return air.nativeHandle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return this == other || (other instanceof MissingBlockState state && key.equals(state.key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return key.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "missing:" + key;
|
||||
}
|
||||
|
||||
private static Map<String, String> properties(String key) {
|
||||
int open = key.indexOf('[');
|
||||
if (open < 0) {
|
||||
return Map.of();
|
||||
}
|
||||
int close = key.lastIndexOf(']');
|
||||
String body = key.substring(open + 1, close < open ? key.length() : close);
|
||||
Map<String, String> parsed = new LinkedHashMap<>();
|
||||
for (String pair : body.split(",")) {
|
||||
int eq = pair.indexOf('=');
|
||||
if (eq > 0) {
|
||||
parsed.put(pair.substring(0, eq).trim(), pair.substring(eq + 1).trim());
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit 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.core.compat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Every gating decision made for one pack instance ({@code IrisData}). Thread-safe; findings are deduplicated on
|
||||
* {@link CompatFinding#dedupKey()} and kept in first-recorded order. Console rendering lives here so Bukkit and modded
|
||||
* print the same text.
|
||||
*/
|
||||
public final class PackCompatReport {
|
||||
private static final Comparator<CompatFinding> BY_ACTION = Comparator.comparingInt(finding -> finding.action().ordinal());
|
||||
|
||||
private final Map<String, CompatFinding> findings = new LinkedHashMap<>();
|
||||
private String incompleteReason;
|
||||
|
||||
/** A report rebuilt from persisted findings, for rendering a boot listing or a command listing again. */
|
||||
public static PackCompatReport of(Iterable<CompatFinding> findings) {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
report.recordAll(findings);
|
||||
return report;
|
||||
}
|
||||
|
||||
public synchronized void record(CompatFinding finding) {
|
||||
if (finding == null) {
|
||||
return;
|
||||
}
|
||||
findings.putIfAbsent(finding.dedupKey(), finding);
|
||||
}
|
||||
|
||||
public synchronized void recordAll(Iterable<CompatFinding> all) {
|
||||
if (all == null) {
|
||||
return;
|
||||
}
|
||||
for (CompatFinding finding : all) {
|
||||
record(finding);
|
||||
}
|
||||
}
|
||||
|
||||
/** Forget every finding and any incomplete marker; a studio hotload rebuilds the pack's report from scratch. */
|
||||
public synchronized void clear() {
|
||||
findings.clear();
|
||||
incompleteReason = null;
|
||||
}
|
||||
|
||||
/** The registry could not be consulted for part of the pack; the report may be missing findings. */
|
||||
public synchronized void markIncomplete(String reason) {
|
||||
if (incompleteReason == null) {
|
||||
incompleteReason = reason == null ? "registry not ready" : reason;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isIncomplete() {
|
||||
return incompleteReason != null;
|
||||
}
|
||||
|
||||
public synchronized String incompleteReason() {
|
||||
return incompleteReason;
|
||||
}
|
||||
|
||||
public synchronized boolean isEmpty() {
|
||||
return findings.isEmpty();
|
||||
}
|
||||
|
||||
public synchronized int size() {
|
||||
return findings.size();
|
||||
}
|
||||
|
||||
public synchronized List<CompatFinding> findings() {
|
||||
return List.copyOf(findings.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Findings grouped by {@link CompatFinding#groupKey()}, keys in first-seen order; inside a key the exclusions come
|
||||
* first, then the drops, then the substitutions (first-seen within each action), so a capped listing shows the
|
||||
* decisions that removed content before the ones that kept it.
|
||||
*/
|
||||
public synchronized Map<String, List<CompatFinding>> byKey() {
|
||||
Map<String, List<CompatFinding>> grouped = new LinkedHashMap<>();
|
||||
for (CompatFinding finding : findings.values()) {
|
||||
grouped.computeIfAbsent(finding.groupKey(), ignored -> new ArrayList<>()).add(finding);
|
||||
}
|
||||
for (List<CompatFinding> group : grouped.values()) {
|
||||
group.sort(BY_ACTION);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
public synchronized Map<CompatAction, Integer> countsByAction() {
|
||||
Map<CompatAction, Integer> counts = new EnumMap<>(CompatAction.class);
|
||||
for (CompatFinding finding : findings.values()) {
|
||||
counts.merge(finding.action(), 1, Integer::sum);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
public synchronized int distinctKeys() {
|
||||
return byKey().size();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code 3 content keys unavailable on Minecraft 26.1.2: 2 excluded, 5 dropped, 1 substituted.} Empty string when
|
||||
* there is nothing to say.
|
||||
*/
|
||||
public synchronized String summaryLine(String minecraftVersion) {
|
||||
if (findings.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
Map<CompatAction, Integer> counts = countsByAction();
|
||||
int keys = distinctKeys();
|
||||
StringBuilder sb = new StringBuilder(96);
|
||||
sb.append(keys).append(keys == 1 ? " content key" : " content keys")
|
||||
.append(" unavailable on Minecraft ").append(versionOrUnknown(minecraftVersion)).append(": ");
|
||||
boolean first = true;
|
||||
for (CompatAction action : CompatAction.values()) {
|
||||
int count = counts.getOrDefault(action, 0);
|
||||
if (count == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!first) {
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(count).append(' ').append(action.label());
|
||||
first = false;
|
||||
}
|
||||
sb.append('.');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot listing: header, one line per key with up to {@code perKeyCap} subjects, then the remediation hint. Empty
|
||||
* list when there are no findings.
|
||||
*/
|
||||
public synchronized List<String> bootLines(String packName, String minecraftVersion, int perKeyCap) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (findings.isEmpty()) {
|
||||
return lines;
|
||||
}
|
||||
lines.add("Pack '" + packName + "': content unavailable on Minecraft " + versionOrUnknown(minecraftVersion));
|
||||
for (String line : keyLines(perKeyCap)) {
|
||||
lines.add(" " + line);
|
||||
}
|
||||
lines.add(" Update the server to a newer Minecraft to restore this content, or declare fallbacks"
|
||||
+ " (dimension blockFallbacks, block backup). /iris pack compat " + packName + " lists everything.");
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* One line per key, with up to {@code perKeyCap} subjects each and a {@code +N more} tail beyond that; pass 0 for
|
||||
* every subject. No header and no remediation hint - the command wraps these in localized text.
|
||||
*/
|
||||
public synchronized List<String> keyLines(int perKeyCap) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (Map.Entry<String, List<CompatFinding>> entry : byKey().entrySet()) {
|
||||
lines.add(renderKeyLine(entry.getValue(), perKeyCap));
|
||||
}
|
||||
if (incompleteReason != null) {
|
||||
lines.add("(incomplete: " + incompleteReason + ")");
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** One line per finding, grouped under a key header line. Empty list when there are no findings. */
|
||||
public synchronized List<String> detailLines() {
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (Map.Entry<String, List<CompatFinding>> entry : byKey().entrySet()) {
|
||||
CompatFinding head = entry.getValue().getFirst();
|
||||
lines.add(head.key() + " (" + head.registry().label() + ")");
|
||||
for (CompatFinding finding : entry.getValue()) {
|
||||
lines.add(" " + finding.subjectLine());
|
||||
}
|
||||
}
|
||||
if (incompleteReason != null) {
|
||||
lines.add("(incomplete: " + incompleteReason + ")");
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static String renderKeyLine(List<CompatFinding> group, int perKeyCap) {
|
||||
CompatFinding head = group.getFirst();
|
||||
StringBuilder sb = new StringBuilder(128);
|
||||
sb.append(head.key()).append(" (").append(head.registry().label()).append("): ");
|
||||
int cap = perKeyCap <= 0 ? Integer.MAX_VALUE : perKeyCap;
|
||||
int shown = 0;
|
||||
for (CompatFinding finding : group) {
|
||||
if (shown == cap) {
|
||||
break;
|
||||
}
|
||||
if (shown > 0) {
|
||||
sb.append("; ");
|
||||
}
|
||||
sb.append(finding.subjectLine());
|
||||
shown++;
|
||||
}
|
||||
int rest = group.size() - shown;
|
||||
if (rest > 0) {
|
||||
sb.append("; +").append(rest).append(" more");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String versionOrUnknown(String minecraftVersion) {
|
||||
return minecraftVersion == null || minecraftVersion.isBlank() ? "unknown" : minecraftVersion;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@
|
||||
|
||||
package art.arcane.iris.core.loader;
|
||||
|
||||
import art.arcane.iris.core.compat.CompatStatus;
|
||||
import art.arcane.iris.core.compat.ContentGate;
|
||||
import art.arcane.iris.core.compat.PackCompatReport;
|
||||
import com.google.gson.ExclusionStrategy;
|
||||
import com.google.gson.FieldAttributes;
|
||||
import com.google.gson.Gson;
|
||||
@@ -103,6 +106,8 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
private static final Set<IrisData> ENGINE_HOLDERS =
|
||||
Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||
private final File dataFolder;
|
||||
private final transient PackCompatReport compatReport = new PackCompatReport();
|
||||
private transient volatile ContentGate contentGate;
|
||||
private final int id;
|
||||
private final boolean datapackCompiler;
|
||||
private volatile boolean closed = false;
|
||||
@@ -429,6 +434,35 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
}
|
||||
|
||||
public void preprocessObject(IrisRegistrant t) {
|
||||
if (t == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
t.setCompat(t.evaluateCompat(getContentGate()));
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
t.setCompat(CompatStatus.OK);
|
||||
}
|
||||
}
|
||||
|
||||
/** Every version-content gating decision made for this pack instance. */
|
||||
public PackCompatReport getCompatReport() {
|
||||
return compatReport;
|
||||
}
|
||||
|
||||
/** The version-content gate for this pack instance (lazy; rebuilt on hotload with the loaders). */
|
||||
public ContentGate getContentGate() {
|
||||
ContentGate gate = contentGate;
|
||||
if (gate == null) {
|
||||
synchronized (this) {
|
||||
gate = contentGate;
|
||||
if (gate == null) {
|
||||
gate = ContentGate.forData(this);
|
||||
contentGate = gate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return gate;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
@@ -481,6 +515,8 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
public synchronized void hotloaded() {
|
||||
StructureGraphCatalog.invalidate(this);
|
||||
IrisObjectScale.invalidate(this);
|
||||
contentGate = null;
|
||||
compatReport.clear();
|
||||
closed = false;
|
||||
possibleSnippets = new KMap<>();
|
||||
builder = new GsonBuilder()
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.core.loader;
|
||||
|
||||
import art.arcane.iris.core.compat.CompatStatus;
|
||||
import art.arcane.iris.core.compat.ContentGate;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import lombok.Data;
|
||||
@@ -34,6 +36,23 @@ public abstract class IrisRegistrant {
|
||||
private transient String loadKey;
|
||||
|
||||
private transient File loadFile;
|
||||
/** Version-content gate verdict; excluded registrants must be filtered out of every pool that could pick them. */
|
||||
@EqualsAndHashCode.Exclude
|
||||
private transient CompatStatus compat = CompatStatus.OK;
|
||||
|
||||
/**
|
||||
* Version-content gate hook, called once per load from {@code IrisData.preprocessObject} before the registrant is
|
||||
* cached. The default runs the gate's annotated-field walker; subclasses whose exclusion also depends on their pools
|
||||
* (spawners, loot tables, regions, the dimension, jigsaw pieces) override, call {@code super}, and combine.
|
||||
*/
|
||||
public CompatStatus evaluateCompat(ContentGate gate) {
|
||||
return gate.evaluate(this);
|
||||
}
|
||||
|
||||
public boolean isCompatExcluded() {
|
||||
CompatStatus status = compat;
|
||||
return status != null && status.excluded();
|
||||
}
|
||||
|
||||
public abstract String getFolderName();
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
|
||||
import art.arcane.iris.core.compat.ContentGate;
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
@@ -141,10 +142,15 @@ public class IrisBlockData extends IrisRegistrant {
|
||||
return computeProperties(getData());
|
||||
}
|
||||
|
||||
/** The full state this entry names, {@code namespace:block[props]}, as the gate and the registries expect it. */
|
||||
public String stateKey() {
|
||||
return keyify(getBlock()) + computeProperties();
|
||||
}
|
||||
|
||||
public PlatformBlockState getBlockData(IrisData data) {
|
||||
return blockdata.aquire(() ->
|
||||
{
|
||||
IrisBlockData customData = data.getBlockLoader().load(getBlock(), false);
|
||||
IrisBlockData customData = data.getBlockLoader() == null ? null : data.getBlockLoader().load(getBlock(), false);
|
||||
|
||||
if (customData != null) {
|
||||
PlatformBlockState customState = customData.getBlockData(data);
|
||||
@@ -168,7 +174,7 @@ public class IrisBlockData extends IrisRegistrant {
|
||||
IrisLogging.debug("Block Data used " + sx + " (CUSTOM)");
|
||||
}
|
||||
|
||||
PlatformBlockState bx = B.getState(sx);
|
||||
PlatformBlockState bx = resolve(data, sx);
|
||||
|
||||
if (bx != null) {
|
||||
return bx;
|
||||
@@ -178,8 +184,8 @@ public class IrisBlockData extends IrisRegistrant {
|
||||
}
|
||||
}
|
||||
|
||||
String ss = keyify(getBlock()) + computeProperties();
|
||||
PlatformBlockState resolved = B.getState(ss);
|
||||
String ss = stateKey();
|
||||
PlatformBlockState resolved = resolve(data, ss);
|
||||
|
||||
if (debug) {
|
||||
IrisLogging.debug("Block Data used " + ss);
|
||||
@@ -193,10 +199,42 @@ public class IrisBlockData extends IrisRegistrant {
|
||||
return backup.getBlockData(data);
|
||||
}
|
||||
|
||||
return B.getState("AIR");
|
||||
IrisLogging.warnOnce("block-missing:" + ss, "Block " + ss + " does not exist on this server and has no backup; generating air");
|
||||
return B.getAirState();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #getBlockData(IrisData)} for match sides such as {@code edit} find lists: a key missing on this server
|
||||
* (after custom blocks, the gate chain and every backup) yields a {@link art.arcane.iris.core.compat.MissingBlockState}
|
||||
* carrying the key instead of air, so a palette placeholder with the same key can still be matched and rewritten.
|
||||
*/
|
||||
public PlatformBlockState getBlockDataOrPlaceholder(IrisData data) {
|
||||
ContentGate gate = data.getContentGate();
|
||||
String state = stateKey();
|
||||
|
||||
if (!gate.ready() || hasCustomBlock(data) || gate.resolveBlock(state) != null) {
|
||||
return getBlockData(data);
|
||||
}
|
||||
|
||||
if (backup != null) {
|
||||
return backup.getBlockDataOrPlaceholder(data);
|
||||
}
|
||||
|
||||
PlatformBlockState placeholder = gate.resolveBlockOrPlaceholder(state);
|
||||
return placeholder == null ? getBlockData(data) : placeholder;
|
||||
}
|
||||
|
||||
private boolean hasCustomBlock(IrisData data) {
|
||||
return data.getBlockLoader() != null && data.getBlockLoader().load(getBlock(), false) != null;
|
||||
}
|
||||
|
||||
/** Registry, legacy rename table, then dimension blockFallbacks; null when the state is missing on this server. */
|
||||
private static PlatformBlockState resolve(IrisData data, String state) {
|
||||
ContentGate.BlockResolution resolution = data.getContentGate().resolveBlock(state);
|
||||
return resolution == null ? null : resolution.state();
|
||||
}
|
||||
|
||||
public TileData tryGetTile(IrisData data) {
|
||||
//TODO Do like a registry thing with the tile data registry. Also update the parsing of data to include **block** entities.
|
||||
PlatformBlockState state = getBlockData(data);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.compat.LegacyBlockRenames;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -121,144 +122,9 @@ public class IrisCompat {
|
||||
|
||||
private static KList<IrisCompatabilityBlockFilter> getDefaultBlockCompatabilityFilters() {
|
||||
KList<IrisCompatabilityBlockFilter> filters = new KList<>();
|
||||
|
||||
filters.add(new IrisCompatabilityBlockFilter("CHAIN", "IRON_CHAIN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("GRASS", "SHORT_GRASS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SHORT_GRASS", "GRASS"));
|
||||
|
||||
// Below 1.16
|
||||
filters.add(new IrisCompatabilityBlockFilter("WEEPING_VINES", "NETHER_FENCE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WEEPING_VINES_PLANT", "NETHER_FENCE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WARPED_WART_BLOCK", "NETHER_WART_BLOCK"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("TWISTING_VINES", "BAMBOO"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("TWISTING_VINES_PLANT", "BAMBOO"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("TARGET", "COBBLESTONE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SOUL_SOIL", "SOULSAND"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SOUL_TORCH", "TORCH"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SOUL_LANTERN", "LANTERN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SOUL_FIRE", "FIRE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SOUL_CAMPFIRE", "CAMPFIRE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SHROOMLIGHT", "GLOWSTONE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("RESPAWN_ANCHOR", "OBSIDIAN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("NETHER_SPROUTS", "RED_MUSHROOM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("NETHER_GOLD_ORE", "GOLD_ORE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("LODESTONE", "STONE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("STRIPPED_WARPED_HYPHAE", "BROWN_MUSHROOM_BLOCK"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("STRIPPED_CRIMSON_HYPHAE", "RED_MUSHROOM_BLOCK"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WARPED_HYPHAE", "MUSHROOM_STEM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRIMSON_HYPHAE", "RED_MUSHROOM_BLOCK"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("GILDED_BLACKSTONE", "COBBLESTONE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRYING_OBSIDIAN", "OBSIDIAN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("STRIPPED_WARPED_STEM", "MUSHROOM_STEM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("STRIPPED_CRIMSON_STEM", "MUSHROOM_STEM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WARPED_STEM", "MUSHROOM_STEM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRIMSON_STEM", "MUSHROOM_STEM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRIMSON_ROOTS", "RED_MUSHROOM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WARPED_ROOTS", "BROWN_MUSHROOM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRIMSON_PLANKS", "OAK_PLANKS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WARPED_PLANKS", "OAK_PLANKS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WARPED_NYLIUM", "MYCELIUM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRIMSON_NYLIUM", "MYCELIUM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WARPED_FUNGUS", "BROWN_MUSHROOM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRIMSON_FUNGUS", "RED_MUSHROOM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRACKED_NETHER_BRICKS", "NETHER_BRICKS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CHISELED_NETHER_BRICKS", "NETHER_BRICKS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("NETHER_FENCE", "LEGACY_NETHER_FENCE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("IRON_CHAIN", "IRON_BARS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("NETHERITE_BLOCK", "QUARTZ_BLOCK"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BLACKSTONE", "COBBLESTONE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BASALT", "STONE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("ANCIENT_DEBRIS", "NETHERRACK"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("NETHERRACK", "LEGACY_NETHERRACK"));
|
||||
|
||||
// Below 1.15
|
||||
filters.add(new IrisCompatabilityBlockFilter("HONEY_BLOCK", "OAK_LEAVES"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BEEHIVE", "OAK_LEAVES"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BEE_NEST", "OAK_LEAVES"));
|
||||
|
||||
// Below 1.14
|
||||
filters.add(new IrisCompatabilityBlockFilter("GRANITE_WALL", "COBBLESTONE_WALL"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BLUE_ICE", "PACKED_ICE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("DIORITE_WALL", "COBBLESTONE_WALL"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("ANDESITE_WALL", "COBBLESTONE_WALL"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SWEET_BERRY_BUSH", "GRASS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("STONECUTTER", "CRAFTING_TABLE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SANDSTONE_STAIRS", "LEGACY_SANDSTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SMOOTH_SANDSTONE_STAIRS", "LEGACY_SANDSTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("MOSSY_COBBLESTONE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("MOSSY_STONE_BRICK_STAIRS", "STONE_BRICK_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("POLISHED_GRANITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("GRANITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("POLISHED_DIORITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("DIORITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("POLISHED_ANDESITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("ANDESITE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("STONE_STAIRS", "COBBLESTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("END_STONE_BRICK_STAIRS", "LEGACY_SANDSTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("NETHER_BRICK_STAIRS", "LEGACY_NETHER_BRICK_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("RED_NETHER_BRICK_STAIRS", "NETHER_BRICK_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SMOOTH_QUARTZ_STAIRS", "LEGACY_QUARTZ_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("QUARTZ_STAIRS", "LEGACY_QUARTZ_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("RED_SANDSTONE_STAIRS", "LEGACY_RED_SANDSTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SMOOTH_RED_SANDSTONE_STAIRS", "LEGACY_RED_SANDSTONE_STAIRS"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("STONE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SMOKER", "FURNACE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SMITHING_TABLE", "CRAFTING_TABLE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("END_STONE_BRICK_SLAB", "SANDSTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("RED_NETHER_BRICK_SLAB", "NETHER_BRICK_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SMOOTH_QUARTZ_SLAB", "QUARTZ_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CUT_SANDSTONE_SLAB", "SANDSTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CUT_RED_SANDSTONE_SLAB", "RED_SANDSTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SMOOTH_RED_SANDSTONE_SLAB", "RED_SANDSTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SMOOTH_SANDSTONE_SLAB", "SANDSTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("MOSSY_COBBLESTONE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("MOSSY_STONE_BRICK_SLAB", "STONE_BRICK_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("STONE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("ANDESITE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("ANDESITE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("DIORITE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("GRANITE_SLAB", "COBBLESTONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("POLISHED_ANDESITE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("POLISHED_DIORITE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("POLISHED_GRANITE_SLAB", "SMOOTH_STONE_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WARPED_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("WARPED_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SPRUCE_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SPRUCE_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("OAK_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("OAK_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("JUNGLE_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("JUNGLE_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("DARK_OAK_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("DARK_OAK_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRIMSON_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CRIMSON_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BIRCH_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BIRCH_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("ACACIA_WALL_SIGN", "LEGACY_WALL_SIGN"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("ACACIA_SIGN", "LEGACY_SIGN_POST"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("SCAFFOLDING", "BIRCH_FENCE"));
|
||||
//filters.add(new IrisCompatabilityBlockFilter("LOOM", "LOOM"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("LECTERN", "BOOKSHELF"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("LANTERN", "REDSTONE_LAMP"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("JIGSAW", "AIR"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("GRINDSTONE", "COBBLESTONE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("FLETCHING_TABLE", "CRAFTING_TABLE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("COMPOSTER", "CHEST"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CARTOGRAPHY_TABLE", "CRAFTING_TABLE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("CAMPFIRE", "DARK_OAK_SLAB"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BLAST_FURNACE", "FURNACE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BELL", "REDSTONE_LAMP"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("minecraft:barrel[facing=south]", "minecraft:hay_bale[axis=z]", true));
|
||||
filters.add(new IrisCompatabilityBlockFilter("minecraft:barrel[facing=north]", "minecraft:hay_bale[axis=z]", true));
|
||||
filters.add(new IrisCompatabilityBlockFilter("minecraft:barrel[facing=east]", "minecraft:hay_bale[axis=x]", true));
|
||||
filters.add(new IrisCompatabilityBlockFilter("minecraft:barrel[facing=west]", "minecraft:hay_bale[axis=x]", true));
|
||||
filters.add(new IrisCompatabilityBlockFilter("minecraft:barrel[facing=up]", "minecraft:hay_bale[axis=y]", true));
|
||||
filters.add(new IrisCompatabilityBlockFilter("minecraft:barrel[facing=down]", "minecraft:hay_bale[axis=y]", true));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BAMBOO", "BIRCH_FENCE"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("BAMBOO_SAPLING", "BIRCH_SAPLING"));
|
||||
filters.add(new IrisCompatabilityBlockFilter("POTTED_BAMBOO", "POTTED_BIRCH_SAPLING"));
|
||||
|
||||
for (LegacyBlockRenames.Rename rename : LegacyBlockRenames.defaults()) {
|
||||
filters.add(new IrisCompatabilityBlockFilter(rename.when(), rename.supplement(), rename.exact()));
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.IrisDatapackCompiler.DimensionHeight;
|
||||
import art.arcane.iris.core.compat.CompatFinding;
|
||||
import art.arcane.iris.core.compat.CompatStatus;
|
||||
import art.arcane.iris.core.compat.ContentGate;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -60,10 +63,12 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -125,6 +130,8 @@ public class IrisDimension extends IrisRegistrant {
|
||||
@MinNumber(0)
|
||||
@Desc("The version of this dimension. Changing this will stop users from accidentally upgrading (and breaking their worlds).")
|
||||
private int version = 1;
|
||||
@Desc("When a block key does not exist on the running Minecraft version, generate this block state instead. Keys are base block keys such as minecraft:sulfur; values are full block states. Applies to every palette, decorator, deposit, replace rule and object in this pack. Content that uses a missing block with no fallback here (and no per-entry backup) is excluded from generation on that version and listed at startup.")
|
||||
private KMap<String, String> blockFallbacks = new KMap<>();
|
||||
@ArrayType(min = 1, type = IrisBlockDrops.class)
|
||||
@Desc("Define custom block drops for this dimension")
|
||||
private KList<IrisBlockDrops> blockDrops = new KList<>();
|
||||
@@ -459,11 +466,18 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return regions.v();
|
||||
}
|
||||
|
||||
for (String key : getRegions()) {
|
||||
for (int index = 0; index < getRegions().size(); index++) {
|
||||
String key = getRegions().get(index);
|
||||
IrisRegion region = data.getRegionLoader().load(key);
|
||||
if (region != null) {
|
||||
regions.put(key, region);
|
||||
if (region == null) {
|
||||
continue;
|
||||
}
|
||||
if (region.isCompatExcluded()) {
|
||||
CompatPools.drop(data, region, "dimension", getLoadKey(),
|
||||
"regions[" + index + "] " + key, null);
|
||||
continue;
|
||||
}
|
||||
regions.put(key, region);
|
||||
}
|
||||
for (IrisImageMapBinding binding : getImageMaps()) {
|
||||
if (binding == null || binding.getApplication() != IrisImageMapApplication.REGION) {
|
||||
@@ -476,7 +490,7 @@ public class IrisDimension extends IrisRegistrant {
|
||||
for (String target : imageMapTargets(map)) {
|
||||
String key = localImageMapTarget(target);
|
||||
IrisRegion region = data.getRegionLoader().load(key);
|
||||
if (region != null) {
|
||||
if (region != null && !region.isCompatExcluded()) {
|
||||
regions.put(key, region);
|
||||
}
|
||||
}
|
||||
@@ -693,9 +707,11 @@ public class IrisDimension extends IrisRegistrant {
|
||||
// comes from the chunk generator's generation-settings getter, not from the datapack.
|
||||
String derivativeKey = irisBiome.getVanillaDerivativeKey();
|
||||
|
||||
ContentGate contentGate = getLoader() == null ? null : getLoader().getContentGate();
|
||||
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
String customBiomeId = customBiome.getId();
|
||||
String json = customBiome.generateJson(fixer);
|
||||
String json = customBiome.generateJson(fixer, contentGate);
|
||||
|
||||
synchronized (biomes) {
|
||||
if (!biomes.add(customBiomeId)) {
|
||||
@@ -891,6 +907,39 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
}
|
||||
|
||||
/** The declared region pool with unloadable and gate-excluded regions removed; drops are reported once. */
|
||||
private KList<IrisRegion> resolveRegionPool(IrisData data, List<CompatFinding> sink) {
|
||||
return CompatPools.load(data == null ? null : data.getRegionLoader(), getRegions(), data,
|
||||
"dimension", getLoadKey(), "regions", sink);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cascade: a dimension whose every region was excluded cannot generate anything. Pack validation turns this into a
|
||||
* blocking error. Loading regions here is safe - regions never load dimensions.
|
||||
*/
|
||||
@Override
|
||||
public CompatStatus evaluateCompat(ContentGate gate) {
|
||||
CompatStatus base = super.evaluateCompat(gate);
|
||||
|
||||
if (base.excluded()) {
|
||||
return base;
|
||||
}
|
||||
|
||||
IrisData data = getLoader();
|
||||
|
||||
if (data == null || getRegions().isEmpty()) {
|
||||
return base;
|
||||
}
|
||||
|
||||
List<CompatFinding> drops = new ArrayList<>();
|
||||
|
||||
if (!resolveRegionPool(data, drops).isEmpty()) {
|
||||
return base;
|
||||
}
|
||||
|
||||
return CompatPools.cascade(data, base, drops, "dimension", getLoadKey(), "no regions remain");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFolderName() {
|
||||
return "dimensions";
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.compat.CompatStatus;
|
||||
import art.arcane.iris.core.compat.ContentGate;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
@@ -363,4 +365,14 @@ public class IrisObject extends IrisRegistrant {
|
||||
public String getTypeName() {
|
||||
return "Object";
|
||||
}
|
||||
|
||||
/**
|
||||
* Objects are never gated on their own: whether a palette key that is missing on this server matters depends on
|
||||
* the placement that stamps it (its {@code edit} rules may rewrite it), so the verdict is made per placement and
|
||||
* per jigsaw piece. Overridden to keep the generic walker out of the block map as well.
|
||||
*/
|
||||
@Override
|
||||
public CompatStatus evaluateCompat(ContentGate gate) {
|
||||
return CompatStatus.OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.compat.ContentGate;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -40,6 +42,8 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -50,6 +54,8 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
public final class IrisObjectIO {
|
||||
private static final String V2_HEADER = "Iris V2 IOB;";
|
||||
private static final int MAX_PALETTE_ENTRIES = 32_767;
|
||||
private static final int PALETTE_CACHE_LIMIT = 8192;
|
||||
private static final Map<String, List<String>> PALETTE_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private IrisObjectIO() {
|
||||
}
|
||||
@@ -86,6 +92,32 @@ public final class IrisObjectIO {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #readPaletteKeys(File)} memoized on path, size and mtime. The version-content gate reads the same object
|
||||
* headers once per placement that lists them, which at boot is the same file dozens of times.
|
||||
*/
|
||||
public static List<String> readPaletteKeysCached(File file) {
|
||||
if (file == null || !file.isFile()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
String key = file.getPath() + '@' + file.lastModified() + '#' + file.length();
|
||||
List<String> cached = PALETTE_CACHE.get(key);
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
List<String> palette = readPaletteKeys(file);
|
||||
|
||||
if (PALETTE_CACHE.size() >= PALETTE_CACHE_LIMIT) {
|
||||
PALETTE_CACHE.clear();
|
||||
}
|
||||
|
||||
PALETTE_CACHE.put(key, palette);
|
||||
return palette;
|
||||
}
|
||||
|
||||
static IrisBlockVector sampleSize(File file) throws IOException {
|
||||
try (DataInputStream din = new DataInputStream(new FileInputStream(file))) {
|
||||
return new IrisBlockVector(din.readInt(), din.readInt(), din.readInt());
|
||||
@@ -104,7 +136,7 @@ public final class IrisObjectIO {
|
||||
|
||||
for (int i = 0; i < s; i++) {
|
||||
IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort());
|
||||
PlatformBlockState data = B.getState(din.readUTF());
|
||||
PlatformBlockState data = resolvePaletteState(self, din.readUTF());
|
||||
if (isStructureMarker(data)) {
|
||||
continue;
|
||||
}
|
||||
@@ -148,7 +180,7 @@ public final class IrisObjectIO {
|
||||
// block count (tens of thousands) instead of times the palette size (hundreds).
|
||||
PlatformBlockState[] resolved = new PlatformBlockState[palette.size()];
|
||||
for (i = 0; i < resolved.length; i++) {
|
||||
resolved[i] = B.getState(palette.get(i));
|
||||
resolved[i] = resolvePaletteState(self, palette.get(i));
|
||||
}
|
||||
|
||||
s = din.readInt();
|
||||
@@ -185,6 +217,39 @@ public final class IrisObjectIO {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Palette key to state. A key the server does not have goes through the pack's dimension {@code blockFallbacks}
|
||||
* before the plain lookup, so a declared fallback actually reaches the world instead of becoming air. Present
|
||||
* keys take exactly one registry lookup, as before.
|
||||
*/
|
||||
private static PlatformBlockState resolvePaletteState(IrisObject self, String key) {
|
||||
PlatformBlockState direct = B.getStateOrNull(key, false);
|
||||
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
IrisData data = self.getLoader();
|
||||
|
||||
if (data != null) {
|
||||
try {
|
||||
ContentGate gate = data.getContentGate();
|
||||
|
||||
if (gate != null && gate.ready()) {
|
||||
PlatformBlockState viaGate = gate.resolveBlockOrPlaceholder(key);
|
||||
|
||||
if (viaGate != null) {
|
||||
return viaGate;
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// The gate is advisory here; object loading must never fail because of it.
|
||||
}
|
||||
}
|
||||
|
||||
return B.getState(key);
|
||||
}
|
||||
|
||||
private static boolean isStructureMarker(PlatformBlockState data) {
|
||||
if (data == null) {
|
||||
return false;
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.Snippet;
|
||||
import art.arcane.iris.core.compat.MissingBlockState;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -287,6 +288,9 @@ public class IrisObjectRotation {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
if (MissingBlockState.isPlaceholder(state)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
StateRotator rotator = PLATFORM_ROTATOR;
|
||||
if (rotator != null) {
|
||||
|
||||
@@ -18,37 +18,20 @@
|
||||
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.compat.ContentGate;
|
||||
import art.arcane.iris.util.common.reflect.KeyedType;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class PotionEffectTypes {
|
||||
private static final Map<String, String> LEGACY_ALIASES;
|
||||
private static final Set<String> MISSING_WARNED = ConcurrentHashMap.newKeySet();
|
||||
|
||||
static {
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("SLOW", "SLOWNESS");
|
||||
m.put("FAST_DIGGING", "HASTE");
|
||||
m.put("SLOW_DIGGING", "MINING_FATIGUE");
|
||||
m.put("INCREASE_DAMAGE", "STRENGTH");
|
||||
m.put("HEAL", "INSTANT_HEALTH");
|
||||
m.put("HARM", "INSTANT_DAMAGE");
|
||||
m.put("JUMP", "JUMP_BOOST");
|
||||
m.put("CONFUSION", "NAUSEA");
|
||||
m.put("DAMAGE_RESISTANCE", "RESISTANCE");
|
||||
LEGACY_ALIASES = Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
private PotionEffectTypes() {
|
||||
}
|
||||
|
||||
@@ -60,7 +43,8 @@ public final class PotionEffectTypes {
|
||||
if (upper.contains(":")) {
|
||||
upper = upper.substring(upper.indexOf(':') + 1);
|
||||
}
|
||||
return LEGACY_ALIASES.getOrDefault(upper, upper);
|
||||
String path = upper.toLowerCase(Locale.ROOT);
|
||||
return ContentGate.POTION_EFFECT_ALIASES.getOrDefault(path, path).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public static PotionEffectType resolve(String rawName) {
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
package art.arcane.iris.core.compat;
|
||||
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.LogLevel;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformEntityType;
|
||||
import art.arcane.iris.spi.PlatformItem;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
/** Registry fakes for the gate tests: every block is present unless listed as missing; other registries are explicit. */
|
||||
final class CompatFixtures {
|
||||
private CompatFixtures() {
|
||||
}
|
||||
|
||||
static FakeRegistries registries(String... missingBlocks) {
|
||||
FakeRegistries registries = new FakeRegistries();
|
||||
registries.missingBlocks.addAll(List.of(missingBlocks));
|
||||
return registries;
|
||||
}
|
||||
|
||||
static FakePlatform bind(FakeRegistries registries) {
|
||||
IrisPlatforms.unbind();
|
||||
FakePlatform platform = new FakePlatform(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
IrisServices.register(PreservationRegistry.class, new NoPreservation());
|
||||
return platform;
|
||||
}
|
||||
|
||||
static void unbind() {
|
||||
IrisPlatforms.unbind();
|
||||
IrisServices.remove(PreservationRegistry.class);
|
||||
}
|
||||
|
||||
/** Loaders register their caches for shutdown; nothing to preserve in a test JVM. */
|
||||
private static final class NoPreservation implements PreservationRegistry {
|
||||
@Override
|
||||
public void register(Thread thread) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(ExecutorService service) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCache(MeteredCache cache) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dereference() {
|
||||
}
|
||||
}
|
||||
|
||||
static final class FakeRegistries implements PlatformRegistries {
|
||||
final Set<String> missingBlocks = new HashSet<>();
|
||||
final List<String> blockKeys = new ArrayList<>(List.of("minecraft:stone", "minecraft:air", "minecraft:sand"));
|
||||
final Set<String> items = new HashSet<>(List.of("minecraft:diamond", "minecraft:stone"));
|
||||
final Set<String> entities = new HashSet<>(List.of("minecraft:cow", "minecraft:zombie"));
|
||||
final Set<String> biomes = new HashSet<>(List.of("minecraft:plains", "minecraft:the_void", "minecraft:desert"));
|
||||
final Set<String> structures = new HashSet<>(List.of("minecraft:village_plains"));
|
||||
final Set<String> enchantments = new HashSet<>(List.of("minecraft:sharpness"));
|
||||
final Set<String> potionEffects = new HashSet<>(List.of("minecraft:speed", "minecraft:strength"));
|
||||
int blockLookups = 0;
|
||||
|
||||
@Override
|
||||
public PlatformBlockState block(String key) {
|
||||
PlatformBlockState state = blockOrNull(key, false);
|
||||
return state == null ? air() : state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState blockOrNull(String key) {
|
||||
return blockOrNull(key, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState blockOrNull(String key, boolean warn) {
|
||||
blockLookups++;
|
||||
String normalized = key.trim().toLowerCase(Locale.ROOT);
|
||||
int props = normalized.indexOf('[');
|
||||
String base = props < 0 ? normalized : normalized.substring(0, props);
|
||||
if (base.indexOf(':') < 0) {
|
||||
base = "minecraft:" + base;
|
||||
normalized = base + (props < 0 ? "" : normalized.substring(props));
|
||||
}
|
||||
if (missingBlocks.contains(base)) {
|
||||
return null;
|
||||
}
|
||||
return new FakeBlockState(normalized);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState air() {
|
||||
return new FakeBlockState("minecraft:air");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) {
|
||||
return ore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBiome biome(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformItem item(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformEntityType entity(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> blockKeys() {
|
||||
return blockKeys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> biomeKeys() {
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> structureKeys() {
|
||||
return new ArrayList<>(structures);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> itemKeys() {
|
||||
return new ArrayList<>(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> entityKeys() {
|
||||
return new ArrayList<>(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> blockTypeKeys() {
|
||||
return blockKeys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> enchantmentKeys() {
|
||||
return new ArrayList<>(enchantments);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> potionEffectKeys() {
|
||||
return new ArrayList<>(potionEffects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> lootTableKeys() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
record FakeBlockState(String key) implements PlatformBlockState {
|
||||
private static final Map<String, Object> HANDLES = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public String namespace() {
|
||||
return key.substring(0, key.indexOf(':'));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAir() {
|
||||
return key.equals("minecraft:air");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid() {
|
||||
return !isAir();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOccluding() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCustom() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFluid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWater() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWaterLogged() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdatable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTreeBlock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoliagePlantable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDecorant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStorageChest() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOre() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeepSlate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVineBlock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlaceOnto(PlatformBlockState onto) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(PlatformBlockState state) {
|
||||
return state != null && key.equals(state.key());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTileEntity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState withProperty(String name, String value) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** One mocked {@code BlockData} per key: the Bukkit rotation path casts the handle before it inspects it. */
|
||||
@Override
|
||||
public Object nativeHandle() {
|
||||
return HANDLES.computeIfAbsent(key, ignored -> org.mockito.Mockito.mock(org.bukkit.block.data.BlockData.class));
|
||||
}
|
||||
}
|
||||
|
||||
static final class FakePlatform implements IrisPlatform {
|
||||
private final PlatformRegistries registries;
|
||||
private final File dataFolder;
|
||||
|
||||
FakePlatform(PlatformRegistries registries) {
|
||||
this.registries = registries;
|
||||
File folder;
|
||||
try {
|
||||
folder = java.nio.file.Files.createTempDirectory("iris-compat-platform").toFile();
|
||||
} catch (java.io.IOException e) {
|
||||
folder = new File(System.getProperty("java.io.tmpdir"), "iris-compat-platform");
|
||||
}
|
||||
this.dataFolder = folder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String platformName() {
|
||||
return "fake";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String minecraftVersion() {
|
||||
return "26.1.2";
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformRegistries registries() {
|
||||
return registries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformScheduler scheduler() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformStructureHooks structureHooks() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBiomeWriter biomeWriter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File dataFolder() {
|
||||
return dataFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File dataFile(String... path) {
|
||||
return new File(dataFolder, String.join(File.separator, path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public File pluginJar() {
|
||||
return new File(".");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int irisVersionNumber() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minecraftVersionNumber() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void callEvent(Object event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchConsoleCommand(String command) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(LogLevel level, String message) {
|
||||
if (level == LogLevel.WARN || level == LogLevel.ERROR) {
|
||||
System.err.println("[compat-test " + level + "] " + message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void msg(String message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportError(Throwable error) {
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package art.arcane.iris.core.compat;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisObjectReplace;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ContentGatePlaceholderTest {
|
||||
private static CompatFixtures.FakeRegistries registries;
|
||||
|
||||
@BeforeClass
|
||||
public static void bindPlatform() {
|
||||
registries = CompatFixtures.registries("minecraft:sulfur", "minecraft:brimstone");
|
||||
CompatFixtures.bind(registries);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void unbindPlatform() {
|
||||
CompatFixtures.unbind();
|
||||
}
|
||||
|
||||
private static ContentGate gate() {
|
||||
return new ContentGate(registries, Map.of(), new PackCompatReport());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placeholderOnlyForMissingKeysAndOnlyWhenReady() {
|
||||
ContentGate gate = gate();
|
||||
PlatformBlockState present = gate.resolveBlockOrPlaceholder("STONE");
|
||||
assertFalse(MissingBlockState.isPlaceholder(present));
|
||||
assertEquals("minecraft:stone", present.key());
|
||||
|
||||
PlatformBlockState missing = gate.resolveBlockOrPlaceholder("minecraft:sulfur[lit=true]");
|
||||
assertTrue(MissingBlockState.isPlaceholder(missing));
|
||||
assertEquals("minecraft:sulfur[lit=true]", missing.key());
|
||||
assertEquals("minecraft:sulfur", missing.materialKey());
|
||||
assertEquals("minecraft", missing.namespace());
|
||||
assertSame(missing, gate.resolveBlockOrPlaceholder("minecraft:sulfur[lit=true]"));
|
||||
assertNull(gate.resolveBlockOrPlaceholder(" "));
|
||||
|
||||
ContentGate unbound = new ContentGate(null, Map.of(), new PackCompatReport());
|
||||
assertNull(unbound.resolveBlockOrPlaceholder("minecraft:sulfur"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placeholderBehavesAsAirButKeepsItsKey() {
|
||||
PlatformBlockState air = registries.air();
|
||||
MissingBlockState placeholder = MissingBlockState.of("minecraft:sulfur[lit=true,axis=y]", air);
|
||||
assertTrue(placeholder.isAir());
|
||||
assertFalse(placeholder.isSolid());
|
||||
assertFalse(placeholder.isCustom());
|
||||
assertFalse(placeholder.hasTileEntity());
|
||||
assertEquals(air.nativeHandle(), placeholder.nativeHandle());
|
||||
assertEquals("minecraft:sulfur", placeholder.missingKey());
|
||||
|
||||
MissingBlockState exact = MissingBlockState.of("minecraft:sulfur[lit=true]", air);
|
||||
MissingBlockState other = MissingBlockState.of("minecraft:brimstone", air);
|
||||
assertTrue(exact.matches(placeholder));
|
||||
assertFalse(placeholder.matches(exact));
|
||||
assertFalse(exact.matches(other));
|
||||
assertFalse(exact.matches(air));
|
||||
assertFalse(air.matches(exact));
|
||||
assertEquals(MissingBlockState.of("minecraft:sulfur[lit=true]", air), exact);
|
||||
assertEquals(exact.hashCode(), MissingBlockState.of("minecraft:sulfur[lit=true]", air).hashCode());
|
||||
assertEquals("minecraft:sulfur[lit=true,axis=y]", exact.withProperty("axis", "y").key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockDataPlaceholderFollowsTheChainBeforeGivingUp() throws Exception {
|
||||
IrisData data = IrisData.get(Files.createTempDirectory("iris-compat-placeholder").toFile());
|
||||
PlatformBlockState present = new IrisBlockData("minecraft:stone").getBlockDataOrPlaceholder(data);
|
||||
assertFalse(MissingBlockState.isPlaceholder(present));
|
||||
assertEquals("minecraft:stone", present.key());
|
||||
|
||||
PlatformBlockState revived = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand")).getBlockDataOrPlaceholder(data);
|
||||
assertEquals("minecraft:sand", revived.key());
|
||||
assertFalse(MissingBlockState.isPlaceholder(revived));
|
||||
|
||||
IrisBlockData missing = new IrisBlockData("minecraft:sulfur");
|
||||
missing.getData().put("lit", true);
|
||||
PlatformBlockState placeholder = missing.getBlockDataOrPlaceholder(data);
|
||||
assertTrue(MissingBlockState.isPlaceholder(placeholder));
|
||||
assertEquals("minecraft:sulfur[lit=true]", placeholder.key());
|
||||
assertTrue(missing.getBlockData(data).isAir());
|
||||
assertFalse(MissingBlockState.isPlaceholder(missing.getBlockData(data)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullChanceEditRuleRewritesAMissingPaletteKeyDuringPlacement() throws Exception {
|
||||
File pack = Files.createTempDirectory("iris-compat-rescue").toFile();
|
||||
writeObject(pack, "rescue", "minecraft:sulfur[lit=true]");
|
||||
IrisData data = IrisData.get(pack);
|
||||
IrisObject object = data.getObjectLoader().load("rescue");
|
||||
assertNotNull(object);
|
||||
PlatformBlockState loaded = object.getBlocks().values().iterator().next();
|
||||
assertTrue(loaded.toString(), MissingBlockState.isPlaceholder(loaded));
|
||||
|
||||
IrisObjectPlacement placement = placement();
|
||||
placement.getEdit().add(replace(1F, false, "minecraft:sulfur", "minecraft:stone"));
|
||||
RecordingPlacer placer = new RecordingPlacer();
|
||||
object.place(0, 64, 0, placer, placement, new RNG(7L), data);
|
||||
assertEquals(1, placer.writes.size());
|
||||
assertEquals("minecraft:stone", placer.writes.getFirst().key());
|
||||
|
||||
IrisObjectPlacement exact = placement();
|
||||
exact.getEdit().add(replace(1F, true, "minecraft:sulfur[lit=true]", "minecraft:sand"));
|
||||
RecordingPlacer exactPlacer = new RecordingPlacer();
|
||||
object.place(0, 64, 0, exactPlacer, exact, new RNG(7L), data);
|
||||
assertEquals("minecraft:sand", exactPlacer.writes.getFirst().key());
|
||||
|
||||
IrisObjectPlacement mismatch = placement();
|
||||
mismatch.getEdit().add(replace(1F, true, "minecraft:sulfur[lit=false]", "minecraft:sand"));
|
||||
RecordingPlacer mismatchPlacer = new RecordingPlacer();
|
||||
object.place(0, 64, 0, mismatchPlacer, mismatch, new RNG(7L), data);
|
||||
PlatformBlockState untouched = mismatchPlacer.writes.getFirst();
|
||||
assertTrue(MissingBlockState.isPlaceholder(untouched));
|
||||
assertTrue(untouched.isAir());
|
||||
assertEquals(registries.air().nativeHandle(), untouched.nativeHandle());
|
||||
}
|
||||
|
||||
private static IrisObjectPlacement placement() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setPlace(new KList<>(List.of("rescue")));
|
||||
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
placement.setRequireSurfaceSupport(false);
|
||||
return placement;
|
||||
}
|
||||
|
||||
private static IrisObjectReplace replace(float chance, boolean exact, String find, String to) {
|
||||
IrisObjectReplace rule = new IrisObjectReplace();
|
||||
rule.setChance(chance);
|
||||
rule.setExact(exact);
|
||||
rule.getFind().add(IrisBlockData.from(find));
|
||||
rule.getReplace().qclear();
|
||||
rule.getReplace().add(to);
|
||||
return rule;
|
||||
}
|
||||
|
||||
/** A one-block V2 .iob whose only palette entry is {@code state}. */
|
||||
private static void writeObject(File pack, String key, String state) throws Exception {
|
||||
File file = new File(pack, "objects/" + key + ".iob");
|
||||
assertTrue(file.getParentFile().mkdirs());
|
||||
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
|
||||
out.writeInt(1);
|
||||
out.writeInt(1);
|
||||
out.writeInt(1);
|
||||
out.writeUTF("Iris V2 IOB;");
|
||||
out.writeShort(1);
|
||||
out.writeUTF(state);
|
||||
out.writeInt(1);
|
||||
out.writeShort(0);
|
||||
out.writeShort(0);
|
||||
out.writeShort(0);
|
||||
out.writeShort(0);
|
||||
out.writeInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingPlacer implements IObjectPlacer {
|
||||
private final List<PlatformBlockState> writes = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
return 64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return 64;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, PlatformBlockState d) {
|
||||
writes.add(d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState get(int x, int y, int z) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int xx, int yy, int zz, T data) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package art.arcane.iris.core.compat;
|
||||
|
||||
import art.arcane.iris.core.compat.ContentGate.BlockResolution;
|
||||
import art.arcane.iris.core.compat.ContentGate.BlockResolution.Source;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ContentGateTest {
|
||||
private static ContentGate gate(CompatFixtures.FakeRegistries registries, Map<String, String> fallbacks) {
|
||||
return new ContentGate(registries, fallbacks, new PackCompatReport());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void presentBlockResolvesFromRegistry() {
|
||||
ContentGate gate = gate(CompatFixtures.registries(), Map.of());
|
||||
BlockResolution resolution = gate.resolveBlock("STONE");
|
||||
assertNotNull(resolution);
|
||||
assertEquals("minecraft:stone", resolution.resolvedKey());
|
||||
assertEquals("minecraft:stone", resolution.state().key());
|
||||
assertFalse(resolution.substituted());
|
||||
assertNull(resolution.substitutedFrom());
|
||||
assertEquals(Source.REGISTRY, resolution.source());
|
||||
assertEquals(KeyStatus.PRESENT, gate.block("minecraft:stone[axis=y]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingBlockResolvesToNull() {
|
||||
ContentGate gate = gate(CompatFixtures.registries("minecraft:sulfur"), Map.of());
|
||||
assertNull(gate.resolveBlock("minecraft:sulfur"));
|
||||
assertNull(gate.resolveBlock("sulfur[lit=true]"));
|
||||
assertEquals(KeyStatus.MISSING, gate.block("minecraft:sulfur"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownWhenRegistriesAbsentOrEmpty() {
|
||||
ContentGate unbound = gate(null, Map.of());
|
||||
assertFalse(unbound.ready());
|
||||
assertNull(unbound.resolveBlock("minecraft:stone"));
|
||||
assertEquals(KeyStatus.UNKNOWN, unbound.block("minecraft:stone"));
|
||||
assertEquals(KeyStatus.UNKNOWN, unbound.entity("minecraft:cow"));
|
||||
|
||||
CompatFixtures.FakeRegistries empty = CompatFixtures.registries();
|
||||
empty.blockKeys.clear();
|
||||
ContentGate notReady = gate(empty, Map.of());
|
||||
assertFalse(notReady.ready());
|
||||
assertEquals(KeyStatus.UNKNOWN, notReady.block("minecraft:stone"));
|
||||
assertEquals(KeyStatus.UNKNOWN, notReady.item("minecraft:diamond"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void perRegistryUnknownWhenThatKeyListIsEmpty() {
|
||||
CompatFixtures.FakeRegistries registries = CompatFixtures.registries();
|
||||
registries.biomes.clear();
|
||||
registries.structures.clear();
|
||||
ContentGate gate = gate(registries, Map.of());
|
||||
assertTrue(gate.ready());
|
||||
assertEquals(KeyStatus.UNKNOWN, gate.biome("minecraft:plains"));
|
||||
assertEquals(KeyStatus.UNKNOWN, gate.structure("minecraft:village_plains"));
|
||||
assertEquals(KeyStatus.PRESENT, gate.entity("COW"));
|
||||
assertEquals(KeyStatus.MISSING, gate.entity("minecraft:camel"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonBlockRegistriesNormalizeKeys() {
|
||||
ContentGate gate = gate(CompatFixtures.registries(), Map.of());
|
||||
assertEquals(KeyStatus.PRESENT, gate.item("DIAMOND"));
|
||||
assertEquals(KeyStatus.MISSING, gate.item("minecraft:netherite_upgrade_template"));
|
||||
assertEquals(KeyStatus.PRESENT, gate.biome("minecraft:plains"));
|
||||
assertEquals(KeyStatus.MISSING, gate.biome("minecraft:pale_garden"));
|
||||
assertEquals(KeyStatus.PRESENT, gate.enchantment("sharpness"));
|
||||
assertEquals(KeyStatus.MISSING, gate.enchantment("minecraft:wind_burst"));
|
||||
assertEquals(KeyStatus.PRESENT, gate.potionEffect("SPEED"));
|
||||
assertEquals(KeyStatus.PRESENT, gate.potionEffect("INCREASE_DAMAGE"));
|
||||
assertEquals(KeyStatus.MISSING, gate.potionEffect("minecraft:infested"));
|
||||
assertEquals(KeyStatus.MISSING, gate.item(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dimensionFallbackSubstitutesAndIsReported() {
|
||||
ContentGate gate = gate(CompatFixtures.registries("minecraft:sulfur"), Map.of("minecraft:sulfur", "minecraft:stone"));
|
||||
BlockResolution resolution = gate.resolveBlock("minecraft:sulfur[lit=true]");
|
||||
assertNotNull(resolution);
|
||||
assertTrue(resolution.substituted());
|
||||
assertEquals(Source.FALLBACK, resolution.source());
|
||||
assertEquals("minecraft:stone", resolution.resolvedKey());
|
||||
assertEquals("minecraft:sulfur[lit=true]", resolution.substitutedFrom());
|
||||
assertEquals("minecraft:stone", gate.fallbacks().get("minecraft:sulfur"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallbackThatIsItselfMissingCountsAsMissing() {
|
||||
ContentGate gate = gate(CompatFixtures.registries("minecraft:sulfur", "minecraft:brimstone"),
|
||||
Map.of("minecraft:sulfur", "minecraft:brimstone"));
|
||||
assertNull(gate.resolveBlock("minecraft:sulfur"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyRenameHitIsSubstitutedButNotReported() {
|
||||
ContentGate gate = gate(CompatFixtures.registries("minecraft:grass"), Map.of());
|
||||
BlockResolution resolution = gate.resolveBlock("minecraft:grass");
|
||||
assertNotNull(resolution);
|
||||
assertEquals("minecraft:short_grass", resolution.resolvedKey());
|
||||
assertEquals(Source.RENAME, resolution.source());
|
||||
assertFalse(resolution.substituted());
|
||||
assertNull(resolution.substitutedFrom());
|
||||
assertTrue(gate.report().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyRenameFollowsChainsAndGivesUpOnCycles() {
|
||||
ContentGate chained = gate(CompatFixtures.registries("minecraft:soul_soil", "minecraft:soulsand"), Map.of());
|
||||
assertNull(chained.resolveBlock("minecraft:soul_soil"));
|
||||
|
||||
ContentGate cyclic = gate(CompatFixtures.registries("minecraft:grass", "minecraft:short_grass"), Map.of());
|
||||
assertNull(cyclic.resolveBlock("minecraft:grass"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyRenameExactEntriesMatchFullState() {
|
||||
ContentGate gate = gate(CompatFixtures.registries("minecraft:barrel"), Map.of());
|
||||
BlockResolution resolution = gate.resolveBlock("minecraft:barrel[facing=east]");
|
||||
assertNotNull(resolution);
|
||||
assertEquals("minecraft:hay_bale[axis=x]", resolution.resolvedKey());
|
||||
assertEquals(Source.RENAME, resolution.source());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renameRunsBeforeFallback() {
|
||||
ContentGate gate = gate(CompatFixtures.registries("minecraft:grass"), Map.of("minecraft:grass", "minecraft:sand"));
|
||||
BlockResolution resolution = gate.resolveBlock("minecraft:grass");
|
||||
assertNotNull(resolution);
|
||||
assertEquals(Source.RENAME, resolution.source());
|
||||
assertEquals("minecraft:short_grass", resolution.resolvedKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolutionsAreCachedPerNormalizedState() {
|
||||
CompatFixtures.FakeRegistries registries = CompatFixtures.registries("minecraft:sulfur");
|
||||
ContentGate gate = gate(registries, Map.of());
|
||||
BlockResolution first = gate.resolveBlock("minecraft:stone");
|
||||
int after = registries.blockLookups;
|
||||
BlockResolution second = gate.resolveBlock("STONE");
|
||||
assertSame(first, second);
|
||||
assertEquals(after, registries.blockLookups);
|
||||
assertNull(gate.resolveBlock("minecraft:sulfur"));
|
||||
int misses = registries.blockLookups;
|
||||
assertNull(gate.resolveBlock("minecraft:sulfur"));
|
||||
assertEquals(misses, registries.blockLookups);
|
||||
gate.invalidate();
|
||||
gate.resolveBlock("minecraft:stone");
|
||||
assertTrue(registries.blockLookups > after);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalizeStateAndBaseKey() {
|
||||
assertEquals("minecraft:stone", ContentGate.normalizeState(" STONE "));
|
||||
assertEquals("minecraft:oak_log[axis=y]", ContentGate.normalizeState("Oak_Log[AXIS=Y]"));
|
||||
assertEquals("create:cogwheel", ContentGate.normalizeState("create:cogwheel"));
|
||||
assertNull(ContentGate.normalizeState(null));
|
||||
assertNull(ContentGate.normalizeState(" "));
|
||||
assertEquals("minecraft:oak_log", ContentGate.baseKey("minecraft:oak_log[axis=y]"));
|
||||
assertEquals("minecraft:oak_log", ContentGate.baseKey("minecraft:oak_log"));
|
||||
assertNull(ContentGate.baseKey(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readBlockFallbacksMergesDimensionFiles() throws Exception {
|
||||
java.io.File pack = java.nio.file.Files.createTempDirectory("iris-compat-fallbacks").toFile();
|
||||
java.io.File dimensions = new java.io.File(pack, "dimensions");
|
||||
assertTrue(dimensions.mkdirs());
|
||||
java.nio.file.Files.writeString(new java.io.File(dimensions, "overworld.json").toPath(),
|
||||
"{\"name\":\"x\",\"blockFallbacks\":{\"minecraft:sulfur\":\"minecraft:stone\"}}");
|
||||
java.nio.file.Files.writeString(new java.io.File(dimensions, "other.json").toPath(),
|
||||
"{\"name\":\"y\",\"blockFallbacks\":{\"minecraft:sulfur\":\"minecraft:sand\",\"minecraft:x\":\"minecraft:dirt\"}}");
|
||||
Map<String, String> read = ContentGate.readBlockFallbacks(pack);
|
||||
assertEquals(2, read.size());
|
||||
assertTrue(List.of("minecraft:stone", "minecraft:sand").contains(read.get("minecraft:sulfur")));
|
||||
assertEquals("minecraft:dirt", read.get("minecraft:x"));
|
||||
assertTrue(ContentGate.readBlockFallbacks(new java.io.File(pack, "nope")).isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
package art.arcane.iris.core.compat;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustomSpawn;
|
||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
import art.arcane.iris.engine.object.IrisEffect;
|
||||
import art.arcane.iris.engine.object.IrisEnchantment;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisLoot;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisObjectReplace;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListItemType;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ContentGateWalkerTest {
|
||||
private static ContentGate gate(String... missingBlocks) {
|
||||
return new ContentGate(CompatFixtures.registries(missingBlocks), Map.of(), new PackCompatReport());
|
||||
}
|
||||
|
||||
private static ContentGate gate(CompatFixtures.FakeRegistries registries) {
|
||||
return new ContentGate(registries, Map.of(), new PackCompatReport());
|
||||
}
|
||||
|
||||
private static IrisBiome biome(String key) {
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setLoadKey(key);
|
||||
return biome;
|
||||
}
|
||||
|
||||
private static IrisBiomePaletteLayer layer(String... blocks) {
|
||||
KList<IrisBlockData> palette = new KList<>();
|
||||
for (String block : blocks) {
|
||||
palette.add(new IrisBlockData(block));
|
||||
}
|
||||
return new IrisBiomePaletteLayer().setPalette(palette);
|
||||
}
|
||||
|
||||
private static IrisLoot loot(String item) {
|
||||
return new IrisLoot().setType(item);
|
||||
}
|
||||
|
||||
/** Reads a field directly; several registrant getters resolve through Bukkit registries that are absent here. */
|
||||
private static Object rawField(Object owner, String name) throws Exception {
|
||||
java.lang.reflect.Field field = owner.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field.get(owner);
|
||||
}
|
||||
|
||||
private static CompatFinding only(CompatStatus status) {
|
||||
assertEquals(status.reasons().toString(), 1, status.reasons().size());
|
||||
return status.reasons().getFirst();
|
||||
}
|
||||
|
||||
private static void assertFinding(CompatFinding finding, CompatRegistry registry, String key, CompatAction action,
|
||||
String subjectType, String subjectKey, String detail) {
|
||||
assertEquals(registry, finding.registry());
|
||||
assertEquals(key, finding.key());
|
||||
assertEquals(action, finding.action());
|
||||
assertEquals(subjectType, finding.subjectType());
|
||||
assertEquals(subjectKey, finding.subjectKey());
|
||||
assertEquals(detail, finding.detail());
|
||||
}
|
||||
|
||||
public static final class Probe extends IrisRegistrant {
|
||||
@RegistryListBlockType
|
||||
private String block = "minecraft:stone";
|
||||
@RegistryListItemType
|
||||
private KList<String> items = new KList<>();
|
||||
private KList<IrisObjectReplace> edit = new KList<>();
|
||||
private IrisObjectPlacement placement = null;
|
||||
private IrisLoot gear = null;
|
||||
private Probe self = null;
|
||||
private KList<Probe> children = new KList<>();
|
||||
private KList<IrisDecorator> decorators = new KList<>();
|
||||
|
||||
@Override
|
||||
public String getFolderName() {
|
||||
return "probes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Probe";
|
||||
}
|
||||
|
||||
void setBlock(String block) {
|
||||
this.block = block;
|
||||
}
|
||||
|
||||
KList<String> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
void setItems(KList<String> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
void setEdit(KList<IrisObjectReplace> edit) {
|
||||
this.edit = edit;
|
||||
}
|
||||
|
||||
void setPlacement(IrisObjectPlacement placement) {
|
||||
this.placement = placement;
|
||||
}
|
||||
|
||||
IrisLoot getGear() {
|
||||
return gear;
|
||||
}
|
||||
|
||||
void setGear(IrisLoot gear) {
|
||||
this.gear = gear;
|
||||
}
|
||||
|
||||
void setSelf(Probe self) {
|
||||
this.self = self;
|
||||
}
|
||||
|
||||
void setChildren(KList<Probe> children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
void setDecorators(KList<IrisDecorator> decorators) {
|
||||
this.decorators = decorators;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingLayerBlockExcludesBiomeWithFieldPath() {
|
||||
ContentGate gate = gate("minecraft:sulfur");
|
||||
IrisBiome biome = biome("cave/sulfur-grotto");
|
||||
biome.setLayers(new KList<>(List.of(layer("minecraft:stone"), layer("minecraft:stone", "minecraft:dirt", "minecraft:sulfur"))));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertTrue(status.excluded());
|
||||
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "biome", "cave/sulfur-grotto", "layers[1].palette[2]");
|
||||
assertEquals(1, gate.report().size());
|
||||
assertEquals(status.reasons().getFirst(), gate.report().findings().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockPropertiesTravelWithTheKey() {
|
||||
ContentGate gate = gate("minecraft:sulfur");
|
||||
IrisBiome biome = biome("b");
|
||||
IrisBlockData entry = new IrisBlockData("sulfur");
|
||||
entry.getData().put("lit", true);
|
||||
biome.setLayers(new KList<>(List.of(new IrisBiomePaletteLayer().setPalette(new KList<>(List.of(entry))))));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertTrue(status.excluded());
|
||||
assertEquals("minecraft:sulfur", only(status).key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backupRevivesEntryAsSubstitution() {
|
||||
ContentGate gate = gate("minecraft:sulfur");
|
||||
IrisBiome biome = biome("desert/dunes");
|
||||
IrisBlockData entry = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand"));
|
||||
biome.setLayers(new KList<>(List.of(new IrisBiomePaletteLayer().setPalette(new KList<>(List.of(entry))))));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.SUBSTITUTED, "biome", "desert/dunes", "layers[0].palette[0] (backup minecraft:sand)");
|
||||
assertEquals(1, gate.report().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backupChainsAndMissingBackupStillExcludes() {
|
||||
ContentGate gate = gate("minecraft:sulfur", "minecraft:brimstone");
|
||||
IrisBiome chained = biome("chained");
|
||||
IrisBlockData deep = new IrisBlockData("minecraft:sulfur")
|
||||
.setBackup(new IrisBlockData("minecraft:brimstone").setBackup(new IrisBlockData("minecraft:sand")));
|
||||
chained.setLayers(new KList<>(List.of(new IrisBiomePaletteLayer().setPalette(new KList<>(List.of(deep))))));
|
||||
CompatStatus revived = gate.evaluate(chained);
|
||||
assertFalse(revived.excluded());
|
||||
assertEquals("layers[0].palette[0] (backup minecraft:sand)", only(revived).detail());
|
||||
|
||||
IrisBiome dead = biome("dead");
|
||||
IrisBlockData deadEntry = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:brimstone"));
|
||||
dead.setLayers(new KList<>(List.of(new IrisBiomePaletteLayer().setPalette(new KList<>(List.of(deadEntry))))));
|
||||
CompatStatus excluded = gate.evaluate(dead);
|
||||
assertTrue(excluded.excluded());
|
||||
assertFinding(only(excluded), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "biome", "dead", "layers[0].palette[0]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dimensionFallbackSubstitutesEntry() {
|
||||
ContentGate gate = new ContentGate(CompatFixtures.registries("minecraft:sulfur"), Map.of("minecraft:sulfur", "minecraft:stone"), new PackCompatReport());
|
||||
IrisBiome biome = biome("desert/flats");
|
||||
biome.setLayers(new KList<>(List.of(layer("minecraft:sulfur"))));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.SUBSTITUTED, "biome", "desert/flats", "layers[0].palette[0] (fallback minecraft:stone)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyRenameHitIsNeitherExcludedNorReported() {
|
||||
ContentGate gate = gate("minecraft:grass");
|
||||
IrisBiome biome = biome("plains");
|
||||
biome.setLayers(new KList<>(List.of(layer("minecraft:grass"))));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertTrue(status.reasons().isEmpty());
|
||||
assertTrue(gate.report().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingEntityTypeExcludesEntity() {
|
||||
ContentGate gate = gate();
|
||||
IrisEntity entity = new IrisEntity().setType("minecraft:camel");
|
||||
entity.setLoadKey("camel");
|
||||
|
||||
CompatStatus status = gate.evaluate(entity);
|
||||
|
||||
assertTrue(status.excluded());
|
||||
assertFinding(only(status), CompatRegistry.ENTITY, "minecraft:camel", CompatAction.EXCLUDED, "entity", "camel", "type");
|
||||
assertEquals("minecraft:camel", entity.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inlinePassengerWithMissingTypeIsDroppedFromItsList() {
|
||||
ContentGate gate = gate();
|
||||
IrisEntity entity = new IrisEntity().setType("minecraft:zombie");
|
||||
entity.setLoadKey("rider");
|
||||
entity.setPassengers(new KList<>(List.of(new IrisEntity().setType("minecraft:cow"), new IrisEntity().setType("minecraft:camel"))));
|
||||
|
||||
CompatStatus status = gate.evaluate(entity);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertEquals(1, entity.getPassengers().size());
|
||||
assertEquals("minecraft:cow", entity.getPassengers().getFirst().getType());
|
||||
assertFinding(only(status), CompatRegistry.ENTITY, "minecraft:camel", CompatAction.DROPPED, "entity", "rider", "passengers[1].type");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customBiomeSpawnIsDroppedBeforeDatapackWrite() {
|
||||
ContentGate gate = gate();
|
||||
IrisBiome biome = biome("custom/lush");
|
||||
IrisBiomeCustom custom = new IrisBiomeCustom().setId("lush");
|
||||
custom.setSpawns(new KList<>(List.of(new IrisBiomeCustomSpawn().setType("minecraft:cow"), new IrisBiomeCustomSpawn().setType("camel"))));
|
||||
biome.setCustomDerivitives(new KList<>(List.of(custom)));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertEquals(1, custom.getSpawns().size());
|
||||
assertEquals("minecraft:cow", custom.getSpawns().getFirst().getTypeKey());
|
||||
assertFinding(only(status), CompatRegistry.ENTITY, "minecraft:camel", CompatAction.DROPPED, "biome", "custom/lush", "customDerivitives[0].spawns[1].type");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lootItemIsDroppedFromTable() {
|
||||
ContentGate gate = gate();
|
||||
IrisLootTable table = new IrisLootTable();
|
||||
table.setLoadKey("chests/common");
|
||||
table.setLoot(new KList<>(List.of(loot("minecraft:diamond"), loot("minecraft:mace"))));
|
||||
|
||||
CompatStatus status = gate.evaluate(table);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertEquals(1, table.getLoot().size());
|
||||
assertEquals("minecraft:diamond", table.getLoot().getFirst().getTypeKey());
|
||||
assertFinding(only(status), CompatRegistry.ITEM, "minecraft:mace", CompatAction.DROPPED, "loot", "chests/common", "loot[1].type");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingEnchantmentIsDroppedButLootEntryStays() {
|
||||
ContentGate gate = gate();
|
||||
IrisLootTable table = new IrisLootTable();
|
||||
table.setLoadKey("chests/rare");
|
||||
IrisLoot sword = loot("minecraft:diamond");
|
||||
sword.setEnchantments(new KList<>(List.of(new IrisEnchantment().setEnchantment("sharpness"), new IrisEnchantment().setEnchantment("minecraft:wind_burst"))));
|
||||
table.setLoot(new KList<>(List.of(sword)));
|
||||
|
||||
CompatStatus status = gate.evaluate(table);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertEquals(1, table.getLoot().size());
|
||||
assertEquals(1, sword.getEnchantments().size());
|
||||
assertEquals("sharpness", sword.getEnchantments().getFirst().getEnchantment());
|
||||
assertFinding(only(status), CompatRegistry.ENCHANTMENT, "minecraft:wind_burst", CompatAction.DROPPED, "loot", "chests/rare", "loot[0].enchantments[1].enchantment");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingPotionEffectDropsEffectAndBlankEffectStays() {
|
||||
ContentGate gate = gate();
|
||||
IrisBiome biome = biome("swamp");
|
||||
biome.setEffects(new KList<>(List.of(new IrisEffect().setPotionEffect("SPEED"), new IrisEffect().setPotionEffect("minecraft:infested"), new IrisEffect().setPotionEffect(""))));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertEquals(2, biome.getEffects().size());
|
||||
assertEquals("SPEED", biome.getEffects().get(0).getPotionEffect());
|
||||
assertEquals("", biome.getEffects().get(1).getPotionEffect());
|
||||
assertFinding(only(status), CompatRegistry.POTION_EFFECT, "minecraft:infested", CompatAction.DROPPED, "biome", "swamp", "effects[1].potionEffect");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scatterEntriesAreDroppedAndBiomeKeepsGenerating() {
|
||||
ContentGate gate = gate();
|
||||
IrisBiome biome = biome("plains");
|
||||
biome.setBiomeScatter(new KList<>(List.of("minecraft:plains", "minecraft:pale_garden")));
|
||||
biome.setBiomeSkyScatter(new KList<>(List.of("minecraft:pale_garden", "minecraft:desert")));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertEquals(List.of("minecraft:plains"), biome.getBiomeScatter());
|
||||
assertEquals(List.of("minecraft:desert"), biome.getBiomeSkyScatter());
|
||||
assertEquals(2, status.reasons().size());
|
||||
assertFinding(status.reasons().get(0), CompatRegistry.BIOME, "minecraft:pale_garden", CompatAction.DROPPED, "biome", "plains", "biomeScatter[1]");
|
||||
assertFinding(status.reasons().get(1), CompatRegistry.BIOME, "minecraft:pale_garden", CompatAction.DROPPED, "biome", "plains", "biomeSkyScatter[0]");
|
||||
assertEquals(1, gate.report().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingDerivativeExcludesBiome() {
|
||||
ContentGate gate = gate();
|
||||
IrisBiome biome = biome("pale");
|
||||
biome.setDerivative("minecraft:pale_garden");
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertTrue(status.excluded());
|
||||
assertFinding(only(status), CompatRegistry.BIOME, "minecraft:pale_garden", CompatAction.EXCLUDED, "biome", "pale", "derivative");
|
||||
assertEquals("minecraft:pale_garden", biome.getDerivativeKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingVanillaDerivativeIsNulled() throws Exception {
|
||||
ContentGate gate = gate();
|
||||
IrisBiome biome = biome("pale");
|
||||
biome.setVanillaDerivative("minecraft:pale_garden");
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertNull(rawField(biome, "vanillaDerivative"));
|
||||
assertFinding(only(status), CompatRegistry.BIOME, "minecraft:pale_garden", CompatAction.DROPPED, "biome", "pale", "vanillaDerivative");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectPlacementsAreNotDescended() {
|
||||
ContentGate gate = gate("minecraft:sulfur");
|
||||
IrisBiome biome = biome("plains");
|
||||
IrisObjectReplace replace = new IrisObjectReplace()
|
||||
.setFind(new KList<>(List.of(new IrisBlockData("minecraft:stone"))))
|
||||
.setReplace(new IrisMaterialPalette().setPalette(new KList<>(List.of(new IrisBlockData("minecraft:sulfur")))));
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement().setPlace(new KList<>(List.of("trees/oak"))).setEdit(new KList<>(List.of(replace)));
|
||||
biome.setObjects(new KList<>(List.of(placement)));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertTrue(status.reasons().isEmpty());
|
||||
assertTrue(gate.report().isEmpty());
|
||||
assertEquals(1, biome.getObjects().size());
|
||||
assertEquals(1, replace.getReplace().getPalette().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replaceFindIsMatchOnlyButReplacePaletteCounts() {
|
||||
ContentGate gate = gate("minecraft:sulfur");
|
||||
Probe matchOnly = new Probe();
|
||||
matchOnly.setLoadKey("match");
|
||||
matchOnly.setEdit(new KList<>(List.of(new IrisObjectReplace()
|
||||
.setFind(new KList<>(List.of(new IrisBlockData("minecraft:sulfur"))))
|
||||
.setReplace(new IrisMaterialPalette().setPalette(new KList<>(List.of(new IrisBlockData("minecraft:stone"))))))));
|
||||
assertFalse(gate.evaluate(matchOnly).excluded());
|
||||
assertTrue(gate.report().isEmpty());
|
||||
|
||||
Probe generated = new Probe();
|
||||
generated.setLoadKey("gen");
|
||||
generated.setEdit(new KList<>(List.of(new IrisObjectReplace()
|
||||
.setFind(new KList<>(List.of(new IrisBlockData("minecraft:stone"))))
|
||||
.setReplace(new IrisMaterialPalette().setPalette(new KList<>(List.of(new IrisBlockData("minecraft:sulfur"))))))));
|
||||
CompatStatus status = gate.evaluate(generated);
|
||||
assertTrue(status.excluded());
|
||||
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "probe", "gen", "edit[0].replace.palette[0]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decoratorMatchListsAreIgnoredButPaletteCounts() {
|
||||
ContentGate gate = gate("minecraft:sulfur");
|
||||
Probe whitelisted = new Probe();
|
||||
whitelisted.setLoadKey("white");
|
||||
whitelisted.setDecorators(new KList<>(List.of(new IrisDecorator()
|
||||
.setWhitelist(new KList<>(List.of(new IrisBlockData("minecraft:sulfur"))))
|
||||
.setBlacklist(new KList<>(List.of(new IrisBlockData("minecraft:sulfur")))))));
|
||||
assertFalse(gate.evaluate(whitelisted).excluded());
|
||||
assertTrue(gate.report().isEmpty());
|
||||
|
||||
Probe placing = new Probe();
|
||||
placing.setLoadKey("place");
|
||||
placing.setDecorators(new KList<>(List.of(new IrisDecorator().setPalette(new KList<>(List.of(new IrisBlockData("minecraft:sulfur")))))));
|
||||
CompatStatus status = gate.evaluate(placing);
|
||||
assertTrue(status.excluded());
|
||||
assertEquals("decorators[0].palette[0]", only(status).detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cyclicGraphsTerminate() {
|
||||
ContentGate gate = gate();
|
||||
Probe probe = new Probe();
|
||||
probe.setLoadKey("loop");
|
||||
probe.setSelf(probe);
|
||||
probe.setChildren(new KList<>(List.of(probe)));
|
||||
|
||||
CompatStatus status = gate.evaluate(probe);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertTrue(status.reasons().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notReadyProducesNoFindingsNoMutationAndMarksIncomplete() {
|
||||
ContentGate gate = new ContentGate(null, Map.of(), new PackCompatReport());
|
||||
IrisBiome biome = biome("custom/lush");
|
||||
IrisBiomeCustom custom = new IrisBiomeCustom().setId("lush");
|
||||
custom.setSpawns(new KList<>(List.of(new IrisBiomeCustomSpawn().setType("minecraft:camel"))));
|
||||
biome.setCustomDerivitives(new KList<>(List.of(custom)));
|
||||
biome.setLayers(new KList<>(List.of(layer("minecraft:sulfur"))));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertTrue(status.reasons().isEmpty());
|
||||
assertEquals(1, custom.getSpawns().size());
|
||||
assertTrue(gate.report().isEmpty());
|
||||
assertTrue(gate.report().isIncomplete());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownSingleRegistryLeavesItsKeysAlone() {
|
||||
CompatFixtures.FakeRegistries registries = CompatFixtures.registries("minecraft:sulfur");
|
||||
registries.biomes.clear();
|
||||
ContentGate gate = gate(registries);
|
||||
IrisBiome biome = biome("plains");
|
||||
biome.setDerivative("minecraft:pale_garden");
|
||||
biome.setBiomeScatter(new KList<>(List.of("minecraft:pale_garden")));
|
||||
biome.setLayers(new KList<>(List.of(layer("minecraft:sulfur"))));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertTrue(status.excluded());
|
||||
assertEquals(1, status.reasons().size());
|
||||
assertEquals(CompatRegistry.BLOCK, only(status).registry());
|
||||
assertEquals(List.of("minecraft:pale_garden"), biome.getBiomeScatter());
|
||||
assertTrue(gate.report().isIncomplete());
|
||||
assertTrue(gate.report().incompleteReason(), gate.report().incompleteReason().contains("biome"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void annotatedStringFieldsOnTheRegistrantFollowTheTable() {
|
||||
ContentGate gate = gate("minecraft:sulfur");
|
||||
Probe blocked = new Probe();
|
||||
blocked.setLoadKey("blocked");
|
||||
blocked.setBlock("minecraft:sulfur");
|
||||
CompatStatus status = gate.evaluate(blocked);
|
||||
assertTrue(status.excluded());
|
||||
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "probe", "blocked", "block");
|
||||
|
||||
Probe items = new Probe();
|
||||
items.setLoadKey("items");
|
||||
items.setItems(new KList<>(List.of("minecraft:diamond", "minecraft:mace")));
|
||||
CompatStatus dropped = gate.evaluate(items);
|
||||
assertFalse(dropped.excluded());
|
||||
assertEquals(List.of("minecraft:diamond"), items.getItems());
|
||||
assertFinding(only(dropped), CompatRegistry.ITEM, "minecraft:mace", CompatAction.DROPPED, "probe", "items", "items[1]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void droppedObjectHeldByDirectFieldIsNulled() {
|
||||
ContentGate gate = gate();
|
||||
Probe probe = new Probe();
|
||||
probe.setLoadKey("gear");
|
||||
probe.setGear(loot("minecraft:mace"));
|
||||
|
||||
CompatStatus status = gate.evaluate(probe);
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertNull(probe.getGear());
|
||||
assertFinding(only(status), CompatRegistry.ITEM, "minecraft:mace", CompatAction.DROPPED, "probe", "gear", "gear.type");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyFindingReachesReasonsAndReportDedups() {
|
||||
ContentGate gate = gate("minecraft:sulfur", "minecraft:brimstone");
|
||||
IrisBiome biome = biome("multi");
|
||||
biome.setLayers(new KList<>(List.of(layer("minecraft:sulfur", "minecraft:brimstone"), layer("minecraft:sulfur"))));
|
||||
|
||||
CompatStatus status = gate.evaluate(biome);
|
||||
|
||||
assertTrue(status.excluded());
|
||||
assertEquals(3, status.reasons().size());
|
||||
assertEquals(2, gate.report().size());
|
||||
assertEquals("layers[0].palette[0]", gate.report().byKey().get("BLOCK:minecraft:sulfur").getFirst().detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customPackBlocksResolveThroughTheLoaderStatus() {
|
||||
IrisBlockData fine = new IrisBlockData("minecraft:stone");
|
||||
fine.setCompat(CompatStatus.OK);
|
||||
IrisBlockData broken = new IrisBlockData("minecraft:sulfur");
|
||||
broken.setCompat(CompatStatus.excludedBy(List.of(new CompatFinding(CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "block", "broken", "block"))));
|
||||
Function<String, IrisBlockData> customBlocks = key -> switch (key) {
|
||||
case "fancy" -> fine;
|
||||
case "broken" -> broken;
|
||||
default -> null;
|
||||
};
|
||||
ContentGate gate = new ContentGate(CompatFixtures.registries("minecraft:sulfur"), Map::of, new PackCompatReport(), customBlocks);
|
||||
|
||||
IrisBiome ok = biome("ok");
|
||||
ok.setLayers(new KList<>(List.of(layer("fancy"))));
|
||||
assertFalse(gate.evaluate(ok).excluded());
|
||||
assertTrue(gate.report().isEmpty());
|
||||
|
||||
IrisBiome bad = biome("bad");
|
||||
bad.setLayers(new KList<>(List.of(layer("broken"))));
|
||||
CompatStatus status = gate.evaluate(bad);
|
||||
assertTrue(status.excluded());
|
||||
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "biome", "bad", "layers[0].palette[0]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockRegistrantEvaluatesItsOwnStateWithoutTheLoader() {
|
||||
Function<String, IrisBlockData> customBlocks = key -> {
|
||||
throw new IllegalStateException("loader must not be consulted for a block registrant");
|
||||
};
|
||||
ContentGate gate = new ContentGate(CompatFixtures.registries("minecraft:sulfur"), Map::of, new PackCompatReport(), customBlocks);
|
||||
IrisBlockData registrant = new IrisBlockData("minecraft:sulfur");
|
||||
registrant.setLoadKey("sulfur-variant");
|
||||
|
||||
CompatStatus status = gate.evaluate(registrant);
|
||||
|
||||
assertTrue(status.excluded());
|
||||
assertFinding(only(status), CompatRegistry.BLOCK, "minecraft:sulfur", CompatAction.EXCLUDED, "block", "sulfur-variant", "block");
|
||||
|
||||
IrisBlockData revived = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand"));
|
||||
revived.setLoadKey("sulfur-backup");
|
||||
CompatStatus substituted = gate.evaluate(revived);
|
||||
assertFalse(substituted.excluded());
|
||||
assertEquals("block (backup minecraft:sand)", only(substituted).detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullRegistrantIsOk() {
|
||||
assertNotNull(gate().evaluate(null));
|
||||
assertFalse(gate().evaluate(null).excluded());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package art.arcane.iris.core.compat;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisBlockDataResolutionTest {
|
||||
private static CompatFixtures.FakeRegistries registries;
|
||||
|
||||
@BeforeClass
|
||||
public static void bindPlatform() {
|
||||
registries = CompatFixtures.registries("minecraft:sulfur", "minecraft:brimstone", "minecraft:grass");
|
||||
CompatFixtures.bind(registries);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void unbindPlatform() {
|
||||
CompatFixtures.unbind();
|
||||
}
|
||||
|
||||
private static IrisData pack(String dimensionJson, String customBlockJson) throws Exception {
|
||||
File folder = Files.createTempDirectory("iris-compat-blockdata").toFile();
|
||||
if (dimensionJson != null) {
|
||||
File dimensions = new File(folder, "dimensions");
|
||||
assertTrue(dimensions.mkdirs());
|
||||
Files.writeString(new File(dimensions, "overworld.json").toPath(), dimensionJson);
|
||||
}
|
||||
if (customBlockJson != null) {
|
||||
File blocks = new File(folder, "blocks");
|
||||
assertTrue(blocks.mkdirs());
|
||||
Files.writeString(new File(blocks, "fancy.json").toPath(), customBlockJson);
|
||||
}
|
||||
return IrisData.get(folder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void presentBlockResolvesWithProperties() throws Exception {
|
||||
IrisData data = pack(null, null);
|
||||
IrisBlockData entry = new IrisBlockData("minecraft:oak_log");
|
||||
entry.getData().put("axis", "y");
|
||||
assertEquals("minecraft:oak_log[axis=y]", entry.getBlockData(data).key());
|
||||
assertEquals("minecraft:stone", new IrisBlockData("STONE").getBlockData(data).key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void backupRevivesMissingBlock() throws Exception {
|
||||
IrisData data = pack(null, null);
|
||||
IrisBlockData entry = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand"));
|
||||
assertEquals("minecraft:sand", entry.getBlockData(data).key());
|
||||
|
||||
IrisBlockData chained = new IrisBlockData("minecraft:sulfur")
|
||||
.setBackup(new IrisBlockData("minecraft:brimstone").setBackup(new IrisBlockData("minecraft:sand")));
|
||||
assertEquals("minecraft:sand", chained.getBlockData(data).key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingBlockWithoutBackupFallsBackToAir() throws Exception {
|
||||
IrisData data = pack(null, null);
|
||||
IrisBlockData entry = new IrisBlockData("minecraft:sulfur");
|
||||
assertTrue(entry.getBlockData(data).isAir());
|
||||
assertTrue(new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:brimstone")).getBlockData(data).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dimensionFallbackAppliesBeforeBackup() throws Exception {
|
||||
IrisData data = pack("{\"name\":\"overworld\",\"blockFallbacks\":{\"minecraft:sulfur\":\"minecraft:stone\"}}", null);
|
||||
IrisBlockData entry = new IrisBlockData("minecraft:sulfur").setBackup(new IrisBlockData("minecraft:sand"));
|
||||
assertEquals("minecraft:stone", entry.getBlockData(data).key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void legacyRenameAppliesOnEveryPlatform() throws Exception {
|
||||
IrisData data = pack(null, null);
|
||||
assertEquals("minecraft:short_grass", new IrisBlockData("minecraft:grass").getBlockData(data).key());
|
||||
assertTrue(data.getCompatReport().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customPackBlockStillResolvesFirstAndUsesItsOwnBackup() throws Exception {
|
||||
IrisData data = pack(null, "{\"block\":\"minecraft:sulfur\",\"backup\":{\"block\":\"minecraft:sand\"}}");
|
||||
IrisBlockData entry = new IrisBlockData("fancy");
|
||||
assertEquals("minecraft:sand", entry.getBlockData(data).key());
|
||||
assertFalse(data.getCompatReport().isEmpty());
|
||||
assertEquals(CompatAction.SUBSTITUTED, data.getCompatReport().findings().getFirst().action());
|
||||
assertEquals("block", data.getCompatReport().findings().getFirst().subjectType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package art.arcane.iris.core.compat;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PackCompatReportTest {
|
||||
private static CompatFinding finding(CompatAction action, String subjectKey, String detail) {
|
||||
return new CompatFinding(CompatRegistry.BLOCK, "minecraft:sulfur", action, "Biome", subjectKey, detail);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recordDedupsOnRegistryKeyActionAndSubject() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
report.record(finding(CompatAction.EXCLUDED, "cave/sulfur", "layers[0].palette[1]"));
|
||||
report.record(finding(CompatAction.EXCLUDED, "cave/sulfur", "layers[2].palette[0]"));
|
||||
report.record(finding(CompatAction.SUBSTITUTED, "cave/sulfur", "layers[1].palette[0] (backup minecraft:sand)"));
|
||||
report.record(finding(CompatAction.EXCLUDED, "desert/flats", "layers[0].palette[0]"));
|
||||
report.record(null);
|
||||
assertEquals(3, report.size());
|
||||
List<CompatFinding> findings = report.findings();
|
||||
assertEquals("layers[0].palette[1]", findings.get(0).detail());
|
||||
assertEquals(CompatAction.SUBSTITUTED, findings.get(1).action());
|
||||
assertEquals("desert/flats", findings.get(2).subjectKey());
|
||||
Map<CompatAction, Integer> counts = report.countsByAction();
|
||||
assertEquals(Integer.valueOf(2), counts.get(CompatAction.EXCLUDED));
|
||||
assertEquals(Integer.valueOf(1), counts.get(CompatAction.SUBSTITUTED));
|
||||
assertEquals(1, report.distinctKeys());
|
||||
assertEquals(3, report.byKey().get("BLOCK:minecraft:sulfur").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void markIncompleteKeepsFirstReason() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
assertFalse(report.isIncomplete());
|
||||
report.markIncomplete("biome registry not ready");
|
||||
report.markIncomplete("structure registry not ready");
|
||||
assertTrue(report.isIncomplete());
|
||||
assertEquals("biome registry not ready", report.incompleteReason());
|
||||
report.clear();
|
||||
assertFalse(report.isIncomplete());
|
||||
assertNull(report.incompleteReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearForgetsFindingsForHotload() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
report.record(finding(CompatAction.DROPPED, "cave/sulfur", "x"));
|
||||
assertFalse(report.isEmpty());
|
||||
report.clear();
|
||||
assertTrue(report.isEmpty());
|
||||
assertEquals(0, report.size());
|
||||
assertTrue(report.bootLines("overworld", "26.1.2", 3).isEmpty());
|
||||
assertEquals("", report.summaryLine("26.1.2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findingLineAndDedupKey() {
|
||||
CompatFinding finding = finding(CompatAction.EXCLUDED, "cave/sulfur", "layers[0].palette[1]");
|
||||
assertEquals("minecraft:sulfur (block): excluded Biome cave/sulfur at layers[0].palette[1]", finding.line());
|
||||
assertEquals("BLOCK:minecraft:sulfur|EXCLUDED|Biome|cave/sulfur", finding.dedupKey());
|
||||
CompatFinding blank = new CompatFinding(CompatRegistry.ENTITY, "minecraft:camel", CompatAction.DROPPED, null, null, null);
|
||||
assertEquals("minecraft:camel (entity): dropped", blank.line());
|
||||
}
|
||||
}
|
||||
@@ -252,6 +252,17 @@ public class SchemaBuilderParityTest {
|
||||
assertEquals("boolean", properties.getJSONObject("requireObjectSurfaceSupport").getString("type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dimensionSchemaListsBlockFallbacksAsPlainObjectProperty() {
|
||||
JSONObject properties = rootProperties(
|
||||
new SchemaBuilder(IrisDimension.class, structureSchemaData()).construct());
|
||||
JSONObject fallbacks = properties.getJSONObject("blockFallbacks");
|
||||
|
||||
assertEquals("object", fallbacks.getString("type"));
|
||||
assertFalse(fallbacks.has("!top"));
|
||||
assertFalse(fallbacks.has("anyOf"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jigsawStructureSchemaExposesBranchFailurePolicy() {
|
||||
JSONObject schema = new SchemaBuilder(IrisStructure.class, structureSchemaData()).construct();
|
||||
|
||||
Reference in New Issue
Block a user