From 1b1f3a791da19706928c3c047cc0fccdb922040e Mon Sep 17 00:00:00 2001 From: koka Date: Sun, 30 Aug 2026 03:13:48 +0200 Subject: [PATCH] feat(redstoner): add storage terminal --- CHANGELOG.md | 12 + README.md | 6 +- build.gradle | 409 ++++++++++++------ gradle.properties | 8 +- .../onlyfun/policy/OnlyFunItemPolicy.java | 6 + .../onlyfun/shop/OnlyFunShopCatalogSmoke.java | 7 + ...pha.216-storage-terminal-to-alpha.217.json | 70 +++ pack/packwiz/index.toml | 26 +- pack/packwiz/pack.toml | 4 +- pack/prism/modrinth.index.json | 2 +- pack/release.json | 4 +- .../sanctuary26/redstoner/RedStonerMod.java | 2 +- .../redstoner/block/StorageTerminalBlock.java | 114 +++++ .../redstoner/client/RedStonerClient.java | 11 + .../client/gui/StorageTerminalScreen.java | 244 +++++++++++ .../network/RedStonerNetworking.java | 29 +- .../network/StorageTerminalActionPayload.java | 56 +++ .../StorageTerminalSnapshotPayload.java | 80 ++++ .../registry/RedStonerRegistries.java | 12 +- .../storage/StorageTerminalInventory.java | 118 +++++ .../storage/StorageTerminalNetwork.java | 92 ++++ .../storage/StorageTerminalRules.java | 26 ++ .../storage/StorageTerminalService.java | 109 +++++ .../storage/StorageTerminalStatus.java | 9 + .../storage/StorageTerminalTopology.java | 44 ++ .../blockstates/storage_terminal.json | 8 + .../redstoner/items/storage_terminal.json | 6 + .../assets/redstoner/lang/en_us.json | 9 +- .../assets/redstoner/lang/fr_fr.json | 9 +- .../assets/redstoner/lang/ru_ru.json | 9 +- .../models/block/storage_terminal.json | 25 ++ .../tags/block/mineable/pickaxe.json | 2 +- .../tags/block/needs_stone_tool.json | 2 +- .../loot_table/blocks/storage_terminal.json | 20 + .../redstoner/recipe/storage_terminal.json | 19 + redstoner/src/main/resources/fabric.mod.json | 2 +- .../storage/StorageTerminalSmoke.java | 103 +++++ sanctuary/build.gradle | 20 +- .../sanctuary/shop/BlackMarketItemPolicy.java | 16 +- .../sanctuary/shop/BlackMarketModelSmoke.java | 3 + 40 files changed, 1574 insertions(+), 179 deletions(-) create mode 100644 pack/migrations/26.2.0-alpha.216-storage-terminal-to-alpha.217.json create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/block/StorageTerminalBlock.java create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/StorageTerminalScreen.java create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/StorageTerminalActionPayload.java create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/StorageTerminalSnapshotPayload.java create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalInventory.java create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalNetwork.java create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalRules.java create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalService.java create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalStatus.java create mode 100644 redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalTopology.java create mode 100644 redstoner/src/main/resources/assets/redstoner/blockstates/storage_terminal.json create mode 100644 redstoner/src/main/resources/assets/redstoner/items/storage_terminal.json create mode 100644 redstoner/src/main/resources/assets/redstoner/models/block/storage_terminal.json create mode 100644 redstoner/src/main/resources/data/redstoner/loot_table/blocks/storage_terminal.json create mode 100644 redstoner/src/main/resources/data/redstoner/recipe/storage_terminal.json create mode 100644 redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalSmoke.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e99661d..91dd8d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ 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. +## `26.2.0-alpha.217` + +Modules modifiés : **Red-Stoner `0.0.0-alpha.11`**, **Only Fun `0.0.0-alpha.25`** et **Sanctuary `0.0.0-alpha.121`**. Les sept autres modules conservent leur version issue de l'alpha.216. + +- ajoute `redstoner:storage_terminal`, une demi-dalle verticale de huit pixels qui se pose uniquement sur la face horizontale d’un coffre, coffre piégé ou baril ; +- découvre sans charger de chunk les conteneurs adjacents par face, verticalement compris, et refuse clairement tout réseau qui dépasse 128 blocs physiques ; +- agrège uniquement les piles dont l’item et tous les composants sont strictement identiques, puis les affiche sur des pages défilantes de 54 entrées ; +- valide côté serveur la distance, le terminal, la topologie, la page et l’identité de chaque pile avant un retrait par pile, un retrait unitaire ou un dépôt par shift-clic ; +- fabrique le terminal avec quatre plaques d’argent, trois verres teintés, un Controller et un lingot de titane, sans entrée Shop, Black Market ni récompense de lootbox ; +- ajoute les traductions FR/EN/RU, les ressources bloc/item/loot/recette et des tests déterministes de topologie, pagination, agrégation et transferts ; +- conserve les données Red-Stoner v3, Only Fun v2 et Sanctuary v11 ; seul le protocole réseau Red-Stoner passe de 5 à 6, sans nouveau monde ni réinitialisation des sauvegardes. + ## `26.2.0-alpha.216` Module modifié : **Ambiance `0.0.0-alpha.19`**. Les neuf autres modules conservent leur version issue de l'alpha.215. diff --git a/README.md b/README.md index daaf4b5..7538525 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`) | `anotherworld` | Another World | choses matérielles | matière, photographie, instruments météo et 108 biomes natifs | | `itsalive` | It's Alive ! | vivant | Creepers, créatures, agriculture et gameplay serveur complet de Canaplia | | `ambiance` | Ambiance | perception | atmosphère, météo, lumières dynamiques, audio positionnel, calendrier et composition visuelle accessible | -| `redstoner` | Red-Stoner | calcul et signaux | ordinateurs, identité serveur, Drawer et Particuleur | +| `redstoner` | Red-Stoner | calcul et signaux | ordinateurs, identité serveur, terminal de stockage, Drawer et Particuleur | | `iliketomoveit` | I Like To Move It | déplacement | portails, véhicules, grappin, poulies et waystones locaux | | `ouch` | Ouch | combat et destruction | arsenal actif (6 armes) et TNT météo | | `onlyfun` | Only Fun | règles ludiques absurdes | lootboxes, Lucky Block, raretés et règles absurdes | @@ -22,10 +22,12 @@ 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 distribué : **`26.2.0-alpha.216`**. Cette release corrective distribue Ambiance **`0.0.0-alpha.19`** ; les neuf autres modules conservent leur version issue de l'alpha.215. +Version courante du pack distribué : **`26.2.0-alpha.217`**. Cette release distribue Red-Stoner **`0.0.0-alpha.11`**, Only Fun **`0.0.0-alpha.25`** et Sanctuary **`0.0.0-alpha.121`** ; les sept autres modules conservent leur version issue de l'alpha.216. La migration [CANA-01 à CANA-09](CANAPLIA.md) porte Canaplia vers Minecraft 26.2. It's Alive **`0.0.0-alpha.35`** enregistre les seize familles, les découvertes, hybridations, résines, effets serveur et le Livre interdit personnel, partageable temporairement sur un pupitre. Ambiance **`0.0.0-alpha.14`** compose les profils visuels bornés avec ses réglages d'accessibilité, et Only Fun **`0.0.0-alpha.21`** exclut explicitement Canaplia et les raccourcis de progression du Shop. +L’alpha.217 ajoute le terminal de stockage Red-Stoner : cette demi-dalle verticale se fixe sur un coffre, un coffre piégé ou un baril et donne accès, par pages défilantes de 54 types, à un réseau contigu borné à 128 conteneurs chargés. Les retraits et dépôts sont validés par le serveur, les composants d’items restent strictement séparés et le terminal endgame est exclu du Shop, du Black Market et des récompenses Only Fun. + L’alpha.216 corrige la lecture lente et dégradée du disque blanc : les quatre tampons PCM initiaux de Minecraft sont maintenant remplis continûment à 48 kHz, avec une vitesse OpenAL normale, au lieu d’être majoritairement complétés par du silence. Les packs Modrinth, Prism et Packwiz sont mis à jour sans exiger de nouveau monde ni réinitialiser les sauvegardes. L’alpha.207 rend le stockage du coffre-fort pratiquement illimité : rubis, saphirs et émeraudes utilisent chacun un solde 64 bits. Les 27 anciennes piles sont additionnées automatiquement lors du chargement, le Shop continue de consommer directement ces soldes et les 20 % pillés sont crédités dans le coffre-fort de l’attaquant sans créer des millions d’entités ou de piles d’objets. diff --git a/build.gradle b/build.gradle index 78d016e..028deb9 100644 --- a/build.gradle +++ b/build.gradle @@ -1505,7 +1505,7 @@ tasks.register("verifyWorkspace") { || personalComputerMigration.compatibility?.redstoner_target_data_version != 3 || personalComputerMigration.compatibility?.redstoner_target_network_protocol != 5 || redstonerManifest.custom?.sanctuary26?.data_version != 3 - || redstonerManifest.custom?.sanctuary26?.network_protocol != 5 + || redstonerManifest.custom?.sanctuary26?.network_protocol != 6 || sanctuaryManifest.custom?.sanctuary26?.data_version < 8 || !sanctuaryIdentity.contains('SAVE_KEY = "server_identity"') || !personalComputerJava.contains('SAVE_KEY = "personal_computers"') @@ -3437,9 +3437,9 @@ 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.216" - || rootProject.sanctuary_version != "0.0.0-alpha.120" - || rootProject.onlyfun_version != "0.0.0-alpha.24" + if (rootProject.pack_version != "26.2.0-alpha.217" + || rootProject.sanctuary_version != "0.0.0-alpha.121" + || rootProject.onlyfun_version != "0.0.0-alpha.25" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"] || migration.target?.modules != [sanctuary: "0.0.0-alpha.109", onlyfun: "0.0.0-alpha.17"] @@ -3487,9 +3487,9 @@ 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.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.anotherworld_version != "0.0.0-alpha.55" - || rootProject.redstoner_version != "0.0.0-alpha.10" + || rootProject.redstoner_version != "0.0.0-alpha.11" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.191" || migration.source?.modules != [anotherworld: "0.0.0-alpha.48", redstoner: "0.0.0-alpha.9"] @@ -3749,7 +3749,7 @@ project(":onlyfun") { def shitCubeRecipe = new JsonSlurper().parse(file( "src/main/resources/data/abjection/recipe/shit_cube.json")) if (manifest.custom.sanctuary26.data_version != 2 - || project.version.toString() != "0.0.0-alpha.24" + || project.version.toString() != "0.0.0-alpha.25" || manifest.depends?.sanctuary != "*" || manifest.provides != ["abjection"] || manifest.custom.sanctuary26.network_protocol != 3 @@ -3971,6 +3971,14 @@ project(":redstoner") { mainClass = "fr.koka99cab.sanctuary26.redstoner.vm.RedstoneProgramSmoke" } + tasks.register("storageTerminalSmoke", JavaExec) { + group = "verification" + description = "Checks bounded Storage Terminal topology, paging and exact-stack transfers." + dependsOn tasks.named("testClasses") + classpath = sourceSets.test.runtimeClasspath + mainClass = "fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalSmoke" + } + tasks.register("verifyRedStoner") { group = "verification" description = "Checks Red-Stoner computation and signal-transmission contracts." @@ -3985,6 +3993,7 @@ project(":redstoner") { inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.167-cobble-dungeon-ownership-to-alpha.168.json")) inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.185-computer-recipes-to-alpha.186.json")) inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.191-mirror-lavender-drawer-to-alpha.192.json")) + inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.216-storage-terminal-to-alpha.217.json")) doLast { def registry = file("src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java").text def javaText = fileTree("src/main/java").matching { include "**/*.java" }.files*.text.join("\n") @@ -4003,7 +4012,10 @@ project(":redstoner") { "pack/migrations/26.2.0-alpha.185-computer-recipes-to-alpha.186.json")) def drawerMigration = new JsonSlurper().parse(rootProject.file( "pack/migrations/26.2.0-alpha.191-mirror-lavender-drawer-to-alpha.192.json")) - def requiredIds = ["eight_bit_computer", "controller", "personal_computer", "drawer", "particuleur"] + def terminalMigration = new JsonSlurper().parse(rootProject.file( + "pack/migrations/26.2.0-alpha.216-storage-terminal-to-alpha.217.json")) + def requiredIds = [ + "eight_bit_computer", "controller", "personal_computer", "drawer", "particuleur", "storage_terminal"] def missingIds = requiredIds.findAll { !registry.contains("id(\"${it}\")") } if (!missingIds.isEmpty()) { throw new GradleException("Missing active Red-Stoner IDs: ${missingIds}") @@ -4027,7 +4039,10 @@ project(":redstoner") { 'SanctuaryAtlasLandmarks.placeManual', 'distanceToSqr', 'readUtf(4096)', 'class ParticuleurBlock', 'implements WorldlyContainer', '"Catalyst"', 'getBestNeighborSignal', 'ParticuleurParticleTable', - 'side != getBlockState().getValue(ParticuleurBlock.FACING)' + 'side != getBlockState().getValue(ParticuleurBlock.FACING)', + 'id("s2c/storage_terminal_snapshot")', 'id("c2s/storage_terminal_action")', + 'class StorageTerminalBlock', 'MAX_CONTAINERS = 128', 'PAGE_SIZE = 54', + 'ItemStack.isSameItemSameComponents', 'level.hasChunkAt(pos)' ] def missingContracts = requiredContracts.findAll { !javaText.contains(it) } if (!missingContracts.isEmpty()) { @@ -4064,7 +4079,13 @@ project(":redstoner") { "screen.redstoner.personal_computer.statistics", "screen.redstoner.personal_computer.events", "screen.redstoner.personal_computer.shutdown", - "message.redstoner.personal_computer.atlas_marker_exists" + "message.redstoner.personal_computer.atlas_marker_exists", + "screen.redstoner.storage_terminal.title", + "screen.redstoner.storage_terminal.summary", + "screen.redstoner.storage_terminal.error.invalid_terminal", + "screen.redstoner.storage_terminal.error.disconnected", + "screen.redstoner.storage_terminal.error.too_large", + "screen.redstoner.storage_terminal.error.locked" ]) def missingKeys = requiredKeys.findAll { !translations.containsKey(it) } if (!missingKeys.isEmpty()) { @@ -4075,10 +4096,41 @@ project(":redstoner") { "src/main/resources/data/redstoner/recipe/eight_bit_computer.json")) def controllerRecipe = new JsonSlurper().parse(file( "src/main/resources/data/redstoner/recipe/controller.json")) - if (project.version.toString() != "0.0.0-alpha.10" + def terminalRecipe = new JsonSlurper().parse(file( + "src/main/resources/data/redstoner/recipe/storage_terminal.json")) + if (project.version.toString() != "0.0.0-alpha.11" || manifest.custom?.sanctuary26?.data_version != 3 - || manifest.custom?.sanctuary26?.network_protocol != 5 - || file("src/main/resources/data/redstoner/recipe/personal_computer.json").exists() + || manifest.custom?.sanctuary26?.network_protocol != 6 + || terminalMigration.source?.pack_version != "26.2.0-alpha.216" + || terminalMigration.source?.modules != [ + redstoner: "0.0.0-alpha.10", onlyfun: "0.0.0-alpha.24", sanctuary: "0.0.0-alpha.120"] + || terminalMigration.target?.pack_version != "26.2.0-alpha.217" + || terminalMigration.target?.modules != [ + redstoner: "0.0.0-alpha.11", onlyfun: "0.0.0-alpha.25", sanctuary: "0.0.0-alpha.121"] + || terminalMigration.storage_terminal?.owner != "redstoner" + || terminalMigration.storage_terminal?.id != "redstoner:storage_terminal" + || terminalMigration.storage_terminal?.shape_depth_pixels != 8 + || terminalMigration.storage_terminal?.supported_containers != [ + "minecraft:chest", "minecraft:trapped_chest", "minecraft:barrel"] + || terminalMigration.storage_terminal?.ender_chest_supported != false + || terminalMigration.storage_terminal?.maximum_physical_containers != 128 + || terminalMigration.storage_terminal?.page_size != 54 + || terminalMigration.storage_terminal?.strict_item_component_aggregation != true + || terminalMigration.storage_terminal?.server_authoritative_transfers != true + || terminalMigration.storage_terminal?.available_in_shop != false + || terminalMigration.storage_terminal?.available_in_black_market != false + || terminalMigration.storage_terminal?.available_in_lootbox_rewards != false + || terminalMigration.compatibility?.redstoner_data_version != 3 + || terminalMigration.compatibility?.redstoner_source_network_protocol != 5 + || terminalMigration.compatibility?.redstoner_target_network_protocol != 6 + || terminalRecipe.pattern != ["PGP", "GCG", "PTP"] + || terminalRecipe.key != [ + P: "anotherworld:silver_plate", G: "minecraft:tinted_glass", + C: "redstoner:controller", T: "anotherworld:titanium_ingot"] + || terminalRecipe.result != [id: "redstoner:storage_terminal", count: 1]) { + throw new GradleException("Red-Stoner alpha.11 Storage Terminal contract is incomplete") + } + if (file("src/main/resources/data/redstoner/recipe/personal_computer.json").exists() || file("src/main/resources/data/redstoner/recipe/drawer.json").exists() || registry.contains('output.accept(PERSONAL_COMPUTER_ITEM)') || registry.contains('output.accept(DRAWER_ITEM)') @@ -4192,6 +4244,7 @@ project(":redstoner") { tasks.named("check") { dependsOn tasks.named("verifyRedStoner") dependsOn tasks.named("redstoneProgramSmoke") + dependsOn tasks.named("storageTerminalSmoke") } } @@ -4206,9 +4259,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.216" - || rootProject.onlyfun_version != "0.0.0-alpha.24" - || rootProject.sanctuary_version != "0.0.0-alpha.120" + if (rootProject.pack_version != "26.2.0-alpha.217" + || rootProject.onlyfun_version != "0.0.0-alpha.25" + || rootProject.sanctuary_version != "0.0.0-alpha.121" || 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" @@ -4251,7 +4304,7 @@ 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.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.itsalive_version != "0.0.0-alpha.36" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.193" @@ -4300,9 +4353,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.216" - || rootProject.onlyfun_version != "0.0.0-alpha.24" - || rootProject.sanctuary_version != "0.0.0-alpha.120" + if (rootProject.pack_version != "26.2.0-alpha.217" + || rootProject.onlyfun_version != "0.0.0-alpha.25" + || rootProject.sanctuary_version != "0.0.0-alpha.121" || 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"] @@ -4346,7 +4399,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.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || migration.source?.pack_version != "26.2.0-alpha.195" || migration.target?.pack_version != "26.2.0-alpha.196" || migration.target?.modules != [ @@ -4364,10 +4417,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.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" - || !packwiz.contains('version = "26.2.0-alpha.216"')) { + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" + || !packwiz.contains('version = "26.2.0-alpha.217"')) { throw new GradleException("The alpha.196 culinary, mailbox and grapple release contract is incomplete") } } @@ -4392,9 +4445,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.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.itsalive_version != "0.0.0-alpha.36" - || rootProject.sanctuary_version != "0.0.0-alpha.120" + || rootProject.sanctuary_version != "0.0.0-alpha.121" || 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"] @@ -4412,10 +4465,10 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") { || migration.distribution?.jei?.target != "30.26.0.186" || migration.compatibility?.sanctuary_target_network_protocol != 19 || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22 - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" - || !packwiz.contains('version = "26.2.0-alpha.216"')) { + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" + || !packwiz.contains('version = "26.2.0-alpha.217"')) { throw new GradleException("The alpha.197 quest-request and cauldron-batch release contract is incomplete") } } @@ -4442,7 +4495,7 @@ 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.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.itsalive_version != "0.0.0-alpha.36" || migration.source?.pack_version != "26.2.0-alpha.197" || migration.target?.pack_version != "26.2.0-alpha.198" @@ -4461,10 +4514,10 @@ tasks.register("verifyFermentationBarrelAutomationRelease") { || migration.distribution?.packwiz_index_refreshed != true || itsAliveManifest.custom?.sanctuary26?.data_version != 3 || itsAliveManifest.custom?.sanctuary26?.network_protocol != 4 - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" - || !packwiz.contains('version = "26.2.0-alpha.216"') + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.36.jar"')) { throw new GradleException("The alpha.198 fermentation barrel release contract is incomplete") } @@ -4492,9 +4545,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.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.itsalive_version != "0.0.0-alpha.36" - || rootProject.sanctuary_version != "0.0.0-alpha.120" + || rootProject.sanctuary_version != "0.0.0-alpha.121" || 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" @@ -4506,12 +4559,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 != 22 - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" - || !packwiz.contains('version = "26.2.0-alpha.216"') + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.36.jar"') - || !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.120.jar"')) { + || !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.121.jar"')) { throw new GradleException("The alpha.199 quest-request contract release is incomplete") } } @@ -4547,7 +4600,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.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.ambiance_version != "0.0.0-alpha.19" || rootProject.ouch_version != "0.0.0-alpha.10" || migration.source?.pack_version != "26.2.0-alpha.199" @@ -4581,10 +4634,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.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" - || !packwiz.contains('version = "26.2.0-alpha.216"') + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.19.jar"') || !packwizIndex.contains('file = "mods/ouch-0.0.0-alpha.10.jar"')) { throw new GradleException("The alpha.200 Weather TNT and Ambiance release is incomplete") @@ -4613,8 +4666,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.216" - || rootProject.sanctuary_version != "0.0.0-alpha.120" + if (rootProject.pack_version != "26.2.0-alpha.217" + || rootProject.sanctuary_version != "0.0.0-alpha.121" || 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" @@ -4628,11 +4681,11 @@ tasks.register("verifyQuestRequestEscrowRelease") { || migration.compatibility?.sanctuary_target_network_protocol != 21 || sanctuaryManifest.custom?.sanctuary26?.data_version != 11 || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22 - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" - || !packwiz.contains('version = "26.2.0-alpha.216"') - || !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.120.jar"')) { + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" + || !packwiz.contains('version = "26.2.0-alpha.217"') + || !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.121.jar"')) { throw new GradleException("The alpha.201 quest-request escrow release is incomplete") } } @@ -4667,11 +4720,11 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") { "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.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.anotherworld_version != "0.0.0-alpha.55" || rootProject.itsalive_version != "0.0.0-alpha.36" || rootProject.iliketomoveit_version != "0.0.0-alpha.15" - || rootProject.sanctuary_version != "0.0.0-alpha.120" + || rootProject.sanctuary_version != "0.0.0-alpha.121" || migration.source?.pack_version != "26.2.0-alpha.201" || migration.source?.modules != [ itsalive: "0.0.0-alpha.30", @@ -4707,14 +4760,14 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") { || moveItManifest.custom?.sanctuary26?.network_protocol != 5 || sanctuaryManifest.custom?.sanctuary26?.data_version != 11 || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 22 - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" - || !packwiz.contains('version = "26.2.0-alpha.216"') + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"') || !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.36.jar"') || !packwizIndex.contains('file = "mods/iliketomoveit-0.0.0-alpha.15.jar"') - || !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.120.jar"')) { + || !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.121.jar"')) { throw new GradleException("The alpha.202 Waystone, Shop and culinary lectern release is incomplete") } } @@ -4827,9 +4880,9 @@ tasks.register("verifySafeVaultFeature") { 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.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.anotherworld_version != "0.0.0-alpha.55" - || rootProject.sanctuary_version != "0.0.0-alpha.120" + || rootProject.sanctuary_version != "0.0.0-alpha.121" || migration.schema_version != 1 || migration.source?.pack_version != "26.2.0-alpha.201" || migration.target?.pack_version != "26.2.0-alpha.202" @@ -5019,7 +5072,7 @@ tasks.register("verifySafeVaultInterfaceAlpha203Release") { 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.28.0.193.jar" } - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.anotherworld_version != "0.0.0-alpha.55" || migration.source?.pack_version != "26.2.0-alpha.202" || migration.source?.modules != [anotherworld: "0.0.0-alpha.50"] @@ -5037,9 +5090,9 @@ tasks.register("verifySafeVaultInterfaceAlpha203Release") { || 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.216.mrpack" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" || prism.versionId != rootProject.pack_version - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"') || packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.50.jar"') || jeiFiles.size() != 1 @@ -5065,7 +5118,7 @@ tasks.register("verifySafeVaultFixAlpha204Release") { 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.28.0.193.jar" } - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.anotherworld_version != "0.0.0-alpha.55" || migration.source?.pack_version != "26.2.0-alpha.203" || migration.source?.modules != [anotherworld: "0.0.0-alpha.51"] @@ -5091,9 +5144,9 @@ tasks.register("verifySafeVaultFixAlpha204Release") { || migration.distribution?.jei?.modrinth_version_id != "OcMayhUH" || migration.distribution?.jei?.prism_manifest_updated != true || release.pack_version != rootProject.pack_version - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" || prism.versionId != rootProject.pack_version - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"') || packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.51.jar"') || jeiFiles.size() != 1 @@ -5119,7 +5172,7 @@ tasks.register("verifySafeVaultCubeAlpha205Release") { 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.28.0.193.jar" } - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.anotherworld_version != "0.0.0-alpha.55" || migration.source?.pack_version != "26.2.0-alpha.204" || migration.source?.modules != [anotherworld: "0.0.0-alpha.52"] @@ -5148,9 +5201,9 @@ tasks.register("verifySafeVaultCubeAlpha205Release") { || migration.distribution?.jei?.modrinth_version_id != "NtfJDMhk" || migration.distribution?.jei?.prism_manifest_updated != true || release.pack_version != rootProject.pack_version - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" || prism.versionId != rootProject.pack_version - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"') || packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.52.jar"') || jeiFiles.size() != 1 @@ -5177,7 +5230,7 @@ tasks.register("verifySafeVaultCtmAlpha206Release") { 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.28.0.193.jar" } - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.anotherworld_version != "0.0.0-alpha.55" || migration.source?.pack_version != "26.2.0-alpha.205" || migration.source?.modules != [anotherworld: "0.0.0-alpha.53"] @@ -5200,9 +5253,9 @@ tasks.register("verifySafeVaultCtmAlpha206Release") { || migration.compatibility?.pack_release_updated != true || migration.distribution?.packwiz_index_refreshed != true || release.pack_version != rootProject.pack_version - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" || prism.versionId != rootProject.pack_version - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"') || packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.53.jar"') || jeiFiles.size() != 1 @@ -5229,7 +5282,7 @@ tasks.register("verifySafeVaultUnlimitedAlpha207Release") { 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.28.0.193.jar" } - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.anotherworld_version != "0.0.0-alpha.55" || migration.source?.pack_version != "26.2.0-alpha.206" || migration.source?.modules != [anotherworld: "0.0.0-alpha.54"] @@ -5259,9 +5312,9 @@ tasks.register("verifySafeVaultUnlimitedAlpha207Release") { || migration.compatibility?.pack_release_updated != true || migration.distribution?.packwiz_index_refreshed != true || release.pack_version != rootProject.pack_version - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" || prism.versionId != rootProject.pack_version - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.55.jar"') || packwizIndex.contains('file = "mods/anotherworld-0.0.0-alpha.54.jar"') || jeiFiles.size() != 1 @@ -5317,7 +5370,7 @@ tasks.register("verifyCanapliaFoundation") { "itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/ItsAliveMod.java").text def visualApiSource = file( "ambiance/src/main/java/fr/koka99cab/sanctuary26/ambiance/api/visual/AmbianceVisualEffectApi.java").text - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.itsalive_version != "0.0.0-alpha.36" || rootProject.ambiance_version != "0.0.0-alpha.19" || migration.issue != 35 @@ -5569,10 +5622,10 @@ tasks.register("verifyCanapliaAlpha209Release") { ambiance: "0.0.0-alpha.14", itsalive: "0.0.0-alpha.33", onlyfun: "0.0.0-alpha.20"] - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.ambiance_version != "0.0.0-alpha.19" || rootProject.itsalive_version != "0.0.0-alpha.36" - || rootProject.onlyfun_version != "0.0.0-alpha.24" + || rootProject.onlyfun_version != "0.0.0-alpha.25" || migration.source?.pack_version != "26.2.0-alpha.207" || migration.source?.modules != expectedSource || migration.target?.pack_version != "26.2.0-alpha.208" @@ -5617,12 +5670,12 @@ tasks.register("verifyCanapliaAlpha209Release") { || fixMigration.compatibility?.network_protocol_changed != false || fixMigration.distribution?.pack_release_updated != true || release.pack_version != rootProject.pack_version - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" || prism.versionId != rootProject.pack_version - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.19.jar"') || !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.36.jar"') - || !packwizIndex.contains('file = "mods/onlyfun-0.0.0-alpha.24.jar"') + || !packwizIndex.contains('file = "mods/onlyfun-0.0.0-alpha.25.jar"') || packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.12.jar"') || packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.33.jar"') || packwizIndex.contains('file = "mods/onlyfun-0.0.0-alpha.19.jar"') @@ -5671,8 +5724,8 @@ tasks.register("verifyAlpha210Release") { itsalive: "0.0.0-alpha.35"] def currentModules = [ ambiance: "0.0.0-alpha.19", - sanctuary: "0.0.0-alpha.120", - onlyfun: "0.0.0-alpha.24", + sanctuary: "0.0.0-alpha.121", + onlyfun: "0.0.0-alpha.25", ouch: "0.0.0-alpha.10", iliketomoveit: "0.0.0-alpha.15", itsalive: "0.0.0-alpha.36"] @@ -5687,9 +5740,9 @@ tasks.register("verifyAlpha210Release") { def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" } - if (rootProject.pack_version != "26.2.0-alpha.216" - || rootProject.sanctuary_version != "0.0.0-alpha.120" - || rootProject.onlyfun_version != "0.0.0-alpha.24" + if (rootProject.pack_version != "26.2.0-alpha.217" + || rootProject.sanctuary_version != "0.0.0-alpha.121" + || rootProject.onlyfun_version != "0.0.0-alpha.25" || rootProject.iliketomoveit_version != expectedTarget.iliketomoveit || rootProject.itsalive_version != "0.0.0-alpha.36" || migration.source?.pack_version != "26.2.0-alpha.209" @@ -5733,10 +5786,10 @@ tasks.register("verifyAlpha210Release") { || manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5 || manifests.itsalive.custom?.sanctuary26?.data_version != 3 || manifests.itsalive.custom?.sanctuary26?.network_protocol != 4 - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" - || !packwiz.contains('version = "26.2.0-alpha.216"') + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" + || !packwiz.contains('version = "26.2.0-alpha.217"') || requiredJars.any { !packwizIndex.contains(it) } || obsoleteJars.any { packwizIndex.contains(it) } || jeiFiles.size() != 1 @@ -5783,7 +5836,8 @@ tasks.register("verifyAlpha211Release") { ouch: "0.0.0-alpha.10", sanctuary: "0.0.0-alpha.119"] def currentModules = expectedTarget + [ - sanctuary: "0.0.0-alpha.120", + sanctuary: "0.0.0-alpha.121", + onlyfun: "0.0.0-alpha.25", ambiance: "0.0.0-alpha.19"] def workspaceModules = currentModules + [ambiance: "0.0.0-alpha.19"] def expectedDataVersions = [ambiance: 2, itsalive: 3, onlyfun: 2, ouch: 1, sanctuary: 11] @@ -5810,7 +5864,7 @@ tasks.register("verifyAlpha211Release") { def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" } - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || workspaceModules.any { id, version -> rootProject.property("${id}_version") != version } || migration.source?.pack_version != "26.2.0-alpha.210" || migration.source?.modules != expectedSource @@ -5838,9 +5892,9 @@ tasks.register("verifyAlpha211Release") { || migration.distribution?.prism_modrinth_index_updated != true || migration.distribution?.shared_client_server_mrpack != true || release.pack_version != rootProject.pack_version - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" || prism.versionId != rootProject.pack_version - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || requiredJars.any { !packwizIndex.contains(it) } || obsoleteJars.any { packwizIndex.contains(it) } || prism.files?.size() != 5 @@ -5848,7 +5902,7 @@ tasks.register("verifyAlpha211Release") { || jeiFiles[0].hashes?.sha1 != "494f9dc787d6c9cd9d32793da2784ab16834faf3" || jeiFiles[0].hashes?.sha512 != "0869f4d5840a732a04bedc7684b40140f087f3d759066c206d9d1ec710007f7c9585e4f5ebb04f10e56bef1cf2eddc6978bb2b2c93541cff5fcf7e1609859468" || !changelog.contains("## `26.2.0-alpha.211`") - || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.216`**")) { + || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.217`**")) { throw new GradleException("The alpha.211 Abjection and gameplay-polish release is incomplete") } } @@ -5882,8 +5936,8 @@ tasks.register("verifyAlpha212Release") { def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" } - if (rootProject.pack_version != "26.2.0-alpha.216" - || rootProject.sanctuary_version != "0.0.0-alpha.120" + if (rootProject.pack_version != "26.2.0-alpha.217" + || rootProject.sanctuary_version != "0.0.0-alpha.121" || migration.source?.pack_version != "26.2.0-alpha.211" || migration.source?.modules != [sanctuary: "0.0.0-alpha.119"] || migration.target?.pack_version != "26.2.0-alpha.212" @@ -5904,16 +5958,16 @@ tasks.register("verifyAlpha212Release") { || manifest.custom?.sanctuary26?.data_version != 11 || manifest.custom?.sanctuary26?.network_protocol != 22 || !minecraftMixin.contains('method = "pickBlockOrEntity"') - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" || prism.files?.size() != 5 || jeiFiles.size() != 1 - || !packwiz.contains('version = "26.2.0-alpha.216"') - || !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.120.jar"') + || !packwiz.contains('version = "26.2.0-alpha.217"') + || !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.121.jar"') || packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.119.jar"') || !changelog.contains("## `26.2.0-alpha.212`") - || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.216`**")) { + || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.217`**")) { throw new GradleException("The alpha.212 Black Market startup-fix release is incomplete") } } @@ -5947,7 +6001,7 @@ tasks.register("verifyAlpha213Release") { def jeiFiles = prism.files.findAll { it.path == "mods/jei-26.2-fabric-30.28.0.193.jar" } - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.ambiance_version != "0.0.0-alpha.19" || migration.source?.pack_version != "26.2.0-alpha.212" || migration.source?.modules != [ambiance: "0.0.0-alpha.15"] @@ -5971,16 +6025,16 @@ tasks.register("verifyAlpha213Release") { || manifest.custom?.sanctuary26?.network_protocol != 6 || !levelEventMixin.contains('@Mixin(LevelEventHandler.class)') || !levelEventMixin.contains('@Inject(method = "levelEvent"') - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" || prism.files?.size() != 5 || jeiFiles.size() != 1 - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.19.jar"') || packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.15.jar"') || !changelog.contains("## `26.2.0-alpha.213`") - || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.216`**")) { + || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.217`**")) { throw new GradleException("The alpha.213 Ambiance startup-fix release is incomplete") } } @@ -6015,7 +6069,7 @@ tasks.register("verifyAlpha214Release") { "ambiance/src/main/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscAudioPlayer.java").text def urlSmoke = file( "ambiance/src/test/java/fr/koka99cab/sanctuary26/ambiance/white_disc/WhiteDiscUrlSmoke.java").text - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.ambiance_version != "0.0.0-alpha.19" || migration.source?.pack_version != "26.2.0-alpha.213" || migration.source?.modules != [ambiance: "0.0.0-alpha.16"] @@ -6044,16 +6098,16 @@ tasks.register("verifyAlpha214Release") { || !audioPlayer.contains('message.ambiance.white_disc.loading') || !audioPlayer.contains('message.ambiance.white_disc.failed') || !urlSmoke.contains("https://youtu.be/Ue5ZBe-GzSM?si=78LSbtBQzUYzhA2Y") - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" || prism.files?.size() != 5 - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.19.jar"') || packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.16.jar"') || !changelog.contains("## `26.2.0-alpha.214`") || changelog.contains("## Modifications non distribuées") - || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.216`**")) { + || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.217`**")) { throw new GradleException("The alpha.214 white-disc solo synchronization release is incomplete") } } @@ -6094,7 +6148,7 @@ tasks.register("verifyAlpha215Release") { "ambiance/src/main/java/fr/koka99cab/sanctuary26/ambiance/client/audio/LavaplayerAudioStream.java").text def playbackSmoke = file( "ambiance/src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java").text - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.ambiance_version != "0.0.0-alpha.19" || migration.source?.pack_version != "26.2.0-alpha.214" || migration.source?.modules != [ambiance: "0.0.0-alpha.17"] @@ -6133,16 +6187,16 @@ tasks.register("verifyAlpha215Release") { || !audioStream.contains("while (output.hasRemaining()) output.put((byte) 0)") || !playbackSmoke.contains("https://www.youtube.com/watch?v=Ue5ZBe-GzSM") || !playbackSmoke.contains("Decoded audible PCM data") - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" || prism.files?.size() != 5 - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.19.jar"') || packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.17.jar"') || !changelog.contains("## `26.2.0-alpha.215`") || changelog.contains("## Modifications non distribuées") - || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.216`**")) { + || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.217`**")) { throw new GradleException("The alpha.215 resilient white-disc playback release is incomplete") } } @@ -6187,7 +6241,7 @@ tasks.register("verifyAlpha216Release") { "ambiance/src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/LavaplayerAudioStreamSmoke.java").text def playbackSmoke = file( "ambiance/src/test/java/fr/koka99cab/sanctuary26/ambiance/client/audio/WhiteDiscPlaybackSmoke.java").text - if (rootProject.pack_version != "26.2.0-alpha.216" + if (rootProject.pack_version != "26.2.0-alpha.217" || rootProject.ambiance_version != "0.0.0-alpha.19" || migration.source?.pack_version != "26.2.0-alpha.215" || migration.source?.modules != [ambiance: "0.0.0-alpha.18"] @@ -6228,26 +6282,119 @@ tasks.register("verifyAlpha216Release") { || !streamSmoke.contains("PCM frames were not read with bounded waits") || !playbackSmoke.contains("audibleRatio < 0.75") || !playbackSmoke.contains("LavaplayerAudioStream.SAMPLE_RATE * 2 * 4") - || release.pack_version != "26.2.0-alpha.216" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.216.mrpack" - || prism.versionId != "26.2.0-alpha.216" + || release.pack_version != "26.2.0-alpha.217" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != "26.2.0-alpha.217" || prism.files?.size() != 5 - || !packwiz.contains('version = "26.2.0-alpha.216"') + || !packwiz.contains('version = "26.2.0-alpha.217"') || !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.19.jar"') || packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.18.jar"') || !changelog.contains("## `26.2.0-alpha.216`") || changelog.contains("## Modifications non distribuées") - || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.216`**")) { + || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.217`**")) { throw new GradleException("The alpha.216 white-disc audio-quality release is incomplete") } } } +tasks.register("verifyAlpha217Release") { + group = "verification" + description = "Checks the Sanctuary 26.2.0-alpha.217 Storage Terminal release." + inputs.file(file("pack/migrations/26.2.0-alpha.216-storage-terminal-to-alpha.217.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.file(file("CHANGELOG.md")) + inputs.file(file("README.md")) + inputs.files(["redstoner", "onlyfun", "sanctuary"].collect { id -> + file("${id}/src/main/resources/fabric.mod.json") + }) + inputs.file(file("redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalRules.java")) + inputs.file(file("redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java")) + inputs.file(file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/policy/OnlyFunItemPolicy.java")) + inputs.file(file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketItemPolicy.java")) + doLast { + def migration = new JsonSlurper().parse(file( + "pack/migrations/26.2.0-alpha.216-storage-terminal-to-alpha.217.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 changelog = file("CHANGELOG.md").text + def readme = file("README.md").text + def manifests = ["redstoner", "onlyfun", "sanctuary"].collectEntries { id -> + [(id): new JsonSlurper().parse(file("${id}/src/main/resources/fabric.mod.json"))] + } + def rules = file( + "redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalRules.java").text + def networking = file( + "redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java").text + def onlyFunPolicy = file( + "onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/policy/OnlyFunItemPolicy.java").text + def blackMarketPolicy = file( + "sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketItemPolicy.java").text + def sourceModules = [ + redstoner: "0.0.0-alpha.10", onlyfun: "0.0.0-alpha.24", sanctuary: "0.0.0-alpha.120"] + def targetModules = [ + redstoner: "0.0.0-alpha.11", onlyfun: "0.0.0-alpha.25", sanctuary: "0.0.0-alpha.121"] + def requiredJars = targetModules.collect { id, version -> + "file = \"mods/${id}-${version}.jar\"" + } + def obsoleteJars = sourceModules.collect { id, version -> + "file = \"mods/${id}-${version}.jar\"" + } + if (rootProject.pack_version != "26.2.0-alpha.217" + || targetModules.any { id, version -> rootProject.property("${id}_version") != version } + || migration.source?.pack_version != "26.2.0-alpha.216" + || migration.source?.modules != sourceModules + || migration.target?.pack_version != "26.2.0-alpha.217" + || migration.target?.modules != targetModules + || migration.storage_terminal?.id != "redstoner:storage_terminal" + || migration.storage_terminal?.shape_depth_pixels != 8 + || migration.storage_terminal?.loaded_chunks_only != true + || migration.storage_terminal?.maximum_physical_containers != 128 + || migration.storage_terminal?.page_size != 54 + || migration.storage_terminal?.server_authoritative_transfers != true + || migration.storage_terminal?.available_in_shop != false + || migration.storage_terminal?.available_in_black_market != false + || migration.storage_terminal?.available_in_lootbox_rewards != false + || migration.compatibility?.existing_saves_supported != true + || migration.compatibility?.redstoner_source_network_protocol != 5 + || migration.compatibility?.redstoner_target_network_protocol != 6 + || migration.distribution?.pack_release_updated != true + || migration.distribution?.packwiz_index_refreshed != true + || migration.distribution?.prism_modrinth_index_updated != true + || manifests.redstoner.custom?.sanctuary26?.data_version != 3 + || manifests.redstoner.custom?.sanctuary26?.network_protocol != 6 + || manifests.onlyfun.custom?.sanctuary26?.data_version != 2 + || manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3 + || manifests.sanctuary.custom?.sanctuary26?.data_version != 11 + || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22 + || !rules.contains("MAX_CONTAINERS = 128") + || !rules.contains("PAGE_SIZE = 54") + || !networking.contains("PROTOCOL_VERSION = 6") + || !onlyFunPolicy.contains('namespace.equals("redstoner") && path.equals("storage_terminal")') + || !blackMarketPolicy.contains('namespace.equals("redstoner") && path.equals("storage_terminal")') + || release.pack_version != rootProject.pack_version + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.217.mrpack" + || prism.versionId != rootProject.pack_version + || !packwiz.contains('version = "26.2.0-alpha.217"') + || requiredJars.any { !packwizIndex.contains(it) } + || obsoleteJars.any { packwizIndex.contains(it) } + || !changelog.contains("## `26.2.0-alpha.217`") + || !readme.contains("Version courante du pack distribué : **`26.2.0-alpha.217`**")) { + throw new GradleException("The alpha.217 Storage Terminal release is incomplete") + } + } +} + tasks.named("verifyWorkspace") { dependsOn tasks.named("verifyAlpha213Release") dependsOn tasks.named("verifyAlpha214Release") dependsOn tasks.named("verifyAlpha215Release") dependsOn tasks.named("verifyAlpha216Release") + dependsOn tasks.named("verifyAlpha217Release") } tasks.named("check") { diff --git a/gradle.properties b/gradle.properties index e4caad8..240394c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ mod_version=0.0.0-alpha.0 module_lifecycle=standby ambiance_version=0.0.0-alpha.19 ambiance_lifecycle=active -redstoner_version=0.0.0-alpha.10 +redstoner_version=0.0.0-alpha.11 redstoner_lifecycle=active anotherworld_version=0.0.0-alpha.55 anotherworld_lifecycle=active @@ -26,9 +26,9 @@ masterkey_version=0.0.0-alpha.8 masterkey_lifecycle=active iliketomoveit_version=0.0.0-alpha.15 iliketomoveit_lifecycle=active -onlyfun_version=0.0.0-alpha.24 +onlyfun_version=0.0.0-alpha.25 onlyfun_lifecycle=active -sanctuary_version=0.0.0-alpha.120 +sanctuary_version=0.0.0-alpha.121 sanctuary_lifecycle=active -pack_version=26.2.0-alpha.216 +pack_version=26.2.0-alpha.217 maven_group=fr.koka99cab.sanctuary26 diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/policy/OnlyFunItemPolicy.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/policy/OnlyFunItemPolicy.java index 2eb6ed7..3ddec10 100644 --- a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/policy/OnlyFunItemPolicy.java +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/policy/OnlyFunItemPolicy.java @@ -44,6 +44,7 @@ public final class OnlyFunItemPolicy { if (namespace.equals("canaplia") || namespace.equals("itsalive")) return true; if (namespace.equals("minecraft") && VANILLA_SHOP_GAMEPLAY_BYPASSES.contains(path)) return true; if (namespace.equals("anotherworld") && isProtectedAnotherWorldItem(path)) return true; + if (isStorageTerminal(namespace, path)) return true; if (TECHNICAL_OR_REWARD_BLOCKS.contains(path) || path.startsWith("infested_") || path.contains("command_block")) return true; if (path.contains("lootbox") || path.contains("loot_box")) return true; @@ -61,6 +62,7 @@ public final class OnlyFunItemPolicy { String namespace = id.getNamespace().toLowerCase(Locale.ROOT); String path = normalizedPath(id); return namespace.equals("canaplia") || path.contains("lootbox") || path.contains("loot_box") + || isStorageTerminal(namespace, path) || (namespace.equals("onlyfun") && path.equals("lucky_block")) || (namespace.equals("sanctuary") && path.equals("delivery_box")) || TECHNICAL_OR_REWARD_BLOCKS.contains(path) || path.startsWith("infested_") @@ -77,6 +79,10 @@ public final class OnlyFunItemPolicy { || path.contains("titanium") || path.contains("rainbow_block"); } + private static boolean isStorageTerminal(String namespace, String path) { + return namespace.equals("redstoner") && path.equals("storage_terminal"); + } + private static String normalizedPath(Identifier id) { return id.getPath().toLowerCase(Locale.ROOT); } diff --git a/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalogSmoke.java b/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalogSmoke.java index 82bc2c8..7151584 100644 --- a/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalogSmoke.java +++ b/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalogSmoke.java @@ -81,6 +81,13 @@ public final class OnlyFunShopCatalogSmoke { require(!OnlyFunItemPolicy.excludedFromShop(Identifier.parse(rawFoodId)), "Raw ingredient was removed with finished food: " + rawFoodId); } + Identifier storageTerminal = Identifier.parse("redstoner:storage_terminal"); + require(OnlyFunItemPolicy.excludedFromShop(storageTerminal), + "Storage Terminal reached the Shop policy"); + require(OnlyFunItemPolicy.excludedFromRewards(storageTerminal), + "Storage Terminal reached lootbox rewards"); + require(find(all, storageTerminal.toString()) == null, + "Storage Terminal reached the generated Shop catalogue"); require(OnlyFunBossShopGate.requiresBoss("iliketomoveit:magic_carpet"), "Magic Carpet must remain boss-gated"); require(OnlyFunBossShopGate.requiresBoss("iliketomoveit:magic_cloud"), diff --git a/pack/migrations/26.2.0-alpha.216-storage-terminal-to-alpha.217.json b/pack/migrations/26.2.0-alpha.216-storage-terminal-to-alpha.217.json new file mode 100644 index 0000000..59e9e57 --- /dev/null +++ b/pack/migrations/26.2.0-alpha.216-storage-terminal-to-alpha.217.json @@ -0,0 +1,70 @@ +{ + "schema_version": 1, + "source": { + "pack_version": "26.2.0-alpha.216", + "modules": { + "redstoner": "0.0.0-alpha.10", + "onlyfun": "0.0.0-alpha.24", + "sanctuary": "0.0.0-alpha.120" + } + }, + "target": { + "pack_version": "26.2.0-alpha.217", + "modules": { + "redstoner": "0.0.0-alpha.11", + "onlyfun": "0.0.0-alpha.25", + "sanctuary": "0.0.0-alpha.121" + } + }, + "storage_terminal": { + "owner": "redstoner", + "id": "redstoner:storage_terminal", + "shape_depth_pixels": 8, + "horizontal_wall_mount_only": true, + "supported_containers": [ + "minecraft:chest", + "minecraft:trapped_chest", + "minecraft:barrel" + ], + "ender_chest_supported": false, + "face_adjacent_network_includes_vertical": true, + "loaded_chunks_only": true, + "maximum_physical_containers": 128, + "oversized_network_rejected": true, + "page_size": 54, + "strict_item_component_aggregation": true, + "server_authoritative_transfers": true, + "left_click_extracts_stack": true, + "right_click_extracts_one": true, + "shift_click_deposits_player_stack": true, + "crafting_pattern": ["PGP", "GCG", "PTP"], + "crafting_ingredients": { + "P": "anotherworld:silver_plate", + "G": "minecraft:tinted_glass", + "C": "redstoner:controller", + "T": "anotherworld:titanium_ingot" + }, + "available_in_shop": false, + "available_in_black_market": false, + "available_in_lootbox_rewards": false + }, + "compatibility": { + "new_world_required": false, + "save_reset_required": false, + "existing_saves_supported": true, + "registry_ids_added": ["redstoner:storage_terminal"], + "redstoner_data_version": 3, + "redstoner_source_network_protocol": 5, + "redstoner_target_network_protocol": 6, + "onlyfun_data_version": 2, + "onlyfun_network_protocol": 3, + "sanctuary_data_version": 11, + "sanctuary_network_protocol": 22 + }, + "distribution": { + "pack_release_updated": true, + "packwiz_index_refreshed": true, + "prism_modrinth_index_updated": true, + "shared_client_server_mrpack": true + } +} diff --git a/pack/packwiz/index.toml b/pack/packwiz/index.toml index ecabcdb..01e0717 100644 --- a/pack/packwiz/index.toml +++ b/pack/packwiz/index.toml @@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0" [[files]] file = "mods/ambiance-0.0.0-alpha.19.jar" -hash = "d3810b758e073d74efbaa501e39b0865335baf1089ff76a2554d24c6e13188ee" +hash = "9138adaaa0fb063100906e44453442d9e2596199769067207a2f7bd44f372756" [[files]] file = "mods/anotherworld-0.0.0-alpha.55.jar" -hash = "56015a9d3c3517c948e336eab973205f84772a4f3a0b790e6f2832824ee44f3a" +hash = "2d6250686739fd0bddcbff2d382e22fb9e403472926931f2879f6870af96ed64" [[files]] file = "mods/events-0.0.0-alpha.0.jar" -hash = "bcc501a0fd6e8074cb3c4de26f607952d7ea63d630a697d95bfa59e90feae057" +hash = "4ac4ec54bf702a164443eb184796cca603cbad84607e794c2b24dbd5c7c221b9" [[files]] file = "mods/fabric-api.pw.toml" @@ -31,7 +31,7 @@ metafile = true [[files]] file = "mods/iliketomoveit-0.0.0-alpha.15.jar" -hash = "551e354b96bb35ed8d64b26a4aac85d1d9441be592ed9fc62d6cd359e0e0e0c5" +hash = "fe73255673eda5cbe16fe0bfd4a13951d9f103f58461ca84020e27cde408e569" [[files]] file = "mods/iris.pw.toml" @@ -40,7 +40,7 @@ metafile = true [[files]] file = "mods/itsalive-0.0.0-alpha.36.jar" -hash = "3ff1e1ee2156553b78bf0bded03140d34844f7ab7f0ea6b48929d2c61a8c34bc" +hash = "49aab5f4b3c9145d3cc4e64ce46bff138e4c6147c83c985d402024920d929d0d" [[files]] file = "mods/jei.pw.toml" @@ -49,23 +49,23 @@ metafile = true [[files]] file = "mods/masterkey-0.0.0-alpha.8.jar" -hash = "268b30dcb808d8d2db1b909863beb9974b432fa558e7cd516dde811f600fd66e" +hash = "9429a818c517b3943fdcbdd442800b9d854bf73c605df0f1ec761366574c129a" [[files]] -file = "mods/onlyfun-0.0.0-alpha.24.jar" -hash = "e6b89c1958449fb5bb70bb011df26cecc8f91a224b79788e96827a3897dfba4a" +file = "mods/onlyfun-0.0.0-alpha.25.jar" +hash = "005202d009ba00c5a3019aec6813cbdc2006d191299b2d1829051f73c2fb604a" [[files]] file = "mods/ouch-0.0.0-alpha.10.jar" -hash = "d12d57580015a829f6fc31310cf5724bad1b442380aaf6272194caeed21a50fc" +hash = "b4b5c5cf8164f6a0ce6131031fcbd0840c048a249d1b7c6e08f5b7aeed0e7656" [[files]] -file = "mods/redstoner-0.0.0-alpha.10.jar" -hash = "d210ed751188e7e3e5ce197f498518c06cb5673e4057103cd0964c1a8d2b0066" +file = "mods/redstoner-0.0.0-alpha.11.jar" +hash = "457b4502ce2946963b4c2f21514a29c941f81df9bd1457f5db43fecdfa859a7e" [[files]] -file = "mods/sanctuary-0.0.0-alpha.120.jar" -hash = "f82fc135319d0ad25bed49f2fe924d506761391343265add2efd6c73d55f939a" +file = "mods/sanctuary-0.0.0-alpha.121.jar" +hash = "ef2c827774c768289e034c9715d4f27c347b533f7476a6d355c7b45123fedcec" [[files]] file = "mods/simple-voice-chat.pw.toml" diff --git a/pack/packwiz/pack.toml b/pack/packwiz/pack.toml index 6ee6d9a..daa47a2 100644 --- a/pack/packwiz/pack.toml +++ b/pack/packwiz/pack.toml @@ -1,12 +1,12 @@ name = "Sanctuary 26.2" author = "KOKA99CAB" -version = "26.2.0-alpha.216" +version = "26.2.0-alpha.217" pack-format = "packwiz:1.1.0" [index] file = "index.toml" hash-format = "sha256" -hash = "7a9a44c1f7dcd46aab475cdd2ac078758f320a550386140d89af994acd7c29d1" +hash = "176382d1bdb3797ba19d8d7acb093b9b49156f3b5b3270db803e59d24c6de7e1" [versions] fabric = "0.19.3" diff --git a/pack/prism/modrinth.index.json b/pack/prism/modrinth.index.json index 753fb7d..154861a 100644 --- a/pack/prism/modrinth.index.json +++ b/pack/prism/modrinth.index.json @@ -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.216", + "versionId": "26.2.0-alpha.217", "files": [ { "path": "mods/fabric-api-0.158.0+26.2.jar", diff --git a/pack/release.json b/pack/release.json index 144b746..755defa 100644 --- a/pack/release.json +++ b/pack/release.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "pack_version": "26.2.0-alpha.216", + "pack_version": "26.2.0-alpha.217", "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.216.mrpack", + "primary_file": "sanctuary-26.2.0-alpha.217.mrpack", "shared_client_server_pack": true, "update_owner": "packwiz-installer", "check_on_launch": true, diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/RedStonerMod.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/RedStonerMod.java index 40cb07a..fac0dee 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/RedStonerMod.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/RedStonerMod.java @@ -27,7 +27,7 @@ public final class RedStonerMod implements ModInitializer { RedStonerApi.registerServerIdentityProvider(SanctuaryServerIdentity::get); RedStonerRegistries.initialize(); RedStonerNetworking.initialize(); - LOGGER.info("[{}] Computers, Drawer and Particuleur ready (data v{}, network v{}).", + LOGGER.info("[{}] Computers, Drawer, Particuleur and Storage Terminal ready (data v{}, network v{}).", DISPLAY_NAME, CURRENT_DATA_VERSION, RedStonerNetworking.PROTOCOL_VERSION); } } diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/block/StorageTerminalBlock.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/block/StorageTerminalBlock.java new file mode 100644 index 0000000..ad486db --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/block/StorageTerminalBlock.java @@ -0,0 +1,114 @@ +package fr.koka99cab.sanctuary26.redstoner.block; + +import com.mojang.serialization.MapCodec; +import fr.koka99cab.sanctuary26.redstoner.network.RedStonerNetworking; +import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalNetwork; +import java.util.Map; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +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.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.ScheduledTickAccess; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +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.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +public final class StorageTerminalBlock extends Block { + public static final MapCodec CODEC = simpleCodec(StorageTerminalBlock::new); + public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + private static final Map SHAPES = Map.of( + Direction.NORTH, Block.box(0.0D, 0.0D, 8.0D, 16.0D, 16.0D, 16.0D), + Direction.SOUTH, Block.box(0.0D, 0.0D, 0.0D, 16.0D, 16.0D, 8.0D), + Direction.WEST, Block.box(8.0D, 0.0D, 0.0D, 16.0D, 16.0D, 16.0D), + Direction.EAST, Block.box(0.0D, 0.0D, 0.0D, 8.0D, 16.0D, 16.0D) + ); + + public StorageTerminalBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override + public MapCodec codec() { + return CODEC; + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + Direction facing = context.getClickedFace(); + if (!facing.getAxis().isHorizontal()) return null; + BlockState state = defaultBlockState().setValue(FACING, facing); + return state.canSurvive(context.getLevel(), context.getClickedPos()) ? state : null; + } + + @Override + protected boolean canSurvive(BlockState state, LevelReader level, BlockPos pos) { + BlockPos support = pos.relative(state.getValue(FACING).getOpposite()); + return StorageTerminalNetwork.isCompatibleStorage(level.getBlockState(support)); + } + + @Override + protected BlockState updateShape(BlockState state, LevelReader level, ScheduledTickAccess ticks, + BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) { + return direction == state.getValue(FACING).getOpposite() && !state.canSurvive(level, pos) + ? Blocks.AIR.defaultBlockState() + : super.updateShape(state, level, ticks, pos, direction, neighborPos, neighborState, random); + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPES.get(state.getValue(FACING)); + } + + @Override + protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, + Player player, InteractionHand hand, BlockHitResult hit) { + return open(level, pos, player); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, + Player player, BlockHitResult hit) { + return open(level, pos, player); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + + @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 state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + private static InteractionResult open(Level level, BlockPos pos, Player player) { + if (level.isClientSide()) return InteractionResult.SUCCESS; + if (player instanceof ServerPlayer serverPlayer) { + RedStonerNetworking.openStorageTerminal(serverPlayer, pos); + return InteractionResult.SUCCESS_SERVER; + } + return InteractionResult.CONSUME; + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/RedStonerClient.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/RedStonerClient.java index 3ab1b90..c1c5a73 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/RedStonerClient.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/RedStonerClient.java @@ -3,9 +3,11 @@ package fr.koka99cab.sanctuary26.redstoner.client; import fr.koka99cab.sanctuary26.redstoner.RedStonerMod; import fr.koka99cab.sanctuary26.redstoner.client.gui.ComputerScreen; import fr.koka99cab.sanctuary26.redstoner.client.gui.PersonalComputerScreen; +import fr.koka99cab.sanctuary26.redstoner.client.gui.StorageTerminalScreen; import fr.koka99cab.sanctuary26.redstoner.client.renderer.DrawerBlockEntityRenderer; import fr.koka99cab.sanctuary26.redstoner.network.OpenComputerPayload; import fr.koka99cab.sanctuary26.redstoner.network.OpenPersonalComputerPayload; +import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalSnapshotPayload; import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; @@ -26,6 +28,15 @@ public final class RedStonerClient implements ClientModInitializer { context.client().gui.setScreen(new PersonalComputerScreen(payload)); } })); + ClientPlayNetworking.registerGlobalReceiver(StorageTerminalSnapshotPayload.TYPE, (payload, context) -> + context.client().execute(() -> { + if (context.client().gui.screen() instanceof StorageTerminalScreen screen + && screen.represents(payload)) { + screen.acceptSnapshot(payload); + } else { + context.client().gui.setScreen(new StorageTerminalScreen(payload)); + } + })); RedStonerMod.LOGGER.info("[{}] Client renderers and laboratory screens ready.", RedStonerMod.DISPLAY_NAME); } diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/StorageTerminalScreen.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/StorageTerminalScreen.java new file mode 100644 index 0000000..f111525 --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/StorageTerminalScreen.java @@ -0,0 +1,244 @@ +package fr.koka99cab.sanctuary26.redstoner.client.gui; + +import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalActionPayload; +import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalSnapshotPayload; +import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules; +import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalStatus; +import java.util.Locale; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; + +public final class StorageTerminalScreen extends Screen { + private static final int PANEL_WIDTH = 204; + private static final int PANEL_HEIGHT = 236; + private static final int SLOT_SIZE = 18; + private static final int GRID_X = 8; + private static final int GRID_Y = 20; + private static final int PLAYER_GRID_Y = 152; + private static final int HOTBAR_Y = 210; + private static final int SCROLL_X = 176; + private static final int SCROLL_WIDTH = 8; + private static final int REFRESH_TICKS = 20; + + private StorageTerminalSnapshotPayload snapshot; + private int requestedPage; + private int refreshCooldown = REFRESH_TICKS; + + public StorageTerminalScreen(StorageTerminalSnapshotPayload snapshot) { + super(Component.translatable("screen.redstoner.storage_terminal.title")); + this.snapshot = snapshot; + this.requestedPage = snapshot.page(); + } + + public boolean represents(StorageTerminalSnapshotPayload other) { + return snapshot.pos().equals(other.pos()); + } + + public void acceptSnapshot(StorageTerminalSnapshotPayload refreshed) { + if (!represents(refreshed)) return; + snapshot = refreshed; + requestedPage = refreshed.page(); + refreshCooldown = REFRESH_TICKS; + } + + @Override + protected void init() { + addRenderableWidget(Button.builder(Component.literal("×"), button -> onClose()) + .bounds(panelX() + PANEL_WIDTH - 22, panelY() + 4, 18, 14).build()); + } + + @Override + public void tick() { + if (--refreshCooldown <= 0) requestPage(requestedPage); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float tickDelta) { + graphics.fill(0, 0, width, height, 0xCC101010); + int x = panelX(); + int y = panelY(); + graphics.fill(x + 3, y + 3, x + PANEL_WIDTH + 3, y + PANEL_HEIGHT + 3, 0xA0000000); + graphics.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, 0xFFC6C6C6); + graphics.outline(x, y, PANEL_WIDTH, PANEL_HEIGHT, 0xFF373737); + graphics.text(font, title, x + 8, y + 7, 0xFF202020, false); + + if (snapshot.status() == StorageTerminalStatus.READY) { + renderTerminalGrid(graphics, x, y, mouseX, mouseY); + } else { + graphics.centeredText(font, Component.translatable("screen.redstoner.storage_terminal.error." + + snapshot.status().name().toLowerCase(Locale.ROOT)), x + 90, y + 70, 0xFFAA3333); + } + renderPlayerInventory(graphics, x, y, mouseX, mouseY); + super.extractRenderState(graphics, mouseX, mouseY, tickDelta); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (snapshot.status() == StorageTerminalStatus.READY && scrollY != 0.0D && insidePanel(mouseX, mouseY)) { + requestPage(requestedPage + (scrollY < 0.0D ? 1 : -1)); + return true; + } + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); + } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { + if (super.mouseClicked(event, doubleClick)) return true; + if (snapshot.status() == StorageTerminalStatus.READY) { + int entryIndex = terminalEntryAt(event.x(), event.y()); + if (entryIndex >= 0 && entryIndex < snapshot.entries().size() + && (event.button() == 0 || event.button() == 1)) { + StorageTerminalSnapshotPayload.Entry entry = snapshot.entries().get(entryIndex); + send(event.button() == 1 ? StorageTerminalActionPayload.Action.TAKE_ONE + : StorageTerminalActionPayload.Action.TAKE_STACK, snapshot.page(), entryIndex, -1, entry.stack()); + return true; + } + if (insideScrollBar(event.x(), event.y()) && event.button() == 0) { + double ratio = Math.max(0.0D, Math.min(0.999D, + (event.y() - panelY() - GRID_Y) / (StorageTerminalRules.PAGE_SIZE / 9.0D * SLOT_SIZE))); + requestPage((int) Math.floor(ratio * snapshot.pageCount())); + return true; + } + int playerSlot = playerSlotAt(event.x(), event.y()); + if (playerSlot >= 0 && event.button() == 0 && event.hasShiftDown() && minecraft.player != null) { + ItemStack stack = minecraft.player.getInventory().getItem(playerSlot); + if (!stack.isEmpty()) { + send(StorageTerminalActionPayload.Action.DEPOSIT_SLOT, snapshot.page(), -1, playerSlot, stack); + return true; + } + } + } + return false; + } + + @Override + public boolean isPauseScreen() { + return false; + } + + private void renderTerminalGrid(GuiGraphicsExtractor graphics, int x, int y, int mouseX, int mouseY) { + for (int index = 0; index < StorageTerminalRules.PAGE_SIZE; index++) { + int slotX = x + GRID_X + index % 9 * SLOT_SIZE; + int slotY = y + GRID_Y + index / 9 * SLOT_SIZE; + drawSlot(graphics, slotX, slotY); + if (index >= snapshot.entries().size()) continue; + StorageTerminalSnapshotPayload.Entry entry = snapshot.entries().get(index); + graphics.item(entry.stack(), slotX + 1, slotY + 1); + graphics.itemDecorations(font, entry.stack(), slotX + 1, slotY + 1, compactCount(entry.total())); + if (inside(mouseX, mouseY, slotX, slotY, SLOT_SIZE, SLOT_SIZE)) { + graphics.setTooltipForNextFrame(font, entry.stack(), mouseX, mouseY); + } + } + + int trackX = x + SCROLL_X; + int trackY = y + GRID_Y; + int trackHeight = 6 * SLOT_SIZE; + graphics.fill(trackX, trackY, trackX + SCROLL_WIDTH, trackY + trackHeight, 0xFF575757); + int thumbHeight = snapshot.pageCount() <= 1 ? trackHeight : Math.max(10, trackHeight / snapshot.pageCount()); + int travel = trackHeight - thumbHeight; + int thumbY = trackY + (snapshot.pageCount() <= 1 ? 0 + : Math.round(travel * snapshot.page() / (float) (snapshot.pageCount() - 1))); + graphics.fill(trackX + 1, thumbY + 1, trackX + SCROLL_WIDTH - 1, thumbY + thumbHeight - 1, 0xFFD8D8D8); + graphics.text(font, Component.translatable("screen.redstoner.storage_terminal.summary", + snapshot.page() + 1, snapshot.pageCount(), snapshot.containerCount(), snapshot.totalEntries()), + x + 8, y + 131, 0xFF555555, false); + } + + private void renderPlayerInventory(GuiGraphicsExtractor graphics, int x, int y, int mouseX, int mouseY) { + graphics.text(font, Component.translatable("container.inventory"), x + 8, y + 141, 0xFF202020, false); + if (minecraft.player == null) return; + Inventory inventory = minecraft.player.getInventory(); + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) { + int inventorySlot = 9 + row * 9 + column; + drawPlayerSlot(graphics, inventory, inventorySlot, x + GRID_X + column * SLOT_SIZE, + y + PLAYER_GRID_Y + row * SLOT_SIZE, mouseX, mouseY); + } + } + for (int column = 0; column < 9; column++) { + drawPlayerSlot(graphics, inventory, column, x + GRID_X + column * SLOT_SIZE, + y + HOTBAR_Y, mouseX, mouseY); + } + } + + private void drawPlayerSlot(GuiGraphicsExtractor graphics, Inventory inventory, int inventorySlot, + int x, int y, int mouseX, int mouseY) { + drawSlot(graphics, x, y); + ItemStack stack = inventory.getItem(inventorySlot); + if (stack.isEmpty()) return; + graphics.item(stack, x + 1, y + 1); + graphics.itemDecorations(font, stack, x + 1, y + 1); + if (inside(mouseX, mouseY, x, y, SLOT_SIZE, SLOT_SIZE)) { + graphics.setTooltipForNextFrame(font, stack, mouseX, mouseY); + } + } + + private static void drawSlot(GuiGraphicsExtractor graphics, int x, int y) { + graphics.fill(x, y, x + SLOT_SIZE, y + SLOT_SIZE, 0xFF373737); + graphics.fill(x + 1, y + 1, x + SLOT_SIZE - 1, y + SLOT_SIZE - 1, 0xFF8B8B8B); + } + + private void requestPage(int page) { + requestedPage = Math.max(0, Math.min(snapshot.pageCount() - 1, page)); + refreshCooldown = REFRESH_TICKS; + send(StorageTerminalActionPayload.Action.REQUEST_PAGE, requestedPage, -1, -1, ItemStack.EMPTY); + } + + private void send(StorageTerminalActionPayload.Action action, int page, int entryIndex, + int playerSlot, ItemStack expected) { + if (ClientPlayNetworking.canSend(StorageTerminalActionPayload.TYPE)) { + ClientPlayNetworking.send(new StorageTerminalActionPayload( + snapshot.pos(), action, page, entryIndex, playerSlot, expected)); + } + } + + private int terminalEntryAt(double mouseX, double mouseY) { + int relativeX = (int) mouseX - panelX() - GRID_X; + int relativeY = (int) mouseY - panelY() - GRID_Y; + if (relativeX < 0 || relativeY < 0 || relativeX >= 9 * SLOT_SIZE || relativeY >= 6 * SLOT_SIZE) return -1; + return relativeY / SLOT_SIZE * 9 + relativeX / SLOT_SIZE; + } + + private int playerSlotAt(double mouseX, double mouseY) { + int relativeX = (int) mouseX - panelX() - GRID_X; + if (relativeX < 0 || relativeX >= 9 * SLOT_SIZE) return -1; + int column = relativeX / SLOT_SIZE; + int mainY = (int) mouseY - panelY() - PLAYER_GRID_Y; + if (mainY >= 0 && mainY < 3 * SLOT_SIZE) return 9 + mainY / SLOT_SIZE * 9 + column; + int hotbarY = (int) mouseY - panelY() - HOTBAR_Y; + return hotbarY >= 0 && hotbarY < SLOT_SIZE ? column : -1; + } + + private boolean insideScrollBar(double mouseX, double mouseY) { + return inside(mouseX, mouseY, panelX() + SCROLL_X, panelY() + GRID_Y, + SCROLL_WIDTH, 6 * SLOT_SIZE); + } + + private boolean insidePanel(double mouseX, double mouseY) { + return inside(mouseX, mouseY, panelX(), panelY(), PANEL_WIDTH, PANEL_HEIGHT); + } + + private static boolean inside(double mouseX, double mouseY, int x, int y, int width, int height) { + return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height; + } + + private int panelX() { + return (width - PANEL_WIDTH) / 2; + } + + private int panelY() { + return (height - PANEL_HEIGHT) / 2; + } + + private static String compactCount(int count) { + if (count >= 1_000_000) return (count / 1_000_000) + "m"; + if (count >= 10_000) return (count / 1_000) + "k"; + return Integer.toString(count); + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java index 38387b8..be09538 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java @@ -4,7 +4,11 @@ import fr.koka99cab.sanctuary26.redstoner.RedStonerMod; import fr.koka99cab.sanctuary26.redstoner.block.entity.ComputerBlockEntity; import fr.koka99cab.sanctuary26.redstoner.block.entity.PersonalComputerBlockEntity; import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries; +import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalNetwork; +import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalService; import fr.koka99cab.sanctuary26.sanctuary.api.atlas.SanctuaryAtlasLandmarks; +import java.util.List; +import net.minecraft.core.BlockPos; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.server.level.ServerPlayer; @@ -12,7 +16,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; public final class RedStonerNetworking { - public static final int PROTOCOL_VERSION = 5; + public static final int PROTOCOL_VERSION = 6; private static boolean initialized; private RedStonerNetworking() { @@ -25,6 +29,8 @@ public final class RedStonerNetworking { initialized = true; PayloadTypeRegistry.clientboundPlay().register(OpenComputerPayload.TYPE, OpenComputerPayload.STREAM_CODEC); PayloadTypeRegistry.clientboundPlay().register(OpenPersonalComputerPayload.TYPE, OpenPersonalComputerPayload.STREAM_CODEC); + PayloadTypeRegistry.clientboundPlay().register(StorageTerminalSnapshotPayload.TYPE, + StorageTerminalSnapshotPayload.STREAM_CODEC); PayloadTypeRegistry.serverboundPlay().register(SaveComputerProgramPayload.TYPE, SaveComputerProgramPayload.STREAM_CODEC); PayloadTypeRegistry.serverboundPlay().register(PlaceAtlasLandmarkPayload.TYPE, PlaceAtlasLandmarkPayload.STREAM_CODEC); PayloadTypeRegistry.serverboundPlay().register(RequestPersonalComputerSnapshotPayload.TYPE, @@ -33,6 +39,8 @@ public final class RedStonerNetworking { AcknowledgePersonalComputerIntroPayload.STREAM_CODEC); PayloadTypeRegistry.serverboundPlay().register(ShutdownPersonalComputerPayload.TYPE, ShutdownPersonalComputerPayload.STREAM_CODEC); + PayloadTypeRegistry.serverboundPlay().register(StorageTerminalActionPayload.TYPE, + StorageTerminalActionPayload.STREAM_CODEC); ServerPlayNetworking.registerGlobalReceiver(SaveComputerProgramPayload.TYPE, (payload, context) -> context.server().execute(() -> saveProgram(context.player(), payload))); ServerPlayNetworking.registerGlobalReceiver(PlaceAtlasLandmarkPayload.TYPE, (payload, context) -> @@ -43,6 +51,8 @@ public final class RedStonerNetworking { context.server().execute(() -> acknowledgePersonalComputerIntro(context.player(), payload))); ServerPlayNetworking.registerGlobalReceiver(ShutdownPersonalComputerPayload.TYPE, (payload, context) -> context.server().execute(() -> shutdownPersonalComputer(context.player(), payload))); + ServerPlayNetworking.registerGlobalReceiver(StorageTerminalActionPayload.TYPE, (payload, context) -> + context.server().execute(() -> storageTerminalAction(context.player(), payload))); RedStonerMod.LOGGER.debug("[{}] Networking contract v{} registered.", RedStonerMod.DISPLAY_NAME, PROTOCOL_VERSION); } @@ -77,6 +87,23 @@ public final class RedStonerNetworking { } } + public static void openStorageTerminal(ServerPlayer player, BlockPos pos) { + sendStorageTerminalSnapshot(player, pos, StorageTerminalService.snapshot(player, pos, 0)); + } + + private static void storageTerminalAction(ServerPlayer player, StorageTerminalActionPayload payload) { + sendStorageTerminalSnapshot(player, payload.pos(), StorageTerminalService.handle(player, payload)); + } + + private static void sendStorageTerminalSnapshot(ServerPlayer player, BlockPos pos, + StorageTerminalNetwork.Snapshot snapshot) { + if (!ServerPlayNetworking.canSend(player, StorageTerminalSnapshotPayload.TYPE)) return; + List entries = snapshot.entries().stream() + .map(entry -> new StorageTerminalSnapshotPayload.Entry(entry.stack(), entry.total())).toList(); + ServerPlayNetworking.send(player, new StorageTerminalSnapshotPayload(pos, snapshot.status(), snapshot.page(), + snapshot.pageCount(), snapshot.containerCount(), snapshot.totalEntries(), entries)); + } + private static void requestPersonalComputerSnapshot(ServerPlayer player, RequestPersonalComputerSnapshotPayload payload) { PersonalComputerBlockEntity computer = validatedPersonalComputer( diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/StorageTerminalActionPayload.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/StorageTerminalActionPayload.java new file mode 100644 index 0000000..9ea01fc --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/StorageTerminalActionPayload.java @@ -0,0 +1,56 @@ +package fr.koka99cab.sanctuary26.redstoner.network; + +import fr.koka99cab.sanctuary26.redstoner.RedStonerMod; +import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.item.ItemStack; + +public record StorageTerminalActionPayload(BlockPos pos, Action action, int page, int entryIndex, + int playerSlot, ItemStack expected) implements CustomPacketPayload { + public static final Type TYPE = + new Type<>(RedStonerMod.id("c2s/storage_terminal_action")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of(StorageTerminalActionPayload::write, StorageTerminalActionPayload::read); + + public StorageTerminalActionPayload { + pos = pos == null ? BlockPos.ZERO : pos.immutable(); + action = action == null ? Action.REQUEST_PAGE : action; + page = Math.max(0, Math.min(StorageTerminalRules.MAX_PAGE_COUNT - 1, page)); + entryIndex = entryIndex >= 0 && entryIndex < StorageTerminalRules.PAGE_SIZE ? entryIndex : -1; + playerSlot = playerSlot >= 0 && playerSlot < StorageTerminalRules.PLAYER_STORAGE_SLOTS ? playerSlot : -1; + expected = expected == null || expected.isEmpty() ? ItemStack.EMPTY : expected.copyWithCount(1); + } + + private static StorageTerminalActionPayload read(RegistryFriendlyByteBuf buffer) { + BlockPos pos = buffer.readBlockPos(); + int ordinal = buffer.readVarInt(); + Action[] values = Action.values(); + Action action = ordinal >= 0 && ordinal < values.length ? values[ordinal] : Action.REQUEST_PAGE; + return new StorageTerminalActionPayload(pos, action, buffer.readVarInt(), buffer.readVarInt(), + buffer.readVarInt(), ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC.decode(buffer)); + } + + private static void write(RegistryFriendlyByteBuf buffer, StorageTerminalActionPayload payload) { + buffer.writeBlockPos(payload.pos); + buffer.writeVarInt(payload.action.ordinal()); + buffer.writeVarInt(payload.page); + buffer.writeVarInt(payload.entryIndex); + buffer.writeVarInt(payload.playerSlot); + ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC.encode(buffer, payload.expected); + } + + @Override + public Type type() { + return TYPE; + } + + public enum Action { + REQUEST_PAGE, + TAKE_STACK, + TAKE_ONE, + DEPOSIT_SLOT + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/StorageTerminalSnapshotPayload.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/StorageTerminalSnapshotPayload.java new file mode 100644 index 0000000..3ecd55a --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/StorageTerminalSnapshotPayload.java @@ -0,0 +1,80 @@ +package fr.koka99cab.sanctuary26.redstoner.network; + +import fr.koka99cab.sanctuary26.redstoner.RedStonerMod; +import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules; +import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalStatus; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.item.ItemStack; + +public record StorageTerminalSnapshotPayload(BlockPos pos, StorageTerminalStatus status, int page, + int pageCount, int containerCount, int totalEntries, List entries) implements CustomPacketPayload { + public static final Type TYPE = + new Type<>(RedStonerMod.id("s2c/storage_terminal_snapshot")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of(StorageTerminalSnapshotPayload::write, StorageTerminalSnapshotPayload::read); + + public StorageTerminalSnapshotPayload { + pos = pos == null ? BlockPos.ZERO : pos.immutable(); + status = status == null ? StorageTerminalStatus.INVALID_TERMINAL : status; + pageCount = Math.max(1, Math.min(StorageTerminalRules.MAX_PAGE_COUNT, pageCount)); + page = Math.max(0, Math.min(pageCount - 1, page)); + containerCount = Math.max(0, Math.min(StorageTerminalRules.MAX_CONTAINERS, containerCount)); + totalEntries = Math.max(0, Math.min(StorageTerminalRules.MAX_ENTRIES, totalEntries)); + entries = entries == null ? List.of() : List.copyOf(entries.stream() + .limit(StorageTerminalRules.PAGE_SIZE).toList()); + } + + private static StorageTerminalSnapshotPayload read(RegistryFriendlyByteBuf buffer) { + BlockPos pos = buffer.readBlockPos(); + StorageTerminalStatus status = status(buffer.readVarInt()); + int page = buffer.readVarInt(); + int pageCount = buffer.readVarInt(); + int containerCount = buffer.readVarInt(); + int totalEntries = buffer.readVarInt(); + int size = buffer.readVarInt(); + if (size < 0 || size > StorageTerminalRules.PAGE_SIZE) { + throw new IllegalArgumentException("Storage terminal page exceeds " + StorageTerminalRules.PAGE_SIZE); + } + List entries = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + entries.add(new Entry(ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC.decode(buffer), buffer.readVarInt())); + } + return new StorageTerminalSnapshotPayload(pos, status, page, pageCount, containerCount, totalEntries, entries); + } + + private static void write(RegistryFriendlyByteBuf buffer, StorageTerminalSnapshotPayload payload) { + buffer.writeBlockPos(payload.pos); + buffer.writeVarInt(payload.status.ordinal()); + buffer.writeVarInt(payload.page); + buffer.writeVarInt(payload.pageCount); + buffer.writeVarInt(payload.containerCount); + buffer.writeVarInt(payload.totalEntries); + buffer.writeVarInt(payload.entries.size()); + for (Entry entry : payload.entries) { + ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC.encode(buffer, entry.stack); + buffer.writeVarInt(entry.total); + } + } + + private static StorageTerminalStatus status(int ordinal) { + StorageTerminalStatus[] values = StorageTerminalStatus.values(); + return values[Math.max(0, Math.min(values.length - 1, ordinal))]; + } + + @Override + public Type type() { + return TYPE; + } + + public record Entry(ItemStack stack, int total) { + public Entry { + stack = stack == null || stack.isEmpty() ? ItemStack.EMPTY : stack.copyWithCount(1); + total = stack.isEmpty() ? 0 : Math.max(1, total); + } + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java index 6741601..1d7d069 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java @@ -5,6 +5,7 @@ import fr.koka99cab.sanctuary26.redstoner.block.ComputerBlock; import fr.koka99cab.sanctuary26.redstoner.block.DrawerBlock; import fr.koka99cab.sanctuary26.redstoner.block.PersonalComputerBlock; import fr.koka99cab.sanctuary26.redstoner.block.ParticuleurBlock; +import fr.koka99cab.sanctuary26.redstoner.block.StorageTerminalBlock; import fr.koka99cab.sanctuary26.redstoner.block.entity.ComputerBlockEntity; import fr.koka99cab.sanctuary26.redstoner.block.entity.DrawerBlockEntity; import fr.koka99cab.sanctuary26.redstoner.block.entity.PersonalComputerBlockEntity; @@ -64,6 +65,12 @@ public final class RedStonerRegistries { ParticuleurBlock::new, particuleurProperties(RedStonerMod.id("particuleur")) ); + public static final StorageTerminalBlock STORAGE_TERMINAL = registerBlock( + RedStonerMod.id("storage_terminal"), + StorageTerminalBlock::new, + BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(4.0F, 6.0F).sound(SoundType.METAL) + .requiresCorrectToolForDrops().noOcclusion().setId(blockKey(RedStonerMod.id("storage_terminal"))) + ); public static final ComputerBlock LEGACY_EIGHT_BIT_COMPUTER = registerBlock( LEGACY_COMPUTER_ID, ComputerBlock::new, computerProperties(LEGACY_COMPUTER_ID, MapColor.COLOR_RED)); @@ -83,6 +90,8 @@ public final class RedStonerRegistries { public static final Item PERSONAL_COMPUTER_ITEM = registerBlockItem(RedStonerMod.id("personal_computer"), PERSONAL_COMPUTER); public static final Item DRAWER_ITEM = registerBlockItem(RedStonerMod.id("drawer"), DRAWER); public static final Item PARTICULEUR_ITEM = registerBlockItem(RedStonerMod.id("particuleur"), PARTICULEUR); + public static final Item STORAGE_TERMINAL_ITEM = registerBlockItem( + RedStonerMod.id("storage_terminal"), STORAGE_TERMINAL); public static final Item LEGACY_EIGHT_BIT_COMPUTER_ITEM = registerBlockItem(LEGACY_COMPUTER_ID, LEGACY_EIGHT_BIT_COMPUTER); public static final Item LEGACY_CONTROLLER_ITEM = registerBlockItem(LEGACY_CONTROLLER_ID, LEGACY_CONTROLLER); @@ -117,6 +126,7 @@ public final class RedStonerRegistries { output.accept(EIGHT_BIT_COMPUTER_ITEM); output.accept(CONTROLLER_ITEM); output.accept(PARTICULEUR_ITEM); + output.accept(STORAGE_TERMINAL_ITEM); }) .build() ); @@ -132,7 +142,7 @@ public final class RedStonerRegistries { } initialized = true; registerDrawerAttack(); - RedStonerMod.LOGGER.info("[{}] Registered five active blocks and five legacy block aliases.", RedStonerMod.DISPLAY_NAME); + RedStonerMod.LOGGER.info("[{}] Registered six active blocks and five legacy block aliases.", RedStonerMod.DISPLAY_NAME); } public static boolean isController(BlockState state) { diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalInventory.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalInventory.java new file mode 100644 index 0000000..e915000 --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalInventory.java @@ -0,0 +1,118 @@ +package fr.koka99cab.sanctuary26.redstoner.storage; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; + +public final class StorageTerminalInventory { + private StorageTerminalInventory() { + } + + public static List aggregate(List containers) { + Map grouped = new LinkedHashMap<>(); + for (Container container : containers) { + for (int slot = 0; slot < container.getContainerSize(); slot++) { + ItemStack stack = container.getItem(slot); + if (stack.isEmpty()) continue; + StackKey key = new StackKey(stack); + MutableEntry entry = grouped.computeIfAbsent(key, ignored -> new MutableEntry(key.stack)); + entry.total = saturatedAdd(entry.total, stack.getCount()); + } + } + List result = new ArrayList<>(grouped.size()); + for (MutableEntry entry : grouped.values()) result.add(new Entry(entry.stack, entry.total)); + return List.copyOf(result); + } + + /** Inserts into compatible existing stacks first, then into empty slots. Mutates {@code source}. */ + public static int insert(List containers, ItemStack source) { + if (source == null || source.isEmpty()) return 0; + int before = source.getCount(); + for (Container container : containers) { + for (int slot = 0; slot < container.getContainerSize() && !source.isEmpty(); slot++) { + ItemStack target = container.getItem(slot); + if (target.isEmpty() || !ItemStack.isSameItemSameComponents(target, source) + || !container.canPlaceItem(slot, source)) continue; + int limit = Math.min(container.getMaxStackSize(source), source.getMaxStackSize()); + int moved = Math.min(source.getCount(), Math.max(0, limit - target.getCount())); + if (moved <= 0) continue; + target.grow(moved); + source.shrink(moved); + container.setChanged(); + } + } + for (Container container : containers) { + for (int slot = 0; slot < container.getContainerSize() && !source.isEmpty(); slot++) { + if (!container.getItem(slot).isEmpty() || !container.canPlaceItem(slot, source)) continue; + int limit = Math.min(container.getMaxStackSize(source), source.getMaxStackSize()); + int moved = Math.min(source.getCount(), Math.max(0, limit)); + if (moved <= 0) continue; + container.setItem(slot, source.copyWithCount(moved)); + source.shrink(moved); + container.setChanged(); + } + } + return before - source.getCount(); + } + + public static ItemStack take(List containers, ItemStack expected, int requested) { + if (expected == null || expected.isEmpty() || requested <= 0) return ItemStack.EMPTY; + int remaining = requested; + int total = 0; + for (Container container : containers) { + for (int slot = 0; slot < container.getContainerSize() && remaining > 0; slot++) { + ItemStack stored = container.getItem(slot); + if (stored.isEmpty() || !ItemStack.isSameItemSameComponents(stored, expected)) continue; + ItemStack removed = container.removeItem(slot, Math.min(remaining, stored.getCount())); + if (removed.isEmpty()) continue; + total += removed.getCount(); + remaining -= removed.getCount(); + container.setChanged(); + } + } + return total <= 0 ? ItemStack.EMPTY : expected.copyWithCount(total); + } + + private static int saturatedAdd(int left, int right) { + return (int) Math.min(Integer.MAX_VALUE, (long) left + Math.max(0, right)); + } + + public record Entry(ItemStack stack, int total) { + public Entry { + stack = stack == null || stack.isEmpty() ? ItemStack.EMPTY : stack.copyWithCount(1); + total = stack.isEmpty() ? 0 : Math.max(1, total); + } + } + + private static final class MutableEntry { + private final ItemStack stack; + private int total; + + private MutableEntry(ItemStack stack) { + this.stack = stack.copyWithCount(1); + } + } + + private static final class StackKey { + private final ItemStack stack; + private final int hash; + + private StackKey(ItemStack stack) { + this.stack = stack.copyWithCount(1); + this.hash = ItemStack.hashItemAndComponents(this.stack); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object other) { + return other instanceof StackKey key && ItemStack.isSameItemSameComponents(stack, key.stack); + } + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalNetwork.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalNetwork.java new file mode 100644 index 0000000..9a9b3cc --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalNetwork.java @@ -0,0 +1,92 @@ +package fr.koka99cab.sanctuary26.redstoner.storage; + +import fr.koka99cab.sanctuary26.redstoner.block.StorageTerminalBlock; +import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BarrelBlockEntity; +import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +public final class StorageTerminalNetwork { + private StorageTerminalNetwork() { + } + + public static boolean isCompatibleStorage(BlockState state) { + return state.is(Blocks.CHEST) || state.is(Blocks.TRAPPED_CHEST) || state.is(Blocks.BARREL); + } + + public static Discovery discover(ServerPlayer player, BlockPos terminalPos) { + if (!(player.level() instanceof ServerLevel level)) return Discovery.error(StorageTerminalStatus.INVALID_TERMINAL); + BlockState terminalState = level.getBlockState(terminalPos); + if (!terminalState.is(RedStonerRegistries.STORAGE_TERMINAL) + || !terminalState.hasProperty(StorageTerminalBlock.FACING)) { + return Discovery.error(StorageTerminalStatus.INVALID_TERMINAL); + } + BlockPos start = terminalPos.relative(terminalState.getValue(StorageTerminalBlock.FACING).getOpposite()); + StorageTerminalTopology.Result topology = StorageTerminalTopology.discover(start, pos -> + level.hasChunkAt(pos) && isCompatibleStorage(level.getBlockState(pos)) && storageEntity(level, pos) != null); + if (topology.tooLarge()) return Discovery.error(StorageTerminalStatus.TOO_LARGE); + if (topology.positions().isEmpty()) return Discovery.error(StorageTerminalStatus.DISCONNECTED); + + List containers = new ArrayList<>(topology.positions().size()); + for (BlockPos pos : topology.positions()) { + BaseContainerBlockEntity container = storageEntity(level, pos); + if (container == null) return Discovery.error(StorageTerminalStatus.DISCONNECTED); + if (container.isLocked()) return Discovery.error(StorageTerminalStatus.LOCKED); + if (container instanceof RandomizableContainer randomizable) randomizable.unpackLootTable(player); + containers.add(container); + } + return new Discovery(StorageTerminalStatus.READY, containers); + } + + public static Snapshot snapshot(Discovery discovery, int requestedPage) { + if (discovery.status() != StorageTerminalStatus.READY) { + return new Snapshot(discovery.status(), 0, 1, 0, 0, List.of()); + } + List all = StorageTerminalInventory.aggregate(discovery.containers()); + int page = StorageTerminalRules.clampPage(requestedPage, all.size()); + int start = StorageTerminalRules.pageStart(page, all.size()); + int end = Math.min(all.size(), start + StorageTerminalRules.PAGE_SIZE); + return new Snapshot(StorageTerminalStatus.READY, page, StorageTerminalRules.pageCount(all.size()), + discovery.containers().size(), all.size(), all.subList(start, end)); + } + + private static BaseContainerBlockEntity storageEntity(ServerLevel level, BlockPos pos) { + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof ChestBlockEntity chest) return chest; + if (blockEntity instanceof BarrelBlockEntity barrel) return barrel; + return null; + } + + public record Discovery(StorageTerminalStatus status, List containers) { + public Discovery { + status = status == null ? StorageTerminalStatus.INVALID_TERMINAL : status; + containers = containers == null ? List.of() : List.copyOf(containers); + } + + public static Discovery error(StorageTerminalStatus status) { + return new Discovery(status, List.of()); + } + + public List inventoryContainers() { + return containers; + } + } + + public record Snapshot(StorageTerminalStatus status, int page, int pageCount, int containerCount, + int totalEntries, List entries) { + public Snapshot { + status = status == null ? StorageTerminalStatus.INVALID_TERMINAL : status; + entries = entries == null ? List.of() : List.copyOf(entries); + } + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalRules.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalRules.java new file mode 100644 index 0000000..4185f3e --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalRules.java @@ -0,0 +1,26 @@ +package fr.koka99cab.sanctuary26.redstoner.storage; + +public final class StorageTerminalRules { + public static final int PAGE_SIZE = 54; + public static final int MAX_CONTAINERS = 128; + public static final int SLOTS_PER_CONTAINER = 27; + public static final int MAX_ENTRIES = MAX_CONTAINERS * SLOTS_PER_CONTAINER; + public static final int MAX_PAGE_COUNT = MAX_ENTRIES / PAGE_SIZE; + public static final int PLAYER_STORAGE_SLOTS = 36; + + private StorageTerminalRules() { + } + + public static int pageCount(int entryCount) { + int bounded = Math.max(0, Math.min(MAX_ENTRIES, entryCount)); + return Math.max(1, (bounded + PAGE_SIZE - 1) / PAGE_SIZE); + } + + public static int clampPage(int requestedPage, int entryCount) { + return Math.max(0, Math.min(pageCount(entryCount) - 1, requestedPage)); + } + + public static int pageStart(int requestedPage, int entryCount) { + return clampPage(requestedPage, entryCount) * PAGE_SIZE; + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalService.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalService.java new file mode 100644 index 0000000..532f5bc --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalService.java @@ -0,0 +1,109 @@ +package fr.koka99cab.sanctuary26.redstoner.storage; + +import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalActionPayload; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +public final class StorageTerminalService { + private static final double MAX_DISTANCE_SQUARED = 64.0D; + + private StorageTerminalService() { + } + + public static StorageTerminalNetwork.Snapshot snapshot(ServerPlayer player, BlockPos pos, int page) { + if (player.distanceToSqr(Vec3.atCenterOf(pos)) > MAX_DISTANCE_SQUARED) { + return StorageTerminalNetwork.snapshot( + StorageTerminalNetwork.Discovery.error(StorageTerminalStatus.INVALID_TERMINAL), 0); + } + return StorageTerminalNetwork.snapshot(StorageTerminalNetwork.discover(player, pos), page); + } + + public static StorageTerminalNetwork.Snapshot handle(ServerPlayer player, StorageTerminalActionPayload payload) { + if (player.distanceToSqr(Vec3.atCenterOf(payload.pos())) > MAX_DISTANCE_SQUARED) { + return StorageTerminalNetwork.snapshot( + StorageTerminalNetwork.Discovery.error(StorageTerminalStatus.INVALID_TERMINAL), 0); + } + StorageTerminalNetwork.Discovery discovery = StorageTerminalNetwork.discover(player, payload.pos()); + StorageTerminalNetwork.Snapshot before = StorageTerminalNetwork.snapshot(discovery, payload.page()); + if (before.status() != StorageTerminalStatus.READY + || payload.action() == StorageTerminalActionPayload.Action.REQUEST_PAGE) return before; + + boolean changed = switch (payload.action()) { + case TAKE_STACK -> take(player, discovery, before, payload, false); + case TAKE_ONE -> take(player, discovery, before, payload, true); + case DEPOSIT_SLOT -> deposit(player, discovery, payload); + case REQUEST_PAGE -> false; + }; + if (changed) { + player.getInventory().setChanged(); + player.containerMenu.broadcastChanges(); + player.inventoryMenu.broadcastChanges(); + } + return StorageTerminalNetwork.snapshot(discovery, before.page()); + } + + private static boolean take(ServerPlayer player, StorageTerminalNetwork.Discovery discovery, + StorageTerminalNetwork.Snapshot snapshot, StorageTerminalActionPayload payload, boolean single) { + if (payload.entryIndex() < 0 || payload.entryIndex() >= snapshot.entries().size()) return false; + StorageTerminalInventory.Entry entry = snapshot.entries().get(payload.entryIndex()); + if (payload.expected().isEmpty() + || !ItemStack.isSameItemSameComponents(entry.stack(), payload.expected())) return false; + int capacity = playerCapacity(player.getInventory(), entry.stack()); + int requested = single ? 1 : entry.stack().getMaxStackSize(); + int amount = Math.min(entry.total(), Math.min(capacity, requested)); + if (amount <= 0) return false; + ItemStack extracted = StorageTerminalInventory.take(discovery.inventoryContainers(), entry.stack(), amount); + if (extracted.isEmpty()) return false; + int inserted = insertPlayer(player.getInventory(), extracted); + if (!extracted.isEmpty()) StorageTerminalInventory.insert(discovery.inventoryContainers(), extracted); + return inserted > 0; + } + + private static boolean deposit(ServerPlayer player, StorageTerminalNetwork.Discovery discovery, + StorageTerminalActionPayload payload) { + int slot = payload.playerSlot(); + if (slot < 0 || slot >= StorageTerminalRules.PLAYER_STORAGE_SLOTS) return false; + Inventory inventory = player.getInventory(); + ItemStack source = inventory.getItem(slot); + if (source.isEmpty() || payload.expected().isEmpty() + || !ItemStack.isSameItemSameComponents(source, payload.expected())) return false; + int inserted = StorageTerminalInventory.insert(discovery.inventoryContainers(), source); + if (source.isEmpty()) inventory.setItem(slot, ItemStack.EMPTY); + return inserted > 0; + } + + private static int playerCapacity(Inventory inventory, ItemStack expected) { + long capacity = 0L; + for (int slot = 0; slot < StorageTerminalRules.PLAYER_STORAGE_SLOTS; slot++) { + ItemStack stack = inventory.getItem(slot); + if (stack.isEmpty()) capacity += expected.getMaxStackSize(); + else if (ItemStack.isSameItemSameComponents(stack, expected)) { + capacity += Math.max(0, stack.getMaxStackSize() - stack.getCount()); + } + } + return (int) Math.min(Integer.MAX_VALUE, capacity); + } + + /** Inserts into the 36 survival storage slots and mutates {@code source}. */ + private static int insertPlayer(Inventory inventory, ItemStack source) { + int before = source.getCount(); + for (int slot = 0; slot < StorageTerminalRules.PLAYER_STORAGE_SLOTS && !source.isEmpty(); slot++) { + ItemStack target = inventory.getItem(slot); + if (target.isEmpty() || !ItemStack.isSameItemSameComponents(target, source)) continue; + int moved = Math.min(source.getCount(), Math.max(0, target.getMaxStackSize() - target.getCount())); + if (moved <= 0) continue; + target.grow(moved); + source.shrink(moved); + } + for (int slot = 0; slot < StorageTerminalRules.PLAYER_STORAGE_SLOTS && !source.isEmpty(); slot++) { + if (!inventory.getItem(slot).isEmpty()) continue; + int moved = Math.min(source.getCount(), source.getMaxStackSize()); + inventory.setItem(slot, source.copyWithCount(moved)); + source.shrink(moved); + } + return before - source.getCount(); + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalStatus.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalStatus.java new file mode 100644 index 0000000..8403c60 --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalStatus.java @@ -0,0 +1,9 @@ +package fr.koka99cab.sanctuary26.redstoner.storage; + +public enum StorageTerminalStatus { + READY, + INVALID_TERMINAL, + DISCONNECTED, + TOO_LARGE, + LOCKED +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalTopology.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalTopology.java new file mode 100644 index 0000000..7c8fac3 --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalTopology.java @@ -0,0 +1,44 @@ +package fr.koka99cab.sanctuary26.redstoner.storage; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; + +public final class StorageTerminalTopology { + private StorageTerminalTopology() { + } + + public static Result discover(BlockPos start, Predicate storageAt) { + if (start == null || storageAt == null) return new Result(List.of(), false); + ArrayDeque pending = new ArrayDeque<>(); + Set visited = new HashSet<>(); + List storage = new ArrayList<>(); + pending.add(start.immutable()); + + while (!pending.isEmpty()) { + BlockPos pos = pending.removeFirst(); + if (!visited.add(pos) || !storageAt.test(pos)) continue; + if (storage.size() >= StorageTerminalRules.MAX_CONTAINERS) { + return new Result(storage, true); + } + storage.add(pos); + for (Direction direction : Direction.values()) { + BlockPos neighbor = pos.relative(direction); + if (!visited.contains(neighbor)) pending.addLast(neighbor.immutable()); + } + } + + return new Result(storage, false); + } + + public record Result(List positions, boolean tooLarge) { + public Result { + positions = positions == null ? List.of() : List.copyOf(positions); + } + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/blockstates/storage_terminal.json b/redstoner/src/main/resources/assets/redstoner/blockstates/storage_terminal.json new file mode 100644 index 0000000..b859060 --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/blockstates/storage_terminal.json @@ -0,0 +1,8 @@ +{ + "variants": { + "facing=north": { "model": "redstoner:block/storage_terminal" }, + "facing=east": { "model": "redstoner:block/storage_terminal", "y": 90 }, + "facing=south": { "model": "redstoner:block/storage_terminal", "y": 180 }, + "facing=west": { "model": "redstoner:block/storage_terminal", "y": 270 } + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/items/storage_terminal.json b/redstoner/src/main/resources/assets/redstoner/items/storage_terminal.json new file mode 100644 index 0000000..c5b1174 --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/items/storage_terminal.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "redstoner:block/storage_terminal" + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/lang/en_us.json b/redstoner/src/main/resources/assets/redstoner/lang/en_us.json index ed33531..60fb008 100644 --- a/redstoner/src/main/resources/assets/redstoner/lang/en_us.json +++ b/redstoner/src/main/resources/assets/redstoner/lang/en_us.json @@ -49,5 +49,12 @@ "message.redstoner.computer.invalid": "Program rejected: %s", "block.redstoner.particuleur": "Particle Emitter", "block.particuleur.particuleur": "Legacy Particle Emitter", - "tooltip.redstoner.particuleur": "Insert a catalyst, then power the block with redstone." + "tooltip.redstoner.particuleur": "Insert a catalyst, then power the block with redstone.", + "block.redstoner.storage_terminal": "Storage Terminal", + "screen.redstoner.storage_terminal.title": "Storage Terminal", + "screen.redstoner.storage_terminal.summary": "%s/%s · %s blocks · %s types", + "screen.redstoner.storage_terminal.error.invalid_terminal": "The terminal is no longer accessible.", + "screen.redstoner.storage_terminal.error.disconnected": "No connected chest or barrel.", + "screen.redstoner.storage_terminal.error.too_large": "Network too large (maximum 128 storage blocks).", + "screen.redstoner.storage_terminal.error.locked": "The network contains a locked storage block." } diff --git a/redstoner/src/main/resources/assets/redstoner/lang/fr_fr.json b/redstoner/src/main/resources/assets/redstoner/lang/fr_fr.json index a979d19..77b1801 100644 --- a/redstoner/src/main/resources/assets/redstoner/lang/fr_fr.json +++ b/redstoner/src/main/resources/assets/redstoner/lang/fr_fr.json @@ -49,5 +49,12 @@ "message.redstoner.computer.invalid": "Programme refusé : %s", "block.redstoner.particuleur": "Particuleur", "block.particuleur.particuleur": "Particuleur historique", - "tooltip.redstoner.particuleur": "Insérez un catalyseur puis alimentez le bloc en redstone." + "tooltip.redstoner.particuleur": "Insérez un catalyseur puis alimentez le bloc en redstone.", + "block.redstoner.storage_terminal": "Terminal de stockage", + "screen.redstoner.storage_terminal.title": "Terminal de stockage", + "screen.redstoner.storage_terminal.summary": "%s/%s · %s blocs · %s types", + "screen.redstoner.storage_terminal.error.invalid_terminal": "Le terminal n'est plus accessible.", + "screen.redstoner.storage_terminal.error.disconnected": "Aucun coffre ou baril connecté.", + "screen.redstoner.storage_terminal.error.too_large": "Réseau trop grand (128 stockages maximum).", + "screen.redstoner.storage_terminal.error.locked": "Le réseau contient un stockage verrouillé." } diff --git a/redstoner/src/main/resources/assets/redstoner/lang/ru_ru.json b/redstoner/src/main/resources/assets/redstoner/lang/ru_ru.json index 3469af0..d6dbc2f 100644 --- a/redstoner/src/main/resources/assets/redstoner/lang/ru_ru.json +++ b/redstoner/src/main/resources/assets/redstoner/lang/ru_ru.json @@ -49,5 +49,12 @@ "message.redstoner.computer.invalid": "Программа отклонена: %s", "block.redstoner.particuleur": "Генератор частиц", "block.particuleur.particuleur": "Старый генератор частиц", - "tooltip.redstoner.particuleur": "Вставьте катализатор и подайте сигнал редстоуна." + "tooltip.redstoner.particuleur": "Вставьте катализатор и подайте сигнал редстоуна.", + "block.redstoner.storage_terminal": "Терминал хранилища", + "screen.redstoner.storage_terminal.title": "Терминал хранилища", + "screen.redstoner.storage_terminal.summary": "%s/%s · блоков: %s · типов: %s", + "screen.redstoner.storage_terminal.error.invalid_terminal": "Терминал больше недоступен.", + "screen.redstoner.storage_terminal.error.disconnected": "Нет подключённых сундуков или бочек.", + "screen.redstoner.storage_terminal.error.too_large": "Сеть слишком велика (не более 128 хранилищ).", + "screen.redstoner.storage_terminal.error.locked": "В сети есть запертое хранилище." } diff --git a/redstoner/src/main/resources/assets/redstoner/models/block/storage_terminal.json b/redstoner/src/main/resources/assets/redstoner/models/block/storage_terminal.json new file mode 100644 index 0000000..fc78d7c --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/models/block/storage_terminal.json @@ -0,0 +1,25 @@ +{ + "parent": "minecraft:block/block", + "textures": { + "particle": "redstoner:block/redstone_controller_front", + "front": "redstoner:block/redstone_controller_front", + "back": "redstoner:block/redstone_controller_back", + "side": "redstoner:block/redstone_controller_right", + "top": "redstoner:block/redstone_controller_top", + "bottom": "redstoner:block/redstone_controller_bottom" + }, + "elements": [ + { + "from": [0, 0, 8], + "to": [16, 16, 16], + "faces": { + "north": { "uv": [0, 0, 16, 16], "texture": "#front" }, + "south": { "uv": [0, 0, 16, 16], "texture": "#back" }, + "west": { "uv": [8, 0, 16, 16], "texture": "#side" }, + "east": { "uv": [0, 0, 8, 16], "texture": "#side" }, + "up": { "uv": [0, 8, 16, 16], "texture": "#top" }, + "down": { "uv": [0, 0, 16, 8], "texture": "#bottom" } + } + } + ] +} diff --git a/redstoner/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json b/redstoner/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json index 4b60656..3ae4246 100644 --- a/redstoner/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json +++ b/redstoner/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -1 +1 @@ -{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]} +{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstoner:storage_terminal","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]} diff --git a/redstoner/src/main/resources/data/minecraft/tags/block/needs_stone_tool.json b/redstoner/src/main/resources/data/minecraft/tags/block/needs_stone_tool.json index 4b60656..3ae4246 100644 --- a/redstoner/src/main/resources/data/minecraft/tags/block/needs_stone_tool.json +++ b/redstoner/src/main/resources/data/minecraft/tags/block/needs_stone_tool.json @@ -1 +1 @@ -{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]} +{"replace":false,"values":["redstoner:eight_bit_computer","redstoner:controller","redstoner:personal_computer","redstoner:particuleur","redstoner:storage_terminal","redstone_computer:redstone_computer","redstone_computer:redstone_controller","redstone_computer:personal_computer","particuleur:particuleur"]} diff --git a/redstoner/src/main/resources/data/redstoner/loot_table/blocks/storage_terminal.json b/redstoner/src/main/resources/data/redstoner/loot_table/blocks/storage_terminal.json new file mode 100644 index 0000000..38b7a7d --- /dev/null +++ b/redstoner/src/main/resources/data/redstoner/loot_table/blocks/storage_terminal.json @@ -0,0 +1,20 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "redstoner:storage_terminal" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ], + "random_sequence": "redstoner:blocks/storage_terminal" +} diff --git a/redstoner/src/main/resources/data/redstoner/recipe/storage_terminal.json b/redstoner/src/main/resources/data/redstoner/recipe/storage_terminal.json new file mode 100644 index 0000000..c5a3f5e --- /dev/null +++ b/redstoner/src/main/resources/data/redstoner/recipe/storage_terminal.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "redstone", + "pattern": [ + "PGP", + "GCG", + "PTP" + ], + "key": { + "P": "anotherworld:silver_plate", + "G": "minecraft:tinted_glass", + "C": "redstoner:controller", + "T": "anotherworld:titanium_ingot" + }, + "result": { + "id": "redstoner:storage_terminal", + "count": 1 + } +} diff --git a/redstoner/src/main/resources/fabric.mod.json b/redstoner/src/main/resources/fabric.mod.json index 8604e3c..d1ca570 100644 --- a/redstoner/src/main/resources/fabric.mod.json +++ b/redstoner/src/main/resources/fabric.mod.json @@ -31,7 +31,7 @@ "system_type": "gameplay-system", "namespace": "redstoner", "data_version": 3, - "network_protocol": 5, + "network_protocol": 6, "lifecycle": "${lifecycle}", "pack_version": "${pack_version}", "descriptions": { diff --git a/redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalSmoke.java b/redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalSmoke.java new file mode 100644 index 0000000..a903de6 --- /dev/null +++ b/redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalSmoke.java @@ -0,0 +1,103 @@ +package fr.koka99cab.sanctuary26.redstoner.storage; + +import fr.koka99cab.sanctuary26.redstoner.network.StorageTerminalActionPayload; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +public final class StorageTerminalSmoke { + private StorageTerminalSmoke() { + } + + public static void main(String[] args) { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + verifyTopology(); + verifyPagination(); + verifyActionBounds(); + verifyAggregationAndTransfers(); + } + + private static void verifyTopology() { + Set line = new HashSet<>(); + for (int x = 0; x < StorageTerminalRules.MAX_CONTAINERS; x++) line.add(new BlockPos(x, 0, 0)); + StorageTerminalTopology.Result exact = StorageTerminalTopology.discover(BlockPos.ZERO, line::contains); + require(!exact.tooLarge() && exact.positions().size() == StorageTerminalRules.MAX_CONTAINERS, + "An exact 128-container network was rejected"); + + line.add(new BlockPos(StorageTerminalRules.MAX_CONTAINERS, 0, 0)); + StorageTerminalTopology.Result oversized = StorageTerminalTopology.discover(BlockPos.ZERO, line::contains); + require(oversized.tooLarge() && oversized.positions().size() == StorageTerminalRules.MAX_CONTAINERS, + "A 129-container network was not bounded"); + + Set loaded = Set.of(BlockPos.ZERO, new BlockPos(0, 1, 0), new BlockPos(0, 2, 0), + new BlockPos(20, 0, 0)); + StorageTerminalTopology.Result bounded = StorageTerminalTopology.discover(BlockPos.ZERO, loaded::contains); + require(!bounded.tooLarge() && bounded.positions().size() == 3 + && !bounded.positions().contains(new BlockPos(20, 0, 0)), + "Topology crossed a disconnected or unloaded gap"); + } + + private static void verifyPagination() { + require(StorageTerminalRules.pageCount(0) == 1, "An empty terminal must keep one page"); + require(StorageTerminalRules.pageCount(54) == 1, "A full visible page overflowed"); + require(StorageTerminalRules.pageCount(55) == 2, "The second page did not open"); + require(StorageTerminalRules.clampPage(999, 55) == 1, "A hostile page index was not clamped"); + } + + private static void verifyActionBounds() { + StorageTerminalActionPayload hostile = new StorageTerminalActionPayload(BlockPos.ZERO, + StorageTerminalActionPayload.Action.DEPOSIT_SLOT, -5, 999, 999, ItemStack.EMPTY); + require(hostile.page() == 0 && hostile.entryIndex() == -1 && hostile.playerSlot() == -1, + "Hostile terminal action indexes were mapped to valid slots"); + StorageTerminalActionPayload missingAction = new StorageTerminalActionPayload( + BlockPos.ZERO, null, 0, -1, -1, ItemStack.EMPTY); + require(missingAction.action() == StorageTerminalActionPayload.Action.REQUEST_PAGE, + "A missing terminal action did not fall back to a read-only request"); + } + + private static void verifyAggregationAndTransfers() { + SimpleContainer first = new SimpleContainer(27); + SimpleContainer second = new SimpleContainer(27); + Holder cobblestone = new Holder.Direct<>(Items.COBBLESTONE, DataComponentMap.builder() + .set(DataComponents.MAX_STACK_SIZE, 64).build()); + first.setItem(0, new ItemStack(cobblestone, 60)); + ItemStack named = new ItemStack(cobblestone, 5); + named.set(DataComponents.CUSTOM_NAME, Component.literal("Distinct")); + second.setItem(3, named); + + ItemStack deposit = new ItemStack(cobblestone, 10); + int inserted = StorageTerminalInventory.insert(List.of(first, second), deposit); + require(inserted == 10 && deposit.isEmpty(), "Terminal deposit lost or retained items"); + require(first.getItem(0).getCount() == 64 && first.getItem(1).getCount() == 6, + "Terminal deposit did not fill matching stacks before empty slots"); + + List entries = StorageTerminalInventory.aggregate(List.of(first, second)); + require(entries.size() == 2, "Items with distinct components were merged"); + StorageTerminalInventory.Entry plain = entries.stream() + .filter(entry -> !entry.stack().has(DataComponents.CUSTOM_NAME)).findFirst().orElseThrow(); + require(plain.total() == 70, "Aggregated terminal quantity is incorrect"); + + ItemStack taken = StorageTerminalInventory.take(List.of(first, second), plain.stack(), 64); + require(taken.getCount() == 64 && ItemStack.isSameItemSameComponents(taken, plain.stack()), + "Terminal extraction changed item identity or quantity"); + List remaining = StorageTerminalInventory.aggregate(List.of(first, second)); + require(remaining.stream().filter(entry -> !entry.stack().has(DataComponents.CUSTOM_NAME)) + .findFirst().orElseThrow().total() == 6, "Terminal extraction did not update physical slots"); + } + + private static void require(boolean condition, String message) { + if (!condition) throw new IllegalStateException(message); + } +} diff --git a/sanctuary/build.gradle b/sanctuary/build.gradle index c39a357..5f83cc5 100644 --- a/sanctuary/build.gradle +++ b/sanctuary/build.gradle @@ -121,7 +121,7 @@ tasks.register("verifyBlackMarket") { def questData = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardData.java").text def quests = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java").text def inventoryView = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java").text - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || !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") @@ -2818,7 +2818,7 @@ tasks.register("verifyShopCurrencyRowsRelease") { "src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java").text def shop = file( "src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || 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 @@ -2984,7 +2984,7 @@ tasks.register("verifyProgressivePauseMenu") { "pack/migrations/26.2.0-alpha.190-progressive-pause-menu-to-alpha.191.json")) def pauseMenu = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/PauseScreenMixin.java").text def costs = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/progression/SanctuaryProgressionCosts.java").text - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || 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 @@ -3022,7 +3022,7 @@ tasks.register("verifyShopPricingLayout") { "pack/migrations/26.2.0-alpha.192-shop-pricing-layout-to-alpha.193.json")) def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryShopScreen.java").text def progression = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopProgression.java").text - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || 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] @@ -3053,7 +3053,7 @@ tasks.register("verifyShopCsvBossGates") { "pack/migrations/26.2.0-alpha.194-shop-csv-boss-gates-to-alpha.195.json")) def provider = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopCatalogProvider.java").text def catalog = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || migration.target?.pack_version != "26.2.0-alpha.195" || migration.target?.modules?.sanctuary != "0.0.0-alpha.111" || migration.compatibility?.sanctuary_data_version != 11 @@ -3084,7 +3084,7 @@ tasks.register("verifyDeliveryBoxOrientation") { def blockstate = new JsonSlurper().parse( file("src/main/resources/assets/sanctuary/blockstates/delivery_box.json")) def expectedVariants = ["facing=north", "facing=east", "facing=south", "facing=west"].toSet() - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || migration.target?.modules?.sanctuary != "0.0.0-alpha.112" || migration.mailbox?.id != "sanctuary:delivery_box" || migration.mailbox?.horizontal_facing != true @@ -3112,7 +3112,7 @@ tasks.register("verifyInventoryShiftClick") { def mixin = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/AbstractContainerMenuMixin.java").text def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/AbstractContainerScreenMixin.java").text def gui = file("INVENTORY_GUI.md").text - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || !transfer.contains("shouldAppendOverflow") || !transfer.contains("isVanillaStorageRange") || !transfer.contains("hotbarStart") @@ -3158,7 +3158,7 @@ tasks.register("verifyQuestRequests") { new JsonSlurper().parse(file("src/main/resources/data/sanctuary/loot_table/blocks/${path}.json")) .pools[0].entries[0].name } - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || migration.target?.modules?.sanctuary != "0.0.0-alpha.113" || migration.quest_requests?.save_key != "quest_requests" || migration.quest_requests?.data_version != 1 @@ -3216,7 +3216,7 @@ tasks.register("verifyQuestRequestContracts") { def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale -> [(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))] } - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || 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" @@ -3285,7 +3285,7 @@ tasks.register("verifyQuestRequestEscrow") { def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale -> [(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))] } - if (project.version.toString() != "0.0.0-alpha.120" + if (project.version.toString() != "0.0.0-alpha.121" || 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" diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketItemPolicy.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketItemPolicy.java index 5762970..b094694 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketItemPolicy.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketItemPolicy.java @@ -29,6 +29,15 @@ public final class BlackMarketItemPolicy { if (stack == null || stack.isEmpty() || stack.has(DataComponents.CREATIVE_SLOT_LOCK)) return true; if (depth > 8 || stack.has(DataComponents.CONTAINER_LOOT)) return true; Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem()); + if (forbidden(id) + || stack.getItem() instanceof BlockItem blockItem && blockItem.getBlock() instanceof GameMasterBlock) return true; + var container = stack.get(DataComponents.CONTAINER); + if (container != null && container.nonEmptyItemCopyStream().anyMatch(item -> forbidden(item, depth + 1))) return true; + var bundle = stack.get(DataComponents.BUNDLE_CONTENTS); + return bundle != null && bundle.itemCopyStream().anyMatch(item -> forbidden(item, depth + 1)); + } + + public static boolean forbidden(Identifier id) { if (id == null) return true; String namespace = id.getNamespace().toLowerCase(Locale.ROOT); String path = id.getPath().toLowerCase(Locale.ROOT); @@ -37,13 +46,10 @@ public final class BlackMarketItemPolicy { || path.contains("rainbow_block")) return true; if (namespace.equals("onlyfun") && path.equals("lucky_block")) return true; if (namespace.equals("sanctuary") && path.equals("delivery_box")) return true; + if (namespace.equals("redstoner") && path.equals("storage_terminal")) return true; if (namespace.equals("anotherworld") && (path.equals("ruby") || path.equals("sapphire") || path.equals("saphire") || path.startsWith("ruby_") || path.startsWith("sapphire_") || path.startsWith("saphire_"))) return true; - if (stack.getItem() instanceof BlockItem blockItem && blockItem.getBlock() instanceof GameMasterBlock) return true; - var container = stack.get(DataComponents.CONTAINER); - if (container != null && container.nonEmptyItemCopyStream().anyMatch(item -> forbidden(item, depth + 1))) return true; - var bundle = stack.get(DataComponents.BUNDLE_CONTENTS); - return bundle != null && bundle.itemCopyStream().anyMatch(item -> forbidden(item, depth + 1)); + return false; } } diff --git a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketModelSmoke.java b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketModelSmoke.java index d7349de..0b42bb8 100644 --- a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketModelSmoke.java +++ b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketModelSmoke.java @@ -1,6 +1,7 @@ package fr.koka99cab.sanctuary26.sanctuary.shop; import java.util.UUID; +import net.minecraft.resources.Identifier; import net.minecraft.world.item.ItemStack; /** Pure ledger/policy smoke; live sign and mailbox behavior is covered by the server smoke. */ @@ -40,6 +41,8 @@ public final class BlackMarketModelSmoke { require(data.parcels().isEmpty() && data.casinoVaultSize() == 1, "An expired parcel did not transfer exactly once to the casino vault"); require(BlackMarketItemPolicy.forbidden(ItemStack.EMPTY), "An empty escrow item was accepted"); + require(BlackMarketItemPolicy.forbidden(Identifier.parse("redstoner:storage_terminal")), + "Storage Terminal reached the Black Market"); } private static void require(boolean value, String message) { -- 2.54.0