feat: livrer et migrer le Magic Carpet après Notch #121
+7
-3
@@ -8,14 +8,18 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
|
||||
## Non distribué
|
||||
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.37`**, **Ouch `0.0.0-alpha.11`**, **Sanctuary `0.0.0-alpha.126`** et **MasterKey `0.0.0-alpha.9`**. Le pack public reste temporairement en `26.2.0-alpha.223` jusqu'à la publication.
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.37`**, **Ouch `0.0.0-alpha.11`**, **I Like To Move It `0.0.0-alpha.17`**, **Only Fun `0.0.0-alpha.26`**, **Sanctuary `0.0.0-alpha.127`** et **MasterKey `0.0.0-alpha.9`**. Le pack public reste temporairement en `26.2.0-alpha.223` jusqu'à la publication.
|
||||
|
||||
- ajoute Notch comme résident unique et persistant de la maison Indev, avec restauration automatique, déduplication et lecture des anciennes entités `portals:notch` ;
|
||||
- transforme Notch en Titan au premier coup volontaire, avec santé bornée selon les participants, poursuite, attaques télégraphiées, boules de feu renvoyables et réinitialisation si le combat est abandonné ;
|
||||
- limite la destruction de blocs à l'explosion initiale de transformation ; les attaques suivantes utilisent uniquement dégâts, poussée, particules et explosions non destructrices ;
|
||||
- enregistre la première victoire comme jalon mondial monotone, crédite les participants, inscrit un marqueur de boss dans leur Atlas déjà découvert et réserve un drapeau de récompense unique pour la suite ;
|
||||
- enregistre la première victoire comme jalon mondial monotone, crédite les participants, inscrit un marqueur de boss dans leur Atlas déjà découvert et réserve une récompense physique unique ;
|
||||
- place le Magic Carpet de Notch dans un coffre déterministe signalé par feu d'artifice, avec réservation et livraison persistées séparément pour récupérer une interruption sans duplication ;
|
||||
- conserve `portals:notch_totem` comme alias caché et convertit inventaires, conteneurs et items au sol vers `iliketomoveit:magic_carpet` en gardant quantité, composants et nom personnalisé ;
|
||||
- maintient la plateforme temporaire 3 × 3 sous autorité serveur, sans écraser le terrain et avec un suivi par propriétaire qui protège les tapis superposés pendant les nettoyages ;
|
||||
- rend l'offre Magic Carpet visible et achetable uniquement après `sanctuary:notch_defeated`, au prix exceptionnel existant, avec paiement normal, livraison Mailbox, provenance et journal de remplacement ;
|
||||
- ajoute les diagnostics `/masterkey notch inspect` et `/masterkey notch restore`, les traductions FR/EN/RU et les contrats de test déterministes associés ;
|
||||
- migre les données It's Alive de v3 à v4 et Sanctuary de v11 à v12, sans changer les protocoles réseau ni le pack public.
|
||||
- migre les données It's Alive de v3 à v4, I Like To Move It de v3 à v4 et Sanctuary de v11 à v13, sans changer les protocoles réseau ni le pack public.
|
||||
|
||||
## `26.2.0-alpha.223`
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ L’alpha.223 ajoute le tri natif des rangées de stockage par clic molette sans
|
||||
|
||||
L’alpha.222 améliore le terminal de stockage avec une recherche Unicode, des quantités compactes et des emplacements qui ne glissent plus lorsqu'une pile est vidée. Elle ajoute aussi le suivi textuel des quêtes actives en haut à gauche, masquable avec une touche configurable, ainsi que des suggestions d’IDs d’items et de mobs dans l’éditeur de demandes. JEI passe à la bêta Fabric 26.2 `30.29.0.196`.
|
||||
|
||||
Le lot non distribué ajoute Notch comme résident unique de la maison Indev et son combat de Titan. It's Alive possède l'entité persistante, Ouch les attaques et dégâts, Sanctuary le jalon mondial de victoire et l'Atlas, tandis que MasterKey fournit les commandes d'inspection et de restauration. Les anciennes entités `portals:notch` restent chargeables.
|
||||
Le lot non distribué ajoute Notch comme résident unique de la maison Indev et son combat de Titan. It's Alive possède l'entité persistante, Ouch les attaques et dégâts, Sanctuary le jalon mondial de victoire et l'Atlas, tandis que MasterKey fournit les commandes d'inspection et de restauration. Les anciennes entités `portals:notch` restent chargeables. La première victoire réserve maintenant un unique `iliketomoveit:magic_carpet` dans un coffre signalé par feu d'artifice. L'ancien `portals:notch_totem` est migré sans perte ; après Notch, Only Fun ouvre l'offre de remplacement au Shop avec paiement serveur et livraison par Mailbox.
|
||||
|
||||
L’alpha.221 corrige la cause réelle du disque blanc ralenti et trop grave : Ambiance décode désormais la source AAC en stéréo native à 48 kHz, puis moyenne explicitement chaque paire gauche/droite vers le flux mono positionnel de Minecraft. OpenAL reste à sa hauteur normale `1.0`, sans compensation artificielle ni changement de durée.
|
||||
|
||||
|
||||
+154
-40
@@ -2915,11 +2915,11 @@ tasks.register("verifyNotchEncounter") {
|
||||
def textureHash = MessageDigest.getInstance("SHA-256").digest(texture.bytes).encodeHex().toString()
|
||||
if (rootProject.itsalive_version != "0.0.0-alpha.37"
|
||||
|| rootProject.ouch_version != "0.0.0-alpha.11"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| rootProject.masterkey_version != "0.0.0-alpha.9"
|
||||
|| rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| manifests.itsalive.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 12
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.ouch.depends?.itsalive != ">=0.0.0-alpha.37"
|
||||
|| !registry.contains('Identifier.fromNamespaceAndPath("portals", "notch")')
|
||||
|| !resident.contains("setCanonicalUuid(canonical.getUUID())")
|
||||
@@ -2964,6 +2964,112 @@ tasks.named("verifyWorkspace") {
|
||||
dependsOn tasks.named("verifyCavernsGeology")
|
||||
}
|
||||
|
||||
tasks.register("verifyMagicCarpet") {
|
||||
group = "verification"
|
||||
description = "Checks CARPET-01 through CARPET-03 reward, migration, platform and Shop contracts."
|
||||
inputs.file(file("iliketomoveit/MAGIC_CARPET.md"))
|
||||
inputs.file(file("pack/migrations/26.2.0-alpha.223-magic-carpet-unreleased.json"))
|
||||
inputs.files(fileTree("iliketomoveit/src/main/java") { include "**/*MagicCarpet*.java", "**/*NotchCarpet*.java" })
|
||||
inputs.files(fileTree("sanctuary/src/main/java") {
|
||||
include "**/NotchVictory*.java", "**/SanctuaryNotchMilestone.java",
|
||||
"**/SanctuaryWorldMilestones.java", "**/SanctuaryItemProvenance.java", "**/ShopService.java"
|
||||
})
|
||||
inputs.files(fileTree("onlyfun/src/main/java") { include "**/OnlyFunBossShopGate.java", "**/OnlyFunShopCatalog.java" })
|
||||
inputs.files(["iliketomoveit", "onlyfun", "sanctuary"].collect {
|
||||
file("${it}/src/main/resources/fabric.mod.json")
|
||||
})
|
||||
inputs.files(["fr_fr", "en_us", "ru_ru"].collect {
|
||||
file("iliketomoveit/src/main/resources/assets/iliketomoveit/lang/${it}.json")
|
||||
})
|
||||
inputs.file(file("iliketomoveit/src/main/resources/assets/portals/items/notch_totem.json"))
|
||||
|
||||
doLast {
|
||||
def migration = new JsonSlurper().parse(file(
|
||||
"pack/migrations/26.2.0-alpha.223-magic-carpet-unreleased.json"))
|
||||
def manifests = ["iliketomoveit", "onlyfun", "sanctuary"].collectEntries { id ->
|
||||
[(id): new JsonSlurper().parse(file("${id}/src/main/resources/fabric.mod.json"))]
|
||||
}
|
||||
def reward = file("iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/transport/NotchCarpetRewardService.java").text
|
||||
def migrationJava = file("iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/transport/MagicCarpetMigration.java").text
|
||||
def controller = file("iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/transport/MagicCarpetController.java").text
|
||||
def ledger = file("iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/transport/MagicCarpetLedger.java").text
|
||||
def items = file("iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/registry/ILikeToMoveItItems.java").text
|
||||
def mixins = new JsonSlurper().parse(file("iliketomoveit/src/main/resources/iliketomoveit.mixins.json"))
|
||||
def victory = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/data/NotchVictoryState.java").text
|
||||
def milestones = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/progression/SanctuaryWorldMilestones.java").text
|
||||
def provenance = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/item/SanctuaryItemProvenance.java").text
|
||||
def gate = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunBossShopGate.java").text
|
||||
def shop = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.17"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifests.iliketomoveit.depends?.sanctuary != ">=0.0.0-alpha.127"
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
|| manifests.onlyfun.depends?.iliketomoveit != ">=0.0.0-alpha.17"
|
||||
|| manifests.onlyfun.depends?.sanctuary != ">=0.0.0-alpha.127"
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22
|
||||
|| migration.status != "unreleased"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.223"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.223"
|
||||
|| migration.magic_carpet?.canonical_item != "iliketomoveit:magic_carpet"
|
||||
|| migration.magic_carpet?.legacy_item != "portals:notch_totem"
|
||||
|| migration.magic_carpet?.legacy_locations != ["player_inventory", "container", "item_entity"]
|
||||
|| migration.magic_carpet?.temporary_platform?.size != [3, 1, 3]
|
||||
|| migration.magic_carpet?.temporary_platform?.air_only_placement != true
|
||||
|| migration.magic_carpet?.temporary_platform?.overlap_owner_tracking != true
|
||||
|| migration.notch_reward?.maximum_canonical_initial_rewards != 1
|
||||
|| migration.notch_reward?.idempotent_recovery != true
|
||||
|| migration.shop_replacement?.hidden_before_notch != true
|
||||
|| migration.shop_replacement?.server_authoritative_gate != true
|
||||
|| migration.shop_replacement?.mailbox_delivery != true
|
||||
|| migration.shop_replacement?.recreates_initial_reward != false
|
||||
|| migration.compatibility?.iliketomoveit_data_version != 4
|
||||
|| migration.compatibility?.sanctuary_data_version != 13
|
||||
|| migration.compatibility?.sanctuary_notch_state_data_version != 2
|
||||
|| !items.contains('Identifier.fromNamespaceAndPath("portals", "notch_totem")')
|
||||
|| !migrationJava.contains("transmuteCopy(ILikeToMoveItItems.MAGIC_CARPET")
|
||||
|| !migrationJava.contains("stampIfAbsent")
|
||||
|| !mixins.mixins.containsAll(["MagicCarpetContainerMixin", "MagicCarpetItemEntityMixin"])
|
||||
|| !reward.contains("claimInitialReward") || !reward.contains("completeInitialReward")
|
||||
|| !reward.contains("Blocks.CHEST") || !reward.contains("FireworkRocketEntity")
|
||||
|| !reward.contains("SanctuaryItemProvenance.NOTCH_REWARD")
|
||||
|| !victory.contains('optionalFieldOf("initial_reward_delivered", false)')
|
||||
|| !controller.contains("if (current.isAir())")
|
||||
|| !controller.contains("resetFallDistance")
|
||||
|| controller.contains("mayfly") || controller.contains("flying")
|
||||
|| !ledger.contains("trackedByAnother") || !ledger.contains("drain()")
|
||||
|| !milestones.contains("SanctuaryNotchMilestone.isDefeated(server)")
|
||||
|| !gate.contains("SanctuaryWorldMilestones.isReached")
|
||||
|| !gate.contains("IMPLEMENTED_MILESTONES")
|
||||
|| !shop.contains("SanctuaryItemProvenance.SHOP_PURCHASE")
|
||||
|| !shop.contains("reward_recreated=false")
|
||||
|| !provenance.contains('ORIGIN_KEY = "sanctuary_origin"')) {
|
||||
throw new GradleException("The CARPET-01 through CARPET-03 contract is incomplete")
|
||||
}
|
||||
["fr_fr", "en_us", "ru_ru"].each { locale ->
|
||||
def values = new JsonSlurper().parse(file(
|
||||
"iliketomoveit/src/main/resources/assets/iliketomoveit/lang/${locale}.json"))
|
||||
def required = ["item.iliketomoveit.magic_carpet", "item.portals.notch_totem",
|
||||
"item.iliketomoveit.magic_carpet.description",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop",
|
||||
"message.iliketomoveit.magic_carpet.reward_created"]
|
||||
if (!values.keySet().containsAll(required)) {
|
||||
throw new GradleException("Missing ${locale} Magic Carpet translations")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("verifyWorkspace") {
|
||||
dependsOn tasks.named("verifyMagicCarpet")
|
||||
}
|
||||
|
||||
tasks.register("verifyWorldgenVisibility") {
|
||||
group = "verification"
|
||||
description = "Checks the alpha.179 Sulfur Cave, stone and Cobble Dungeon visibility contract."
|
||||
@@ -3523,8 +3629,8 @@ tasks.register("verifyProgressivePauseMenuRelease") {
|
||||
"sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))]
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|
||||
|| 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"]
|
||||
|| migration.target?.modules != [sanctuary: "0.0.0-alpha.109", onlyfun: "0.0.0-alpha.17"]
|
||||
@@ -3835,8 +3941,9 @@ project(":onlyfun") {
|
||||
def shitCubeRecipe = new JsonSlurper().parse(file(
|
||||
"src/main/resources/data/abjection/recipe/shit_cube.json"))
|
||||
if (manifest.custom.sanctuary26.data_version != 2
|
||||
|| project.version.toString() != "0.0.0-alpha.25"
|
||||
|| manifest.depends?.sanctuary != "*"
|
||||
|| project.version.toString() != "0.0.0-alpha.26"
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.127"
|
||||
|| manifest.depends?.iliketomoveit != ">=0.0.0-alpha.17"
|
||||
|| manifest.provides != ["abjection"]
|
||||
|| manifest.custom.sanctuary26.network_protocol != 3
|
||||
|| !javaText.contains('NAMESPACE = "abjection"')
|
||||
@@ -4364,8 +4471,8 @@ tasks.register("verifyShopPricingLayoutRelease") {
|
||||
def release = new JsonSlurper().parse(file("pack/release.json"))
|
||||
def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| 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"
|
||||
@@ -4458,8 +4565,8 @@ tasks.register("verifyShopCsvBossGatesRelease") {
|
||||
def catalog = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalog.java").text
|
||||
def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| 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"]
|
||||
@@ -4478,7 +4585,8 @@ tasks.register("verifyShopCsvBossGatesRelease") {
|
||||
|| csvLines.size() != 1985
|
||||
|| !catalog.contains("ShopCatalogCsv.load()")
|
||||
|| !catalog.contains("OnlyFunBossShopGate.isAvailable")
|
||||
|| !bossGate.contains("BossUnlockResolver LOCKED")
|
||||
|| !bossGate.contains("IMPLEMENTED_MILESTONES")
|
||||
|| !bossGate.contains("SanctuaryWorldMilestones.isReached")
|
||||
|| !bossGate.contains("MAGIC_CLOUD, MAGIC_CARPET")
|
||||
|| !offerCatalog.contains("currentProvider.isAvailable(server, itemId)")) {
|
||||
throw new GradleException("The alpha.195 CSV Shop and boss-gate release contract is incomplete")
|
||||
@@ -4551,7 +4659,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.223"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.37"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| 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"]
|
||||
@@ -4651,7 +4759,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.223"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.37"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| 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"
|
||||
@@ -4771,7 +4879,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.223"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| 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"
|
||||
@@ -4783,7 +4891,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 != 12
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 13
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22
|
||||
|| release.pack_version != "26.2.0-alpha.223"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.223.mrpack"
|
||||
@@ -4827,8 +4935,8 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.37"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.16"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.17"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.source?.modules != [
|
||||
itsalive: "0.0.0-alpha.30",
|
||||
@@ -4860,9 +4968,9 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
|| migration.compatibility?.registry_ids_changed != false
|
||||
|| itsAliveManifest.custom?.sanctuary26?.data_version != 4
|
||||
|| itsAliveManifest.custom?.sanctuary26?.network_protocol != 4
|
||||
|| moveItManifest.custom?.sanctuary26?.data_version != 3
|
||||
|| moveItManifest.custom?.sanctuary26?.data_version != 4
|
||||
|| moveItManifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 12
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 13
|
||||
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22
|
||||
|| release.pack_version != "26.2.0-alpha.223"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.223.mrpack"
|
||||
@@ -4986,7 +5094,7 @@ tasks.register("verifySafeVaultFeature") {
|
||||
}.keySet()
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| migration.schema_version != 1
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.202"
|
||||
@@ -5729,7 +5837,7 @@ tasks.register("verifyCanapliaAlpha209Release") {
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.22"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.37"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.207"
|
||||
|| migration.source?.modules != expectedSource
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.208"
|
||||
@@ -5845,9 +5953,9 @@ tasks.register("verifyAlpha210Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.29.0.198.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|
||||
|| rootProject.iliketomoveit_version != currentModules.iliketomoveit
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.17"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.37"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.209"
|
||||
|| migration.source?.modules != expectedSource
|
||||
@@ -5882,11 +5990,11 @@ 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 != 12
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 3
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifests.itsalive.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.itsalive.custom?.sanctuary26?.network_protocol != 4
|
||||
@@ -5945,9 +6053,10 @@ tasks.register("verifyAlpha211Release") {
|
||||
ambiance: "0.0.0-alpha.22",
|
||||
iliketomoveit: "0.0.0-alpha.16"]
|
||||
def workspaceModules = currentModules + [
|
||||
sanctuary: "0.0.0-alpha.126", itsalive: "0.0.0-alpha.37", ouch: "0.0.0-alpha.11"]
|
||||
sanctuary: "0.0.0-alpha.127", onlyfun: "0.0.0-alpha.26",
|
||||
iliketomoveit: "0.0.0-alpha.17", itsalive: "0.0.0-alpha.37", ouch: "0.0.0-alpha.11"]
|
||||
def expectedDataVersions = [ambiance: 2, itsalive: 3, onlyfun: 2, ouch: 1, sanctuary: 11]
|
||||
def workspaceDataVersions = [ambiance: 2, itsalive: 4, onlyfun: 2, ouch: 1, sanctuary: 12]
|
||||
def workspaceDataVersions = [ambiance: 2, itsalive: 4, onlyfun: 2, ouch: 1, sanctuary: 13]
|
||||
def expectedProtocols = [ambiance: 6, itsalive: 4, onlyfun: 3, ouch: 1, sanctuary: 22]
|
||||
def manifests = expectedTarget.collectEntries { id, version ->
|
||||
[(id): new JsonSlurper().parse(file("${id}/src/main/resources/fabric.mod.json"))]
|
||||
@@ -6044,7 +6153,7 @@ tasks.register("verifyAlpha212Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.29.0.198.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| 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"
|
||||
@@ -6062,7 +6171,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 != 12
|
||||
|| manifest.custom?.sanctuary26?.data_version != 13
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 22
|
||||
|| !minecraftMixin.contains('method = "pickBlockOrEntity"')
|
||||
|| release.pack_version != "26.2.0-alpha.223"
|
||||
@@ -6534,10 +6643,14 @@ tasks.register("verifyAlpha218Release") {
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| currentModules.findAll { id, version -> id != "redstoner" && id != "sanctuary" }.any {
|
||||
|| currentModules.findAll { id, version ->
|
||||
id != "redstoner" && id != "sanctuary" && id != "onlyfun" && id != "iliketomoveit"
|
||||
}.any {
|
||||
id, version -> rootProject.property("${id}_version") != version
|
||||
}
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.17"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.217"
|
||||
|| migration.source?.modules != sourceModules
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.218"
|
||||
@@ -6561,7 +6674,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 != 12
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22
|
||||
|| !rules.contains("MAX_CONTAINERS = 128")
|
||||
|| !rules.contains("PAGE_SIZE = 54")
|
||||
@@ -6621,7 +6734,8 @@ tasks.register("verifyAlpha219Release") {
|
||||
sanctuary: "0.0.0-alpha.122", iliketomoveit: "0.0.0-alpha.16", ambiance: "0.0.0-alpha.21"]
|
||||
def currentModules = targetModules + [
|
||||
ambiance: "0.0.0-alpha.22", sanctuary: "0.0.0-alpha.125"]
|
||||
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.126"]
|
||||
def workspaceModules = currentModules + [
|
||||
sanctuary: "0.0.0-alpha.127", iliketomoveit: "0.0.0-alpha.17"]
|
||||
def requiredJars = currentModules.collect { id, version ->
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
@@ -6661,9 +6775,9 @@ 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 != 12
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 3
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifests.ambiance.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.ambiance.custom?.sanctuary26?.network_protocol != 6
|
||||
@@ -6946,7 +7060,7 @@ tasks.register("verifyAlpha222Release") {
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.221"
|
||||
|| migration.source?.modules != [
|
||||
redstoner: "0.0.0-alpha.12", sanctuary: "0.0.0-alpha.122"]
|
||||
@@ -6988,7 +7102,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 != 12
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 13
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22
|
||||
|| !rules.contains("MAX_SEARCH_LENGTH = 32")
|
||||
|| !rules.contains("compactCount(int count)")
|
||||
@@ -7104,7 +7218,7 @@ tasks.register("verifyAlpha223Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.29.0.198.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.223"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.127"
|
||||
|| 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"
|
||||
@@ -7140,7 +7254,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 != 12
|
||||
|| manifest.custom?.sanctuary26?.data_version != 13
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 22
|
||||
|| !inventorySort.contains("COOLDOWN_TICKS = 8")
|
||||
|| !inventorySort.contains("if (row == 0) continue")
|
||||
|
||||
+3
-3
@@ -24,11 +24,11 @@ ouch_version=0.0.0-alpha.11
|
||||
ouch_lifecycle=active
|
||||
masterkey_version=0.0.0-alpha.9
|
||||
masterkey_lifecycle=active
|
||||
iliketomoveit_version=0.0.0-alpha.16
|
||||
iliketomoveit_version=0.0.0-alpha.17
|
||||
iliketomoveit_lifecycle=active
|
||||
onlyfun_version=0.0.0-alpha.25
|
||||
onlyfun_version=0.0.0-alpha.26
|
||||
onlyfun_lifecycle=active
|
||||
sanctuary_version=0.0.0-alpha.126
|
||||
sanctuary_version=0.0.0-alpha.127
|
||||
sanctuary_lifecycle=active
|
||||
pack_version=26.2.0-alpha.223
|
||||
maven_group=fr.koka99cab.sanctuary26
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Magic Carpet et Totem de Notch
|
||||
|
||||
I Like To Move It possède l'objet, la plateforme temporaire, la remise physique
|
||||
de la récompense et la compatibilité de l'ancien Totem. Sanctuary reste
|
||||
propriétaire du jalon mondial et de la réservation unique ; Only Fun possède le
|
||||
catalogue et applique le jalon avant d'exposer l'offre.
|
||||
|
||||
## Identités et provenance
|
||||
|
||||
| Rôle | ID | Statut |
|
||||
|---|---|---|
|
||||
| objet canonique | `iliketomoveit:magic_carpet` | distribué après Notch puis achetable au Shop |
|
||||
| Totem historique | `portals:notch_totem` | alias caché, lisible et converti vers l'objet canonique |
|
||||
| jalon mondial | `sanctuary:notch_defeated` | autorité serveur Sanctuary |
|
||||
|
||||
La conversion utilise `ItemStack.transmuteCopy` : quantité, composants et nom
|
||||
personnalisé sont conservés. Les inventaires joueurs sont traités à la connexion,
|
||||
les conteneurs vanilla lors de leur chargement et les entités d'item dès leur
|
||||
tick serveur. Un Totem produit exactement un tapis et les nouveaux mondes ne
|
||||
distribuent jamais l'ancien ID. La donnée I Like To Move It passe en v4 ; une
|
||||
pile déjà convertie ne correspond plus à l'alias et ne peut donc pas être
|
||||
convertie une seconde fois.
|
||||
|
||||
Chaque tapis porte une provenance Sanctuary sans autorité de gameplay :
|
||||
`notch_reward`, `shop_purchase` ou `legacy_notch_totem`. Le serveur ne se fie
|
||||
jamais à cette étiquette pour autoriser un achat ; elle sert à l'historique, au
|
||||
diagnostic et à l'infobulle.
|
||||
|
||||
## Première récompense
|
||||
|
||||
Après la première victoire mondiale, Sanctuary réserve atomiquement la
|
||||
récompense puis I Like To Move It crée un coffre déterministe près de la maison
|
||||
de Notch. Le tapis unique est placé au centre et un feu d'artifice signale le
|
||||
coffre. Sanctuary ne marque la livraison terminée qu'après avoir retrouvé ou
|
||||
créé ce coffre.
|
||||
|
||||
Les états `initial_reward_claimed` et `initial_reward_delivered` sont distincts.
|
||||
Une reprise entre les deux relance la création de façon idempotente ; un coffre
|
||||
déjà créé et marqué est reconnu sans recevoir de second tapis. Les anciennes
|
||||
sauvegardes v1 dont la récompense était déjà déclarée remise sont migrées vers
|
||||
l'état livré afin de ne rien dupliquer.
|
||||
|
||||
## Plateforme temporaire
|
||||
|
||||
Tenir le tapis en main principale ou secondaire maintient neuf blocs de verre
|
||||
sous le joueur. Seul l'air est remplacé : terrain, liquides, plantes et blocs
|
||||
construits restent intacts. Le joueur conserve ses capacités normales ; seul le
|
||||
dégât de chute est réinitialisé tant que la plateforme est active.
|
||||
|
||||
Chaque position possède un ensemble implicite de propriétaires. Deux tapis
|
||||
peuvent se chevaucher et le verre ne disparaît qu'après le départ du dernier.
|
||||
Le nettoyage est effectué côté serveur au relâchement, à la déconnexion, au
|
||||
changement de dimension et pendant l'arrêt normal du serveur.
|
||||
|
||||
## Remplacement au Shop
|
||||
|
||||
Only Fun associe le Magic Carpet au resolver public
|
||||
`SanctuaryWorldMilestones`. Avant `sanctuary:notch_defeated`, l'offre est
|
||||
absente des snapshots et toute tentative réseau est rejetée par Sanctuary.
|
||||
Après la victoire, elle rejoint la rotation normale au prix configuré de
|
||||
1 536 rubis et 128 saphirs : les saphirs débloquent le catalogue, les rubis
|
||||
paient chaque remplacement et la Mailbox effectue la livraison différée.
|
||||
|
||||
Un achat porte la provenance `shop_purchase`, est journalisé comme remplacement
|
||||
et ne touche jamais aux états de réservation ou de coffre de la récompense
|
||||
initiale.
|
||||
+19
-10
@@ -32,6 +32,14 @@ tasks.register("alphaPortalContractSmoke", JavaExec) {
|
||||
dependsOn tasks.named("testClasses")
|
||||
}
|
||||
|
||||
tasks.register("magicCarpetLedgerSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks Magic Carpet overlap, dimension-change and shutdown cleanup ownership."
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetLedgerSmoke"
|
||||
dependsOn tasks.named("testClasses")
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
// Runtime contracts are executable smoke tasks, not JUnit engine tests.
|
||||
failOnNoDiscoveredTests = false
|
||||
@@ -100,11 +108,11 @@ tasks.register("verifyWaystones") {
|
||||
if (!missingResources.isEmpty()) throw new GradleException("Missing Waystone resources: ${missingResources}")
|
||||
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (manifest.custom?.sanctuary26?.data_version != 3
|
||||
if (manifest.custom?.sanctuary26?.data_version != 4
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifest.depends?.anotherworld != ">=0.0.0-alpha.46"
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.21") {
|
||||
throw new GradleException("I Like To Move It must expose data v3, protocol 5 and its Another World alpha.46/Sanctuary alpha.21 dependencies")
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.127") {
|
||||
throw new GradleException("I Like To Move It must expose data v4, protocol 5 and its Another World alpha.46/Sanctuary alpha.127 dependencies")
|
||||
}
|
||||
["fr_fr", "en_us", "ru_ru"].each { language ->
|
||||
def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json"))
|
||||
@@ -203,9 +211,9 @@ tasks.register("verifyGrappling") {
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (!manifest.mixins?.any { it.config == "iliketomoveit.mixins.json" }
|
||||
|| !manifest.mixins?.any { it.config == "iliketomoveit.client.mixins.json" && it.environment == "client" }
|
||||
|| manifest.custom?.sanctuary26?.data_version != 3
|
||||
|| manifest.custom?.sanctuary26?.data_version != 4
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 5) {
|
||||
throw new GradleException("I Like To Move It must keep aggregate data v3/protocol 5 and register its common/client mixins")
|
||||
throw new GradleException("I Like To Move It must expose aggregate data v4/protocol 5 and register its common/client mixins")
|
||||
}
|
||||
["fr_fr", "en_us", "ru_ru"].each { language ->
|
||||
def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json"))
|
||||
@@ -313,7 +321,7 @@ tasks.register("verifyPortals") {
|
||||
}
|
||||
def atlasCavernsMigration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.149-atlas-caverns-to-alpha.150.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.17"
|
||||
|| atlasCavernsMigration.target?.pack_version != "26.2.0-alpha.150"
|
||||
|| atlasCavernsMigration.target?.modules?.iliketomoveit != "0.0.0-alpha.9"
|
||||
|| atlasCavernsMigration.portals?.active_routes != [
|
||||
@@ -349,7 +357,7 @@ tasks.register("verifySilverAircraft") {
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
def migration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.17"
|
||||
|| recipe.pattern != ["PGP", "GCG", "PBP"]
|
||||
|| recipe.key != [B: "anotherworld:silver_block", C: "anotherworld:aircraft_cockpit",
|
||||
G: "anotherworld:silver_gear", P: "anotherworld:silver_plate"]
|
||||
@@ -378,7 +386,7 @@ tasks.register("verifyGrapplePrediction") {
|
||||
def payload = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java").text
|
||||
def controller = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java").text
|
||||
def prediction = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.17"
|
||||
|| migration.target?.modules?.iliketomoveit != "0.0.0-alpha.13"
|
||||
|| migration.grappling?.owner_prediction_payload != "iliketomoveit:s2c/grapple_state"
|
||||
|| migration.compatibility?.iliketomoveit_source_network_protocol != 4
|
||||
@@ -410,7 +418,7 @@ tasks.register("verifyWaystoneDescendingRelease") {
|
||||
"src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRules.java").text
|
||||
def teleportation = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/WaystoneTeleportation.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.17"
|
||||
|| migration.target?.modules?.iliketomoveit != "0.0.0-alpha.14"
|
||||
|| migration.waystone_elevator?.undiscovered_downward_destination_allowed != true
|
||||
|| migration.waystone_elevator?.destination_discovered_after_arrival != true
|
||||
@@ -444,7 +452,7 @@ tasks.register("verifyAlphaPortal") {
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
|
||||
new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${it}.json"))
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.17"
|
||||
|| route.schema_version != 2 || route.source_dimension != "minecraft:overworld"
|
||||
|| route.destination_dimension != "sanctuary:alpha"
|
||||
|| route.destination_resolver != "fixed"
|
||||
@@ -466,6 +474,7 @@ tasks.register("verifyAlphaPortal") {
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("grapplePhysicsSmoke")
|
||||
dependsOn tasks.named("magicCarpetLedgerSmoke")
|
||||
dependsOn tasks.named("waystoneElevatorRulesSmoke")
|
||||
dependsOn tasks.named("happyGhastCruiseSpeedSmoke")
|
||||
dependsOn tasks.named("verifyWaystones")
|
||||
|
||||
+5
-1
@@ -8,6 +8,8 @@ import fr.koka99cab.sanctuary26.iliketomoveit.pulley.PulleyHeadController;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.GrapplingLootTables;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItRegistries;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetController;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetMigrationService;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.NotchCarpetRewardService;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneElevatorController;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
@@ -17,7 +19,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class ILikeToMoveItMod implements ModInitializer {
|
||||
public static final String MOD_ID = "iliketomoveit";
|
||||
public static final String DISPLAY_NAME = "I Like To Move It";
|
||||
public static final int CURRENT_DATA_VERSION = 3;
|
||||
public static final int CURRENT_DATA_VERSION = 4;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public ILikeToMoveItMod() {
|
||||
@@ -37,6 +39,8 @@ public final class ILikeToMoveItMod implements ModInitializer {
|
||||
GrappleController.register();
|
||||
PulleyHeadController.register();
|
||||
MagicCarpetController.register();
|
||||
MagicCarpetMigrationService.register();
|
||||
NotchCarpetRewardService.register();
|
||||
WaystoneElevatorController.initialize();
|
||||
LOGGER.info("[{}] Portals, vehicles, grappling, pulleys, Magic Carpet and dimension-local Waystones ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, ILikeToMoveItNetworking.PROTOCOL_VERSION);
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
|
||||
/** Epic carpet with explicit behaviour and provenance in its tooltip. */
|
||||
public final class MagicCarpetItem extends Item {
|
||||
public MagicCarpetItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.description")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
var origin = SanctuaryItemProvenance.origin(stack);
|
||||
if (SanctuaryItemProvenance.NOTCH_REWARD.equals(origin)) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.origin.notch")
|
||||
.withStyle(ChatFormatting.GOLD));
|
||||
} else if (SanctuaryItemProvenance.SHOP_PURCHASE.equals(origin)) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.origin.shop")
|
||||
.withStyle(ChatFormatting.AQUA));
|
||||
} else if (SanctuaryItemProvenance.LEGACY_NOTCH_TOTEM.equals(origin)) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.origin.legacy")
|
||||
.withStyle(ChatFormatting.DARK_PURPLE));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoil(ItemStack stack) {
|
||||
return SanctuaryItemProvenance.hasOrigin(stack, SanctuaryItemProvenance.NOTCH_REWARD)
|
||||
|| super.isFoil(stack);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetMigration;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
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;
|
||||
|
||||
/** Converts historical Totems as loaded chests and other vanilla containers are decoded. */
|
||||
@Mixin(BaseContainerBlockEntity.class)
|
||||
abstract class MagicCarpetContainerMixin {
|
||||
@Inject(method = "loadAdditional", at = @At("TAIL"))
|
||||
private void sanctuary26$migrateNotchTotem(ValueInput input, CallbackInfo ci) {
|
||||
MagicCarpetMigration.migrateContainer((Container) (Object) this);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetMigration;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
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;
|
||||
|
||||
/** Converts historical Totems already present as loaded item entities. */
|
||||
@Mixin(ItemEntity.class)
|
||||
abstract class MagicCarpetItemEntityMixin {
|
||||
@Inject(method = "tick", at = @At("HEAD"))
|
||||
private void sanctuary26$migrateNotchTotem(CallbackInfo ci) {
|
||||
ItemEntity self = (ItemEntity) (Object) this;
|
||||
ItemStack current = self.getItem();
|
||||
ItemStack replacement = MagicCarpetMigration.migrateStack(current);
|
||||
if (replacement != current) self.setItem(replacement);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.iliketomoveit.registry;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.item.AircraftItem;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.item.GrapplingRodItem;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.item.MagicCarpetItem;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -25,7 +26,11 @@ public final class ILikeToMoveItItems {
|
||||
key -> new BoatItem(ILikeToMoveItEntityTypes.MAGIC_CLOUD,
|
||||
new Item.Properties().stacksTo(1).rarity(Rarity.UNCOMMON).setId(key)));
|
||||
public static final Item MAGIC_CARPET = register("magic_carpet",
|
||||
key -> new Item(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC).setId(key)));
|
||||
key -> new MagicCarpetItem(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC).setId(key)));
|
||||
/** Hidden read-preserving alias for inventories saved by the historical MagicCarpet mod. */
|
||||
public static final Item LEGACY_NOTCH_TOTEM = register(
|
||||
Identifier.fromNamespaceAndPath("portals", "notch_totem"),
|
||||
key -> new MagicCarpetItem(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC).setId(key)));
|
||||
public static final Item GRAPPLING_HAND = register("grappling_hand",
|
||||
key -> new Item(new Item.Properties().setId(key)));
|
||||
public static final Item GRAPPLING_ROD = register("grappling_rod",
|
||||
|
||||
+16
-25
@@ -1,10 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItItems;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@@ -27,7 +24,7 @@ public final class MagicCarpetController {
|
||||
private static final int RADIUS = 1;
|
||||
private static final Block CARPET_BLOCK = Blocks.GLASS;
|
||||
private static final BlockState CARPET_STATE = CARPET_BLOCK.defaultBlockState();
|
||||
private static final Map<UUID, CarpetState> ACTIVE_CARPETS = new HashMap<>();
|
||||
private static final MagicCarpetLedger ACTIVE_CARPETS = new MagicCarpetLedger();
|
||||
|
||||
private MagicCarpetController() {
|
||||
}
|
||||
@@ -45,8 +42,8 @@ public final class MagicCarpetController {
|
||||
connected.add(id);
|
||||
boolean active = !player.isSpectator() && (isMagicCarpet(player.getMainHandItem()) || isMagicCarpet(player.getOffhandItem()));
|
||||
if (active) {
|
||||
if (!ACTIVE_CARPETS.containsKey(id)) {
|
||||
ACTIVE_CARPETS.put(id, CarpetState.empty(player.level().dimension()));
|
||||
if (!ACTIVE_CARPETS.contains(id)) {
|
||||
ACTIVE_CARPETS.track(id, player.level().dimension(), Set.of());
|
||||
player.sendOverlayMessage(Component.translatable("message.iliketomoveit.magic_carpet.enabled"));
|
||||
}
|
||||
updateCarpet(id, player);
|
||||
@@ -54,27 +51,26 @@ public final class MagicCarpetController {
|
||||
deactivate(server, id, true);
|
||||
}
|
||||
}
|
||||
Iterator<Map.Entry<UUID, CarpetState>> iterator = ACTIVE_CARPETS.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<UUID, CarpetState> entry = iterator.next();
|
||||
if (!connected.contains(entry.getKey())) {
|
||||
removePositions(server, entry.getKey(), entry.getValue(), entry.getValue().positions());
|
||||
iterator.remove();
|
||||
for (UUID owner : ACTIVE_CARPETS.owners()) {
|
||||
if (!connected.contains(owner)) {
|
||||
MagicCarpetLedger.State state = ACTIVE_CARPETS.remove(owner);
|
||||
if (state != null) removePositions(server, owner, state, state.positions());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMagicCarpet(ItemStack stack) {
|
||||
return !stack.isEmpty() && stack.is(ILikeToMoveItItems.MAGIC_CARPET);
|
||||
return MagicCarpetMigration.isAnyCarpet(stack);
|
||||
}
|
||||
|
||||
private static void updateCarpet(UUID owner, ServerPlayer player) {
|
||||
ServerLevel level = player.level();
|
||||
CarpetState previous = ACTIVE_CARPETS.getOrDefault(owner, CarpetState.empty(level.dimension()));
|
||||
MagicCarpetLedger.State previous = ACTIVE_CARPETS.state(owner, level.dimension());
|
||||
Set<BlockPos> target = carpetPositions(player.blockPosition().below(player.isCrouching() ? 2 : 1));
|
||||
if (!previous.dimension().equals(level.dimension())) {
|
||||
removePositions(level.getServer(), owner, previous, previous.positions());
|
||||
previous = CarpetState.empty(level.dimension());
|
||||
ACTIVE_CARPETS.remove(owner);
|
||||
previous = ACTIVE_CARPETS.state(owner, level.dimension());
|
||||
} else {
|
||||
Set<BlockPos> stale = new HashSet<>(previous.positions());
|
||||
stale.removeAll(target);
|
||||
@@ -91,7 +87,7 @@ public final class MagicCarpetController {
|
||||
placed.add(pos);
|
||||
}
|
||||
}
|
||||
ACTIVE_CARPETS.put(owner, new CarpetState(level.dimension(), Set.copyOf(placed)));
|
||||
ACTIVE_CARPETS.track(owner, level.dimension(), placed);
|
||||
player.resetFallDistance();
|
||||
}
|
||||
|
||||
@@ -104,14 +100,14 @@ public final class MagicCarpetController {
|
||||
}
|
||||
|
||||
private static void deactivate(MinecraftServer server, UUID owner, boolean notify) {
|
||||
CarpetState state = ACTIVE_CARPETS.remove(owner);
|
||||
MagicCarpetLedger.State state = ACTIVE_CARPETS.remove(owner);
|
||||
if (state == null) return;
|
||||
removePositions(server, owner, state, state.positions());
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(owner);
|
||||
if (notify && player != null) player.sendOverlayMessage(Component.translatable("message.iliketomoveit.magic_carpet.disabled"));
|
||||
}
|
||||
|
||||
private static void removePositions(MinecraftServer server, UUID owner, CarpetState state, Set<BlockPos> positions) {
|
||||
private static void removePositions(MinecraftServer server, UUID owner, MagicCarpetLedger.State state, Set<BlockPos> positions) {
|
||||
ServerLevel level = server.getLevel(state.dimension());
|
||||
if (level == null) return;
|
||||
for (BlockPos pos : positions) {
|
||||
@@ -122,18 +118,13 @@ public final class MagicCarpetController {
|
||||
}
|
||||
|
||||
private static boolean isTrackedByAnother(UUID owner, ResourceKey<Level> dimension, BlockPos pos) {
|
||||
return ACTIVE_CARPETS.entrySet().stream().anyMatch(entry -> !entry.getKey().equals(owner)
|
||||
&& entry.getValue().dimension().equals(dimension) && entry.getValue().positions().contains(pos));
|
||||
return ACTIVE_CARPETS.trackedByAnother(owner, dimension, pos);
|
||||
}
|
||||
|
||||
private static void reset(MinecraftServer server) {
|
||||
Map<UUID, CarpetState> copy = new HashMap<>(ACTIVE_CARPETS);
|
||||
ACTIVE_CARPETS.clear();
|
||||
Map<UUID, MagicCarpetLedger.State> copy = ACTIVE_CARPETS.drain();
|
||||
copy.forEach((owner, state) -> removePositions(server, owner, state, state.positions()));
|
||||
ILikeToMoveItMod.LOGGER.debug("Magic Carpet temporary platforms cleaned.");
|
||||
}
|
||||
|
||||
private record CarpetState(ResourceKey<Level> dimension, Set<BlockPos> positions) {
|
||||
private static CarpetState empty(ResourceKey<Level> dimension) { return new CarpetState(dimension, Set.of()); }
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Pure ownership ledger preventing overlapping temporary platforms from deleting each other. */
|
||||
public final class MagicCarpetLedger {
|
||||
private final Map<UUID, State> carpets = new HashMap<>();
|
||||
|
||||
public boolean contains(UUID owner) {
|
||||
return carpets.containsKey(owner);
|
||||
}
|
||||
|
||||
public State state(UUID owner, ResourceKey<Level> dimension) {
|
||||
return carpets.getOrDefault(owner, State.empty(dimension));
|
||||
}
|
||||
|
||||
public void track(UUID owner, ResourceKey<Level> dimension, Set<BlockPos> positions) {
|
||||
carpets.put(owner, new State(dimension, Set.copyOf(positions)));
|
||||
}
|
||||
|
||||
public State remove(UUID owner) {
|
||||
return carpets.remove(owner);
|
||||
}
|
||||
|
||||
public Set<UUID> owners() {
|
||||
return Set.copyOf(carpets.keySet());
|
||||
}
|
||||
|
||||
public boolean trackedByAnother(UUID owner, ResourceKey<Level> dimension, BlockPos pos) {
|
||||
return carpets.entrySet().stream().anyMatch(entry -> !entry.getKey().equals(owner)
|
||||
&& entry.getValue().dimension().equals(dimension)
|
||||
&& entry.getValue().positions().contains(pos));
|
||||
}
|
||||
|
||||
public Map<UUID, State> drain() {
|
||||
Map<UUID, State> copy = Map.copyOf(carpets);
|
||||
carpets.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
public record State(ResourceKey<Level> dimension, Set<BlockPos> positions) {
|
||||
public State {
|
||||
positions = Set.copyOf(positions);
|
||||
}
|
||||
|
||||
private static State empty(ResourceKey<Level> dimension) {
|
||||
return new State(dimension, Set.of());
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItItems;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Read-preserving conversion from the historical Totem identity to the canonical carpet. */
|
||||
public final class MagicCarpetMigration {
|
||||
private MagicCarpetMigration() {
|
||||
}
|
||||
|
||||
public static boolean isAnyCarpet(ItemStack stack) {
|
||||
return stack != null && !stack.isEmpty()
|
||||
&& (stack.is(ILikeToMoveItItems.MAGIC_CARPET)
|
||||
|| stack.is(ILikeToMoveItItems.LEGACY_NOTCH_TOTEM));
|
||||
}
|
||||
|
||||
public static ItemStack migrateStack(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty() || !stack.is(ILikeToMoveItItems.LEGACY_NOTCH_TOTEM)) return stack;
|
||||
ItemStack migrated = stack.transmuteCopy(ILikeToMoveItItems.MAGIC_CARPET, stack.getCount());
|
||||
SanctuaryItemProvenance.stampIfAbsent(migrated, SanctuaryItemProvenance.LEGACY_NOTCH_TOTEM);
|
||||
return migrated;
|
||||
}
|
||||
|
||||
public static int migrateContainer(Container container) {
|
||||
if (container == null) return 0;
|
||||
int migrated = 0;
|
||||
for (int slot = 0; slot < container.getContainerSize(); slot++) {
|
||||
ItemStack current = container.getItem(slot);
|
||||
ItemStack replacement = migrateStack(current);
|
||||
if (replacement == current) continue;
|
||||
container.setItem(slot, replacement);
|
||||
migrated++;
|
||||
}
|
||||
if (migrated > 0) container.setChanged();
|
||||
return migrated;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.data.ILikeToMoveItData;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Converts online inventories once their historical alias has been decoded. */
|
||||
public final class MagicCarpetMigrationService {
|
||||
private MagicCarpetMigrationService() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> migrate(handler.player));
|
||||
ServerTickEvents.END_SERVER_TICK.register(MagicCarpetMigrationService::tick);
|
||||
}
|
||||
|
||||
private static void tick(MinecraftServer server) {
|
||||
ILikeToMoveItData.get(server);
|
||||
if (server.overworld().getGameTime() % 20L != 0L) return;
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) migrate(player);
|
||||
}
|
||||
|
||||
private static void migrate(ServerPlayer player) {
|
||||
int migrated = MagicCarpetMigration.migrateContainer(player.getInventory());
|
||||
migrated += MagicCarpetMigration.migrateContainer(player.getEnderChestInventory());
|
||||
if (migrated > 0) {
|
||||
ILikeToMoveItMod.LOGGER.info("[Magic Carpet] Migrated {} historical Totem stack(s) for {}.",
|
||||
migrated, player.getGameProfile().name());
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItItems;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.world.SanctuaryAlphaWorld;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.component.FireworkExplosion;
|
||||
import net.minecraft.world.item.component.Fireworks;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
|
||||
/** Idempotently materializes the globally reserved Magic Carpet in Alpha. */
|
||||
public final class NotchCarpetRewardService {
|
||||
private static final int REWARD_SLOT = 13;
|
||||
|
||||
private NotchCarpetRewardService() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ServerTickEvents.END_SERVER_TICK.register(NotchCarpetRewardService::tick);
|
||||
}
|
||||
|
||||
private static void tick(MinecraftServer server) {
|
||||
if (server.overworld().getGameTime() % 20L != 0L) return;
|
||||
var state = SanctuaryNotchMilestone.snapshot(server);
|
||||
if (!state.defeated() || state.initialRewardDelivered()) return;
|
||||
if (!state.initialRewardClaimed() && !SanctuaryNotchMilestone.claimInitialReward(server)) return;
|
||||
ServerLevel alpha = server.getLevel(SanctuaryDimensions.ALPHA);
|
||||
if (alpha == null) return;
|
||||
BlockPos home = SanctuaryAlphaWorld.residence(alpha.getSeed());
|
||||
if (!alpha.isLoaded(home)) return;
|
||||
RewardChest result = ensureRewardChest(alpha, home);
|
||||
if (result == null || !SanctuaryNotchMilestone.completeInitialReward(server)) return;
|
||||
if (result.created()) {
|
||||
launchFirework(alpha, result.pos());
|
||||
server.getPlayerList().broadcastSystemMessage(
|
||||
Component.translatable("message.iliketomoveit.magic_carpet.reward_created"), false);
|
||||
}
|
||||
ILikeToMoveItMod.LOGGER.info(
|
||||
"[Magic Carpet] Completed unique Notch reward at {} (origin=notch_reward, recovered={}).",
|
||||
result.pos().toShortString(), !result.created());
|
||||
}
|
||||
|
||||
private static RewardChest ensureRewardChest(ServerLevel level, BlockPos home) {
|
||||
List<BlockPos> candidates = candidates(home);
|
||||
for (BlockPos pos : candidates) {
|
||||
if (containsInitialReward(level, pos)) return new RewardChest(pos, false);
|
||||
}
|
||||
for (BlockPos pos : candidates) {
|
||||
if (!level.getBlockState(pos).isAir() || !level.getBlockState(pos.above()).isAir()
|
||||
|| level.getBlockState(pos.below()).isAir()) continue;
|
||||
if (!level.setBlock(pos, Blocks.CHEST.defaultBlockState(), Block.UPDATE_ALL)) continue;
|
||||
if (level.getBlockEntity(pos) instanceof ChestBlockEntity chest) {
|
||||
chest.setItem(REWARD_SLOT, rewardStack());
|
||||
chest.setChanged();
|
||||
level.sendBlockUpdated(pos, level.getBlockState(pos), level.getBlockState(pos), Block.UPDATE_ALL);
|
||||
return new RewardChest(pos, true);
|
||||
}
|
||||
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean containsInitialReward(ServerLevel level, BlockPos pos) {
|
||||
if (level.getBlockState(pos).getBlock() != Blocks.CHEST
|
||||
|| !(level.getBlockEntity(pos) instanceof Container container)) return false;
|
||||
for (ItemStack stack : container) {
|
||||
if (stack.is(ILikeToMoveItItems.MAGIC_CARPET)
|
||||
&& SanctuaryItemProvenance.hasOrigin(stack, SanctuaryItemProvenance.NOTCH_REWARD)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ItemStack rewardStack() {
|
||||
ItemStack reward = new ItemStack(ILikeToMoveItItems.MAGIC_CARPET);
|
||||
reward.set(DataComponents.CUSTOM_NAME,
|
||||
Component.translatable("item.iliketomoveit.magic_carpet.notch_reward_name"));
|
||||
SanctuaryItemProvenance.stamp(reward, SanctuaryItemProvenance.NOTCH_REWARD);
|
||||
return reward;
|
||||
}
|
||||
|
||||
private static List<BlockPos> candidates(BlockPos home) {
|
||||
ArrayList<BlockPos> positions = new ArrayList<>();
|
||||
for (int y : new int[] {0, 1, -1, 2}) {
|
||||
for (int radius = 2; radius <= 7; radius++) {
|
||||
for (int x = -radius; x <= radius; x++) for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
BlockPos candidate = home.offset(x, y, z).immutable();
|
||||
if (SanctuaryAlphaWorld.containsHorizontal(candidate)) positions.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(positions);
|
||||
}
|
||||
|
||||
private static void launchFirework(ServerLevel level, BlockPos pos) {
|
||||
ItemStack rocket = new ItemStack(Items.FIREWORK_ROCKET);
|
||||
FireworkExplosion explosion = new FireworkExplosion(FireworkExplosion.Shape.STAR,
|
||||
new IntArrayList(new int[] {0xE8B923, 0xF1F1F1}),
|
||||
new IntArrayList(new int[] {0x9B59B6}), true, true);
|
||||
rocket.set(DataComponents.FIREWORKS, new Fireworks(1, List.of(explosion)));
|
||||
level.addFreshEntity(new FireworkRocketEntity(level, pos.getX() + 0.5D,
|
||||
pos.getY() + 1.2D, pos.getZ() + 0.5D, rocket));
|
||||
}
|
||||
|
||||
private record RewardChest(BlockPos pos, boolean created) {
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,13 @@
|
||||
"itemGroup.iliketomoveit.transport": "I Like To Move It",
|
||||
"item.iliketomoveit.aircraft": "Aircraft",
|
||||
"item.iliketomoveit.magic_cloud": "Magic Cloud",
|
||||
"item.iliketomoveit.magic_carpet": "Magic Carpet (laboratory)",
|
||||
"item.iliketomoveit.magic_carpet": "Magic Carpet",
|
||||
"item.portals.notch_totem": "Notch Totem (legacy)",
|
||||
"item.iliketomoveit.magic_carpet.notch_reward_name": "Notch's Magic Carpet",
|
||||
"item.iliketomoveit.magic_carpet.description": "Hold it to maintain a temporary 3 × 3 platform beneath your feet.",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch": "Unique world reward from Notch",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop": "Replacement purchased from the Shop",
|
||||
"item.iliketomoveit.magic_carpet.origin.legacy": "Converted from the historical Notch Totem",
|
||||
"block.iliketomoveit.waystone": "Waystone",
|
||||
"block.waystone.waystone": "Waystone (legacy)",
|
||||
"entity.iliketomoveit.aircraft": "Aircraft",
|
||||
@@ -11,6 +17,7 @@
|
||||
"container.iliketomoveit.aircraft_engine": "Aircraft Engine",
|
||||
"message.iliketomoveit.magic_carpet.enabled": "Magic Carpet enabled",
|
||||
"message.iliketomoveit.magic_carpet.disabled": "Magic Carpet disabled",
|
||||
"message.iliketomoveit.magic_carpet.reward_created": "Notch's unique Magic Carpet appeared in a chest near his house.",
|
||||
"subtitles.iliketomoveit.magic_cloud.fly": "Magic Cloud flies",
|
||||
"screen.iliketomoveit.waystone.title": "Waystones",
|
||||
"screen.iliketomoveit.waystone.empty": "No other waystone in this dimension",
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
"itemGroup.iliketomoveit.transport": "I Like To Move It",
|
||||
"item.iliketomoveit.aircraft": "Avion",
|
||||
"item.iliketomoveit.magic_cloud": "Nuage magique",
|
||||
"item.iliketomoveit.magic_carpet": "Tapis magique (laboratoire)",
|
||||
"item.iliketomoveit.magic_carpet": "Tapis magique",
|
||||
"item.portals.notch_totem": "Totem de Notch (hérité)",
|
||||
"item.iliketomoveit.magic_carpet.notch_reward_name": "Tapis magique de Notch",
|
||||
"item.iliketomoveit.magic_carpet.description": "Tenu en main, il maintient une plateforme temporaire 3 × 3 sous vos pieds.",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch": "Récompense mondiale unique de Notch",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop": "Remplacement acheté au Shop",
|
||||
"item.iliketomoveit.magic_carpet.origin.legacy": "Converti depuis le Totem de Notch historique",
|
||||
"block.iliketomoveit.waystone": "Pierre de voyage",
|
||||
"block.waystone.waystone": "Pierre de voyage (héritée)",
|
||||
"entity.iliketomoveit.aircraft": "Avion",
|
||||
@@ -11,6 +17,7 @@
|
||||
"container.iliketomoveit.aircraft_engine": "Moteur de l'avion",
|
||||
"message.iliketomoveit.magic_carpet.enabled": "Tapis magique activé",
|
||||
"message.iliketomoveit.magic_carpet.disabled": "Tapis magique désactivé",
|
||||
"message.iliketomoveit.magic_carpet.reward_created": "Le Tapis magique unique de Notch est apparu dans un coffre près de sa maison.",
|
||||
"subtitles.iliketomoveit.magic_cloud.fly": "Le nuage magique vole",
|
||||
"screen.iliketomoveit.waystone.title": "Pierres de voyage",
|
||||
"screen.iliketomoveit.waystone.empty": "Aucune autre pierre dans cette dimension",
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
"itemGroup.iliketomoveit.transport": "I Like To Move It",
|
||||
"item.iliketomoveit.aircraft": "Самолёт",
|
||||
"item.iliketomoveit.magic_cloud": "Волшебное облако",
|
||||
"item.iliketomoveit.magic_carpet": "Ковёр-самолёт (лаборатория)",
|
||||
"item.iliketomoveit.magic_carpet": "Ковёр-самолёт",
|
||||
"item.portals.notch_totem": "Тотем Нотча (старый)",
|
||||
"item.iliketomoveit.magic_carpet.notch_reward_name": "Ковёр-самолёт Нотча",
|
||||
"item.iliketomoveit.magic_carpet.description": "В руке поддерживает временную платформу 3 × 3 под ногами.",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch": "Уникальная мировая награда за Нотча",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop": "Замена, купленная в Магазине",
|
||||
"item.iliketomoveit.magic_carpet.origin.legacy": "Преобразован из исторического Тотема Нотча",
|
||||
"block.iliketomoveit.waystone": "Камень пути",
|
||||
"block.waystone.waystone": "Камень пути (старый)",
|
||||
"entity.iliketomoveit.aircraft": "Самолёт",
|
||||
@@ -11,6 +17,7 @@
|
||||
"container.iliketomoveit.aircraft_engine": "Двигатель самолёта",
|
||||
"message.iliketomoveit.magic_carpet.enabled": "Ковёр-самолёт включён",
|
||||
"message.iliketomoveit.magic_carpet.disabled": "Ковёр-самолёт выключен",
|
||||
"message.iliketomoveit.magic_carpet.reward_created": "Уникальный ковёр-самолёт Нотча появился в сундуке возле его дома.",
|
||||
"subtitles.iliketomoveit.magic_cloud.fly": "Волшебное облако летит",
|
||||
"screen.iliketomoveit.waystone.title": "Камни пути",
|
||||
"screen.iliketomoveit.waystone.empty": "В этом измерении нет других камней",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "iliketomoveit:item/magic_carpet"
|
||||
}
|
||||
}
|
||||
@@ -33,13 +33,13 @@
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"anotherworld": ">=0.0.0-alpha.46",
|
||||
"sanctuary": ">=0.0.0-alpha.21"
|
||||
"sanctuary": ">=0.0.0-alpha.127"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "iliketomoveit",
|
||||
"data_version": 3,
|
||||
"data_version": 4,
|
||||
"network_protocol": 5,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"package": "fr.koka99cab.sanctuary26.iliketomoveit.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"HappyGhastMixin"
|
||||
"HappyGhastMixin",
|
||||
"MagicCarpetContainerMixin",
|
||||
"MagicCarpetItemEntityMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Regression checks for overlap, world changes and shutdown cleanup ownership. */
|
||||
public final class MagicCarpetLedgerSmoke {
|
||||
private MagicCarpetLedgerSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
MagicCarpetLedger ledger = new MagicCarpetLedger();
|
||||
UUID first = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||
UUID second = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||
ResourceKey<Level> firstWorld = ResourceKey.create(Registries.DIMENSION,
|
||||
Identifier.fromNamespaceAndPath("test", "first"));
|
||||
ResourceKey<Level> secondWorld = ResourceKey.create(Registries.DIMENSION,
|
||||
Identifier.fromNamespaceAndPath("test", "second"));
|
||||
BlockPos shared = new BlockPos(4, 70, 4);
|
||||
ledger.track(first, firstWorld, Set.of(shared));
|
||||
ledger.track(second, firstWorld, Set.of(shared));
|
||||
if (!ledger.trackedByAnother(first, firstWorld, shared)) {
|
||||
throw new AssertionError("Overlapping carpets lost their second owner");
|
||||
}
|
||||
ledger.remove(first);
|
||||
if (ledger.trackedByAnother(second, firstWorld, shared)) {
|
||||
throw new AssertionError("Removed owner still controls the shared glass");
|
||||
}
|
||||
ledger.track(second, secondWorld, Set.of(new BlockPos(0, 64, 0)));
|
||||
if (!ledger.state(second, secondWorld).dimension().equals(secondWorld)
|
||||
|| !ledger.state(second, firstWorld).dimension().equals(secondWorld)) {
|
||||
throw new AssertionError("Dimension change did not replace the tracked world");
|
||||
}
|
||||
Map<UUID, MagicCarpetLedger.State> shutdown = ledger.drain();
|
||||
if (shutdown.size() != 1 || !ledger.owners().isEmpty()) {
|
||||
throw new AssertionError("Server shutdown did not atomically drain temporary platforms");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public final class OnlyFunMod implements ModInitializer {
|
||||
OnlyFunCommands.register();
|
||||
AbjectionActions.initialize();
|
||||
AbjectionCommands.register();
|
||||
LOGGER.info("[{}] Loot generator, natural Lucky Blocks, mob lootboxes and chicken rules ready (data v{}, network v{}).",
|
||||
LOGGER.info("[{}] Loot generator, boss-gated Shop, natural Lucky Blocks, mob lootboxes and chicken rules ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, OnlyFunNetworking.PROTOCOL_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-17
@@ -1,29 +1,22 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.shop;
|
||||
|
||||
import java.util.Objects;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.progression.SanctuaryWorldMilestones;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Keeps exceptional boss rewards out of the Shop until their future boss owner unlocks them. */
|
||||
/** Keeps exceptional boss rewards out of the Shop until their public world milestone is reached. */
|
||||
public final class OnlyFunBossShopGate {
|
||||
public static final Identifier MAGIC_CLOUD = Identifier.fromNamespaceAndPath("iliketomoveit", "magic_cloud");
|
||||
public static final Identifier MAGIC_CARPET = Identifier.fromNamespaceAndPath("iliketomoveit", "magic_carpet");
|
||||
private static final Set<Identifier> BOSS_REWARDS = Set.of(MAGIC_CLOUD, MAGIC_CARPET);
|
||||
private static final BossUnlockResolver LOCKED = (server, reward) -> false;
|
||||
private static volatile BossUnlockResolver resolver = LOCKED;
|
||||
private static final Map<Identifier, Identifier> IMPLEMENTED_MILESTONES = Map.of(
|
||||
MAGIC_CARPET, SanctuaryNotchMilestone.NOTCH_DEFEATED);
|
||||
|
||||
private OnlyFunBossShopGate() {}
|
||||
|
||||
/** Called later by the module that owns the bosses and their persistent defeat state. */
|
||||
public static synchronized void registerUnlockResolver(BossUnlockResolver value) {
|
||||
Objects.requireNonNull(value, "value");
|
||||
if (resolver != LOCKED && resolver != value) {
|
||||
throw new IllegalStateException("A boss Shop unlock resolver is already registered");
|
||||
}
|
||||
resolver = value;
|
||||
}
|
||||
|
||||
public static boolean requiresBoss(String itemId) {
|
||||
Identifier id = Identifier.tryParse(itemId);
|
||||
return id != null && BOSS_REWARDS.contains(id);
|
||||
@@ -31,11 +24,13 @@ public final class OnlyFunBossShopGate {
|
||||
|
||||
public static boolean isAvailable(MinecraftServer server, String itemId) {
|
||||
Identifier id = Identifier.tryParse(itemId);
|
||||
return id == null || !BOSS_REWARDS.contains(id) || resolver.isUnlocked(server, id);
|
||||
if (id == null || !BOSS_REWARDS.contains(id)) return true;
|
||||
Identifier milestone = requiredMilestone(id);
|
||||
return milestone != null && SanctuaryWorldMilestones.isReached(server, milestone);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface BossUnlockResolver {
|
||||
boolean isUnlocked(MinecraftServer server, Identifier rewardItem);
|
||||
/** Returns null while the corresponding boss contract is not implemented. */
|
||||
public static Identifier requiredMilestone(Identifier rewardItem) {
|
||||
return IMPLEMENTED_MILESTONES.get(rewardItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"minecraft": "~26.2",
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"iliketomoveit": "*",
|
||||
"sanctuary": "*"
|
||||
"iliketomoveit": ">=0.0.0-alpha.17",
|
||||
"sanctuary": ">=0.0.0-alpha.127"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
|
||||
+7
-6
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.onlyfun.shop;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.policy.OnlyFunItemPolicy;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopOffer;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopOfferCatalog;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -94,12 +95,12 @@ public final class OnlyFunShopCatalogSmoke {
|
||||
"Magic Cloud must remain boss-gated");
|
||||
require(!OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_carpet")
|
||||
&& !OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_cloud"),
|
||||
"Boss rewards must be unavailable while their bosses do not exist");
|
||||
OnlyFunBossShopGate.registerUnlockResolver((server, reward) ->
|
||||
reward.equals(OnlyFunBossShopGate.MAGIC_CARPET));
|
||||
require(OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_carpet")
|
||||
&& !OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_cloud"),
|
||||
"Each future boss must unlock only its corresponding reward");
|
||||
"Boss rewards must reject lookups without authoritative server state");
|
||||
require(SanctuaryNotchMilestone.NOTCH_DEFEATED.equals(
|
||||
OnlyFunBossShopGate.requiredMilestone(OnlyFunBossShopGate.MAGIC_CARPET)),
|
||||
"Magic Carpet must resolve the public Notch milestone");
|
||||
require(OnlyFunBossShopGate.requiredMilestone(OnlyFunBossShopGate.MAGIC_CLOUD) == null,
|
||||
"Magic Cloud must stay locked until its own boss is implemented");
|
||||
|
||||
for (String protectedId : List.of(
|
||||
"anotherworld:ruby", "anotherworld:ruby_block", "anotherworld:deepslate_ruby_ore",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "unreleased",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.223",
|
||||
"modules": {
|
||||
"iliketomoveit": "0.0.0-alpha.16",
|
||||
"onlyfun": "0.0.0-alpha.25",
|
||||
"sanctuary": "0.0.0-alpha.126"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.223",
|
||||
"modules": {
|
||||
"iliketomoveit": "0.0.0-alpha.17",
|
||||
"onlyfun": "0.0.0-alpha.26",
|
||||
"sanctuary": "0.0.0-alpha.127"
|
||||
}
|
||||
},
|
||||
"magic_carpet": {
|
||||
"owner": "iliketomoveit",
|
||||
"canonical_item": "iliketomoveit:magic_carpet",
|
||||
"legacy_item": "portals:notch_totem",
|
||||
"legacy_alias_hidden": true,
|
||||
"legacy_components_preserved": true,
|
||||
"legacy_custom_name_preserved": true,
|
||||
"legacy_locations": ["player_inventory", "container", "item_entity"],
|
||||
"one_legacy_stack_becomes_one_canonical_stack": true,
|
||||
"new_worlds_distribute_legacy_item": false,
|
||||
"temporary_platform": {
|
||||
"size": [3, 1, 3],
|
||||
"block": "minecraft:glass",
|
||||
"air_only_placement": true,
|
||||
"creative_flight_granted": false,
|
||||
"overlap_owner_tracking": true,
|
||||
"cleanup": ["deactivation", "disconnect", "dimension_change", "server_stop"]
|
||||
}
|
||||
},
|
||||
"notch_reward": {
|
||||
"milestone_owner": "sanctuary",
|
||||
"milestone": "sanctuary:notch_defeated",
|
||||
"reservation": "initial_reward_claimed",
|
||||
"completion": "initial_reward_delivered",
|
||||
"delivery_owner": "iliketomoveit",
|
||||
"delivery": "deterministic_chest_near_notch_residence",
|
||||
"firework_signal": true,
|
||||
"idempotent_recovery": true,
|
||||
"maximum_canonical_initial_rewards": 1,
|
||||
"provenance": "sanctuary:notch_reward"
|
||||
},
|
||||
"shop_replacement": {
|
||||
"catalog_owner": "onlyfun",
|
||||
"transaction_owner": "sanctuary",
|
||||
"unlock_resolver": "SanctuaryWorldMilestones",
|
||||
"hidden_before_notch": true,
|
||||
"server_authoritative_gate": true,
|
||||
"configured_ruby_price": 1536,
|
||||
"configured_sapphire_price": 128,
|
||||
"daily_rotation_rules_preserved": true,
|
||||
"mailbox_delivery": true,
|
||||
"recreates_initial_reward": false,
|
||||
"provenance": "sanctuary:shop_purchase",
|
||||
"replacement_logged": true
|
||||
},
|
||||
"compatibility": {
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"pack_release_updated": false,
|
||||
"iliketomoveit_data_version": 4,
|
||||
"iliketomoveit_network_protocol": 5,
|
||||
"onlyfun_data_version": 2,
|
||||
"onlyfun_network_protocol": 3,
|
||||
"sanctuary_data_version": 13,
|
||||
"sanctuary_notch_state_data_version": 2,
|
||||
"sanctuary_network_protocol": 22
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -49,5 +49,15 @@ Ouch possède les attaques et les dégâts ; seule l'explosion de transformation
|
||||
peut casser des blocs. La victoire est enregistrée une seule fois dans les
|
||||
données globales Sanctuary, même après redémarrage ou restauration de l'entité.
|
||||
Les participants reçoivent le jalon `sanctuary:notch_defeated` et un marqueur
|
||||
de boss dans leur Atlas si Alpha y était déjà découverte. Le drapeau de
|
||||
récompense initiale reste séparé et monotone pour la future étape CARPET-01.
|
||||
de boss dans leur Atlas si Alpha y était déjà découverte. Sanctuary réserve
|
||||
ensuite une récompense mondiale unique : I Like To Move It place le
|
||||
`iliketomoveit:magic_carpet` dans un coffre près de la maison et le signale par
|
||||
un feu d'artifice. La réservation et la livraison sont persistées séparément
|
||||
afin qu'une reprise serveur termine le coffre sans créer un second tapis.
|
||||
|
||||
L'ancien `portals:notch_totem` reste lisible et devient un Magic Carpet en
|
||||
conservant ses composants et son nom. Après la victoire, Only Fun autorise
|
||||
l'offre exceptionnelle dans la rotation normale du Shop ; les remplacements
|
||||
sont payés et livrés par la Mailbox sans recréer la récompense initiale. Le
|
||||
contrat détaillé est dans
|
||||
[`iliketomoveit/MAGIC_CARPET.md`](../iliketomoveit/MAGIC_CARPET.md).
|
||||
|
||||
+13
-13
@@ -175,7 +175,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| !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")
|
||||
@@ -1184,13 +1184,13 @@ tasks.register("verifySanctuary") {
|
||||
}
|
||||
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (manifest.custom?.sanctuary26?.data_version != 12
|
||||
if (manifest.custom?.sanctuary26?.data_version != 13
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 22
|
||||
|| 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 v11, protocol 22, JEI and the client entrypoint")
|
||||
throw new GradleException("Sanctuary must expose data v13, protocol 22, 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"))
|
||||
@@ -2891,7 +2891,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| 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
|
||||
@@ -3057,7 +3057,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| 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
|
||||
@@ -3095,7 +3095,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| 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]
|
||||
@@ -3126,7 +3126,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.195"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|
||||
|| migration.compatibility?.sanctuary_data_version != 11
|
||||
@@ -3157,7 +3157,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|
||||
|| migration.mailbox?.id != "sanctuary:delivery_box"
|
||||
|| migration.mailbox?.horizontal_facing != true
|
||||
@@ -3185,7 +3185,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| !transfer.contains("shouldAppendOverflow")
|
||||
|| !transfer.contains("isVanillaStorageRange")
|
||||
|| !transfer.contains("hotbarStart")
|
||||
@@ -3231,7 +3231,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|
||||
|| migration.quest_requests?.save_key != "quest_requests"
|
||||
|| migration.quest_requests?.data_version != 1
|
||||
@@ -3289,7 +3289,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| 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"
|
||||
@@ -3358,7 +3358,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| 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"
|
||||
@@ -3430,7 +3430,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.126"
|
||||
if (project.version.toString() != "0.0.0-alpha.127"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.122"
|
||||
|| migration.alpha_world?.dimension != "sanctuary:alpha"
|
||||
|| migration.alpha_world?.legacy_dimension != "portals:alpha"
|
||||
|
||||
@@ -34,7 +34,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 = 12;
|
||||
public static final int CURRENT_DATA_VERSION = 13;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public SanctuaryMod() {
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
|
||||
/** Stable item metadata used to distinguish boss rewards, migrations and Shop replacements. */
|
||||
public final class SanctuaryItemProvenance {
|
||||
public static final Identifier NOTCH_REWARD = SanctuaryMod.id("notch_reward");
|
||||
public static final Identifier SHOP_PURCHASE = SanctuaryMod.id("shop_purchase");
|
||||
public static final Identifier LEGACY_NOTCH_TOTEM = SanctuaryMod.id("legacy_notch_totem");
|
||||
private static final String ORIGIN_KEY = "sanctuary_origin";
|
||||
private static final String VERSION_KEY = "sanctuary_origin_version";
|
||||
|
||||
private SanctuaryItemProvenance() {
|
||||
}
|
||||
|
||||
public static void stamp(ItemStack stack, Identifier origin) {
|
||||
if (stack == null || stack.isEmpty() || origin == null) return;
|
||||
CustomData.update(DataComponents.CUSTOM_DATA, stack, tag -> {
|
||||
tag.putString(ORIGIN_KEY, origin.toString());
|
||||
tag.putInt(VERSION_KEY, 1);
|
||||
});
|
||||
}
|
||||
|
||||
public static void stampIfAbsent(ItemStack stack, Identifier origin) {
|
||||
if (origin(stack) == null) stamp(stack, origin);
|
||||
}
|
||||
|
||||
public static Identifier origin(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty()) return null;
|
||||
CustomData data = stack.get(DataComponents.CUSTOM_DATA);
|
||||
if (data == null) return null;
|
||||
return Identifier.tryParse(data.copyTag().getStringOr(ORIGIN_KEY, ""));
|
||||
}
|
||||
|
||||
public static boolean hasOrigin(ItemStack stack, Identifier origin) {
|
||||
return origin != null && origin.equals(origin(stack));
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -57,6 +57,11 @@ public final class SanctuaryNotchMilestone {
|
||||
return NotchVictoryData.get(server).claimInitialReward();
|
||||
}
|
||||
|
||||
/** Completes the reservation only after the physical reward chest exists. */
|
||||
public static boolean completeInitialReward(MinecraftServer server) {
|
||||
return NotchVictoryData.get(server).completeInitialReward();
|
||||
}
|
||||
|
||||
private static VictoryResult record(ServerLevel alphaLevel, List<UUID> participants,
|
||||
boolean rewardAlreadyClaimed, boolean announce) {
|
||||
NotchVictoryData data = NotchVictoryData.get(alphaLevel.getServer());
|
||||
@@ -73,7 +78,8 @@ public final class SanctuaryNotchMilestone {
|
||||
alphaLevel.getServer().getPlayerList().broadcastSystemMessage(
|
||||
Component.translatable("message.sanctuary.notch_defeated").withStyle(ChatFormatting.GOLD), false);
|
||||
}
|
||||
return new VictoryResult(newlyDefeated, state.participants(), state.initialRewardClaimed());
|
||||
return new VictoryResult(newlyDefeated, state.participants(), state.initialRewardClaimed(),
|
||||
state.initialRewardDelivered());
|
||||
}
|
||||
|
||||
private static List<UUID> sanitizeParticipants(ServerLevel level, Collection<UUID> candidates) {
|
||||
@@ -91,7 +97,7 @@ public final class SanctuaryNotchMilestone {
|
||||
}
|
||||
|
||||
public record VictoryResult(boolean newlyDefeated, List<UUID> participants,
|
||||
boolean initialRewardClaimed) {
|
||||
boolean initialRewardClaimed, boolean initialRewardDelivered) {
|
||||
public VictoryResult {
|
||||
participants = List.copyOf(participants);
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.api.progression;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Public, server-authoritative resolver for world-wide progression milestones. */
|
||||
public final class SanctuaryWorldMilestones {
|
||||
private SanctuaryWorldMilestones() {
|
||||
}
|
||||
|
||||
public static boolean isReached(MinecraftServer server, Identifier milestone) {
|
||||
if (server == null || milestone == null) return false;
|
||||
return milestone.equals(SanctuaryNotchMilestone.NOTCH_DEFEATED)
|
||||
&& SanctuaryNotchMilestone.isDefeated(server);
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -10,7 +10,7 @@ import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** SavedData wrapper for the world-wide Notch victory and future unique reward claim. */
|
||||
/** SavedData wrapper for the world-wide Notch victory and two-phase unique reward delivery. */
|
||||
public final class NotchVictoryData extends SavedData {
|
||||
private static final SavedDataType<NotchVictoryData> TYPE = new SavedDataType<>(
|
||||
SanctuaryMod.id("notch_victory"),
|
||||
@@ -57,4 +57,12 @@ public final class NotchVictoryData extends SavedData {
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean completeInitialReward() {
|
||||
NotchVictoryState updated = state.completeInitialReward();
|
||||
if (updated.equals(state)) return false;
|
||||
state = updated;
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+25
-9
@@ -13,34 +13,42 @@ public record NotchVictoryState(
|
||||
boolean defeated,
|
||||
long defeatedAt,
|
||||
List<String> participantIds,
|
||||
boolean initialRewardClaimed
|
||||
boolean initialRewardClaimed,
|
||||
boolean initialRewardDelivered
|
||||
) {
|
||||
public static final int DATA_VERSION = 1;
|
||||
public static final int DATA_VERSION = 2;
|
||||
public static final int MAX_PARTICIPANTS = 16;
|
||||
public static final Codec<NotchVictoryState> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(NotchVictoryState::dataVersion),
|
||||
Codec.INT.optionalFieldOf("data_version", 1).forGetter(NotchVictoryState::dataVersion),
|
||||
Codec.BOOL.optionalFieldOf("notch_defeated", false).forGetter(NotchVictoryState::defeated),
|
||||
Codec.LONG.optionalFieldOf("defeated_at", 0L).forGetter(NotchVictoryState::defeatedAt),
|
||||
Codec.STRING.listOf().optionalFieldOf("participants", List.of()).forGetter(NotchVictoryState::participantIds),
|
||||
Codec.BOOL.optionalFieldOf("initial_reward_claimed", false)
|
||||
.forGetter(NotchVictoryState::initialRewardClaimed)
|
||||
.forGetter(NotchVictoryState::initialRewardClaimed),
|
||||
Codec.BOOL.optionalFieldOf("initial_reward_delivered", false)
|
||||
.forGetter(NotchVictoryState::initialRewardDelivered)
|
||||
).apply(instance, NotchVictoryState::new));
|
||||
|
||||
public NotchVictoryState {
|
||||
int storedVersion = dataVersion;
|
||||
dataVersion = DATA_VERSION;
|
||||
defeatedAt = Math.max(0L, defeatedAt);
|
||||
participantIds = normalizeStrings(participantIds);
|
||||
initialRewardClaimed = defeated && initialRewardClaimed;
|
||||
// Version 1 only knew the claimed flag. Treat those historical claims as
|
||||
// already delivered so migrated worlds never receive a duplicate carpet.
|
||||
initialRewardDelivered = initialRewardClaimed
|
||||
&& (initialRewardDelivered || storedVersion < DATA_VERSION);
|
||||
}
|
||||
|
||||
public static NotchVictoryState empty() {
|
||||
return new NotchVictoryState(DATA_VERSION, false, 0L, List.of(), false);
|
||||
return new NotchVictoryState(DATA_VERSION, false, 0L, List.of(), false, false);
|
||||
}
|
||||
|
||||
public NotchVictoryState defeat(Collection<UUID> participants, long gameTime, boolean rewardAlreadyClaimed) {
|
||||
if (defeated) {
|
||||
return rewardAlreadyClaimed && !initialRewardClaimed
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true)
|
||||
return rewardAlreadyClaimed && !initialRewardDelivered
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true, true)
|
||||
: this;
|
||||
}
|
||||
List<String> ids = participants == null ? List.of() : participants.stream()
|
||||
@@ -48,12 +56,20 @@ public record NotchVictoryState(
|
||||
.map(UUID::toString)
|
||||
.toList();
|
||||
return new NotchVictoryState(DATA_VERSION, true, Math.max(0L, gameTime), ids,
|
||||
rewardAlreadyClaimed);
|
||||
rewardAlreadyClaimed, rewardAlreadyClaimed);
|
||||
}
|
||||
|
||||
/** Reserves the unique reward before the physical chest is created. */
|
||||
public NotchVictoryState claimInitialReward() {
|
||||
return defeated && !initialRewardClaimed
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true)
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true, false)
|
||||
: this;
|
||||
}
|
||||
|
||||
/** Marks the reserved reward delivered after its idempotent chest exists. */
|
||||
public NotchVictoryState completeInitialReward() {
|
||||
return defeated && initialRewardClaimed && !initialRewardDelivered
|
||||
? new NotchVictoryState(DATA_VERSION, true, defeatedAt, participantIds, true, true)
|
||||
: this;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.ShopActionPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.ShopSnapshotPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryProgressionService;
|
||||
@@ -29,6 +30,7 @@ public final class ShopService {
|
||||
public static final boolean OPEN = true;
|
||||
private static final Identifier RUBY_ID = Identifier.fromNamespaceAndPath("anotherworld", "ruby");
|
||||
private static final Identifier SAPPHIRE_ID = Identifier.fromNamespaceAndPath("anotherworld", "sapphire");
|
||||
private static final String MAGIC_CARPET_ID = "iliketomoveit:magic_carpet";
|
||||
private static final int DELIVERY_RADIUS = 256;
|
||||
static final int MINIMUM_DELIVERY_TICKS = 20 * 20;
|
||||
static final int MAXIMUM_DELIVERY_TICKS = 5 * 60 * 20;
|
||||
@@ -118,6 +120,11 @@ public final class ShopService {
|
||||
delaySeconds / 60, delaySeconds % 60));
|
||||
SanctuaryMod.LOGGER.info("[Shop] Scheduled {} x{} for {} in {} ticks.", itemId, parcel.getCount(),
|
||||
player.getGameProfile().name(), delayTicks);
|
||||
if (MAGIC_CARPET_ID.equals(itemId)) {
|
||||
SanctuaryMod.LOGGER.info(
|
||||
"[Shop] Logged Magic Carpet replacement for {} (origin=shop_purchase, reward_recreated=false).",
|
||||
player.getGameProfile().name());
|
||||
}
|
||||
return "purchase_scheduled";
|
||||
}
|
||||
|
||||
@@ -141,6 +148,9 @@ public final class ShopService {
|
||||
Optional<ShopData.DeliveryDestination> destination = data.loadedDeliveryBox(server, pending.playerId());
|
||||
if (destination.isEmpty()) continue;
|
||||
ItemStack parcel = new ItemStack(item, pending.count());
|
||||
if (MAGIC_CARPET_ID.equals(pending.item())) {
|
||||
SanctuaryItemProvenance.stamp(parcel, SanctuaryItemProvenance.SHOP_PURCHASE);
|
||||
}
|
||||
DeliveryBoxBlockEntity box = destination.get().box();
|
||||
if (!box.insertDelivery(parcel)) continue;
|
||||
data.completeDelivery(pending);
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"sanctuary26": {
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "sanctuary",
|
||||
"data_version": 12,
|
||||
"data_version": 13,
|
||||
"network_protocol": 22,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
|
||||
+21
-6
@@ -1,5 +1,6 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.data;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -16,25 +17,39 @@ public final class NotchVictoryStateSmoke {
|
||||
List.of(first, second, first), 1234L, false);
|
||||
if (!defeated.defeated() || defeated.defeatedAt() != 1234L
|
||||
|| !defeated.participants().equals(List.of(first, second))
|
||||
|| defeated.initialRewardClaimed()) {
|
||||
|| defeated.initialRewardClaimed() || defeated.initialRewardDelivered()) {
|
||||
throw new AssertionError("First Notch victory was not normalized");
|
||||
}
|
||||
NotchVictoryState replayed = defeated.defeat(List.of(UUID.randomUUID()), 9999L, false);
|
||||
if (replayed != defeated) throw new AssertionError("World victory is not monotone");
|
||||
NotchVictoryState claimed = defeated.claimInitialReward();
|
||||
if (!claimed.initialRewardClaimed() || claimed.claimInitialReward() != claimed) {
|
||||
if (!claimed.initialRewardClaimed() || claimed.initialRewardDelivered()
|
||||
|| claimed.claimInitialReward() != claimed) {
|
||||
throw new AssertionError("Unique reward claim is not monotone");
|
||||
}
|
||||
NotchVictoryState delivered = claimed.completeInitialReward();
|
||||
if (!delivered.initialRewardDelivered() || delivered.completeInitialReward() != delivered) {
|
||||
throw new AssertionError("Unique reward delivery is not recoverable and monotone");
|
||||
}
|
||||
ArrayList<String> malformed = new ArrayList<>();
|
||||
malformed.add("invalid");
|
||||
for (int index = 0; index < 40; index++) malformed.add(new UUID(0L, index + 10L).toString());
|
||||
NotchVictoryState bounded = new NotchVictoryState(0, true, -5L, malformed, true);
|
||||
NotchVictoryState bounded = new NotchVictoryState(1, true, -5L, malformed, true, false);
|
||||
if (bounded.dataVersion() != NotchVictoryState.DATA_VERSION || bounded.defeatedAt() != 0L
|
||||
|| bounded.participantIds().size() != NotchVictoryState.MAX_PARTICIPANTS) {
|
||||
|| bounded.participantIds().size() != NotchVictoryState.MAX_PARTICIPANTS
|
||||
|| !bounded.initialRewardDelivered()) {
|
||||
throw new AssertionError("Legacy Notch victory normalization is unbounded");
|
||||
}
|
||||
var encoded = NotchVictoryState.CODEC.encodeStart(JsonOps.INSTANCE, claimed).getOrThrow();
|
||||
JsonObject implicitV1 = new JsonObject();
|
||||
implicitV1.addProperty("notch_defeated", true);
|
||||
implicitV1.addProperty("initial_reward_claimed", true);
|
||||
NotchVictoryState implicitLegacyClaim = NotchVictoryState.CODEC
|
||||
.parse(JsonOps.INSTANCE, implicitV1).getOrThrow();
|
||||
if (!implicitLegacyClaim.initialRewardDelivered()) {
|
||||
throw new AssertionError("An omitted v1 data_version duplicated the historical reward");
|
||||
}
|
||||
var encoded = NotchVictoryState.CODEC.encodeStart(JsonOps.INSTANCE, delivered).getOrThrow();
|
||||
NotchVictoryState decoded = NotchVictoryState.CODEC.parse(JsonOps.INSTANCE, encoded).getOrThrow();
|
||||
if (!decoded.equals(claimed)) throw new AssertionError("Notch victory codec did not round-trip");
|
||||
if (!decoded.equals(delivered)) throw new AssertionError("Notch victory codec did not round-trip");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user