feat(sanctuary): ajouter un suivi d'avancement des quetes #86
@@ -6,6 +6,14 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
- Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46.
|
||||
- Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées.
|
||||
|
||||
## Non distribué
|
||||
|
||||
Module modifié : **Sanctuary `0.0.0-alpha.123`**. Le pack public reste temporairement en `26.2.0-alpha.221` jusqu'à la publication.
|
||||
|
||||
- ajoute un suivi d'avancement des quêtes, affichable ou masquable par une touche configurable (`J` par défaut) ;
|
||||
- synchronise un snapshot borné des quêtes acceptées et des contrats réservés, sans les demandes que le joueur a lui-même créées, ni changer les données Sanctuary v11 ni le protocole réseau 22 ;
|
||||
- propose une saisie assistée des IDs d'item et de mob dans l'éditeur de demande, avec icône et noms proches.
|
||||
|
||||
## `26.2.0-alpha.221`
|
||||
|
||||
Module modifié : **Ambiance `0.0.0-alpha.22`**. Les neuf autres modules conservent leur version issue de l'alpha.220.
|
||||
|
||||
+17
-13
@@ -3438,7 +3438,7 @@ tasks.register("verifyProgressivePauseMenuRelease") {
|
||||
"sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))]
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"]
|
||||
@@ -4279,7 +4279,7 @@ tasks.register("verifyShopPricingLayoutRelease") {
|
||||
def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.192"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.193"
|
||||
@@ -4373,7 +4373,7 @@ tasks.register("verifyShopCsvBossGatesRelease") {
|
||||
def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.194"
|
||||
|| migration.source?.modules != [onlyfun: "0.0.0-alpha.18", sanctuary: "0.0.0-alpha.110"]
|
||||
@@ -4465,7 +4465,7 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") {
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| 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"]
|
||||
@@ -4565,7 +4565,7 @@ tasks.register("verifyQuestRequestContractsRelease") {
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| 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"
|
||||
@@ -4685,7 +4685,7 @@ tasks.register("verifyQuestRequestEscrowRelease") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| 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"
|
||||
@@ -4742,7 +4742,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.16"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.source?.modules != [
|
||||
itsalive: "0.0.0-alpha.30",
|
||||
@@ -4900,7 +4900,7 @@ tasks.register("verifySafeVaultFeature") {
|
||||
}.keySet()
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| migration.schema_version != 1
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.202"
|
||||
@@ -5759,7 +5759,7 @@ tasks.register("verifyAlpha210Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.28.0.193.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.25"
|
||||
|| rootProject.iliketomoveit_version != currentModules.iliketomoveit
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
@@ -5858,7 +5858,7 @@ tasks.register("verifyAlpha211Release") {
|
||||
onlyfun: "0.0.0-alpha.25",
|
||||
ambiance: "0.0.0-alpha.22",
|
||||
iliketomoveit: "0.0.0-alpha.16"]
|
||||
def workspaceModules = currentModules
|
||||
def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.123"]
|
||||
def expectedDataVersions = [ambiance: 2, itsalive: 3, onlyfun: 2, ouch: 1, sanctuary: 11]
|
||||
def expectedProtocols = [ambiance: 6, itsalive: 4, onlyfun: 3, ouch: 1, sanctuary: 22]
|
||||
def manifests = expectedTarget.collectEntries { id, version ->
|
||||
@@ -5956,7 +5956,7 @@ tasks.register("verifyAlpha212Release") {
|
||||
it.path == "mods/jei-26.2-fabric-30.28.0.193.jar"
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.122"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.211"
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.119"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.212"
|
||||
@@ -6446,9 +6446,10 @@ tasks.register("verifyAlpha218Release") {
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| currentModules.findAll { id, version -> id != "redstoner" }.any {
|
||||
|| currentModules.findAll { id, version -> id != "redstoner" && id != "sanctuary" }.any {
|
||||
id, version -> rootProject.property("${id}_version") != version
|
||||
}
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.217"
|
||||
|| migration.source?.modules != sourceModules
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.218"
|
||||
@@ -6546,7 +6547,10 @@ tasks.register("verifyAlpha219Release") {
|
||||
exclude "**/build/**", "**/.gradle/**", "**/.git/**"
|
||||
}.files
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| currentModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| currentModules.findAll { id, version -> id != "sanctuary" }.any {
|
||||
id, version -> rootProject.property("${id}_version") != version
|
||||
}
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.123"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.218"
|
||||
|| migration.source?.modules != sourceModules
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.219"
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ iliketomoveit_version=0.0.0-alpha.16
|
||||
iliketomoveit_lifecycle=active
|
||||
onlyfun_version=0.0.0-alpha.25
|
||||
onlyfun_lifecycle=active
|
||||
sanctuary_version=0.0.0-alpha.122
|
||||
sanctuary_version=0.0.0-alpha.123
|
||||
sanctuary_lifecycle=active
|
||||
pack_version=26.2.0-alpha.221
|
||||
maven_group=fr.koka99cab.sanctuary26
|
||||
|
||||
+32
-12
@@ -68,6 +68,22 @@ tasks.register("questRequestModelSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.shop.QuestRequestModelSmoke"
|
||||
}
|
||||
|
||||
tasks.register("questTrackerModelSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks the bounded quest-tracker snapshot used by the HUD concept."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.shop.QuestTrackerModelSmoke"
|
||||
}
|
||||
|
||||
tasks.register("questIdSuggestionsSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks accent-folded quest-panel item name ranking and inventory pins."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.shop.QuestIdSuggestionsSmoke"
|
||||
}
|
||||
|
||||
tasks.register("shopHeaderLayoutSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks that the daily Shop timer and unlocked-offer status cannot overlap."
|
||||
@@ -135,7 +151,7 @@ tasks.register("verifyBlackMarket") {
|
||||
def questData = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardData.java").text
|
||||
def quests = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/QuestBoardService.java").text
|
||||
def inventoryView = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/inventory/SanctuaryPlayerInventoryView.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| !data.contains('SAVE_KEY = "black_market"') || !data.contains("DATA_VERSION = 2")
|
||||
|| !data.contains("ItemStack.CODEC.fieldOf(\"item\")") || !data.contains("replaceListing")
|
||||
|| !service.contains("LISTING_FEE_RUBIES = 1") || !service.contains("COMMISSION_PERCENT = 5")
|
||||
@@ -278,7 +294,9 @@ tasks.register("verifySanctuary") {
|
||||
inputs.files(fileTree("tools"))
|
||||
dependsOn tasks.named("progressionModelSmoke"), tasks.named("serverIdentityModelSmoke"),
|
||||
tasks.named("shopDeliveryTimingSmoke"), tasks.named("blackMarketModelSmoke"),
|
||||
tasks.named("questBoardModelSmoke"), tasks.named("shopHeaderLayoutSmoke"), tasks.named("zombieNamesSmoke"),
|
||||
tasks.named("questBoardModelSmoke"), tasks.named("questTrackerModelSmoke"),
|
||||
tasks.named("questIdSuggestionsSmoke"),
|
||||
tasks.named("shopHeaderLayoutSmoke"), tasks.named("zombieNamesSmoke"),
|
||||
tasks.named("capeJoinSyncSmoke"),
|
||||
tasks.named("verifyBlackMarket")
|
||||
|
||||
@@ -2832,7 +2850,7 @@ tasks.register("verifyShopCurrencyRowsRelease") {
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/BlackMarketService.java").text
|
||||
def shop = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopService.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.117"
|
||||
|| migration.shop_currency_inventory?.all_unlocked_rows_counted != true
|
||||
|| migration.shop_currency_inventory?.overflow_rows_counted != true
|
||||
@@ -2998,7 +3016,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.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| 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
|
||||
@@ -3036,7 +3054,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.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| 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]
|
||||
@@ -3067,7 +3085,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.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.195"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|
||||
|| migration.compatibility?.sanctuary_data_version != 11
|
||||
@@ -3098,7 +3116,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.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|
||||
|| migration.mailbox?.id != "sanctuary:delivery_box"
|
||||
|| migration.mailbox?.horizontal_facing != true
|
||||
@@ -3126,7 +3144,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.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| !transfer.contains("shouldAppendOverflow")
|
||||
|| !transfer.contains("isVanillaStorageRange")
|
||||
|| !transfer.contains("hotbarStart")
|
||||
@@ -3172,7 +3190,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.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|
||||
|| migration.quest_requests?.save_key != "quest_requests"
|
||||
|| migration.quest_requests?.data_version != 1
|
||||
@@ -3230,7 +3248,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.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| 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"
|
||||
@@ -3299,7 +3317,7 @@ tasks.register("verifyQuestRequestEscrow") {
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
|
||||
[(locale): new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${locale}.json"))]
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| 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"
|
||||
@@ -3371,7 +3389,7 @@ tasks.register("verifyAlphaIndev") {
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
|
||||
new JsonSlurper().parse(file("src/main/resources/assets/sanctuary/lang/${it}.json"))
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.122"
|
||||
if (project.version.toString() != "0.0.0-alpha.123"
|
||||
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.122"
|
||||
|| migration.alpha_world?.dimension != "sanctuary:alpha"
|
||||
|| migration.alpha_world?.legacy_dimension != "portals:alpha"
|
||||
@@ -3402,6 +3420,8 @@ tasks.named("check") {
|
||||
dependsOn tasks.named("verifyShopCurrencyRowsRelease")
|
||||
dependsOn tasks.named("celestialGraphSmoke")
|
||||
dependsOn tasks.named("questRequestModelSmoke")
|
||||
dependsOn tasks.named("questTrackerModelSmoke")
|
||||
dependsOn tasks.named("questIdSuggestionsSmoke")
|
||||
dependsOn tasks.named("alphaIndevGeneratorSmoke")
|
||||
dependsOn tasks.named("verifyAlphaIndev")
|
||||
}
|
||||
|
||||
+310
-10
@@ -1,14 +1,29 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.inventory.SanctuaryPlayerInventoryView;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestEditorPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestSubmitPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestIdSuggestions;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestRequestService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
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.components.EditBox;
|
||||
import net.minecraft.client.gui.components.events.GuiEventListener;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
/** Complete bounded editor for mission target and promised reward contracts. */
|
||||
public final class QuestRequestEditorScreen extends Screen {
|
||||
@@ -19,8 +34,12 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
QuestRequestService.REWARD_GIFT, QuestRequestService.REWARD_PAYMENT,
|
||||
QuestRequestService.REWARD_LOAN
|
||||
};
|
||||
private static final int SUGGESTION_ROW = 16;
|
||||
private static final int SUGGESTION_MARGIN = 4;
|
||||
private final Screen parent;
|
||||
private final QuestRequestEditorPayload payload;
|
||||
private final List<QuestIdSuggestions.Entry> itemCatalog = new ArrayList<>();
|
||||
private final List<QuestIdSuggestions.Entry> mobCatalog = new ArrayList<>();
|
||||
private EditBox alias;
|
||||
private EditBox objective;
|
||||
private EditBox target;
|
||||
@@ -34,6 +53,10 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
private int actionIndex;
|
||||
private int rewardIndex;
|
||||
private int killTargetIndex;
|
||||
private int selectedSuggestion;
|
||||
private boolean applyingCompletion;
|
||||
private boolean suggestionsOpen;
|
||||
private List<QuestIdSuggestions.Match> suggestions = List.of();
|
||||
|
||||
public QuestRequestEditorScreen(Screen parent, QuestRequestEditorPayload payload) {
|
||||
super(Component.translatable("screen.sanctuary.quest_request.title"));
|
||||
@@ -51,6 +74,7 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
@Override
|
||||
protected void init() {
|
||||
int center = width / 2;
|
||||
fillCatalogs();
|
||||
alias = addRenderableWidget(new EditBox(font, center - 152, 48, 304, 20,
|
||||
Component.translatable("screen.sanctuary.quest_request.alias")));
|
||||
alias.setMaxLength(QuestRequestService.MAX_ALIAS_LENGTH);
|
||||
@@ -80,15 +104,17 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
quantity.setMaxLength(4);
|
||||
quantity.setValue(Integer.toString(payload.quantity()));
|
||||
|
||||
target = addRenderableWidget(new EditBox(font, center + 52, 118, 100, 20,
|
||||
target = addRenderableWidget(new EditBox(font, center + 70, 118, 82, 20,
|
||||
Component.translatable("screen.sanctuary.quest_request.target")));
|
||||
target.setMaxLength(QuestRequestService.MAX_TARGET_LENGTH);
|
||||
target.setValue(payload.target());
|
||||
target.setResponder(value -> onIdTyped());
|
||||
|
||||
reward = addRenderableWidget(Button.builder(rewardLabel(), button -> {
|
||||
rewardIndex = (rewardIndex + 1) % REWARDS.length;
|
||||
button.setMessage(rewardLabel());
|
||||
rewardTarget.setHint(rewardHint());
|
||||
closeSuggestions();
|
||||
}).bounds(center - 152, 157, 84, 20).build());
|
||||
|
||||
rewardQuantity = addRenderableWidget(new EditBox(font, center - 64, 157, 46, 20,
|
||||
@@ -96,11 +122,12 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
rewardQuantity.setMaxLength(4);
|
||||
rewardQuantity.setValue(Integer.toString(payload.rewardQuantity()));
|
||||
|
||||
rewardTarget = addRenderableWidget(new EditBox(font, center - 14, 157, 166, 20,
|
||||
rewardTarget = addRenderableWidget(new EditBox(font, center + 8, 157, 144, 20,
|
||||
Component.translatable("screen.sanctuary.quest_request.reward_target")));
|
||||
rewardTarget.setMaxLength(QuestRequestService.MAX_TARGET_LENGTH);
|
||||
rewardTarget.setValue(payload.rewardTarget());
|
||||
rewardTarget.setHint(rewardHint());
|
||||
rewardTarget.setResponder(value -> onIdTyped());
|
||||
|
||||
submit = addRenderableWidget(Button.builder(Component.translatable("screen.sanctuary.quest_request.publish"),
|
||||
button -> submit()).bounds(center - 152, 199, 149, 20).build());
|
||||
@@ -112,9 +139,16 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (suggestionsOpen) refreshSuggestions();
|
||||
submit.active = values() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFocused(GuiEventListener listener) {
|
||||
super.setFocused(listener);
|
||||
if (listener != target && listener != rewardTarget) closeSuggestions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float tickDelta) {
|
||||
int center = width / 2;
|
||||
@@ -129,9 +163,53 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
center - 152, 107, 0xFFDBB7CA);
|
||||
graphics.text(font, Component.translatable("screen.sanctuary.quest_request.reward"),
|
||||
center - 152, 146, 0xFFF2C4D7);
|
||||
graphics.centeredText(font, Component.translatable("screen.sanctuary.quest_request.help"),
|
||||
center, 183, 0xFF9F8B95);
|
||||
graphics.centeredText(font, Component.translatable(idFieldFocused()
|
||||
? "screen.sanctuary.quest_request.id_help"
|
||||
: "screen.sanctuary.quest_request.help"), center, 183, 0xFF9F8B95);
|
||||
drawPreview(graphics, target, targetPreview(mouseX, mouseY), center + 52, 120);
|
||||
drawPreview(graphics, rewardTarget, rewardPreview(mouseX, mouseY), center - 14, 159);
|
||||
super.extractRenderState(graphics, mouseX, mouseY, tickDelta);
|
||||
renderSuggestions(graphics, mouseX, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(KeyEvent event) {
|
||||
if (!suggestions.isEmpty()) {
|
||||
if (event.key() == GLFW.GLFW_KEY_DOWN) {
|
||||
selectedSuggestion = (selectedSuggestion + 1) % suggestions.size();
|
||||
return true;
|
||||
}
|
||||
if (event.key() == GLFW.GLFW_KEY_UP) {
|
||||
selectedSuggestion = (selectedSuggestion + suggestions.size() - 1) % suggestions.size();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
int index = suggestionIndexAt(event.x(), event.y());
|
||||
if (index >= 0) {
|
||||
applySuggestion(index);
|
||||
return true;
|
||||
}
|
||||
boolean handled = super.mouseClicked(event, doubleClick);
|
||||
if (clickedSearchField(event.x(), event.y())) {
|
||||
suggestionsOpen = true;
|
||||
refreshSuggestions();
|
||||
} else closeSuggestions();
|
||||
return handled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
if (!suggestions.isEmpty() && suggestionPanel().contains(mouseX, mouseY)) {
|
||||
int delta = scrollY > 0 ? -1 : 1;
|
||||
selectedSuggestion = Math.floorMod(selectedSuggestion + delta, suggestions.size());
|
||||
return true;
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
private void refreshMissionControls() {
|
||||
@@ -139,6 +217,7 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
targetKind.setMessage(targetKindLabel());
|
||||
targetKind.active = QuestRequestService.KILL.equals(currentAction());
|
||||
target.setHint(targetHint());
|
||||
closeSuggestions();
|
||||
}
|
||||
|
||||
private String currentAction() { return ACTIONS[actionIndex]; }
|
||||
@@ -172,14 +251,16 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
}
|
||||
|
||||
private Values values() {
|
||||
if (objective.getValue().isBlank() || target.getValue().isBlank()
|
||||
|| rewardTarget.getValue().isBlank()) return null;
|
||||
if (objective.getValue().isBlank()) return null;
|
||||
String resolvedTarget = resolveTarget();
|
||||
String resolvedReward = QuestIdSuggestions.resolve(itemCatalog, rewardTarget.getValue(), inventoryIds());
|
||||
if (resolvedTarget == null || resolvedReward == null) return null;
|
||||
try {
|
||||
int amount = Integer.parseInt(quantity.getValue());
|
||||
int rewardAmount = Integer.parseInt(rewardQuantity.getValue());
|
||||
if (amount < 1 || amount > QuestRequestService.MAX_QUANTITY
|
||||
|| rewardAmount < 1 || rewardAmount > QuestRequestService.MAX_QUANTITY) return null;
|
||||
return new Values(amount, rewardAmount);
|
||||
return new Values(resolvedTarget, amount, resolvedReward, rewardAmount);
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
@@ -189,13 +270,232 @@ public final class QuestRequestEditorScreen extends Screen {
|
||||
Values values = values();
|
||||
if (values == null || !ClientPlayNetworking.canSend(QuestRequestSubmitPayload.ID)) return;
|
||||
ClientPlayNetworking.send(new QuestRequestSubmitPayload(payload.pos(), alias.getValue(),
|
||||
objective.getValue(), currentAction(), currentTargetKind(), target.getValue(), values.quantity(),
|
||||
currentReward(), rewardTarget.getValue(), values.rewardQuantity()));
|
||||
objective.getValue(), currentAction(), currentTargetKind(), values.target(), values.quantity(),
|
||||
currentReward(), values.rewardTarget(), values.rewardQuantity()));
|
||||
onClose();
|
||||
}
|
||||
|
||||
private void fillCatalogs() {
|
||||
itemCatalog.clear();
|
||||
for (Item item : BuiltInRegistries.ITEM) {
|
||||
if (item == Items.AIR) continue;
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(item);
|
||||
itemCatalog.add(new QuestIdSuggestions.Entry(id.toString(), id.getPath(),
|
||||
new ItemStack(item).getHoverName().getString()));
|
||||
}
|
||||
mobCatalog.clear();
|
||||
for (var type : BuiltInRegistries.ENTITY_TYPE) {
|
||||
Identifier id = BuiltInRegistries.ENTITY_TYPE.getKey(type);
|
||||
if ("minecraft:player".equals(id.toString())) continue;
|
||||
mobCatalog.add(new QuestIdSuggestions.Entry(id.toString(), id.getPath(),
|
||||
type.getDescription().getString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void onIdTyped() {
|
||||
if (applyingCompletion) return;
|
||||
suggestionsOpen = !focusedSearchValue().isBlank();
|
||||
refreshSuggestions();
|
||||
}
|
||||
|
||||
private void closeSuggestions() {
|
||||
suggestionsOpen = false;
|
||||
suggestions = List.of();
|
||||
selectedSuggestion = 0;
|
||||
}
|
||||
|
||||
private void refreshSuggestions() {
|
||||
if (applyingCompletion || !suggestionsOpen) {
|
||||
if (!suggestionsOpen) suggestions = List.of();
|
||||
return;
|
||||
}
|
||||
EditBox field = focusedIdField();
|
||||
if (field == null || field.getValue().isBlank()) {
|
||||
closeSuggestions();
|
||||
return;
|
||||
}
|
||||
boolean itemField = field == rewardTarget || QuestRequestService.ITEM.equals(currentTargetKind());
|
||||
List<QuestIdSuggestions.Match> next = QuestIdSuggestions.suggest(
|
||||
itemField ? itemCatalog : mobCatalog, field.getValue(),
|
||||
itemField ? inventoryIds() : Set.of(), QuestIdSuggestions.LIMIT);
|
||||
if (!next.equals(suggestions)) {
|
||||
suggestions = next;
|
||||
selectedSuggestion = 0;
|
||||
} else if (selectedSuggestion >= suggestions.size()) {
|
||||
selectedSuggestion = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void applySuggestion(int index) {
|
||||
EditBox field = focusedIdField();
|
||||
if (field == null || index < 0 || index >= suggestions.size()) return;
|
||||
writeId(field, suggestions.get(index).id());
|
||||
closeSuggestions();
|
||||
}
|
||||
|
||||
private boolean clickedSearchField(double mouseX, double mouseY) {
|
||||
if (contains(rewardTarget, mouseX, mouseY)) return true;
|
||||
return contains(target, mouseX, mouseY)
|
||||
&& !QuestRequestService.PLAYER.equals(currentTargetKind());
|
||||
}
|
||||
|
||||
private boolean contains(EditBox field, double mouseX, double mouseY) {
|
||||
return field != null && mouseX >= field.getX() && mouseX < field.getX() + field.getWidth()
|
||||
&& mouseY >= field.getY() && mouseY < field.getY() + field.getHeight();
|
||||
}
|
||||
|
||||
private String focusedSearchValue() {
|
||||
EditBox field = focusedIdField();
|
||||
return field == null ? "" : field.getValue();
|
||||
}
|
||||
|
||||
private void writeId(EditBox field, String id) {
|
||||
applyingCompletion = true;
|
||||
field.setValue(id);
|
||||
field.moveCursorToEnd(false);
|
||||
applyingCompletion = false;
|
||||
}
|
||||
|
||||
private String resolveTarget() {
|
||||
if (QuestRequestService.PLAYER.equals(currentTargetKind())) {
|
||||
String name = target.getValue().strip();
|
||||
return name.matches("[A-Za-z0-9_]{1,16}") ? name : null;
|
||||
}
|
||||
boolean item = QuestRequestService.ITEM.equals(currentTargetKind());
|
||||
return QuestIdSuggestions.resolve(item ? itemCatalog : mobCatalog, target.getValue(),
|
||||
item ? inventoryIds() : Set.of());
|
||||
}
|
||||
|
||||
private QuestIdSuggestions.Match targetPreview(int mouseX, int mouseY) {
|
||||
if (QuestRequestService.PLAYER.equals(currentTargetKind())) return null;
|
||||
return previewFor(target, mouseX, mouseY);
|
||||
}
|
||||
|
||||
private QuestIdSuggestions.Match rewardPreview(int mouseX, int mouseY) {
|
||||
return previewFor(rewardTarget, mouseX, mouseY);
|
||||
}
|
||||
|
||||
private QuestIdSuggestions.Match previewFor(EditBox field, int mouseX, int mouseY) {
|
||||
if (field == null) return null;
|
||||
if (field.isFocused() && !suggestions.isEmpty()) {
|
||||
int hover = suggestionIndexAt(mouseX, mouseY);
|
||||
return suggestions.get(hover >= 0 ? hover : selectedSuggestion);
|
||||
}
|
||||
boolean item = field == rewardTarget || QuestRequestService.ITEM.equals(currentTargetKind());
|
||||
return QuestIdSuggestions.exact(item ? itemCatalog : mobCatalog, field.getValue(),
|
||||
item ? inventoryIds() : Set.of());
|
||||
}
|
||||
|
||||
private void drawPreview(GuiGraphicsExtractor graphics, EditBox field,
|
||||
QuestIdSuggestions.Match match, int x, int y) {
|
||||
if (field == null || match == null) return;
|
||||
graphics.fill(x, y, x + 16, y + 16, 0x66101010);
|
||||
graphics.outline(x, y, 16, 16, 0x88E8A8C3);
|
||||
ItemStack stack = iconFor(match.id(), field == rewardTarget
|
||||
|| QuestRequestService.ITEM.equals(currentTargetKind()));
|
||||
if (!stack.isEmpty()) graphics.item(stack, x, y);
|
||||
}
|
||||
|
||||
private void renderSuggestions(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
if (suggestions.isEmpty()) return;
|
||||
Panel panel = suggestionPanel();
|
||||
graphics.fill(panel.x(), panel.y(), panel.x() + panel.width(), panel.y() + panel.height(), 0xF2181417);
|
||||
graphics.outline(panel.x(), panel.y(), panel.width(), panel.height(), 0xFFE8A8C3);
|
||||
boolean itemField = focusedIdField() == rewardTarget
|
||||
|| QuestRequestService.ITEM.equals(currentTargetKind());
|
||||
int hover = suggestionIndexAt(mouseX, mouseY);
|
||||
for (int index = 0; index < suggestions.size(); index++) {
|
||||
QuestIdSuggestions.Match match = suggestions.get(index);
|
||||
int top = panel.y() + 1 + index * SUGGESTION_ROW;
|
||||
if (index == selectedSuggestion || index == hover) {
|
||||
graphics.fill(panel.x() + 1, top, panel.x() + panel.width() - 1, top + SUGGESTION_ROW, 0x55E8A8C3);
|
||||
}
|
||||
ItemStack stack = iconFor(match.id(), itemField);
|
||||
if (!stack.isEmpty()) graphics.item(stack, panel.x() + 2, top);
|
||||
graphics.text(font, Component.literal(ellipsize(match.displayName(), panel.width() - 24)),
|
||||
panel.x() + 20, top + 4, 0xFFF2C4D7);
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack iconFor(String id, boolean item) {
|
||||
Identifier identifier = Identifier.tryParse(id);
|
||||
if (identifier == null) return ItemStack.EMPTY;
|
||||
if (item) {
|
||||
return BuiltInRegistries.ITEM.getOptional(identifier).map(ItemStack::new).orElse(ItemStack.EMPTY);
|
||||
}
|
||||
Identifier egg = Identifier.fromNamespaceAndPath(identifier.getNamespace(),
|
||||
identifier.getPath() + "_spawn_egg");
|
||||
return BuiltInRegistries.ITEM.getOptional(egg).map(ItemStack::new).orElse(ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
private Set<String> inventoryIds() {
|
||||
Set<String> ids = new LinkedHashSet<>();
|
||||
if (minecraft == null || minecraft.player == null) return ids;
|
||||
ItemStack held = minecraft.player.getMainHandItem();
|
||||
if (!held.isEmpty()) ids.add(BuiltInRegistries.ITEM.getKey(held.getItem()).toString());
|
||||
for (var slot : SanctuaryPlayerInventoryView.unlockedRows(minecraft.player)) {
|
||||
if (!slot.stack().isEmpty()) {
|
||||
ids.add(BuiltInRegistries.ITEM.getKey(slot.stack().getItem()).toString());
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private EditBox focusedIdField() {
|
||||
if (target != null && target.isFocused() && !QuestRequestService.PLAYER.equals(currentTargetKind())) {
|
||||
return target;
|
||||
}
|
||||
if (rewardTarget != null && rewardTarget.isFocused()) return rewardTarget;
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean idFieldFocused() {
|
||||
return focusedIdField() != null;
|
||||
}
|
||||
|
||||
private Panel suggestionPanel() {
|
||||
EditBox field = focusedIdField();
|
||||
if (field == null) return new Panel(0, 0, 0, 0);
|
||||
int rows = Math.max(1, suggestions.size());
|
||||
int height = rows * SUGGESTION_ROW + 2;
|
||||
int width = 200;
|
||||
int x = Math.max(SUGGESTION_MARGIN, Math.min(this.width / 2 - width / 2,
|
||||
this.width - width - SUGGESTION_MARGIN));
|
||||
int below = field.getY() + field.getHeight() + 2;
|
||||
int above = field.getY() - 2 - height;
|
||||
boolean preferAbove = field == rewardTarget;
|
||||
int y = preferAbove && above >= SUGGESTION_MARGIN ? above : below;
|
||||
if (y + height > this.height - SUGGESTION_MARGIN) y = above;
|
||||
y = Math.max(SUGGESTION_MARGIN, Math.min(y, this.height - height - SUGGESTION_MARGIN));
|
||||
return new Panel(x, y, width, height);
|
||||
}
|
||||
|
||||
private int suggestionIndexAt(double mouseX, double mouseY) {
|
||||
if (suggestions.isEmpty()) return -1;
|
||||
Panel panel = suggestionPanel();
|
||||
if (!panel.contains(mouseX, mouseY)) return -1;
|
||||
int index = (int) ((mouseY - panel.y() - 1) / SUGGESTION_ROW);
|
||||
return index >= 0 && index < suggestions.size() ? index : -1;
|
||||
}
|
||||
|
||||
private String ellipsize(String value, int maxWidth) {
|
||||
if (font.width(value) <= maxWidth) return value;
|
||||
String suffix = "…";
|
||||
int suffixWidth = font.width(suffix);
|
||||
StringBuilder cut = new StringBuilder(value);
|
||||
while (cut.length() > 0 && font.width(cut.toString()) + suffixWidth > maxWidth) {
|
||||
cut.deleteCharAt(cut.length() - 1);
|
||||
}
|
||||
return cut + suffix;
|
||||
}
|
||||
|
||||
@Override public void onClose() { if (minecraft != null) minecraft.gui.setScreen(parent); }
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
|
||||
private record Values(int quantity, int rewardQuantity) {}
|
||||
private record Values(String target, int quantity, String rewardTarget, int rewardQuantity) {}
|
||||
private record Panel(int x, int y, int width, int height) {
|
||||
boolean contains(double mouseX, double mouseY) {
|
||||
return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestTrackerSnapshotPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestTrackerSync;
|
||||
import java.util.List;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Compact toggleable overlay for accepted quests. */
|
||||
final class QuestTrackerHud {
|
||||
private static final int PANEL_WIDTH = 168;
|
||||
private static final int MARGIN = 6;
|
||||
private static final int HEADER_HEIGHT = 14;
|
||||
private static final int ENTRY_HEIGHT = 22;
|
||||
private static final int EMPTY_HEIGHT = 22;
|
||||
private static final int BACKGROUND = 0xD0181417;
|
||||
private static final int OUTLINE = 0xFFE8A8C3;
|
||||
private static final int TITLE = 0xFFF2C4D7;
|
||||
private static final int BODY = 0xFFE9DDE2;
|
||||
private static final int MUTED = 0xFF9F8B95;
|
||||
private static final int BAR_BACK = 0xFF3A2A30;
|
||||
|
||||
private QuestTrackerHud() {}
|
||||
|
||||
static void register() {
|
||||
HudElementRegistry.addLast(SanctuaryMod.id("quest_tracker"), QuestTrackerHud::render);
|
||||
}
|
||||
|
||||
private static void render(GuiGraphicsExtractor graphics, DeltaTracker tickCounter) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (client.player == null || client.level == null || client.gui.screen() != null) return;
|
||||
if (!SanctuaryClientQuests.visible()) return;
|
||||
List<QuestTrackerSnapshotPayload.Entry> entries = SanctuaryClientQuests.entries();
|
||||
int height = HEADER_HEIGHT + 6 + (entries.isEmpty() ? EMPTY_HEIGHT : entries.size() * ENTRY_HEIGHT);
|
||||
int left = MARGIN;
|
||||
int top = MARGIN;
|
||||
int right = left + PANEL_WIDTH;
|
||||
int bottom = top + height;
|
||||
graphics.fill(left, top, right, bottom, BACKGROUND);
|
||||
graphics.outline(left, top, PANEL_WIDTH, height, OUTLINE);
|
||||
graphics.text(client.font, Component.translatable("hud.sanctuary.quest_tracker.title"),
|
||||
left + 6, top + 4, TITLE);
|
||||
if (entries.isEmpty()) {
|
||||
graphics.text(client.font, Component.translatable("hud.sanctuary.quest_tracker.empty"),
|
||||
left + 6, top + HEADER_HEIGHT + 4, MUTED);
|
||||
return;
|
||||
}
|
||||
int y = top + HEADER_HEIGHT + 2;
|
||||
for (QuestTrackerSnapshotPayload.Entry entry : entries) {
|
||||
drawEntry(graphics, client.font, entry, left + 6, y, PANEL_WIDTH - 12);
|
||||
y += ENTRY_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawEntry(GuiGraphicsExtractor graphics, Font font,
|
||||
QuestTrackerSnapshotPayload.Entry entry, int x, int y, int width) {
|
||||
int accent = 0xFF000000 | (entry.accent() & 0xFFFFFF);
|
||||
graphics.text(font, ellipsize(font, title(entry), width), x, y, accent);
|
||||
Component progress = Component.translatable("hud.sanctuary.quest_tracker.progress",
|
||||
action(entry), describeTarget(entry), entry.progress(), entry.required());
|
||||
graphics.text(font, ellipsize(font, progress, width), x, y + 9, BODY);
|
||||
int barTop = y + 18;
|
||||
int barRight = x + width;
|
||||
graphics.fill(x, barTop, barRight, barTop + 2, BAR_BACK);
|
||||
int filled = Math.round(width * Math.min(1.0F, entry.progress() / (float) entry.required()));
|
||||
if (filled > 0) graphics.fill(x, barTop, x + filled, barTop + 2, accent);
|
||||
}
|
||||
|
||||
private static Component title(QuestTrackerSnapshotPayload.Entry entry) {
|
||||
if (!entry.title().isBlank()) return Component.literal(entry.title());
|
||||
return Component.translatable("hud.sanctuary.quest_tracker.source." + entry.source());
|
||||
}
|
||||
|
||||
private static Component action(QuestTrackerSnapshotPayload.Entry entry) {
|
||||
if (QuestTrackerSync.SOURCE_BOARD.equals(entry.source())) {
|
||||
return Component.translatable("sign.sanctuary.quest_board.action." + entry.action());
|
||||
}
|
||||
return Component.translatable("message.sanctuary.quest_request.action." + entry.action());
|
||||
}
|
||||
|
||||
private static Component describeTarget(QuestTrackerSnapshotPayload.Entry entry) {
|
||||
Identifier id = Identifier.tryParse(entry.target());
|
||||
if (id == null) return Component.literal(entry.target());
|
||||
return BuiltInRegistries.ENTITY_TYPE.getOptional(id).map(type -> type.getDescription())
|
||||
.or(() -> BuiltInRegistries.ITEM.getOptional(id).map(item -> new ItemStack(item).getHoverName()))
|
||||
.orElse(Component.literal(id.getPath()));
|
||||
}
|
||||
|
||||
private static Component ellipsize(Font font, Component text, int maxWidth) {
|
||||
if (font.width(text) <= maxWidth) return text;
|
||||
String value = text.getString();
|
||||
while (!value.isEmpty() && font.width(value + "…") > maxWidth) {
|
||||
value = value.substring(0, value.length() - 1);
|
||||
}
|
||||
return Component.literal(value + "…");
|
||||
}
|
||||
}
|
||||
+25
@@ -14,6 +14,7 @@ import fr.koka99cab.sanctuary26.sanctuary.network.BlackMarketEditorPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.BlackMarketInteractPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestEditorPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestRequestConfirmPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestTrackerSnapshotPayload;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryProgressionCosts;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryUnlock;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
@@ -48,6 +49,7 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
private static KeyMapping zoomKey;
|
||||
private static KeyMapping celestialKey;
|
||||
private static KeyMapping switchHotbarRowKey;
|
||||
private static KeyMapping questTrackerKey;
|
||||
private static boolean zoomWasHeld;
|
||||
private static boolean smokeScreenOpened;
|
||||
private static boolean inventorySmokeScreenOpened;
|
||||
@@ -91,7 +93,14 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
GLFW.GLFW_KEY_TAB,
|
||||
SANCTUARY_CATEGORY
|
||||
));
|
||||
questTrackerKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||
"key.sanctuary.quest_tracker",
|
||||
InputConstants.Type.KEYSYM,
|
||||
GLFW.GLFW_KEY_J,
|
||||
SANCTUARY_CATEGORY
|
||||
));
|
||||
SanctuaryAtlasOptions.load();
|
||||
SanctuaryClientQuests.load();
|
||||
SanctuaryBulkActionsClient.initialize();
|
||||
SanctuaryHealthPreview.register();
|
||||
CelestialSpyglassSelection.register();
|
||||
@@ -124,6 +133,8 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
ClientPlayNetworking.registerGlobalReceiver(QuestRequestConfirmPayload.ID, (payload, context) ->
|
||||
context.client().execute(() -> context.client().gui.setScreen(
|
||||
new QuestRequestConfirmScreen(context.client().gui.screen(), payload))));
|
||||
ClientPlayNetworking.registerGlobalReceiver(QuestTrackerSnapshotPayload.ID, (payload, context) ->
|
||||
context.client().execute(() -> SanctuaryClientQuests.update(payload)));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {
|
||||
SanctuaryAtlasVisitedChunks.saveIfDirty(client);
|
||||
SanctuaryAtlasMapCache.clear();
|
||||
@@ -135,6 +146,7 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
CelestialSpyglassSelection.reset();
|
||||
SanctuaryClientShop.clear();
|
||||
SanctuaryClientCapes.clear();
|
||||
SanctuaryClientQuests.clear();
|
||||
zoomWasHeld = false;
|
||||
});
|
||||
ClientHotbarScrollEvents.ALLOW.register((inventory, selectedSlot, scrollDirection, horizontalAmount, verticalAmount) -> {
|
||||
@@ -142,6 +154,7 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
return !SanctuaryAtlasHud.handleScroll(amount);
|
||||
});
|
||||
SanctuaryAtlasHud.register();
|
||||
QuestTrackerHud.register();
|
||||
SanctuaryMod.LOGGER.info("[{}] Atlas ready on M; Mouse 5 controls the HUD preview.", SanctuaryMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
@@ -181,6 +194,10 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
}
|
||||
}
|
||||
while (openAtlasKey.consumeClick()) toggleAtlas(client);
|
||||
while (questTrackerKey.consumeClick()) {
|
||||
if (client.player != null && client.level != null && client.gui.screen() == null)
|
||||
toggleQuestTracker(client);
|
||||
}
|
||||
while (celestialKey.consumeClick()) {
|
||||
if (client.player != null && client.level != null && client.gui.screen() == null)
|
||||
client.gui.setScreen(new CelestialSkyScreen(null));
|
||||
@@ -275,6 +292,14 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
&& client.level != null && client.gui.screen() == null;
|
||||
}
|
||||
|
||||
private static void toggleQuestTracker(Minecraft client) {
|
||||
boolean visible = SanctuaryClientQuests.toggle();
|
||||
if (client.player == null) return;
|
||||
client.player.sendOverlayMessage(Component.translatable(visible
|
||||
? "hud.sanctuary.quest_tracker.toggle_on"
|
||||
: "hud.sanctuary.quest_tracker.toggle_off"));
|
||||
}
|
||||
|
||||
private static void toggleAtlas(Minecraft client) {
|
||||
if (client.gui.screen() instanceof SanctuaryAtlasScreen) {
|
||||
client.gui.setScreen(null);
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestTrackerSnapshotPayload;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
/** Client-side quest tracker snapshot and visibility preference. */
|
||||
final class SanctuaryClientQuests {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Path FILE = FabricLoader.getInstance().getConfigDir()
|
||||
.resolve("sanctuary-quest-tracker.json");
|
||||
private static Settings settings = new Settings();
|
||||
private static boolean loaded;
|
||||
private static List<QuestTrackerSnapshotPayload.Entry> entries = List.of();
|
||||
|
||||
private SanctuaryClientQuests() {}
|
||||
|
||||
static void load() {
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
if (!Files.isRegularFile(FILE)) return;
|
||||
try (Reader reader = Files.newBufferedReader(FILE, StandardCharsets.UTF_8)) {
|
||||
Settings decoded = GSON.fromJson(reader, Settings.class);
|
||||
if (decoded != null) settings = decoded;
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
SanctuaryMod.LOGGER.warn("[{}] Could not read quest tracker options.",
|
||||
SanctuaryMod.DISPLAY_NAME, exception);
|
||||
}
|
||||
}
|
||||
|
||||
static void update(QuestTrackerSnapshotPayload payload) {
|
||||
entries = payload.entries();
|
||||
}
|
||||
|
||||
static void clear() {
|
||||
entries = List.of();
|
||||
}
|
||||
|
||||
static List<QuestTrackerSnapshotPayload.Entry> entries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
static boolean visible() {
|
||||
load();
|
||||
return settings.visible;
|
||||
}
|
||||
|
||||
static boolean toggle() {
|
||||
load();
|
||||
settings.visible = !settings.visible;
|
||||
save();
|
||||
return settings.visible;
|
||||
}
|
||||
|
||||
private static void save() {
|
||||
try {
|
||||
Files.createDirectories(FILE.getParent());
|
||||
try (Writer writer = Files.newBufferedWriter(FILE, StandardCharsets.UTF_8)) {
|
||||
GSON.toJson(settings, writer);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
SanctuaryMod.LOGGER.warn("[{}] Could not save quest tracker options.",
|
||||
SanctuaryMod.DISPLAY_NAME, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Settings {
|
||||
boolean visible = true;
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.network;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
/** Bounded snapshot of the player's tracked community requests and reserved boards. */
|
||||
public record QuestTrackerSnapshotPayload(List<Entry> entries) implements CustomPacketPayload {
|
||||
public static final int MAX_ENTRIES = 8;
|
||||
public static final Type<QuestTrackerSnapshotPayload> ID =
|
||||
new Type<>(SanctuaryMod.id("s2c/quest_tracker"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, QuestTrackerSnapshotPayload> CODEC =
|
||||
CustomPacketPayload.codec(QuestTrackerSnapshotPayload::write, QuestTrackerSnapshotPayload::new);
|
||||
|
||||
public QuestTrackerSnapshotPayload {
|
||||
entries = List.copyOf(entries == null ? List.of()
|
||||
: entries.stream().limit(MAX_ENTRIES).toList());
|
||||
}
|
||||
|
||||
private QuestTrackerSnapshotPayload(RegistryFriendlyByteBuf buffer) {
|
||||
this(readEntries(buffer));
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buffer) {
|
||||
buffer.writeVarInt(Math.min(MAX_ENTRIES, entries.size()));
|
||||
for (int index = 0; index < Math.min(MAX_ENTRIES, entries.size()); index++) {
|
||||
Entry entry = entries.get(index);
|
||||
buffer.writeUtf(entry.source(), 8);
|
||||
buffer.writeUtf(entry.id(), 36);
|
||||
buffer.writeUtf(entry.title(), 96);
|
||||
buffer.writeUtf(entry.action(), 16);
|
||||
buffer.writeUtf(entry.target(), 64);
|
||||
buffer.writeVarInt(entry.progress());
|
||||
buffer.writeVarInt(entry.required());
|
||||
buffer.writeInt(entry.accent());
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Entry> readEntries(RegistryFriendlyByteBuf buffer) {
|
||||
int count = Math.clamp(buffer.readVarInt(), 0, MAX_ENTRIES);
|
||||
List<Entry> values = new ArrayList<>(count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
values.add(new Entry(buffer.readUtf(8), buffer.readUtf(36), buffer.readUtf(96),
|
||||
buffer.readUtf(16), buffer.readUtf(64), Math.max(0, buffer.readVarInt()),
|
||||
Math.max(1, buffer.readVarInt()), buffer.readInt()));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public record Entry(String source, String id, String title, String action, String target,
|
||||
int progress, int required, int accent) {
|
||||
public Entry {
|
||||
source = source == null ? "" : source;
|
||||
id = id == null ? "" : id;
|
||||
title = title == null ? "" : title;
|
||||
action = action == null ? "" : action;
|
||||
target = target == null ? "" : target;
|
||||
progress = Math.max(0, progress);
|
||||
required = Math.max(1, required);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -12,6 +12,7 @@ import fr.koka99cab.sanctuary26.sanctuary.celestial.CelestialSkyService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.BlackMarketService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestRequestService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.QuestTrackerSync;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCapeService;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
@@ -42,6 +43,7 @@ public final class SanctuaryNetworking {
|
||||
PayloadTypeRegistry.clientboundPlay().register(BlackMarketEditorPayload.ID, BlackMarketEditorPayload.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(QuestRequestEditorPayload.ID, QuestRequestEditorPayload.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(QuestRequestConfirmPayload.ID, QuestRequestConfirmPayload.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(QuestTrackerSnapshotPayload.ID, QuestTrackerSnapshotPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(PurchaseProgressionPayload.ID, PurchaseProgressionPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(BuildControlPayload.ID, BuildControlPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(MiningControlPayload.ID, MiningControlPayload.CODEC);
|
||||
@@ -95,6 +97,7 @@ public final class SanctuaryNetworking {
|
||||
SanctuaryMealHeartsService.join(handler.player);
|
||||
CelestialSkyService.join(handler.player);
|
||||
SanctuaryCapeService.join(handler.player);
|
||||
QuestTrackerSync.sync(handler.player);
|
||||
});
|
||||
SanctuaryMod.LOGGER.debug("[{}] Networking contract v{} ready; Atlas snapshot registered.",
|
||||
SanctuaryMod.DISPLAY_NAME, PROTOCOL_VERSION);
|
||||
|
||||
+8
-1
@@ -167,6 +167,7 @@ public final class QuestBoardService {
|
||||
data.audit(new QuestBoardData.AuditEntry(now, "reserve", player.getUUID().toString(), board.id(),
|
||||
contract.id(), deposit.currency + ":" + deposit.amount));
|
||||
writeSign(level, sign, updated);
|
||||
QuestTrackerSync.sync(player);
|
||||
level.playSound(null, sign.getBlockPos(), SoundEvents.VILLAGER_YES, SoundSource.BLOCKS, 0.7F, 1.1F);
|
||||
player.sendSystemMessage(Component.translatable("message.sanctuary.quest_board.reserved",
|
||||
contract.objective().required(), describeTarget(contract.objective()), minutes(board.questTier().reservationMillis())));
|
||||
@@ -213,6 +214,7 @@ public final class QuestBoardService {
|
||||
QuestBoardData.Board updated = board.withContract(Optional.of(advanced));
|
||||
data.replace(updated);
|
||||
refreshSign(level.getServer(), updated);
|
||||
QuestTrackerSync.sync(player);
|
||||
if (advanced.objective().complete()) complete(player, level.getServer(), data, updated, advanced);
|
||||
}
|
||||
}
|
||||
@@ -234,6 +236,7 @@ public final class QuestBoardService {
|
||||
data.audit(new QuestBoardData.AuditEntry(now, "complete", player.getUUID().toString(), board.id(),
|
||||
contract.id(), rewardDetail(contract.reward())));
|
||||
refreshSign(server, completedBoard);
|
||||
QuestTrackerSync.sync(player);
|
||||
ServerLevel boardLevel = level(server, board.dimension());
|
||||
BlockPos boardPos = BlockPos.of(board.pos());
|
||||
if (boardLevel != null && boardLevel.isLoaded(boardPos))
|
||||
@@ -252,6 +255,7 @@ public final class QuestBoardService {
|
||||
data.audit(new QuestBoardData.AuditEntry(System.currentTimeMillis(), "release", player.getUUID().toString(),
|
||||
board.id(), contract.id(), reason));
|
||||
writeSign(level, sign, released);
|
||||
QuestTrackerSync.sync(player);
|
||||
message(player, "abandoned");
|
||||
}
|
||||
|
||||
@@ -276,7 +280,9 @@ public final class QuestBoardService {
|
||||
data.replace(released);
|
||||
data.audit(new QuestBoardData.AuditEntry(now, "expire", claim.player(), board.id(), contract.id(),
|
||||
claim.depositCurrency() + ":" + claim.depositAmount()));
|
||||
refreshSign(server, released); continue;
|
||||
refreshSign(server, released);
|
||||
QuestTrackerSync.sync(server, claim.playerId());
|
||||
continue;
|
||||
}
|
||||
if (!board.frozen() && now >= board.nextRefreshAt()
|
||||
&& (contract == null || contract.claim().isEmpty())) {
|
||||
@@ -303,6 +309,7 @@ public final class QuestBoardService {
|
||||
});
|
||||
data.audit(new QuestBoardData.AuditEntry(now, "remove_board", actor, board.id(),
|
||||
board.contract().map(QuestBoardData.Contract::id).orElse(""), reason));
|
||||
QuestTrackerSync.syncBoard(server, board);
|
||||
}
|
||||
|
||||
static boolean setFrozen(MinecraftServer server, String id, boolean frozen, String actor) {
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import java.text.Normalizer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/** Client-side ranking for quest-panel item and mob identifiers. */
|
||||
public final class QuestIdSuggestions {
|
||||
public static final int LIMIT = 5;
|
||||
|
||||
public record Entry(String id, String path, String displayName) {
|
||||
public Entry(String id, String displayName) {
|
||||
this(id, pathOf(id), displayName);
|
||||
}
|
||||
}
|
||||
|
||||
public record Match(String id, String displayName, boolean preferred, int rank) {}
|
||||
|
||||
private QuestIdSuggestions() {}
|
||||
|
||||
public static List<Match> suggest(List<Entry> catalog, String query, Collection<String> preferred,
|
||||
int limit) {
|
||||
String needle = normalize(query);
|
||||
if (needle.isEmpty()) return List.of();
|
||||
Set<String> pinned = preferred == null ? Set.of() : Set.copyOf(preferred);
|
||||
List<Match> matches = new ArrayList<>();
|
||||
for (Entry entry : catalog) {
|
||||
Integer rank = rank(entry, needle);
|
||||
if (rank == null) continue;
|
||||
boolean pin = pinned.contains(entry.id());
|
||||
matches.add(new Match(entry.id(), entry.displayName(), pin, pin ? rank - 10 : rank));
|
||||
}
|
||||
matches.sort(Comparator.comparingInt(Match::rank)
|
||||
.thenComparing(Match::displayName, String.CASE_INSENSITIVE_ORDER)
|
||||
.thenComparing(Match::id));
|
||||
return matches.size() <= limit ? List.copyOf(matches) : List.copyOf(matches.subList(0, limit));
|
||||
}
|
||||
|
||||
public static String resolve(List<Entry> catalog, String query, Collection<String> preferred) {
|
||||
Match match = exact(catalog, query, preferred);
|
||||
return match == null ? null : match.id();
|
||||
}
|
||||
|
||||
public static Match exact(List<Entry> catalog, String query, Collection<String> preferred) {
|
||||
String raw = query == null ? "" : query.strip();
|
||||
if (raw.isEmpty()) return null;
|
||||
String folded = normalize(raw);
|
||||
for (Match match : suggest(catalog, raw, preferred, LIMIT)) {
|
||||
if (normalize(match.id()).equals(folded) || normalize(pathOf(match.id())).equals(folded)
|
||||
|| normalize(match.displayName()).equals(folded)) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String normalize(String value) {
|
||||
if (value == null || value.isBlank()) return "";
|
||||
String folded = Normalizer.normalize(value.strip().toLowerCase(Locale.ROOT), Normalizer.Form.NFD)
|
||||
.replaceAll("\\p{M}+", "");
|
||||
return folded.startsWith("minecraft:") ? folded.substring("minecraft:".length()) : folded;
|
||||
}
|
||||
|
||||
private static Integer rank(Entry entry, String needle) {
|
||||
String id = normalize(entry.id());
|
||||
String path = normalize(entry.path());
|
||||
String name = normalize(entry.displayName());
|
||||
if (id.equals(needle) || path.equals(needle) || name.equals(needle)) return 0;
|
||||
if (path.startsWith(needle) || name.startsWith(needle)) return 1;
|
||||
if (id.startsWith(needle)) return 2;
|
||||
if (name.contains(needle) || path.contains(needle)) return 3;
|
||||
if (id.contains(needle)) return 4;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String pathOf(String id) {
|
||||
int colon = id.indexOf(':');
|
||||
return colon < 0 ? id : id.substring(colon + 1);
|
||||
}
|
||||
}
|
||||
+8
@@ -118,6 +118,7 @@ public final class QuestRequestService {
|
||||
return;
|
||||
}
|
||||
writeSign(level, sign, draft);
|
||||
QuestTrackerSync.sync(player);
|
||||
openEditor(player, draft);
|
||||
}
|
||||
|
||||
@@ -165,6 +166,8 @@ public final class QuestRequestService {
|
||||
QuestRequestData.Request released = request.withClaim(Optional.empty());
|
||||
if (data.replace(released)) {
|
||||
writeSign(level, sign, released);
|
||||
QuestTrackerSync.syncRequest(level.getServer(), released);
|
||||
QuestTrackerSync.sync(player);
|
||||
message(player, "abandoned");
|
||||
}
|
||||
} else if (BRING.equals(request.action())) {
|
||||
@@ -225,6 +228,7 @@ public final class QuestRequestService {
|
||||
return;
|
||||
}
|
||||
writeSign(level, sign, claimed);
|
||||
QuestTrackerSync.syncRequest(level.getServer(), claimed);
|
||||
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)));
|
||||
@@ -298,6 +302,7 @@ public final class QuestRequestService {
|
||||
queueStacks(level.getServer(), request.ownerId(), plan.refund(), request.id(),
|
||||
"quest_request_escrow_adjustment", player);
|
||||
writeSign(level, sign, configured);
|
||||
QuestTrackerSync.sync(player);
|
||||
level.playSound(null, payload.pos(), SoundEvents.AMETHYST_BLOCK_CHIME,
|
||||
SoundSource.BLOCKS, 0.8F, 1.0F);
|
||||
message(player, "created");
|
||||
@@ -336,6 +341,7 @@ public final class QuestRequestService {
|
||||
complete(killer, level.getServer(), data, advanced, List.of());
|
||||
} else if (data.replace(advanced)) {
|
||||
refreshSign(level.getServer(), advanced);
|
||||
QuestTrackerSync.syncRequest(level.getServer(), advanced);
|
||||
killer.sendSystemMessage(Component.translatable("message.sanctuary.quest_request.progress",
|
||||
advanced.progress(), advanced.quantity()));
|
||||
}
|
||||
@@ -362,6 +368,7 @@ public final class QuestRequestService {
|
||||
queueStacks(server, claim.playerId(), reward, request.id(),
|
||||
"quest_request_reward", actor);
|
||||
refreshSign(server, completed);
|
||||
QuestTrackerSync.syncRequest(server, completed);
|
||||
ServerLevel signLevel = level(server, request.dimension());
|
||||
BlockPos signPos = BlockPos.of(request.pos());
|
||||
if (signLevel != null && signLevel.isLoaded(signPos)) signLevel.playSound(null, signPos,
|
||||
@@ -519,6 +526,7 @@ public final class QuestRequestService {
|
||||
if (!data.remove(request)) return;
|
||||
queueStacks(server, request.ownerId(), request.rewardEscrow(), request.id(),
|
||||
"quest_request_" + reason, null);
|
||||
QuestTrackerSync.syncRequest(server, request);
|
||||
refreshedLayouts.remove(request.id());
|
||||
ServerPlayer owner = server.getPlayerList().getPlayer(request.ownerId());
|
||||
if (owner != null && !request.rewardEscrow().isEmpty()) message(owner, "reward_refunded");
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestTrackerSnapshotPayload;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Pushes a bounded tracker snapshot of quests the player accepted. */
|
||||
public final class QuestTrackerSync {
|
||||
public static final String SOURCE_REQUEST = "request";
|
||||
public static final String SOURCE_OWNED = "owned";
|
||||
public static final String SOURCE_BOARD = "board";
|
||||
|
||||
private QuestTrackerSync() {}
|
||||
|
||||
public static void sync(ServerPlayer player) {
|
||||
if (!ServerPlayNetworking.canSend(player, QuestTrackerSnapshotPayload.ID)) return;
|
||||
ServerPlayNetworking.send(player, new QuestTrackerSnapshotPayload(entries(player)));
|
||||
}
|
||||
|
||||
public static void sync(MinecraftServer server, UUID... players) {
|
||||
if (server == null || players == null) return;
|
||||
for (UUID id : players) {
|
||||
if (id == null) continue;
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(id);
|
||||
if (player != null) sync(player);
|
||||
}
|
||||
}
|
||||
|
||||
public static void syncRequest(MinecraftServer server, QuestRequestData.Request request) {
|
||||
if (request == null) return;
|
||||
sync(server, request.ownerId(),
|
||||
request.claim().map(QuestRequestData.Claim::playerId).orElse(null));
|
||||
}
|
||||
|
||||
public static void syncBoard(MinecraftServer server, QuestBoardData.Board board) {
|
||||
if (board == null) return;
|
||||
sync(server, board.contract().flatMap(QuestBoardData.Contract::claim)
|
||||
.map(QuestBoardData.Claim::playerId).orElse(null));
|
||||
}
|
||||
|
||||
private static List<QuestTrackerSnapshotPayload.Entry> entries(ServerPlayer player) {
|
||||
List<QuestTrackerSnapshotPayload.Entry> entries = new ArrayList<>();
|
||||
UUID playerId = player.getUUID();
|
||||
MinecraftServer server = player.level().getServer();
|
||||
for (QuestRequestData.Request request : QuestRequestData.get(server).requests()) {
|
||||
if (entries.size() >= QuestTrackerSnapshotPayload.MAX_ENTRIES) break;
|
||||
if (!tracksAcceptedRequest(request, playerId)) continue;
|
||||
entries.add(new QuestTrackerSnapshotPayload.Entry(
|
||||
SOURCE_REQUEST, clip(request.id(), 36), clip(request.objective(), 96),
|
||||
clip(request.action(), 16), clip(request.target(), 64),
|
||||
request.progress(), request.quantity(), request.ownerColor()));
|
||||
}
|
||||
for (QuestBoardData.Board board : QuestBoardData.get(server).boards()) {
|
||||
if (entries.size() >= QuestTrackerSnapshotPayload.MAX_ENTRIES) break;
|
||||
QuestBoardData.Contract contract = board.contract().orElse(null);
|
||||
if (contract == null || contract.claim().isEmpty()
|
||||
|| !contract.claim().get().playerId().equals(playerId)) continue;
|
||||
QuestBoardData.Objective objective = contract.objective();
|
||||
entries.add(new QuestTrackerSnapshotPayload.Entry(
|
||||
SOURCE_BOARD, clip(board.id(), 36), "",
|
||||
clip(objective.kind(), 16), clip(objective.target(), 64),
|
||||
objective.progress(), objective.required(), tierColor(board.questTier())));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
static boolean tracksAcceptedRequest(QuestRequestData.Request request, UUID playerId) {
|
||||
if (request == null || playerId == null || !request.configured() || request.completed()) {
|
||||
return false;
|
||||
}
|
||||
return request.claim().map(claim -> claim.playerId().equals(playerId)).orElse(false);
|
||||
}
|
||||
|
||||
private static int tierColor(QuestBoardTier tier) {
|
||||
return switch (tier) {
|
||||
case RUBY -> 0xFF5555;
|
||||
case SAPPHIRE -> 0x55FFFF;
|
||||
case EMERALD -> 0x55FF55;
|
||||
};
|
||||
}
|
||||
|
||||
private static String clip(String value, int maximumLength) {
|
||||
if (value == null) return "";
|
||||
return value.length() <= maximumLength ? value : value.substring(0, maximumLength);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@
|
||||
"screen.sanctuary.quest_request.reward_hint.loan": "minecraft:elytra",
|
||||
"screen.sanctuary.quest_request.publish": "Publish",
|
||||
"screen.sanctuary.quest_request.help": "The reward is removed from your inventory when published",
|
||||
"screen.sanctuary.quest_request.id_help": "Type a name, then click the item to fill its ID",
|
||||
"screen.sanctuary.quest_request.confirm.title": "Confirm request",
|
||||
"screen.sanctuary.quest_request.confirm.accept": "Accept quest",
|
||||
"screen.sanctuary.quest_request.confirm.objective": "Objective: %s",
|
||||
@@ -242,6 +243,15 @@
|
||||
"key.sanctuary.companion_power": "Companion power",
|
||||
"key.sanctuary.zoom": "Telephoto zoom",
|
||||
"key.sanctuary.switch_hotbar_row": "Cycle hotbar row",
|
||||
"key.sanctuary.quest_tracker": "Quest tracker",
|
||||
"hud.sanctuary.quest_tracker.title": "Quests",
|
||||
"hud.sanctuary.quest_tracker.empty": "No quest in progress",
|
||||
"hud.sanctuary.quest_tracker.toggle_on": "Quest tracker shown",
|
||||
"hud.sanctuary.quest_tracker.toggle_off": "Quest tracker hidden",
|
||||
"hud.sanctuary.quest_tracker.source.request": "Accepted request",
|
||||
"hud.sanctuary.quest_tracker.source.owned": "Your request",
|
||||
"hud.sanctuary.quest_tracker.source.board": "Daily contract",
|
||||
"hud.sanctuary.quest_tracker.progress": "%s · %s · %s/%s",
|
||||
"message.sanctuary.zoom": "Zoom · FOV %s",
|
||||
"message.sanctuary.companion.passive_only": "This essence only has a passive power.",
|
||||
"message.sanctuary.companion.cooldown": "Power ready in %s s.",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"screen.sanctuary.quest_request.reward_hint.loan": "minecraft:elytra",
|
||||
"screen.sanctuary.quest_request.publish": "Publier",
|
||||
"screen.sanctuary.quest_request.help": "La récompense est retirée de ton inventaire à la publication",
|
||||
"screen.sanctuary.quest_request.id_help": "Tape un nom puis clique l'item pour remplir l'ID",
|
||||
"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",
|
||||
@@ -242,6 +243,15 @@
|
||||
"key.sanctuary.companion_power": "Pouvoir du familier",
|
||||
"key.sanctuary.zoom": "Zoom téléobjectif",
|
||||
"key.sanctuary.switch_hotbar_row": "Changer de rangée de hotbar",
|
||||
"key.sanctuary.quest_tracker": "Suivi des quêtes",
|
||||
"hud.sanctuary.quest_tracker.title": "Quêtes",
|
||||
"hud.sanctuary.quest_tracker.empty": "Aucune quête en cours",
|
||||
"hud.sanctuary.quest_tracker.toggle_on": "Suivi des quêtes affiché",
|
||||
"hud.sanctuary.quest_tracker.toggle_off": "Suivi des quêtes masqué",
|
||||
"hud.sanctuary.quest_tracker.source.request": "Demande acceptée",
|
||||
"hud.sanctuary.quest_tracker.source.owned": "Votre demande",
|
||||
"hud.sanctuary.quest_tracker.source.board": "Contrat du jour",
|
||||
"hud.sanctuary.quest_tracker.progress": "%s · %s · %s/%s",
|
||||
"message.sanctuary.zoom": "Zoom · FOV %s",
|
||||
"message.sanctuary.companion.passive_only": "Cette essence ne possède qu'un pouvoir passif.",
|
||||
"message.sanctuary.companion.cooldown": "Pouvoir disponible dans %s s.",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"screen.sanctuary.quest_request.reward_hint.loan": "minecraft:elytra",
|
||||
"screen.sanctuary.quest_request.publish": "Опубликовать",
|
||||
"screen.sanctuary.quest_request.help": "При публикации награда изымается из инвентаря",
|
||||
"screen.sanctuary.quest_request.id_help": "Введите название, затем кликните предмет, чтобы подставить ID",
|
||||
"screen.sanctuary.quest_request.confirm.title": "Подтвердить запрос",
|
||||
"screen.sanctuary.quest_request.confirm.accept": "Принять задание",
|
||||
"screen.sanctuary.quest_request.confirm.objective": "Цель: %s",
|
||||
@@ -154,6 +155,15 @@
|
||||
"key.sanctuary.companion_power": "Способность спутника",
|
||||
"key.sanctuary.zoom": "Телефото-зум",
|
||||
"key.sanctuary.switch_hotbar_row": "Сменить ряд панели быстрого доступа",
|
||||
"key.sanctuary.quest_tracker": "Трекер заданий",
|
||||
"hud.sanctuary.quest_tracker.title": "Задания",
|
||||
"hud.sanctuary.quest_tracker.empty": "Нет активного задания",
|
||||
"hud.sanctuary.quest_tracker.toggle_on": "Трекер заданий показан",
|
||||
"hud.sanctuary.quest_tracker.toggle_off": "Трекер заданий скрыт",
|
||||
"hud.sanctuary.quest_tracker.source.request": "Принятый запрос",
|
||||
"hud.sanctuary.quest_tracker.source.owned": "Ваш запрос",
|
||||
"hud.sanctuary.quest_tracker.source.board": "Дневной контракт",
|
||||
"hud.sanctuary.quest_tracker.progress": "%s · %s · %s/%s",
|
||||
"message.sanctuary.zoom": "Зум · FOV %s",
|
||||
"message.sanctuary.companion.passive_only": "У этой сущности есть только пассивная способность.",
|
||||
"message.sanctuary.companion.cooldown": "Способность будет готова через %s с.",
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** Ranking for typed item names, accent-folded labels and inventory pins. */
|
||||
public final class QuestIdSuggestionsSmoke {
|
||||
private QuestIdSuggestionsSmoke() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
List<QuestIdSuggestions.Entry> catalog = List.of(
|
||||
new QuestIdSuggestions.Entry("minecraft:diamond", "Diamant"),
|
||||
new QuestIdSuggestions.Entry("minecraft:diamond_sword", "Épée en diamant"),
|
||||
new QuestIdSuggestions.Entry("minecraft:wheat", "Blé"),
|
||||
new QuestIdSuggestions.Entry("anotherworld:ruby", "Rubis"),
|
||||
new QuestIdSuggestions.Entry("minecraft:zombie_head", "Tête de zombie"));
|
||||
require(QuestIdSuggestions.suggest(catalog, "diam", Set.of(), 7).getFirst().id()
|
||||
.equals("minecraft:diamond"), "Prefix must prefer the shortest close name");
|
||||
require(QuestIdSuggestions.suggest(catalog, "epee", Set.of(), 7).getFirst().id()
|
||||
.equals("minecraft:diamond_sword"), "Accent-folded display names must match");
|
||||
require(QuestIdSuggestions.suggest(catalog, "ble", Set.of(), 7).getFirst().id()
|
||||
.equals("minecraft:wheat"), "Blé must resolve from ble");
|
||||
require(QuestIdSuggestions.resolve(catalog, "rubis", Set.of())
|
||||
.equals("anotherworld:ruby"), "Exact display names must resolve to their ID");
|
||||
require(QuestIdSuggestions.resolve(catalog, "diamond", Set.of())
|
||||
.equals("minecraft:diamond"), "Bare vanilla paths must resolve");
|
||||
require(QuestIdSuggestions.suggest(catalog, "", Set.of("anotherworld:ruby"), 7).isEmpty(),
|
||||
"An empty query must not invent suggestions");
|
||||
require(QuestIdSuggestions.suggest(catalog, "zzz", Set.of(), 7).isEmpty(),
|
||||
"Unknown names must produce no suggestion");
|
||||
require(QuestIdSuggestions.resolve(catalog, "", Set.of()) == null,
|
||||
"Blank fields must not invent an ID");
|
||||
require(QuestIdSuggestions.resolve(catalog, "diam", Set.of()) == null,
|
||||
"A partial name must wait for an explicit click");
|
||||
}
|
||||
|
||||
private static void require(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.shop;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.QuestTrackerSnapshotPayload;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Bounds and source tags for the concept quest-tracker snapshot. */
|
||||
public final class QuestTrackerModelSmoke {
|
||||
private QuestTrackerModelSmoke() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
List<QuestTrackerSnapshotPayload.Entry> overflow = new ArrayList<>();
|
||||
for (int index = 0; index < QuestTrackerSnapshotPayload.MAX_ENTRIES + 3; index++) {
|
||||
overflow.add(new QuestTrackerSnapshotPayload.Entry(QuestTrackerSync.SOURCE_REQUEST,
|
||||
"id-" + index, "Objective " + index, "kill", "minecraft:zombie", index, 10, 0xCC77AA));
|
||||
}
|
||||
QuestTrackerSnapshotPayload payload = new QuestTrackerSnapshotPayload(overflow);
|
||||
require(payload.entries().size() == QuestTrackerSnapshotPayload.MAX_ENTRIES,
|
||||
"Tracker snapshot must stay bounded");
|
||||
require(payload.entries().getFirst().source().equals(QuestTrackerSync.SOURCE_REQUEST),
|
||||
"Request source tag must be preserved");
|
||||
QuestTrackerSnapshotPayload.Entry owned = new QuestTrackerSnapshotPayload.Entry(
|
||||
QuestTrackerSync.SOURCE_OWNED, "owned", "Posted", "bring", "minecraft:wheat", 0, 4, 0xFFFFFF);
|
||||
QuestTrackerSnapshotPayload.Entry board = new QuestTrackerSnapshotPayload.Entry(
|
||||
QuestTrackerSync.SOURCE_BOARD, "board", "", "kill_exact", "minecraft:skeleton", 2, 8, 0x55FF55);
|
||||
QuestTrackerSnapshotPayload mixed = new QuestTrackerSnapshotPayload(List.of(owned, board));
|
||||
require(mixed.entries().get(0).required() == 4 && mixed.entries().get(1).progress() == 2,
|
||||
"Owned and board progress must be preserved");
|
||||
require(new QuestTrackerSnapshotPayload(null).entries().isEmpty(),
|
||||
"Null snapshot must become an empty tracker");
|
||||
UUID owner = UUID.fromString("6c43c35a-3498-41bb-a77e-10c679e635d1");
|
||||
UUID claimant = UUID.fromString("8ff11ade-a604-4d77-a770-d487684367a1");
|
||||
QuestRequestData.Request posted = new QuestRequestData.Request("request", "minecraft:overworld",
|
||||
42L, true, owner.toString(), 0xCC77AA, "Koka", "Posted", QuestRequestService.KILL,
|
||||
QuestRequestService.MOB, "minecraft:zombie", 3, QuestRequestService.REWARD_GIFT,
|
||||
"minecraft:diamond", 1, true, Optional.empty(), List.of(), 0, false);
|
||||
require(!QuestTrackerSync.tracksAcceptedRequest(posted, owner),
|
||||
"A posted request must stay off the author's tracker");
|
||||
QuestRequestData.Request claimed = posted.withClaim(Optional.of(
|
||||
new QuestRequestData.Claim(claimant.toString(), "Hunter", 0x44AAFF, 10L)));
|
||||
require(!QuestTrackerSync.tracksAcceptedRequest(claimed, owner),
|
||||
"The author must not see a claimed request they posted");
|
||||
require(QuestTrackerSync.tracksAcceptedRequest(claimed, claimant),
|
||||
"Only the player who accepted the request appears on the tracker");
|
||||
}
|
||||
|
||||
private static void require(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user