Public Access
This commit is contained in:
1 parent
2f1e6bd6f2
commit
2b2605b277
21 files changed
+638
-4
No files matched your search
@@ -233,6 +233,9 @@ Data-pack runtime profiles are documented in
|
|||||||
- NeoForge 21.1.264 or newer in the 21.1 line
|
- NeoForge 21.1.264 or newer in the 21.1 line
|
||||||
- Dynamic Dimensions AR Edition 0.9.1 and Bad Packets 0.8.2 (client and server)
|
- Dynamic Dimensions AR Edition 0.9.1 and Bad Packets 0.8.2 (client and server)
|
||||||
|
|
||||||
|
Ruins is optional. When installed, its configured structures have individual
|
||||||
|
authorable pages; see [Ruins compatibility](docs/RUINS_COMPATIBILITY.md).
|
||||||
|
|
||||||
Create is optional. When installed, Rivencraft uses its Ponder renderer for
|
Create is optional. When installed, Rivencraft uses its Ponder renderer for
|
||||||
book previews when Immersive Portals is absent. Immersive Portals is also
|
book previews when Immersive Portals is absent. Immersive Portals is also
|
||||||
optional and takes priority in the default `AUTO` mode, providing a live
|
optional and takes priority in the default `AUTO` mode, providing a live
|
||||||
|
|||||||
@@ -245,6 +245,11 @@ dependencies {
|
|||||||
// Keep them on the compile classpath without forcing either mod at runtime.
|
// Keep them on the compile classpath without forcing either mod at runtime.
|
||||||
compileOnly "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
|
compileOnly "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
|
||||||
compileOnly "maven.modrinth:immersive-portals-neoforge:${immersive_portals_version}"
|
compileOnly "maven.modrinth:immersive-portals-neoforge:${immersive_portals_version}"
|
||||||
|
// Ruins is optional; its pre-spawn event is only registered when Ruins is loaded.
|
||||||
|
compileOnly 'curse.maven:ruins-227873:6391825'
|
||||||
|
if (providers.gradleProperty('withRuins').map { it.toBoolean() }.orElse(false).get()) {
|
||||||
|
localRuntime 'curse.maven:ruins-227873:6391825'
|
||||||
|
}
|
||||||
// TerraBlender's region API is only used by the guarded compat adapter.
|
// TerraBlender's region API is only used by the guarded compat adapter.
|
||||||
compileOnly "com.github.glitchfiend:TerraBlender-neoforge:${minecraft_version}-${terrablender_version}"
|
compileOnly "com.github.glitchfiend:TerraBlender-neoforge:${minecraft_version}-${terrablender_version}"
|
||||||
if (providers.gradleProperty('withTerraBlender').map { it.toBoolean() }.orElse(false).get()) {
|
if (providers.gradleProperty('withTerraBlender').map { it.toBoolean() }.orElse(false).get()) {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Optional Ruins template pages (issue #91)
|
||||||
|
|
||||||
|
Rivencraft supports Ruins 1.21.1.2NF as an optional mod. When it is installed,
|
||||||
|
every `.tml` template in Ruins' active `config/ruins_config/<generic-or-biome>/`
|
||||||
|
folders receives a separate Ruins page in Rivencraft's creative and JEI page
|
||||||
|
feeds. New templates added to Ruins' config appear after a restart; removing a
|
||||||
|
template makes its page unavailable to new books. With Ruins absent, no Ruins
|
||||||
|
pages or structure integration are active.
|
||||||
|
|
||||||
|
An authored Age with Ruins pages is admitted through Ruins' dimension gate.
|
||||||
|
Rivencraft also admits the authored templates through their individual
|
||||||
|
`dimensionsToSpawnIn` gates, which usually list only `overworld` and would
|
||||||
|
otherwise prevent them from loading in an Age. Ruins still owns chunk
|
||||||
|
inspection, spawn chance, surface validation, biome pools, and spacing.
|
||||||
|
Rivencraft filters Ruins' weighted
|
||||||
|
template choice to the exact pages in that Age and vetoes an unauthorized
|
||||||
|
template in Ruins' pre-spawn event. A page therefore selects *which* template
|
||||||
|
may appear; it does not guarantee a structure in every chunk. Manual
|
||||||
|
`/testruin` placements are unaffected.
|
||||||
|
|
||||||
|
Repeating a template page multiplies that template's Ruins selection weight by
|
||||||
|
its page count and adds one spawn attempt per chunk for each duplicate page.
|
||||||
|
The extra attempts are capped at 32 per chunk (or Ruins' configured count if
|
||||||
|
higher). Distinct template pages do not add attempts, and Ruins' spacing and
|
||||||
|
surface checks still apply. As with Rivencraft's standard
|
||||||
|
structure pages, the first three copies of each template are instability-free;
|
||||||
|
each additional copy adds 100 instability. Ruins retains its own biome and
|
||||||
|
surface checks, so the standard structure forced-biome penalty does not apply.
|
||||||
|
|
||||||
|
Ruins' event identifies a template only by its filename. If two active folders
|
||||||
|
contain the same `.tml` basename, Rivencraft cannot distinguish them and
|
||||||
|
disables all Ruins pages for that configuration. Give each template a unique
|
||||||
|
filename to restore support. Ruins must also be able to parse the template and
|
||||||
|
find a suitable surface in the selected biome; a page cannot repair an invalid
|
||||||
|
or biome-incompatible template.
|
||||||
|
|
||||||
|
The adapter and dimension/selection mixin are version-sensitive to Ruins
|
||||||
|
1.21.1.2NF. The published Rivencraft jar does not include Ruins. To include it
|
||||||
|
in a development run, use `-PwithRuins=true`.
|
||||||
|
|
||||||
|
## Manual check
|
||||||
|
|
||||||
|
1. Start a client with Ruins 1.21.1.2NF and Rivencraft 0.6.9.3. Confirm the
|
||||||
|
Ruins Pages tab contains one page per configured `.tml` file, and search a
|
||||||
|
distinctive template name in JEI.
|
||||||
|
2. Bind a new Descriptive Book with a Link Panel, Standard Terrain, a suitable
|
||||||
|
surface biome, and one Ruins page (for example ZombieHut). Enter the Age and
|
||||||
|
explore *unvisited* chunks. Ruins marks inspected chunks and will not retry
|
||||||
|
chunks explored under the earlier broken build. Only the selected Ruins
|
||||||
|
template should appear. Ruins'
|
||||||
|
own chance and distance settings still determine how far exploration takes.
|
||||||
|
3. For duplicate weighting, bind an Age with one copy of one template page and
|
||||||
|
three copies of a second template page valid in the same biome. Across enough
|
||||||
|
new candidate chunks, the second should be selected more often relative to
|
||||||
|
the first, and there should be two additional spawn attempts per inspected
|
||||||
|
chunk. Four copies of one template should add 100 instability, visible
|
||||||
|
through the OP Age instability diagnostic, while three copies add none.
|
||||||
|
4. Make another Age without a Ruins page and confirm Ruins structures do not
|
||||||
|
appear there. Restart the game and confirm the authored Age keeps its page
|
||||||
|
selection. Then remove Ruins and confirm Rivencraft starts normally without
|
||||||
|
a Ruins tab.
|
||||||
|
|
||||||
|
The Ruins-enabled GameTest server passed all 90 required tests and loaded the
|
||||||
|
template-dimension and attempt mixins. Its test world was removed afterward.
|
||||||
|
Use the client check above to verify actual Ruins generation and the increased
|
||||||
|
attempt rate.
|
||||||
+1
-1
@@ -36,7 +36,7 @@ mod_name=Rivencraft
|
|||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=LGPL-3.0-or-later
|
mod_license=LGPL-3.0-or-later
|
||||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||||
mod_version=0.6.8.11
|
mod_version=0.6.9.3
|
||||||
immersive_portals_version=6.0.7
|
immersive_portals_version=6.0.7
|
||||||
terrablender_version=4.1.0.8
|
terrablender_version=4.1.0.8
|
||||||
serene_seasons_version_id=pHEgQQUE
|
serene_seasons_version_id=pHEgQQUE
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import net.dnorberg.rivencraft.network.ModNetworking;
|
|||||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||||
import net.dnorberg.rivencraft.config.RivencraftClientConfig;
|
import net.dnorberg.rivencraft.config.RivencraftClientConfig;
|
||||||
import net.dnorberg.rivencraft.compat.SereneSeasonsCompatibility;
|
import net.dnorberg.rivencraft.compat.SereneSeasonsCompatibility;
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsCompatibility;
|
||||||
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||||
import net.dnorberg.rivencraft.book.AgeWeatherService;
|
import net.dnorberg.rivencraft.book.AgeWeatherService;
|
||||||
import net.dnorberg.rivencraft.book.AgeInstabilityRuntime;
|
import net.dnorberg.rivencraft.book.AgeInstabilityRuntime;
|
||||||
@@ -43,6 +44,7 @@ public final class Rivencraft {
|
|||||||
|
|
||||||
public Rivencraft(IEventBus modEventBus, ModContainer modContainer) {
|
public Rivencraft(IEventBus modEventBus, ModContainer modContainer) {
|
||||||
AgeDimensionService.register();
|
AgeDimensionService.register();
|
||||||
|
RuinsCompatibility.register();
|
||||||
AgeStatusEffectProviders.register();
|
AgeStatusEffectProviders.register();
|
||||||
AgeDecayProviders.register();
|
AgeDecayProviders.register();
|
||||||
AgeHazardProviders.register();
|
AgeHazardProviders.register();
|
||||||
@@ -82,6 +84,8 @@ public final class Rivencraft {
|
|||||||
NeoForge.EVENT_BUS.addListener(AgeInstabilityRuntime::onServerStopping);
|
NeoForge.EVENT_BUS.addListener(AgeInstabilityRuntime::onServerStopping);
|
||||||
NeoForge.EVENT_BUS.addListener(AgeEnvironmentalProviders::onServerStopping);
|
NeoForge.EVENT_BUS.addListener(AgeEnvironmentalProviders::onServerStopping);
|
||||||
NeoForge.EVENT_BUS.addListener(AgeStatusEffectProviders::onServerStopping);
|
NeoForge.EVENT_BUS.addListener(AgeStatusEffectProviders::onServerStopping);
|
||||||
|
NeoForge.EVENT_BUS.addListener((net.neoforged.neoforge.event.server.ServerStoppingEvent event) ->
|
||||||
|
RuinsCompatibility.onServerStopping());
|
||||||
NeoForge.EVENT_BUS.addListener(RivencraftCommands::register);
|
NeoForge.EVENT_BUS.addListener(RivencraftCommands::register);
|
||||||
NeoForge.EVENT_BUS.addListener(LibraryLocateService::onServerTickPost);
|
NeoForge.EVENT_BUS.addListener(LibraryLocateService::onServerTickPost);
|
||||||
NeoForge.EVENT_BUS.addListener(LibraryLocateService::onServerStopping);
|
NeoForge.EVENT_BUS.addListener(LibraryLocateService::onServerStopping);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import net.dnorberg.rivencraft.Rivencraft;
|
|||||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||||
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
|
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
|
||||||
import net.dnorberg.rivencraft.compat.SereneSeasonsCompatibility;
|
import net.dnorberg.rivencraft.compat.SereneSeasonsCompatibility;
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsCompatibility;
|
||||||
import net.dnorberg.rivencraft.compat.TerraBlenderBiomes;
|
import net.dnorberg.rivencraft.compat.TerraBlenderBiomes;
|
||||||
import net.dnorberg.rivencraft.writing.BiomeSymbolPages;
|
import net.dnorberg.rivencraft.writing.BiomeSymbolPages;
|
||||||
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
|
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
|
||||||
@@ -84,6 +85,7 @@ public final class AgeDimensionService {
|
|||||||
if (level != null) {
|
if (level != null) {
|
||||||
applyCelestialClock(level, age);
|
applyCelestialClock(level, age);
|
||||||
SereneSeasonsCompatibility.registerAge(server, age);
|
SereneSeasonsCompatibility.registerAge(server, age);
|
||||||
|
RuinsCompatibility.registerAge(age);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
synchronizeImmersivePortals(server);
|
synchronizeImmersivePortals(server);
|
||||||
@@ -141,6 +143,7 @@ public final class AgeDimensionService {
|
|||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
applyCelestialClock(existing, definition);
|
applyCelestialClock(existing, definition);
|
||||||
SereneSeasonsCompatibility.registerAge(server, definition);
|
SereneSeasonsCompatibility.registerAge(server, definition);
|
||||||
|
RuinsCompatibility.registerAge(definition);
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +160,7 @@ public final class AgeDimensionService {
|
|||||||
} else {
|
} else {
|
||||||
applyCelestialClock(loaded, definition);
|
applyCelestialClock(loaded, definition);
|
||||||
SereneSeasonsCompatibility.registerAge(server, definition);
|
SereneSeasonsCompatibility.registerAge(server, definition);
|
||||||
|
RuinsCompatibility.registerAge(definition);
|
||||||
synchronizeImmersivePortals(server);
|
synchronizeImmersivePortals(server);
|
||||||
}
|
}
|
||||||
return loaded;
|
return loaded;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
|
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||||
@@ -235,6 +236,9 @@ public final class AgeFeatureSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String error(ResourceLocation choice, AgeRuntimeProfile profile, RegistryAccess registries) {
|
public static String error(ResourceLocation choice, AgeRuntimeProfile profile, RegistryAccess registries) {
|
||||||
|
if (RuinsTemplatePages.isSymbol(choice))
|
||||||
|
return RuinsTemplatePages.contains(choice) ? null : "Ruins template " + choice
|
||||||
|
+ " is unavailable; restore Ruins and its template before loading this Age";
|
||||||
if (AgeStructureSelection.feature(choice))
|
if (AgeStructureSelection.feature(choice))
|
||||||
return AgeStructureSelection.error(choice, profile, null, registries);
|
return AgeStructureSelection.error(choice, profile, null, registries);
|
||||||
if (choice.equals(AgeDenseOres.ID))
|
if (choice.equals(AgeDenseOres.ID))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||||
import net.minecraft.core.RegistryAccess;
|
import net.minecraft.core.RegistryAccess;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.nbt.ListTag;
|
import net.minecraft.nbt.ListTag;
|
||||||
@@ -150,9 +151,12 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
|||||||
|| !(symbol.semantics().value()
|
|| !(symbol.semantics().value()
|
||||||
instanceof net.dnorberg.rivencraft.writing.SymbolSemantics.ResourceValue resource)) continue;
|
instanceof net.dnorberg.rivencraft.writing.SymbolSemantics.ResourceValue resource)) continue;
|
||||||
String name = COUNTED_STRUCTURES.get(resource.id());
|
String name = COUNTED_STRUCTURES.get(resource.id());
|
||||||
|
if (name == null && RuinsTemplatePages.isSymbol(resource.id()))
|
||||||
|
name = RuinsTemplatePages.fallbackName(resource.id());
|
||||||
if (name == null) continue;
|
if (name == null) continue;
|
||||||
int count = structureCounts.merge(resource.id(), 1, Integer::sum);
|
int count = structureCounts.merge(resource.id(), 1, Integer::sum);
|
||||||
if (AgeStructureSelection.requiresBiomeOverride(resource.id(), plan.biomeSelection(), registries)) {
|
if (COUNTED_STRUCTURES.containsKey(resource.id())
|
||||||
|
&& AgeStructureSelection.requiresBiomeOverride(resource.id(), plan.biomeSelection(), registries)) {
|
||||||
contributions.add(new Contribution("forced_biome_" + resource.id().getPath(), symbol.index(),
|
contributions.add(new Contribution("forced_biome_" + resource.id().getPath(), symbol.index(),
|
||||||
FORCED_STRUCTURE_BIOME_COST,
|
FORCED_STRUCTURE_BIOME_COST,
|
||||||
name + " occurrence " + count + " is forced into an unsupported biome"));
|
name + " occurrence " + count + " is forced into an unsupported biome"));
|
||||||
@@ -168,7 +172,7 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
|||||||
|
|
||||||
static int featureCost(ResourceLocation feature, int occurrence) {
|
static int featureCost(ResourceLocation feature, int occurrence) {
|
||||||
if (occurrence < 1) throw new IllegalArgumentException("Feature occurrence must be positive");
|
if (occurrence < 1) throw new IllegalArgumentException("Feature occurrence must be positive");
|
||||||
return COUNTED_STRUCTURES.containsKey(feature) && occurrence > 3
|
return (COUNTED_STRUCTURES.containsKey(feature) || RuinsTemplatePages.isSymbol(feature)) && occurrence > 3
|
||||||
? EXCESS_LEGACY_STRUCTURE_COST : 0;
|
? EXCESS_LEGACY_STRUCTURE_COST : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,13 @@ public final class AgePersistenceGameTests {
|
|||||||
"Modern repeated structures must use the same deterministic scoring path");
|
"Modern repeated structures must use the same deterministic scoring path");
|
||||||
helper.assertValueEqual(AgeInstability.featureCost(ResourceLocation.parse("rivencraft:dungeons"), 4), 0,
|
helper.assertValueEqual(AgeInstability.featureCost(ResourceLocation.parse("rivencraft:dungeons"), 4), 0,
|
||||||
"A Legacy-neutral feature must remain neutral when repeated");
|
"A Legacy-neutral feature must remain neutral when repeated");
|
||||||
|
ResourceLocation ruin = net.dnorberg.rivencraft.compat.RuinsTemplatePages.symbolId("ZombieHut.tml");
|
||||||
|
helper.assertValueEqual(AgeInstability.featureCost(ruin, 1), 0,
|
||||||
|
"The first Ruins template page is free like a standard structure");
|
||||||
|
helper.assertValueEqual(AgeInstability.featureCost(ruin, 3), 0,
|
||||||
|
"The first three copies of a Ruins page are free");
|
||||||
|
helper.assertValueEqual(AgeInstability.featureCost(ruin, 4), 100,
|
||||||
|
"Each Ruins page beyond the third adds 100 instability");
|
||||||
|
|
||||||
OrderedWriting duplicateBright = new OrderedWriting(List.of(
|
OrderedWriting duplicateBright = new OrderedWriting(List.of(
|
||||||
WritingEntry.linkPanel(),
|
WritingEntry.linkPanel(),
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package net.dnorberg.rivencraft.compat;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.fml.ModList;
|
||||||
|
|
||||||
|
/** Small optional bridge between frozen Age pages and Ruins' dimension gate. */
|
||||||
|
public final class RuinsCompatibility {
|
||||||
|
private static final Set<ResourceLocation> KNOWN_AGES = ConcurrentHashMap.newKeySet();
|
||||||
|
private static final Map<ResourceLocation, Map<ResourceLocation, Integer>> AUTHORED_TEMPLATES =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private RuinsCompatibility() {}
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
if (ModList.get().isLoaded("ruins")) RuinsEventAdapter.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerAge(AgeDefinition age) {
|
||||||
|
if (!ModList.get().isLoaded("ruins")) return;
|
||||||
|
KNOWN_AGES.add(age.dimensionId());
|
||||||
|
List<AgeFeatureSelection.Choice> choices = age.plan() == null
|
||||||
|
? age.randomFeatures() : AgeFeatureSelection.resolved(age.plan());
|
||||||
|
Map<ResourceLocation, Integer> selected = templateCounts(choices);
|
||||||
|
if (!selected.isEmpty()) {
|
||||||
|
AUTHORED_TEMPLATES.put(age.dimensionId(), selected);
|
||||||
|
} else {
|
||||||
|
AUTHORED_TEMPLATES.remove(age.dimensionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called only by the optional Ruins FileHandler mixin. */
|
||||||
|
public static boolean allowsAge(ResourceLocation dimension) {
|
||||||
|
return AUTHORED_TEMPLATES.containsKey(dimension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isKnownAge(ResourceLocation dimension) {
|
||||||
|
return KNOWN_AGES.contains(dimension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean permits(ResourceLocation dimension, ResourceLocation template) {
|
||||||
|
return templateCount(dimension, template) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int templateCount(ResourceLocation dimension, ResourceLocation template) {
|
||||||
|
return AUTHORED_TEMPLATES.getOrDefault(dimension, Map.of()).getOrDefault(template, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extra pages increase attempts, without making distinct template choices denser. */
|
||||||
|
public static int attemptsPerChunk(ResourceLocation dimension, int configuredAttempts) {
|
||||||
|
return attemptsPerChunk(configuredAttempts, AUTHORED_TEMPLATES.get(dimension));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int attemptsPerChunk(int configuredAttempts, Map<ResourceLocation, Integer> counts) {
|
||||||
|
if (counts == null || counts.isEmpty()) return configuredAttempts;
|
||||||
|
int duplicates = counts.values().stream().mapToInt(count -> count - 1).sum();
|
||||||
|
return (int) Math.min(Math.max(configuredAttempts, 32L), (long) configuredAttempts + duplicates);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<ResourceLocation, Integer> templateCounts(List<AgeFeatureSelection.Choice> choices) {
|
||||||
|
Map<ResourceLocation, Integer> counts = new java.util.HashMap<>();
|
||||||
|
for (AgeFeatureSelection.Choice choice : choices) {
|
||||||
|
if (RuinsTemplatePages.contains(choice.id())) counts.merge(choice.id(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
return Map.copyOf(counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onServerStopping() {
|
||||||
|
KNOWN_AGES.clear();
|
||||||
|
AUTHORED_TEMPLATES.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package net.dnorberg.rivencraft.compat;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeReference;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||||
|
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||||
|
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import net.neoforged.fml.loading.FMLPaths;
|
||||||
|
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||||
|
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||||
|
import net.minecraft.gametest.framework.GameTest;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.neoforged.fml.ModList;
|
||||||
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
|
/** Each discoverable Ruins template must have an authorable page, or none do. */
|
||||||
|
@GameTestHolder("rivencraft")
|
||||||
|
@PrefixGameTestTemplate(false)
|
||||||
|
public final class RuinsCompatibilityGameTests {
|
||||||
|
private RuinsCompatibilityGameTests() {}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void ruinsPagesMatchInstalledTemplates(GameTestHelper helper) {
|
||||||
|
var templates = RuinsTemplatePages.templates();
|
||||||
|
if (!ModList.get().isLoaded("ruins")) {
|
||||||
|
helper.assertTrue(templates.isEmpty(), "Ruins pages must be absent without the optional mod");
|
||||||
|
} else {
|
||||||
|
var creative = PageItem.creativeSymbolPages(helper.getLevel().registryAccess(),
|
||||||
|
RuinsTemplatePages.CATEGORY);
|
||||||
|
helper.assertValueEqual(creative.size(), templates.size(),
|
||||||
|
"Creative and JEI must expose exactly one page per Ruins template");
|
||||||
|
for (String name : templates) {
|
||||||
|
var id = RuinsTemplatePages.symbolId(name);
|
||||||
|
helper.assertTrue(SymbolCatalog.isKnown(new SymbolReference(id)),
|
||||||
|
"Ruins template " + name + " must be authorable");
|
||||||
|
helper.assertTrue(PageItem.isUsableWriting(PageItem.createCreativeRuinsPage(name)),
|
||||||
|
"Ruins template " + name + " needs a usable page");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void duplicatePagesRetainTheirCount(GameTestHelper helper) {
|
||||||
|
if (RuinsTemplatePages.templates().isEmpty()) {
|
||||||
|
helper.succeed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var template = RuinsTemplatePages.symbolId(RuinsTemplatePages.templates().getFirst());
|
||||||
|
var counts = RuinsCompatibility.templateCounts(List.of(
|
||||||
|
new AgeFeatureSelection.Choice(template, Optional.empty()),
|
||||||
|
new AgeFeatureSelection.Choice(template, Optional.empty()),
|
||||||
|
new AgeFeatureSelection.Choice(template, Optional.empty())));
|
||||||
|
helper.assertValueEqual(counts.get(template), 3,
|
||||||
|
"Three authored Ruins pages must triple that template's relative selection weight");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void duplicatePagesIncreaseAttemptsWithinBudget(GameTestHelper helper) {
|
||||||
|
var first = RuinsTemplatePages.symbolId("First.tml");
|
||||||
|
var second = RuinsTemplatePages.symbolId("Second.tml");
|
||||||
|
helper.assertValueEqual(RuinsCompatibility.attemptsPerChunk(6, Map.of(first, 1)), 6,
|
||||||
|
"One Ruins page must retain the configured attempt count");
|
||||||
|
helper.assertValueEqual(RuinsCompatibility.attemptsPerChunk(6, Map.of(first, 3, second, 1)), 8,
|
||||||
|
"Only duplicate pages must add one attempt each");
|
||||||
|
helper.assertValueEqual(RuinsCompatibility.attemptsPerChunk(6, Map.of(first, 50)), 32,
|
||||||
|
"Duplicate pages must not exceed the per-chunk attempt budget");
|
||||||
|
helper.assertValueEqual(RuinsCompatibility.attemptsPerChunk(40, Map.of(first, 2)), 40,
|
||||||
|
"The budget must not lower an intentionally higher Ruins configuration");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void authoredTemplateBypassesOverworldOnlyGate(GameTestHelper helper) throws Exception {
|
||||||
|
if (!ModList.get().isLoaded("ruins")) {
|
||||||
|
helper.succeed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String name = "tikihead1.tml";
|
||||||
|
var path = FMLPaths.GAMEDIR.get().resolve("config/ruins_config/generic").resolve(name);
|
||||||
|
if (!Files.exists(path) || !RuinsTemplatePages.contains(RuinsTemplatePages.symbolId(name))) {
|
||||||
|
helper.succeed(); // Custom Ruins packs need not include the bundled Tiki Head.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var writing = new OrderedWriting(List.of(
|
||||||
|
WritingEntry.linkPanel(),
|
||||||
|
WritingEntry.symbol(new SymbolReference(net.minecraft.resources.ResourceLocation.parse(
|
||||||
|
"rivencraft:terrain/standard"))),
|
||||||
|
WritingEntry.symbol(new SymbolReference(net.minecraft.resources.ResourceLocation.parse(
|
||||||
|
"rivencraft:biome/plains"))),
|
||||||
|
WritingEntry.symbol(new SymbolReference(net.minecraft.resources.ResourceLocation.parse(
|
||||||
|
"rivencraft:biome/single"))),
|
||||||
|
WritingEntry.symbol(new SymbolReference(RuinsTemplatePages.symbolId(name)))));
|
||||||
|
var age = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||||
|
new AgeReference(UUID.randomUUID(), "Ruins Tiki Gate"), writing);
|
||||||
|
RuinsCompatibility.registerAge(age);
|
||||||
|
var ruinsClass = Class.forName("atomicstryker.ruins.common.RuinsMod");
|
||||||
|
Object ruins = ruinsClass.getMethod("getInstance").invoke(null);
|
||||||
|
var currentLevel = ruinsClass.getDeclaredField("lastLoadedLevel");
|
||||||
|
currentLevel.setAccessible(true);
|
||||||
|
Object previousLevel = currentLevel.get(ruins);
|
||||||
|
var templateClass = Class.forName("atomicstryker.ruins.common.RuinTemplate");
|
||||||
|
try {
|
||||||
|
currentLevel.set(ruins, helper.getLevel());
|
||||||
|
Object template = templateClass.getConstructor(String.class, String.class)
|
||||||
|
.newInstance(path.toString(), name);
|
||||||
|
boolean accepted = (boolean) templateClass.getMethod("acceptsDimension", String.class)
|
||||||
|
.invoke(template, age.dimensionId().getPath());
|
||||||
|
helper.assertTrue(accepted, "The authored Tiki Head must load despite dimensionsToSpawnIn=overworld");
|
||||||
|
} finally {
|
||||||
|
currentLevel.set(ruins, previousLevel);
|
||||||
|
}
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package net.dnorberg.rivencraft.compat;
|
||||||
|
|
||||||
|
import atomicstryker.ruins.common.EventRuinTemplateSpawn;
|
||||||
|
import atomicstryker.ruins.common.RuinTemplate;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
|
|
||||||
|
/** Loaded only when Ruins is present. Its pre-spawn event can veto exact templates. */
|
||||||
|
public final class RuinsEventAdapter {
|
||||||
|
private RuinsEventAdapter() {}
|
||||||
|
|
||||||
|
static void register() {
|
||||||
|
NeoForge.EVENT_BUS.addListener(RuinsEventAdapter::onTemplateSpawn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void onTemplateSpawn(EventRuinTemplateSpawn event) {
|
||||||
|
if (!event.isPrePhase || event.testingRuin
|
||||||
|
|| !(event.getLevel() instanceof ServerLevel level)
|
||||||
|
|| !RuinsCompatibility.isKnownAge(level.dimension().location())) return;
|
||||||
|
|
||||||
|
var template = RuinsTemplatePages.featureId(event.template.getName());
|
||||||
|
if (!RuinsTemplatePages.contains(template)
|
||||||
|
|| !RuinsCompatibility.permits(level.dimension().location(), template)) event.setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preserve Ruins' weights and multiply each by its authored page count. */
|
||||||
|
public static Object chooseTemplate(net.minecraft.resources.ResourceLocation dimension,
|
||||||
|
Collection<?> pool, RandomSource random) {
|
||||||
|
if (pool == null || pool.isEmpty()) return null;
|
||||||
|
var candidates = pool.stream().filter(RuinTemplate.class::isInstance)
|
||||||
|
.map(RuinTemplate.class::cast)
|
||||||
|
.filter(template -> RuinsCompatibility.permits(dimension,
|
||||||
|
RuinsTemplatePages.featureId(template.getName())))
|
||||||
|
.sorted(Comparator.comparing(RuinTemplate::getName)).toList();
|
||||||
|
double total = candidates.stream().mapToDouble(template -> authoredWeight(dimension, template)).sum();
|
||||||
|
if (total <= 0.0) return null;
|
||||||
|
double roll = random.nextDouble() * total;
|
||||||
|
for (RuinTemplate candidate : candidates) {
|
||||||
|
roll -= authoredWeight(dimension, candidate);
|
||||||
|
if (roll < 0.0) return candidate;
|
||||||
|
}
|
||||||
|
return candidates.getLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double authoredWeight(net.minecraft.resources.ResourceLocation dimension,
|
||||||
|
RuinTemplate template) {
|
||||||
|
return Math.max(0.0, template.getWeight()) * RuinsCompatibility.templateCount(dimension,
|
||||||
|
RuinsTemplatePages.featureId(template.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package net.dnorberg.rivencraft.compat;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.writing.SymbolDefinition;
|
||||||
|
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.fml.ModList;
|
||||||
|
import net.neoforged.fml.loading.FMLPaths;
|
||||||
|
|
||||||
|
/** Discovers Ruins' template filenames without making its jar mandatory. */
|
||||||
|
public final class RuinsTemplatePages {
|
||||||
|
public static final ResourceLocation CATEGORY = ResourceLocation.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "ruins");
|
||||||
|
private static final String PREFIX = "ruins/";
|
||||||
|
private static volatile Map<ResourceLocation, String> names = Map.of();
|
||||||
|
private static volatile boolean initialized;
|
||||||
|
|
||||||
|
private RuinsTemplatePages() {}
|
||||||
|
|
||||||
|
public static void refresh() {
|
||||||
|
if (!ModList.get().isLoaded("ruins")) {
|
||||||
|
names = Map.of();
|
||||||
|
initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<ResourceLocation, String> discovered = new HashMap<>();
|
||||||
|
Map<String, String> sources = new HashMap<>();
|
||||||
|
Set<String> duplicateNames = new HashSet<>();
|
||||||
|
// Ruins itself reads <gameDir>/config/ruins_config, not NeoForge's
|
||||||
|
// overrideable CONFIGDIR; mirror its active folder exactly.
|
||||||
|
Path configured = FMLPaths.GAMEDIR.get().resolve("config/ruins_config");
|
||||||
|
if (Files.isDirectory(configured)) {
|
||||||
|
try (var paths = Files.walk(configured)) {
|
||||||
|
paths.filter(Files::isRegularFile)
|
||||||
|
.filter(path -> configured.relativize(path).getNameCount() == 2)
|
||||||
|
.filter(path -> isTemplate(path.getFileName().toString()))
|
||||||
|
.sorted().forEach(path -> add(discovered, sources, duplicateNames,
|
||||||
|
path.getFileName().toString(), configured.relativize(path).toString()));
|
||||||
|
} catch (IOException exception) {
|
||||||
|
Rivencraft.LOGGER.warn("Could not read Ruins templates from {}", configured, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ruins extracts this archive on first launch. It also supplies the creative
|
||||||
|
// pages before extraction has completed on a fresh installation.
|
||||||
|
if (discovered.isEmpty() && !Files.isDirectory(configured)) {
|
||||||
|
try (InputStream stream = RuinsTemplatePages.class.getClassLoader()
|
||||||
|
.getResourceAsStream("assets/ruins_config.zip")) {
|
||||||
|
if (stream != null) {
|
||||||
|
try (ZipInputStream zip = new ZipInputStream(stream)) {
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = zip.getNextEntry()) != null) {
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
String name = entry.getName();
|
||||||
|
name = name.substring(name.lastIndexOf('/') + 1);
|
||||||
|
if (isTemplate(name)) add(discovered, sources, duplicateNames,
|
||||||
|
name, entry.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException exception) {
|
||||||
|
Rivencraft.LOGGER.warn("Could not read bundled Ruins templates", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ruins' spawn event exposes only the basename. If two directories use
|
||||||
|
// that basename, Rivencraft cannot identify which structure was chosen.
|
||||||
|
// Disable the bridge instead of advertising pages with ambiguous effects.
|
||||||
|
if (!duplicateNames.isEmpty()) {
|
||||||
|
Rivencraft.LOGGER.error("Ruins compatibility disabled: duplicate template names {}. "
|
||||||
|
+ "Give every .tml file a unique basename.", duplicateNames);
|
||||||
|
names = Map.of();
|
||||||
|
initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
names = Map.copyOf(discovered);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> templates() {
|
||||||
|
if (!initialized) refresh();
|
||||||
|
List<String> templates = new ArrayList<>(names.values());
|
||||||
|
templates.sort(Comparator.comparing(String::toLowerCase));
|
||||||
|
return List.copyOf(templates);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSymbol(ResourceLocation id) {
|
||||||
|
return id.getNamespace().equals(Rivencraft.MOD_ID) && id.getPath().startsWith(PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean contains(ResourceLocation id) {
|
||||||
|
return isSymbol(id) && names.containsKey(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResourceLocation symbolId(String templateName) {
|
||||||
|
if (templateName.isBlank()) throw new IllegalArgumentException("Ruins template has no usable name");
|
||||||
|
// Hex is reversible and collision-free even for case-sensitive or Unicode
|
||||||
|
// filenames; normalizing to lowercase would merge distinct templates.
|
||||||
|
String key = HexFormat.of().formatHex(templateName.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, PREFIX + "t_" + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResourceLocation featureId(String templateName) {
|
||||||
|
return symbolId(templateName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String fallbackName(ResourceLocation symbolId) {
|
||||||
|
String encoded = symbolId.getPath().substring(PREFIX.length());
|
||||||
|
if (!encoded.startsWith("t_")) return encoded + " Ruin";
|
||||||
|
String name;
|
||||||
|
try {
|
||||||
|
name = new String(HexFormat.of().parseHex(encoded.substring(2)), StandardCharsets.UTF_8);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
name = encoded;
|
||||||
|
}
|
||||||
|
return stripExtension(name).replace('_', ' ') + " Ruin";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SymbolDefinition definition(String templateName) {
|
||||||
|
ResourceLocation id = featureId(templateName);
|
||||||
|
return new SymbolDefinition(SymbolDefinition.CURRENT_SCHEMA_VERSION,
|
||||||
|
"symbol.rivencraft.ruins.template", "symbol.rivencraft.ruins.description",
|
||||||
|
CATEGORY, Optional.of(new SymbolSemantics(SymbolSemantics.Role.FEATURE,
|
||||||
|
new SymbolSemantics.ResourceValue(id))), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isTemplate(String name) {
|
||||||
|
return name.toLowerCase(Locale.ROOT).endsWith(".tml");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripExtension(String name) {
|
||||||
|
return isTemplate(name) ? name.substring(0, name.length() - 4) : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void add(Map<ResourceLocation, String> templates, Map<String, String> sources,
|
||||||
|
Set<String> duplicateNames, String name, String source) {
|
||||||
|
String previousSource = sources.putIfAbsent(name, source);
|
||||||
|
if (previousSource != null && !previousSource.equals(source)) duplicateNames.add(name);
|
||||||
|
ResourceLocation id = symbolId(name);
|
||||||
|
String previous = templates.putIfAbsent(id, name);
|
||||||
|
if (previous != null && !previous.equals(name)) {
|
||||||
|
Rivencraft.LOGGER.warn("Ruins templates {} and {} share page ID {}; using {}",
|
||||||
|
previous, name, id, previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||||
import net.dnorberg.rivencraft.book.LinkProperties;
|
import net.dnorberg.rivencraft.book.LinkProperties;
|
||||||
import net.dnorberg.rivencraft.compat.TerraBlenderBiomes;
|
import net.dnorberg.rivencraft.compat.TerraBlenderBiomes;
|
||||||
@@ -71,6 +72,7 @@ public final class PageItem extends Item {
|
|||||||
categories.add(BlockSymbolPages.CATEGORY);
|
categories.add(BlockSymbolPages.CATEGORY);
|
||||||
categories.add(BlockSymbolPages.FLUID_CATEGORY);
|
categories.add(BlockSymbolPages.FLUID_CATEGORY);
|
||||||
categories.add(CAVE_BIOME_CATEGORY);
|
categories.add(CAVE_BIOME_CATEGORY);
|
||||||
|
if (!RuinsTemplatePages.templates().isEmpty()) categories.add(RuinsTemplatePages.CATEGORY);
|
||||||
return List.copyOf(categories);
|
return List.copyOf(categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +80,11 @@ public final class PageItem extends Item {
|
|||||||
if (category.equals(CAVE_BIOME_CATEGORY)) {
|
if (category.equals(CAVE_BIOME_CATEGORY)) {
|
||||||
return createCreativeBiomePage(ResourceLocation.withDefaultNamespace("lush_caves"));
|
return createCreativeBiomePage(ResourceLocation.withDefaultNamespace("lush_caves"));
|
||||||
}
|
}
|
||||||
|
if (category.equals(RuinsTemplatePages.CATEGORY)) {
|
||||||
|
return RuinsTemplatePages.templates().stream().findFirst()
|
||||||
|
.map(PageItem::createCreativeRuinsPage)
|
||||||
|
.orElseGet(() -> createBundledCreativePage("feature/villages"));
|
||||||
|
}
|
||||||
if (category.equals(BlockSymbolPages.CATEGORY) || category.equals(BlockSymbolPages.FLUID_CATEGORY)) {
|
if (category.equals(BlockSymbolPages.CATEGORY) || category.equals(BlockSymbolPages.FLUID_CATEGORY)) {
|
||||||
return createCreativeBlockPage(ResourceLocation.withDefaultNamespace(
|
return createCreativeBlockPage(ResourceLocation.withDefaultNamespace(
|
||||||
category.equals(BlockSymbolPages.FLUID_CATEGORY) ? "water" : "stone"));
|
category.equals(BlockSymbolPages.FLUID_CATEGORY) ? "water" : "stone"));
|
||||||
@@ -93,6 +100,7 @@ public final class PageItem extends Item {
|
|||||||
public static List<ItemStack> creativeSymbolPages(HolderLookup.Provider registries) {
|
public static List<ItemStack> creativeSymbolPages(HolderLookup.Provider registries) {
|
||||||
List<ItemStack> pages = creativeBundledAndBiomePages(registries);
|
List<ItemStack> pages = creativeBundledAndBiomePages(registries);
|
||||||
BlockSymbolPages.availableBlocks().forEach(id -> pages.add(createCreativeBlockPage(id)));
|
BlockSymbolPages.availableBlocks().forEach(id -> pages.add(createCreativeBlockPage(id)));
|
||||||
|
RuinsTemplatePages.templates().forEach(name -> pages.add(createCreativeRuinsPage(name)));
|
||||||
return List.copyOf(pages);
|
return List.copyOf(pages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +123,9 @@ public final class PageItem extends Item {
|
|||||||
return BlockSymbolPages.availableBlocks(category).stream()
|
return BlockSymbolPages.availableBlocks(category).stream()
|
||||||
.map(PageItem::createCreativeBlockPage).toList();
|
.map(PageItem::createCreativeBlockPage).toList();
|
||||||
}
|
}
|
||||||
|
if (category.equals(RuinsTemplatePages.CATEGORY)) {
|
||||||
|
return RuinsTemplatePages.templates().stream().map(PageItem::createCreativeRuinsPage).toList();
|
||||||
|
}
|
||||||
Set<ResourceLocation> caveBiomes = category.equals(BIOME_CATEGORY) || category.equals(CAVE_BIOME_CATEGORY)
|
Set<ResourceLocation> caveBiomes = category.equals(BIOME_CATEGORY) || category.equals(CAVE_BIOME_CATEGORY)
|
||||||
? caveBiomeIds(registries) : Set.of();
|
? caveBiomeIds(registries) : Set.of();
|
||||||
return creativeBundledAndBiomePages(registries).stream()
|
return creativeBundledAndBiomePages(registries).stream()
|
||||||
@@ -180,6 +191,14 @@ public final class PageItem extends Item {
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ItemStack createCreativeRuinsPage(String templateName) {
|
||||||
|
var symbolId = RuinsTemplatePages.symbolId(templateName);
|
||||||
|
ItemStack page = create(PageContent.symbol(new SymbolReference(symbolId)));
|
||||||
|
page.set(ModDataComponents.PAGE_SYMBOL_DISPLAY.get(),
|
||||||
|
RuinsTemplatePages.definition(templateName).display(symbolId));
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
private static Map<String, CreativeSymbol> loadCreativeSymbols() {
|
private static Map<String, CreativeSymbol> loadCreativeSymbols() {
|
||||||
String resource = "/assets/rivencraft/creative_symbol_pages.json";
|
String resource = "/assets/rivencraft/creative_symbol_pages.json";
|
||||||
try (var stream = PageItem.class.getResourceAsStream(resource)) {
|
try (var stream = PageItem.class.getResourceAsStream(resource)) {
|
||||||
@@ -321,6 +340,10 @@ public final class PageItem extends Item {
|
|||||||
return Component.translatable(metadata.nameTranslationKey()).withStyle(ChatFormatting.OBFUSCATED);
|
return Component.translatable(metadata.nameTranslationKey()).withStyle(ChatFormatting.OBFUSCATED);
|
||||||
}
|
}
|
||||||
return reference.id().getNamespace().equals(Rivencraft.MOD_ID)
|
return reference.id().getNamespace().equals(Rivencraft.MOD_ID)
|
||||||
|
&& RuinsTemplatePages.isSymbol(reference.id())
|
||||||
|
? Component.translatableWithFallback(metadata.nameTranslationKey(),
|
||||||
|
RuinsTemplatePages.fallbackName(reference.id()))
|
||||||
|
: reference.id().getNamespace().equals(Rivencraft.MOD_ID)
|
||||||
&& reference.id().getPath().startsWith("biome/")
|
&& reference.id().getPath().startsWith("biome/")
|
||||||
? Component.translatableWithFallback(metadata.nameTranslationKey(),
|
? Component.translatableWithFallback(metadata.nameTranslationKey(),
|
||||||
BiomeSymbolPages.fallbackName(reference.id()))
|
BiomeSymbolPages.fallbackName(reference.id()))
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package net.dnorberg.rivencraft.mixin;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsCompatibility;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Pseudo;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
|
||||||
|
/** Increase Ruins' per-chunk attempt loop only for Ages with duplicated Ruins pages. */
|
||||||
|
@Pseudo
|
||||||
|
@Mixin(targets = "atomicstryker.ruins.common.RuinGenerator", remap = false)
|
||||||
|
public abstract class RuinsAttemptMixin {
|
||||||
|
@ModifyExpressionValue(method = "generateNormal", at = @At(value = "FIELD",
|
||||||
|
target = "Latomicstryker/ruins/common/FileHandler;triesPerChunkNormal:I"), remap = false)
|
||||||
|
private int rivencraft$normalAttempts(int configured, Level level) {
|
||||||
|
return RuinsCompatibility.attemptsPerChunk(level.dimension().location(), configured);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ModifyExpressionValue(method = "generateNether", at = @At(value = "FIELD",
|
||||||
|
target = "Latomicstryker/ruins/common/FileHandler;triesPerChunkNether:I"), remap = false)
|
||||||
|
private int rivencraft$netherAttempts(int configured, Level level) {
|
||||||
|
return RuinsCompatibility.attemptsPerChunk(level.dimension().location(), configured);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package net.dnorberg.rivencraft.mixin;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsCompatibility;
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsEventAdapter;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import org.spongepowered.asm.mixin.Final;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Pseudo;
|
||||||
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
/** Ruins defaults to a fixed vanilla whitelist, so authored Ages need a narrow override. */
|
||||||
|
@Pseudo
|
||||||
|
@Mixin(targets = "atomicstryker.ruins.common.FileHandler", remap = false)
|
||||||
|
public abstract class RuinsDimensionMixin {
|
||||||
|
@Shadow @Final private ResourceLocation dimension;
|
||||||
|
@Shadow @Final private HashMap<String, HashSet<?>> templates;
|
||||||
|
|
||||||
|
@Inject(method = "allowsDimension", at = @At("HEAD"), cancellable = true, remap = false)
|
||||||
|
private void rivencraft$allowAuthoredAge(String path, CallbackInfoReturnable<Boolean> result) {
|
||||||
|
if (RuinsCompatibility.isKnownAge(dimension)) {
|
||||||
|
result.setReturnValue(RuinsCompatibility.allowsAge(dimension));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject(method = "getTemplate", at = @At("HEAD"), cancellable = true, remap = false)
|
||||||
|
private void rivencraft$selectAuthoredTemplate(RandomSource random, String biome,
|
||||||
|
CallbackInfoReturnable<Object> result) {
|
||||||
|
if (RuinsCompatibility.isKnownAge(dimension)) {
|
||||||
|
result.setReturnValue(RuinsEventAdapter.chooseTemplate(dimension, templates.get(biome), random));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package net.dnorberg.rivencraft.mixin;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsCompatibility;
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import org.spongepowered.asm.mixin.Final;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Pseudo;
|
||||||
|
import org.spongepowered.asm.mixin.Shadow;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
/** An authored Ruins page overrides a template's vanilla-only dimension list for that Age. */
|
||||||
|
@Pseudo
|
||||||
|
@Mixin(targets = "atomicstryker.ruins.common.RuinTemplate", remap = false)
|
||||||
|
public abstract class RuinsTemplateDimensionMixin {
|
||||||
|
@Shadow @Final private String name;
|
||||||
|
|
||||||
|
@Inject(method = "acceptsDimension", at = @At("HEAD"), cancellable = true, remap = false)
|
||||||
|
private void rivencraft$allowAuthoredAge(String dimensionPath, CallbackInfoReturnable<Boolean> result) {
|
||||||
|
ResourceLocation dimension = ResourceLocation.fromNamespaceAndPath("rivencraft", dimensionPath);
|
||||||
|
if (RuinsCompatibility.isKnownAge(dimension)) {
|
||||||
|
result.setReturnValue(RuinsCompatibility.permits(dimension, RuinsTemplatePages.featureId(name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ public final class ModCreativeTabs {
|
|||||||
.build());
|
.build());
|
||||||
|
|
||||||
private static final List<String> CATEGORY_ORDER = List.of(
|
private static final List<String> CATEGORY_ORDER = List.of(
|
||||||
"terrain", "biome", "cave_biome", "biome_generation", "weather", "season", "lighting", "sky", "color", "number", "celestial", "block", "fluid", "feature", "environment", "corrupted");
|
"terrain", "biome", "cave_biome", "biome_generation", "weather", "season", "lighting", "sky", "color", "number", "celestial", "block", "fluid", "feature", "ruins", "environment", "corrupted");
|
||||||
|
|
||||||
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
|
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
|
||||||
registerSymbolPageTabs();
|
registerSymbolPageTabs();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.compat.RuinsTemplatePages;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.core.RegistryAccess;
|
import net.minecraft.core.RegistryAccess;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
@@ -73,6 +74,9 @@ public final class SymbolCatalog {
|
|||||||
BiomeSymbolPages.definition(id)));
|
BiomeSymbolPages.definition(id)));
|
||||||
BlockSymbolPages.availableBlocks().forEach(id -> loaded.putIfAbsent(
|
BlockSymbolPages.availableBlocks().forEach(id -> loaded.putIfAbsent(
|
||||||
BlockSymbolPages.symbolId(id), BlockSymbolPages.definition(id)));
|
BlockSymbolPages.symbolId(id), BlockSymbolPages.definition(id)));
|
||||||
|
RuinsTemplatePages.refresh();
|
||||||
|
RuinsTemplatePages.templates().forEach(name -> loaded.putIfAbsent(
|
||||||
|
RuinsTemplatePages.symbolId(name), RuinsTemplatePages.definition(name)));
|
||||||
symbols = Map.copyOf(loaded);
|
symbols = Map.copyOf(loaded);
|
||||||
Rivencraft.LOGGER.info("Loaded {} writing symbols", loaded.size());
|
Rivencraft.LOGGER.info("Loaded {} writing symbols", loaded.size());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,6 +431,8 @@
|
|||||||
"rivencraft.ponder.crystal_portal.text_3": "Insert a bound Linking or Descriptive Book to activate the portal.",
|
"rivencraft.ponder.crystal_portal.text_3": "Insert a bound Linking or Descriptive Book to activate the portal.",
|
||||||
"rivencraft.ponder.crystal_portal.text_4": "Step through the portal. Remove the book to close it.",
|
"rivencraft.ponder.crystal_portal.text_4": "Step through the portal. Remove the book to close it.",
|
||||||
"itemGroup.rivencraft": "Rivencraft",
|
"itemGroup.rivencraft": "Rivencraft",
|
||||||
|
"itemGroup.rivencraft.pages.ruins": "Rivencraft Ruins Pages",
|
||||||
|
"symbol.rivencraft.ruins.description": "Allows this Ruins template to appear in the Age.",
|
||||||
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
||||||
"itemGroup.rivencraft.pages.terrain": "Rivencraft Terrain Pages",
|
"itemGroup.rivencraft.pages.terrain": "Rivencraft Terrain Pages",
|
||||||
"itemGroup.rivencraft.pages.biome": "Rivencraft Biome Pages",
|
"itemGroup.rivencraft.pages.biome": "Rivencraft Biome Pages",
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
"AgeDungeonPlacementMixin",
|
"AgeDungeonPlacementMixin",
|
||||||
"AgeStructureBiomeMixin",
|
"AgeStructureBiomeMixin",
|
||||||
"MobDropChanceAccessor",
|
"MobDropChanceAccessor",
|
||||||
|
"RuinsDimensionMixin",
|
||||||
|
"RuinsAttemptMixin",
|
||||||
|
"RuinsTemplateDimensionMixin",
|
||||||
"StructurePlacementAccessor"
|
"StructurePlacementAccessor"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
|
|||||||
Reference in new issue
Block a user