Public Access
This commit is contained in:
1 parent
78a0f7ee51
commit
7b5597dd47
19 files changed
+387
-43
No files matched your search
@@ -51,6 +51,8 @@ public final class AgeDecayProviders {
|
||||
AgeDefinition age = AgeRegistrySavedData.get(level).findByDimension(level.dimension().location()).orElse(null);
|
||||
if (age == null) return -1;
|
||||
ResourceLocation id = id("decay" + color.getSerializedName());
|
||||
if (!RivencraftConfig.instabilityEnabled() || !AgeInstabilityControls.get(level).enabled(age.id())
|
||||
|| !RivencraftConfig.instabilityProviderEnabled(id)) return -1;
|
||||
int changed = 0;
|
||||
for (int i = 0; i < Math.min(count, 32); i++) {
|
||||
AgeInstabilityRuntime.Budget budget = new AgeInstabilityRuntime.Budget(1);
|
||||
|
||||
@@ -60,6 +60,9 @@ public final class AgeEnvironmentalProviders {
|
||||
if (!AgeInstabilityRuntime.isAgeDimension(level.dimension().location())) return -1;
|
||||
AgeDefinition age = AgeRegistrySavedData.get(level).findByDimension(level.dimension().location()).orElse(null);
|
||||
if (age == null) return -1;
|
||||
if (!RivencraftConfig.instabilityEnabled() || !AgeInstabilityControls.get(level).enabled(age.id())
|
||||
|| !RivencraftConfig.instabilityProviderEnabled(ResourceLocation.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, effect.equals("meteor") ? "meteors" : effect))) return 0;
|
||||
List<ChunkPos> chunks = AgeHazardProviders.loadedChunksNearPlayers(level, 16);
|
||||
if (chunks.isEmpty()) return 0;
|
||||
var context = new AgeInstabilityRuntime.Context(level, age, chunks,
|
||||
@@ -165,7 +168,8 @@ public final class AgeEnvironmentalProviders {
|
||||
return;
|
||||
}
|
||||
AgeDefinition age = AgeRegistrySavedData.get(level).findByDimension(level.dimension().location()).orElse(null);
|
||||
if (age == null) {
|
||||
if (age == null || !AgeInstabilityControls.get(level).enabled(age.id())
|
||||
|| !RivencraftConfig.instabilityProviderEnabled(METEORS)) {
|
||||
ACTIVE.remove(level);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ public final class AgeHazardProviders {
|
||||
|| !AgeInstabilityRuntime.isAgeDimension(level.dimension().location())) return -1;
|
||||
AgeDefinition age = AgeRegistrySavedData.get(level).findByDimension(level.dimension().location()).orElse(null);
|
||||
if (age == null) return -1;
|
||||
if (!RivencraftConfig.instabilityEnabled() || !AgeInstabilityControls.get(level).enabled(age.id())
|
||||
|| !RivencraftConfig.instabilityProviderEnabled(lightning ? LIGHTNING : EXPLOSIONS)) return -1;
|
||||
AgeHazardSavedData data = AgeHazardSavedData.get(level);
|
||||
if (!data.ready(lightning, level.getGameTime(), RivencraftConfig.hazardsMinInterval())) return 0;
|
||||
ChunkPos center = player.chunkPosition();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.datafix.DataFixTypes;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
|
||||
/** Per-world operator overrides. Absence means enabled; scores and plans stay immutable. */
|
||||
public final class AgeInstabilityControls extends SavedData {
|
||||
private static final String DATA_NAME = "rivencraft_instability_controls";
|
||||
private static final Factory<AgeInstabilityControls> FACTORY = new Factory<>(
|
||||
AgeInstabilityControls::new, AgeInstabilityControls::load, DataFixTypes.SAVED_DATA_MAP_DATA);
|
||||
private final Set<UUID> disabled = new HashSet<>();
|
||||
|
||||
public static AgeInstabilityControls get(ServerLevel level) {
|
||||
return level.getServer().overworld().getDataStorage().computeIfAbsent(FACTORY, DATA_NAME);
|
||||
}
|
||||
|
||||
public boolean enabled(UUID ageId) {
|
||||
return !disabled.contains(ageId);
|
||||
}
|
||||
|
||||
public boolean setEnabled(UUID ageId, boolean enabled) {
|
||||
boolean changed = enabled ? disabled.remove(ageId) : disabled.add(ageId);
|
||||
if (changed) setDirty();
|
||||
return changed;
|
||||
}
|
||||
|
||||
static AgeInstabilityControls load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
AgeInstabilityControls controls = new AgeInstabilityControls();
|
||||
ListTag entries = tag.getList("DisabledAges", CompoundTag.TAG_COMPOUND);
|
||||
for (int index = 0; index < entries.size(); index++) {
|
||||
CompoundTag entry = entries.getCompound(index);
|
||||
if (entry.hasUUID("Id")) controls.disabled.add(entry.getUUID("Id"));
|
||||
}
|
||||
return controls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
ListTag entries = new ListTag();
|
||||
disabled.stream().sorted().forEach(id -> {
|
||||
CompoundTag entry = new CompoundTag();
|
||||
entry.putUUID("Id", id);
|
||||
entries.add(entry);
|
||||
});
|
||||
tag.put("DisabledAges", entries);
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,8 @@ public final class AgeInstabilityRuntime {
|
||||
if (!admitted(state.eligible, level.dimension().location(), tick / cooldown,
|
||||
RivencraftConfig.instabilityMaxAges())) return;
|
||||
AgeDefinition age = AgeRegistrySavedData.get(level).findByDimension(level.dimension().location()).orElse(null);
|
||||
if (age == null || age.instabilityPlan().providers().isEmpty()) return;
|
||||
if (age == null || age.instabilityPlan().providers().isEmpty()
|
||||
|| !AgeInstabilityControls.get(level).enabled(age.id())) return;
|
||||
// Legacy's per-chunk hazards must not wait for the rotating two-provider sample.
|
||||
AgeHazardProviders.tickPlanned(level, age, tick);
|
||||
AgeEnvironmentalProviders.tickPlanned(level, age, tick);
|
||||
@@ -74,6 +75,8 @@ public final class AgeInstabilityRuntime {
|
||||
|
||||
/** Shared handler guard, including quarantine, for rotating and per-chunk provider lanes. */
|
||||
static boolean invoke(Context context, ResourceLocation id, int level) {
|
||||
if (!RivencraftConfig.instabilityEnabled()
|
||||
|| !AgeInstabilityControls.get(context.level()).enabled(context.age().id())) return false;
|
||||
Handler handler = HANDLERS.get(id);
|
||||
if (handler == null) {
|
||||
if (!InstabilityProviderCatalog.contains(id) && UNKNOWN_LOGGED.add(id)) {
|
||||
@@ -81,6 +84,7 @@ public final class AgeInstabilityRuntime {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!RivencraftConfig.instabilityProviderEnabled(id)) return false;
|
||||
if (handler.worldMutating() && !RivencraftConfig.instabilityWorldMutation()) return false;
|
||||
long retryAfter = FAILURES.computeIfAbsent(context.level(), ignored -> new HashMap<>())
|
||||
.getOrDefault(id, 0L);
|
||||
@@ -107,6 +111,40 @@ public final class AgeInstabilityRuntime {
|
||||
return id.getNamespace().equals(Rivencraft.MOD_ID) && id.getPath().startsWith("age/");
|
||||
}
|
||||
|
||||
/** Current runtime state, distinct from the Age's immutable score and provider selection. */
|
||||
public static String providerStatus(ServerLevel level, AgeDefinition age, ResourceLocation id) {
|
||||
if (!RivencraftConfig.instabilityEnabled()) return "global config off";
|
||||
if (!AgeInstabilityControls.get(level).enabled(age.id())) return "Age disabled";
|
||||
Handler handler = HANDLERS.get(id);
|
||||
if (handler == null) return "handler unavailable";
|
||||
if (!RivencraftConfig.instabilityProviderEnabled(id)) return "provider config off";
|
||||
if (handler.worldMutating() && !RivencraftConfig.instabilityWorldMutation()) return "world mutation off";
|
||||
String path = id.getPath();
|
||||
if (path.startsWith("decay") && !RivencraftConfig.decayEnabled()) return "decay config off";
|
||||
if ((path.equals("lightning") || path.equals("explosions")) && !RivencraftConfig.hazardsEnabled())
|
||||
return "hazards config off";
|
||||
if (path.equals("meteors") && !RivencraftConfig.meteorEnabled()) return "meteor config off";
|
||||
if (path.equals("burning") && !RivencraftConfig.scorchEnabled()) return "burning config off";
|
||||
if (path.equals("erosion") && !RivencraftConfig.erosionEnabled()) return "erosion config off";
|
||||
if (!path.startsWith("decay") && !path.equals("lightning") && !path.equals("explosions")
|
||||
&& !path.equals("meteors") && !path.equals("burning") && !path.equals("erosion")
|
||||
&& !path.equals("crumble") && !RivencraftConfig.statusEffectsEnabled())
|
||||
return "status effects config off";
|
||||
if (path.startsWith("enemy") && !RivencraftConfig.statusEffectsHostiles())
|
||||
return "hostile status effects config off";
|
||||
if (!path.startsWith("enemy") && !path.startsWith("decay") && !path.equals("lightning")
|
||||
&& !path.equals("explosions") && !path.equals("meteors") && !path.equals("burning")
|
||||
&& !path.equals("erosion") && !RivencraftConfig.statusEffectsPlayers()
|
||||
&& !RivencraftConfig.statusEffectsNonplayers())
|
||||
return "status effect targets disabled";
|
||||
ServerLevel ageLevel = level.getServer().getLevel(AgeDimensionService.dimensionKey(age));
|
||||
if (ageLevel == null) return "Age unloaded";
|
||||
if (ageLevel.players().isEmpty()) return "no players in Age";
|
||||
long retryAfter = FAILURES.getOrDefault(ageLevel, Map.of()).getOrDefault(id, 0L);
|
||||
if (ageLevel.getServer().getTickCount() < retryAfter) return "quarantined until tick " + retryAfter;
|
||||
return "eligible; chance, cooldown, and protection still apply";
|
||||
}
|
||||
|
||||
static boolean admitted(List<ResourceLocation> eligible, ResourceLocation id, long run,
|
||||
int maximum) {
|
||||
int index = eligible.indexOf(id);
|
||||
@@ -122,6 +160,7 @@ public final class AgeInstabilityRuntime {
|
||||
ResourceLocation id = level.dimension().location();
|
||||
if (isAgeDimension(id) && !level.players().isEmpty()
|
||||
&& registry.findByDimension(id).filter(age -> !age.instabilityPlan().providers().isEmpty())
|
||||
.filter(age -> AgeInstabilityControls.get(level).enabled(age.id()))
|
||||
.isPresent()) eligible.add(id);
|
||||
}
|
||||
eligible.sort(Comparator.comparing(ResourceLocation::toString));
|
||||
|
||||
@@ -59,6 +59,53 @@ public final class AgeInstabilityRuntimeGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void perAgeControlsPersistWithoutChangingScoresOrOtherAges(GameTestHelper helper) {
|
||||
UUID first = UUID.randomUUID();
|
||||
UUID second = UUID.randomUUID();
|
||||
AgeInstabilityControls controls = new AgeInstabilityControls();
|
||||
helper.assertTrue(controls.enabled(first) && controls.enabled(second),
|
||||
"Ages must be enabled by default");
|
||||
helper.assertTrue(controls.setEnabled(first, false) && !controls.enabled(first)
|
||||
&& controls.enabled(second), "Disabling one Age must not change another");
|
||||
AgeInstabilityControls restored = AgeInstabilityControls.load(
|
||||
controls.save(new CompoundTag(), helper.getLevel().registryAccess()),
|
||||
helper.getLevel().registryAccess());
|
||||
helper.assertTrue(!restored.enabled(first) && restored.enabled(second),
|
||||
"The per-Age override must survive a save round trip");
|
||||
helper.assertTrue(restored.setEnabled(first, true) && restored.enabled(first),
|
||||
"Re-enabling must remove the override");
|
||||
|
||||
var oldAge = new AgeDefinition(first, "Old Age", 1L, AgeGenerationProfiles.STANDARD,
|
||||
AgeDefinition.CURRENT_GENERATION_VERSION, 0L, AgeDefinition.dimensionId(first), null,
|
||||
AgeTraits.LEGACY_DEFAULTS);
|
||||
helper.assertValueEqual(oldAge.instability().score(), 0,
|
||||
"A pre-scoring Age must remain neutral regardless of runtime controls");
|
||||
helper.assertTrue(oldAge.instabilityPlan().providers().isEmpty(),
|
||||
"Runtime controls must not generate providers for old Ages");
|
||||
AgeInstabilityControls live = AgeInstabilityControls.get(helper.getLevel());
|
||||
live.setEnabled(first, false);
|
||||
helper.assertValueEqual(AgeInstabilityRuntime.providerStatus(helper.getLevel(), oldAge,
|
||||
ResourceLocation.parse("rivencraft:poison")), "Age disabled",
|
||||
"Diagnostics must distinguish an Age override from the saved plan");
|
||||
live.setEnabled(first, true);
|
||||
helper.assertValueEqual(AgeInstabilityRuntime.providerStatus(helper.getLevel(), oldAge,
|
||||
ResourceLocation.parse("example:missing_handler")), "handler unavailable",
|
||||
"Unknown saved providers must not be reported as active");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void instabilityControlCommandsRequireOperatorPermission(GameTestHelper helper) {
|
||||
var server = helper.getLevel().getServer();
|
||||
var command = server.getCommands().getDispatcher().getRoot().getChild("rivencraft");
|
||||
helper.assertTrue(command != null, "Rivencraft commands must be registered on a dedicated server");
|
||||
helper.assertTrue(!command.canUse(server.createCommandSourceStack().withPermission(0))
|
||||
&& command.canUse(server.createCommandSourceStack().withPermission(2)),
|
||||
"Non-operators cannot reach Age instability enable/disable commands");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void ageScopeRotationAndActionBudgetAreBounded(GameTestHelper helper) {
|
||||
ResourceLocation first = ResourceLocation.parse("rivencraft:age/first");
|
||||
|
||||
@@ -75,7 +75,7 @@ public final class AgeStatusEffectProviders {
|
||||
candidates.sort(Comparator.comparing(Entity::getUUID));
|
||||
LivingEntity target = candidates.get(random.nextInt(candidates.size()));
|
||||
if (!context.budget().claim() || !target.addEffect(instance)) return;
|
||||
leases.add(new Lease(new WeakReference<>(target), instance,
|
||||
leases.add(new Lease(id, new WeakReference<>(target), instance,
|
||||
context.serverTick() + duration, spec.target()));
|
||||
}
|
||||
|
||||
@@ -100,7 +100,12 @@ public final class AgeStatusEffectProviders {
|
||||
if (event.getServer().getTickCount() % 20 != 0) return;
|
||||
boolean disabled = !RivencraftConfig.instabilityEnabled() || !RivencraftConfig.statusEffectsEnabled();
|
||||
long tick = event.getServer().getTickCount();
|
||||
for (var entry : LEASES.entrySet()) prune(entry.getKey(), entry.getValue(), tick, disabled);
|
||||
for (var entry : LEASES.entrySet()) {
|
||||
AgeDefinition age = AgeRegistrySavedData.get(entry.getKey())
|
||||
.findByDimension(entry.getKey().dimension().location()).orElse(null);
|
||||
prune(entry.getKey(), entry.getValue(), tick, disabled || age == null
|
||||
|| !AgeInstabilityControls.get(entry.getKey()).enabled(age.id()));
|
||||
}
|
||||
LEASES.values().removeIf(List::isEmpty);
|
||||
}
|
||||
|
||||
@@ -146,7 +151,8 @@ public final class AgeStatusEffectProviders {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
boolean revoke = removeAll || entity.level() != level
|
||||
boolean revoke = removeAll || !RivencraftConfig.instabilityProviderEnabled(lease.provider())
|
||||
|| entity.level() != level
|
||||
|| !allowedByPolicy(lease.target(), entity);
|
||||
if (revoke || tick >= lease.expiresAt()) {
|
||||
removeOwnedEffect(entity, lease);
|
||||
@@ -208,11 +214,11 @@ public final class AgeStatusEffectProviders {
|
||||
|
||||
private enum Target { GENERAL, HOSTILE }
|
||||
private record Spec(Holder<MobEffect> effect, int legacyDuration, boolean global, Target target) {}
|
||||
private record Lease(WeakReference<LivingEntity> entity, MobEffectInstance instance, long expiresAt,
|
||||
private record Lease(ResourceLocation provider, WeakReference<LivingEntity> entity, MobEffectInstance instance, long expiresAt,
|
||||
Target target, int amplifier, int originalDuration) {
|
||||
private Lease(WeakReference<LivingEntity> entity, MobEffectInstance instance, long expiresAt,
|
||||
private Lease(ResourceLocation provider, WeakReference<LivingEntity> entity, MobEffectInstance instance, long expiresAt,
|
||||
Target target) {
|
||||
this(entity, instance, expiresAt, target, instance.getAmplifier(), instance.getDuration());
|
||||
this(provider, entity, instance, expiresAt, target, instance.getAmplifier(), instance.getDuration());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.client.screen.DescriptiveBookScreen;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
@@ -87,8 +88,12 @@ public final class ClientBookInteractionHandler {
|
||||
currentBook = minecraft.player.getItemInHand(hand);
|
||||
}
|
||||
// Ignore delayed replies if the player has moved or replaced the book.
|
||||
ItemStack currentWithoutSummary = currentBook.copy();
|
||||
ItemStack replyWithoutSummary = payload.book().copy();
|
||||
currentWithoutSummary.remove(ModDataComponents.AGE_INSTABILITY_SCORE.get());
|
||||
replyWithoutSummary.remove(ModDataComponents.AGE_INSTABILITY_SCORE.get());
|
||||
if (!currentBook.is(ModItems.DESCRIPTIVE_BOOK.get())
|
||||
|| !ItemStack.isSameItemSameComponents(currentBook, payload.book())) {
|
||||
|| !ItemStack.isSameItemSameComponents(currentWithoutSummary, replyWithoutSummary)) {
|
||||
return;
|
||||
}
|
||||
minecraft.setScreen(payload.fromStand()
|
||||
|
||||
@@ -54,6 +54,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
||||
private final List<SymbolDisplayMetadata> writingSymbols;
|
||||
private final boolean pageFree;
|
||||
private final String author;
|
||||
private final Integer instabilityScore;
|
||||
private final InteractionHand hand;
|
||||
private final BlockPos bookStandPos;
|
||||
private DestinationPreviewRenderer preview;
|
||||
@@ -74,6 +75,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
||||
this.writingSymbols = List.copyOf(writingSymbols);
|
||||
pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL);
|
||||
author = book.get(ModDataComponents.BOOK_AUTHOR.get());
|
||||
instabilityScore = book.get(ModDataComponents.AGE_INSTABILITY_SCORE.get());
|
||||
this.hand = hand;
|
||||
bookStandPos = null;
|
||||
}
|
||||
@@ -88,6 +90,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
||||
this.writingSymbols = List.copyOf(writingSymbols);
|
||||
pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL);
|
||||
author = book.get(ModDataComponents.BOOK_AUTHOR.get());
|
||||
instabilityScore = book.get(ModDataComponents.AGE_INSTABILITY_SCORE.get());
|
||||
hand = InteractionHand.MAIN_HAND;
|
||||
this.bookStandPos = bookStandPos.immutable();
|
||||
}
|
||||
@@ -250,7 +253,12 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
||||
int detailsY = drawWrappedWithoutShadow(graphics,
|
||||
author == null || author.isBlank()
|
||||
? Component.translatable("screen.rivencraft.age.unknown_author")
|
||||
: Component.literal(author), x + 10, nameEnd + 2, 100) + 10;
|
||||
: Component.literal(author), x + 10, nameEnd + 2, 100) + 4;
|
||||
if (instabilityScore != null) {
|
||||
detailsY = drawWrappedWithoutShadow(graphics,
|
||||
Component.translatable("item.rivencraft.descriptive_book.instability", instabilityScore),
|
||||
x, detailsY, 110, instabilityScore > 0 ? UNRESOLVED_TEXT_COLOR : BOOK_TEXT_COLOR) + 6;
|
||||
}
|
||||
if (!pageFree) return;
|
||||
if (generationProfile != null) {
|
||||
Component profileName = generationProfile.getNamespace().equals("rivencraft")
|
||||
|
||||
@@ -9,12 +9,17 @@ import net.dnorberg.rivencraft.book.AgeHazardProviders;
|
||||
import net.dnorberg.rivencraft.book.AgeEnvironmentalProviders;
|
||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||
import net.dnorberg.rivencraft.book.AgeInstabilityControls;
|
||||
import net.dnorberg.rivencraft.book.AgeInstabilityRuntime;
|
||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Administrative commands for inspecting and safely unloading persistent Ages. */
|
||||
public final class RivencraftCommands {
|
||||
@@ -30,21 +35,23 @@ public final class RivencraftCommands {
|
||||
.then(Commands.literal("instability")
|
||||
.executes(context -> inspectInstability(context.getSource(),
|
||||
context.getSource().getLevel().dimension().location().toString()))
|
||||
.then(Commands.literal("enable")
|
||||
.then(Commands.argument("dimension", StringArgumentType.string())
|
||||
.suggests((context, builder) -> suggestAges(context.getSource(), builder))
|
||||
.executes(context -> setInstability(context.getSource(),
|
||||
StringArgumentType.getString(context, "dimension"), true))))
|
||||
.then(Commands.literal("disable")
|
||||
.then(Commands.argument("dimension", StringArgumentType.string())
|
||||
.suggests((context, builder) -> suggestAges(context.getSource(), builder))
|
||||
.executes(context -> setInstability(context.getSource(),
|
||||
StringArgumentType.getString(context, "dimension"), false))))
|
||||
.then(Commands.argument("dimension", StringArgumentType.string())
|
||||
.suggests((context, builder) -> {
|
||||
AgeRegistrySavedData.get(context.getSource().getLevel()).definitions()
|
||||
.forEach(age -> builder.suggest(age.dimensionId().toString()));
|
||||
return builder.buildFuture();
|
||||
})
|
||||
.suggests((context, builder) -> suggestAges(context.getSource(), builder))
|
||||
.executes(context -> inspectInstability(context.getSource(),
|
||||
StringArgumentType.getString(context, "dimension")))))
|
||||
.then(Commands.literal("unload")
|
||||
.then(Commands.argument("dimension", StringArgumentType.string())
|
||||
.suggests((context, builder) -> {
|
||||
AgeRegistrySavedData.get(context.getSource().getLevel()).definitions()
|
||||
.forEach(age -> builder.suggest(age.dimensionId().toString()));
|
||||
return builder.buildFuture();
|
||||
})
|
||||
.suggests((context, builder) -> suggestAges(context.getSource(), builder))
|
||||
.executes(context -> unloadAge(
|
||||
context.getSource(),
|
||||
StringArgumentType.getString(context, "dimension")
|
||||
@@ -100,16 +107,8 @@ public final class RivencraftCommands {
|
||||
}
|
||||
|
||||
private static int unloadAge(net.minecraft.commands.CommandSourceStack source, String requestedDimension) {
|
||||
ResourceLocation dimension = ResourceLocation.tryParse(requestedDimension);
|
||||
if (dimension == null) {
|
||||
source.sendFailure(Component.literal("Invalid dimension id: " + requestedDimension));
|
||||
return 0;
|
||||
}
|
||||
AgeDefinition age = AgeRegistrySavedData.get(source.getLevel()).findByDimension(dimension).orElse(null);
|
||||
if (age == null) {
|
||||
source.sendFailure(Component.literal("Unknown Rivencraft Age: " + dimension));
|
||||
return 0;
|
||||
}
|
||||
AgeDefinition age = resolveAge(source, requestedDimension);
|
||||
if (age == null) return 0;
|
||||
|
||||
AgeDimensionService.UnloadResult result = AgeDimensionService.unload(source.getServer(), age);
|
||||
return switch (result) {
|
||||
@@ -131,27 +130,96 @@ public final class RivencraftCommands {
|
||||
}
|
||||
|
||||
private static int inspectInstability(CommandSourceStack source, String requestedDimension) {
|
||||
ResourceLocation dimension = ResourceLocation.tryParse(requestedDimension);
|
||||
if (dimension == null) {
|
||||
source.sendFailure(Component.literal("Invalid dimension id: " + requestedDimension));
|
||||
return 0;
|
||||
}
|
||||
AgeDefinition age = AgeRegistrySavedData.get(source.getLevel()).findByDimension(dimension).orElse(null);
|
||||
if (age == null) {
|
||||
source.sendFailure(Component.literal("Unknown Rivencraft Age: " + dimension));
|
||||
return 0;
|
||||
}
|
||||
AgeDefinition age = resolveAge(source, requestedDimension);
|
||||
if (age == null) return 0;
|
||||
var totals = age.instability().totals();
|
||||
source.sendSuccess(() -> Component.literal(age.name() + " — " + age.dimensionId()), false);
|
||||
source.sendSuccess(() -> Component.literal("Instability: base " + totals.baseScore()
|
||||
+ ", stability bonus " + totals.stabilityBonus()
|
||||
+ ", penalty " + totals.instabilityPenalty()
|
||||
+ ", effective " + totals.effectiveScore()), false);
|
||||
boolean enabled = AgeInstabilityControls.get(source.getLevel()).enabled(age.id());
|
||||
source.sendSuccess(() -> Component.literal("Runtime: " + (enabled ? "Age enabled" : "Age disabled")
|
||||
+ ", global " + (RivencraftConfig.instabilityEnabled() ? "enabled" : "disabled")
|
||||
+ "; saved score and plan are unchanged by controls"), false);
|
||||
source.sendSuccess(() -> Component.literal("Score breakdown (saved schema v"
|
||||
+ age.instability().version() + "):"), false);
|
||||
age.instability().breakdown().forEach(part -> source.sendSuccess(() -> Component.literal(
|
||||
" " + (part.pageIndex() < 0 ? "Age" : "page " + (part.pageIndex() + 1))
|
||||
+ " +" + part.points() + " " + part.reason() + ": " + part.detail()), false));
|
||||
source.sendSuccess(() -> Component.literal("Selected providers (saved plan v"
|
||||
+ age.instabilityPlan().version() + "): " + age.instabilityPlan().providers().size()), false);
|
||||
age.instabilityPlan().providers().forEach(provider -> source.sendSuccess(() -> Component.literal(
|
||||
" " + provider.id() + " level " + provider.level() + " — "
|
||||
+ AgeInstabilityRuntime.providerStatus(source.getLevel(), age, provider.id())), false));
|
||||
if (age.instabilityPlan().providers().isEmpty()) source.sendSuccess(() -> Component.literal(
|
||||
" No providers selected; an old neutral Age stays neutral."), false);
|
||||
source.sendSuccess(() -> Component.literal("Limits: cooldown " + RivencraftConfig.instabilityCooldown()
|
||||
+ " ticks, " + RivencraftConfig.instabilityMaxAges() + " Ages/tick, "
|
||||
+ RivencraftConfig.instabilityMaxProviders() + " providers/Age, "
|
||||
+ RivencraftConfig.instabilityMaxActions() + " actions/Age, level cap "
|
||||
+ RivencraftConfig.instabilityMaxLevel()), false);
|
||||
source.sendSuccess(() -> Component.literal("Mutable player bonuses: disabled (no active Legacy registration)"),
|
||||
false);
|
||||
return totals.effectiveScore();
|
||||
}
|
||||
|
||||
private static java.util.concurrent.CompletableFuture<com.mojang.brigadier.suggestion.Suggestions> suggestAges(
|
||||
CommandSourceStack source, com.mojang.brigadier.suggestion.SuggestionsBuilder builder) {
|
||||
AgeRegistrySavedData.get(source.getLevel()).definitions()
|
||||
.forEach(age -> {
|
||||
builder.suggest(age.dimensionId().toString());
|
||||
if (!age.name().contains(" ")) builder.suggest(age.name());
|
||||
});
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
/** Bare input is an Age name or short path, never a minecraft-namespace resource ID. */
|
||||
static List<AgeDefinition> matchingAges(List<AgeDefinition> ages, String requested) {
|
||||
String input = requested.strip();
|
||||
if (input.isEmpty()) return List.of();
|
||||
if (input.contains(":")) {
|
||||
ResourceLocation id = ResourceLocation.tryParse(input);
|
||||
return id == null ? List.of() : ages.stream()
|
||||
.filter(age -> age.dimensionId().equals(id)).toList();
|
||||
}
|
||||
List<AgeDefinition> names = ages.stream()
|
||||
.filter(age -> age.name().equalsIgnoreCase(input)).toList();
|
||||
if (!names.isEmpty()) return names;
|
||||
String path = input.toLowerCase(Locale.ROOT);
|
||||
if (!path.startsWith("age/")) path = "age/" + path;
|
||||
String expectedPath = path;
|
||||
return ages.stream().filter(age -> age.dimensionId().getPath().equals(expectedPath)).toList();
|
||||
}
|
||||
|
||||
private static AgeDefinition resolveAge(CommandSourceStack source, String requested) {
|
||||
List<AgeDefinition> matches = matchingAges(
|
||||
AgeRegistrySavedData.get(source.getLevel()).definitions(), requested);
|
||||
if (matches.size() == 1) return matches.getFirst();
|
||||
if (matches.size() > 1) {
|
||||
source.sendFailure(Component.literal("Ambiguous Age name '" + requested
|
||||
+ "'. Use its full dimension ID from /rivencraft age list."));
|
||||
} else {
|
||||
source.sendFailure(Component.literal("Unknown Rivencraft Age '" + requested
|
||||
+ "'. Use its name or dimension ID from /rivencraft age list."));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int setInstability(CommandSourceStack source, String requestedDimension, boolean enabled) {
|
||||
if (!source.hasPermission(2)) {
|
||||
source.sendFailure(Component.literal("Operator permission is required to change Age instability."));
|
||||
return 0;
|
||||
}
|
||||
AgeDefinition age = resolveAge(source, requestedDimension);
|
||||
if (age == null) return 0;
|
||||
boolean changed = AgeInstabilityControls.get(source.getLevel()).setEnabled(age.id(), enabled);
|
||||
source.sendSuccess(() -> Component.literal(age.name() + " instability "
|
||||
+ (enabled ? "enabled" : "disabled") + (changed ? "" : " (already set)")
|
||||
+ ". The saved score and provider plan are unchanged."), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int pulseDecay(net.minecraft.commands.CommandSourceStack source, String colorName, int attempts) {
|
||||
DecayBlock.Color color;
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package net.dnorberg.rivencraft.command;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeTraits;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Command-facing Age lookup regression checks. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class RivencraftCommandsGameTests {
|
||||
private RivencraftCommandsGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void bareAgeNameDoesNotBecomeMinecraftResourceId(GameTestHelper helper) {
|
||||
AgeDefinition fort = age("Fort", "fort");
|
||||
AgeDefinition tower = age("Tower", "tower");
|
||||
List<AgeDefinition> ages = List.of(fort, tower);
|
||||
for (String query : List.of("fort", "Fort", "age/fort", "rivencraft:age/fort")) {
|
||||
helper.assertValueEqual(RivencraftCommands.matchingAges(ages, query), List.of(fort),
|
||||
"Age lookup must accept its name, short path, and full dimension ID");
|
||||
}
|
||||
helper.assertTrue(RivencraftCommands.matchingAges(ages, "minecraft:fort").isEmpty(),
|
||||
"An explicit wrong namespace must not resolve to a Rivencraft Age");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void duplicateNamesRequireFullDimensionId(GameTestHelper helper) {
|
||||
AgeDefinition first = age("Fort", "fort");
|
||||
AgeDefinition second = age("Fort", "fort_2");
|
||||
List<AgeDefinition> ages = List.of(first, second);
|
||||
helper.assertValueEqual(RivencraftCommands.matchingAges(ages, "fort").size(), 2,
|
||||
"Duplicate display names must remain ambiguous, not silently pick an Age");
|
||||
helper.assertValueEqual(RivencraftCommands.matchingAges(ages, "rivencraft:age/fort_2"),
|
||||
List.of(second), "A full dimension ID must select the intended duplicate");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static AgeDefinition age(String name, String path) {
|
||||
UUID id = UUID.randomUUID();
|
||||
return new AgeDefinition(id, name, 1L, AgeGenerationProfiles.STANDARD,
|
||||
AgeDefinition.CURRENT_GENERATION_VERSION, 0L,
|
||||
net.minecraft.resources.ResourceLocation.parse("rivencraft:age/" + path), null,
|
||||
AgeTraits.LEGACY_DEFAULTS);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package net.dnorberg.rivencraft.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||
|
||||
/** Server-configurable gameplay policies. */
|
||||
@@ -56,6 +59,7 @@ public final class RivencraftConfig {
|
||||
private static final ModConfigSpec.BooleanValue SCORCH_PLAYERS;
|
||||
private static final ModConfigSpec.IntValue SCORCH_MAX_TARGETS;
|
||||
private static final ModConfigSpec.BooleanValue EROSION_ENABLED;
|
||||
private static final Map<String, ModConfigSpec.BooleanValue> INSTABILITY_PROVIDERS = new HashMap<>();
|
||||
|
||||
static {
|
||||
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
|
||||
@@ -170,6 +174,19 @@ public final class RivencraftConfig {
|
||||
"Ages with a destructive instability score may spread adjacent fluid into one safe block per pass.")
|
||||
.define("enabled", true);
|
||||
builder.pop();
|
||||
builder.push("instability_providers");
|
||||
for (String provider : new String[] {
|
||||
"blindness", "blindness_global", "enemyregen_global", "enemyresist_global",
|
||||
"fatigue", "fatigue_global", "hunger", "hunger_global", "nausea", "nausea_global",
|
||||
"poison", "poison_global", "slow", "slow_global", "weakness", "weakness_global",
|
||||
"wither", "wither_global", "burning", "crumble", "decayblue", "decaypurple",
|
||||
"decayred", "decaywhite", "explosions", "lightning", "meteors", "erosion"
|
||||
}) {
|
||||
INSTABILITY_PROVIDERS.put(provider, builder.comment(
|
||||
"Allow this saved provider to run. Disabling it does not change an Age's frozen plan.")
|
||||
.define(provider, true));
|
||||
}
|
||||
builder.pop();
|
||||
SPEC = builder.build();
|
||||
}
|
||||
|
||||
@@ -193,6 +210,11 @@ public final class RivencraftConfig {
|
||||
}
|
||||
|
||||
public static boolean instabilityEnabled() { return INSTABILITY_ENABLED.get(); }
|
||||
public static boolean instabilityProviderEnabled(ResourceLocation id) {
|
||||
ModConfigSpec.BooleanValue value = id.getNamespace().equals("rivencraft")
|
||||
? INSTABILITY_PROVIDERS.get(id.getPath()) : null;
|
||||
return value != null && value.get();
|
||||
}
|
||||
public static boolean instabilityWorldMutation() { return INSTABILITY_WORLD_MUTATION.get(); }
|
||||
public static int instabilityCooldown() { return INSTABILITY_COOLDOWN.get(); }
|
||||
public static int instabilityMaxAges() { return INSTABILITY_MAX_AGES.get(); }
|
||||
|
||||
@@ -112,6 +112,11 @@ public final class DescriptiveBookItem extends Item {
|
||||
? Component.translatable("item.rivencraft.descriptive_book.unwritten")
|
||||
: Component.translatable("item.rivencraft.descriptive_book.age", age.name());
|
||||
tooltipComponents.add(status.withStyle(ChatFormatting.GRAY));
|
||||
Integer instability = stack.get(ModDataComponents.AGE_INSTABILITY_SCORE.get());
|
||||
if (age != null && instability != null) {
|
||||
tooltipComponents.add(Component.translatable("item.rivencraft.descriptive_book.instability", instability)
|
||||
.withStyle(instability > 0 ? ChatFormatting.DARK_RED : ChatFormatting.DARK_GREEN));
|
||||
}
|
||||
stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
|
||||
.forDescriptiveBook().appendTooltip(tooltipComponents);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,11 @@ public final class ModNetworking {
|
||||
access.stack().set(ModDataComponents.AGE_GENERATION_PROFILE.get(), definition.generationProfile());
|
||||
access.sync().run();
|
||||
}
|
||||
if (definition != null && !java.util.Objects.equals(
|
||||
access.stack().get(ModDataComponents.AGE_INSTABILITY_SCORE.get()), definition.instability().score())) {
|
||||
access.stack().set(ModDataComponents.AGE_INSTABILITY_SCORE.get(), definition.instability().score());
|
||||
access.sync().run();
|
||||
}
|
||||
// Requests run on the same server thread as Age creation. A request queued
|
||||
// during generation can only open the finished book, never a stale client copy.
|
||||
player.inventoryMenu.broadcastChanges();
|
||||
@@ -170,8 +175,10 @@ public final class ModNetworking {
|
||||
|
||||
access.stack().set(ModDataComponents.AGE_REFERENCE.get(), reference);
|
||||
access.stack().set(ModDataComponents.BOOK_AUTHOR.get(), player.getGameProfile().getName());
|
||||
AgeRegistrySavedData.get(player.serverLevel()).find(reference.id()).ifPresent(definition ->
|
||||
access.stack().set(ModDataComponents.AGE_GENERATION_PROFILE.get(), definition.generationProfile()));
|
||||
AgeRegistrySavedData.get(player.serverLevel()).find(reference.id()).ifPresent(definition -> {
|
||||
access.stack().set(ModDataComponents.AGE_GENERATION_PROFILE.get(), definition.generationProfile());
|
||||
access.stack().set(ModDataComponents.AGE_INSTABILITY_SCORE.get(), definition.instability().score());
|
||||
});
|
||||
access.stack().set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
access.sync().run();
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
|
||||
@@ -81,6 +81,13 @@ public final class ModDataComponents {
|
||||
.networkSynchronized(ByteBufCodecs.STRING_UTF8)
|
||||
.cacheEncoding()
|
||||
.build());
|
||||
/** Server-written display summary only; detailed scoring and provider plans remain server-owned. */
|
||||
public static final DeferredHolder<DataComponentType<?>, DataComponentType<Integer>> AGE_INSTABILITY_SCORE =
|
||||
DATA_COMPONENTS.register("age_instability_score", () -> DataComponentType.<Integer>builder()
|
||||
.persistent(Codec.intRange(0, Integer.MAX_VALUE))
|
||||
.networkSynchronized(ByteBufCodecs.VAR_INT)
|
||||
.cacheEncoding()
|
||||
.build());
|
||||
public static final DeferredHolder<DataComponentType<?>, DataComponentType<Integer>> INK_REMAINING =
|
||||
DATA_COMPONENTS.register("ink_remaining", () -> DataComponentType.<Integer>builder()
|
||||
.persistent(Codec.intRange(0, 16))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"item.rivencraft.descriptive_book": "Descriptive Book",
|
||||
"item.rivencraft.descriptive_book.unwritten": "Unwritten",
|
||||
"item.rivencraft.descriptive_book.age": "%s",
|
||||
"item.rivencraft.descriptive_book.instability": "Instability: %s (potential)",
|
||||
"item.rivencraft.linking_book": "Linking Book",
|
||||
"item.rivencraft.linking_book.unbound": "Unbound — sneak-use to bind",
|
||||
"item.rivencraft.linking_book.destination": "%s (%s, %s, %s)",
|
||||
|
||||
Reference in new issue
Block a user