diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cee3e4..f2d7385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,14 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve ## Non publié -Aucun changement non publié. +Modules modifiés : **Sanctuary `0.0.0-alpha.139`** et **Red-Stoner `0.0.0-alpha.20`**. + +- remplace le bloc-écran `redstoner:trophy_panel` par un vrai panneau Vanilla en chêne pâle, posable au sol ou au mur, sans changer l’ID de l’item déjà présent dans les inventaires et recettes ; +- ouvre à la première pose un éditeur blanc permettant de rechercher une statistique Vanilla, composer et réordonner jusqu’à seize pages, puis choisir entre noms et valeurs, noms seuls ou valeurs seules ; +- verrouille la configuration et la destruction au créateur ou à un opérateur, tout en laissant le clic droit public faire défiler les pages ; +- conserve l’ancien bloc uniquement comme alias de migration : chaque plaque déjà posée devient automatiquement un panneau mural en gardant propriétaire, statistiques et page courante ; +- répare les classements importés qui montraient les huit premiers caractères de l’UUID : Sanctuary récupère désormais le vrai pseudo depuis le cache local du serveur et utilise `?` plutôt qu’un fragment d’UUID si aucun nom n’est connu ; +- ajoute les payloads bornés de l’éditeur, fait passer Red-Stoner aux données v5 et au protocole réseau 9, sans changer les données v15 ni le protocole 24 de Sanctuary. Client et serveur Red-Stoner doivent être mis à jour ensemble. ## `26.2.0-alpha.232` diff --git a/README.md b/README.md index 6e36bb2..8477d42 100644 --- a/README.md +++ b/README.md @@ -325,9 +325,9 @@ Le guide détaillé est dans [`masterkey/tools/PANORAMA.md`](masterkey/tools/PAN ## Red-Stoner et photographie -Red-Stoner possède sept blocs actifs sous son namespace : `eight_bit_computer`, `controller`, `personal_computer`, `drawer`, `particuleur`, `storage_terminal` et `trophy_panel`. Son onglet créatif expose aussi les trois appareils Canon et les seize styles de pellicule. +Red-Stoner possède ses appareils sous son namespace ainsi que la paire de panneaux Vanilla `trophy_sign`/`trophy_wall_sign`. L’item historique `redstoner:trophy_panel` est conservé afin que les inventaires et recettes existants restent valides ; il place désormais cette paire, tandis que l’ancien bloc mural du même ID ne subsiste que le temps de sa migration automatique. Son onglet créatif expose le panneau de trophées avec les ordinateurs, le Particuleur et le terminal de stockage. -Le panneau mural `redstoner:trophy_panel` consomme l’API publique de classement de Sanctuary sans conserver les scores. Le joueur qui le pose peut ajouter des pages statistiques en cliquant avec un item, tandis que tous les visiteurs peuvent faire défiler les pages. Sanctuary importe une fois les statistiques vanilla existantes, garde les classements par UUID — champion déconnecté compris — et fournit seulement un podium borné à trois entrées. Les composants privés de l’item servant d’icône et la base complète des joueurs ne sont jamais synchronisés au client. +Le panneau de trophées consomme l’API publique de classement de Sanctuary sans conserver les scores. Sa première pose ouvre un éditeur blanc : le créateur choisit dans le catalogue Vanilla jusqu’à seize statistiques, leur ordre et un affichage noms+valeurs, noms seuls ou valeurs seules. `Maj` + clic droit rouvre ensuite cet éditeur pour le créateur ou un opérateur ; le clic droit ordinaire fait défiler publiquement les pages. La configuration et la destruction sont protégées par UUID. Chaque page utilise les quatre lignes natives du panneau — titre puis trois rangs — et se rafraîchit côté serveur. Sanctuary importe une fois les statistiques Vanilla existantes, conserve les classements par UUID — champion déconnecté compris — et résout les vrais pseudos depuis le cache du serveur au lieu d’afficher un début d’UUID. L'ordinateur 8 bits et le contrôleur exécutent la machine virtuelle historique : programme assembleur borné à 4 096 caractères, RAM, inventaire, six ports redstone directionnels, impulsions et sortie comparateur. Le contrôleur dispose de 1 024 cellules de RAM contre 256 pour l'ordinateur. L'instruction destinée à la matrice d'affichage renvoie volontairement une erreur claire tant que ce périphérique reste différé. diff --git a/build.gradle b/build.gradle index 98ad119..9d41da4 100644 --- a/build.gradle +++ b/build.gradle @@ -1506,8 +1506,8 @@ tasks.register("verifyWorkspace") { || personalComputerMigration.compatibility?.sanctuary_network_protocol != 13 || personalComputerMigration.compatibility?.redstoner_target_data_version != 3 || personalComputerMigration.compatibility?.redstoner_target_network_protocol != 5 - || redstonerManifest.custom?.sanctuary26?.data_version != 4 - || redstonerManifest.custom?.sanctuary26?.network_protocol != 8 + || redstonerManifest.custom?.sanctuary26?.data_version != 5 + || redstonerManifest.custom?.sanctuary26?.network_protocol != 9 || sanctuaryManifest.custom?.sanctuary26?.data_version < 8 || !sanctuaryIdentity.contains('SAVE_KEY = "server_identity"') || !personalComputerJava.contains('SAVE_KEY = "personal_computers"') @@ -2916,7 +2916,7 @@ tasks.register("verifyNotchEncounter") { def textureHash = MessageDigest.getInstance("SHA-256").digest(texture.bytes).encodeHex().toString() if (rootProject.itsalive_version != "0.0.0-alpha.38" || rootProject.ouch_version != "0.0.0-alpha.11" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || rootProject.masterkey_version != "0.0.0-alpha.9" || rootProject.pack_version != "26.2.0-alpha.232" || manifests.itsalive.custom?.sanctuary26?.data_version != 4 @@ -3010,7 +3010,7 @@ tasks.register("verifyMagicCarpet") { if (rootProject.pack_version != "26.2.0-alpha.232" || rootProject.iliketomoveit_version != "0.0.0-alpha.18" || rootProject.onlyfun_version != "0.0.0-alpha.27" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || manifests.iliketomoveit.custom?.sanctuary26?.data_version != 4 || manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5 || manifests.iliketomoveit.depends?.sanctuary != ">=0.0.0-alpha.127" @@ -3641,7 +3641,7 @@ tasks.register("verifyProgressivePauseMenuRelease") { "sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))] } if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || rootProject.onlyfun_version != "0.0.0-alpha.27" || 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"] @@ -3692,7 +3692,7 @@ tasks.register("verifyMirrorLavenderDrawerRelease") { def drawerRecipe = file("redstoner/src/main/resources/data/redstoner/recipe/drawer.json") if (rootProject.pack_version != "26.2.0-alpha.232" || rootProject.anotherworld_version != "0.0.0-alpha.58" - || rootProject.redstoner_version != "0.0.0-alpha.19" + || rootProject.redstoner_version != "0.0.0-alpha.20" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.191" || migration.source?.modules != [anotherworld: "0.0.0-alpha.48", redstoner: "0.0.0-alpha.9"] @@ -4208,12 +4208,15 @@ project(":redstoner") { tasks.register("verifyTrophyPanel") { group = "verification" - description = "Checks the Sanctuary ranking API and Red-Stoner trophy-panel integration contract." + description = "Checks the Sanctuary ranking API and vanilla trophy-sign integration contract." inputs.file(rootProject.file("pack/migrations/trophy-rankings-data-v15-v4.json")) inputs.file(rootProject.file("pack/migrations/alpha231-follow-up-cape-terminal-trophy.json")) - inputs.files(fileTree("src/main/java") { include "**/*TrophyPanel*.java" }) + inputs.file(rootProject.file("pack/migrations/trophy-panel-to-vanilla-signs.json")) + inputs.files(fileTree("src/main/java") { include "**/*TrophyPanel*.java", "**/*TrophySign*.java" }) inputs.files(fileTree(rootProject.file("sanctuary/src/main/java")) { include "**/api/ranking/*.java" }) - inputs.files(fileTree("src/main/resources") { include "**/*trophy_panel*", "**/lang/*.json", "fabric.mod.json" }) + inputs.files(fileTree("src/main/resources") { + include "**/*trophy_panel*", "**/*trophy_sign*", "**/lang/*.json", "fabric.mod.json" + }) inputs.files(fileTree(rootProject.file("sanctuary/src/main/resources/assets/sanctuary/lang")) { include "*.json" }) inputs.file(rootProject.file("sanctuary/src/main/resources/fabric.mod.json")) doLast { @@ -4221,6 +4224,8 @@ project(":redstoner") { "pack/migrations/trophy-rankings-data-v15-v4.json")) def followUp = new JsonSlurper().parse(rootProject.file( "pack/migrations/alpha231-follow-up-cape-terminal-trophy.json")) + def vanillaSigns = new JsonSlurper().parse(rootProject.file( + "pack/migrations/trophy-panel-to-vanilla-signs.json")) def redstonerManifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json")) def sanctuaryManifest = new JsonSlurper().parse(rootProject.file( "sanctuary/src/main/resources/fabric.mod.json")) @@ -4232,16 +4237,34 @@ project(":redstoner") { "src/main/java/fr/koka99cab/sanctuary26/redstoner/block/TrophyPanelBlock.java").text def renderer = file( "src/main/java/fr/koka99cab/sanctuary26/redstoner/client/renderer/TrophyPanelBlockEntityRenderer.java").text - def model = new JsonSlurper().parse(file( - "src/main/resources/assets/redstoner/models/block/trophy_panel.json")) + def service = file( + "src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignService.java").text + def signData = file( + "src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignData.java").text + def signItem = file( + "src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignItem.java").text + def editor = file( + "src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/TrophySignEditorScreen.java").text + def networking = file( + "src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java").text + def standingModel = new JsonSlurper().parse(file( + "src/main/resources/assets/redstoner/models/block/trophy_sign_rot_0.json")) + def wallModel = new JsonSlurper().parse(file( + "src/main/resources/assets/redstoner/models/block/trophy_wall_sign.json")) + def recipe = new JsonSlurper().parse(file( + "src/main/resources/data/redstoner/recipe/trophy_panel.json")) def rankingApi = rootProject.file( "sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingApi.java").text def rankingData = rootProject.file( "sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingData.java").text def requiredKeys = [ - "block.redstoner.trophy_panel", "message.redstoner.trophy_panel.not_owner", - "message.redstoner.trophy_panel.configured", "block.redstoner.trophy_panel.page_title", - "block.redstoner.trophy_panel.player", "block.redstoner.trophy_panel.value"] + "block.redstoner.trophy_panel", "block.redstoner.trophy_sign", + "block.redstoner.trophy_wall_sign", "block.redstoner.trophy_sign.page_title", + "block.redstoner.trophy_sign.names_and_values", "block.redstoner.trophy_sign.names", + "block.redstoner.trophy_sign.values", "screen.redstoner.trophy_sign.title", + "screen.redstoner.trophy_sign.search", "screen.redstoner.trophy_sign.pages", + "screen.redstoner.trophy_sign.display", "message.redstoner.trophy_sign.not_owner", + "message.redstoner.trophy_sign.protected", "message.redstoner.trophy_sign.saved"] def redstonerLocales = ["fr_fr", "en_us", "ru_ru"].collect { new JsonSlurper().parse(file("src/main/resources/assets/redstoner/lang/${it}.json")) } @@ -4249,10 +4272,11 @@ project(":redstoner") { new JsonSlurper().parse(rootProject.file( "sanctuary/src/main/resources/assets/sanctuary/lang/${it}.json")) } - if (rootProject.redstoner_version != "0.0.0-alpha.19" - || rootProject.sanctuary_version != "0.0.0-alpha.138" - || redstonerManifest.custom?.sanctuary26?.data_version != 4 - || redstonerManifest.custom?.sanctuary26?.network_protocol != 8 + if (rootProject.redstoner_version != "0.0.0-alpha.20" + || rootProject.sanctuary_version != "0.0.0-alpha.139" + || redstonerManifest.custom?.sanctuary26?.data_version != 5 + || redstonerManifest.custom?.sanctuary26?.network_protocol != 9 + || redstonerManifest.depends?.sanctuary != ">=0.0.0-alpha.139" || sanctuaryManifest.custom?.sanctuary26?.data_version != 15 || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 24 || migration.modules?.sanctuary?.source_module_version != "0.0.0-alpha.134" @@ -4272,7 +4296,26 @@ project(":redstoner") { || followUp.fixes?.trophy_panel_names_then_values != true || followUp.fixes?.spawn_egg_maps_to_entity_killed_statistic != true || followUp.compatibility?.persistent_data_formats_changed != false + || vanillaSigns.status != "in_development" + || vanillaSigns.modules?.sanctuary?.target_module_version != "0.0.0-alpha.139" + || vanillaSigns.modules?.redstoner?.target_module_version != "0.0.0-alpha.20" + || vanillaSigns.modules?.redstoner?.target_data_version != 5 + || vanillaSigns.modules?.redstoner?.target_network_protocol != 9 + || vanillaSigns.trophy_sign?.item_id_preserved != "redstoner:trophy_panel" + || vanillaSigns.trophy_sign?.standing_block_id != "redstoner:trophy_sign" + || vanillaSigns.trophy_sign?.wall_block_id != "redstoner:trophy_wall_sign" + || vanillaSigns.trophy_sign?.vanilla_sign_block_entity != true + || vanillaSigns.trophy_sign?.maximum_pages_per_sign != 16 + || vanillaSigns.trophy_sign?.display_modes != ["names_and_values", "names", "values"] + || vanillaSigns.ranking_api?.cached_player_names_repair_uuid_prefix_placeholders != true + || vanillaSigns.ranking_api?.uuid_prefixes_never_used_as_display_fallback != true || !registry.contains('RedStonerMod.id("trophy_panel")') + || !registry.contains('RedStonerMod.id("trophy_sign")') + || !registry.contains('RedStonerMod.id("trophy_wall_sign")') + || !registry.contains("new StandingSignBlock(WoodType.PALE_OAK") + || !registry.contains("new WallSignBlock(WoodType.PALE_OAK") + || !registry.contains("signType.addValidBlock(TROPHY_SIGN)") + || !registry.contains("new TrophySignItem(TROPHY_SIGN, TROPHY_WALL_SIGN") || !panel.contains("REFRESH_INTERVAL_TICKS = 20") || !panel.contains("CLIENT_SNAPSHOT_TAG") || !panel.contains("PAGE_CODEC.listOf(0, TrophyPanelRules.MAX_PAGES)") @@ -4282,23 +4325,42 @@ project(":redstoner") { || !renderer.contains("TrophyPanelRenderState.LINE_COUNT") || !renderer.contains("panel.valuesView()") || !renderer.contains("state.lightCoords, TEXT_COLOR, 0, 0") - || model.textures?.wood != "minecraft:block/oak_planks" - || model.textures?.containsKey("screen") - || model.elements?.size() != 1 - || model.elements[0].from != [0, 0, 14] - || model.elements[0].to != [16, 16, 16] + || !signItem.contains("extends SignItem") + || !signItem.contains("activatePlacedSign") + || !service.contains("PlayerBlockBreakEvents.BEFORE") + || !service.contains("player.isSecondaryUseActive()") + || !service.contains("MAX_INSPECTIONS_PER_REFRESH = 128") + || !service.contains("distanceToSqr") + || !service.contains("isConfigurableMetric") + || !signData.contains("MAX_SIGNS = 16_384") + || !signData.contains("SAVE_KEY = \"trophy_signs\"") + || !editor.contains("TrophyPanelRules.MAX_PAGES") + || !networking.contains("SaveTrophySignPayload.STREAM_CODEC") + || standingModel.parent != "minecraft:block/template_sign_rot_0" + || standingModel.textures?.all != "minecraft:block/pale_oak_sign" + || wallModel.parent != "minecraft:block/template_wall_sign" + || wallModel.textures?.all != "minecraft:block/pale_oak_sign" + || recipe.ingredients != ["#minecraft:signs", "minecraft:white_dye"] + || recipe.result != [id: "redstoner:trophy_panel", count: 1] || !rankingApi.contains("public static RankingSnapshot snapshot") || !rankingApi.contains("public static Component displayName") + || !rankingApi.contains("public static List configurableMetrics()") || !rankingApi.contains("!player.isCreative() && !player.isSpectator()") || !rankingData.contains("vanillaStatsImported") || !rankingData.contains("MAX_VALUE") + || !rankingData.contains("resolveCachedNames") + || !rankingData.contains('orElse("?")') + || rankingData.contains("substring(0, 8)") || redstonerLocales.any { locale -> requiredKeys.any { !locale.containsKey(it) } } || sanctuaryLocales.any { !it.containsKey("metric.sanctuary.ranking.play_time") - || !it.containsKey("metric.sanctuary.ranking.killed") } - || !file("src/main/resources/assets/redstoner/models/block/trophy_panel.json").isFile() - || !file("src/main/resources/data/redstoner/loot_table/blocks/trophy_panel.json").isFile() + || !it.containsKey("metric.sanctuary.ranking.killed") + || !it.containsKey("metric.sanctuary.ranking.killed_by") } + || !file("src/main/resources/assets/redstoner/blockstates/trophy_sign.json").isFile() + || !file("src/main/resources/assets/redstoner/blockstates/trophy_wall_sign.json").isFile() + || !file("src/main/resources/data/redstoner/loot_table/blocks/trophy_sign.json").isFile() + || !file("src/main/resources/data/redstoner/loot_table/blocks/trophy_wall_sign.json").isFile() || !file("src/main/resources/data/redstoner/recipe/trophy_panel.json").isFile()) { - throw new GradleException("The automatic trophy-panel ranking contract is incomplete") + throw new GradleException("The vanilla trophy-sign ranking contract is incomplete") } } } @@ -4433,9 +4495,9 @@ project(":redstoner") { "src/main/resources/data/redstoner/recipe/controller.json")) def terminalRecipe = new JsonSlurper().parse(file( "src/main/resources/data/redstoner/recipe/storage_terminal.json")) - if (project.version.toString() != "0.0.0-alpha.19" - || manifest.custom?.sanctuary26?.data_version != 4 - || manifest.custom?.sanctuary26?.network_protocol != 8 + if (project.version.toString() != "0.0.0-alpha.20" + || manifest.custom?.sanctuary26?.data_version != 5 + || manifest.custom?.sanctuary26?.network_protocol != 9 || terminalMigration.source?.pack_version != "26.2.0-alpha.217" || terminalMigration.source?.modules != [ redstoner: "0.0.0-alpha.10", onlyfun: "0.0.0-alpha.24", sanctuary: "0.0.0-alpha.120"] @@ -4501,7 +4563,7 @@ project(":redstoner") { || drawerMigration.compatibility?.redstoner_network_protocol != 5 || manifest.depends?.containsKey("ambiance") || manifest.depends?.anotherworld != "*" - || manifest.depends?.sanctuary != "*" + || manifest.depends?.sanctuary != ">=0.0.0-alpha.139" || ownershipMigration.target?.modules?.redstoner != "0.0.0-alpha.8" || ownershipMigration.ownership?.canon?.previous_owner != "redstoner" || ownershipMigration.ownership?.weather_forecast?.previous_owner != "redstoner" @@ -4609,7 +4671,7 @@ tasks.register("verifyShopPricingLayoutRelease") { def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json")) if (rootProject.pack_version != "26.2.0-alpha.232" || rootProject.onlyfun_version != "0.0.0-alpha.27" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || 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" @@ -4703,7 +4765,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.232" || rootProject.onlyfun_version != "0.0.0-alpha.27" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || 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"] @@ -4796,7 +4858,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.232" || rootProject.itsalive_version != "0.0.0-alpha.38" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || 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"] @@ -4896,7 +4958,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.232" || rootProject.itsalive_version != "0.0.0-alpha.38" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || 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" @@ -5016,7 +5078,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.232" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || 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" @@ -5073,7 +5135,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") { || rootProject.anotherworld_version != "0.0.0-alpha.58" || rootProject.itsalive_version != "0.0.0-alpha.38" || rootProject.iliketomoveit_version != "0.0.0-alpha.18" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || migration.source?.pack_version != "26.2.0-alpha.201" || migration.source?.modules != [ itsalive: "0.0.0-alpha.30", @@ -5231,7 +5293,7 @@ tasks.register("verifySafeVaultFeature") { }.keySet() if (rootProject.pack_version != "26.2.0-alpha.232" || rootProject.anotherworld_version != "0.0.0-alpha.58" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || migration.schema_version != 1 || migration.source?.pack_version != "26.2.0-alpha.201" || migration.target?.pack_version != "26.2.0-alpha.202" @@ -6091,7 +6153,7 @@ tasks.register("verifyAlpha210Release") { it.path == "mods/jei-26.2-fabric-30.29.0.201.jar" } if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || rootProject.onlyfun_version != "0.0.0-alpha.27" || rootProject.iliketomoveit_version != "0.0.0-alpha.18" || rootProject.itsalive_version != "0.0.0-alpha.38" @@ -6193,7 +6255,7 @@ tasks.register("verifyAlpha211Release") { itsalive: "0.0.0-alpha.38", ouch: "0.0.0-alpha.11", masterkey: "0.0.0-alpha.9"] - def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.138"] + def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.139"] def expectedDataVersions = [ambiance: 2, itsalive: 3, onlyfun: 2, ouch: 1, sanctuary: 11] def workspaceDataVersions = [ambiance: 2, itsalive: 4, onlyfun: 2, ouch: 1, sanctuary: 15] def expectedProtocols = [ambiance: 6, itsalive: 4, onlyfun: 3, ouch: 1, sanctuary: 22] @@ -6293,7 +6355,7 @@ tasks.register("verifyAlpha212Release") { it.path == "mods/jei-26.2-fabric-30.29.0.201.jar" } if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || 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" @@ -6784,7 +6846,7 @@ tasks.register("verifyAlpha218Release") { "file = \"mods/${id}-${version}.jar\"" } if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.redstoner_version != "0.0.0-alpha.19" + || rootProject.redstoner_version != "0.0.0-alpha.20" || currentModules.findAll { id, version -> id != "redstoner" && id != "sanctuary" && id != "onlyfun" && id != "iliketomoveit" }.any { @@ -6792,7 +6854,7 @@ tasks.register("verifyAlpha218Release") { } || rootProject.onlyfun_version != "0.0.0-alpha.27" || rootProject.iliketomoveit_version != "0.0.0-alpha.18" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || migration.source?.pack_version != "26.2.0-alpha.217" || migration.source?.modules != sourceModules || migration.target?.pack_version != "26.2.0-alpha.218" @@ -6812,15 +6874,15 @@ tasks.register("verifyAlpha218Release") { || migration.distribution?.pack_release_updated != true || migration.distribution?.packwiz_index_refreshed != true || migration.distribution?.prism_modrinth_index_updated != true - || manifests.redstoner.custom?.sanctuary26?.data_version != 4 - || manifests.redstoner.custom?.sanctuary26?.network_protocol != 8 + || manifests.redstoner.custom?.sanctuary26?.data_version != 5 + || manifests.redstoner.custom?.sanctuary26?.network_protocol != 9 || manifests.onlyfun.custom?.sanctuary26?.data_version != 2 || manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3 || manifests.sanctuary.custom?.sanctuary26?.data_version != 15 || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24 || !rules.contains("MAX_CONTAINERS = 128") || !rules.contains("PAGE_SIZE = 54") - || !networking.contains("PROTOCOL_VERSION = 8") + || !networking.contains("PROTOCOL_VERSION = 9") || !onlyFunPolicy.contains('namespace.equals("redstoner") && path.equals("storage_terminal")') || !blackMarketPolicy.contains('namespace.equals("redstoner") && path.equals("storage_terminal")') || release.pack_version != rootProject.pack_version @@ -6878,7 +6940,7 @@ tasks.register("verifyAlpha219Release") { ambiance: "0.0.0-alpha.22", sanctuary: "0.0.0-alpha.138", iliketomoveit: "0.0.0-alpha.18", onlyfun: "0.0.0-alpha.27", itsalive: "0.0.0-alpha.38", ouch: "0.0.0-alpha.11", masterkey: "0.0.0-alpha.9"] - def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.138"] + def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.139"] def requiredJars = currentModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } @@ -6973,7 +7035,7 @@ tasks.register("verifyAlpha220Release") { def networking = file( "redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java").text if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.redstoner_version != "0.0.0-alpha.19" + || rootProject.redstoner_version != "0.0.0-alpha.20" || migration.source?.pack_version != "26.2.0-alpha.219" || migration.source?.modules != [redstoner: "0.0.0-alpha.11"] || migration.target?.pack_version != "26.2.0-alpha.220" @@ -6997,8 +7059,8 @@ tasks.register("verifyAlpha220Release") { || migration.compatibility?.redstoner_data_version != 3 || migration.compatibility?.redstoner_source_network_protocol != 6 || migration.compatibility?.redstoner_target_network_protocol != 7 - || manifest.custom?.sanctuary26?.data_version != 4 - || manifest.custom?.sanctuary26?.network_protocol != 8 + || manifest.custom?.sanctuary26?.data_version != 5 + || manifest.custom?.sanctuary26?.network_protocol != 9 || !menu.contains("extends AbstractContainerMenu") || !menu.contains("addStandardInventorySlots") || !menu.contains("SanctuaryPlayerInventoryView.unlockedRows") @@ -7009,7 +7071,7 @@ tasks.register("verifyAlpha220Release") { || !screen.contains("extends AbstractContainerScreen") || !screen.contains("textures/gui/container/generic_54.png") || screen.contains('Button.builder(Component.translatable("gui.done")') - || !networking.contains("PROTOCOL_VERSION = 8") + || !networking.contains("PROTOCOL_VERSION = 9") || networking.contains("storage_terminal_snapshot") || networking.contains("storage_terminal_action") || release.pack_version != rootProject.pack_version @@ -7202,8 +7264,8 @@ tasks.register("verifyAlpha222Release") { it.path == "mods/jei-26.2-fabric-30.29.0.201.jar" } if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.redstoner_version != "0.0.0-alpha.19" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.redstoner_version != "0.0.0-alpha.20" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || migration.source?.pack_version != "26.2.0-alpha.221" || migration.source?.modules != [ redstoner: "0.0.0-alpha.12", sanctuary: "0.0.0-alpha.122"] @@ -7243,8 +7305,8 @@ tasks.register("verifyAlpha222Release") { || migration.distribution?.jei?.target != "30.29.0.196" || migration.distribution?.jei?.modrinth_version_id != "vvwsu2BR" || migration.distribution?.jei?.packwiz_and_prism_synchronized != true - || manifests.redstoner.custom?.sanctuary26?.data_version != 4 - || manifests.redstoner.custom?.sanctuary26?.network_protocol != 8 + || manifests.redstoner.custom?.sanctuary26?.data_version != 5 + || manifests.redstoner.custom?.sanctuary26?.network_protocol != 9 || manifests.sanctuary.custom?.sanctuary26?.data_version != 15 || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24 || !rules.contains("MAX_SEARCH_LENGTH = 32") @@ -7260,7 +7322,7 @@ tasks.register("verifyAlpha222Release") { || !screen.contains("searchBox.setResponder(this::searchChanged)") || !smoke.contains("verifySearch()") || !smoke.contains("verifyStableLayout()") - || !networking.contains("PROTOCOL_VERSION = 8") + || !networking.contains("PROTOCOL_VERSION = 9") || networking.contains("storage_terminal_search") || !questPayload.contains("MAX_ENTRIES = 8") || !questPayload.contains('SanctuaryMod.id("s2c/quest_tracker")') @@ -7421,7 +7483,7 @@ tasks.register("verifyAlpha223Release") { it.path == "mods/jei-26.2-fabric-30.29.0.201.jar" } if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || migration.source?.pack_version != "26.2.0-alpha.222" || migration.source?.modules != [sanctuary: "0.0.0-alpha.124"] || migration.target?.pack_version != "26.2.0-alpha.223" @@ -7542,7 +7604,7 @@ tasks.register("verifyAlpha224Release") { def currentModules = targetModules + [ itsalive: "0.0.0-alpha.38", iliketomoveit: "0.0.0-alpha.18", onlyfun: "0.0.0-alpha.27", sanctuary: "0.0.0-alpha.138"] - def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.138"] + def workspaceModules = currentModules + [sanctuary: "0.0.0-alpha.139"] def inventorySort = file( "sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/gameplay/SanctuaryInventorySort.java").text def clientProgress = file( @@ -7664,11 +7726,12 @@ tasks.register("verifyAlpha225Release") { sanctuary: "0.0.0-alpha.129"] def currentModules = targetModules + [sanctuary: "0.0.0-alpha.138"] def currentWorkspaceModules = currentModules + [ - anotherworld: "0.0.0-alpha.58", sanctuary: "0.0.0-alpha.138"] + anotherworld: "0.0.0-alpha.58", sanctuary: "0.0.0-alpha.139"] def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def releasedModules = workspaceModules + def releasedModules = workspaceModules + [ + sanctuary: "0.0.0-alpha.138", redstoner: "0.0.0-alpha.19"] def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } @@ -7828,12 +7891,13 @@ tasks.register("verifyAlpha226Release") { def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def releasedModules = workspaceModules + def releasedModules = workspaceModules + [ + sanctuary: "0.0.0-alpha.138", redstoner: "0.0.0-alpha.19"] def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || migration.status != "released" || migration.source?.pack_version != "26.2.0-alpha.225" || migration.source?.modules != [sanctuary: "0.0.0-alpha.129"] @@ -7919,13 +7983,14 @@ tasks.register("verifyAlpha227Release") { def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def releasedModules = workspaceModules + def releasedModules = workspaceModules + [ + sanctuary: "0.0.0-alpha.138", redstoner: "0.0.0-alpha.19"] def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } if (rootProject.pack_version != "26.2.0-alpha.232" || rootProject.anotherworld_version != "0.0.0-alpha.58" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || migration.status != "released" || migration.source?.pack_version != "26.2.0-alpha.226" || migration.source?.modules != [ @@ -8017,13 +8082,14 @@ tasks.register("verifyAlpha228Release") { def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def releasedModules = workspaceModules + def releasedModules = workspaceModules + [ + sanctuary: "0.0.0-alpha.138", redstoner: "0.0.0-alpha.19"] def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.redstoner_version != "0.0.0-alpha.19" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.redstoner_version != "0.0.0-alpha.20" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || migration.status != "released" || migration.source?.pack_version != "26.2.0-alpha.227" || migration.source?.modules != [ @@ -8055,8 +8121,8 @@ tasks.register("verifyAlpha228Release") { || migration.distribution?.pack_release_updated != true || migration.distribution?.packwiz_index_refreshed != true || migration.distribution?.prism_modrinth_index_updated != true - || redStonerManifest.custom?.sanctuary26?.data_version != 4 - || redStonerManifest.custom?.sanctuary26?.network_protocol != 8 + || redStonerManifest.custom?.sanctuary26?.data_version != 5 + || redStonerManifest.custom?.sanctuary26?.network_protocol != 9 || sanctuaryManifest.custom?.sanctuary26?.data_version != 15 || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 24 || !terminalSearch.contains("DataComponents.CONTAINER") @@ -8122,7 +8188,8 @@ tasks.register("verifyAlpha229Release") { def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def releasedModules = workspaceModules + def releasedModules = workspaceModules + [ + sanctuary: "0.0.0-alpha.138", redstoner: "0.0.0-alpha.19"] def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } @@ -8132,8 +8199,8 @@ tasks.register("verifyAlpha229Release") { "screen.redstoner.storage_terminal.sort.count_ascending", "screen.redstoner.storage_terminal.sort.count_descending"] if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.redstoner_version != "0.0.0-alpha.19" - || rootProject.sanctuary_version != "0.0.0-alpha.138" + || rootProject.redstoner_version != "0.0.0-alpha.20" + || rootProject.sanctuary_version != "0.0.0-alpha.139" || migration.status != "released" || migration.source?.pack_version != "26.2.0-alpha.228" || migration.source?.modules != [redstoner: "0.0.0-alpha.15"] @@ -8157,8 +8224,8 @@ tasks.register("verifyAlpha229Release") { || migration.distribution?.pack_release_updated != true || migration.distribution?.packwiz_index_refreshed != true || migration.distribution?.prism_modrinth_index_updated != true - || manifest.custom?.sanctuary26?.data_version != 4 - || manifest.custom?.sanctuary26?.network_protocol != 8 + || manifest.custom?.sanctuary26?.data_version != 5 + || manifest.custom?.sanctuary26?.network_protocol != 9 || locales.any { locale -> requiredTranslationKeys.any { !locale.containsKey(it) } } || !sort.contains('ALPHABETICAL("alphabetical")') || !sort.contains('COUNT_ASCENDING("count_ascending")') @@ -8210,15 +8277,16 @@ tasks.register("verifyAlpha230Release") { def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def releasedModules = workspaceModules + def releasedModules = workspaceModules + [ + sanctuary: "0.0.0-alpha.138", redstoner: "0.0.0-alpha.19"] def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } def indexHash = java.security.MessageDigest.getInstance("SHA-256") .digest(packwizIndexFile.bytes).encodeHex().toString() if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.sanctuary_version != "0.0.0-alpha.138" - || rootProject.redstoner_version != "0.0.0-alpha.19" + || rootProject.sanctuary_version != "0.0.0-alpha.139" + || rootProject.redstoner_version != "0.0.0-alpha.20" || migration.schema_version != 1 || migration.status != "released" || migration.source?.pack_version != "26.2.0-alpha.229" @@ -8249,8 +8317,8 @@ tasks.register("verifyAlpha230Release") { || migration.distribution?.shared_client_server_mrpack != true || manifests.sanctuary.custom?.sanctuary26?.data_version != 15 || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24 - || manifests.redstoner.custom?.sanctuary26?.data_version != 4 - || manifests.redstoner.custom?.sanctuary26?.network_protocol != 8 + || manifests.redstoner.custom?.sanctuary26?.data_version != 5 + || manifests.redstoner.custom?.sanctuary26?.network_protocol != 9 || release.pack_version != "26.2.0-alpha.232" || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.232.mrpack" || prism.versionId != "26.2.0-alpha.232" @@ -8316,15 +8384,16 @@ tasks.register("verifyAlpha231Release") { def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def releasedModules = workspaceModules + def releasedModules = workspaceModules + [ + sanctuary: "0.0.0-alpha.138", redstoner: "0.0.0-alpha.19"] def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } def indexHash = java.security.MessageDigest.getInstance("SHA-256") .digest(packwizIndexFile.bytes).encodeHex().toString() if (rootProject.pack_version != "26.2.0-alpha.232" - || rootProject.sanctuary_version != "0.0.0-alpha.138" - || rootProject.redstoner_version != "0.0.0-alpha.19" + || rootProject.sanctuary_version != "0.0.0-alpha.139" + || rootProject.redstoner_version != "0.0.0-alpha.20" || migration.schema_version != 1 || migration.status != "released" || migration.source?.pack_version != "26.2.0-alpha.230" @@ -8363,8 +8432,8 @@ tasks.register("verifyAlpha231Release") { || migration.distribution?.jei?.packwiz_and_prism_synchronized != true || manifests.sanctuary.custom?.sanctuary26?.data_version != 15 || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24 - || manifests.redstoner.custom?.sanctuary26?.data_version != 4 - || manifests.redstoner.custom?.sanctuary26?.network_protocol != 8 + || manifests.redstoner.custom?.sanctuary26?.data_version != 5 + || manifests.redstoner.custom?.sanctuary26?.network_protocol != 9 || !layout.contains("LOOT_CHEST_CELL_SIZE_CHUNKS = 3") || !layout.contains("EXIT_BED_CELL_SIZE_CHUNKS = 3") || !layout.contains("lootChestChunk(") @@ -8460,15 +8529,17 @@ tasks.register("verifyAlpha232Release") { def workspaceModules = moduleIds.collectEntries { id -> [(id): project(":${id}").version.toString()] } - def requiredJars = workspaceModules.collect { id, version -> + def releasedModules = workspaceModules + [ + sanctuary: "0.0.0-alpha.138", redstoner: "0.0.0-alpha.19"] + def requiredJars = releasedModules.collect { id, version -> "file = \"mods/${id}-${version}.jar\"" } def indexHash = MessageDigest.getInstance("SHA-256") .digest(packwizIndexFile.bytes).encodeHex().toString() if (rootProject.pack_version != "26.2.0-alpha.232" || rootProject.anotherworld_version != "0.0.0-alpha.58" - || rootProject.sanctuary_version != "0.0.0-alpha.138" - || rootProject.redstoner_version != "0.0.0-alpha.19" + || rootProject.sanctuary_version != "0.0.0-alpha.139" + || rootProject.redstoner_version != "0.0.0-alpha.20" || migration.schema_version != 1 || migration.status != "released" || migration.source?.pack_version != "26.2.0-alpha.231" @@ -8511,8 +8582,8 @@ tasks.register("verifyAlpha232Release") { || manifests.anotherworld.custom?.sanctuary26?.network_protocol != 2 || manifests.sanctuary.custom?.sanctuary26?.data_version != 15 || manifests.sanctuary.custom?.sanctuary26?.network_protocol != 24 - || manifests.redstoner.custom?.sanctuary26?.data_version != 4 - || manifests.redstoner.custom?.sanctuary26?.network_protocol != 8 + || manifests.redstoner.custom?.sanctuary26?.data_version != 5 + || manifests.redstoner.custom?.sanctuary26?.network_protocol != 9 || !capture.contains("CanonScreenshotFiles.reserve(") || !capture.contains("screenshot.copyRect(exported") || !capture.contains("applyStyle(image, style)") diff --git a/gradle.properties b/gradle.properties index d68c2b0..aefdcb6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ mod_version=0.0.0-alpha.0 module_lifecycle=standby ambiance_version=0.0.0-alpha.22 ambiance_lifecycle=active -redstoner_version=0.0.0-alpha.19 +redstoner_version=0.0.0-alpha.20 redstoner_lifecycle=active anotherworld_version=0.0.0-alpha.58 anotherworld_lifecycle=active @@ -28,7 +28,7 @@ iliketomoveit_version=0.0.0-alpha.18 iliketomoveit_lifecycle=active onlyfun_version=0.0.0-alpha.27 onlyfun_lifecycle=active -sanctuary_version=0.0.0-alpha.138 +sanctuary_version=0.0.0-alpha.139 sanctuary_lifecycle=active pack_version=26.2.0-alpha.232 maven_group=fr.koka99cab.sanctuary26 diff --git a/pack/migrations/trophy-panel-to-vanilla-signs.json b/pack/migrations/trophy-panel-to-vanilla-signs.json new file mode 100644 index 0000000..6db384a --- /dev/null +++ b/pack/migrations/trophy-panel-to-vanilla-signs.json @@ -0,0 +1,54 @@ +{ + "schema_version": 1, + "status": "in_development", + "modules": { + "sanctuary": { + "source_module_version": "0.0.0-alpha.138", + "target_module_version": "0.0.0-alpha.139", + "data_version": 15, + "network_protocol": 24 + }, + "redstoner": { + "source_module_version": "0.0.0-alpha.19", + "target_module_version": "0.0.0-alpha.20", + "source_data_version": 4, + "target_data_version": 5, + "source_network_protocol": 8, + "target_network_protocol": 9 + } + }, + "trophy_sign": { + "owner": "redstoner", + "item_id_preserved": "redstoner:trophy_panel", + "standing_block_id": "redstoner:trophy_sign", + "wall_block_id": "redstoner:trophy_wall_sign", + "legacy_block_id": "redstoner:trophy_panel", + "legacy_blocks_migrate_on_load": true, + "vanilla_sign_block_entity": true, + "vanilla_wood_type": "pale_oak", + "save_key": "redstoner:trophy_signs", + "save_data_version": 1, + "maximum_signs": 16384, + "maximum_pages_per_sign": 16, + "editor_opens_on_first_placement": true, + "owner_or_operator_configuration": true, + "owner_or_operator_breaking": true, + "public_right_click_page_cycle": true, + "display_modes": ["names_and_values", "names", "values"], + "server_revalidates_distance_owner_block_and_metrics": true + }, + "ranking_api": { + "client_catalog_built_from_synchronized_registries": true, + "catalog_sent_over_network": false, + "cached_player_names_repair_uuid_prefix_placeholders": true, + "uuid_prefixes_never_used_as_display_fallback": true + }, + "compatibility": { + "existing_saves_supported": true, + "existing_item_stacks_supported": true, + "new_world_required": false, + "save_reset_required": false, + "monotone_data_migration": true, + "client_server_update_required": true + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/RedStonerMod.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/RedStonerMod.java index 4fb7e68..4a13381 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/RedStonerMod.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/RedStonerMod.java @@ -4,6 +4,7 @@ import fr.koka99cab.sanctuary26.redstoner.api.RedStonerApi; import fr.koka99cab.sanctuary26.redstoner.network.RedStonerNetworking; import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries; import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerMenus; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophySignService; import fr.koka99cab.sanctuary26.sanctuary.api.server.SanctuaryServerIdentity; import net.fabricmc.api.ModInitializer; import net.minecraft.resources.Identifier; @@ -13,7 +14,7 @@ import org.slf4j.LoggerFactory; public final class RedStonerMod implements ModInitializer { public static final String MOD_ID = "redstoner"; public static final String DISPLAY_NAME = "Red-Stoner"; - public static final int CURRENT_DATA_VERSION = 4; + public static final int CURRENT_DATA_VERSION = 5; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); public RedStonerMod() { @@ -27,6 +28,7 @@ public final class RedStonerMod implements ModInitializer { public void onInitialize() { RedStonerApi.registerServerIdentityProvider(SanctuaryServerIdentity::get); RedStonerRegistries.initialize(); + TrophySignService.initialize(); RedStonerMenus.initialize(); RedStonerNetworking.initialize(); LOGGER.info("[{}] Computers, Drawer, Particuleur, Storage Terminal and Trophy Panel ready (data v{}, network v{}).", diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/block/entity/TrophyPanelBlockEntity.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/block/entity/TrophyPanelBlockEntity.java index 963ba6e..9849311 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/block/entity/TrophyPanelBlockEntity.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/block/entity/TrophyPanelBlockEntity.java @@ -5,6 +5,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import fr.koka99cab.sanctuary26.redstoner.block.TrophyPanelBlock; import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries; import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophySignService; import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingMetric; import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingSnapshot; import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit; @@ -63,6 +64,7 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn private int currentPage; private boolean valuesView; private long lastRefreshTick = Long.MIN_VALUE; + private boolean migrationAttempted; private ClientSnapshot visibleSnapshot = ClientSnapshot.empty(SanctuaryRankingApi.defaultMetric()); public TrophyPanelBlockEntity(BlockPos pos, BlockState state) { @@ -72,6 +74,13 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn public static void serverTick(net.minecraft.world.level.Level level, BlockPos pos, BlockState state, TrophyPanelBlockEntity panel) { if (!(level instanceof ServerLevel serverLevel)) return; + if (!panel.migrationAttempted) { + panel.migrationAttempted = true; + if (TrophySignService.migrateLegacy(serverLevel, pos, state.getValue(TrophyPanelBlock.FACING), + panel.owner, panel.ownerName, panel.pages.stream().map(Page::metricId).toList(), panel.currentPage)) { + return; + } + } long now = level.getGameTime(); if (panel.lastRefreshTick != Long.MIN_VALUE && now - panel.lastRefreshTick < REFRESH_INTERVAL_TICKS) return; panel.lastRefreshTick = now; diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/RedStonerClient.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/RedStonerClient.java index cf2c2d3..4909042 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/RedStonerClient.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/RedStonerClient.java @@ -4,10 +4,12 @@ import fr.koka99cab.sanctuary26.redstoner.RedStonerMod; import fr.koka99cab.sanctuary26.redstoner.client.gui.ComputerScreen; import fr.koka99cab.sanctuary26.redstoner.client.gui.PersonalComputerScreen; import fr.koka99cab.sanctuary26.redstoner.client.gui.StorageTerminalScreen; +import fr.koka99cab.sanctuary26.redstoner.client.gui.TrophySignEditorScreen; import fr.koka99cab.sanctuary26.redstoner.client.renderer.DrawerBlockEntityRenderer; import fr.koka99cab.sanctuary26.redstoner.client.renderer.TrophyPanelBlockEntityRenderer; import fr.koka99cab.sanctuary26.redstoner.network.OpenComputerPayload; import fr.koka99cab.sanctuary26.redstoner.network.OpenPersonalComputerPayload; +import fr.koka99cab.sanctuary26.redstoner.network.OpenTrophySignEditorPayload; import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerMenus; import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries; import net.fabricmc.api.ClientModInitializer; @@ -33,6 +35,9 @@ public final class RedStonerClient implements ClientModInitializer { context.client().gui.setScreen(new PersonalComputerScreen(payload)); } })); + ClientPlayNetworking.registerGlobalReceiver(OpenTrophySignEditorPayload.TYPE, (payload, context) -> + context.client().execute(() -> context.client().gui.setScreen( + new TrophySignEditorScreen(context.client().gui.screen(), payload)))); RedStonerMod.LOGGER.info("[{}] Client renderers and laboratory screens ready.", RedStonerMod.DISPLAY_NAME); } diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/TrophySignEditorScreen.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/TrophySignEditorScreen.java new file mode 100644 index 0000000..f500291 --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/TrophySignEditorScreen.java @@ -0,0 +1,265 @@ +package fr.koka99cab.sanctuary26.redstoner.client.gui; + +import fr.koka99cab.sanctuary26.redstoner.network.OpenTrophySignEditorPayload; +import fr.koka99cab.sanctuary26.redstoner.network.SaveTrophySignPayload; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyDisplayMode; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules; +import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingMetric; +import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit; +import fr.koka99cab.sanctuary26.sanctuary.api.ranking.SanctuaryRankingApi; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +/** White, bounded editor inspired by the community quest-sign screen. */ +public final class TrophySignEditorScreen extends Screen { + private static final int VISIBLE_ROWS = 5; + private static final int PANEL_WIDTH = 328; + private static final int PANEL_HEIGHT = 250; + private static final int COLUMN_WIDTH = 148; + private static final int GAP = 8; + private static final int BACKDROP = 0xD0F1F0EC; + private static final int BORDER = 0xFFFFFFFF; + private static final int TITLE = 0xFF343434; + private static final int MUTED = 0xFF6A6A6A; + + private final Screen parent; + private final OpenTrophySignEditorPayload payload; + private final List pages; + private final List catalog = new ArrayList<>(); + private final List filtered = new ArrayList<>(); + private final Button[] resultButtons = new Button[VISIBLE_ROWS]; + private final Button[] pageButtons = new Button[VISIBLE_ROWS]; + private EditBox search; + private Button previousResults; + private Button nextResults; + private Button previousPages; + private Button nextPages; + private Button moveUp; + private Button moveDown; + private Button remove; + private Button displayMode; + private Button save; + private TrophyDisplayMode mode; + private int catalogOffset; + private int pageOffset; + private int selectedPage = -1; + + public TrophySignEditorScreen(Screen parent, OpenTrophySignEditorPayload payload) { + super(Component.translatable("screen.redstoner.trophy_sign.title")); + this.parent = parent; + this.payload = payload; + this.pages = new ArrayList<>(payload.pages()); + this.mode = TrophyDisplayMode.byKey(payload.display()); + if (!pages.isEmpty()) selectedPage = 0; + } + + @Override + protected void init() { + int x = panelX() + 12; + int y = panelY(); + buildCatalog(); + search = addRenderableWidget(new EditBox(font, x, y + 38, COLUMN_WIDTH, 20, + Component.translatable("screen.redstoner.trophy_sign.search"))); + search.setMaxLength(64); + search.setHint(Component.translatable("screen.redstoner.trophy_sign.search")); + search.setResponder(value -> filterCatalog()); + + for (int slot = 0; slot < VISIBLE_ROWS; slot++) { + int resultSlot = slot; + resultButtons[slot] = addRenderableWidget(Button.builder(Component.empty(), + button -> addResult(resultSlot)).bounds(x, y + 62 + slot * 22, COLUMN_WIDTH, 20).build()); + int pageSlot = slot; + pageButtons[slot] = addRenderableWidget(Button.builder(Component.empty(), + button -> selectPage(pageSlot)).bounds(x + COLUMN_WIDTH + GAP, y + 38 + slot * 22, + COLUMN_WIDTH, 20).build()); + } + + previousResults = addRenderableWidget(Button.builder(Component.literal("‹"), button -> moveCatalog(-VISIBLE_ROWS)) + .bounds(x, y + 174, 72, 20).build()); + nextResults = addRenderableWidget(Button.builder(Component.literal("›"), button -> moveCatalog(VISIBLE_ROWS)) + .bounds(x + 76, y + 174, 72, 20).build()); + previousPages = addRenderableWidget(Button.builder(Component.literal("‹"), button -> movePageWindow(-VISIBLE_ROWS)) + .bounds(x + COLUMN_WIDTH + GAP, y + 174, 72, 20).build()); + nextPages = addRenderableWidget(Button.builder(Component.literal("›"), button -> movePageWindow(VISIBLE_ROWS)) + .bounds(x + COLUMN_WIDTH + GAP + 76, y + 174, 72, 20).build()); + + moveUp = addRenderableWidget(Button.builder(Component.literal("↑"), button -> moveSelected(-1)) + .bounds(x + COLUMN_WIDTH + GAP, y + 150, 46, 20).build()); + moveDown = addRenderableWidget(Button.builder(Component.literal("↓"), button -> moveSelected(1)) + .bounds(x + COLUMN_WIDTH + GAP + 50, y + 150, 46, 20).build()); + remove = addRenderableWidget(Button.builder(Component.translatable("screen.redstoner.trophy_sign.remove"), + button -> removeSelected()).bounds(x + COLUMN_WIDTH + GAP + 100, y + 150, 48, 20).build()); + + displayMode = addRenderableWidget(Button.builder(displayLabel(), button -> { + mode = mode.next(); + button.setMessage(displayLabel()); + }).bounds(x, y + 198, COLUMN_WIDTH * 2 + GAP, 20).build()); + save = addRenderableWidget(Button.builder(Component.translatable("gui.done"), button -> submit()) + .bounds(x, y + 224, COLUMN_WIDTH, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose()) + .bounds(x + COLUMN_WIDTH + GAP, y + 224, COLUMN_WIDTH, 20).build()); + + filterCatalog(); + refreshWidgets(); + setInitialFocus(search); + } + + @Override + public void tick() { + refreshWidgets(); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float tickDelta) { + int x = panelX(); + int y = panelY(); + graphics.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, BACKDROP); + graphics.outline(x, y, PANEL_WIDTH, PANEL_HEIGHT, BORDER); + graphics.centeredText(font, title, width / 2, y + 10, TITLE); + graphics.text(font, Component.translatable("screen.redstoner.trophy_sign.available"), + x + 12, y + 27, MUTED); + graphics.text(font, Component.translatable("screen.redstoner.trophy_sign.pages", + pages.size(), TrophyPanelRules.MAX_PAGES), x + 12 + COLUMN_WIDTH + GAP, y + 27, MUTED); + super.extractRenderState(graphics, mouseX, mouseY, tickDelta); + } + + private void buildCatalog() { + catalog.clear(); + for (RankingMetric metric : SanctuaryRankingApi.configurableMetrics()) { + Component label = SanctuaryRankingApi.displayName(metric); + catalog.add(new MetricOption(metric, label, (label.getString() + " " + metric.id()) + .toLowerCase(Locale.ROOT))); + } + catalog.sort(Comparator.comparing(option -> option.label().getString(), String.CASE_INSENSITIVE_ORDER)); + } + + private void filterCatalog() { + if (search == null) return; + String query = search.getValue().strip().toLowerCase(Locale.ROOT); + filtered.clear(); + for (MetricOption option : catalog) if (query.isEmpty() || option.searchText().contains(query)) filtered.add(option); + catalogOffset = 0; + refreshWidgets(); + } + + private void addResult(int slot) { + int index = catalogOffset + slot; + if (index < 0 || index >= filtered.size()) return; + Identifier id = filtered.get(index).metric().id(); + int existing = pages.indexOf(id); + if (existing >= 0) { + selectedPage = existing; + ensureSelectedVisible(); + } else if (pages.size() < TrophyPanelRules.MAX_PAGES) { + pages.add(id); + selectedPage = pages.size() - 1; + ensureSelectedVisible(); + } + refreshWidgets(); + } + + private void selectPage(int slot) { + int index = pageOffset + slot; + if (index >= 0 && index < pages.size()) selectedPage = index; + refreshWidgets(); + } + + private void moveSelected(int offset) { + int target = selectedPage + offset; + if (selectedPage < 0 || target < 0 || target >= pages.size()) return; + Identifier page = pages.remove(selectedPage); + pages.add(target, page); + selectedPage = target; + ensureSelectedVisible(); + refreshWidgets(); + } + + private void removeSelected() { + if (selectedPage < 0 || selectedPage >= pages.size()) return; + pages.remove(selectedPage); + selectedPage = pages.isEmpty() ? -1 : Math.min(selectedPage, pages.size() - 1); + ensureSelectedVisible(); + refreshWidgets(); + } + + private void moveCatalog(int amount) { + int maximum = Math.max(0, filtered.size() - VISIBLE_ROWS); + catalogOffset = Math.clamp(catalogOffset + amount, 0, maximum); + refreshWidgets(); + } + + private void movePageWindow(int amount) { + int maximum = Math.max(0, pages.size() - VISIBLE_ROWS); + pageOffset = Math.clamp(pageOffset + amount, 0, maximum); + refreshWidgets(); + } + + private void ensureSelectedVisible() { + if (selectedPage < pageOffset) pageOffset = selectedPage; + if (selectedPage >= pageOffset + VISIBLE_ROWS) pageOffset = selectedPage - VISIBLE_ROWS + 1; + pageOffset = Math.max(0, pageOffset); + } + + private void refreshWidgets() { + if (save == null) return; + for (int slot = 0; slot < VISIBLE_ROWS; slot++) { + int resultIndex = catalogOffset + slot; + Button result = resultButtons[slot]; + result.visible = resultIndex < filtered.size(); + if (result.visible) { + MetricOption option = filtered.get(resultIndex); + result.setMessage(Component.literal(pages.contains(option.metric().id()) ? "✓ " : "+ ") + .append(option.label())); + result.active = pages.contains(option.metric().id()) || pages.size() < TrophyPanelRules.MAX_PAGES; + } + + int pageIndex = pageOffset + slot; + Button page = pageButtons[slot]; + page.visible = pageIndex < pages.size(); + if (page.visible) page.setMessage(Component.literal(pageIndex == selectedPage ? "▶ " : "") + .append(Component.literal((pageIndex + 1) + ". ")).append(metricLabel(pages.get(pageIndex)))); + } + previousResults.active = catalogOffset > 0; + nextResults.active = catalogOffset + VISIBLE_ROWS < filtered.size(); + previousPages.active = pageOffset > 0; + nextPages.active = pageOffset + VISIBLE_ROWS < pages.size(); + moveUp.active = selectedPage > 0; + moveDown.active = selectedPage >= 0 && selectedPage + 1 < pages.size(); + remove.active = selectedPage >= 0; + save.active = !pages.isEmpty() && ClientPlayNetworking.canSend(SaveTrophySignPayload.TYPE); + } + + private Component metricLabel(Identifier id) { + RankingMetric metric = SanctuaryRankingApi.metric(id).orElseGet(() -> + new RankingMetric(id, "metric.sanctuary.ranking.unknown", RankingUnit.COUNT)); + return SanctuaryRankingApi.displayName(metric); + } + + private Component displayLabel() { + return Component.translatable("screen.redstoner.trophy_sign.display", + Component.translatable("screen.redstoner.trophy_sign.display." + mode.key())); + } + + private void submit() { + if (pages.isEmpty() || !ClientPlayNetworking.canSend(SaveTrophySignPayload.TYPE)) return; + ClientPlayNetworking.send(new SaveTrophySignPayload(payload.pos(), pages, mode.key())); + onClose(); + } + + private int panelX() { return (width - PANEL_WIDTH) / 2; } + private int panelY() { return Math.max(2, (height - PANEL_HEIGHT) / 2); } + + @Override public void onClose() { if (minecraft != null) minecraft.gui.setScreen(parent); } + @Override public boolean isPauseScreen() { return false; } + + private record MetricOption(RankingMetric metric, Component label, String searchText) {} +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/OpenTrophySignEditorPayload.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/OpenTrophySignEditorPayload.java new file mode 100644 index 0000000..1bf5222 --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/OpenTrophySignEditorPayload.java @@ -0,0 +1,51 @@ +package fr.koka99cab.sanctuary26.redstoner.network; + +import fr.koka99cab.sanctuary26.redstoner.RedStonerMod; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyDisplayMode; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; + +/** Opens the white trophy-sign editor with only its small current configuration. */ +public record OpenTrophySignEditorPayload(BlockPos pos, List pages, String display) + implements CustomPacketPayload { + public static final Type TYPE = + new Type<>(RedStonerMod.id("s2c/open_trophy_sign_editor")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of(OpenTrophySignEditorPayload::write, OpenTrophySignEditorPayload::read); + + public OpenTrophySignEditorPayload { + pos = pos == null ? BlockPos.ZERO : pos.immutable(); + pages = TrophyPanelRules.sanitizeMetricIds(pages); + display = TrophyDisplayMode.byKey(display).key(); + } + + private static OpenTrophySignEditorPayload read(RegistryFriendlyByteBuf buffer) { + BlockPos pos = buffer.readBlockPos(); + int count = boundedCount(buffer.readVarInt()); + List pages = new ArrayList<>(count); + for (int index = 0; index < count; index++) pages.add(Identifier.parse(buffer.readUtf(256))); + return new OpenTrophySignEditorPayload(pos, pages, buffer.readUtf(24)); + } + + private static void write(RegistryFriendlyByteBuf buffer, OpenTrophySignEditorPayload payload) { + buffer.writeBlockPos(payload.pos); + buffer.writeVarInt(payload.pages.size()); + for (Identifier page : payload.pages) buffer.writeUtf(page.toString(), 256); + buffer.writeUtf(payload.display, 24); + } + + private static int boundedCount(int count) { + if (count < 0 || count > TrophyPanelRules.MAX_PAGES) { + throw new IllegalArgumentException("Invalid trophy-sign page count: " + count); + } + return count; + } + + @Override public Type type() { return TYPE; } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java index a2d4de9..284ced6 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java @@ -4,6 +4,7 @@ import fr.koka99cab.sanctuary26.redstoner.RedStonerMod; import fr.koka99cab.sanctuary26.redstoner.block.entity.ComputerBlockEntity; import fr.koka99cab.sanctuary26.redstoner.block.entity.PersonalComputerBlockEntity; import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophySignService; import fr.koka99cab.sanctuary26.sanctuary.api.atlas.SanctuaryAtlasLandmarks; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; @@ -12,7 +13,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; public final class RedStonerNetworking { - public static final int PROTOCOL_VERSION = 8; + public static final int PROTOCOL_VERSION = 9; private static boolean initialized; private RedStonerNetworking() { @@ -25,6 +26,8 @@ public final class RedStonerNetworking { initialized = true; PayloadTypeRegistry.clientboundPlay().register(OpenComputerPayload.TYPE, OpenComputerPayload.STREAM_CODEC); PayloadTypeRegistry.clientboundPlay().register(OpenPersonalComputerPayload.TYPE, OpenPersonalComputerPayload.STREAM_CODEC); + PayloadTypeRegistry.clientboundPlay().register(OpenTrophySignEditorPayload.TYPE, + OpenTrophySignEditorPayload.STREAM_CODEC); PayloadTypeRegistry.serverboundPlay().register(SaveComputerProgramPayload.TYPE, SaveComputerProgramPayload.STREAM_CODEC); PayloadTypeRegistry.serverboundPlay().register(PlaceAtlasLandmarkPayload.TYPE, PlaceAtlasLandmarkPayload.STREAM_CODEC); PayloadTypeRegistry.serverboundPlay().register(RequestPersonalComputerSnapshotPayload.TYPE, @@ -33,6 +36,8 @@ public final class RedStonerNetworking { AcknowledgePersonalComputerIntroPayload.STREAM_CODEC); PayloadTypeRegistry.serverboundPlay().register(ShutdownPersonalComputerPayload.TYPE, ShutdownPersonalComputerPayload.STREAM_CODEC); + PayloadTypeRegistry.serverboundPlay().register(SaveTrophySignPayload.TYPE, + SaveTrophySignPayload.STREAM_CODEC); ServerPlayNetworking.registerGlobalReceiver(SaveComputerProgramPayload.TYPE, (payload, context) -> context.server().execute(() -> saveProgram(context.player(), payload))); ServerPlayNetworking.registerGlobalReceiver(PlaceAtlasLandmarkPayload.TYPE, (payload, context) -> @@ -43,6 +48,8 @@ public final class RedStonerNetworking { context.server().execute(() -> acknowledgePersonalComputerIntro(context.player(), payload))); ServerPlayNetworking.registerGlobalReceiver(ShutdownPersonalComputerPayload.TYPE, (payload, context) -> context.server().execute(() -> shutdownPersonalComputer(context.player(), payload))); + ServerPlayNetworking.registerGlobalReceiver(SaveTrophySignPayload.TYPE, (payload, context) -> + context.server().execute(() -> TrophySignService.save(context.player(), payload))); RedStonerMod.LOGGER.debug("[{}] Networking contract v{} registered.", RedStonerMod.DISPLAY_NAME, PROTOCOL_VERSION); } diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/SaveTrophySignPayload.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/SaveTrophySignPayload.java new file mode 100644 index 0000000..3209837 --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/SaveTrophySignPayload.java @@ -0,0 +1,51 @@ +package fr.koka99cab.sanctuary26.redstoner.network; + +import fr.koka99cab.sanctuary26.redstoner.RedStonerMod; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyDisplayMode; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; + +/** Bounded editor submission; ownership, distance, block and metrics are all revalidated server-side. */ +public record SaveTrophySignPayload(BlockPos pos, List pages, String display) + implements CustomPacketPayload { + public static final Type TYPE = + new Type<>(RedStonerMod.id("c2s/save_trophy_sign")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of(SaveTrophySignPayload::write, SaveTrophySignPayload::read); + + public SaveTrophySignPayload { + pos = pos == null ? BlockPos.ZERO : pos.immutable(); + pages = TrophyPanelRules.sanitizeMetricIds(pages); + display = TrophyDisplayMode.byKey(display).key(); + } + + private static SaveTrophySignPayload read(RegistryFriendlyByteBuf buffer) { + BlockPos pos = buffer.readBlockPos(); + int count = boundedCount(buffer.readVarInt()); + List pages = new ArrayList<>(count); + for (int index = 0; index < count; index++) pages.add(Identifier.parse(buffer.readUtf(256))); + return new SaveTrophySignPayload(pos, pages, buffer.readUtf(24)); + } + + private static void write(RegistryFriendlyByteBuf buffer, SaveTrophySignPayload payload) { + buffer.writeBlockPos(payload.pos); + buffer.writeVarInt(payload.pages.size()); + for (Identifier page : payload.pages) buffer.writeUtf(page.toString(), 256); + buffer.writeUtf(payload.display, 24); + } + + private static int boundedCount(int count) { + if (count < 0 || count > TrophyPanelRules.MAX_PAGES) { + throw new IllegalArgumentException("Invalid trophy-sign page count: " + count); + } + return count; + } + + @Override public Type type() { return TYPE; } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java index 4a5e1bb..b99b0e0 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java @@ -12,8 +12,10 @@ import fr.koka99cab.sanctuary26.redstoner.block.entity.DrawerBlockEntity; import fr.koka99cab.sanctuary26.redstoner.block.entity.PersonalComputerBlockEntity; import fr.koka99cab.sanctuary26.redstoner.block.entity.ParticuleurBlockEntity; import fr.koka99cab.sanctuary26.redstoner.block.entity.TrophyPanelBlockEntity; +import fr.koka99cab.sanctuary26.redstoner.trophy.TrophySignItem; import java.util.function.Function; import net.fabricmc.fabric.api.event.player.AttackBlockCallback; +import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityType; import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; @@ -28,10 +30,15 @@ import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.StandingSignBlock; +import net.minecraft.world.level.block.WallSignBlock; import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.BlockEntityTypes; import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.WoodType; import net.minecraft.world.level.material.MapColor; public final class RedStonerRegistries { @@ -79,6 +86,18 @@ public final class RedStonerRegistries { BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).strength(2.0F).sound(SoundType.WOOD) .requiresCorrectToolForDrops().noOcclusion().setId(blockKey(RedStonerMod.id("trophy_panel"))) ); + public static final StandingSignBlock TROPHY_SIGN = registerBlock( + RedStonerMod.id("trophy_sign"), + properties -> new StandingSignBlock(WoodType.PALE_OAK, properties), + BlockBehaviour.Properties.ofFullCopy(Blocks.PALE_OAK_SIGN).mapColor(MapColor.SNOW) + .setId(blockKey(RedStonerMod.id("trophy_sign"))) + ); + public static final WallSignBlock TROPHY_WALL_SIGN = registerBlock( + RedStonerMod.id("trophy_wall_sign"), + properties -> new WallSignBlock(WoodType.PALE_OAK, properties), + BlockBehaviour.Properties.ofFullCopy(Blocks.PALE_OAK_WALL_SIGN).mapColor(MapColor.SNOW) + .setId(blockKey(RedStonerMod.id("trophy_wall_sign"))) + ); public static final ComputerBlock LEGACY_EIGHT_BIT_COMPUTER = registerBlock( LEGACY_COMPUTER_ID, ComputerBlock::new, computerProperties(LEGACY_COMPUTER_ID, MapColor.COLOR_RED)); @@ -100,7 +119,7 @@ public final class RedStonerRegistries { public static final Item PARTICULEUR_ITEM = registerBlockItem(RedStonerMod.id("particuleur"), PARTICULEUR); public static final Item STORAGE_TERMINAL_ITEM = registerBlockItem( RedStonerMod.id("storage_terminal"), STORAGE_TERMINAL); - public static final Item TROPHY_PANEL_ITEM = registerBlockItem(RedStonerMod.id("trophy_panel"), TROPHY_PANEL); + public static final Item TROPHY_PANEL_ITEM = registerTrophySignItem(); public static final Item LEGACY_EIGHT_BIT_COMPUTER_ITEM = registerBlockItem(LEGACY_COMPUTER_ID, LEGACY_EIGHT_BIT_COMPUTER); public static final Item LEGACY_CONTROLLER_ITEM = registerBlockItem(LEGACY_CONTROLLER_ID, LEGACY_CONTROLLER); @@ -153,8 +172,16 @@ public final class RedStonerRegistries { return; } initialized = true; + FabricBlockEntityType signType = (FabricBlockEntityType) (Object) BlockEntityTypes.SIGN; + signType.addValidBlock(TROPHY_SIGN); + signType.addValidBlock(TROPHY_WALL_SIGN); registerDrawerAttack(); - RedStonerMod.LOGGER.info("[{}] Registered seven active blocks and five legacy block aliases.", RedStonerMod.DISPLAY_NAME); + RedStonerMod.LOGGER.info("[{}] Registered the vanilla trophy-sign pair and retained the old panel for migration.", + RedStonerMod.DISPLAY_NAME); + } + + public static boolean isTrophySign(Block block) { + return block == TROPHY_SIGN || block == TROPHY_WALL_SIGN; } public static boolean isController(BlockState state) { @@ -199,6 +226,14 @@ public final class RedStonerRegistries { new BlockItem(block, new Item.Properties().useBlockDescriptionPrefix().setId(key))); } + private static Item registerTrophySignItem() { + Identifier id = RedStonerMod.id("trophy_panel"); + ResourceKey key = ResourceKey.create(Registries.ITEM, id); + return Registry.register(BuiltInRegistries.ITEM, key, + new TrophySignItem(TROPHY_SIGN, TROPHY_WALL_SIGN, + new Item.Properties().useBlockDescriptionPrefix().setId(key))); + } + private static ResourceKey blockKey(Identifier id) { return ResourceKey.create(Registries.BLOCK, id); } diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyDisplayMode.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyDisplayMode.java new file mode 100644 index 0000000..ca8c08a --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyDisplayMode.java @@ -0,0 +1,28 @@ +package fr.koka99cab.sanctuary26.redstoner.trophy; + +/** Stable, persisted choice for the three ranking rows of a vanilla trophy sign. */ +public enum TrophyDisplayMode { + NAMES_AND_VALUES("names_and_values"), + NAMES("names"), + VALUES("values"); + + private final String key; + + TrophyDisplayMode(String key) { + this.key = key; + } + + public String key() { + return key; + } + + public TrophyDisplayMode next() { + TrophyDisplayMode[] modes = values(); + return modes[(ordinal() + 1) % modes.length]; + } + + public static TrophyDisplayMode byKey(String key) { + for (TrophyDisplayMode mode : values()) if (mode.key.equals(key)) return mode; + return NAMES_AND_VALUES; + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyPanelRules.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyPanelRules.java index 81c95dd..1659f4e 100644 --- a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyPanelRules.java +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyPanelRules.java @@ -9,6 +9,9 @@ import net.minecraft.resources.Identifier; /** Pure bounded rules shared by the trophy panel and its smoke tests. */ public final class TrophyPanelRules { public static final int MAX_PAGES = 16; + public static final int SIGN_LINE_COUNT = 4; + public static final int SIGN_ENTRIES_PER_PAGE = SIGN_LINE_COUNT - 1; + /** Legacy plate dimensions kept only while old block entities migrate. */ public static final int LINE_COUNT = 8; public static final int ENTRIES_PER_VIEW = LINE_COUNT - 1; private static final int VIEWS_PER_METRIC = 2; @@ -59,5 +62,13 @@ public final class TrophyPanelRules { return List.copyOf(new ArrayList<>(merged).subList(0, Math.min(MAX_PAGES, merged.size()))); } + public static List sanitizeMetricIds(List requested) { + LinkedHashSet unique = new LinkedHashSet<>(); + if (requested != null) { + requested.stream().filter(java.util.Objects::nonNull).limit(MAX_PAGES).forEach(unique::add); + } + return List.copyOf(unique); + } + public record DisplayPage(int metricPage, boolean valuesView) {} } diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignData.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignData.java new file mode 100644 index 0000000..9a44a71 --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignData.java @@ -0,0 +1,180 @@ +package fr.koka99cab.sanctuary26.redstoner.trophy; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import fr.koka99cab.sanctuary26.redstoner.RedStonerMod; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.SavedDataType; + +/** Versioned, server-owned configurations for the vanilla block entities used by trophy signs. */ +public final class TrophySignData extends SavedData { + public static final int DATA_VERSION = 1; + public static final String SAVE_KEY = "trophy_signs"; + public static final int MAX_SIGNS = 16_384; + + private static final Codec CONFIG_CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.fieldOf("dimension").forGetter(Config::dimension), + Codec.LONG.fieldOf("pos").forGetter(Config::pos), + Codec.STRING.fieldOf("owner").forGetter(Config::owner), + Codec.STRING.optionalFieldOf("owner_name", "?").forGetter(Config::ownerName), + Identifier.CODEC.listOf(0, TrophyPanelRules.MAX_PAGES).optionalFieldOf("pages", List.of()) + .forGetter(Config::pages), + Codec.INT.optionalFieldOf("current_page", 0).forGetter(Config::currentPage), + Codec.STRING.optionalFieldOf("display", TrophyDisplayMode.NAMES_AND_VALUES.key()) + .forGetter(Config::display), + Codec.BOOL.optionalFieldOf("configured", false).forGetter(Config::configured) + ).apply(instance, Config::new)); + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.optionalFieldOf("data_version", DATA_VERSION).forGetter(data -> data.dataVersion), + CONFIG_CODEC.listOf().optionalFieldOf("signs", List.of()).forGetter(TrophySignData::entriesForCodec) + ).apply(instance, TrophySignData::new)); + private static final SavedDataType TYPE = new SavedDataType<>( + RedStonerMod.id(SAVE_KEY), TrophySignData::new, CODEC, null); + + private int dataVersion; + private final List signs; + + public TrophySignData() { + this(DATA_VERSION, List.of()); + } + + private TrophySignData(int dataVersion, List signs) { + this.dataVersion = dataVersion; + this.signs = new ArrayList<>(signs == null ? List.of() + : signs.stream().limit(MAX_SIGNS).toList()); + } + + public static TrophySignData get(MinecraftServer server) { + ServerLevel overworld = server.getLevel(Level.OVERWORLD); + if (overworld == null) throw new IllegalStateException("Overworld unavailable for trophy-sign data"); + TrophySignData data = overworld.getDataStorage().computeIfAbsent(TYPE); + data.upgradeIfRequired(); + return data; + } + + public boolean writable() { + return dataVersion <= DATA_VERSION; + } + + public List signs() { + return List.copyOf(signs); + } + + public Optional signAt(ServerLevel level, BlockPos pos) { + return signAt(dimension(level), pos.asLong()); + } + + public Optional signAt(String dimension, long pos) { + return signs.stream().filter(sign -> sign.dimension().equals(dimension) && sign.pos() == pos).findFirst(); + } + + public boolean add(Config config) { + if (!writable() || signs.size() >= MAX_SIGNS + || signAt(config.dimension(), config.pos()).isPresent()) return false; + signs.add(config); + setDirty(); + return true; + } + + public boolean replace(Config replacement) { + if (!writable()) return false; + for (int index = 0; index < signs.size(); index++) { + Config current = signs.get(index); + if (!current.dimension().equals(replacement.dimension()) || current.pos() != replacement.pos()) continue; + signs.set(index, replacement); + setDirty(); + return true; + } + return false; + } + + public boolean remove(Config config) { + if (!writable()) return false; + boolean removed = signs.remove(config); + if (removed) setDirty(); + return removed; + } + + private List entriesForCodec() { + return List.copyOf(signs); + } + + private void upgradeIfRequired() { + if (dataVersion < DATA_VERSION) { + int previous = dataVersion; + dataVersion = DATA_VERSION; + setDirty(); + RedStonerMod.LOGGER.info("[{}] Migrated trophy-sign data from v{} to v{}.", + RedStonerMod.DISPLAY_NAME, previous, dataVersion); + } else if (dataVersion > DATA_VERSION) { + RedStonerMod.LOGGER.warn("[{}] Trophy-sign data v{} is newer than supported v{}.", + RedStonerMod.DISPLAY_NAME, dataVersion, DATA_VERSION); + } + } + + public static String dimension(ServerLevel level) { + return level.dimension().identifier().toString(); + } + + public record Config(String dimension, long pos, String owner, String ownerName, + List pages, int currentPage, String display, boolean configured) { + public Config { + dimension = clean(dimension, 128, "minecraft:overworld"); + owner = cleanUuid(owner); + ownerName = clean(ownerName, 32, "?"); + pages = TrophyPanelRules.sanitizeMetricIds(pages); + currentPage = pages.isEmpty() ? 0 : Math.clamp(currentPage, 0, pages.size() - 1); + display = TrophyDisplayMode.byKey(display).key(); + configured = configured && !pages.isEmpty(); + } + + public static Config draft(ServerLevel level, BlockPos pos, UUID ownerId, String ownerName) { + return new Config(TrophySignData.dimension(level), pos.asLong(), ownerId.toString(), ownerName, + List.of(), 0, TrophyDisplayMode.NAMES_AND_VALUES.key(), false); + } + + public UUID ownerId() { + return UUID.fromString(owner); + } + + public TrophyDisplayMode displayMode() { + return TrophyDisplayMode.byKey(display); + } + + public Identifier currentMetric() { + return pages.get(Math.clamp(currentPage, 0, pages.size() - 1)); + } + + public Config withConfiguration(List requestedPages, TrophyDisplayMode mode, + String retainedOwnerName) { + List sanitized = TrophyPanelRules.sanitizeMetricIds(requestedPages); + return new Config(dimension, pos, owner, retainedOwnerName, sanitized, 0, mode.key(), true); + } + + public Config withCurrentPage(int page) { + return new Config(dimension, pos, owner, ownerName, pages, page, display, configured); + } + } + + private static String clean(String value, int maximum, String fallback) { + String clean = value == null || value.isBlank() ? fallback : value.strip(); + return clean.substring(0, Math.min(maximum, clean.length())); + } + + private static String cleanUuid(String value) { + try { + return UUID.fromString(value).toString(); + } catch (IllegalArgumentException | NullPointerException ignored) { + return new UUID(0L, 0L).toString(); + } + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignItem.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignItem.java new file mode 100644 index 0000000..65c3cf2 --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignItem.java @@ -0,0 +1,30 @@ +package fr.koka99cab.sanctuary26.redstoner.trophy; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SignItem; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** The historical trophy-panel item now places an ordinary standing or wall sign. */ +public final class TrophySignItem extends SignItem { + public TrophySignItem(Block standing, Block wall, Properties properties) { + super(standing, wall, properties); + } + + @Override + protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, Player player, + ItemStack stack, BlockState state) { + if (level instanceof ServerLevel serverLevel && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof SignBlockEntity sign) { + TrophySignService.activatePlacedSign(serverPlayer, serverLevel, pos, sign); + } + // The bounded trophy editor replaces the vanilla text editor. + return true; + } +} diff --git a/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignService.java b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignService.java new file mode 100644 index 0000000..80e078d --- /dev/null +++ b/redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophySignService.java @@ -0,0 +1,271 @@ +package fr.koka99cab.sanctuary26.redstoner.trophy; + +import fr.koka99cab.sanctuary26.redstoner.network.OpenTrophySignEditorPayload; +import fr.koka99cab.sanctuary26.redstoner.network.SaveTrophySignPayload; +import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries; +import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingMetric; +import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingSnapshot; +import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit; +import fr.koka99cab.sanctuary26.sanctuary.api.ranking.SanctuaryRankingApi; +import java.util.List; +import java.util.UUID; +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.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.Permissions; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SignBlock; +import net.minecraft.world.level.block.WallSignBlock; +import net.minecraft.world.level.block.entity.SignBlockEntity; +import net.minecraft.world.level.block.entity.SignText; + +/** Server-authoritative ownership, paging, editing and refresh behavior for white vanilla trophy signs. */ +public final class TrophySignService { + private static final int REFRESH_INTERVAL_TICKS = 20; + private static final int MAX_INSPECTIONS_PER_REFRESH = 128; + private static int tickCounter; + private static int refreshCursor; + private static boolean initialized; + + private TrophySignService() {} + + public static void initialize() { + if (initialized) return; + initialized = true; + UseBlockCallback.EVENT.register((player, level, hand, hit) -> { + BlockPos pos = hit.getBlockPos(); + if (hand != InteractionHand.MAIN_HAND + || !RedStonerRegistries.isTrophySign(level.getBlockState(pos).getBlock()) + || !(level.getBlockEntity(pos) instanceof SignBlockEntity sign)) { + return InteractionResult.PASS; + } + if (level.isClientSide()) return InteractionResult.SUCCESS; + return player instanceof ServerPlayer serverPlayer + ? interact(serverPlayer, (ServerLevel) level, pos, sign) + : InteractionResult.PASS; + }); + PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> { + if (!(level instanceof ServerLevel serverLevel) || !RedStonerRegistries.isTrophySign(state.getBlock())) { + return true; + } + TrophySignData.Config config = TrophySignData.get(serverLevel.getServer()) + .signAt(serverLevel, pos).orElse(null); + if (config == null || config.ownerId().equals(player.getUUID())) return true; + if (player instanceof ServerPlayer serverPlayer && isOperator(serverPlayer)) return true; + if (player instanceof ServerPlayer serverPlayer) message(serverPlayer, "protected"); + return false; + }); + PlayerBlockBreakEvents.AFTER.register((level, player, pos, state, blockEntity) -> { + if (!(level instanceof ServerLevel serverLevel) || !RedStonerRegistries.isTrophySign(state.getBlock())) { + return; + } + TrophySignData data = TrophySignData.get(serverLevel.getServer()); + data.signAt(serverLevel, pos).ifPresent(data::remove); + }); + ServerTickEvents.END_SERVER_TICK.register(TrophySignService::tick); + } + + /** Creates a blank owned sign and immediately opens its custom editor. */ + public static void activatePlacedSign(ServerPlayer player, ServerLevel level, BlockPos pos, + SignBlockEntity sign) { + TrophySignData data = TrophySignData.get(level.getServer()); + TrophySignData.Config existing = data.signAt(level, pos).orElse(null); + if (existing == null) { + TrophySignData.Config draft = TrophySignData.Config.draft(level, pos, player.getUUID(), + player.getGameProfile().name()); + if (!data.add(draft)) { + message(player, data.writable() ? "registry_full" : "newer_data"); + return; + } + existing = draft; + } + writeBlankSign(level, sign); + openEditor(player, existing); + } + + private static InteractionResult interact(ServerPlayer player, ServerLevel level, BlockPos pos, + SignBlockEntity sign) { + TrophySignData data = TrophySignData.get(level.getServer()); + TrophySignData.Config config = data.signAt(level, pos).orElse(null); + if (config == null) { + activatePlacedSign(player, level, pos, sign); + return InteractionResult.SUCCESS_SERVER; + } + boolean canConfigure = TrophyPanelRules.canConfigure(config.ownerId(), player.getUUID(), isOperator(player)); + if (!config.configured() || player.isSecondaryUseActive()) { + if (canConfigure) openEditor(player, config); + else message(player, "not_owner"); + return InteractionResult.SUCCESS_SERVER; + } + int page = TrophyPanelRules.nextPage(config.currentPage(), config.pages().size()); + TrophySignData.Config advanced = config.withCurrentPage(page); + if (!data.replace(advanced)) { + message(player, "newer_data"); + return InteractionResult.CONSUME; + } + writeSign(level, sign, advanced); + message(player, "page", advanced.currentPage() + 1, advanced.pages().size()); + return InteractionResult.SUCCESS_SERVER; + } + + public static void save(ServerPlayer player, SaveTrophySignPayload payload) { + if (player.distanceToSqr(net.minecraft.world.phys.Vec3.atCenterOf(payload.pos())) > 64.0D) { + message(player, "too_far"); + return; + } + ServerLevel level = player.level(); + if (!RedStonerRegistries.isTrophySign(level.getBlockState(payload.pos()).getBlock()) + || !(level.getBlockEntity(payload.pos()) instanceof SignBlockEntity sign)) { + message(player, "missing"); + return; + } + TrophySignData data = TrophySignData.get(level.getServer()); + TrophySignData.Config config = data.signAt(level, payload.pos()).orElse(null); + if (config == null || !TrophyPanelRules.canConfigure(config.ownerId(), player.getUUID(), isOperator(player))) { + message(player, "not_owner"); + return; + } + List pages = TrophyPanelRules.sanitizeMetricIds(payload.pages()).stream() + .filter(SanctuaryRankingApi::isConfigurableMetric).toList(); + if (pages.isEmpty() || pages.size() != payload.pages().size()) { + message(player, "invalid"); + return; + } + TrophySignData.Config configured = config.withConfiguration(pages, + TrophyDisplayMode.byKey(payload.display()), config.ownerName()); + if (!data.replace(configured)) { + message(player, "newer_data"); + return; + } + writeSign(level, sign, configured); + message(player, "saved"); + } + + public static boolean migrateLegacy(ServerLevel level, BlockPos pos, Direction facing, UUID owner, + String ownerName, List requestedPages, int currentPage) { + TrophySignData data = TrophySignData.get(level.getServer()); + if (!data.writable()) return false; + UUID retainedOwner = owner == null ? new UUID(0L, 0L) : owner; + TrophySignData.Config previous = data.signAt(level, pos).orElse(null); + if (previous == null && data.signs().size() >= TrophySignData.MAX_SIGNS) return false; + List pages = TrophyPanelRules.sanitizeMetricIds(requestedPages).stream() + .filter(SanctuaryRankingApi::isConfigurableMetric).toList(); + if (pages.isEmpty()) pages = List.of(SanctuaryRankingApi.defaultMetric().id()); + var state = RedStonerRegistries.TROPHY_WALL_SIGN.defaultBlockState() + .setValue(WallSignBlock.FACING, facing) + .setValue(SignBlock.WATERLOGGED, false); + if (!state.canSurvive(level, pos) || !level.setBlock(pos, state, Block.UPDATE_ALL)) return false; + TrophySignData.Config migrated = new TrophySignData.Config(TrophySignData.dimension(level), pos.asLong(), + retainedOwner.toString(), ownerName, pages, currentPage, TrophyDisplayMode.NAMES_AND_VALUES.key(), true); + boolean stored = previous == null ? data.add(migrated) : data.replace(migrated); + if (!stored) return false; + if (level.getBlockEntity(pos) instanceof SignBlockEntity sign) writeSign(level, sign, migrated); + return true; + } + + private static void openEditor(ServerPlayer player, TrophySignData.Config config) { + if (!ServerPlayNetworking.canSend(player, OpenTrophySignEditorPayload.TYPE)) { + message(player, "client_required"); + return; + } + ServerPlayNetworking.send(player, new OpenTrophySignEditorPayload( + BlockPos.of(config.pos()), config.pages(), config.display())); + } + + private static void tick(MinecraftServer server) { + if (++tickCounter < REFRESH_INTERVAL_TICKS) return; + tickCounter = 0; + TrophySignData data = TrophySignData.get(server); + List signs = data.signs(); + if (signs.isEmpty()) { + refreshCursor = 0; + return; + } + int inspections = Math.min(MAX_INSPECTIONS_PER_REFRESH, signs.size()); + for (int offset = 0; offset < inspections; offset++) { + TrophySignData.Config config = signs.get(Math.floorMod(refreshCursor + offset, signs.size())); + ServerLevel level = level(server, config.dimension()); + BlockPos pos = BlockPos.of(config.pos()); + if (level == null || !level.isLoaded(pos)) continue; + if (!RedStonerRegistries.isTrophySign(level.getBlockState(pos).getBlock()) + || !(level.getBlockEntity(pos) instanceof SignBlockEntity sign)) { + data.remove(config); + continue; + } + if (config.configured()) writeSign(level, sign, config); + else writeBlankSign(level, sign); + } + refreshCursor = Math.floorMod(refreshCursor + inspections, Math.max(1, signs.size())); + } + + private static void writeBlankSign(ServerLevel level, SignBlockEntity sign) { + writeText(level, sign, new SignText()); + } + + private static void writeSign(ServerLevel level, SignBlockEntity sign, TrophySignData.Config config) { + Identifier metricId = config.currentMetric(); + RankingSnapshot snapshot = SanctuaryRankingApi.snapshot(level.getServer(), metricId, config.ownerId()); + RankingMetric metric = snapshot.metric(); + RankingUnit unit = metric.unit(); + SignText text = new SignText().setMessage(0, + Component.translatable("block.redstoner.trophy_sign.page_title", + config.currentPage() + 1, config.pages().size(), SanctuaryRankingApi.displayName(metric)) + .withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.BOLD)); + if (snapshot.podium().isEmpty()) { + text = text.setMessage(1, Component.translatable("block.redstoner.trophy_sign.no_score") + .withStyle(ChatFormatting.GRAY)); + } else { + for (int index = 0; index < TrophyPanelRules.SIGN_ENTRIES_PER_PAGE + && index < snapshot.podium().size(); index++) { + RankingSnapshot.Entry entry = snapshot.podium().get(index); + text = text.setMessage(index + 1, rankingLine(config.displayMode(), entry, unit)); + } + } + writeText(level, sign, text); + } + + private static Component rankingLine(TrophyDisplayMode mode, RankingSnapshot.Entry entry, RankingUnit unit) { + return switch (mode) { + case NAMES_AND_VALUES -> Component.translatable("block.redstoner.trophy_sign.names_and_values", + entry.rank(), entry.playerName(), unit.format(entry.value())); + case NAMES -> Component.translatable("block.redstoner.trophy_sign.names", + entry.rank(), entry.playerName()); + case VALUES -> Component.translatable("block.redstoner.trophy_sign.values", + entry.rank(), unit.format(entry.value())); + }; + } + + private static void writeText(ServerLevel level, SignBlockEntity sign, SignText text) { + sign.setText(text, true); + sign.setText(text, false); + sign.setWaxed(true); + sign.setChanged(); + level.sendBlockUpdated(sign.getBlockPos(), sign.getBlockState(), sign.getBlockState(), 3); + } + + private static ServerLevel level(MinecraftServer server, String dimension) { + for (ServerLevel level : server.getAllLevels()) { + if (TrophySignData.dimension(level).equals(dimension)) return level; + } + return null; + } + + private static boolean isOperator(ServerPlayer player) { + return player.createCommandSourceStack().permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER); + } + + private static void message(ServerPlayer player, String key, Object... args) { + player.sendSystemMessage(Component.translatable("message.redstoner.trophy_sign." + key, args)); + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/blockstates/trophy_sign.json b/redstoner/src/main/resources/assets/redstoner/blockstates/trophy_sign.json new file mode 100644 index 0000000..3960229 --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/blockstates/trophy_sign.json @@ -0,0 +1,20 @@ +{ + "variants": { + "rotation=0": { "model": "redstoner:block/trophy_sign_rot_0" }, + "rotation=1": { "model": "redstoner:block/trophy_sign_rot_1" }, + "rotation=2": { "model": "redstoner:block/trophy_sign_rot_2" }, + "rotation=3": { "model": "redstoner:block/trophy_sign_rot_3" }, + "rotation=4": { "model": "redstoner:block/trophy_sign_rot_0", "y": 90 }, + "rotation=5": { "model": "redstoner:block/trophy_sign_rot_1", "y": 90 }, + "rotation=6": { "model": "redstoner:block/trophy_sign_rot_2", "y": 90 }, + "rotation=7": { "model": "redstoner:block/trophy_sign_rot_3", "y": 90 }, + "rotation=8": { "model": "redstoner:block/trophy_sign_rot_0", "y": 180 }, + "rotation=9": { "model": "redstoner:block/trophy_sign_rot_1", "y": 180 }, + "rotation=10": { "model": "redstoner:block/trophy_sign_rot_2", "y": 180 }, + "rotation=11": { "model": "redstoner:block/trophy_sign_rot_3", "y": 180 }, + "rotation=12": { "model": "redstoner:block/trophy_sign_rot_0", "y": 270 }, + "rotation=13": { "model": "redstoner:block/trophy_sign_rot_1", "y": 270 }, + "rotation=14": { "model": "redstoner:block/trophy_sign_rot_2", "y": 270 }, + "rotation=15": { "model": "redstoner:block/trophy_sign_rot_3", "y": 270 } + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/blockstates/trophy_wall_sign.json b/redstoner/src/main/resources/assets/redstoner/blockstates/trophy_wall_sign.json new file mode 100644 index 0000000..31a0b73 --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/blockstates/trophy_wall_sign.json @@ -0,0 +1,8 @@ +{ + "variants": { + "facing=east": { "model": "redstoner:block/trophy_wall_sign", "y": 270 }, + "facing=north": { "model": "redstoner:block/trophy_wall_sign", "y": 180 }, + "facing=south": { "model": "redstoner:block/trophy_wall_sign" }, + "facing=west": { "model": "redstoner:block/trophy_wall_sign", "y": 90 } + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/items/trophy_panel.json b/redstoner/src/main/resources/assets/redstoner/items/trophy_panel.json index 0850c2d..809ebdc 100644 --- a/redstoner/src/main/resources/assets/redstoner/items/trophy_panel.json +++ b/redstoner/src/main/resources/assets/redstoner/items/trophy_panel.json @@ -1,6 +1,6 @@ { "model": { "type": "minecraft:model", - "model": "redstoner:block/trophy_panel" + "model": "redstoner:item/trophy_sign" } } diff --git a/redstoner/src/main/resources/assets/redstoner/lang/en_us.json b/redstoner/src/main/resources/assets/redstoner/lang/en_us.json index 74a04e1..d4cca6f 100644 --- a/redstoner/src/main/resources/assets/redstoner/lang/en_us.json +++ b/redstoner/src/main/resources/assets/redstoner/lang/en_us.json @@ -64,6 +64,32 @@ "screen.redstoner.storage_terminal.error.too_large": "Network too large (maximum 128 storage blocks).", "screen.redstoner.storage_terminal.error.locked": "The network contains a locked storage block.", "block.redstoner.trophy_panel": "Trophy Panel", + "block.redstoner.trophy_sign": "Trophy Sign", + "block.redstoner.trophy_wall_sign": "Wall Trophy Sign", + "block.redstoner.trophy_sign.page_title": "[%s/%s] %s", + "block.redstoner.trophy_sign.names_and_values": "#%s %s · %s", + "block.redstoner.trophy_sign.names": "#%s %s", + "block.redstoner.trophy_sign.values": "#%s %s", + "block.redstoner.trophy_sign.no_score": "No score yet", + "screen.redstoner.trophy_sign.title": "Configure Trophy Sign", + "screen.redstoner.trophy_sign.search": "Search for a statistic…", + "screen.redstoner.trophy_sign.available": "Available statistics", + "screen.redstoner.trophy_sign.pages": "Pages %s/%s", + "screen.redstoner.trophy_sign.remove": "Remove", + "screen.redstoner.trophy_sign.display": "Display: %s", + "screen.redstoner.trophy_sign.display.names_and_values": "names and values", + "screen.redstoner.trophy_sign.display.names": "names only", + "screen.redstoner.trophy_sign.display.values": "values only", + "message.redstoner.trophy_sign.not_owner": "Only the creator or an operator can configure this sign.", + "message.redstoner.trophy_sign.protected": "Only the creator or an operator can break this sign.", + "message.redstoner.trophy_sign.saved": "Trophy sign configured.", + "message.redstoner.trophy_sign.page": "Page %s/%s", + "message.redstoner.trophy_sign.invalid": "The configuration contains an invalid statistic.", + "message.redstoner.trophy_sign.missing": "This trophy sign no longer exists.", + "message.redstoner.trophy_sign.too_far": "You are too far away from this sign.", + "message.redstoner.trophy_sign.client_required": "An up-to-date Red-Stoner client is required to configure this sign.", + "message.redstoner.trophy_sign.registry_full": "This world has reached its trophy-sign limit.", + "message.redstoner.trophy_sign.newer_data": "This sign data comes from a newer version and remains read-only.", "block.redstoner.trophy_panel.page_title": "[%s/%s] %s", "block.redstoner.trophy_panel.podium": "#%s %s · %s", "block.redstoner.trophy_panel.player": "#%s %s", diff --git a/redstoner/src/main/resources/assets/redstoner/lang/fr_fr.json b/redstoner/src/main/resources/assets/redstoner/lang/fr_fr.json index b1e7d16..ca582be 100644 --- a/redstoner/src/main/resources/assets/redstoner/lang/fr_fr.json +++ b/redstoner/src/main/resources/assets/redstoner/lang/fr_fr.json @@ -64,6 +64,32 @@ "screen.redstoner.storage_terminal.error.too_large": "Réseau trop grand (128 stockages maximum).", "screen.redstoner.storage_terminal.error.locked": "Le réseau contient un stockage verrouillé.", "block.redstoner.trophy_panel": "Panneau de trophées", + "block.redstoner.trophy_sign": "Panneau de trophées", + "block.redstoner.trophy_wall_sign": "Panneau mural de trophées", + "block.redstoner.trophy_sign.page_title": "[%s/%s] %s", + "block.redstoner.trophy_sign.names_and_values": "#%s %s · %s", + "block.redstoner.trophy_sign.names": "#%s %s", + "block.redstoner.trophy_sign.values": "#%s %s", + "block.redstoner.trophy_sign.no_score": "Aucun score", + "screen.redstoner.trophy_sign.title": "Configurer le panneau de trophées", + "screen.redstoner.trophy_sign.search": "Rechercher une statistique…", + "screen.redstoner.trophy_sign.available": "Statistiques disponibles", + "screen.redstoner.trophy_sign.pages": "Pages %s/%s", + "screen.redstoner.trophy_sign.remove": "Retirer", + "screen.redstoner.trophy_sign.display": "Affichage : %s", + "screen.redstoner.trophy_sign.display.names_and_values": "noms et valeurs", + "screen.redstoner.trophy_sign.display.names": "noms seulement", + "screen.redstoner.trophy_sign.display.values": "valeurs seulement", + "message.redstoner.trophy_sign.not_owner": "Seul le créateur ou un opérateur peut configurer ce panneau.", + "message.redstoner.trophy_sign.protected": "Seul le créateur ou un opérateur peut casser ce panneau.", + "message.redstoner.trophy_sign.saved": "Panneau de trophées configuré.", + "message.redstoner.trophy_sign.page": "Page %s/%s", + "message.redstoner.trophy_sign.invalid": "La configuration contient une statistique invalide.", + "message.redstoner.trophy_sign.missing": "Ce panneau de trophées n’existe plus.", + "message.redstoner.trophy_sign.too_far": "Tu es trop loin de ce panneau.", + "message.redstoner.trophy_sign.client_required": "Un client Red-Stoner à jour est requis pour configurer ce panneau.", + "message.redstoner.trophy_sign.registry_full": "La limite de panneaux de trophées du monde est atteinte.", + "message.redstoner.trophy_sign.newer_data": "Ces données de panneau proviennent d’une version plus récente et restent en lecture seule.", "block.redstoner.trophy_panel.page_title": "[%s/%s] %s", "block.redstoner.trophy_panel.podium": "#%s %s · %s", "block.redstoner.trophy_panel.player": "#%s %s", diff --git a/redstoner/src/main/resources/assets/redstoner/lang/ru_ru.json b/redstoner/src/main/resources/assets/redstoner/lang/ru_ru.json index 3f20be1..9a33ccf 100644 --- a/redstoner/src/main/resources/assets/redstoner/lang/ru_ru.json +++ b/redstoner/src/main/resources/assets/redstoner/lang/ru_ru.json @@ -64,6 +64,32 @@ "screen.redstoner.storage_terminal.error.too_large": "Сеть слишком велика (не более 128 хранилищ).", "screen.redstoner.storage_terminal.error.locked": "В сети есть запертое хранилище.", "block.redstoner.trophy_panel": "Панель трофеев", + "block.redstoner.trophy_sign": "Табличка трофеев", + "block.redstoner.trophy_wall_sign": "Настенная табличка трофеев", + "block.redstoner.trophy_sign.page_title": "[%s/%s] %s", + "block.redstoner.trophy_sign.names_and_values": "#%s %s · %s", + "block.redstoner.trophy_sign.names": "#%s %s", + "block.redstoner.trophy_sign.values": "#%s %s", + "block.redstoner.trophy_sign.no_score": "Результатов пока нет", + "screen.redstoner.trophy_sign.title": "Настройка таблички трофеев", + "screen.redstoner.trophy_sign.search": "Найти статистику…", + "screen.redstoner.trophy_sign.available": "Доступная статистика", + "screen.redstoner.trophy_sign.pages": "Страницы %s/%s", + "screen.redstoner.trophy_sign.remove": "Удалить", + "screen.redstoner.trophy_sign.display": "Отображение: %s", + "screen.redstoner.trophy_sign.display.names_and_values": "имена и значения", + "screen.redstoner.trophy_sign.display.names": "только имена", + "screen.redstoner.trophy_sign.display.values": "только значения", + "message.redstoner.trophy_sign.not_owner": "Настраивать эту табличку может только создатель или оператор.", + "message.redstoner.trophy_sign.protected": "Сломать эту табличку может только создатель или оператор.", + "message.redstoner.trophy_sign.saved": "Табличка трофеев настроена.", + "message.redstoner.trophy_sign.page": "Страница %s/%s", + "message.redstoner.trophy_sign.invalid": "Настройка содержит недопустимую статистику.", + "message.redstoner.trophy_sign.missing": "Этой таблички трофеев больше нет.", + "message.redstoner.trophy_sign.too_far": "Вы слишком далеко от этой таблички.", + "message.redstoner.trophy_sign.client_required": "Для настройки нужен обновлённый клиент Red-Stoner.", + "message.redstoner.trophy_sign.registry_full": "Достигнут предел табличек трофеев для этого мира.", + "message.redstoner.trophy_sign.newer_data": "Данные таблички созданы более новой версией и доступны только для чтения.", "block.redstoner.trophy_panel.page_title": "[%s/%s] %s", "block.redstoner.trophy_panel.podium": "#%s %s · %s", "block.redstoner.trophy_panel.player": "#%s %s", diff --git a/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_0.json b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_0.json new file mode 100644 index 0000000..5535472 --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_0.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/template_sign_rot_0", + "textures": { + "all": "minecraft:block/pale_oak_sign", + "particle": "minecraft:block/pale_oak_planks" + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_1.json b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_1.json new file mode 100644 index 0000000..e2eded3 --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_1.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/template_sign_rot_1", + "textures": { + "all": "minecraft:block/pale_oak_sign", + "particle": "minecraft:block/pale_oak_planks" + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_2.json b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_2.json new file mode 100644 index 0000000..8cac3b7 --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_2.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/template_sign_rot_2", + "textures": { + "all": "minecraft:block/pale_oak_sign", + "particle": "minecraft:block/pale_oak_planks" + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_3.json b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_3.json new file mode 100644 index 0000000..3daf53f --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_sign_rot_3.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/template_sign_rot_3", + "textures": { + "all": "minecraft:block/pale_oak_sign", + "particle": "minecraft:block/pale_oak_planks" + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/models/block/trophy_wall_sign.json b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_wall_sign.json new file mode 100644 index 0000000..98c1b36 --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/models/block/trophy_wall_sign.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/template_wall_sign", + "textures": { + "all": "minecraft:block/pale_oak_sign", + "particle": "minecraft:block/pale_oak_planks" + } +} diff --git a/redstoner/src/main/resources/assets/redstoner/models/item/trophy_sign.json b/redstoner/src/main/resources/assets/redstoner/models/item/trophy_sign.json new file mode 100644 index 0000000..f10beef --- /dev/null +++ b/redstoner/src/main/resources/assets/redstoner/models/item/trophy_sign.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/pale_oak_sign" + } +} diff --git a/redstoner/src/main/resources/data/minecraft/tags/block/mineable/axe.json b/redstoner/src/main/resources/data/minecraft/tags/block/mineable/axe.json new file mode 100644 index 0000000..aed7d29 --- /dev/null +++ b/redstoner/src/main/resources/data/minecraft/tags/block/mineable/axe.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["redstoner:trophy_sign", "redstoner:trophy_wall_sign"] +} diff --git a/redstoner/src/main/resources/data/minecraft/tags/block/standing_signs.json b/redstoner/src/main/resources/data/minecraft/tags/block/standing_signs.json new file mode 100644 index 0000000..fa4b977 --- /dev/null +++ b/redstoner/src/main/resources/data/minecraft/tags/block/standing_signs.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["redstoner:trophy_sign"] +} diff --git a/redstoner/src/main/resources/data/minecraft/tags/block/wall_signs.json b/redstoner/src/main/resources/data/minecraft/tags/block/wall_signs.json new file mode 100644 index 0000000..89cb3ed --- /dev/null +++ b/redstoner/src/main/resources/data/minecraft/tags/block/wall_signs.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["redstoner:trophy_wall_sign"] +} diff --git a/redstoner/src/main/resources/data/minecraft/tags/item/signs.json b/redstoner/src/main/resources/data/minecraft/tags/item/signs.json new file mode 100644 index 0000000..b82a6db --- /dev/null +++ b/redstoner/src/main/resources/data/minecraft/tags/item/signs.json @@ -0,0 +1,4 @@ +{ + "replace": false, + "values": ["redstoner:trophy_panel"] +} diff --git a/redstoner/src/main/resources/data/redstoner/loot_table/blocks/trophy_sign.json b/redstoner/src/main/resources/data/redstoner/loot_table/blocks/trophy_sign.json new file mode 100644 index 0000000..5d9d793 --- /dev/null +++ b/redstoner/src/main/resources/data/redstoner/loot_table/blocks/trophy_sign.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "conditions": [ + { "condition": "minecraft:survives_explosion" } + ], + "entries": [ + { "type": "minecraft:item", "name": "redstoner:trophy_panel" } + ], + "rolls": 1 + } + ], + "random_sequence": "redstoner:blocks/trophy_sign" +} diff --git a/redstoner/src/main/resources/data/redstoner/loot_table/blocks/trophy_wall_sign.json b/redstoner/src/main/resources/data/redstoner/loot_table/blocks/trophy_wall_sign.json new file mode 100644 index 0000000..eefb80f --- /dev/null +++ b/redstoner/src/main/resources/data/redstoner/loot_table/blocks/trophy_wall_sign.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "conditions": [ + { "condition": "minecraft:survives_explosion" } + ], + "entries": [ + { "type": "minecraft:item", "name": "redstoner:trophy_panel" } + ], + "rolls": 1 + } + ], + "random_sequence": "redstoner:blocks/trophy_wall_sign" +} diff --git a/redstoner/src/main/resources/data/redstoner/recipe/trophy_panel.json b/redstoner/src/main/resources/data/redstoner/recipe/trophy_panel.json index 9ca3035..c6718d9 100644 --- a/redstoner/src/main/resources/data/redstoner/recipe/trophy_panel.json +++ b/redstoner/src/main/resources/data/redstoner/recipe/trophy_panel.json @@ -1,17 +1,10 @@ { - "type": "minecraft:crafting_shaped", - "category": "redstone", - "pattern": [ - "NGN", - "GRG", - "NSN" + "type": "minecraft:crafting_shapeless", + "category": "building", + "ingredients": [ + "#minecraft:signs", + "minecraft:white_dye" ], - "key": { - "N": "minecraft:gold_nugget", - "G": "minecraft:black_stained_glass_pane", - "R": "minecraft:redstone_lamp", - "S": "minecraft:oak_sign" - }, "result": { "id": "redstoner:trophy_panel", "count": 1 diff --git a/redstoner/src/main/resources/fabric.mod.json b/redstoner/src/main/resources/fabric.mod.json index 44cee24..5be3190 100644 --- a/redstoner/src/main/resources/fabric.mod.json +++ b/redstoner/src/main/resources/fabric.mod.json @@ -24,14 +24,14 @@ "java": ">=25", "fabric-api": ">=0.154.2+26.2", "anotherworld": "*", - "sanctuary": "*" + "sanctuary": ">=0.0.0-alpha.139" }, "custom": { "sanctuary26": { "system_type": "gameplay-system", "namespace": "redstoner", - "data_version": 4, - "network_protocol": 8, + "data_version": 5, + "network_protocol": 9, "lifecycle": "${lifecycle}", "pack_version": "${pack_version}", "descriptions": { diff --git a/redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyPanelRulesSmoke.java b/redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyPanelRulesSmoke.java index 2522af4..9d80d3e 100644 --- a/redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyPanelRulesSmoke.java +++ b/redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/trophy/TrophyPanelRulesSmoke.java @@ -31,6 +31,12 @@ public final class TrophyPanelRulesSmoke { && TrophyPanelRules.displayPageNumber(1, true, 2) == 4 && TrophyPanelRules.displayPageCount(2) == 4, "The panel does not use eight lines or number both metric views correctly"); + require(TrophyPanelRules.SIGN_LINE_COUNT == 4 && TrophyPanelRules.SIGN_ENTRIES_PER_PAGE == 3, + "The vanilla trophy sign must reserve one title line and three ranking lines"); + require(TrophyDisplayMode.byKey("names") == TrophyDisplayMode.NAMES + && TrophyDisplayMode.NAMES.next() == TrophyDisplayMode.VALUES + && TrophyDisplayMode.byKey("invalid") == TrophyDisplayMode.NAMES_AND_VALUES, + "Persisted trophy-sign display modes are not stable or safely defaulted"); Identifier first = Identifier.parse("sanctuary:first"); Identifier second = Identifier.parse("sanctuary:second"); @@ -41,6 +47,9 @@ public final class TrophyPanelRulesSmoke { require(merged.get(0).equals(first) && merged.get(1).equals(second), "Existing page order was not preserved"); require(merged.stream().distinct().count() == merged.size(), "Duplicate metric pages were retained"); + List sanitized = TrophyPanelRules.sanitizeMetricIds(List.of(first, second, first)); + require(sanitized.equals(List.of(first, second)), + "The trophy editor did not preserve page order while removing duplicates"); } private static void require(boolean condition, String message) { diff --git a/sanctuary/build.gradle b/sanctuary/build.gradle index d75e20a..f5056b2 100644 --- a/sanctuary/build.gradle +++ b/sanctuary/build.gradle @@ -215,7 +215,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || !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") @@ -2931,7 +2931,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || 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 @@ -3097,7 +3097,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || 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 @@ -3135,7 +3135,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || 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] @@ -3166,7 +3166,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || migration.target?.pack_version != "26.2.0-alpha.195" || migration.target?.modules?.sanctuary != "0.0.0-alpha.111" || migration.compatibility?.sanctuary_data_version != 11 @@ -3197,7 +3197,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || migration.target?.modules?.sanctuary != "0.0.0-alpha.112" || migration.mailbox?.id != "sanctuary:delivery_box" || migration.mailbox?.horizontal_facing != true @@ -3225,7 +3225,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || !transfer.contains("shouldAppendOverflow") || !transfer.contains("isVanillaStorageRange") || !transfer.contains("hotbarStart") @@ -3271,7 +3271,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || migration.target?.modules?.sanctuary != "0.0.0-alpha.113" || migration.quest_requests?.save_key != "quest_requests" || migration.quest_requests?.data_version != 1 @@ -3333,7 +3333,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || 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" @@ -3402,7 +3402,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || 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" @@ -3481,7 +3481,7 @@ tasks.register("verifyCraftableBannerCape") { 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.138" + if (project.version.toString() != "0.0.0-alpha.139" || migration.target?.modules?.sanctuary != "0.0.0-alpha.130" || migration.white_cape?.crafting_material_count != 6 || migration.white_cape?.maximum_pattern_layers != 6 @@ -3533,7 +3533,7 @@ tasks.register("verifyFinalSanctuaryTickets") { 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.138" + if (project.version.toString() != "0.0.0-alpha.139" || !capeRenderer.contains("SHIELD_FACE_WIDTH = 12.0F") || !capeRenderer.contains("SHIELD_FACE_HEIGHT = 22.0F") || !capeRenderer.contains("CAPE_WIDTH / SHIELD_FACE_WIDTH") @@ -3587,7 +3587,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.138" + if (project.version.toString() != "0.0.0-alpha.139" || migration.target?.modules?.sanctuary != "0.0.0-alpha.122" || migration.alpha_world?.dimension != "sanctuary:alpha" || migration.alpha_world?.legacy_dimension != "portals:alpha" @@ -3647,7 +3647,7 @@ tasks.register("verifyBackroom") { 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.138" + if (project.version.toString() != "0.0.0-alpha.139" || migration.source_data_version != 13 || migration.target_data_version != 14 || migration.integration_modules?.anotherworld?.source != "0.0.0-alpha.56" || migration.integration_modules?.anotherworld?.target != "0.0.0-alpha.57" diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingApi.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingApi.java index 53a89a9..9330541 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingApi.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingApi.java @@ -30,8 +30,11 @@ public final class SanctuaryRankingApi { if (initialized) return; initialized = true; register(DEFAULT_METRIC); - ServerLifecycleEvents.SERVER_STARTED.register(server -> - SanctuaryRankingData.get(server).importVanillaStats(server)); + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + SanctuaryRankingData data = SanctuaryRankingData.get(server); + data.importVanillaStats(server); + data.resolveCachedNames(server); + }); ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { SanctuaryRankingData data = SanctuaryRankingData.get(server); data.updateName(handler.player.getUUID(), handler.player.getGameProfile().name()); @@ -67,11 +70,29 @@ public final class SanctuaryRankingApi { return metrics; } + /** + * Complete client-safe catalog used by configuration screens. The catalog is rebuilt from + * synchronized vanilla registries, so it never has to be transferred in one large payload. + */ + public static List configurableMetrics() { + return VanillaRankingMetric.configurableMetrics(METRICS.values()); + } + + /** True when a submitted identifier resolves to a registered or existing vanilla statistic. */ + public static boolean isConfigurableMetric(Identifier id) { + return id != null && (METRICS.containsKey(id) || VanillaRankingMetric.isAvailable(id)); + } + /** Localized label for a metric configured from its representative item. */ public static Component displayName(RankingMetric metric, ItemStack icon) { return VanillaRankingMetric.displayName(metric, icon); } + /** Localized label resolved directly from the metric target, without requiring a representative item. */ + public static Component displayName(RankingMetric metric) { + return VanillaRankingMetric.displayName(metric, ItemStack.EMPTY); + } + public static boolean eligible(ServerPlayer player) { return isRealOnlinePlayer(player) && !player.isCreative() && !player.isSpectator(); } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingData.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingData.java index c06bd33..46ad9a2 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingData.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingData.java @@ -200,7 +200,7 @@ final class SanctuaryRankingData extends SavedData { if (Files.isDirectory(statsDirectory)) { try (Stream files = Files.list(statsDirectory)) { files.filter(path -> path.getFileName().toString().endsWith(".json")) - .sorted().forEach(this::importVanillaFile); + .sorted().forEach(path -> importVanillaFile(server, path)); } catch (IOException exception) { SanctuaryMod.LOGGER.warn("[{}] Could not import existing vanilla statistics.", SanctuaryMod.DISPLAY_NAME, exception); @@ -212,6 +212,10 @@ final class SanctuaryRankingData extends SavedData { } void importVanillaFile(Path file) { + importVanillaFile(null, file); + } + + private void importVanillaFile(MinecraftServer server, Path file) { String fileName = file.getFileName().toString(); String idText = fileName.substring(0, fileName.length() - ".json".length()); UUID playerId; @@ -221,6 +225,7 @@ final class SanctuaryRankingData extends SavedData { return; } try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + String resolvedName = cachedName(server, playerId); JsonObject root = JsonParser.parseReader(reader).getAsJsonObject(); JsonObject stats = root.has("stats") && root.get("stats").isJsonObject() ? root.getAsJsonObject("stats") : new JsonObject(); @@ -233,8 +238,8 @@ final class SanctuaryRankingData extends SavedData { || !valueEntry.getValue().getAsJsonPrimitive().isNumber()) continue; Identifier metricId = VanillaRankingMetric.id(type, target); long value = Math.max(0L, valueEntry.getValue().getAsLong()); - recordValue(playerId, shortFallbackName(playerId), metricId, value); - observeVanilla(playerId, shortFallbackName(playerId), metricId, value, false); + recordValue(playerId, resolvedName, metricId, value); + observeVanilla(playerId, resolvedName, metricId, value, false); } } } catch (RuntimeException | IOException exception) { @@ -243,6 +248,21 @@ final class SanctuaryRankingData extends SavedData { } } + /** Repairs the old eight-character UUID placeholders from the server's local profile cache. */ + void resolveCachedNames(MinecraftServer server) { + if (!writable() || server == null) return; + boolean changed = false; + for (Map.Entry entry : new ArrayList<>(players.entrySet())) { + UUID playerId = UUID.fromString(entry.getKey()); + Profile profile = entry.getValue(); + if (!isUuidFallback(profile.lastName(), playerId)) continue; + String resolved = cachedName(server, playerId); + players.put(entry.getKey(), new Profile(resolved, profile.metrics(), profile.vanillaObserved())); + changed = true; + } + if (changed) setDirty(); + } + private long takeReachedOrder() { long current = nextReachedOrder; if (nextReachedOrder < Long.MAX_VALUE) nextReachedOrder++; @@ -269,8 +289,17 @@ final class SanctuaryRankingData extends SavedData { return clean.length() > 32 ? clean.substring(0, 32) : clean; } - private static String shortFallbackName(UUID playerId) { - return playerId.toString().substring(0, 8); + private static String cachedName(MinecraftServer server, UUID playerId) { + if (server == null) return "?"; + return server.services().nameToIdCache().get(playerId) + .map(profile -> cleanName(profile.name())).orElse("?"); + } + + private static boolean isUuidFallback(String name, UUID playerId) { + if (name == null || name.isBlank() || name.equals("?")) return true; + String uuid = playerId.toString(); + return (name.length() == 8 || name.length() == 32) + && uuid.regionMatches(true, 0, name, 0, name.length()); } record Score(long value, long reachedOrder) { diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/VanillaRankingMetric.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/VanillaRankingMetric.java index 1898260..4912360 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/VanillaRankingMetric.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/VanillaRankingMetric.java @@ -2,7 +2,10 @@ package fr.koka99cab.sanctuary26.sanctuary.api.ranking; import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod; import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; @@ -19,6 +22,17 @@ final class VanillaRankingMetric { private static final String PREFIX = "vanilla/"; private static final List ITEM_FAMILIES = List.of( "mined", "crafted", "used", "broken", "picked_up", "dropped"); + private static final List ENTITY_FAMILIES = List.of("killed", "killed_by"); + private static final List TIME_STATISTICS = List.of( + Stats.PLAY_TIME, Stats.TOTAL_WORLD_TIME, Stats.TIME_SINCE_DEATH, + Stats.TIME_SINCE_REST, Stats.CROUCH_TIME); + private static final List DISTANCE_STATISTICS = List.of( + Stats.WALK_ONE_CM, Stats.CROUCH_ONE_CM, Stats.SPRINT_ONE_CM, + Stats.WALK_ON_WATER_ONE_CM, Stats.FALL_ONE_CM, Stats.CLIMB_ONE_CM, + Stats.FLY_ONE_CM, Stats.WALK_UNDER_WATER_ONE_CM, Stats.MINECART_ONE_CM, + Stats.BOAT_ONE_CM, Stats.PIG_ONE_CM, Stats.HAPPY_GHAST_ONE_CM, + Stats.HORSE_ONE_CM, Stats.AVIATE_ONE_CM, Stats.SWIM_ONE_CM, + Stats.STRIDER_ONE_CM, Stats.NAUTILUS_ONE_CM); private VanillaRankingMetric() {} @@ -36,12 +50,46 @@ final class VanillaRankingMetric { if (source == null) { return new RankingMetric(id, "metric.sanctuary.ranking.unknown", RankingUnit.COUNT); } - boolean playTime = source.type().getPath().equals("custom") && source.target().equals(Stats.PLAY_TIME); - RankingUnit unit = playTime ? RankingUnit.TICKS : RankingUnit.COUNT; + boolean custom = source.type().getPath().equals("custom"); + boolean playTime = custom && source.target().equals(Stats.PLAY_TIME); + RankingUnit unit = custom && TIME_STATISTICS.contains(source.target()) ? RankingUnit.TICKS + : custom && DISTANCE_STATISTICS.contains(source.target()) ? RankingUnit.CENTIMETERS + : RankingUnit.COUNT; return new RankingMetric(id, "metric.sanctuary.ranking." + (playTime ? "play_time" : source.type().getPath()), unit); } + static List configurableMetrics(Collection registered) { + Map metrics = new LinkedHashMap<>(); + if (registered != null) registered.forEach(metric -> metrics.put(metric.id(), metric)); + for (Identifier target : BuiltInRegistries.CUSTOM_STAT) { + RankingMetric metric = descriptor(id(Identifier.withDefaultNamespace("custom"), target)); + metrics.putIfAbsent(metric.id(), metric); + } + for (BlockItem blockItem : BuiltInRegistries.ITEM.stream() + .filter(BlockItem.class::isInstance).map(BlockItem.class::cast).toList()) { + Identifier target = BuiltInRegistries.BLOCK.getKey(blockItem.getBlock()); + RankingMetric metric = descriptor(id(Identifier.withDefaultNamespace("mined"), target)); + metrics.putIfAbsent(metric.id(), metric); + } + for (Item item : BuiltInRegistries.ITEM) { + Identifier target = BuiltInRegistries.ITEM.getKey(item); + for (String family : ITEM_FAMILIES) { + if (family.equals("mined")) continue; + RankingMetric metric = descriptor(id(Identifier.withDefaultNamespace(family), target)); + metrics.putIfAbsent(metric.id(), metric); + } + } + for (var entityType : BuiltInRegistries.ENTITY_TYPE) { + Identifier target = BuiltInRegistries.ENTITY_TYPE.getKey(entityType); + for (String family : ENTITY_FAMILIES) { + RankingMetric metric = descriptor(id(Identifier.withDefaultNamespace(family), target)); + metrics.putIfAbsent(metric.id(), metric); + } + } + return List.copyOf(metrics.values()); + } + static List itemMetrics(ItemStack icon) { if (icon == null || icon.isEmpty()) return List.of(); Item item = icon.getItem(); @@ -64,14 +112,35 @@ final class VanillaRankingMetric { static Component displayName(RankingMetric metric, ItemStack icon) { Source source = parse(metric.id()).orElse(null); - if (source != null && source.type().getPath().equals("killed")) { - Component target = BuiltInRegistries.ENTITY_TYPE.getOptional(source.target()) - .map(type -> type.getDescription()).orElseGet(() -> iconName(icon)); - return Component.translatable(metric.translationKey(), target); + if (source == null) return Component.translatable(metric.translationKey()); + String family = source.type().getPath(); + if (family.equals("custom")) { + return source.target().equals(Stats.PLAY_TIME) + ? Component.translatable(metric.translationKey()) + : Component.translatable(source.target().toLanguageKey("stat")); } - return icon == null || icon.isEmpty() - ? Component.translatable(metric.translationKey()) - : Component.translatable(metric.translationKey(), icon.getHoverName()); + Component target = switch (family) { + case "mined" -> BuiltInRegistries.BLOCK.getOptional(source.target()) + .map(block -> block.getName()).orElseGet(() -> iconName(icon)); + case "killed", "killed_by" -> BuiltInRegistries.ENTITY_TYPE.getOptional(source.target()) + .map(type -> type.getDescription()).orElseGet(() -> iconName(icon)); + default -> BuiltInRegistries.ITEM.getOptional(source.target()) + .map(item -> new ItemStack(item).getHoverName()).orElseGet(() -> iconName(icon)); + }; + return Component.translatable(metric.translationKey(), target); + } + + static boolean isAvailable(Identifier metricId) { + Source source = parse(metricId).orElse(null); + if (source == null) return false; + return switch (source.type().getPath()) { + case "custom" -> BuiltInRegistries.CUSTOM_STAT.getOptional(source.target()).isPresent(); + case "mined" -> BuiltInRegistries.BLOCK.getOptional(source.target()).isPresent(); + case "crafted", "used", "broken", "picked_up", "dropped" -> + BuiltInRegistries.ITEM.getOptional(source.target()).isPresent(); + case "killed", "killed_by" -> BuiltInRegistries.ENTITY_TYPE.getOptional(source.target()).isPresent(); + default -> false; + }; } static Optional parse(Identifier metricId) { @@ -102,6 +171,8 @@ final class VanillaRankingMetric { .map(item -> (long) player.getStats().getValue(Stats.ITEM_DROPPED, item)).orElse(0L); case "killed" -> BuiltInRegistries.ENTITY_TYPE.getOptional(target) .map(type -> (long) player.getStats().getValue(Stats.ENTITY_KILLED, type)).orElse(0L); + case "killed_by" -> BuiltInRegistries.ENTITY_TYPE.getOptional(target) + .map(type -> (long) player.getStats().getValue(Stats.ENTITY_KILLED_BY, type)).orElse(0L); case "custom" -> (long) player.getStats().getValue(Stats.CUSTOM, target); default -> 0L; }; diff --git a/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json b/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json index 58773a5..d6341bc 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json +++ b/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json @@ -539,6 +539,7 @@ "metric.sanctuary.ranking.picked_up": "%s picked up", "metric.sanctuary.ranking.dropped": "%s dropped", "metric.sanctuary.ranking.killed": "Killed: %s", + "metric.sanctuary.ranking.killed_by": "Killed by: %s", "metric.sanctuary.ranking.custom": "Custom statistic", "metric.sanctuary.ranking.unknown": "Unknown statistic" } diff --git a/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json b/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json index a600de2..5f2246b 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json +++ b/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json @@ -539,6 +539,7 @@ "metric.sanctuary.ranking.picked_up": "%s ramassé", "metric.sanctuary.ranking.dropped": "%s jeté", "metric.sanctuary.ranking.killed": "Tués : %s", + "metric.sanctuary.ranking.killed_by": "Tué par : %s", "metric.sanctuary.ranking.custom": "Statistique personnalisée", "metric.sanctuary.ranking.unknown": "Statistique inconnue" } diff --git a/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json b/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json index c52414f..52aa85f 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json +++ b/sanctuary/src/main/resources/assets/sanctuary/lang/ru_ru.json @@ -495,6 +495,7 @@ "metric.sanctuary.ranking.picked_up": "Подобрано: %s", "metric.sanctuary.ranking.dropped": "Выброшено: %s", "metric.sanctuary.ranking.killed": "Убито: %s", + "metric.sanctuary.ranking.killed_by": "Убит существом: %s", "metric.sanctuary.ranking.custom": "Пользовательская статистика", "metric.sanctuary.ranking.unknown": "Неизвестная статистика" } diff --git a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingModelSmoke.java b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingModelSmoke.java index 95b0c3b..810b1ea 100644 --- a/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingModelSmoke.java +++ b/sanctuary/src/test/java/fr/koka99cab/sanctuary26/sanctuary/api/ranking/SanctuaryRankingModelSmoke.java @@ -93,6 +93,9 @@ public final class SanctuaryRankingModelSmoke { imported.importVanillaFile(oldStats); require(imported.score(alex, usedStick) == 12L, "Existing vanilla statistics were not imported monotonically exactly once"); + require(imported.snapshot(VanillaRankingMetric.descriptor(usedStick), alex).podium().getFirst() + .playerName().equals("?"), + "An import without a cached profile must use '?' rather than exposing a shortened UUID"); Files.deleteIfExists(oldStats); Files.deleteIfExists(temporary);