Sanctuary 26.2.0-alpha.201 — demandes financées et terminables #11

Merged
botsu merged 1 commits from release/26.2.0-alpha.201 into main 2026-08-27 09:46:59 +00:00
25 changed files with 922 additions and 272 deletions
+13
View File
@@ -6,6 +6,19 @@ 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.201`
Module modifié : **Sanctuary `0.0.0-alpha.116`**. Les neuf autres modules conservent leur version issue de lalpha.200.
- éclaircit le panneau communautaire avec une palette rose poudré moins saturée, sans changer ses silhouettes 32×32/16×16, son alpha ni ses IDs ;
- affiche le pseudonyme du demandeur avec sa couleur d’équipe persistée, sur le panneau comme dans les détails du chat ;
- remplace la commande cliquable par une fenêtre de confirmation et un payload borné, tout en revalidant côté serveur la distance, le panneau, le propriétaire, le financement et la réservation ;
- retire réellement la récompense des rangées dinventaire débloquées lors de la publication, conserve les composants exacts des piles sous séquestre et rembourse lancien dépôt par Mailbox lors dune modification ou de la casse du panneau ;
- compte automatiquement les éliminations KILL de mobs et de joueurs, termine BRING au clic droit en envoyant les objets au demandeur et termine PAY quand le joueur désigné confirme sur le panneau ;
- expédie une seule fois la récompense séquestrée vers la Mailbox du preneur à la complétion et conserve un état terminé récupérable par le propriétaire ;
- migre monotoniquement `quest_requests` de la donnée v2 à v3 ; les demandes alpha.199 existantes restent lisibles mais leur auteur doit les modifier pour déposer la récompense ;
- porte le protocole Sanctuary de 20 à 21 et conserve les données Sanctuary v11, les formats antérieurs et tous les IDs de registre.
## `26.2.0-alpha.200`
Modules modifiés : **Ambiance `0.0.0-alpha.12`** et **Ouch `0.0.0-alpha.9`**. Les huit autres modules conservent leur version issue de lalpha.199.
+3 -1
View File
@@ -22,7 +22,9 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans les fichiers de langue. Les images fournies servent d'icônes Fabric de leur module.
Version courante du pack local : **`26.2.0-alpha.200`**. Ambiance passe en **`0.0.0-alpha.12`** et Ouch en **`0.0.0-alpha.9`** ; les huit autres modules restent inchangés.
Version courante du pack local : **`26.2.0-alpha.201`**. Sanctuary passe en **`0.0.0-alpha.116`** ; les neuf autres modules conservent leur version de lalpha.200.
Lalpha.201 rend les demandes communautaires réellement terminables. Le demandeur dépose la récompense lors de la publication, le preneur confirme sans commande après avoir lu les détails, KILL progresse automatiquement, BRING livre les objets au demandeur et PAY est confirmé par le joueur désigné. La récompense séquestrée est ensuite expédiée une seule fois dans la Mailbox du preneur. Le pseudonyme conserve la couleur du joueur et le panneau rose adopte une palette plus claire et poudrée.
Lalpha.200 fait passer la TNT météo dOuch par lAPI publique dAmbiance. Son explosion impose un temps clair pendant douze heures, laisse la pluie et lorage disparaître progressivement et empêche le profil quotidien de reprendre la main avant lexpiration. Les IDs historiques, les sauvegardes et les protocoles restent inchangés.
+84 -38
View File
@@ -3427,8 +3427,8 @@ tasks.register("verifyProgressivePauseMenuRelease") {
[(locale): new JsonSlurper().parse(file(
"sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))]
}
if (rootProject.pack_version != "26.2.0-alpha.200"
|| rootProject.sanctuary_version != "0.0.0-alpha.115"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
|| rootProject.onlyfun_version != "0.0.0-alpha.19"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"]
@@ -3477,7 +3477,7 @@ 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.200"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.anotherworld_version != "0.0.0-alpha.49"
|| rootProject.redstoner_version != "0.0.0-alpha.10"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
@@ -4122,9 +4122,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.200"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.onlyfun_version != "0.0.0-alpha.19"
|| rootProject.sanctuary_version != "0.0.0-alpha.115"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.pack_version != "26.2.0-alpha.192"
|| migration.target?.pack_version != "26.2.0-alpha.193"
@@ -4167,7 +4167,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.200"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.itsalive_version != "0.0.0-alpha.30"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.pack_version != "26.2.0-alpha.193"
@@ -4216,9 +4216,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.200"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.onlyfun_version != "0.0.0-alpha.19"
|| rootProject.sanctuary_version != "0.0.0-alpha.115"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|| migration.source?.pack_version != "26.2.0-alpha.194"
|| migration.source?.modules != [onlyfun: "0.0.0-alpha.18", sanctuary: "0.0.0-alpha.110"]
@@ -4262,7 +4262,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.200"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| migration.source?.pack_version != "26.2.0-alpha.195"
|| migration.target?.pack_version != "26.2.0-alpha.196"
|| migration.target?.modules != [
@@ -4280,10 +4280,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.200"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.200.mrpack"
|| prism.versionId != "26.2.0-alpha.200"
|| !packwiz.contains('version = "26.2.0-alpha.200"')) {
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')) {
throw new GradleException("The alpha.196 culinary, mailbox and grapple release contract is incomplete")
}
}
@@ -4308,9 +4308,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.200"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.itsalive_version != "0.0.0-alpha.30"
|| rootProject.sanctuary_version != "0.0.0-alpha.115"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
|| 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"]
@@ -4327,11 +4327,11 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") {
|| migration.distribution?.jei?.prism_source != "30.24.0.176"
|| migration.distribution?.jei?.target != "30.26.0.186"
|| migration.compatibility?.sanctuary_target_network_protocol != 19
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 20
|| release.pack_version != "26.2.0-alpha.200"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.200.mrpack"
|| prism.versionId != "26.2.0-alpha.200"
|| !packwiz.contains('version = "26.2.0-alpha.200"')) {
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')) {
throw new GradleException("The alpha.197 quest-request and cauldron-batch release contract is incomplete")
}
}
@@ -4358,7 +4358,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.200"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.itsalive_version != "0.0.0-alpha.30"
|| migration.source?.pack_version != "26.2.0-alpha.197"
|| migration.target?.pack_version != "26.2.0-alpha.198"
@@ -4377,10 +4377,10 @@ tasks.register("verifyFermentationBarrelAutomationRelease") {
|| migration.distribution?.packwiz_index_refreshed != true
|| itsAliveManifest.custom?.sanctuary26?.data_version != 1
|| itsAliveManifest.custom?.sanctuary26?.network_protocol != 3
|| release.pack_version != "26.2.0-alpha.200"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.200.mrpack"
|| prism.versionId != "26.2.0-alpha.200"
|| !packwiz.contains('version = "26.2.0-alpha.200"')
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.30.jar"')) {
throw new GradleException("The alpha.198 fermentation barrel release contract is incomplete")
}
@@ -4408,9 +4408,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.200"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.itsalive_version != "0.0.0-alpha.30"
|| rootProject.sanctuary_version != "0.0.0-alpha.115"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
|| 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"
@@ -4421,13 +4421,13 @@ tasks.register("verifyQuestRequestContractsRelease") {
|| migration.quest_request_contracts?.right_click_shows_details_before_claim != true
|| migration.quest_request_contracts?.chat_accept_revalidates_distance_owner_sign_and_claim != true
|| migration.compatibility?.sanctuary_target_network_protocol != 20
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 20
|| release.pack_version != "26.2.0-alpha.200"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.200.mrpack"
|| prism.versionId != "26.2.0-alpha.200"
|| !packwiz.contains('version = "26.2.0-alpha.200"')
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')
|| !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.30.jar"')
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.115.jar"')) {
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.116.jar"')) {
throw new GradleException("The alpha.199 quest-request contract release is incomplete")
}
}
@@ -4463,7 +4463,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.200"
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.ambiance_version != "0.0.0-alpha.12"
|| rootProject.ouch_version != "0.0.0-alpha.9"
|| migration.source?.pack_version != "26.2.0-alpha.199"
@@ -4497,10 +4497,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.200"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.200.mrpack"
|| prism.versionId != "26.2.0-alpha.200"
|| !packwiz.contains('version = "26.2.0-alpha.200"')
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')
|| !packwizIndex.contains('file = "mods/ambiance-0.0.0-alpha.12.jar"')
|| !packwizIndex.contains('file = "mods/ouch-0.0.0-alpha.9.jar"')) {
throw new GradleException("The alpha.200 Weather TNT and Ambiance release is incomplete")
@@ -4512,6 +4512,52 @@ tasks.named("verifyWorkspace") {
dependsOn tasks.named("verifyWeatherTntAmbianceRelease")
}
tasks.register("verifyQuestRequestEscrowRelease") {
group = "verification"
description = "Checks the complete Sanctuary 26.2.0-alpha.201 funded quest-request release."
inputs.file(file("pack/migrations/26.2.0-alpha.200-quest-request-escrow-to-alpha.201.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("sanctuary/src/main/resources/fabric.mod.json"))
doLast {
def migration = new JsonSlurper().parse(file(
"pack/migrations/26.2.0-alpha.200-quest-request-escrow-to-alpha.201.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 sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
if (rootProject.pack_version != "26.2.0-alpha.201"
|| rootProject.sanctuary_version != "0.0.0-alpha.116"
|| 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"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.116"
|| migration.quest_request_escrow?.target_data_version != 3
|| migration.quest_request_escrow?.reward_removed_on_publish != true
|| migration.quest_request_escrow?.acceptance_uses_bounded_payload != true
|| migration.quest_request_escrow?.acceptance_executes_command != false
|| migration.quest_request_escrow?.kill_progress_automatic != true
|| migration.quest_request_escrow?.reward_delivery_to_claimant_mailbox != true
|| migration.compatibility?.sanctuary_target_network_protocol != 21
|| sanctuaryManifest.custom?.sanctuary26?.data_version != 11
|| sanctuaryManifest.custom?.sanctuary26?.network_protocol != 21
|| release.pack_version != "26.2.0-alpha.201"
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.201.mrpack"
|| prism.versionId != "26.2.0-alpha.201"
|| !packwiz.contains('version = "26.2.0-alpha.201"')
|| !packwizIndex.contains('file = "mods/sanctuary-0.0.0-alpha.116.jar"')) {
throw new GradleException("The alpha.201 quest-request escrow release is incomplete")
}
}
}
tasks.named("verifyWorkspace") {
dependsOn tasks.named("verifyQuestRequestEscrowRelease")
}
tasks.named("check") {
dependsOn tasks.named("verifyWorkspace")
dependsOn tasks.named("verifyTransport")
+2 -2
View File
@@ -28,7 +28,7 @@ iliketomoveit_version=0.0.0-alpha.13
iliketomoveit_lifecycle=active
onlyfun_version=0.0.0-alpha.19
onlyfun_lifecycle=active
sanctuary_version=0.0.0-alpha.115
sanctuary_version=0.0.0-alpha.116
sanctuary_lifecycle=active
pack_version=26.2.0-alpha.200
pack_version=26.2.0-alpha.201
maven_group=fr.koka99cab.sanctuary26
@@ -0,0 +1,48 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.200",
"modules": {
"sanctuary": "0.0.0-alpha.115"
}
},
"target": {
"pack_version": "26.2.0-alpha.201",
"modules": {
"sanctuary": "0.0.0-alpha.116"
}
},
"quest_request_escrow": {
"save_key": "quest_requests",
"source_data_version": 2,
"target_data_version": 3,
"existing_requests_preserved": true,
"legacy_requests_require_owner_funding": true,
"owner_color_persisted": true,
"reward_removed_on_publish": true,
"reward_components_preserved": true,
"acceptance_uses_bounded_payload": true,
"acceptance_executes_command": false,
"kill_progress_automatic": true,
"bring_delivery_to_owner_mailbox": true,
"pay_completion_confirmed_by_target": true,
"reward_delivery_to_claimant_mailbox": true,
"reward_delivery_exactly_once": true,
"break_refunds_escrow_to_owner_mailbox": true,
"pale_pink_textures": [
"sanctuary:block/pink_quest_request_sign",
"sanctuary:item/pink_quest_request_sign"
]
},
"distribution": {
"prism_instance": "Sanctuary-26.2-dev",
"packwiz_index_refreshed": true
},
"compatibility": {
"sanctuary_data_version": 11,
"sanctuary_source_network_protocol": 20,
"sanctuary_target_network_protocol": 21,
"save_migration_monotone": true,
"registry_ids_changed": false
}
}
+11 -11
View File
@@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
[[files]]
file = "mods/ambiance-0.0.0-alpha.12.jar"
hash = "682065501c54379b4018afe470e8dea5e655203a89f0efcadd5224a3c1eb07f3"
hash = "6d131ff56488ecc6352a86c87e29bef6a198ad672983838ad71941661d6567af"
[[files]]
file = "mods/anotherworld-0.0.0-alpha.49.jar"
hash = "164612384447d4a8f05404c4aae5efef876d15fbc08ba921472d430df8029f9d"
hash = "b245c849985c208ca547da3dc41121ce6dd2bc1ffbbb1bfd8f6a7326356cfbdc"
[[files]]
file = "mods/events-0.0.0-alpha.0.jar"
hash = "f439ed5ed33ce55e6960e92bceb0802059a4df72b012000eb6b54142779b30fe"
hash = "d8dac51f88f72bfb1f7cd92279447897ee349e4cbc78b070cb33e99d5acbb965"
[[files]]
file = "mods/fabric-api.pw.toml"
@@ -31,7 +31,7 @@ metafile = true
[[files]]
file = "mods/iliketomoveit-0.0.0-alpha.13.jar"
hash = "a7b9d2bb2447aafa03b23d790accd12cf4339a1b7a48ecc263a1ebad7e5ee8f7"
hash = "cf3e90860f475f6a7466eb29686d230c95d5629a36fac89c60077fbae4ede865"
[[files]]
file = "mods/iris.pw.toml"
@@ -40,7 +40,7 @@ metafile = true
[[files]]
file = "mods/itsalive-0.0.0-alpha.30.jar"
hash = "b39bfc4353df65d17e69f4f3f8e3aab14ba631977c159fca26635e30c2514ddf"
hash = "c311281a771bb8fec63be8e8a0d792be919737e4dfcf3da25ef08c6878662d55"
[[files]]
file = "mods/jei.pw.toml"
@@ -49,23 +49,23 @@ metafile = true
[[files]]
file = "mods/masterkey-0.0.0-alpha.8.jar"
hash = "b78b0ca215cf1b3df0b86c2c23af0f2b84b46674bcab4d34f01403559d36c86a"
hash = "4fac6589b42df15599915849cbf624d6cb8fbd41f47623357d5d6fa27e6f992f"
[[files]]
file = "mods/onlyfun-0.0.0-alpha.19.jar"
hash = "7c76b04d5bbc32805d33edc784510b7f8af8dd5999512d07da138b39c57233c4"
hash = "cf8f3afb8503ed9689fcce6153ad72c5621f4bb2441af867326d536d347756ed"
[[files]]
file = "mods/ouch-0.0.0-alpha.9.jar"
hash = "0b5a3a787ec956d8e6785c5c0ea5d1e508b0d1cd4aca1af0a4f3909a4faaccd4"
hash = "fc0f678eb72354c6594c90c59af78bcc7420190d91e922d1cd74a45f5eaa948e"
[[files]]
file = "mods/redstoner-0.0.0-alpha.10.jar"
hash = "58ec122796495fd1937bc495c141ed429bce914f4c2ed9239d6a6d658142befc"
hash = "6325d6edf66b1b0f102f53152239982c4c0c7011a59848e8d6e569f360b7ff0f"
[[files]]
file = "mods/sanctuary-0.0.0-alpha.115.jar"
hash = "43241163a209bd992e9a3e9e5e88cd7374d653a631731da1a0a19c5a5c1fa57a"
file = "mods/sanctuary-0.0.0-alpha.116.jar"
hash = "c1f856995a259f6471cf0780e1b02f5f73e487f21d39b0519b5a02299c480516"
[[files]]
file = "mods/simple-voice-chat.pw.toml"
+2 -2
View File
@@ -1,12 +1,12 @@
name = "Sanctuary 26.2"
author = "KOKA99CAB"
version = "26.2.0-alpha.200"
version = "26.2.0-alpha.201"
pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "cc7a8a0bd96d6d6ef171cf609d81695adefe8c17c48974e73995bfb3580269e7"
hash = "52d4011015ea5b087f0cd0c06fb2f762e83e2cd6d227441984d5d204fb6aedd1"
[versions]
fabric = "0.19.3"
+1 -1
View File
@@ -3,7 +3,7 @@
"game": "minecraft",
"name": "Sanctuary 26.2",
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
"versionId": "26.2.0-alpha.200",
"versionId": "26.2.0-alpha.201",
"files": [
{
"path": "mods/fabric-api-0.158.0+26.2.jar",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"pack_version": "26.2.0-alpha.200",
"pack_version": "26.2.0-alpha.201",
"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.200.mrpack",
"primary_file": "sanctuary-26.2.0-alpha.201.mrpack",
"shared_client_server_pack": true,
"update_owner": "packwiz-installer",
"check_on_launch": true,
+89 -15
View File
@@ -1052,12 +1052,12 @@ tasks.register("verifySanctuary") {
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
if (manifest.custom?.sanctuary26?.data_version != 11
|| manifest.custom?.sanctuary26?.network_protocol != 20
|| manifest.custom?.sanctuary26?.network_protocol != 21
|| manifest.depends?.jei != ">=30.24.0.176"
|| manifest.depends?."fabric-api" != ">=0.155.0+26.2"
|| !manifest.entrypoints?.client?.contains("fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClient")
|| !manifest.mixins?.contains("sanctuary.mixins.json")) {
throw new GradleException("Sanctuary must expose data v11, protocol 20, JEI and the client entrypoint")
throw new GradleException("Sanctuary must expose data v11, protocol 21, JEI and the client entrypoint")
}
def capeMigration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.155-cape-equipment-to-alpha.156.json"))
@@ -2886,7 +2886,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.115"
if (project.version.toString() != "0.0.0-alpha.116"
|| migration.target?.pack_version != "26.2.0-alpha.191"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.109"
|| migration.pause_menu?.progression_full_width != true
@@ -2924,7 +2924,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.115"
if (project.version.toString() != "0.0.0-alpha.116"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.110"
|| migration.shop_layout?.locked_offer_buttons != 6
|| migration.shop_layout?.button_slots != [4, 5, 6, 7, 8, 9]
@@ -2955,7 +2955,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.115"
if (project.version.toString() != "0.0.0-alpha.116"
|| migration.target?.pack_version != "26.2.0-alpha.195"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|| migration.compatibility?.sanctuary_data_version != 11
@@ -2986,7 +2986,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.115"
if (project.version.toString() != "0.0.0-alpha.116"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|| migration.mailbox?.id != "sanctuary:delivery_box"
|| migration.mailbox?.horizontal_facing != true
@@ -3014,7 +3014,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.115"
if (project.version.toString() != "0.0.0-alpha.116"
|| !transfer.contains("shouldAppendOverflow")
|| !transfer.contains("isVanillaStorageRange")
|| !transfer.contains("hotbarStart")
@@ -3060,7 +3060,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.115"
if (project.version.toString() != "0.0.0-alpha.116"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|| migration.quest_requests?.save_key != "quest_requests"
|| migration.quest_requests?.data_version != 1
@@ -3069,11 +3069,12 @@ tasks.register("verifyQuestRequests") {
|| migration.quest_requests?.drops_recoverable_item != true
|| migration.quest_requests?.payloads?.server_validated_and_bounded != true
|| migration.compatibility?.sanctuary_target_network_protocol != 19
|| !data.contains('SAVE_KEY = "quest_requests"') || !data.contains("DATA_VERSION = 2")
|| !data.contains('SAVE_KEY = "quest_requests"') || !data.contains("DATA_VERSION = 3")
|| !service.contains("PlayerBlockBreakEvents.BEFORE")
|| !service.contains("player.blockPosition().distSqr(payload.pos()) > 64.0D")
|| !network.contains("PROTOCOL_VERSION = 20")
|| !network.contains("PROTOCOL_VERSION = 21")
|| !network.contains("QuestRequestSubmitPayload.ID")
|| !network.contains("QuestRequestAcceptPayload.ID")
|| !registry.contains('SanctuaryMod.id("quest_request_sign")')
|| lootIds != ["sanctuary:quest_request_sign", "sanctuary:quest_request_sign"]
|| locales.any { locale, values ->
@@ -3117,7 +3118,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.115"
if (project.version.toString() != "0.0.0-alpha.116"
|| 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"
@@ -3129,14 +3130,15 @@ tasks.register("verifyQuestRequestContracts") {
|| migration.quest_request_contracts?.right_click_shows_details_before_claim != true
|| migration.quest_request_contracts?.chat_accept_revalidates_distance_owner_sign_and_claim != true
|| migration.compatibility?.sanctuary_target_network_protocol != 20
|| !data.contains("DATA_VERSION = 2")
|| !data.contains("DATA_VERSION = 3")
|| !data.contains('optionalFieldOf("reward_type"')
|| !service.contains('Commands.literal("sanctuary_quest_accept")')
|| !service.contains("new ClickEvent.RunCommand")
|| !data.contains('optionalFieldOf("reward_escrow"')
|| !service.contains("openConfirmation(player, request)")
|| !service.contains("QuestRequestAcceptPayload")
|| !service.contains('REWARD_LOAN = "loan"')
|| !service.contains('PLAYER.equals(targetKind)')
|| !screen.contains("rewardQuantity") || !screen.contains("killTargetIndex")
|| !network.contains("PROTOCOL_VERSION = 20")
|| !network.contains("PROTOCOL_VERSION = 21")
|| standingModel.textures?.all != "sanctuary:block/pink_quest_request_sign"
|| itemModel.textures?.layer0 != "sanctuary:item/pink_quest_request_sign"
|| blockTexture == null || blockTexture.width != 32 || blockTexture.height != 32
@@ -3155,6 +3157,77 @@ tasks.register("verifyQuestRequestContracts") {
}
}
tasks.register("verifyQuestRequestEscrow") {
group = "verification"
description = "Checks alpha.116 funded quest requests, command-free claims and mailbox completion."
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.200-quest-request-escrow-to-alpha.201.json"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestRequestData.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestRequestService.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/QuestRequestAcceptPayload.java"))
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/QuestRequestConfirmScreen.java"))
inputs.files(fileTree("src/main/resources/assets/sanctuary/lang") { include "*.json" })
inputs.files(file(
"src/main/resources/assets/sanctuary/textures/block/pink_quest_request_sign.png"), file(
"src/main/resources/assets/sanctuary/textures/item/pink_quest_request_sign.png"))
doLast {
def migration = new JsonSlurper().parse(rootProject.file(
"pack/migrations/26.2.0-alpha.200-quest-request-escrow-to-alpha.201.json"))
def data = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestRequestData.java").text
def service = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestRequestService.java").text
def network = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/SanctuaryNetworking.java").text
def acceptPayload = file(
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/network/QuestRequestAcceptPayload.java").text
def confirmScreen = file(
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/QuestRequestConfirmScreen.java").text
def blockTexture = ImageIO.read(file(
"src/main/resources/assets/sanctuary/textures/block/pink_quest_request_sign.png"))
def itemTexture = ImageIO.read(file(
"src/main/resources/assets/sanctuary/textures/item/pink_quest_request_sign.png"))
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
[(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))]
}
if (project.version.toString() != "0.0.0-alpha.116"
|| 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"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.116"
|| migration.quest_request_escrow?.source_data_version != 2
|| migration.quest_request_escrow?.target_data_version != 3
|| migration.quest_request_escrow?.acceptance_executes_command != false
|| migration.quest_request_escrow?.reward_delivery_exactly_once != true
|| migration.quest_request_escrow?.bring_delivery_to_owner_mailbox != true
|| migration.quest_request_escrow?.pay_completion_confirmed_by_target != true
|| migration.compatibility?.sanctuary_target_network_protocol != 21
|| !data.contains("DATA_VERSION = 3")
|| !data.contains('optionalFieldOf("owner_color"')
|| !data.contains('optionalFieldOf("reward_escrow"')
|| !data.contains('optionalFieldOf("completed"')
|| !service.contains("SanctuaryPlayerInventoryView.unlockedRows")
|| !service.contains("ServerLivingEntityEvents.AFTER_DEATH")
|| !service.contains('"quest_request_reward"')
|| !service.contains('"quest_request_delivery"')
|| service.contains("sanctuary_quest_accept")
|| !network.contains("PROTOCOL_VERSION = 21")
|| !network.contains("QuestRequestAcceptPayload.ID")
|| !network.contains("QuestRequestConfirmPayload.ID")
|| !acceptPayload.contains("readUtf(36)")
|| !confirmScreen.contains("ClientPlayNetworking.send(new QuestRequestAcceptPayload")
|| blockTexture == null || blockTexture.width != 32 || blockTexture.height != 32
|| !blockTexture.colorModel.hasAlpha()
|| itemTexture == null || itemTexture.width != 16 || itemTexture.height != 16
|| !itemTexture.colorModel.hasAlpha()
|| locales.any { locale, values ->
!values.containsKey("screen.sanctuary.quest_request.confirm.accept")
|| !values.containsKey("message.sanctuary.quest_request.missing_reward")
|| !values.containsKey("message.sanctuary.quest_request.reward_queued")
|| !values.containsKey("sign.sanctuary.quest_request.completed")
}) {
throw new GradleException("Sanctuary alpha.116 quest-request escrow contract is incomplete")
}
}
}
tasks.named("verifySanctuary") {
dependsOn tasks.named("verifyInventoryShiftClick")
}
@@ -3164,6 +3237,7 @@ tasks.named("check") {
dependsOn tasks.named("verifyDeliveryBoxOrientation")
dependsOn tasks.named("verifyQuestRequests")
dependsOn tasks.named("verifyQuestRequestContracts")
dependsOn tasks.named("verifyQuestRequestEscrow")
dependsOn tasks.named("celestialGraphSmoke")
dependsOn tasks.named("questRequestModelSmoke")
}
@@ -0,0 +1,60 @@
package fr.koka99cab.sanctuary26.sanctuary.client;
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestAcceptPayload;
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestConfirmPayload;
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.network.chat.Component;
/** Explicit confirmation screen; claiming never executes a chat command. */
public final class QuestRequestConfirmScreen extends Screen {
private final Screen parent;
private final QuestRequestConfirmPayload payload;
public QuestRequestConfirmScreen(Screen parent, QuestRequestConfirmPayload payload) {
super(Component.translatable("screen.sanctuary.quest_request.confirm.title"));
this.parent = parent;
this.payload = payload;
}
@Override protected void init() {
int center = width / 2;
addRenderableWidget(Button.builder(Component.translatable("screen.sanctuary.quest_request.confirm.accept"),
button -> accept()).bounds(center - 152, 188, 149, 20).build());
addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose())
.bounds(center + 3, 188, 149, 20).build());
}
@Override public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY,
float tickDelta) {
int center = width / 2;
graphics.fill(center - 164, 25, center + 164, 217, 0xE0181417);
graphics.outline(center - 164, 25, 328, 192, 0xFFE8A8C3);
graphics.centeredText(font, title, center, 34, 0xFFF2C4D7);
graphics.centeredText(font, Component.literal(payload.alias()).withStyle(style ->
style.withColor(payload.ownerColor())), center, 51, 0xFFFFFFFF);
graphics.text(font, Component.translatable("screen.sanctuary.quest_request.confirm.objective",
payload.objective()), center - 150, 75, 0xFFE9DDE2);
graphics.text(font, Component.translatable("screen.sanctuary.quest_request.confirm.mission",
Component.translatable("message.sanctuary.quest_request.action." + payload.action()),
payload.quantity(), payload.target()), center - 150, 100, 0xFFE9DDE2);
graphics.text(font, Component.translatable("screen.sanctuary.quest_request.confirm.reward",
Component.translatable("message.sanctuary.quest_request.reward." + payload.rewardType()),
payload.rewardQuantity(), payload.rewardTarget()), center - 150, 125, 0xFFF2C4D7);
graphics.centeredText(font, Component.translatable("screen.sanctuary.quest_request.confirm.help"),
center, 158, 0xFFAE9EA5);
super.extractRenderState(graphics, mouseX, mouseY, tickDelta);
}
private void accept() {
if (ClientPlayNetworking.canSend(QuestRequestAcceptPayload.ID)) {
ClientPlayNetworking.send(new QuestRequestAcceptPayload(payload.requestId()));
}
onClose();
}
@Override public void onClose() { if (minecraft != null) minecraft.gui.setScreen(parent); }
@Override public boolean isPauseScreen() { return false; }
}
@@ -118,9 +118,9 @@ public final class QuestRequestEditorScreen extends Screen {
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float tickDelta) {
int center = width / 2;
graphics.fill(center - 164, 18, center + 164, 226, 0xD0101010);
graphics.outline(center - 164, 18, 328, 208, 0xFFE83E8C);
graphics.centeredText(font, title, center, 25, 0xFFFF74B8);
graphics.fill(center - 164, 18, center + 164, 226, 0xD0181417);
graphics.outline(center - 164, 18, 328, 208, 0xFFE8A8C3);
graphics.centeredText(font, title, center, 25, 0xFFF2C4D7);
graphics.text(font, Component.translatable("screen.sanctuary.quest_request.alias"),
center - 152, 37, 0xFFDBB7CA);
graphics.text(font, Component.translatable("screen.sanctuary.quest_request.objective"),
@@ -128,7 +128,7 @@ public final class QuestRequestEditorScreen extends Screen {
graphics.text(font, Component.translatable("screen.sanctuary.quest_request.mission"),
center - 152, 107, 0xFFDBB7CA);
graphics.text(font, Component.translatable("screen.sanctuary.quest_request.reward"),
center - 152, 146, 0xFFFF74B8);
center - 152, 146, 0xFFF2C4D7);
graphics.centeredText(font, Component.translatable("screen.sanctuary.quest_request.help"),
center, 183, 0xFF9F8B95);
super.extractRenderState(graphics, mouseX, mouseY, tickDelta);
@@ -12,6 +12,7 @@ import fr.koka99cab.sanctuary26.sanctuary.network.ShopSnapshotPayload;
import fr.koka99cab.sanctuary26.sanctuary.network.CapeSyncPayload;
import fr.koka99cab.sanctuary26.sanctuary.network.BlackMarketEditorPayload;
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestEditorPayload;
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestConfirmPayload;
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryProgressionCosts;
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryUnlock;
import net.fabricmc.api.ClientModInitializer;
@@ -117,6 +118,9 @@ public final class SanctuaryClient implements ClientModInitializer {
ClientPlayNetworking.registerGlobalReceiver(QuestRequestEditorPayload.ID, (payload, context) ->
context.client().execute(() -> context.client().gui.setScreen(
new QuestRequestEditorScreen(context.client().gui.screen(), payload))));
ClientPlayNetworking.registerGlobalReceiver(QuestRequestConfirmPayload.ID, (payload, context) ->
context.client().execute(() -> context.client().gui.setScreen(
new QuestRequestConfirmScreen(context.client().gui.screen(), payload))));
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {
SanctuaryAtlasVisitedChunks.saveIfDirty(client);
SanctuaryAtlasMapCache.clear();
@@ -0,0 +1,24 @@
package fr.koka99cab.sanctuary26.sanctuary.network;
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
/** Command-free acceptance of one server-owned quest request. */
public record QuestRequestAcceptPayload(String requestId) implements CustomPacketPayload {
public static final Type<QuestRequestAcceptPayload> ID =
new Type<>(SanctuaryMod.id("c2s/quest_request_accept"));
public static final StreamCodec<RegistryFriendlyByteBuf, QuestRequestAcceptPayload> CODEC =
CustomPacketPayload.codec(QuestRequestAcceptPayload::write, QuestRequestAcceptPayload::new);
private QuestRequestAcceptPayload(RegistryFriendlyByteBuf buffer) {
this(buffer.readUtf(36));
}
private void write(RegistryFriendlyByteBuf buffer) {
buffer.writeUtf(requestId == null ? "" : requestId, 36);
}
@Override public Type<? extends CustomPacketPayload> type() { return ID; }
}
@@ -0,0 +1,37 @@
package fr.koka99cab.sanctuary26.sanctuary.network;
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
/** Bounded request details shown before the player confirms a claim. */
public record QuestRequestConfirmPayload(String requestId, String alias, int ownerColor,
String objective, String action, String target, int quantity, String rewardType,
String rewardTarget, int rewardQuantity) implements CustomPacketPayload {
public static final Type<QuestRequestConfirmPayload> ID =
new Type<>(SanctuaryMod.id("s2c/quest_request_confirm"));
public static final StreamCodec<RegistryFriendlyByteBuf, QuestRequestConfirmPayload> CODEC =
CustomPacketPayload.codec(QuestRequestConfirmPayload::write, QuestRequestConfirmPayload::new);
private QuestRequestConfirmPayload(RegistryFriendlyByteBuf buffer) {
this(buffer.readUtf(36), buffer.readUtf(32), buffer.readInt(), buffer.readUtf(96),
buffer.readUtf(8), buffer.readUtf(64), buffer.readVarInt(), buffer.readUtf(8),
buffer.readUtf(64), buffer.readVarInt());
}
private void write(RegistryFriendlyByteBuf buffer) {
buffer.writeUtf(requestId, 36);
buffer.writeUtf(alias, 32);
buffer.writeInt(ownerColor);
buffer.writeUtf(objective, 96);
buffer.writeUtf(action, 8);
buffer.writeUtf(target, 64);
buffer.writeVarInt(quantity);
buffer.writeUtf(rewardType, 8);
buffer.writeUtf(rewardTarget, 64);
buffer.writeVarInt(rewardQuantity);
}
@Override public Type<? extends CustomPacketPayload> type() { return ID; }
}
@@ -22,7 +22,7 @@ import net.minecraft.server.level.ServerPlayer;
* Networking contract for Sanctuary. Payload IDs must use the module namespace.
*/
public final class SanctuaryNetworking {
public static final int PROTOCOL_VERSION = 20;
public static final int PROTOCOL_VERSION = 21;
private static boolean initialized;
private SanctuaryNetworking() {
@@ -41,6 +41,7 @@ public final class SanctuaryNetworking {
PayloadTypeRegistry.clientboundPlay().register(CapeSyncPayload.ID, CapeSyncPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(BlackMarketEditorPayload.ID, BlackMarketEditorPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(QuestRequestEditorPayload.ID, QuestRequestEditorPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(QuestRequestConfirmPayload.ID, QuestRequestConfirmPayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(PurchaseProgressionPayload.ID, PurchaseProgressionPayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(BuildControlPayload.ID, BuildControlPayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(MiningControlPayload.ID, MiningControlPayload.CODEC);
@@ -53,6 +54,7 @@ public final class SanctuaryNetworking {
PayloadTypeRegistry.serverboundPlay().register(ShopActionPayload.ID, ShopActionPayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(BlackMarketCreatePayload.ID, BlackMarketCreatePayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(QuestRequestSubmitPayload.ID, QuestRequestSubmitPayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(QuestRequestAcceptPayload.ID, QuestRequestAcceptPayload.CODEC);
ServerPlayNetworking.registerGlobalReceiver(PurchaseProgressionPayload.ID, (payload, context) ->
context.server().execute(() -> SanctuaryProgressionService.purchase(context.player(), payload)));
ServerPlayNetworking.registerGlobalReceiver(BuildControlPayload.ID, (payload, context) ->
@@ -83,6 +85,8 @@ public final class SanctuaryNetworking {
context.server().execute(() -> BlackMarketService.create(context.player(), payload)));
ServerPlayNetworking.registerGlobalReceiver(QuestRequestSubmitPayload.ID, (payload, context) ->
context.server().execute(() -> QuestRequestService.submit(context.player(), payload)));
ServerPlayNetworking.registerGlobalReceiver(QuestRequestAcceptPayload.ID, (payload, context) ->
context.server().execute(() -> QuestRequestService.accept(context.player(), payload)));
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
SanctuaryProgressionService.join(handler.player);
SanctuaryMealHeartsService.join(handler.player);
@@ -1,6 +1,7 @@
package fr.koka99cab.sanctuary26.sanctuary.shop;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
import java.util.ArrayList;
@@ -9,6 +10,7 @@ import java.util.Optional;
import java.util.UUID;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.saveddata.SavedDataType;
@@ -16,7 +18,7 @@ import net.minecraft.world.level.saveddata.SavedDataType;
/** Versioned server state for player-authored quest requests attached to physical signs. */
public final class QuestRequestData extends SavedData {
public static final String SAVE_KEY = "quest_requests";
public static final int DATA_VERSION = 2;
public static final int DATA_VERSION = 3;
private static final Codec<Claim> CLAIM_CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.STRING.fieldOf("player").forGetter(Claim::player),
@@ -24,24 +26,41 @@ public final class QuestRequestData extends SavedData {
Codec.INT.optionalFieldOf("player_color", 0xFFFFFF).forGetter(Claim::playerColor),
Codec.LONG.fieldOf("accepted_at").forGetter(Claim::acceptedAt)
).apply(instance, Claim::new));
private static final MapCodec<RequestIdentity> REQUEST_IDENTITY_CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.STRING.fieldOf("id").forGetter(RequestIdentity::id),
Codec.STRING.fieldOf("dimension").forGetter(RequestIdentity::dimension),
Codec.LONG.fieldOf("pos").forGetter(RequestIdentity::pos),
Codec.BOOL.optionalFieldOf("front", true).forGetter(RequestIdentity::front),
Codec.STRING.fieldOf("owner").forGetter(RequestIdentity::owner)
).apply(instance, RequestIdentity::new));
private static final MapCodec<RequestContract> REQUEST_CONTRACT_CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.STRING.optionalFieldOf("alias", "").forGetter(RequestContract::alias),
Codec.STRING.optionalFieldOf("objective", "").forGetter(RequestContract::objective),
Codec.STRING.optionalFieldOf("action", QuestRequestService.KILL).forGetter(RequestContract::action),
Codec.STRING.optionalFieldOf("target_kind", QuestRequestService.MOB).forGetter(RequestContract::targetKind),
Codec.STRING.optionalFieldOf("target", "").forGetter(RequestContract::target),
Codec.INT.optionalFieldOf("quantity", 1).forGetter(RequestContract::quantity),
Codec.STRING.optionalFieldOf("reward_type", QuestRequestService.REWARD_NONE).forGetter(RequestContract::rewardType),
Codec.STRING.optionalFieldOf("reward_target", "").forGetter(RequestContract::rewardTarget),
Codec.INT.optionalFieldOf("reward_quantity", 0).forGetter(RequestContract::rewardQuantity)
).apply(instance, RequestContract::new));
private static final Codec<Request> REQUEST_CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.STRING.fieldOf("id").forGetter(Request::id),
Codec.STRING.fieldOf("dimension").forGetter(Request::dimension),
Codec.LONG.fieldOf("pos").forGetter(Request::pos),
Codec.BOOL.optionalFieldOf("front", true).forGetter(Request::front),
Codec.STRING.fieldOf("owner").forGetter(Request::owner),
Codec.STRING.optionalFieldOf("alias", "").forGetter(Request::alias),
Codec.STRING.optionalFieldOf("objective", "").forGetter(Request::objective),
Codec.STRING.optionalFieldOf("action", QuestRequestService.KILL).forGetter(Request::action),
Codec.STRING.optionalFieldOf("target_kind", QuestRequestService.MOB).forGetter(Request::targetKind),
Codec.STRING.optionalFieldOf("target", "").forGetter(Request::target),
Codec.INT.optionalFieldOf("quantity", 1).forGetter(Request::quantity),
Codec.STRING.optionalFieldOf("reward_type", QuestRequestService.REWARD_NONE).forGetter(Request::rewardType),
Codec.STRING.optionalFieldOf("reward_target", "").forGetter(Request::rewardTarget),
Codec.INT.optionalFieldOf("reward_quantity", 0).forGetter(Request::rewardQuantity),
REQUEST_IDENTITY_CODEC.forGetter(request -> new RequestIdentity(request.id(), request.dimension(),
request.pos(), request.front(), request.owner())),
REQUEST_CONTRACT_CODEC.forGetter(request -> new RequestContract(request.alias(), request.objective(),
request.action(), request.targetKind(), request.target(), request.quantity(), request.rewardType(),
request.rewardTarget(), request.rewardQuantity())),
Codec.INT.optionalFieldOf("owner_color", 0xFFFFFF).forGetter(Request::ownerColor),
Codec.BOOL.optionalFieldOf("configured", false).forGetter(Request::configured),
CLAIM_CODEC.optionalFieldOf("claim").forGetter(Request::claim)
).apply(instance, Request::new));
CLAIM_CODEC.optionalFieldOf("claim").forGetter(Request::claim),
ItemStack.CODEC.listOf().optionalFieldOf("reward_escrow", List.of()).forGetter(Request::rewardEscrow),
Codec.INT.optionalFieldOf("progress", 0).forGetter(Request::progress),
Codec.BOOL.optionalFieldOf("completed", false).forGetter(Request::completed)
).apply(instance, (identity, contract, ownerColor, configured, claim, rewardEscrow, progress, completed) ->
new Request(identity.id(), identity.dimension(), identity.pos(), identity.front(), identity.owner(),
ownerColor, contract.alias(), contract.objective(), contract.action(), contract.targetKind(),
contract.target(), contract.quantity(), contract.rewardType(), contract.rewardTarget(),
contract.rewardQuantity(), configured, claim, rewardEscrow, progress, completed)));
private static final Codec<QuestRequestData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(data -> data.dataVersion),
REQUEST_CODEC.listOf().optionalFieldOf("requests", List.of()).forGetter(data -> data.requests)
@@ -115,30 +134,52 @@ public final class QuestRequestData extends SavedData {
}
}
public record Request(String id, String dimension, long pos, boolean front, String owner,
private record RequestIdentity(String id, String dimension, long pos, boolean front, String owner) {}
private record RequestContract(String alias, String objective, String action, String targetKind,
String target, int quantity, String rewardType, String rewardTarget, int rewardQuantity) {}
public record Request(String id, String dimension, long pos, boolean front, String owner, int ownerColor,
String alias, String objective, String action, String targetKind, String target, int quantity,
String rewardType, String rewardTarget, int rewardQuantity, boolean configured, Optional<Claim> claim) {
String rewardType, String rewardTarget, int rewardQuantity, boolean configured, Optional<Claim> claim,
List<ItemStack> rewardEscrow, int progress, boolean completed) {
public Request {
quantity = Math.max(1, quantity);
rewardQuantity = Math.max(0, rewardQuantity);
claim = claim == null ? Optional.empty() : claim;
rewardEscrow = rewardEscrow == null ? List.of()
: List.copyOf(rewardEscrow.stream().filter(stack -> stack != null && !stack.isEmpty())
.map(ItemStack::copy).toList());
progress = Math.clamp(progress, 0, quantity);
}
public UUID ownerId() {
return UUID.fromString(owner);
}
public Request withDetails(String newAlias, String newObjective, String newAction,
public Request withDetails(String newAlias, int newOwnerColor, String newObjective, String newAction,
String newTargetKind, String newTarget, int newQuantity, String newRewardType,
String newRewardTarget, int newRewardQuantity) {
return new Request(id, dimension, pos, front, owner, newAlias, newObjective, newAction,
String newRewardTarget, int newRewardQuantity, List<ItemStack> newRewardEscrow) {
return new Request(id, dimension, pos, front, owner, newOwnerColor, newAlias, newObjective, newAction,
newTargetKind, newTarget, newQuantity, newRewardType, newRewardTarget,
newRewardQuantity, true, Optional.empty());
newRewardQuantity, true, Optional.empty(), newRewardEscrow, 0, false);
}
public Request withClaim(Optional<Claim> newClaim) {
return new Request(id, dimension, pos, front, owner, alias, objective, action, targetKind,
target, quantity, rewardType, rewardTarget, rewardQuantity, configured, newClaim);
return new Request(id, dimension, pos, front, owner, ownerColor, alias, objective, action, targetKind,
target, quantity, rewardType, rewardTarget, rewardQuantity, configured, newClaim,
rewardEscrow, newClaim.isEmpty() ? 0 : progress, completed);
}
public Request withProgress(int newProgress) {
return new Request(id, dimension, pos, front, owner, ownerColor, alias, objective, action, targetKind,
target, quantity, rewardType, rewardTarget, rewardQuantity, configured, claim,
rewardEscrow, newProgress, completed);
}
public Request withCompletion() {
return new Request(id, dimension, pos, front, owner, ownerColor, alias, objective, action, targetKind,
target, quantity, rewardType, rewardTarget, rewardQuantity, configured, claim,
List.of(), quantity, true);
}
}
}
@@ -1,26 +1,27 @@
package fr.koka99cab.sanctuary26.sanctuary.shop;
import com.mojang.brigadier.arguments.StringArgumentType;
import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryPlayerInventoryView;
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestAcceptPayload;
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestConfirmPayload;
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestEditorPayload;
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestSubmitPayload;
import fr.koka99cab.sanctuary26.sanctuary.registry.SanctuaryRegistries;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.Commands;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.HoverEvent;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -30,11 +31,15 @@ import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.level.block.entity.SignText;
/** Server-authoritative community requests: edit, inspect, reserve, abandon and recover. */
/** Server-authoritative community requests with reward escrow and mailbox completion. */
public final class QuestRequestService {
public static final String KILL = "kill";
public static final String BRING = "bring";
@@ -86,15 +91,11 @@ public final class QuestRequestService {
if (!(level instanceof ServerLevel serverLevel)
|| !SanctuaryRegistries.isQuestRequestSign(state.getBlock())) return;
QuestRequestData data = QuestRequestData.get(serverLevel.getServer());
data.requestAt(dimension(serverLevel), pos.asLong()).ifPresent(data::remove);
data.requestAt(dimension(serverLevel), pos.asLong()).ifPresent(request ->
cancel(serverLevel.getServer(), data, request, "sign_destroyed"));
});
ServerLivingEntityEvents.AFTER_DEATH.register(QuestRequestService::afterDeath);
ServerTickEvents.END_SERVER_TICK.register(QuestRequestService::tick);
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) ->
dispatcher.register(Commands.literal("sanctuary_quest_accept")
.requires(source -> source.getPlayer() != null)
.then(Commands.argument("request", StringArgumentType.word())
.executes(context -> acceptFromChat(context.getSource().getPlayer(),
StringArgumentType.getString(context, "request"))))));
}
/** Creates an owned draft and opens its editor after the custom sign is placed. */
@@ -109,8 +110,9 @@ public final class QuestRequestService {
}
QuestRequestData.Request draft = new QuestRequestData.Request(
UUID.randomUUID().toString(), dimension(level), pos.asLong(), sign.isFacingFrontText(player),
player.getUUID().toString(), player.getGameProfile().name(), "", KILL, MOB, "", 1,
REWARD_GIFT, "", 1, false, Optional.empty());
player.getUUID().toString(), player.getTeamColor() & 0xFFFFFF,
player.getGameProfile().name(), "", KILL, MOB, "", 1,
REWARD_GIFT, "", 1, false, Optional.empty(), List.of(), 0, false);
if (!data.add(draft)) {
message(player, "occupied");
return;
@@ -132,6 +134,11 @@ public final class QuestRequestService {
else message(player, "configuring");
return InteractionResult.SUCCESS_SERVER;
}
if (request.completed()) {
inspect(player, request, false);
message(player, "completed");
return InteractionResult.SUCCESS_SERVER;
}
if (request.ownerId().equals(player.getUUID()) && player.isSecondaryUseActive()
&& request.claim().isEmpty()) {
openEditor(player, request);
@@ -142,49 +149,70 @@ public final class QuestRequestService {
if (request.ownerId().equals(player.getUUID())) {
inspect(player, request, false);
message(player, "owner_cannot_claim");
} else inspect(player, request, true);
} else {
inspect(player, request, true);
openConfirmation(player, request);
}
return InteractionResult.SUCCESS_SERVER;
}
if (claim.playerId().equals(player.getUUID()) && player.isSecondaryUseActive()) {
QuestRequestData.Request released = request.withClaim(Optional.empty());
if (data.replace(released)) {
writeSign(level, sign, released);
message(player, "abandoned");
}
if (PAY.equals(request.action())
&& request.target().equalsIgnoreCase(player.getGameProfile().name())) {
complete(player, level.getServer(), data, request.withProgress(request.quantity()), List.of());
return InteractionResult.SUCCESS_SERVER;
}
if (claim.playerId().equals(player.getUUID())) {
if (player.isSecondaryUseActive()) {
QuestRequestData.Request released = request.withClaim(Optional.empty());
if (data.replace(released)) {
writeSign(level, sign, released);
message(player, "abandoned");
}
} else if (BRING.equals(request.action())) {
deliverBring(player, data, request);
} else inspect(player, request, false);
return InteractionResult.SUCCESS_SERVER;
}
inspect(player, request, false);
return InteractionResult.SUCCESS_SERVER;
}
private static int acceptFromChat(ServerPlayer player, String requestId) {
/** Accepts a bounded packet after revalidating distance, ownership, sign and escrow. */
public static void accept(ServerPlayer player, QuestRequestAcceptPayload payload) {
QuestRequestData data = QuestRequestData.get(player.level().getServer());
QuestRequestData.Request request = data.request(requestId).orElse(null);
if (request == null || !request.configured()) {
QuestRequestData.Request request = data.request(payload.requestId()).orElse(null);
if (request == null || !request.configured() || request.completed()) {
message(player, "missing");
return 0;
return;
}
if (request.ownerId().equals(player.getUUID())) {
message(player, "owner_cannot_claim");
return 0;
return;
}
if (request.claim().isPresent()) {
message(player, "already_claimed");
return 0;
return;
}
if (!funded(request)) {
message(player, "not_funded");
return;
}
if (PAY.equals(request.action())
&& request.target().equalsIgnoreCase(player.getGameProfile().name())) {
message(player, "pay_target_cannot_claim");
return;
}
ServerLevel level = level(player.level().getServer(), request.dimension());
BlockPos pos = BlockPos.of(request.pos());
if (level == null || player.level() != level || player.blockPosition().distSqr(pos) > 64.0D) {
message(player, "too_far");
return 0;
return;
}
if (!SanctuaryRegistries.isQuestRequestSign(level.getBlockState(pos).getBlock())
|| !(level.getBlockEntity(pos) instanceof SignBlockEntity sign)) {
message(player, "missing");
return 0;
return;
}
claim(player, level, sign, data, request);
return 1;
}
private static void claim(ServerPlayer player, ServerLevel level, SignBlockEntity sign,
@@ -200,10 +228,12 @@ public final class QuestRequestService {
level.playSound(null, sign.getBlockPos(), SoundEvents.VILLAGER_YES, SoundSource.BLOCKS, 0.7F, 1.1F);
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.claimed",
describeAction(claimed.action()), claimed.quantity(), describeTarget(claimed)));
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.completion."
+ claimed.action(), describeTarget(claimed)));
inspect(player, claimed, false);
}
/** Accepts one bounded editor submission after revalidating ownership, distance and targets. */
/** Accepts one bounded editor submission and moves the real reward into server escrow. */
public static void submit(ServerPlayer player, QuestRequestSubmitPayload payload) {
if (player.blockPosition().distSqr(payload.pos()) > 64.0D) {
message(player, "too_far");
@@ -217,7 +247,8 @@ public final class QuestRequestService {
}
QuestRequestData data = QuestRequestData.get(level.getServer());
QuestRequestData.Request request = data.requestAt(dimension(level), payload.pos().asLong()).orElse(null);
if (request == null || !request.ownerId().equals(player.getUUID()) || request.claim().isPresent()) {
if (request == null || !request.ownerId().equals(player.getUUID()) || request.claim().isPresent()
|| request.completed()) {
message(player, "not_owner");
return;
}
@@ -242,13 +273,30 @@ public final class QuestRequestService {
message(player, "invalid_target");
return;
}
QuestRequestData.Request configured = request.withDetails(alias, objective, action,
Item rewardItem = BuiltInRegistries.ITEM.getOptional(Identifier.parse(canonicalReward)).orElse(null);
if (rewardItem == null) {
message(player, "invalid_target");
return;
}
EscrowPlan plan = planEscrow(request.rewardEscrow(), rewardItem, payload.rewardQuantity());
if (countPlayerItem(player, rewardItem) < plan.missing()) {
message(player, "missing_reward");
return;
}
List<ItemStack> taken = removePlayerItem(player, rewardItem, plan.missing());
List<ItemStack> escrow = new ArrayList<>(plan.retained());
escrow.addAll(taken);
QuestRequestData.Request configured = request.withDetails(alias, player.getTeamColor() & 0xFFFFFF,
objective, action,
normalizedTargetKind(action, targetKind), canonicalTarget, payload.quantity(), rewardType,
canonicalReward, payload.rewardQuantity());
canonicalReward, payload.rewardQuantity(), escrow);
if (!data.replace(configured)) {
restoreImmediate(player, taken);
message(player, "missing");
return;
}
queueStacks(level.getServer(), request.ownerId(), plan.refund(), request.id(),
"quest_request_escrow_adjustment", player);
writeSign(level, sign, configured);
level.playSound(null, payload.pos(), SoundEvents.AMETHYST_BLOCK_CHIME,
SoundSource.BLOCKS, 0.8F, 1.0F);
@@ -256,6 +304,288 @@ public final class QuestRequestService {
inspect(player, configured, false);
}
private static void deliverBring(ServerPlayer player, QuestRequestData data,
QuestRequestData.Request request) {
Identifier id = Identifier.tryParse(request.target());
Item item = id == null ? null : BuiltInRegistries.ITEM.getOptional(id).orElse(null);
int remaining = request.quantity() - request.progress();
if (item == null || remaining <= 0 || countPlayerItem(player, item) < remaining) {
message(player, "missing_delivery");
return;
}
List<ItemStack> delivered = removePlayerItem(player, item, remaining);
complete(player, player.level().getServer(), data, request.withProgress(request.quantity()), delivered);
}
private static void afterDeath(LivingEntity entity, DamageSource source) {
if (!(entity.level() instanceof ServerLevel level)) return;
ServerPlayer killer = killer(entity, source);
if (killer == null || killer.isSpectator()) return;
Identifier killedType = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType());
QuestRequestData data = QuestRequestData.get(level.getServer());
for (QuestRequestData.Request request : data.requests()) {
if (request.completed() || !KILL.equals(request.action()) || request.claim().isEmpty()
|| !request.claim().get().playerId().equals(killer.getUUID())) continue;
boolean matches = PLAYER.equals(request.targetKind())
? entity instanceof ServerPlayer victim
&& request.target().equalsIgnoreCase(victim.getGameProfile().name())
: request.target().equals(killedType.toString());
if (!matches) continue;
QuestRequestData.Request advanced = request.withProgress(request.progress() + 1);
if (advanced.progress() >= advanced.quantity()) {
complete(killer, level.getServer(), data, advanced, List.of());
} else if (data.replace(advanced)) {
refreshSign(level.getServer(), advanced);
killer.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.progress",
advanced.progress(), advanced.quantity()));
}
}
}
private static void complete(ServerPlayer actor, MinecraftServer server, QuestRequestData data,
QuestRequestData.Request request, List<ItemStack> delivered) {
QuestRequestData.Claim claim = request.claim().orElse(null);
if (claim == null || request.completed() || !funded(request)) {
restoreImmediate(actor, delivered);
message(actor, "not_funded");
return;
}
List<ItemStack> reward = request.rewardEscrow().stream().map(ItemStack::copy).toList();
QuestRequestData.Request completed = request.withCompletion();
if (!data.replace(completed)) {
restoreImmediate(actor, delivered);
message(actor, "missing");
return;
}
queueStacks(server, request.ownerId(), delivered, request.id(),
"quest_request_delivery", actor);
queueStacks(server, claim.playerId(), reward, request.id(),
"quest_request_reward", actor);
refreshSign(server, completed);
ServerLevel signLevel = level(server, request.dimension());
BlockPos signPos = BlockPos.of(request.pos());
if (signLevel != null && signLevel.isLoaded(signPos)) signLevel.playSound(null, signPos,
SoundEvents.PLAYER_LEVELUP, SoundSource.BLOCKS, 0.8F, 1.2F);
ServerPlayer claimant = server.getPlayerList().getPlayer(claim.playerId());
if (claimant != null) claimant.sendSystemMessage(Component.translatable(
"message.sanctuary.quest_request.reward_queued", request.rewardQuantity(), describeReward(request)));
if (actor != claimant) message(actor, "payment_confirmed");
}
private static void openEditor(ServerPlayer player, QuestRequestData.Request request) {
if (!ServerPlayNetworking.canSend(player, QuestRequestEditorPayload.ID)) {
message(player, "client_required");
return;
}
ServerPlayNetworking.send(player, new QuestRequestEditorPayload(BlockPos.of(request.pos()),
request.alias(), request.objective(), request.action(), request.targetKind(), request.target(),
request.quantity(), legacyRewardType(request), request.rewardTarget(),
Math.max(1, request.rewardQuantity())));
}
private static void openConfirmation(ServerPlayer player, QuestRequestData.Request request) {
if (!funded(request)) {
message(player, "not_funded");
return;
}
if (!ServerPlayNetworking.canSend(player, QuestRequestConfirmPayload.ID)) {
message(player, "client_required");
return;
}
ServerPlayNetworking.send(player, new QuestRequestConfirmPayload(request.id(), request.alias(),
request.ownerColor(), request.objective(), request.action(), request.target(), request.quantity(),
request.rewardType(), request.rewardTarget(), request.rewardQuantity()));
}
private static String legacyRewardType(QuestRequestData.Request request) {
return REWARD_TYPES.contains(request.rewardType()) ? request.rewardType() : REWARD_GIFT;
}
private static void inspect(ServerPlayer player, QuestRequestData.Request request, boolean offerAcceptance) {
Component owner = Component.literal(request.alias()).withStyle(style -> style.withColor(request.ownerColor()));
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.header", owner)
.withStyle(ChatFormatting.LIGHT_PURPLE, ChatFormatting.BOLD));
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.objective",
request.objective()));
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.mission",
describeAction(request.action()), request.quantity(), describeTarget(request)));
if (funded(request) || request.completed()) {
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.reward",
describeRewardType(request.rewardType()), request.rewardQuantity(), describeReward(request)));
} else player.sendSystemMessage(Component.translatable(
"message.sanctuary.quest_request.details.reward_legacy").withStyle(ChatFormatting.GRAY));
QuestRequestData.Claim claim = request.claim().orElse(null);
if (request.completed()) {
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.completed")
.withStyle(ChatFormatting.GREEN));
} else if (claim != null) {
Component claimant = Component.literal(claim.playerName())
.withStyle(style -> style.withColor(claim.playerColor()));
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.claimed_by", claimant)
.withStyle(ChatFormatting.GRAY));
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.progress",
request.progress(), request.quantity()));
} else if (offerAcceptance && funded(request)) {
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.accept_prompt")
.withStyle(ChatFormatting.GREEN));
}
}
private static Component describeAction(String action) {
return Component.translatable("message.sanctuary.quest_request.action." + action);
}
private static Component describeTarget(QuestRequestData.Request request) {
if (PAY.equals(request.action()) || PLAYER.equals(request.targetKind())) {
return Component.literal(request.target());
}
Identifier id = Identifier.tryParse(request.target());
if (id == null) return Component.literal(request.target());
if (KILL.equals(request.action())) return BuiltInRegistries.ENTITY_TYPE.getOptional(id)
.map(type -> type.getDescription()).orElse(Component.literal(request.target()));
return BuiltInRegistries.ITEM.getOptional(id)
.map(item -> new ItemStack(item).getHoverName()).orElse(Component.literal(request.target()));
}
private static Component describeRewardType(String rewardType) {
return Component.translatable("message.sanctuary.quest_request.reward." + rewardType);
}
private static Component describeReward(QuestRequestData.Request request) {
if (!request.rewardEscrow().isEmpty()) return request.rewardEscrow().getFirst().getHoverName();
Identifier id = Identifier.tryParse(request.rewardTarget());
if (id == null) return Component.literal(request.rewardTarget());
return BuiltInRegistries.ITEM.getOptional(id)
.map(item -> new ItemStack(item).getHoverName()).orElse(Component.literal(request.rewardTarget()));
}
private static void writeSign(ServerLevel level, SignBlockEntity sign, QuestRequestData.Request request) {
SignText text;
if (!request.configured()) {
text = new SignText()
.setMessage(0, Component.translatable("sign.sanctuary.quest_request.title")
.withStyle(ChatFormatting.LIGHT_PURPLE, ChatFormatting.BOLD))
.setMessage(1, Component.translatable("sign.sanctuary.quest_request.configure")
.withStyle(ChatFormatting.GRAY));
} else {
text = new SignText()
.setMessage(0, Component.translatable("sign.sanctuary.quest_request.title")
.withStyle(ChatFormatting.LIGHT_PURPLE, ChatFormatting.BOLD))
.setMessage(1, Component.literal(shortText(request.alias(), 24))
.withStyle(style -> style.withColor(request.ownerColor())));
if (request.completed()) {
text = text.setMessage(2, Component.translatable("sign.sanctuary.quest_request.completed")
.withStyle(ChatFormatting.DARK_GRAY));
} else if (request.claim().isPresent()) {
text = text.setMessage(2, Component.translatable("sign.sanctuary.quest_request.claimed")
.withStyle(ChatFormatting.DARK_GRAY));
} else text = text.setMessage(2, Component.translatable("sign.sanctuary.quest_request.available")
.withStyle(ChatFormatting.DARK_GRAY));
text = text.setMessage(3, Component.translatable("sign.sanctuary.quest_request.details")
.withStyle(ChatFormatting.GRAY));
}
sign.setText(text, request.front());
sign.setWaxed(true);
sign.setChanged();
level.sendBlockUpdated(sign.getBlockPos(), sign.getBlockState(), sign.getBlockState(), 3);
}
private static void refreshSign(MinecraftServer server, QuestRequestData.Request request) {
ServerLevel level = level(server, request.dimension());
BlockPos pos = BlockPos.of(request.pos());
if (level != null && level.isLoaded(pos)
&& level.getBlockEntity(pos) instanceof SignBlockEntity sign) writeSign(level, sign, request);
}
private static void tick(MinecraftServer server) {
if (++tickCounter < 20) return;
tickCounter = 0;
QuestRequestData data = QuestRequestData.get(server);
for (QuestRequestData.Request request : data.requests()) {
ServerLevel level = level(server, request.dimension());
BlockPos pos = BlockPos.of(request.pos());
if (level == null || !level.isLoaded(pos)) continue;
if (!SanctuaryRegistries.isQuestRequestSign(level.getBlockState(pos).getBlock())
|| !(level.getBlockEntity(pos) instanceof SignBlockEntity sign)) {
cancel(server, data, request, "sign_missing");
continue;
}
if (refreshedLayouts.add(request.id())) writeSign(level, sign, request);
}
}
private static void cancel(MinecraftServer server, QuestRequestData data,
QuestRequestData.Request request, String reason) {
if (!data.remove(request)) return;
queueStacks(server, request.ownerId(), request.rewardEscrow(), request.id(),
"quest_request_" + reason, null);
refreshedLayouts.remove(request.id());
ServerPlayer owner = server.getPlayerList().getPlayer(request.ownerId());
if (owner != null && !request.rewardEscrow().isEmpty()) message(owner, "reward_refunded");
}
private static EscrowPlan planEscrow(List<ItemStack> previous, Item rewardItem, int amount) {
int remaining = amount;
List<ItemStack> retained = new ArrayList<>();
List<ItemStack> refund = new ArrayList<>();
for (ItemStack original : previous) {
ItemStack stack = original.copy();
if (remaining > 0 && stack.is(rewardItem)) {
int used = Math.min(remaining, stack.getCount());
retained.add(stack.copyWithCount(used));
remaining -= used;
stack.shrink(used);
}
if (!stack.isEmpty()) refund.add(stack);
}
return new EscrowPlan(List.copyOf(retained), List.copyOf(refund), remaining);
}
private static boolean funded(QuestRequestData.Request request) {
Identifier id = Identifier.tryParse(request.rewardTarget());
Item item = id == null ? null : BuiltInRegistries.ITEM.getOptional(id).orElse(null);
if (item == null || request.rewardQuantity() < 1) return false;
long count = 0L;
for (ItemStack stack : request.rewardEscrow()) if (stack.is(item)) count += stack.getCount();
return count >= request.rewardQuantity();
}
private static int countPlayerItem(ServerPlayer player, Item item) {
long count = 0L;
for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) {
if (slot.stack().is(item)) count += slot.stack().getCount();
}
return (int) Math.min(Integer.MAX_VALUE, count);
}
private static List<ItemStack> removePlayerItem(ServerPlayer player, Item item, int amount) {
int remaining = amount;
List<ItemStack> removed = new ArrayList<>();
for (var slot : SanctuaryPlayerInventoryView.unlockedRows(player)) {
if (remaining <= 0) break;
ItemStack stack = slot.stack();
if (!stack.is(item)) continue;
int take = Math.min(remaining, stack.getCount());
removed.add(slot.remove(take));
remaining -= take;
}
return List.copyOf(removed);
}
private static void restoreImmediate(ServerPlayer player, List<ItemStack> stacks) {
if (player == null) return;
for (ItemStack stack : stacks) player.getInventory().placeItemBackInInventory(stack.copy());
}
private static void queueStacks(MinecraftServer server, UUID recipient, List<ItemStack> stacks,
String requestId, String reason, ServerPlayer randomSource) {
if (stacks == null || stacks.isEmpty()) return;
BlackMarketData market = BlackMarketData.get(server);
long now = System.currentTimeMillis();
for (ItemStack stack : stacks) if (!stack.isEmpty()) BlackMarketService.queueDelivery(
market, recipient, stack, requestId, reason, now, randomSource);
}
private static String canonicalTarget(String action, String targetKind, String value) {
if (PAY.equals(action)) return validPlayerName(value) ? value : null;
if (KILL.equals(action) && PLAYER.equals(targetKind)) return validPlayerName(value) ? value : null;
@@ -286,117 +616,10 @@ public final class QuestRequestService {
return PLAYER.equals(targetKind) ? PLAYER : MOB;
}
private static void openEditor(ServerPlayer player, QuestRequestData.Request request) {
if (!ServerPlayNetworking.canSend(player, QuestRequestEditorPayload.ID)) {
message(player, "client_required");
return;
}
ServerPlayNetworking.send(player, new QuestRequestEditorPayload(BlockPos.of(request.pos()),
request.alias(), request.objective(), request.action(), request.targetKind(), request.target(),
request.quantity(), legacyRewardType(request), request.rewardTarget(),
Math.max(1, request.rewardQuantity())));
}
private static String legacyRewardType(QuestRequestData.Request request) {
return REWARD_TYPES.contains(request.rewardType()) ? request.rewardType() : REWARD_GIFT;
}
private static void inspect(ServerPlayer player, QuestRequestData.Request request, boolean offerAcceptance) {
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.header",
request.alias()).withStyle(ChatFormatting.LIGHT_PURPLE, ChatFormatting.BOLD));
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.objective",
request.objective()));
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.mission",
describeAction(request.action()), request.quantity(), describeTarget(request)));
if (REWARD_TYPES.contains(request.rewardType()) && !request.rewardTarget().isBlank()) {
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.details.reward",
describeRewardType(request.rewardType()), request.rewardQuantity(), describeReward(request)));
} else player.sendSystemMessage(Component.translatable(
"message.sanctuary.quest_request.details.reward_legacy").withStyle(ChatFormatting.GRAY));
QuestRequestData.Claim claim = request.claim().orElse(null);
if (claim != null) {
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.claimed_by",
claim.playerName()).withStyle(ChatFormatting.GRAY));
} else if (offerAcceptance) {
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.accept")
.withStyle(style -> style.withColor(ChatFormatting.GREEN).withBold(true).withUnderlined(true)
.withClickEvent(new ClickEvent.RunCommand("/sanctuary_quest_accept " + request.id()))
.withHoverEvent(new HoverEvent.ShowText(Component.translatable(
"message.sanctuary.quest_request.accept_hover")))));
}
}
private static Component describeAction(String action) {
return Component.translatable("message.sanctuary.quest_request.action." + action);
}
private static Component describeTarget(QuestRequestData.Request request) {
if (PAY.equals(request.action()) || PLAYER.equals(request.targetKind())) {
return Component.literal(request.target());
}
Identifier id = Identifier.tryParse(request.target());
if (id == null) return Component.literal(request.target());
if (KILL.equals(request.action())) return BuiltInRegistries.ENTITY_TYPE.getOptional(id)
.map(type -> type.getDescription()).orElse(Component.literal(request.target()));
return BuiltInRegistries.ITEM.getOptional(id)
.map(item -> new ItemStack(item).getHoverName()).orElse(Component.literal(request.target()));
}
private static Component describeRewardType(String rewardType) {
return Component.translatable("message.sanctuary.quest_request.reward." + rewardType);
}
private static Component describeReward(QuestRequestData.Request request) {
Identifier id = Identifier.tryParse(request.rewardTarget());
if (id == null) return Component.literal(request.rewardTarget());
return BuiltInRegistries.ITEM.getOptional(id)
.map(item -> new ItemStack(item).getHoverName()).orElse(Component.literal(request.rewardTarget()));
}
private static void writeSign(ServerLevel level, SignBlockEntity sign, QuestRequestData.Request request) {
SignText text;
if (!request.configured()) {
text = new SignText()
.setMessage(0, Component.translatable("sign.sanctuary.quest_request.title")
.withStyle(ChatFormatting.LIGHT_PURPLE, ChatFormatting.BOLD))
.setMessage(1, Component.translatable("sign.sanctuary.quest_request.configure")
.withStyle(ChatFormatting.GRAY));
} else {
text = new SignText()
.setMessage(0, Component.translatable("sign.sanctuary.quest_request.title")
.withStyle(ChatFormatting.LIGHT_PURPLE, ChatFormatting.BOLD))
.setMessage(1, Component.literal(shortText(request.alias(), 24))
.withStyle(ChatFormatting.DARK_PURPLE));
if (request.claim().isPresent()) {
text = text.setMessage(2, Component.translatable("sign.sanctuary.quest_request.claimed")
.withStyle(ChatFormatting.DARK_GRAY));
} else text = text.setMessage(2, Component.translatable("sign.sanctuary.quest_request.available")
.withStyle(ChatFormatting.DARK_GRAY));
text = text.setMessage(3, Component.translatable("sign.sanctuary.quest_request.details")
.withStyle(ChatFormatting.GRAY));
}
sign.setText(text, request.front());
sign.setWaxed(true);
sign.setChanged();
level.sendBlockUpdated(sign.getBlockPos(), sign.getBlockState(), sign.getBlockState(), 3);
}
private static void tick(MinecraftServer server) {
if (++tickCounter < 20) return;
tickCounter = 0;
QuestRequestData data = QuestRequestData.get(server);
for (QuestRequestData.Request request : data.requests()) {
ServerLevel level = level(server, request.dimension());
BlockPos pos = BlockPos.of(request.pos());
if (level == null || !level.isLoaded(pos)) continue;
if (!SanctuaryRegistries.isQuestRequestSign(level.getBlockState(pos).getBlock())
|| !(level.getBlockEntity(pos) instanceof SignBlockEntity sign)) {
data.remove(request);
refreshedLayouts.remove(request.id());
continue;
}
if (refreshedLayouts.add(request.id())) writeSign(level, sign, request);
}
private static ServerPlayer killer(LivingEntity entity, DamageSource source) {
Entity attacker = source.getEntity();
if (attacker instanceof ServerPlayer player) return player;
return entity.getKillCredit() instanceof ServerPlayer player ? player : null;
}
private static ServerLevel level(MinecraftServer server, String dimension) {
@@ -428,4 +651,6 @@ public final class QuestRequestService {
private static void message(ServerPlayer player, String key) {
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_request." + key));
}
private record EscrowPlan(List<ItemStack> retained, List<ItemStack> refund, int missing) {}
}
@@ -27,7 +27,7 @@
"screen.sanctuary.quest_request.target_hint.kill.player": "Player name to eliminate",
"screen.sanctuary.quest_request.target_hint.bring.item": "minecraft:wheat",
"screen.sanctuary.quest_request.target_hint.pay.player": "Player name to pay",
"screen.sanctuary.quest_request.reward": "Promised reward: form · quantity · item",
"screen.sanctuary.quest_request.reward": "Deposited reward: form · quantity · item",
"screen.sanctuary.quest_request.reward_type.gift": "GIFT",
"screen.sanctuary.quest_request.reward_type.payment": "PAYMENT",
"screen.sanctuary.quest_request.reward_type.loan": "LOAN",
@@ -37,7 +37,13 @@
"screen.sanctuary.quest_request.reward_hint.payment": "anotherworld:ruby",
"screen.sanctuary.quest_request.reward_hint.loan": "minecraft:elytra",
"screen.sanctuary.quest_request.publish": "Publish",
"screen.sanctuary.quest_request.help": "Right click: details · then accept in chat",
"screen.sanctuary.quest_request.help": "The reward is removed from your inventory when published",
"screen.sanctuary.quest_request.confirm.title": "Confirm request",
"screen.sanctuary.quest_request.confirm.accept": "Accept quest",
"screen.sanctuary.quest_request.confirm.objective": "Objective: %s",
"screen.sanctuary.quest_request.confirm.mission": "Mission: %s · %s × %s",
"screen.sanctuary.quest_request.confirm.reward": "Reward: %s · %s × %s",
"screen.sanctuary.quest_request.confirm.help": "The reward is secured and will be delivered to your Mailbox",
"message.sanctuary.black_market.banned": "You are banned from the Black Market.",
"message.sanctuary.black_market.protected_sign": "Only the seller or an operator can remove this listing sign.",
"message.sanctuary.black_market.modified": "Listing %s updated.",
@@ -85,18 +91,33 @@
"message.sanctuary.quest_request.details.header": "— Request from %s —",
"message.sanctuary.quest_request.details.objective": "Objective: %s",
"message.sanctuary.quest_request.details.mission": "Mission: %s · %s × %s",
"message.sanctuary.quest_request.details.reward": "Promised reward: %s · %s × %s",
"message.sanctuary.quest_request.details.reward_legacy": "No reward was specified on this legacy request.",
"message.sanctuary.quest_request.details.reward": "Escrowed reward: %s · %s × %s",
"message.sanctuary.quest_request.details.reward_legacy": "Its author must edit this legacy request and deposit the reward.",
"message.sanctuary.quest_request.details.progress": "Progress: %s/%s.",
"message.sanctuary.quest_request.details.completed": "Request completed · reward dispatched.",
"message.sanctuary.quest_request.accept": "[ACCEPT QUEST]",
"message.sanctuary.quest_request.accept_hover": "Click to confirm after reading every detail.",
"message.sanctuary.quest_request.accept_prompt": "Read the details, then confirm in the acceptance screen.",
"message.sanctuary.quest_request.claimed_by": "Taken by %s.",
"message.sanctuary.quest_request.too_far": "You are too far from the request sign.",
"message.sanctuary.quest_request.missing": "This request sign no longer exists.",
"message.sanctuary.quest_request.not_owner": "Only the requester can edit this available sign.",
"message.sanctuary.quest_request.invalid": "The request contains an empty or out-of-range value.",
"message.sanctuary.quest_request.invalid_target": "Unknown target: use a valid mob/item ID or a valid player name.",
"message.sanctuary.quest_request.client_required": "An up-to-date Sanctuary 26.2 client is required to configure this sign.",
"message.sanctuary.quest_request.created": "Quest request published.",
"message.sanctuary.quest_request.client_required": "An up-to-date Sanctuary 26.2 client is required to configure or accept this sign.",
"message.sanctuary.quest_request.created": "Quest request published. Its reward is now in escrow.",
"message.sanctuary.quest_request.not_funded": "This request does not have a deposited reward yet.",
"message.sanctuary.quest_request.missing_reward": "You do not have the full reward in your unlocked inventory rows.",
"message.sanctuary.quest_request.missing_delivery": "Put the complete delivery in your unlocked inventory rows, then click the sign again.",
"message.sanctuary.quest_request.pay_target_cannot_claim": "The player who must confirm payment cannot take this request.",
"message.sanctuary.quest_request.progress": "Request progress: %s/%s.",
"message.sanctuary.quest_request.completion.kill": "Kills of %s are tracked automatically.",
"message.sanctuary.quest_request.completion.bring": "Return with the %s items and right-click the sign.",
"message.sanctuary.quest_request.completion.pay": "%s confirms payment by right-clicking the sign.",
"message.sanctuary.quest_request.reward_queued": "Request completed: %s × %s is being sent to your Mailbox.",
"message.sanctuary.quest_request.payment_confirmed": "Payment confirmed; the claimant's reward is being dispatched.",
"message.sanctuary.quest_request.reward_refunded": "The sign was recovered; its reward is returning through your Mailbox.",
"message.sanctuary.quest_request.completed": "This request is already complete. Its owner may recover the sign.",
"message.sanctuary.quest_request.action.kill": "Kill",
"message.sanctuary.quest_request.action.bring": "Bring",
"message.sanctuary.quest_request.action.pay": "Pay",
@@ -423,6 +444,7 @@
"sign.sanctuary.quest_request.action.pay": "PAY",
"sign.sanctuary.quest_request.available": "AVAILABLE",
"sign.sanctuary.quest_request.claimed": "TAKEN",
"sign.sanctuary.quest_request.completed": "COMPLETED",
"sign.sanctuary.quest_request.details": "Click: details",
"message.sanctuary.companion.active_locked": "Unlock the active companion power in Progression for %s XP levels.",
"message.sanctuary.shop.delivery_box_registered": "Mailbox registered at %s, %s, %s.",
@@ -27,7 +27,7 @@
"screen.sanctuary.quest_request.target_hint.kill.player": "Pseudonyme à éliminer",
"screen.sanctuary.quest_request.target_hint.bring.item": "minecraft:wheat",
"screen.sanctuary.quest_request.target_hint.pay.player": "Pseudonyme à payer",
"screen.sanctuary.quest_request.reward": "Récompense promise : forme · quantité · item",
"screen.sanctuary.quest_request.reward": "Récompense déposée : forme · quantité · item",
"screen.sanctuary.quest_request.reward_type.gift": "DON",
"screen.sanctuary.quest_request.reward_type.payment": "PAIEMENT",
"screen.sanctuary.quest_request.reward_type.loan": "PRÊT",
@@ -37,7 +37,13 @@
"screen.sanctuary.quest_request.reward_hint.payment": "anotherworld:ruby",
"screen.sanctuary.quest_request.reward_hint.loan": "minecraft:elytra",
"screen.sanctuary.quest_request.publish": "Publier",
"screen.sanctuary.quest_request.help": "Clic droit : détails · accepter ensuite dans le chat",
"screen.sanctuary.quest_request.help": "La récompense est retirée de ton inventaire à la publication",
"screen.sanctuary.quest_request.confirm.title": "Confirmer la demande",
"screen.sanctuary.quest_request.confirm.accept": "Accepter la quête",
"screen.sanctuary.quest_request.confirm.objective": "Objectif : %s",
"screen.sanctuary.quest_request.confirm.mission": "Mission : %s · %s × %s",
"screen.sanctuary.quest_request.confirm.reward": "Récompense : %s · %s × %s",
"screen.sanctuary.quest_request.confirm.help": "La récompense est déjà sécurisée et sera livrée dans ta Mailbox",
"message.sanctuary.black_market.banned": "Vous êtes banni du Black Market.",
"message.sanctuary.black_market.protected_sign": "Seuls le vendeur et les opérateurs peuvent retirer ce panneau d'annonce.",
"message.sanctuary.black_market.modified": "Annonce %s modifiée.",
@@ -85,18 +91,33 @@
"message.sanctuary.quest_request.details.header": "— Demande de %s —",
"message.sanctuary.quest_request.details.objective": "Objectif : %s",
"message.sanctuary.quest_request.details.mission": "Mission : %s · %s × %s",
"message.sanctuary.quest_request.details.reward": "Récompense promise : %s · %s × %s",
"message.sanctuary.quest_request.details.reward_legacy": "Aucune récompense indiquée sur cette ancienne demande.",
"message.sanctuary.quest_request.details.reward": "Récompense sous séquestre : %s · %s × %s",
"message.sanctuary.quest_request.details.reward_legacy": "Cette ancienne demande doit être modifiée par son auteur pour déposer sa récompense.",
"message.sanctuary.quest_request.details.progress": "Progression : %s/%s.",
"message.sanctuary.quest_request.details.completed": "Demande terminée · récompense expédiée.",
"message.sanctuary.quest_request.accept": "[ACCEPTER LA QUÊTE]",
"message.sanctuary.quest_request.accept_hover": "Clique pour confirmer après avoir lu tous les détails.",
"message.sanctuary.quest_request.accept_prompt": "Lis les détails puis confirme dans la fenêtre d'acceptation.",
"message.sanctuary.quest_request.claimed_by": "Prise par %s.",
"message.sanctuary.quest_request.too_far": "Tu es trop loin du panneau de demande.",
"message.sanctuary.quest_request.missing": "Ce panneau de demande n'existe plus.",
"message.sanctuary.quest_request.not_owner": "Seul le demandeur peut modifier ce panneau libre.",
"message.sanctuary.quest_request.invalid": "La demande contient une valeur vide ou hors limites.",
"message.sanctuary.quest_request.invalid_target": "Cible inconnue : utilise un ID de mob/item valide ou un pseudonyme de joueur valide.",
"message.sanctuary.quest_request.client_required": "Le client Sanctuary 26.2 à jour est requis pour configurer ce panneau.",
"message.sanctuary.quest_request.created": "Demande de quête publiée.",
"message.sanctuary.quest_request.client_required": "Le client Sanctuary 26.2 à jour est requis pour configurer ou accepter ce panneau.",
"message.sanctuary.quest_request.created": "Demande publiée. La récompense est maintenant sous séquestre.",
"message.sanctuary.quest_request.not_funded": "Cette demande n'a pas encore de récompense déposée.",
"message.sanctuary.quest_request.missing_reward": "Tu ne possèdes pas toute la récompense demandée dans tes rangées d'inventaire débloquées.",
"message.sanctuary.quest_request.missing_delivery": "Place toute la livraison demandée dans tes rangées d'inventaire débloquées puis reclique le panneau.",
"message.sanctuary.quest_request.pay_target_cannot_claim": "Le joueur qui doit confirmer le paiement ne peut pas prendre cette demande.",
"message.sanctuary.quest_request.progress": "Progression de la demande : %s/%s.",
"message.sanctuary.quest_request.completion.kill": "Les éliminations de %s sont comptées automatiquement.",
"message.sanctuary.quest_request.completion.bring": "Reviens avec les objets %s et fais un clic droit sur le panneau.",
"message.sanctuary.quest_request.completion.pay": "%s confirmera le paiement par un clic droit sur le panneau.",
"message.sanctuary.quest_request.reward_queued": "Demande terminée : %s × %s est expédié vers ta Mailbox.",
"message.sanctuary.quest_request.payment_confirmed": "Paiement confirmé ; la récompense du preneur est expédiée.",
"message.sanctuary.quest_request.reward_refunded": "Le panneau a été récupéré ; sa récompense revient par ta Mailbox.",
"message.sanctuary.quest_request.completed": "Cette demande est déjà terminée. Son propriétaire peut récupérer le panneau.",
"message.sanctuary.quest_request.action.kill": "Tuer",
"message.sanctuary.quest_request.action.bring": "Ramener",
"message.sanctuary.quest_request.action.pay": "Payer",
@@ -423,6 +444,7 @@
"sign.sanctuary.quest_request.action.pay": "PAYER",
"sign.sanctuary.quest_request.available": "DISPONIBLE",
"sign.sanctuary.quest_request.claimed": "PRISE",
"sign.sanctuary.quest_request.completed": "TERMINÉE",
"sign.sanctuary.quest_request.details": "Clic : détails",
"message.sanctuary.companion.active_locked": "Débloquez le pouvoir actif du familier dans Progression pour %s niveaux d'XP.",
"message.sanctuary.shop.delivery_box_registered": "Boîte aux lettres enregistrée en %s, %s, %s.",
@@ -20,7 +20,7 @@
"screen.sanctuary.quest_request.target_hint.kill.player": "Имя игрока-цели",
"screen.sanctuary.quest_request.target_hint.bring.item": "minecraft:wheat",
"screen.sanctuary.quest_request.target_hint.pay.player": "Имя получателя",
"screen.sanctuary.quest_request.reward": "Обещанная награда: вид · количество · предмет",
"screen.sanctuary.quest_request.reward": "Внесённая награда: вид · количество · предмет",
"screen.sanctuary.quest_request.reward_type.gift": "ПОДАРОК",
"screen.sanctuary.quest_request.reward_type.payment": "ОПЛАТА",
"screen.sanctuary.quest_request.reward_type.loan": "ВЗАЙМЫ",
@@ -30,7 +30,13 @@
"screen.sanctuary.quest_request.reward_hint.payment": "anotherworld:ruby",
"screen.sanctuary.quest_request.reward_hint.loan": "minecraft:elytra",
"screen.sanctuary.quest_request.publish": "Опубликовать",
"screen.sanctuary.quest_request.help": КМ: подробности · затем принять в чате",
"screen.sanctuary.quest_request.help": ри публикации награда изымается из инвентаря",
"screen.sanctuary.quest_request.confirm.title": "Подтвердить запрос",
"screen.sanctuary.quest_request.confirm.accept": "Принять задание",
"screen.sanctuary.quest_request.confirm.objective": "Цель: %s",
"screen.sanctuary.quest_request.confirm.mission": "Задание: %s · %s × %s",
"screen.sanctuary.quest_request.confirm.reward": "Награда: %s · %s × %s",
"screen.sanctuary.quest_request.confirm.help": "Награда уже защищена и будет доставлена в ваш почтовый ящик",
"screen.sanctuary.atlas": "Атлас",
"screen.sanctuary.atlas.chunk_count": "Посещённые чанки: %s (видно: %s)",
"screen.sanctuary.atlas.marker_count": "Видимые этапы: %s",
@@ -350,6 +356,7 @@
"sign.sanctuary.quest_request.action.pay": "ПЛАТИТЬ",
"sign.sanctuary.quest_request.available": "СВОБОДНО",
"sign.sanctuary.quest_request.claimed": "ЗАНЯТО",
"sign.sanctuary.quest_request.completed": "ВЫПОЛНЕНО",
"sign.sanctuary.quest_request.details": "ПКМ: детали",
"message.sanctuary.quest_request.occupied": "На этой табличке уже есть запрос.",
"message.sanctuary.quest_request.protected": "Только заказчик или оператор может забрать эту табличку.",
@@ -362,18 +369,33 @@
"message.sanctuary.quest_request.details.header": "— Запрос от %s —",
"message.sanctuary.quest_request.details.objective": "Цель: %s",
"message.sanctuary.quest_request.details.mission": "Задание: %s · %s × %s",
"message.sanctuary.quest_request.details.reward": "Обещанная награда: %s · %s × %s",
"message.sanctuary.quest_request.details.reward_legacy": "В этом старом запросе награда не указана.",
"message.sanctuary.quest_request.details.reward": "Награда на хранении: %s · %s × %s",
"message.sanctuary.quest_request.details.reward_legacy": "Автор должен изменить этот старый запрос и внести награду.",
"message.sanctuary.quest_request.details.progress": "Прогресс: %s/%s.",
"message.sanctuary.quest_request.details.completed": "Запрос выполнен · награда отправлена.",
"message.sanctuary.quest_request.accept": "[ПРИНЯТЬ ЗАДАНИЕ]",
"message.sanctuary.quest_request.accept_hover": "Нажмите, чтобы подтвердить после чтения всех подробностей.",
"message.sanctuary.quest_request.accept_prompt": "Прочитайте подробности и подтвердите принятие в открывшемся окне.",
"message.sanctuary.quest_request.claimed_by": "Взял: %s.",
"message.sanctuary.quest_request.too_far": "Вы слишком далеко от таблички запроса.",
"message.sanctuary.quest_request.missing": "Этой таблички запроса больше нет.",
"message.sanctuary.quest_request.not_owner": "Только заказчик может изменять свободную табличку.",
"message.sanctuary.quest_request.invalid": "В запросе есть пустое или недопустимое значение.",
"message.sanctuary.quest_request.invalid_target": "Неизвестная цель: укажите ID моба/предмета или допустимое имя игрока.",
"message.sanctuary.quest_request.client_required": "Для настройки нужен актуальный клиент Sanctuary 26.2.",
"message.sanctuary.quest_request.created": "Запрос задания опубликован.",
"message.sanctuary.quest_request.client_required": "Для настройки или принятия нужен актуальный клиент Sanctuary 26.2.",
"message.sanctuary.quest_request.created": "Запрос опубликован. Награда помещена на хранение.",
"message.sanctuary.quest_request.not_funded": "Для этого запроса награда ещё не внесена.",
"message.sanctuary.quest_request.missing_reward": "В открытых рядах инвентаря нет всей указанной награды.",
"message.sanctuary.quest_request.missing_delivery": "Положите всю доставку в открытые ряды инвентаря и снова нажмите на табличку.",
"message.sanctuary.quest_request.pay_target_cannot_claim": "Игрок, подтверждающий оплату, не может взять этот запрос.",
"message.sanctuary.quest_request.progress": "Прогресс запроса: %s/%s.",
"message.sanctuary.quest_request.completion.kill": "Убийства цели %s учитываются автоматически.",
"message.sanctuary.quest_request.completion.bring": "Вернитесь с предметами %s и нажмите ПКМ по табличке.",
"message.sanctuary.quest_request.completion.pay": "%s подтверждает оплату нажатием ПКМ по табличке.",
"message.sanctuary.quest_request.reward_queued": "Запрос выполнен: %s × %s отправляется в ваш почтовый ящик.",
"message.sanctuary.quest_request.payment_confirmed": "Оплата подтверждена; награда исполнителя отправлена.",
"message.sanctuary.quest_request.reward_refunded": "Табличка снята; награда возвращается через ваш почтовый ящик.",
"message.sanctuary.quest_request.completed": "Этот запрос уже выполнен. Владелец может забрать табличку.",
"message.sanctuary.quest_request.action.kill": "Убить",
"message.sanctuary.quest_request.action.bring": "Принести",
"message.sanctuary.quest_request.action.pay": "Заплатить",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 B

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 429 B

+1 -1
View File
@@ -33,7 +33,7 @@
"system_type": "gameplay-system",
"namespace": "sanctuary",
"data_version": 11,
"network_protocol": 20,
"network_protocol": 21,
"lifecycle": "${lifecycle}",
"pack_version": "${pack_version}",
"descriptions": {
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.sanctuary.shop;
import java.util.Optional;
import java.util.UUID;
import java.util.List;
/** Pure ownership, configuration and reservation smoke for community requests. */
public final class QuestRequestModelSmoke {
@@ -11,27 +12,32 @@ public final class QuestRequestModelSmoke {
UUID owner = UUID.fromString("6c43c35a-3498-41bb-a77e-10c679e635d1");
UUID claimant = UUID.fromString("8ff11ade-a604-4d77-a770-d487684367a1");
QuestRequestData.Request draft = new QuestRequestData.Request("request", "minecraft:overworld",
42L, true, owner.toString(), "Koka", "", QuestRequestService.KILL,
42L, true, owner.toString(), 0xCC77AA, "Koka", "", QuestRequestService.KILL,
QuestRequestService.MOB, "", 1, QuestRequestService.REWARD_GIFT, "", 1,
false, Optional.empty());
false, Optional.empty(), List.of(), 0, false);
QuestRequestData data = new QuestRequestData();
require(data.add(draft), "Request draft creation failed");
require(!data.add(draft), "Duplicate request position accepted");
QuestRequestData.Request configured = draft.withDetails("Koka", "Nettoyer les ruines",
QuestRequestData.Request configured = draft.withDetails("Koka", 0xCC77AA, "Nettoyer les ruines",
QuestRequestService.KILL, QuestRequestService.PLAYER, "Hunter", 1,
QuestRequestService.REWARD_LOAN, "minecraft:elytra", 1);
QuestRequestService.REWARD_LOAN, "minecraft:elytra", 1,
List.of());
require(data.replace(configured), "Request configuration failed");
require(configured.targetKind().equals(QuestRequestService.PLAYER),
"Player kill targets must be preserved");
require(configured.rewardType().equals(QuestRequestService.REWARD_LOAN)
&& configured.rewardTarget().equals("minecraft:elytra"),
"Loan rewards must be preserved");
"Loan reward metadata must be preserved");
require(configured.ownerColor() == 0xCC77AA, "Requester color must be preserved");
QuestRequestData.Claim claim = new QuestRequestData.Claim(claimant.toString(), "Hunter",
0x44AAFF, 10L);
QuestRequestData.Request claimed = configured.withClaim(Optional.of(claim));
require(data.replace(claimed), "Request claim failed");
require(data.requestAt("minecraft:overworld", 42L).orElseThrow().claim().orElseThrow()
.playerId().equals(claimant), "Claimant UUID was not preserved");
QuestRequestData.Request completed = claimed.withProgress(1).withCompletion();
require(completed.completed() && completed.progress() == completed.quantity()
&& completed.rewardEscrow().isEmpty(), "Completion must consume escrow exactly once");
require(data.replace(claimed.withClaim(Optional.empty())), "Request release failed");
require(data.remove(data.request("request").orElseThrow()), "Recovering the request sign failed");
}