feat(sanctuary): rétablir le Black Market au clic molette (#68) #74

Merged
koka merged 2 commits from feature/black-market-middle-click into main 2026-08-29 16:17:00 +00:00
13 changed files with 165 additions and 39 deletions
+3 -1
View File
@@ -8,10 +8,12 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
## Non distribué
Module modifié : **It's Alive `0.0.0-alpha.36`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
Modules modifiés : **It's Alive `0.0.0-alpha.36`** et **Sanctuary `0.0.0-alpha.119`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
- remplace les pages noires du Livre interdit par laffichage clair du livre vanilla ;
- conserve les seize entrées, les découvertes personnelles, le partage sur pupitre, les IDs, les données v3 et le protocole réseau 4.
- 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.210`
+14 -14
View File
@@ -3429,7 +3429,7 @@ tasks.register("verifyProgressivePauseMenuRelease") {
"sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))]
}
if (rootProject.pack_version != "26.2.0-alpha.210"
|| rootProject.sanctuary_version != "0.0.0-alpha.118"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| rootProject.onlyfun_version != "0.0.0-alpha.21"
|| 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.210"
|| rootProject.onlyfun_version != "0.0.0-alpha.21"
|| rootProject.sanctuary_version != "0.0.0-alpha.118"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| 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.210"
|| rootProject.onlyfun_version != "0.0.0-alpha.21"
|| rootProject.sanctuary_version != "0.0.0-alpha.118"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| 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.210"
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|| rootProject.sanctuary_version != "0.0.0-alpha.118"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| 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.210"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|| prism.versionId != "26.2.0-alpha.210"
@@ -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.210"
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|| rootProject.sanctuary_version != "0.0.0-alpha.118"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| 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.210"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|| prism.versionId != "26.2.0-alpha.210"
@@ -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.210"
|| rootProject.sanctuary_version != "0.0.0-alpha.118"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| 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.210"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|| prism.versionId != "26.2.0-alpha.210"
@@ -4591,7 +4591,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|| rootProject.iliketomoveit_version != "0.0.0-alpha.15"
|| rootProject.sanctuary_version != "0.0.0-alpha.118"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| 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.210"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.210.mrpack"
|| prism.versionId != "26.2.0-alpha.210"
@@ -4749,7 +4749,7 @@ tasks.register("verifySafeVaultFeature") {
}.keySet()
if (rootProject.pack_version != "26.2.0-alpha.210"
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|| rootProject.sanctuary_version != "0.0.0-alpha.118"
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| migration.schema_version != 1
|| migration.source?.pack_version != "26.2.0-alpha.201"
|| migration.target?.pack_version != "26.2.0-alpha.202"
@@ -5597,7 +5597,7 @@ tasks.register("verifyAlpha210Release") {
it.path == "mods/jei-26.2-fabric-30.28.0.193.jar"
}
if (rootProject.pack_version != "26.2.0-alpha.210"
|| rootProject.sanctuary_version != expectedTarget.sanctuary
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|| rootProject.onlyfun_version != expectedTarget.onlyfun
|| rootProject.iliketomoveit_version != expectedTarget.iliketomoveit
|| rootProject.itsalive_version != "0.0.0-alpha.36"
@@ -5635,7 +5635,7 @@ tasks.register("verifyAlpha210Release") {
|| migration.distribution?.jei?.modrinth_version_id != "qaCQ7LGO"
|| migration.distribution?.jei?.packwiz_and_prism_synchronized != true
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 11
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 21
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 2
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 3
+1 -1
View File
@@ -28,7 +28,7 @@ iliketomoveit_version=0.0.0-alpha.15
iliketomoveit_lifecycle=active
onlyfun_version=0.0.0-alpha.21
onlyfun_lifecycle=active
sanctuary_version=0.0.0-alpha.118
sanctuary_version=0.0.0-alpha.119
sanctuary_lifecycle=active
pack_version=26.2.0-alpha.210
maven_group=fr.koka99cab.sanctuary26
+8 -3
View File
@@ -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.
+41 -15
View File
@@ -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.119"
|| !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.118"
if (project.version.toString() != "0.0.0-alpha.119"
|| 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.118"
if (project.version.toString() != "0.0.0-alpha.119"
|| 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.118"
if (project.version.toString() != "0.0.0-alpha.119"
|| 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.118"
if (project.version.toString() != "0.0.0-alpha.119"
|| 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.118"
if (project.version.toString() != "0.0.0-alpha.119"
|| 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.118"
if (project.version.toString() != "0.0.0-alpha.119"
|| !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.118"
if (project.version.toString() != "0.0.0-alpha.119"
|| 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.118"
if (project.version.toString() != "0.0.0-alpha.119"
|| 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.118"
if (project.version.toString() != "0.0.0-alpha.119"
|| 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)")
@@ -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;
@@ -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<String> callback) {
callback.setReturnValue(callback.getReturnValue().replaceFirst("^Minecraft\\*?", "Sanctuary"));
@@ -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<BlackMarketInteractPayload> ID =
new Type<>(SanctuaryMod.id("c2s/black_market_interact"));
public static final StreamCodec<RegistryFriendlyByteBuf, BlackMarketInteractPayload> 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<? extends CustomPacketPayload> type() {
return ID;
}
}
@@ -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) ->
@@ -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);
}
}
@@ -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<BlackMarketData.Listing> 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;
+1 -1
View File
@@ -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": {
@@ -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();