feat: reconstruire la Backroom sur cinq niveaux #137

Merged
koka merged 3 commits from feature/backroom-five-level-generation into main 2026-09-01 16:23:25 +00:00
16 changed files with 753 additions and 240 deletions
+7 -1
View File
@@ -8,8 +8,14 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
## Non publié
Module modifié : **Red-Stoner `0.0.0-alpha.15`**.
Modules modifiés : **Sanctuary `0.0.0-alpha.132`** et **Red-Stoner `0.0.0-alpha.15`**.
- étend `sanctuary:backroom` à toute la hauteur Y -64 → 383 et empile cinq niveaux thématiques de haut en bas : seuil modulaire, archives, ville, services et glitch ;
- remplace les volumes ouverts par des pièces et couloirs raccordés, quatre angles de salle déclinés en cinq matériaux, des bibliothèques immenses, des tours plus serrées, des autoroutes continues et de rares parcs ;
- ajoute au niveau inférieur une grille de prismes 3D et un labyrinthe déterministe connecté, avec portes physiquement ouvertes, salles fusionnées et trous volontaires vers le vide ;
- fait correspondre exactement chaque ouverture entre le sol supérieur et le plafond inférieur, place une torche récupérable à chaque arrivée et raréfie fortement les lits de sortie ;
- supprime le ciel, les astres, la lumière ambiante et tous les monstres, y compris ceux injectés par un autre système ;
- conserve les IDs, les données Sanctuary v14 et le protocole réseau 24 ; seuls les nouveaux chunks Backroom utilisent la nouvelle architecture complète ;
- rend le champ de recherche du terminal inactif à louverture puis focalisable au clic, libère le focus au clic extérieur et laisse toujours la touche dinventaire fermer le terminal ;
- permet à la recherche du terminal de retrouver une boîte de Shulker par le nom ou lidentifiant dun objet quelle contient, tout en conservant la boîte comme résultat afin que son aperçu vanilla reste directement consultable ;
+2
View File
@@ -24,6 +24,8 @@ Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans
Version courante du pack distribué : **`26.2.0-alpha.227`**. Cette release distribue Another World **`0.0.0-alpha.57`** et Sanctuary **`0.0.0-alpha.131`** ; les huit autres modules conservent leur version issue de l'alpha.226.
Le prochain module Sanctuary **`0.0.0-alpha.132`** reconstruit la Backroom sur cinq niveaux empilés entre Y -64 et Y 383. Le monde devient entièrement noir et sans monstres ; larrivée est signalée par une torche récupérable. Les nouveaux chunks composent seuils modulaires, bibliothèques et bureaux, ville dense avec autoroutes et parcs, couloirs techniques, puis une grille 3D et un labyrinthe connecté dont certains trous mènent au vide. Les chunks déjà générés restent intacts.
Lalpha.227 ajoute la dimension persistante `sanctuary:backroom`. On y entre accroupi devant un lit et on nen ressort que par un lit. L’équipement dentrée est transféré atomiquement vers la Mailbox, tandis que les objets perdus ou abandonnés dans la dimension alimentent des coffres renouvelables sans duplication. Les anciennes sauvegardes sont migrées vers Sanctuary data v14 sans réinitialisation ; le protocole réseau reste en version 24.
Lalpha.225 corrige quatre tickets : posséder un coffre-fort devient obligatoire pour en braquer un autre, les familiers temporaires ne donnent plus de butin, dexpérience, de lootbox ni de progression de quête, les seize variantes Canaplia survivent à tout leur cycle de culture, et le Magic Carpet retrouve la texture historique du Totem de Notch à la place de la laine violette. Les IDs, sauvegardes, formats de données et protocoles restent compatibles.
+25 -24
View File
@@ -2916,7 +2916,7 @@ tasks.register("verifyNotchEncounter") {
def textureHash = MessageDigest.getInstance("SHA-256").digest(texture.bytes).encodeHex().toString()
if (rootProject.itsalive_version != "0.0.0-alpha.38"
|| rootProject.ouch_version != "0.0.0-alpha.11"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| rootProject.masterkey_version != "0.0.0-alpha.9"
|| rootProject.pack_version != "26.2.0-alpha.227"
|| manifests.itsalive.custom?.sanctuary26?.data_version != 4
@@ -3010,7 +3010,7 @@ tasks.register("verifyMagicCarpet") {
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|| manifests.iliketomoveit.depends?.sanctuary != ">=0.0.0-alpha.127"
@@ -3641,7 +3641,7 @@ tasks.register("verifyProgressivePauseMenuRelease") {
"sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))]
}
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"]
@@ -4484,7 +4484,7 @@ tasks.register("verifyShopPricingLayoutRelease") {
def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.pack_version != "26.2.0-alpha.192"
|| migration.target?.pack_version != "26.2.0-alpha.193"
@@ -4578,7 +4578,7 @@ tasks.register("verifyShopCsvBossGatesRelease") {
def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.pack_version != "26.2.0-alpha.194"
|| migration.source?.modules != [onlyfun: "0.0.0-alpha.18", sanctuary: "0.0.0-alpha.110"]
@@ -4671,7 +4671,7 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") {
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.196"
|| migration.target?.pack_version != "26.2.0-alpha.197"
|| migration.target?.modules != [itsalive: "0.0.0-alpha.29", sanctuary: "0.0.0-alpha.113"]
@@ -4771,7 +4771,7 @@ tasks.register("verifyQuestRequestContractsRelease") {
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.198"
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.114"
|| migration.target?.pack_version != "26.2.0-alpha.199"
@@ -4891,7 +4891,7 @@ tasks.register("verifyQuestRequestEscrowRelease") {
def packwizIndex = file("pack/packwiz/index.toml").text
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.200"
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.115"
|| migration.target?.pack_version != "26.2.0-alpha.201"
@@ -4948,7 +4948,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.201"
|| migration.source?.modules != [
itsalive: "0.0.0-alpha.30",
@@ -5106,7 +5106,7 @@ tasks.register("verifySafeVaultFeature") {
}.keySet()
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.schema_version != 1
|| migration.source?.pack_version != "26.2.0-alpha.201"
|| migration.target?.pack_version != "26.2.0-alpha.202"
@@ -5966,7 +5966,7 @@ tasks.register("verifyAlpha210Release") {
it.path == "mods/jei-26.2-fabric-30.29.0.199.jar"
}
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|| rootProject.itsalive_version != "0.0.0-alpha.38"
@@ -6068,7 +6068,7 @@ tasks.register("verifyAlpha211Release") {
itsalive: "0.0.0-alpha.38",
ouch: "0.0.0-alpha.11",
masterkey: "0.0.0-alpha.9"]
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"]
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.132"]
def expectedDataVersions = [ambiance: 2, itsalive: 3, onlyfun: 2, ouch: 1, sanctuary: 11]
def workspaceDataVersions = [ambiance: 2, itsalive: 4, onlyfun: 2, ouch: 1, sanctuary: 14]
def expectedProtocols = [ambiance: 6, itsalive: 4, onlyfun: 3, ouch: 1, sanctuary: 22]
@@ -6168,7 +6168,7 @@ tasks.register("verifyAlpha212Release") {
it.path == "mods/jei-26.2-fabric-30.29.0.199.jar"
}
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.211"
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.119"]
|| migration.target?.pack_version != "26.2.0-alpha.212"
@@ -6667,7 +6667,7 @@ tasks.register("verifyAlpha218Release") {
}
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.217"
|| migration.source?.modules != sourceModules
|| migration.target?.pack_version != "26.2.0-alpha.218"
@@ -6753,7 +6753,7 @@ tasks.register("verifyAlpha219Release") {
ambiance: "0.0.0-alpha.22", sanctuary: "0.0.0-alpha.131",
iliketomoveit: "0.0.0-alpha.18", onlyfun: "0.0.0-alpha.27",
itsalive: "0.0.0-alpha.38", ouch: "0.0.0-alpha.11", masterkey: "0.0.0-alpha.9"]
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"]
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.132"]
def requiredJars = currentModules.collect { id, version ->
"file = \"mods/${id}-${version}.jar\""
}
@@ -7078,7 +7078,7 @@ tasks.register("verifyAlpha222Release") {
}
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.redstoner_version != "0.0.0-alpha.15"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.221"
|| migration.source?.modules != [
redstoner: "0.0.0-alpha.12", sanctuary: "0.0.0-alpha.122"]
@@ -7256,7 +7256,7 @@ tasks.register("verifyAlpha223Release") {
it.path == "mods/jei-26.2-fabric-30.29.0.199.jar"
}
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.222"
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.124"]
|| migration.target?.pack_version != "26.2.0-alpha.223"
@@ -7377,7 +7377,7 @@ tasks.register("verifyAlpha224Release") {
def currentModules = targetModules + [
itsalive: "0.0.0-alpha.38", iliketomoveit: "0.0.0-alpha.18",
onlyfun: "0.0.0-alpha.27", sanctuary: "0.0.0-alpha.131"]
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"]
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.132"]
def inventorySort = file(
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryInventorySort.java").text
def clientProgress = file(
@@ -7499,12 +7499,12 @@ tasks.register("verifyAlpha225Release") {
sanctuary: "0.0.0-alpha.129"]
def currentModules = targetModules + [sanctuary: "0.0.0-alpha.131"]
def currentWorkspaceModules = currentModules + [
anotherworld: "0.0.0-alpha.57", sanctuary: "0.0.0-alpha.131"]
anotherworld: "0.0.0-alpha.57", sanctuary: "0.0.0-alpha.132"]
def workspaceModules = moduleIds.collectEntries { id ->
[(id): project(":${id}").version.toString()]
}
def releasedModules = workspaceModules + [
redstoner: "0.0.0-alpha.13"]
redstoner: "0.0.0-alpha.13", sanctuary: "0.0.0-alpha.131"]
def requiredJars = releasedModules.collect { id, version ->
"file = \"mods/${id}-${version}.jar\""
}
@@ -7665,12 +7665,12 @@ tasks.register("verifyAlpha226Release") {
[(id): project(":${id}").version.toString()]
}
def releasedModules = workspaceModules + [
redstoner: "0.0.0-alpha.13"]
redstoner: "0.0.0-alpha.13", sanctuary: "0.0.0-alpha.131"]
def requiredJars = releasedModules.collect { id, version ->
"file = \"mods/${id}-${version}.jar\""
}
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.status != "released"
|| migration.source?.pack_version != "26.2.0-alpha.225"
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.129"]
@@ -7756,13 +7756,14 @@ tasks.register("verifyAlpha227Release") {
def workspaceModules = moduleIds.collectEntries { id ->
[(id): project(":${id}").version.toString()]
}
def releasedModules = workspaceModules + [redstoner: "0.0.0-alpha.13"]
def releasedModules = workspaceModules + [
redstoner: "0.0.0-alpha.13", sanctuary: "0.0.0-alpha.131"]
def requiredJars = releasedModules.collect { id, version ->
"file = \"mods/${id}-${version}.jar\""
}
if (rootProject.pack_version != "26.2.0-alpha.227"
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|| rootProject.sanctuary_version != "0.0.0-alpha.132"
|| migration.status != "released"
|| migration.source?.pack_version != "26.2.0-alpha.226"
|| migration.source?.modules != [
+1 -1
View File
@@ -28,7 +28,7 @@ iliketomoveit_version=0.0.0-alpha.18
iliketomoveit_lifecycle=active
onlyfun_version=0.0.0-alpha.27
onlyfun_lifecycle=active
sanctuary_version=0.0.0-alpha.131
sanctuary_version=0.0.0-alpha.132
sanctuary_lifecycle=active
pack_version=26.2.0-alpha.227
maven_group=fr.koka99cab.sanctuary26
+35 -10
View File
@@ -22,6 +22,9 @@ Sa zone architecturale forme un disque de 2 000 blocs autour de lorigine.
- Toute autre téléportation vers la Backroom est refusée. Une téléportation forcée vers lextérieur
reste possible pour ladministration, mais replace dabord le butin porté dans le registre : elle
ne constitue jamais une extraction réussie.
- Chaque arrivée se fait sur le dernier plancher du niveau 0. Le serveur libère exactement les deux
blocs nécessaires au joueur et pose une torche adjacente : elle peut être récupérée et devient le
premier outil dexploration dans une dimension autrement noire.
## Objets perdus et coffres
@@ -44,19 +47,41 @@ Pendant une expédition, chaque coffre distinct est un essai personnel. Le premi
de proposer une perte récente du joueur, le deuxième 90 %, le troisième 100 %, puis les suivants
65 %. Sans perte personnelle disponible, le coffre suit simplement son tirage général.
## Architecture
## Architecture verticale
Les plans sont déterministes par grandes zones et utilisent cinq familles composées :
La liste [Normal Levels I](https://backrooms-wiki.wikidot.com/normal-levels-i) sert uniquement de
référence dambiance et de vocabulaire. Les plans, palettes et règles ci-dessous sont originaux au
pack ; aucun texte ni visuel du wiki nest embarqué.
- mégalopoles en béton, verre, pierre et voirie ;
- bureaux superposés en bois, clay, textile et éléments techniques ;
- plaines naturelles avec arbres et rares ruines ;
- agrégats de plateformes et de pièces flottantes reliées verticalement ;
- pièces construites visuellement à 90° ou à lenvers, sans modifier la gravité globale.
La dimension couvre maintenant toute la hauteur de **Y = -64 à Y = 383**. Ses cinq niveaux sont
numérotés de haut en bas afin quune chute fasse toujours avancer vers le niveau suivant :
Les matériaux très marqués dAnother World napparaissent que ponctuellement aux anomalies. Aucun
bloc précieux ne sert de matériau structurel ; de très rares blocs de diamant sont cachés dans les
fondations.
- **niveau 0 — Seuil, Y 295 à 383** : cinq plans de pièces jaunes et sableuses, avec couloirs,
cloisons à 90°, colonnes et quatre angles de salle. Chaque angle possède cinq variantes de
matériaux ; des trous rares relient les plans superposés ;
- **niveau 1 — Archives, Y 206 à 294** : quatre étages de bureaux et de bibliothèques pouvant
couvrir plusieurs chunks, avec longues rangées de rayonnages, grandes fenêtres intérieures,
salles de lecture et cloisons ouvertes ;
- **niveau 2 — Ville, Y 117 à 205** : bâtiments rapprochés séparés par de petites ruelles,
autoroutes orthogonales continues, tours à plusieurs étages et rares parcs enfermés sous le
plafond de la dimension ;
- **niveau 3 — Services, Y 28 à 116** : cinq plans de couloirs techniques en briques, grilles et
béton, dont toutes les jonctions de portes restent physiquement ouvertes ;
- **niveau 4 — Glitch, Y -64 à 27** : prismes de briques en grille 3D autour dun toit où lon peut
tomber, puis réseau déterministe de salles et couloirs. Chaque cellule du labyrinthe conserve une
route ouverte vers son origine ; certaines cloisons disparaissent pour fusionner des salles de
tailles différentes. De rares trous dans le dernier plancher mènent au vrai vide.
Les ouvertures entre deux niveaux sont calculées une seule fois puis appliquées à la fois au sol
supérieur et au plafond inférieur : une chute annoncée ne débouche donc pas sur un bloc fermé. Les
imperfections, morceaux suspendus et raccords volontairement cassés font partie de lesthétique,
mais les portes des couloirs restent traversables. Les lits de sortie générés sont désormais rares.
La Backroom ne possède ni ciel, ni soleil, ni lune, ni étoiles, ni lumière ambiante. Son biome ne
déclare aucune créature et le serveur retire aussi toute entité de catégorie monstre qui y serait
injectée par un autre système. Les matériaux très marqués dAnother World napparaissent que
ponctuellement aux anomalies. Aucun bloc précieux ne sert de matériau structurel ; de très rares
blocs de diamant restent cachés dans les fondations.
## Exploitation
+28 -14
View File
@@ -199,7 +199,7 @@ tasks.register("verifyBlackMarket") {
def questData = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardData.java").text
def quests = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java").text
def inventoryView = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java").text
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| !data.contains('SAVE_KEY = "black_market"') || !data.contains("DATA_VERSION = 2")
|| !data.contains("ItemStack.CODEC.fieldOf(\"item\")") || !data.contains("replaceListing")
|| !service.contains("LISTING_FEE_RUBIES = 1") || !service.contains("COMMISSION_PERCENT = 5")
@@ -2915,7 +2915,7 @@ tasks.register("verifyShopCurrencyRowsRelease") {
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java").text
def shop = file(
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.117"
|| migration.shop_currency_inventory?.all_unlocked_rows_counted != true
|| migration.shop_currency_inventory?.overflow_rows_counted != true
@@ -3081,7 +3081,7 @@ tasks.register("verifyProgressivePauseMenu") {
"pack/migrations/26.2.0-alpha.190-progressive-pause-menu-to-alpha.191.json"))
def pauseMenu = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/PauseScreenMixin.java").text
def costs = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/progression/SanctuaryProgressionCosts.java").text
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.target?.pack_version != "26.2.0-alpha.191"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.109"
|| migration.pause_menu?.progression_full_width != true
@@ -3119,7 +3119,7 @@ tasks.register("verifyShopPricingLayout") {
"pack/migrations/26.2.0-alpha.192-shop-pricing-layout-to-alpha.193.json"))
def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryShopScreen.java").text
def progression = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopProgression.java").text
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.110"
|| migration.shop_layout?.locked_offer_buttons != 6
|| migration.shop_layout?.button_slots != [4, 5, 6, 7, 8, 9]
@@ -3150,7 +3150,7 @@ tasks.register("verifyShopCsvBossGates") {
"pack/migrations/26.2.0-alpha.194-shop-csv-boss-gates-to-alpha.195.json"))
def provider = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopCatalogProvider.java").text
def catalog = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.target?.pack_version != "26.2.0-alpha.195"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|| migration.compatibility?.sanctuary_data_version != 11
@@ -3181,7 +3181,7 @@ tasks.register("verifyDeliveryBoxOrientation") {
def blockstate = new JsonSlurper().parse(
file("src/main/resources/assets/sanctuary/blockstates/delivery_box.json"))
def expectedVariants = ["facing=north", "facing=east", "facing=south", "facing=west"].toSet()
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|| migration.mailbox?.id != "sanctuary:delivery_box"
|| migration.mailbox?.horizontal_facing != true
@@ -3209,7 +3209,7 @@ tasks.register("verifyInventoryShiftClick") {
def mixin = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/AbstractContainerMenuMixin.java").text
def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AbstractContainerScreenMixin.java").text
def gui = file("INVENTORY_GUI.md").text
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| !transfer.contains("shouldAppendOverflow")
|| !transfer.contains("isVanillaStorageRange")
|| !transfer.contains("hotbarStart")
@@ -3255,7 +3255,7 @@ tasks.register("verifyQuestRequests") {
new JsonSlurper().parse(file("src/main/resources/data/sanctuary/loot_table/blocks/${path}.json"))
.pools[0].entries[0].name
}
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|| migration.quest_requests?.save_key != "quest_requests"
|| migration.quest_requests?.data_version != 1
@@ -3313,7 +3313,7 @@ tasks.register("verifyQuestRequestContracts") {
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
[(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))]
}
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.198"
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.114"
|| migration.target?.pack_version != "26.2.0-alpha.199"
@@ -3382,7 +3382,7 @@ tasks.register("verifyQuestRequestEscrow") {
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
[(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))]
}
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.source?.pack_version != "26.2.0-alpha.200"
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.115"
|| migration.target?.pack_version != "26.2.0-alpha.201"
@@ -3461,7 +3461,7 @@ tasks.register("verifyCraftableBannerCape") {
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json"))
}
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.130"
|| migration.white_cape?.crafting_material_count != 6
|| migration.white_cape?.maximum_pattern_layers != 6
@@ -3516,7 +3516,7 @@ tasks.register("verifyAlphaIndev") {
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json"))
}
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.122"
|| migration.alpha_world?.dimension != "sanctuary:alpha"
|| migration.alpha_world?.legacy_dimension != "portals:alpha"
@@ -3550,12 +3550,15 @@ tasks.register("verifyBackroom") {
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java"),
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlockEntity.java"),
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java"),
file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java"),
file("src/main/resources/data/minecraft/worldgen/world_preset/normal.json"),
file("src/main/resources/data/sanctuary/worldgen/world_preset/overworld.json"),
file("src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json"))
doLast {
def migration = new JsonSlurper().parse(rootProject.file("pack/migrations/sanctuary-backroom-data-v14.json"))
def dimensionType = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/dimension_type/backroom.json"))
def noiseSettings = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/worldgen/noise_settings/backroom.json"))
def biome = new JsonSlurper().parse(file("src/main/resources/data/sanctuary/worldgen/biome/backroom.json"))
def presets = [
file("src/main/resources/data/minecraft/worldgen/world_preset/normal.json"),
file("src/main/resources/data/sanctuary/worldgen/world_preset/overworld.json"),
@@ -3567,12 +3570,13 @@ tasks.register("verifyBackroom") {
def chest = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomChestService.java").text
def mailbox = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlockEntity.java").text
def architecture = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomArchitectureFeature.java").text
def layout = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/BackroomLayout.java").text
def materialApi = file(
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json"))
}
if (project.version.toString() != "0.0.0-alpha.131"
if (project.version.toString() != "0.0.0-alpha.132"
|| migration.source_data_version != 13 || migration.target_data_version != 14
|| migration.integration_modules?.anotherworld?.source != "0.0.0-alpha.56"
|| migration.integration_modules?.anotherworld?.target != "0.0.0-alpha.57"
@@ -3580,6 +3584,12 @@ tasks.register("verifyBackroom") {
|| presets.any { it.dimensions?.get("sanctuary:backroom") == null }
|| dimensionType.attributes?.get("minecraft:gameplay/bed_rule")?.can_set_spawn != "never"
|| dimensionType.attributes?.get("minecraft:gameplay/bed_rule")?.can_sleep != "never"
|| dimensionType.min_y != -64 || dimensionType.height != 448 || dimensionType.logical_height != 448
|| dimensionType.ambient_light != 0.0 || dimensionType.has_ceiling != true
|| dimensionType.attributes?.get("minecraft:visual/fog_color") != "#000000"
|| noiseSettings.noise?.min_y != -64 || noiseSettings.noise?.height != 448
|| noiseSettings.disable_mob_generation != true
|| biome.creature_spawn_probability != 0.0 || !biome.spawners?.monster?.isEmpty()
|| !data.contains("MAX_LOST_ITEMS = 16_384") || !data.contains("MAX_CHEST_STATES = 65_536")
|| !data.contains("PERSONAL_RESERVATION_TICKS = 24_000L")
|| !data.contains("indexOfLoss(marker)") || !data.contains("BACKROOM_LOSS_COMPONENT")
@@ -3587,17 +3597,21 @@ tasks.register("verifyBackroom") {
|| !service.contains("ARRIVAL_RADIUS = 2_000") || !service.contains("insertTransaction")
|| !service.contains("AUTHORIZED_TRANSITIONS")
|| !service.contains("level -> !isBackroom(level)")
|| !service.contains("MobCategory.MONSTER") || !service.contains("Blocks.TORCH")
|| !mailbox.contains("MAX_PROCESSED_TRANSACTIONS = 128")
|| !chest.contains("REFRESH_TICKS = 12_000L") || !chest.contains("ordinal == 3")
|| !architecture.contains("PLAYABLE_RADIUS = 2_000")
|| !architecture.contains("BackroomMaterialApi.anomalyMaterial()")
|| !layout.contains("WORLD_MIN_Y = -64") || !layout.contains("WORLD_MAX_Y_EXCLUSIVE = 384")
|| !layout.contains("LEVEL_0(295, 383)") || !layout.contains("LEVEL_4(-64, 27)")
|| !layout.contains("mazeConnected") || !layout.contains("RoomCorner")
|| !materialApi.contains("registerAnomalyMaterial")
|| locales.any { !it.containsKey("dimension.sanctuary.backroom")
|| !it.containsKey("message.sanctuary.backroom.bed_entry_only")
|| !it.containsKey("message.sanctuary.backroom.registry_busy")
|| !it.containsKey("message.sanctuary.backroom.mailbox_full")
|| !it.containsKey("message.sanctuary.backroom.exited_with_losses") }) {
throw new GradleException("Sanctuary alpha.131 Backroom contract is incomplete")
throw new GradleException("Sanctuary alpha.132 Backroom contract is incomplete")
}
}
}
@@ -3,12 +3,15 @@ package fr.koka99cab.sanctuary26.sanctuary.backroom;
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopData;
import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.commands.CommandSourceStack;
@@ -18,8 +21,9 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.portal.TeleportTransition;
import net.minecraft.world.phys.Vec3;
@@ -36,6 +40,9 @@ public final class BackroomService {
if (initialized) return;
initialized = true;
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> reconcile(handler.player));
ServerEntityEvents.ENTITY_LOAD.register((entity, level) -> {
if (isBackroom(level) && entity.getType().getCategory() == MobCategory.MONSTER) entity.discard();
});
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
dispatcher.register(Commands.literal("backroom")
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
@@ -101,6 +108,7 @@ public final class BackroomService {
data.finishEntry(player.getUUID());
BlockPos arrival = chooseArrival(destination, player.getUUID(), data);
placeArrivalTorch(destination, arrival);
data.beginExpedition(player.getUUID(), arrival.asLong());
TeleportTransition transition = new TeleportTransition(destination, Vec3.atBottomCenterOf(arrival),
Vec3.ZERO, player.getYRot(), player.getXRot(),
@@ -166,15 +174,14 @@ public final class BackroomService {
private static BlockPos chooseArrival(ServerLevel level, UUID player, BackroomData data) {
RandomSource random = level.getRandom();
List<Long> recent = data.recentArrivals(player);
int arrivalY = BackroomLayout.ENTRY_FLOOR_Y + 1;
BlockPos best = null;
for (int attempt = 0; attempt < ARRIVAL_ATTEMPTS; attempt++) {
double angle = random.nextDouble() * Math.PI * 2.0D;
double radius = Math.sqrt(random.nextDouble()) * ARRIVAL_RADIUS;
int x = Mth.floor(Math.cos(angle) * radius);
int z = Mth.floor(Math.sin(angle) * radius);
int y = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z),
level.getMinY() + 2, level.getMaxY() - 3);
BlockPos candidate = new BlockPos(x, y, z);
BlockPos candidate = new BlockPos(x, arrivalY, z);
best = candidate;
if (farFromRecent(candidate, recent) && safe(level, candidate)) return candidate;
}
@@ -182,9 +189,7 @@ public final class BackroomService {
if (fallback != null) return fallback;
fallback = findSafeNear(level, BlockPos.ZERO, 48);
if (fallback != null) return fallback;
int originY = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, 0, 0),
level.getMinY() + 2, level.getMaxY() - 3);
return new BlockPos(0, originY, 0);
return new BlockPos(8, arrivalY, 8);
}
private static boolean farFromRecent(BlockPos candidate, List<Long> recent) {
@@ -207,15 +212,32 @@ public final class BackroomService {
if (ring > 0 && Math.abs(dx) != ring && Math.abs(dz) != ring) continue;
int x = center.getX() + dx, z = center.getZ() + dz;
if ((long) x * x + (long) z * z > (long) ARRIVAL_RADIUS * ARRIVAL_RADIUS) continue;
int y = Mth.clamp(level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z),
level.getMinY() + 2, level.getMaxY() - 3);
BlockPos candidate = new BlockPos(x, y, z);
BlockPos candidate = new BlockPos(x, BackroomLayout.ENTRY_FLOOR_Y + 1, z);
if (safe(level, candidate)) return candidate;
}
}
return null;
}
private static void placeArrivalTorch(ServerLevel level, BlockPos arrival) {
level.setBlock(arrival.below(), Blocks.BIRCH_PLANKS.defaultBlockState(), 3);
level.setBlock(arrival, Blocks.AIR.defaultBlockState(), 3);
level.setBlock(arrival.above(), Blocks.AIR.defaultBlockState(), 3);
for (var direction : net.minecraft.core.Direction.Plane.HORIZONTAL) {
BlockPos torch = arrival.relative(direction);
if (!BackroomArchitectureFeature.withinPlayableArea(torch.getX(), torch.getZ())) continue;
if (level.getBlockState(torch).isAir()
&& level.getBlockState(torch.below()).isCollisionShapeFullBlock(level, torch.below())) {
level.setBlock(torch, Blocks.TORCH.defaultBlockState(), 3);
return;
}
}
BlockPos torch = arrival.east();
if (!BackroomArchitectureFeature.withinPlayableArea(torch.getX(), torch.getZ())) torch = arrival.west();
level.setBlock(torch.below(), Blocks.BIRCH_PLANKS.defaultBlockState(), 3);
level.setBlock(torch, Blocks.TORCH.defaultBlockState(), 3);
}
private static void notify(ServerPlayer player, String translationKey) {
player.sendOverlayMessage(Component.translatable(translationKey));
}
@@ -1,8 +1,9 @@
package fr.koka99cab.sanctuary26.sanctuary.worldgen.feature;
import com.mojang.serialization.Codec;
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
import fr.koka99cab.sanctuary26.sanctuary.api.backroom.BackroomMaterialApi;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.BackroomLevel;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.RoomCorner;
import java.util.Random;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@@ -19,20 +20,42 @@ import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext;
import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;
/**
* Deterministic, chunk-local architecture for the persistent Backroom.
* Macro cells select authored palettes; randomness only varies plans inside a coherent family.
* Five deterministic Backroom levels stacked from Y=384 to Y=-64. Generation is chunk-local,
* while every wall, doorway, highway and hole is derived from world coordinates so chunk seams
* remain open and coherent.
*/
public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfiguration> {
public static final int PLAYABLE_RADIUS = 2_000;
private static final int CHUNK_SIZE = 16;
private static final int MACRO_CHUNKS = 8;
private static final int[] THRESHOLD_DECKS = {295, 313, 331, 349, 367};
private static final int[] ARCHIVE_DECKS = {206, 228, 250, 272};
private static final int[] UTILITY_DECKS = {28, 46, 64, 82, 100};
private static final int[] LANDMARK_FLOORS = {349, 250, 117, 64, BackroomLayout.MAZE_FLOOR_Y};
private static final BlockState CONCRETE_GRAY = Blocks.CONCRETE.pick(DyeColor.GRAY).defaultBlockState();
private static final BlockState CONCRETE_LIGHT = Blocks.CONCRETE.pick(DyeColor.LIGHT_GRAY).defaultBlockState();
private static final BlockState CONCRETE_WHITE = Blocks.CONCRETE.pick(DyeColor.WHITE).defaultBlockState();
private static final BlockState GLASS_BLACK = Blocks.STAINED_GLASS.pick(DyeColor.BLACK).defaultBlockState();
private static final BlockState GLASS_BROWN = Blocks.STAINED_GLASS.pick(DyeColor.BROWN).defaultBlockState();
private static final BlockState GLASS_CYAN = Blocks.STAINED_GLASS.pick(DyeColor.CYAN).defaultBlockState();
private static final BlockState CLAY_BROWN = Blocks.DYED_TERRACOTTA.pick(DyeColor.BROWN).defaultBlockState();
private static final BlockState CLAY_ORANGE = Blocks.DYED_TERRACOTTA.pick(DyeColor.ORANGE).defaultBlockState();
private static final BlockState CLAY_YELLOW = Blocks.DYED_TERRACOTTA.pick(DyeColor.YELLOW).defaultBlockState();
private static final BlockState LEAVES = Blocks.OAK_LEAVES.defaultBlockState().setValue(LeavesBlock.PERSISTENT, true);
private static final BlockState[] CORNER_MATERIALS = {
Blocks.BRICKS.defaultBlockState(),
Blocks.MUD_BRICKS.defaultBlockState(),
Blocks.STONE_BRICKS.defaultBlockState(),
CLAY_YELLOW,
Blocks.SMOOTH_SANDSTONE.defaultBlockState()
};
private static final BlockState[] GLITCH_MATERIALS = {
Blocks.BRICKS.defaultBlockState(),
Blocks.MUD_BRICKS.defaultBlockState(),
Blocks.STONE_BRICKS.defaultBlockState(),
Blocks.CRACKED_STONE_BRICKS.defaultBlockState(),
CLAY_BROWN
};
public BackroomArchitectureFeature(Codec<NoneFeatureConfiguration> codec) {
super(codec);
@@ -47,17 +70,15 @@ public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfig
int startZ = chunkZ * CHUNK_SIZE;
if (!withinPlayableArea(startX + 8, startZ + 8)) return false;
long seed = mix(level.getSeed(), chunkX, chunkZ);
Random random = new Random(seed);
Architecture architecture = architecture(level.getSeed(), chunkX, chunkZ);
switch (architecture) {
case CITY -> city(level, startX, startZ, random);
case OFFICES -> offices(level, startX, startZ, random);
case PLAINS -> plains(level, startX, startZ, random);
case AGGREGATES -> aggregates(level, startX, startZ, random);
case ROTATED -> rotated(level, startX, startZ, random);
}
placeLootLandmarks(level, startX, startZ, random, architecture);
long worldSeed = level.getSeed();
Random random = new Random(fingerprint(worldSeed, chunkX, chunkZ));
buildLevelShells(level, worldSeed, startX, startZ);
buildThreshold(level, worldSeed, startX, startZ);
buildArchives(level, worldSeed, startX, startZ);
buildCity(level, worldSeed, startX, startZ, chunkX, chunkZ);
buildUtilities(level, worldSeed, startX, startZ);
buildGlitchMaze(level, worldSeed, startX, startZ);
placeLootLandmarks(level, startX, startZ, random);
placeRareAnomaly(level, startX, startZ, random);
return true;
}
@@ -66,161 +87,387 @@ public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfig
return (long) x * x + (long) z * z <= (long) PLAYABLE_RADIUS * PLAYABLE_RADIUS;
}
public static Architecture architecture(long worldSeed, int chunkX, int chunkZ) {
int macroX = Math.floorDiv(chunkX, MACRO_CHUNKS);
int macroZ = Math.floorDiv(chunkZ, MACRO_CHUNKS);
int index = Math.floorMod((int) mix(worldSeed ^ 0x4f1bbcdcL, macroX, macroZ), Architecture.values().length);
return Architecture.values()[index];
}
/** Stable fingerprint used to prevent palette selection drift. */
/** Stable fingerprint used to prevent authored layout drift. */
public static long fingerprint(long worldSeed, int chunkX, int chunkZ) {
return mix(worldSeed ^ architecture(worldSeed, chunkX, chunkZ).ordinal(), chunkX, chunkZ);
return mix(worldSeed ^ 0x4f1bbcdcL, chunkX, chunkZ);
}
private static void city(WorldGenLevel level, int x0, int z0, Random random) {
int y = 64;
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
boolean road = Math.floorMod(x, 32) < 5 || Math.floorMod(z, 32) < 5;
set(level, x, y, z, road ? CONCRETE_GRAY : CONCRETE_LIGHT);
if (road && (Math.floorMod(x, 8) == 0 || Math.floorMod(z, 8) == 0)) set(level, x, y, z, Blocks.SMOOTH_STONE.defaultBlockState());
}
if (Math.floorMod(x0, 32) < 5 || Math.floorMod(z0, 32) < 5) return;
int minX = x0 + 2, maxX = x0 + 13, minZ = z0 + 2, maxZ = z0 + 13;
int height = 18 + random.nextInt(42);
for (int floor = 0; floor <= height; floor++) {
int py = y + 1 + floor;
boolean slab = floor == 0 || floor == height || floor % 5 == 0;
for (int x = minX; x <= maxX; x++) for (int z = minZ; z <= maxZ; z++) {
boolean edge = x == minX || x == maxX || z == minZ || z == maxZ;
if (slab) set(level, x, py, z, CONCRETE_LIGHT);
else if (edge) set(level, x, py, z, floor % 5 == 1 ? CONCRETE_WHITE : GLASS_CYAN);
private static void buildLevelShells(WorldGenLevel level, long seed, int x0, int z0) {
for (BackroomLevel zone : BackroomLevel.values()) {
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
if (!floorHole(seed, zone, x, z)) set(level, x, zone.floorY(), z, floorMaterial(zone));
if (!ceilingHole(seed, zone, x, z)) set(level, x, zone.ceilingY(), z, ceilingMaterial(zone));
}
}
}
private static void offices(WorldGenLevel level, int x0, int z0, Random random) {
for (int baseY : new int[]{72, 84, 96}) {
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
set(level, x, baseY, z, Blocks.OAK_PLANKS.defaultBlockState());
set(level, x, baseY + 9, z, CLAY_ORANGE);
boolean wall = Math.floorMod(x, 12) == 0 || Math.floorMod(z, 12) == 0;
if (wall) for (int y = baseY + 1; y < baseY + 9; y++) {
BlockState material = y == baseY + 4 ? Blocks.STAINED_GLASS.pick(DyeColor.BROWN).defaultBlockState() : CLAY_BROWN;
set(level, x, y, z, material);
private static boolean floorHole(long seed, BackroomLevel zone, int x, int z) {
return zone == BackroomLevel.LEVEL_4
? BackroomLayout.voidHole(seed, x, z)
: BackroomLayout.transitionHole(seed, zone.ordinal(), x, z);
}
private static boolean ceilingHole(long seed, BackroomLevel zone, int x, int z) {
return zone != BackroomLevel.LEVEL_0
&& BackroomLayout.transitionHole(seed, zone.ordinal() - 1, x, z);
}
private static BlockState floorMaterial(BackroomLevel zone) {
return switch (zone) {
case LEVEL_0 -> Blocks.BIRCH_PLANKS.defaultBlockState();
case LEVEL_1 -> Blocks.DARK_OAK_PLANKS.defaultBlockState();
case LEVEL_2 -> CONCRETE_GRAY;
case LEVEL_3 -> Blocks.POLISHED_ANDESITE.defaultBlockState();
case LEVEL_4 -> Blocks.STONE_BRICKS.defaultBlockState();
};
}
private static BlockState ceilingMaterial(BackroomLevel zone) {
return switch (zone) {
case LEVEL_0 -> CLAY_YELLOW;
case LEVEL_1 -> CONCRETE_LIGHT;
case LEVEL_2 -> CONCRETE_GRAY;
case LEVEL_3 -> Blocks.BRICKS.defaultBlockState();
case LEVEL_4 -> Blocks.MUD_BRICKS.defaultBlockState();
};
}
private static void buildThreshold(WorldGenLevel level, long seed, int x0, int z0) {
int cellX = Math.floorDiv(x0, CHUNK_SIZE);
int cellZ = Math.floorDiv(z0, CHUNK_SIZE);
for (int deck = 0; deck < THRESHOLD_DECKS.length; deck++) {
int floorY = THRESHOLD_DECKS[deck];
int roofY = deck + 1 < THRESHOLD_DECKS.length ? THRESHOLD_DECKS[deck + 1]
: BackroomLevel.LEVEL_0.ceilingY();
if (deck > 0) fillDeck(level, seed, floorY, x0, z0, Blocks.BIRCH_PLANKS.defaultBlockState());
int pattern = BackroomLayout.roomPattern(seed, BackroomLevel.LEVEL_0, cellX, cellZ, deck);
BlockState wall = pattern % 2 == 0 ? CLAY_YELLOW : Blocks.SMOOTH_SANDSTONE.defaultBlockState();
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
int localX = Math.floorMod(x, CHUNK_SIZE), localZ = Math.floorMod(z, CHUNK_SIZE);
boolean boundaryX = localX == 0, boundaryZ = localZ == 0;
if (!boundaryX && !boundaryZ) continue;
boolean doorway = boundaryX != boundaryZ && doorway(boundaryX ? localZ : localX, 7, 9);
if (doorway) continue;
for (int y = floorY + 1; y < roofY; y++) set(level, x, y, z, wall);
}
placeThresholdPattern(level, x0, floorY, z0, roofY, pattern, wall);
for (RoomCorner corner : RoomCorner.values()) {
placeCorner(level, seed, x0, floorY, z0, cellX, cellZ, deck, corner);
}
}
}
private static void placeThresholdPattern(WorldGenLevel level, int x0, int floorY, int z0,
int roofY, int pattern, BlockState wall) {
switch (pattern) {
case 1 -> partitionX(level, x0 + 5, floorY, roofY, z0 + 2, z0 + 13, z0 + 8, wall, false);
case 2 -> partitionZ(level, z0 + 10, floorY, roofY, x0 + 2, x0 + 13, x0 + 8, wall, false);
case 3 -> {
partitionX(level, x0 + 5, floorY, roofY, z0 + 2, z0 + 11, z0 + 7, wall, false);
partitionZ(level, z0 + 10, floorY, roofY, x0 + 5, x0 + 13, x0 + 10, wall, false);
}
case 4 -> partitionX(level, x0 + 8, floorY, roofY, z0 + 2, z0 + 13, z0 + 8, wall, true);
case 5 -> {
for (int x : new int[]{x0 + 5, x0 + 10}) for (int z : new int[]{z0 + 5, z0 + 10}) {
for (int y = floorY + 1; y <= Math.min(floorY + 6, roofY - 1); y++) {
set(level, x, y, z, y % 2 == 0 ? CLAY_ORANGE : CLAY_BROWN);
}
}
}
if (random.nextInt(3) == 0) {
int deskX = x0 + 5 + random.nextInt(6), deskZ = z0 + 5 + random.nextInt(6);
set(level, deskX, baseY + 1, deskZ, Blocks.OAK_SLAB.defaultBlockState());
set(level, deskX + 1, baseY + 1, deskZ, Blocks.BOOKSHELF.defaultBlockState());
set(level, deskX, baseY + 1, deskZ + 1, Blocks.CARPET.pick(DyeColor.BROWN).defaultBlockState());
}
default -> { }
}
}
private static void plains(WorldGenLevel level, int x0, int z0, Random random) {
int baseY = 68 + random.nextInt(3);
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) {
set(level, x, baseY - 2, z, Blocks.STONE.defaultBlockState());
set(level, x, baseY - 1, z, Blocks.DIRT.defaultBlockState());
set(level, x, baseY, z, Blocks.GRASS_BLOCK.defaultBlockState());
}
if (random.nextInt(3) == 0) tree(level, x0 + 4 + random.nextInt(8), baseY + 1, z0 + 4 + random.nextInt(8));
if (random.nextInt(7) == 0) {
for (int x = x0 + 3; x <= x0 + 11; x++) for (int z = z0 + 3; z <= z0 + 11; z++) {
boolean edge = x == x0 + 3 || x == x0 + 11 || z == z0 + 3 || z == z0 + 11;
if (edge) for (int y = baseY + 1; y <= baseY + 4; y++) set(level, x, y, z, Blocks.MUD_BRICKS.defaultBlockState());
}
}
}
private static void aggregates(WorldGenLevel level, int x0, int z0, Random random) {
int baseY = 74 + random.nextInt(10);
platform(level, x0, z0, baseY, CONCRETE_LIGHT);
boxRoom(level, x0 + 2, baseY + 1, z0 + 2, x0 + 12, baseY + 7, z0 + 12,
Blocks.SMOOTH_STONE.defaultBlockState(), Blocks.OAK_PLANKS.defaultBlockState());
if (random.nextBoolean()) {
int upper = baseY + 20 + random.nextInt(12);
platform(level, x0 + 4, z0 + 4, upper, CLAY_ORANGE);
for (int y = baseY + 1; y <= upper; y++) set(level, x0 + 8, y, z0 + 8, Blocks.IRON_CHAIN.defaultBlockState());
}
}
private static void rotated(WorldGenLevel level, int x0, int z0, Random random) {
int y0 = 70 + random.nextInt(12);
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) set(level, x, y0, z, CONCRETE_LIGHT);
boolean alongX = random.nextBoolean();
for (int depth = 2; depth <= 13; depth++) for (int y = y0 + 1; y <= y0 + 13; y++) {
int x = alongX ? x0 + depth : x0 + 3;
int z = alongX ? z0 + 3 : z0 + depth;
BlockState state = (y == y0 + 1 || y == y0 + 13 || depth == 2 || depth == 13)
? CLAY_BROWN : Blocks.AIR.defaultBlockState();
set(level, x, y, z, state);
}
// Furniture fixed to the vertical "floor" sells the rotated-room illusion without changing gravity.
for (int offset = 4; offset <= 10; offset += 3) {
int x = alongX ? x0 + offset : x0 + 4;
int z = alongX ? z0 + 4 : z0 + offset;
set(level, x, y0 + 4, z, Blocks.BOOKSHELF.defaultBlockState());
}
}
private static void placeLootLandmarks(WorldGenLevel level, int x0, int z0, Random random, Architecture architecture) {
int y = switch (architecture) {
case CITY -> 65;
case OFFICES -> 73;
case PLAINS -> 72;
case AGGREGATES -> 83;
case ROTATED -> 84;
private static void placeCorner(WorldGenLevel level, long seed, int x0, int floorY, int z0,
int cellX, int cellZ, int deck, RoomCorner corner) {
int x = switch (corner) {
case NORTH_WEST, SOUTH_WEST -> x0 + 2;
case NORTH_EAST, SOUTH_EAST -> x0 + 13;
};
int x = x0 + 7, z = z0 + 7;
while (!level.getBlockState(new BlockPos(x, y, z)).isAir() && y < 180) y++;
while (!level.getBlockState(new BlockPos(x, y - 1, z)).isCollisionShapeFullBlock(level, new BlockPos(x, y - 1, z)) && y > 2) y--;
if (random.nextInt(5) == 0) set(level, x, y, z, Blocks.CHEST.defaultBlockState());
if (random.nextInt(17) == 0) placeBed(level, x0 + 9, y, z0 + 8);
int z = switch (corner) {
case NORTH_WEST, NORTH_EAST -> z0 + 2;
case SOUTH_WEST, SOUTH_EAST -> z0 + 13;
};
BlockState material = CORNER_MATERIALS[BackroomLayout.cornerVariant(
seed ^ (long) deck * 31L, BackroomLevel.LEVEL_0, cellX, cellZ, corner)];
for (int y = floorY + 1; y <= floorY + 4; y++) set(level, x, y, z, material);
int towardX = corner == RoomCorner.NORTH_WEST || corner == RoomCorner.SOUTH_WEST ? 1 : -1;
int towardZ = corner == RoomCorner.NORTH_WEST || corner == RoomCorner.NORTH_EAST ? 1 : -1;
for (int distance = 1; distance <= 2; distance++) {
set(level, x + towardX * distance, floorY + 1, z, material);
set(level, x, floorY + 1, z + towardZ * distance, material);
}
}
private static void buildArchives(WorldGenLevel level, long seed, int x0, int z0) {
int macroX = Math.floorDiv(x0, 64), macroZ = Math.floorDiv(z0, 64);
for (int deck = 0; deck < ARCHIVE_DECKS.length; deck++) {
int floorY = ARCHIVE_DECKS[deck];
int roofY = deck + 1 < ARCHIVE_DECKS.length ? ARCHIVE_DECKS[deck + 1]
: BackroomLevel.LEVEL_1.ceilingY();
if (deck > 0) fillDeck(level, seed, floorY, x0, z0, Blocks.DARK_OAK_PLANKS.defaultBlockState());
boolean library = Math.floorMod((int) fingerprint(seed ^ deck, macroX, macroZ), 3) != 0;
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
int localX = Math.floorMod(x, 32), localZ = Math.floorMod(z, 32);
boolean wallX = localX == 0, wallZ = localZ == 0;
if (!wallX && !wallZ) continue;
boolean open = wallX != wallZ && doorway(wallX ? localZ : localX, 14, 18);
if (open) continue;
for (int y = floorY + 1; y < roofY; y++) {
BlockState state = y >= floorY + 5 && y <= floorY + 9 ? GLASS_BROWN : CONCRETE_LIGHT;
set(level, x, y, z, state);
}
}
if (library) buildLibraryRoom(level, x0, floorY, z0, roofY);
else buildOfficeRoom(level, seed, x0, floorY, z0, roofY, deck);
}
}
private static void buildLibraryRoom(WorldGenLevel level, int x0, int floorY, int z0, int roofY) {
for (int x = x0 + 1; x < x0 + 15; x++) for (int z = z0 + 1; z < z0 + 15; z++) {
int roomX = Math.floorMod(x, 32), roomZ = Math.floorMod(z, 32);
boolean shelf = (Math.floorMod(x, 8) == 3 || Math.floorMod(x, 8) == 4)
&& roomX > 3 && roomX < 28 && (roomZ < 12 || roomZ > 20);
if (!shelf) continue;
for (int y = floorY + 1; y <= Math.min(floorY + 6, roofY - 1); y++) {
set(level, x, y, z, Blocks.BOOKSHELF.defaultBlockState());
}
}
if (Math.floorMod(x0 + z0 + floorY, 3) == 0) {
for (int x = x0 + 6; x <= x0 + 9; x++) set(level, x, floorY + 1, z0 + 8,
Blocks.OAK_SLAB.defaultBlockState());
}
}
private static void buildOfficeRoom(WorldGenLevel level, long seed, int x0, int floorY, int z0,
int roofY, int deck) {
boolean alongX = (fingerprint(seed, Math.floorDiv(x0, 16), Math.floorDiv(z0, 16)) + deck & 1L) == 0L;
if (alongX) partitionX(level, x0 + 8, floorY, roofY, z0 + 1, z0 + 14, z0 + 8, CONCRETE_WHITE, true);
else partitionZ(level, z0 + 8, floorY, roofY, x0 + 1, x0 + 14, x0 + 8, CONCRETE_WHITE, true);
set(level, x0 + 4, floorY + 1, z0 + 4, Blocks.OAK_SLAB.defaultBlockState());
set(level, x0 + 5, floorY + 1, z0 + 4, Blocks.BOOKSHELF.defaultBlockState());
set(level, x0 + 11, floorY + 1, z0 + 11, Blocks.OAK_SLAB.defaultBlockState());
}
private static void buildCity(WorldGenLevel level, long seed, int x0, int z0, int chunkX, int chunkZ) {
boolean roadFree = true;
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
if (BackroomLayout.transitionHole(seed, BackroomLevel.LEVEL_2.ordinal(), x, z)) continue;
boolean highway = BackroomLayout.cityHighway(x, z);
if (highway) roadFree = false;
BlockState surface = highway ? CONCRETE_GRAY : CONCRETE_LIGHT;
if (highway && (Math.floorMod(x, 8) == 0 || Math.floorMod(z, 8) == 0)) surface = CONCRETE_WHITE;
set(level, x, BackroomLevel.LEVEL_2.floorY(), z, surface);
}
boolean park = roadFree && BackroomLayout.cityPark(seed, chunkX, chunkZ);
if (park) {
buildPark(level, seed, x0, z0);
return;
}
if (!roadFree) return;
Random random = new Random(fingerprint(seed ^ 0x514e7a2c39d8b60fL, chunkX, chunkZ));
int floorY = BackroomLevel.LEVEL_2.floorY();
int roofY = floorY + 24 + random.nextInt(56);
int minX = x0 + 1, maxX = x0 + 14, minZ = z0 + 1, maxZ = z0 + 14;
BlockState frame = random.nextBoolean() ? CONCRETE_LIGHT : Blocks.STONE_BRICKS.defaultBlockState();
BlockState glass = random.nextBoolean() ? GLASS_CYAN : GLASS_BLACK;
for (int y = floorY + 1; y <= roofY; y++) {
boolean slab = y == roofY || Math.floorMod(y - floorY, 7) == 0;
for (int x = minX; x <= maxX; x++) for (int z = minZ; z <= maxZ; z++) {
boolean edge = x == minX || x == maxX || z == minZ || z == maxZ;
boolean entrance = y <= floorY + 4 && (((x == minX || x == maxX) && doorway(z - z0, 7, 9))
|| ((z == minZ || z == maxZ) && doorway(x - x0, 7, 9)));
if (entrance) continue;
if (slab) set(level, x, y, z, frame);
else if (edge) set(level, x, y, z, Math.floorMod(y - floorY, 7) <= 2 ? frame : glass);
}
}
}
private static void buildPark(WorldGenLevel level, long seed, int x0, int z0) {
int floorY = BackroomLevel.LEVEL_2.floorY();
for (int x = x0 + 1; x < x0 + 15; x++) for (int z = z0 + 1; z < z0 + 15; z++) {
if (BackroomLayout.transitionHole(seed, BackroomLevel.LEVEL_2.ordinal(), x, z)) continue;
BlockState surface = x == x0 + 8 || z == z0 + 8
? Blocks.SMOOTH_STONE.defaultBlockState() : Blocks.GRASS_BLOCK.defaultBlockState();
set(level, x, floorY, z, surface);
}
Random random = new Random(fingerprint(seed ^ 0x1974acb65d203e8fL,
Math.floorDiv(x0, 16), Math.floorDiv(z0, 16)));
int trees = 1 + random.nextInt(2);
for (int index = 0; index < trees; index++) {
tree(level, x0 + 4 + random.nextInt(8), floorY + 1, z0 + 4 + random.nextInt(8));
}
}
private static void buildUtilities(WorldGenLevel level, long seed, int x0, int z0) {
for (int deck = 0; deck < UTILITY_DECKS.length; deck++) {
int floorY = UTILITY_DECKS[deck];
int roofY = deck + 1 < UTILITY_DECKS.length ? UTILITY_DECKS[deck + 1]
: BackroomLevel.LEVEL_3.ceilingY();
if (deck > 0) fillDeck(level, seed, floorY, x0, z0, Blocks.POLISHED_ANDESITE.defaultBlockState());
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
int localX = Math.floorMod(x, 24), localZ = Math.floorMod(z, 24);
boolean wallX = localX == 0, wallZ = localZ == 0;
if (wallX || wallZ) {
boolean open = wallX != wallZ && doorway(wallX ? localZ : localX, 10, 14);
if (!open) for (int y = floorY + 1; y < roofY; y++) {
BlockState state = y == floorY + 8 ? Blocks.IRON_BARS.defaultBlockState()
: (deck + Math.floorDiv(x + z, 24)) % 2 == 0
? Blocks.BRICKS.defaultBlockState() : Blocks.MUD_BRICKS.defaultBlockState();
set(level, x, y, z, state);
}
}
boolean overheadGrid = floorY + 12 < roofY
&& ((localX == 4 && localZ >= 7 && localZ <= 17)
|| (localZ == 4 && localX >= 7 && localX <= 17));
if (overheadGrid) set(level, x, floorY + 12, z, Blocks.IRON_BARS.defaultBlockState());
}
}
}
private static void buildGlitchMaze(WorldGenLevel level, long seed, int x0, int z0) {
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
if (!BackroomLayout.deckHole(seed, BackroomLayout.MAZE_FLOOR_Y, x, z)) {
set(level, x, BackroomLayout.MAZE_FLOOR_Y, z, Blocks.STONE_BRICKS.defaultBlockState());
}
if (!BackroomLayout.mazeRoofHole(seed, x, z)) {
set(level, x, BackroomLayout.MAZE_ROOF_Y, z, Blocks.MUD_BRICKS.defaultBlockState());
}
for (int y = BackroomLevel.LEVEL_4.floorY() + 1; y < BackroomLevel.LEVEL_4.ceilingY(); y++) {
if (y >= BackroomLayout.MAZE_FLOOR_Y && y <= BackroomLayout.MAZE_ROOF_Y) continue;
boolean edgeX = Math.floorMod(x, 8) == 0;
boolean edgeY = Math.floorMod(y - BackroomLevel.LEVEL_4.floorY(), 8) == 0;
boolean edgeZ = Math.floorMod(z, 8) == 0;
if ((edgeX && edgeY) || (edgeX && edgeZ) || (edgeY && edgeZ)) {
set(level, x, y, z, glitchMaterial(seed, x, y, z));
}
}
}
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
boolean wallX = Math.floorMod(x, CHUNK_SIZE) == 0;
boolean wallZ = Math.floorMod(z, CHUNK_SIZE) == 0;
if (!wallX && !wallZ) continue;
boolean open = false;
if (wallX != wallZ) {
if (wallX) {
int rightX = Math.floorDiv(x, CHUNK_SIZE), cellZ = Math.floorDiv(z, CHUNK_SIZE);
boolean connected = BackroomLayout.mazeConnected(seed, rightX - 1, cellZ, rightX, cellZ);
boolean merged = BackroomLayout.mergedMazeRooms(seed, rightX - 1, cellZ, rightX, cellZ);
open = merged || connected && doorway(Math.floorMod(z, CHUNK_SIZE), 7, 9);
} else {
int cellX = Math.floorDiv(x, CHUNK_SIZE), southZ = Math.floorDiv(z, CHUNK_SIZE);
boolean connected = BackroomLayout.mazeConnected(seed, cellX, southZ - 1, cellX, southZ);
boolean merged = BackroomLayout.mergedMazeRooms(seed, cellX, southZ - 1, cellX, southZ);
open = merged || connected && doorway(Math.floorMod(x, CHUNK_SIZE), 7, 9);
}
}
if (open) continue;
for (int y = BackroomLayout.MAZE_FLOOR_Y + 1; y < BackroomLayout.MAZE_ROOF_Y; y++) {
BlockState state = y >= BackroomLayout.MAZE_FLOOR_Y + 4
&& y <= BackroomLayout.MAZE_FLOOR_Y + 6 ? GLASS_BLACK : glitchMaterial(seed, x, y, z);
set(level, x, y, z, state);
}
}
int cellX = Math.floorDiv(x0, CHUNK_SIZE), cellZ = Math.floorDiv(z0, CHUNK_SIZE);
int pattern = BackroomLayout.roomPattern(seed, BackroomLevel.LEVEL_4, cellX, cellZ, 0);
BlockState wall = GLITCH_MATERIALS[Math.floorMod(pattern, GLITCH_MATERIALS.length)];
placeThresholdPattern(level, x0, BackroomLayout.MAZE_FLOOR_Y, z0,
BackroomLayout.MAZE_ROOF_Y, pattern, wall);
}
private static BlockState glitchMaterial(long seed, int x, int y, int z) {
int index = Math.floorMod((int) fingerprint(seed ^ y, Math.floorDiv(x, 8), Math.floorDiv(z, 8)),
GLITCH_MATERIALS.length);
return GLITCH_MATERIALS[index];
}
private static void fillDeck(WorldGenLevel level, long seed, int floorY, int x0, int z0,
BlockState state) {
for (int x = x0; x < x0 + CHUNK_SIZE; x++) for (int z = z0; z < z0 + CHUNK_SIZE; z++) {
if (!BackroomLayout.deckHole(seed, floorY, x, z)) set(level, x, floorY, z, state);
}
}
private static void partitionX(WorldGenLevel level, int x, int floorY, int roofY,
int minZ, int maxZ, int doorZ, BlockState wall, boolean window) {
for (int z = minZ; z <= maxZ; z++) {
if (Math.abs(z - doorZ) <= 1) continue;
for (int y = floorY + 1; y < roofY; y++) {
set(level, x, y, z, window && y >= floorY + 5 && y <= floorY + 8 ? GLASS_BROWN : wall);
}
}
}
private static void partitionZ(WorldGenLevel level, int z, int floorY, int roofY,
int minX, int maxX, int doorX, BlockState wall, boolean window) {
for (int x = minX; x <= maxX; x++) {
if (Math.abs(x - doorX) <= 1) continue;
for (int y = floorY + 1; y < roofY; y++) {
set(level, x, y, z, window && y >= floorY + 5 && y <= floorY + 8 ? GLASS_BROWN : wall);
}
}
}
private static boolean doorway(int coordinate, int minimum, int maximum) {
return coordinate >= minimum && coordinate <= maximum;
}
private static void placeLootLandmarks(WorldGenLevel level, int x0, int z0, Random random) {
if (random.nextInt(5) == 0) {
int floorY = LANDMARK_FLOORS[random.nextInt(LANDMARK_FLOORS.length)];
int x = x0 + 7, y = floorY + 1, z = z0 + 7;
set(level, x, floorY, z, floorMaterial(BackroomLayout.levelAt(floorY)));
set(level, x, y, z, Blocks.CHEST.defaultBlockState());
set(level, x, y + 1, z, Blocks.AIR.defaultBlockState());
}
if (random.nextInt(96) == 0) {
int floorY = 272;
int x = x0 + 7, z = z0 + 8;
set(level, x, floorY, z, Blocks.DARK_OAK_PLANKS.defaultBlockState());
set(level, x + 1, floorY, z, Blocks.DARK_OAK_PLANKS.defaultBlockState());
for (int dx = 0; dx <= 1; dx++) for (int dy = 1; dy <= 2; dy++) {
set(level, x + dx, floorY + dy, z, Blocks.AIR.defaultBlockState());
}
placeBed(level, x, floorY + 1, z);
}
}
private static void placeRareAnomaly(WorldGenLevel level, int x0, int z0, Random random) {
if (random.nextInt(96) == 0) {
Block anomaly = BackroomMaterialApi.anomalyMaterial();
for (int i = 0; i < 5; i++) set(level, x0 + 5 + random.nextInt(6), 66 + random.nextInt(20),
z0 + 5 + random.nextInt(6), anomaly.defaultBlockState());
int floorY = LANDMARK_FLOORS[random.nextInt(LANDMARK_FLOORS.length)];
for (int index = 0; index < 5; index++) {
set(level, x0 + 5 + random.nextInt(6), floorY + 1 + random.nextInt(4),
z0 + 5 + random.nextInt(6), anomaly.defaultBlockState());
}
}
if (random.nextInt(4_096) == 0
&& withinPlayableArea(x0 + 7, z0 + 7) && withinPlayableArea(x0 + 9, z0 + 9)) {
for (int dx = -1; dx <= 1; dx++) for (int dy = -1; dy <= 1; dy++) for (int dz = -1; dz <= 1; dz++) {
set(level, x0 + 8 + dx, 40 + dy, z0 + 8 + dz, Blocks.SMOOTH_STONE.defaultBlockState());
set(level, x0 + 8 + dx, -40 + dy, z0 + 8 + dz, Blocks.SMOOTH_STONE.defaultBlockState());
}
set(level, x0 + 8, 40, z0 + 8, Blocks.DIAMOND_BLOCK.defaultBlockState());
set(level, x0 + 8, -40, z0 + 8, Blocks.DIAMOND_BLOCK.defaultBlockState());
}
}
private static void placeBed(WorldGenLevel level, int x, int y, int z) {
if (!withinPlayableArea(x, z) || !withinPlayableArea(x + 1, z)) return;
Block bed = Blocks.BED.pick(DyeColor.WHITE);
BlockState foot = bed.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST).setValue(BedBlock.PART, BedPart.FOOT);
BlockState foot = bed.defaultBlockState().setValue(BedBlock.FACING, Direction.EAST)
.setValue(BedBlock.PART, BedPart.FOOT);
BlockState head = foot.setValue(BedBlock.PART, BedPart.HEAD);
set(level, x, y, z, foot);
set(level, x + 1, y, z, head);
}
private static void platform(WorldGenLevel level, int x0, int z0, int y, BlockState state) {
for (int x = x0; x < x0 + 16; x++) for (int z = z0; z < z0 + 16; z++) set(level, x, y, z, state);
}
private static void boxRoom(WorldGenLevel level, int minX, int minY, int minZ,
int maxX, int maxY, int maxZ, BlockState shell, BlockState floor) {
for (int x = minX; x <= maxX; x++) for (int y = minY; y <= maxY; y++) for (int z = minZ; z <= maxZ; z++) {
boolean boundary = x == minX || x == maxX || y == minY || y == maxY || z == minZ || z == maxZ;
if (boundary) set(level, x, y, z, y == minY ? floor : shell);
}
}
private static void tree(WorldGenLevel level, int x, int y, int z) {
for (int dy = 0; dy < 5; dy++) set(level, x, y + dy, z, Blocks.OAK_LOG.defaultBlockState());
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) for (int dy = 3; dy <= 6; dy++) {
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy - 5) <= 5) set(level, x + dx, y + dy, z + dz, LEAVES);
for (int dy = 0; dy < 6; dy++) set(level, x, y + dy, z, Blocks.OAK_LOG.defaultBlockState());
for (int dx = -2; dx <= 2; dx++) for (int dz = -2; dz <= 2; dz++) for (int dy = 4; dy <= 7; dy++) {
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy - 6) <= 5) set(level, x + dx, y + dy, z + dz, LEAVES);
}
}
@@ -238,6 +485,4 @@ public final class BackroomArchitectureFeature extends Feature<NoneFeatureConfig
value *= 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
public enum Architecture { CITY, OFFICES, PLAINS, AGGREGATES, ROTATED }
}
@@ -0,0 +1,155 @@
package fr.koka99cab.sanctuary26.sanctuary.worldgen.feature;
import java.util.Objects;
/**
* Pure deterministic layout rules shared by Backroom generation, arrivals and executable tests.
* Level numbers run from the top down so a fall always moves toward the next numbered level.
*/
public final class BackroomLayout {
public static final int WORLD_MIN_Y = -64;
public static final int WORLD_MAX_Y_EXCLUSIVE = 384;
public static final int ENTRY_FLOOR_Y = 367;
public static final int ROOM_SIZE = 16;
public static final int MAZE_FLOOR_Y = 8;
public static final int MAZE_ROOF_Y = 18;
private static final int HOLE_CELL_SIZE = 64;
private BackroomLayout() {}
public static BackroomLevel levelAt(int y) {
for (BackroomLevel level : BackroomLevel.values()) {
if (y >= level.floorY && y <= level.ceilingY) return level;
}
throw new IllegalArgumentException("Y outside the Backroom: " + y);
}
/** A matching opening through an upper floor and the lower level's ceiling. */
public static boolean transitionHole(long seed, int transition, int x, int z) {
if (transition < 0 || transition >= BackroomLevel.values().length - 1) return false;
return authoredHole(seed ^ 0x2f53a71d4e6c890bL, transition, x, z, 1);
}
/** Sparse holes between decks create vertical routes without turning every room into a shaft. */
public static boolean deckHole(long seed, int floorY, int x, int z) {
return authoredHole(seed ^ 0x74c8e6219b0d35afL, floorY, x, z, 1);
}
/** The last floor deliberately has occasional exits into the actual void. */
public static boolean voidHole(long seed, int x, int z) {
return authoredHole(seed ^ 0x56de9c183af247b1L, 5, x, z, 2);
}
/** Openings in the maze roof let a player who landed on it enter the generated rooms. */
public static boolean mazeRoofHole(long seed, int x, int z) {
return authoredHole(seed ^ 0x6b8f1de425a390c7L, 6, x, z, 1);
}
public static int roomPattern(long seed, BackroomLevel level, int cellX, int cellZ, int deck) {
return Math.floorMod((int) mix(seed ^ 0x3c79ac492ba7b653L,
cellX + level.ordinal() * 1_009, cellZ + deck * 4_099), 6);
}
/** Each of the four authored corner directions independently reaches five material variants. */
public static int cornerVariant(long seed, BackroomLevel level, int cellX, int cellZ,
RoomCorner corner) {
Objects.requireNonNull(level, "level");
Objects.requireNonNull(corner, "corner");
return Math.floorMod((int) mix(seed ^ 0x1c69b3f74ac4ae35L,
cellX + corner.ordinal() * 8_191, cellZ + level.ordinal() * 16_381), 5);
}
/**
* Symmetric room-edge rule. Every maze cell has a parent one step nearer the origin, which makes
* the complete graph connected; extra deterministic edges introduce loops and glitches.
*/
public static boolean mazeConnected(long seed, int ax, int az, int bx, int bz) {
if (Math.abs(ax - bx) + Math.abs(az - bz) != 1) return false;
if (isParent(ax, az, bx, bz) || isParent(bx, bz, ax, az)) return true;
int minX = Math.min(ax, bx), minZ = Math.min(az, bz);
int edgeAxis = ax == bx ? 1 : 0;
return Math.floorMod((int) mix(seed ^ 0x5d42c713e9a86bf0L,
minX * 2 + edgeAxis, minZ * 2 + 1 - edgeAxis), 5) == 0;
}
/** Some connected cells completely lose their partition and become rooms at a larger scale. */
public static boolean mergedMazeRooms(long seed, int ax, int az, int bx, int bz) {
if (!mazeConnected(seed, ax, az, bx, bz)) return false;
int minX = Math.min(ax, bx), minZ = Math.min(az, bz);
int edgeAxis = ax == bx ? 1 : 0;
return Math.floorMod((int) mix(seed ^ 0x0b4e8f619c32d75aL,
minX * 2 + edgeAxis, minZ * 2 + 1 - edgeAxis), 11) == 0;
}
/** Infinite orthogonal highways, nine blocks wide and 96 blocks apart. */
public static boolean cityHighway(int x, int z) {
return centeredModulo(x - 8, 96) <= 4 || centeredModulo(z - 8, 96) <= 4;
}
public static boolean cityPark(long seed, int chunkX, int chunkZ) {
return Math.floorMod((int) mix(seed ^ 0x43c2a1769f0e85bdL, chunkX, chunkZ), 11) == 0;
}
private static boolean isParent(int childX, int childZ, int parentX, int parentZ) {
if (childX == 0 && childZ == 0) return false;
int expectedX = childX, expectedZ = childZ;
if (Math.abs(childX) >= Math.abs(childZ) && childX != 0) expectedX -= Integer.signum(childX);
else expectedZ -= Integer.signum(childZ);
return expectedX == parentX && expectedZ == parentZ;
}
private static boolean authoredHole(long seed, int salt, int x, int z, int radius) {
int cellX = Math.floorDiv(x, HOLE_CELL_SIZE);
int cellZ = Math.floorDiv(z, HOLE_CELL_SIZE);
long hash = mix(seed ^ (long) salt * 0x9e3779b97f4a7c15L, cellX, cellZ);
int centerX = cellX * HOLE_CELL_SIZE + 8 + Math.floorMod((int) hash, HOLE_CELL_SIZE - 16);
int centerZ = cellZ * HOLE_CELL_SIZE + 8
+ Math.floorMod((int) (hash >>> 32), HOLE_CELL_SIZE - 16);
return Math.abs(x - centerX) <= radius && Math.abs(z - centerZ) <= radius;
}
private static int centeredModulo(int value, int period) {
int wrapped = Math.floorMod(value + period / 2, period) - period / 2;
return Math.abs(wrapped);
}
private static long mix(long seed, int x, int z) {
long value = seed ^ (long) x * 0x9E3779B97F4A7C15L ^ (long) z * 0xC2B2AE3D27D4EB4FL;
value ^= value >>> 30;
value *= 0xBF58476D1CE4E5B9L;
value ^= value >>> 27;
value *= 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
public enum BackroomLevel {
LEVEL_0(295, 383),
LEVEL_1(206, 294),
LEVEL_2(117, 205),
LEVEL_3(28, 116),
LEVEL_4(-64, 27);
private final int floorY;
private final int ceilingY;
BackroomLevel(int floorY, int ceilingY) {
this.floorY = floorY;
this.ceilingY = ceilingY;
}
public int floorY() {
return this.floorY;
}
public int ceilingY() {
return this.ceilingY;
}
}
public enum RoomCorner {
NORTH_WEST,
NORTH_EAST,
SOUTH_WEST,
SOUTH_EAST
}
}
@@ -522,7 +522,7 @@
"message.sanctuary.backroom.registry_busy": "The Backroom registry is settling too many interrupted transfers. Try again later.",
"message.sanctuary.backroom.mailbox_missing": "Load and register your Mailbox before entering the Backroom.",
"message.sanctuary.backroom.mailbox_full": "Your Mailbox has no room for all your gear, companion egg and cape.",
"message.sanctuary.backroom.entered": "You slip through the bed with no equipment…",
"message.sanctuary.backroom.entered": "You slip through the bed with no equipment… A torch marks your arrival point.",
"message.sanctuary.backroom.bed_entry_only": "Only a bed can take you into the Backroom.",
"message.sanctuary.backroom.exited": "The bed returns you to your spawn point with your findings.",
"message.sanctuary.backroom.exited_with_losses": "The bed returns you with your findings and settles the recovered lost items.",
@@ -522,7 +522,7 @@
"message.sanctuary.backroom.registry_busy": "Le registre Backroom termine trop de transferts interrompus. Réessaie plus tard.",
"message.sanctuary.backroom.mailbox_missing": "Charge et enregistre ta Mailbox avant dentrer dans la Backroom.",
"message.sanctuary.backroom.mailbox_full": "Ta Mailbox na pas assez de place pour tout ton équipement, l’œuf et la cape.",
"message.sanctuary.backroom.entered": "Tu traverses le lit sans aucun équipement…",
"message.sanctuary.backroom.entered": "Tu traverses le lit sans aucun équipement… Une torche marque ton point darrivée.",
"message.sanctuary.backroom.bed_entry_only": "Seul un lit permet dentrer dans la Backroom.",
"message.sanctuary.backroom.exited": "Le lit te ramène à ton point dapparition avec tes trouvailles.",
"message.sanctuary.backroom.exited_with_losses": "Le lit te ramène avec tes trouvailles et valide les objets perdus récupérés.",
@@ -478,7 +478,7 @@
"message.sanctuary.backroom.registry_busy": "Реестр Бэкрума завершает слишком много прерванных переносов. Повторите попытку позже.",
"message.sanctuary.backroom.mailbox_missing": "Загрузите и зарегистрируйте почтовый ящик перед входом в Бэкрум.",
"message.sanctuary.backroom.mailbox_full": "В почтовом ящике недостаточно места для снаряжения, яйца спутника и плаща.",
"message.sanctuary.backroom.entered": "Вы проваливаетесь сквозь кровать без снаряжения…",
"message.sanctuary.backroom.entered": "Вы проваливаетесь сквозь кровать без снаряжения… Факел отмечает место прибытия.",
"message.sanctuary.backroom.bed_entry_only": "В Бэкрум можно попасть только через кровать.",
"message.sanctuary.backroom.exited": "Кровать возвращает вас к точке возрождения со всеми находками.",
"message.sanctuary.backroom.exited_with_losses": "Кровать возвращает вас с находками и закрепляет найденные потерянные предметы.",
@@ -1,5 +1,5 @@
{
"ambient_light": 0.35,
"ambient_light": 0.0,
"attributes": {
"minecraft:audio/ambient_sounds": {
"mood": {
@@ -17,25 +17,25 @@
"minecraft:gameplay/respawn_anchor_works": false,
"minecraft:gameplay/sky_light_level": 0.0,
"minecraft:gameplay/water_evaporates": false,
"minecraft:visual/ambient_light_color": "#d5d8ce",
"minecraft:visual/fog_color": "#b7b9ad",
"minecraft:visual/fog_end_distance": 112.0,
"minecraft:visual/fog_start_distance": 20.0,
"minecraft:visual/sky_light_color": "#d9dccf",
"minecraft:visual/ambient_light_color": "#000000",
"minecraft:visual/fog_color": "#000000",
"minecraft:visual/fog_end_distance": 96.0,
"minecraft:visual/fog_start_distance": 12.0,
"minecraft:visual/sky_light_color": "#000000",
"minecraft:visual/sky_light_factor": 0.0
},
"cardinal_light": "nether",
"coordinate_scale": 1.0,
"has_ceiling": false,
"has_ceiling": true,
"has_ender_dragon_fight": false,
"has_fixed_time": true,
"has_skylight": false,
"height": 192,
"height": 448,
"infiniburn": "#minecraft:infiniburn_overworld",
"logical_height": 192,
"min_y": 0,
"monster_spawn_block_light_limit": 15,
"monster_spawn_light_level": 7,
"logical_height": 448,
"min_y": -64,
"monster_spawn_block_light_limit": 0,
"monster_spawn_light_level": 0,
"skybox": "none",
"timelines": "#minecraft:in_nether"
}
@@ -6,22 +6,18 @@
"effects": {
"water_color": "#596f75",
"water_fog_color": "#303b3e",
"fog_color": "#b7b9ad",
"sky_color": "#b7b9ad",
"fog_color": "#000000",
"sky_color": "#000000",
"grass_color": "#70845d",
"foliage_color": "#657b58"
},
"creature_spawn_probability": 0.08,
"creature_spawn_probability": 0.0,
"spawners": {
"ambient": [],
"axolotls": [],
"creature": [],
"misc": [],
"monster": [
{ "type": "minecraft:zombie", "maxCount": 2, "minCount": 1, "weight": 7 },
{ "type": "minecraft:skeleton", "maxCount": 2, "minCount": 1, "weight": 7 },
{ "type": "minecraft:enderman", "maxCount": 1, "minCount": 1, "weight": 2 }
],
"monster": [],
"underground_water_creature": [],
"water_ambient": [],
"water_creature": []
@@ -2,11 +2,11 @@
"aquifers_enabled": false,
"default_block": { "Name": "minecraft:air" },
"default_fluid": { "Name": "minecraft:air" },
"disable_mob_generation": false,
"disable_mob_generation": true,
"legacy_random_source": false,
"noise": {
"height": 192,
"min_y": 0,
"height": 448,
"min_y": -64,
"size_horizontal": 1,
"size_vertical": 2
},
@@ -1,8 +1,14 @@
package fr.koka99cab.sanctuary26.sanctuary.backroom;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature;
import java.util.EnumSet;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.BackroomLevel;
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomLayout.RoomCorner;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
@@ -18,7 +24,9 @@ public final class BackroomModelSmoke {
entryTransactionsAreBounded();
personalRecoveryIsPrioritized();
recentLossesRemainPersonal();
architectureIsDeterministicAndComposed();
architectureIsDeterministicAndStacked();
roomCornersReachEveryVariant();
mazeDoorsStayConnected();
}
private static void entryTransactionsAreBounded() {
@@ -74,27 +82,66 @@ public final class BackroomModelSmoke {
"An ownerless loss was not immediately eligible for shared loot");
}
private static void architectureIsDeterministicAndComposed() {
private static void architectureIsDeterministicAndStacked() {
long seed = 26_200_132L;
EnumSet<BackroomArchitectureFeature.Architecture> families =
EnumSet.noneOf(BackroomArchitectureFeature.Architecture.class);
for (int macroX = -12; macroX <= 12; macroX++) for (int macroZ = -12; macroZ <= 12; macroZ++) {
int chunkX = macroX * 8, chunkZ = macroZ * 8;
BackroomArchitectureFeature.Architecture family =
BackroomArchitectureFeature.architecture(seed, chunkX, chunkZ);
families.add(family);
require(family == BackroomArchitectureFeature.architecture(seed, chunkX + 7, chunkZ + 7),
"One macro cell mixed unrelated architecture palettes");
require(BackroomArchitectureFeature.fingerprint(seed, chunkX, chunkZ)
== BackroomArchitectureFeature.fingerprint(seed, chunkX, chunkZ),
"Architecture fingerprint is not deterministic");
require(BackroomLayout.WORLD_MIN_Y == -64 && BackroomLayout.WORLD_MAX_Y_EXCLUSIVE == 384,
"Backroom no longer occupies the complete requested vertical range");
for (int y = BackroomLayout.WORLD_MIN_Y; y < BackroomLayout.WORLD_MAX_Y_EXCLUSIVE; y++) {
require(BackroomLayout.levelAt(y) != null, "A Y layer is not assigned to one of the five levels");
}
require(families.size() == BackroomArchitectureFeature.Architecture.values().length,
"Not all authored architecture families are reachable");
require(BackroomLevel.values().length == 5, "Backroom no longer has exactly five stacked levels");
require(BackroomLayout.levelAt(BackroomLayout.ENTRY_FLOOR_Y) == BackroomLevel.LEVEL_0,
"Arrival deck left the top level");
for (int transition = 0; transition < 4; transition++) {
boolean found = false;
for (int x = -64; x < 64 && !found; x++) for (int z = -64; z < 64; z++) {
if (BackroomLayout.transitionHole(seed, transition, x, z)) {
found = true;
break;
}
}
require(found, "One level transition has no reachable authored opening");
}
require(BackroomArchitectureFeature.fingerprint(seed, 12, -7)
== BackroomArchitectureFeature.fingerprint(seed, 12, -7),
"Architecture fingerprint is not deterministic");
require(BackroomLayout.cityHighway(8, 1_423) && !BackroomLayout.cityHighway(48, 48),
"City highways are no longer long continuous axes separated from building cells");
require(BackroomArchitectureFeature.withinPlayableArea(2_000, 0), "Playable boundary moved inward");
require(!BackroomArchitectureFeature.withinPlayableArea(2_001, 0), "Architecture escaped the 2,000-block zone");
}
private static void roomCornersReachEveryVariant() {
long seed = 26_200_132L;
Map<RoomCorner, Set<Integer>> variants = new EnumMap<>(RoomCorner.class);
for (RoomCorner corner : RoomCorner.values()) variants.put(corner, new HashSet<>());
for (int x = -32; x <= 32; x++) for (int z = -32; z <= 32; z++) {
for (RoomCorner corner : RoomCorner.values()) {
variants.get(corner).add(BackroomLayout.cornerVariant(seed, BackroomLevel.LEVEL_0, x, z, corner));
}
}
for (RoomCorner corner : RoomCorner.values()) {
require(variants.get(corner).equals(Set.of(0, 1, 2, 3, 4)),
"A room corner lost one of its five authored material variants: " + corner);
}
}
private static void mazeDoorsStayConnected() {
long seed = 26_200_132L;
for (int x = -16; x <= 16; x++) for (int z = -16; z <= 16; z++) {
if (x == 0 && z == 0) continue;
boolean descends = false;
for (int[] direction : new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}) {
int neighborX = x + direction[0], neighborZ = z + direction[1];
boolean forward = BackroomLayout.mazeConnected(seed, x, z, neighborX, neighborZ);
boolean reverse = BackroomLayout.mazeConnected(seed, neighborX, neighborZ, x, z);
require(forward == reverse, "Maze doorway is not symmetric across a room boundary");
if (forward && Math.abs(neighborX) + Math.abs(neighborZ) < Math.abs(x) + Math.abs(z)) descends = true;
}
require(descends, "A generated maze room has no open route toward the connected root");
}
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}