feat: ajouter la dimension persistante Backroom #133
@@ -6,6 +6,18 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
- Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46.
|
||||
- Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées.
|
||||
|
||||
## Non publié
|
||||
|
||||
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 s’accroupissant devant un lit et quittée uniquement par un lit, avec arrivées variées dans une zone architecturale de 2 000 blocs ;
|
||||
- transfère atomiquement vers la Mailbox tout l’équipement d’entrée, y compris les rangées étendues, l’œuf compagnon et la cape, avec reprise idempotente après interruption ;
|
||||
- conserve les objets perdus au dépop, dans le vide et lors des morts en Backroom dans un registre borné et non duplicable ;
|
||||
- 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 d’Another World aux anomalies et les blocs précieux à de rares cachettes ;
|
||||
- fait enregistrer la mousse turquoise par Another World via l’API publique de matériaux Backroom, sans dépendance circulaire ;
|
||||
- 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`
|
||||
|
||||
Module modifié : **Sanctuary `0.0.0-alpha.130`**. Les neuf autres modules conservent leur version issue de l'alpha.225.
|
||||
|
||||
@@ -1956,7 +1956,7 @@ tasks.register("verifyAnotherWorldCatalog") {
|
||||
"src/main/resources/assets/redstoner/textures/item/weather_gauge.png",
|
||||
"src/main/resources/data/redstoner/recipe/weather_gauge.json"
|
||||
]
|
||||
if (project.version.toString() != "0.0.0-alpha.56"
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| sulfurMigration.target?.modules?.anotherworld != "0.0.0-alpha.44"
|
||||
|| sulfurMigration.sulfur_caves?.sanctuary?.dripstone_cell_period != 2
|
||||
|| !javaText.contains('Identifier.parse("sanctuary:sulfur_caves")')
|
||||
@@ -3273,7 +3273,7 @@ tasks.register("verifyClimateBiomeMixer") {
|
||||
"pack/migrations/26.2.0-alpha.188-beta-climate-provinces-to-alpha.189.json"))
|
||||
def mixer = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/anotherworld/mixin/MultiNoiseBiomeSourceParameterListPresetMixin.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.56"
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.188"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.189"
|
||||
|| migration.target?.modules?.anotherworld != "0.0.0-alpha.48"
|
||||
@@ -3300,3 +3300,33 @@ tasks.register("verifyClimateBiomeMixer") {
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyClimateBiomeMixer")
|
||||
}
|
||||
|
||||
tasks.register("verifyBackroomMaterialIntegration") {
|
||||
group = "verification"
|
||||
description = "Checks the one-way Another World material integration with Sanctuary Backroom."
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldBlocks.java"))
|
||||
inputs.file(rootProject.file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java"))
|
||||
doLast {
|
||||
def initializer = file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java").text
|
||||
def blocks = file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldBlocks.java").text
|
||||
def api = rootProject.file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text
|
||||
def rootBuild = rootProject.file("build.gradle").text
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| !initializer.contains("BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS)")
|
||||
|| !blocks.contains('"turquoise_moss"')
|
||||
|| !api.contains("public static void registerAnomalyMaterial")
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.131"
|
||||
|| !rootBuild.contains('project(":anotherworld")')
|
||||
|| !rootBuild.contains('implementation project(":sanctuary")')) {
|
||||
throw new GradleException("Another World alpha.57 Backroom material integration is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyBackroomMaterialIntegration")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.item.WeatherCompassItem;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.network.AnotherWorldNetworking;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldRegistries;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlocks;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.backroom.BackroomMaterialApi;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
@@ -26,6 +28,7 @@ public final class AnotherWorldMod implements ModInitializer {
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
AnotherWorldRegistries.initialize();
|
||||
BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS);
|
||||
VaultService.initialize();
|
||||
WeatherCompassItem.registerFrameInteraction();
|
||||
CanonMod.initialize();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"ambiance": "*",
|
||||
"sanctuary": ">=0.0.0-alpha.117"
|
||||
"sanctuary": ">=0.0.0-alpha.131"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
|
||||
+51
-44
@@ -2916,11 +2916,11 @@ tasks.register("verifyNotchEncounter") {
|
||||
def textureHash = MessageDigest.getInstance("SHA-256").digest(texture.bytes).encodeHex().toString()
|
||||
if (rootProject.itsalive_version != "0.0.0-alpha.38"
|
||||
|| rootProject.ouch_version != "0.0.0-alpha.11"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| rootProject.masterkey_version != "0.0.0-alpha.9"
|
||||
|| rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| manifests.itsalive.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.ouch.depends?.itsalive != ">=0.0.0-alpha.37"
|
||||
|| !registry.contains('Identifier.fromNamespaceAndPath("portals", "notch")')
|
||||
|| !resident.contains("setCanonicalUuid(canonical.getUUID())")
|
||||
@@ -3010,7 +3010,7 @@ tasks.register("verifyMagicCarpet") {
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifests.iliketomoveit.depends?.sanctuary != ">=0.0.0-alpha.127"
|
||||
@@ -3018,7 +3018,7 @@ tasks.register("verifyMagicCarpet") {
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
|| manifests.onlyfun.depends?.iliketomoveit != ">=0.0.0-alpha.17"
|
||||
|| manifests.onlyfun.depends?.sanctuary != ">=0.0.0-alpha.129"
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| migration.status != "released"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.223"
|
||||
@@ -3641,7 +3641,7 @@ tasks.register("verifyProgressivePauseMenuRelease") {
|
||||
"sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))]
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"]
|
||||
@@ -3691,7 +3691,7 @@ tasks.register("verifyMirrorLavenderDrawerRelease") {
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java").text
|
||||
def drawerRecipe = file("redstoner/src/main/resources/data/redstoner/recipe/drawer.json")
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.191"
|
||||
@@ -4484,7 +4484,7 @@ tasks.register("verifyShopPricingLayoutRelease") {
|
||||
def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.192"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.193"
|
||||
@@ -4578,7 +4578,7 @@ tasks.register("verifyShopCsvBossGatesRelease") {
|
||||
def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.194"
|
||||
|| migration.source?.modules != [onlyfun: "0.0.0-alpha.18", sanctuary: "0.0.0-alpha.110"]
|
||||
@@ -4671,7 +4671,7 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") {
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.196"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.197"
|
||||
|| migration.target?.modules != [itsalive: "0.0.0-alpha.29", sanctuary: "0.0.0-alpha.113"]
|
||||
@@ -4771,7 +4771,7 @@ tasks.register("verifyQuestRequestContractsRelease") {
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.198"
|
||||
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.114"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.199"
|
||||
@@ -4891,7 +4891,7 @@ tasks.register("verifyQuestRequestEscrowRelease") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -4903,7 +4903,7 @@ tasks.register("verifyQuestRequestEscrowRelease") {
|
||||
|| migration.quest_request_escrow?.kill_progress_automatic != true
|
||||
|| migration.quest_request_escrow?.reward_delivery_to_claimant_mailbox != true
|
||||
|| migration.compatibility?.sanctuary_target_network_protocol != 21
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 13
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 14
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| release.pack_version != "26.2.0-alpha.226"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.226.mrpack"
|
||||
@@ -4945,10 +4945,10 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file(
|
||||
"sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.source?.modules != [
|
||||
itsalive: "0.0.0-alpha.30",
|
||||
@@ -4982,7 +4982,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
|| itsAliveManifest.custom?.sanctuary26?.network_protocol != 4
|
||||
|| moveItManifest.custom?.sanctuary26?.data_version != 4
|
||||
|| moveItManifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 13
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 14
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| release.pack_version != "26.2.0-alpha.226"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.226.mrpack"
|
||||
@@ -5105,8 +5105,8 @@ tasks.register("verifySafeVaultFeature") {
|
||||
image == null || image.width != size[0] || image.height != size[1]
|
||||
}.keySet()
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.schema_version != 1
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.202"
|
||||
@@ -5226,7 +5226,7 @@ tasks.register("verifySafeVaultFeature") {
|
||||
|| unlimitedMigration.compatibility?.registry_ids_changed != false
|
||||
|| unlimitedMigration.compatibility?.pack_release_updated != true
|
||||
|| unlimitedMigration.distribution?.packwiz_index_refreshed != true
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.117"
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.131"
|
||||
|| manifest.custom?.sanctuary26?.data_version != 5
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 2
|
||||
|| !file("build.gradle").text.contains('implementation project(":sanctuary")')
|
||||
@@ -5297,7 +5297,7 @@ tasks.register("verifySafeVaultInterfaceAlpha203Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.202"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.50"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.203"
|
||||
@@ -5343,7 +5343,7 @@ tasks.register("verifySafeVaultFixAlpha204Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.203"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.51"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.204"
|
||||
@@ -5397,7 +5397,7 @@ tasks.register("verifySafeVaultCubeAlpha205Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.204"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.52"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.205"
|
||||
@@ -5455,7 +5455,7 @@ tasks.register("verifySafeVaultCtmAlpha206Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.205"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.53"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.206"
|
||||
@@ -5507,7 +5507,7 @@ tasks.register("verifySafeVaultUnlimitedAlpha207Release") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.198.jar" }
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.56"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.206"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.54"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.207"
|
||||
@@ -5966,7 +5966,7 @@ tasks.register("verifyAlpha210Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.29.0.198.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.38"
|
||||
@@ -6003,7 +6003,7 @@ tasks.register("verifyAlpha210Release") {
|
||||
|| migration.distribution?.jei?.target != "30.28.0.193"
|
||||
|| migration.distribution?.jei?.modrinth_version_id != "qaCQ7LGO"
|
||||
|| migration.distribution?.jei?.packwiz_and_prism_synchronized != true
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
@@ -6068,9 +6068,9 @@ tasks.register("verifyAlpha211Release") {
|
||||
itsalive: "0.0.0-alpha.38",
|
||||
ouch: "0.0.0-alpha.11",
|
||||
masterkey: "0.0.0-alpha.9"]
|
||||
def workspaceModules = currentModules
|
||||
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"]
|
||||
def expectedDataVersions = [ambiance: 2, itsalive: 3, onlyfun: 2, ouch: 1, sanctuary: 11]
|
||||
def workspaceDataVersions = [ambiance: 2, itsalive: 4, onlyfun: 2, ouch: 1, sanctuary: 13]
|
||||
def workspaceDataVersions = [ambiance: 2, itsalive: 4, onlyfun: 2, ouch: 1, sanctuary: 14]
|
||||
def expectedProtocols = [ambiance: 6, itsalive: 4, onlyfun: 3, ouch: 1, sanctuary: 22]
|
||||
def workspaceProtocols = expectedProtocols + [sanctuary: 24]
|
||||
def manifests = expectedTarget.collectEntries { id, version ->
|
||||
@@ -6168,7 +6168,7 @@ tasks.register("verifyAlpha212Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.29.0.198.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.211"
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.119"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.212"
|
||||
@@ -6186,7 +6186,7 @@ tasks.register("verifyAlpha212Release") {
|
||||
|| migration.distribution?.packwiz_index_refreshed != true
|
||||
|| migration.distribution?.prism_modrinth_index_updated != true
|
||||
|| migration.distribution?.shared_client_server_mrpack != true
|
||||
|| manifest.custom?.sanctuary26?.data_version != 13
|
||||
|| manifest.custom?.sanctuary26?.data_version != 14
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !minecraftMixin.contains('method = "pickBlockOrEntity"')
|
||||
|| release.pack_version != "26.2.0-alpha.226"
|
||||
@@ -6667,7 +6667,7 @@ tasks.register("verifyAlpha218Release") {
|
||||
}
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.27"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.18"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.217"
|
||||
|| migration.source?.modules != sourceModules
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.218"
|
||||
@@ -6691,7 +6691,7 @@ tasks.register("verifyAlpha218Release") {
|
||||
|| manifests.redstoner.custom?.sanctuary26?.network_protocol != 7
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !rules.contains("MAX_CONTAINERS = 128")
|
||||
|| !rules.contains("PAGE_SIZE = 54")
|
||||
@@ -6753,7 +6753,7 @@ tasks.register("verifyAlpha219Release") {
|
||||
ambiance: "0.0.0-alpha.22", sanctuary: "0.0.0-alpha.130",
|
||||
iliketomoveit: "0.0.0-alpha.18", onlyfun: "0.0.0-alpha.27",
|
||||
itsalive: "0.0.0-alpha.38", ouch: "0.0.0-alpha.11", masterkey: "0.0.0-alpha.9"]
|
||||
def workspaceModules = currentModules
|
||||
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"]
|
||||
def requiredJars = currentModules.collect { id, version ->
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
@@ -6793,7 +6793,7 @@ tasks.register("verifyAlpha219Release") {
|
||||
|| migration.compatibility?.iliketomoveit_network_protocol != 5
|
||||
|| migration.compatibility?.ambiance_data_version != 2
|
||||
|| migration.compatibility?.ambiance_network_protocol != 6
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
@@ -7078,7 +7078,7 @@ tasks.register("verifyAlpha222Release") {
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.221"
|
||||
|| migration.source?.modules != [
|
||||
redstoner: "0.0.0-alpha.12", sanctuary: "0.0.0-alpha.122"]
|
||||
@@ -7120,7 +7120,7 @@ tasks.register("verifyAlpha222Release") {
|
||||
|| migration.distribution?.jei?.packwiz_and_prism_synchronized != true
|
||||
|| manifests.redstoner.custom?.sanctuary26?.data_version != 3
|
||||
|| manifests.redstoner.custom?.sanctuary26?.network_protocol != 7
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !rules.contains("MAX_SEARCH_LENGTH = 32")
|
||||
|| !rules.contains("compactCount(int count)")
|
||||
@@ -7236,7 +7236,7 @@ tasks.register("verifyAlpha223Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.29.0.198.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.222"
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.124"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.223"
|
||||
@@ -7272,7 +7272,7 @@ tasks.register("verifyAlpha223Release") {
|
||||
|| migration.distribution?.jei?.target != "30.29.0.198"
|
||||
|| migration.distribution?.jei?.modrinth_version_id != "67Dvn8sL"
|
||||
|| migration.distribution?.jei?.packwiz_and_prism_synchronized != true
|
||||
|| manifest.custom?.sanctuary26?.data_version != 13
|
||||
|| manifest.custom?.sanctuary26?.data_version != 14
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !inventorySort.contains("COOLDOWN_TICKS = 8")
|
||||
|| !inventorySort.contains("if (row == 0) continue")
|
||||
@@ -7357,6 +7357,7 @@ tasks.register("verifyAlpha224Release") {
|
||||
def currentModules = targetModules + [
|
||||
itsalive: "0.0.0-alpha.38", iliketomoveit: "0.0.0-alpha.18",
|
||||
onlyfun: "0.0.0-alpha.27", sanctuary: "0.0.0-alpha.130"]
|
||||
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.131"]
|
||||
def inventorySort = file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryInventorySort.java").text
|
||||
def clientProgress = file(
|
||||
@@ -7376,7 +7377,7 @@ tasks.register("verifyAlpha224Release") {
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| currentModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| workspaceModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| migration.status != "released"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.223"
|
||||
|| migration.source?.modules != sourceModules
|
||||
@@ -7407,7 +7408,7 @@ tasks.register("verifyAlpha224Release") {
|
||||
|| migration.distribution?.prism_modrinth_index_updated != true
|
||||
|| manifests.itsalive.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 14
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24
|
||||
|| !inventorySort.contains("instanceof ChestMenu")
|
||||
|| !inventorySort.contains("instanceof ShulkerBoxMenu")
|
||||
@@ -7477,10 +7478,14 @@ tasks.register("verifyAlpha225Release") {
|
||||
iliketomoveit: "0.0.0-alpha.18", onlyfun: "0.0.0-alpha.27",
|
||||
sanctuary: "0.0.0-alpha.129"]
|
||||
def currentModules = targetModules + [sanctuary: "0.0.0-alpha.130"]
|
||||
def currentWorkspaceModules = currentModules + [
|
||||
anotherworld: "0.0.0-alpha.57", sanctuary: "0.0.0-alpha.131"]
|
||||
def workspaceModules = moduleIds.collectEntries { id ->
|
||||
[(id): project(":${id}").version.toString()]
|
||||
}
|
||||
def requiredJars = workspaceModules.collect { id, version ->
|
||||
def releasedModules = workspaceModules + [
|
||||
anotherworld: "0.0.0-alpha.56", sanctuary: "0.0.0-alpha.130"]
|
||||
def requiredJars = releasedModules.collect { id, version ->
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
def obsoleteJars = sourceModules.collect { id, version ->
|
||||
@@ -7518,7 +7523,7 @@ tasks.register("verifyAlpha225Release") {
|
||||
def carpetTextureHash = MessageDigest.getInstance("SHA-256")
|
||||
.digest(carpetTexture.bytes).encodeHex().toString()
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| currentModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| currentWorkspaceModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| migration.status != "released"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.224"
|
||||
|| migration.source?.modules != sourceModules
|
||||
@@ -7639,11 +7644,13 @@ tasks.register("verifyAlpha226Release") {
|
||||
def workspaceModules = moduleIds.collectEntries { id ->
|
||||
[(id): project(":${id}").version.toString()]
|
||||
}
|
||||
def requiredJars = workspaceModules.collect { id, version ->
|
||||
def releasedModules = workspaceModules + [
|
||||
anotherworld: "0.0.0-alpha.56", sanctuary: "0.0.0-alpha.130"]
|
||||
def requiredJars = releasedModules.collect { id, version ->
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.226"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.130"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.131"
|
||||
|| migration.status != "released"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.225"
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.129"]
|
||||
@@ -7660,7 +7667,7 @@ tasks.register("verifyAlpha226Release") {
|
||||
|| migration.compatibility?.existing_saves_supported != true
|
||||
|| migration.compatibility?.new_world_required != false
|
||||
|| migration.compatibility?.save_reset_required != false
|
||||
|| manifest.custom?.sanctuary26?.data_version != 13
|
||||
|| manifest.custom?.sanctuary26?.data_version != 14
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| baseRecipe.type != "minecraft:crafting_shaped"
|
||||
|| baseRecipe.pattern != ["WW", "WW", "WW"]
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ ambiance_version=0.0.0-alpha.22
|
||||
ambiance_lifecycle=active
|
||||
redstoner_version=0.0.0-alpha.13
|
||||
redstoner_lifecycle=active
|
||||
anotherworld_version=0.0.0-alpha.56
|
||||
anotherworld_version=0.0.0-alpha.57
|
||||
anotherworld_lifecycle=active
|
||||
itsalive_version=0.0.0-alpha.38
|
||||
itsalive_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.130
|
||||
sanctuary_version=0.0.0-alpha.131
|
||||
sanctuary_lifecycle=active
|
||||
pack_version=26.2.0-alpha.226
|
||||
maven_group=fr.koka99cab.sanctuary26
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "pending_release",
|
||||
"module": "sanctuary",
|
||||
"source_module_version": "0.0.0-alpha.130",
|
||||
"target_module_version": "0.0.0-alpha.131",
|
||||
"integration_modules": {
|
||||
"anotherworld": {
|
||||
"source": "0.0.0-alpha.56",
|
||||
"target": "0.0.0-alpha.57",
|
||||
"data_version_changed": false,
|
||||
"network_protocol_changed": false
|
||||
}
|
||||
},
|
||||
"source_data_version": 13,
|
||||
"target_data_version": 14,
|
||||
"migration": {
|
||||
"kind": "monotone_addition",
|
||||
"save_key": "sanctuary:backroom",
|
||||
"existing_player_progress_preserved": true,
|
||||
"existing_shop_data_preserved": true,
|
||||
"existing_mailboxes_preserved": true,
|
||||
"mailbox_transaction_history_default": [],
|
||||
"backroom_loss_registry_default": [],
|
||||
"backroom_chest_registry_default": []
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"network_protocol_changed": false,
|
||||
"registry_ids_added": [
|
||||
"sanctuary:backroom",
|
||||
"sanctuary:backroom_loss_id",
|
||||
"sanctuary:backroom_architecture"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Backroom
|
||||
|
||||
La Backroom est une dimension persistante de récupération et de construction. Elle n’est jamais
|
||||
réinitialisée : routes, bases, coffres et bâtiments posés par les habitants restent dans le monde.
|
||||
Sa zone architecturale forme un disque de 2 000 blocs autour de l’origine.
|
||||
|
||||
## Entrer et sortir
|
||||
|
||||
- S’accroupir et utiliser un lit hors de la Backroom déclenche l’entrée, sans remplacer l’usage
|
||||
normal du lit.
|
||||
- Avant toute téléportation, le serveur transfère atomiquement vers la Mailbox enregistrée les
|
||||
43 emplacements vanilla, les 18 emplacements étendus, l’œuf compagnon et la cape équipée.
|
||||
- Si aucune Mailbox extérieure n’est chargée, ou si elle ne peut pas recevoir l’intégralité du
|
||||
colis, rien ne bouge et l’entrée est refusée. Une Mailbox construite dans la Backroom n’est jamais
|
||||
une destination d’entrée et ne permet donc pas d’importer du matériel.
|
||||
- L’identifiant de transaction est conservé par la Mailbox. Après une interruption, la connexion
|
||||
suivante termine exactement une fois le vidage correspondant ou annule une transaction qui n’a
|
||||
jamais atteint la Mailbox.
|
||||
- Dans la Backroom, tout lit — généré ou posé par un joueur — ramène au lit de réapparition normal,
|
||||
ou au point d’apparition global s’il n’existe plus. Le joueur conserve alors ses trouvailles.
|
||||
- Aucun autre usage de lit ne définit un point de réapparition dans la dimension.
|
||||
- Toute autre téléportation vers la Backroom est refusée. Une téléportation forcée vers l’extérieur
|
||||
reste possible pour l’administration, mais replace d’abord le butin porté dans le registre : elle
|
||||
ne constitue jamais une extraction réussie.
|
||||
|
||||
## Objets perdus et coffres
|
||||
|
||||
Les piles qui disparaissent naturellement ou tombent dans le vide rejoignent un registre mondial
|
||||
borné. Une mort de joueur dans la Backroom y replace aussi tout ce qu’il transportait. Chaque pile
|
||||
réelle matérialisée reçoit un identifiant persistant : elle peut passer du registre à un coffre, puis
|
||||
à un joueur et revenir au registre, mais elle ne peut jamais exister deux fois.
|
||||
Une perte associée à un joueur lui reste réservée pendant 24 000 ticks avant de pouvoir rejoindre
|
||||
les tirages historiques partagés ; une perte sans propriétaire est immédiatement partagée.
|
||||
|
||||
Tous les coffres vanilla de la Backroom, générés ou posés, deviennent des nœuds de butin renouvelés
|
||||
toutes les 12 000 ticks lors de leur prochaine ouverture. Le renouvellement rend au registre les
|
||||
objets perdus non pris, efface le contenu ordinaire et produit le plus souvent du vide ou des
|
||||
ressources pauvres : terre, pierre, planches, torches, nourriture et outils simples. Le butin ancien
|
||||
partagé et les objets de valeur restent rares. Reposer un coffre au même emplacement pendant le
|
||||
même cycle ne crée pas un nouveau tirage ; multiplier les emplacements donne en revanche bien des
|
||||
tirages indépendants, au prix des ressources trouvées ou produites sur place.
|
||||
|
||||
Pendant une expédition, chaque coffre distinct est un essai personnel. Le premier a 75 % de chance
|
||||
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
|
||||
|
||||
Les plans sont déterministes par grandes zones et utilisent cinq familles composées :
|
||||
|
||||
- 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 à l’envers, sans modifier la gravité globale.
|
||||
|
||||
Les matériaux très marqués d’Another World n’apparaissent 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.
|
||||
|
||||
## Exploitation
|
||||
|
||||
Les données `sanctuary:backroom` utilisent leur propre `data_version`, ainsi que des limites pour les
|
||||
transactions, pertes, coffres suivis, visites et arrivées récentes. Les opérateurs disposent de
|
||||
`/backroom status` et `/backroom inspect <joueur>` pour contrôler les registres sans les modifier.
|
||||
+90
-16
@@ -156,6 +156,14 @@ tasks.register("notchVictoryStateSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.data.NotchVictoryStateSmoke"
|
||||
}
|
||||
|
||||
tasks.register("backroomModelSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks bounded Backroom entry data, personal loot priority and composed architecture zones."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomModelSmoke"
|
||||
}
|
||||
|
||||
tasks.register("verifyBlackMarket") {
|
||||
group = "verification"
|
||||
description = "Checks the physical sign market, flash Shop, escrow ledger and mailbox delivery."
|
||||
@@ -191,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| !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")
|
||||
@@ -1200,13 +1208,13 @@ tasks.register("verifySanctuary") {
|
||||
}
|
||||
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (manifest.custom?.sanctuary26?.data_version != 13
|
||||
if (manifest.custom?.sanctuary26?.data_version != 14
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 24
|
||||
|| manifest.depends?.jei != ">=30.24.0.176"
|
||||
|| manifest.depends?."fabric-api" != ">=0.155.0+26.2"
|
||||
|| !manifest.entrypoints?.client?.contains("fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClient")
|
||||
|| !manifest.mixins?.contains("sanctuary.mixins.json")) {
|
||||
throw new GradleException("Sanctuary must expose data v13, protocol 24, JEI and the client entrypoint")
|
||||
throw new GradleException("Sanctuary must expose data v14, protocol 24, JEI and the client entrypoint")
|
||||
}
|
||||
def capeMigration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.155-cape-equipment-to-alpha.156.json"))
|
||||
@@ -1691,8 +1699,8 @@ tasks.register("verifySanctuary") {
|
||||
|
||||
def worldgenRoot = file("src/main/resources/data/sanctuary/worldgen")
|
||||
def worldgenCounts = [
|
||||
density_function: 12, biome: 6, configured_feature: 13, configured_carver: 1,
|
||||
placed_feature: 19, noise_settings: 3, world_preset: 2
|
||||
density_function: 12, biome: 7, configured_feature: 14, configured_carver: 1,
|
||||
placed_feature: 20, noise_settings: 4, world_preset: 2
|
||||
]
|
||||
worldgenCounts.each { type, expected ->
|
||||
def actual = fileTree(new File(worldgenRoot, type.toString())).matching { include "**/*.json" }.files.size()
|
||||
@@ -2907,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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
|
||||
@@ -3073,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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
|
||||
@@ -3111,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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]
|
||||
@@ -3142,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.195"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|
||||
|| migration.compatibility?.sanctuary_data_version != 11
|
||||
@@ -3173,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|
||||
|| migration.mailbox?.id != "sanctuary:delivery_box"
|
||||
|| migration.mailbox?.horizontal_facing != true
|
||||
@@ -3201,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| !transfer.contains("shouldAppendOverflow")
|
||||
|| !transfer.contains("isVanillaStorageRange")
|
||||
|| !transfer.contains("hotbarStart")
|
||||
@@ -3247,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|
||||
|| migration.quest_requests?.save_key != "quest_requests"
|
||||
|| migration.quest_requests?.data_version != 1
|
||||
@@ -3305,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -3374,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| 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"
|
||||
@@ -3453,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.130"
|
||||
|| migration.white_cape?.crafting_material_count != 6
|
||||
|| migration.white_cape?.maximum_pattern_layers != 6
|
||||
@@ -3508,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.130"
|
||||
if (project.version.toString() != "0.0.0-alpha.131"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.122"
|
||||
|| migration.alpha_world?.dimension != "sanctuary:alpha"
|
||||
|| migration.alpha_world?.legacy_dimension != "portals:alpha"
|
||||
@@ -3530,7 +3538,73 @@ tasks.register("verifyAlphaIndev") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyBackroom") {
|
||||
group = "verification"
|
||||
description = "Checks Backroom dimension, migration, Mailbox boundary, loss registry and translations."
|
||||
inputs.file(file("BACKROOM.md"))
|
||||
inputs.file(rootProject.file("pack/migrations/sanctuary-backroom-data-v14.json"))
|
||||
inputs.files(fileTree("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom"))
|
||||
inputs.files(fileTree("src/main/resources/data/sanctuary") { include "**/*backroom*.json" })
|
||||
inputs.files(fileTree("src/main/resources/assets/sanctuary/lang") { include "*.json" })
|
||||
inputs.files(
|
||||
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/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 presets = [
|
||||
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")
|
||||
].collect { new JsonSlurper().parse(it) }
|
||||
def data = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomData.java").text
|
||||
def inventory = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomInventory.java").text
|
||||
def service = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/backroom/BackroomService.java").text
|
||||
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 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"
|
||||
|| 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"
|
||||
|| migration.compatibility?.network_protocol_changed != false
|
||||
|| 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"
|
||||
|| !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")
|
||||
|| !inventory.contains("companionEgg") || !inventory.contains("equippedCape")
|
||||
|| !service.contains("ARRIVAL_RADIUS = 2_000") || !service.contains("insertTransaction")
|
||||
|| !service.contains("AUTHORIZED_TRANSITIONS")
|
||||
|| !service.contains("level -> !isBackroom(level)")
|
||||
|| !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()")
|
||||
|| !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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("backroomModelSmoke")
|
||||
dependsOn tasks.named("verifyBackroom")
|
||||
dependsOn tasks.named("notchVictoryStateSmoke")
|
||||
dependsOn tasks.named("verifySanctuary")
|
||||
dependsOn tasks.named("verifyDeliveryBoxOrientation")
|
||||
|
||||
@@ -26,6 +26,7 @@ import fr.koka99cab.sanctuary26.sanctuary.shop.ShopService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.BlackMarketService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestBoardService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestRequestService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomService;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
@@ -34,7 +35,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class SanctuaryMod implements ModInitializer {
|
||||
public static final String MOD_ID = "sanctuary";
|
||||
public static final String DISPLAY_NAME = "Sanctuary";
|
||||
public static final int CURRENT_DATA_VERSION = 13;
|
||||
public static final int CURRENT_DATA_VERSION = 14;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public SanctuaryMod() {
|
||||
@@ -53,6 +54,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
SanctuarySpawn.register();
|
||||
SanctuaryWorldProfileData.register();
|
||||
SanctuaryRegistries.initialize();
|
||||
BackroomService.initialize();
|
||||
SanctuaryBulkActions.initialize();
|
||||
SanctuaryInventoryCapacity.initialize();
|
||||
SanctuaryHotbarRows.initialize();
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.backroom;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Public, one-way integration point for rare materials owned by other modules. */
|
||||
public final class BackroomMaterialApi {
|
||||
private static Block anomalyMaterial = Blocks.WARPED_WART_BLOCK;
|
||||
|
||||
private BackroomMaterialApi() {}
|
||||
|
||||
public static void registerAnomalyMaterial(Block material) {
|
||||
anomalyMaterial = Objects.requireNonNull(material, "Backroom anomaly material");
|
||||
}
|
||||
|
||||
public static Block anomalyMaterial() {
|
||||
return anomalyMaterial;
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.ChestType;
|
||||
|
||||
/** Renewable loot nodes backed by the non-duplicating real-item registry. */
|
||||
public final class BackroomChestService {
|
||||
public static final long REFRESH_TICKS = 12_000L;
|
||||
|
||||
private BackroomChestService() {}
|
||||
|
||||
public static void open(ChestBlockEntity opened, ServerPlayer player) {
|
||||
if (!(opened.getLevel() instanceof ServerLevel level) || !BackroomService.isBackroom(level)) return;
|
||||
ChestGroup group = group(level, opened);
|
||||
BackroomData data = BackroomData.get(level.getServer());
|
||||
long cycle = Math.floorDiv(level.getGameTime(), REFRESH_TICKS);
|
||||
BackroomData.ChestState state = data.chest(group.anchor).orElse(null);
|
||||
ArrayList<UUID> assigned = state == null ? new ArrayList<>() : new ArrayList<>(state.assignedIds());
|
||||
boolean refreshed = state == null || state.cycle() != cycle;
|
||||
|
||||
if (refreshed) {
|
||||
data.reconcileChest(group.anchor, markers(group));
|
||||
group.clear();
|
||||
assigned.clear();
|
||||
}
|
||||
|
||||
BackroomData.ChestVisit visit = data.visitChest(player.getUUID(), group.anchor);
|
||||
boolean personalInserted = false;
|
||||
if (visit.firstVisit() && personalRoll(player.getRandom(), visit.ordinal())) {
|
||||
BackroomData.MaterializedLoss loss = data.takePersonal(player.getUUID(), group.anchor).orElse(null);
|
||||
if (loss != null && group.insertLoss(loss.stack())) {
|
||||
assigned.add(loss.id());
|
||||
personalInserted = true;
|
||||
} else if (loss != null) {
|
||||
data.capture(loss.stack(), player.getUUID(), "chest_full", level.getGameTime());
|
||||
}
|
||||
}
|
||||
if (refreshed) rollGeneralLoot(level, group, data, assigned, cycle);
|
||||
if (refreshed || personalInserted) data.updateChest(group.anchor, cycle, assigned);
|
||||
}
|
||||
|
||||
public static boolean personalRoll(RandomSource random, int ordinal) {
|
||||
if (ordinal <= 0) return false;
|
||||
if (ordinal == 1) return random.nextFloat() < 0.75F;
|
||||
if (ordinal == 2) return random.nextFloat() < 0.90F;
|
||||
if (ordinal == 3) return true;
|
||||
return random.nextFloat() < 0.65F;
|
||||
}
|
||||
|
||||
private static void rollGeneralLoot(ServerLevel level, ChestGroup group, BackroomData data,
|
||||
List<UUID> assigned, long cycle) {
|
||||
RandomSource random = RandomSource.create(level.getSeed() ^ group.anchor ^ cycle * 0x9E3779B97F4A7C15L);
|
||||
int roll = random.nextInt(100);
|
||||
if (roll < 55) return;
|
||||
if (roll < 74) {
|
||||
insert(group, random, Items.DIRT, 4, 20);
|
||||
if (random.nextBoolean()) insert(group, random, Items.OAK_PLANKS, 2, 10);
|
||||
return;
|
||||
}
|
||||
if (roll < 90) {
|
||||
insert(group, random, Items.COBBLESTONE, 6, 24);
|
||||
insert(group, random, random.nextBoolean() ? Items.TORCH : Items.BREAD, 2, 7);
|
||||
if (random.nextInt(3) == 0) group.insert(new ItemStack(Items.WOODEN_PICKAXE));
|
||||
return;
|
||||
}
|
||||
if (roll < 97) {
|
||||
data.takeShared(group.anchor, level.getGameTime()).ifPresent(loss -> {
|
||||
if (group.insertLoss(loss.stack())) assigned.add(loss.id());
|
||||
else data.capture(loss.stack(), null, "chest_full", level.getGameTime());
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (roll < 99) {
|
||||
group.insert(new ItemStack(Items.IRON_PICKAXE));
|
||||
insert(group, random, Items.ENDER_PEARL, 1, 2);
|
||||
} else {
|
||||
group.insert(new ItemStack(Items.DIAMOND));
|
||||
}
|
||||
}
|
||||
|
||||
private static void insert(ChestGroup group, RandomSource random, Item item, int minimum, int maximum) {
|
||||
group.insert(new ItemStack(item, minimum + random.nextInt(maximum - minimum + 1)));
|
||||
}
|
||||
|
||||
private static List<UUID> markers(ChestGroup group) {
|
||||
ArrayList<UUID> result = new ArrayList<>();
|
||||
for (ChestBlockEntity chest : group.chests) for (int slot = 0; slot < chest.getContainerSize(); slot++) {
|
||||
UUID marker = chest.getItem(slot).get(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
if (marker != null) result.add(marker);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ChestGroup group(ServerLevel level, ChestBlockEntity opened) {
|
||||
BlockPos pos = opened.getBlockPos();
|
||||
BlockState state = opened.getBlockState();
|
||||
if (!(state.getBlock() instanceof ChestBlock) || state.getValue(ChestBlock.TYPE) == ChestType.SINGLE) {
|
||||
return new ChestGroup(pos.asLong(), List.of(opened));
|
||||
}
|
||||
BlockPos connectedPos = ChestBlock.getConnectedBlockPos(pos, state);
|
||||
if (!(level.getBlockEntity(connectedPos) instanceof ChestBlockEntity connected)) {
|
||||
return new ChestGroup(pos.asLong(), List.of(opened));
|
||||
}
|
||||
long anchor = Math.min(pos.asLong(), connectedPos.asLong());
|
||||
return pos.asLong() == anchor
|
||||
? new ChestGroup(anchor, List.of(opened, connected))
|
||||
: new ChestGroup(anchor, List.of(connected, opened));
|
||||
}
|
||||
|
||||
private record ChestGroup(long anchor, List<ChestBlockEntity> chests) {
|
||||
private void clear() {
|
||||
chests.forEach(chest -> { chest.clearContent(); chest.setChanged(); });
|
||||
}
|
||||
|
||||
private boolean insert(ItemStack offered) {
|
||||
if (offered == null || offered.isEmpty()) return false;
|
||||
for (ChestBlockEntity chest : chests) if (insertInto(chest, offered)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Keeps every real loss in the physical anchor chest tracked by the persisted state. */
|
||||
private boolean insertLoss(ItemStack offered) {
|
||||
return offered != null && !offered.isEmpty() && insertInto(chests.getFirst(), offered);
|
||||
}
|
||||
|
||||
private static boolean insertInto(ChestBlockEntity chest, ItemStack offered) {
|
||||
for (int slot = 0; slot < chest.getContainerSize(); slot++) {
|
||||
if (!chest.getItem(slot).isEmpty()) continue;
|
||||
chest.setItem(slot, offered.copy());
|
||||
chest.setChanged();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Persistent, bounded state for Backroom entries and the real-item loss pool. */
|
||||
public final class BackroomData extends SavedData {
|
||||
public static final String SAVE_KEY = "backroom";
|
||||
public static final int DATA_VERSION = 1;
|
||||
public static final int MAX_PENDING_ENTRIES = 256;
|
||||
public static final int MAX_ENTRY_SLOTS = 64;
|
||||
public static final int MAX_LOST_ITEMS = 16_384;
|
||||
public static final int MAX_CHEST_STATES = 65_536;
|
||||
public static final int MAX_PLAYER_STATES = 4_096;
|
||||
public static final int MAX_VISITED_CHESTS = 128;
|
||||
public static final int MAX_RECENT_ARRIVALS = 8;
|
||||
public static final long PERSONAL_RESERVATION_TICKS = 24_000L;
|
||||
public static final String LOST_POOL = "pool";
|
||||
public static final String LOST_CHEST = "chest";
|
||||
|
||||
public static final Codec<EntrySlot> ENTRY_SLOT_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("location").forGetter(EntrySlot::location),
|
||||
Codec.INT.fieldOf("index").forGetter(EntrySlot::index),
|
||||
ItemStack.CODEC.fieldOf("item").forGetter(EntrySlot::item)
|
||||
).apply(instance, EntrySlot::new));
|
||||
private static final Codec<PendingEntry> PENDING_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("player").forGetter(PendingEntry::player),
|
||||
Codec.STRING.fieldOf("transaction").forGetter(PendingEntry::transaction),
|
||||
ENTRY_SLOT_CODEC.listOf(0, MAX_ENTRY_SLOTS).fieldOf("slots").forGetter(PendingEntry::slots)
|
||||
).apply(instance, PendingEntry::new));
|
||||
private static final Codec<LostItem> LOST_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("id").forGetter(LostItem::id),
|
||||
Codec.STRING.optionalFieldOf("owner", "").forGetter(LostItem::owner),
|
||||
Codec.STRING.fieldOf("cause").forGetter(LostItem::cause),
|
||||
Codec.LONG.fieldOf("lost_at").forGetter(LostItem::lostAt),
|
||||
ItemStack.CODEC.fieldOf("item").forGetter(LostItem::item),
|
||||
Codec.STRING.optionalFieldOf("state", LOST_POOL).forGetter(LostItem::state),
|
||||
Codec.LONG.optionalFieldOf("chest", Long.MIN_VALUE).forGetter(LostItem::chest)
|
||||
).apply(instance, LostItem::new));
|
||||
private static final Codec<ChestState> CHEST_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.LONG.fieldOf("pos").forGetter(ChestState::pos),
|
||||
Codec.LONG.fieldOf("cycle").forGetter(ChestState::cycle),
|
||||
Codec.STRING.listOf(0, 54).optionalFieldOf("assigned", List.of()).forGetter(ChestState::assigned)
|
||||
).apply(instance, ChestState::new));
|
||||
private static final Codec<PlayerState> PLAYER_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("player").forGetter(PlayerState::player),
|
||||
Codec.INT.optionalFieldOf("personal_attempts", 0).forGetter(PlayerState::personalAttempts),
|
||||
Codec.LONG.listOf(0, MAX_VISITED_CHESTS).optionalFieldOf("visited_chests", List.of()).forGetter(PlayerState::visitedChests),
|
||||
Codec.LONG.listOf(0, MAX_RECENT_ARRIVALS).optionalFieldOf("recent_arrivals", List.of()).forGetter(PlayerState::recentArrivals)
|
||||
).apply(instance, PlayerState::new));
|
||||
private static final Codec<BackroomData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(data -> data.dataVersion),
|
||||
PENDING_CODEC.listOf(0, MAX_PENDING_ENTRIES).optionalFieldOf("pending_entries", List.of()).forGetter(data -> data.pendingEntries),
|
||||
LOST_CODEC.listOf(0, MAX_LOST_ITEMS).optionalFieldOf("lost_items", List.of()).forGetter(data -> data.lostItems),
|
||||
CHEST_CODEC.listOf(0, MAX_CHEST_STATES).optionalFieldOf("chests", List.of()).forGetter(data -> data.chests),
|
||||
PLAYER_CODEC.listOf(0, MAX_PLAYER_STATES).optionalFieldOf("players", List.of()).forGetter(data -> data.players)
|
||||
).apply(instance, BackroomData::new));
|
||||
private static final SavedDataType<BackroomData> TYPE = new SavedDataType<>(
|
||||
SanctuaryMod.id(SAVE_KEY), BackroomData::new, CODEC, null);
|
||||
|
||||
private int dataVersion;
|
||||
private final List<PendingEntry> pendingEntries;
|
||||
private final List<LostItem> lostItems;
|
||||
private final List<ChestState> chests;
|
||||
private final Map<Long, Integer> chestIndices;
|
||||
private final List<PlayerState> players;
|
||||
|
||||
public BackroomData() {
|
||||
this(DATA_VERSION, List.of(), List.of(), List.of(), List.of());
|
||||
}
|
||||
|
||||
private BackroomData(int dataVersion, List<PendingEntry> pendingEntries, List<LostItem> lostItems,
|
||||
List<ChestState> chests, List<PlayerState> players) {
|
||||
this.dataVersion = Math.max(DATA_VERSION, dataVersion);
|
||||
this.pendingEntries = bounded(pendingEntries, MAX_PENDING_ENTRIES);
|
||||
this.lostItems = bounded(lostItems, MAX_LOST_ITEMS);
|
||||
this.chests = bounded(chests, MAX_CHEST_STATES);
|
||||
this.chestIndices = new HashMap<>();
|
||||
for (int index = 0; index < this.chests.size(); index++) chestIndices.put(this.chests.get(index).pos, index);
|
||||
this.players = bounded(players, MAX_PLAYER_STATES);
|
||||
}
|
||||
|
||||
public static BackroomData get(MinecraftServer server) {
|
||||
ServerLevel overworld = server.getLevel(Level.OVERWORLD);
|
||||
if (overworld == null) throw new IllegalStateException("Overworld unavailable for Backroom data");
|
||||
BackroomData data = overworld.getDataStorage().computeIfAbsent(TYPE);
|
||||
if (data.dataVersion < DATA_VERSION) {
|
||||
data.dataVersion = DATA_VERSION;
|
||||
data.setDirty();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public Optional<PendingEntry> pendingEntry(UUID player) {
|
||||
String id = player.toString();
|
||||
return pendingEntries.stream().filter(entry -> entry.player.equals(id)).findFirst();
|
||||
}
|
||||
|
||||
public Optional<PendingEntry> prepareEntry(UUID player, List<EntrySlot> slots) {
|
||||
pendingEntries.removeIf(entry -> entry.player.equals(player.toString()));
|
||||
if (pendingEntries.size() >= MAX_PENDING_ENTRIES) return Optional.empty();
|
||||
PendingEntry entry = new PendingEntry(player.toString(), UUID.randomUUID().toString(), copySlots(slots));
|
||||
pendingEntries.add(entry);
|
||||
setDirty();
|
||||
return Optional.of(entry);
|
||||
}
|
||||
|
||||
public void finishEntry(UUID player) {
|
||||
if (pendingEntries.removeIf(entry -> entry.player.equals(player.toString()))) setDirty();
|
||||
}
|
||||
|
||||
/** Adds a new loss or returns a previously materialized loss to the pool. */
|
||||
public Optional<UUID> capture(ItemStack source, UUID owner, String cause, long gameTime) {
|
||||
if (source == null || source.isEmpty()) return Optional.empty();
|
||||
ItemStack clean = source.copy();
|
||||
UUID marker = clean.get(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
clean.remove(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
if (marker != null) {
|
||||
int existing = indexOfLoss(marker);
|
||||
if (existing >= 0) {
|
||||
LostItem previous = lostItems.get(existing);
|
||||
lostItems.set(existing, new LostItem(previous.id, owner == null ? previous.owner : owner.toString(),
|
||||
cause, gameTime, clean, LOST_POOL, Long.MIN_VALUE));
|
||||
setDirty();
|
||||
return Optional.of(marker);
|
||||
}
|
||||
}
|
||||
if (lostItems.size() >= MAX_LOST_ITEMS) return Optional.empty();
|
||||
UUID id = marker == null ? UUID.randomUUID() : marker;
|
||||
lostItems.add(new LostItem(id.toString(), owner == null ? "" : owner.toString(), cause,
|
||||
gameTime, clean, LOST_POOL, Long.MIN_VALUE));
|
||||
setDirty();
|
||||
return Optional.of(id);
|
||||
}
|
||||
|
||||
public Optional<MaterializedLoss> takePersonal(UUID owner, long chestPos) {
|
||||
String player = owner.toString();
|
||||
return lostItems.stream()
|
||||
.filter(loss -> loss.state.equals(LOST_POOL) && loss.owner.equals(player))
|
||||
.max(Comparator.comparingLong(LostItem::lostAt))
|
||||
.flatMap(loss -> materialize(loss, chestPos));
|
||||
}
|
||||
|
||||
public Optional<MaterializedLoss> takeShared(long chestPos, long gameTime) {
|
||||
return lostItems.stream()
|
||||
.filter(loss -> loss.state.equals(LOST_POOL)
|
||||
&& sharedEligible(loss.owner, loss.lostAt, gameTime))
|
||||
.min(Comparator.comparingLong(LostItem::lostAt))
|
||||
.flatMap(loss -> materialize(loss, chestPos));
|
||||
}
|
||||
|
||||
static boolean sharedEligible(String owner, long lostAt, long gameTime) {
|
||||
return owner == null || owner.isEmpty()
|
||||
|| gameTime >= lostAt && gameTime - lostAt >= PERSONAL_RESERVATION_TICKS;
|
||||
}
|
||||
|
||||
private Optional<MaterializedLoss> materialize(LostItem loss, long chestPos) {
|
||||
int index = lostItems.indexOf(loss);
|
||||
if (index < 0) return Optional.empty();
|
||||
ItemStack marked = loss.item.copy();
|
||||
UUID id = loss.uuid();
|
||||
marked.set(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT, id);
|
||||
lostItems.set(index, new LostItem(loss.id, loss.owner, loss.cause, loss.lostAt,
|
||||
loss.item.copy(), LOST_CHEST, chestPos));
|
||||
setDirty();
|
||||
return Optional.of(new MaterializedLoss(id, marked));
|
||||
}
|
||||
|
||||
/** Reconciles real items allocated during the chest's previous loot cycle. */
|
||||
public void reconcileChest(long chestPos, List<UUID> markersStillPresent) {
|
||||
List<String> present = markersStillPresent.stream().map(UUID::toString).toList();
|
||||
boolean changed = false;
|
||||
for (int index = lostItems.size() - 1; index >= 0; index--) {
|
||||
LostItem loss = lostItems.get(index);
|
||||
if (!loss.state.equals(LOST_CHEST) || loss.chest != chestPos) continue;
|
||||
if (present.contains(loss.id)) {
|
||||
lostItems.set(index, new LostItem(loss.id, loss.owner, loss.cause, loss.lostAt,
|
||||
loss.item.copy(), LOST_POOL, Long.MIN_VALUE));
|
||||
} else {
|
||||
lostItems.remove(index);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if (changed) setDirty();
|
||||
}
|
||||
|
||||
/** Settles a marked item that successfully left the Backroom. */
|
||||
public void settle(UUID lossId) {
|
||||
if (lossId != null && lostItems.removeIf(loss -> loss.id.equals(lossId.toString()))) setDirty();
|
||||
}
|
||||
|
||||
public Optional<ChestState> chest(long pos) {
|
||||
Integer index = chestIndices.get(pos);
|
||||
return index == null ? Optional.empty() : Optional.of(chests.get(index));
|
||||
}
|
||||
|
||||
public void updateChest(long pos, long cycle, List<UUID> assigned) {
|
||||
ChestState replacement = new ChestState(pos, cycle, assigned.stream().limit(54).map(UUID::toString).toList());
|
||||
Integer existing = chestIndices.get(pos);
|
||||
if (existing != null) {
|
||||
chests.set(existing, replacement);
|
||||
} else {
|
||||
if (chests.size() >= MAX_CHEST_STATES) {
|
||||
chests.removeFirst();
|
||||
chestIndices.clear();
|
||||
for (int index = 0; index < chests.size(); index++) chestIndices.put(chests.get(index).pos, index);
|
||||
}
|
||||
chestIndices.put(pos, chests.size());
|
||||
chests.add(replacement);
|
||||
}
|
||||
setDirty();
|
||||
}
|
||||
|
||||
/** Marks a chest and returns whether it is a new personal-recovery attempt. */
|
||||
public ChestVisit visitChest(UUID player, long chestPos) {
|
||||
PlayerState state = playerState(player);
|
||||
ArrayList<Long> visited = new ArrayList<>(state.visitedChests);
|
||||
boolean firstVisit = !visited.contains(chestPos);
|
||||
if (firstVisit) {
|
||||
if (visited.size() >= MAX_VISITED_CHESTS) visited.removeFirst();
|
||||
visited.add(chestPos);
|
||||
state = replacePlayer(state, state.personalAttempts + 1, visited, state.recentArrivals);
|
||||
}
|
||||
return new ChestVisit(firstVisit, state.personalAttempts);
|
||||
}
|
||||
|
||||
public void beginExpedition(UUID player, long packedArrival) {
|
||||
PlayerState state = playerState(player);
|
||||
ArrayList<Long> arrivals = new ArrayList<>(state.recentArrivals);
|
||||
arrivals.remove(packedArrival);
|
||||
arrivals.addFirst(packedArrival);
|
||||
while (arrivals.size() > MAX_RECENT_ARRIVALS) arrivals.removeLast();
|
||||
replacePlayer(state, 0, List.of(), arrivals);
|
||||
}
|
||||
|
||||
public List<Long> recentArrivals(UUID player) {
|
||||
return List.copyOf(playerState(player).recentArrivals);
|
||||
}
|
||||
|
||||
public int lostItemCount() { return lostItems.size(); }
|
||||
public long pooledItemCount() { return lostItems.stream().filter(loss -> loss.state.equals(LOST_POOL)).count(); }
|
||||
public long pooledItemCount(UUID owner) {
|
||||
String id = owner.toString();
|
||||
return lostItems.stream().filter(loss -> loss.state.equals(LOST_POOL) && loss.owner.equals(id)).count();
|
||||
}
|
||||
public int trackedChestCount() { return chests.size(); }
|
||||
|
||||
private PlayerState playerState(UUID player) {
|
||||
String id = player.toString();
|
||||
return players.stream().filter(state -> state.player.equals(id)).findFirst().orElseGet(() -> {
|
||||
if (players.size() >= MAX_PLAYER_STATES) players.removeFirst();
|
||||
PlayerState state = new PlayerState(id, 0, List.of(), List.of());
|
||||
players.add(state);
|
||||
setDirty();
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
private PlayerState replacePlayer(PlayerState old, int attempts, List<Long> visited, List<Long> arrivals) {
|
||||
PlayerState replacement = new PlayerState(old.player, Math.max(0, attempts),
|
||||
bounded(visited, MAX_VISITED_CHESTS), bounded(arrivals, MAX_RECENT_ARRIVALS));
|
||||
players.set(players.indexOf(old), replacement);
|
||||
setDirty();
|
||||
return replacement;
|
||||
}
|
||||
|
||||
private int indexOfLoss(UUID id) {
|
||||
String value = id.toString();
|
||||
for (int index = 0; index < lostItems.size(); index++) if (lostItems.get(index).id.equals(value)) return index;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static List<EntrySlot> copySlots(List<EntrySlot> slots) {
|
||||
return slots.stream().filter(slot -> slot != null && !slot.item.isEmpty()).limit(MAX_ENTRY_SLOTS)
|
||||
.map(slot -> new EntrySlot(slot.location, slot.index, slot.item.copy())).toList();
|
||||
}
|
||||
|
||||
private static <T> ArrayList<T> bounded(List<T> source, int maximum) {
|
||||
List<T> safe = source == null ? List.of() : source;
|
||||
int start = Math.max(0, safe.size() - maximum);
|
||||
return new ArrayList<>(safe.subList(start, safe.size()));
|
||||
}
|
||||
|
||||
public record EntrySlot(String location, int index, ItemStack item) {
|
||||
public EntrySlot { item = item == null ? ItemStack.EMPTY : item.copy(); }
|
||||
}
|
||||
public record PendingEntry(String player, String transaction, List<EntrySlot> slots) {
|
||||
public UUID transactionId() { return UUID.fromString(transaction); }
|
||||
}
|
||||
public record LostItem(String id, String owner, String cause, long lostAt, ItemStack item,
|
||||
String state, long chest) {
|
||||
public UUID uuid() { return UUID.fromString(id); }
|
||||
}
|
||||
public record ChestState(long pos, long cycle, List<String> assigned) {
|
||||
public List<UUID> assignedIds() { return assigned.stream().map(UUID::fromString).toList(); }
|
||||
}
|
||||
private record PlayerState(String player, int personalAttempts, List<Long> visitedChests,
|
||||
List<Long> recentArrivals) {}
|
||||
public record MaterializedLoss(UUID id, ItemStack stack) {}
|
||||
public record ChestVisit(boolean firstVisit, int ordinal) {}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventoryCapacity;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryPlayerProgress;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Exact inventory boundary used by Backroom entry, death and exit. */
|
||||
public final class BackroomInventory {
|
||||
private static final String INVENTORY = "inventory";
|
||||
private static final String OVERFLOW = "overflow";
|
||||
private static final String EGG = "companion_egg";
|
||||
private static final String CAPE = "cape";
|
||||
|
||||
private BackroomInventory() {}
|
||||
|
||||
public static List<BackroomData.EntrySlot> snapshot(ServerPlayer player) {
|
||||
ArrayList<BackroomData.EntrySlot> result = new ArrayList<>();
|
||||
Inventory inventory = player.getInventory();
|
||||
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
|
||||
add(result, INVENTORY, slot, inventory.getItem(slot));
|
||||
}
|
||||
for (int slot = 0; slot < SanctuaryPlayerProgress.OVERFLOW_SLOT_COUNT; slot++) {
|
||||
add(result, OVERFLOW, slot, SanctuaryInventoryCapacity.overflowItem(player, slot));
|
||||
}
|
||||
add(result, EGG, 0, SanctuaryInventoryCapacity.companionEgg(player));
|
||||
add(result, CAPE, 0, SanctuaryInventoryCapacity.equippedCape(player));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<ItemStack> parcel(List<BackroomData.EntrySlot> slots) {
|
||||
return slots.stream().map(BackroomData.EntrySlot::item).filter(stack -> !stack.isEmpty())
|
||||
.map(ItemStack::copy).toList();
|
||||
}
|
||||
|
||||
/** Clears only exact snapshot values, so recovery never destroys a later replacement item. */
|
||||
public static void clearMatching(ServerPlayer player, List<BackroomData.EntrySlot> slots) {
|
||||
for (BackroomData.EntrySlot slot : slots) {
|
||||
ItemStack current = get(player, slot);
|
||||
if (ItemStack.matches(current, slot.item())) set(player, slot, ItemStack.EMPTY);
|
||||
}
|
||||
player.getInventory().setChanged();
|
||||
SanctuaryInventoryCapacity.markOverflowChanged(player);
|
||||
}
|
||||
|
||||
public static int captureAll(ServerPlayer player, String cause) {
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
long now = player.level().getGameTime();
|
||||
int captured = 0;
|
||||
for (BackroomData.EntrySlot slot : snapshot(player)) {
|
||||
if (data.capture(slot.item(), player.getUUID(), cause, now).isEmpty()) continue;
|
||||
set(player, slot, ItemStack.EMPTY);
|
||||
captured++;
|
||||
}
|
||||
player.getInventory().setChanged();
|
||||
SanctuaryInventoryCapacity.markOverflowChanged(player);
|
||||
return captured;
|
||||
}
|
||||
|
||||
/** Removes registry markers only when their real item successfully crosses a bed exit. */
|
||||
public static int settleCarriedLosses(ServerPlayer player) {
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
int settled = 0;
|
||||
for (BackroomData.EntrySlot slot : snapshot(player)) {
|
||||
ItemStack item = get(player, slot);
|
||||
UUID id = item.get(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
if (id == null) continue;
|
||||
item.remove(SanctuaryRegistries.BACKROOM_LOSS_COMPONENT);
|
||||
data.settle(id);
|
||||
set(player, slot, item);
|
||||
settled++;
|
||||
}
|
||||
return settled;
|
||||
}
|
||||
|
||||
private static void add(List<BackroomData.EntrySlot> target, String location, int index, ItemStack stack) {
|
||||
if (stack != null && !stack.isEmpty()) target.add(new BackroomData.EntrySlot(location, index, stack));
|
||||
}
|
||||
|
||||
private static ItemStack get(ServerPlayer player, BackroomData.EntrySlot slot) {
|
||||
return switch (slot.location()) {
|
||||
case INVENTORY -> slot.index() >= 0 && slot.index() < player.getInventory().getContainerSize()
|
||||
? player.getInventory().getItem(slot.index()) : ItemStack.EMPTY;
|
||||
case OVERFLOW -> SanctuaryInventoryCapacity.overflowItem(player, slot.index());
|
||||
case EGG -> SanctuaryInventoryCapacity.companionEgg(player);
|
||||
case CAPE -> SanctuaryInventoryCapacity.equippedCape(player);
|
||||
default -> ItemStack.EMPTY;
|
||||
};
|
||||
}
|
||||
|
||||
private static void set(ServerPlayer player, BackroomData.EntrySlot slot, ItemStack stack) {
|
||||
switch (slot.location()) {
|
||||
case INVENTORY -> {
|
||||
if (slot.index() >= 0 && slot.index() < player.getInventory().getContainerSize())
|
||||
player.getInventory().setItem(slot.index(), stack);
|
||||
}
|
||||
case OVERFLOW -> SanctuaryInventoryCapacity.setOverflowItem(player, slot.index(), stack);
|
||||
case EGG -> SanctuaryInventoryCapacity.setCompanionEgg(player, stack);
|
||||
case CAPE -> SanctuaryInventoryCapacity.setEquippedCape(player, stack);
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
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 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.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.arguments.EntityArgument;
|
||||
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.item.ItemStack;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Bed gateway and crash-safe Mailbox boundary for Backroom expeditions. */
|
||||
public final class BackroomService {
|
||||
public static final int ARRIVAL_RADIUS = 2_000;
|
||||
private static final int ARRIVAL_ATTEMPTS = 48;
|
||||
private static final Set<UUID> AUTHORIZED_TRANSITIONS = new HashSet<>();
|
||||
private static boolean initialized;
|
||||
|
||||
private BackroomService() {}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> reconcile(handler.player));
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
|
||||
dispatcher.register(Commands.literal("backroom")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
|
||||
.executes(context -> status(context.getSource()))
|
||||
.then(Commands.literal("status").executes(context -> status(context.getSource())))
|
||||
.then(Commands.literal("inspect")
|
||||
.then(Commands.argument("player", EntityArgument.player())
|
||||
.executes(context -> inspect(context.getSource(), EntityArgument.getPlayer(context, "player"))))));
|
||||
});
|
||||
SanctuaryMod.LOGGER.info("[{}] Backroom bed gateway ready (arrival radius {} blocks).",
|
||||
SanctuaryMod.DISPLAY_NAME, ARRIVAL_RADIUS);
|
||||
}
|
||||
|
||||
/** Returns true when the bed interaction belongs to the Backroom feature. */
|
||||
public static boolean useBed(ServerPlayer player) {
|
||||
if (isBackroom(player.level())) {
|
||||
exit(player);
|
||||
return true;
|
||||
}
|
||||
if (!player.isSecondaryUseActive()) return false;
|
||||
enter(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isBackroom(ServerLevel level) {
|
||||
return level != null && level.dimension().equals(SanctuaryDimensions.BACKROOM);
|
||||
}
|
||||
|
||||
public static boolean enter(ServerPlayer player) {
|
||||
ServerLevel destination = player.level().getServer().getLevel(SanctuaryDimensions.BACKROOM);
|
||||
if (destination == null) {
|
||||
notify(player, "message.sanctuary.backroom.unavailable");
|
||||
return false;
|
||||
}
|
||||
if (!reconcile(player)) {
|
||||
notify(player, "message.sanctuary.backroom.pending");
|
||||
return false;
|
||||
}
|
||||
|
||||
List<BackroomData.EntrySlot> slots = BackroomInventory.snapshot(player);
|
||||
ShopData.DeliveryDestination mailbox = null;
|
||||
if (!slots.isEmpty()) {
|
||||
mailbox = ShopData.get(player.level().getServer()).loadedDeliveryBox(
|
||||
player.level().getServer(), player.getUUID(), level -> !isBackroom(level)).orElse(null);
|
||||
if (mailbox == null) {
|
||||
notify(player, "message.sanctuary.backroom.mailbox_missing");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
BackroomData.PendingEntry pending = data.prepareEntry(player.getUUID(), slots).orElse(null);
|
||||
if (pending == null) {
|
||||
notify(player, "message.sanctuary.backroom.registry_busy");
|
||||
return false;
|
||||
}
|
||||
if (mailbox != null && !mailbox.box().insertTransaction(pending.transactionId(), BackroomInventory.parcel(slots))) {
|
||||
data.finishEntry(player.getUUID());
|
||||
notify(player, "message.sanctuary.backroom.mailbox_full");
|
||||
return false;
|
||||
}
|
||||
BackroomInventory.clearMatching(player, slots);
|
||||
data.finishEntry(player.getUUID());
|
||||
|
||||
BlockPos arrival = chooseArrival(destination, player.getUUID(), data);
|
||||
data.beginExpedition(player.getUUID(), arrival.asLong());
|
||||
TeleportTransition transition = new TeleportTransition(destination, Vec3.atBottomCenterOf(arrival),
|
||||
Vec3.ZERO, player.getYRot(), player.getXRot(),
|
||||
TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
|
||||
ServerPlayer teleported;
|
||||
AUTHORIZED_TRANSITIONS.add(player.getUUID());
|
||||
try {
|
||||
teleported = player.teleport(transition);
|
||||
} finally {
|
||||
AUTHORIZED_TRANSITIONS.remove(player.getUUID());
|
||||
}
|
||||
if (teleported == null) {
|
||||
SanctuaryMod.LOGGER.error("Backroom teleport failed after Mailbox transaction {} for {}",
|
||||
pending.transaction(), player.getGameProfile().name());
|
||||
return false;
|
||||
}
|
||||
notify(player, "message.sanctuary.backroom.entered");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean exit(ServerPlayer player) {
|
||||
if (!isBackroom(player.level())) return false;
|
||||
TeleportTransition transition = player.findRespawnPositionAndUseSpawnBlock(false,
|
||||
TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
|
||||
if (isBackroom(transition.newLevel())) {
|
||||
transition = TeleportTransition.createDefault(player,
|
||||
TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET));
|
||||
}
|
||||
ServerPlayer teleported;
|
||||
AUTHORIZED_TRANSITIONS.add(player.getUUID());
|
||||
try {
|
||||
teleported = player.teleport(transition);
|
||||
} finally {
|
||||
AUTHORIZED_TRANSITIONS.remove(player.getUUID());
|
||||
}
|
||||
if (teleported == null) return false;
|
||||
int recovered = BackroomInventory.settleCarriedLosses(teleported);
|
||||
notify(teleported, recovered == 0
|
||||
? "message.sanctuary.backroom.exited"
|
||||
: "message.sanctuary.backroom.exited_with_losses");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isAuthorizedTransition(UUID player) {
|
||||
return player != null && AUTHORIZED_TRANSITIONS.contains(player);
|
||||
}
|
||||
|
||||
/** Resolves a crash between Mailbox insertion and inventory clearing. */
|
||||
public static boolean reconcile(ServerPlayer player) {
|
||||
BackroomData data = BackroomData.get(player.level().getServer());
|
||||
BackroomData.PendingEntry pending = data.pendingEntry(player.getUUID()).orElse(null);
|
||||
if (pending == null) return true;
|
||||
ShopData.DeliveryDestination mailbox = ShopData.get(player.level().getServer())
|
||||
.loadedDeliveryBox(player.level().getServer(), player.getUUID(), level -> !isBackroom(level)).orElse(null);
|
||||
if (mailbox == null) return false;
|
||||
if (mailbox.box().hasProcessedTransaction(pending.transactionId())) {
|
||||
BackroomInventory.clearMatching(player, pending.slots());
|
||||
}
|
||||
data.finishEntry(player.getUUID());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BlockPos chooseArrival(ServerLevel level, UUID player, BackroomData data) {
|
||||
RandomSource random = level.getRandom();
|
||||
List<Long> recent = data.recentArrivals(player);
|
||||
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);
|
||||
best = candidate;
|
||||
if (farFromRecent(candidate, recent) && safe(level, candidate)) return candidate;
|
||||
}
|
||||
BlockPos fallback = findSafeNear(level, best == null ? BlockPos.ZERO : best, 32);
|
||||
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);
|
||||
}
|
||||
|
||||
private static boolean farFromRecent(BlockPos candidate, List<Long> recent) {
|
||||
for (long packed : recent) {
|
||||
BlockPos old = BlockPos.of(packed);
|
||||
long dx = old.getX() - candidate.getX(), dz = old.getZ() - candidate.getZ();
|
||||
if (dx * dx + dz * dz < 128L * 128L) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean safe(ServerLevel level, BlockPos feet) {
|
||||
return level.getBlockState(feet).isAir() && level.getBlockState(feet.above()).isAir()
|
||||
&& level.getBlockState(feet.below()).isCollisionShapeFullBlock(level, feet.below());
|
||||
}
|
||||
|
||||
private static BlockPos findSafeNear(ServerLevel level, BlockPos center, int radius) {
|
||||
for (int ring = 0; ring <= radius; ring += 2) {
|
||||
for (int dx = -ring; dx <= ring; dx += 2) for (int dz = -ring; dz <= ring; dz += 2) {
|
||||
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);
|
||||
if (safe(level, candidate)) return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void notify(ServerPlayer player, String translationKey) {
|
||||
player.sendOverlayMessage(Component.translatable(translationKey));
|
||||
}
|
||||
|
||||
private static int status(CommandSourceStack source) {
|
||||
BackroomData data = BackroomData.get(source.getServer());
|
||||
source.sendSuccess(() -> Component.literal("Backroom: " + data.lostItemCount() + " lost stacks ("
|
||||
+ data.pooledItemCount() + " pooled), " + data.trackedChestCount() + " tracked chest positions; data v"
|
||||
+ BackroomData.DATA_VERSION + "."), false);
|
||||
return data.lostItemCount();
|
||||
}
|
||||
|
||||
private static int inspect(CommandSourceStack source, ServerPlayer player) {
|
||||
BackroomData data = BackroomData.get(source.getServer());
|
||||
boolean pending = data.pendingEntry(player.getUUID()).isPresent();
|
||||
source.sendSuccess(() -> Component.literal("Backroom player " + player.getGameProfile().name() + ": "
|
||||
+ data.pooledItemCount(player.getUUID()) + " personal pooled stacks, entry transaction "
|
||||
+ (pending ? "pending" : "clear") + ", " + data.recentArrivals(player.getUUID()).size()
|
||||
+ " remembered arrivals."), false);
|
||||
return pending ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomService;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BedBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(BedBlock.class)
|
||||
abstract class BedBlockMixin {
|
||||
@Inject(method = "useWithoutItem", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$useBackroomBed(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit, CallbackInfoReturnable<InteractionResult> callback) {
|
||||
if (player instanceof ServerPlayer serverPlayer && BackroomService.useBed(serverPlayer)) {
|
||||
callback.setReturnValue(InteractionResult.SUCCESS_SERVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomChestService;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ContainerUser;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ChestBlockEntity.class)
|
||||
abstract class ChestBlockEntityMixin {
|
||||
@Inject(method = "startOpen", at = @At("HEAD"))
|
||||
private void sanctuary$refreshBackroomChest(ContainerUser user, CallbackInfo callback) {
|
||||
if (user.getLivingEntity() instanceof ServerPlayer player) {
|
||||
BackroomChestService.open((ChestBlockEntity) (Object) this, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomData;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ItemEntity.class)
|
||||
abstract class ItemEntityMixin {
|
||||
@Shadow private @Nullable UUID target;
|
||||
|
||||
@Inject(method = "tick", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$captureLostItem(CallbackInfo callback) {
|
||||
ItemEntity item = (ItemEntity) (Object) this;
|
||||
if (!(item.level() instanceof ServerLevel level) || item.getItem().isEmpty()) return;
|
||||
boolean voidLoss = item.getY() < level.getMinY() - 64.0D;
|
||||
boolean despawn = item.getAge() >= 5_999;
|
||||
if (!voidLoss && !despawn) return;
|
||||
UUID owner = target;
|
||||
Entity thrower = item.getOwner();
|
||||
if (owner == null && thrower instanceof ServerPlayer player) owner = player.getUUID();
|
||||
if (BackroomData.get(level.getServer()).capture(item.getItem(), owner,
|
||||
voidLoss ? "void" : "despawn", level.getGameTime()).isEmpty()) return;
|
||||
item.discard();
|
||||
callback.cancel();
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomInventory;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.backroom.BackroomService;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.damagesource.DamageTypes;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(ServerPlayer.class)
|
||||
abstract class ServerPlayerBackroomMixin {
|
||||
@Inject(method = "teleport", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$enforceBackroomExtraction(TeleportTransition transition,
|
||||
CallbackInfoReturnable<ServerPlayer> callback) {
|
||||
ServerPlayer player = (ServerPlayer) (Object) this;
|
||||
boolean leaving = BackroomService.isBackroom(player.level())
|
||||
&& !BackroomService.isBackroom(transition.newLevel());
|
||||
boolean entering = !BackroomService.isBackroom(player.level())
|
||||
&& BackroomService.isBackroom(transition.newLevel());
|
||||
if ((!leaving && !entering) || BackroomService.isAuthorizedTransition(player.getUUID())) return;
|
||||
if (entering) {
|
||||
player.sendOverlayMessage(Component.translatable("message.sanctuary.backroom.bed_entry_only"));
|
||||
callback.setReturnValue(null);
|
||||
return;
|
||||
}
|
||||
BackroomInventory.captureAll(player, "backroom_forced_exit");
|
||||
}
|
||||
|
||||
@Inject(method = "die", at = @At("HEAD"))
|
||||
private void sanctuary$captureBackroomDeath(DamageSource source, CallbackInfo callback) {
|
||||
ServerPlayer player = (ServerPlayer) (Object) this;
|
||||
if (BackroomService.isBackroom(player.level())) {
|
||||
BackroomInventory.captureAll(player, "backroom_death");
|
||||
} else if (source.is(DamageTypes.FELL_OUT_OF_WORLD)) {
|
||||
BackroomInventory.captureAll(player, "void_death");
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -9,6 +9,9 @@ import fr.koka99cab.sanctuary26.sanctuary.shop.QuestRequestSignItem;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestSignItem;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCapes;
|
||||
import java.util.function.Function;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
|
||||
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityType;
|
||||
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
|
||||
@@ -37,6 +40,16 @@ import net.minecraft.world.level.material.MapColor;
|
||||
* Feature registries must be called from here to keep initialization ordering explicit.
|
||||
*/
|
||||
public final class SanctuaryRegistries {
|
||||
public static final Identifier BACKROOM_LOSS_ID = SanctuaryMod.id("backroom_loss_id");
|
||||
public static final DataComponentType<UUID> BACKROOM_LOSS_COMPONENT = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
BACKROOM_LOSS_ID,
|
||||
DataComponentType.<UUID>builder()
|
||||
.persistent(UUIDUtil.CODEC)
|
||||
.networkSynchronized(UUIDUtil.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build()
|
||||
);
|
||||
public static final Identifier DELIVERY_BOX_ID = SanctuaryMod.id("delivery_box");
|
||||
public static final DeliveryBoxBlock DELIVERY_BOX = registerBlock(
|
||||
DELIVERY_BOX_ID,
|
||||
|
||||
+58
@@ -1,6 +1,10 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.NonNullList;
|
||||
@@ -19,7 +23,9 @@ import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
public final class DeliveryBoxBlockEntity extends BaseContainerBlockEntity {
|
||||
public static final int SIZE = 27;
|
||||
private static final int MAX_PROCESSED_TRANSACTIONS = 128;
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
private final Set<UUID> processedTransactions = new LinkedHashSet<>();
|
||||
private UUID owner;
|
||||
|
||||
public DeliveryBoxBlockEntity(BlockPos pos, BlockState state) {
|
||||
@@ -68,16 +74,68 @@ public final class DeliveryBoxBlockEntity extends BaseContainerBlockEntity {
|
||||
return remaining.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically inserts a complete Backroom parcel. Replaying the same transaction is a no-op,
|
||||
* which makes the mailbox side of an entry safe across a server crash.
|
||||
*/
|
||||
public boolean insertTransaction(UUID transactionId, List<ItemStack> deliveries) {
|
||||
if (transactionId == null || deliveries == null) return false;
|
||||
if (processedTransactions.contains(transactionId)) return true;
|
||||
NonNullList<ItemStack> simulated = copyItems(items);
|
||||
for (ItemStack delivery : deliveries) {
|
||||
if (delivery == null || delivery.isEmpty()) continue;
|
||||
if (!insertInto(simulated, delivery.copy())) return false;
|
||||
}
|
||||
items = simulated;
|
||||
processedTransactions.add(transactionId);
|
||||
while (processedTransactions.size() > MAX_PROCESSED_TRANSACTIONS) {
|
||||
processedTransactions.remove(processedTransactions.iterator().next());
|
||||
}
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean hasProcessedTransaction(UUID transactionId) {
|
||||
return transactionId != null && processedTransactions.contains(transactionId);
|
||||
}
|
||||
|
||||
private static NonNullList<ItemStack> copyItems(List<ItemStack> source) {
|
||||
NonNullList<ItemStack> copy = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
for (int slot = 0; slot < Math.min(SIZE, source.size()); slot++) copy.set(slot, source.get(slot).copy());
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static boolean insertInto(NonNullList<ItemStack> target, ItemStack offered) {
|
||||
for (int slot = 0; slot < target.size() && !offered.isEmpty(); slot++) {
|
||||
ItemStack existing = target.get(slot);
|
||||
if (existing.isEmpty() || !ItemStack.isSameItemSameComponents(existing, offered)) continue;
|
||||
int moved = Math.min(offered.getCount(), existing.getMaxStackSize() - existing.getCount());
|
||||
if (moved > 0) { existing.grow(moved); offered.shrink(moved); }
|
||||
}
|
||||
for (int slot = 0; slot < target.size() && !offered.isEmpty(); slot++) {
|
||||
if (!target.get(slot).isEmpty()) continue;
|
||||
int moved = Math.min(offered.getCount(), offered.getMaxStackSize());
|
||||
target.set(slot, offered.copyWithCount(moved));
|
||||
offered.shrink(moved);
|
||||
}
|
||||
return offered.isEmpty();
|
||||
}
|
||||
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
ContainerHelper.saveAllItems(output, items);
|
||||
if (owner != null) output.store("Owner", UUIDUtil.CODEC, owner);
|
||||
output.store("BackroomTransactions", UUIDUtil.CODEC.listOf(0, MAX_PROCESSED_TRANSACTIONS),
|
||||
new ArrayList<>(processedTransactions));
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
ContainerHelper.loadAllItems(input, items);
|
||||
owner = input.read("Owner", UUIDUtil.CODEC).orElse(null);
|
||||
processedTransactions.clear();
|
||||
input.read("BackroomTransactions", UUIDUtil.CODEC.listOf(0, MAX_PROCESSED_TRANSACTIONS)).orElse(List.of()).stream()
|
||||
.limit(MAX_PROCESSED_TRANSACTIONS).forEach(processedTransactions::add);
|
||||
}
|
||||
@Override public int getContainerSize() { return SIZE; }
|
||||
@Override protected NonNullList<ItemStack> getItems() { return items; }
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -107,6 +108,11 @@ public final class ShopData extends SavedData {
|
||||
}
|
||||
|
||||
public Optional<DeliveryDestination> loadedDeliveryBox(MinecraftServer server, UUID owner) {
|
||||
return loadedDeliveryBox(server, owner, level -> true);
|
||||
}
|
||||
|
||||
public Optional<DeliveryDestination> loadedDeliveryBox(MinecraftServer server, UUID owner,
|
||||
Predicate<ServerLevel> allowedLevel) {
|
||||
String ownerId = owner.toString();
|
||||
boolean changed = false;
|
||||
Iterator<DeliveryAddress> iterator = deliveryBoxes.iterator();
|
||||
@@ -117,7 +123,7 @@ public final class ShopData extends SavedData {
|
||||
for (ServerLevel candidate : server.getAllLevels()) {
|
||||
if (dimension(candidate).equals(address.dimension)) { level = candidate; break; }
|
||||
}
|
||||
if (level == null) continue;
|
||||
if (level == null || allowedLevel == null || !allowedLevel.test(level)) continue;
|
||||
BlockPos pos = BlockPos.of(address.pos);
|
||||
if (!level.isLoaded(pos)) continue;
|
||||
if (!(level.getBlockEntity(pos) instanceof DeliveryBoxBlockEntity box) || !owner.equals(box.owner())) {
|
||||
|
||||
+3
-1
@@ -26,6 +26,7 @@ public final class SanctuaryDimensions {
|
||||
public static final ResourceKey<Level> PAINTED_CANYONS = dimension("painted_canyons");
|
||||
public static final ResourceKey<Level> BREAKER_SEA = dimension("breaker_sea");
|
||||
public static final ResourceKey<Level> LAPUTA = dimension("laputa");
|
||||
public static final ResourceKey<Level> BACKROOM = dimension("backroom");
|
||||
|
||||
public static final ResourceKey<LevelStem> MAIN_STEM = stem("main");
|
||||
public static final ResourceKey<LevelStem> OVERWORLD_STEM = stem("overworld");
|
||||
@@ -37,11 +38,12 @@ public final class SanctuaryDimensions {
|
||||
public static final ResourceKey<LevelStem> PAINTED_CANYONS_STEM = stem("painted_canyons");
|
||||
public static final ResourceKey<LevelStem> BREAKER_SEA_STEM = stem("breaker_sea");
|
||||
public static final ResourceKey<LevelStem> LAPUTA_STEM = stem("laputa");
|
||||
public static final ResourceKey<LevelStem> BACKROOM_STEM = stem("backroom");
|
||||
|
||||
public static final Set<Identifier> RESERVED_IDS = Set.of(
|
||||
MAIN.identifier(), OVERWORLD.identifier(), CAVERNS.identifier(), ALPHA.identifier(),
|
||||
SHEEP_LAND.identifier(), SKELETON_WASTE.identifier(), LOST_CITY.identifier(),
|
||||
PAINTED_CANYONS.identifier(), BREAKER_SEA.identifier(), LAPUTA.identifier()
|
||||
PAINTED_CANYONS.identifier(), BREAKER_SEA.identifier(), LAPUTA.identifier(), BACKROOM.identifier()
|
||||
);
|
||||
|
||||
private SanctuaryDimensions() {
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.MatterDepthMaterializ
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.AlphaIndevChunkFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.SanctuaryWaterLakeFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.SulfurCaveGeologyFeature;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeSelectors;
|
||||
import net.minecraft.core.Registry;
|
||||
@@ -22,6 +23,9 @@ import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
|
||||
/** Registers the two generator-specific features migrated from Sanctuary 26.1.2. */
|
||||
public final class SanctuaryFeatures {
|
||||
public static final Identifier BACKROOM_ARCHITECTURE_ID = SanctuaryMod.id("backroom_architecture");
|
||||
public static final Feature<NoneFeatureConfiguration> BACKROOM_ARCHITECTURE =
|
||||
new BackroomArchitectureFeature(NoneFeatureConfiguration.CODEC);
|
||||
public static final Identifier ALPHA_INDEV_CHUNK_ID = SanctuaryMod.id("alpha_indev_chunk");
|
||||
public static final Feature<NoneFeatureConfiguration> ALPHA_INDEV_CHUNK =
|
||||
new AlphaIndevChunkFeature(NoneFeatureConfiguration.CODEC);
|
||||
@@ -56,6 +60,7 @@ public final class SanctuaryFeatures {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
Registry.register(BuiltInRegistries.FEATURE, BACKROOM_ARCHITECTURE_ID, BACKROOM_ARCHITECTURE);
|
||||
Registry.register(BuiltInRegistries.FEATURE, ALPHA_INDEV_CHUNK_ID, ALPHA_INDEV_CHUNK);
|
||||
Registry.register(BuiltInRegistries.FEATURE,
|
||||
MATTER_DEPTH_MATERIALIZATION_ID, MATTER_DEPTH_MATERIALIZATION);
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
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 java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.BedBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LeavesBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
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.
|
||||
*/
|
||||
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 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_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 LEAVES = Blocks.OAK_LEAVES.defaultBlockState().setValue(LeavesBlock.PERSISTENT, true);
|
||||
|
||||
public BackroomArchitectureFeature(Codec<NoneFeatureConfiguration> codec) {
|
||||
super(codec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean place(FeaturePlaceContext<NoneFeatureConfiguration> context) {
|
||||
WorldGenLevel level = context.level();
|
||||
int chunkX = Math.floorDiv(context.origin().getX(), CHUNK_SIZE);
|
||||
int chunkZ = Math.floorDiv(context.origin().getZ(), CHUNK_SIZE);
|
||||
int startX = chunkX * CHUNK_SIZE;
|
||||
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);
|
||||
placeRareAnomaly(level, startX, startZ, random);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean withinPlayableArea(int x, int z) {
|
||||
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. */
|
||||
public static long fingerprint(long worldSeed, int chunkX, int chunkZ) {
|
||||
return mix(worldSeed ^ architecture(worldSeed, chunkX, chunkZ).ordinal(), 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 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);
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
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, 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
private static void set(WorldGenLevel level, int x, int y, int z, BlockState state) {
|
||||
if (withinPlayableArea(x, z) && y >= level.getMinY() && y < level.getMaxY()) {
|
||||
level.setBlock(new BlockPos(x, y, z), state, 2);
|
||||
}
|
||||
}
|
||||
|
||||
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 Architecture { CITY, OFFICES, PLAINS, AGGREGATES, ROTATED }
|
||||
}
|
||||
@@ -516,6 +516,16 @@
|
||||
"item.sanctuary.blue_scroll_cape": "Blue Scroll Cape",
|
||||
"item.sanctuary.white_cape": "White Cape",
|
||||
"dimension.sanctuary.alpha": "Alpha / Indev Island",
|
||||
"dimension.sanctuary.backroom": "Backroom",
|
||||
"message.sanctuary.backroom.unavailable": "The Backroom is unavailable in this world.",
|
||||
"message.sanctuary.backroom.pending": "Your previous Backroom transfer is waiting for your Mailbox to be loaded.",
|
||||
"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.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.",
|
||||
"screen.sanctuary.atlas.landmark.boss": "Defeated boss",
|
||||
"message.sanctuary.notch_defeated": "Notch Titan has been defeated. The victory is now recorded in this world."
|
||||
}
|
||||
|
||||
@@ -516,6 +516,16 @@
|
||||
"item.sanctuary.blue_scroll_cape": "Cape parchemin bleu",
|
||||
"item.sanctuary.white_cape": "Cape blanche",
|
||||
"dimension.sanctuary.alpha": "Île Alpha / Indev",
|
||||
"dimension.sanctuary.backroom": "Backroom",
|
||||
"message.sanctuary.backroom.unavailable": "La Backroom n’est pas disponible dans ce monde.",
|
||||
"message.sanctuary.backroom.pending": "Ton ancien transfert Backroom attend que ta Mailbox soit chargée.",
|
||||
"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 d’entrer dans la Backroom.",
|
||||
"message.sanctuary.backroom.mailbox_full": "Ta Mailbox n’a 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.bed_entry_only": "Seul un lit permet d’entrer dans la Backroom.",
|
||||
"message.sanctuary.backroom.exited": "Le lit te ramène à ton point d’apparition 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.",
|
||||
"screen.sanctuary.atlas.landmark.boss": "Boss vaincu",
|
||||
"message.sanctuary.notch_defeated": "Notch Titan a été vaincu. La victoire est désormais inscrite dans ce monde."
|
||||
}
|
||||
|
||||
@@ -472,6 +472,16 @@
|
||||
"item.sanctuary.blue_scroll_cape": "Плащ синего свитка",
|
||||
"item.sanctuary.white_cape": "Белый плащ",
|
||||
"dimension.sanctuary.alpha": "Остров Альфа / Indev",
|
||||
"dimension.sanctuary.backroom": "Бэкрум",
|
||||
"message.sanctuary.backroom.unavailable": "Бэкрум недоступен в этом мире.",
|
||||
"message.sanctuary.backroom.pending": "Предыдущий перенос в Бэкрум ожидает загрузки вашего почтового ящика.",
|
||||
"message.sanctuary.backroom.registry_busy": "Реестр Бэкрума завершает слишком много прерванных переносов. Повторите попытку позже.",
|
||||
"message.sanctuary.backroom.mailbox_missing": "Загрузите и зарегистрируйте почтовый ящик перед входом в Бэкрум.",
|
||||
"message.sanctuary.backroom.mailbox_full": "В почтовом ящике недостаточно места для снаряжения, яйца спутника и плаща.",
|
||||
"message.sanctuary.backroom.entered": "Вы проваливаетесь сквозь кровать без снаряжения…",
|
||||
"message.sanctuary.backroom.bed_entry_only": "В Бэкрум можно попасть только через кровать.",
|
||||
"message.sanctuary.backroom.exited": "Кровать возвращает вас к точке возрождения со всеми находками.",
|
||||
"message.sanctuary.backroom.exited_with_losses": "Кровать возвращает вас с находками и закрепляет найденные потерянные предметы.",
|
||||
"screen.sanctuary.atlas.landmark.boss": "Побеждённый босс",
|
||||
"message.sanctuary.notch_defeated": "Нотч-Титан побеждён. Победа навсегда записана в этом мире."
|
||||
}
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
"settings": "minecraft:overworld"
|
||||
}
|
||||
},
|
||||
"sanctuary:backroom": {
|
||||
"type": "sanctuary:backroom",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": { "type": "minecraft:fixed", "biome": "sanctuary:backroom" },
|
||||
"settings": "sanctuary:backroom"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "sanctuary:backroom",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:fixed",
|
||||
"biome": "sanctuary:backroom"
|
||||
},
|
||||
"settings": "sanctuary:backroom"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"ambient_light": 0.35,
|
||||
"attributes": {
|
||||
"minecraft:audio/ambient_sounds": {
|
||||
"mood": {
|
||||
"block_search_extent": 8,
|
||||
"offset": 2.0,
|
||||
"sound": "minecraft:ambient.cave",
|
||||
"tick_delay": 5000
|
||||
}
|
||||
},
|
||||
"minecraft:gameplay/bed_rule": {
|
||||
"can_set_spawn": "never",
|
||||
"can_sleep": "never"
|
||||
},
|
||||
"minecraft:gameplay/can_start_raid": false,
|
||||
"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/sky_light_factor": 0.0
|
||||
},
|
||||
"cardinal_light": "nether",
|
||||
"coordinate_scale": 1.0,
|
||||
"has_ceiling": false,
|
||||
"has_ender_dragon_fight": false,
|
||||
"has_fixed_time": true,
|
||||
"has_skylight": false,
|
||||
"height": 192,
|
||||
"infiniburn": "#minecraft:infiniburn_overworld",
|
||||
"logical_height": 192,
|
||||
"min_y": 0,
|
||||
"monster_spawn_block_light_limit": 15,
|
||||
"monster_spawn_light_level": 7,
|
||||
"skybox": "none",
|
||||
"timelines": "#minecraft:in_nether"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"temperature": 0.65,
|
||||
"downfall": 0.0,
|
||||
"has_precipitation": false,
|
||||
"attributes": {},
|
||||
"effects": {
|
||||
"water_color": "#596f75",
|
||||
"water_fog_color": "#303b3e",
|
||||
"fog_color": "#b7b9ad",
|
||||
"sky_color": "#b7b9ad",
|
||||
"grass_color": "#70845d",
|
||||
"foliage_color": "#657b58"
|
||||
},
|
||||
"creature_spawn_probability": 0.08,
|
||||
"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 }
|
||||
],
|
||||
"underground_water_creature": [],
|
||||
"water_ambient": [],
|
||||
"water_creature": []
|
||||
},
|
||||
"spawn_costs": {},
|
||||
"carvers": [],
|
||||
"features": [[], [], [], [], ["sanctuary:backroom_architecture"], [], [], [], [], [], []]
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "sanctuary:backroom_architecture",
|
||||
"config": {}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"aquifers_enabled": false,
|
||||
"default_block": { "Name": "minecraft:air" },
|
||||
"default_fluid": { "Name": "minecraft:air" },
|
||||
"disable_mob_generation": false,
|
||||
"legacy_random_source": false,
|
||||
"noise": {
|
||||
"height": 192,
|
||||
"min_y": 0,
|
||||
"size_horizontal": 1,
|
||||
"size_vertical": 2
|
||||
},
|
||||
"noise_router": {
|
||||
"barrier": 0.0,
|
||||
"continents": 0.0,
|
||||
"depth": 0.0,
|
||||
"erosion": 0.0,
|
||||
"final_density": -1.0,
|
||||
"fluid_level_floodedness": 0.0,
|
||||
"fluid_level_spread": 0.0,
|
||||
"lava": 0.0,
|
||||
"preliminary_surface_level": 0.0,
|
||||
"ridges": 0.0,
|
||||
"temperature": 0.0,
|
||||
"vegetation": 0.0,
|
||||
"vein_gap": 0.0,
|
||||
"vein_ridged": 0.0,
|
||||
"vein_toggle": 0.0
|
||||
},
|
||||
"ore_veins_enabled": false,
|
||||
"sea_level": 63,
|
||||
"spawn_target": [],
|
||||
"surface_rule": {
|
||||
"type": "minecraft:block",
|
||||
"result_state": { "Name": "minecraft:air" }
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"feature": "sanctuary:backroom_architecture",
|
||||
"placement": [
|
||||
{ "type": "minecraft:count", "count": 1 },
|
||||
{ "type": "minecraft:biome" }
|
||||
]
|
||||
}
|
||||
@@ -58,6 +58,14 @@
|
||||
"settings": "sanctuary:alpha"
|
||||
}
|
||||
},
|
||||
"sanctuary:backroom": {
|
||||
"type": "sanctuary:backroom",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": { "type": "minecraft:fixed", "biome": "sanctuary:backroom" },
|
||||
"settings": "sanctuary:backroom"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
|
||||
@@ -58,6 +58,14 @@
|
||||
"settings": "sanctuary:alpha"
|
||||
}
|
||||
},
|
||||
"sanctuary:backroom": {
|
||||
"type": "sanctuary:backroom",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": { "type": "minecraft:fixed", "biome": "sanctuary:backroom" },
|
||||
"settings": "sanctuary:backroom"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"sanctuary26": {
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "sanctuary",
|
||||
"data_version": 13,
|
||||
"data_version": 14,
|
||||
"network_protocol": 24,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"AbstractContainerMenuMixin",
|
||||
"BedBlockMixin",
|
||||
"ChestBlockEntityMixin",
|
||||
"ConsumableMixin",
|
||||
"EntityCompanionMixin",
|
||||
"EntityAirSupplyMixin",
|
||||
"FoodDataMixin",
|
||||
"InventoryMixin",
|
||||
"InventoryMenuMixin",
|
||||
"ItemEntityMixin",
|
||||
"ItemTooltipMixin",
|
||||
"LivingEntityCompanionMixin",
|
||||
"PlayerOverflowMixin",
|
||||
"SlotMixin",
|
||||
"ServerGamePacketListenerMixin"
|
||||
"ServerGamePacketListenerMixin",
|
||||
"ServerPlayerBackroomMixin"
|
||||
],
|
||||
"client": [
|
||||
"client.AbstractButtonMixin",
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.backroom;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.worldgen.feature.BackroomArchitectureFeature;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.util.RandomSource;
|
||||
|
||||
/** Executable invariants for bounded entry data, personal loot priority and architecture zoning. */
|
||||
public final class BackroomModelSmoke {
|
||||
private BackroomModelSmoke() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
entryTransactionsAreBounded();
|
||||
personalRecoveryIsPrioritized();
|
||||
recentLossesRemainPersonal();
|
||||
architectureIsDeterministicAndComposed();
|
||||
}
|
||||
|
||||
private static void entryTransactionsAreBounded() {
|
||||
BackroomData data = new BackroomData();
|
||||
UUID player = UUID.fromString("c6ff53b1-8b3e-4d7d-9a46-81ef366b02ba");
|
||||
require(BackroomData.MAX_ENTRY_SLOTS == 64, "Entry snapshot bound no longer covers exactly one player inventory");
|
||||
BackroomData.PendingEntry first = data.prepareEntry(player, List.of()).orElseThrow();
|
||||
BackroomData.PendingEntry replacement = data.prepareEntry(player, List.of()).orElseThrow();
|
||||
require(!first.transaction().equals(replacement.transaction()), "Reprepared entry reused its transaction ID");
|
||||
require(data.pendingEntry(player).orElseThrow().slots().isEmpty(),
|
||||
"A player has more than one pending entry transaction");
|
||||
data.finishEntry(player);
|
||||
require(data.pendingEntry(player).isEmpty(), "Finished entry transaction remained persisted");
|
||||
for (int index = 0; index < BackroomData.MAX_PENDING_ENTRIES; index++) {
|
||||
require(data.prepareEntry(new UUID(0L, index + 1L), List.of()).isPresent(),
|
||||
"Pending-entry registry filled before its documented bound");
|
||||
}
|
||||
require(data.prepareEntry(new UUID(1L, 1L), List.of()).isEmpty(),
|
||||
"A new entry evicted an unresolved Mailbox transaction");
|
||||
|
||||
for (int index = 0; index < BackroomData.MAX_RECENT_ARRIVALS + 12; index++) {
|
||||
data.beginExpedition(player, index);
|
||||
}
|
||||
require(data.recentArrivals(player).size() == BackroomData.MAX_RECENT_ARRIVALS,
|
||||
"Arrival history exceeded its persistence bound");
|
||||
}
|
||||
|
||||
private static void personalRecoveryIsPrioritized() {
|
||||
RandomSource first = RandomSource.create(26_200_132L);
|
||||
RandomSource second = RandomSource.create(26_200_133L);
|
||||
RandomSource later = RandomSource.create(26_200_134L);
|
||||
int firstHits = 0, secondHits = 0, laterHits = 0;
|
||||
for (int sample = 0; sample < 20_000; sample++) {
|
||||
if (BackroomChestService.personalRoll(first, 1)) firstHits++;
|
||||
if (BackroomChestService.personalRoll(second, 2)) secondHits++;
|
||||
if (!BackroomChestService.personalRoll(second, 3)) throw new AssertionError("Third personal chest is not guaranteed");
|
||||
if (BackroomChestService.personalRoll(later, 4)) laterHits++;
|
||||
}
|
||||
require(firstHits > 14_500 && firstHits < 15_500, "First-chest personal chance drifted from 75%");
|
||||
require(secondHits > 17_500 && secondHits < 18_500, "Second-chest personal chance drifted from 90%");
|
||||
require(laterHits > 12_500 && laterHits < 13_500, "Later personal chance drifted from 65%");
|
||||
require(BackroomChestService.REFRESH_TICKS == 12_000L, "Chest renewal is no longer twice per Minecraft day");
|
||||
}
|
||||
|
||||
private static void recentLossesRemainPersonal() {
|
||||
require(!BackroomData.sharedEligible("owner", 10_000L,
|
||||
10_000L + BackroomData.PERSONAL_RESERVATION_TICKS - 1L),
|
||||
"A recent owned loss escaped into historical loot");
|
||||
require(BackroomData.sharedEligible("owner", 10_000L,
|
||||
10_000L + BackroomData.PERSONAL_RESERVATION_TICKS),
|
||||
"An old owned loss never became historical loot");
|
||||
require(BackroomData.sharedEligible("", 10_000L, 10_000L),
|
||||
"An ownerless loss was not immediately eligible for shared loot");
|
||||
}
|
||||
|
||||
private static void architectureIsDeterministicAndComposed() {
|
||||
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(families.size() == BackroomArchitectureFeature.Architecture.values().length,
|
||||
"Not all authored architecture families are reachable");
|
||||
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 require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user