diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c678a..c429511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve ## Non distribué -Modules modifiés : **Another World `0.0.0-alpha.57`**, **Only Fun `0.0.0-alpha.26`** et **Sanctuary `0.0.0-alpha.125`**. Le pack public reste temporairement en `26.2.0-alpha.222` jusqu'à la publication. +Modules modifiés : **Another World `0.0.0-alpha.58`**, **I Like To Move It `0.0.0-alpha.17`**, **Only Fun `0.0.0-alpha.26`** et **Sanctuary `0.0.0-alpha.126`**. Le pack public reste temporairement en `26.2.0-alpha.222` jusqu'à la publication. - exige que l'attaquant possède son propre coffre-fort actif avant de pouvoir lancer ou achever le forage d'un autre coffre-fort ; - refuse aussi le crédit du butin côté données si le coffre-fort de l'attaquant disparaît pendant le forage, sans changer les données v4 ni les sauvegardes existantes ; @@ -17,6 +17,9 @@ Modules modifiés : **Another World `0.0.0-alpha.57`**, **Only Fun `0.0.0-alpha. - migre automatiquement les contrats Phantom déjà sauvegardés vers `itsalive:ghost` en conservant leur progression, leur réservation et leur récompense ; les données du panneau de quêtes passent de v1 à v2 tandis que les données Sanctuary v11 et le protocole 22 restent inchangés. - autorise la récompense « livre enchanté » des lootboxes Only Fun à être remise au joueur, tout en la conservant hors du Shop ; les données Only Fun v2 et le protocole 3 restent inchangés. - déclare l’épée en titane dans le contrat vanilla `minecraft:swords`, afin que ses attaques et propriétés d’épée s’appliquent aussi aux créatures aquatiques comme les poulpes. +- permet aux petites et grandes boules de feu lancées par les pouvoirs de compagnons d’allumer un cadre de portail valide, avec les mêmes contrôles serveur et sans explosion destructrice lorsque le tir atteint ce cadre. +- permet aux cisailles en titane de tondre les moutons avec le même butin et les mêmes événements que les cisailles vanilla, tout en conservant leur propriété incassable. +- ajoute une cape blanche personnalisable en la combinant avec n’importe quelle bannière ; sa couleur et jusqu’à 16 motifs sont conservés, synchronisés et rendus avec la physique de cape vanilla. ## `26.2.0-alpha.222` diff --git a/anotherworld/build.gradle b/anotherworld/build.gradle index 38dc52c..c746ff6 100644 --- a/anotherworld/build.gradle +++ b/anotherworld/build.gradle @@ -1957,7 +1957,7 @@ tasks.register("verifyAnotherWorldCatalog") { "src/main/resources/assets/redstoner/textures/item/weather_gauge.png", "src/main/resources/data/redstoner/recipe/weather_gauge.json" ] - if (project.version.toString() != "0.0.0-alpha.57" + if (project.version.toString() != "0.0.0-alpha.58" || sulfurMigration.target?.modules?.anotherworld != "0.0.0-alpha.44" || sulfurMigration.sulfur_caves?.sanctuary?.dripstone_cell_period != 2 || !javaText.contains('Identifier.parse("sanctuary:sulfur_caves")') @@ -3268,6 +3268,35 @@ tasks.named("check") { dependsOn tasks.named("vaultRulesSmoke") } +tasks.register("verifyTitaniumShears") { + group = "verification" + description = "Checks that titanium shears reproduce vanilla sheep shearing despite the hard-coded vanilla item check." + inputs.files( + file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/item/TitaniumShearsItem.java"), + file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldItems.java") + ) + doLast { + def shears = file( + "src/main/java/fr/koka99cab/sanctuary26/anotherworld/item/TitaniumShearsItem.java").text + def registry = file( + "src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldItems.java").text + if (!registry.contains("TitaniumShearsItem::new") + || !shears.contains("class TitaniumShearsItem extends ShearsItem") + || !shears.contains("entity instanceof Sheep sheep") + || !shears.contains("sheep.readyForShearing()") + || !shears.contains("sheep.shear(level, SoundSource.PLAYERS, stack)") + || !shears.contains("sheep.gameEvent(GameEvent.SHEAR, player)") + || !shears.contains("stack.hurtAndBreak(1, player, hand.asEquipmentSlot())") + || !shears.contains("return InteractionResult.SUCCESS_SERVER")) { + throw new GradleException("Titanium shears must reproduce the vanilla sheep shearing contract") + } + } +} + +tasks.named("check") { + dependsOn tasks.named("verifyTitaniumShears") +} + tasks.register("verifyClimateBiomeMixer") { group = "verification" description = "Checks the alpha.189 climate-coordinate biome classification and vanilla-majority shares." @@ -3278,7 +3307,7 @@ tasks.register("verifyClimateBiomeMixer") { "pack/migrations/26.2.0-alpha.188-beta-climate-provinces-to-alpha.189.json")) def mixer = file( "src/main/java/fr/koka99cab/sanctuary26/anotherworld/mixin/MultiNoiseBiomeSourceParameterListPresetMixin.java").text - if (project.version.toString() != "0.0.0-alpha.57" + if (project.version.toString() != "0.0.0-alpha.58" || migration.source?.pack_version != "26.2.0-alpha.188" || migration.target?.pack_version != "26.2.0-alpha.189" || migration.target?.modules?.anotherworld != "0.0.0-alpha.48" diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/item/TitaniumShearsItem.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/item/TitaniumShearsItem.java new file mode 100644 index 0000000..526bbb6 --- /dev/null +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/item/TitaniumShearsItem.java @@ -0,0 +1,38 @@ +package fr.koka99cab.sanctuary26.anotherworld.item; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.animal.sheep.Sheep; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ShearsItem; +import net.minecraft.world.level.gameevent.GameEvent; + +/** Titanium shears with the vanilla sheep interaction that Minecraft reserves for its own item id. */ +public final class TitaniumShearsItem extends ShearsItem { + public TitaniumShearsItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResult interactLivingEntity(ItemStack stack, Player player, LivingEntity entity, + InteractionHand hand) { + if (!(entity instanceof Sheep sheep)) { + return super.interactLivingEntity(stack, player, entity, hand); + } + if (!(sheep.level() instanceof ServerLevel level)) { + return InteractionResult.CONSUME; + } + if (!sheep.readyForShearing()) { + return InteractionResult.CONSUME; + } + + sheep.shear(level, SoundSource.PLAYERS, stack); + sheep.gameEvent(GameEvent.SHEAR, player); + stack.hurtAndBreak(1, player, hand.asEquipmentSlot()); + return InteractionResult.SUCCESS_SERVER; + } +} diff --git a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldItems.java b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldItems.java index e3f00f2..e85b727 100644 --- a/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldItems.java +++ b/anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldItems.java @@ -1,9 +1,10 @@ package fr.koka99cab.sanctuary26.anotherworld.registry; import fr.koka99cab.sanctuary26.anotherworld.AnotherWorldMod; -import fr.koka99cab.sanctuary26.anotherworld.item.WeatherCompassItem; -import fr.koka99cab.sanctuary26.anotherworld.item.TitaniumEquipment; import fr.koka99cab.sanctuary26.anotherworld.item.TitaniumDrillItem; +import fr.koka99cab.sanctuary26.anotherworld.item.TitaniumEquipment; +import fr.koka99cab.sanctuary26.anotherworld.item.TitaniumShearsItem; +import fr.koka99cab.sanctuary26.anotherworld.item.WeatherCompassItem; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -95,7 +96,7 @@ public final class AnotherWorldItems { ); public static final Item TITANIUM_SHEARS = register( "titanium_shears", - ShearsItem::new, + TitaniumShearsItem::new, TitaniumEquipment.uniqueProperties() .durability(238) .component(DataComponents.TOOL, ShearsItem.createToolProperties()) diff --git a/build.gradle b/build.gradle index 2c6dc44..3cc74ac 100644 --- a/build.gradle +++ b/build.gradle @@ -3438,7 +3438,7 @@ tasks.register("verifyProgressivePauseMenuRelease") { "sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))] } if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || 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"] @@ -3488,7 +3488,7 @@ tasks.register("verifyMirrorLavenderDrawerRelease") { "redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java").text def drawerRecipe = file("redstoner/src/main/resources/data/redstoner/recipe/drawer.json") if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.anotherworld_version != "0.0.0-alpha.57" + || rootProject.anotherworld_version != "0.0.0-alpha.58" || rootProject.redstoner_version != "0.0.0-alpha.13" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.191" @@ -4279,7 +4279,7 @@ tasks.register("verifyShopPricingLayoutRelease") { def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json")) if (rootProject.pack_version != "26.2.0-alpha.222" || rootProject.onlyfun_version != "0.0.0-alpha.26" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || 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" @@ -4373,7 +4373,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.222" || rootProject.onlyfun_version != "0.0.0-alpha.26" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || 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"] @@ -4465,7 +4465,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.222" || rootProject.itsalive_version != "0.0.0-alpha.36" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || 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"] @@ -4482,7 +4482,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 != 22 + || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 23 || release.pack_version != "26.2.0-alpha.222" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.222.mrpack" || prism.versionId != "26.2.0-alpha.222" @@ -4565,7 +4565,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.222" || rootProject.itsalive_version != "0.0.0-alpha.36" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || 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" @@ -4576,7 +4576,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 != 22 + || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 23 || release.pack_version != "26.2.0-alpha.222" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.222.mrpack" || prism.versionId != "26.2.0-alpha.222" @@ -4685,7 +4685,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.222" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || 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" @@ -4698,7 +4698,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 != 22 + || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 23 || release.pack_version != "26.2.0-alpha.222" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.222.mrpack" || prism.versionId != "26.2.0-alpha.222" @@ -4739,10 +4739,10 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") { def sanctuaryManifest = new JsonSlurper().parse(file( "sanctuary/src/main/resources/fabric.mod.json")) if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.anotherworld_version != "0.0.0-alpha.57" + || rootProject.anotherworld_version != "0.0.0-alpha.58" || rootProject.itsalive_version != "0.0.0-alpha.36" - || rootProject.iliketomoveit_version != "0.0.0-alpha.16" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.iliketomoveit_version != "0.0.0-alpha.17" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || migration.source?.pack_version != "26.2.0-alpha.201" || migration.source?.modules != [ itsalive: "0.0.0-alpha.30", @@ -4777,7 +4777,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 != 22 + || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 23 || release.pack_version != "26.2.0-alpha.222" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.222.mrpack" || prism.versionId != "26.2.0-alpha.222" @@ -4899,8 +4899,8 @@ tasks.register("verifySafeVaultFeature") { image == null || image.width != size[0] || image.height != size[1] }.keySet() if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.anotherworld_version != "0.0.0-alpha.57" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.anotherworld_version != "0.0.0-alpha.58" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || migration.schema_version != 1 || migration.source?.pack_version != "26.2.0-alpha.201" || migration.target?.pack_version != "26.2.0-alpha.202" @@ -5091,7 +5091,7 @@ tasks.register("verifySafeVaultInterfaceAlpha203Release") { def packwizIndex = file("pack/packwiz/index.toml").text def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.196.jar" } if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.anotherworld_version != "0.0.0-alpha.57" + || rootProject.anotherworld_version != "0.0.0-alpha.58" || migration.source?.pack_version != "26.2.0-alpha.202" || migration.source?.modules != [anotherworld: "0.0.0-alpha.50"] || migration.target?.pack_version != "26.2.0-alpha.203" @@ -5137,7 +5137,7 @@ tasks.register("verifySafeVaultFixAlpha204Release") { def packwizIndex = file("pack/packwiz/index.toml").text def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.196.jar" } if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.anotherworld_version != "0.0.0-alpha.57" + || rootProject.anotherworld_version != "0.0.0-alpha.58" || migration.source?.pack_version != "26.2.0-alpha.203" || migration.source?.modules != [anotherworld: "0.0.0-alpha.51"] || migration.target?.pack_version != "26.2.0-alpha.204" @@ -5191,7 +5191,7 @@ tasks.register("verifySafeVaultCubeAlpha205Release") { def packwizIndex = file("pack/packwiz/index.toml").text def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.196.jar" } if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.anotherworld_version != "0.0.0-alpha.57" + || rootProject.anotherworld_version != "0.0.0-alpha.58" || migration.source?.pack_version != "26.2.0-alpha.204" || migration.source?.modules != [anotherworld: "0.0.0-alpha.52"] || migration.target?.pack_version != "26.2.0-alpha.205" @@ -5249,7 +5249,7 @@ tasks.register("verifySafeVaultCtmAlpha206Release") { def packwizIndex = file("pack/packwiz/index.toml").text def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.196.jar" } if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.anotherworld_version != "0.0.0-alpha.57" + || rootProject.anotherworld_version != "0.0.0-alpha.58" || migration.source?.pack_version != "26.2.0-alpha.205" || migration.source?.modules != [anotherworld: "0.0.0-alpha.53"] || migration.target?.pack_version != "26.2.0-alpha.206" @@ -5301,7 +5301,7 @@ tasks.register("verifySafeVaultUnlimitedAlpha207Release") { def packwizIndex = file("pack/packwiz/index.toml").text def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.29.0.196.jar" } if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.anotherworld_version != "0.0.0-alpha.57" + || rootProject.anotherworld_version != "0.0.0-alpha.58" || migration.source?.pack_version != "26.2.0-alpha.206" || migration.source?.modules != [anotherworld: "0.0.0-alpha.54"] || migration.target?.pack_version != "26.2.0-alpha.207" @@ -5759,9 +5759,9 @@ tasks.register("verifyAlpha210Release") { it.path == "mods/jei-26.2-fabric-30.29.0.196.jar" } if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || rootProject.onlyfun_version != "0.0.0-alpha.26" - || rootProject.iliketomoveit_version != currentModules.iliketomoveit + || rootProject.iliketomoveit_version != "0.0.0-alpha.17" || rootProject.itsalive_version != "0.0.0-alpha.36" || migration.source?.pack_version != "26.2.0-alpha.209" || migration.source?.modules != expectedSource @@ -5797,7 +5797,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 != 22 + || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 23 || manifests.onlyfun.custom?.sanctuary26?.data_version != 2 || manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3 || manifests.iliketomoveit.custom?.sanctuary26?.data_version != 3 @@ -5858,9 +5858,11 @@ tasks.register("verifyAlpha211Release") { onlyfun: "0.0.0-alpha.25", ambiance: "0.0.0-alpha.22", iliketomoveit: "0.0.0-alpha.16"] - def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.125", onlyfun: "0.0.0-alpha.26"] + def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.126", onlyfun: "0.0.0-alpha.26", + iliketomoveit: "0.0.0-alpha.17"] def expectedDataVersions = [ambiance: 2, itsalive: 3, onlyfun: 2, ouch: 1, sanctuary: 11] def expectedProtocols = [ambiance: 6, itsalive: 4, onlyfun: 3, ouch: 1, sanctuary: 22] + def workspaceProtocols = expectedProtocols + [sanctuary: 23] def manifests = expectedTarget.collectEntries { id, version -> [(id): new JsonSlurper().parse(file("${id}/src/main/resources/fabric.mod.json"))] } @@ -5905,7 +5907,7 @@ tasks.register("verifyAlpha211Release") { || migration.compatibility?.data_versions != expectedDataVersions || migration.compatibility?.network_protocols != expectedProtocols || manifestDataVersions != expectedDataVersions - || manifestProtocols != expectedProtocols + || manifestProtocols != workspaceProtocols || migration.distribution?.pack_release_updated != true || migration.distribution?.packwiz_index_refreshed != true || migration.distribution?.prism_modrinth_index_updated != true @@ -5956,7 +5958,7 @@ tasks.register("verifyAlpha212Release") { it.path == "mods/jei-26.2-fabric-30.29.0.196.jar" } if (rootProject.pack_version != "26.2.0-alpha.222" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || 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" @@ -5975,7 +5977,7 @@ tasks.register("verifyAlpha212Release") { || migration.distribution?.prism_modrinth_index_updated != true || migration.distribution?.shared_client_server_mrpack != true || manifest.custom?.sanctuary26?.data_version != 11 - || manifest.custom?.sanctuary26?.network_protocol != 22 + || manifest.custom?.sanctuary26?.network_protocol != 23 || !minecraftMixin.contains('method = "pickBlockOrEntity"') || release.pack_version != "26.2.0-alpha.222" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.222.mrpack" @@ -6438,7 +6440,8 @@ tasks.register("verifyAlpha218Release") { redstoner: "0.0.0-alpha.11", onlyfun: "0.0.0-alpha.25", sanctuary: "0.0.0-alpha.121"] def currentModules = targetModules + [redstoner: "0.0.0-alpha.13", sanctuary: "0.0.0-alpha.124", iliketomoveit: "0.0.0-alpha.16", ambiance: "0.0.0-alpha.22"] - def workspaceModules = currentModules + [onlyfun: "0.0.0-alpha.26", sanctuary: "0.0.0-alpha.125"] + def workspaceModules = currentModules + [onlyfun: "0.0.0-alpha.26", sanctuary: "0.0.0-alpha.126", + iliketomoveit: "0.0.0-alpha.17"] def requiredJars = currentModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } @@ -6450,7 +6453,7 @@ tasks.register("verifyAlpha218Release") { || workspaceModules.findAll { id, version -> id != "redstoner" && id != "sanctuary" }.any { id, version -> rootProject.property("${id}_version") != version } - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || migration.source?.pack_version != "26.2.0-alpha.217" || migration.source?.modules != sourceModules || migration.target?.pack_version != "26.2.0-alpha.218" @@ -6475,7 +6478,7 @@ tasks.register("verifyAlpha218Release") { || manifests.onlyfun.custom?.sanctuary26?.data_version != 2 || manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3 || manifests.sanctuary.custom?.sanctuary26?.data_version != 11 - || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22 + || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 23 || !rules.contains("MAX_CONTAINERS = 128") || !rules.contains("PAGE_SIZE = 54") || !networking.contains("PROTOCOL_VERSION = 7") @@ -6534,7 +6537,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.124"] - def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.125", onlyfun: "0.0.0-alpha.26"] + def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.126", onlyfun: "0.0.0-alpha.26", + iliketomoveit: "0.0.0-alpha.17"] def requiredJars = currentModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } @@ -6575,7 +6579,7 @@ tasks.register("verifyAlpha219Release") { || migration.compatibility?.ambiance_data_version != 2 || migration.compatibility?.ambiance_network_protocol != 6 || manifests.sanctuary.custom?.sanctuary26?.data_version != 11 - || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22 + || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 23 || manifests.iliketomoveit.custom?.sanctuary26?.data_version != 3 || manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5 || manifests.ambiance.custom?.sanctuary26?.data_version != 2 @@ -6859,7 +6863,7 @@ tasks.register("verifyAlpha222Release") { } if (rootProject.pack_version != "26.2.0-alpha.222" || rootProject.redstoner_version != "0.0.0-alpha.13" - || rootProject.sanctuary_version != "0.0.0-alpha.125" + || rootProject.sanctuary_version != "0.0.0-alpha.126" || migration.source?.pack_version != "26.2.0-alpha.221" || migration.source?.modules != [ redstoner: "0.0.0-alpha.12", sanctuary: "0.0.0-alpha.122"] @@ -6902,7 +6906,7 @@ tasks.register("verifyAlpha222Release") { || manifests.redstoner.custom?.sanctuary26?.data_version != 3 || manifests.redstoner.custom?.sanctuary26?.network_protocol != 7 || manifests.sanctuary.custom?.sanctuary26?.data_version != 11 - || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22 + || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 23 || !rules.contains("MAX_SEARCH_LENGTH = 32") || !rules.contains("compactCount(int count)") || !layout.contains("Keeps terminal slot identities stable") diff --git a/gradle.properties b/gradle.properties index c2fc638..362b992 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ ambiance_version=0.0.0-alpha.22 ambiance_lifecycle=active redstoner_version=0.0.0-alpha.13 redstoner_lifecycle=active -anotherworld_version=0.0.0-alpha.57 +anotherworld_version=0.0.0-alpha.58 anotherworld_lifecycle=active itsalive_version=0.0.0-alpha.36 itsalive_lifecycle=active @@ -24,11 +24,11 @@ ouch_version=0.0.0-alpha.10 ouch_lifecycle=active masterkey_version=0.0.0-alpha.8 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.26 onlyfun_lifecycle=active -sanctuary_version=0.0.0-alpha.125 +sanctuary_version=0.0.0-alpha.126 sanctuary_lifecycle=active pack_version=26.2.0-alpha.222 maven_group=fr.koka99cab.sanctuary26 diff --git a/iliketomoveit/build.gradle b/iliketomoveit/build.gradle index 6f68977..dd0c692 100644 --- a/iliketomoveit/build.gradle +++ b/iliketomoveit/build.gradle @@ -250,11 +250,13 @@ tasks.register("verifyPortals") { doLast { def javaText = fileTree("src/main/java").matching { include "**/*.java" }.files*.text.join("\n") + def mixins = new JsonSlurper().parse(file("src/main/resources/iliketomoveit.mixins.json")) def requiredJava = [ 'class PortalDefinitionReloadListener', 'FileToIdConverter.json("sanctuary_portal")', 'class PortalBlockEntity', 'class PortalSavedData', 'SAVE_KEY = "portal_links"', 'DATA_VERSION = 1', 'MAX_ENDPOINTS = 8192', 'UUID.randomUUID()', - 'class PortalIgnitionHandler', 'PortalDefinitions.matching', 'class PortalTeleporter', + 'class PortalIgnitionHandler', 'PortalDefinitions.matching', 'matchingFrame', + 'tryIgniteFromFireball', 'class PortalTeleporter', 'data.link(source, target)', 'data.deactivate', 'destination_unavailable', 'LEGACY_ALPHA_PORTAL', 'LEGACY_CAVERNS_PORTAL', 'LEGACY_OVERWORLD_PORTAL', 'LEGACY_SHEEP_LAND_PORTAL', 'LEGACY_SKELETON_WASTE_PORTAL', @@ -265,6 +267,9 @@ tasks.register("verifyPortals") { ] def missingJava = requiredJava.findAll { !javaText.contains(it) } if (!missingJava.isEmpty()) throw new GradleException("Incomplete portal runtime contract: ${missingJava}") + if (!mixins.mixins.containsAll(["LargeFireballMixin", "SmallFireballMixin"])) { + throw new GradleException("Player-owned fireball portal ignition mixins are missing") + } def expectedDestinations = [ overworld: "sanctuary:overworld", caverns: "sanctuary:caverns", alpha: "sanctuary:alpha", @@ -313,7 +318,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 +354,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 +383,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 +415,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 +449,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" diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/mixin/LargeFireballMixin.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/mixin/LargeFireballMixin.java new file mode 100644 index 0000000..403661b --- /dev/null +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/mixin/LargeFireballMixin.java @@ -0,0 +1,23 @@ +package fr.koka99cab.sanctuary26.iliketomoveit.mixin; + +import fr.koka99cab.sanctuary26.iliketomoveit.portal.PortalIgnitionHandler; +import net.minecraft.world.entity.projectile.hurtingprojectile.LargeFireball; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +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; + +/** Lets a player-owned ghast fireball ignite a valid custom frame without destroying it. */ +@Mixin(LargeFireball.class) +abstract class LargeFireballMixin { + @Inject(method = "onHit", at = @At("HEAD"), cancellable = true) + private void iliketomoveit$ignitePortal(HitResult hit, CallbackInfo ci) { + if (hit instanceof BlockHitResult blockHit + && PortalIgnitionHandler.tryIgniteFromFireball((LargeFireball) (Object) this, blockHit)) { + ((LargeFireball) (Object) this).discard(); + ci.cancel(); + } + } +} diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/mixin/SmallFireballMixin.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/mixin/SmallFireballMixin.java new file mode 100644 index 0000000..5d6f328 --- /dev/null +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/mixin/SmallFireballMixin.java @@ -0,0 +1,20 @@ +package fr.koka99cab.sanctuary26.iliketomoveit.mixin; + +import fr.koka99cab.sanctuary26.iliketomoveit.portal.PortalIgnitionHandler; +import net.minecraft.world.entity.projectile.hurtingprojectile.SmallFireball; +import net.minecraft.world.phys.BlockHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Uses a player-owned blaze fireball as portal ignition instead of placing ordinary fire. */ +@Mixin(SmallFireball.class) +abstract class SmallFireballMixin { + @Inject(method = "onHitBlock", at = @At("HEAD"), cancellable = true) + private void iliketomoveit$ignitePortal(BlockHitResult hit, CallbackInfo ci) { + if (PortalIgnitionHandler.tryIgniteFromFireball((SmallFireball) (Object) this, hit)) { + ci.cancel(); + } + } +} diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/PortalDefinitions.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/PortalDefinitions.java index 67c0be7..2e8c232 100644 --- a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/PortalDefinitions.java +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/PortalDefinitions.java @@ -45,11 +45,22 @@ public final class PortalDefinitions { public static List matching(ItemStack activation, BlockState frame) { return definitions.values().stream() .filter(definition -> definition.matchesActivation(activation) && definition.matchesFrame(frame)) - .sorted(Comparator.comparingInt(PortalDefinition::priority).reversed() - .thenComparing(definition -> definition.id().toString())) + .sorted(priorityOrder()) .toList(); } + public static List matchingFrame(BlockState frame) { + return definitions.values().stream() + .filter(definition -> definition.matchesFrame(frame)) + .sorted(priorityOrder()) + .toList(); + } + + private static Comparator priorityOrder() { + return Comparator.comparingInt(PortalDefinition::priority).reversed() + .thenComparing(definition -> definition.id().toString()); + } + static void replace(Map replacement) { List missing = new ArrayList<>(REQUIRED_ROUTES); missing.removeAll(replacement.keySet()); diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/PortalIgnitionHandler.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/PortalIgnitionHandler.java index 1b0c3a2..9e81027 100644 --- a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/PortalIgnitionHandler.java +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/PortalIgnitionHandler.java @@ -9,7 +9,10 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.BlockHitResult; /** Ignites the highest-priority matching data-driven route. */ public final class PortalIgnitionHandler { @@ -21,36 +24,59 @@ public final class PortalIgnitionHandler { ItemStack stack = player.getItemInHand(hand); BlockPos clicked = hit.getBlockPos(); BlockPos interior = clicked.relative(hit.getDirection()); - PortalDefinition selected = null; - PortalFrame selectedFrame = null; - for (PortalDefinition definition : PortalDefinitions.matching(stack, level.getBlockState(clicked))) { - Optional frame = PortalFrame.findForIgnition(level, interior, definition::matchesFrame); - if (frame.isPresent()) { - selected = definition; - selectedFrame = frame.get(); - break; - } - } - if (selected == null || selectedFrame == null) return InteractionResult.PASS; + Optional target = findTarget(level, interior, + PortalDefinitions.matching(stack, level.getBlockState(clicked))); + if (target.isEmpty()) return InteractionResult.PASS; if (level.isClientSide()) return InteractionResult.SUCCESS; - if (!level.mayInteract(player, clicked) || !level.mayInteract(player, interior)) { - return InteractionResult.FAIL; - } - ServerLevel serverLevel = (ServerLevel) level; - if (serverLevel.getServer().getLevel( - selected.targetDimension(serverLevel.getServer(), serverLevel.dimension())) == null) { - player.sendOverlayMessage(Component.translatable( - "message.iliketomoveit.portal.destination_unavailable")); + if (!ignite(serverLevel, player, clicked, interior, target.get(), SoundEvents.FLINTANDSTEEL_USE)) { return InteractionResult.FAIL; } - PortalEndpoint endpoint = PortalSavedData.get(serverLevel.getServer()) - .register(serverLevel, selectedFrame, selected.id()); - selectedFrame.fill(serverLevel, selected, endpoint, ILikeToMoveItBlocks.PORTAL); - serverLevel.playSound(player, interior, SoundEvents.FLINTANDSTEEL_USE, - SoundSource.BLOCKS, 1.0F, serverLevel.getRandom().nextFloat() * 0.4F + 0.8F); stack.hurtAndBreak(1, player, hand); return InteractionResult.SUCCESS; }); } + + /** Handles player-owned fireballs without coupling Sanctuary companion code to portal internals. */ + public static boolean tryIgniteFromFireball(Fireball fireball, BlockHitResult hit) { + if (!(fireball.level() instanceof ServerLevel level) || !(fireball.getOwner() instanceof Player player)) { + return false; + } + BlockPos clicked = hit.getBlockPos(); + BlockPos interior = clicked.relative(hit.getDirection()); + Optional target = findTarget(level, interior, + PortalDefinitions.matchingFrame(level.getBlockState(clicked))); + if (target.isEmpty()) return false; + ignite(level, player, clicked, interior, target.get(), SoundEvents.FIRECHARGE_USE); + return true; + } + + private static Optional findTarget(net.minecraft.world.level.LevelReader level, + BlockPos interior, Iterable definitions) { + for (PortalDefinition definition : definitions) { + Optional frame = PortalFrame.findForIgnition(level, interior, definition::matchesFrame); + if (frame.isPresent()) return Optional.of(new IgnitionTarget(definition, frame.get())); + } + return Optional.empty(); + } + + private static boolean ignite(ServerLevel level, Player player, BlockPos clicked, BlockPos interior, + IgnitionTarget target, net.minecraft.sounds.SoundEvent sound) { + if (!level.mayInteract(player, clicked) || !level.mayInteract(player, interior)) return false; + PortalDefinition definition = target.definition(); + if (level.getServer().getLevel( + definition.targetDimension(level.getServer(), level.dimension())) == null) { + player.sendOverlayMessage(Component.translatable( + "message.iliketomoveit.portal.destination_unavailable")); + return false; + } + PortalEndpoint endpoint = PortalSavedData.get(level.getServer()) + .register(level, target.frame(), definition.id()); + target.frame().fill(level, definition, endpoint, ILikeToMoveItBlocks.PORTAL); + level.playSound(player, interior, sound, SoundSource.BLOCKS, 1.0F, + level.getRandom().nextFloat() * 0.4F + 0.8F); + return true; + } + + private record IgnitionTarget(PortalDefinition definition, PortalFrame frame) {} } diff --git a/iliketomoveit/src/main/resources/iliketomoveit.mixins.json b/iliketomoveit/src/main/resources/iliketomoveit.mixins.json index 3974ac0..7d2011c 100644 --- a/iliketomoveit/src/main/resources/iliketomoveit.mixins.json +++ b/iliketomoveit/src/main/resources/iliketomoveit.mixins.json @@ -3,7 +3,9 @@ "package": "fr.koka99cab.sanctuary26.iliketomoveit.mixin", "compatibilityLevel": "JAVA_25", "mixins": [ - "HappyGhastMixin" + "HappyGhastMixin", + "LargeFireballMixin", + "SmallFireballMixin" ], "injectors": { "defaultRequire": 1 diff --git a/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/AlphaPortalContractSmoke.java b/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/AlphaPortalContractSmoke.java index 0d732f0..f108bd2 100644 --- a/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/AlphaPortalContractSmoke.java +++ b/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/portal/AlphaPortalContractSmoke.java @@ -1,10 +1,18 @@ package fr.koka99cab.sanctuary26.iliketomoveit.portal; import fr.koka99cab.sanctuary26.sanctuary.api.world.SanctuaryAlphaWorld; +import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions; +import java.util.Map; import java.util.Set; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; import net.minecraft.server.Bootstrap; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; /** Executable cross-module contract for the active mossy Alpha portal. */ public final class AlphaPortalContractSmoke { @@ -19,6 +27,7 @@ public final class AlphaPortalContractSmoke { require(Set.copyOf(PortalDefinitions.ACTIVE_ROUTES).equals(Set.of( PortalDefinitions.OVERWORLD, PortalDefinitions.CAVERNS, PortalDefinitions.ALPHA)), "Exactly the Overworld, Caverns and Alpha routes must be active"); + verifyFrameOnlySelection(); for (long seed : new long[] { 0L, 1L, 46L }) { BlockPos arrival = SanctuaryAlphaWorld.portalArrival(seed); @@ -31,6 +40,25 @@ public final class AlphaPortalContractSmoke { } } + private static void verifyFrameOnlySelection() { + PortalDefinition overworld = route(PortalDefinitions.OVERWORLD, Blocks.DIRT, + SanctuaryDimensions.OVERWORLD, 100); + PortalDefinition caverns = route(PortalDefinitions.CAVERNS, Blocks.STONE, + SanctuaryDimensions.CAVERNS, 100); + PortalDefinition alpha = route(PortalDefinitions.ALPHA, Blocks.MOSSY_COBBLESTONE, + SanctuaryDimensions.ALPHA, 100); + PortalDefinitions.replace(Map.of(overworld.id(), overworld, caverns.id(), caverns, alpha.id(), alpha)); + require(PortalDefinitions.matchingFrame(Blocks.STONE.defaultBlockState()).equals(java.util.List.of(caverns)), + "Fireball ignition must select a route from its frame without an activation item"); + } + + private static PortalDefinition route(Identifier id, Block frame, ResourceKey destination, + int priority) { + return new PortalDefinition(id, Set.of(frame), frame, Set.of(Items.FLINT_AND_STEEL), + Level.OVERWORLD, destination, PortalDestinationResolver.FIXED, 1.0D, + PortalArrival.SURFACE, 0, priority); + } + private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); } diff --git a/sanctuary/build.gradle b/sanctuary/build.gradle index 73e835a..c1e7626 100644 --- a/sanctuary/build.gradle +++ b/sanctuary/build.gradle @@ -108,6 +108,66 @@ tasks.register("capeJoinSyncSmoke", JavaExec) { mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeJoinSyncQueueSmoke" } +tasks.register("capeBannerAppearanceSmoke", JavaExec) { + group = "verification" + description = "Checks the immutable 16-layer bound used by customizable banner capes." + dependsOn tasks.named("testClasses") + classpath = sourceSets.test.runtimeClasspath + mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeBannerAppearanceSmoke" +} + +tasks.register("verifyBannerCapes") { + group = "verification" + description = "Checks the white-cape recipe, translations, bounded sync and vanilla-physics renderer." + inputs.files( + file("src/main/resources/data/sanctuary/recipe/banner_cape.json"), + file("src/main/resources/assets/sanctuary/models/item/white_cape.json"), + file("src/main/resources/assets/sanctuary/items/white_cape.json"), + file("src/main/resources/assets/sanctuary/textures/entity/cape/white_cape.png"), + file("src/main/resources/assets/sanctuary/textures/item/cape/white_cape.png"), + file("src/main/resources/fabric.mod.json") + ) + inputs.files(fileTree("src/main/java").matching { + include "**/CapeBannerAppearance.java", "**/CapeSyncPayload.java", "**/SanctuaryCapes.java", + "**/BannerCapeLayer.java", "**/BannerCapeModel.java", "**/AvatarRendererMixin.java" + }) + inputs.files(["en_us", "fr_fr", "ru_ru"].collect { + file("src/main/resources/assets/sanctuary/lang/${it}.json") + }) + doLast { + def recipe = new JsonSlurper().parse(file( + "src/main/resources/data/sanctuary/recipe/banner_cape.json")) + def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json")) + def javaText = fileTree("src/main/java").matching { include "**/*.java" }.files*.text.join("\n") + def entityTexture = ImageIO.read(file( + "src/main/resources/assets/sanctuary/textures/entity/cape/white_cape.png")) + def itemTexture = ImageIO.read(file( + "src/main/resources/assets/sanctuary/textures/item/cape/white_cape.png")) + def translations = ["en_us", "fr_fr", "ru_ru"].collect { + new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json")) + } + if (project.version.toString() != "0.0.0-alpha.126" + || manifest.custom?.sanctuary26?.data_version != 11 + || manifest.custom?.sanctuary26?.network_protocol != 23 + || recipe.type != "minecraft:crafting_special_shielddecoration" + || recipe.banner != "#minecraft:banners" + || recipe.target != "sanctuary:white_cape" + || recipe.result?.id != "sanctuary:white_cape" + || entityTexture?.width != 64 || entityTexture?.height != 32 + || itemTexture?.width != 16 || itemTexture?.height != 16 + || translations.any { !it.containsKey("item.sanctuary.white_cape") } + || !javaText.contains('SanctuaryMod.id("white_cape")') + || !javaText.contains("DataComponents.BANNER_PATTERNS") + || !javaText.contains("MAX_PATTERNS = 16") + || !javaText.contains("ByteBufCodecs.list(CapeBannerAppearance.MAX_PATTERNS)") + || !javaText.contains("BannerRenderer.submitPatterns") + || !javaText.contains("LivingEntityRenderLayerRegistrationCallback.EVENT.register") + || !javaText.contains("extends PlayerCapeModel")) { + throw new GradleException("Sanctuary customizable banner-cape contract is incomplete") + } + } +} + tasks.register("alphaIndevGeneratorSmoke", JavaExec) { group = "verification" description = "Checks deterministic finite Indev islands, residences and safe portal arrivals." @@ -151,7 +211,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || !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") @@ -165,7 +225,7 @@ tasks.register("verifyBlackMarket") { || !minecraftMixin.contains('method = "pickBlockOrEntity"') || !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("PROTOCOL_VERSION = 23") || !network.contains("BlackMarketService.middleClick(context.player())") || !service.contains("loadedDeliveryBox") || !service.contains("PARCEL_RETENTION_MILLIS") || !policy.contains("DataComponents.CONTAINER") || !policy.contains("DataComponents.BUNDLE_CONTENTS") @@ -1144,7 +1204,7 @@ 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 != 22 + || manifest.custom?.sanctuary26?.network_protocol != 23 || 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") @@ -2850,7 +2910,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || 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 @@ -3016,7 +3076,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || 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 @@ -3054,7 +3114,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || 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] @@ -3085,7 +3145,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || migration.target?.pack_version != "26.2.0-alpha.195" || migration.target?.modules?.sanctuary != "0.0.0-alpha.111" || migration.compatibility?.sanctuary_data_version != 11 @@ -3116,7 +3176,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || migration.target?.modules?.sanctuary != "0.0.0-alpha.112" || migration.mailbox?.id != "sanctuary:delivery_box" || migration.mailbox?.horizontal_facing != true @@ -3144,7 +3204,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || !transfer.contains("shouldAppendOverflow") || !transfer.contains("isVanillaStorageRange") || !transfer.contains("hotbarStart") @@ -3190,7 +3250,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || migration.target?.modules?.sanctuary != "0.0.0-alpha.113" || migration.quest_requests?.save_key != "quest_requests" || migration.quest_requests?.data_version != 1 @@ -3202,7 +3262,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 = 22") + || !network.contains("PROTOCOL_VERSION = 23") || !network.contains("QuestRequestSubmitPayload.ID") || !network.contains("QuestRequestAcceptPayload.ID") || !registry.contains('SanctuaryMod.id("quest_request_sign")') @@ -3248,7 +3308,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || 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" @@ -3268,7 +3328,7 @@ tasks.register("verifyQuestRequestContracts") { || !service.contains('REWARD_LOAN = "loan"') || !service.contains('PLAYER.equals(targetKind)') || !screen.contains("rewardQuantity") || !screen.contains("killTargetIndex") - || !network.contains("PROTOCOL_VERSION = 22") + || !network.contains("PROTOCOL_VERSION = 23") || 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 @@ -3317,7 +3377,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || 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" @@ -3338,7 +3398,7 @@ tasks.register("verifyQuestRequestEscrow") { || !service.contains('"quest_request_reward"') || !service.contains('"quest_request_delivery"') || service.contains("sanctuary_quest_accept") - || !network.contains("PROTOCOL_VERSION = 22") + || !network.contains("PROTOCOL_VERSION = 23") || !network.contains("QuestRequestAcceptPayload.ID") || !network.contains("QuestRequestConfirmPayload.ID") || !acceptPayload.contains("readUtf(36)") @@ -3389,7 +3449,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.125" + if (project.version.toString() != "0.0.0-alpha.126" || migration.target?.modules?.sanctuary != "0.0.0-alpha.122" || migration.alpha_world?.dimension != "sanctuary:alpha" || migration.alpha_world?.legacy_dimension != "portals:alpha" @@ -3413,6 +3473,8 @@ tasks.register("verifyAlphaIndev") { tasks.named("check") { dependsOn tasks.named("verifySanctuary") + dependsOn tasks.named("verifyBannerCapes") + dependsOn tasks.named("capeBannerAppearanceSmoke") dependsOn tasks.named("verifyDeliveryBoxOrientation") dependsOn tasks.named("verifyQuestRequests") dependsOn tasks.named("verifyQuestRequestContracts") diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/BannerCapeLayer.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/BannerCapeLayer.java new file mode 100644 index 0000000..ec51e3b --- /dev/null +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/BannerCapeLayer.java @@ -0,0 +1,70 @@ +package fr.koka99cab.sanctuary26.sanctuary.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeBannerAppearance; +import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityFeatureRenderEvents; +import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityRenderLayerRegistrationCallback; +import net.minecraft.client.model.player.PlayerModel; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BannerRenderer; +import net.minecraft.client.renderer.entity.RenderLayerParent; +import net.minecraft.client.renderer.entity.layers.RenderLayer; +import net.minecraft.client.renderer.entity.player.AvatarRenderer; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.resources.model.EquipmentAssetManager; +import net.minecraft.client.resources.model.EquipmentClientInfo; +import net.minecraft.client.resources.model.sprite.SpriteGetter; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.equipment.Equippable; + +/** Renders the white cape with the base color and bounded pattern layers copied from a banner. */ +public final class BannerCapeLayer extends RenderLayer { + private final BannerCapeModel model = new BannerCapeModel(); + private final SpriteGetter sprites; + private final EquipmentAssetManager equipmentAssets; + + private BannerCapeLayer(RenderLayerParent parent, + SpriteGetter sprites, EquipmentAssetManager equipmentAssets) { + super(parent); + this.sprites = sprites; + this.equipmentAssets = equipmentAssets; + } + + public static void register() { + LivingEntityRenderLayerRegistrationCallback.EVENT.register((entityType, renderer, helper, context) -> { + if (entityType != EntityTypes.PLAYER || !(renderer instanceof AvatarRenderer)) return; + @SuppressWarnings("unchecked") + RenderLayerParent parent = + (RenderLayerParent) (Object) renderer; + helper.register(new BannerCapeLayer(parent, context.getSprites(), context.getEquipmentAssets())); + }); + LivingEntityFeatureRenderEvents.ALLOW_CAPE_RENDER.register(state -> + !((BannerCapeRenderState) (Object) state).sanctuary$hasBannerCape()); + } + + @Override + public void submit(PoseStack poseStack, SubmitNodeCollector collector, int light, + AvatarRenderState state, float yRot, float xRot) { + CapeBannerAppearance appearance = + ((BannerCapeRenderState) (Object) state).sanctuary$bannerCape(); + if (appearance == null || state.isInvisible || !state.showCape + || hasLayer(state.chestEquipment, EquipmentClientInfo.LayerType.WINGS)) return; + + poseStack.pushPose(); + if (hasLayer(state.chestEquipment, EquipmentClientInfo.LayerType.HUMANOID)) { + poseStack.translate(0.0F, -0.053125F, 0.06875F); + } + BannerRenderer.submitPatterns(sprites, poseStack, collector, light, OverlayTexture.NO_OVERLAY, + model, state, true, appearance.baseColor(), appearance.patterns(), null); + poseStack.popPose(); + } + + private boolean hasLayer(ItemStack stack, EquipmentClientInfo.LayerType layerType) { + Equippable equippable = stack.get(DataComponents.EQUIPPABLE); + if (equippable == null || equippable.assetId().isEmpty()) return false; + return !equipmentAssets.get(equippable.assetId().orElseThrow()).getLayers(layerType).isEmpty(); + } +} diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/BannerCapeModel.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/BannerCapeModel.java new file mode 100644 index 0000000..e96bc37 --- /dev/null +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/BannerCapeModel.java @@ -0,0 +1,39 @@ +package fr.koka99cab.sanctuary26.sanctuary.client; + +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.client.model.player.PlayerCapeModel; +import net.minecraft.client.model.player.PlayerModel; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; + +/** Cape geometry whose UV area matches a 20x40 vanilla banner, scaled to cape dimensions. */ +final class BannerCapeModel extends PlayerCapeModel { + private final ModelPart bannerCape; + + BannerCapeModel() { + super(createLayer().bakeRoot()); + bannerCape = body.getChild("cape"); + } + + @Override + public void setupAnim(AvatarRenderState state) { + super.setupAnim(state); + bannerCape.xScale = 0.5F; + bannerCape.yScale = 0.4F; + } + + private static LayerDefinition createLayer() { + MeshDefinition mesh = PlayerModel.createMesh(net.minecraft.client.model.geom.builders.CubeDeformation.NONE, + false); + PartDefinition root = mesh.getRoot().clearRecursively(); + PartDefinition body = root.getChild("body"); + body.addOrReplaceChild("cape", CubeListBuilder.create().texOffs(0, 0) + .addBox(-10.0F, 0.0F, -1.0F, 20.0F, 40.0F, 1.0F), + PartPose.offsetAndRotation(0.0F, 0.0F, 2.0F, 0.0F, (float) Math.PI, 0.0F)); + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/BannerCapeRenderState.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/BannerCapeRenderState.java new file mode 100644 index 0000000..ee9b433 --- /dev/null +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/BannerCapeRenderState.java @@ -0,0 +1,13 @@ +package fr.koka99cab.sanctuary26.sanctuary.client; + +import fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeBannerAppearance; + +/** Client-only extension carrying one extracted banner cape appearance into render layers. */ +public interface BannerCapeRenderState { + CapeBannerAppearance sanctuary$bannerCape(); + void sanctuary$setBannerCape(CapeBannerAppearance appearance); + + default boolean sanctuary$hasBannerCape() { + return sanctuary$bannerCape() != null; + } +} 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 22a9189..3a78643 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 @@ -104,6 +104,7 @@ public final class SanctuaryClient implements ClientModInitializer { SanctuaryBulkActionsClient.initialize(); SanctuaryHealthPreview.register(); CelestialSpyglassSelection.register(); + BannerCapeLayer.register(); EntityRendererRegistry.register(SanctuaryRegistries.SEAT, NoopRenderer::new); ClientTickEvents.END_CLIENT_TICK.register(SanctuaryClient::tick); ClientPlayNetworking.registerGlobalReceiver(ProgressionSnapshotPayload.ID, (payload, context) -> diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClientCapes.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClientCapes.java index ec56cd7..f2339e1 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClientCapes.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryClientCapes.java @@ -1,6 +1,7 @@ package fr.koka99cab.sanctuary26.sanctuary.client; import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCapes; +import fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeBannerAppearance; import fr.koka99cab.sanctuary26.sanctuary.network.CapeSyncPayload; import java.util.HashMap; import java.util.Map; @@ -9,22 +10,31 @@ import net.minecraft.resources.Identifier; /** Client mirror of server-authoritative equipped cape choices. */ public final class SanctuaryClientCapes { - private static final Map TEXTURES = new HashMap<>(); + private static final Map VISUALS = new HashMap<>(); private SanctuaryClientCapes() { } public static void update(CapeSyncPayload payload) { Identifier texture = SanctuaryCapes.texture(payload.capeItemId()); - if (texture == null) TEXTURES.remove(payload.playerId()); - else TEXTURES.put(payload.playerId(), texture); + CapeBannerAppearance banner = SanctuaryCapes.isBannerCape(payload.capeItemId()) + ? new CapeBannerAppearance(payload.baseColor(), payload.bannerPatterns()) : null; + if (texture == null) VISUALS.remove(payload.playerId()); + else VISUALS.put(payload.playerId(), new Visual(texture, banner)); } public static Identifier texture(UUID playerId) { - return TEXTURES.get(playerId); + Visual visual = VISUALS.get(playerId); + return visual == null ? null : visual.texture(); + } + + public static Visual visual(UUID playerId) { + return VISUALS.get(playerId); } public static void clear() { - TEXTURES.clear(); + VISUALS.clear(); } + + public record Visual(Identifier texture, CapeBannerAppearance banner) {} } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/CapeBannerAppearance.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/CapeBannerAppearance.java new file mode 100644 index 0000000..53d5cc3 --- /dev/null +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/CapeBannerAppearance.java @@ -0,0 +1,20 @@ +package fr.koka99cab.sanctuary26.sanctuary.gameplay; + +import java.util.List; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.block.entity.BannerPatternLayers; + +/** Bounded banner appearance stored on and synchronized for the customizable white cape. */ +public record CapeBannerAppearance(DyeColor baseColor, BannerPatternLayers patterns) { + public static final int MAX_PATTERNS = 16; + public static final CapeBannerAppearance WHITE = new CapeBannerAppearance( + DyeColor.WHITE, BannerPatternLayers.EMPTY); + + public CapeBannerAppearance { + baseColor = baseColor == null ? DyeColor.WHITE : baseColor; + List layers = patterns == null + ? List.of() : patterns.layers(); + if (layers.size() > MAX_PATTERNS) layers = layers.subList(0, MAX_PATTERNS); + patterns = new BannerPatternLayers(List.copyOf(layers)); + } +} diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapeService.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapeService.java index 186f524..b198937 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapeService.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapeService.java @@ -8,6 +8,7 @@ import net.fabricmc.fabric.api.networking.v1.PlayerLookup; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; /** Server authority and multiplayer distribution for equipped cape visuals. */ public final class SanctuaryCapeService { @@ -62,7 +63,11 @@ public final class SanctuaryCapeService { private static void send(ServerPlayer receiver, ServerPlayer owner) { if (!ServerPlayNetworking.canSend(receiver, CapeSyncPayload.ID)) return; - String capeId = SanctuaryCapes.itemId(SanctuaryInventoryCapacity.equippedCape(owner)); - ServerPlayNetworking.send(receiver, new CapeSyncPayload(owner.getUUID(), capeId)); + ItemStack cape = SanctuaryInventoryCapacity.equippedCape(owner); + String capeId = SanctuaryCapes.itemId(cape); + CapeBannerAppearance banner = SanctuaryCapes.bannerAppearance(cape); + if (banner == null) banner = CapeBannerAppearance.WHITE; + ServerPlayNetworking.send(receiver, new CapeSyncPayload(owner.getUUID(), capeId, + banner.baseColor(), banner.patterns())); } } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapes.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapes.java index b90b95b..65807d0 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapes.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryCapes.java @@ -5,17 +5,21 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.DyeColor; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BannerPatternLayers; /** Registration and immutable visual definitions for Sanctuary's collectible capes. */ public final class SanctuaryCapes { + public static final Identifier WHITE_CAPE_ID = SanctuaryMod.id("white_cape"); public static final List PATHS = List.of( "transgender_cape", "mojang_cape", "founders_cape", "nes_cape", "pride_cape", "pink_cape", "feesh_gang_cape", "gameboy_cape", "blue_cape", "chicken_cape", @@ -28,7 +32,7 @@ public final class SanctuaryCapes { "vanilla_cape", "java_edition_cape", "migration_cape", "creeper_anniversary_cape", "java_twitch_cape", "minecon_enderman_cape", "iron_golem_minecon_cape", "minecon_2013_cape", "crafter_twitchcon_cape", "minecon_pickaxe_cape", "minecon_2011_cape", "mojang_classic_cape", - "blue_scroll_cape" + "blue_scroll_cape", "white_cape" ); private static final Map DEFINITIONS = registerDefinitions(); private static final ResourceKey CREATIVE_TAB_KEY = ResourceKey.create( @@ -74,6 +78,17 @@ public final class SanctuaryCapes { return definition == null ? null : definition.texture(); } + public static boolean isBannerCape(String itemId) { + return WHITE_CAPE_ID.toString().equals(itemId); + } + + public static CapeBannerAppearance bannerAppearance(ItemStack stack) { + if (!WHITE_CAPE_ID.toString().equals(itemId(stack))) return null; + return new CapeBannerAppearance( + stack.getOrDefault(DataComponents.BASE_COLOR, DyeColor.WHITE), + stack.getOrDefault(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY)); + } + public static List definitions() { return List.copyOf(DEFINITIONS.values()); } @@ -83,8 +98,13 @@ public final class SanctuaryCapes { for (String path : PATHS) { Identifier id = SanctuaryMod.id(path); ResourceKey key = ResourceKey.create(Registries.ITEM, id); + Item.Properties properties = new Item.Properties().stacksTo(1).setId(key); + if (id.equals(WHITE_CAPE_ID)) { + properties.component(DataComponents.BASE_COLOR, DyeColor.WHITE) + .component(DataComponents.BANNER_PATTERNS, BannerPatternLayers.EMPTY); + } Item item = Registry.register(BuiltInRegistries.ITEM, key, - new SanctuaryCapeItem(new Item.Properties().stacksTo(1).setId(key))); + new SanctuaryCapeItem(properties)); definitions.put(id, new Definition(id, item, SanctuaryMod.id("entity/cape/" + path))); } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AvatarRenderStateMixin.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AvatarRenderStateMixin.java new file mode 100644 index 0000000..fe64492 --- /dev/null +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AvatarRenderStateMixin.java @@ -0,0 +1,23 @@ +package fr.koka99cab.sanctuary26.sanctuary.mixin.client; + +import fr.koka99cab.sanctuary26.sanctuary.client.BannerCapeRenderState; +import fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeBannerAppearance; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +/** Stores the synchronized banner appearance on the transient avatar render state. */ +@Mixin(AvatarRenderState.class) +public final class AvatarRenderStateMixin implements BannerCapeRenderState { + @Unique private CapeBannerAppearance sanctuary$bannerCape; + + @Override + public CapeBannerAppearance sanctuary$bannerCape() { + return sanctuary$bannerCape; + } + + @Override + public void sanctuary$setBannerCape(CapeBannerAppearance appearance) { + sanctuary$bannerCape = appearance; + } +} diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AvatarRendererMixin.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AvatarRendererMixin.java index a89c0da..2f2b9c5 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AvatarRendererMixin.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AvatarRendererMixin.java @@ -1,6 +1,7 @@ package fr.koka99cab.sanctuary26.sanctuary.mixin.client; import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClientCapes; +import fr.koka99cab.sanctuary26.sanctuary.client.BannerCapeRenderState; import net.minecraft.client.renderer.entity.player.AvatarRenderer; import net.minecraft.client.renderer.entity.state.AvatarRenderState; import net.minecraft.core.ClientAsset; @@ -21,11 +22,15 @@ abstract class AvatarRendererMixin { ) private void sanctuary$applyEquippedCape(Avatar avatar, AvatarRenderState state, float partialTick, CallbackInfo ci) { - Identifier texture = SanctuaryClientCapes.texture(avatar.getUUID()); + SanctuaryClientCapes.Visual visual = SanctuaryClientCapes.visual(avatar.getUUID()); + ((BannerCapeRenderState) (Object) state).sanctuary$setBannerCape( + visual == null ? null : visual.banner()); + Identifier texture = visual == null ? null : visual.texture(); PlayerSkin skin = state.skin; if (texture == null || skin == null) return; + state.showCape = true; + if (visual.banner() != null) return; state.skin = PlayerSkin.insecure(skin.body(), new ClientAsset.ResourceTexture(texture), skin.elytra(), skin.model()); - state.showCape = true; } } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/CapeSyncPayload.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/CapeSyncPayload.java index 034f943..5e8adae 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/CapeSyncPayload.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/CapeSyncPayload.java @@ -1,29 +1,44 @@ package fr.koka99cab.sanctuary26.sanctuary.network; import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod; +import fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeBannerAppearance; import java.util.UUID; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.block.entity.BannerPatternLayers; /** Announces one player's currently equipped Sanctuary cape to clients. */ -public record CapeSyncPayload(UUID playerId, String capeItemId) implements CustomPacketPayload { +public record CapeSyncPayload(UUID playerId, String capeItemId, DyeColor baseColor, + BannerPatternLayers bannerPatterns) implements CustomPacketPayload { public static final Type ID = new Type<>(SanctuaryMod.id("s2c/cape_sync")); + private static final StreamCodec BOUNDED_PATTERNS_CODEC = + BannerPatternLayers.Layer.STREAM_CODEC + .apply(ByteBufCodecs.list(CapeBannerAppearance.MAX_PATTERNS)) + .map(BannerPatternLayers::new, BannerPatternLayers::layers); public static final StreamCodec CODEC = CustomPacketPayload.codec(CapeSyncPayload::write, CapeSyncPayload::new); public CapeSyncPayload { if (playerId == null) throw new IllegalArgumentException("Cape player UUID cannot be null"); capeItemId = capeItemId == null ? "" : capeItemId; + CapeBannerAppearance bounded = new CapeBannerAppearance(baseColor, bannerPatterns); + baseColor = bounded.baseColor(); + bannerPatterns = bounded.patterns(); } private CapeSyncPayload(RegistryFriendlyByteBuf buffer) { - this(buffer.readUUID(), buffer.readUtf(128)); + this(buffer.readUUID(), buffer.readUtf(128), DyeColor.STREAM_CODEC.decode(buffer), + BOUNDED_PATTERNS_CODEC.decode(buffer)); } private void write(RegistryFriendlyByteBuf buffer) { buffer.writeUUID(playerId); buffer.writeUtf(capeItemId, 128); + DyeColor.STREAM_CODEC.encode(buffer, baseColor); + BOUNDED_PATTERNS_CODEC.encode(buffer, bannerPatterns); } @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 1cffe80..a03f9dd 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 @@ -23,7 +23,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 = 22; + public static final int PROTOCOL_VERSION = 23; private static boolean initialized; private SanctuaryNetworking() { diff --git a/sanctuary/src/main/resources/assets/sanctuary/items/white_cape.json b/sanctuary/src/main/resources/assets/sanctuary/items/white_cape.json new file mode 100644 index 0000000..eaa3b0a --- /dev/null +++ b/sanctuary/src/main/resources/assets/sanctuary/items/white_cape.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "sanctuary:item/white_cape" + } +} diff --git a/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json b/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json index 341de47..8f76a95 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json +++ b/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json @@ -505,5 +505,6 @@ "item.sanctuary.minecon_2011_cape": "Minecon 2011 Cape", "item.sanctuary.mojang_classic_cape": "Mojang Classic Cape", "item.sanctuary.blue_scroll_cape": "Blue Scroll Cape", + "item.sanctuary.white_cape": "White Cape", "dimension.sanctuary.alpha": "Alpha / Indev Island" } diff --git a/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json b/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json index ae46910..5036859 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json +++ b/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json @@ -505,5 +505,6 @@ "item.sanctuary.minecon_2011_cape": "Cape Minecon 2011", "item.sanctuary.mojang_classic_cape": "Cape Mojang classique", "item.sanctuary.blue_scroll_cape": "Cape parchemin bleu", + "item.sanctuary.white_cape": "Cape blanche", "dimension.sanctuary.alpha": "Île Alpha / Indev" } diff --git a/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json b/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json index 0e47ea1..bd33b3b 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json +++ b/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json @@ -461,5 +461,6 @@ "item.sanctuary.minecon_2011_cape": "Плащ Minecon 2011", "item.sanctuary.mojang_classic_cape": "Классический плащ Mojang", "item.sanctuary.blue_scroll_cape": "Плащ синего свитка", + "item.sanctuary.white_cape": "Белый плащ", "dimension.sanctuary.alpha": "Остров Альфа / Indev" } diff --git a/sanctuary/src/main/resources/assets/sanctuary/models/item/white_cape.json b/sanctuary/src/main/resources/assets/sanctuary/models/item/white_cape.json new file mode 100644 index 0000000..7e810cc --- /dev/null +++ b/sanctuary/src/main/resources/assets/sanctuary/models/item/white_cape.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "sanctuary:item/cape/white_cape" + } +} diff --git a/sanctuary/src/main/resources/assets/sanctuary/textures/entity/cape/white_cape.png b/sanctuary/src/main/resources/assets/sanctuary/textures/entity/cape/white_cape.png new file mode 100644 index 0000000..11f5535 Binary files /dev/null and b/sanctuary/src/main/resources/assets/sanctuary/textures/entity/cape/white_cape.png differ diff --git a/sanctuary/src/main/resources/assets/sanctuary/textures/item/cape/white_cape.png b/sanctuary/src/main/resources/assets/sanctuary/textures/item/cape/white_cape.png new file mode 100644 index 0000000..005371a Binary files /dev/null and b/sanctuary/src/main/resources/assets/sanctuary/textures/item/cape/white_cape.png differ diff --git a/sanctuary/src/main/resources/data/sanctuary/recipe/banner_cape.json b/sanctuary/src/main/resources/data/sanctuary/recipe/banner_cape.json new file mode 100644 index 0000000..a3504b1 --- /dev/null +++ b/sanctuary/src/main/resources/data/sanctuary/recipe/banner_cape.json @@ -0,0 +1,8 @@ +{ + "type": "minecraft:crafting_special_shielddecoration", + "banner": "#minecraft:banners", + "result": { + "id": "sanctuary:white_cape" + }, + "target": "sanctuary:white_cape" +} diff --git a/sanctuary/src/main/resources/fabric.mod.json b/sanctuary/src/main/resources/fabric.mod.json index 4c86231..b3429e7 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": 22, + "network_protocol": 23, "lifecycle": "${lifecycle}", "pack_version": "${pack_version}", "descriptions": { diff --git a/sanctuary/src/main/resources/sanctuary.mixins.json b/sanctuary/src/main/resources/sanctuary.mixins.json index c788bf4..f6e559f 100644 --- a/sanctuary/src/main/resources/sanctuary.mixins.json +++ b/sanctuary/src/main/resources/sanctuary.mixins.json @@ -17,6 +17,7 @@ ], "client": [ "client.AbstractButtonMixin", + "client.AvatarRenderStateMixin", "client.AvatarRendererMixin", "client.AbstractContainerScreenAccessor", "client.AbstractContainerScreenMixin", diff --git a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/CapeBannerAppearanceSmoke.java b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/CapeBannerAppearanceSmoke.java new file mode 100644 index 0000000..ab5607d --- /dev/null +++ b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/CapeBannerAppearanceSmoke.java @@ -0,0 +1,33 @@ +package fr.koka99cab.sanctuary26.sanctuary.gameplay; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.block.entity.BannerPatternLayers; + +/** Executable coverage for the network and renderer banner-layer bound. */ +public final class CapeBannerAppearanceSmoke { + private CapeBannerAppearanceSmoke() { + } + + public static void main(String[] args) { + List source = new ArrayList<>(); + for (int index = 0; index < 20; index++) { + source.add(new BannerPatternLayers.Layer(null, DyeColor.RED)); + } + CapeBannerAppearance bounded = new CapeBannerAppearance( + DyeColor.BLUE, new BannerPatternLayers(source)); + require(bounded.baseColor() == DyeColor.BLUE, "Banner cape base color changed"); + require(bounded.patterns().layers().size() == CapeBannerAppearance.MAX_PATTERNS, + "Banner cape patterns escaped their network/render bound"); + source.clear(); + require(bounded.patterns().layers().size() == CapeBannerAppearance.MAX_PATTERNS, + "Banner cape retained a mutable source list"); + require(new CapeBannerAppearance(null, null).equals(CapeBannerAppearance.WHITE), + "Missing banner components did not fall back to a plain white cape"); + } + + private static void require(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } +}