From bf09bdfa78303aef5f637ed098074684f84a24e3 Mon Sep 17 00:00:00 2001 From: koka Date: Sat, 29 Aug 2026 14:31:20 +0200 Subject: [PATCH] feat(sanctuary): ouvrir le Black Market au clic molette --- CHANGELOG.md | 4 +- build.gradle | 24 ++++---- gradle.properties | 2 +- sanctuary/BLACK_MARKET.md | 11 +++- sanctuary/build.gradle | 56 ++++++++++++++----- .../sanctuary/client/SanctuaryClient.java | 12 ++++ .../mixin/client/MinecraftMixin.java | 7 +++ .../network/BlackMarketInteractPayload.java | 27 +++++++++ .../network/SanctuaryNetworking.java | 5 +- .../shop/BlackMarketInteractionRules.java | 16 ++++++ .../sanctuary/shop/BlackMarketService.java | 21 ++++++- sanctuary/src/main/resources/fabric.mod.json | 2 +- .../sanctuary/shop/BlackMarketModelSmoke.java | 13 +++++ 13 files changed, 163 insertions(+), 37 deletions(-) create mode 100644 sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/BlackMarketInteractPayload.java create mode 100644 sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketInteractionRules.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e7ab8d..d17fae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,15 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve ## Non distribué -Modules modifiés : **I Like To Move It `0.0.0-alpha.15`** et **It's Alive `0.0.0-alpha.35`**. Le pack distribué reste en `26.2.0-alpha.209` jusqu’à la prochaine release. +Modules modifiés : **I Like To Move It `0.0.0-alpha.15`**, **It's Alive `0.0.0-alpha.35`** et **Sanctuary `0.0.0-alpha.118`**. Le pack distribué reste en `26.2.0-alpha.209` jusqu’à la prochaine release. - double la vitesse de croisière du Happy Ghast lorsqu’un joueur le pilote, sans accélérer ses déplacements autonomes ; - conserve les données v3 et le protocole réseau 5, sans migration de sauvegarde ni nouvel ID. - transforme le Codex Canaplia en « Livre interdit » aux pages noires, sans page d'accueil, et fait découvrir une entrée aléatoire encore inconnue à chaque page consommée ; - garde les découvertes du Livre interdit personnelles en main et partage temporairement celles du propriétaire uniquement lorsque le livre est posé sur un pupitre ; - conserve les IDs et les anciennes données partagées, ainsi que les données v3 et le protocole réseau 4 d'It's Alive, sans migration de sauvegarde. +- rétablit l'accès au Black Market par clic molette sur un panneau, avec une intention vide et une nouvelle validation complète de la cible côté serveur ; +- conserve les données Sanctuary v11 et porte son protocole réseau de 21 à 22, sans nouvel ID persistant. ## `26.2.0-alpha.209` diff --git a/build.gradle b/build.gradle index f1b0be3..f88db74 100644 --- a/build.gradle +++ b/build.gradle @@ -3429,7 +3429,7 @@ tasks.register("verifyProgressivePauseMenuRelease") { "sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))] } if (rootProject.pack_version != "26.2.0-alpha.209" - || rootProject.sanctuary_version != "0.0.0-alpha.117" + || rootProject.sanctuary_version != "0.0.0-alpha.118" || rootProject.onlyfun_version != "0.0.0-alpha.20" || 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"] @@ -4128,7 +4128,7 @@ tasks.register("verifyShopPricingLayoutRelease") { def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json")) if (rootProject.pack_version != "26.2.0-alpha.209" || rootProject.onlyfun_version != "0.0.0-alpha.20" - || rootProject.sanctuary_version != "0.0.0-alpha.117" + || rootProject.sanctuary_version != "0.0.0-alpha.118" || 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" @@ -4222,7 +4222,7 @@ tasks.register("verifyShopCsvBossGatesRelease") { def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text if (rootProject.pack_version != "26.2.0-alpha.209" || rootProject.onlyfun_version != "0.0.0-alpha.20" - || rootProject.sanctuary_version != "0.0.0-alpha.117" + || rootProject.sanctuary_version != "0.0.0-alpha.118" || 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"] @@ -4314,7 +4314,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.209" || rootProject.itsalive_version != "0.0.0-alpha.35" - || rootProject.sanctuary_version != "0.0.0-alpha.117" + || rootProject.sanctuary_version != "0.0.0-alpha.118" || 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"] @@ -4331,7 +4331,7 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") { || migration.distribution?.jei?.prism_source != "30.24.0.176" || migration.distribution?.jei?.target != "30.26.0.186" || migration.compatibility?.sanctuary_target_network_protocol != 19 - || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21 + || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22 || release.pack_version != "26.2.0-alpha.209" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack" || prism.versionId != "26.2.0-alpha.209" @@ -4414,7 +4414,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.209" || rootProject.itsalive_version != "0.0.0-alpha.35" - || rootProject.sanctuary_version != "0.0.0-alpha.117" + || rootProject.sanctuary_version != "0.0.0-alpha.118" || 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" @@ -4425,7 +4425,7 @@ tasks.register("verifyQuestRequestContractsRelease") { || migration.quest_request_contracts?.right_click_shows_details_before_claim != true || migration.quest_request_contracts?.chat_accept_revalidates_distance_owner_sign_and_claim != true || migration.compatibility?.sanctuary_target_network_protocol != 20 - || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21 + || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22 || release.pack_version != "26.2.0-alpha.209" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack" || prism.versionId != "26.2.0-alpha.209" @@ -4534,7 +4534,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.209" - || rootProject.sanctuary_version != "0.0.0-alpha.117" + || rootProject.sanctuary_version != "0.0.0-alpha.118" || 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" @@ -4547,7 +4547,7 @@ tasks.register("verifyQuestRequestEscrowRelease") { || migration.quest_request_escrow?.reward_delivery_to_claimant_mailbox != true || migration.compatibility?.sanctuary_target_network_protocol != 21 || sanctuaryManifest.custom?.sanctuary26?.data_version != 11 - || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21 + || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22 || release.pack_version != "26.2.0-alpha.209" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack" || prism.versionId != "26.2.0-alpha.209" @@ -4591,7 +4591,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") { || rootProject.anotherworld_version != "0.0.0-alpha.55" || rootProject.itsalive_version != "0.0.0-alpha.35" || rootProject.iliketomoveit_version != "0.0.0-alpha.15" - || rootProject.sanctuary_version != "0.0.0-alpha.117" + || rootProject.sanctuary_version != "0.0.0-alpha.118" || migration.source?.pack_version != "26.2.0-alpha.201" || migration.source?.modules != [ itsalive: "0.0.0-alpha.30", @@ -4626,7 +4626,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") { || moveItManifest.custom?.sanctuary26?.data_version != 3 || moveItManifest.custom?.sanctuary26?.network_protocol != 5 || sanctuaryManifest.custom?.sanctuary26?.data_version != 11 - || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21 + || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22 || release.pack_version != "26.2.0-alpha.209" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.209.mrpack" || prism.versionId != "26.2.0-alpha.209" @@ -4749,7 +4749,7 @@ tasks.register("verifySafeVaultFeature") { }.keySet() if (rootProject.pack_version != "26.2.0-alpha.209" || rootProject.anotherworld_version != "0.0.0-alpha.55" - || rootProject.sanctuary_version != "0.0.0-alpha.117" + || rootProject.sanctuary_version != "0.0.0-alpha.118" || migration.schema_version != 1 || migration.source?.pack_version != "26.2.0-alpha.201" || migration.target?.pack_version != "26.2.0-alpha.202" diff --git a/gradle.properties b/gradle.properties index a6f07d9..edd214e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,7 +28,7 @@ iliketomoveit_version=0.0.0-alpha.15 iliketomoveit_lifecycle=active onlyfun_version=0.0.0-alpha.20 onlyfun_lifecycle=active -sanctuary_version=0.0.0-alpha.117 +sanctuary_version=0.0.0-alpha.118 sanctuary_lifecycle=active pack_version=26.2.0-alpha.209 maven_group=fr.koka99cab.sanctuary26 diff --git a/sanctuary/BLACK_MARKET.md b/sanctuary/BLACK_MARKET.md index c64ddef..e131339 100644 --- a/sanctuary/BLACK_MARKET.md +++ b/sanctuary/BLACK_MARKET.md @@ -9,18 +9,23 @@ n'importe aucune donnée du Shop. ## Utilisation 1. Le vendeur pose une Mailbox dans sa dimension et reste dans un rayon de 256 blocs. -2. Il tient le stack à vendre, s'accroupit et fait clic droit sur un panneau libre. +2. Il tient le stack à vendre et fait un clic molette sur un panneau libre. L'ancien + raccourci accroupi + clic droit reste accepté pour les habitudes existantes. 3. L'éditeur du panneau choisit quantité, prix libre et devise (`ruby` ou `sapphire`). -4. Un clic droit du vendeur rouvre l'éditeur. Il peut modifier prix, devise et quantité. +4. Un clic molette du vendeur rouvre l'éditeur. Il peut modifier prix, devise et quantité. Pour augmenter la quantité, il doit tenir des objets aux composants strictement identiques. La diminution retourne par colis. Shift + clic droit annule l'annonce. -5. Un autre joueur fait clic droit pour acheter. Paiement, retrait de l'annonce, +5. Un autre joueur fait clic molette pour acheter. Paiement, retrait de l'annonce, séquestre et création des colis s'exécutent atomiquement sur le thread serveur. Le panneau affiche l'objet, le prix et le pseudonyme coloré du vendeur. Il est protégé : seuls le vendeur et un opérateur peuvent le casser. Il n'existe aucune limite arbitraire d'annonces et aucune expiration tant que le panneau existe. +Le clic molette remplace uniquement l'action vanilla « choisir le bloc » lorsque la cible +est un panneau. Le client n'envoie ni position, ni objet, ni prix : le serveur refait son +propre rayon, vérifie la portée et résout le panneau avant toute opération. + ## Économie et livraison - Mise en vente : 1 rubis versé à la trésorerie serveur. diff --git a/sanctuary/build.gradle b/sanctuary/build.gradle index 703f71e..a965830 100644 --- a/sanctuary/build.gradle +++ b/sanctuary/build.gradle @@ -92,7 +92,12 @@ tasks.register("verifyBlackMarket") { inputs.file(file("BLACK_MARKET.md")) inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketData.java")) inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketInteractionRules.java")) inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketItemPolicy.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClient.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/MinecraftMixin.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/BlackMarketInteractPayload.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java")) inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java")) inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardData.java")) inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardGenerator.java")) @@ -102,15 +107,36 @@ tasks.register("verifyBlackMarket") { doLast { def data = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketData.java").text def service = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java").text + def interactionRules = file( + "src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketInteractionRules.java").text def policy = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketItemPolicy.java").text + def client = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClient.java").text + def minecraftMixin = file( + "src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/MinecraftMixin.java").text + def middleClickPayload = file( + "src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/BlackMarketInteractPayload.java").text + def network = file( + "src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java").text def shop = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text 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 (!data.contains('SAVE_KEY = "black_market"') || !data.contains("DATA_VERSION = 2") + if (project.version.toString() != "0.0.0-alpha.118" + || !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") || !service.contains("PlayerBlockBreakEvents.BEFORE") || !service.contains('"modify"') + || !service.contains("public static void middleClick(ServerPlayer player)") + || !service.contains("player.pick(") + || !service.contains("player.blockInteractionRange() + 1.0D") + || !interactionRules.contains("middleClick || secondaryUse") + || !client.contains("handleBlackMarketMiddleClick") + || !client.contains("ClientPlayNetworking.send(new BlackMarketInteractPayload())") + || !minecraftMixin.contains('method = "pickBlock"') + || !middleClickPayload.contains('SanctuaryMod.id("c2s/black_market_interact")') + || !middleClickPayload.contains("No client position, listing ID, item or price is trusted") + || !network.contains("PROTOCOL_VERSION = 22") + || !network.contains("BlackMarketService.middleClick(context.player())") || !service.contains("loadedDeliveryBox") || !service.contains("PARCEL_RETENTION_MILLIS") || !policy.contains("DataComponents.CONTAINER") || !policy.contains("DataComponents.BUNDLE_CONTENTS") || !shop.contains("public static final boolean OPEN = true") @@ -1086,12 +1112,12 @@ tasks.register("verifySanctuary") { def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json")) if (manifest.custom?.sanctuary26?.data_version != 11 - || manifest.custom?.sanctuary26?.network_protocol != 21 + || 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 21, JEI and the client entrypoint") + throw new GradleException("Sanctuary must expose data v11, 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")) @@ -2792,7 +2818,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.117" + if (project.version.toString() != "0.0.0-alpha.118" || 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 @@ -2958,7 +2984,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.117" + if (project.version.toString() != "0.0.0-alpha.118" || 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 @@ -2996,7 +3022,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.117" + if (project.version.toString() != "0.0.0-alpha.118" || 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] @@ -3027,7 +3053,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.117" + if (project.version.toString() != "0.0.0-alpha.118" || migration.target?.pack_version != "26.2.0-alpha.195" || migration.target?.modules?.sanctuary != "0.0.0-alpha.111" || migration.compatibility?.sanctuary_data_version != 11 @@ -3058,7 +3084,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.117" + if (project.version.toString() != "0.0.0-alpha.118" || migration.target?.modules?.sanctuary != "0.0.0-alpha.112" || migration.mailbox?.id != "sanctuary:delivery_box" || migration.mailbox?.horizontal_facing != true @@ -3086,7 +3112,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.117" + if (project.version.toString() != "0.0.0-alpha.118" || !transfer.contains("shouldAppendOverflow") || !transfer.contains("isVanillaStorageRange") || !transfer.contains("hotbarStart") @@ -3132,7 +3158,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.117" + if (project.version.toString() != "0.0.0-alpha.118" || migration.target?.modules?.sanctuary != "0.0.0-alpha.113" || migration.quest_requests?.save_key != "quest_requests" || migration.quest_requests?.data_version != 1 @@ -3144,7 +3170,7 @@ tasks.register("verifyQuestRequests") { || !data.contains('SAVE_KEY = "quest_requests"') || !data.contains("DATA_VERSION = 3") || !service.contains("PlayerBlockBreakEvents.BEFORE") || !service.contains("player.blockPosition().distSqr(payload.pos()) > 64.0D") - || !network.contains("PROTOCOL_VERSION = 21") + || !network.contains("PROTOCOL_VERSION = 22") || !network.contains("QuestRequestSubmitPayload.ID") || !network.contains("QuestRequestAcceptPayload.ID") || !registry.contains('SanctuaryMod.id("quest_request_sign")') @@ -3190,7 +3216,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.117" + if (project.version.toString() != "0.0.0-alpha.118" || 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" @@ -3210,7 +3236,7 @@ tasks.register("verifyQuestRequestContracts") { || !service.contains('REWARD_LOAN = "loan"') || !service.contains('PLAYER.equals(targetKind)') || !screen.contains("rewardQuantity") || !screen.contains("killTargetIndex") - || !network.contains("PROTOCOL_VERSION = 21") + || !network.contains("PROTOCOL_VERSION = 22") || standingModel.textures?.all != "sanctuary:block/pink_quest_request_sign" || itemModel.textures?.layer0 != "sanctuary:item/pink_quest_request_sign" || blockTexture == null || blockTexture.width != 32 || blockTexture.height != 32 @@ -3259,7 +3285,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.117" + if (project.version.toString() != "0.0.0-alpha.118" || 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" @@ -3280,7 +3306,7 @@ tasks.register("verifyQuestRequestEscrow") { || !service.contains('"quest_request_reward"') || !service.contains('"quest_request_delivery"') || service.contains("sanctuary_quest_accept") - || !network.contains("PROTOCOL_VERSION = 21") + || !network.contains("PROTOCOL_VERSION = 22") || !network.contains("QuestRequestAcceptPayload.ID") || !network.contains("QuestRequestConfirmPayload.ID") || !acceptPayload.contains("readUtf(36)") diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClient.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClient.java index 6a5806b..04b25f4 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClient.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClient.java @@ -11,6 +11,7 @@ import fr.koka99cab.sanctuary26.sanctuary.network.CelestialSkySnapshotPayload; import fr.koka99cab.sanctuary26.sanctuary.network.ShopSnapshotPayload; import fr.koka99cab.sanctuary26.sanctuary.network.CapeSyncPayload; import fr.koka99cab.sanctuary26.sanctuary.network.BlackMarketEditorPayload; +import fr.koka99cab.sanctuary26.sanctuary.network.BlackMarketInteractPayload; import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestEditorPayload; import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestConfirmPayload; import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryProgressionCosts; @@ -34,6 +35,8 @@ import net.minecraft.network.chat.Component; import net.minecraft.world.SimpleContainer; import net.minecraft.world.inventory.ChestMenu; import net.minecraft.world.inventory.CraftingMenu; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.minecraft.world.phys.BlockHitResult; import org.lwjgl.glfw.GLFW; public final class SanctuaryClient implements ClientModInitializer { @@ -202,6 +205,15 @@ public final class SanctuaryClient implements ClientModInitializer { return switchHotbarRowKey != null && switchHotbarRowKey.matches(event); } + public static boolean handleBlackMarketMiddleClick(Minecraft client) { + if (client.player == null || client.level == null || client.gui.screen() != null + || !(client.hitResult instanceof BlockHitResult hit) + || !(client.level.getBlockEntity(hit.getBlockPos()) instanceof SignBlockEntity) + || !ClientPlayNetworking.canSend(BlackMarketInteractPayload.ID)) return false; + ClientPlayNetworking.send(new BlackMarketInteractPayload()); + return true; + } + private static void openInventorySmokeScreen(Minecraft client) { inventorySmokeScreenOpened = true; inventorySmokeOpenDelay = -1; diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/MinecraftMixin.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/MinecraftMixin.java index 46b2769..16c2913 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/MinecraftMixin.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/MinecraftMixin.java @@ -1,14 +1,21 @@ package fr.koka99cab.sanctuary26.sanctuary.mixin.client; +import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClient; import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; /** Gives the game window the Sanctuary identity while preserving its connection suffix. */ @Mixin(Minecraft.class) public abstract class MinecraftMixin { + @Inject(method = "pickBlock", at = @At("HEAD"), cancellable = true) + private void sanctuary$blackMarketMiddleClick(CallbackInfo callback) { + if (SanctuaryClient.handleBlackMarketMiddleClick((Minecraft) (Object) this)) callback.cancel(); + } + @Inject(method = "createTitle", at = @At("RETURN"), cancellable = true) private void sanctuary$replaceMinecraftWindowTitle(CallbackInfoReturnable callback) { callback.setReturnValue(callback.getReturnValue().replaceFirst("^Minecraft\\*?", "Sanctuary")); diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/BlackMarketInteractPayload.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/BlackMarketInteractPayload.java new file mode 100644 index 0000000..09dcf97 --- /dev/null +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/BlackMarketInteractPayload.java @@ -0,0 +1,27 @@ +package fr.koka99cab.sanctuary26.sanctuary.network; + +import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; + +/** Empty middle-click intent; the server resolves and validates the targeted sign. */ +public record BlackMarketInteractPayload() implements CustomPacketPayload { + public static final Type ID = + new Type<>(SanctuaryMod.id("c2s/black_market_interact")); + public static final StreamCodec CODEC = + CustomPacketPayload.codec(BlackMarketInteractPayload::write, BlackMarketInteractPayload::new); + + private BlackMarketInteractPayload(RegistryFriendlyByteBuf buffer) { + this(); + } + + private void write(RegistryFriendlyByteBuf buffer) { + // No client position, listing ID, item or price is trusted. + } + + @Override + public Type type() { + return ID; + } +} diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java index be41a7a..7e665fb 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java @@ -22,7 +22,7 @@ import net.minecraft.server.level.ServerPlayer; * Networking contract for Sanctuary. Payload IDs must use the module namespace. */ public final class SanctuaryNetworking { - public static final int PROTOCOL_VERSION = 21; + public static final int PROTOCOL_VERSION = 22; private static boolean initialized; private SanctuaryNetworking() { @@ -53,6 +53,7 @@ public final class SanctuaryNetworking { PayloadTypeRegistry.serverboundPlay().register(ShopRequestPayload.ID, ShopRequestPayload.CODEC); PayloadTypeRegistry.serverboundPlay().register(ShopActionPayload.ID, ShopActionPayload.CODEC); PayloadTypeRegistry.serverboundPlay().register(BlackMarketCreatePayload.ID, BlackMarketCreatePayload.CODEC); + PayloadTypeRegistry.serverboundPlay().register(BlackMarketInteractPayload.ID, BlackMarketInteractPayload.CODEC); PayloadTypeRegistry.serverboundPlay().register(QuestRequestSubmitPayload.ID, QuestRequestSubmitPayload.CODEC); PayloadTypeRegistry.serverboundPlay().register(QuestRequestAcceptPayload.ID, QuestRequestAcceptPayload.CODEC); ServerPlayNetworking.registerGlobalReceiver(PurchaseProgressionPayload.ID, (payload, context) -> @@ -83,6 +84,8 @@ public final class SanctuaryNetworking { context.server().execute(() -> ShopService.handle(context.player(), payload))); ServerPlayNetworking.registerGlobalReceiver(BlackMarketCreatePayload.ID, (payload, context) -> context.server().execute(() -> BlackMarketService.create(context.player(), payload))); + ServerPlayNetworking.registerGlobalReceiver(BlackMarketInteractPayload.ID, (payload, context) -> + context.server().execute(() -> BlackMarketService.middleClick(context.player()))); ServerPlayNetworking.registerGlobalReceiver(QuestRequestSubmitPayload.ID, (payload, context) -> context.server().execute(() -> QuestRequestService.submit(context.player(), payload))); ServerPlayNetworking.registerGlobalReceiver(QuestRequestAcceptPayload.ID, (payload, context) -> diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketInteractionRules.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketInteractionRules.java new file mode 100644 index 0000000..a63f53c --- /dev/null +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketInteractionRules.java @@ -0,0 +1,16 @@ +package fr.koka99cab.sanctuary26.sanctuary.shop; + +/** Pure interaction gates shared by the server implementation and smoke tests. */ +final class BlackMarketInteractionRules { + private BlackMarketInteractionRules() { + } + + static boolean acceptsMiddleClick(boolean alive, boolean spectator, + boolean targetsSign, boolean mayInteract) { + return alive && !spectator && targetsSign && mayInteract; + } + + static boolean opensBlankListing(boolean middleClick, boolean secondaryUse, boolean heldItem) { + return heldItem && (middleClick || secondaryUse); + } +} diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java index 0161fa8..bbf233b 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java @@ -75,7 +75,7 @@ public final class BlackMarketService { ? InteractionResult.SUCCESS : InteractionResult.PASS; } return player instanceof ServerPlayer serverPlayer - ? interact(serverPlayer, (ServerLevel) level, hit.getBlockPos(), sign) + ? interact(serverPlayer, (ServerLevel) level, hit.getBlockPos(), sign, false) : InteractionResult.PASS; }); PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> { @@ -101,8 +101,22 @@ public final class BlackMarketService { BlackMarketCommands.register(dispatcher)); } + public static void middleClick(ServerPlayer player) { + net.minecraft.world.phys.HitResult hit = player.pick( + player.blockInteractionRange() + 1.0D, 1.0F, false); + boolean targetsSign = hit instanceof net.minecraft.world.phys.BlockHitResult blockHit + && player.level().getBlockEntity(blockHit.getBlockPos()) instanceof SignBlockEntity; + BlockPos pos = targetsSign + ? ((net.minecraft.world.phys.BlockHitResult) hit).getBlockPos() : BlockPos.ZERO; + boolean mayInteract = targetsSign && player.level().mayInteract(player, pos); + if (!BlackMarketInteractionRules.acceptsMiddleClick( + player.isAlive(), player.isSpectator(), targetsSign, mayInteract)) return; + SignBlockEntity sign = (SignBlockEntity) player.level().getBlockEntity(pos); + interact(player, (ServerLevel) player.level(), pos, sign, true); + } + private static InteractionResult interact(ServerPlayer player, ServerLevel level, BlockPos pos, - SignBlockEntity sign) { + SignBlockEntity sign, boolean middleClick) { BlackMarketData data = BlackMarketData.get(level.getServer()); Optional existing = data.listingAt(dimension(level), pos.asLong()); if (existing.isPresent()) { @@ -115,7 +129,8 @@ public final class BlackMarketService { } return InteractionResult.SUCCESS_SERVER; } - if (!player.isSecondaryUseActive() || player.getMainHandItem().isEmpty()) return InteractionResult.PASS; + if (!BlackMarketInteractionRules.opensBlankListing(middleClick, + player.isSecondaryUseActive(), !player.getMainHandItem().isEmpty())) return InteractionResult.PASS; if (data.isBanned(player.getUUID())) { player.sendSystemMessage(Component.translatable("message.sanctuary.black_market.banned")); return InteractionResult.CONSUME; diff --git a/sanctuary/src/main/resources/fabric.mod.json b/sanctuary/src/main/resources/fabric.mod.json index b7cd986..4c86231 100644 --- a/sanctuary/src/main/resources/fabric.mod.json +++ b/sanctuary/src/main/resources/fabric.mod.json @@ -33,7 +33,7 @@ "system_type": "gameplay-system", "namespace": "sanctuary", "data_version": 11, - "network_protocol": 21, + "network_protocol": 22, "lifecycle": "${lifecycle}", "pack_version": "${pack_version}", "descriptions": { diff --git a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketModelSmoke.java b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketModelSmoke.java index c984996..d7349de 100644 --- a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketModelSmoke.java +++ b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketModelSmoke.java @@ -8,6 +8,19 @@ public final class BlackMarketModelSmoke { private BlackMarketModelSmoke() {} public static void main(String[] args) { + require(BlackMarketInteractionRules.acceptsMiddleClick(true, false, true, true), + "A valid middle-click sign target was rejected"); + require(!BlackMarketInteractionRules.acceptsMiddleClick(false, false, true, true), + "A dead player may middle-click the Black Market"); + require(!BlackMarketInteractionRules.acceptsMiddleClick(true, true, true, true), + "A spectator may middle-click the Black Market"); + require(!BlackMarketInteractionRules.acceptsMiddleClick(true, false, false, true), + "A non-sign target may open the Black Market"); + require(BlackMarketInteractionRules.opensBlankListing(true, false, true), + "Middle-click should create a listing without crouching"); + require(!BlackMarketInteractionRules.opensBlankListing(true, false, false), + "An empty hand should not create a listing"); + UUID seller = UUID.fromString("6c43c35a-3498-41bb-a77e-10c679e635d1"); ItemStack escrow = ItemStack.EMPTY; BlackMarketData data = new BlackMarketData(); -- 2.54.0