Compare commits

...
Author SHA1 Message Date
koka e552bd68fd Merge remote-tracking branch 'origin/main' into integration/storage-terminal-sorting-alpha228
Verification 26.2 / check (pull_request) Canceled after 0s
# Conflicts:
#	build.gradle
2026-09-01 18:41:49 +02:00
koka 69dd9f919d Merge pull request 'release: publier Sanctuary 26.2.0-alpha.228' (#140) from release/26.2.0-alpha.228 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #140
2026-09-01 16:36:46 +00:00
koka 2d212ae6a1 release: prepare Sanctuary 26.2.0-alpha.228
Verification 26.2 / check (pull_request) Canceled after 0s
2026-09-01 18:33:28 +02:00
koka 2b614ce5a8 Merge remote-tracking branch 'origin/main' into feature/storage-terminal-sorting
Verification 26.2 / check (pull_request) Canceled after 0s
# Conflicts:
#	CHANGELOG.md
#	build.gradle
2026-09-01 18:31:32 +02:00
koka 99ac5fc3a7 Add terminal sorting modes 2026-09-01 18:30:53 +02:00
koka ab82c3929e Merge pull request 'feat: reconstruire la Backroom sur cinq niveaux' (#137) from feature/backroom-five-level-generation into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #137
2026-09-01 16:23:25 +00:00
koka 4820c736f4 Merge remote-tracking branch 'origin/main' into feature/backroom-five-level-generation
Verification 26.2 / check (pull_request) Canceled after 0s
# Conflicts:
#	CHANGELOG.md
#	build.gradle
2026-09-01 18:16:37 +02:00
koka a16347140a Merge pull request 'feat(redstoner): rechercher dans les boîtes de Shulker' (#138) from feature/storage-terminal-shulker-search into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #138
2026-09-01 16:14:20 +00:00
koka 75fa88cfc5 Add Shulker contents to terminal search
Verification 26.2 / check (pull_request) Canceled after 0s
2026-09-01 18:11:05 +02:00
koka f04aeb02b2 Merge remote-tracking branch 'origin/main' into feature/backroom-five-level-generation
Verification 26.2 / check (pull_request) Canceled after 0s
# Conflicts:
#	CHANGELOG.md
#	build.gradle
2026-09-01 18:09:41 +02:00
koka 4ca42cb18a Merge pull request 'fix(redstoner): rendre la recherche du terminal cliquable' (#135) from fix/storage-terminal-search-focus into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #135
2026-09-01 15:57:57 +00:00
koka 985e8b9192 feat(backroom): stack five dark generation levels
Verification 26.2 / check (pull_request) Canceled after 0s
2026-09-01 17:55:40 +02:00
koka 1d8ab0b30a Merge remote-tracking branch 'origin/main' into fix/storage-terminal-search-focus
Verification 26.2 / check (pull_request) Canceled after 0s
# Conflicts:
#	build.gradle
2026-09-01 17:19:44 +02:00
koka 083c3449d5 Merge pull request 'release: publier Sanctuary 26.2.0-alpha.227' (#134) from release/26.2.0-alpha.227 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #134
2026-09-01 15:10:06 +00:00
koka 51d630037d fix(redstoner): make terminal search focus clickable
Verification 26.2 / check (pull_request) Canceled after 0s
2026-09-01 17:04:01 +02:00
koka 341f1eb8e5 release: prepare Sanctuary 26.2.0-alpha.227
Verification 26.2 / check (pull_request) Canceled after 0s
2026-09-01 17:00:02 +02:00
koka 10b606d01d Merge pull request 'feat: ajouter la dimension persistante Backroom' (#133) from feature/backroom into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #133
2026-09-01 14:50:33 +00:00
33 changed files with 1608 additions and 517 deletions
+20
View File
@@ -8,6 +8,25 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
## Non publié
Module modifié : **Red-Stoner `0.0.0-alpha.16`**.
- ajoute au terminal les tris alphabétique, par quantité croissante et par quantité décroissante, appliqués explicitement sans déplacer les objets sous le curseur lors des rafraîchissements périodiques ;
## `26.2.0-alpha.228`
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 ;
## `26.2.0-alpha.227`
Modules modifiés : **Sanctuary `0.0.0-alpha.131`** et **Another World `0.0.0-alpha.57`**.
- ajoute la dimension persistante `sanctuary:backroom`, accessible en saccroupissant devant un lit et quittée uniquement par un lit, avec arrivées variées dans une zone architecturale de 2 000 blocs ;
@@ -16,6 +35,7 @@ Modules modifiés : **Sanctuary `0.0.0-alpha.131`** et **Another World `0.0.0-al
- transforme tous les coffres de la dimension en butin renouvelable, pauvre la plupart du temps, qui privilégie les pertes personnelles dans les premiers coffres avant les pertes anciennes partagées ;
- génère des villes, bureaux, plaines, agrégats flottants et pièces visuellement retournées à partir de palettes composées, en réservant les matériaux dAnother World aux anomalies et les blocs précieux à de rares cachettes ;
- fait enregistrer la mousse turquoise par Another World via lAPI publique de matériaux Backroom, sans dépendance circulaire ;
- actualise Fabric API vers `0.159.0+26.2` et JEI vers `30.29.0.199`, avec empreintes Packwiz et Prism synchronisées ;
- migre Sanctuary vers les données v14 sans changer le protocole réseau 24, sans réinitialiser les sauvegardes ni effacer les constructions.
## `26.2.0-alpha.226`
+9 -3
View File
@@ -1,6 +1,6 @@
# Sanctuary 26.2
Workspace Fabric multi-projet de Sanctuary 2.0. Il construit dix JAR indépendants sur Minecraft 26.2, Fabric Loader 0.19.3, Fabric API 0.158.0+26.2, Loom 1.17, Gradle 9.5.1 et Java 25.
Workspace Fabric multi-projet de Sanctuary 2.0. Il construit dix JAR indépendants sur Minecraft 26.2, Fabric Loader 0.19.3, Fabric API 0.159.0+26.2, Loom 1.17, Gradle 9.5.1 et Java 25.
Auteur : **KOKA99CAB**
Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
@@ -22,7 +22,13 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans les fichiers de langue. Les images fournies servent d'icônes Fabric de leur module.
Version courante du pack distribué : **`26.2.0-alpha.226`**. Cette release distribue Sanctuary **`0.0.0-alpha.130`** ; les neuf autres modules conservent leur version issue de l'alpha.225.
Version courante du pack distribué : **`26.2.0-alpha.228`**. Cette release distribue Red-Stoner **`0.0.0-alpha.15`** et Sanctuary **`0.0.0-alpha.132`** ; les huit autres modules conservent leur version issue de l'alpha.227.
Le prochain module Red-Stoner **`0.0.0-alpha.16`** ajoute au terminal les tris alphabétique, par quantité croissante et par quantité décroissante. Le tri ne change que sur demande explicite et les rafraîchissements périodiques conservent lordre ainsi que les objets sous le curseur.
Lalpha.228 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. Red-Stoner corrige aussi le focus du terminal et permet de retrouver une boîte de Shulker par son contenu. Les chunks déjà générés et les sauvegardes existantes restent compatibles.
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.
@@ -40,7 +46,7 @@ Lalpha.220 transforme le terminal de stockage en véritable menu de conteneur
Lalpha.219 porte l'île finie Minecraft Indev dans `sanctuary:alpha` : 128 × 128 × 64 blocs déterministes, plages, cavernes, minerais, fluides, végétation et maison historique, le tout borné sans chunks parasites. Le cadre 4 × 5 en cobblestone moussue est maintenant actif et crée une paire persistante à une arrivée sûre hors de la maison. Ambiance peut enfin composer localement Golden Days Base puis Alpha uniquement dans cette dimension, sans redistribuer leurs textures et en restaurant exactement la pile précédente. Voir le [contrat Alpha](sanctuary/ALPHA.md), le [contrat des portails](iliketomoveit/PORTALS.md) et le [guide Golden Days](ambiance/GOLDEN_DAYS.md).
Lalpha.218 ajoute le terminal de stockage Red-Stoner : cette demi-dalle verticale se fixe sur un coffre, un coffre piégé ou un baril et donne accès, par pages défilantes de 54 types, à un réseau contigu borné à 128 conteneurs chargés. Les retraits et dépôts sont validés par le serveur, les composants ditems restent strictement séparés et le terminal endgame est exclu du Shop, du Black Market et des récompenses Only Fun.
Lalpha.218 ajoute le terminal de stockage Red-Stoner : cette demi-dalle verticale se fixe sur un coffre, un coffre piégé ou un baril et donne accès, par pages défilantes de 54 types, à un réseau contigu borné à 128 conteneurs chargés. Les retraits et dépôts sont validés par le serveur, les composants ditems restent strictement séparés et le terminal endgame est exclu du Shop, du Black Market et des récompenses Only Fun. La recherche retrouve aussi une boîte de Shulker daprès son contenu direct ; la boîte reste affichée dans la grille afin que son aperçu vanilla soit consultable sans modifier les règles de retrait. Un bouton permet enfin de trier explicitement les résultats par nom, quantité croissante ou quantité décroissante.
Lalpha.217 tentait de compenser la hauteur trop grave du disque blanc par un facteur OpenAL de `4/3`. Cette compensation est retirée par lalpha.221, qui corrige directement la conversion stéréo vers mono.
+501 -247
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -6,7 +6,7 @@ org.gradle.configuration-cache=false
minecraft_version=26.2
loader_version=0.19.3
loom_version=1.17-SNAPSHOT
fabric_api_version=0.158.0+26.2
fabric_api_version=0.159.0+26.2
# Sanctuary 26.2
# Standby modules stay at zero until their migration really begins.
@@ -14,7 +14,7 @@ mod_version=0.0.0-alpha.0
module_lifecycle=standby
ambiance_version=0.0.0-alpha.22
ambiance_lifecycle=active
redstoner_version=0.0.0-alpha.13
redstoner_version=0.0.0-alpha.16
redstoner_lifecycle=active
anotherworld_version=0.0.0-alpha.57
anotherworld_lifecycle=active
@@ -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.226
pack_version=26.2.0-alpha.228
maven_group=fr.koka99cab.sanctuary26
@@ -0,0 +1,60 @@
{
"schema_version": 1,
"status": "released",
"source": {
"pack_version": "26.2.0-alpha.226",
"modules": {
"anotherworld": "0.0.0-alpha.56",
"sanctuary": "0.0.0-alpha.130"
}
},
"target": {
"pack_version": "26.2.0-alpha.227",
"modules": {
"anotherworld": "0.0.0-alpha.57",
"sanctuary": "0.0.0-alpha.131"
}
},
"backroom": {
"owner": "sanctuary",
"dimension": "sanctuary:backroom",
"entry_requires_sneaking_at_bed": true,
"exit_requires_bed": true,
"arrival_radius_blocks": 2000,
"entry_inventory_to_mailbox": true,
"entry_transfer_atomic": true,
"entry_transfer_idempotent": true,
"lost_items_persisted": true,
"lost_item_registry_bounded": true,
"renewable_chests": true,
"personal_losses_prioritized": true,
"shared_old_losses_supported": true,
"anotherworld_material_api": "BackroomMaterialApi"
},
"compatibility": {
"existing_saves_supported": true,
"new_world_required": false,
"save_reset_required": false,
"sanctuary_source_data_version": 13,
"sanctuary_target_data_version": 14,
"sanctuary_network_protocol": 24,
"network_protocol_changed": false,
"client_server_update_required": true
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true,
"fabric_api": {
"source": "0.158.0+26.2",
"target": "0.159.0+26.2",
"modrinth_version_id": "BgeCGgGZ"
},
"jei": {
"source": "30.29.0.198",
"target": "30.29.0.199",
"modrinth_version_id": "astSvyl6"
}
}
}
@@ -0,0 +1,58 @@
{
"schema_version": 1,
"status": "released",
"source": {
"pack_version": "26.2.0-alpha.227",
"modules": {
"redstoner": "0.0.0-alpha.13",
"sanctuary": "0.0.0-alpha.131"
}
},
"target": {
"pack_version": "26.2.0-alpha.228",
"modules": {
"redstoner": "0.0.0-alpha.15",
"sanctuary": "0.0.0-alpha.132"
}
},
"backroom": {
"level_count": 5,
"minimum_y": -64,
"maximum_y_exclusive": 384,
"entry_floor_y": 367,
"monsters_disabled": true,
"arrival_torch": true,
"matching_vertical_transitions": true,
"connected_maze": true,
"dense_city_and_highways": true,
"three_dimensional_brick_grid": true,
"intentional_void_holes": true
},
"storage_terminal": {
"click_to_focus": true,
"inventory_key_closes": true,
"shulker_contents_searchable": true,
"shulker_remains_result": true,
"vanilla_shulker_preview_preserved": true
},
"compatibility": {
"existing_saves_supported": true,
"new_world_required": false,
"save_reset_required": false,
"persistent_data_formats_changed": false,
"network_protocols_changed": false,
"redstoner_data_version": 3,
"redstoner_network_protocol": 7,
"sanctuary_data_version": 14,
"sanctuary_network_protocol": 24,
"client_server_update_required": true,
"existing_backroom_chunks_preserved": true,
"new_backroom_generation_applies_to_new_chunks": true
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"status": "pending_release",
"status": "released",
"module": "sanctuary",
"source_module_version": "0.0.0-alpha.130",
"target_module_version": "0.0.0-alpha.131",
+15 -15
View File
@@ -14,24 +14,24 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
[[files]]
file = "mods/ambiance-0.0.0-alpha.22.jar"
hash = "0e862db1c5efb5c0745b82f991d5480f4305822c3d3f53ed38e2d2d47efabee1"
hash = "619acaea8062f7a4bc3ab4eaf288a2a8f39f79f63399cb77bc3cad9ae0069dca"
[[files]]
file = "mods/anotherworld-0.0.0-alpha.56.jar"
hash = "cec8ed493f3c86e4c4481081be110382407f7a5c631b7cf27dd80f80c6f2e92a"
file = "mods/anotherworld-0.0.0-alpha.57.jar"
hash = "ed2a20b1da66e91e6b0c81e2dd69dc00d950533090d7546ba300ddb605274666"
[[files]]
file = "mods/events-0.0.0-alpha.0.jar"
hash = "16bf9fc194988086f51ba6987546b6aa5bc634fe8fcb118b88540ed16991049c"
hash = "5dfe8aa4a18e1d11982e4675113e308b95140309062b94640e77bda6f23ee0ef"
[[files]]
file = "mods/fabric-api.pw.toml"
hash = "df032a6b051d0262cf24945c43e0e6b915e8cc1d62af62fe933932e7f35960c4"
hash = "b8ea5f403a0cdfbfd5ab1704bb61278e8dcf31b525db9c242b22047852d34f85"
metafile = true
[[files]]
file = "mods/iliketomoveit-0.0.0-alpha.18.jar"
hash = "94044341de08c9b89e0c1e6af6857da64b472fadbc94051ab76fc3cf1a0f0006"
hash = "d605656061456d4fae3b99a0febc7503c83b1ab35895d961ab0c84eae77bca56"
[[files]]
file = "mods/iris.pw.toml"
@@ -40,32 +40,32 @@ metafile = true
[[files]]
file = "mods/itsalive-0.0.0-alpha.38.jar"
hash = "a42885703439646687ac281b1c124d1cddb3ca0595a41d930733b94bd8f1aa2f"
hash = "a163fe50c0c58923db69b592aacb5df90bd33219db40e149b2e4e7da1994fca0"
[[files]]
file = "mods/jei.pw.toml"
hash = "cffb13bae33cd8df605c62e7fa66089428dc684e0ab973f662a8a710c45c306d"
hash = "9cc4a91f57effc48b94f6acc1f43067bb8eb99fd1df3397612c55761de9ec2c2"
metafile = true
[[files]]
file = "mods/masterkey-0.0.0-alpha.9.jar"
hash = "979c14e0a527be63688e97af8349be07b027d3149107c3e9e728a2b1e1ec65ff"
hash = "6e9e7f3462b17f209cced5a16594bd954a86ba654a9adecff06a5d0bdce2df65"
[[files]]
file = "mods/onlyfun-0.0.0-alpha.27.jar"
hash = "023ba80a8f3326a6b534b343753d5630d3be0700d25613ea21057cbfe44fdc73"
hash = "0cee938fa050ba7aad0fc9dba341585c3f1c726780d052082a4c09ea68e897a2"
[[files]]
file = "mods/ouch-0.0.0-alpha.11.jar"
hash = "2c15c9d0adeb315b2c3a6796f27c9dc62dd33365f35ea584552d97500b833eb8"
hash = "761c7e6b8987cbe1b0c762c0d1566abfe36ec8dd7dd998bf20c1d66de6d4f203"
[[files]]
file = "mods/redstoner-0.0.0-alpha.13.jar"
hash = "753835558c1037e39ca669737190013bd59a3ab16f7d5156c52e940c54ff9c60"
file = "mods/redstoner-0.0.0-alpha.15.jar"
hash = "6d0e88c39201bc1c23fdcc82ef8488200660ec58f1f96fd2b9b109de5df990f7"
[[files]]
file = "mods/sanctuary-0.0.0-alpha.130.jar"
hash = "8906be26de369903a0d5152d80a95a1c47b3468e4afca17ab4683b8300ad69a9"
file = "mods/sanctuary-0.0.0-alpha.132.jar"
hash = "e7cd60e8dee7bd97c1f77c392aaad4a0b3ea0ae44d63d0dc324fb2729c9cc601"
[[files]]
file = "mods/simple-voice-chat.pw.toml"
+4 -4
View File
@@ -1,14 +1,14 @@
name = "Fabric API"
filename = "fabric-api-0.158.0+26.2.jar"
filename = "fabric-api-0.159.0+26.2.jar"
side = "both"
pin = true
[download]
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/NqwNSxwA/fabric-api-0.158.0%2B26.2.jar"
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/BgeCGgGZ/fabric-api-0.159.0%2B26.2.jar"
hash-format = "sha512"
hash = "4c2c1ebe74ffd54875a01ff371b53ba3d8674ac98d561f7dae02a96d3d37fbdbc5f5abc6e820f73b6154d6f873ddd05a442b0998ed2d456863dc0ad972e040a6"
hash = "4d5d9c3daafe19be187f638f2d6de3db26420b8720ef5a266cfa81f79d9019cab8c86e6d47478bbb0b0ee0b8f769204b72c843caa2e4aabd94adc41f3dafb518"
[update]
[update.modrinth]
mod-id = "P7dR8mSH"
version = "NqwNSxwA"
version = "BgeCGgGZ"
+4 -4
View File
@@ -1,14 +1,14 @@
name = "Just Enough Items (JEI)"
filename = "jei-26.2-fabric-30.29.0.198.jar"
filename = "jei-26.2-fabric-30.29.0.199.jar"
side = "both"
pin = true
[download]
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/67Dvn8sL/jei-26.2-fabric-30.29.0.198.jar"
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/astSvyl6/jei-26.2-fabric-30.29.0.199.jar"
hash-format = "sha512"
hash = "06131c9528549840e07ddc9634e23454cd454380d7749a23d818f0efe0dacc02874e967ceb74a24ee82efb13e0fe2df4026f988a68f54f890586967e91dcaa5b"
hash = "2849ff6a4cc91822798f0c70e7749c298b911c1037fc74e9ab0881734b8d5cb6507fb3169b4a240ef6c37eb6f7c573794c46073ff352f160feee245233b2abe5"
[update]
[update.modrinth]
mod-id = "u6dRKJwZ"
version = "67Dvn8sL"
version = "astSvyl6"
+2 -2
View File
@@ -1,12 +1,12 @@
name = "Sanctuary 26.2"
author = "KOKA99CAB"
version = "26.2.0-alpha.226"
version = "26.2.0-alpha.228"
pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "d3b929a596081adc3960866a2f939d4155d1b67ecb22bfe6050931a7d1d0f0a2"
hash = "e9064ff6017f1b49a2f774a9d68f456b0c8b77ee5ffd130a70c5b9fb5cdeb216"
[versions]
fabric = "0.19.3"
+11 -11
View File
@@ -3,37 +3,37 @@
"game": "minecraft",
"name": "Sanctuary 26.2",
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
"versionId": "26.2.0-alpha.226",
"versionId": "26.2.0-alpha.228",
"files": [
{
"path": "mods/fabric-api-0.158.0+26.2.jar",
"path": "mods/fabric-api-0.159.0+26.2.jar",
"hashes": {
"sha1": "7d0fb560579d28b933bdbc36b47449efe78d3bda",
"sha512": "4c2c1ebe74ffd54875a01ff371b53ba3d8674ac98d561f7dae02a96d3d37fbdbc5f5abc6e820f73b6154d6f873ddd05a442b0998ed2d456863dc0ad972e040a6"
"sha1": "5307b87f8336f27e8f963263ac514f4bc6175d90",
"sha512": "4d5d9c3daafe19be187f638f2d6de3db26420b8720ef5a266cfa81f79d9019cab8c86e6d47478bbb0b0ee0b8f769204b72c843caa2e4aabd94adc41f3dafb518"
},
"env": {
"client": "required",
"server": "required"
},
"downloads": [
"https://cdn.modrinth.com/data/P7dR8mSH/versions/NqwNSxwA/fabric-api-0.158.0%2B26.2.jar"
"https://cdn.modrinth.com/data/P7dR8mSH/versions/BgeCGgGZ/fabric-api-0.159.0%2B26.2.jar"
],
"fileSize": 2535765
"fileSize": 2535793
},
{
"path": "mods/jei-26.2-fabric-30.29.0.198.jar",
"path": "mods/jei-26.2-fabric-30.29.0.199.jar",
"hashes": {
"sha1": "652e964c71a699fdfdd96cda265ea703b13c6ef4",
"sha512": "06131c9528549840e07ddc9634e23454cd454380d7749a23d818f0efe0dacc02874e967ceb74a24ee82efb13e0fe2df4026f988a68f54f890586967e91dcaa5b"
"sha1": "1d6ab9269be16f85fb13a34a36b34ad07a9b32a7",
"sha512": "2849ff6a4cc91822798f0c70e7749c298b911c1037fc74e9ab0881734b8d5cb6507fb3169b4a240ef6c37eb6f7c573794c46073ff352f160feee245233b2abe5"
},
"env": {
"client": "required",
"server": "required"
},
"downloads": [
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/67Dvn8sL/jei-26.2-fabric-30.29.0.198.jar"
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/astSvyl6/jei-26.2-fabric-30.29.0.199.jar"
],
"fileSize": 1839279
"fileSize": 1849188
},
{
"path": "mods/voicechat-fabric-2.6.22+26.2.jar",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"pack_version": "26.2.0-alpha.226",
"pack_version": "26.2.0-alpha.228",
"release_channel": "alpha",
"minecraft_version": "26.2",
"fabric_loader_version": "0.19.3",
@@ -9,7 +9,7 @@
"project_id": "FgOqyAXd",
"project_slug": "sanctuary-koka99",
"project_url": "https://modrinth.com/modpack/sanctuary-koka99",
"primary_file": "sanctuary-26.2.0-alpha.226.mrpack",
"primary_file": "sanctuary-26.2.0-alpha.228.mrpack",
"shared_client_server_pack": true,
"update_owner": "packwiz-installer",
"check_on_launch": true,
@@ -2,12 +2,15 @@ package fr.koka99cab.sanctuary26.redstoner.client.gui;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalMenu;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalSort;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalStatus;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.input.MouseButtonEvent;
@@ -28,8 +31,10 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
private static final int SCROLL_WIDTH = 4;
private int requestedPage;
private EditBox searchBox;
private Button sortButton;
private String searchValue = "";
private String sentSearchValue = "";
private StorageTerminalSort sortMode = StorageTerminalSort.ALPHABETICAL;
public StorageTerminalScreen(StorageTerminalMenu menu, Inventory inventory, Component title) {
super(menu, inventory, title, 176, 222);
@@ -40,13 +45,16 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
protected void init() {
super.init();
titleLabelY = 6;
searchBox = addRenderableWidget(new EditBox(font, leftPos + 8, topPos + 2, 126, 14,
searchBox = addRenderableWidget(new EditBox(font, leftPos + 8, topPos + 2, 96, 14,
Component.translatable("screen.redstoner.storage_terminal.search")));
searchBox.setMaxLength(StorageTerminalRules.MAX_SEARCH_LENGTH);
searchBox.setHint(Component.translatable("screen.redstoner.storage_terminal.search"));
searchBox.setValue(searchValue);
searchBox.setResponder(this::searchChanged);
setInitialFocus(searchBox);
sortButton = addRenderableWidget(Button.builder(sortButtonLabel(), button -> requestSort())
.bounds(leftPos + 108, topPos + 2, 34, 14)
.tooltip(Tooltip.create(sortTooltip()))
.build());
}
@Override
@@ -114,6 +122,10 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
@Override
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
if (searchBox != null && searchBox.isFocused()
&& !searchBox.isMouseOver(event.x(), event.y())) {
clearSearchFocus();
}
if (event.button() == 0
&& isHovering(SCROLL_X - 2, TERMINAL_TOP, SCROLL_WIDTH + 4, TERMINAL_HEIGHT,
event.x(), event.y())) {
@@ -127,9 +139,9 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
@Override
public boolean keyPressed(KeyEvent event) {
if (searchBox != null && searchBox.isFocused() && event.key() != 256) {
searchBox.keyPressed(event);
return true;
if (searchBox != null && searchBox.isFocused() && minecraft != null
&& minecraft.options.keyInventory.matches(event)) {
clearSearchFocus();
}
return super.keyPressed(event);
}
@@ -148,6 +160,28 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
}
}
private void requestSort() {
sortMode = sortMode.next();
requestedPage = 0;
sortButton.setMessage(sortButtonLabel());
sortButton.setTooltip(Tooltip.create(sortTooltip()));
sendMenuButton(StorageTerminalMenu.SORT_BUTTON_BASE + sortMode.ordinal());
}
private Component sortButtonLabel() {
return Component.literal(switch (sortMode) {
case ALPHABETICAL -> "A→Z";
case COUNT_ASCENDING -> "1↑";
case COUNT_DESCENDING -> "1↓";
});
}
private Component sortTooltip() {
return Component.translatable("screen.redstoner.storage_terminal.sort",
Component.translatable("screen.redstoner.storage_terminal.sort."
+ sortMode.translationSuffix()));
}
private void searchChanged(String value) {
String previous = sentSearchValue;
searchValue = value;
@@ -173,6 +207,11 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
}
}
private void clearSearchFocus() {
searchBox.setFocused(false);
setFocused(null);
}
private void extractScrollBar(GuiGraphicsExtractor graphics) {
int x = leftPos + SCROLL_X;
int y = topPos + TERMINAL_TOP;
@@ -27,6 +27,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
public static final int PAGE_BUTTON_BASE = 10;
public static final int SEARCH_CLEAR_BUTTON = 100;
public static final int SEARCH_BACKSPACE_BUTTON = 101;
public static final int SORT_BUTTON_BASE = 200;
public static final int SEARCH_CODEPOINT_BUTTON_BASE = 1_000;
private static final int PLAYER_INVENTORY_Y = 140;
private static final int REFRESH_INTERVAL = 20;
@@ -49,6 +50,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
private final StorageTerminalLayout layout = new StorageTerminalLayout();
private List<StorageTerminalInventory.Entry> entries = List.of();
private String searchQuery = "";
private StorageTerminalSort sortMode = StorageTerminalSort.ALPHABETICAL;
private int refreshCooldown = REFRESH_INTERVAL;
public StorageTerminalMenu(int containerId, Inventory inventory) {
@@ -131,6 +133,9 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
if (searchQuery.isEmpty()) return true;
searchQuery = searchQuery.substring(0,
searchQuery.offsetByCodePoints(searchQuery.length(), -1));
} else if (buttonId >= SORT_BUTTON_BASE
&& buttonId < SORT_BUTTON_BASE + StorageTerminalSort.values().length) {
sortMode = StorageTerminalSort.values()[buttonId - SORT_BUTTON_BASE];
} else {
int codePoint = buttonId - SEARCH_CODEPOINT_BUTTON_BASE;
if (!isSearchCodePoint(codePoint)) return false;
@@ -211,7 +216,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
if (serverPlayer == null || terminalPos == null) return;
discovery = StorageTerminalNetwork.discover(serverPlayer, terminalPos);
List<StorageTerminalInventory.Entry> liveEntries =
StorageTerminalNetwork.filteredEntries(discovery, searchQuery);
sortMode.order(StorageTerminalNetwork.filteredEntries(discovery, searchQuery));
layout.update(liveEntries, resetOrder);
int page = discovery.status() == StorageTerminalStatus.READY
@@ -5,12 +5,16 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.ShulkerBoxBlock;
/** Server-side matching for the bounded terminal search field. */
public final class StorageTerminalSearch {
private static final int SHULKER_SLOT_COUNT = 27;
private static final Pattern COMBINING_MARKS = Pattern.compile("\\p{M}+");
private static final Pattern NON_ALPHANUMERIC = Pattern.compile("[^\\p{L}\\p{N}]+");
@@ -38,14 +42,27 @@ public final class StorageTerminalSearch {
}
private static boolean matches(ItemStack stack, String[] terms) {
StringBuilder searchable = new StringBuilder(stack.getHoverName().getString());
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
if (id != null) searchable.append(' ').append(id);
searchable.append(' ').append(stack.getItem().getDescriptionId());
StringBuilder searchable = new StringBuilder();
appendSearchableText(searchable, stack);
if (stack.getItem() instanceof BlockItem blockItem
&& blockItem.getBlock() instanceof ShulkerBoxBlock) {
var contents = stack.get(DataComponents.CONTAINER);
if (contents != null) {
contents.nonEmptyItemCopyStream().limit(SHULKER_SLOT_COUNT).forEach(contained ->
appendSearchableText(searchable, contained));
}
}
String normalized = normalize(searchable.toString());
for (String term : terms) {
if (!normalized.contains(term)) return false;
}
return true;
}
private static void appendSearchableText(StringBuilder searchable, ItemStack stack) {
searchable.append(' ').append(stack.getHoverName().getString());
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
if (id != null) searchable.append(' ').append(id);
searchable.append(' ').append(stack.getItem().getDescriptionId());
}
}
@@ -0,0 +1,59 @@
package fr.koka99cab.sanctuary26.redstoner.storage;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.ItemStack;
/** Explicit server-side ordering modes for terminal entries. */
public enum StorageTerminalSort {
ALPHABETICAL("alphabetical"),
COUNT_ASCENDING("count_ascending"),
COUNT_DESCENDING("count_descending");
private final String translationSuffix;
StorageTerminalSort(String translationSuffix) {
this.translationSuffix = translationSuffix;
}
public String translationSuffix() {
return translationSuffix;
}
public StorageTerminalSort next() {
StorageTerminalSort[] values = values();
return values[(ordinal() + 1) % values.length];
}
public List<StorageTerminalInventory.Entry> order(
List<StorageTerminalInventory.Entry> entries) {
List<StorageTerminalInventory.Entry> ordered = new ArrayList<>(entries);
ordered.sort(comparator());
return List.copyOf(ordered);
}
private Comparator<StorageTerminalInventory.Entry> comparator() {
Comparator<StorageTerminalInventory.Entry> alphabetical = Comparator
.comparing((StorageTerminalInventory.Entry entry) -> alphabeticalName(entry.stack()))
.thenComparing(entry -> itemId(entry.stack()));
return switch (this) {
case ALPHABETICAL -> alphabetical;
case COUNT_ASCENDING -> Comparator
.comparingInt(StorageTerminalInventory.Entry::total).thenComparing(alphabetical);
case COUNT_DESCENDING -> Comparator
.comparingInt(StorageTerminalInventory.Entry::total).reversed().thenComparing(alphabetical);
};
}
private static String alphabeticalName(ItemStack stack) {
return StorageTerminalSearch.normalize(stack.getHoverName().getString());
}
private static String itemId(ItemStack stack) {
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
return id == null ? "" : id.toString();
}
}
@@ -55,6 +55,10 @@
"screen.redstoner.storage_terminal.search": "Search…",
"screen.redstoner.storage_terminal.no_results": "No items found.",
"screen.redstoner.storage_terminal.summary": "%s/%s · %s blocks · %s types",
"screen.redstoner.storage_terminal.sort": "Sort: %s",
"screen.redstoner.storage_terminal.sort.alphabetical": "alphabetical",
"screen.redstoner.storage_terminal.sort.count_ascending": "quantity, ascending",
"screen.redstoner.storage_terminal.sort.count_descending": "quantity, descending",
"screen.redstoner.storage_terminal.error.invalid_terminal": "The terminal is no longer accessible.",
"screen.redstoner.storage_terminal.error.disconnected": "No connected chest or barrel.",
"screen.redstoner.storage_terminal.error.too_large": "Network too large (maximum 128 storage blocks).",
@@ -55,6 +55,10 @@
"screen.redstoner.storage_terminal.search": "Rechercher…",
"screen.redstoner.storage_terminal.no_results": "Aucun objet trouvé.",
"screen.redstoner.storage_terminal.summary": "%s/%s · %s blocs · %s types",
"screen.redstoner.storage_terminal.sort": "Tri : %s",
"screen.redstoner.storage_terminal.sort.alphabetical": "alphabétique",
"screen.redstoner.storage_terminal.sort.count_ascending": "quantité croissante",
"screen.redstoner.storage_terminal.sort.count_descending": "quantité décroissante",
"screen.redstoner.storage_terminal.error.invalid_terminal": "Le terminal n'est plus accessible.",
"screen.redstoner.storage_terminal.error.disconnected": "Aucun coffre ou baril connecté.",
"screen.redstoner.storage_terminal.error.too_large": "Réseau trop grand (128 stockages maximum).",
@@ -55,6 +55,10 @@
"screen.redstoner.storage_terminal.search": "Поиск…",
"screen.redstoner.storage_terminal.no_results": "Предметы не найдены.",
"screen.redstoner.storage_terminal.summary": "%s/%s · блоков: %s · типов: %s",
"screen.redstoner.storage_terminal.sort": "Сортировка: %s",
"screen.redstoner.storage_terminal.sort.alphabetical": "по алфавиту",
"screen.redstoner.storage_terminal.sort.count_ascending": "по возрастанию количества",
"screen.redstoner.storage_terminal.sort.count_descending": "по убыванию количества",
"screen.redstoner.storage_terminal.error.invalid_terminal": "Терминал больше недоступен.",
"screen.redstoner.storage_terminal.error.disconnected": "Нет подключённых сундуков или бочек.",
"screen.redstoner.storage_terminal.error.too_large": "Сеть слишком велика (не более 128 хранилищ).",
@@ -14,6 +14,7 @@ import net.minecraft.world.SimpleContainer;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.ItemContainerContents;
public final class StorageTerminalSmoke {
private StorageTerminalSmoke() {
@@ -26,6 +27,7 @@ public final class StorageTerminalSmoke {
verifyPagination();
verifyAggregationAndTransfers();
verifySearch();
verifySorting();
verifyStableLayout();
}
@@ -115,6 +117,14 @@ public final class StorageTerminalSmoke {
ItemStack russian = new ItemStack(stackable(Items.REDSTONE), 3);
russian.set(DataComponents.CUSTOM_NAME, Component.literal("Красный камень"));
container.setItem(2, russian);
Holder<Item> shulkerItem = new Holder.Direct<>(Items.SHULKER_BOX, DataComponentMap.builder()
.set(DataComponents.MAX_STACK_SIZE, 1).build());
ItemStack shulker = new ItemStack(shulkerItem);
ItemStack nestedDiamond = new ItemStack(stackable(Items.DIAMOND), 7);
nestedDiamond.set(DataComponents.CUSTOM_NAME, Component.literal("Trésor intérieur"));
shulker.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(List.of(
nestedDiamond, new ItemStack(stackable(Items.OAK_LOG), 32))));
container.setItem(3, shulker);
List<StorageTerminalInventory.Entry> entries = StorageTerminalInventory.aggregate(List.of(container));
require(StorageTerminalSearch.filter(entries, "minecraft:cobblestone").size() == 1,
@@ -125,6 +135,12 @@ public final class StorageTerminalSmoke {
"Terminal search did not ignore accents in a custom name");
require(StorageTerminalSearch.filter(entries, "красный камень").size() == 1,
"Terminal search did not preserve non-Latin keyboard input");
require(StorageTerminalSearch.filter(entries, "minecraft:oak_log").size() == 1,
"Terminal search did not find an item id inside a Shulker box");
require(StorageTerminalSearch.filter(entries, "tresor interieur oak log").size() == 1,
"Terminal search did not index the composed contents of a Shulker box");
require(StorageTerminalSearch.filter(entries, "shulker box").size() == 1,
"Searching Shulker contents broke matching against the box itself");
require(StorageTerminalSearch.filter(entries, "missing").isEmpty(),
"Terminal search retained an unrelated item");
}
@@ -134,6 +150,47 @@ public final class StorageTerminalSmoke {
.set(DataComponents.MAX_STACK_SIZE, 64).build());
}
private static void verifySorting() {
ItemStack eclair = new ItemStack(stackable(Items.STONE));
eclair.set(DataComponents.CUSTOM_NAME, Component.literal("Éclair"));
ItemStack banana = new ItemStack(stackable(Items.DIRT));
banana.set(DataComponents.CUSTOM_NAME, Component.literal("Banane"));
ItemStack zebra = new ItemStack(stackable(Items.DIAMOND));
zebra.set(DataComponents.CUSTOM_NAME, Component.literal("Zèbre"));
List<StorageTerminalInventory.Entry> entries = List.of(
new StorageTerminalInventory.Entry(eclair, 64),
new StorageTerminalInventory.Entry(banana, 8),
new StorageTerminalInventory.Entry(zebra, 2));
require(names(StorageTerminalSort.ALPHABETICAL.order(entries))
.equals(List.of("Banane", "Éclair", "Zèbre")),
"Alphabetical terminal sorting ignored names or accent folding");
require(names(StorageTerminalSort.COUNT_ASCENDING.order(entries))
.equals(List.of("Zèbre", "Banane", "Éclair")),
"Ascending terminal quantity sorting is incorrect");
require(names(StorageTerminalSort.COUNT_DESCENDING.order(entries))
.equals(List.of("Éclair", "Banane", "Zèbre")),
"Descending terminal quantity sorting is incorrect");
require(StorageTerminalSort.ALPHABETICAL.next() == StorageTerminalSort.COUNT_ASCENDING
&& StorageTerminalSort.COUNT_ASCENDING.next() == StorageTerminalSort.COUNT_DESCENDING
&& StorageTerminalSort.COUNT_DESCENDING.next() == StorageTerminalSort.ALPHABETICAL,
"Terminal sorting modes do not cycle deterministically");
StorageTerminalLayout stable = new StorageTerminalLayout();
stable.update(StorageTerminalSort.COUNT_DESCENDING.order(entries), true);
List<StorageTerminalInventory.Entry> changedCounts = List.of(
new StorageTerminalInventory.Entry(eclair, 1),
new StorageTerminalInventory.Entry(banana, 80),
new StorageTerminalInventory.Entry(zebra, 2));
stable.update(StorageTerminalSort.COUNT_DESCENDING.order(changedCounts), false);
require(names(stable.page(0).subList(0, 3)).equals(List.of("Éclair", "Banane", "Zèbre")),
"A periodic quantity refresh moved terminal entries under the cursor");
}
private static List<String> names(List<StorageTerminalInventory.Entry> entries) {
return entries.stream().map(entry -> entry.stack().getHoverName().getString()).toList();
}
private static void verifyStableLayout() {
StorageTerminalInventory.Entry stone = new StorageTerminalInventory.Entry(
new ItemStack(stackable(Items.STONE)), 64);
+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);
}