Add alpha.17 experimental forgotten structure networks
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.17 — 2026-09-09
|
||||
|
||||
- Prototype de trois réseaux oubliés sur l’île initiale : mines et ateliers,
|
||||
salles étranges, infrastructures abandonnées. Accès depuis la surface,
|
||||
couloirs et embranchements, outils vanilla utilisables, mobilier et traces de
|
||||
chantier. Placement borné, hors ville et eaux protégées, avec appuis vérifiés.
|
||||
- Option FR/EN « Structures expérimentales » dans Personnaliser, enregistrée
|
||||
avec la taille du nouveau monde. Les connexions entre réseaux, le réacteur,
|
||||
la progression et les reliques ne sont pas encore implémentés.
|
||||
- Codec `sanctuary:island_v17`, pièces natives indépendantes, journal 17.
|
||||
Les mondes 16 et antérieurs conservent leurs plans et futurs chunks.
|
||||
L’option ne supprime ni n’ajoute rétroactivement des structures.
|
||||
|
||||
`check build assemblePack` passe. Création et réouverture natives : 15 pièces,
|
||||
7 639 blocs contrôlés, 49 cibles accessibles, 12 appuis et 3 coffres conservés.
|
||||
Le cas désactivé et la sauvegarde des trois tailles passent. La génération 16
|
||||
conserve les empreintes natives et le journal des témoins publiés. Artefacts
|
||||
vérifiés, publication en cours ; voir [le protocole](docs/testing-alpha17.md).
|
||||
|
||||
## 0.1.0-alpha.16 — 2026-09-09
|
||||
|
||||
- Ville compacte obligatoire sur la nouvelle île initiale, structures activées.
|
||||
|
||||
@@ -19,6 +19,29 @@ décrits dans la vision ne sont pas encore implémentés. **L’alpha.16 est pub
|
||||
piscines, des parkings profonds et des égouts ouverts. Le canal packwiz et la
|
||||
même instance Prism sont à jour ; voir [Distribution](docs/packwiz.md).
|
||||
|
||||
## Alpha.17 — prototype des réseaux oubliés
|
||||
|
||||
La nouvelle génération expérimentale recherche trois petits réseaux sur l’île
|
||||
initiale : mines et ateliers, salles étranges, installations techniques
|
||||
abandonnées. Chaque réseau possède une entrée de surface et des salles reliées,
|
||||
avec mobilier, outils vanilla et traces de travaux interrompus. Les continents
|
||||
conservent leur génération urbaine sans recevoir ces réseaux supplémentaires.
|
||||
|
||||
Dans **Personnaliser**, l’option **Structures expérimentales** accompagne les
|
||||
trois tailles habituelles. Elle est enregistrée avec chaque nouveau monde ;
|
||||
elle permet de comparer un monde avec les nouveaux lieux et un autre avec le
|
||||
socle urbain seul. La présence des réseaux dépend des emplacements admissibles
|
||||
trouvés dans un budget borné. Les connexions entre familles restent à construire.
|
||||
|
||||
Le prototype demande un **nouveau monde**. Les mondes 16 et antérieurs gardent
|
||||
leur génération. Revenir au binaire 16 ne permet pas d’ouvrir un monde 17 : les
|
||||
mondes d’essai restent séparés. Voir [le contrat alpha.17](docs/testing-alpha17.md).
|
||||
Le réacteur, la progression et les reliques restent prévus pour la suite.
|
||||
|
||||
`check build assemblePack`, création/réouverture et option désactivée passent.
|
||||
Les empreintes natives de la génération 16 restent identiques aux témoins publiés.
|
||||
Les artefacts 17 sont vérifiés ; publication et mise à jour du canal en cours.
|
||||
|
||||
## Alpha.16 — Sanctuary habitée avant nous
|
||||
|
||||
Une ville compacte apparaît sur la nouvelle île de départ lorsque les structures
|
||||
@@ -381,7 +404,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.16 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.17 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -410,7 +433,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.16.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.17.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -668,6 +668,35 @@ Release et canal packwiz publiés ; même instance Prism synchronisée deux fois
|
||||
avec les 767 fichiers personnels et réglages suivis conservés. Voir [le reçu](packwiz.md).
|
||||
Les choix des futurs réseaux et donjons sont consignés dans la vision.
|
||||
|
||||
### WG-20 — Réseaux oubliés, prototype réversible — alpha.17
|
||||
|
||||
**Branche :** `codex/structures-experiment-alpha17`.
|
||||
|
||||
**Contrat :** rendre explorables les trois familles validées : mines et ateliers,
|
||||
lieux étranges physiques dans l’île, infrastructures abandonnées. Les traces
|
||||
privilégient des installations utilisables et des chantiers interrompus.
|
||||
|
||||
- Nouveau générateur 17 et option « Structures expérimentales » dans le bouton
|
||||
natif Personnaliser. La taille et l’option sont enregistrées avec le nouveau
|
||||
monde ; aucune activation rétroactive ou suppression de structures existantes.
|
||||
- Conserver les codecs, pièces, plans et futurs chunks des générations publiées,
|
||||
dont 16. La ville 16 reste le socle de 17 ; les nouveaux réseaux disposent de
|
||||
leurs pièces natives, emprises protégées et calculs bornés propres.
|
||||
- Trois petits réseaux recherchés sur l’île initiale, avec accès de surface,
|
||||
salles, galeries et mobilier. Les continents ne reçoivent pas ce prototype.
|
||||
Les connexions entre réseaux seront évaluées après les premières implantations.
|
||||
- Ne pas implémenter de réacteur, progression, reliques ou règles de récompenses
|
||||
personnelles. Les coffres éventuels utilisent des tables vanilla différées.
|
||||
- Tester terrain/supports, connexions internes, accès, réglage sauvegardé,
|
||||
placement natif et réouverture. Livrer le pack et synchroniser l’instance
|
||||
existante selon le protocole packwiz. Donner graines et coordonnées d’essai.
|
||||
|
||||
**État : code et artefacts vérifiés, publication en cours.** Création/réouverture,
|
||||
option désactivée, compatibilité 16 et `check build assemblePack` passent.
|
||||
Retour arrière par branche/artefact et option désactivée
|
||||
pour un autre nouveau monde. Aucun retour binaire vers 16 dans une sauvegarde 17
|
||||
n’est déclaré compatible ; les mondes d’essai restent séparés.
|
||||
|
||||
## Réserve de thèmes futurs
|
||||
|
||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Alpha.17 — réseaux oubliés, prototype réversible
|
||||
|
||||
**État : code et artefacts vérifiés ; publication en cours.** Ticket WG-20, branche
|
||||
`codex/structures-experiment-alpha17`. Minecraft 26.3-pre-2, mêmes dépendances
|
||||
que l’alpha.16. Cet essai porte sur les lieux et leur génération.
|
||||
|
||||
## Contrat
|
||||
|
||||
Créer un **nouveau monde Sanctuary**, puis **Personnaliser** : Petit 512,
|
||||
Moyen 724 par défaut, Grand 1 024 et **Structures expérimentales** (activées par
|
||||
défaut). Le choix est enregistré dans le générateur 17, avec la taille. Désactiver
|
||||
l’option pour un autre nouveau monde permet de comparer avec le socle urbain 16.
|
||||
Ce bouton ne supprime ni ne rajoute de structures dans une sauvegarde existante.
|
||||
|
||||
Les anciens mondes gardent leurs codecs, rendus, pièces et futurs chunks. Le
|
||||
preset 16 historique est conservé à l’identique et caché à la création. Le 17
|
||||
possède son propre journal ; aucune conversion ni rétrogradation de sauvegarde
|
||||
n’est proposée. Le retour au binaire 16 concerne seulement les mondes 16 et
|
||||
antérieurs, jamais un monde 17.
|
||||
|
||||
Les nouvelles structures sont recherchées sur l’île initiale uniquement, hors
|
||||
ville et eaux protégées. Trois petits réseaux distincts : mines et ateliers,
|
||||
salles étranges, installations techniques abandonnées. Salles et couloirs se
|
||||
raccordent à des accès de surface. Les fondations reposent sur le terrain ou sur
|
||||
des appuis courts certifiés. La présence dépend des emplacements trouvés dans
|
||||
un budget borné. Les jonctions entre familles restent une étape suivante.
|
||||
|
||||
Les établis, fourneaux et outils vanilla donnent des usages aux installations ;
|
||||
rails, soutènements et chantiers interrompus suggèrent les anciens occupants.
|
||||
Les lieux étranges sont physiques dans l’île, distincts de la future dimension
|
||||
Backrooms. Aucun réacteur, progression, relique collective, récompense personnelle
|
||||
ou nouveau spawner n’est ajouté par ce prototype. Les coffres éventuels utilisent
|
||||
les tables de butin vanilla.
|
||||
|
||||
## Vérification et essai
|
||||
|
||||
Le monde **Moyen, graine 0**, option expérimentale activée, reçoit trois réseaux
|
||||
avec cinq pièces chacun. Les contrôles natifs portent sur **15 pièces**, **7 639
|
||||
blocs**, **13 chunks FULL**, **12 appuis**, **49 cibles de parcours** et **3 coffres
|
||||
à butin différé**. Les empreintes des starts, le plan et un bloc d’or témoin sont
|
||||
conservés après réouverture dans une seconde JVM. Les trappes sont ouvertes pour
|
||||
vérifier le parcours réel d’un joueur, puis refermées avant la sauvegarde.
|
||||
|
||||
| Réseau | Entrée X / Y / Z |
|
||||
| --- | --- |
|
||||
| Mine et atelier | **78 / 252 / 179** |
|
||||
| Salles étranges et archives | **−194 / 254 / −22** |
|
||||
| Locaux techniques | **116 / 253 / −157** |
|
||||
|
||||
Avec les commandes autorisées :
|
||||
|
||||
```text
|
||||
/locate structure sanctuary:forgotten_networks
|
||||
```
|
||||
|
||||
La commande donne une entrée de surface. Le prototype recherche les trois
|
||||
familles, sans les garantir sur chaque graine. Les neuf types de salles et
|
||||
couloirs plus l’accès sont testés sur des dimensions et ouvertures variées ;
|
||||
les plafonds, appuis de mobilier, orientations et chemins sont contrôlés.
|
||||
|
||||
Les trois smokes dédiés passent. Les huit contrôles moteur du socle passent,
|
||||
avec conservation des cavités et eaux naturelles. Les trois tailles et les deux
|
||||
valeurs de l’option passent le stockage réel des paramètres sur disque.
|
||||
|
||||
La planification des réseaux prend environ une seconde après celle de la ville
|
||||
sur le témoin 0 de ce Mac : 915 colonnes lues et 11 974 sondages, pour des plafonds
|
||||
respectifs de 5 000 et 250 000. Cela ne mesure ni le rendu client ni le chargement
|
||||
complet sur une autre machine. Rapports locaux ignorés :
|
||||
`build/alpha17-evidence/ruins17-create.json` et `ruins17-reload.json`.
|
||||
Logs : `build/alpha17-native-create-final.log`, `alpha17-native-reload.log`,
|
||||
`alpha17-first-native-base.log`, `alpha17-planner-piece-smokes.log`.
|
||||
|
||||
L’essai natif avec **Structures expérimentales désactivées** passe : plan vide,
|
||||
zéro sondage expérimental et booléen conservé. Rapport :
|
||||
`build/alpha17-evidence/ruins17-disabled.json`, log `build/alpha17-native-disabled.log`.
|
||||
La création et la réouverture actives utilisent les processus 32029 et 32123 ; le
|
||||
cas désactivé utilise 32176. Aucune sauvegarde personnelle n’a été ouverte.
|
||||
|
||||
La création et la réouverture des mondes de génération **16** passent sous le
|
||||
binaire 17 : les 71 pièces de la ville initiale et du continent témoin gardent
|
||||
leurs empreintes natives, ainsi que le journal, les paramètres et le bloc d’or
|
||||
témoin de la version publiée. Rapports :
|
||||
`build/alpha17-evidence/legacy16-create.json` et `legacy16-reload.json`.
|
||||
Le preset 16 historique est également identique octet par octet. La comparaison
|
||||
avec les reçus publiés est consignée dans `legacy16-comparison.json` :
|
||||
création sous 17 dans le processus 32244, réouverture dans 32429.
|
||||
|
||||
`./gradlew check build assemblePack` passe : 52 tâches, huit GameTests moteur
|
||||
et les trois smokes du prototype. Le JAR de production exclut les classes de
|
||||
test ; les versions, les hashes du pack, le MRpack et le ZIP d’amorçage Prism
|
||||
sont vérifiés. Journal final : `build/alpha17-final-check-build-pack.log`.
|
||||
|
||||
La publication et la synchronisation de l’instance sont en cours. Aucun essai
|
||||
Windows natif ni revue visuelle dans le client n’est revendiqué ; le prochain
|
||||
essai de Koka validera l’ambiance.
|
||||
@@ -231,6 +231,11 @@ L'ambition est de reprendre puis d'étendre la centaine de biomes du catalogue T
|
||||
spawners pourront rester exploitables après exploration sans régénérer les
|
||||
donjons. Les règles précises restent à concevoir. Le réacteur et ces nouveaux
|
||||
systèmes ne font pas partie de l’alpha.16.
|
||||
Le prototype alpha.17 commence par trois petits réseaux indépendants, avec
|
||||
accès de surface, ateliers, galeries, salles étranges et locaux techniques.
|
||||
L’option « Structures expérimentales » se choisit pour un nouveau monde.
|
||||
Les connexions entre familles et les systèmes de butin spécifiques restent
|
||||
à construire ; voir [le contrat du prototype](testing-alpha17.md).
|
||||
- **Secret rooms et challenge rooms** : des accès présents dans le monde mènent à des indoors aménagés, avec des équipements, des épreuves ou des gisements de fer, d'or, d'argent ou de titane.
|
||||
|
||||
### Dimensions
|
||||
|
||||
+2
-2
@@ -8,6 +8,6 @@ loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
mod_version=0.1.0-alpha.16
|
||||
pack_version=0.1.0-alpha.16
|
||||
mod_version=0.1.0-alpha.17
|
||||
pack_version=0.1.0-alpha.17
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -40,12 +40,14 @@ tasks.named('runGameTest') {
|
||||
systemProperty('sanctuary.test.unified', providers.gradleProperty('sanctuaryUnifiedTests').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.diameter', providers.gradleProperty('sanctuaryTestDiameter').getOrElse('724'))
|
||||
systemProperty('sanctuary.test.generation', providers.gradleProperty('sanctuaryTestGeneration').getOrElse(
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : '16'))
|
||||
providers.gradleProperty('sanctuaryLostCityTests').map { it.toBoolean() }.getOrElse(false) ? '14' : providers.gradleProperty('sanctuaryLostCity15Tests').map { it.toBoolean() }.getOrElse(false) ? '15' : providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false) ? '16' : '17'))
|
||||
systemProperty('sanctuary.test.expansion', providers.gradleProperty('sanctuaryExpansionTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.expansionReload', providers.gradleProperty('sanctuaryExpansionReload').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity', providers.gradleProperty('sanctuaryLostCityTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity15', providers.gradleProperty('sanctuaryLostCity15Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.lostCity16', providers.gradleProperty('sanctuaryLostCity16Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.ruins17', providers.gradleProperty('sanctuaryRuins17Tests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.experimentalStructures', providers.gradleProperty('sanctuaryExperimentalStructures').getOrElse('true'))
|
||||
systemProperty('sanctuary.test.cityMatrix', providers.gradleProperty('sanctuaryCityMatrix').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.cityMatrixOnly', providers.gradleProperty('sanctuaryCityMatrixOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.voidReuse', providers.gradleProperty('sanctuaryVoidReuseTests').getOrElse('false'))
|
||||
@@ -65,10 +67,12 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('lostCity15', lostCity15)
|
||||
def lostCity16 = providers.gradleProperty('sanctuaryLostCity16Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lostCity16', lostCity16)
|
||||
def ruins17 = providers.gradleProperty('sanctuaryRuins17Tests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('ruins17', ruins17)
|
||||
def voidReuse = providers.gradleProperty('sanctuaryVoidReuseTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('voidReuse', voidReuse)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand(gametest_entrypoint: voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
|
||||
expand(gametest_entrypoint: ruins17 ? 'fr.koka.sanctuary.gametest.Ruins17WorldGameTests' : voidReuse ? 'fr.koka.sanctuary.gametest.ExpansionVoidReuseGameTests' : lostCity16 ? 'fr.koka.sanctuary.gametest.LostCity16WorldGameTests' : lostCity15 ? 'fr.koka.sanctuary.gametest.LostCity15WorldGameTests' : lostCity ? 'fr.koka.sanctuary.gametest.LostCityWorldGameTests' : expansion ? 'fr.koka.sanctuary.gametest.ExpansionWorldGameTests' : benchmark
|
||||
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||
: unified ? 'fr.koka.sanctuary.gametest.UnifiedWorldGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
@@ -400,3 +404,14 @@ tasks.register('lostCityFoundation16Smoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.city.LostCityFoundation16Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('lostCityFoundation16Smoke') }
|
||||
|
||||
// The experimental pieces and algorithms are additive; historical city suites remain unchanged.
|
||||
['Planner', 'Renderer', 'Piece'].each { component ->
|
||||
def smoke = tasks.register("ruins17${component}Smoke", JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka.sanctuary.worldgen.ruins.Ruins${component}17Smoke"
|
||||
}
|
||||
tasks.named('check') { dependsOn(smoke) }
|
||||
}
|
||||
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPiece17;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsRenderer17;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsStructures17;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Opt-in native proof confined to the new generation17 starter island in a disposable development world. */
|
||||
public final class Ruins17WorldGameTests {
|
||||
private static final Direction[] HORIZONTAL = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void forgottenNetworksPersistNatively(GameTestHelper helper) {
|
||||
helper.assertTrue(helper.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,
|
||||
"The forgotten-networks proof requires a Sanctuary generator");
|
||||
var generator = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
helper.assertTrue(generator.rootGeneration() == 17, "Use a fresh generation17 test world; do not convert old saves");
|
||||
new Scenario(helper, generator, Boolean.getBoolean("sanctuary.test.expansionReload")).advance();
|
||||
}
|
||||
|
||||
private static final class Scenario {
|
||||
final GameTestHelper helper;
|
||||
final ServerLevel level;
|
||||
final SanctuaryChunkGenerator generator;
|
||||
final RuinsPlan17 plan;
|
||||
final boolean reload;
|
||||
final Map<String, Object> report = new LinkedHashMap<>();
|
||||
final List<ChunkPos> chunks = new ArrayList<>();
|
||||
final List<StructureStart> starts = new ArrayList<>();
|
||||
JsonObject previous;
|
||||
BlockPos gold;
|
||||
int nextChunk;
|
||||
boolean prepared;
|
||||
|
||||
Scenario(GameTestHelper helper, SanctuaryChunkGenerator generator, boolean reload) {
|
||||
this.helper = helper; this.level = helper.getLevel(); this.generator = generator; this.reload = reload;
|
||||
this.plan = generator.ruinsPlan17();
|
||||
report.put("seed", level.getSeed()); report.put("size", generator.initialDiameter());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("world_directory", world(level).toString());
|
||||
report.put("experimental_structures", generator.experimentalStructures());
|
||||
report.put("structures_enabled", level.structureManager().shouldGenerateStructures());
|
||||
report.put("scope", "Native starts, references, room blocks, player-sized routes and unopened loot on the starter island only. "
|
||||
+ "No expansion is created and no personal save is opened. A single outer-wall gold edit in this disposable world "
|
||||
+ "is saved and checked in a second JVM. Open hatches are restored after traversal; no free-fluid simulation is claimed.");
|
||||
}
|
||||
void advance() {
|
||||
try {
|
||||
if (!prepared) {
|
||||
prepare(); prepared = true;
|
||||
if (plan.isEmpty()) { finishDisabled(); return; }
|
||||
}
|
||||
if (nextChunk < chunks.size()) {
|
||||
var pos = chunks.get(nextChunk++);
|
||||
var loaded = level.getChunk(pos.x(), pos.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(loaded instanceof LevelChunk, "Every room footprint must reach native FULL: " + pos);
|
||||
if (nextChunk % 10 == 0) SanctuaryMod.LOGGER.info("Forgotten networks17: loaded {}/{} footprint chunks", nextChunk, chunks.size());
|
||||
helper.runAtTickTime(helper.getTick() + 1, this::advance); return;
|
||||
}
|
||||
verifyPlaced();
|
||||
report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(gold != null && level.getBlockState(gold).isCollisionShapeFullBlock(level, gold),
|
||||
"Gameplay witness must replace a solid outer wall away from every doorway");
|
||||
level.setBlock(gold, Blocks.GOLD_BLOCK.defaultBlockState(), 3);
|
||||
report.put("gold_witness", coords(gold));
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush native structures, deferred loot and the gold edit");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
} else {
|
||||
helper.assertTrue(level.getBlockState(gold).is(Blocks.GOLD_BLOCK), "Reload replaced the existing wall edit");
|
||||
report.put("gold_witness", coords(gold)); report.put("gold_witness_preserved", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
} catch (Exception failure) {
|
||||
report.put("passed", false); report.put("failure", failure.toString());
|
||||
try { write(level, report, reload); } catch (IOException ignored) { }
|
||||
throw new IllegalStateException("Forgotten networks17 native " + (reload ? "reload" : "creation") + " failed", failure);
|
||||
}
|
||||
}
|
||||
void prepare() throws IOException {
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var encoded = ChunkGenerator.CODEC.encodeStart(ops, generator).getOrThrow();
|
||||
var decoded = ChunkGenerator.CODEC.parse(ops, encoded).getOrThrow();
|
||||
helper.assertTrue(decoded instanceof SanctuaryChunkGenerator copy && copy.rootGeneration() == 17
|
||||
&& copy.experimentalStructures() == generator.experimentalStructures(), "The generation17 codec must preserve its experimental switch");
|
||||
report.put("generator_codec", encoded); report.put("plan_sha256", planHash(plan)); report.put("planning_samples", plan.samples());
|
||||
helper.assertTrue(plan.samples() <= RuinsPlan17.MAX_SAMPLES, "Forgotten-network planning exceeded its budget");
|
||||
if (reload) {
|
||||
previous = JsonParser.parseString(Files.readString(proof(level))).getAsJsonObject();
|
||||
helper.assertTrue(previous.get("passed").getAsBoolean() && previous.get("world_flushed_before_shutdown").getAsBoolean(),
|
||||
"Reload requires the successful, flushed creation proof");
|
||||
helper.assertTrue(previous.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload must run in a separate JVM");
|
||||
helper.assertTrue(previous.get("seed").getAsLong() == level.getSeed()
|
||||
&& previous.get("size").getAsInt() == generator.initialDiameter()
|
||||
&& previous.get("world_directory").getAsString().equals(world(level).toString()), "Reload used another world, seed or size");
|
||||
helper.assertTrue(previous.get("generator_codec").equals(encoded) && previous.get("plan_sha256").getAsString().equals(planHash(plan)),
|
||||
"Saved switch or planned generation17 room identities changed on reload");
|
||||
if (previous.has("gold_witness")) gold = readPos(previous.getAsJsonArray("gold_witness"));
|
||||
}
|
||||
boolean enabled = generator.experimentalStructures() && level.structureManager().shouldGenerateStructures();
|
||||
if (!enabled) {
|
||||
helper.assertTrue(plan.isEmpty(), "Experimental=false or generateStructures=false must publish no forgotten network");
|
||||
return;
|
||||
}
|
||||
helper.assertTrue(!plan.isEmpty(), "Enabled reference island has no experimental network");
|
||||
var families = EnumSet.noneOf(RuinsPlan17.Family.class);
|
||||
plan.networks().forEach(n -> families.add(n.family()));
|
||||
report.put("families", families.stream().map(Enum::name).toList()); report.put("networks", plan.networks().size());
|
||||
if (level.getSeed() == 0) helper.assertTrue(families.equals(EnumSet.allOf(RuinsPlan17.Family.class)),
|
||||
"Seed0 reference must demonstrate all three experimental families; found " + families);
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var origins = new HashSet<Long>(); var uniqueChunks = new HashSet<ChunkPos>();
|
||||
for (var network : plan.networks()) {
|
||||
long origin = new ChunkPos(network.startChunkX(), network.startChunkZ()).pack();
|
||||
if (!origins.add(origin)) continue;
|
||||
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start != null && start.isValid(), "Accepted experimental network has no native start: " + network.id());
|
||||
starts.add(start);
|
||||
for (var piece : start.getPieces()) {
|
||||
helper.assertTrue(piece instanceof RuinsPiece17, "Forgotten network loaded a foreign or historical piece type");
|
||||
piece.getBoundingBox().intersectingChunks().forEach(uniqueChunks::add);
|
||||
}
|
||||
}
|
||||
helper.assertTrue(uniqueChunks.size() <= 225, "The bounded root-only proof unexpectedly covers more than225 chunks");
|
||||
chunks.addAll(uniqueChunks.stream().sorted(java.util.Comparator.comparingInt(ChunkPos::x).thenComparingInt(ChunkPos::z)).toList());
|
||||
}
|
||||
void finishDisabled() throws IOException {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var chunk = level.getChunk(0, 0, ChunkStatus.STRUCTURE_STARTS, true);
|
||||
var start = chunk.getStartForStructure(structure.value());
|
||||
helper.assertTrue(start == null || !start.isValid(), "Disabled experimental generation emitted a native start");
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located == null, "Disabled experimental structures must not be located or prepared");
|
||||
report.put("empty_plan_verified", true); report.put("passed", true);
|
||||
if (!reload) {
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Save the disabled experiment setting");
|
||||
report.put("world_flushed_before_shutdown", true);
|
||||
}
|
||||
write(level, report, reload); helper.succeed();
|
||||
}
|
||||
void verifyPlaced() {
|
||||
var structure = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(RuinsStructures17.KEY);
|
||||
var context = StructurePieceSerializationContext.fromLevel(level);
|
||||
var sites = new ArrayList<Map<String, Object>>();
|
||||
int blocks = 0, supports = 0, chests = 0, pieces = 0;
|
||||
for (var start : starts) {
|
||||
var origin = start.getChunkPos(); var saved = start.createTag(context, origin);
|
||||
var copy = StructureStart.loadStaticStart(context, saved, level.getSeed());
|
||||
helper.assertTrue(copy != null && copy.isValid() && copy.createTag(context, origin).equals(saved), "The native start changed on an NBT roundtrip");
|
||||
var expectedRooms = plan.networks().stream().filter(n -> n.startChunkX() == origin.x() && n.startChunkZ() == origin.z())
|
||||
.flatMap(n -> n.rooms().stream()).toList();
|
||||
var actualRooms = start.getPieces().stream().map(RuinsPiece17.class::cast).map(RuinsPiece17::room).toList();
|
||||
helper.assertTrue(actualRooms.size() == expectedRooms.size() && new HashSet<>(actualRooms).equals(new HashSet<>(expectedRooms)),
|
||||
"Native start did not persist every planned room exactly once");
|
||||
String nativeHash = hash(saved.toString());
|
||||
if (reload) {
|
||||
JsonObject prior = null;
|
||||
for (var value : previous.getAsJsonArray("starts")) {
|
||||
var site = value.getAsJsonObject(); var at = site.getAsJsonArray("start_chunk");
|
||||
if (at.get(0).getAsInt() == origin.x() && at.get(1).getAsInt() == origin.z()) prior = site;
|
||||
}
|
||||
helper.assertTrue(prior != null && prior.get("native_start_sha256").getAsString().equals(nativeHash),
|
||||
"Reload altered native room parameters, supports or reference count");
|
||||
}
|
||||
var loot = new ArrayList<Map<String, Object>>();
|
||||
for (var piece : start.getPieces()) {
|
||||
var roomPiece = (RuinsPiece17) piece; var room = roomPiece.room(); pieces++;
|
||||
piece.getBoundingBox().intersectingChunks().forEach(at -> {
|
||||
var full = level.getChunk(at.x(), at.z(), ChunkStatus.FULL, true);
|
||||
helper.assertTrue(full.getReferencesForStructure(structure.value()).contains(origin.pack()), "A saved FULL chunk lost the native structure reference");
|
||||
});
|
||||
var roomTag = roomPiece.createTag(context);
|
||||
var roundtrip = new RuinsPiece17(context, roomTag);
|
||||
helper.assertTrue(roundtrip.room().equals(room) && roundtrip.createTag(context).equals(roomTag), "The native room failed its exact saved-field roundtrip");
|
||||
var corrupt = roomTag.copy(); var payload = corrupt.getCompound("SanctuaryRoom").orElseThrow(); payload.putInt("Schema", 99);
|
||||
boolean rejected = false;
|
||||
try { new RuinsPiece17(context, corrupt); } catch (IllegalArgumentException expected) { rejected = true; }
|
||||
helper.assertTrue(rejected, "An unknown generation17 piece schema must be refused");
|
||||
var expected = rendered(room); blocks += expected.blocks().size();
|
||||
for (var entry : expected.blocks().entrySet()) {
|
||||
var at = entry.getKey(); var wanted = entry.getValue();
|
||||
helper.assertTrue(RuinsPiece17.bounds(room).isInside(at), "Renderer escaped its saved room/support bounds");
|
||||
if (reload && at.equals(gold)) continue;
|
||||
helper.assertTrue(level.getBlockState(at).is(wanted.getBlock()),
|
||||
"Native clipping or later decoration changed " + room.kind() + " at " + at + ": expected " + wanted + " got " + level.getBlockState(at));
|
||||
helper.assertTrue(!level.getBlockState(at).is(Blocks.SPAWNER), "Experimental architecture must not create a spawner");
|
||||
}
|
||||
for (var support : room.supports()) {
|
||||
supports++;
|
||||
var below = new BlockPos(support.x(), support.minY() - 1, support.z());
|
||||
helper.assertTrue(level.getBlockState(below).isSolid() && level.getFluidState(below).isEmpty(), "Room footing has lost its native rock anchor");
|
||||
}
|
||||
for (var chest : expected.chests()) {
|
||||
helper.assertTrue(level.getBlockEntity(chest.position()) instanceof RandomizableContainerBlockEntity, "Deferred native chest block entity is missing");
|
||||
var entity = (RandomizableContainerBlockEntity) level.getBlockEntity(chest.position());
|
||||
helper.assertTrue(chest.table().equals(entity.getLootTable()) && entity.getLootTableSeed() == chest.seed(),
|
||||
"Native chest loot must remain deferred with its exact table and seed");
|
||||
loot.add(Map.of("position", coords(chest.position()), "table", chest.table().identifier().toString(), "seed", chest.seed())); chests++;
|
||||
}
|
||||
if (!reload && gold == null && room.kind() != RuinsPlan17.Kind.ACCESS && room.kind() != RuinsPlan17.Kind.CORRIDOR)
|
||||
gold = new BlockPos(room.minX(), room.floorY() + 1, room.minZ() + 1);
|
||||
}
|
||||
sites.add(Map.of("start_chunk", List.of(origin.x(), origin.z()), "native_start_sha256", nativeHash,
|
||||
"pieces", start.getPieces().size(), "unopened_chests", loot));
|
||||
}
|
||||
var traversal = new ArrayList<Map<String, Object>>();
|
||||
for (var network : plan.networks()) traversal.add(verifyAccess(helper, network));
|
||||
var located = generator.findNearestMapStructure(level, HolderSet.direct(structure), new BlockPos(0, 250, 0), 100, false);
|
||||
helper.assertTrue(located != null && located.getSecond().is(RuinsStructures17.KEY), "Native locate cannot find a saved forgotten network");
|
||||
helper.assertTrue(plan.networks().stream().flatMap(n -> n.rooms().stream()).filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
|
||||
.anyMatch(r -> RuinsPiece17.locator(r).equals(located.getFirst())), "Locate must lead to a surface entrance rather than an underground floor");
|
||||
report.put("starts", sites); report.put("native_pieces", pieces); report.put("native_blocks_checked", blocks);
|
||||
report.put("full_chunks_checked", chunks.size()); report.put("rock_supports_checked", supports);
|
||||
report.put("unopened_deferred_chests", chests); report.put("native_routes", traversal); report.put("locate", coords(located.getFirst()));
|
||||
}
|
||||
}
|
||||
private record Chest(BlockPos position, ResourceKey<LootTable> table, long seed) {}
|
||||
private record Rendered(Map<BlockPos, BlockState> blocks, List<Chest> chests) {}
|
||||
private static Rendered rendered(RuinsPlan17.Room room) {
|
||||
var blocks = new HashMap<BlockPos, BlockState>(); var chests = new ArrayList<Chest>();
|
||||
RuinsRenderer17.render(room, new LostCityRenderer.BlockWriter() {
|
||||
@Override public void set(int x, int y, int z, BlockState state) { blocks.put(at(room, x, y, z), state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var position = at(room, x, y, z); blocks.put(position, Blocks.CHEST.defaultBlockState()); chests.add(new Chest(position, table, RandomSource.create(seed).nextLong()));
|
||||
}
|
||||
});
|
||||
return new Rendered(blocks, chests);
|
||||
}
|
||||
private static Map<String, Object> verifyAccess(GameTestHelper helper, RuinsPlan17.Network network) {
|
||||
var level = helper.getLevel(); var access = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElseThrow();
|
||||
var hatches = new HashMap<BlockPos, BlockState>();
|
||||
rendered(access).blocks().forEach((at, state) -> { if (state.is(Blocks.DARK_OAK_TRAPDOOR)) hatches.put(at, level.getBlockState(at)); });
|
||||
helper.assertTrue(hatches.size() == 1, "Each network must have one usable wooden surface hatch");
|
||||
hatches.forEach((at, state) -> {
|
||||
helper.assertTrue(state.is(Blocks.DARK_OAK_TRAPDOOR) && state.canSurvive(level, at), "Surface hatch is missing its native support");
|
||||
level.setBlock(at, state.setValue(BlockStateProperties.OPEN, true), 2);
|
||||
});
|
||||
try {
|
||||
Predicate<BlockPos> inside = pos -> network.rooms().stream().anyMatch(r -> r.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
var start = RuinsPiece17.locator(access); var reached = walk(helper, start, inside);
|
||||
int targets = 0;
|
||||
for (var room : network.rooms()) for (var target : RuinsRenderer17.targets(room)) {
|
||||
var point = at(room, target.x(), target.y(), target.z());
|
||||
helper.assertTrue(reached.contains(point), "Native network " + network.id() + " cannot reach " + room.kind() + "/" + target.name() + " at " + point);
|
||||
targets++;
|
||||
}
|
||||
for (var hatch : hatches.keySet()) helper.assertTrue(reached.contains(hatch), "Surface approach cannot enter its ladder shaft");
|
||||
return Map.of("id", network.id(), "family", network.family().name(), "rooms", network.rooms().size(),
|
||||
"entrance", coords(start), "walkable_targets", targets, "standing_cells", reached.size());
|
||||
} finally { hatches.forEach((at, state) -> level.setBlock(at, state, 2)); }
|
||||
}
|
||||
private static Set<BlockPos> walk(GameTestHelper helper, BlockPos start, Predicate<BlockPos> inside) {
|
||||
var level = helper.getLevel(); helper.assertTrue(inside.test(start) && standable(level, start), "Blocked native surface approach: " + start);
|
||||
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var pos = queue.removeFirst();
|
||||
for (var direction : HORIZONTAL) for (int dy = -1; dy <= 1; dy++) {
|
||||
var next = pos.relative(direction).offset(0, dy, 0);
|
||||
if (!inside.test(next) || dy > 0 && !clearBody(level, pos.above())) continue;
|
||||
if (standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
for (int dy : new int[] {-1, 1}) {
|
||||
var next = pos.offset(0, dy, 0); if (!inside.test(next)) continue;
|
||||
if ((level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(next).is(Blocks.LADDER)
|
||||
|| level.getBlockState(pos.below()).is(Blocks.LADDER)) && standable(level, next) && seen.add(next)) queue.add(next);
|
||||
}
|
||||
helper.assertTrue(seen.size() <= 50_000, "Native traversal exceeded its room bounds");
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean standable(ServerLevel level, BlockPos pos) {
|
||||
if (!clearBody(level, pos)) return false;
|
||||
if (level.getBlockState(pos).is(Blocks.LADDER) || level.getBlockState(pos.below()).is(Blocks.LADDER)) return true;
|
||||
var below = pos.below();
|
||||
return level.getBlockState(below).getCollisionShape(level, below).toAabbs().stream()
|
||||
.anyMatch(b -> b.maxY >= .5 && b.minX <= .5 && b.maxX >= .5 && b.minZ <= .5 && b.maxZ >= .5);
|
||||
}
|
||||
private static boolean clearBody(ServerLevel level, BlockPos pos) {
|
||||
var body = new AABB(pos.getX() + .2, pos.getY(), pos.getZ() + .2, pos.getX() + .8, pos.getY() + 1.8, pos.getZ() + .8);
|
||||
for (var at : new BlockPos[] {pos, pos.above()}) {
|
||||
if (!level.getFluidState(at).isEmpty()) return false;
|
||||
for (var shape : level.getBlockState(at).getCollisionShape(level, at).toAabbs())
|
||||
if (shape.move(at.getX(), at.getY(), at.getZ()).intersects(body)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static BlockPos at(RuinsPlan17.Room room, int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
private static List<Integer> coords(BlockPos pos) { return List.of(pos.getX(), pos.getY(), pos.getZ()); }
|
||||
private static BlockPos readPos(com.google.gson.JsonArray value) { return new BlockPos(value.get(0).getAsInt(), value.get(1).getAsInt(), value.get(2).getAsInt()); }
|
||||
private static Path world(ServerLevel level) { return level.getServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); }
|
||||
private static Path proof(ServerLevel level) { return world(level).resolve("ruins17-create-proof.json"); }
|
||||
private static String planHash(RuinsPlan17 plan) { return hash(plan.networks().toString()); }
|
||||
private static void write(ServerLevel level, Map<String, Object> report, boolean reload) throws IOException {
|
||||
var path = Path.of("diagnostics", "ruins17-" + (reload ? "reload" : "create") + ".json"); Files.createDirectories(path.getParent());
|
||||
var value = new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
|
||||
Files.writeString(path, value);
|
||||
if (!reload) Files.writeString(proof(level), value);
|
||||
}
|
||||
private static String hash(String value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); }
|
||||
catch (NoSuchAlgorithmException impossible) { throw new AssertionError(impossible); }
|
||||
}
|
||||
}
|
||||
+15
-7
@@ -42,8 +42,11 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
var historical15 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v15"))).value().createWorldDimensions();
|
||||
helper.assertTrue(historical15.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 15 && old.source().generation() == 15,
|
||||
"Historical alpha.15 keeps its own biome codec and city generator");
|
||||
var historical16 = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary_v16"))).value().createWorldDimensions();
|
||||
helper.assertTrue(historical16.overworld() instanceof SanctuaryChunkGenerator old && old.rootGeneration() == 16
|
||||
&& old.source().generation() == 16 && !old.experimentalStructures(), "Historical16 never gains experimental structures");
|
||||
var generator = (SanctuaryChunkGenerator) standard.overworld();
|
||||
helper.assertTrue(generator.rootGeneration() == 16 && generator.source().generation() == 16, "The public preset uses generation16");
|
||||
helper.assertTrue(generator.rootGeneration() == 17 && generator.source().generation() == 17 && generator.experimentalStructures(), "The public preset uses generation17 with its new-world option");
|
||||
helper.assertTrue(generator.initialDiameter() == 724, "The public preset defaults to Medium / 724 blocks");
|
||||
for (var tag : List.of(WorldPresetTags.NORMAL, WorldPresetTags.EXTENDED)) {
|
||||
var visible = presets.getOrThrow(tag).stream().map(holder -> holder.getRegisteredName())
|
||||
@@ -53,8 +56,13 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
}
|
||||
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
var activeCodec = generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
|
||||
var activeCodec = generator.rootGeneration() == 17 ? SanctuaryChunkGenerator.RUINS17_CODEC : generator.rootGeneration() == 16 ? SanctuaryChunkGenerator.CITY16_CODEC : generator.rootGeneration() == 15 ? SanctuaryChunkGenerator.CITY15_CODEC : generator.rootGeneration() == 14 ? SanctuaryChunkGenerator.CITY_CODEC : SanctuaryChunkGenerator.CODEC;
|
||||
var serialized = activeCodec.codec().encodeStart(ops, generator).getOrThrow().getAsJsonObject();
|
||||
serialized.remove("experimental_structures");
|
||||
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().experimentalStructures(),
|
||||
"Generation17 omitted experimental option defaults to enabled");
|
||||
helper.assertTrue(!((SanctuaryChunkGenerator) historical16.overworld()).withExperimentalStructures(true).experimentalStructures(),
|
||||
"The new-world switch cannot activate experimental generation in a historical16 generator");
|
||||
serialized.remove("initial_diameter");
|
||||
helper.assertTrue(activeCodec.codec().parse(ops, serialized).getOrThrow().initialDiameter() == 724,
|
||||
"An omitted new-generation size defaults to Medium");
|
||||
@@ -67,8 +75,8 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
Files.createDirectories(directory);
|
||||
var storage = LevelStorageSource.createDefault(directory);
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
for (int diameter : new int[]{512, 724, 1024}) {
|
||||
var chosen = generator.withInitialDiameter(diameter);
|
||||
for (int diameter : new int[]{512, 724, 1024}) for (boolean experimental : new boolean[]{false, true}) {
|
||||
var chosen = generator.withInitialDiameter(diameter).withExperimentalStructures(experimental);
|
||||
helper.assertTrue(generator.initialDiameter() == 724 && chosen.source() != generator.source(),
|
||||
"Customizing a new world must copy the generator and its mutable biome source");
|
||||
var dimensions = standard.replaceOverworldGenerator(access, chosen);
|
||||
@@ -77,17 +85,17 @@ final class UnifiedWorldSettingsDiagnostics {
|
||||
"Customizing the island preserves the other dimensions");
|
||||
}
|
||||
var original = new WorldGenSettings(new WorldOptions(helper.getLevel().getSeed(), false, true), dimensions);
|
||||
String name = "diameter-" + diameter;
|
||||
String name = "diameter-" + diameter + "-experimental-" + experimental;
|
||||
try (var save = storage.createAccess(name)) {
|
||||
LevelStorageSource.writeWorldGenSettings(access, storage.getLevelPath(name), original);
|
||||
var restored = LevelStorageSource.readExistingSavedData(save, access, WorldGenSettings.TYPE).getOrThrow();
|
||||
helper.assertTrue(restored.dimensions().overworld() instanceof SanctuaryChunkGenerator reloaded
|
||||
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generator.rootGeneration(),
|
||||
&& reloaded.initialDiameter() == diameter && reloaded.rootGeneration() == generator.rootGeneration() && reloaded.experimentalStructures() == experimental,
|
||||
"The selected diameter must survive real world-settings storage");
|
||||
helper.assertTrue(WorldGenSettings.CODEC.encodeStart(ops, restored).getOrThrow()
|
||||
.equals(WorldGenSettings.CODEC.encodeStart(ops, original).getOrThrow()),
|
||||
"Seed, generation options and all dimensions survive the disk round trip");
|
||||
results.add(Map.of("diameter", diameter, "disk_round_trip", true));
|
||||
results.add(Map.of("diameter", diameter, "experimental_structures", experimental, "disk_round_trip", true));
|
||||
}
|
||||
}
|
||||
Files.writeString(directory.resolve("round-trip.json"), new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
|
||||
+6
-5
@@ -21,7 +21,7 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
// generation test's deadline before its IO/workers can complete. This is test-only pacing.
|
||||
@Inject(method = "waitUntilNextTick", at = @At("RETURN"))
|
||||
private void sanctuary$paceExpansionWorkers(CallbackInfo ci) {
|
||||
if (Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|
||||
if (Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.test.expansion") || Boolean.getBoolean("sanctuary.test.lostCity")
|
||||
|| Boolean.getBoolean("sanctuary.test.lostCity16") || Boolean.getBoolean("sanctuary.test.lostCity15") || Boolean.getBoolean("sanctuary.test.voidReuse")) {
|
||||
long until = System.nanoTime() + 20_000_000L;
|
||||
long remaining;
|
||||
@@ -46,9 +46,9 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 16);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 16 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
int generation = Integer.getInteger("sanctuary.test.generation", 17);
|
||||
if (generation != 13 && generation != 14 && generation != 15 && generation != 16 && generation != 17) throw new IllegalArgumentException("Unsupported test generation");
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(generation == 17 ? "sanctuary" : "sanctuary_v" + generation));
|
||||
}
|
||||
if (Boolean.getBoolean("sanctuary.test.expansion")) return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("expansion_lab"));
|
||||
int players = Integer.getInteger("sanctuary.test.players", 10);
|
||||
@@ -70,7 +70,8 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
if (Boolean.getBoolean("sanctuary.test.unified")) {
|
||||
var generator = (fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator) dimensions.overworld();
|
||||
return dimensions.replaceOverworldGenerator(context.datapackWorldRegistries(),
|
||||
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724)));
|
||||
generator.withInitialDiameter(Integer.getInteger("sanctuary.test.diameter", 724))
|
||||
.withExperimentalStructures(Boolean.parseBoolean(System.getProperty("sanctuary.test.experimentalStructures", "true"))));
|
||||
}
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,10 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v15"), SanctuaryBiomeSource.CITY15_CODEC);
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v16"), SanctuaryChunkGenerator.CITY16_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v16"), SanctuaryBiomeSource.CITY16_CODEC);
|
||||
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, id("island_v17"), SanctuaryChunkGenerator.RUINS17_CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("island_v17"), SanctuaryBiomeSource.RUINS17_CODEC);
|
||||
fr.koka.sanctuary.worldgen.city.LostCityStructures.register();
|
||||
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.register();
|
||||
Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("population_island"), PopulationIslandDensity.CODEC);
|
||||
Registry.register(BuiltInRegistries.BIOME_SOURCE, id("population_island"), PopulationIslandBiomeSource.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("population_groves"), PopulationGrovesFeature.CODEC);
|
||||
|
||||
+11
-2
@@ -12,12 +12,13 @@ import net.minecraft.client.gui.screens.worldselection.WorldCreationContext;
|
||||
import net.minecraft.network.chat.CommonComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Edits only the pending new world's island size. Cancel never changes its dimensions. */
|
||||
/** Edits size and the saved experiment switch of a pending new world. Cancel applies neither. */
|
||||
public final class SanctuaryWorldOptionsScreen extends Screen {
|
||||
private final CreateWorldScreen parent;
|
||||
private final SanctuaryChunkGenerator generator;
|
||||
private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this);
|
||||
private IslandSize selectedSize;
|
||||
private boolean experimental;
|
||||
private MultiLineTextWidget diameterHint;
|
||||
|
||||
public SanctuaryWorldOptionsScreen(CreateWorldScreen parent, WorldCreationContext context) {
|
||||
@@ -28,6 +29,7 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
|
||||
}
|
||||
generator = sanctuary;
|
||||
selectedSize = IslandSize.fromDiameter(generator.initialDiameter());
|
||||
experimental = generator.experimentalStructures();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,6 +51,13 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
|
||||
contents.addChild(sizeButton);
|
||||
diameterHint = contents.addChild(new MultiLineTextWidget(diameterHint(), font)
|
||||
.setMaxWidth(310).setCentered(true));
|
||||
if (generator.rootGeneration() == 17) {
|
||||
contents.addChild(CycleButton.builder((Boolean enabled) -> enabled ? CommonComponents.OPTION_ON : CommonComponents.OPTION_OFF, experimental)
|
||||
.withValues(true, false).create(0, 0, 280, 20, Component.translatable("sanctuary.world_options.experimental"),
|
||||
(button, value) -> experimental = value));
|
||||
contents.addChild(new MultiLineTextWidget(Component.translatable("sanctuary.world_options.experimental_hint"), font)
|
||||
.setMaxWidth(310).setCentered(true));
|
||||
}
|
||||
layout.addToContents(contents);
|
||||
|
||||
LinearLayout actions = LinearLayout.horizontal().spacing(8);
|
||||
@@ -65,7 +74,7 @@ public final class SanctuaryWorldOptionsScreen extends Screen {
|
||||
|
||||
private void applyAndClose() {
|
||||
parent.getUiState().updateDimensions((registries, dimensions) -> dimensions.replaceOverworldGenerator(
|
||||
registries, generator.withInitialDiameter(selectedSize.diameter)));
|
||||
registries, generator.withInitialDiameter(selectedSize.diameter).withExperimentalStructures(experimental)));
|
||||
onClose();
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ public class ExpansionChunkGenerator extends ChunkGenerator {
|
||||
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state, StructureManager structures,
|
||||
ChunkAccess chunk, StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
fr.koka.sanctuary.worldgen.city.LostCityStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
ExpansionStructures.generate(this, registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
|
||||
@@ -41,7 +41,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
if (!((spatialGeneration(generation) ? DIAMETERS : LEGACY_DIAMETERS).contains(diameter) || (id.equals("sanctuary") && (diameter == 724 || (generation == 13 || spatialGeneration(generation)) && diameter == 1024)))) throw new IllegalArgumentException("Unsupported nominal diameter");
|
||||
if (Math.abs((long) centerX) > 100_000 || Math.abs((long) centerZ) > 100_000 || (centerX & 15) != 0 || (centerZ & 15) != 0) throw new IllegalArgumentException("Invalid island centre");
|
||||
if (!List.of("reserved", "ready").contains(status) || (generation != GENERATION && !spatialGeneration(generation) && !(generation == 13 && id.equals("sanctuary")))) throw new IllegalArgumentException("Unsupported island state or generation");
|
||||
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15 or 16 expansion");
|
||||
if (climate.equals("lost_city") && (!spatialGeneration(generation) || id.equals("sanctuary"))) throw new IllegalArgumentException("Lost City requires a generation 14, 15, 16 or 17 expansion");
|
||||
if (reuseToken == null || !reuseToken.isEmpty() && (!spatialGeneration(generation) || id.equals("sanctuary") || !reuseToken.matches("[0-9a-f]{64}")))
|
||||
throw new IllegalArgumentException("Invalid void reuse certificate token");
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public record ExpansionIsland(String id, String parent, String direction, String
|
||||
return new ExpansionIsland("sanctuary", "sanctuary", "north", "temperate", diameter, 0, 0, seed, "natural", "ready", generation);
|
||||
}
|
||||
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16; }
|
||||
public static boolean spatialGeneration(int generation) { return generation == 14 || generation == 15 || generation == 16 || generation == 17; }
|
||||
public boolean origin() { return id.equals("sanctuary"); }
|
||||
public double referenceDiameter() { return origin() && diameter == 724 ? Math.sqrt(2) * 512 : 512; }
|
||||
public double horizontalScale() { return origin() ? 1.0 : diameter / referenceDiameter(); }
|
||||
|
||||
@@ -139,6 +139,9 @@ public final class ExpansionStructures {
|
||||
if (fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime.protects(binding.generator(), binding.random(),
|
||||
new BlockPos(centreX, total.minY(), centreZ), radius, total.getYSpan() + 16, 26)) return "protected-hydrology";
|
||||
}
|
||||
if (generator instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator city
|
||||
&& city.intersectsExperimentalStructures(total.minX() - 1, total.minY() - (temple ? 24 : 12), total.minZ() - 1,
|
||||
total.maxX() + 1, total.maxY() + 4, total.maxZ() + 1)) return "experimental-structure";
|
||||
if (!inside(island, total.minX(), total.minZ(), 4) || !inside(island, total.maxX(), total.minZ(), 4)
|
||||
|| !inside(island, total.minX(), total.maxZ(), 4) || !inside(island, total.maxX(), total.maxZ(), 4)) return "outside-island";
|
||||
if (total.minY() < chunk.getMinY() || total.maxY() > chunk.getMaxY() - 4) return "outside-height";
|
||||
|
||||
+3
-2
@@ -24,7 +24,8 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
var owner = generator.islandForChunk(center.x(), center.z());
|
||||
var destination = generator.islandForChunk(pos.getX() >> 4, pos.getZ() >> 4);
|
||||
if (owner == null || destination == null || !owner.id().equals(destination.id())
|
||||
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing() && generator.protectsCityDecoration(pos))
|
||||
|| !fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||
&& !fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.isPlacing() && generator.protectsCityDecoration(pos))
|
||||
callback.setReturnValue(false);
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ public abstract class UnifiedDecorationProtectionMixin {
|
||||
if (state.is(Blocks.SUGAR_CANE)) return;
|
||||
// Generation16 reserves its architectural volume before decoration. Only its own
|
||||
// native pieces may replace water there; published generations retain the old rule.
|
||||
if (generator.rootGeneration() == 16
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)
|
||||
&& fr.koka.sanctuary.worldgen.city.LostCityStructures.isPlacing()
|
||||
&& generator.protectsCityDecoration(pos)) return;
|
||||
if (generator.protectsDecoration(pos) && !region.getBlockState(pos).equals(state)) {
|
||||
|
||||
@@ -38,6 +38,10 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 16)));
|
||||
|
||||
public static final MapCodec<SanctuaryBiomeSource> RUINS17_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
Biome.CODEC.listOf().fieldOf("biomes").forGetter(source -> source.allBiomes)
|
||||
).apply(i, biomes -> new SanctuaryBiomeSource(biomes, 17)));
|
||||
|
||||
private final List<Holder<Biome>> allBiomes;
|
||||
private final PopulationIslandBiomeSource population;
|
||||
private final boolean cities;
|
||||
@@ -71,7 +75,7 @@ public final class SanctuaryBiomeSource extends ExpansionBiomeSource {
|
||||
cityPlans = java.util.Map.copyOf(plans);
|
||||
}
|
||||
|
||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
|
||||
@Override protected MapCodec<SanctuaryBiomeSource> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : cities ? CITY_CODEC : CODEC; }
|
||||
|
||||
@Override public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
BiomeResolver woodlands = population.createResolver(sampler);
|
||||
|
||||
+50
-11
@@ -39,6 +39,15 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter)
|
||||
).apply(i, (source, diameter) -> new SanctuaryChunkGenerator(source, diameter, 16)));
|
||||
|
||||
public static final MapCodec<SanctuaryChunkGenerator> RUINS17_CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
|
||||
SanctuaryBiomeSource.RUINS17_CODEC.fieldOf("biome_source").forGetter(SanctuaryChunkGenerator::source),
|
||||
DIAMETER_CODEC.optionalFieldOf("initial_diameter", 724).forGetter(SanctuaryChunkGenerator::initialDiameter),
|
||||
Codec.BOOL.optionalFieldOf("experimental_structures", true).forGetter(SanctuaryChunkGenerator::experimentalStructures)
|
||||
).apply(i, (source, diameter, experimental) -> new SanctuaryChunkGenerator(source, diameter, 17, experimental)));
|
||||
|
||||
private final boolean experimentalStructures;
|
||||
private volatile fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17 = fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.EMPTY;
|
||||
private boolean ruinsPrepared;
|
||||
private volatile Context originContext;
|
||||
private final int generation;
|
||||
private net.minecraft.world.level.LevelHeightAccessor heightAccessor;
|
||||
@@ -70,14 +79,26 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
this(source, initialDiameter, 13);
|
||||
}
|
||||
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation) {
|
||||
this(source, initialDiameter, generation, false);
|
||||
}
|
||||
private SanctuaryChunkGenerator(SanctuaryBiomeSource source, int initialDiameter, int generation, boolean experimental) {
|
||||
super(source, initialDiameter, true);
|
||||
this.experimentalStructures = generation == 17 && experimental;
|
||||
this.generation = generation;
|
||||
if (generation != source.generation()) throw new IllegalArgumentException("Sanctuary generator and biome source versions must agree");
|
||||
}
|
||||
@Override public SanctuaryBiomeSource source() { return (SanctuaryBiomeSource) super.source(); }
|
||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
|
||||
@Override protected MapCodec<SanctuaryChunkGenerator> codec() { return generation == 17 ? RUINS17_CODEC : generation == 16 ? CITY16_CODEC : generation == 15 ? CITY15_CODEC : generation == 14 ? CITY_CODEC : CODEC; }
|
||||
public SanctuaryChunkGenerator withInitialDiameter(int diameter) {
|
||||
return new SanctuaryChunkGenerator(source().copy(), diameter, generation);
|
||||
return new SanctuaryChunkGenerator(source().copy(), diameter, generation, experimentalStructures);
|
||||
}
|
||||
public boolean experimentalStructures() { return experimentalStructures; }
|
||||
public SanctuaryChunkGenerator withExperimentalStructures(boolean enabled) {
|
||||
return new SanctuaryChunkGenerator(source().copy(), initialDiameter(), generation, enabled);
|
||||
}
|
||||
public fr.koka.sanctuary.worldgen.ruins.RuinsPlan17 ruinsPlan17() { return ruinsPlan17; }
|
||||
public boolean intersectsExperimentalStructures(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return generation == 17 && ruinsPlan17.intersectsProtection(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
public IslandCapacity capacity() {
|
||||
return switch (initialDiameter()) {
|
||||
@@ -112,10 +133,12 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
// Publish one root context before any generation workers use it.
|
||||
context(islands.getFirst());
|
||||
prepareCities(islands);
|
||||
prepareRuins(islands);
|
||||
}
|
||||
@Override public void setIslands(List<ExpansionIsland> islands) {
|
||||
// Complete and publish city geometry before workers can observe the new owner list.
|
||||
prepareCities(islands);
|
||||
prepareRuins(islands);
|
||||
super.setIslands(islands);
|
||||
}
|
||||
private void prepareCities(List<ExpansionIsland> islands) {
|
||||
@@ -126,7 +149,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
return;
|
||||
}
|
||||
if (generation == 16 && !cityStructuresEnabled) {
|
||||
if ((generation == 16 || generation == 17) && !cityStructuresEnabled) {
|
||||
cityProtection16 = fr.koka.sanctuary.worldgen.city.LostCityProtection16.EMPTY;
|
||||
activeCityPlans = java.util.Map.of();
|
||||
source().setCityPlans(java.util.Map.of());
|
||||
@@ -142,7 +165,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
}
|
||||
cityProtection15 = protection.build();
|
||||
}
|
||||
if (generation == 16) {
|
||||
if (generation == 16 || generation == 17) {
|
||||
var protection = new fr.koka.sanctuary.worldgen.city.LostCityProtection16.Builder();
|
||||
for (var island : islands) {
|
||||
protection.add(published.get(island.id()), undergroundPlan16(island));
|
||||
@@ -156,6 +179,19 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
activeCityPlans = java.util.Map.copyOf(published);
|
||||
source().setCityPlans(published);
|
||||
}
|
||||
private void prepareRuins(List<ExpansionIsland> islands) {
|
||||
if (generation != 17 || !experimentalStructures || !cityStructuresEnabled || ruinsPrepared) return;
|
||||
var root = islands.stream().filter(ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var terrain = context(root);
|
||||
var sampled = new fr.koka.sanctuary.worldgen.ruins.RuinsTerrain17(root, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> protectsDecoration(pos) || cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ()));
|
||||
long start = System.nanoTime();
|
||||
var plan = fr.koka.sanctuary.worldgen.ruins.RuinsPlanner17.plan(root.seed(), root.centerX(), root.centerZ(), root.diameter(), sampled);
|
||||
ruinsPlan17 = plan;
|
||||
ruinsPrepared = true;
|
||||
SanctuaryMod.LOGGER.info("Experimental ruins17 seed {}: {} networks, {} probes, {} noise columns in {} ms",
|
||||
root.seed(), plan.networks().size(), plan.samples(), sampled.columnsUsed(), (System.nanoTime() - start) / 1_000_000);
|
||||
}
|
||||
private static ExpansionIsland cityIdentity(ExpansionIsland island) {
|
||||
// State and receipt do not change the terrain geometry used by a cached plan.
|
||||
var ready = island.status().equals("ready") ? island : island.ready();
|
||||
@@ -181,7 +217,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return cityPlans.computeIfAbsent(cityIdentity(island), ignored -> {
|
||||
long start = System.nanoTime();
|
||||
var terrain = context(island);
|
||||
if (generation == 16) {
|
||||
if (generation == 16 || generation == 17) {
|
||||
var sampled = new fr.koka.sanctuary.worldgen.city.LostCityTerrain16(island, terrain.generator(), terrain.random(),
|
||||
heightAccessor, pos -> island.origin() && protectsDecoration(pos));
|
||||
var placement = LostCityPlanner16.plan(island.seed(), island.centerX(), island.centerZ(),
|
||||
@@ -252,7 +288,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
});
|
||||
}
|
||||
public boolean protectsCityDecoration(net.minecraft.core.BlockPos pos) {
|
||||
if (generation == 16) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 16 || generation == 17) return cityProtection16.contains(pos.getX(), pos.getY(), pos.getZ())
|
||||
|| generation == 17 && ruinsPlan17.protects(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation == 15) return cityProtection15.contains(pos.getX(), pos.getY(), pos.getZ());
|
||||
if (generation != 14) return false;
|
||||
var island = islandAt(pos.getX(), pos.getZ());
|
||||
@@ -274,20 +311,20 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
return citySupports.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public boolean intersectsCityConstruction16(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
return generation == 16 && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
return (generation == 16 || generation == 17) && cityProtection16.intersects(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
public fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan undergroundPlan16(ExpansionIsland island) {
|
||||
if (generation != 16) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||
if (generation != 16 && generation != 17) return new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0);
|
||||
lostCityPlan(island);
|
||||
return undergroundPlans16.getOrDefault(cityIdentity(island), new fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Plan(List.of(), 0));
|
||||
}
|
||||
public List<LostCityPlanner16.Support> citySupports16(ExpansionIsland island) {
|
||||
if (generation != 16) return List.of();
|
||||
if (generation != 16 && generation != 17) return List.of();
|
||||
lostCityPlan(island);
|
||||
return citySupports16.getOrDefault(cityIdentity(island), List.of());
|
||||
}
|
||||
public LostCityPlanner16.Fallback cityReservation16(ExpansionIsland island) {
|
||||
if (generation != 16) return null;
|
||||
if (generation != 16 && generation != 17) return null;
|
||||
lostCityPlan(island);
|
||||
return cityReservations16.get(cityIdentity(island));
|
||||
}
|
||||
@@ -299,6 +336,8 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
net.minecraft.core.BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.city.LostCityStructures.KEY))
|
||||
return fr.koka.sanctuary.worldgen.city.LostCityStructures.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
if (generation == 17 && structures.size() == 1 && structures.get(0).is(fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.KEY))
|
||||
return fr.koka.sanctuary.worldgen.ruins.RuinsStructures17.findNearest(level, structures, origin, radius, skipReferenced);
|
||||
return super.findNearestMapStructure(level, structures, origin, radius, skipReferenced);
|
||||
}
|
||||
@Override protected Context context(ExpansionIsland island) {
|
||||
@@ -326,7 +365,7 @@ public final class SanctuaryChunkGenerator extends ExpansionChunkGenerator {
|
||||
@Override protected void afterTerrain(ExpansionIsland island, ChunkAccess chunk) {
|
||||
// Root water/lava are already applied by the Population hook on the named delegate.
|
||||
if (!island.origin()) super.afterTerrain(island, chunk);
|
||||
if (generation == 16 && cityStructuresEnabled) {
|
||||
if ((generation == 16 || generation == 17) && cityStructuresEnabled) {
|
||||
var reserved = cityReservations16.get(cityIdentity(island));
|
||||
if (reserved != null) {
|
||||
var mutable = new net.minecraft.core.BlockPos.MutableBlockPos();
|
||||
|
||||
@@ -29,17 +29,17 @@ public final class LostCityStructure extends Structure {
|
||||
throw new IllegalStateException("Lost City district exceeds native structure reference range");
|
||||
}
|
||||
return Optional.of(new GenerationStub(LostCityStructures.locator(district), pieces -> {
|
||||
if (generator.rootGeneration() == 16 && generator.cityReservation16(island) != null)
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) && generator.cityReservation16(island) != null)
|
||||
pieces.addPiece(new LostCityFoundationPiece16(generator.cityReservation16(island), district.seed()));
|
||||
if (generator.rootGeneration() == 15) for (var support : generator.citySupports(island)) {
|
||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||
pieces.addPiece(new LostCitySupportPiece15(support));
|
||||
}
|
||||
if (generator.rootGeneration() == 16) for (var support : generator.citySupports16(island)) {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var support : generator.citySupports16(island)) {
|
||||
if (support.startChunkX() == district.startChunkX() && support.startChunkZ() == district.startChunkZ())
|
||||
pieces.addPiece(new LostCitySupportPiece16(support));
|
||||
}
|
||||
district.parcels().forEach(parcel -> pieces.addPiece(generator.rootGeneration() == 16 ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
||||
district.parcels().forEach(parcel -> pieces.addPiece((generator.rootGeneration() == 16 || generator.rootGeneration() == 17) ? new LostCityPiece16(parcel) : generator.rootGeneration() == 15
|
||||
? new LostCityPiece15(parcel) : new LostCityPiece(parcel)));
|
||||
if (generator.rootGeneration() == 15) for (var network : generator.undergroundPlan(island).networks()) {
|
||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||
@@ -49,7 +49,7 @@ public final class LostCityStructure extends Structure {
|
||||
network.modules().stream().filter(m -> m.kind() == LostCityUnderground15.Kind.ACCESS)
|
||||
.forEach(m -> pieces.addPiece(new LostCityUndergroundPiece15(m)));
|
||||
}
|
||||
if (generator.rootGeneration() == 16) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||
if ((generator.rootGeneration() == 16 || generator.rootGeneration() == 17)) for (var network : generator.undergroundPlan16(island).networks()) {
|
||||
if (network.startChunkX() != district.startChunkX() || network.startChunkZ() != district.startChunkZ()) continue;
|
||||
// Surface comes first, rooms/corridors next, shafts last so every entrance remains open.
|
||||
network.modules().stream().filter(m -> m.kind() != LostCityUnderground16.Kind.ACCESS)
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.StructurePiece;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Saved generation17 room geometry is rendered without replanning or reading neighboring chunks. */
|
||||
public final class RuinsPiece17 extends StructurePiece {
|
||||
private static final Set<String> ROOM_KEYS = Set.of("Schema", "Kind", "MinX", "MinY", "MinZ", "Width",
|
||||
"Height", "Depth", "Openings", "RoomSeed", "Supports");
|
||||
private static final Set<String> SUPPORT_KEYS = Set.of("X", "MinY", "Z", "MaxY");
|
||||
private final RuinsPlan17.Room room;
|
||||
|
||||
public RuinsPiece17(RuinsPlan17.Room room) {
|
||||
super(RuinsStructures17.PIECE, 0, bounds(room));
|
||||
this.room = room;
|
||||
setOrientation(null);
|
||||
}
|
||||
public RuinsPiece17(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
super(RuinsStructures17.PIECE, tag);
|
||||
room = decodeRoom(tag.getCompound("SanctuaryRoom")
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing saved forgotten room")));
|
||||
if (!getBoundingBox().equals(bounds(room)))
|
||||
throw new IllegalArgumentException("Forgotten room disagrees with its saved native bounding box");
|
||||
setOrientation(null);
|
||||
}
|
||||
public RuinsPlan17.Room room() { return room; }
|
||||
@Override public BlockPos getLocatorPosition() { return locator(room); }
|
||||
public static BlockPos locator(RuinsPlan17.Room room) {
|
||||
return new BlockPos(room.minX() + room.width() / 2,
|
||||
room.kind() == RuinsPlan17.Kind.ACCESS ? room.minY() + room.height() - 2 : room.floorY() + 1,
|
||||
room.minZ() + room.depth() / 2);
|
||||
}
|
||||
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {
|
||||
tag.put("SanctuaryRoom", encodeRoom(room));
|
||||
}
|
||||
public static CompoundTag encodeRoom(RuinsPlan17.Room room) {
|
||||
var tag = new CompoundTag(); tag.putInt("Schema", 1); tag.putString("Kind", room.kind().name());
|
||||
tag.putInt("MinX", room.minX()); tag.putInt("MinY", room.minY()); tag.putInt("MinZ", room.minZ());
|
||||
tag.putInt("Width", room.width()); tag.putInt("Height", room.height()); tag.putInt("Depth", room.depth());
|
||||
tag.putInt("Openings", room.openings()); tag.putLong("RoomSeed", room.seed());
|
||||
var supports = new ListTag();
|
||||
for (var support : room.supports()) {
|
||||
var value = new CompoundTag(); value.putInt("X", support.x()); value.putInt("MinY", support.minY());
|
||||
value.putInt("Z", support.z()); value.putInt("MaxY", support.maxY()); supports.add(value);
|
||||
}
|
||||
tag.put("Supports", supports);
|
||||
return tag;
|
||||
}
|
||||
public static RuinsPlan17.Room decodeRoom(CompoundTag tag) {
|
||||
if (!tag.keySet().equals(ROOM_KEYS)) throw new IllegalArgumentException("Incomplete or unknown forgotten room fields");
|
||||
if (integer(tag, "Schema") != 1) throw new IllegalArgumentException("Unsupported forgotten room schema");
|
||||
var kind = RuinsPlan17.Kind.valueOf(tag.getString("Kind").orElseThrow(() -> new IllegalArgumentException("Missing room kind")));
|
||||
type(tag, "RoomSeed", Tag.TAG_LONG); type(tag, "Supports", Tag.TAG_LIST);
|
||||
var saved = tag.getList("Supports").orElseThrow();
|
||||
if (saved.size() > 64) throw new IllegalArgumentException("Too many saved forgotten room supports");
|
||||
var supports = new ArrayList<RuinsPlan17.Support>();
|
||||
for (var entry : saved) {
|
||||
if (!(entry instanceof CompoundTag value) || !value.keySet().equals(SUPPORT_KEYS))
|
||||
throw new IllegalArgumentException("Invalid forgotten room support fields");
|
||||
supports.add(new RuinsPlan17.Support(integer(value, "X"), integer(value, "MinY"), integer(value, "Z"), integer(value, "MaxY")));
|
||||
}
|
||||
return new RuinsPlan17.Room(kind, integer(tag, "MinX"), integer(tag, "MinY"), integer(tag, "MinZ"),
|
||||
integer(tag, "Width"), integer(tag, "Height"), integer(tag, "Depth"), integer(tag, "Openings"),
|
||||
tag.getLong("RoomSeed").orElseThrow(), supports);
|
||||
}
|
||||
private static int integer(CompoundTag tag, String key) {
|
||||
type(tag, key, Tag.TAG_INT);
|
||||
return tag.getInt(key).orElseThrow();
|
||||
}
|
||||
private static void type(CompoundTag tag, String key, byte id) {
|
||||
if (tag.get(key) == null || tag.get(key).getId() != id)
|
||||
throw new IllegalArgumentException("Invalid saved forgotten room field: " + key);
|
||||
}
|
||||
@Override public void postProcess(WorldGenLevel level, StructureManager manager, ChunkGenerator generator,
|
||||
RandomSource random, BoundingBox clip, ChunkPos chunk, BlockPos pivot) {
|
||||
RuinsStructures17.placing(() -> RuinsRenderer17.render(room, new LostCityRenderer.BlockWriter() {
|
||||
private BlockPos world(int x, int y, int z) { return new BlockPos(room.minX() + x, room.minY() + y, room.minZ() + z); }
|
||||
@Override public void set(int x, int y, int z, BlockState block) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
placeBlock(level, block, position.getX(), position.getY(), position.getZ(), clip);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> loot, long seed) {
|
||||
var position = world(x, y, z);
|
||||
if (getBoundingBox().isInside(position) && clip.isInside(position))
|
||||
createChest(level, clip, RandomSource.create(seed), position, loot, null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
public static BoundingBox bounds(RuinsPlan17.Room room) {
|
||||
int minY = room.minY(), maxY = Math.addExact(room.minY(), room.height() - 1);
|
||||
for (var support : room.supports()) {
|
||||
minY = Math.min(minY, support.minY()); maxY = Math.max(maxY, support.maxY());
|
||||
}
|
||||
return new BoundingBox(room.minX(), minY, room.minZ(), Math.addExact(room.minX(), room.width() - 1),
|
||||
maxY, Math.addExact(room.minZ(), room.depth() - 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/** Immutable generation17 experiment. Native pieces persist every room and its bounded footing. */
|
||||
public record RuinsPlan17(List<Network> networks, int samples) {
|
||||
public static final int MAX_SAMPLES = 250_000;
|
||||
public static final RuinsPlan17 EMPTY = new RuinsPlan17(List.of(), 0);
|
||||
public enum Family { MINE, STRANGE, INFRASTRUCTURE }
|
||||
public enum Kind { ACCESS, CORRIDOR, MINE_GALLERY, WORKSHOP, STORE,
|
||||
TILED_HALL, ARCHIVE, SERVICE_HALL, PUMP_HALL, CONTROL_ROOM }
|
||||
|
||||
/** One-block architectural pier. Its immediately lower block remains native solid rock. */
|
||||
public record Support(int x, int minY, int z, int maxY) {
|
||||
public Support {
|
||||
if (minY < 9 || maxY >= 380 || minY > maxY || maxY - minY >= 12)
|
||||
throw new IllegalArgumentException("Invalid generation17 ruins support");
|
||||
}
|
||||
}
|
||||
|
||||
/** Openings N=1,E=2,S=4,W=8: centered three wide, local y2..4. Floor is local y1.
|
||||
* Adjacent bodies never overlap, including their opened boundary walls. */
|
||||
public record Room(Kind kind, int minX, int minY, int minZ, int width, int height, int depth,
|
||||
int openings, long seed, List<Support> supports) {
|
||||
public Room {
|
||||
supports = List.copyOf(supports);
|
||||
if (kind == null || width < 5 || width > 17 || depth < 5 || depth > 17
|
||||
|| (width & 1) == 0 || (depth & 1) == 0 || height < 6 || height > 40
|
||||
|| minY < 10 || minY + height > 383 || openings < 0 || openings > 15 || supports.size() > 9)
|
||||
throw new IllegalArgumentException("Invalid generation17 ruins room");
|
||||
if (kind == Kind.ACCESS && (width != 5 || depth != 5))
|
||||
throw new IllegalArgumentException("Ruins access must be five by five");
|
||||
if (kind != Kind.ACCESS && kind != Kind.CORRIDOR && (width < 9 || depth < 9 || height < 7))
|
||||
throw new IllegalArgumentException("Thematic ruins rooms need space for their central passage and furnishings");
|
||||
var positions = new HashSet<Long>();
|
||||
for (var support : supports) if (support.x() < minX || support.x() >= minX + width
|
||||
|| support.z() < minZ || support.z() >= minZ + depth || support.maxY() != minY - 1
|
||||
|| !positions.add(key(support.x(), support.z())))
|
||||
throw new IllegalArgumentException("Ruins support does not meet its own room floor");
|
||||
}
|
||||
public int maxX() { return minX + width - 1; }
|
||||
public int maxY() { return minY + height - 1; }
|
||||
public int maxZ() { return minZ + depth - 1; }
|
||||
public int centerX() { return minX + width / 2; }
|
||||
public int centerZ() { return minZ + depth / 2; }
|
||||
public int floorY() { return minY + 1; }
|
||||
public int boundsMinY() { return supports.stream().mapToInt(Support::minY).min().orElse(minY); }
|
||||
public int boundsMaxY() { return maxY(); }
|
||||
public boolean contains(int x, int y, int z) {
|
||||
if (x < minX || x > maxX() || z < minZ || z > maxZ()) return false;
|
||||
if (y >= minY && y <= maxY()) return true;
|
||||
for (var support : supports) if (support.x() == x && support.z() == z && y >= support.minY() && y <= support.maxY()) return true;
|
||||
return false;
|
||||
}
|
||||
public boolean intersects(int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||
if (x0 > maxX() || x1 < minX || z0 > maxZ() || z1 < minZ) return false;
|
||||
if (y0 <= maxY() && y1 >= minY) return true;
|
||||
for (var support : supports) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
|
||||
&& support.minY() <= y1 && support.maxY() >= y0) return true;
|
||||
return false;
|
||||
}
|
||||
public Room withOpening(int direction) {
|
||||
if (direction < 0 || direction > 3) throw new IllegalArgumentException("Invalid opening direction");
|
||||
return new Room(kind, minX, minY, minZ, width, height, depth, openings | 1 << direction, seed, supports);
|
||||
}
|
||||
public Room withSupports(List<Support> values) {
|
||||
return new Room(kind, minX, minY, minZ, width, height, depth, openings, seed, values);
|
||||
}
|
||||
}
|
||||
|
||||
public record Network(String id, Family family, int startChunkX, int startChunkZ, List<Room> rooms) {
|
||||
public Network {
|
||||
rooms = List.copyOf(rooms);
|
||||
if (id == null || !id.matches("[a-z][a-z0-9_]{0,31}") || family == null || rooms.size() < 3 || rooms.size() > 6
|
||||
|| rooms.stream().filter(room -> room.kind() == Kind.ACCESS).count() != 1)
|
||||
throw new IllegalArgumentException("Invalid generation17 ruins network");
|
||||
for (var room : rooms) if (Math.abs((room.minX() >> 4) - startChunkX) > 7
|
||||
|| Math.abs((room.maxX() >> 4) - startChunkX) > 7
|
||||
|| Math.abs((room.minZ() >> 4) - startChunkZ) > 7 || Math.abs((room.maxZ() >> 4) - startChunkZ) > 7)
|
||||
throw new IllegalArgumentException("Ruins network exceeds native reference range");
|
||||
}
|
||||
}
|
||||
|
||||
public RuinsPlan17 {
|
||||
networks = List.copyOf(networks);
|
||||
if (networks.size() > 3 || samples < 0 || samples > MAX_SAMPLES)
|
||||
throw new IllegalArgumentException("Invalid generation17 ruins planning budget");
|
||||
var ids = new HashSet<String>();
|
||||
for (var network : networks) if (!ids.add(network.id())) throw new IllegalArgumentException("Duplicate ruins network id");
|
||||
}
|
||||
public boolean isEmpty() { return networks.isEmpty(); }
|
||||
public boolean contains(int x, int y, int z) {
|
||||
for (var network : networks) for (var room : network.rooms()) if (room.contains(x, y, z)) return true;
|
||||
return false;
|
||||
}
|
||||
public boolean intersects(int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||
if (x0 > x1 || y0 > y1 || z0 > z1) throw new IllegalArgumentException("Reversed ruins bounds");
|
||||
for (var network : networks) for (var room : network.rooms()) if (room.intersects(x0, y0, z0, x1, y1, z1)) return true;
|
||||
return false;
|
||||
}
|
||||
/** Construction plus the unpainted native rock which carries its floors and short piers. */
|
||||
public boolean protects(int x, int y, int z) {
|
||||
if (contains(x, y, z)) return true;
|
||||
for (var network : networks) for (var room : network.rooms()) {
|
||||
if (x >= room.minX() && x <= room.maxX() && z >= room.minZ() && z <= room.maxZ()
|
||||
&& y >= room.minY() - 2 && y < room.minY()) return true;
|
||||
for (var support : room.supports()) if (x == support.x() && z == support.z() && y == support.minY() - 1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public boolean intersectsProtection(int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||
if (intersects(x0, y0, z0, x1, y1, z1)) return true;
|
||||
for (var network : networks) for (var room : network.rooms()) {
|
||||
if (x0 <= room.maxX() && x1 >= room.minX() && z0 <= room.maxZ() && z1 >= room.minZ()
|
||||
&& y0 < room.minY() && y1 >= room.minY() - 2) return true;
|
||||
for (var support : room.supports()) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
|
||||
&& support.minY() - 1 >= y0 && support.minY() - 1 <= y1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Family;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Kind;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Network;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Room;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Support;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/** Opt-in generation17 prototype. Three independently budgeted families have discoverable
|
||||
* surface shafts and compact, connected interiors. No existing chunk or world state is read.
|
||||
* Families remain separate in this prototype; no speculative cross-network door is published. */
|
||||
public final class RuinsPlanner17 {
|
||||
public static final int MAX_COLUMNS = 5_000;
|
||||
public static final int MAX_SAMPLES = RuinsPlan17.MAX_SAMPLES;
|
||||
private static final int INVALID = Integer.MIN_VALUE;
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
private RuinsPlanner17() {}
|
||||
|
||||
public interface Terrain {
|
||||
boolean owns(int x, int z);
|
||||
boolean solid(int x, int y, int z);
|
||||
boolean protectedAt(int x, int y, int z);
|
||||
/** Highest dry solid block; -1 when the column has no accessible surface. */
|
||||
int surfaceY(int x, int z);
|
||||
}
|
||||
|
||||
public static RuinsPlan17 plan(long seed, int centerX, int centerZ, int diameter, Terrain terrain) {
|
||||
if (diameter < 64 || diameter > 1024) throw new IllegalArgumentException("Unsupported ruins island size");
|
||||
var probe = new Probe(terrain, centerX, centerZ, diameter);
|
||||
var networks = new ArrayList<Network>();
|
||||
for (var family : Family.values()) {
|
||||
int remaining = Family.values().length - family.ordinal() - 1;
|
||||
probe.columnLimit = MAX_COLUMNS - remaining * (MAX_COLUMNS / 3);
|
||||
probe.sampleLimit = MAX_SAMPLES - remaining * (MAX_SAMPLES / 3);
|
||||
Network best = null;
|
||||
try {
|
||||
search: for (int attempt = 0; attempt < 6; attempt++) {
|
||||
Site site = site(seed, family, attempt, centerX, centerZ, diameter, probe);
|
||||
if (site == null) continue;
|
||||
for (int drop : new int[] {14, 22, 30}) {
|
||||
Network candidate = network(seed, family, site, drop, networks, probe);
|
||||
if (candidate != null && (best == null || candidate.rooms().size() > best.rooms().size())) best = candidate;
|
||||
if (best != null && best.rooms().size() == 5) break search;
|
||||
}
|
||||
}
|
||||
} catch (BudgetExceeded exhausted) { probe.reject(family.name().toLowerCase(Locale.ROOT) + "-budget"); }
|
||||
if (best != null) networks.add(best);
|
||||
}
|
||||
if (Boolean.getBoolean("sanctuary.test.ruins17") || Boolean.getBoolean("sanctuary.ruins.plannerDiagnostics"))
|
||||
System.getLogger(RuinsPlanner17.class.getName()).log(System.Logger.Level.INFO,
|
||||
"Ruins17 seed=" + seed + " diameter=" + diameter + " networks="
|
||||
+ networks.stream().map(n -> n.family() + ":" + n.rooms().size()).toList()
|
||||
+ " columns=" + probe.columns.size() + " samples=" + probe.samples + " rejected=" + probe.rejections);
|
||||
return new RuinsPlan17(networks, probe.samples);
|
||||
}
|
||||
|
||||
private static Site site(long seed, Family family, int attempt, int cx, int cz, int diameter, Probe probe) {
|
||||
long salt = mix(seed ^ family.ordinal() * 0x9E3779B97F4A7C15L ^ attempt * 0xD1342543DE82EF95L ^ 0x1700L);
|
||||
double base = unit(mix(seed ^ 0x17BA5EL)) * Math.PI * 2 + family.ordinal() * Math.PI * 2 / 3;
|
||||
double angle = base + new double[] {0, .30, -.30, .48, -.48, .72}[attempt];
|
||||
double radius = Math.min(diameter * .27, 216) * (attempt == 3 || attempt == 4 ? .72 : 1);
|
||||
int x = cx + (int) Math.round(Math.cos(angle) * radius), z = cz + (int) Math.round(Math.sin(angle) * radius);
|
||||
int low = 384, high = -1;
|
||||
// Five coarse witnesses reject cliffs, water and unowned corners before a volume scan.
|
||||
for (int[] offset : new int[][] {{-2, -2}, {-2, 2}, {2, -2}, {2, 2}, {0, 0}}) {
|
||||
int px = x + offset[0], pz = z + offset[1], top = probe.surface(px, pz);
|
||||
if (top < 54 || top > 376 || !probe.solid(px, top, pz) || probe.protectedAt(px, top, pz)) {
|
||||
probe.reject("coarse-entrance"); return null;
|
||||
}
|
||||
low = Math.min(low, top); high = Math.max(high, top);
|
||||
}
|
||||
if (high - low > 4) { probe.reject("entrance-cliff"); return null; }
|
||||
int direction = Math.abs(x - cx) >= Math.abs(z - cz) ? x < cx ? 1 : 3 : z < cz ? 2 : 0;
|
||||
if ((salt & 1) != 0) direction = (direction + (attempt % 2 == 0 ? 1 : 3)) & 3;
|
||||
return new Site(x, z, high, direction, salt);
|
||||
}
|
||||
|
||||
private static Network network(long seed, Family family, Site site, int drop, List<Network> previous, Probe probe) {
|
||||
int top = site.top();
|
||||
for (int z = site.z() - 2; z <= site.z() + 2; z++) for (int x = site.x() - 2; x <= site.x() + 2; x++) {
|
||||
int height = probe.surface(x, z);
|
||||
if (height < site.top() - 4 || height > site.top() + 4) { probe.reject("entrance-surface"); return null; }
|
||||
top = Math.max(top, height);
|
||||
}
|
||||
int floor = top - drop;
|
||||
Room access = new Room(Kind.ACCESS, site.x() - 2, floor - 1, site.z() - 2,
|
||||
5, drop + 4, 5, 0, mix(seed ^ site.seed() ^ drop), List.of());
|
||||
var rooms = new ArrayList<Room>();
|
||||
access = certify(access, rooms, previous, probe);
|
||||
if (access == null) return null;
|
||||
rooms.add(access);
|
||||
if (!attach(rooms, 0, Kind.CORRIDOR, site.direction(), mix(site.seed() ^ drop ^ 0xC0L), previous, probe)) return null;
|
||||
Kind[] kinds = switch (family) {
|
||||
case MINE -> new Kind[] {Kind.MINE_GALLERY, Kind.WORKSHOP, Kind.STORE};
|
||||
case STRANGE -> new Kind[] {Kind.TILED_HALL, Kind.ARCHIVE, Kind.STORE};
|
||||
case INFRASTRUCTURE -> new Kind[] {Kind.SERVICE_HALL, Kind.PUMP_HALL, Kind.CONTROL_ROOM};
|
||||
};
|
||||
if (!attach(rooms, 1, kinds[0], site.direction(), mix(site.seed() ^ drop ^ 0xA1L), previous, probe)) return null;
|
||||
// Keep already connected rooms when an optional branch encounters protected terrain.
|
||||
int side = (site.direction() + ((site.seed() & 2) == 0 ? 1 : 3)) & 3;
|
||||
try {
|
||||
attach(rooms, 2, kinds[1], side, mix(site.seed() ^ drop ^ 0xB2L), previous, probe);
|
||||
attach(rooms, 2, kinds[2], site.direction(), mix(site.seed() ^ drop ^ 0xC3L), previous, probe);
|
||||
} catch (BudgetExceeded exhausted) { probe.reject("optional-branch-budget"); }
|
||||
return new Network(family.name().toLowerCase(Locale.ROOT), family, site.x() >> 4, site.z() >> 4, rooms);
|
||||
}
|
||||
|
||||
private static boolean attach(List<Room> rooms, int parentIndex, Kind kind, int direction, long seed,
|
||||
List<Network> previous, Probe probe) {
|
||||
Room parent = rooms.get(parentIndex);
|
||||
int width = kind == Kind.CORRIDOR ? 5 : kind == Kind.MINE_GALLERY ? 13 : 9;
|
||||
int depth = kind == Kind.CORRIDOR ? 5 : 9, height = kind == Kind.CORRIDOR ? 6 : 7;
|
||||
int x = parent.centerX() - width / 2, z = parent.centerZ() - depth / 2;
|
||||
if (direction == 0) z = parent.minZ() - depth;
|
||||
if (direction == 1) x = parent.maxX() + 1;
|
||||
if (direction == 2) z = parent.maxZ() + 1;
|
||||
if (direction == 3) x = parent.minX() - width;
|
||||
Room room = new Room(kind, x, parent.minY(), z, width, height, depth, 1 << ((direction + 2) & 3), seed, List.of());
|
||||
room = certify(room, rooms, previous, probe);
|
||||
if (room == null) return false;
|
||||
rooms.set(parentIndex, parent.withOpening(direction)); rooms.add(room);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Room certify(Room draft, List<Room> siblings, List<Network> previous, Probe probe) {
|
||||
for (Room room : siblings) if (conflicts(room, draft)) { probe.reject("sibling-overlap"); return null; }
|
||||
for (Network network : previous) for (Room room : network.rooms()) if (conflicts(room, draft)) {
|
||||
probe.reject("network-overlap"); return null;
|
||||
}
|
||||
for (int z = draft.minZ(); z <= draft.maxZ(); z++) for (int x = draft.minX(); x <= draft.maxX(); x++) {
|
||||
int top = probe.surface(x, z);
|
||||
if (top == INVALID || top < draft.minY() + 5 || draft.kind() != Kind.ACCESS && top < draft.maxY() + 1) {
|
||||
probe.reject("room-exposed"); return null;
|
||||
}
|
||||
for (int y = draft.minY(); y <= draft.maxY(); y++) if (probe.protectedAt(x, y, z)) {
|
||||
probe.reject("room-protected"); return null;
|
||||
}
|
||||
}
|
||||
var supports = new ArrayList<Support>();
|
||||
for (int z : new int[] {draft.minZ() + 1, draft.centerZ(), draft.maxZ() - 1})
|
||||
for (int x : new int[] {draft.minX() + 1, draft.centerX(), draft.maxX() - 1}) {
|
||||
int bottom = draft.minY() - 1;
|
||||
if (probe.solid(x, bottom, z) && probe.solid(x, bottom - 1, z)) {
|
||||
if (probe.protectedAt(x, bottom, z) || probe.protectedAt(x, bottom - 1, z)) { probe.reject("footing-protected"); return null; }
|
||||
continue;
|
||||
}
|
||||
int rock = -1;
|
||||
for (int y = bottom - 1; y >= Math.max(9, draft.minY() - 13); y--)
|
||||
if (probe.solid(x, y, z) && probe.solid(x, y - 1, z)) { rock = y; break; }
|
||||
if (rock < 0) { probe.reject("footing-void"); return null; }
|
||||
for (int y = rock; y <= bottom; y++) if (probe.protectedAt(x, y, z)) { probe.reject("pier-protected"); return null; }
|
||||
supports.add(new Support(x, rock + 1, z, bottom));
|
||||
}
|
||||
Room result = draft.withSupports(supports);
|
||||
for (Room sibling : siblings) if (conflicts(sibling, result)) { probe.reject("pier-sibling"); return null; }
|
||||
for (Network network : previous) for (Room room : network.rooms()) if (conflicts(room, result)) {
|
||||
probe.reject("pier-network"); return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean conflicts(Room a, Room b) {
|
||||
if (protectsBounds(a, b.minX(), b.minY(), b.minZ(), b.maxX(), b.maxY(), b.maxZ())
|
||||
|| protectsBounds(b, a.minX(), a.minY(), a.minZ(), a.maxX(), a.maxY(), a.maxZ())) return true;
|
||||
for (var support : b.supports()) if (protectsBounds(a, support.x(), support.minY(), support.z(), support.x(), support.maxY(), support.z())) return true;
|
||||
for (var support : a.supports()) if (protectsBounds(b, support.x(), support.minY(), support.z(), support.x(), support.maxY(), support.z())) return true;
|
||||
return false;
|
||||
}
|
||||
private static boolean protectsBounds(Room room, int x0, int y0, int z0, int x1, int y1, int z1) {
|
||||
if (room.intersects(x0, y0, z0, x1, y1, z1)) return true;
|
||||
if (x0 <= room.maxX() && x1 >= room.minX() && z0 <= room.maxZ() && z1 >= room.minZ()
|
||||
&& y0 < room.minY() && y1 >= room.minY() - 2) return true;
|
||||
for (var support : room.supports()) if (support.x() >= x0 && support.x() <= x1 && support.z() >= z0 && support.z() <= z1
|
||||
&& support.minY() - 1 >= y0 && support.minY() - 1 <= y1) return true;
|
||||
return false;
|
||||
}
|
||||
private record Site(int x, int z, int top, int direction, long seed) {}
|
||||
private static final class BudgetExceeded extends RuntimeException {}
|
||||
private static final class Probe {
|
||||
final Terrain terrain; final int centerX, centerZ; final double radius;
|
||||
final Map<Long, Integer> columns = new HashMap<>();
|
||||
final Map<String, Integer> rejections = new java.util.TreeMap<>();
|
||||
int samples, columnLimit, sampleLimit;
|
||||
Probe(Terrain terrain, int centerX, int centerZ, int diameter) {
|
||||
this.terrain = java.util.Objects.requireNonNull(terrain); this.centerX = centerX; this.centerZ = centerZ;
|
||||
radius = diameter / 2.0 - 3;
|
||||
}
|
||||
int surface(int x, int z) {
|
||||
if (Math.hypot((double) x - centerX, (double) z - centerZ) > radius || !terrain.owns(x, z)) return INVALID;
|
||||
long key = (long) x << 32 ^ z & 0xffffffffL;
|
||||
var cached = columns.get(key); if (cached != null) return cached;
|
||||
if (columns.size() >= columnLimit) throw new BudgetExceeded();
|
||||
count(); int top = terrain.surfaceY(x, z); columns.put(key, top); return top;
|
||||
}
|
||||
boolean solid(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384 || surface(x, z) == INVALID) return false;
|
||||
count(); return terrain.solid(x, y, z);
|
||||
}
|
||||
boolean protectedAt(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384 || surface(x, z) == INVALID) return true;
|
||||
count(); return terrain.protectedAt(x, y, z);
|
||||
}
|
||||
void count() { if (samples >= sampleLimit) throw new BudgetExceeded(); samples++; }
|
||||
void reject(String reason) { rejections.merge(reason, 1, Integer::sum); }
|
||||
}
|
||||
private static double unit(long value) { return (value >>> 11) * 0x1.0p-53; }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ value >>> 27) * 0x94d049bb133111ebL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityPalette;
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.AttachFace;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
|
||||
/** Generation17 experiments: original, physical rooms reconstructed solely from their saved inputs.
|
||||
* Coordinates given to the writer are local to the saved room. There are no terrain reads, entities,
|
||||
* active machines or free fluids. Openings are restored last so connected pieces share dry thresholds. */
|
||||
public final class RuinsRenderer17 {
|
||||
private static final Direction[] DIRECTIONS = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST};
|
||||
private RuinsRenderer17() {}
|
||||
|
||||
/** Local standing positions for native path checks, including approaches to useful fixtures. */
|
||||
public record Target(String name, int x, int y, int z) {}
|
||||
|
||||
public static List<Target> targets(RuinsPlan17.Room room) {
|
||||
var targets = new ArrayList<Target>();
|
||||
if (room.kind() == RuinsPlan17.Kind.ACCESS) {
|
||||
targets.add(new Target("surface_hatch_approach", room.width() / 2, room.height() - 2, room.depth() / 2));
|
||||
targets.add(new Target("shaft_bottom", room.width() / 2, 2, room.depth() / 2));
|
||||
} else {
|
||||
targets.add(new Target("central_crossing", room.width() / 2, 2, room.depth() / 2));
|
||||
switch (room.kind()) {
|
||||
case WORKSHOP -> {
|
||||
targets.add(decorTarget(room, "workbench", 2, 2, 2));
|
||||
targets.add(decorTarget(room, "unfinished_work_platform", room.width() - 3, 4, room.depth() - 3));
|
||||
}
|
||||
case STORE -> targets.add(decorTarget(room, "supply_chest", room.width() - 3, 2, room.depth() - 2));
|
||||
case ARCHIVE -> targets.add(decorTarget(room, "reading_desk", 2, 2, 2));
|
||||
case PUMP_HALL, CONTROL_ROOM -> targets.add(decorTarget(room, "controls", 2, 2, 2));
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0) {
|
||||
int x = direction == 1 ? room.width() - 1 : direction == 3 ? 0 : room.width() / 2;
|
||||
int z = direction == 2 ? room.depth() - 1 : direction == 0 ? 0 : room.depth() / 2;
|
||||
targets.add(new Target("opening_" + DIRECTIONS[direction].getName(), x, 2, z));
|
||||
}
|
||||
return List.copyOf(targets);
|
||||
}
|
||||
|
||||
public static void render(RuinsPlan17.Room room, LostCityRenderer.BlockWriter output) {
|
||||
var b = new Builder(room, output);
|
||||
b.shell();
|
||||
switch (room.kind()) {
|
||||
case ACCESS -> b.access();
|
||||
case CORRIDOR -> b.corridor();
|
||||
case MINE_GALLERY -> b.mine();
|
||||
case WORKSHOP -> b.workshop();
|
||||
case STORE -> b.store();
|
||||
case TILED_HALL -> b.tiledHall();
|
||||
case ARCHIVE -> b.archive();
|
||||
case SERVICE_HALL -> b.service();
|
||||
case PUMP_HALL -> b.pumps();
|
||||
case CONTROL_ROOM -> b.controls();
|
||||
}
|
||||
if (room.kind() != RuinsPlan17.Kind.ACCESS) b.lights();
|
||||
b.openings();
|
||||
for (var support : room.supports()) for (int y = support.minY(); y <= support.maxY(); y++)
|
||||
output.set(support.x() - room.minX(), y - room.minY(), support.z() - room.minZ(),
|
||||
y == support.minY() ? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.STONE_BRICKS.defaultBlockState());
|
||||
}
|
||||
|
||||
private static boolean turn(RuinsPlan17.Room room) { return (room.seed() & 1L) != 0; }
|
||||
private static Target decorTarget(RuinsPlan17.Room room, String name, int x, int y, int z) {
|
||||
return new Target(name, turn(room) ? room.width() - 1 - x : x, y, turn(room) ? room.depth() - 1 - z : z);
|
||||
}
|
||||
|
||||
private static final class Builder {
|
||||
final RuinsPlan17.Room room;
|
||||
final LostCityRenderer.BlockWriter out;
|
||||
final int w, h, d, cx, cz;
|
||||
final boolean reverse;
|
||||
Builder(RuinsPlan17.Room room, LostCityRenderer.BlockWriter out) {
|
||||
this.room = room; this.out = out; w = room.width(); h = room.height(); d = room.depth();
|
||||
cx = w / 2; cz = d / 2; reverse = turn(room);
|
||||
}
|
||||
void set(int x, int y, int z, BlockState state) { out.set(x, y, z, state); }
|
||||
void set(int x, int y, int z, net.minecraft.world.level.block.Block block) { set(x, y, z, block.defaultBlockState()); }
|
||||
void decor(int x, int y, int z, BlockState state) {
|
||||
out.set(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z,
|
||||
reverse ? state.rotate(Rotation.CLOCKWISE_180) : state);
|
||||
}
|
||||
void decor(int x, int y, int z, net.minecraft.world.level.block.Block block) { decor(x, y, z, block.defaultBlockState()); }
|
||||
boolean crossing(int x, int z) { return Math.abs(x - cx) <= 1 || Math.abs(z - cz) <= 1; }
|
||||
|
||||
void shell() {
|
||||
for (int y = 0; y < h; y++) for (int z = 0; z < d; z++) for (int x = 0; x < w; x++) {
|
||||
BlockState state;
|
||||
boolean wall = x == 0 || x == w - 1 || z == 0 || z == d - 1;
|
||||
if (y == 0) state = Blocks.STONE.defaultBlockState();
|
||||
else if (y == 1) state = floor(x, z);
|
||||
else if (wall || y == h - 1) state = LostCityPalette.masonry(room.seed(), x, y, z);
|
||||
else state = Blocks.AIR.defaultBlockState();
|
||||
set(x, y, z, state);
|
||||
}
|
||||
// A continuous ceiling cornice and stone thresholds remain intact despite the weathering.
|
||||
for (int x = 0; x < w; x++) for (int z = 0; z < d; z++) if (x == 0 || x == w - 1 || z == 0 || z == d - 1)
|
||||
set(x, h - 2, z, Blocks.SMOOTH_STONE);
|
||||
}
|
||||
BlockState floor(int x, int z) {
|
||||
return switch (room.kind()) {
|
||||
case MINE_GALLERY -> (x == cx ? Blocks.DARK_OAK_PLANKS : (x + z) % 5 == 0 ? Blocks.ANDESITE : Blocks.STONE).defaultBlockState();
|
||||
case WORKSHOP, STORE -> (x == 1 || x == w - 2 || z == 1 || z == d - 2
|
||||
? Blocks.DARK_OAK_PLANKS : Blocks.SMOOTH_STONE).defaultBlockState();
|
||||
case TILED_HALL, ARCHIVE -> ((x / 2 + z / 2) % 2 == 0 ? Blocks.CALCITE : Blocks.POLISHED_ANDESITE).defaultBlockState();
|
||||
case SERVICE_HALL, PUMP_HALL, CONTROL_ROOM -> (x == cx || z == cz ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE).defaultBlockState();
|
||||
default -> LostCityPalette.pavement(room.seed(), x, z);
|
||||
};
|
||||
}
|
||||
|
||||
void access() {
|
||||
int exit = Integer.numberOfTrailingZeros(room.openings() == 0 ? 1 : room.openings());
|
||||
int wall = (exit + 2) & 3;
|
||||
int x = wall == 1 ? w - 2 : wall == 3 ? 1 : cx;
|
||||
int z = wall == 2 ? d - 2 : wall == 0 ? 1 : cz;
|
||||
Direction facing = DIRECTIONS[wall].getOpposite();
|
||||
if ((room.openings() & 1 << wall) != 0) {
|
||||
// A branching shaft cannot attach its lowest ladder to another open doorway.
|
||||
// This corner post leaves all three doorway cells and the central crossing usable.
|
||||
x = 1; z = 2; facing = Direction.SOUTH;
|
||||
for (int y = 2; y <= h - 3; y++) set(1, y, 1, Blocks.STONE_BRICKS);
|
||||
}
|
||||
BlockState ladder = Blocks.LADDER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, facing);
|
||||
for (int y = 2; y <= h - 3; y++) set(x, y, z, ladder);
|
||||
for (int zz = 0; zz < d; zz++) for (int xx = 0; xx < w; xx++) {
|
||||
set(xx, h - 3, zz, xx == x && zz == z ? ladder : Blocks.SMOOTH_STONE.defaultBlockState());
|
||||
set(xx, h - 2, zz, Blocks.AIR); set(xx, h - 1, zz, Blocks.AIR);
|
||||
}
|
||||
set(x, h - 2, z, Blocks.DARK_OAK_TRAPDOOR.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, facing));
|
||||
}
|
||||
void corridor() {
|
||||
boolean alongX = w > d;
|
||||
int length = alongX ? w : d;
|
||||
for (int n = 2; n < length - 1; n += 4) {
|
||||
if (alongX) {
|
||||
for (int y = 2; y <= h - 1; y++) { set(n, y, 0, Blocks.POLISHED_ANDESITE); set(n, y, d - 1, Blocks.POLISHED_ANDESITE); }
|
||||
} else for (int y = 2; y <= h - 1; y++) { set(0, y, n, Blocks.POLISHED_ANDESITE); set(w - 1, y, n, Blocks.POLISHED_ANDESITE); }
|
||||
}
|
||||
}
|
||||
void mine() {
|
||||
for (int z : new int[] {2, d - 3}) {
|
||||
for (int x : new int[] {2, w - 3}) for (int y = 2; y <= h - 2; y++) decor(x, y, z, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int x = 2; x <= w - 3; x++) decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState()
|
||||
.setValue(BlockStateProperties.AXIS, Direction.Axis.X));
|
||||
}
|
||||
for (int z = 1; z < d - 1; z++) set(cx, 2, z, Blocks.RAIL);
|
||||
// A short second siding ends at a buffer; the main track and cross passage remain open.
|
||||
if (w >= 13) {
|
||||
for (int z = 3; z < d - 2; z++) decor(w - 5, 2, z, Blocks.RAIL);
|
||||
decor(w - 5, 2, d - 2, Blocks.DARK_OAK_FENCE);
|
||||
}
|
||||
decor(1, 2, 1, Blocks.BARREL);
|
||||
decor(w - 2, 2, d - 2, Blocks.ANDESITE);
|
||||
if (LostCityPalette.roll(room.seed(), 1, 0, 0, 2) == 0) decor(w - 2, 3, d - 2, Blocks.COBBLESTONE);
|
||||
}
|
||||
void workshop() {
|
||||
decor(1, 2, 1, Blocks.CRAFTING_TABLE);
|
||||
decor(2, 2, 1, Blocks.FURNACE.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
decor(1, 2, d - 3, Blocks.GRINDSTONE.defaultBlockState()
|
||||
.setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
|
||||
decor(w - 2, 2, 1, Blocks.STONECUTTER.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
|
||||
decor(1, 2, d - 2, Blocks.CAULDRON);
|
||||
decor(2, 2, d - 2, Blocks.ANVIL);
|
||||
// Two grounded posts and a small planked deck preserve the trace of unfinished repairs.
|
||||
for (int x = w - 3; x <= w - 2; x++) {
|
||||
decor(x, 2, d - 3, Blocks.STRIPPED_OAK_LOG);
|
||||
for (int z = d - 3; z <= d - 2; z++) decor(x, 3, z, Blocks.DARK_OAK_PLANKS);
|
||||
}
|
||||
for (int y = 2; y <= 3; y++) decor(w - 3, y, d - 4, Blocks.LADDER.defaultBlockState()
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.NORTH));
|
||||
decor(w - 2, 2, d - 2, Blocks.BARREL);
|
||||
}
|
||||
void store() {
|
||||
for (int z = 1; z < d - 1; z++) if (!crossing(1, z)) {
|
||||
decor(1, 2, z, Blocks.BARREL);
|
||||
if ((z & 1) == 0) decor(1, 3, z, Blocks.BARREL);
|
||||
decor(1, 4, z, Blocks.DARK_OAK_SLAB);
|
||||
}
|
||||
for (int x = cx + 2; x <= w - 2; x++) decor(x, 2, 1, Blocks.DARK_OAK_PLANKS);
|
||||
decor(w - 2, 3, 1, Blocks.BARREL);
|
||||
loot(w - 2, 2, d - 2, BuiltInLootTables.VILLAGE_PLAINS_HOUSE);
|
||||
}
|
||||
void tiledHall() {
|
||||
// Repeated columns are interrupted by complete, centered cross passages.
|
||||
for (int x : new int[] {2, w - 3}) for (int z : new int[] {2, d - 3})
|
||||
for (int y = 2; y < h - 1; y++) decor(x, y, z, y == 2 || y == h - 2 ? Blocks.SMOOTH_STONE : Blocks.CALCITE);
|
||||
if (w >= 13) for (int x : new int[] {3, w - 4}) for (int z = 1; z <= d - 2; z++)
|
||||
if (Math.abs(z - cz) > 1) for (int y = 2; y < h - 1; y++)
|
||||
decor(x, y, z, y == h - 2 ? Blocks.POLISHED_ANDESITE : Blocks.SMOOTH_STONE);
|
||||
for (int x = cx - 1; x <= cx + 1; x++) {
|
||||
decor(x, h - 2, 1, Blocks.POLISHED_ANDESITE);
|
||||
decor(x, h - 2, d - 2, Blocks.POLISHED_ANDESITE);
|
||||
}
|
||||
// Mismatched surviving benches give these physically reachable side niches a former use.
|
||||
decor(1, 2, 1, seat(Direction.WEST));
|
||||
decor(w - 2, 2, 1, seat(Direction.EAST));
|
||||
}
|
||||
void archive() {
|
||||
for (int z = 1; z <= d - 2; z++) if (Math.abs(z - cz) > 1)
|
||||
for (int y = 2; y <= 4; y++) decor(w - 2, y, z,
|
||||
y == 4 && (z & 1) == 0 ? Blocks.DARK_OAK_PLANKS : Blocks.BOOKSHELF);
|
||||
decor(1, 2, 1, Blocks.BOOKSHELF);
|
||||
decor(1, 2, 2, Blocks.LECTERN.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.EAST));
|
||||
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
|
||||
decor(2, 2, d - 3, Blocks.BARREL);
|
||||
loot(1, 2, d - 3, BuiltInLootTables.VILLAGE_CARTOGRAPHER);
|
||||
}
|
||||
void service() {
|
||||
for (int z = 1; z < d - 1; z++) if (Math.abs(z - cz) > 1) {
|
||||
decor(1, 2, z, Blocks.POLISHED_ANDESITE);
|
||||
decor(1, 3, z, Blocks.IRON_BARS);
|
||||
}
|
||||
for (int z : new int[] {1, d - 2}) for (int x = 1; x < w - 1; x++)
|
||||
decor(x, h - 2, z, Blocks.STRIPPED_OAK_LOG.defaultBlockState().setValue(BlockStateProperties.AXIS, Direction.Axis.X));
|
||||
decor(w - 2, 2, 1, Blocks.BARREL);
|
||||
decor(w - 2, 2, d - 2, Blocks.CRAFTING_TABLE);
|
||||
decor(2, 2, d - 2, seat(Direction.SOUTH));
|
||||
}
|
||||
void pumps() {
|
||||
for (int z : new int[] {1, d - 2}) {
|
||||
decor(1, 2, z, Blocks.DISPENSER.defaultBlockState().setValue(BlockStateProperties.FACING, Direction.EAST));
|
||||
decor(2, 2, z, Blocks.WATER_CAULDRON.defaultBlockState().setValue(BlockStateProperties.LEVEL_CAULDRON, 3));
|
||||
decor(1, 3, z, Blocks.POLISHED_ANDESITE);
|
||||
decor(1, 4, z, Blocks.IRON_BARS);
|
||||
decor(w - 2, 2, z, Blocks.CAULDRON);
|
||||
}
|
||||
for (int z = 1; z <= d - 2; z++) decor(1, h - 2, z, Blocks.POLISHED_ANDESITE);
|
||||
decor(w - 2, 2, 2, Blocks.SMOOTH_STONE);
|
||||
decor(w - 2, 3, 2, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.WEST));
|
||||
}
|
||||
void controls() {
|
||||
for (int x = 1; x <= cx - 2; x++) {
|
||||
decor(x, 2, 1, Blocks.SMOOTH_STONE);
|
||||
decor(x, 3, 1, Blocks.LEVER.defaultBlockState().setValue(BlockStateProperties.ATTACH_FACE, AttachFace.FLOOR)
|
||||
.setValue(BlockStateProperties.HORIZONTAL_FACING, Direction.SOUTH));
|
||||
decor(x, 3, 0, Blocks.REDSTONE_LAMP);
|
||||
}
|
||||
decor(1, 2, 2, seat(Direction.WEST));
|
||||
decor(w - 2, 2, 1, Blocks.CRAFTING_TABLE);
|
||||
decor(w - 2, 2, d - 2, Blocks.BARREL);
|
||||
// An inspection window remains backed by a complete wall: there is no opening into unverified rock or water.
|
||||
for (int z = cz + 2; z < d - 1; z++) decor(0, 3, z, Blocks.GLASS);
|
||||
decor(1, 2, d - 2, Blocks.CARTOGRAPHY_TABLE);
|
||||
}
|
||||
BlockState seat(Direction back) { return Blocks.DARK_OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING, back); }
|
||||
void lights() {
|
||||
// Every lantern hangs directly from the full masonry ceiling; never from a bottom slab.
|
||||
for (int[] p : new int[][] {{1, 1}, {w - 2, d - 2}})
|
||||
set(p[0], h - 2, p[1], Blocks.LANTERN.defaultBlockState().setValue(BlockStateProperties.HANGING, true));
|
||||
}
|
||||
void openings() {
|
||||
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0)
|
||||
for (int side = -1; side <= 1; side++) {
|
||||
int x = direction == 1 ? w - 1 : direction == 3 ? 0 : cx + side;
|
||||
int z = direction == 2 ? d - 1 : direction == 0 ? 0 : cz + side;
|
||||
set(x, 1, z, Blocks.SMOOTH_STONE);
|
||||
for (int y = 2; y <= 4; y++) set(x, y, z, Blocks.AIR);
|
||||
}
|
||||
}
|
||||
void loot(int x, int y, int z, net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> table) {
|
||||
out.chest(reverse ? w - 1 - x : x, y, reverse ? d - 1 - z : z, table, room.seed() ^ 0x17A6C41L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
|
||||
/** One start contains every planned experimental network sharing its origin chunk. */
|
||||
public final class RuinsStructure17 extends Structure {
|
||||
public static final MapCodec<RuinsStructure17> CODEC = simpleCodec(RuinsStructure17::new);
|
||||
public RuinsStructure17(StructureSettings settings) { super(settings); }
|
||||
|
||||
@Override protected Optional<GenerationStub> findGenerationPoint(GenerationContext context) {
|
||||
if (!(context.chunkGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()) return Optional.empty();
|
||||
var island = generator.islandForChunk(context.chunkPos().x(), context.chunkPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != 17) return Optional.empty();
|
||||
var networks = generator.ruinsPlan17().networks().stream()
|
||||
.filter(n -> n.startChunkX() == context.chunkPos().x() && n.startChunkZ() == context.chunkPos().z()).toList();
|
||||
if (networks.isEmpty()) return Optional.empty();
|
||||
for (var network : networks) for (var room : network.rooms()) {
|
||||
var bounds = RuinsPiece17.bounds(room);
|
||||
if (Math.abs((long) Math.floorDiv(bounds.minX(), 16) - network.startChunkX()) > 8
|
||||
|| Math.abs((long) Math.floorDiv(bounds.maxX(), 16) - network.startChunkX()) > 8
|
||||
|| Math.abs((long) Math.floorDiv(bounds.minZ(), 16) - network.startChunkZ()) > 8
|
||||
|| Math.abs((long) Math.floorDiv(bounds.maxZ(), 16) - network.startChunkZ()) > 8)
|
||||
throw new IllegalStateException("Forgotten network exceeds native reference range: " + network.id());
|
||||
}
|
||||
return Optional.of(new GenerationStub(RuinsStructures17.locator(networks.getFirst()), pieces -> {
|
||||
// Shafts come last so their surface hatches cannot be filled by a connected room.
|
||||
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() != RuinsPlan17.Kind.ACCESS)
|
||||
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
|
||||
networks.forEach(n -> n.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS)
|
||||
.forEach(r -> pieces.addPiece(new RuinsPiece17(r))));
|
||||
}));
|
||||
}
|
||||
@Override public StructureType<?> type() { return RuinsStructures17.TYPE; }
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.expansion.ExpansionChunkGenerator;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureType;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceType;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
/** Experimental generation17 uses independent native starts, references and saved room pieces. */
|
||||
public final class RuinsStructures17 {
|
||||
public static final ResourceKey<Structure> KEY = ResourceKey.create(Registries.STRUCTURE, SanctuaryMod.id("forgotten_networks"));
|
||||
public static final StructureType<RuinsStructure17> TYPE = Registry.register(BuiltInRegistries.STRUCTURE_TYPE,
|
||||
SanctuaryMod.id("forgotten_networks_v17"), () -> RuinsStructure17.CODEC);
|
||||
public static final StructurePieceType PIECE = Registry.register(BuiltInRegistries.STRUCTURE_PIECE,
|
||||
SanctuaryMod.id("forgotten_room_v17"), RuinsPiece17::new);
|
||||
private static final ThreadLocal<Integer> PLACEMENT_DEPTH = ThreadLocal.withInitial(() -> 0);
|
||||
|
||||
private RuinsStructures17() {}
|
||||
public static void register() { /* Initialize serializers before the dynamic structure registry is read. */ }
|
||||
public static boolean isPlacing() { return PLACEMENT_DEPTH.get() > 0; }
|
||||
static void placing(Runnable renderer) {
|
||||
int previous = PLACEMENT_DEPTH.get();
|
||||
PLACEMENT_DEPTH.set(previous + 1);
|
||||
try { renderer.run(); }
|
||||
finally {
|
||||
if (previous == 0) PLACEMENT_DEPTH.remove();
|
||||
else PLACEMENT_DEPTH.set(previous);
|
||||
}
|
||||
}
|
||||
public static void generate(ExpansionChunkGenerator generator, RegistryAccess registries,
|
||||
ChunkGeneratorStructureState state, StructureManager manager, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> level) {
|
||||
if (!Level.OVERWORLD.equals(level) || !manager.shouldGenerateStructures()
|
||||
|| !(generator instanceof SanctuaryChunkGenerator sanctuary) || generator.rootGeneration() != 17
|
||||
|| !sanctuary.experimentalStructures()) return;
|
||||
var island = generator.islandForChunk(chunk.getPos().x(), chunk.getPos().z());
|
||||
if (island == null || !island.origin() || island.generation() != 17) return;
|
||||
var plan = sanctuary.ruinsPlan17();
|
||||
if (plan.networks().stream().noneMatch(n -> n.startChunkX() == chunk.getPos().x()
|
||||
&& n.startChunkZ() == chunk.getPos().z())) return;
|
||||
Holder<Structure> holder = registries.lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
Structure structure = holder.value();
|
||||
StructureStart previous = manager.getStartForStructure(structure, chunk);
|
||||
if (previous != null && previous.isValid()) return;
|
||||
var random = generator.randomState(island);
|
||||
var sampler = random.createClimateSampler(SamplerContext.builder().enableCaches().build());
|
||||
var start = structure.generate(holder, level, registries, generator, generator.getBiomeSource(), sampler,
|
||||
random, templates, island.seed(), chunk.getPos(), previous == null ? 0 : previous.getReferences(),
|
||||
chunk, structure.biomes()::contains);
|
||||
if (start.isValid()) manager.setStartForStructure(structure, start, chunk);
|
||||
}
|
||||
/** Starts are loaded at STRUCTURE_STARTS; locating never creates an expansion or rebuilds an existing start. */
|
||||
public static Pair<BlockPos, Holder<Structure>> findNearest(ServerLevel level, HolderSet<Structure> structures,
|
||||
BlockPos origin, int radius, boolean skipReferenced) {
|
||||
if (!Level.OVERWORLD.equals(level.dimension())
|
||||
|| !(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator generator)
|
||||
|| generator.rootGeneration() != 17 || !generator.experimentalStructures()
|
||||
|| !level.structureManager().shouldGenerateStructures()) return null;
|
||||
Holder<Structure> holder = level.registryAccess().lookupOrThrow(Registries.STRUCTURE).getOrThrow(KEY);
|
||||
if (!structures.contains(holder)) return null;
|
||||
var candidates = new ArrayList<RuinsPlan17.Network>();
|
||||
int originChunkX = origin.getX() >> 4, originChunkZ = origin.getZ() >> 4;
|
||||
for (var network : generator.ruinsPlan17().networks()) {
|
||||
var island = generator.islandForChunk(network.startChunkX(), network.startChunkZ());
|
||||
if (island == null || !island.origin() || island.generation() != 17 || !island.status().equals("ready")) continue;
|
||||
if (Math.abs((long) network.startChunkX() - originChunkX) <= radius
|
||||
&& Math.abs((long) network.startChunkZ() - originChunkZ) <= radius) candidates.add(network);
|
||||
}
|
||||
candidates.sort(Comparator.comparingDouble(n -> origin.distSqr(locator(n))));
|
||||
var visited = new HashSet<Long>();
|
||||
for (var network : candidates) {
|
||||
long key = ((long) network.startChunkX() << 32) ^ (network.startChunkZ() & 0xffffffffL);
|
||||
if (!visited.add(key)) continue;
|
||||
var chunk = level.getChunk(network.startChunkX(), network.startChunkZ(), ChunkStatus.STRUCTURE_STARTS, true);
|
||||
StructureStart start = level.structureManager().getStartForStructure(holder.value(), chunk);
|
||||
if (start == null || !start.isValid() || skipReferenced && !start.canBeReferenced()) continue;
|
||||
if (skipReferenced) level.structureManager().addReference(start);
|
||||
var entrance = start.getPieces().stream().filter(p -> p instanceof RuinsPiece17 room
|
||||
&& room.room().kind() == RuinsPlan17.Kind.ACCESS).findFirst().orElse(start.getPieces().getFirst());
|
||||
return Pair.of(entrance.getLocatorPosition(), holder);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static BlockPos locator(RuinsPlan17.Network network) {
|
||||
var room = network.rooms().stream().filter(r -> r.kind() == RuinsPlan17.Kind.ACCESS).findFirst()
|
||||
.orElseGet(() -> network.rooms().getFirst());
|
||||
return RuinsPiece17.locator(room);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
|
||||
/** Read-only, bounded native-noise cache. Unknown columns always reject construction. */
|
||||
public final class RuinsTerrain17 implements RuinsPlanner17.Terrain {
|
||||
public static final int MAX_COLUMNS = 5_000;
|
||||
private final ExpansionIsland island;
|
||||
private final NoiseBasedChunkGenerator generator;
|
||||
private final RandomState random;
|
||||
private final LevelHeightAccessor height;
|
||||
private final Predicate<BlockPos> protection;
|
||||
private final Map<Long, NoiseColumn> columns = new HashMap<>();
|
||||
|
||||
public RuinsTerrain17(ExpansionIsland island, NoiseBasedChunkGenerator generator, RandomState random,
|
||||
LevelHeightAccessor height, Predicate<BlockPos> protection) {
|
||||
this.island = island; this.generator = generator; this.random = random;
|
||||
this.height = height; this.protection = protection;
|
||||
}
|
||||
private NoiseColumn column(int x, int z) {
|
||||
if (!owns(x, z)) return null;
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
var found = columns.get(key);
|
||||
if (found != null) return found;
|
||||
if (columns.size() >= MAX_COLUMNS) return null;
|
||||
var result = generator.getBaseColumn(x, z, height, random);
|
||||
columns.put(key, result);
|
||||
return result;
|
||||
}
|
||||
@Override public boolean owns(int x, int z) { return island.contains(x, z); }
|
||||
@Override public boolean solid(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return false;
|
||||
var column = column(x, z);
|
||||
return column != null && column.getBlock(y).isSolid() && column.getBlock(y).getFluidState().isEmpty();
|
||||
}
|
||||
@Override public boolean protectedAt(int x, int y, int z) {
|
||||
if (y < 0 || y >= 384) return true;
|
||||
var column = column(x, z);
|
||||
return column == null || !column.getBlock(y).getFluidState().isEmpty()
|
||||
|| protection.test(new BlockPos(x, y, z));
|
||||
}
|
||||
@Override public int surfaceY(int x, int z) {
|
||||
var column = column(x, z);
|
||||
if (column == null) return -1;
|
||||
for (int y = 380; y >= 1; y--) {
|
||||
var state = column.getBlock(y);
|
||||
if (!state.isAir()) return state.isSolid() && state.getFluidState().isEmpty() ? y : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
public int columnsUsed() { return columns.size(); }
|
||||
}
|
||||
@@ -124,5 +124,8 @@
|
||||
"sanctuary.expansion.error.city_generation_required": "This profile or size requires a generation 14 or 15 Sanctuary world. This save keeps its original generation.",
|
||||
"sanctuary.expansion.reuse_reload": "Expansion %s reserved in previously generated void. Close and reopen the world to begin preparation.",
|
||||
"sanctuary.expansion.error.reload_required": "This expansion is waiting for the world to reopen before reusing verified void chunks.",
|
||||
"sanctuary.expansion.error.void_reuse_incomplete": "Void reuse is incomplete. The reservation is retained; check the server log."
|
||||
"sanctuary.expansion.error.void_reuse_incomplete": "Void reuse is incomplete. The reservation is retained; check the server log.",
|
||||
"sanctuary.world_options.experimental": "Experimental structures",
|
||||
"sanctuary.world_options.experimental_hint": "Mines, strange rooms and abandoned facilities. This choice is saved with the new world.",
|
||||
"structure.sanctuary.forgotten_networks": "Forgotten networks"
|
||||
}
|
||||
|
||||
@@ -124,5 +124,8 @@
|
||||
"sanctuary.expansion.error.city_generation_required": "Ce profil ou cette taille demande un monde Sanctuary de génération 14 ou 15. Cette sauvegarde conserve sa génération.",
|
||||
"sanctuary.expansion.reuse_reload": "Expansion %s réservée dans du vide déjà généré. Fermez puis rouvrez le monde pour démarrer sa préparation.",
|
||||
"sanctuary.expansion.error.reload_required": "Cette expansion attend la réouverture du monde pour réutiliser les chunks de vide vérifiés.",
|
||||
"sanctuary.expansion.error.void_reuse_incomplete": "La réutilisation du vide est incomplète. La réservation est conservée ; consultez le journal du serveur."
|
||||
"sanctuary.expansion.error.void_reuse_incomplete": "La réutilisation du vide est incomplète. La réservation est conservée ; consultez le journal du serveur.",
|
||||
"sanctuary.world_options.experimental": "Structures expérimentales",
|
||||
"sanctuary.world_options.experimental_hint": "Mines, salles étranges et installations abandonnées. Ce choix reste attaché au nouveau monde.",
|
||||
"structure.sanctuary.forgotten_networks": "Réseaux oubliés"
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"#minecraft:is_overworld",
|
||||
"sanctuary:cavern_bamboo_grove",
|
||||
"sanctuary:cavern_birch_forest",
|
||||
"sanctuary:cavern_clearing",
|
||||
"sanctuary:cavern_dark_grove",
|
||||
"sanctuary:cavern_dripstone_caves",
|
||||
"sanctuary:cavern_dry_woodland",
|
||||
"sanctuary:cavern_lush_caves",
|
||||
"sanctuary:cavern_oak_forest",
|
||||
"sanctuary:cavern_rocky_heath",
|
||||
"sanctuary:cavern_sulfur_depths",
|
||||
"sanctuary:layered_flower_meadow",
|
||||
"sanctuary:layered_lower_grove",
|
||||
"sanctuary:layered_meadow",
|
||||
"sanctuary:layered_mossy_terraces",
|
||||
"sanctuary:layered_rocky_terraces",
|
||||
"sanctuary:lost_city",
|
||||
"sanctuary:population_bamboo_grove",
|
||||
"sanctuary:population_birch_forest",
|
||||
"sanctuary:population_clearing",
|
||||
"sanctuary:population_dark_grove",
|
||||
"sanctuary:population_dripstone_caves",
|
||||
"sanctuary:population_dry_woodland",
|
||||
"sanctuary:population_lush_caves",
|
||||
"sanctuary:population_oak_forest",
|
||||
"sanctuary:population_rocky_heath",
|
||||
"sanctuary:population_sulfur_depths",
|
||||
"sanctuary:rift_bamboo_grove",
|
||||
"sanctuary:rift_birch_forest",
|
||||
"sanctuary:rift_clearing",
|
||||
"sanctuary:rift_dark_grove",
|
||||
"sanctuary:rift_dry_woodland",
|
||||
"sanctuary:rift_oak_forest",
|
||||
"sanctuary:rift_rocky_heath",
|
||||
"sanctuary:rift_sulfur_depths",
|
||||
"sanctuary:starter_forest",
|
||||
"sanctuary:starter_forest_hydrology",
|
||||
"sanctuary:temperate_birch_forest",
|
||||
"sanctuary:temperate_dappled_forest",
|
||||
"sanctuary:temperate_flower_forest",
|
||||
"sanctuary:temperate_forest",
|
||||
"sanctuary:temperate_plains",
|
||||
"sanctuary:woodland_bamboo_grove",
|
||||
"sanctuary:woodland_birch_forest",
|
||||
"sanctuary:woodland_clearing",
|
||||
"sanctuary:woodland_dark_grove",
|
||||
"sanctuary:woodland_dry_woodland",
|
||||
"sanctuary:woodland_oak_forest",
|
||||
"sanctuary:woodland_rocky_heath",
|
||||
"sanctuary:woodland_sulfur_depths"
|
||||
]
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "sanctuary:forgotten_networks_v17",
|
||||
"biomes": "#sanctuary:has_structure/forgotten_networks",
|
||||
"step": "underground_structures",
|
||||
"terrain_adaptation": "none",
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_384",
|
||||
"generator": {
|
||||
"type": "sanctuary:island_v16",
|
||||
"type": "sanctuary:island_v17",
|
||||
"biome_source": {
|
||||
"biomes": [
|
||||
"minecraft:the_void",
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"dimensions": {
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_384",
|
||||
"generator": {
|
||||
"type": "sanctuary:island_v16",
|
||||
"biome_source": {
|
||||
"biomes": [
|
||||
"minecraft:the_void",
|
||||
"minecraft:forest",
|
||||
"minecraft:birch_forest",
|
||||
"minecraft:flower_forest",
|
||||
"minecraft:plains",
|
||||
"minecraft:meadow",
|
||||
"minecraft:taiga",
|
||||
"minecraft:snowy_taiga",
|
||||
"minecraft:snowy_plains",
|
||||
"minecraft:grove",
|
||||
"minecraft:ice_spikes",
|
||||
"minecraft:frozen_peaks",
|
||||
"minecraft:old_growth_spruce_taiga",
|
||||
"minecraft:dark_forest",
|
||||
"minecraft:swamp",
|
||||
"minecraft:pale_garden",
|
||||
"minecraft:jungle",
|
||||
"minecraft:sparse_jungle",
|
||||
"minecraft:bamboo_jungle",
|
||||
"minecraft:mangrove_swamp",
|
||||
"minecraft:savanna",
|
||||
"minecraft:savanna_plateau",
|
||||
"minecraft:desert",
|
||||
"minecraft:badlands",
|
||||
"minecraft:eroded_badlands",
|
||||
"minecraft:wooded_badlands",
|
||||
"minecraft:windswept_hills",
|
||||
"minecraft:windswept_gravelly_hills",
|
||||
"minecraft:stony_peaks",
|
||||
"minecraft:dappled_forest",
|
||||
"minecraft:cherry_grove",
|
||||
"minecraft:lush_caves",
|
||||
"minecraft:dripstone_caves",
|
||||
"minecraft:sulfur_caves",
|
||||
"sanctuary:population_oak_forest",
|
||||
"sanctuary:population_birch_forest",
|
||||
"sanctuary:population_clearing",
|
||||
"sanctuary:population_dry_woodland",
|
||||
"sanctuary:population_rocky_heath",
|
||||
"sanctuary:population_dark_grove",
|
||||
"sanctuary:population_bamboo_grove",
|
||||
"sanctuary:population_sulfur_depths",
|
||||
"sanctuary:population_lush_caves",
|
||||
"sanctuary:population_dripstone_caves",
|
||||
"sanctuary:lost_city"
|
||||
]
|
||||
},
|
||||
"initial_diameter": 724
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:the_end"
|
||||
},
|
||||
"settings": "minecraft:end"
|
||||
}
|
||||
},
|
||||
"minecraft:the_nether": {
|
||||
"type": "minecraft:the_nether",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:multi_noise",
|
||||
"preset": "minecraft:nether"
|
||||
},
|
||||
"settings": "minecraft:nether"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,21 @@ public final class ExpansionJournalSmoke {
|
||||
require(Arrays.equals(ruinsBytes, Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME)))
|
||||
&& Arrays.equals(detailBytes, Files.readAllBytes(detailDirectory.resolve(ExpansionJournal.FILE_NAME))),
|
||||
"Rejected migrations preserve both published15 and16 bytes");
|
||||
var experimentRoot = ExpansionIsland.origin(724, SEED, 17);
|
||||
var experimentDirectory = scratch.resolve("experiment-17");
|
||||
var experimentJournal = new ExpansionJournal(experimentDirectory, SEED, 724, 17);
|
||||
var experimentChild = ExpansionPlacement.candidates(SEED, List.of(experimentRoot), "experiment", "sanctuary", "east", "lost_city", 128, "natural", 0).getFirst()
|
||||
.withReuseToken("e".repeat(64));
|
||||
experimentJournal.commit(List.of(experimentRoot, experimentChild));
|
||||
require(new ExpansionJournal(experimentDirectory, SEED, 724, 17).islands().equals(List.of(experimentRoot, experimentChild)),
|
||||
"Generation17 journal and child preserve their independent identity");
|
||||
byte[] experimentBytes = Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME));
|
||||
reject(() -> new ExpansionJournal(experimentDirectory, SEED, 724, 16), "No implicit downgrade from17 to16");
|
||||
reject(() -> new ExpansionJournal(ruinsDirectory, SEED, 724, 17), "No implicit upgrade from16 to17");
|
||||
reject(() -> experimentJournal.commit(List.of(experimentRoot, ruinsChild)), "No generation16 child in17");
|
||||
require(Arrays.equals(experimentBytes, Files.readAllBytes(experimentDirectory.resolve(ExpansionJournal.FILE_NAME)))
|
||||
&& Arrays.equals(ruinsBytes, Files.readAllBytes(ruinsDirectory.resolve(ExpansionJournal.FILE_NAME))),
|
||||
"Rejected cross-generation changes preserve experiment and published16 bytes");
|
||||
try {
|
||||
ExpansionPlacement.candidates(SEED, List.of(ExpansionIsland.origin(724, SEED, 13)), "city", "sanctuary", "east", "lost_city", 256, "natural", 0);
|
||||
throw new AssertionError("Lost City must not silently activate in a saved alpha.13 world");
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
|
||||
/** Generation17's independent saved payload: exact roundtrips and fail-closed malformed geometry. */
|
||||
public final class RuinsPiece17Smoke {
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
|
||||
System.out.println("RuinsPiece17Smoke: all room kinds, strict schema/types, supports, bounds and invalid geometry passed.");
|
||||
}
|
||||
public static void run() {
|
||||
for (var kind : RuinsPlan17.Kind.values()) {
|
||||
int size = kind == RuinsPlan17.Kind.ACCESS ? 5 : 17;
|
||||
var room = new RuinsPlan17.Room(kind, -32, 120, 40, size, kind == RuinsPlan17.Kind.ACCESS ? 18 : 9, size,
|
||||
10, Long.MIN_VALUE + kind.ordinal(), List.of(new RuinsPlan17.Support(-31, 114, 41, 119)));
|
||||
var saved = RuinsPiece17.encodeRoom(room);
|
||||
var decoded = RuinsPiece17.decodeRoom(saved);
|
||||
require(room.equals(decoded) && RuinsPiece17.encodeRoom(decoded).equals(saved), "Saved room fields changed for " + kind);
|
||||
var bounds = RuinsPiece17.bounds(room);
|
||||
require(bounds.minX() == -32 && bounds.minZ() == 40 && bounds.minY() == 114
|
||||
&& bounds.maxX() == room.maxX() && bounds.maxZ() == room.maxZ() && bounds.maxY() == room.maxY(), "Bounds excluded a saved footing");
|
||||
if (kind == RuinsPlan17.Kind.ACCESS) require(RuinsPiece17.locator(room).getY() == room.maxY() - 1,
|
||||
"Locate points beneath the surface hatch");
|
||||
var missing = saved.copy(); missing.remove("Openings"); rejects(missing);
|
||||
var version = saved.copy(); version.putInt("Schema", 2); rejects(version);
|
||||
var coercion = saved.copy(); coercion.putDouble("MinY", 120.0); rejects(coercion);
|
||||
var seedType = saved.copy(); seedType.putInt("RoomSeed", 1); rejects(seedType);
|
||||
var unknown = saved.copy(); unknown.putInt("FutureField", 1); rejects(unknown);
|
||||
var geometry = saved.copy(); geometry.putInt("Width", 18); rejects(geometry);
|
||||
var opening = saved.copy(); opening.putInt("Openings", 16); rejects(opening);
|
||||
var footings = new ListTag(); var bad = new CompoundTag();
|
||||
bad.putInt("X", room.maxX() + 1); bad.putInt("MinY", 114); bad.putInt("Z", 41); bad.putInt("MaxY", 119);
|
||||
footings.add(bad); var outside = saved.copy(); outside.put("Supports", footings); rejects(outside);
|
||||
bad.putInt("X", -31); bad.putInt("MinY", 100); rejects(outside); // Overlong support.
|
||||
bad.putInt("MinY", 114); bad.putInt("MaxY", 118); rejects(outside); // Detached support.
|
||||
bad.putInt("MaxY", 119); footings.add(bad.copy()); rejects(outside); // Duplicate anchor column.
|
||||
}
|
||||
var supportList = new ArrayList<RuinsPlan17.Support>();
|
||||
supportList.add(new RuinsPlan17.Support(1, 114, 1, 119));
|
||||
var room = new RuinsPlan17.Room(RuinsPlan17.Kind.ACCESS, 0, 120, 0, 5, 18, 5, 2, 42, supportList);
|
||||
supportList.clear();
|
||||
require(RuinsPiece17.decodeRoom(RuinsPiece17.encodeRoom(room)).supports().size() == 1, "External list mutation changed saved support inputs");
|
||||
}
|
||||
private static void rejects(CompoundTag tag) {
|
||||
try { RuinsPiece17.decodeRoom(tag); }
|
||||
catch (IllegalArgumentException expected) { return; }
|
||||
throw new AssertionError("Malformed forgotten room payload was accepted: " + tag);
|
||||
}
|
||||
private static void require(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Family;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Kind;
|
||||
import fr.koka.sanctuary.worldgen.ruins.RuinsPlan17.Room;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
/** Pure terrain-contract tests: entrances, exact doorway joins, independent budgets and short supports. */
|
||||
public final class RuinsPlanner17Smoke {
|
||||
private static final int[] DX = {0, 1, 0, -1}, DZ = {-1, 0, 1, 0};
|
||||
public static void main(String[] args) {
|
||||
Set<Kind> kinds = EnumSet.noneOf(Kind.class);
|
||||
for (int diameter : new int[] {512, 724, 1024}) for (long seed : new long[] {0, 1, 42, -1}) {
|
||||
Fixture terrain = new Fixture((x, z) -> 220);
|
||||
var plan = RuinsPlanner17.plan(seed, 0, 0, diameter, terrain);
|
||||
check(plan, terrain);
|
||||
require(plan.networks().size() == 3, "Unobstructed starter does not receive all three families");
|
||||
require(plan.networks().stream().map(RuinsPlan17.Network::family).distinct().count() == 3, "A family was duplicated");
|
||||
require(plan.networks().stream().allMatch(n -> n.rooms().size() == 5), "Flat terrain loses a certified branch");
|
||||
plan.networks().forEach(n -> n.rooms().forEach(r -> kinds.add(r.kind())));
|
||||
require(plan.equals(RuinsPlanner17.plan(seed, 0, 0, diameter, new Fixture((x, z) -> 220))), "Ruins depend on planning order");
|
||||
}
|
||||
require(kinds.equals(EnumSet.allOf(Kind.class)), "A requested thematic kind is never generated");
|
||||
var flat = RuinsPlanner17.plan(0, 0, 0, 724, new Fixture((x, z) -> 220));
|
||||
require(!flat.equals(RuinsPlanner17.plan(1, 0, 0, 724, new Fixture((x, z) -> 220))), "Network locations ignore the seed");
|
||||
Fixture shifted = new Fixture((x, z) -> 220);
|
||||
var translated = RuinsPlanner17.plan(42, -1024, 512, 724, shifted);
|
||||
require(translated.networks().size() == 3, "Negative world coordinates lose a network"); check(translated, shifted);
|
||||
|
||||
Fixture wet = new Fixture((x, z) -> 220); wet.protection = (x, y, z) -> true;
|
||||
var refusedWater = RuinsPlanner17.plan(0, 0, 0, 724, wet);
|
||||
check(refusedWater, wet); require(refusedWater.isEmpty(), "Ruins overwrite protected water or a city");
|
||||
Fixture absent = new Fixture((x, z) -> -1);
|
||||
var refusedVoid = RuinsPlanner17.plan(0, 0, 0, 724, absent);
|
||||
check(refusedVoid, absent); require(refusedVoid.isEmpty(), "Void receives an artificial entry platform");
|
||||
Fixture thin = new Fixture((x, z) -> 220); thin.rock = (x, y, z) -> y >= 218 && y <= 220;
|
||||
var refusedThin = RuinsPlanner17.plan(0, 0, 0, 724, thin);
|
||||
check(refusedThin, thin); require(refusedThin.isEmpty(), "A shaft descends from a shelf with no reachable bearing rock");
|
||||
|
||||
Fixture cavity = new Fixture((x, z) -> 220);
|
||||
cavity.rock = (x, y, z) -> y >= 8 && y <= 220 && (y < 200 || y > 204);
|
||||
var supported = RuinsPlanner17.plan(0, 0, 0, 724, cavity);
|
||||
check(supported, cavity);
|
||||
require(supported.networks().size() == 3 && supported.networks().stream().flatMap(n -> n.rooms().stream())
|
||||
.anyMatch(r -> !r.supports().isEmpty()), "Short cave gaps cannot receive certified architectural supports");
|
||||
|
||||
// The first family's sector is obstructed. Its retries must not eliminate the other two.
|
||||
var first = flat.networks().getFirst().rooms().getFirst();
|
||||
double radiusSquared = (double) first.centerX() * first.centerX() + (double) first.centerZ() * first.centerZ();
|
||||
Fixture obstructed = new Fixture((x, z) -> 220);
|
||||
obstructed.protection = (x, y, z) -> (double) x * first.centerX() + (double) z * first.centerZ() > radiusSquared * .5;
|
||||
var partial = RuinsPlanner17.plan(0, 0, 0, 724, obstructed);
|
||||
check(partial, obstructed);
|
||||
require(partial.networks().stream().noneMatch(n -> n.family() == Family.MINE), "Obstructed family escaped its own candidate sector");
|
||||
require(partial.networks().stream().anyMatch(n -> n.family() == Family.STRANGE)
|
||||
&& partial.networks().stream().anyMatch(n -> n.family() == Family.INFRASTRUCTURE), "One family starved all later families");
|
||||
for (int diameter : new int[] {64, 128, 256}) {
|
||||
Fixture small = new Fixture((x, z) -> 220);
|
||||
check(RuinsPlanner17.plan(42, 0, 0, diameter, small), small);
|
||||
}
|
||||
boolean rejected = false;
|
||||
try { new Room(Kind.WORKSHOP, 0, 100, 0, 5, 7, 5, 0, 0, List.of()); }
|
||||
catch (IllegalArgumentException expected) { rejected = true; }
|
||||
require(rejected, "Thematic room accepts dimensions too small for its central passage");
|
||||
System.out.println("RuinsPlanner17Smoke passed: three independent families, discoverable shafts, exact connected doorways, protected-volume rejection, short native footings and bounded per-family searches");
|
||||
}
|
||||
|
||||
private static void check(RuinsPlan17 plan, Fixture terrain) {
|
||||
require(plan.samples() == terrain.samples && plan.samples() <= RuinsPlanner17.MAX_SAMPLES, "Probe budget drifted");
|
||||
require(terrain.columns.size() <= RuinsPlanner17.MAX_COLUMNS, "Column budget exceeded");
|
||||
Set<Long> starts = new HashSet<>(); Set<Point> written = new HashSet<>();
|
||||
for (var network : plan.networks()) {
|
||||
require(starts.add(key(network.startChunkX(), network.startChunkZ())), "Two networks collide at one native start");
|
||||
require(network.rooms().size() >= 3 && network.rooms().size() <= 6, "Network size is unbounded");
|
||||
require(network.rooms().stream().filter(r -> r.kind() == Kind.ACCESS).count() == 1, "Network has no unique surface shaft");
|
||||
Map<Long, Integer> walk = new HashMap<>();
|
||||
for (int index = 0; index < network.rooms().size(); index++) {
|
||||
Room room = network.rooms().get(index);
|
||||
require(Math.abs((room.minX() >> 4) - network.startChunkX()) <= 7
|
||||
&& Math.abs((room.maxX() >> 4) - network.startChunkX()) <= 7
|
||||
&& Math.abs((room.minZ() >> 4) - network.startChunkZ()) <= 7
|
||||
&& Math.abs((room.maxZ() >> 4) - network.startChunkZ()) <= 7, "Native piece reference exceeds seven chunks");
|
||||
int highest = -1;
|
||||
for (int z = room.minZ(); z <= room.maxZ(); z++) for (int x = room.minX(); x <= room.maxX(); x++) {
|
||||
highest = Math.max(highest, terrain.height.applyAsInt(x, z));
|
||||
for (int y = room.minY(); y <= room.maxY(); y++) {
|
||||
require(written.add(new Point(x, y, z)), "Independent rooms write the same voxel");
|
||||
require(!terrain.protection.test(x, y, z), "Room overwrites protected terrain or water");
|
||||
require(plan.contains(x, y, z) && plan.protects(x, y, z), "Room is absent from the construction mask");
|
||||
}
|
||||
require(plan.protects(x, room.minY() - 2, z), "Native floor bearing is exposed to decoration");
|
||||
if (passage(room, x, z)) require(walk.put(key(x, z), index) == null, "Room walking planes overlap");
|
||||
}
|
||||
if (room.kind() == Kind.ACCESS) require(room.maxY() == highest + 2, "Entry is buried below the sampled surface");
|
||||
for (int z : new int[] {room.minZ() + 1, room.centerZ(), room.maxZ() - 1})
|
||||
for (int x : new int[] {room.minX() + 1, room.centerX(), room.maxX() - 1}) {
|
||||
var support = room.supports().stream().filter(s -> s.x() == x && s.z() == z).findFirst();
|
||||
if (support.isEmpty()) require(terrain.rock.test(x, room.minY() - 1, z) && terrain.rock.test(x, room.minY() - 2, z),
|
||||
"Floor has neither a native rock footing nor a short pier");
|
||||
}
|
||||
for (var support : room.supports()) {
|
||||
require(support.maxY() == room.minY() - 1 && support.maxY() - support.minY() < 12,
|
||||
"Support misses the floor or descends too far");
|
||||
require(terrain.rock.test(support.x(), support.minY() - 1, support.z())
|
||||
&& terrain.rock.test(support.x(), support.minY() - 2, support.z()), "Support does not end on certified native rock");
|
||||
require(plan.protects(support.x(), support.minY() - 1, support.z())
|
||||
&& plan.intersectsProtection(support.x(), support.minY() - 1, support.z(), support.x(), support.minY() - 1, support.z()),
|
||||
"Support bearing is missing from the decoration/structure protection masks");
|
||||
for (int y = support.minY(); y <= support.maxY(); y++) {
|
||||
require(written.add(new Point(support.x(), y, support.z())), "Support collides with another room or support");
|
||||
require(!terrain.protection.test(support.x(), y, support.z()), "Support overwrites protected water");
|
||||
require(plan.contains(support.x(), y, support.z()), "Support is missing from the exact volume mask");
|
||||
}
|
||||
}
|
||||
for (int direction = 0; direction < 4; direction++) if ((room.openings() & 1 << direction) != 0) {
|
||||
Room neighbor = neighbor(room, direction, network.rooms());
|
||||
require(neighbor != null, "A published doorway opens into unplanned rock");
|
||||
require(neighbor.floorY() == room.floorY() && (neighbor.openings() & 1 << ((direction + 2) & 3)) != 0,
|
||||
"Doorways disagree on height or reciprocal opening");
|
||||
}
|
||||
}
|
||||
require(!walk.isEmpty(), "Network has no walkable central passage");
|
||||
Set<Long> reached = new HashSet<>(); var queue = new ArrayDeque<Long>();
|
||||
Room access = network.rooms().stream().filter(r -> r.kind() == Kind.ACCESS).findFirst().orElseThrow();
|
||||
long origin = key(access.centerX(), access.centerZ()); reached.add(origin); queue.add(origin);
|
||||
while (!queue.isEmpty()) {
|
||||
long position = queue.removeFirst(); int x = (int) (position >> 32), z = (int) position;
|
||||
for (int direction = 0; direction < 4; direction++) {
|
||||
long next = key(x + DX[direction], z + DZ[direction]);
|
||||
if (walk.containsKey(next) && reached.add(next)) queue.addLast(next);
|
||||
}
|
||||
}
|
||||
require(reached.equals(walk.keySet()), "A thematic room is disconnected from its discoverable entrance");
|
||||
}
|
||||
require(!plan.contains(100_000, 200, 100_000) && !plan.intersectsProtection(100_000, 0, 100_000, 100_005, 383, 100_005),
|
||||
"Ruins protection escapes the accepted networks");
|
||||
}
|
||||
private static boolean passage(Room r, int x, int z) {
|
||||
if (x == r.minX()) return (r.openings() & 8) != 0 && Math.abs(z - r.centerZ()) <= 1;
|
||||
if (x == r.maxX()) return (r.openings() & 2) != 0 && Math.abs(z - r.centerZ()) <= 1;
|
||||
if (z == r.minZ()) return (r.openings() & 1) != 0 && Math.abs(x - r.centerX()) <= 1;
|
||||
if (z == r.maxZ()) return (r.openings() & 4) != 0 && Math.abs(x - r.centerX()) <= 1;
|
||||
return Math.abs(x - r.centerX()) <= 1 || Math.abs(z - r.centerZ()) <= 1;
|
||||
}
|
||||
private static Room neighbor(Room room, int direction, List<Room> rooms) {
|
||||
int x = direction == 1 ? room.maxX() + 1 : direction == 3 ? room.minX() - 1 : room.centerX();
|
||||
int z = direction == 2 ? room.maxZ() + 1 : direction == 0 ? room.minZ() - 1 : room.centerZ();
|
||||
return rooms.stream().filter(r -> r != room && x >= r.minX() && x <= r.maxX() && z >= r.minZ() && z <= r.maxZ()
|
||||
&& (DX[direction] == 0 ? r.centerX() == room.centerX() : r.centerZ() == room.centerZ())).findFirst().orElse(null);
|
||||
}
|
||||
@FunctionalInterface private interface Volume { boolean test(int x, int y, int z); }
|
||||
private static final class Fixture implements RuinsPlanner17.Terrain {
|
||||
final IntBinaryOperator height; final Set<Long> columns = new HashSet<>(); int samples;
|
||||
Volume protection = (x, y, z) -> false;
|
||||
Volume rock;
|
||||
Fixture(IntBinaryOperator height) { this.height = height; rock = (x, y, z) -> y >= 8 && y <= height.applyAsInt(x, z); }
|
||||
@Override public boolean owns(int x, int z) { return true; }
|
||||
@Override public boolean solid(int x, int y, int z) { samples++; return rock.test(x, y, z); }
|
||||
@Override public boolean protectedAt(int x, int y, int z) { samples++; return protection.test(x, y, z); }
|
||||
@Override public int surfaceY(int x, int z) {
|
||||
samples++; require(columns.add(key(x, z)), "Column noise was sampled more than once"); return height.applyAsInt(x, z);
|
||||
}
|
||||
}
|
||||
private record Point(int x, int y, int z) {}
|
||||
private static long key(int x, int z) { return (long) x << 32 ^ z & 0xffffffffL; }
|
||||
private static void require(boolean valid, String message) { if (!valid) throw new AssertionError(message); }
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
package fr.koka.sanctuary.worldgen.ruins;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.city.LostCityRenderer;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.RailBlock;
|
||||
import net.minecraft.world.level.block.StairBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
|
||||
/** Exercises the final native block states, saved bounds and meaningful circulation of all prototypes. */
|
||||
public final class RuinsRenderer17Smoke {
|
||||
private RuinsRenderer17Smoke() {}
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); run();
|
||||
System.out.println("RuinsRenderer17Smoke passed: ten physical room types, saved seed variation, dry connected openings, usable work platform, native fixture supports, deferred loot and chunk-order reconstruction");
|
||||
}
|
||||
public static void run() {
|
||||
Set<Map<BlockPos, BlockState>> kinds = new HashSet<>();
|
||||
for (var kind : RuinsPlan17.Kind.values()) {
|
||||
for (int size = 0; size < 3; size++) for (int openings : new int[] {1, 2, 4, 8, 5, 10, 15})
|
||||
for (long seed : new long[] {0, 1}) {
|
||||
var room = room(kind, size, openings, seed);
|
||||
var c = render(room); var repeated = render(room);
|
||||
require(c.blocks.equals(repeated.blocks) && c.loot.equals(repeated.loot), label(room) + " is not deterministic");
|
||||
if (size == 0 && openings == 15 && seed == 0) kinds.add(c.blocks);
|
||||
var start = RuinsRenderer17.targets(room).getFirst();
|
||||
Set<BlockPos> reached = walk(c, position(start));
|
||||
for (var target : RuinsRenderer17.targets(room)) require(reached.contains(position(target)),
|
||||
label(room) + " cannot reach " + target.name() + " at " + position(target));
|
||||
openings(c, reached);
|
||||
supports(c);
|
||||
fixtures(c, reached);
|
||||
identity(c);
|
||||
for (var chest : c.loot.keySet()) require(approachable(chest, reached), label(room) + " chest is inaccessible at " + chest);
|
||||
require(c.blocks.values().stream().noneMatch(b -> b.is(Blocks.SPAWNER) || !b.getFluidState().isEmpty()),
|
||||
label(room) + " introduced a spawner or uncontrolled fluid");
|
||||
}
|
||||
var a = render(room(kind, 1, 15, 0)); var b = render(room(kind, 1, 15, 1));
|
||||
require(!a.blocks.equals(b.blocks), kind + " ignores its saved variation seed");
|
||||
if (kind != RuinsPlan17.Kind.ACCESS && kind != RuinsPlan17.Kind.CORRIDOR)
|
||||
require(!furniture(a).equals(furniture(b)), kind + " varies only stone weathering, not physical use or furniture placement");
|
||||
}
|
||||
require(kinds.size() == RuinsPlan17.Kind.values().length, "Room names disguise identical rendered interiors");
|
||||
nativeChunkPartition();
|
||||
}
|
||||
|
||||
private static RuinsPlan17.Room room(RuinsPlan17.Kind kind, int size, int openings, long seed) {
|
||||
int width = size == 0 ? 9 : size == 1 ? 13 : 17;
|
||||
int depth = size < 2 ? 9 : 13, height = size < 2 ? 7 : 9;
|
||||
if (kind == RuinsPlan17.Kind.ACCESS) { width = depth = 5; height = new int[] {14, 27, 40}[size]; }
|
||||
if (kind == RuinsPlan17.Kind.CORRIDOR) { width = size == 2 ? 17 : 5; depth = size == 1 ? 13 : 5; height = 6; }
|
||||
int x = -43, y = 150, z = 59;
|
||||
return new RuinsPlan17.Room(kind, x, y, z, width, height, depth, openings, seed,
|
||||
List.of(new RuinsPlan17.Support(x, y - 5, z, y - 1),
|
||||
new RuinsPlan17.Support(x + width - 1, y - 12, z + depth - 1, y - 1)));
|
||||
}
|
||||
private static String label(RuinsPlan17.Room r) {
|
||||
return r.kind() + " " + r.width() + "x" + r.height() + "x" + r.depth() + " openings=" + r.openings() + " seed=" + r.seed();
|
||||
}
|
||||
private static BlockPos position(RuinsRenderer17.Target target) { return new BlockPos(target.x(), target.y(), target.z()); }
|
||||
private static Capture render(RuinsPlan17.Room room) {
|
||||
var result = new Capture(room); RuinsRenderer17.render(room, result); return result;
|
||||
}
|
||||
private static void openings(Capture c, Set<BlockPos> reached) {
|
||||
var r = c.room;
|
||||
for (int direction = 0; direction < 4; direction++) for (int side = -1; side <= 1; side++) {
|
||||
int x = direction == 1 ? r.width() - 1 : direction == 3 ? 0 : r.width() / 2 + side;
|
||||
int z = direction == 2 ? r.depth() - 1 : direction == 0 ? 0 : r.depth() / 2 + side;
|
||||
var threshold = new BlockPos(x, 1, z);
|
||||
require(solid(c.block(threshold)), label(r) + " has no dry threshold at " + threshold);
|
||||
if ((r.openings() & 1 << direction) == 0) {
|
||||
require(!c.block(threshold.above()).isAir(), label(r) + " opens an unconnected wall");
|
||||
continue;
|
||||
}
|
||||
for (int y = 2; y <= 4; y++) require(c.block(new BlockPos(x, y, z)).isAir(),
|
||||
label(r) + " obstructs its three-wide opening at " + x + "/" + z);
|
||||
require(reached.contains(threshold.above()), label(r) + " opening edge is not connected to the room");
|
||||
}
|
||||
}
|
||||
private static void supports(Capture c) {
|
||||
var r = c.room;
|
||||
for (var support : r.supports()) {
|
||||
for (int y = support.minY(); y <= support.maxY(); y++) require(solid(c.block(new BlockPos(
|
||||
support.x() - r.minX(), y - r.minY(), support.z() - r.minZ()))), label(r) + " interrupts its saved footing");
|
||||
require(solid(c.block(new BlockPos(support.x() - r.minX(), 0, support.z() - r.minZ()))),
|
||||
label(r) + " footing does not meet the structural floor");
|
||||
}
|
||||
for (int x = 0; x < r.width(); x++) for (int z = 0; z < r.depth(); z++)
|
||||
require(solid(c.block(new BlockPos(x, 0, z))), label(r) + " has a perforated structural floor");
|
||||
}
|
||||
private static void fixtures(Capture c, Set<BlockPos> reached) {
|
||||
for (var entry : c.blocks.entrySet()) {
|
||||
var p = entry.getKey(); var block = entry.getValue();
|
||||
if (block.is(Blocks.LANTERN)) {
|
||||
require(block.getValue(BlockStateProperties.HANGING), "Lantern must hang from the ceiling");
|
||||
require(solid(c.block(p.above())), label(c.room) + " floating lantern at " + p);
|
||||
}
|
||||
if (block.is(Blocks.LADDER)) {
|
||||
Direction facing = block.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||
require(solid(c.block(p.relative(facing.getOpposite()))), label(c.room) + " ladder lacks a back wall at " + p);
|
||||
}
|
||||
if (block.is(Blocks.LEVER) || block.is(Blocks.GRINDSTONE)) require(solid(c.block(p.below())),
|
||||
label(c.room) + " unsupported usable fixture at " + p);
|
||||
if (block.is(Blocks.DARK_OAK_STAIRS)) {
|
||||
Direction back = block.getValue(BlockStateProperties.HORIZONTAL_FACING);
|
||||
require(solid(c.block(p.below())), label(c.room) + " seat has no floor");
|
||||
require(reached.contains(p.relative(back.getOpposite())), label(c.room) + " seat faces an obstruction at " + p);
|
||||
}
|
||||
if (block.is(Blocks.CRAFTING_TABLE) || block.is(Blocks.FURNACE) || block.is(Blocks.GRINDSTONE)
|
||||
|| block.is(Blocks.STONECUTTER) || block.is(Blocks.CARTOGRAPHY_TABLE) || block.is(Blocks.LECTERN))
|
||||
require(approachable(p, reached), label(c.room) + " useful furniture cannot be approached at " + p);
|
||||
}
|
||||
}
|
||||
private static void identity(Capture c) {
|
||||
int expectedLoot = c.room.kind() == RuinsPlan17.Kind.STORE || c.room.kind() == RuinsPlan17.Kind.ARCHIVE ? 1 : 0;
|
||||
require(c.loot.size() == expectedLoot, label(c.room) + " has unbounded or missing deferred loot");
|
||||
switch (c.room.kind()) {
|
||||
case ACCESS -> { has(c, Blocks.LADDER); has(c, Blocks.DARK_OAK_TRAPDOOR); }
|
||||
case CORRIDOR -> has(c, Blocks.POLISHED_ANDESITE);
|
||||
case MINE_GALLERY -> { has(c, Blocks.RAIL); has(c, Blocks.STRIPPED_OAK_LOG); has(c, Blocks.BARREL); }
|
||||
case WORKSHOP -> { has(c, Blocks.CRAFTING_TABLE); has(c, Blocks.FURNACE); has(c, Blocks.GRINDSTONE); has(c, Blocks.STONECUTTER); has(c, Blocks.ANVIL); has(c, Blocks.LADDER); }
|
||||
case STORE -> { has(c, Blocks.BARREL); has(c, Blocks.DARK_OAK_SLAB); }
|
||||
case TILED_HALL -> { has(c, Blocks.CALCITE); has(c, Blocks.POLISHED_ANDESITE); has(c, Blocks.DARK_OAK_STAIRS); }
|
||||
case ARCHIVE -> { has(c, Blocks.BOOKSHELF); has(c, Blocks.LECTERN); has(c, Blocks.CARTOGRAPHY_TABLE); }
|
||||
case SERVICE_HALL -> { has(c, Blocks.IRON_BARS); has(c, Blocks.CRAFTING_TABLE); }
|
||||
case PUMP_HALL -> { has(c, Blocks.WATER_CAULDRON); has(c, Blocks.DISPENSER); has(c, Blocks.LEVER); }
|
||||
case CONTROL_ROOM -> { has(c, Blocks.LEVER); has(c, Blocks.REDSTONE_LAMP); has(c, Blocks.CARTOGRAPHY_TABLE); }
|
||||
}
|
||||
}
|
||||
private static void has(Capture c, Block block) {
|
||||
require(c.blocks.values().stream().anyMatch(b -> b.is(block)), label(c.room) + " lost its recognizable " + block);
|
||||
}
|
||||
private static Map<BlockPos, BlockState> furniture(Capture c) {
|
||||
var result = new HashMap<BlockPos, BlockState>();
|
||||
c.blocks.forEach((p, b) -> {
|
||||
if (p.getY() >= 2 && p.getY() < c.room.height() - 2 && !b.isAir()
|
||||
&& !b.is(Blocks.STONE_BRICKS) && !b.is(Blocks.CRACKED_STONE_BRICKS)
|
||||
&& !b.is(Blocks.MOSSY_STONE_BRICKS) && !b.is(Blocks.SMOOTH_STONE)) result.put(p, b);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
private static boolean approachable(BlockPos p, Set<BlockPos> reached) {
|
||||
for (Direction d : Direction.Plane.HORIZONTAL) if (reached.contains(p.relative(d))) return true;
|
||||
return false;
|
||||
}
|
||||
private static Set<BlockPos> walk(Capture c, BlockPos start) {
|
||||
require(stand(c, start), label(c.room) + " starts without two-block standing space at " + start);
|
||||
var seen = new HashSet<BlockPos>(); var queue = new ArrayDeque<BlockPos>(); seen.add(start); queue.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
var p = queue.removeFirst();
|
||||
for (Direction direction : Direction.Plane.HORIZONTAL) for (int dy : new int[] {0, 1, -1}) {
|
||||
var q = p.relative(direction).offset(0, dy, 0);
|
||||
if (stand(c, q) && seen.add(q)) queue.add(q);
|
||||
}
|
||||
for (int dy : new int[] {-1, 1}) {
|
||||
var q = p.offset(0, dy, 0);
|
||||
if ((climb(c.block(p)) || climb(c.block(q)) || climb(c.block(p.below())))
|
||||
&& stand(c, q) && seen.add(q)) queue.add(q);
|
||||
}
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
private static boolean stand(Capture c, BlockPos p) {
|
||||
if (p.getX() < 0 || p.getX() >= c.room.width() || p.getZ() < 0 || p.getZ() >= c.room.depth()
|
||||
|| p.getY() < 2 || p.getY() >= c.room.height() - 1) return false;
|
||||
return passable(c.block(p)) && passable(c.block(p.above()))
|
||||
&& (solid(c.block(p.below())) || c.block(p.below()).getBlock() instanceof StairBlock
|
||||
|| climb(c.block(p)) || climb(c.block(p.below())));
|
||||
}
|
||||
private static boolean climb(BlockState b) { return b.is(Blocks.LADDER) || b.is(Blocks.DARK_OAK_TRAPDOOR); }
|
||||
private static boolean passable(BlockState b) { return b.isAir() || climb(b) || b.getBlock() instanceof RailBlock || b.is(Blocks.LEVER); }
|
||||
private static boolean solid(BlockState b) { return b.canOcclude() && b.getFluidState().isEmpty(); }
|
||||
|
||||
private static void nativeChunkPartition() {
|
||||
var r = room(RuinsPlan17.Kind.WORKSHOP, 2, 15, 1); var all = render(r); var chunks = new Capture(r);
|
||||
for (int cz = r.maxZ() >> 4; cz >= r.minZ() >> 4; cz--) for (int cx = r.maxX() >> 4; cx >= r.minX() >> 4; cx--) {
|
||||
final int chunkX = cx, chunkZ = cz;
|
||||
RuinsRenderer17.render(r, new LostCityRenderer.BlockWriter() {
|
||||
private boolean belongs(int x, int z) { return ((r.minX() + x) >> 4) == chunkX && ((r.minZ() + z) >> 4) == chunkZ; }
|
||||
@Override public void set(int x, int y, int z, BlockState state) { if (belongs(x, z)) chunks.set(x, y, z, state); }
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) { if (belongs(x, z)) chunks.chest(x, y, z, table, seed); }
|
||||
});
|
||||
}
|
||||
require(chunks.blocks.equals(all.blocks) && chunks.loot.equals(all.loot), "Chunk order changes saved room rendering");
|
||||
}
|
||||
private record Loot(ResourceKey<LootTable> table, long seed) {}
|
||||
private static final class Capture implements LostCityRenderer.BlockWriter {
|
||||
final RuinsPlan17.Room room;
|
||||
final Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
final Map<BlockPos, Loot> loot = new HashMap<>();
|
||||
Capture(RuinsPlan17.Room room) { this.room = room; }
|
||||
@Override public void set(int x, int y, int z, BlockState state) {
|
||||
int wx = room.minX() + x, wy = room.minY() + y, wz = room.minZ() + z;
|
||||
require(room.contains(wx, wy, wz), label(room) + " wrote outside its saved exact body/support mask: " + x + "/" + y + "/" + z);
|
||||
require(wy >= room.boundsMinY() && wy <= room.boundsMaxY(), "Write escapes native vertical bounds");
|
||||
blocks.put(new BlockPos(x, y, z), state);
|
||||
}
|
||||
@Override public void chest(int x, int y, int z, ResourceKey<LootTable> table, long seed) {
|
||||
var pos = new BlockPos(x, y, z);
|
||||
require(loot.put(pos, new Loot(table, seed)) == null, "Duplicate deferred chest");
|
||||
set(x, y, z, Blocks.CHEST.defaultBlockState());
|
||||
}
|
||||
BlockState block(BlockPos p) { return blocks.getOrDefault(p, Blocks.AIR.defaultBlockState()); }
|
||||
}
|
||||
private static void require(boolean test, String message) { if (!test) throw new AssertionError(message); }
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.16"
|
||||
version = "0.1.0-alpha.17"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user