Compare commits

...
Author SHA1 Message Date
koka fe491c0ebd chore(anotherworld): version safe vault fix
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-27 16:09:04 +02:00
koka 64b54c9b44 Merge remote-tracking branch 'origin/main' into feature/safe-vault-interface 2026-08-27 16:06:04 +02:00
koka 6b01d8f2f1 fix(anotherworld): finish safe vault formation 2026-08-27 16:03:59 +02:00
koka fd9fd1435b Merge pull request 'release: Sanctuary 26.2.0-alpha.203' (#20) from release/26.2.0-alpha.203 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #20
2026-08-27 13:23:35 +00:00
koka 1b9ee0aac0 release: prepare 26.2.0-alpha.203
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-27 15:19:15 +02:00
koka b0838ff9fd Merge pull request 'feat(anotherworld): add upright safe vault controls' (#19) from feature/safe-vault-interface into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #19
2026-08-27 13:13:01 +00:00
koka 95ad129656 feat(anotherworld): add upright safe vault controls
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-27 14:41:22 +02:00
koka 51c1ad0e11 Merge pull request 'release: Sanctuary 26.2.0-alpha.202' (#15) from release/26.2.0-alpha.202 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #15
Reviewed-by: koka <cnstiout@gmail.com>
2026-08-27 12:04:35 +00:00
koka dd305cd64e merge: include safe vault in alpha.202
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-27 14:02:29 +02:00
koka c9a8d601a8 Merge pull request 'feat(anotherworld): ajouter le coffre-fort joueur 3 × 3' (#16) from feature/safe-vault into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #16
Reviewed-by: botsu <botsu@botsu.net>
2026-08-27 11:51:01 +00:00
koka 9e302e12ef feat(anotherworld): add player safe vault raids
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-27 13:22:57 +02:00
koka 414afb15da release: prepare 26.2.0-alpha.202
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-27 12:20:14 +02:00
koka 0e1037168d Merge pull request 'feat(itsalive): poser le livre culinaire sur un pupitre vanilla' (#12) from feature/culinary-lectern-book into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #12
Reviewed-by: botsu <botsu@botsu.net>
2026-08-27 10:00:25 +00:00
koka e7359ac917 Merge pull request 'fix(sanctuary): compter rubis et saphirs aux rangs inventaire 4/5 et 5/5' (#10) from fix/shop-currency-overflow-rows into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #10
Reviewed-by: botsu <botsu@botsu.net>
2026-08-27 10:00:07 +00:00
koka b8cf6bcf41 Merge pull request 'fix(iliketomoveit): descendre vers une waystone inconnue (alpha.202)' (#9) from fix/waystone-ascenseur-descente into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #9
Reviewed-by: koka <cnstiout@gmail.com>
2026-08-27 09:58:47 +00:00
botsu 747162e2ae Merge pull request #11 from release/26.2.0-alpha.201
Verification 26.2 / check (push) Canceled after 0s
Sanctuary 26.2.0-alpha.201 — demandes financées et terminables
2026-08-27 09:46:59 +00:00
ChrisM-PekandCursor a802295c08 feat(itsalive): poser le livre culinaire sur un pupitre vanilla
Verification 26.2 / check (pull_request) Canceled after 0s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 10:55:32 +02:00
ChrisM-PekandCursor 3eed6b5a65 fix(sanctuary): compter aussi les gemmes au rang inventaire 4/5
Verification 26.2 / check (pull_request) Canceled after 0s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 10:27:12 +02:00
koka 452347e672 fix(iliketomoveit): autoriser la descente vers un etage inconnu
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-27 10:21:01 +02:00
ChrisM-PekandCursor 3deeb90ffc fix(sanctuary): compter rubis et saphirs sur toutes les rangees d'inventaire
Verification 26.2 / check (pull_request) Canceled after 0s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 09:17:29 +02:00
62 changed files with 2750 additions and 156 deletions
+39
View File
@@ -6,6 +6,45 @@ 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.
## En développement — correction finale du Safe Block
Module modifié : **Another World `0.0.0-alpha.52`**. Le pack distribué reste en **`26.2.0-alpha.203`**.
- charge la texture 16×16 du Safe Block depuis latlas de blocs afin de supprimer la texture manquante avant formation ;
- refuse explicitement les lignes horizontales ou verticales de 3 × 1 et nactive le CTM 48×48 quaprès validation des neuf blocs du mur 3 × 3 ;
- décale le panneau dinventaire étendu sous les trois lignes monétaires afin de rendre l’émeraude visible et laisse Sanctuary afficher un seul libellé dinventaire ;
- conserve les données v3, le protocole 1, les IDs et les soldes existants sans migration de sauvegarde.
## `26.2.0-alpha.203`
Module modifié : **Another World `0.0.0-alpha.51`**. Les neuf autres modules conservent leur version issue de lalpha.202.
- corrige la structure du Safe Block : les neuf segments doivent former un mur vertical exact de 3 × 3, et non une plateforme qui apparaît en 3 × 1 de face ;
- conserve un modèle cubique individuel en main et tant que la structure est incomplète ;
- active uniquement après formation les neuf modèles CTM qui découpent les textures 48×48 du coffre-fort ;
- remplace le coffre vanilla à 27 cases par une interface dédiée à trois soldes centrés — rubis, saphirs et émeraudes — avec les boutons ` ×64`, ` ×1`, `+ ×1` et `+ ×64` ;
- valide tous les transferts côté serveur, utilise seulement les rangées Sanctuary débloquées et refuse un retrait plutôt que de jeter des gemmes lorsque linventaire est plein ;
- actualise JEI de `30.26.0.186` à `30.27.0.187` dans Packwiz et le manifeste Prism/Modrinth ;
- préserve les soldes existants et le stockage persistant à 27 piles, porte Another World de la donnée v2 à v3 et `safe_vaults` de v1 à v2 ; une ancienne plateforme horizontale doit être reconstruite verticalement.
## `26.2.0-alpha.202`
Modules modifiés : **Another World `0.0.0-alpha.50`**, **It's Alive `0.0.0-alpha.31`**, **I Like To Move It `0.0.0-alpha.14`** et **Sanctuary `0.0.0-alpha.117`**. Les six autres modules conservent leur version issue de lalpha.201.
- ajoute `anotherworld:safe_block`, crafté avec huit blocs dor autour dun coffre, et active un unique coffre-fort par joueur lorsque neuf blocs du même propriétaire forment un carré horizontal exact de 3 × 3 ;
- conserve 27 emplacements globaux pour rubis, saphirs et émeraudes sous la clé versionnée `anotherworld_safe_vaults`, ce qui garde le solde accessible au Shop lorsque le chunk de la structure est déchargé ;
- expose dans Sanctuary une API publique de fournisseurs de monnaie et relie explicitement `Another World → Sanctuary` dans Gradle et Fabric, sans cycle ni copie des règles du Shop ;
- ajoute `anotherworld:titanium_drill` avec sa recette en titane, redstone, piston, blocs de fer et pioche en diamant ;
- impose un forage serveur continu de 60 secondes sur un coffre ennemi dont le propriétaire est connecté, alerte celui-ci, consomme la foreuse en cas de réussite et transfère 20 % des gemmes au pillard ;
- protège le coffre pendant 24 heures après un pillage réussi, bloque les attaques concurrentes et interdit la casse par un tiers ou le démontage dun coffre rempli ;
- autorise la descente directe par un ascenseur de Waystones vers l’étage immédiatement inférieur encore inconnu et le découvre après larrivée ;
- exige toujours une découverte préalable pour monter par lascenseur ou emprunter un trajet Waystone ordinaire ;
- compte et consomme les rubis, saphirs et émeraudes du Shop et du marché noir dans toutes les rangées Sanctuary débloquées, y compris les rangées doverflow, sans compter deux fois une pile visible par plusieurs vues ;
- permet de poser le livre de recettes culinaires Its Alive sur un pupitre vanilla et dy consulter le catalogue appris par le joueur qui la placé ;
- efface cette inscription lorsque le livre est retiré ou le pupitre cassé, afin que le livre redevienne personnel dans linventaire ;
- ajoute linfobulle FR/EN/RU du livre partageable et le tag vanilla `minecraft:lectern_books` ;
- porte les données Another World de v1 à v2 pour la nouvelle sauvegarde et conserve les données Its Alive v1, I Like To Move It v3 et Sanctuary v11, leurs protocoles 1, 3, 5 et 21 ainsi que les sauvegardes existantes.
## `26.2.0-alpha.201`
Module modifié : **Sanctuary `0.0.0-alpha.116`**. Les neuf autres modules conservent leur version issue de lalpha.200.
+8 -2
View File
@@ -22,7 +22,13 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans les fichiers de langue. Les images fournies servent d'icônes Fabric de leur module.
Version courante du pack local : **`26.2.0-alpha.201`**. Sanctuary passe en **`0.0.0-alpha.116`** ; les neuf autres modules conservent leur version de lalpha.200.
Version courante du pack distribué : **`26.2.0-alpha.203`**. Dans le workspace, le correctif du Safe Block porte Another World en développement à **`0.0.0-alpha.52`** sans produire une nouvelle version du pack.
Lalpha.203 porte Another World en **`0.0.0-alpha.51`** et JEI en **`30.27.0.187`**. Le Safe Block forme désormais un mur vertical exact de 3 × 3 : ses blocs restent des cubes individuels avant validation, puis les neuf segments composent les textures connectées 48×48. Son interface affiche trois soldes centrés — rubis, saphirs et émeraudes — avec les transferts serveur ` ×64`, ` ×1`, `+ ×1` et `+ ×64`.
Lalpha.202 permet de descendre par un ascenseur de Waystones vers l’étage immédiatement inférieur encore inconnu, sans ouvrir la montée ni les trajets ordinaires aux destinations non découvertes. Le Shop et le marché noir comptent et consomment désormais les rubis et saphirs de toutes les rangées débloquées, y compris loverflow. Le livre culinaire peut enfin être posé sur un pupitre vanilla pour partager le catalogue de recettes de son propriétaire ; retiré du pupitre, il redevient personnel.
Le coffre-fort joueur utilise neuf blocs Another World disposés en mur vertical de 3 × 3. Il conserve les rubis, saphirs et émeraudes dans un compte lié au joueur et les rend disponibles aux transactions du Shop même lorsque la structure est déchargée. Une foreuse en titane maintenue pendant 60 secondes permet de piller 20 % du contenu dun propriétaire connecté, qui reçoit immédiatement une alerte ; une réussite consomme la foreuse et protège ensuite le coffre pendant 24 heures.
Lalpha.201 rend les demandes communautaires réellement terminables. Le demandeur dépose la récompense lors de la publication, le preneur confirme sans commande après avoir lu les détails, KILL progresse automatiquement, BRING livre les objets au demandeur et PAY est confirmé par le joueur désigné. La récompense séquestrée est ensuite expédiée une seule fois dans la Mailbox du preneur. Le pseudonyme conserve la couleur du joueur et le panneau rose adopte une palette plus claire et poudrée.
@@ -315,7 +321,7 @@ Le fog of war distingue le fond de biomes calculé depuis la seed, volontairemen
## Waystones locaux
I Like To Move It possède désormais `iliketomoveit:waystone`. Chaque joueur doit visiter physiquement une pierre pour la découvrir : seules ses pierres découvertes apparaissent dans le réseau et dans l'Atlas. Dans l'écran, `R` renomme la pierre ouverte, `Entrée` valide et `Échap` annule. Un clic droit ouvre les destinations découvertes de la dimension courante, triées par distance ; le serveur facture un niveau d'XP par tranche commencée de 512 blocs, cherche une arrivée sûre puis applique sons et particules. Deux pierres ou plus sur les mêmes coordonnées X/Z deviennent un ascenseur vertical gratuit et ne se mélangent pas au réseau ordinaire. Sur cet ascenseur, s'accroupir descend vers l'étage découvert immédiatement inférieur et sauter depuis la pierre monte vers l'étage découvert immédiatement supérieur, sans ouvrir l'interface.
I Like To Move It possède désormais `iliketomoveit:waystone`. Chaque joueur doit visiter physiquement une pierre pour la découvrir : seules ses pierres découvertes apparaissent dans le réseau et dans l'Atlas. Dans l'écran, `R` renomme la pierre ouverte, `Entrée` valide et `Échap` annule. Un clic droit ouvre les destinations découvertes de la dimension courante, triées par distance ; le serveur facture un niveau d'XP par tranche commencée de 512 blocs, cherche une arrivée sûre puis applique sons et particules. Deux pierres ou plus sur les mêmes coordonnées X/Z deviennent un ascenseur vertical gratuit et ne se mélangent pas au réseau ordinaire. Sur cet ascenseur, s'accroupir descend vers l'étage immédiatement inférieur même s'il n'était pas encore découvert, puis le découvre à l'arrivée ; sauter depuis la pierre monte uniquement vers l'étage découvert immédiatement supérieur, sans ouvrir l'interface.
Le déblocage `waystones` dans Progression est obligatoire hors mode créatif. Le serveur ne fait confiance à aucune distance, aucun coût ni aucune cible envoyés par le client. Les anciennes pierres `waystone:waystone` restent chargeables et l'index `waystone:waystones` est copié une seule fois vers la donnée v2 `iliketomoveit:waystones`, sans réécrire la source. L'ancien JAR Waystone ne doit donc plus être chargé en parallèle, car I Like To Move It réserve son ID historique comme alias de sauvegarde.
+159 -14
View File
@@ -7,6 +7,18 @@ def generatedResources = layout.buildDirectory.dir("generated/resources")
sourceSets.main.resources.srcDir(generatedResources)
tasks.register("vaultRulesSmoke", JavaExec) {
group = "verification"
description = "Checks the 3x3 vault, one-minute raid, 20% loot and 24-hour protection rules."
dependsOn tasks.named("testClasses")
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.anotherworld.vault.VaultRulesSmoke"
}
tasks.named("test") {
failOnNoDiscoveredTests = false
}
tasks.register("generateAnotherWorldResources") {
group = "build"
description = "Generates the reproducible Another World 26.2 material catalog."
@@ -165,6 +177,57 @@ tasks.register("generateAnotherWorldResources") {
// Mirror, creative-only rainbow blocks, gems and first-pass metal art.
simpleBlock("mirror", "minecraft:block/cube_all", "anotherworld:block/mirror_glass")
def vaultParts = [
[id: "bottom_left", column: 0, row: 0], [id: "bottom_center", column: 1, row: 0],
[id: "bottom_right", column: 2, row: 0], [id: "middle_left", column: 0, row: 1],
[id: "center", column: 1, row: 1], [id: "middle_right", column: 2, row: 1],
[id: "top_left", column: 0, row: 2], [id: "top_center", column: 1, row: 2],
[id: "top_right", column: 2, row: 2]
]
def safeMultipart = [[when: [formed: "false"], apply: [model: "anotherworld:block/safe_block_unformed"]]]
[north: 0, east: 90, south: 180, west: 270].each { facing, rotation ->
vaultParts.each { part ->
def apply = [model: "anotherworld:block/safe_block/${part.id}"]
if (rotation != 0) apply.y = rotation
safeMultipart.add([when: [formed: "true", facing: facing.toString(), part: part.id], apply: apply])
}
}
writeJson("assets/anotherworld/blockstates/safe_block.json", [multipart: safeMultipart])
writeJson("assets/anotherworld/models/block/safe_block_unformed.json", [
parent: "minecraft:block/cube_all", textures: [all: "anotherworld:block/safe_block"]
])
def third = 16.0D / 3.0D
def segmentUv = { int column, int row ->
[column * third, (2 - row) * third, (column + 1) * third, (3 - row) * third]
}
vaultParts.each { part ->
def frontUv = segmentUv(part.column as int, part.row as int)
def backUv = segmentUv(2 - (part.column as int), part.row as int)
def sideUv = segmentUv(1, part.row as int)
def topUv = segmentUv(part.column as int, 1)
writeJson("assets/anotherworld/models/block/safe_block/${part.id}.json", [
textures: [
particle: "anotherworld:block/safe_vault/front",
bottom: "anotherworld:block/safe_vault/bottom",
top: "anotherworld:block/safe_vault/top",
front: "anotherworld:block/safe_vault/front",
side: "anotherworld:block/safe_vault/side"
],
elements: [[from: [0, 0, 0], to: [16, 16, 16], faces: [
down: [uv: topUv, texture: "#bottom", cullface: "down"],
up: [uv: topUv, texture: "#top", cullface: "up"],
north: [uv: frontUv, texture: "#front", cullface: "north"],
south: [uv: backUv, texture: "#side", cullface: "south"],
west: [uv: sideUv, texture: "#side", cullface: "west"],
east: [uv: sideUv, texture: "#side", cullface: "east"]
]]]
])
}
writeJson("assets/anotherworld/models/item/safe_block.json", [
parent: "anotherworld:block/safe_block_unformed"
])
writeJson("assets/anotherworld/items/safe_block.json", modelItem("anotherworld:item/safe_block"))
selfLoot("safe_block")
simpleBlock("black_sand", "minecraft:block/cube_all", "anotherworld:block/black_sand")
def rainbowColors = ["white", "light_gray", "gray", "black", "brown", "red", "orange", "yellow", "lime", "green", "cyan", "light_blue", "blue", "purple", "magenta", "pink"]
rainbowColors.each { color ->
@@ -215,6 +278,7 @@ tasks.register("generateAnotherWorldResources") {
titanium_nugget: "anotherworld:item/titanium_nugget"
].each { path, texture -> standaloneItem(path, texture) }
standaloneItem("golden_egg", "anotherworld:item/golden_egg")
standaloneItem("titanium_drill", "anotherworld:item/titanium_drill")
// Lootbox-only titanium uniques: diamond-grade properties, 7.5 mining speed and unbreakable.
def titaniumEquipment = [
@@ -431,6 +495,12 @@ tasks.register("generateAnotherWorldResources") {
standaloneItem("${family}_${part}", "anotherworld:item/${family}_${part}")
}
}
shaped("safe_block", "building", ["GGG", "GCG", "GGG"],
[G: "minecraft:gold_block", C: "minecraft:chest"], "anotherworld:safe_block")
shaped("titanium_drill", "equipment", ["TTT", "RPR", "FDF"], [
T: "anotherworld:titanium_ingot", R: "minecraft:redstone", P: "minecraft:piston",
F: "minecraft:iron_block", D: "minecraft:diamond_pickaxe"
], "anotherworld:titanium_drill")
// The five full mossy-wood blocks persist four visual growth stages.
def mossModel = { String base, int level -> level == 0 ? base : "${base}_moss_${level}" }
@@ -652,7 +722,7 @@ tasks.register("generateAnotherWorldResources") {
tag("item", "minecraft", "stairs", coloredBrickStairs)
tag("block", "minecraft", "slabs", coloredBrickSlabs)
tag("item", "minecraft", "slabs", coloredBrickSlabs)
def pickaxe = ["anotherworld:mirror", "anotherworld:ruby_ore", "anotherworld:deepslate_ruby_ore", "anotherworld:ruby_block", "anotherworld:sapphire_ore", "anotherworld:deepslate_sapphire_ore", "anotherworld:sapphire_block", "anotherworld:deepslate_silver_ore", "anotherworld:raw_silver_block", "anotherworld:silver_block", "anotherworld:deepslate_titanium_ore", "anotherworld:raw_titanium_block", "anotherworld:titanium_block"]
def pickaxe = ["anotherworld:mirror", "anotherworld:safe_block", "anotherworld:ruby_ore", "anotherworld:deepslate_ruby_ore", "anotherworld:ruby_block", "anotherworld:sapphire_ore", "anotherworld:deepslate_sapphire_ore", "anotherworld:sapphire_block", "anotherworld:deepslate_silver_ore", "anotherworld:raw_silver_block", "anotherworld:silver_block", "anotherworld:deepslate_titanium_ore", "anotherworld:raw_titanium_block", "anotherworld:titanium_block"]
pickaxe.addAll(coloredBricks)
pickaxe.addAll(coloredBrickStairs)
pickaxe.addAll(coloredBrickSlabs)
@@ -660,7 +730,7 @@ tasks.register("generateAnotherWorldResources") {
tag("block", "minecraft", "mineable/shovel", coloredClay + ["anotherworld:black_sand"])
tag("block", "minecraft", "mineable/hoe", coloredLeaves + flowerBlocks + ["anotherworld:turquoise_moss", "anotherworld:giant_flower_stem", "anotherworld:giant_flower_pistil"])
tag("block", "minecraft", "needs_iron_tool", ["anotherworld:ruby_ore", "anotherworld:deepslate_ruby_ore", "anotherworld:ruby_block", "anotherworld:raw_silver_block", "anotherworld:silver_block"])
tag("block", "minecraft", "needs_diamond_tool", ["anotherworld:sapphire_ore", "anotherworld:deepslate_sapphire_ore", "anotherworld:sapphire_block", "anotherworld:deepslate_silver_ore", "anotherworld:deepslate_titanium_ore", "anotherworld:raw_titanium_block", "anotherworld:titanium_block"])
tag("block", "minecraft", "needs_diamond_tool", ["anotherworld:safe_block", "anotherworld:sapphire_ore", "anotherworld:deepslate_sapphire_ore", "anotherworld:sapphire_block", "anotherworld:deepslate_silver_ore", "anotherworld:deepslate_titanium_ore", "anotherworld:raw_titanium_block", "anotherworld:titanium_block"])
[
"gems/ruby": ["anotherworld:ruby"], "gems/sapphire": ["anotherworld:sapphire"],
"raw_materials/silver": ["anotherworld:raw_silver"], "ingots/silver": ["anotherworld:silver_ingot"], "nuggets/silver": ["anotherworld:silver_nugget"],
@@ -1452,13 +1522,13 @@ tasks.register("generateAnotherWorldResources") {
def languageEntries = [
en_us: [
description: "Owns the material things that make up the world.", tab: "Another World",
mirror: "Mirror", black_sand: "Black Sand", ruby_ore: "Ruby Ore", deepslate_ruby_ore: "Deepslate Ruby Ore", ruby_block: "Block of Ruby",
mirror: "Mirror", safe_block: "Safe Block", black_sand: "Black Sand", ruby_ore: "Ruby Ore", deepslate_ruby_ore: "Deepslate Ruby Ore", ruby_block: "Block of Ruby",
sapphire_ore: "Sapphire Ore", deepslate_sapphire_ore: "Deepslate Sapphire Ore", sapphire_block: "Block of Sapphire",
deepslate_silver_ore: "Deepslate Silver Ore", raw_silver_block: "Block of Raw Silver", silver_block: "Block of Silver",
deepslate_titanium_ore: "Deepslate Titanium Ore", raw_titanium_block: "Block of Raw Titanium", titanium_block: "Block of Titanium",
ruby: "Ruby", sapphire: "Sapphire", raw_silver: "Raw Silver", silver_ingot: "Silver Ingot", silver_nugget: "Silver Nugget",
silver_rod: "Silver Rod", silver_plate: "Silver Plate", silver_gear: "Silver Gear", aircraft_cockpit: "Aircraft Cockpit",
raw_titanium: "Raw Titanium", titanium_ingot: "Titanium Ingot", titanium_nugget: "Titanium Nugget", golden_egg: "Golden Egg",
raw_titanium: "Raw Titanium", titanium_ingot: "Titanium Ingot", titanium_nugget: "Titanium Nugget", titanium_drill: "Titanium Drill", golden_egg: "Golden Egg",
titanium_sword: "Titanium Sword", titanium_shovel: "Titanium Shovel", titanium_pickaxe: "Titanium Pickaxe", titanium_axe: "Titanium Axe",
titanium_hoe: "Titanium Hoe", titanium_spear: "Titanium Spear", titanium_shears: "Titanium Shears",
titanium_helmet: "Titanium Helmet", titanium_chestplate: "Titanium Chestplate", titanium_leggings: "Titanium Leggings", titanium_boots: "Titanium Boots",
@@ -1469,13 +1539,13 @@ tasks.register("generateAnotherWorldResources") {
],
fr_fr: [
description: "Possède les choses matérielles qui composent le monde.", tab: "Another World",
mirror: "Miroir", black_sand: "Sable noir", ruby_ore: "Minerai de rubis", deepslate_ruby_ore: "Minerai de rubis des abîmes", ruby_block: "Bloc de rubis",
mirror: "Miroir", safe_block: "Bloc de coffre-fort", black_sand: "Sable noir", ruby_ore: "Minerai de rubis", deepslate_ruby_ore: "Minerai de rubis des abîmes", ruby_block: "Bloc de rubis",
sapphire_ore: "Minerai de saphir", deepslate_sapphire_ore: "Minerai de saphir des abîmes", sapphire_block: "Bloc de saphir",
deepslate_silver_ore: "Minerai dargent des abîmes", raw_silver_block: "Bloc dargent brut", silver_block: "Bloc dargent",
deepslate_titanium_ore: "Minerai de titane des abîmes", raw_titanium_block: "Bloc de titane brut", titanium_block: "Bloc de titane",
ruby: "Rubis", sapphire: "Saphir", raw_silver: "Argent brut", silver_ingot: "Lingot dargent", silver_nugget: "Pépite dargent",
silver_rod: "Tige dargent", silver_plate: "Plaque dargent", silver_gear: "Engrenage dargent", aircraft_cockpit: "Cockpit davion",
raw_titanium: "Titane brut", titanium_ingot: "Lingot de titane", titanium_nugget: "Pépite de titane", golden_egg: "Œuf doré",
raw_titanium: "Titane brut", titanium_ingot: "Lingot de titane", titanium_nugget: "Pépite de titane", titanium_drill: "Foreuse en titane", golden_egg: "Œuf doré",
titanium_sword: "Épée en titane", titanium_shovel: "Pelle en titane", titanium_pickaxe: "Pioche en titane", titanium_axe: "Hache en titane",
titanium_hoe: "Houe en titane", titanium_spear: "Lance en titane", titanium_shears: "Cisailles en titane",
titanium_helmet: "Casque en titane", titanium_chestplate: "Plastron en titane", titanium_leggings: "Jambières en titane", titanium_boots: "Bottes en titane",
@@ -1486,13 +1556,13 @@ tasks.register("generateAnotherWorldResources") {
],
ru_ru: [
description: "Содержит материальные элементы, из которых состоит мир.", tab: "Another World",
mirror: "Зеркало", black_sand: "Чёрный песок", ruby_ore: "Рубиновая руда", deepslate_ruby_ore: "Рубиноносный глубинный сланец", ruby_block: "Рубиновый блок",
mirror: "Зеркало", safe_block: "Блок сейфа", black_sand: "Чёрный песок", ruby_ore: "Рубиновая руда", deepslate_ruby_ore: "Рубиноносный глубинный сланец", ruby_block: "Рубиновый блок",
sapphire_ore: "Сапфировая руда", deepslate_sapphire_ore: "Сапфироносный глубинный сланец", sapphire_block: "Сапфировый блок",
deepslate_silver_ore: "Сереброносный глубинный сланец", raw_silver_block: "Блок необработанного серебра", silver_block: "Серебряный блок",
deepslate_titanium_ore: "Титаноносный глубинный сланец", raw_titanium_block: "Блок необработанного титана", titanium_block: "Титановый блок",
ruby: "Рубин", sapphire: "Сапфир", raw_silver: "Необработанное серебро", silver_ingot: "Серебряный слиток", silver_nugget: "Кусочек серебра",
silver_rod: "Серебряный стержень", silver_plate: "Серебряная пластина", silver_gear: "Серебряная шестерня", aircraft_cockpit: "Кабина самолёта",
raw_titanium: "Необработанный титан", titanium_ingot: "Титановый слиток", titanium_nugget: "Кусочек титана", golden_egg: "Золотое яйцо",
raw_titanium: "Необработанный титан", titanium_ingot: "Титановый слиток", titanium_nugget: "Кусочек титана", titanium_drill: "Титановая дрель", golden_egg: "Золотое яйцо",
titanium_sword: "Титановый меч", titanium_shovel: "Титановая лопата", titanium_pickaxe: "Титановая кирка", titanium_axe: "Титановый топор",
titanium_hoe: "Титановая мотыга", titanium_spear: "Титановое копьё", titanium_shears: "Титановые ножницы",
titanium_helmet: "Титановый шлем", titanium_chestplate: "Титановый нагрудник", titanium_leggings: "Титановые поножи", titanium_boots: "Титановые ботинки",
@@ -1548,6 +1618,80 @@ tasks.register("generateAnotherWorldResources") {
lang["options.miroir.reflection.tooltip"] = locale == "fr_fr"
? "Dose le mélange entre métal sombre et couleur réfléchie."
: locale == "ru_ru" ? "Настраивает смешивание тёмного металла и отражённого цвета." : "Blends dark metal with the reflected color."
def vaultText = locale == "fr_fr" ? [
"container.anotherworld.safe_vault": "Coffre-fort",
"tooltip.anotherworld.titanium_drill.use": "Maintenir sur un coffre-fort ennemi pour le forer.",
"tooltip.anotherworld.titanium_drill.consumed": "Consommée après un pillage réussi.",
"message.anotherworld.safe_vault.incomplete": "Le coffre-fort exige exactement 9 blocs appartenant au même joueur, posés en mur vertical de 3 × 3.",
"message.anotherworld.safe_vault.locked": "Ce coffre-fort appartient à un autre joueur.",
"message.anotherworld.safe_vault.formed": "Coffre-fort vertical 3 × 3 lié à vous en %s, %s, %s.",
"message.anotherworld.safe_vault.deposit_failed": "Impossible de déposer %s : inventaire vide ou coffre-fort plein.",
"message.anotherworld.safe_vault.withdraw_failed": "Impossible de retirer %s : solde vide ou inventaire plein.",
"message.anotherworld.safe_vault.already_owned": "Vous possédez déjà un coffre-fort actif.",
"message.anotherworld.safe_vault.break_not_owner": "Seul le propriétaire peut démonter ce bloc de coffre-fort.",
"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.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).",
"message.anotherworld.safe_vault.already_drilling": "Une autre foreuse attaque déjà ce coffre-fort.",
"message.anotherworld.safe_vault.alarm": "ALERTE : %s fore votre coffre-fort en %s, %s, %s !",
"message.anotherworld.safe_vault.drilling_started": "Forage lancé : gardez la foreuse dirigée vers la structure pendant 60 secondes.",
"message.anotherworld.safe_vault.drilling_progress": "Forage : %s %%",
"message.anotherworld.safe_vault.drilling_cancelled": "Forage interrompu.",
"message.anotherworld.safe_vault.raided_owner": "%s a pillé %s gemme(s) dans votre coffre-fort.",
"message.anotherworld.safe_vault.raided": "Pillage réussi : %s gemme(s) récupérée(s)."
] : locale == "ru_ru" ? [
"container.anotherworld.safe_vault": "Сейф",
"tooltip.anotherworld.titanium_drill.use": "Удерживайте на чужом сейфе, чтобы просверлить его.",
"tooltip.anotherworld.titanium_drill.consumed": "Расходуется после успешного ограбления.",
"message.anotherworld.safe_vault.incomplete": "Сейф требует вертикальную стену 3 × 3 из 9 блоков одного владельца.",
"message.anotherworld.safe_vault.locked": "Этот сейф принадлежит другому игроку.",
"message.anotherworld.safe_vault.formed": "Вертикальный сейф 3 × 3 привязан к вам в точке %s, %s, %s.",
"message.anotherworld.safe_vault.deposit_failed": "Не удалось внести %s: предметов нет или сейф заполнен.",
"message.anotherworld.safe_vault.withdraw_failed": "Не удалось забрать %s: баланс пуст или инвентарь заполнен.",
"message.anotherworld.safe_vault.already_owned": "У вас уже есть активный сейф.",
"message.anotherworld.safe_vault.break_not_owner": "Разобрать этот блок может только владелец.",
"message.anotherworld.safe_vault.empty_before_break": "Опустошите сейф и дождитесь окончания сверления перед разборкой.",
"message.anotherworld.safe_vault.drill_own": "Нельзя сверлить собственный сейф.",
"message.anotherworld.safe_vault.inactive": "Этот сейф не активен.",
"message.anotherworld.safe_vault.empty": "Этот сейф пуст.",
"message.anotherworld.safe_vault.owner_offline": "Владелец должен быть в сети, чтобы атаковать сейф.",
"message.anotherworld.safe_vault.protected": "Сейф защищён ещё примерно %s минут(ы).",
"message.anotherworld.safe_vault.already_drilling": "Этот сейф уже сверлит другой игрок.",
"message.anotherworld.safe_vault.alarm": "ТРЕВОГА: %s сверлит ваш сейф в точке %s, %s, %s!",
"message.anotherworld.safe_vault.drilling_started": "Сверление началось: держите дрель наведённой 60 секунд.",
"message.anotherworld.safe_vault.drilling_progress": "Сверление: %s %%",
"message.anotherworld.safe_vault.drilling_cancelled": "Сверление прервано.",
"message.anotherworld.safe_vault.raided_owner": "%s похитил(а) %s драгоценностей из вашего сейфа.",
"message.anotherworld.safe_vault.raided": "Ограбление удалось: получено %s драгоценностей."
] : [
"container.anotherworld.safe_vault": "Safe",
"tooltip.anotherworld.titanium_drill.use": "Hold on an enemy safe to drill it.",
"tooltip.anotherworld.titanium_drill.consumed": "Consumed after a successful raid.",
"message.anotherworld.safe_vault.incomplete": "A safe requires an upright 3 × 3 wall of exactly 9 blocks owned by one player.",
"message.anotherworld.safe_vault.locked": "This safe belongs to another player.",
"message.anotherworld.safe_vault.formed": "Upright 3 × 3 safe linked to you at %s, %s, %s.",
"message.anotherworld.safe_vault.deposit_failed": "Could not deposit %s: none available or the safe is full.",
"message.anotherworld.safe_vault.withdraw_failed": "Could not withdraw %s: the balance is empty or inventory is full.",
"message.anotherworld.safe_vault.already_owned": "You already own an active safe.",
"message.anotherworld.safe_vault.break_not_owner": "Only the owner may dismantle this safe block.",
"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.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).",
"message.anotherworld.safe_vault.already_drilling": "Another drill is already attacking this safe.",
"message.anotherworld.safe_vault.alarm": "ALARM: %s is drilling your safe at %s, %s, %s!",
"message.anotherworld.safe_vault.drilling_started": "Drilling started: keep aiming at the structure for 60 seconds.",
"message.anotherworld.safe_vault.drilling_progress": "Drilling: %s %%",
"message.anotherworld.safe_vault.drilling_cancelled": "Drilling interrupted.",
"message.anotherworld.safe_vault.raided_owner": "%s stole %s gem item(s) from your safe.",
"message.anotherworld.safe_vault.raided": "Raid successful: recovered %s gem item(s)."
]
lang.putAll(vaultText)
def tagTranslations = locale == "fr_fr" ? [
"tag.item.anotherworld.lavender_logs": "Bûches de lavande", "tag.item.anotherworld.ebony_logs": "Bûches d’ébène", "tag.item.anotherworld.mossy_logs": "Bûches moussues", "tag.item.anotherworld.blueberry_logs": "Bûches de myrtille",
"tag.item.anotherworld.colored_bricks": "Blocs de briques colorées", "tag.item.anotherworld.colored_brick_items": "Briques colorées", "tag.item.anotherworld.colored_clay": "Argiles colorées", "tag.item.anotherworld.colored_clay_balls": "Boules dargile colorées", "tag.item.anotherworld.colored_flower_blocks": "Blocs de fleurs colorés", "tag.item.anotherworld.colored_leaves": "Feuilles colorées",
@@ -1574,7 +1718,7 @@ tasks.register("generateAnotherWorldResources") {
names.findAll { key, ignored -> !(key in ["description", "tab"]) }.each { path, name ->
def registry = path in (["ruby", "sapphire", "raw_silver", "silver_ingot", "silver_nugget",
"silver_rod", "silver_plate", "silver_gear", "aircraft_cockpit",
"raw_titanium", "titanium_ingot", "titanium_nugget", "golden_egg"] + titaniumEquipment) ? "item" : "block"
"raw_titanium", "titanium_ingot", "titanium_nugget", "titanium_drill", "golden_egg"] + titaniumEquipment) ? "item" : "block"
lang["${registry}.anotherworld.${path}"] = name
}
rainbowColors.each { color ->
@@ -1656,7 +1800,7 @@ tasks.named("sourcesJar") {
tasks.register("verifyAnotherWorldCatalog") {
group = "verification"
description = "Checks the Another World alpha.49 catalog and climate-aware biome mixer."
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(rootProject.file("sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary.json"))
@@ -1794,7 +1938,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.49"
if (project.version.toString() != "0.0.0-alpha.52"
|| sulfurMigration.target?.modules?.anotherworld != "0.0.0-alpha.44"
|| sulfurMigration.sulfur_caves?.sanctuary?.dripstone_cell_period != 2
|| !javaText.contains('Identifier.parse("sanctuary:sulfur_caves")')
@@ -2217,7 +2361,7 @@ tasks.register("verifyAnotherWorldCatalog") {
def parts = ["planks", "log", "wood", "stripped_log", "stripped_wood", "leaves", "sapling", "stairs", "slab", "fence", "fence_gate", "door", "trapdoor", "pressure_plate", "button"]
def signParts = ["sign", "wall_sign", "hanging_sign", "wall_hanging_sign"]
def blocks = [
"mirror", "black_sand", "ruby_ore", "deepslate_ruby_ore", "ruby_block", "sapphire_ore", "deepslate_sapphire_ore", "sapphire_block",
"mirror", "safe_block", "black_sand", "ruby_ore", "deepslate_ruby_ore", "ruby_block", "sapphire_ore", "deepslate_sapphire_ore", "sapphire_block",
"deepslate_silver_ore", "raw_silver_block", "silver_block", "deepslate_titanium_ore", "raw_titanium_block", "titanium_block",
"lavender", "pale_lavender", "pink_lavender", "blue_lavender", "dusk_lavender", "hibiscus", "narcissus", "green_bell_mushroom", "brown_shelf_mushroom", "rose", "blue_rose",
"turquoise_moss", "giant_flower_stem", "giant_flower_pistil"
@@ -2238,7 +2382,7 @@ tasks.register("verifyAnotherWorldCatalog") {
"titanium_spear", "titanium_shears", "titanium_helmet", "titanium_chestplate",
"titanium_leggings", "titanium_boots"
]
def standaloneItems = ["ruby", "sapphire", "raw_silver", "silver_ingot", "silver_nugget", "silver_rod", "silver_plate", "silver_gear", "aircraft_cockpit", "raw_titanium", "titanium_ingot", "titanium_nugget", "golden_egg"] + titaniumEquipmentIds
def standaloneItems = ["ruby", "sapphire", "raw_silver", "silver_ingot", "silver_nugget", "silver_rod", "silver_plate", "silver_gear", "aircraft_cockpit", "raw_titanium", "titanium_ingot", "titanium_nugget", "titanium_drill", "golden_egg"] + titaniumEquipmentIds
standaloneItems.addAll(colors.collect { "${it}_brick" })
standaloneItems.addAll(colors.collect { "${it}_clay_ball" })
standaloneItems.addAll(woodFamilies.collectMany { family -> ["${family}_boat", "${family}_chest_boat"] })
@@ -3098,6 +3242,7 @@ tasks.register("verifyAnotherWorldCatalog") {
tasks.named("check") {
dependsOn tasks.named("verifyAnotherWorldCatalog")
dependsOn tasks.named("vaultRulesSmoke")
}
tasks.register("verifyClimateBiomeMixer") {
@@ -3110,7 +3255,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.49"
if (project.version.toString() != "0.0.0-alpha.52"
|| 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"
@@ -4,6 +4,7 @@ import fr.koka99cab.canon.CanonMod;
import fr.koka99cab.sanctuary26.anotherworld.item.WeatherCompassItem;
import fr.koka99cab.sanctuary26.anotherworld.network.AnotherWorldNetworking;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldRegistries;
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultService;
import net.fabricmc.api.ModInitializer;
import net.minecraft.resources.Identifier;
import org.slf4j.Logger;
@@ -12,7 +13,7 @@ import org.slf4j.LoggerFactory;
public final class AnotherWorldMod implements ModInitializer {
public static final String MOD_ID = "anotherworld";
public static final String DISPLAY_NAME = "Another World";
public static final int CURRENT_DATA_VERSION = 1;
public static final int CURRENT_DATA_VERSION = 3;
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
public AnotherWorldMod() {
@@ -25,6 +26,7 @@ public final class AnotherWorldMod implements ModInitializer {
@Override
public void onInitialize() {
AnotherWorldRegistries.initialize();
VaultService.initialize();
WeatherCompassItem.registerFrameInteraction();
CanonMod.initialize();
AnotherWorldNetworking.initialize();
@@ -0,0 +1,124 @@
package fr.koka99cab.sanctuary26.anotherworld.block;
import com.mojang.serialization.MapCodec;
import fr.koka99cab.sanctuary26.anotherworld.block.entity.SafeVaultBlockEntity;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldItems;
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultData;
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultDrillingService;
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultService;
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultStructure;
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultPart;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.phys.BlockHitResult;
public final class SafeVaultBlock extends BaseEntityBlock {
public static final MapCodec<SafeVaultBlock> CODEC = simpleCodec(SafeVaultBlock::new);
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;
public static final BooleanProperty FORMED = BooleanProperty.create("formed");
public static final EnumProperty<VaultPart> PART = EnumProperty.create("part", VaultPart.class);
public SafeVaultBlock(Properties properties) {
super(properties);
registerDefaultState(stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(FORMED, false)
.setValue(PART, VaultPart.CENTER));
}
@Override protected MapCodec<? extends BaseEntityBlock> codec() { return CODEC; }
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new SafeVaultBlockEntity(pos, state); }
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}
@Override protected BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
}
@Override protected BlockState mirror(BlockState state, Mirror mirror) {
return rotate(state, mirror.getRotation(state.getValue(FACING)));
}
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING, FORMED, PART);
}
@Override
public void setPlacedBy(Level level, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) {
super.setPlacedBy(level, pos, state, placer, stack);
if (!(level instanceof ServerLevel serverLevel) || !(placer instanceof ServerPlayer player)) return;
if (level.getBlockEntity(pos) instanceof SafeVaultBlockEntity blockEntity) {
blockEntity.setOwner(player.getUUID());
VaultService.onSegmentPlaced(serverLevel, pos, player);
}
}
@Override
protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos,
Player player, InteractionHand hand, BlockHitResult hit) {
if (!stack.is(AnotherWorldItems.TITANIUM_DRILL)) return InteractionResult.TRY_WITH_EMPTY_HAND;
if (level.isClientSide()) {
player.startUsingItem(hand);
return InteractionResult.SUCCESS;
}
if (player instanceof ServerPlayer serverPlayer
&& VaultDrillingService.begin(serverPlayer, (ServerLevel) level, pos, hand)) {
player.startUsingItem(hand);
return InteractionResult.SUCCESS_SERVER;
}
return InteractionResult.CONSUME;
}
@Override
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
Player player, BlockHitResult hit) {
if (level.isClientSide()) return InteractionResult.SUCCESS;
VaultStructure.Formed formed = VaultStructure.find(level, pos).orElse(null);
if (formed == null) {
player.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.incomplete"));
return InteractionResult.CONSUME;
}
ServerLevel serverLevel = (ServerLevel) level;
VaultData data = VaultData.get(serverLevel.getServer());
if (!data.isRegistered(serverLevel, formed.center(), formed.owner())
&& !VaultService.tryRegister(serverLevel, formed, player instanceof ServerPlayer sp ? sp : null)) {
return InteractionResult.CONSUME;
}
if (!formed.owner().equals(player.getUUID())) {
player.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.locked"));
return InteractionResult.CONSUME;
}
if (level.getBlockEntity(formed.center()) instanceof SafeVaultBlockEntity controller) {
player.openMenu(controller);
return InteractionResult.SUCCESS_SERVER;
}
return InteractionResult.CONSUME;
}
@Override
protected void affectNeighborsAfterRemoval(BlockState state, ServerLevel level, BlockPos pos,
boolean movedByPiston) {
if (!movedByPiston) VaultService.onSegmentRemoved(level, pos, state);
super.affectNeighborsAfterRemoval(state, level, pos, movedByPiston);
}
}
@@ -0,0 +1,60 @@
package fr.koka99cab.sanctuary26.anotherworld.block.entity;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlockEntities;
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultData;
import fr.koka99cab.sanctuary26.anotherworld.vault.SafeVaultMenu;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.UUIDUtil;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
/** One owned segment of a player vault. The shared inventory lives in versioned world data. */
public final class SafeVaultBlockEntity extends BlockEntity implements MenuProvider {
private UUID owner;
public SafeVaultBlockEntity(BlockPos pos, BlockState state) {
super(AnotherWorldBlockEntities.SAFE_VAULT, pos, state);
}
public UUID owner() {
return owner;
}
public void setOwner(UUID owner) {
this.owner = owner;
setChanged();
}
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);
if (owner != null) output.store("Owner", UUIDUtil.CODEC, owner);
}
@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);
owner = input.read("Owner", UUIDUtil.CODEC).orElse(null);
}
@Override
public Component getDisplayName() {
return Component.translatable("container.anotherworld.safe_vault");
}
@Override
public AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) {
if (owner == null || !owner.equals(player.getUUID()) || !(level instanceof ServerLevel serverLevel)) return null;
return new SafeVaultMenu(containerId, inventory, VaultData.get(serverLevel.getServer()),
serverLevel, getBlockPos(), owner);
}
}
@@ -4,8 +4,10 @@ import fr.koka99cab.canon.client.CanonClient;
import fr.koka99cab.sanctuary26.anotherworld.client.render.MirrorBlockEntityRenderer;
import fr.koka99cab.sanctuary26.anotherworld.client.render.AnotherWorldBoatRendering;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlockEntities;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldMenus;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry;
import net.minecraft.client.gui.screens.MenuScreens;
public final class AnotherWorldClient implements ClientModInitializer {
@Override
@@ -14,5 +16,6 @@ public final class AnotherWorldClient implements ClientModInitializer {
MiroirClientSettings.load();
AnotherWorldBoatRendering.initialize();
BlockEntityRendererRegistry.register(AnotherWorldBlockEntities.MIRROR, MirrorBlockEntityRenderer::new);
MenuScreens.register(AnotherWorldMenus.SAFE_VAULT, SafeVaultScreen::new);
}
}
@@ -0,0 +1,66 @@
package fr.koka99cab.sanctuary26.anotherworld.client;
import fr.koka99cab.sanctuary26.anotherworld.vault.SafeVaultMenu;
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultRules;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
/** Three centered currency balances with explicit one/stack deposit and withdrawal controls. */
public final class SafeVaultScreen extends AbstractContainerScreen<SafeVaultMenu> {
private static final Identifier TEXTURE = Identifier.withDefaultNamespace(
"textures/gui/container/crafting_table.png");
private static final String[] LABELS = {" ×64", " ×1", "+ ×1", "+ ×64"};
private static final int[] BUTTON_X = {7, 43, 105, 135};
private static final int[] BUTTON_WIDTH = {34, 30, 28, 34};
private static final int SLOT_X = 79;
private static final int FIRST_ROW_Y = 17;
private static final int ROW_HEIGHT = 22;
public SafeVaultScreen(SafeVaultMenu menu, Inventory inventory, Component title) {
super(menu, inventory, title);
}
@Override
protected void init() {
super.init();
titleLabelX = (imageWidth - font.width(title)) / 2;
for (int currency = 0; currency < VaultRules.CURRENCY_SLOTS; currency++) {
int y = topPos + FIRST_ROW_Y + currency * ROW_HEIGHT;
for (int action = 0; action < VaultRules.BUTTONS_PER_CURRENCY; action++) {
int buttonId = SafeVaultMenu.buttonId(currency, action);
addRenderableWidget(Button.builder(Component.literal(LABELS[action]),
button -> sendTransfer(buttonId))
.bounds(leftPos + BUTTON_X[action], y, BUTTON_WIDTH[action], 20).build());
}
}
}
@Override
public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float tickDelta) {
super.extractBackground(graphics, mouseX, mouseY, tickDelta);
graphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, leftPos, topPos, 0.0F, 0.0F,
imageWidth, imageHeight, 256, 256);
graphics.fill(leftPos + 5, topPos + 15, leftPos + 171, topPos + 80, 0xFFC6C6C6);
for (int currency = 0; currency < VaultRules.CURRENCY_SLOTS; currency++) {
int x = leftPos + SLOT_X;
int y = topPos + FIRST_ROW_Y + currency * ROW_HEIGHT + 1;
graphics.outline(x, y, 18, 18, 0xFF373737);
graphics.fill(x + 1, y + 1, x + 17, y + 17, 0xFF8B8B8B);
graphics.item(new ItemStack(SafeVaultMenu.currency(currency)), x + 1, y + 1);
Component balance = Component.literal(Integer.toString(menu.balance(currency)));
graphics.text(font, balance, x + 17 - font.width(balance), y + 9, 0xFFFFFFFF, true);
}
}
private void sendTransfer(int buttonId) {
if (minecraft != null && minecraft.gameMode != null) {
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, buttonId);
}
}
}
@@ -0,0 +1,41 @@
package fr.koka99cab.sanctuary26.anotherworld.item;
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultDrillingService;
import java.util.function.Consumer;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.ItemUseAnimation;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.component.TooltipDisplay;
import net.minecraft.world.level.Level;
public final class TitaniumDrillItem extends Item {
public TitaniumDrillItem(Properties properties) {
super(properties);
}
@Override
public void onUseTick(Level level, LivingEntity entity, ItemStack stack, int remainingUseTicks) {
if (entity instanceof ServerPlayer player) VaultDrillingService.tick(player, stack);
}
@Override
public boolean releaseUsing(ItemStack stack, Level level, LivingEntity entity, int remainingUseTicks) {
if (entity instanceof ServerPlayer player) VaultDrillingService.cancel(player, true);
return false;
}
@Override public int getUseDuration(ItemStack stack, LivingEntity entity) { return 72_000; }
@Override public ItemUseAnimation getUseAnimation(ItemStack stack) { return ItemUseAnimation.NONE; }
@Override
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
Consumer<Component> tooltip, TooltipFlag flag) {
tooltip.accept(Component.translatable("tooltip.anotherworld.titanium_drill.use").withStyle(ChatFormatting.GRAY));
tooltip.accept(Component.translatable("tooltip.anotherworld.titanium_drill.consumed").withStyle(ChatFormatting.DARK_GRAY));
}
}
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.anotherworld.registry;
import fr.koka99cab.sanctuary26.anotherworld.AnotherWorldMod;
import fr.koka99cab.sanctuary26.anotherworld.block.entity.MirrorBlockEntity;
import fr.koka99cab.sanctuary26.anotherworld.block.entity.SafeVaultBlockEntity;
import java.util.ArrayList;
import java.util.List;
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityType;
@@ -14,6 +15,11 @@ import net.minecraft.world.level.block.entity.BlockEntityTypes;
public final class AnotherWorldBlockEntities {
public static final BlockEntityType<MirrorBlockEntity> MIRROR = registerMirror();
public static final BlockEntityType<SafeVaultBlockEntity> SAFE_VAULT = Registry.register(
BuiltInRegistries.BLOCK_ENTITY_TYPE,
AnotherWorldMod.id("safe_vault"),
FabricBlockEntityTypeBuilder.create(SafeVaultBlockEntity::new, AnotherWorldBlocks.SAFE_BLOCK).build()
);
private AnotherWorldBlockEntities() {
}
@@ -33,7 +39,7 @@ public final class AnotherWorldBlockEntities {
hangingSignType.addValidBlock(family.hangingSign());
hangingSignType.addValidBlock(family.wallHangingSign());
});
AnotherWorldMod.LOGGER.debug("[{}] Mirror block entity and custom sign compatibility registered.", AnotherWorldMod.DISPLAY_NAME);
AnotherWorldMod.LOGGER.debug("[{}] Mirror, safe-vault and custom sign block entities registered.", AnotherWorldMod.DISPLAY_NAME);
}
private static BlockEntityType<MirrorBlockEntity> registerMirror() {
@@ -14,6 +14,7 @@ import fr.koka99cab.sanctuary26.anotherworld.block.PricklyRoseBlock;
import fr.koka99cab.sanctuary26.anotherworld.block.ProgressiveMossBlock;
import fr.koka99cab.sanctuary26.anotherworld.block.ProgressiveMossPillarBlock;
import fr.koka99cab.sanctuary26.anotherworld.block.RainbowBlock;
import fr.koka99cab.sanctuary26.anotherworld.block.SafeVaultBlock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -55,6 +56,7 @@ import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.properties.BlockSetType;
import net.minecraft.world.level.block.state.properties.WoodType;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.level.material.PushReaction;
public final class AnotherWorldBlocks {
public static final List<String> DYE_COLORS = List.of(
@@ -93,6 +95,16 @@ public final class AnotherWorldBlocks {
.sound(SoundType.GLASS)
.noOcclusion()
);
public static final SafeVaultBlock SAFE_BLOCK = register(
"safe_block",
SafeVaultBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.IRON_BLOCK)
.mapColor(MapColor.GOLD)
.requiresCorrectToolForDrops()
.strength(8.0F, 1_200.0F)
.sound(SoundType.METAL)
.pushReaction(PushReaction.BLOCK)
);
public static final Map<String, RainbowBlock> RAINBOW_BLOCKS = registerRainbowBlocks();
public static final Map<String, Block> COLORED_BRICKS = registerColoredBlocks("bricks", Blocks.BRICKS);
@@ -3,6 +3,7 @@ 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 java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -54,6 +55,11 @@ public final class AnotherWorldItems {
public static final Item RAW_TITANIUM = register("raw_titanium");
public static final Item TITANIUM_INGOT = registerTrimMaterial("titanium_ingot", TITANIUM_TRIM_MATERIAL);
public static final Item TITANIUM_NUGGET = register("titanium_nugget");
public static final Item TITANIUM_DRILL = register(
"titanium_drill",
TitaniumDrillItem::new,
new Item.Properties().stacksTo(1)
);
public static final Item TITANIUM_SWORD = register(
"titanium_sword",
Item::new,
@@ -0,0 +1,23 @@
package fr.koka99cab.sanctuary26.anotherworld.registry;
import fr.koka99cab.sanctuary26.anotherworld.AnotherWorldMod;
import fr.koka99cab.sanctuary26.anotherworld.vault.SafeVaultMenu;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.flag.FeatureFlags;
import net.minecraft.world.inventory.MenuType;
public final class AnotherWorldMenus {
public static final MenuType<SafeVaultMenu> SAFE_VAULT = Registry.register(
BuiltInRegistries.MENU,
AnotherWorldMod.id("safe_vault"),
new MenuType<>(SafeVaultMenu::new, FeatureFlags.VANILLA_SET)
);
private AnotherWorldMenus() {
}
public static void initialize() {
AnotherWorldMod.LOGGER.debug("[{}] Safe-vault menu registered.", AnotherWorldMod.DISPLAY_NAME);
}
}
@@ -21,6 +21,7 @@ public final class AnotherWorldRegistries {
AnotherWorldEntities.initialize();
AnotherWorldItems.initialize();
AnotherWorldBlockEntities.initialize();
AnotherWorldMenus.initialize();
AnotherWorldCreativeTab.initialize();
AnotherWorldWorldGeneration.initialize();
AnotherWorldMod.LOGGER.info(
@@ -0,0 +1,195 @@
package fr.koka99cab.sanctuary26.anotherworld.vault;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldItems;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldMenus;
import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryPlayerInventoryView;
import java.util.List;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.SimpleContainerData;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
/** Server-authoritative three-currency vault controls backed by the existing saved account. */
public final class SafeVaultMenu extends AbstractContainerMenu {
private static final int PLAYER_INVENTORY_Y = 108;
private static final List<Item> CURRENCIES = List.of(
AnotherWorldItems.RUBY,
AnotherWorldItems.SAPPHIRE,
Items.EMERALD
);
private final ContainerData balances;
private final VaultData vaultData;
private final ServerLevel serverLevel;
private final UUID owner;
private final BlockPos center;
public SafeVaultMenu(int containerId, Inventory inventory) {
this(containerId, inventory, new SimpleContainerData(VaultRules.CURRENCY_SLOTS),
null, null, null, null);
}
public SafeVaultMenu(int containerId, Inventory inventory, VaultData data,
ServerLevel level, BlockPos center, UUID owner) {
this(containerId, inventory, new VaultBalanceData(data, owner), data, level,
center.immutable(), owner);
}
private SafeVaultMenu(int containerId, Inventory inventory, ContainerData balances,
VaultData vaultData, ServerLevel serverLevel, BlockPos center, UUID owner) {
super(AnotherWorldMenus.SAFE_VAULT, containerId);
checkContainerDataCount(balances, VaultRules.CURRENCY_SLOTS);
this.balances = balances;
this.vaultData = vaultData;
this.serverLevel = serverLevel;
this.center = center;
this.owner = owner;
// Leave all three currency rows visible above Sanctuary's extended inventory panel.
addStandardInventorySlots(inventory, 8, PLAYER_INVENTORY_Y);
addDataSlots(balances);
}
public int balance(int currency) {
return currency >= 0 && currency < VaultRules.CURRENCY_SLOTS ? balances.get(currency) : 0;
}
public static Item currency(int currency) {
return CURRENCIES.get(Math.clamp(currency, 0, CURRENCIES.size() - 1));
}
public static int buttonId(int currency, int action) {
return currency * VaultRules.BUTTONS_PER_CURRENCY + action;
}
@Override
public boolean clickMenuButton(Player player, int buttonId) {
if (!(player instanceof ServerPlayer serverPlayer) || vaultData == null || !stillValid(player)) return false;
int currencyIndex = VaultRules.buttonCurrency(buttonId);
int amount = VaultRules.buttonAmount(buttonId);
if (currencyIndex < 0 || amount <= 0) return false;
Item item = currency(currencyIndex);
int moved = VaultRules.buttonDeposits(buttonId)
? deposit(serverPlayer, item, amount)
: withdraw(serverPlayer, item, amount);
if (moved <= 0) {
serverPlayer.sendOverlayMessage(Component.translatable(
VaultRules.buttonDeposits(buttonId)
? "message.anotherworld.safe_vault.deposit_failed"
: "message.anotherworld.safe_vault.withdraw_failed",
new ItemStack(item).getHoverName()));
}
broadcastChanges();
return true;
}
@Override
public ItemStack quickMoveStack(Player player, int index) {
return ItemStack.EMPTY;
}
@Override
public boolean stillValid(Player player) {
if (serverLevel == null || vaultData == null || center == null || owner == null) return true;
if (!player.getUUID().equals(owner)
|| player.distanceToSqr(center.getX() + 0.5D, center.getY() + 0.5D, center.getZ() + 0.5D) > 64.0D) {
return false;
}
return vaultData.isRegistered(serverLevel, center, owner)
&& VaultStructure.find(serverLevel, center)
.filter(formed -> formed.center().equals(center) && formed.owner().equals(owner)).isPresent();
}
private int deposit(ServerPlayer player, Item item, int requested) {
int available = countPlayerItem(player, item);
int amount = Math.min(requested, Math.min(available, vaultData.capacity(owner, item)));
if (amount <= 0) return 0;
int removed = removePlayerItem(player, item, amount);
int stored = vaultData.add(owner, item, removed);
if (stored < removed) insertPlayerItem(player, item, removed - stored);
return stored;
}
private int withdraw(ServerPlayer player, Item item, int requested) {
int amount = Math.min(requested,
Math.min(vaultData.count(owner, item), playerCapacity(player, item)));
if (amount <= 0) return 0;
int removed = vaultData.remove(owner, item, amount);
int inserted = insertPlayerItem(player, item, removed);
if (inserted < removed) vaultData.add(owner, item, removed - inserted);
return inserted;
}
private static int countPlayerItem(ServerPlayer player, Item item) {
long count = 0L;
for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) {
if (slot.stack().is(item)) count += slot.stack().getCount();
}
return (int) Math.min(Integer.MAX_VALUE, count);
}
private static int removePlayerItem(ServerPlayer player, Item item, int amount) {
int remaining = amount;
for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) {
if (remaining <= 0) break;
ItemStack stack = slot.stack();
if (!stack.is(item)) continue;
int removed = Math.min(remaining, stack.getCount());
slot.remove(removed);
remaining -= removed;
}
return amount - remaining;
}
private static int playerCapacity(ServerPlayer player, Item item) {
long capacity = 0L;
for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) {
ItemStack stack = slot.stack();
if (stack.isEmpty()) capacity += item.getDefaultMaxStackSize();
else if (stack.is(item)) capacity += Math.max(0, stack.getMaxStackSize() - stack.getCount());
}
return (int) Math.min(Integer.MAX_VALUE, capacity);
}
private static int insertPlayerItem(ServerPlayer player, Item item, int amount) {
int remaining = amount;
var slots = SanctuaryPlayerInventoryView.unlockedRows(player);
for (var slot : slots) {
if (remaining <= 0) break;
ItemStack stack = slot.stack();
if (!stack.is(item) || stack.getCount() >= stack.getMaxStackSize()) continue;
int inserted = Math.min(remaining, stack.getMaxStackSize() - stack.getCount());
stack.grow(inserted);
slot.markChanged();
remaining -= inserted;
}
for (var slot : slots) {
if (remaining <= 0) break;
if (!slot.stack().isEmpty()) continue;
int inserted = Math.min(remaining, item.getDefaultMaxStackSize());
slot.container().setItem(slot.slot(), new ItemStack(item, inserted));
slot.markChanged();
remaining -= inserted;
}
return amount - remaining;
}
private record VaultBalanceData(VaultData data, UUID owner) implements ContainerData {
@Override public int get(int index) {
return index >= 0 && index < CURRENCIES.size() ? data.count(owner, CURRENCIES.get(index)) : 0;
}
@Override public void set(int index, int value) {
}
@Override public int getCount() {
return VaultRules.CURRENCY_SLOTS;
}
}
}
@@ -0,0 +1,306 @@
package fr.koka99cab.sanctuary26.anotherworld.vault;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka99cab.sanctuary26.anotherworld.AnotherWorldMod;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.NonNullList;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.saveddata.SavedDataType;
/** Global player-linked vault inventory, available to the Shop even when its structure is unloaded. */
public final class VaultData extends SavedData {
public static final String SAVE_KEY = "safe_vaults";
public static final int DATA_VERSION = 2;
private static final Identifier RUBY = Identifier.fromNamespaceAndPath("anotherworld", "ruby");
private static final Identifier SAPPHIRE = Identifier.fromNamespaceAndPath("anotherworld", "sapphire");
private static final Identifier EMERALD = Identifier.withDefaultNamespace("emerald");
private static final Codec<StoredSlot> SLOT_CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.INT.fieldOf("slot").forGetter(StoredSlot::slot),
ItemStack.CODEC.fieldOf("item").forGetter(StoredSlot::item)
).apply(instance, StoredSlot::new));
private static final Codec<StoredAccount> ACCOUNT_CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.STRING.fieldOf("owner").forGetter(StoredAccount::owner),
SLOT_CODEC.listOf().optionalFieldOf("items", List.of()).forGetter(StoredAccount::items),
Codec.LONG.optionalFieldOf("protected_until", 0L).forGetter(StoredAccount::protectedUntil)
).apply(instance, StoredAccount::new));
private static final Codec<VaultAddress> ADDRESS_CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.STRING.fieldOf("dimension").forGetter(VaultAddress::dimension),
Codec.LONG.fieldOf("pos").forGetter(VaultAddress::pos),
Codec.STRING.fieldOf("owner").forGetter(VaultAddress::owner)
).apply(instance, VaultAddress::new));
private static final Codec<VaultData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(data -> data.dataVersion),
ACCOUNT_CODEC.listOf().optionalFieldOf("accounts", List.of()).forGetter(VaultData::storedAccounts),
ADDRESS_CODEC.listOf().optionalFieldOf("vaults", List.of()).forGetter(data -> data.vaults)
).apply(instance, VaultData::new));
private static final SavedDataType<VaultData> TYPE = new SavedDataType<>(
AnotherWorldMod.id(SAVE_KEY), VaultData::new, CODEC, null);
private int dataVersion;
private final Map<UUID, Account> accounts = new LinkedHashMap<>();
private final List<VaultAddress> vaults = new ArrayList<>();
public VaultData() {
this(DATA_VERSION, List.of(), List.of());
}
private VaultData(int dataVersion, List<StoredAccount> accounts, List<VaultAddress> vaults) {
this.dataVersion = dataVersion;
for (StoredAccount stored : accounts == null ? List.<StoredAccount>of() : accounts) {
try {
UUID owner = UUID.fromString(stored.owner());
NonNullList<ItemStack> items = NonNullList.withSize(VaultRules.STORAGE_SLOTS, ItemStack.EMPTY);
for (StoredSlot slot : stored.items()) {
if (slot.slot() >= 0 && slot.slot() < items.size() && isCurrency(slot.item())) {
items.set(slot.slot(), slot.item().copy());
}
}
this.accounts.put(owner, new Account(items, Math.max(0L, stored.protectedUntil())));
} catch (IllegalArgumentException ignored) {
AnotherWorldMod.LOGGER.warn("[Vault] Ignored account with invalid owner {}.", stored.owner());
}
}
if (vaults != null) this.vaults.addAll(vaults);
}
public static VaultData get(MinecraftServer server) {
ServerLevel overworld = server.getLevel(Level.OVERWORLD);
if (overworld == null) throw new IllegalStateException("Overworld unavailable for vault data");
VaultData data = overworld.getDataStorage().computeIfAbsent(TYPE);
if (data.dataVersion < DATA_VERSION) {
data.dataVersion = DATA_VERSION;
data.setDirty();
}
return data;
}
public boolean register(ServerLevel level, BlockPos center, UUID owner) {
String dimension = dimension(level);
String ownerId = owner.toString();
for (VaultAddress address : vaults) {
if (address.owner().equals(ownerId)) {
return address.dimension().equals(dimension) && address.pos() == center.asLong();
}
}
vaults.add(new VaultAddress(dimension, center.asLong(), ownerId));
accounts.computeIfAbsent(owner, ignored -> new Account());
setDirty();
return true;
}
public boolean isRegistered(ServerLevel level, BlockPos center, UUID owner) {
String dimension = dimension(level), ownerId = owner.toString();
return vaults.stream().anyMatch(address -> address.dimension().equals(dimension)
&& address.pos() == center.asLong() && address.owner().equals(ownerId));
}
public void unregisterNear(ServerLevel level, BlockPos member) {
String dimension = dimension(level);
boolean changed = vaults.removeIf(address -> {
if (!address.dimension().equals(dimension)) return false;
BlockPos center = BlockPos.of(address.pos());
return Math.abs(center.getX() - member.getX()) <= 1
&& Math.abs(center.getY() - member.getY()) <= 1
&& Math.abs(center.getZ() - member.getZ()) <= 1;
});
if (changed) setDirty();
}
public Container container(UUID owner, ServerLevel level, BlockPos center) {
accounts.computeIfAbsent(owner, ignored -> new Account());
return new VaultContainer(this, owner, level, center.immutable());
}
public int count(UUID owner, Item item) {
if (!isCurrency(item)) return 0;
Account account = accounts.get(owner);
if (account == null) return 0;
long count = 0L;
for (ItemStack stack : account.items) if (stack.is(item)) count += stack.getCount();
return (int) Math.min(Integer.MAX_VALUE, count);
}
public int remove(UUID owner, Item item, int amount) {
if (!isCurrency(item) || amount <= 0) return 0;
Account account = accounts.get(owner);
if (account == null) return 0;
int remaining = amount;
for (ItemStack stack : account.items) {
if (remaining <= 0) break;
if (!stack.is(item)) continue;
int removed = Math.min(remaining, stack.getCount());
stack.shrink(removed);
remaining -= removed;
}
if (remaining != amount) setDirty();
return amount - remaining;
}
public int capacity(UUID owner, Item item) {
if (!isCurrency(item)) return 0;
long capacity = 0L;
for (ItemStack stack : items(owner)) {
if (stack.isEmpty()) capacity += item.getDefaultMaxStackSize();
else if (stack.is(item)) capacity += Math.max(0, stack.getMaxStackSize() - stack.getCount());
}
return (int) Math.min(Integer.MAX_VALUE, capacity);
}
public int add(UUID owner, Item item, int amount) {
if (!isCurrency(item) || amount <= 0) return 0;
NonNullList<ItemStack> stored = items(owner);
int remaining = amount;
for (ItemStack stack : stored) {
if (remaining <= 0) break;
if (!stack.is(item) || stack.getCount() >= stack.getMaxStackSize()) continue;
int inserted = Math.min(remaining, stack.getMaxStackSize() - stack.getCount());
stack.grow(inserted);
remaining -= inserted;
}
for (int slot = 0; slot < stored.size() && remaining > 0; slot++) {
if (!stored.get(slot).isEmpty()) continue;
int inserted = Math.min(remaining, item.getDefaultMaxStackSize());
stored.set(slot, new ItemStack(item, inserted));
remaining -= inserted;
}
if (remaining != amount) setDirty();
return amount - remaining;
}
public boolean hasContents(UUID owner) {
Account account = accounts.get(owner);
return account != null && account.items.stream().anyMatch(stack -> !stack.isEmpty());
}
public List<ItemStack> raidLoot(UUID owner) {
List<ItemStack> loot = new ArrayList<>();
for (Identifier id : List.of(RUBY, SAPPHIRE, EMERALD)) {
Item item = BuiltInRegistries.ITEM.getOptional(id).orElse(null);
if (item == null) continue;
int amount = VaultRules.raidLoot(count(owner, item));
int removed = remove(owner, item, amount);
while (removed > 0) {
int stackSize = Math.min(removed, item.getDefaultMaxStackSize());
loot.add(new ItemStack(item, stackSize));
removed -= stackSize;
}
}
return List.copyOf(loot);
}
public long protectionRemaining(UUID owner, long gameTime) {
Account account = accounts.get(owner);
return account == null ? 0L : Math.max(0L, account.protectedUntil - gameTime);
}
public void protectAfterRaid(UUID owner, long gameTime) {
Account account = accounts.computeIfAbsent(owner, ignored -> new Account());
account.protectedUntil = gameTime + VaultRules.RAID_PROTECTION_TICKS;
setDirty();
}
public static boolean isCurrency(ItemStack stack) {
return stack != null && !stack.isEmpty() && isCurrency(stack.getItem());
}
public static boolean isCurrency(Item item) {
if (item == null) return false;
Identifier id = BuiltInRegistries.ITEM.getKey(item);
return RUBY.equals(id) || SAPPHIRE.equals(id) || EMERALD.equals(id);
}
private NonNullList<ItemStack> items(UUID owner) {
return accounts.computeIfAbsent(owner, ignored -> new Account()).items;
}
private List<StoredAccount> storedAccounts() {
List<StoredAccount> stored = new ArrayList<>();
accounts.forEach((owner, account) -> {
List<StoredSlot> slots = new ArrayList<>();
for (int index = 0; index < account.items.size(); index++) {
ItemStack stack = account.items.get(index);
if (!stack.isEmpty()) slots.add(new StoredSlot(index, stack));
}
stored.add(new StoredAccount(owner.toString(), List.copyOf(slots), account.protectedUntil));
});
return List.copyOf(stored);
}
private static String dimension(ServerLevel level) {
return level.dimension().identifier().toString();
}
private static final class Account {
private final NonNullList<ItemStack> items;
private long protectedUntil;
private Account() { this(NonNullList.withSize(VaultRules.STORAGE_SLOTS, ItemStack.EMPTY), 0L); }
private Account(NonNullList<ItemStack> items, long protectedUntil) {
this.items = items;
this.protectedUntil = protectedUntil;
}
}
private static final class VaultContainer implements Container {
private final VaultData data;
private final UUID owner;
private final ServerLevel level;
private final BlockPos center;
private VaultContainer(VaultData data, UUID owner, ServerLevel level, BlockPos center) {
this.data = data;
this.owner = owner;
this.level = level;
this.center = center;
}
@Override public int getContainerSize() { return VaultRules.STORAGE_SLOTS; }
@Override public boolean isEmpty() { return data.items(owner).stream().allMatch(ItemStack::isEmpty); }
@Override public ItemStack getItem(int slot) { return data.items(owner).get(slot); }
@Override public ItemStack removeItem(int slot, int amount) {
ItemStack removed = ContainerHelper.removeItem(data.items(owner), slot, amount);
if (!removed.isEmpty()) setChanged();
return removed;
}
@Override public ItemStack removeItemNoUpdate(int slot) {
ItemStack removed = ContainerHelper.takeItem(data.items(owner), slot);
if (!removed.isEmpty()) setChanged();
return removed;
}
@Override public void setItem(int slot, ItemStack stack) {
if (stack != null && !stack.isEmpty() && !isCurrency(stack)) return;
data.items(owner).set(slot, stack == null ? ItemStack.EMPTY : stack);
setChanged();
}
@Override public void setChanged() { data.setDirty(); }
@Override public boolean stillValid(Player player) {
if (player == null || !player.getUUID().equals(owner)
|| player.distanceToSqr(center.getX() + 0.5D, center.getY() + 0.5D, center.getZ() + 0.5D) > 64.0D)
return false;
return data.isRegistered(level, center, owner)
&& VaultStructure.find(level, center).filter(formed -> formed.center().equals(center)).isPresent();
}
@Override public void clearContent() { data.items(owner).clear(); setChanged(); }
@Override public boolean canPlaceItem(int slot, ItemStack stack) { return isCurrency(stack); }
}
private record StoredSlot(int slot, ItemStack item) {}
private record StoredAccount(String owner, List<StoredSlot> items, long protectedUntil) {}
private record VaultAddress(String dimension, long pos, String owner) {}
}
@@ -0,0 +1,176 @@
package fr.koka99cab.sanctuary26.anotherworld.vault;
import fr.koka99cab.sanctuary26.anotherworld.AnotherWorldMod;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldItems;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
/** Server-authoritative continuous titanium-drill raid session. */
public final class VaultDrillingService {
private static final Map<UUID, Session> SESSIONS = new HashMap<>();
private static final Map<Target, UUID> ACTIVE_TARGETS = new HashMap<>();
private static boolean initialized;
private VaultDrillingService() {
}
public static void initialize() {
if (initialized) return;
initialized = true;
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> cancel(handler.player, false));
}
public static boolean begin(ServerPlayer attacker, ServerLevel level, BlockPos clicked, InteractionHand hand) {
VaultStructure.Formed formed = VaultStructure.find(level, clicked).orElse(null);
if (formed == null) {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.incomplete"));
return false;
}
if (formed.owner().equals(attacker.getUUID())) {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.drill_own"));
return false;
}
VaultData data = VaultData.get(level.getServer());
if (!data.isRegistered(level, formed.center(), formed.owner())) {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.inactive"));
return false;
}
if (!data.hasContents(formed.owner())) {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.empty"));
return false;
}
ServerPlayer owner = level.getServer().getPlayerList().getPlayer(formed.owner());
if (owner == null) {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.owner_offline"));
return false;
}
long remaining = data.protectionRemaining(formed.owner(), level.getServer().overworld().getGameTime());
if (remaining > 0L) {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.protected",
Math.max(1L, (remaining + 1199L) / 1200L)));
return false;
}
Target target = Target.of(level, formed.center());
UUID active = ACTIVE_TARGETS.get(target);
if (active != null && !active.equals(attacker.getUUID())) {
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.already_drilling"));
return false;
}
cancel(attacker, false);
Session session = new Session(target, formed.owner(), hand, 0);
SESSIONS.put(attacker.getUUID(), session);
ACTIVE_TARGETS.put(target, attacker.getUUID());
owner.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.alarm",
attacker.getDisplayName(), formed.center().getX(), formed.center().getY(), formed.center().getZ()));
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.drilling_started"));
level.playSound(null, clicked, SoundEvents.PISTON_EXTEND, SoundSource.BLOCKS, 1.0F, 0.65F);
AnotherWorldMod.LOGGER.info("[Vault] {} started drilling {}'s vault at {}.",
attacker.getGameProfile().name(), owner.getGameProfile().name(), formed.center().toShortString());
return true;
}
public static void tick(ServerPlayer attacker, ItemStack drill) {
Session session = SESSIONS.get(attacker.getUUID());
if (session == null) return;
if (!drill.is(AnotherWorldItems.TITANIUM_DRILL)
|| attacker.getUsedItemHand() != session.hand()
|| !(attacker.level() instanceof ServerLevel level)
|| !session.target().dimension().equals(dimension(level))) {
cancel(attacker, true);
return;
}
HitResult hit = attacker.pick(attacker.blockInteractionRange() + 1.0D, 1.0F, false);
VaultStructure.Formed formed = hit instanceof BlockHitResult blockHit
? VaultStructure.find(level, blockHit.getBlockPos()).orElse(null) : null;
if (formed == null || !formed.center().equals(session.target().center())
|| !formed.owner().equals(session.owner())
|| level.getServer().getPlayerList().getPlayer(session.owner()) == null) {
cancel(attacker, true);
attacker.stopUsingItem();
return;
}
int elapsed = session.elapsedTicks() + 1;
Session progressed = session.withElapsed(elapsed);
SESSIONS.put(attacker.getUUID(), progressed);
if (elapsed % 10 == 0) {
BlockPos center = formed.center();
level.playSound(null, center, SoundEvents.ANVIL_LAND, SoundSource.BLOCKS, 0.35F, 1.65F);
level.sendParticles(ParticleTypes.ELECTRIC_SPARK, center.getX() + 0.5D, center.getY() + 1.0D,
center.getZ() + 0.5D, 5, 0.7D, 0.25D, 0.7D, 0.04D);
}
if (elapsed % 20 == 0) {
attacker.sendOverlayMessage(Component.translatable("message.anotherworld.safe_vault.drilling_progress",
VaultRules.progressPercent(elapsed)));
}
if (elapsed >= VaultRules.RAID_DURATION_TICKS) complete(attacker, level, progressed, formed);
}
private static void complete(ServerPlayer attacker, ServerLevel level, Session session,
VaultStructure.Formed formed) {
VaultData data = VaultData.get(level.getServer());
List<ItemStack> loot = data.raidLoot(session.owner());
if (loot.isEmpty()) {
cancel(attacker, true);
attacker.stopUsingItem();
return;
}
int total = loot.stream().mapToInt(ItemStack::getCount).sum();
loot.forEach(stack -> attacker.getInventory().placeItemBackInInventory(stack));
data.protectAfterRaid(session.owner(), level.getServer().overworld().getGameTime());
ItemStack drill = attacker.getItemInHand(session.hand());
if (!attacker.hasInfiniteMaterials()) drill.shrink(1);
ServerPlayer owner = level.getServer().getPlayerList().getPlayer(session.owner());
if (owner != null) owner.sendSystemMessage(Component.translatable(
"message.anotherworld.safe_vault.raided_owner", attacker.getDisplayName(), total));
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.raided", total));
level.playSound(null, formed.center(), SoundEvents.VAULT_OPEN_SHUTTER, SoundSource.BLOCKS, 1.0F, 0.8F);
level.sendParticles(ParticleTypes.CRIT, formed.center().getX() + 0.5D, formed.center().getY() + 1.0D,
formed.center().getZ() + 0.5D, 24, 0.8D, 0.4D, 0.8D, 0.12D);
AnotherWorldMod.LOGGER.info("[Vault] {} stole {} currency items from {} at {}.",
attacker.getGameProfile().name(), total, session.owner(), formed.center().toShortString());
cancel(attacker, false);
attacker.stopUsingItem();
}
public static void cancel(ServerPlayer player, boolean notify) {
Session removed = SESSIONS.remove(player.getUUID());
if (removed == null) return;
ACTIVE_TARGETS.remove(removed.target(), player.getUUID());
if (notify) player.sendOverlayMessage(Component.translatable("message.anotherworld.safe_vault.drilling_cancelled"));
}
public static boolean isActive(ServerLevel level, BlockPos center) {
return ACTIVE_TARGETS.containsKey(Target.of(level, center));
}
private static String dimension(ServerLevel level) {
return level.dimension().identifier().toString();
}
private record Target(String dimension, BlockPos center) {
private static Target of(ServerLevel level, BlockPos center) {
return new Target(VaultDrillingService.dimension(level), center.immutable());
}
}
private record Session(Target target, UUID owner, InteractionHand hand, int elapsedTicks) {
private Session withElapsed(int elapsed) { return new Session(target, owner, hand, elapsed); }
}
}
@@ -0,0 +1,46 @@
package fr.koka99cab.sanctuary26.anotherworld.vault;
import net.minecraft.util.StringRepresentable;
/** Stable position of one segment in the front-facing 3x3 vault wall. */
public enum VaultPart implements StringRepresentable {
BOTTOM_LEFT("bottom_left", 0, 0),
BOTTOM_CENTER("bottom_center", 1, 0),
BOTTOM_RIGHT("bottom_right", 2, 0),
MIDDLE_LEFT("middle_left", 0, 1),
CENTER("center", 1, 1),
MIDDLE_RIGHT("middle_right", 2, 1),
TOP_LEFT("top_left", 0, 2),
TOP_CENTER("top_center", 1, 2),
TOP_RIGHT("top_right", 2, 2);
private final String serializedName;
private final int column;
private final int row;
VaultPart(String serializedName, int column, int row) {
this.serializedName = serializedName;
this.column = column;
this.row = row;
}
public int column() {
return column;
}
public int row() {
return row;
}
public static VaultPart at(int column, int row) {
for (VaultPart part : values()) {
if (part.column == column && part.row == row) return part;
}
throw new IllegalArgumentException("Invalid vault part: " + column + ", " + row);
}
@Override
public String getSerializedName() {
return serializedName;
}
}
@@ -0,0 +1,53 @@
package fr.koka99cab.sanctuary26.anotherworld.vault;
/** Pure balancing rules shared by runtime code and smoke tests. */
public final class VaultRules {
public static final int STRUCTURE_WIDTH = 3;
public static final int STRUCTURE_RADIUS = 1;
public static final int STRUCTURE_BLOCKS = STRUCTURE_WIDTH * STRUCTURE_WIDTH;
public static final int COMPLETE_STRUCTURE_MASK = (1 << STRUCTURE_BLOCKS) - 1;
public static final int STORAGE_SLOTS = 27;
public static final int CURRENCY_SLOTS = 3;
public static final int BUTTONS_PER_CURRENCY = 4;
public static final int RAID_DURATION_TICKS = 60 * 20;
public static final int RAID_LOOT_PERCENT = 20;
public static final long RAID_PROTECTION_TICKS = 24L * 60L * 60L * 20L;
private VaultRules() {
}
public static int raidLoot(int stored) {
if (stored <= 0) return 0;
long rounded = ((long) stored * RAID_LOOT_PERCENT + 99L) / 100L;
return (int) Math.min(stored, Math.max(1L, rounded));
}
public static int progressPercent(int elapsedTicks) {
return Math.clamp((int) ((long) Math.max(0, elapsedTicks) * 100L / RAID_DURATION_TICKS), 0, 100);
}
public static boolean isCompleteStructureMask(int partMask) {
return partMask == COMPLETE_STRUCTURE_MASK;
}
public static int buttonCurrency(int buttonId) {
return validButton(buttonId) ? buttonId / BUTTONS_PER_CURRENCY : -1;
}
public static int buttonAmount(int buttonId) {
if (!validButton(buttonId)) return 0;
return switch (buttonId % BUTTONS_PER_CURRENCY) {
case 0, 3 -> 64;
case 1, 2 -> 1;
default -> 0;
};
}
public static boolean buttonDeposits(int buttonId) {
return validButton(buttonId) && buttonId % BUTTONS_PER_CURRENCY >= 2;
}
private static boolean validButton(int buttonId) {
return buttonId >= 0 && buttonId < CURRENCY_SLOTS * BUTTONS_PER_CURRENCY;
}
}
@@ -0,0 +1,99 @@
package fr.koka99cab.sanctuary26.anotherworld.vault;
import fr.koka99cab.sanctuary26.anotherworld.AnotherWorldMod;
import fr.koka99cab.sanctuary26.anotherworld.block.entity.SafeVaultBlockEntity;
import fr.koka99cab.sanctuary26.anotherworld.block.SafeVaultBlock;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlocks;
import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryCurrencyStorage;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.permissions.Permissions;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
/** Owns vault lifecycle, break protection and Sanctuary currency-storage integration. */
public final class VaultService {
private static boolean initialized;
private VaultService() {
}
public static void initialize() {
if (initialized) return;
initialized = true;
SanctuaryCurrencyStorage.register(AnotherWorldMod.id("safe_vault"), new SanctuaryCurrencyStorage.Provider() {
@Override public int count(ServerPlayer player, net.minecraft.world.item.Item item) {
return VaultData.get(player.level().getServer()).count(player.getUUID(), item);
}
@Override public int remove(ServerPlayer player, net.minecraft.world.item.Item item, int amount) {
return VaultData.get(player.level().getServer()).remove(player.getUUID(), item, amount);
}
});
PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> {
if (!state.is(AnotherWorldBlocks.SAFE_BLOCK) || !(level instanceof ServerLevel serverLevel)
|| !(blockEntity instanceof SafeVaultBlockEntity segment)) return true;
if (isOperator(player)) return true;
if (segment.owner() == null || !segment.owner().equals(player.getUUID())) {
player.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.break_not_owner"));
return false;
}
VaultStructure.Formed formed = VaultStructure.find(serverLevel, pos).orElse(null);
if (formed != null && (VaultData.get(serverLevel.getServer()).hasContents(formed.owner())
|| VaultDrillingService.isActive(serverLevel, formed.center()))) {
player.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.empty_before_break"));
return false;
}
return true;
});
VaultDrillingService.initialize();
}
public static void onSegmentPlaced(ServerLevel level, net.minecraft.core.BlockPos pos, ServerPlayer player) {
VaultStructure.find(level, pos).ifPresent(formed -> tryRegister(level, formed, player));
}
public static void onSegmentRemoved(ServerLevel level, net.minecraft.core.BlockPos pos, BlockState state) {
VaultData.get(level.getServer()).unregisterNear(level, pos);
if (!state.getValue(SafeVaultBlock.FORMED)) return;
net.minecraft.core.BlockPos center = VaultStructure.centerFor(pos, state);
var facing = state.getValue(SafeVaultBlock.FACING);
var width = facing.getClockWise();
for (int column = 0; column < VaultRules.STRUCTURE_WIDTH; column++) {
for (int row = 0; row < VaultRules.STRUCTURE_WIDTH; row++) {
var member = center.relative(width, column - VaultRules.STRUCTURE_RADIUS)
.above(row - VaultRules.STRUCTURE_RADIUS);
BlockState memberState = level.getBlockState(member);
if (!memberState.is(AnotherWorldBlocks.SAFE_BLOCK)) continue;
level.setBlock(member, memberState
.setValue(SafeVaultBlock.FORMED, false)
.setValue(SafeVaultBlock.PART, VaultPart.CENTER), Block.UPDATE_CLIENTS);
}
}
}
public static boolean tryRegister(ServerLevel level, VaultStructure.Formed formed, ServerPlayer player) {
VaultData data = VaultData.get(level.getServer());
if (data.register(level, formed.center(), formed.owner())) {
formed.members().forEach(member -> level.setBlock(member,
level.getBlockState(member)
.setValue(SafeVaultBlock.FACING, formed.facing())
.setValue(SafeVaultBlock.FORMED, true)
.setValue(SafeVaultBlock.PART, formed.part(member)),
Block.UPDATE_CLIENTS));
if (player != null) player.sendSystemMessage(Component.translatable(
"message.anotherworld.safe_vault.formed",
formed.center().getX(), formed.center().getY(), formed.center().getZ()));
return true;
}
if (player != null) player.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.already_owned"));
return false;
}
private static boolean isOperator(Player player) {
return player instanceof ServerPlayer serverPlayer && serverPlayer.createCommandSourceStack().permissions()
.hasPermission(Permissions.COMMANDS_GAMEMASTER);
}
}
@@ -0,0 +1,92 @@
package fr.koka99cab.sanctuary26.anotherworld.vault;
import fr.koka99cab.sanctuary26.anotherworld.block.entity.SafeVaultBlockEntity;
import fr.koka99cab.sanctuary26.anotherworld.block.SafeVaultBlock;
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlocks;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.Level;
/** Detects one isolated, upright 3x3 vault wall owned by a single player. */
public final class VaultStructure {
private VaultStructure() {
}
public static Optional<Formed> find(Level level, BlockPos member) {
if (level == null || member == null || !level.getBlockState(member).is(AnotherWorldBlocks.SAFE_BLOCK)) {
return Optional.empty();
}
Direction preferred = level.getBlockState(member).getValue(SafeVaultBlock.FACING);
for (Direction facing : List.of(preferred, preferred.getOpposite(),
preferred.getClockWise(), preferred.getCounterClockWise())) {
Direction width = facing.getClockWise();
for (int offsetWidth = -VaultRules.STRUCTURE_RADIUS;
offsetWidth <= VaultRules.STRUCTURE_RADIUS; offsetWidth++) {
for (int offsetY = -VaultRules.STRUCTURE_RADIUS;
offsetY <= VaultRules.STRUCTURE_RADIUS; offsetY++) {
BlockPos center = member.relative(width, -offsetWidth).below(offsetY);
Formed formed = validate(level, center, facing);
if (formed != null && formed.contains(member)) return Optional.of(formed);
}
}
}
return Optional.empty();
}
private static Formed validate(Level level, BlockPos center, Direction facing) {
UUID owner = null;
List<BlockPos> members = new ArrayList<>(VaultRules.STRUCTURE_BLOCKS);
int partMask = 0;
Direction width = facing.getClockWise();
for (int column = 0; column < VaultRules.STRUCTURE_WIDTH; column++) {
for (int row = 0; row < VaultRules.STRUCTURE_WIDTH; row++) {
BlockPos pos = center.relative(width, column - VaultRules.STRUCTURE_RADIUS)
.above(row - VaultRules.STRUCTURE_RADIUS);
if (!level.getBlockState(pos).is(AnotherWorldBlocks.SAFE_BLOCK)
|| !(level.getBlockEntity(pos) instanceof SafeVaultBlockEntity blockEntity)
|| blockEntity.owner() == null) return null;
if (owner == null) owner = blockEntity.owner();
else if (!owner.equals(blockEntity.owner())) return null;
members.add(pos.immutable());
partMask |= 1 << (row * VaultRules.STRUCTURE_WIDTH + column);
}
}
if (!VaultRules.isCompleteStructureMask(partMask)) return null;
// Reject ambiguous 3x4/4x3 walls: a vault is one discrete upright 3x3 structure.
for (int widthOffset = -2; widthOffset <= 2; widthOffset++) {
for (int yOffset = -2; yOffset <= 2; yOffset++) {
if (Math.abs(widthOffset) <= 1 && Math.abs(yOffset) <= 1) continue;
if (level.getBlockState(center.relative(width, widthOffset).above(yOffset))
.is(AnotherWorldBlocks.SAFE_BLOCK)) return null;
}
}
return new Formed(center.immutable(), owner, List.copyOf(members), facing);
}
public static BlockPos centerFor(BlockPos member, BlockState state) {
VaultPart part = state.getValue(SafeVaultBlock.PART);
Direction width = state.getValue(SafeVaultBlock.FACING).getClockWise();
return member.relative(width, VaultRules.STRUCTURE_RADIUS - part.column())
.below(part.row() - VaultRules.STRUCTURE_RADIUS).immutable();
}
public record Formed(BlockPos center, UUID owner, List<BlockPos> members, Direction facing) {
public boolean contains(BlockPos pos) {
return pos != null && members.contains(pos);
}
public VaultPart part(BlockPos pos) {
Direction width = facing.getClockWise();
int column = (pos.getX() - center.getX()) * width.getStepX()
+ (pos.getZ() - center.getZ()) * width.getStepZ() + VaultRules.STRUCTURE_RADIUS;
int row = pos.getY() - center.getY() + VaultRules.STRUCTURE_RADIUS;
return VaultPart.at(column, row);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

@@ -30,13 +30,14 @@
"minecraft": "~26.2",
"java": ">=25",
"fabric-api": ">=0.154.2+26.2",
"ambiance": "*"
"ambiance": "*",
"sanctuary": ">=0.0.0-alpha.117"
},
"custom": {
"sanctuary26": {
"system_type": "gameplay-system",
"namespace": "anotherworld",
"data_version": 1,
"data_version": 3,
"network_protocol": 1,
"lifecycle": "${lifecycle}",
"pack_version": "${pack_version}",
@@ -0,0 +1,45 @@
package fr.koka99cab.sanctuary26.anotherworld.vault;
public final class VaultRulesSmoke {
private VaultRulesSmoke() {
}
public static void main(String[] args) {
require(VaultRules.STRUCTURE_WIDTH == 3, "vault width");
require(VaultRules.STRUCTURE_BLOCKS == 9, "upright vault wall");
require(!VaultRules.isCompleteStructureMask(0b000_000_111),
"horizontal 3x1 must stay unformed");
require(!VaultRules.isCompleteStructureMask(0b001_001_001),
"vertical 3x1 must stay unformed");
require(VaultRules.isCompleteStructureMask(0b111_111_111),
"only all nine parts form the vault");
require(VaultRules.STORAGE_SLOTS == 27, "vault capacity");
require(VaultRules.CURRENCY_SLOTS == 3, "currency controls");
require(VaultPart.at(0, 0) == VaultPart.BOTTOM_LEFT, "bottom-left CTM part");
require(VaultPart.at(1, 1) == VaultPart.CENTER, "center CTM part");
require(VaultPart.at(2, 2) == VaultPart.TOP_RIGHT, "top-right CTM part");
require(VaultRules.buttonCurrency(0) == 0 && VaultRules.buttonCurrency(11) == 2,
"button currency routing");
require(VaultRules.buttonAmount(0) == 64 && VaultRules.buttonAmount(1) == 1
&& VaultRules.buttonAmount(2) == 1 && VaultRules.buttonAmount(3) == 64,
"button transfer amounts");
require(!VaultRules.buttonDeposits(1) && VaultRules.buttonDeposits(2),
"button transfer directions");
require(VaultRules.buttonCurrency(12) == -1 && VaultRules.buttonAmount(-1) == 0,
"invalid button rejection");
require(VaultRules.RAID_DURATION_TICKS == 1_200, "raid duration");
require(VaultRules.RAID_PROTECTION_TICKS == 1_728_000L, "raid protection");
require(VaultRules.raidLoot(0) == 0, "empty loot");
require(VaultRules.raidLoot(1) == 1, "minimum loot");
require(VaultRules.raidLoot(5) == 1, "exact 20 percent");
require(VaultRules.raidLoot(64) == 13, "rounded 20 percent");
require(VaultRules.raidLoot(Integer.MAX_VALUE) == 429_496_730, "bounded large balance");
require(VaultRules.progressPercent(0) == 0, "initial progress");
require(VaultRules.progressPercent(600) == 50, "half progress");
require(VaultRules.progressPercent(1_200) == 100, "complete progress");
}
private static void require(boolean condition, String contract) {
if (!condition) throw new IllegalStateException("Invalid vault contract: " + contract);
}
}
+312 -54
View File
@@ -329,7 +329,7 @@ tasks.register("verifyWorkspace") {
}
def progressionJeiMigration = new JsonSlurper().parse(file(
"pack/migrations/26.2.0-alpha.87-progression-jei-audio-to-alpha.88.json"))
def jeiFiles = prismIndex.files.findAll { it.path == "mods/jei-26.2-fabric-30.26.0.186.jar" }
def jeiFiles = prismIndex.files.findAll { it.path == "mods/jei-26.2-fabric-30.27.0.187.jar" }
if (progressionJeiMigration.source?.pack_version != "26.2.0-alpha.87"
|| progressionJeiMigration.target?.pack_version != "26.2.0-alpha.88"
|| progressionJeiMigration.target?.modules?.sanctuary != "0.0.0-alpha.38"
@@ -344,8 +344,8 @@ tasks.register("verifyWorkspace") {
|| progressionJeiMigration.compatibility?.network_protocol != 10
|| progressionJeiMigration.compatibility?.network_protocol_changed != true
|| jeiFiles.size() != 1
|| jeiFiles[0].hashes?.sha1 != "950efcf2d63a3a0560eae81223479577ad1a78cd"
|| jeiFiles[0].hashes?.sha512 != "6d8aaad5066d607e460db6f83cba28807129104b46c580b5ba3372d7b7cef84e28ade393c0a3d6bbb38191bcac053af691d0f58d8075f211c687c7b56c74337c"
|| jeiFiles[0].hashes?.sha1 != "cbd62af1f8e568cbe29c7892ae1b2917c0784a44"
|| jeiFiles[0].hashes?.sha512 != "f8bf88c089c7fe3d56031d7629170ba01eb1d0c5cc4d33f2fd0ac9413369cdef84a0fde999528621426d3dca878c9cfaf7a790622e2cd1d54802e1086fb073f0"
|| jeiFiles[0].env?.client != "required" || jeiFiles[0].env?.server != "required") {
throw new GradleException("The alpha.88 progression, JEI and Minecrafter audio contract is incomplete")
}
@@ -1815,7 +1815,7 @@ tasks.register("verifyWorkspace") {
|| terramixMigration.compatibility?.new_registry_ids != 100
|| terramixMigration.compatibility?.worldgen_changed != true
|| terramixMigration.compatibility?.save_migration_required != false
|| anotherWorldManifest?.custom?.sanctuary26?.data_version != 1
|| anotherWorldManifest?.custom?.sanctuary26?.data_version != 3
|| anotherWorldManifest?.custom?.sanctuary26?.network_protocol != 1
|| sanctuaryManifest?.custom?.sanctuary26?.data_version < 9
) {
@@ -3427,8 +3427,8 @@ tasks.register("verifyProgressivePauseMenuRelease") {
[(locale): new JsonSlurper().parse(file(
"sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))]
}
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|| rootProject.onlyfun_version != "0.0.0-alpha.19"
|| 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"]
@@ -3477,8 +3477,8 @@ tasks.register("verifyMirrorLavenderDrawerRelease") {
def redStonerRegistry = file(
"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.201"
|| rootProject.anotherworld_version != "0.0.0-alpha.49"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.anotherworld_version != "0.0.0-alpha.52"
|| rootProject.redstoner_version != "0.0.0-alpha.10"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.pack_version != "26.2.0-alpha.191"
@@ -3586,6 +3586,7 @@ project(":itsalive") {
project(":anotherworld") {
dependencies {
implementation project(":ambiance")
implementation project(":sanctuary")
}
}
@@ -4122,9 +4123,9 @@ tasks.register("verifyShopPricingLayoutRelease") {
"pack/migrations/26.2.0-alpha.192-shop-pricing-layout-to-alpha.193.json"))
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.201"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.onlyfun_version != "0.0.0-alpha.19"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|| 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"
@@ -4167,8 +4168,8 @@ tasks.register("verifyCuisineCropsRelease") {
def climate = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ClimateCropForaging.java").text
def rice = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/RiceCropBlock.java").text
def bean = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/BeanCropBlock.java").text
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.itsalive_version != "0.0.0-alpha.30"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.itsalive_version != "0.0.0-alpha.31"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.pack_version != "26.2.0-alpha.193"
|| migration.source?.modules != [itsalive: "0.0.0-alpha.26"]
@@ -4216,9 +4217,9 @@ tasks.register("verifyShopCsvBossGatesRelease") {
def bossGate = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunBossShopGate.java").text
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.201"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.onlyfun_version != "0.0.0-alpha.19"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|| 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"]
@@ -4262,7 +4263,7 @@ tasks.register("verifyCulinaryMailboxGrappleFixesRelease") {
def release = new JsonSlurper().parse(file("pack/release.json"))
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
def packwiz = file("pack/packwiz/pack.toml").text
if (rootProject.pack_version != "26.2.0-alpha.201"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| migration.source?.pack_version != "26.2.0-alpha.195"
|| migration.target?.pack_version != "26.2.0-alpha.196"
|| migration.target?.modules != [
@@ -4280,10 +4281,10 @@ tasks.register("verifyCulinaryMailboxGrappleFixesRelease") {
|| migration.distribution?.jei?.target != "30.26.0.182"
|| migration.compatibility?.save_migration_required != false
|| migration.compatibility?.registry_ids_changed != false
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')) {
|| release.pack_version != "26.2.0-alpha.203"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.203.mrpack"
|| prism.versionId != "26.2.0-alpha.203"
|| !packwiz.contains('version = "26.2.0-alpha.203"')) {
throw new GradleException("The alpha.196 culinary, mailbox and grapple release contract is incomplete")
}
}
@@ -4308,9 +4309,9 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") {
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
def packwiz = file("pack/packwiz/pack.toml").text
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.itsalive_version != "0.0.0-alpha.30"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.itsalive_version != "0.0.0-alpha.31"
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|| 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"]
@@ -4328,10 +4329,10 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") {
|| migration.distribution?.jei?.target != "30.26.0.186"
|| migration.compatibility?.sanctuary_target_network_protocol != 19
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')) {
|| release.pack_version != "26.2.0-alpha.203"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.203.mrpack"
|| prism.versionId != "26.2.0-alpha.203"
|| !packwiz.contains('version = "26.2.0-alpha.203"')) {
throw new GradleException("The alpha.197 quest-request and cauldron-batch release contract is incomplete")
}
}
@@ -4358,8 +4359,8 @@ tasks.register("verifyFermentationBarrelAutomationRelease") {
def packwiz = file("pack/packwiz/pack.toml").text
def packwizIndex = file("pack/packwiz/index.toml").text
def itsAliveManifest = new JsonSlurper().parse(file("itsalive/src/main/resources/fabric.mod.json"))
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.itsalive_version != "0.0.0-alpha.30"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.itsalive_version != "0.0.0-alpha.31"
|| migration.source?.pack_version != "26.2.0-alpha.197"
|| migration.target?.pack_version != "26.2.0-alpha.198"
|| migration.target?.modules != [itsalive: "0.0.0-alpha.30", sanctuary: "0.0.0-alpha.114"]
@@ -4377,11 +4378,11 @@ tasks.register("verifyFermentationBarrelAutomationRelease") {
|| migration.distribution?.packwiz_index_refreshed != true
|| itsAliveManifest.custom?.sanctuary26?.data_version != 1
|| itsAliveManifest.custom?.sanctuary26?.network_protocol != 3
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.30.jar"')) {
|| release.pack_version != "26.2.0-alpha.203"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.203.mrpack"
|| prism.versionId != "26.2.0-alpha.203"
|| !packwiz.contains('version = "26.2.0-alpha.203"')
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.31.jar"')) {
throw new GradleException("The alpha.198 fermentation barrel release contract is incomplete")
}
}
@@ -4408,9 +4409,9 @@ tasks.register("verifyQuestRequestContractsRelease") {
def packwiz = file("pack/packwiz/pack.toml").text
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.201"
|| rootProject.itsalive_version != "0.0.0-alpha.30"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.itsalive_version != "0.0.0-alpha.31"
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|| 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"
@@ -4422,12 +4423,12 @@ tasks.register("verifyQuestRequestContractsRelease") {
|| migration.quest_request_contracts?.chat_accept_revalidates_distance_owner_sign_and_claim != true
|| migration.compatibility?.sanctuary_target_network_protocol != 20
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.30.jar"')
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.116.jar"')) {
|| release.pack_version != "26.2.0-alpha.203"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.203.mrpack"
|| prism.versionId != "26.2.0-alpha.203"
|| !packwiz.contains('version = "26.2.0-alpha.203"')
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.31.jar"')
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.117.jar"')) {
throw new GradleException("The alpha.199 quest-request contract release is incomplete")
}
}
@@ -4463,7 +4464,7 @@ tasks.register("verifyWeatherTntAmbianceRelease") {
def ambianceManifest = new JsonSlurper().parse(file("ambiance/src/main/resources/fabric.mod.json"))
def ouchManifest = new JsonSlurper().parse(file("ouch/src/main/resources/fabric.mod.json"))
def rootBuild = file("build.gradle").text
if (rootProject.pack_version != "26.2.0-alpha.201"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.ambiance_version != "0.0.0-alpha.12"
|| rootProject.ouch_version != "0.0.0-alpha.9"
|| migration.source?.pack_version != "26.2.0-alpha.199"
@@ -4497,10 +4498,10 @@ tasks.register("verifyWeatherTntAmbianceRelease") {
|| !weatherTnt.contains("AmbianceDailyWeather.CLEAR")
|| !weatherTnt.contains("CLEAR_WEATHER_TICKS = 864_000")
|| weatherTnt.contains("setWeatherParameters")
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')
|| release.pack_version != "26.2.0-alpha.203"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.203.mrpack"
|| prism.versionId != "26.2.0-alpha.203"
|| !packwiz.contains('version = "26.2.0-alpha.203"')
|| !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.12.jar"')
|| !packwizIndex.contains('file = "mods/ouch-0.0.0-alpha.9.jar"')) {
throw new GradleException("The alpha.200 Weather TNT and Ambiance release is incomplete")
@@ -4529,8 +4530,8 @@ tasks.register("verifyQuestRequestEscrowRelease") {
def packwiz = file("pack/packwiz/pack.toml").text
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.201"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|| 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"
@@ -4544,11 +4545,11 @@ tasks.register("verifyQuestRequestEscrowRelease") {
|| migration.compatibility?.sanctuary_target_network_protocol != 21
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 11
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.116.jar"')) {
|| release.pack_version != "26.2.0-alpha.203"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.203.mrpack"
|| prism.versionId != "26.2.0-alpha.203"
|| !packwiz.contains('version = "26.2.0-alpha.203"')
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.117.jar"')) {
throw new GradleException("The alpha.201 quest-request escrow release is incomplete")
}
}
@@ -4558,6 +4559,263 @@ tasks.named("verifyWorkspace") {
dependsOn tasks.named("verifyQuestRequestEscrowRelease")
}
tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
group = "verification"
description = "Checks the complete Sanctuary 26.2.0-alpha.202 Waystone, Shop and culinary lectern release."
inputs.file(file("pack/migrations/26.2.0-alpha.201-waystone-shop-culinary-to-alpha.202.json"))
inputs.file(file("pack/release.json"))
inputs.file(file("pack/prism/modrinth.index.json"))
inputs.file(file("pack/packwiz/pack.toml"))
inputs.file(file("pack/packwiz/index.toml"))
inputs.files(file(
"itsalive/src/main/resources/fabric.mod.json"), file(
"iliketomoveit/src/main/resources/fabric.mod.json"), file(
"sanctuary/src/main/resources/fabric.mod.json"))
doLast {
def migration = new JsonSlurper().parse(file(
"pack/migrations/26.2.0-alpha.201-waystone-shop-culinary-to-alpha.202.json"))
def release = new JsonSlurper().parse(file("pack/release.json"))
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
def packwiz = file("pack/packwiz/pack.toml").text
def packwizIndex = file("pack/packwiz/index.toml").text
def itsAliveManifest = new JsonSlurper().parse(file(
"itsalive/src/main/resources/fabric.mod.json"))
def moveItManifest = new JsonSlurper().parse(file(
"iliketomoveit/src/main/resources/fabric.mod.json"))
def sanctuaryManifest = new JsonSlurper().parse(file(
"sanctuary/src/main/resources/fabric.mod.json"))
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.anotherworld_version != "0.0.0-alpha.52"
|| rootProject.itsalive_version != "0.0.0-alpha.31"
|| rootProject.iliketomoveit_version != "0.0.0-alpha.14"
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|| migration.source?.pack_version != "26.2.0-alpha.201"
|| migration.source?.modules != [
itsalive: "0.0.0-alpha.30",
iliketomoveit: "0.0.0-alpha.13",
sanctuary: "0.0.0-alpha.116"]
|| migration.target?.pack_version != "26.2.0-alpha.202"
|| migration.target?.modules != [
itsalive: "0.0.0-alpha.31",
iliketomoveit: "0.0.0-alpha.14",
sanctuary: "0.0.0-alpha.117"]
|| migration.waystone_elevator?.undiscovered_downward_destination_allowed != true
|| migration.waystone_elevator?.destination_discovered_after_arrival != true
|| migration.waystone_elevator?.undiscovered_upward_destination_allowed != false
|| migration.waystone_elevator?.undiscovered_ordinary_destination_allowed != false
|| migration.shop_currency_inventory?.all_unlocked_rows_counted != true
|| migration.shop_currency_inventory?.overflow_rows_counted != true
|| migration.shop_currency_inventory?.duplicate_stack_views_deduplicated != true
|| migration.culinary_lectern?.book_tag != "minecraft:lectern_books"
|| migration.culinary_lectern?.owner_catalog_shared_while_placed != true
|| migration.culinary_lectern?.inscription_removed_when_taken != true
|| migration.culinary_lectern?.inscription_removed_when_lectern_broken != true
|| migration.compatibility?.itsalive_data_version != 1
|| migration.compatibility?.itsalive_network_protocol != 3
|| migration.compatibility?.iliketomoveit_data_version != 3
|| migration.compatibility?.iliketomoveit_network_protocol != 5
|| migration.compatibility?.sanctuary_data_version != 11
|| migration.compatibility?.sanctuary_network_protocol != 21
|| migration.compatibility?.save_migration_required != false
|| migration.compatibility?.registry_ids_changed != false
|| itsAliveManifest.custom?.sanctuary26?.data_version != 1
|| itsAliveManifest.custom?.sanctuary26?.network_protocol != 3
|| moveItManifest.custom?.sanctuary26?.data_version != 3
|| moveItManifest.custom?.sanctuary26?.network_protocol != 5
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 11
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|| release.pack_version != "26.2.0-alpha.203"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.203.mrpack"
|| prism.versionId != "26.2.0-alpha.203"
|| !packwiz.contains('version = "26.2.0-alpha.203"')
|| !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.51.jar"')
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.31.jar"')
|| !packwizIndex.contains('file = "mods/iliketomoveit-0.0.0-alpha.14.jar"')
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.117.jar"')) {
throw new GradleException("The alpha.202 Waystone, Shop and culinary lectern release is incomplete")
}
}
}
tasks.register("verifySafeVaultFeature") {
group = "verification"
description = "Checks the current upright 3x3 safe, custom currency interface, drill and Shop contract."
dependsOn ":anotherworld:generateAnotherWorldResources"
inputs.file(file("pack/migrations/anotherworld-safe-vault-data-v2.json"))
inputs.file(file("pack/migrations/anotherworld-safe-vault-data-v3.json"))
inputs.file(file("pack/migrations/anotherworld-safe-vault-fix-alpha52.json"))
inputs.file(file("anotherworld/src/main/resources/fabric.mod.json"))
inputs.file(file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryCurrencyStorage.java"))
inputs.files(fileTree("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault") { include "*.java" })
inputs.file(file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/SafeVaultBlock.java"))
inputs.file(file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/SafeVaultScreen.java"))
inputs.files(fileTree("anotherworld/src/main/resources/assets/anotherworld/textures/block/safe_vault") { include "*.png" })
inputs.file(file("anotherworld/src/main/resources/assets/anotherworld/textures/block/safe_block.png"))
inputs.file(file("anotherworld/src/main/resources/assets/anotherworld/textures/item/safe_block.png"))
inputs.file(file("anotherworld/src/main/resources/assets/anotherworld/textures/item/titanium_drill.png"))
doLast {
def migration = new JsonSlurper().parse(file("pack/migrations/anotherworld-safe-vault-data-v2.json"))
def interfaceMigration = new JsonSlurper().parse(file("pack/migrations/anotherworld-safe-vault-data-v3.json"))
def fixMigration = new JsonSlurper().parse(file("pack/migrations/anotherworld-safe-vault-fix-alpha52.json"))
def manifest = new JsonSlurper().parse(file("anotherworld/src/main/resources/fabric.mod.json"))
def rules = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultRules.java").text
def data = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultData.java").text
def structure = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/VaultStructure.java").text
def menu = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/vault/SafeVaultMenu.java").text
def block = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/block/SafeVaultBlock.java").text
def screen = file("anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/client/SafeVaultScreen.java").text
def currencyApi = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryCurrencyStorage.java").text
def generated = file("anotherworld/build/generated/resources")
def safeRecipe = new JsonSlurper().parse(new File(generated, "data/anotherworld/recipe/safe_block.json"))
def drillRecipe = new JsonSlurper().parse(new File(generated, "data/anotherworld/recipe/titanium_drill.json"))
def safeBlockState = new JsonSlurper().parse(new File(generated, "assets/anotherworld/blockstates/safe_block.json"))
def safeUnformedModel = new JsonSlurper().parse(new File(generated, "assets/anotherworld/models/block/safe_block_unformed.json"))
def safeItemModel = new JsonSlurper().parse(new File(generated, "assets/anotherworld/models/item/safe_block.json"))
def requiredTextures = [
"block/safe_vault/bottom.png": [48, 48], "block/safe_vault/front.png": [48, 48],
"block/safe_vault/side.png": [48, 48], "block/safe_vault/top.png": [48, 48],
"block/safe_block.png": [16, 16], "item/safe_block.png": [16, 16],
"item/titanium_drill.png": [16, 16]
]
def invalidTextures = requiredTextures.findAll { path, size ->
def image = javax.imageio.ImageIO.read(file("anotherworld/src/main/resources/assets/anotherworld/textures/${path}"))
image == null || image.width != size[0] || image.height != size[1]
}.keySet()
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.anotherworld_version != "0.0.0-alpha.52"
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|| migration.schema_version != 1
|| migration.source?.pack_version != "26.2.0-alpha.201"
|| migration.target?.pack_version != "26.2.0-alpha.202"
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.49", sanctuary: "0.0.0-alpha.116"]
|| migration.target?.modules != [anotherworld: "0.0.0-alpha.50", sanctuary: "0.0.0-alpha.117"]
|| migration.safe_vault?.footprint != "3x3_horizontal"
|| migration.safe_vault?.blocks_required != 9
|| migration.safe_vault?.accepted_items != ["anotherworld:ruby", "anotherworld:sapphire", "minecraft:emerald"]
|| migration.safe_vault?.shop_access_when_structure_unloaded != true
|| migration.raid?.continuous_use_ticks != 1200
|| migration.raid?.loot_percent != 20
|| migration.raid?.protection_ticks_after_success != 1728000
|| migration.compatibility?.anotherworld_source_data_version != 1
|| migration.compatibility?.anotherworld_target_data_version != 2
|| migration.compatibility?.anotherworld_network_protocol != 1
|| migration.compatibility?.sanctuary_network_protocol != 21
|| migration.compatibility?.pack_release_updated != true
|| interfaceMigration.source?.pack_version != "26.2.0-alpha.202"
|| interfaceMigration.source?.modules != [anotherworld: "0.0.0-alpha.50"]
|| interfaceMigration.target?.pack_version != "26.2.0-alpha.203"
|| interfaceMigration.target?.modules != [anotherworld: "0.0.0-alpha.51"]
|| interfaceMigration.safe_vault_structure?.source_footprint != "3x3_horizontal"
|| interfaceMigration.safe_vault_structure?.target_footprint != "3x3_upright"
|| interfaceMigration.safe_vault_structure?.blocks_required != 9
|| interfaceMigration.safe_vault_structure?.connected_texture_parts != 9
|| interfaceMigration.safe_vault_interface?.currency_slots != 3
|| interfaceMigration.safe_vault_interface?.controls_per_currency != ["withdraw_64", "withdraw_1", "deposit_1", "deposit_64"]
|| interfaceMigration.compatibility?.anotherworld_target_data_version != 3
|| interfaceMigration.compatibility?.vault_target_data_version != 2
|| interfaceMigration.compatibility?.saved_currency_balances_preserved != true
|| interfaceMigration.compatibility?.pack_release_updated != true
|| fixMigration.source?.pack_version != "26.2.0-alpha.203"
|| fixMigration.source?.modules != [anotherworld: "0.0.0-alpha.51"]
|| fixMigration.target?.pack_version != "26.2.0-alpha.203"
|| fixMigration.target?.modules != [anotherworld: "0.0.0-alpha.52"]
|| fixMigration.safe_vault_structure_fix?.unformed_block_texture != "anotherworld:block/safe_block"
|| fixMigration.safe_vault_structure_fix?.horizontal_3x1_rejected != true
|| fixMigration.safe_vault_structure_fix?.vertical_3x1_rejected != true
|| fixMigration.safe_vault_structure_fix?.complete_3x3_mask_required != true
|| fixMigration.safe_vault_structure_fix?.connected_texture_parts != 9
|| fixMigration.safe_vault_interface_fix?.currency_slots != 3
|| fixMigration.safe_vault_interface_fix?.emerald_row_visible != true
|| fixMigration.safe_vault_interface_fix?.extended_inventory_starts_below_currency_controls != true
|| fixMigration.safe_vault_interface_fix?.native_inventory_label_suppressed != true
|| fixMigration.compatibility?.anotherworld_data_version != 3
|| fixMigration.compatibility?.vault_data_version != 2
|| fixMigration.compatibility?.anotherworld_network_protocol != 1
|| fixMigration.compatibility?.save_migration_required != false
|| fixMigration.compatibility?.registry_ids_changed != false
|| fixMigration.compatibility?.saved_currency_balances_preserved != true
|| fixMigration.compatibility?.pack_release_updated != false
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.117"
|| manifest.custom?.sanctuary26?.data_version != 3
|| !file("build.gradle").text.contains('implementation project(":sanctuary")')
|| !currencyApi.contains("public interface Provider")
|| !currencyApi.contains("countAdditional") || !currencyApi.contains("removeAdditional")
|| !rules.contains("STRUCTURE_WIDTH = 3") || !rules.contains("COMPLETE_STRUCTURE_MASK")
|| !rules.contains("CURRENCY_SLOTS = 3")
|| !rules.contains("BUTTONS_PER_CURRENCY = 4") || !rules.contains("RAID_DURATION_TICKS = 60 * 20")
|| !rules.contains("RAID_LOOT_PERCENT = 20") || !rules.contains("24L * 60L * 60L * 20L")
|| !data.contains('SAVE_KEY = "safe_vaults"') || !data.contains("DATA_VERSION = 2")
|| !structure.contains("upright 3x3") || !structure.contains(".above(row - VaultRules.STRUCTURE_RADIUS)")
|| !structure.contains("isCompleteStructureMask(partMask)")
|| !block.contains("BooleanProperty FORMED") || !block.contains("EnumProperty<VaultPart> PART")
|| !menu.contains("clickMenuButton") || !menu.contains("SanctuaryPlayerInventoryView.unlockedRows")
|| !menu.contains("PLAYER_INVENTORY_Y = 108")
|| !screen.contains("handleInventoryButtonClick") || !screen.contains('" ×64", " ×1", "+ ×1", "+ ×64"')
|| screen.contains("inventoryLabelY =")
|| safeBlockState.multipart?.size() != 37
|| safeBlockState.multipart?.count { it.when?.formed == "true" } != 36
|| safeUnformedModel.textures?.all != "anotherworld:block/safe_block"
|| safeItemModel.parent != "anotherworld:block/safe_block_unformed"
|| safeRecipe.pattern != ["GGG", "GCG", "GGG"]
|| drillRecipe.pattern != ["TTT", "RPR", "FDF"]
|| !invalidTextures.isEmpty()) {
throw new GradleException("The safe-vault feature contract is incomplete: textures=${invalidTextures}")
}
}
}
tasks.register("verifySafeVaultInterfaceAlpha203Release") {
group = "verification"
description = "Checks the complete Sanctuary 26.2.0-alpha.203 upright safe-vault interface release."
inputs.file(file("pack/migrations/anotherworld-safe-vault-data-v3.json"))
inputs.file(file("pack/release.json"))
inputs.file(file("pack/prism/modrinth.index.json"))
inputs.file(file("pack/packwiz/pack.toml"))
inputs.file(file("pack/packwiz/index.toml"))
doLast {
def migration = new JsonSlurper().parse(file("pack/migrations/anotherworld-safe-vault-data-v3.json"))
def release = new JsonSlurper().parse(file("pack/release.json"))
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
def packwiz = file("pack/packwiz/pack.toml").text
def packwizIndex = file("pack/packwiz/index.toml").text
def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.27.0.187.jar" }
if (rootProject.pack_version != "26.2.0-alpha.203"
|| rootProject.anotherworld_version != "0.0.0-alpha.52"
|| 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"
|| migration.target?.modules != [anotherworld: "0.0.0-alpha.51"]
|| migration.compatibility?.anotherworld_source_data_version != 2
|| migration.compatibility?.anotherworld_target_data_version != 3
|| migration.compatibility?.vault_source_data_version != 1
|| migration.compatibility?.vault_target_data_version != 2
|| migration.compatibility?.anotherworld_network_protocol != 1
|| migration.compatibility?.pack_release_updated != true
|| migration.distribution?.packwiz_index_refreshed != true
|| migration.distribution?.jei?.source != "30.26.0.186"
|| migration.distribution?.jei?.target != "30.27.0.187"
|| migration.distribution?.jei?.modrinth_version_id != "XXTFqqwG"
|| migration.distribution?.jei?.prism_manifest_updated != true
|| release.pack_version != rootProject.pack_version
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.203.mrpack"
|| prism.versionId != rootProject.pack_version
|| !packwiz.contains('version = "26.2.0-alpha.203"')
|| !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.51.jar"')
|| packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.50.jar"')
|| jeiFiles.size() != 1
|| jeiFiles[0].hashes?.sha1 != "cbd62af1f8e568cbe29c7892ae1b2917c0784a44"
|| jeiFiles[0].hashes?.sha512 != "f8bf88c089c7fe3d56031d7629170ba01eb1d0c5cc4d33f2fd0ac9413369cdef84a0fde999528621426d3dca878c9cfaf7a790622e2cd1d54802e1086fb073f0") {
throw new GradleException("The alpha.203 upright safe-vault interface release is incomplete")
}
}
}
tasks.named("verifyWorkspace") {
dependsOn tasks.named("verifyWaystoneShopCulinaryAlpha202Release")
dependsOn tasks.named("verifySafeVaultFeature")
dependsOn tasks.named("verifySafeVaultInterfaceAlpha203Release")
}
tasks.named("check") {
dependsOn tasks.named("verifyWorkspace")
dependsOn tasks.named("verifyTransport")
+5 -5
View File
@@ -16,19 +16,19 @@ ambiance_version=0.0.0-alpha.12
ambiance_lifecycle=active
redstoner_version=0.0.0-alpha.10
redstoner_lifecycle=active
anotherworld_version=0.0.0-alpha.49
anotherworld_version=0.0.0-alpha.52
anotherworld_lifecycle=active
itsalive_version=0.0.0-alpha.30
itsalive_version=0.0.0-alpha.31
itsalive_lifecycle=active
ouch_version=0.0.0-alpha.9
ouch_lifecycle=active
masterkey_version=0.0.0-alpha.8
masterkey_lifecycle=active
iliketomoveit_version=0.0.0-alpha.13
iliketomoveit_version=0.0.0-alpha.14
iliketomoveit_lifecycle=active
onlyfun_version=0.0.0-alpha.19
onlyfun_lifecycle=active
sanctuary_version=0.0.0-alpha.116
sanctuary_version=0.0.0-alpha.117
sanctuary_lifecycle=active
pack_version=26.2.0-alpha.201
pack_version=26.2.0-alpha.203
maven_group=fr.koka99cab.sanctuary26
+49 -4
View File
@@ -8,8 +8,16 @@ tasks.register("grapplePhysicsSmoke", JavaExec) {
dependsOn tasks.named("testClasses")
}
tasks.register("waystoneElevatorRulesSmoke", JavaExec) {
group = "verification"
description = "Checks directional discovery rules for direct Waystone elevator travel."
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneElevatorRulesSmoke"
dependsOn tasks.named("testClasses")
}
tasks.named("test") {
// The physics contract is an executable smoke task, not a JUnit engine test.
// Runtime contracts are executable smoke tasks, not JUnit engine tests.
failOnNoDiscoveredTests = false
}
@@ -46,6 +54,10 @@ tasks.register("verifyWaystones") {
'event.key() == 82',
'new WaystoneRenamePayload(source, name)',
'class WaystoneElevatorController',
'class WaystoneElevatorRules',
'canUseDirectDestination(upward',
'canTeleport(elevatorRide, source.getY(), target.getY()',
'discoveries.discover(player.getUUID(), level, target)',
'player.isShiftKeyDown()',
'player.getDeltaMovement().y > JUMP_VELOCITY_THRESHOLD',
'useElevatorControl(player, supportingStone, false)',
@@ -283,7 +295,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.13"
if (project.version.toString() != "0.0.0-alpha.14"
|| atlasCavernsMigration.target?.pack_version != "26.2.0-alpha.150"
|| atlasCavernsMigration.target?.modules?.iliketomoveit != "0.0.0-alpha.9"
|| atlasCavernsMigration.portals?.active_routes != [
@@ -319,7 +331,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.13"
if (project.version.toString() != "0.0.0-alpha.14"
|| recipe.pattern != ["PGP", "GCG", "PBP"]
|| recipe.key != [B: "anotherworld:silver_block", C: "anotherworld:aircraft_cockpit",
G: "anotherworld:silver_gear", P: "anotherworld:silver_plate"]
@@ -348,7 +360,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.13"
if (project.version.toString() != "0.0.0-alpha.14"
|| 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
@@ -365,11 +377,44 @@ tasks.register("verifyGrapplePrediction") {
}
}
tasks.register("verifyWaystoneDescendingRelease") {
group = "verification"
description = "Checks alpha.14 downward discovery for direct Waystone elevators."
inputs.file(rootProject.file(
"pack/migrations/26.2.0-alpha.201-waystone-shop-culinary-to-alpha.202.json"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRules.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/WaystoneTeleportation.java"))
inputs.file(file("src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRulesSmoke.java"))
doLast {
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.201-waystone-shop-culinary-to-alpha.202.json"))
def rules = file(
"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.14"
|| 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
|| migration.waystone_elevator?.undiscovered_upward_destination_allowed != false
|| migration.waystone_elevator?.undiscovered_ordinary_destination_allowed != false
|| migration.compatibility?.iliketomoveit_data_version != 3
|| migration.compatibility?.iliketomoveit_network_protocol != 5
|| !rules.contains("return !upward || targetDiscovered;")
|| !rules.contains("elevatorRide && targetY < sourceY")
|| !teleportation.contains("discoveries.discover(player.getUUID(), level, target)")) {
throw new GradleException("I Like To Move It alpha.14 downward Waystone discovery contract is incomplete")
}
}
}
tasks.named("check") {
dependsOn tasks.named("grapplePhysicsSmoke")
dependsOn tasks.named("waystoneElevatorRulesSmoke")
dependsOn tasks.named("verifyWaystones")
dependsOn tasks.named("verifyGrappling")
dependsOn tasks.named("verifyGrapplePrediction")
dependsOn tasks.named("verifyWaystoneDescendingRelease")
dependsOn tasks.named("verifyPortals")
dependsOn tasks.named("verifySilverAircraft")
}
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.iliketomoveit.network;
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItBlocks;
import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneEntry;
import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneDiscoveryData;
import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneElevatorRules;
import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneSavedData;
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryProgressionService;
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryUnlock;
@@ -112,16 +113,17 @@ public final class WaystoneTeleportation {
WaystoneSyncService.syncNow(player);
return;
}
if (!discoveries.knows(player.getUUID(), level, source)
|| !discoveries.knows(player.getUUID(), level, target)) {
player.sendOverlayMessage(Component.translatable("message.iliketomoveit.waystone.undiscovered"));
return;
}
List<WaystoneEntry> entries = data.entriesFor(level);
boolean sourceElevator = isElevatorPos(source, entries);
boolean targetElevator = isElevatorPos(target, entries);
boolean elevatorRide = sourceElevator && targetElevator && isElevatorRide(source, target);
boolean sourceDiscovered = discoveries.knows(player.getUUID(), level, source);
boolean targetDiscovered = discoveries.knows(player.getUUID(), level, target);
if (!WaystoneElevatorRules.canTeleport(elevatorRide, source.getY(), target.getY(),
sourceDiscovered, targetDiscovered)) {
player.sendOverlayMessage(Component.translatable("message.iliketomoveit.waystone.undiscovered"));
return;
}
if ((sourceElevator || targetElevator) && !elevatorRide) {
player.sendOverlayMessage(Component.translatable("message.iliketomoveit.waystone.elevator_only"));
return;
@@ -148,6 +150,7 @@ public final class WaystoneTeleportation {
player.setDeltaMovement(Vec3.ZERO);
player.resetFallDistance();
spawnWarpEffect(level, feet);
if (discoveries.discover(player.getUUID(), level, target)) WaystoneSyncService.syncNow(player);
player.sendOverlayMessage(Component.translatable("message.iliketomoveit.waystone.teleported", cost));
}
@@ -190,7 +193,8 @@ public final class WaystoneTeleportation {
discoveries.discover(player.getUUID(), level, source);
Optional<BlockPos> target = data.entriesFor(level).stream()
.map(WaystoneEntry::pos)
.filter(pos -> discoveries.knows(player.getUUID(), level, pos))
.filter(pos -> WaystoneElevatorRules.canUseDirectDestination(upward,
discoveries.knows(player.getUUID(), level, pos)))
.filter(pos -> pos.getX() == source.getX() && pos.getZ() == source.getZ())
.filter(pos -> upward ? pos.getY() > source.getY() : pos.getY() < source.getY())
.filter(pos -> isWaystone(level, pos))
@@ -0,0 +1,18 @@
package fr.koka99cab.sanctuary26.iliketomoveit.waystone;
/** Directional discovery rules shared by direct controls and authoritative teleport validation. */
public final class WaystoneElevatorRules {
private WaystoneElevatorRules() {
}
/** Descending may discover the next floor; ascending still requires prior discovery. */
public static boolean canUseDirectDestination(boolean upward, boolean targetDiscovered) {
return !upward || targetDiscovered;
}
/** Only a real downward elevator ride may bypass discovery of its target. */
public static boolean canTeleport(boolean elevatorRide, int sourceY, int targetY,
boolean sourceDiscovered, boolean targetDiscovered) {
return sourceDiscovered && (targetDiscovered || elevatorRide && targetY < sourceY);
}
}
@@ -0,0 +1,33 @@
package fr.koka99cab.sanctuary26.iliketomoveit.waystone;
/** Locks the intentional discovery asymmetry between elevator descent and ascent. */
public final class WaystoneElevatorRulesSmoke {
private WaystoneElevatorRulesSmoke() {
}
public static void main(String[] args) {
assertAllowed(WaystoneElevatorRules.canUseDirectDestination(false, false),
"descent should select an undiscovered lower floor");
assertDenied(WaystoneElevatorRules.canUseDirectDestination(true, false),
"ascent should not select an undiscovered upper floor");
assertAllowed(WaystoneElevatorRules.canUseDirectDestination(true, true),
"ascent should keep selecting discovered upper floors");
assertAllowed(WaystoneElevatorRules.canTeleport(true, 80, 48, true, false),
"a real downward elevator ride should accept an undiscovered target");
assertDenied(WaystoneElevatorRules.canTeleport(true, 48, 80, true, false),
"an upward elevator ride should reject an undiscovered target");
assertDenied(WaystoneElevatorRules.canTeleport(false, 80, 48, true, false),
"ordinary travel should reject an undiscovered target");
assertDenied(WaystoneElevatorRules.canTeleport(true, 80, 48, false, false),
"an undiscovered source should never authorize travel");
}
private static void assertAllowed(boolean value, String message) {
if (!value) throw new IllegalStateException(message);
}
private static void assertDenied(boolean value, String message) {
if (value) throw new IllegalStateException(message);
}
}
+80 -5
View File
@@ -417,7 +417,7 @@ tasks.register("verifyItsAlive") {
def climateSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ClimateCropForaging.java").text
def pineappleSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/PineappleItem.java").text
def worldgenSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ItsAliveAgricultureWorldGeneration.java").text
if (project.version.toString() != "0.0.0-alpha.30"
if (project.version.toString() != "0.0.0-alpha.31"
|| climateMigration.target?.pack_version != "26.2.0-alpha.157"
|| climateMigration.target?.modules?.itsalive != "0.0.0-alpha.24"
|| !climateSource.contains("PlayerBlockBreakEvents.AFTER.register")
@@ -1041,6 +1041,30 @@ tasks.register("verifyItsAlive") {
throw new GradleException("Missing culinary substitution-family tag: itsalive:${path}")
}
}
def lecternBooks = new JsonSlurper().parse(file("src/main/resources/data/minecraft/tags/item/lectern_books.json"))
def lecternSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryLectern.java").text
if (!(lecternBooks.values instanceof List) || !lecternBooks.values.contains("itsalive:culinary_recipe_book")
|| !lecternSource.contains("UseBlockCallback.EVENT.register")
|| !lecternSource.contains("LecternBlock.HAS_BOOK")
|| !lecternSource.contains("CulinaryRecipeKnowledge.inscribe")
|| !lecternSource.contains("CulinaryRecipeKnowledge.openBook(serverPlayer, book)")
|| !lecternSource.contains("CulinaryRecipeKnowledge.forgetInscription")
|| !lecternSource.contains("PlayerBlockBreakEvents.BEFORE")
|| !lecternSource.contains("isSecondaryUseActive()")) {
throw new GradleException("The culinary recipe book must sit on vanilla lecterns and open its owner's catalogue")
}
def inscription = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryBookInscription.java").text
def components = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveDataComponents.java").text
def knowledge = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryRecipeKnowledge.java").text
def bookItem = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/item/CulinaryRecipeBookItem.java").text
if (!inscription.contains("ownerId")
|| !components.contains("CULINARY_BOOK")
|| !knowledge.contains("openLearned(player, inscription.ownerId())")
|| !knowledge.contains("stack.set(ItsAliveDataComponents.CULINARY_BOOK")
|| !knowledge.contains("forgetInscription")
|| bookItem.contains("openBook(serverPlayer, player.getItemInHand")) {
throw new GradleException("A placed culinary book must keep the placer's recipe discoveries only while on a lectern")
}
def creativeTab = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveCreativeTab.java").text
if (!creativeTab.contains('ItsAliveMod.id("agriculture")') || !creativeTab.contains("PLANTING_ITEMS") || !creativeTab.contains("PRODUCE_ITEMS")) {
throw new GradleException("The itsalive:agriculture creative tab must expose planting and produce items")
@@ -1066,7 +1090,8 @@ tasks.register("verifyItsAlive") {
] }
def culinaryKeys = (culinaryCatalogItems + ["vinegar", "failed_dish"]).collect { "item.itsalive." + it } + [
"tag.item.itsalive.culinary_mushrooms", "tag.item.itsalive.culinary_fish",
"tag.item.itsalive.culinary_berries", "tag.item.itsalive.culinary_cooked_meats"
"tag.item.itsalive.culinary_berries", "tag.item.itsalive.culinary_cooked_meats",
"tooltip.itsalive.culinary_recipe_book.blank"
]
(livingKeys + moobloomKeys + infectedKeys + culinaryKeys + ["hud.itsalive.hunter.struggle"]).each { key ->
if (!language.containsKey(key)) {
@@ -1149,7 +1174,7 @@ tasks.register("verifyCulinaryReliability") {
}
}
}
if (project.version.toString() != "0.0.0-alpha.30"
if (project.version.toString() != "0.0.0-alpha.31"
|| migration.target?.modules?.itsalive != "0.0.0-alpha.28"
|| migration.culinary_fixes?.food_press_contents_destroyed_on_break != true
|| migration.culinary_fixes?.comparator_contract?.blocks?.size() != 6
@@ -1187,7 +1212,7 @@ tasks.register("verifyCauldronBatching") {
def recipe = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java").text
def cauldron = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java").text
def timed = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java").text
if (project.version.toString() != "0.0.0-alpha.30"
if (project.version.toString() != "0.0.0-alpha.31"
|| migration.target?.modules?.itsalive != "0.0.0-alpha.29"
|| migration.cauldron_batches?.logical_stack_limit != 64
|| migration.cauldron_batches?.identity != "same_item_same_components"
@@ -1225,7 +1250,7 @@ tasks.register("verifyFermentationBarrelAutomation") {
def partialMatching = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java").text
def blockstate = new JsonSlurper().parse(
file("src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json"))
if (project.version.toString() != "0.0.0-alpha.30"
if (project.version.toString() != "0.0.0-alpha.31"
|| migration.source?.modules?.itsalive != "0.0.0-alpha.29"
|| migration.target?.modules?.itsalive != "0.0.0-alpha.30"
|| migration.fermentation_barrel?.maximum_units_per_input_slot != 1
@@ -1248,11 +1273,61 @@ tasks.register("verifyFermentationBarrelAutomation") {
}
}
tasks.register("verifyCulinaryLecternRelease") {
group = "verification"
description = "Checks alpha.31 shared culinary books on vanilla lecterns."
inputs.file(rootProject.file(
"pack/migrations/26.2.0-alpha.201-waystone-shop-culinary-to-alpha.202.json"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryLectern.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryRecipeKnowledge.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveDataComponents.java"))
inputs.file(file("src/main/resources/data/minecraft/tags/item/lectern_books.json"))
inputs.files(fileTree("src/main/resources/assets/itsalive/lang") { include "*.json" })
doLast {
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.201-waystone-shop-culinary-to-alpha.202.json"))
def lectern = file(
"src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryLectern.java").text
def knowledge = file(
"src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/knowledge/CulinaryRecipeKnowledge.java").text
def components = file(
"src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveDataComponents.java").text
def lecternBooks = new JsonSlurper().parse(file(
"src/main/resources/data/minecraft/tags/item/lectern_books.json"))
def locales = ["en_us", "fr_fr", "ru_ru"].collectEntries { locale ->
[(locale): new JsonSlurper().parse(file(
"src/main/resources/assets/itsalive/lang/${locale}.json"))]
}
if (project.version.toString() != "0.0.0-alpha.31"
|| migration.target?.modules?.itsalive != "0.0.0-alpha.31"
|| migration.culinary_lectern?.book_tag != "minecraft:lectern_books"
|| migration.culinary_lectern?.owner_catalog_shared_while_placed != true
|| migration.culinary_lectern?.inscription_removed_when_taken != true
|| migration.culinary_lectern?.inscription_removed_when_lectern_broken != true
|| migration.compatibility?.itsalive_data_version != 1
|| migration.compatibility?.itsalive_network_protocol != 3
|| !lectern.contains("UseBlockCallback.EVENT.register(CulinaryLectern::use)")
|| !lectern.contains("PlayerBlockBreakEvents.BEFORE.register")
|| !lectern.contains("CulinaryRecipeKnowledge.openBook(serverPlayer, book)")
|| !lectern.contains("CulinaryRecipeKnowledge.forgetInscription(taken)")
|| !knowledge.contains("stack.set(ItsAliveDataComponents.CULINARY_BOOK")
|| !knowledge.contains("stack.remove(ItsAliveDataComponents.CULINARY_BOOK)")
|| !components.contains('ItsAliveMod.id("culinary_book")')
|| !lecternBooks["values"]?.contains("itsalive:culinary_recipe_book")
|| locales.any { locale, values ->
!values.containsKey("tooltip.itsalive.culinary_recipe_book.blank")
}) {
throw new GradleException("It's Alive alpha.31 culinary lectern contract is incomplete")
}
}
}
tasks.named("check") {
dependsOn tasks.named("verifyItsAlive")
dependsOn tasks.named("verifyCulinaryReliability")
dependsOn tasks.named("verifyCauldronBatching")
dependsOn tasks.named("culinaryBatchingSmoke")
dependsOn tasks.named("verifyFermentationBarrelAutomation")
dependsOn tasks.named("verifyCulinaryLecternRelease")
dependsOn tasks.named("culinaryPartialMatchingSmoke")
}
@@ -1,14 +1,20 @@
package fr.koka99cab.sanctuary26.itsalive.culinary.item;
import fr.koka99cab.sanctuary26.itsalive.culinary.knowledge.CulinaryRecipeKnowledge;
import java.util.function.Consumer;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.component.TooltipDisplay;
import net.minecraft.world.level.Level;
/** Opens the persistent recipe catalogue without gating recipes the player can guess. */
/** Opens the holder's personal catalogue. Shared recipes are only readable on a lectern. */
public final class CulinaryRecipeBookItem extends Item {
public CulinaryRecipeBookItem(Properties properties) {
super(properties);
@@ -25,4 +31,16 @@ public final class CulinaryRecipeBookItem extends Item {
}
return InteractionResult.PASS;
}
@Override
public void appendHoverText(
ItemStack stack,
TooltipContext context,
TooltipDisplay display,
Consumer<Component> tooltip,
TooltipFlag flag
) {
tooltip.accept(Component.translatable("tooltip.itsalive.culinary_recipe_book.blank")
.withStyle(ChatFormatting.DARK_GRAY));
}
}
@@ -0,0 +1,47 @@
package fr.koka99cab.sanctuary26.itsalive.culinary.knowledge;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.UUID;
import net.minecraft.core.UUIDUtil;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
/** Binds a physical culinary book to the player who last placed it on a lectern. */
public record CulinaryBookInscription(UUID ownerId, String ownerName) {
private static final int MAX_NAME_LENGTH = 32;
public static final Codec<CulinaryBookInscription> CODEC = RecordCodecBuilder.create(instance ->
instance.group(
UUIDUtil.CODEC.fieldOf("owner_id").forGetter(CulinaryBookInscription::ownerId),
Codec.STRING.fieldOf("owner_name").forGetter(CulinaryBookInscription::ownerName)
).apply(instance, CulinaryBookInscription::new)
);
public static final StreamCodec<RegistryFriendlyByteBuf, CulinaryBookInscription> STREAM_CODEC =
StreamCodec.composite(
UUIDUtil.STREAM_CODEC,
CulinaryBookInscription::ownerId,
ByteBufCodecs.STRING_UTF8,
CulinaryBookInscription::ownerName,
CulinaryBookInscription::new
);
public CulinaryBookInscription {
if (ownerId == null) {
throw new IllegalArgumentException("Culinary book owner is required");
}
ownerName = sanitizeName(ownerName);
}
public static CulinaryBookInscription of(UUID ownerId, String ownerName) {
return new CulinaryBookInscription(ownerId, ownerName);
}
private static String sanitizeName(String ownerName) {
String cleaned = ownerName == null ? "" : ownerName.replaceAll("\\p{Cntrl}", "").trim();
if (cleaned.length() > MAX_NAME_LENGTH) {
cleaned = cleaned.substring(0, MAX_NAME_LENGTH);
}
return cleaned.isEmpty() ? "?" : cleaned;
}
}
@@ -0,0 +1,95 @@
package fr.koka99cab.sanctuary26.itsalive.culinary.knowledge;
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveFoodProcessing;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LecternBlock;
import net.minecraft.world.level.block.entity.LecternBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
/**
* Lets the culinary recipe book sit on a vanilla lectern.
* Only a book still on the lectern exposes the placer's catalogue.
*/
public final class CulinaryLectern {
private CulinaryLectern() {
}
public static void initialize() {
UseBlockCallback.EVENT.register(CulinaryLectern::use);
PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> {
if (blockEntity instanceof LecternBlockEntity lectern) {
CulinaryRecipeKnowledge.forgetInscription(lectern.getBook());
}
return true;
});
}
private static InteractionResult use(Player player, Level level, InteractionHand hand, BlockHitResult hit) {
if (hand != InteractionHand.MAIN_HAND || player.isSpectator()) {
return InteractionResult.PASS;
}
BlockPos pos = hit.getBlockPos();
BlockState state = level.getBlockState(pos);
if (!state.is(Blocks.LECTERN)) {
return InteractionResult.PASS;
}
ItemStack held = player.getItemInHand(hand);
if (!state.getValue(LecternBlock.HAS_BOOK)) {
if (held.is(ItsAliveFoodProcessing.CULINARY_RECIPE_BOOK)
&& !level.isClientSide()
&& player instanceof ServerPlayer serverPlayer) {
CulinaryRecipeKnowledge.inscribe(held, serverPlayer);
}
return InteractionResult.PASS;
}
if (!(level.getBlockEntity(pos) instanceof LecternBlockEntity lectern)) {
return InteractionResult.PASS;
}
ItemStack book = lectern.getBook();
if (!book.is(ItsAliveFoodProcessing.CULINARY_RECIPE_BOOK)) {
return InteractionResult.PASS;
}
if (level.isClientSide()) {
return InteractionResult.SUCCESS;
}
if (!(player instanceof ServerPlayer serverPlayer) || !level.mayInteract(serverPlayer, pos)) {
return InteractionResult.FAIL;
}
if (serverPlayer.isSecondaryUseActive()) {
return takeBook(serverPlayer, level, pos, state, lectern);
}
CulinaryRecipeKnowledge.openBook(serverPlayer, book);
return InteractionResult.SUCCESS;
}
private static InteractionResult takeBook(
ServerPlayer player,
Level level,
BlockPos pos,
BlockState state,
LecternBlockEntity lectern
) {
ItemStack taken = lectern.getBook().copy();
if (taken.isEmpty()) {
return InteractionResult.PASS;
}
CulinaryRecipeKnowledge.forgetInscription(taken);
lectern.setBook(ItemStack.EMPTY);
LecternBlock.resetBookState(player, level, pos, state, false);
player.getInventory().placeItemBackInInventory(taken);
level.playSound(null, pos, SoundEvents.ITEM_FRAME_REMOVE_ITEM, SoundSource.BLOCKS, 1.0F, 1.0F);
return InteractionResult.SUCCESS;
}
}
@@ -4,12 +4,14 @@ import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
import fr.koka99cab.sanctuary26.itsalive.culinary.recipe.CulinaryProcessRecipe;
import fr.koka99cab.sanctuary26.itsalive.network.CulinaryRecipeBookPayload;
import fr.koka99cab.sanctuary26.itsalive.recipe.RedstoneMillingRecipe;
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveDataComponents;
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveFoodProcessing;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.core.Holder;
@@ -78,6 +80,7 @@ public final class CulinaryRecipeKnowledge {
return;
}
initialized = true;
CulinaryLectern.initialize();
ServerLivingEntityEvents.AFTER_DEATH.register(CulinaryRecipeKnowledge::afterDeath);
ItsAliveMod.LOGGER.info(
"[{}] Culinary discoveries enabled: books 1/{} hostile, pages 1/{} hostile.",
@@ -107,9 +110,39 @@ public final class CulinaryRecipeKnowledge {
return true;
}
public static void inscribe(ItemStack stack, ServerPlayer player) {
if (!stack.is(ItsAliveFoodProcessing.CULINARY_RECIPE_BOOK)) {
return;
}
stack.set(ItsAliveDataComponents.CULINARY_BOOK, CulinaryBookInscription.of(
player.getUUID(),
player.getGameProfile().name()
));
}
public static void forgetInscription(ItemStack stack) {
if (!stack.is(ItsAliveFoodProcessing.CULINARY_RECIPE_BOOK)) {
return;
}
stack.remove(ItsAliveDataComponents.CULINARY_BOOK);
}
public static void openBook(ServerPlayer player) {
openLearned(player, player.getUUID());
}
public static void openBook(ServerPlayer player, ItemStack book) {
CulinaryBookInscription inscription = book.get(ItsAliveDataComponents.CULINARY_BOOK);
if (inscription == null) {
openLearned(player, player.getUUID());
return;
}
openLearned(player, inscription.ownerId());
}
private static void openLearned(ServerPlayer player, UUID ownerId) {
List<CatalogRecipe> recipes = catalog(player.level().getServer());
Set<String> learned = CulinaryRecipeKnowledgeData.get(player.level().getServer()).learned(player.getUUID());
Set<String> learned = CulinaryRecipeKnowledgeData.get(player.level().getServer()).learned(ownerId);
List<CulinaryRecipeBookPayload.Entry> entries = new ArrayList<>();
for (int index = 0; index < recipes.size(); index++) {
CatalogRecipe recipe = recipes.get(index);
@@ -0,0 +1,26 @@
package fr.koka99cab.sanctuary26.itsalive.registry;
import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
import fr.koka99cab.sanctuary26.itsalive.culinary.knowledge.CulinaryBookInscription;
import net.minecraft.core.Registry;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.BuiltInRegistries;
/** Persistent item components owned by It's Alive. */
public final class ItsAliveDataComponents {
public static final DataComponentType<CulinaryBookInscription> CULINARY_BOOK = Registry.register(
BuiltInRegistries.DATA_COMPONENT_TYPE,
ItsAliveMod.id("culinary_book"),
DataComponentType.<CulinaryBookInscription>builder()
.persistent(CulinaryBookInscription.CODEC)
.networkSynchronized(CulinaryBookInscription.STREAM_CODEC)
.cacheEncoding()
.build()
);
private ItsAliveDataComponents() {
}
public static void initialize() {
}
}
@@ -21,6 +21,7 @@ public final class ItsAliveRegistries {
return;
}
initialized = true;
ItsAliveDataComponents.initialize();
ItsAliveEntityTypes.register();
ItsAliveBlocks.register();
ItsAliveItems.register();
@@ -270,6 +270,7 @@
"item.itsalive.burger": "Burger",
"item.itsalive.culinary_recipe_book": "Culinary Recipe Book",
"item.itsalive.recipe_page": "Recipe Page",
"tooltip.itsalive.culinary_recipe_book.blank": "Place it on a lectern to share your recipes.",
"message.itsalive.recipe_page.learned": "Recipe discovered: %s (page %s/%s)",
"message.itsalive.recipe_page.complete": "You already know every culinary recipe.",
"book.itsalive.appliance.mill": "Redstone mill",
@@ -270,6 +270,7 @@
"item.itsalive.burger": "Burger",
"item.itsalive.culinary_recipe_book": "Livre de recettes culinaires",
"item.itsalive.recipe_page": "Page de recette",
"tooltip.itsalive.culinary_recipe_book.blank": "Pose-le sur un pupitre pour partager tes recettes.",
"message.itsalive.recipe_page.learned": "Recette découverte : %s (page %s/%s)",
"message.itsalive.recipe_page.complete": "Tu connais déjà toutes les recettes culinaires.",
"book.itsalive.appliance.mill": "Moulin redstone",
@@ -270,6 +270,7 @@
"item.itsalive.burger": "Burger",
"item.itsalive.culinary_recipe_book": "Culinary Recipe Book",
"item.itsalive.recipe_page": "Recipe Page",
"tooltip.itsalive.culinary_recipe_book.blank": "Положи на кафедру, чтобы поделиться рецептами.",
"message.itsalive.recipe_page.learned": "Recipe discovered: %s (page %s/%s)",
"message.itsalive.recipe_page.complete": "You already know every culinary recipe.",
"book.itsalive.appliance.mill": "Redstone mill",
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"itsalive:culinary_recipe_book"
]
}
@@ -0,0 +1,56 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.201",
"modules": {
"itsalive": "0.0.0-alpha.30",
"iliketomoveit": "0.0.0-alpha.13",
"sanctuary": "0.0.0-alpha.116"
}
},
"target": {
"pack_version": "26.2.0-alpha.202",
"modules": {
"itsalive": "0.0.0-alpha.31",
"iliketomoveit": "0.0.0-alpha.14",
"sanctuary": "0.0.0-alpha.117"
}
},
"waystone_elevator": {
"undiscovered_downward_destination_allowed": true,
"destination_discovered_after_arrival": true,
"undiscovered_upward_destination_allowed": false,
"undiscovered_ordinary_destination_allowed": false,
"source_discovery_required": true
},
"shop_currency_inventory": {
"all_unlocked_rows_counted": true,
"overflow_rows_counted": true,
"offhand_counted": true,
"duplicate_stack_views_deduplicated": true,
"server_authoritative_removal": true
},
"culinary_lectern": {
"vanilla_lectern_supported": true,
"book_tag": "minecraft:lectern_books",
"owner_catalog_shared_while_placed": true,
"inscription_removed_when_taken": true,
"inscription_removed_when_lectern_broken": true,
"inventory_book_remains_personal": true,
"owner_name_max_length": 32
},
"distribution": {
"prism_instance": "Sanctuary-26.2-dev",
"packwiz_index_refreshed": true
},
"compatibility": {
"itsalive_data_version": 1,
"itsalive_network_protocol": 3,
"iliketomoveit_data_version": 3,
"iliketomoveit_network_protocol": 5,
"sanctuary_data_version": 11,
"sanctuary_network_protocol": 21,
"save_migration_required": false,
"registry_ids_changed": false
}
}
@@ -0,0 +1,77 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.201",
"modules": {
"anotherworld": "0.0.0-alpha.49",
"sanctuary": "0.0.0-alpha.116"
}
},
"target": {
"pack_version": "26.2.0-alpha.202",
"modules": {
"anotherworld": "0.0.0-alpha.50",
"sanctuary": "0.0.0-alpha.117"
}
},
"safe_vault": {
"owner": "anotherworld",
"shop_currency_owner": "sanctuary",
"footprint": "3x3_horizontal",
"blocks_required": 9,
"single_owner_required": true,
"maximum_active_vaults_per_player": 1,
"storage_slots": 27,
"accepted_items": [
"anotherworld:ruby",
"anotherworld:sapphire",
"minecraft:emerald"
],
"shop_access_when_structure_unloaded": true,
"storage_provider_api": "SanctuaryCurrencyStorage",
"save_key": "anotherworld_safe_vaults",
"save_data_version": 1
},
"raid": {
"tool": "anotherworld:titanium_drill",
"continuous_use_ticks": 1200,
"owner_must_be_online": true,
"loot_percent": 20,
"drill_consumed_on_success": true,
"protection_ticks_after_success": 1728000,
"server_authoritative": true
},
"recipes": {
"safe_block": {
"pattern": ["GGG", "GCG", "GGG"],
"ingredients": {
"G": "minecraft:gold_block",
"C": "minecraft:chest"
}
},
"titanium_drill": {
"pattern": ["TTT", "RPR", "FDF"],
"ingredients": {
"T": "anotherworld:titanium_ingot",
"R": "minecraft:redstone",
"P": "minecraft:piston",
"F": "minecraft:iron_block",
"D": "minecraft:diamond_pickaxe"
}
}
},
"compatibility": {
"anotherworld_source_data_version": 1,
"anotherworld_target_data_version": 2,
"anotherworld_network_protocol": 1,
"sanctuary_data_version": 11,
"sanctuary_network_protocol": 21,
"new_registry_ids": [
"anotherworld:safe_block",
"anotherworld:safe_vault",
"anotherworld:titanium_drill"
],
"existing_world_reset_required": false,
"pack_release_updated": true
}
}
@@ -0,0 +1,55 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.202",
"modules": {
"anotherworld": "0.0.0-alpha.50"
}
},
"target": {
"pack_version": "26.2.0-alpha.203",
"modules": {
"anotherworld": "0.0.0-alpha.51"
}
},
"safe_vault_structure": {
"source_footprint": "3x3_horizontal",
"target_footprint": "3x3_upright",
"blocks_required": 9,
"unformed_blocks_use_individual_cube_model": true,
"connected_48x48_texture_requires_complete_structure": true,
"connected_texture_parts": 9
},
"safe_vault_interface": {
"currency_slots": 3,
"currencies": [
"anotherworld:ruby",
"anotherworld:sapphire",
"minecraft:emerald"
],
"controls_per_currency": ["withdraw_64", "withdraw_1", "deposit_1", "deposit_64"],
"server_authoritative": true,
"persisted_storage_slots_preserved": 27
},
"distribution": {
"packwiz_index_refreshed": true,
"jei": {
"source": "30.26.0.186",
"target": "30.27.0.187",
"modrinth_version_id": "XXTFqqwG",
"prism_manifest_updated": true
}
},
"compatibility": {
"anotherworld_source_data_version": 2,
"anotherworld_target_data_version": 3,
"vault_source_data_version": 1,
"vault_target_data_version": 2,
"anotherworld_network_protocol": 1,
"saved_currency_balances_preserved": true,
"legacy_horizontal_structure_requires_rebuild": true,
"new_registry_ids": ["menu:anotherworld:safe_vault"],
"removed_registry_ids": [],
"pack_release_updated": true
}
}
@@ -0,0 +1,37 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.203",
"modules": {
"anotherworld": "0.0.0-alpha.51"
}
},
"target": {
"pack_version": "26.2.0-alpha.203",
"modules": {
"anotherworld": "0.0.0-alpha.52"
}
},
"safe_vault_structure_fix": {
"unformed_block_texture": "anotherworld:block/safe_block",
"horizontal_3x1_rejected": true,
"vertical_3x1_rejected": true,
"complete_3x3_mask_required": true,
"connected_texture_parts": 9
},
"safe_vault_interface_fix": {
"currency_slots": 3,
"emerald_row_visible": true,
"extended_inventory_starts_below_currency_controls": true,
"native_inventory_label_suppressed": true
},
"compatibility": {
"anotherworld_data_version": 3,
"vault_data_version": 2,
"anotherworld_network_protocol": 1,
"save_migration_required": false,
"registry_ids_changed": false,
"saved_currency_balances_preserved": true,
"pack_release_updated": false
}
}
+15 -15
View File
@@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
[[files]]
file = "mods/ambiance-0.0.0-alpha.12.jar"
hash = "6d131ff56488ecc6352a86c87e29bef6a198ad672983838ad71941661d6567af"
hash = "40de39da187fa42f3b947f83872286ef6f049ff8bac0ac1a66d46d18ee14f911"
[[files]]
file = "mods/anotherworld-0.0.0-alpha.49.jar"
hash = "b245c849985c208ca547da3dc41121ce6dd2bc1ffbbb1bfd8f6a7326356cfbdc"
file = "mods/anotherworld-0.0.0-alpha.51.jar"
hash = "caa4d8b29af65dd0a43e9aa2a7b897255ae10869595f1ca706f579b15d37c5a2"
[[files]]
file = "mods/events-0.0.0-alpha.0.jar"
hash = "d8dac51f88f72bfb1f7cd92279447897ee349e4cbc78b070cb33e99d5acbb965"
hash = "09047070e419df4bbd510c114e6c23c609757e03ef1142ba4d38f6b99ecb0a7c"
[[files]]
file = "mods/fabric-api.pw.toml"
@@ -30,8 +30,8 @@ hash = "df032a6b051d0262cf24945c43e0e6b915e8cc1d62af62fe933932e7f35960c4"
metafile = true
[[files]]
file = "mods/iliketomoveit-0.0.0-alpha.13.jar"
hash = "cf3e90860f475f6a7466eb29686d230c95d5629a36fac89c60077fbae4ede865"
file = "mods/iliketomoveit-0.0.0-alpha.14.jar"
hash = "e8027e82ebcb214c94e028d7c2780057d745dcb810ca785ee35601bbbc1c3ec0"
[[files]]
file = "mods/iris.pw.toml"
@@ -39,33 +39,33 @@ hash = "84a3cef87679c0d21dceb4641167dc0368b9a3593698f6a3632485e9b3a8d5da"
metafile = true
[[files]]
file = "mods/itsalive-0.0.0-alpha.30.jar"
hash = "c311281a771bb8fec63be8e8a0d792be919737e4dfcf3da25ef08c6878662d55"
file = "mods/itsalive-0.0.0-alpha.31.jar"
hash = "04fcdc3312c0bd4e4271602c2e9eb8eb419de10c7b61b6ab47182033de736452"
[[files]]
file = "mods/jei.pw.toml"
hash = "0461b1b0c56d4f9bd9af2e8c53dcf121ad4ff29f740d6fbe7363649fcc6d8aa5"
hash = "7196839cc369a00ea9cffd0a85594f8fd46c0978f4f2fc708e61368a8b451270"
metafile = true
[[files]]
file = "mods/masterkey-0.0.0-alpha.8.jar"
hash = "4fac6589b42df15599915849cbf624d6cb8fbd41f47623357d5d6fa27e6f992f"
hash = "1a14be8288a9eb2b93922e84cf14d701fb86bd67d6227adb16a534961dae5f87"
[[files]]
file = "mods/onlyfun-0.0.0-alpha.19.jar"
hash = "cf8f3afb8503ed9689fcce6153ad72c5621f4bb2441af867326d536d347756ed"
hash = "c5b8bf67efd0a001c142172110f307ec2a59ac3d7457ed8e4f53f8e683033c1d"
[[files]]
file = "mods/ouch-0.0.0-alpha.9.jar"
hash = "fc0f678eb72354c6594c90c59af78bcc7420190d91e922d1cd74a45f5eaa948e"
hash = "781f89dd44e3277dd8fd51e60ff2da9677a6376faeafd4000e1c07cf6f818759"
[[files]]
file = "mods/redstoner-0.0.0-alpha.10.jar"
hash = "6325d6edf66b1b0f102f53152239982c4c0c7011a59848e8d6e569f360b7ff0f"
hash = "2d6ea237771f7a75a6d0184a68e910a7513159c0259e3c2b06f0a411a0979cbe"
[[files]]
file = "mods/sanctuary-0.0.0-alpha.116.jar"
hash = "c1f856995a259f6471cf0780e1b02f5f73e487f21d39b0519b5a02299c480516"
file = "mods/sanctuary-0.0.0-alpha.117.jar"
hash = "10a7403a4b10ef049a7e557fd00eed56178f6d5383a2b7f28ff60b951eb4e053"
[[files]]
file = "mods/simple-voice-chat.pw.toml"
+4 -4
View File
@@ -1,14 +1,14 @@
name = "Just Enough Items (JEI)"
filename = "jei-26.2-fabric-30.26.0.186.jar"
filename = "jei-26.2-fabric-30.27.0.187.jar"
side = "both"
pin = true
[download]
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/ZLvOB7Pk/jei-26.2-fabric-30.26.0.186.jar"
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/XXTFqqwG/jei-26.2-fabric-30.27.0.187.jar"
hash-format = "sha512"
hash = "6d8aaad5066d607e460db6f83cba28807129104b46c580b5ba3372d7b7cef84e28ade393c0a3d6bbb38191bcac053af691d0f58d8075f211c687c7b56c74337c"
hash = "f8bf88c089c7fe3d56031d7629170ba01eb1d0c5cc4d33f2fd0ac9413369cdef84a0fde999528621426d3dca878c9cfaf7a790622e2cd1d54802e1086fb073f0"
[update]
[update.modrinth]
mod-id = "u6dRKJwZ"
version = "ZLvOB7Pk"
version = "XXTFqqwG"
+2 -2
View File
@@ -1,12 +1,12 @@
name = "Sanctuary 26.2"
author = "KOKA99CAB"
version = "26.2.0-alpha.201"
version = "26.2.0-alpha.203"
pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "52d4011015ea5b087f0cd0c06fb2f762e83e2cd6d227441984d5d204fb6aedd1"
hash = "0cb034db4939949917ee7d06d1eb205268b1453c2fb78a3fe57d64d090b54a87"
[versions]
fabric = "0.19.3"
+6 -6
View File
@@ -3,7 +3,7 @@
"game": "minecraft",
"name": "Sanctuary 26.2",
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
"versionId": "26.2.0-alpha.201",
"versionId": "26.2.0-alpha.203",
"files": [
{
"path": "mods/fabric-api-0.158.0+26.2.jar",
@@ -21,19 +21,19 @@
"fileSize": 2535765
},
{
"path": "mods/jei-26.2-fabric-30.26.0.186.jar",
"path": "mods/jei-26.2-fabric-30.27.0.187.jar",
"hashes": {
"sha1": "950efcf2d63a3a0560eae81223479577ad1a78cd",
"sha512": "6d8aaad5066d607e460db6f83cba28807129104b46c580b5ba3372d7b7cef84e28ade393c0a3d6bbb38191bcac053af691d0f58d8075f211c687c7b56c74337c"
"sha1": "cbd62af1f8e568cbe29c7892ae1b2917c0784a44",
"sha512": "f8bf88c089c7fe3d56031d7629170ba01eb1d0c5cc4d33f2fd0ac9413369cdef84a0fde999528621426d3dca878c9cfaf7a790622e2cd1d54802e1086fb073f0"
},
"env": {
"client": "required",
"server": "required"
},
"downloads": [
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/ZLvOB7Pk/jei-26.2-fabric-30.26.0.186.jar"
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/XXTFqqwG/jei-26.2-fabric-30.27.0.187.jar"
],
"fileSize": 1818022
"fileSize": 1818136
},
{
"path": "mods/voicechat-fabric-2.6.22+26.2.jar",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"pack_version": "26.2.0-alpha.201",
"pack_version": "26.2.0-alpha.203",
"release_channel": "alpha",
"minecraft_version": "26.2",
"fabric_loader_version": "0.19.3",
@@ -9,7 +9,7 @@
"project_id": "FgOqyAXd",
"project_slug": "sanctuary-koka99",
"project_url": "https://modrinth.com/modpack/sanctuary-koka99",
"primary_file": "sanctuary-26.2.0-alpha.201.mrpack",
"primary_file": "sanctuary-26.2.0-alpha.203.mrpack",
"shared_client_server_pack": true,
"update_owner": "packwiz-installer",
"check_on_launch": true,
+54 -8
View File
@@ -74,6 +74,7 @@ tasks.register("verifyBlackMarket") {
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardGenerator.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardTier.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java"))
doLast {
def data = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketData.java").text
def service = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java").text
@@ -81,6 +82,7 @@ tasks.register("verifyBlackMarket") {
def shop = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text
def questData = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardData.java").text
def quests = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java").text
def inventoryView = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java").text
if (!data.contains('SAVE_KEY = "black_market"') || !data.contains("DATA_VERSION = 2")
|| !data.contains("ItemStack.CODEC.fieldOf(\"item\")") || !data.contains("replaceListing")
|| !service.contains("LISTING_FEE_RUBIES = 1") || !service.contains("COMMISSION_PERCENT = 5")
@@ -88,6 +90,11 @@ tasks.register("verifyBlackMarket") {
|| !service.contains("loadedDeliveryBox") || !service.contains("PARCEL_RETENTION_MILLIS")
|| !policy.contains("DataComponents.CONTAINER") || !policy.contains("DataComponents.BUNDLE_CONTENTS")
|| !shop.contains("public static final boolean OPEN = true")
|| !shop.contains("BlackMarketService.countPlayerItem")
|| !shop.contains("BlackMarketService.removePlayerItem")
|| shop.contains("getContainerSize()")
|| !inventoryView.contains("getNonEquipmentItems")
|| !service.contains("currencyStacks")
|| !questData.contains('SAVE_KEY = "black_market_quests"')
|| !quests.contains("ServerLivingEntityEvents.AFTER_DEATH")
|| !quests.contains("QuestBoardTier.ACTIVATION_COST")
@@ -2741,6 +2748,44 @@ tasks.register("verifyCompanionPowerTooltips") {
}
}
tasks.register("verifyShopCurrencyRowsRelease") {
group = "verification"
description = "Checks alpha.117 Shop currency across every unlocked inventory row."
inputs.file(rootProject.file(
"pack/migrations/26.2.0-alpha.201-waystone-shop-culinary-to-alpha.202.json"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java"))
doLast {
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.201-waystone-shop-culinary-to-alpha.202.json"))
def inventory = file(
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java").text
def market = file(
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java").text
def shop = file(
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text
if (project.version.toString() != "0.0.0-alpha.117"
|| 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
|| migration.shop_currency_inventory?.duplicate_stack_views_deduplicated != true
|| migration.shop_currency_inventory?.server_authoritative_removal != true
|| migration.compatibility?.sanctuary_data_version != 11
|| migration.compatibility?.sanctuary_network_protocol != 21
|| !inventory.contains("public static List<SlotReference> unlockedRows(Player player)")
|| !inventory.contains("SanctuaryOverflowAccess.of(player)")
|| !market.contains("IdentityHashMap<ItemStack, Boolean> seen")
|| !market.contains("player.inventoryMenu.slots")
|| !market.contains("SanctuaryPlayerInventoryView.unlockedRows(player)")
|| !market.contains("SanctuaryPlayerInventoryView.offhand(player)")
|| !shop.contains("BlackMarketService.countPlayerItem(player, item)")
|| !shop.contains("BlackMarketService.removePlayerItem(player, item, count)")) {
throw new GradleException("Sanctuary alpha.117 unlocked-row Shop currency contract is incomplete")
}
}
}
tasks.named("verifySanctuary") {
dependsOn tasks.named("verifyCompanionPowerTooltips")
}
@@ -2886,7 +2931,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.116"
if (project.version.toString() != "0.0.0-alpha.117"
|| 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
@@ -2924,7 +2969,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.116"
if (project.version.toString() != "0.0.0-alpha.117"
|| 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]
@@ -2955,7 +3000,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.116"
if (project.version.toString() != "0.0.0-alpha.117"
|| migration.target?.pack_version != "26.2.0-alpha.195"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|| migration.compatibility?.sanctuary_data_version != 11
@@ -2986,7 +3031,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.116"
if (project.version.toString() != "0.0.0-alpha.117"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|| migration.mailbox?.id != "sanctuary:delivery_box"
|| migration.mailbox?.horizontal_facing != true
@@ -3014,7 +3059,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.116"
if (project.version.toString() != "0.0.0-alpha.117"
|| !transfer.contains("shouldAppendOverflow")
|| !transfer.contains("isVanillaStorageRange")
|| !transfer.contains("hotbarStart")
@@ -3060,7 +3105,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.116"
if (project.version.toString() != "0.0.0-alpha.117"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|| migration.quest_requests?.save_key != "quest_requests"
|| migration.quest_requests?.data_version != 1
@@ -3118,7 +3163,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.116"
if (project.version.toString() != "0.0.0-alpha.117"
|| 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"
@@ -3187,7 +3232,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.116"
if (project.version.toString() != "0.0.0-alpha.117"
|| 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"
@@ -3238,6 +3283,7 @@ tasks.named("check") {
dependsOn tasks.named("verifyQuestRequests")
dependsOn tasks.named("verifyQuestRequestContracts")
dependsOn tasks.named("verifyQuestRequestEscrow")
dependsOn tasks.named("verifyShopCurrencyRowsRelease")
dependsOn tasks.named("celestialGraphSmoke")
dependsOn tasks.named("questRequestModelSmoke")
}
@@ -0,0 +1,61 @@
package fr.koka99cab.sanctuary26.sanctuary.api.inventory;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.Item;
/**
* Public extension point for player-owned currency stores outside the Sanctuary inventory.
*
* <p>Providers keep ownership of their data and must validate every mutation on the server.
* Sanctuary only aggregates their balances for Shop and Black Market transactions.</p>
*/
public final class SanctuaryCurrencyStorage {
private static final Map<Identifier, Provider> PROVIDERS = new LinkedHashMap<>();
private SanctuaryCurrencyStorage() {
}
public static synchronized void register(Identifier id, Provider provider) {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(provider, "provider");
if (PROVIDERS.putIfAbsent(id, provider) != null) {
throw new IllegalStateException("Currency storage provider already registered: " + id);
}
}
public static int countAdditional(ServerPlayer player, Item item) {
if (player == null || item == null) return 0;
long count = 0L;
for (Provider provider : snapshot()) {
count += Math.max(0, provider.count(player, item));
if (count >= Integer.MAX_VALUE) return Integer.MAX_VALUE;
}
return (int) count;
}
public static int removeAdditional(ServerPlayer player, Item item, int amount) {
if (player == null || item == null || amount <= 0) return 0;
int remaining = amount;
for (Provider provider : snapshot()) {
if (remaining <= 0) break;
int removed = Math.clamp(provider.remove(player, item, remaining), 0, remaining);
remaining -= removed;
}
return amount - remaining;
}
private static synchronized java.util.List<Provider> snapshot() {
return java.util.List.copyOf(PROVIDERS.values());
}
public interface Provider {
int count(ServerPlayer player, Item item);
/** Returns the exact number removed, between zero and {@code amount}. */
int remove(ServerPlayer player, Item item, int amount);
}
}
@@ -33,8 +33,10 @@ public final class SanctuaryPlayerInventoryView {
List<SlotReference> slots = new ArrayList<>(rows * SanctuaryInventoryCapacity.ROW_SIZE);
Inventory inventory = player.getInventory();
int vanillaSlots = Math.min(rows, SanctuaryInventoryCapacity.VANILLA_ROW_COUNT)
* SanctuaryInventoryCapacity.ROW_SIZE;
int vanillaSlots = Math.min(
Math.min(rows, SanctuaryInventoryCapacity.VANILLA_ROW_COUNT)
* SanctuaryInventoryCapacity.ROW_SIZE,
inventory.getNonEquipmentItems().size());
for (int slot = 0; slot < vanillaSlots; slot++) {
slots.add(new SlotReference(inventory, slot, slot < SanctuaryInventoryCapacity.ROW_SIZE));
}
@@ -65,11 +67,16 @@ public final class SanctuaryPlayerInventoryView {
public record SlotReference(Container container, int slot, boolean hotbar) {
public ItemStack stack() {
if (container instanceof Inventory inventory && slot >= 0 && slot < Inventory.INVENTORY_SIZE) {
List<ItemStack> items = inventory.getNonEquipmentItems();
if (slot < items.size()) return items.get(slot);
}
return container.getItem(slot);
}
public ItemStack remove(int count) {
ItemStack removed = container.removeItem(slot, count);
ItemStack stored = stack();
ItemStack removed = stored.split(count);
container.setChanged();
return removed;
}
@@ -2,13 +2,19 @@ package fr.koka99cab.sanctuary26.sanctuary.shop;
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryPlayerInventoryView;
import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryCurrencyStorage;
import fr.koka99cab.sanctuary26.sanctuary.network.BlackMarketCreatePayload;
import fr.koka99cab.sanctuary26.sanctuary.network.BlackMarketEditorPayload;
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryOverflowAccess;
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryOverflowContainer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
@@ -29,8 +35,11 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.permissions.Permissions;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.Container;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.SignBlockEntity;
@@ -345,21 +354,59 @@ public final class BlackMarketService {
}
private static int countCurrency(ServerPlayer player, Item item) { return countPlayerItem(player, item); }
static int countPlayerItem(ServerPlayer player, Item item) {
long count = 0L;
for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player))
if (slot.stack().is(item)) count += slot.stack().getCount();
if (item == null) return 0;
long count = SanctuaryCurrencyStorage.countAdditional(player, item);
for (ItemStack stack : currencyStacks(player)) {
if (stack.is(item)) count += stack.getCount();
}
return (int) Math.min(Integer.MAX_VALUE, count);
}
private static void removeCurrency(ServerPlayer player, Item item, int amount) { removePlayerItem(player, item, amount); }
static void removePlayerItem(ServerPlayer player, Item item, int amount) {
int remaining = amount;
for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) {
for (ItemStack stack : currencyStacks(player)) {
if (remaining <= 0) break;
ItemStack stack = slot.stack();
if (!stack.is(item)) continue;
int removed = Math.min(remaining, stack.getCount());
stack.shrink(removed); remaining -= removed; slot.markChanged();
stack.shrink(removed);
remaining -= removed;
}
player.getInventory().setChanged();
SanctuaryOverflowAccess.of(player).setChanged();
if (remaining > 0) SanctuaryCurrencyStorage.removeAdditional(player, item, remaining);
}
/**
* Rank 4 (5 rows) puts the second-to-last visual bank on vanilla slots 27-35;
* rank 5 (6 rows) puts it on overflow. Count both the 26.2 main stacks and the
* inventory menu so those banks match what the player sees.
*/
private static List<ItemStack> currencyStacks(ServerPlayer player) {
IdentityHashMap<ItemStack, Boolean> seen = new IdentityHashMap<>();
List<ItemStack> stacks = new ArrayList<>();
for (Slot slot : player.inventoryMenu.slots) {
if (!countsAsPlayerStorage(slot)) continue;
remember(stacks, seen, slot.getItem());
}
for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) {
remember(stacks, seen, slot.stack());
}
remember(stacks, seen, SanctuaryPlayerInventoryView.offhand(player).stack());
return stacks;
}
private static boolean countsAsPlayerStorage(Slot slot) {
Container container = slot.container;
if (container instanceof SanctuaryOverflowContainer) return slot.isActive();
if (!(container instanceof Inventory)) return false;
int index = slot.getContainerSlot();
return index >= 0 && index < Inventory.INVENTORY_SIZE;
}
private static void remember(List<ItemStack> stacks, IdentityHashMap<ItemStack, Boolean> seen,
ItemStack stack) {
if (stack == null || stack.isEmpty() || seen.put(stack, Boolean.TRUE) != null) return;
stacks.add(stack);
}
private static void writeSign(ServerLevel level, SignBlockEntity sign, BlackMarketData.Listing listing) {
SignText text = new SignText()
@@ -237,24 +237,13 @@ public final class ShopService {
}
private static Item currency(Identifier id) { return BuiltInRegistries.ITEM.getOptional(id).orElse(null); }
private static int countCurrency(ServerPlayer player, Item item) {
if (item == null) return 0;
int total = 0;
for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) {
ItemStack stack = player.getInventory().getItem(slot); if (stack.is(item)) total += stack.getCount();
}
return total;
return item == null ? 0 : BlackMarketService.countPlayerItem(player, item);
}
private static boolean hasCurrency(ServerPlayer player, Item item, int count) {
return countCurrency(player, item) >= count;
}
private static void removeCurrency(ServerPlayer player, Item item, int count) {
int remaining = count;
for (int slot = 0; slot < player.getInventory().getContainerSize() && remaining > 0; slot++) {
ItemStack stack = player.getInventory().getItem(slot);
if (!stack.is(item)) continue;
int removed = Math.min(remaining, stack.getCount()); stack.shrink(removed); remaining -= removed;
}
player.getInventory().setChanged();
BlackMarketService.removePlayerItem(player, item, count);
}
private static void celebrate(ServerLevel level, BlockPos pos, java.util.UUID owner, ItemStack parcel) {
level.playSound(null, pos, SoundEvents.PLAYER_LEVELUP, SoundSource.BLOCKS, 0.65F, 1.35F);
@@ -1,5 +1,7 @@
package fr.koka99cab.sanctuary26.sanctuary.shop;
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventoryCapacity;
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryStat;
import net.minecraft.util.RandomSource;
public final class ShopDeliveryTimingSmoke {
@@ -35,5 +37,13 @@ public final class ShopDeliveryTimingSmoke {
|| ShopProgression.slotCost(10) != -1) {
throw new AssertionError("Shop capacity bounds are invalid");
}
if (SanctuaryInventoryCapacity.VANILLA_ROW_COUNT != 4
|| SanctuaryInventoryCapacity.MAX_ROW_COUNT != 6) {
throw new AssertionError("Shop currency counting must include overflow rows 5-6");
}
if (SanctuaryStat.INVENTORY.valueAt(4) != 5.0
|| SanctuaryStat.INVENTORY.valueAt(5) != 6.0) {
throw new AssertionError("Inventory ranks 4/5 and 5/5 no longer map to 5 and 6 rows");
}
}
}