Compare commits

...
47 changed files with 774 additions and 128 deletions
+15
View File
@@ -6,6 +6,21 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
- Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46.
- Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées.
## Non distribué
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 ;
- ajoute le refus explicite en français, anglais et russe.
- remplace les quêtes saphir de Phantoms, désactivés dans Sanctuary, par les fantômes disponibles dIt's Alive ;
- 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 sappliquent aussi aux créatures aquatiques comme les poulpes.
- permet aux petites et grandes boules de feu lancées par les pouvoirs de compagnons dallumer 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 nimporte 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`
Modules modifiés : **Red-Stoner `0.0.0-alpha.13`** et **Sanctuary `0.0.0-alpha.124`**. Les huit autres modules conservent leur version issue de l'alpha.221.
+39 -2
View File
@@ -1647,6 +1647,7 @@ tasks.register("generateAnotherWorldResources") {
"message.anotherworld.safe_vault.empty_before_break": "Videz le coffre-fort et attendez la fin du forage avant de le démonter.",
"message.anotherworld.safe_vault.drill_own": "Vous ne pouvez pas forer votre propre coffre-fort.",
"message.anotherworld.safe_vault.inactive": "Ce coffre-fort nest pas actif.",
"message.anotherworld.safe_vault.raid_requires_vault": "Vous devez posséder un coffre-fort actif pour en braquer un autre.",
"message.anotherworld.safe_vault.empty": "Ce coffre-fort est vide.",
"message.anotherworld.safe_vault.owner_offline": "Le propriétaire doit être connecté pour que le coffre-fort soit attaquable.",
"message.anotherworld.safe_vault.protected": "Ce coffre-fort reste protégé pendant environ %s minute(s).",
@@ -1671,6 +1672,7 @@ tasks.register("generateAnotherWorldResources") {
"message.anotherworld.safe_vault.empty_before_break": "Опустошите сейф и дождитесь окончания сверления перед разборкой.",
"message.anotherworld.safe_vault.drill_own": "Нельзя сверлить собственный сейф.",
"message.anotherworld.safe_vault.inactive": "Этот сейф не активен.",
"message.anotherworld.safe_vault.raid_requires_vault": "Чтобы ограбить чужой сейф, у вас должен быть собственный активный сейф.",
"message.anotherworld.safe_vault.empty": "Этот сейф пуст.",
"message.anotherworld.safe_vault.owner_offline": "Владелец должен быть в сети, чтобы атаковать сейф.",
"message.anotherworld.safe_vault.protected": "Сейф защищён ещё примерно %s минут(ы).",
@@ -1695,6 +1697,7 @@ tasks.register("generateAnotherWorldResources") {
"message.anotherworld.safe_vault.empty_before_break": "Empty the safe and wait for drilling to end before dismantling it.",
"message.anotherworld.safe_vault.drill_own": "You cannot drill your own safe.",
"message.anotherworld.safe_vault.inactive": "This safe is not active.",
"message.anotherworld.safe_vault.raid_requires_vault": "You must own an active safe before raiding another one.",
"message.anotherworld.safe_vault.empty": "This safe is empty.",
"message.anotherworld.safe_vault.owner_offline": "The owner must be online before the safe can be raided.",
"message.anotherworld.safe_vault.protected": "This safe remains protected for about %s minute(s).",
@@ -1818,6 +1821,7 @@ tasks.register("verifyAnotherWorldCatalog") {
description = "Checks the Another World alpha.51 catalog, player vault and climate-aware biome mixer."
dependsOn tasks.named("generateAnotherWorldResources")
inputs.file(file("src/main/resources/data/anotherworld/sanctuary_biome_catalog.json"))
inputs.file(file("src/main/resources/data/minecraft/tags/item/swords.json"))
inputs.file(rootProject.file("sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary.json"))
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.90-mossy-forest-hover-to-alpha.91.json"))
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.100-mossy-forest-lush-to-alpha.101.json"))
@@ -1953,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.55"
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")')
@@ -3231,6 +3235,10 @@ tasks.register("verifyAnotherWorldCatalog") {
["anotherworld:deepslate_silver_ore", "anotherworld:deepslate_titanium_ore"].each { requiredOre ->
if (!diamondToolValues.contains(requiredOre)) throw new GradleException("Diamond pickaxe requirement missing for ${requiredOre}")
}
def swordTag = readJson("data/minecraft/tags/item/swords.json")
if (swordTag.replace != false || !swordTag.values.contains("anotherworld:titanium_sword")) {
throw new GradleException("Titanium sword must participate in the vanilla sword contract")
}
def expectedColoredStairs = colors.collect { "anotherworld:${it}_brick_stairs".toString() }
def expectedColoredSlabs = colors.collect { "anotherworld:${it}_brick_slab".toString() }
["block", "item"].each { registry ->
@@ -3260,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."
@@ -3270,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.55"
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"
@@ -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;
}
}
@@ -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())
@@ -113,6 +113,12 @@ public final class VaultData extends SavedData {
&& address.pos() == center.asLong() && address.owner().equals(ownerId));
}
public boolean hasRegisteredVault(UUID owner) {
if (owner == null) return false;
String ownerId = owner.toString();
return vaults.stream().anyMatch(address -> address.owner().equals(ownerId));
}
public void unregisterNear(ServerLevel level, BlockPos member) {
String dimension = dimension(level);
boolean changed = vaults.removeIf(address -> {
@@ -163,7 +169,7 @@ public final class VaultData extends SavedData {
/** Transfers the exact 20% raid share into the attacker's unlimited balances without item spam. */
public long transferRaid(UUID owner, UUID attacker) {
if (owner == null || attacker == null || owner.equals(attacker)) return 0L;
if (!canReceiveRaidLoot(owner, attacker, hasRegisteredVault(attacker))) return 0L;
Account source = accounts.get(owner);
if (source == null) return 0L;
Account target = accounts.computeIfAbsent(attacker, ignored -> new Account());
@@ -180,6 +186,10 @@ public final class VaultData extends SavedData {
return total;
}
static boolean canReceiveRaidLoot(UUID owner, UUID attacker, boolean attackerHasRegisteredVault) {
return owner != null && attacker != null && !owner.equals(attacker) && attackerHasRegisteredVault;
}
public long protectionRemaining(UUID owner, long gameTime) {
Account account = accounts.get(owner);
return account == null ? 0L : Math.max(0L, account.protectedUntil - gameTime);
@@ -48,6 +48,10 @@ public final class VaultDrillingService {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.inactive"));
return false;
}
if (!data.hasRegisteredVault(attacker.getUUID())) {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.raid_requires_vault"));
return false;
}
if (!data.hasContents(formed.owner())) {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.empty"));
return false;
@@ -104,6 +108,13 @@ public final class VaultDrillingService {
attacker.stopUsingItem();
return;
}
if (!VaultData.get(level.getServer()).hasRegisteredVault(attacker.getUUID())) {
cancel(attacker, false);
attacker.sendOverlayMessage(Component.translatable(
"message.anotherworld.safe_vault.raid_requires_vault"));
attacker.stopUsingItem();
return;
}
int elapsed = session.elapsedTicks() + 1;
Session progressed = session.withElapsed(elapsed);
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"anotherworld:titanium_sword"
]
}
@@ -1,5 +1,7 @@
package fr.koka99cab.sanctuary26.anotherworld.vault;
import java.util.UUID;
public final class VaultRulesSmoke {
private VaultRulesSmoke() {
}
@@ -55,6 +57,14 @@ public final class VaultRulesSmoke {
require(VaultRules.raidLoot(Integer.MAX_VALUE) == 429_496_730L, "32-bit balance raid");
require(VaultRules.raidLoot(Long.MAX_VALUE) == 1_844_674_407_370_955_162L,
"64-bit balance raid without overflow");
UUID owner = UUID.fromString("00000000-0000-0000-0000-000000000001");
UUID attacker = UUID.fromString("00000000-0000-0000-0000-000000000002");
require(!VaultData.canReceiveRaidLoot(owner, attacker, false),
"an attacker without a registered vault cannot receive raid loot");
require(VaultData.canReceiveRaidLoot(owner, attacker, true),
"an attacker with a registered vault can receive raid loot");
require(!VaultData.canReceiveRaidLoot(owner, owner, true),
"a vault owner cannot receive loot from their own vault");
require(VaultRules.saturatingAdd(Long.MAX_VALUE - 2L, 10L) == Long.MAX_VALUE,
"balance saturation without overflow");
for (long balance : new long[] {0L, Integer.MAX_VALUE, (long) Integer.MAX_VALUE + 1L, Long.MAX_VALUE}) {
+47 -42
View File
@@ -3438,8 +3438,8 @@ 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.124"
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|| 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"]
|| migration.target?.modules != [sanctuary: "0.0.0-alpha.109", 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.55"
|| 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"
@@ -3749,7 +3749,7 @@ project(":onlyfun") {
def shitCubeRecipe = new JsonSlurper().parse(file(
"src/main/resources/data/abjection/recipe/shit_cube.json"))
if (manifest.custom.sanctuary26.data_version != 2
|| project.version.toString() != "0.0.0-alpha.25"
|| project.version.toString() != "0.0.0-alpha.26"
|| manifest.depends?.sanctuary != "*"
|| manifest.provides != ["abjection"]
|| manifest.custom.sanctuary26.network_protocol != 3
@@ -4278,8 +4278,8 @@ tasks.register("verifyShopPricingLayoutRelease") {
def release = new JsonSlurper().parse(file("pack/release.json"))
def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
if (rootProject.pack_version != "26.2.0-alpha.222"
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|| rootProject.sanctuary_version != "0.0.0-alpha.124"
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|| 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"
@@ -4372,8 +4372,8 @@ tasks.register("verifyShopCsvBossGatesRelease") {
def catalog = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalog.java").text
def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
if (rootProject.pack_version != "26.2.0-alpha.222"
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|| rootProject.sanctuary_version != "0.0.0-alpha.124"
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|| 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.124"
|| 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.124"
|| 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.124"
|| 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.55"
|| 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.124"
|| 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.55"
|| rootProject.sanctuary_version != "0.0.0-alpha.124"
|| 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.55"
|| 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.55"
|| 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.55"
|| 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.55"
|| 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.55"
|| 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"
@@ -5643,7 +5643,7 @@ tasks.register("verifyCanapliaAlpha209Release") {
if (rootProject.pack_version != "26.2.0-alpha.222"
|| rootProject.ambiance_version != "0.0.0-alpha.22"
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|| migration.source?.pack_version != "26.2.0-alpha.207"
|| migration.source?.modules != expectedSource
|| migration.target?.pack_version != "26.2.0-alpha.208"
@@ -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.124"
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|| rootProject.iliketomoveit_version != currentModules.iliketomoveit
|| rootProject.sanctuary_version != "0.0.0-alpha.126"
|| rootProject.onlyfun_version != "0.0.0-alpha.26"
|| rootProject.iliketomoveit_version != "0.0.0-alpha.17"
|| rootProject.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
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.124"
|| 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,6 +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.126",
iliketomoveit: "0.0.0-alpha.17"]
def requiredJars = currentModules.collect { id, version ->
"file = \"mods/${id}-${version}.jar\""
}
@@ -6446,10 +6450,10 @@ tasks.register("verifyAlpha218Release") {
}
if (rootProject.pack_version != "26.2.0-alpha.222"
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|| currentModules.findAll { id, version -> id != "redstoner" && id != "sanctuary" }.any {
|| workspaceModules.findAll { id, version -> id != "redstoner" && id != "sanctuary" }.any {
id, version -> rootProject.property("${id}_version") != version
}
|| rootProject.sanctuary_version != "0.0.0-alpha.124"
|| 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"
@@ -6474,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")
@@ -6533,6 +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.126", onlyfun: "0.0.0-alpha.26",
iliketomoveit: "0.0.0-alpha.17"]
def requiredJars = currentModules.collect { id, version ->
"file = \"mods/${id}-${version}.jar\""
}
@@ -6548,7 +6554,7 @@ tasks.register("verifyAlpha219Release") {
exclude "**/build/**", "**/.gradle/**", "**/.git/**"
}.files
if (rootProject.pack_version != "26.2.0-alpha.222"
|| currentModules.any { id, version -> rootProject.property("${id}_version") != version }
|| workspaceModules.any { id, version -> rootProject.property("${id}_version") != version }
|| migration.source?.pack_version != "26.2.0-alpha.218"
|| migration.source?.modules != sourceModules
|| migration.target?.pack_version != "26.2.0-alpha.219"
@@ -6573,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
@@ -6857,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.124"
|| 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"]
@@ -6900,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")
@@ -6943,7 +6949,6 @@ tasks.register("verifyAlpha222Release") {
|| jeiFiles[0].hashes?.sha1 != "c2f2c6b5822a560fe9872921b908ffc2f3297d00"
|| jeiFiles[0].hashes?.sha512 != "c62a3e264d4b0ec398b9aa717a4832eecb8663bbcec4483dc58a9999d5e4e55ca4f5bca285a0d4a003220c698c3e87c8a3294366e085e72545e4afe84aec92c7"
|| !changelog.contains("## `26.2.0-alpha.222`")
|| changelog.contains("## Non distribué")
|| !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.222`**")) {
throw new GradleException("The alpha.222 terminal-search and quest-tracker release is incomplete")
}
+4 -4
View File
@@ -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.55
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.25
onlyfun_version=0.0.0-alpha.26
onlyfun_lifecycle=active
sanctuary_version=0.0.0-alpha.124
sanctuary_version=0.0.0-alpha.126
sanctuary_lifecycle=active
pack_version=26.2.0-alpha.222
maven_group=fr.koka99cab.sanctuary26
+11 -6
View File
@@ -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"
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -45,11 +45,22 @@ public final class PortalDefinitions {
public static List<PortalDefinition> 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<PortalDefinition> matchingFrame(BlockState frame) {
return definitions.values().stream()
.filter(definition -> definition.matchesFrame(frame))
.sorted(priorityOrder())
.toList();
}
private static Comparator<PortalDefinition> priorityOrder() {
return Comparator.comparingInt(PortalDefinition::priority).reversed()
.thenComparing(definition -> definition.id().toString());
}
static void replace(Map<Identifier, PortalDefinition> replacement) {
List<Identifier> missing = new ArrayList<>(REQUIRED_ROUTES);
missing.removeAll(replacement.keySet());
@@ -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<PortalFrame> 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<IgnitionTarget> 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<IgnitionTarget> 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<IgnitionTarget> findTarget(net.minecraft.world.level.LevelReader level,
BlockPos interior, Iterable<PortalDefinition> definitions) {
for (PortalDefinition definition : definitions) {
Optional<PortalFrame> 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) {}
}
@@ -3,7 +3,9 @@
"package": "fr.koka99cab.sanctuary26.iliketomoveit.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"HappyGhastMixin"
"HappyGhastMixin",
"LargeFireballMixin",
"SmallFireballMixin"
],
"injectors": {
"defaultRequire": 1
@@ -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<Level> 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);
}
@@ -21,8 +21,9 @@ public final class OnlyFunItemPolicy {
"budding_amethyst", "petrified_oak_slab", "suspicious_sand", "suspicious_gravel",
"air", "cave_air", "void_air", "fire", "soul_fire", "water", "lava",
"nether_portal", "end_portal", "end_gateway", "frosted_ice", "farmland", "dirt_path",
"dragon_egg", "filled_map", "written_book", "enchanted_book"
"dragon_egg", "filled_map", "written_book"
);
private static final Set<String> SHOP_ONLY_BLOCKS = Set.of("enchanted_book");
private static final Set<String> VANILLA_SHOP_GAMEPLAY_BYPASSES = Set.of(
"baked_potato", "beetroot_soup", "bread", "cake",
"cooked_beef", "cooked_chicken", "cooked_cod", "cooked_mutton",
@@ -45,7 +46,8 @@ public final class OnlyFunItemPolicy {
if (namespace.equals("minecraft") && VANILLA_SHOP_GAMEPLAY_BYPASSES.contains(path)) return true;
if (namespace.equals("anotherworld") && isProtectedAnotherWorldItem(path)) return true;
if (isStorageTerminal(namespace, path)) return true;
if (TECHNICAL_OR_REWARD_BLOCKS.contains(path) || path.startsWith("infested_")
if (SHOP_ONLY_BLOCKS.contains(path) || TECHNICAL_OR_REWARD_BLOCKS.contains(path)
|| path.startsWith("infested_")
|| path.contains("command_block")) return true;
if (path.contains("lootbox") || path.contains("loot_box")) return true;
if (namespace.equals("onlyfun") && path.equals("lucky_block")) return true;
@@ -19,6 +19,8 @@ public final class OnlyFunGeneratorSmoke {
OnlyFunShopCatalogSmoke.verify();
CobbleDungeonPlacementSmoke.verify();
OnlyFunGenerator.initialize();
require(OnlyFunGenerator.outcomes().stream().anyMatch(outcome -> outcome.id().equals("enchanted_book")),
"The enchanted-book lootbox outcome disappeared");
require(OnlyFunGenerator.ANOMALY_DENOMINATOR == 256, "Anomaly odds changed from 1/256");
require(OnlyFunGenerator.PITY_START == 24, "Pity threshold changed");
require(OnlyFunGenerator.outcomes().stream().map(OnlyFunOutcome::id).distinct().count()
@@ -10,6 +10,7 @@ import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.SpawnEggItem;
/** Registry-backed smoke: every eligible vanilla block and spawn egg appears exactly once. */
@@ -88,6 +89,13 @@ public final class OnlyFunShopCatalogSmoke {
"Storage Terminal reached lootbox rewards");
require(find(all, storageTerminal.toString()) == null,
"Storage Terminal reached the generated Shop catalogue");
Identifier enchantedBook = Identifier.parse("minecraft:enchanted_book");
require(OnlyFunItemPolicy.excludedFromShop(enchantedBook),
"Enchanted books must remain excluded from the Shop");
require(!OnlyFunItemPolicy.excludedFromRewards(Items.ENCHANTED_BOOK),
"Enchanted-book lootbox rewards must be deliverable");
require(find(all, enchantedBook.toString()) == null,
"Enchanted books reached the generated Shop catalogue");
require(OnlyFunBossShopGate.requiresBoss("iliketomoveit:magic_carpet"),
"Magic Carpet must remain boss-gated");
require(OnlyFunBossShopGate.requiresBoss("iliketomoveit:magic_cloud"),
+78 -16
View File
@@ -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.124"
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.124"
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.124"
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.124"
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.124"
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.124"
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.124"
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.124"
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.124"
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.124"
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.124"
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")
@@ -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<AvatarRenderState, PlayerModel> {
private final BannerCapeModel model = new BannerCapeModel();
private final SpriteGetter sprites;
private final EquipmentAssetManager equipmentAssets;
private BannerCapeLayer(RenderLayerParent<AvatarRenderState, PlayerModel> 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<AvatarRenderState, PlayerModel> parent =
(RenderLayerParent<AvatarRenderState, PlayerModel>) (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();
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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) ->
@@ -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<UUID, Identifier> TEXTURES = new HashMap<>();
private static final Map<UUID, Visual> 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) {}
}
@@ -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<BannerPatternLayers.Layer> layers = patterns == null
? List.of() : patterns.layers();
if (layers.size() > MAX_PATTERNS) layers = layers.subList(0, MAX_PATTERNS);
patterns = new BannerPatternLayers(List.copyOf(layers));
}
}
@@ -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()));
}
}
@@ -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<String> 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<Identifier, Definition> DEFINITIONS = registerDefinitions();
private static final ResourceKey<CreativeModeTab> 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<Definition> definitions() {
return List.copyOf(DEFINITIONS.values());
}
@@ -83,8 +98,13 @@ public final class SanctuaryCapes {
for (String path : PATHS) {
Identifier id = SanctuaryMod.id(path);
ResourceKey<Item> 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)));
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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<CapeSyncPayload> ID = new Type<>(SanctuaryMod.id("s2c/cape_sync"));
private static final StreamCodec<RegistryFriendlyByteBuf, BannerPatternLayers> BOUNDED_PATTERNS_CODEC =
BannerPatternLayers.Layer.STREAM_CODEC
.apply(ByteBufCodecs.list(CapeBannerAppearance.MAX_PATTERNS))
.map(BannerPatternLayers::new, BannerPatternLayers::layers);
public static final StreamCodec<RegistryFriendlyByteBuf, CapeSyncPayload> 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<? extends CustomPacketPayload> type() { return ID; }
@@ -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() {
@@ -17,7 +17,8 @@ import net.minecraft.world.level.saveddata.SavedDataType;
/** Server-authoritative state for physical, renewable quest signs. */
public final class QuestBoardData extends SavedData {
public static final String SAVE_KEY = "black_market_quests";
public static final int DATA_VERSION = 1;
private static final int LEGACY_DATA_VERSION = 1;
public static final int DATA_VERSION = 2;
private static final int MAX_AUDIT_ENTRIES = 4096;
private static final Codec<Objective> OBJECTIVE_CODEC = RecordCodecBuilder.create(instance -> instance.group(
@@ -66,7 +67,7 @@ public final class QuestBoardData extends SavedData {
Codec.STRING.optionalFieldOf("detail", "").forGetter(AuditEntry::detail)
).apply(instance, AuditEntry::new));
private static final Codec<QuestBoardData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(data -> data.dataVersion),
Codec.INT.optionalFieldOf("data_version", LEGACY_DATA_VERSION).forGetter(data -> data.dataVersion),
BOARD_CODEC.listOf().optionalFieldOf("boards", List.of()).forGetter(data -> data.boards),
AUDIT_CODEC.listOf().optionalFieldOf("audit", List.of()).forGetter(data -> data.audit)
).apply(instance, QuestBoardData::new));
@@ -79,7 +80,7 @@ public final class QuestBoardData extends SavedData {
public QuestBoardData() { this(DATA_VERSION, List.of(), List.of()); }
private QuestBoardData(int dataVersion, List<Board> boards, List<AuditEntry> audit) {
this.dataVersion = DATA_VERSION;
this.dataVersion = Math.max(LEGACY_DATA_VERSION, dataVersion);
this.boards = new ArrayList<>(boards);
this.audit = new ArrayList<>(audit.stream().skip(Math.max(0, audit.size() - MAX_AUDIT_ENTRIES)).toList());
}
@@ -88,10 +89,27 @@ public final class QuestBoardData extends SavedData {
ServerLevel overworld = server.getLevel(Level.OVERWORLD);
if (overworld == null) throw new IllegalStateException("Overworld unavailable for quest-board data");
QuestBoardData data = overworld.getDataStorage().computeIfAbsent(TYPE);
if (data.dataVersion < DATA_VERSION) { data.dataVersion = DATA_VERSION; data.setDirty(); }
if (data.migrateToCurrent()) data.setDirty();
return data;
}
private boolean migrateToCurrent() {
if (dataVersion >= DATA_VERSION) return false;
if (dataVersion < 2) {
for (int index = 0; index < boards.size(); index++) {
Board board = boards.get(index);
Contract contract = board.contract().orElse(null);
if (contract == null) continue;
Objective migrated = QuestBoardGenerator.migrateUnavailableTarget(contract.objective());
if (migrated != contract.objective()) {
boards.set(index, board.withContract(Optional.of(contract.withObjective(migrated))));
}
}
}
dataVersion = DATA_VERSION;
return true;
}
public List<Board> boards() { return List.copyOf(boards); }
public Optional<Board> board(String id) { return boards.stream().filter(value -> value.id.equals(id)).findFirst(); }
public Optional<Board> boardAt(String dimension, long pos) {
@@ -46,7 +46,7 @@ final class QuestBoardGenerator {
private static final List<Template> HARD = List.of(
new Template(KILL_EXACT, "minecraft:wither_skeleton", 12, 24),
new Template(KILL_EXACT, "minecraft:blaze", 16, 28),
new Template(KILL_EXACT, "minecraft:phantom", 16, 28),
new Template(KILL_EXACT, "itsalive:ghost", 16, 28),
new Template(KILL_EXACT, "minecraft:enderman", 24, 40),
new Template(DELIVER_EXACT, "minecraft:diamond", 3, 7),
new Template(DELIVER_EXACT, "minecraft:ghast_tear", 4, 8),
@@ -78,11 +78,7 @@ final class QuestBoardGenerator {
long day = Math.floorDiv(now, 86_400_000L);
long seed = boardId.hashCode() * 0x9E3779B97F4A7C15L ^ day * 0xD1B54A32D192ED03L ^ tier.ordinal();
RandomSource random = RandomSource.create(seed);
List<Template> pool = switch (tier) {
case EMERALD -> EASY;
case RUBY -> MEDIUM;
case SAPPHIRE -> HARD;
};
List<Template> pool = templates(tier);
Template template = selectable(server, pool, random);
int count = template.minimum + random.nextInt(template.maximum - template.minimum + 1);
QuestBoardData.Objective objective = new QuestBoardData.Objective(template.kind, template.target, count, 0);
@@ -93,6 +89,28 @@ final class QuestBoardGenerator {
java.util.Optional.empty(), now);
}
private static List<Template> templates(QuestBoardTier tier) {
return switch (tier) {
case EMERALD -> EASY;
case RUBY -> MEDIUM;
case SAPPHIRE -> HARD;
};
}
static boolean offersKillTarget(QuestBoardTier tier, String target) {
return templates(tier).stream().anyMatch(template -> KILL_EXACT.equals(template.kind)
&& template.target.equals(target));
}
static QuestBoardData.Objective migrateUnavailableTarget(QuestBoardData.Objective objective) {
if (objective != null && KILL_EXACT.equals(objective.kind())
&& "minecraft:phantom".equals(objective.target())) {
return new QuestBoardData.Objective(objective.kind(), "itsalive:ghost",
objective.required(), objective.progress());
}
return objective;
}
static long nextRefreshAt(String boardId, long now) {
LocalDate tomorrow = Instant.ofEpochMilli(now).atZone(ZoneOffset.UTC).toLocalDate().plusDays(1);
long midnight = tomorrow.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli();
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:item/white_cape"
}
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "sanctuary:item/cape/white_cape"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

@@ -0,0 +1,8 @@
{
"type": "minecraft:crafting_special_shielddecoration",
"banner": "#minecraft:banners",
"result": {
"id": "sanctuary:white_cape"
},
"target": "sanctuary:white_cape"
}
+1 -1
View File
@@ -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": {
@@ -17,6 +17,7 @@
],
"client": [
"client.AbstractButtonMixin",
"client.AvatarRenderStateMixin",
"client.AvatarRendererMixin",
"client.AbstractContainerScreenAccessor",
"client.AbstractContainerScreenMixin",
@@ -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<BannerPatternLayers.Layer> 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);
}
}
@@ -35,6 +35,16 @@ public final class QuestBoardModelSmoke {
"A mismatched dedicated sign accepted the wrong quest tier");
require(QuestBoardService.canActivateDedicatedSign(QuestBoardTier.SAPPHIRE, QuestBoardTier.SAPPHIRE),
"A dedicated quest sign rejected its own tier");
require(!QuestBoardGenerator.offersKillTarget(QuestBoardTier.SAPPHIRE, "minecraft:phantom"),
"Sapphire quests still offer disabled vanilla phantoms");
require(QuestBoardGenerator.offersKillTarget(QuestBoardTier.SAPPHIRE, "itsalive:ghost"),
"Sapphire quests do not offer the available ghost replacement");
QuestBoardData.Objective legacyPhantom = new QuestBoardData.Objective(
QuestBoardGenerator.KILL_EXACT, "minecraft:phantom", 16, 7);
QuestBoardData.Objective migratedGhost = QuestBoardGenerator.migrateUnavailableTarget(legacyPhantom);
require(migratedGhost.target().equals("itsalive:ghost")
&& migratedGhost.required() == 16 && migratedGhost.progress() == 7,
"Saved phantom quest migration must preserve requirement and progress");
long now = 1_800_000_000_000L;
long refresh = QuestBoardGenerator.nextRefreshAt("board", now);
require(refresh > now && refresh - now < 28L * 60L * 60L * 1_000L,