Compare commits

...
Author SHA1 Message Date
koka ddaf71c7cd release: prepare Sanctuary 26.2.0-alpha.232
Verification 26.2 / check (pull_request) Canceled after 0s
2026-09-03 02:22:46 +02:00
koka 7e1ed85307 Merge pull request 'Corriger la cape, le terminal et le panneau de trophées d’alpha.231' (#152) from fix/alpha231-cape-terminal-trophy-panel into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #152
2026-09-02 23:37:50 +00:00
koka 5840621acf fix: correct alpha231 cape terminal and trophy panel
Verification 26.2 / check (pull_request) Canceled after 0s
2026-09-03 01:35:09 +02:00
koka 974cea20b5 Merge pull request 'release: Sanctuary 26.2.0-alpha.231' (#151) from release/26.2.0-alpha.231 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #151
2026-09-02 22:44:12 +00:00
koka 81a98ed197 release: include readable trophy panels in alpha.231
Verification 26.2 / check (pull_request) Canceled after 0s
2026-09-02 18:42:33 +02:00
koka b022be5739 Merge pull request 'fix(redstoner): rendre les panneaux de trophées lisibles' (#150) from fix/trophy-panel-display into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #150
2026-09-02 16:29:25 +00:00
koka 274d4c1ffa release: prepare Sanctuary 26.2.0-alpha.231 2026-09-02 15:16:01 +02:00
42 changed files with 1274 additions and 396 deletions
+20 -1
View File
@@ -8,13 +8,32 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
## Non publié
Aucun changement non publié.
## `26.2.0-alpha.232`
Modules modifiés : **Another World `0.0.0-alpha.58`**, **Sanctuary `0.0.0-alpha.138`** et **Red-Stoner `0.0.0-alpha.19`**.
- ferme le ticket #146 en enregistrant chaque cliché pris avec un appareil Canon dans le dossier `screenshots` de linstance, à côté des captures F2 ;
- exporte le carré cadré à sa résolution native avec le filtre de la pellicule, sans le réduire aux 128 × 128 pixels de la carte en jeu ;
- écrit le PNG hors du thread de rendu et réserve un nom horodaté unique afin d’éviter les freezes comme les écrasements lors de plusieurs clichés rapprochés ;
- remplace le volume de la cape à motifs par les deux faces opposées de l’îlot UV exact du bouclier, redimensionnées aux proportions 10 × 16 de la cape, afin de corriger la projection et le culling sans afficher le manche ni l’épaisseur du bouclier ;
- laisse la touche dinventaire fermer le terminal tant que la recherche nest pas focalisée, mais la traite comme du texte dans le champ de recherche afin que la lettre `E` ne ferme plus l’écran ;
- utilise les huit lignes du panneau de trophées pour afficher le titre et jusqu’à sept rangs, avec une page de noms suivie dune page de valeurs pour chaque statistique afin que les longues durées de jeu restent lisibles ;
- fait configurer par un œuf dapparition le classement vanilla des créatures correspondantes tuées, par exemple les zombies tués avec l’œuf de zombie ;
- conserve les IDs et les sauvegardes Sanctuary v15/Red-Stoner v4 ; le snapshot client du panneau passant de trois à sept entrées, le protocole Red-Stoner passe de 7 à 8 et exige la mise à jour du client et du serveur.
## `26.2.0-alpha.231`
Modules modifiés : **Sanctuary `0.0.0-alpha.137`** et **Red-Stoner `0.0.0-alpha.18`**.
- transforme le panneau de trophées en plaque verticale de chêne, corrige le texte auparavant masqué et utilise les quatre lignes pour le titre paginé ainsi que les trois places du podium ;
- permet de passer à la page suivante dun clic droit et de revenir à la précédente avec `Maj` + clic droit, sans changer les IDs, les sauvegardes Red-Stoner v4 ni le protocole réseau 7.
- garantit sur chaque niveau de la Backroom un coffre par cellule de 3 × 3 chunks dans les nouveaux chunks, au lieu dun coffre aléatoire réparti entre les cinq niveaux ;
- resserre aussi la grille des lits de sortie de 10 × 10 à 3 × 3 chunks sur chacun des cinq niveaux afin quils soient réellement trouvables ;
- ne change ni le butin ordinaire des coffres, ni les données Sanctuary v15, ni le protocole réseau 24 ; les nouveaux coffres et lits apparaissent dans les chunks qui nont pas encore été générés.
- actualise JEI vers la bêta Fabric `30.29.0.201`, qui corrige un crash avec les mods utilisant latlas de textures ;
- ne change ni le butin ordinaire des coffres, ni les données Sanctuary v15/Red-Stoner v4, ni leurs protocoles réseau 24/7 ; les nouveaux coffres et lits apparaissent dans les chunks qui nont pas encore été générés.
## `26.2.0-alpha.230`
+4 -2
View File
@@ -22,9 +22,11 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans les fichiers de langue. Les images fournies servent d'icônes Fabric de leur module.
Version courante du pack distribué : **`26.2.0-alpha.230`**. Cette release distribue Sanctuary **`0.0.0-alpha.136`** et Red-Stoner **`0.0.0-alpha.17`** ; les huit autres modules conservent leur version issue de l'alpha.229.
Version courante du pack distribué : **`26.2.0-alpha.232`**. Cette release distribue Another World **`0.0.0-alpha.58`**, Sanctuary **`0.0.0-alpha.138`** et Red-Stoner **`0.0.0-alpha.19`** ; les sept autres modules conservent leur version issue de l'alpha.231.
Le prochain Red-Stoner **`0.0.0-alpha.18`** présente le panneau de trophées comme une plaque verticale de chêne et rend enfin ses quatre lignes lisibles : titre et numéro de page, puis les trois places du podium. Un clic droit avance dune page et `Maj` + clic droit revient à la précédente ; les données v4 et le protocole réseau 7 restent inchangés.
Lalpha.232 enregistre automatiquement chaque cliché Canon dans le dossier `screenshots` de linstance, à côté des captures F2. Le PNG conserve le carré réellement cadré, sa résolution et le filtre de la pellicule ; l’écriture asynchrone et les noms uniques évitent les freezes et les écrasements. Cette release corrige aussi la cape blanche décorée, la touche `E` du terminal et étend le panneau de trophées à huit lignes. Chaque statistique y possède une page de noms puis une page de valeurs ; un œuf dapparition ajoute directement le classement des créatures correspondantes tuées. Les sauvegardes restent en données Another World v5, Sanctuary v15 et Red-Stoner v4 ; le snapshot du panneau passant à sept entrées, le protocole Red-Stoner passe à 8.
Lalpha.231 rend les sorties et le butin de la Backroom réellement trouvables sur ses cinq niveaux. Chaque niveau garantit désormais un coffre et un lit de sortie par cellule de 3 × 3 chunks dans les nouveaux chunks, sans ajouter de livres au butin. Le panneau de trophées devient une plaque verticale de chêne avec quatre lignes lisibles : titre et numéro de page, puis les trois places du podium. Un clic droit avance dune page et `Maj` + clic droit revient à la précédente. JEI passe à la bêta Fabric `30.29.0.201`, qui corrige un crash lié à latlas de textures. Les sauvegardes et les chunks déjà générés restent compatibles ; Sanctuary conserve les données v15/protocole 24 et Red-Stoner les données v4/protocole 7.
Lalpha.230 attribue les quêtes communautaires terminées à leur preneur et répartit les lits de sortie rares sur chacun des cinq niveaux de la Backroom. Elle ajoute aussi les classements publics et le panneau mural `redstoner:trophy_panel`, corrige la projection des couleurs et motifs de la cape blanche avec les sprites de bouclier, et permet d’équiper une poule adulte avec larmure de loup vanilla en écailles de tatou. Les sauvegardes restent compatibles ; Sanctuary passe aux données v15 et Red-Stoner aux données v4 sans changer leurs protocoles réseau 24 et 7.
+12 -3
View File
@@ -15,6 +15,14 @@ tasks.register("vaultRulesSmoke", JavaExec) {
mainClass = "fr.koka99cab.sanctuary26.anotherworld.vault.VaultRulesSmoke"
}
tasks.register("canonScreenshotFilesSmoke", JavaExec) {
group = "verification"
description = "Checks Canon photo exports use the vanilla screenshots directory and collision-safe names."
dependsOn tasks.named("testClasses")
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.canon.client.CanonScreenshotFilesSmoke"
}
tasks.named("test") {
failOnNoDiscoveredTests = false
}
@@ -1956,7 +1964,7 @@ tasks.register("verifyAnotherWorldCatalog") {
"src/main/resources/assets/redstoner/textures/item/weather_gauge.png",
"src/main/resources/data/redstoner/recipe/weather_gauge.json"
]
if (project.version.toString() != "0.0.0-alpha.57"
if (project.version.toString() != "0.0.0-alpha.58"
|| sulfurMigration.target?.modules?.anotherworld != "0.0.0-alpha.44"
|| sulfurMigration.sulfur_caves?.sanctuary?.dripstone_cell_period != 2
|| !javaText.contains('Identifier.parse("sanctuary:sulfur_caves")')
@@ -3261,6 +3269,7 @@ tasks.register("verifyAnotherWorldCatalog") {
tasks.named("check") {
dependsOn tasks.named("verifyAnotherWorldCatalog")
dependsOn tasks.named("vaultRulesSmoke")
dependsOn tasks.named("canonScreenshotFilesSmoke")
}
tasks.register("verifyClimateBiomeMixer") {
@@ -3273,7 +3282,7 @@ tasks.register("verifyClimateBiomeMixer") {
"pack/migrations/26.2.0-alpha.188-beta-climate-provinces-to-alpha.189.json"))
def mixer = file(
"src/main/java/fr/koka99cab/sanctuary26/anotherworld/mixin/MultiNoiseBiomeSourceParameterListPresetMixin.java").text
if (project.version.toString() != "0.0.0-alpha.57"
if (project.version.toString() != "0.0.0-alpha.58"
|| migration.source?.pack_version != "26.2.0-alpha.188"
|| migration.target?.pack_version != "26.2.0-alpha.189"
|| migration.target?.modules?.anotherworld != "0.0.0-alpha.48"
@@ -3315,7 +3324,7 @@ tasks.register("verifyBackroomMaterialIntegration") {
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text
def rootBuild = rootProject.file("build.gradle").text
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
if (project.version.toString() != "0.0.0-alpha.57"
if (project.version.toString() != "0.0.0-alpha.58"
|| !initializer.contains("BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS)")
|| !blocks.contains('"turquoise_moss"')
|| !api.contains("public static void registerAnomalyMaterial")
@@ -0,0 +1,38 @@
package fr.koka99cab.canon.client;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
final class CanonScreenshotFiles {
private static final String SCREENSHOT_DIRECTORY = "screenshots";
private static final int MAX_SUFFIX = 10_000;
private CanonScreenshotFiles() {
}
static Path reserve(Path gameDirectory, String timestamp) throws IOException {
Path screenshots = gameDirectory.resolve(SCREENSHOT_DIRECTORY);
Files.createDirectories(screenshots);
for (int suffix = 1; suffix <= MAX_SUFFIX; suffix++) {
Path candidate = screenshots.resolve(fileName(timestamp, suffix));
try {
return Files.createFile(candidate);
} catch (FileAlreadyExistsException ignored) {
// Keep the vanilla timestamp readable while avoiding same-second collisions.
}
}
throw new IOException("Could not reserve a unique Canon screenshot filename");
}
static String fileName(String timestamp, int suffix) {
if (timestamp == null || timestamp.isBlank()) {
throw new IllegalArgumentException("timestamp must not be blank");
}
if (suffix < 1) {
throw new IllegalArgumentException("suffix must be positive");
}
return timestamp + "-canon" + (suffix == 1 ? "" : "_" + suffix) + ".png";
}
}
@@ -8,12 +8,15 @@ import fr.koka99cab.canon.network.CapturePhotoPayload;
import fr.koka99cab.canon.photo.PhotoMapPalette;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.platform.Window;
import java.nio.file.Files;
import java.nio.file.Path;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Screenshot;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.util.ARGB;
import net.minecraft.util.Util;
import net.minecraft.world.item.ItemStack;
public final class ClientPhotoCapture {
@@ -65,6 +68,7 @@ public final class ClientPhotoCapture {
ClientPlayNetworking.send(new CapturePhotoPayload(colors));
client.player.getCooldowns().addCooldown(capture.cameraStack(), CanonConstants.CAMERA_COOLDOWN_TICKS);
client.player.playSound(SoundEvents.SPYGLASS_USE, 0.7F, 1.35F);
exportScreenshot(client, screenshot, capture.style(), offsetX, offsetY, cropWidth, cropHeight);
} catch (Exception exception) {
CanonMod.LOGGER.warn("Failed to capture photo", exception);
if (client.player != null) {
@@ -74,6 +78,72 @@ public final class ClientPhotoCapture {
});
}
private static void exportScreenshot(
Minecraft client,
NativeImage screenshot,
PhotoStyle style,
int offsetX,
int offsetY,
int cropWidth,
int cropHeight
) {
NativeImage exported = null;
Path target = null;
try {
exported = new NativeImage(cropWidth, cropHeight, false);
screenshot.copyRect(exported, offsetX, offsetY, 0, 0, cropWidth, cropHeight, false, false);
target = CanonScreenshotFiles.reserve(client.gameDirectory.toPath(), Util.getFilenameFormattedDateTime());
} catch (Exception exception) {
if (exported != null) exported.close();
if (target != null) deleteReservedFile(target);
notifyScreenshotFailure(client, exception);
return;
}
NativeImage image = exported;
Path output = target;
Util.ioPool().execute(() -> {
try (image) {
applyStyle(image, style);
image.writeToFile(output);
client.execute(() -> {
if (client.player != null) {
client.player.sendSystemMessage(Component.translatable(
"message.canon.camera.screenshot_saved", output.getFileName().toString()));
}
});
} catch (Exception exception) {
deleteReservedFile(output);
notifyScreenshotFailure(client, exception);
}
});
}
private static void applyStyle(NativeImage image, PhotoStyle style) {
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
image.setPixel(x, y, style.applyFilter(image.getPixel(x, y)));
}
}
}
private static void notifyScreenshotFailure(Minecraft client, Exception exception) {
CanonMod.LOGGER.warn("Failed to save Canon photo in the screenshots directory", exception);
client.execute(() -> {
if (client.player != null) {
client.player.sendSystemMessage(Component.translatable("message.canon.camera.screenshot_failed"));
}
});
}
private static void deleteReservedFile(Path target) {
try {
Files.deleteIfExists(target);
} catch (Exception cleanupException) {
CanonMod.LOGGER.debug("Could not remove incomplete Canon screenshot {}", target, cleanupException);
}
}
private static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
@@ -24,6 +24,8 @@
"message.canon.camera.full": "The camera is already full.",
"message.canon.camera.captured": "Photo captured.",
"message.canon.camera.capture_failed": "Could not capture the current frame.",
"message.canon.camera.screenshot_saved": "Photo saved to screenshots: %s",
"message.canon.camera.screenshot_failed": "The in-game photo was captured, but its PNG could not be saved to screenshots.",
"style.canon.white": "Neutral",
"style.canon.white.effect": "Neutral stock with soft paper whites.",
"style.canon.light_gray": "Matte",
@@ -24,6 +24,8 @@
"message.canon.camera.full": "L'appareil photo est deja plein.",
"message.canon.camera.captured": "Photo prise.",
"message.canon.camera.capture_failed": "Impossible de capturer l'image actuelle.",
"message.canon.camera.screenshot_saved": "Photo sauvegardee dans screenshots : %s",
"message.canon.camera.screenshot_failed": "La photo en jeu est prise, mais son PNG n'a pas pu etre sauvegarde dans screenshots.",
"style.canon.white": "Neutre",
"style.canon.white.effect": "Rendu neutre avec des blancs doux.",
"style.canon.light_gray": "Mat",
@@ -24,6 +24,8 @@
"message.canon.camera.full": "Фотоаппарат уже полностью заряжен.",
"message.canon.camera.captured": "Фотография сделана.",
"message.canon.camera.capture_failed": "Не удалось снять текущий кадр.",
"message.canon.camera.screenshot_saved": "Фотография сохранена в screenshots: %s",
"message.canon.camera.screenshot_failed": "Снимок в игре сделан, но PNG не удалось сохранить в screenshots.",
"style.canon.white": "Нейтральный",
"style.canon.white.effect": "Нейтральная плёнка с мягкими белыми тонами.",
"style.canon.light_gray": "Матовый",
@@ -0,0 +1,33 @@
package fr.koka99cab.canon.client;
import java.nio.file.Files;
import java.nio.file.Path;
public final class CanonScreenshotFilesSmoke {
private CanonScreenshotFilesSmoke() {
}
public static void main(String[] args) throws Exception {
String timestamp = "2026-09-03_01.23.45";
require(CanonScreenshotFiles.fileName(timestamp, 1).equals("2026-09-03_01.23.45-canon.png"),
"first screenshot filename");
require(CanonScreenshotFiles.fileName(timestamp, 2).equals("2026-09-03_01.23.45-canon_2.png"),
"same-second screenshot filename");
Path gameDirectory = Files.createTempDirectory("canon-screenshot-smoke-");
Path first = CanonScreenshotFiles.reserve(gameDirectory, timestamp);
Path second = CanonScreenshotFiles.reserve(gameDirectory, timestamp);
require(first.getParent().equals(gameDirectory.resolve("screenshots")),
"screenshots directory");
require(first.getFileName().toString().equals("2026-09-03_01.23.45-canon.png"),
"first reserved screenshot");
require(second.getFileName().toString().equals("2026-09-03_01.23.45-canon_2.png"),
"collision-safe screenshot");
require(Files.isRegularFile(first) && Files.isRegularFile(second),
"reserved screenshot files");
}
private static void require(boolean condition, String contract) {
if (!condition) throw new IllegalStateException("Invalid Canon screenshot contract: " + contract);
}
}
+606 -310
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -14,9 +14,9 @@ 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.18
redstoner_version=0.0.0-alpha.19
redstoner_lifecycle=active
anotherworld_version=0.0.0-alpha.57
anotherworld_version=0.0.0-alpha.58
anotherworld_lifecycle=active
itsalive_version=0.0.0-alpha.38
itsalive_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.137
sanctuary_version=0.0.0-alpha.138
sanctuary_lifecycle=active
pack_version=26.2.0-alpha.230
pack_version=26.2.0-alpha.232
maven_group=fr.koka99cab.sanctuary26
@@ -0,0 +1,56 @@
{
"schema_version": 1,
"status": "released",
"source": {
"pack_version": "26.2.0-alpha.230",
"modules": {
"sanctuary": "0.0.0-alpha.136",
"redstoner": "0.0.0-alpha.17"
}
},
"target": {
"pack_version": "26.2.0-alpha.231",
"modules": {
"sanctuary": "0.0.0-alpha.137",
"redstoner": "0.0.0-alpha.18"
}
},
"backroom_landmarks": {
"level_count": 5,
"loot_chest_cell_size_chunks": 3,
"exit_bed_source_cell_size_chunks": 10,
"exit_bed_target_cell_size_chunks": 3,
"books_added_to_loot": false,
"new_chunks_only": true
},
"trophy_panel": {
"wall_plate_model": "oak",
"readable_line_count": 4,
"page_title_and_three_podium_rows": true,
"right_click_next_page": true,
"sneak_right_click_previous_page": true
},
"compatibility": {
"existing_saves_supported": true,
"new_world_required": false,
"save_reset_required": false,
"persistent_data_formats_changed": false,
"network_protocols_changed": false,
"sanctuary_data_version": 15,
"sanctuary_network_protocol": 24,
"redstoner_data_version": 4,
"redstoner_network_protocol": 7,
"client_server_update_required": true
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true,
"jei": {
"source": "30.29.0.199",
"target": "30.29.0.201",
"packwiz_and_prism_synchronized": true
}
}
}
@@ -0,0 +1,58 @@
{
"schema_version": 1,
"status": "released",
"source": {
"pack_version": "26.2.0-alpha.231",
"modules": {
"anotherworld": "0.0.0-alpha.57",
"sanctuary": "0.0.0-alpha.137",
"redstoner": "0.0.0-alpha.18"
}
},
"target": {
"pack_version": "26.2.0-alpha.232",
"modules": {
"anotherworld": "0.0.0-alpha.58",
"sanctuary": "0.0.0-alpha.138",
"redstoner": "0.0.0-alpha.19"
}
},
"fixed_issues": [146],
"canon_photo_screenshots": {
"directory": "screenshots",
"viewfinder_crop_only": true,
"film_filter_preserved": true,
"full_crop_resolution": true,
"asynchronous_png_write": true,
"collision_safe_names": true
},
"alpha231_follow_up": {
"migration": "alpha231-follow-up-cape-terminal-trophy.json",
"banner_cape_fixed": true,
"terminal_inventory_key_fixed": true,
"trophy_panel_visible_lines": 8,
"trophy_panel_entries_per_view": 7,
"spawn_egg_kill_ranking": true
},
"compatibility": {
"existing_saves_supported": true,
"new_world_required": false,
"save_reset_required": false,
"persistent_data_formats_changed": false,
"network_protocols_changed": true,
"anotherworld_data_version": 5,
"anotherworld_network_protocol": 2,
"sanctuary_data_version": 15,
"sanctuary_network_protocol": 24,
"redstoner_data_version": 4,
"redstoner_source_network_protocol": 7,
"redstoner_target_network_protocol": 8,
"client_server_update_required": true
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true
}
}
@@ -0,0 +1,37 @@
{
"schema_version": 1,
"status": "released",
"released_in": "26.2.0-alpha.232",
"modules": {
"sanctuary": {
"source_module_version": "0.0.0-alpha.137",
"target_module_version": "0.0.0-alpha.138",
"data_version": 15,
"network_protocol": 24
},
"redstoner": {
"source_module_version": "0.0.0-alpha.18",
"target_module_version": "0.0.0-alpha.19",
"data_version": 4,
"source_network_protocol": 7,
"target_network_protocol": 8
}
},
"fixes": {
"banner_cape_flat_shield_uv_faces": true,
"banner_cape_double_sided_culling": true,
"terminal_inventory_key_captured_while_searching": true,
"trophy_panel_visible_lines": 8,
"trophy_panel_entries_per_view": 7,
"trophy_panel_names_then_values": true,
"spawn_egg_maps_to_entity_killed_statistic": true
},
"compatibility": {
"existing_saves_supported": true,
"new_world_required": false,
"save_reset_required": false,
"persistent_data_formats_changed": false,
"network_protocols_changed": true,
"client_server_update_required": true
}
}
+14 -14
View File
@@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
[[files]]
file = "mods/ambiance-0.0.0-alpha.22.jar"
hash = "cdc60ed8f15ca910f41954d9acb6a74ac103e85c73f665fd1bc67be7eab1f15a"
hash = "0e25a85502da59ff111485d47223a6e653db4466cf59f84e7abf8c6b1405aa6a"
[[files]]
file = "mods/anotherworld-0.0.0-alpha.57.jar"
hash = "634a122b406bd7206fd19947687fd7cb37e4cc4098822eecf3f0b7d1fea79d90"
file = "mods/anotherworld-0.0.0-alpha.58.jar"
hash = "c28aaeedc1fb6c8fb8613ba0f12af9a1327edc2ef0eeb20cb73f685c52a29034"
[[files]]
file = "mods/events-0.0.0-alpha.0.jar"
hash = "09bd8dfccca004de46ad3237728ef532de50d04c31b88f1e33721204d3c70cd8"
hash = "4dd9eeccc4f9d2ab2804339320a1a2a24174706cc8a4ac2853b16765b8b2a050"
[[files]]
file = "mods/fabric-api.pw.toml"
@@ -31,7 +31,7 @@ metafile = true
[[files]]
file = "mods/iliketomoveit-0.0.0-alpha.18.jar"
hash = "cd026c4735e945ec6cc19261165033dc10bcbbba75af412f4145e54437a61854"
hash = "a445606ebd5f1a35efdc205de87e4b6b83720d31b06787ae67cc21bc19df9e26"
[[files]]
file = "mods/iris.pw.toml"
@@ -40,32 +40,32 @@ metafile = true
[[files]]
file = "mods/itsalive-0.0.0-alpha.38.jar"
hash = "d905a05edbcb841b173ef5b0090fe7a117ec06e119fba7e184338fec7671fde6"
hash = "d4605837e99bcbf86bbb46a1a71931a39b8e3087cdb0bee648b61996360b942e"
[[files]]
file = "mods/jei.pw.toml"
hash = "9cc4a91f57effc48b94f6acc1f43067bb8eb99fd1df3397612c55761de9ec2c2"
hash = "80d0fa55c1bbc0659e792b047c2bb64c446c2bf92557d9d74777333a8a68a318"
metafile = true
[[files]]
file = "mods/masterkey-0.0.0-alpha.9.jar"
hash = "cef973db96c7523fe828a2ca57ce1283ed1da3b1ae3c646a88ea601ecfe4e825"
hash = "d36b860a0bae9a4e52d2093146a1aeca8e7882b72462bd2cb8575fff262ebf23"
[[files]]
file = "mods/onlyfun-0.0.0-alpha.27.jar"
hash = "1ff5b8853c5a813bbb6c6cf5787c8311d8e72760ed0e9c2adb1da897241e4e1e"
hash = "a77aafb200faaf1162f6f5c0f047bfb5672702ca9d32d0e16e176f3f2455e24c"
[[files]]
file = "mods/ouch-0.0.0-alpha.11.jar"
hash = "fd609cfe5f96ae1d2b41b2da7faf077047514772b56bbe130251db3bf72c2ac8"
hash = "d234613542669e2eddf6e393d3451b2a2786f18ae5a01a61940611199f5e16ef"
[[files]]
file = "mods/redstoner-0.0.0-alpha.17.jar"
hash = "d7bef2bdf533d2c83f7b05549c01a77028dd5edc60418c7244447ca61160f216"
file = "mods/redstoner-0.0.0-alpha.19.jar"
hash = "0891d96adb71c3843028dfe704ab33cac8470712277cef8f9527131f9c02e471"
[[files]]
file = "mods/sanctuary-0.0.0-alpha.136.jar"
hash = "973c12a3f26eb516bc1a053ebd18dab3f845645807448ff5638a1e9b26ba28bd"
file = "mods/sanctuary-0.0.0-alpha.138.jar"
hash = "233c84a9ba7ffb172618e541ab3635f3b675a858bacf80c4712feb329069c99c"
[[files]]
file = "mods/simple-voice-chat.pw.toml"
+4 -4
View File
@@ -1,14 +1,14 @@
name = "Just Enough Items (JEI)"
filename = "jei-26.2-fabric-30.29.0.199.jar"
filename = "jei-26.2-fabric-30.29.0.201.jar"
side = "both"
pin = true
[download]
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/astSvyl6/jei-26.2-fabric-30.29.0.199.jar"
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/d3HeviSL/jei-26.2-fabric-30.29.0.201.jar"
hash-format = "sha512"
hash = "2849ff6a4cc91822798f0c70e7749c298b911c1037fc74e9ab0881734b8d5cb6507fb3169b4a240ef6c37eb6f7c573794c46073ff352f160feee245233b2abe5"
hash = "2279cb0ec4e41d9eebad80c2ecac76d19c8bc8e5aac69ba13f35b77494625c70d66edc74d59959116012bfb2f464b55806dd69fbf6387f25679b1553953e329a"
[update]
[update.modrinth]
mod-id = "u6dRKJwZ"
version = "astSvyl6"
version = "d3HeviSL"
+2 -2
View File
@@ -1,12 +1,12 @@
name = "Sanctuary 26.2"
author = "KOKA99CAB"
version = "26.2.0-alpha.230"
version = "26.2.0-alpha.232"
pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "a78df91699f1ffa661e1bf59101891a217bd74334ee338b96824f5a9b745e443"
hash = "0a35e9686a1c272ed05694b8509273334bf7770951349465c0280428b2c06338"
[versions]
fabric = "0.19.3"
+6 -6
View File
@@ -3,7 +3,7 @@
"game": "minecraft",
"name": "Sanctuary 26.2",
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
"versionId": "26.2.0-alpha.230",
"versionId": "26.2.0-alpha.232",
"files": [
{
"path": "mods/fabric-api-0.159.0+26.2.jar",
@@ -21,19 +21,19 @@
"fileSize": 2535793
},
{
"path": "mods/jei-26.2-fabric-30.29.0.199.jar",
"path": "mods/jei-26.2-fabric-30.29.0.201.jar",
"hashes": {
"sha1": "1d6ab9269be16f85fb13a34a36b34ad07a9b32a7",
"sha512": "2849ff6a4cc91822798f0c70e7749c298b911c1037fc74e9ab0881734b8d5cb6507fb3169b4a240ef6c37eb6f7c573794c46073ff352f160feee245233b2abe5"
"sha1": "e82447aff07e87a372cf817bc9c28b0d77b6f55c",
"sha512": "2279cb0ec4e41d9eebad80c2ecac76d19c8bc8e5aac69ba13f35b77494625c70d66edc74d59959116012bfb2f464b55806dd69fbf6387f25679b1553953e329a"
},
"env": {
"client": "required",
"server": "required"
},
"downloads": [
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/astSvyl6/jei-26.2-fabric-30.29.0.199.jar"
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/d3HeviSL/jei-26.2-fabric-30.29.0.201.jar"
],
"fileSize": 1849188
"fileSize": 1860103
},
{
"path": "mods/voicechat-fabric-2.6.22+26.2.jar",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"pack_version": "26.2.0-alpha.230",
"pack_version": "26.2.0-alpha.232",
"release_channel": "alpha",
"minecraft_version": "26.2",
"fabric_loader_version": "0.19.3",
@@ -9,7 +9,7 @@
"project_id": "FgOqyAXd",
"project_slug": "sanctuary-koka99",
"project_url": "https://modrinth.com/modpack/sanctuary-koka99",
"primary_file": "sanctuary-26.2.0-alpha.230.mrpack",
"primary_file": "sanctuary-26.2.0-alpha.232.mrpack",
"shared_client_server_pack": true,
"update_owner": "packwiz-installer",
"check_on_launch": true,
@@ -36,6 +36,7 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
private static final int DATA_VERSION = 1;
private static final int REFRESH_INTERVAL_TICKS = 20;
private static final String CLIENT_SNAPSHOT_TAG = "VisibleSnapshot";
private static final String CLIENT_VALUES_VIEW_TAG = "ValuesView";
private static final Codec<Page> PAGE_CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.fieldOf("metric").forGetter(Page::metricId),
ItemStack.OPTIONAL_CODEC.fieldOf("icon").forGetter(Page::icon)
@@ -60,6 +61,7 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
private String ownerName = "?";
private List<Page> pages = List.of(defaultPage());
private int currentPage;
private boolean valuesView;
private long lastRefreshTick = Long.MIN_VALUE;
private ClientSnapshot visibleSnapshot = ClientSnapshot.empty(SanctuaryRankingApi.defaultMetric());
@@ -104,6 +106,7 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
currentPage = Math.max(0, pages.indexOf(pages.stream()
.filter(page -> offered.stream().anyMatch(metric -> metric.id().equals(page.metricId())))
.findFirst().orElse(pages.getFirst())));
valuesView = false;
ownerName = cleanName(player.getGameProfile().name());
syncPersistent();
if (level instanceof ServerLevel serverLevel) refresh(serverLevel);
@@ -120,7 +123,10 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
private boolean movePage(int offset) {
if (!writable() || pages.isEmpty()) return false;
currentPage = TrophyPanelRules.pageByOffset(currentPage, pages.size(), offset);
TrophyPanelRules.DisplayPage displayPage = TrophyPanelRules.displayPageByOffset(
currentPage, valuesView, pages.size(), offset);
currentPage = displayPage.metricPage();
valuesView = displayPage.valuesView();
syncPersistent();
if (level instanceof ServerLevel serverLevel) refresh(serverLevel);
return true;
@@ -131,11 +137,15 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
}
public int pageNumber() {
return currentPage + 1;
return TrophyPanelRules.displayPageNumber(currentPage, valuesView, pages.size());
}
public int pageCount() {
return pages.size();
return TrophyPanelRules.displayPageCount(pages.size());
}
public boolean valuesView() {
return valuesView;
}
public String ownerName() {
@@ -174,6 +184,7 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
pages = input.read("Pages", PAGE_CODEC.listOf(0, TrophyPanelRules.MAX_PAGES))
.map(TrophyPanelBlockEntity::sanitizePages).orElseGet(() -> List.of(defaultPage()));
currentPage = Math.clamp(input.getIntOr("CurrentPage", 0), 0, pages.size() - 1);
valuesView = input.getBooleanOr(CLIENT_VALUES_VIEW_TAG, false);
visibleSnapshot = input.read(CLIENT_SNAPSHOT_TAG, CLIENT_SNAPSHOT_CODEC)
.orElseGet(() -> ClientSnapshot.empty(metricFor(currentPage().metricId())));
}
@@ -187,6 +198,7 @@ public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwn
public CompoundTag getUpdateTag(HolderLookup.Provider lookup) {
CompoundTag tag = saveCustomOnly(lookup);
tag.store(CLIENT_SNAPSHOT_TAG, CLIENT_SNAPSHOT_CODEC, visibleSnapshot);
tag.putBoolean(CLIENT_VALUES_VIEW_TAG, valuesView);
return tag;
}
@@ -1,5 +1,6 @@
package fr.koka99cab.sanctuary26.redstoner.client.gui;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalKeyRules;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalMenu;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules;
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalSort;
@@ -139,10 +140,9 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
@Override
public boolean keyPressed(KeyEvent event) {
if (searchBox != null && searchBox.isFocused() && minecraft != null
&& minecraft.options.keyInventory.matches(event)) {
clearSearchFocus();
}
if (StorageTerminalKeyRules.capturesInventoryKey(
searchBox != null && searchBox.isFocused(), minecraft != null
&& minecraft.options.keyInventory.matches(event))) return true;
return super.keyPressed(event);
}
@@ -5,7 +5,11 @@ import com.mojang.math.Axis;
import fr.koka99cab.sanctuary26.redstoner.block.TrophyPanelBlock;
import fr.koka99cab.sanctuary26.redstoner.block.entity.TrophyPanelBlockEntity;
import fr.koka99cab.sanctuary26.redstoner.client.renderer.state.TrophyPanelRenderState;
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.Arrays;
import net.minecraft.client.gui.Font;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
@@ -21,7 +25,7 @@ public final class TrophyPanelBlockEntityRenderer
implements BlockEntityRenderer<TrophyPanelBlockEntity, TrophyPanelRenderState> {
private static final int MAX_TEXT_WIDTH = 90;
private static final float TEXT_SCALE = 0.010416667F;
private static final float[] LINE_HEIGHTS = {0.70F, 0.59F, 0.48F, 0.37F};
private static final float[] LINE_HEIGHTS = {0.86F, 0.75F, 0.64F, 0.53F, 0.42F, 0.31F, 0.20F, 0.09F};
private static final int TEXT_COLOR = 0xFF3D281A;
private final Font font;
@@ -39,26 +43,27 @@ public final class TrophyPanelBlockEntityRenderer
float tickProgress, Vec3 cameraPos, ModelFeatureRenderer.CrumblingOverlay crumblingOverlay) {
BlockEntityRenderer.super.extractRenderState(panel, state, tickProgress, cameraPos, crumblingOverlay);
state.facing = panel.getBlockState().getValue(TrophyPanelBlock.FACING);
Arrays.fill(state.lines, Component.empty());
ItemStack icon = panel.currentPage().icon();
TrophyPanelBlockEntity.ClientSnapshot snapshot = panel.visibleSnapshot();
RankingUnit unit = snapshot.unit();
Component metric = !icon.isEmpty() ? Component.translatable(snapshot.translationKey(), icon.getHoverName())
: Component.translatable(snapshot.translationKey());
Component metric = SanctuaryRankingApi.displayName(
new RankingMetric(snapshot.metricId(), snapshot.translationKey(), unit), icon);
state.lines[0] = Component.translatable("block.redstoner.trophy_panel.page_title",
panel.pageNumber(), panel.pageCount(), metric);
if (snapshot.podium().isEmpty()) {
state.lines[1] = Component.translatable("block.redstoner.trophy_panel.no_score");
state.lines[2] = Component.empty();
state.lines[3] = Component.empty();
} else {
for (int index = 0; index < 3; index++) {
for (int index = 0; index < TrophyPanelRules.ENTRIES_PER_VIEW; index++) {
if (index >= snapshot.podium().size()) {
state.lines[index + 1] = Component.empty();
continue;
break;
}
TrophyPanelBlockEntity.ClientEntry entry = snapshot.podium().get(index);
state.lines[index + 1] = Component.translatable("block.redstoner.trophy_panel.podium",
entry.rank(), entry.name(), unit.format(entry.value()));
state.lines[index + 1] = panel.valuesView()
? Component.translatable("block.redstoner.trophy_panel.value",
entry.rank(), unit.format(entry.value()))
: Component.translatable("block.redstoner.trophy_panel.player",
entry.rank(), entry.name());
}
}
}
@@ -1,13 +1,16 @@
package fr.koka99cab.sanctuary26.redstoner.client.renderer.state;
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules;
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
public final class TrophyPanelRenderState extends BlockEntityRenderState {
public static final int LINE_COUNT = 4;
public final Component[] lines = {
Component.empty(), Component.empty(), Component.empty(), Component.empty()
};
public static final int LINE_COUNT = TrophyPanelRules.LINE_COUNT;
public final Component[] lines = new Component[LINE_COUNT];
public Direction facing = Direction.NORTH;
public TrophyPanelRenderState() {
java.util.Arrays.fill(lines, Component.empty());
}
}
@@ -12,7 +12,7 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
public final class RedStonerNetworking {
public static final int PROTOCOL_VERSION = 7;
public static final int PROTOCOL_VERSION = 8;
private static boolean initialized;
private RedStonerNetworking() {
@@ -0,0 +1,10 @@
package fr.koka99cab.sanctuary26.redstoner.storage;
/** Keyboard routing shared by the terminal screen and its deterministic smoke test. */
public final class StorageTerminalKeyRules {
private StorageTerminalKeyRules() {}
public static boolean capturesInventoryKey(boolean searchFocused, boolean inventoryKey) {
return searchFocused && inventoryKey;
}
}
@@ -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 LINE_COUNT = 8;
public static final int ENTRIES_PER_VIEW = LINE_COUNT - 1;
private static final int VIEWS_PER_METRIC = 2;
private TrophyPanelRules() {}
@@ -28,10 +31,33 @@ public final class TrophyPanelRules {
return pageCount <= 0 ? 0 : Math.floorMod(currentPage + offset, pageCount);
}
public static DisplayPage displayPageByOffset(int metricPage, boolean valuesView,
int metricCount, int offset) {
int safeMetricCount = Math.max(1, metricCount);
int safeMetricPage = Math.clamp(metricPage, 0, safeMetricCount - 1);
int currentDisplayPage = safeMetricPage * VIEWS_PER_METRIC + (valuesView ? 1 : 0);
int displayPage = Math.floorMod(currentDisplayPage + offset,
safeMetricCount * VIEWS_PER_METRIC);
return new DisplayPage(displayPage / VIEWS_PER_METRIC,
displayPage % VIEWS_PER_METRIC == 1);
}
public static int displayPageNumber(int metricPage, boolean valuesView, int metricCount) {
int safeMetricCount = Math.max(1, metricCount);
return Math.clamp(metricPage, 0, safeMetricCount - 1) * VIEWS_PER_METRIC
+ (valuesView ? 2 : 1);
}
public static int displayPageCount(int metricCount) {
return Math.max(1, metricCount) * VIEWS_PER_METRIC;
}
public static List<Identifier> mergeMetricIds(List<Identifier> current, List<Identifier> offered) {
LinkedHashSet<Identifier> merged = new LinkedHashSet<>();
if (current != null) merged.addAll(current);
if (offered != null) merged.addAll(offered);
return List.copyOf(new ArrayList<>(merged).subList(0, Math.min(MAX_PAGES, merged.size())));
}
public record DisplayPage(int metricPage, boolean valuesView) {}
}
@@ -66,6 +66,8 @@
"block.redstoner.trophy_panel": "Trophy Panel",
"block.redstoner.trophy_panel.page_title": "[%s/%s] %s",
"block.redstoner.trophy_panel.podium": "#%s %s · %s",
"block.redstoner.trophy_panel.player": "#%s %s",
"block.redstoner.trophy_panel.value": "#%s %s",
"block.redstoner.trophy_panel.no_score": "No score yet",
"block.redstoner.trophy_panel.champion": "#1 %s · %s",
"block.redstoner.trophy_panel.owner_rank": "%s · #%s · %s",
@@ -66,6 +66,8 @@
"block.redstoner.trophy_panel": "Panneau de trophées",
"block.redstoner.trophy_panel.page_title": "[%s/%s] %s",
"block.redstoner.trophy_panel.podium": "#%s %s · %s",
"block.redstoner.trophy_panel.player": "#%s %s",
"block.redstoner.trophy_panel.value": "#%s %s",
"block.redstoner.trophy_panel.no_score": "Aucun score",
"block.redstoner.trophy_panel.champion": "#1 %s · %s",
"block.redstoner.trophy_panel.owner_rank": "%s · #%s · %s",
@@ -66,6 +66,8 @@
"block.redstoner.trophy_panel": "Панель трофеев",
"block.redstoner.trophy_panel.page_title": "[%s/%s] %s",
"block.redstoner.trophy_panel.podium": "#%s %s · %s",
"block.redstoner.trophy_panel.player": "#%s %s",
"block.redstoner.trophy_panel.value": "#%s %s",
"block.redstoner.trophy_panel.no_score": "Результатов пока нет",
"block.redstoner.trophy_panel.champion": "#1 %s · %s",
"block.redstoner.trophy_panel.owner_rank": "%s · #%s · %s",
+1 -1
View File
@@ -31,7 +31,7 @@
"system_type": "gameplay-system",
"namespace": "redstoner",
"data_version": 4,
"network_protocol": 7,
"network_protocol": 8,
"lifecycle": "${lifecycle}",
"pack_version": "${pack_version}",
"descriptions": {
@@ -27,6 +27,7 @@ public final class StorageTerminalSmoke {
verifyPagination();
verifyAggregationAndTransfers();
verifySearch();
verifySearchKeyboardRouting();
verifySorting();
verifyStableLayout();
}
@@ -145,6 +146,15 @@ public final class StorageTerminalSmoke {
"Terminal search retained an unrelated item");
}
private static void verifySearchKeyboardRouting() {
require(StorageTerminalKeyRules.capturesInventoryKey(true, true),
"The inventory key escaped the focused terminal search field");
require(!StorageTerminalKeyRules.capturesInventoryKey(false, true),
"The inventory key no longer closes an unfocused terminal");
require(!StorageTerminalKeyRules.capturesInventoryKey(true, false),
"The terminal captured an unrelated search-field key");
}
private static Holder<Item> stackable(Item item) {
return new Holder.Direct<>(item, DataComponentMap.builder()
.set(DataComponents.MAX_STACK_SIZE, 64).build());
@@ -19,6 +19,18 @@ public final class TrophyPanelRulesSmoke {
"Reverse public page cycling does not wrap");
require(TrophyPanelRules.pageByOffset(4, 0, 1) == 0,
"An empty panel produced an invalid page index");
TrophyPanelRules.DisplayPage values = TrophyPanelRules.displayPageByOffset(0, false, 2, 1);
TrophyPanelRules.DisplayPage secondMetric = TrophyPanelRules.displayPageByOffset(
values.metricPage(), values.valuesView(), 2, 1);
TrophyPanelRules.DisplayPage wrapped = TrophyPanelRules.displayPageByOffset(0, false, 2, -1);
require(values.metricPage() == 0 && values.valuesView()
&& secondMetric.metricPage() == 1 && !secondMetric.valuesView()
&& wrapped.metricPage() == 1 && wrapped.valuesView(),
"Name and value views do not cycle as consecutive public pages");
require(TrophyPanelRules.LINE_COUNT == 8 && TrophyPanelRules.ENTRIES_PER_VIEW == 7
&& TrophyPanelRules.displayPageNumber(1, true, 2) == 4
&& TrophyPanelRules.displayPageCount(2) == 4,
"The panel does not use eight lines or number both metric views correctly");
Identifier first = Identifier.parse("sanctuary:first");
Identifier second = Identifier.parse("sanctuary:second");
+30 -16
View File
@@ -124,6 +124,14 @@ tasks.register("capeSyncPayloadSmoke", JavaExec) {
mainClass = "fr.koka99cab.sanctuary26.sanctuary.network.CapeSyncPayloadSmoke"
}
tasks.register("capeModelSmoke", JavaExec) {
group = "verification"
description = "Checks the double-sided shield-pattern plane used by banner capes."
dependsOn tasks.named("testClasses")
classpath = sourceSets.test.runtimeClasspath
mainClass = "fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryBannerCapeModelSmoke"
}
tasks.register("chickenArmorSmoke", JavaExec) {
group = "verification"
description = "Checks that only adult unarmored chickens accept armadillo-scute armor."
@@ -207,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| !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")
@@ -2923,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| 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
@@ -3089,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| 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
@@ -3127,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| 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]
@@ -3158,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| migration.target?.pack_version != "26.2.0-alpha.195"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.111"
|| migration.compatibility?.sanctuary_data_version != 11
@@ -3189,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.112"
|| migration.mailbox?.id != "sanctuary:delivery_box"
|| migration.mailbox?.horizontal_facing != true
@@ -3217,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| !transfer.contains("shouldAppendOverflow")
|| !transfer.contains("isVanillaStorageRange")
|| !transfer.contains("hotbarStart")
@@ -3263,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.113"
|| migration.quest_requests?.save_key != "quest_requests"
|| migration.quest_requests?.data_version != 1
@@ -3325,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| 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"
@@ -3394,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| 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"
@@ -3473,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.130"
|| migration.white_cape?.crafting_material_count != 6
|| migration.white_cape?.maximum_pattern_layers != 6
@@ -3525,9 +3533,14 @@ 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.137"
|| !capeRenderer.contains("SHIELD_PATTERN_X_SCALE = 1.2F")
|| !capeRenderer.contains("SHIELD_PATTERN_Y_SCALE = 1.375F")
if (project.version.toString() != "0.0.0-alpha.138"
|| !capeRenderer.contains("SHIELD_FACE_WIDTH = 12.0F")
|| !capeRenderer.contains("SHIELD_FACE_HEIGHT = 22.0F")
|| !capeRenderer.contains("CAPE_WIDTH / SHIELD_FACE_WIDTH")
|| !capeRenderer.contains("CAPE_HEIGHT / SHIELD_FACE_HEIGHT")
|| !capeRenderer.contains('addOrReplaceChild("front"')
|| !capeRenderer.contains('addOrReplaceChild("back"')
|| !capeRenderer.contains("Set.of(Direction.NORTH)")
|| !capeRenderer.contains("model, state, false, appearance.baseColor()")
|| !armorLayer.contains("EquipmentClientInfo.LayerType.WOLF_BODY")
|| !armorRules.contains("held.is(Items.WOLF_ARMOR)")
@@ -3574,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| migration.target?.modules?.sanctuary != "0.0.0-alpha.122"
|| migration.alpha_world?.dimension != "sanctuary:alpha"
|| migration.alpha_world?.legacy_dimension != "portals:alpha"
@@ -3634,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.137"
if (project.version.toString() != "0.0.0-alpha.138"
|| 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"
@@ -3696,6 +3709,7 @@ tasks.named("check") {
dependsOn tasks.named("alphaIndevGeneratorSmoke")
dependsOn tasks.named("capeJoinSyncSmoke")
dependsOn tasks.named("capeSyncPayloadSmoke")
dependsOn tasks.named("capeModelSmoke")
dependsOn tasks.named("chickenArmorSmoke")
dependsOn tasks.named("inventorySortSmoke")
dependsOn tasks.named("entityPolicySmoke")
@@ -4,9 +4,9 @@ import java.util.List;
import java.util.UUID;
import net.minecraft.resources.Identifier;
/** Server snapshot intentionally bounded to one visible metric and three podium entries. */
/** Server snapshot intentionally bounded to one visible metric and seven panel rows. */
public record RankingSnapshot(RankingMetric metric, List<Entry> podium, Placement owner) {
public static final int MAX_PODIUM_SIZE = 3;
public static final int MAX_PODIUM_SIZE = 7;
public RankingSnapshot {
podium = podium == null ? List.of() : List.copyOf(podium.stream().limit(MAX_PODIUM_SIZE).toList());
@@ -8,6 +8,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
@@ -66,6 +67,11 @@ public final class SanctuaryRankingApi {
return metrics;
}
/** Localized label for a metric configured from its representative item. */
public static Component displayName(RankingMetric metric, ItemStack icon) {
return VanillaRankingMetric.displayName(metric, icon);
}
public static boolean eligible(ServerPlayer player) {
return isRealOnlinePlayer(player) && !player.isCreative() && !player.isSpectator();
}
@@ -5,12 +5,14 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.stats.Stats;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.SpawnEggItem;
/** Stable encoding and server reader for persisted vanilla statistic identifiers. */
final class VanillaRankingMetric {
@@ -43,6 +45,13 @@ final class VanillaRankingMetric {
static List<RankingMetric> itemMetrics(ItemStack icon) {
if (icon == null || icon.isEmpty()) return List.of();
Item item = icon.getItem();
if (item instanceof SpawnEggItem) {
var entityType = SpawnEggItem.getType(icon);
if (entityType == null) return List.of();
Identifier entityId = BuiltInRegistries.ENTITY_TYPE.getKey(entityType);
return List.of(new RankingMetric(id(Identifier.withDefaultNamespace("killed"), entityId),
"metric.sanctuary.ranking.killed", RankingUnit.COUNT));
}
Identifier itemId = BuiltInRegistries.ITEM.getKey(item);
List<RankingMetric> metrics = new ArrayList<>();
for (String family : ITEM_FAMILIES) {
@@ -53,6 +62,18 @@ final class VanillaRankingMetric {
return List.copyOf(metrics);
}
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);
}
return icon == null || icon.isEmpty()
? Component.translatable(metric.translationKey())
: Component.translatable(metric.translationKey(), icon.getHoverName());
}
static Optional<Source> parse(Identifier metricId) {
if (!metricId.getNamespace().equals(SanctuaryMod.MOD_ID)
|| !metricId.getPath().startsWith(PREFIX)) return Optional.empty();
@@ -79,10 +100,16 @@ final class VanillaRankingMetric {
.map(item -> (long) player.getStats().getValue(Stats.ITEM_PICKED_UP, item)).orElse(0L);
case "dropped" -> BuiltInRegistries.ITEM.getOptional(target)
.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 "custom" -> (long) player.getStats().getValue(Stats.CUSTOM, target);
default -> 0L;
};
}
private static Component iconName(ItemStack icon) {
return icon == null || icon.isEmpty() ? Component.literal("?") : icon.getHoverName();
}
record Source(Identifier type, Identifier target) {}
}
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.sanctuary.client;
import com.mojang.blaze3d.vertex.PoseStack;
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
import java.util.Set;
import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityRenderLayerRegistrationCallback;
import net.fabricmc.fabric.api.client.rendering.v1.ModelLayerRegistry;
import net.minecraft.client.model.geom.ModelLayerLocation;
@@ -23,6 +24,7 @@ import net.minecraft.client.renderer.entity.state.AvatarRenderState;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.client.resources.model.EquipmentAssetManager;
import net.minecraft.client.resources.model.EquipmentClientInfo;
import net.minecraft.core.Direction;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.item.ItemStack;
@@ -30,8 +32,10 @@ import net.minecraft.world.item.equipment.Equippable;
/** Renders a banner-decorated cape with vanilla cape physics and banner pattern sprites. */
public final class SanctuaryBannerCapeLayer extends RenderLayer<AvatarRenderState, PlayerModel> {
private static final float SHIELD_PATTERN_X_SCALE = 1.2F;
private static final float SHIELD_PATTERN_Y_SCALE = 1.375F;
private static final float SHIELD_FACE_WIDTH = 12.0F;
private static final float SHIELD_FACE_HEIGHT = 22.0F;
private static final float CAPE_WIDTH = 10.0F;
private static final float CAPE_HEIGHT = 16.0F;
private static final ModelLayerLocation MODEL_LAYER =
new ModelLayerLocation(SanctuaryMod.id("banner_cape"), "main");
private final PlayerCapeModel model;
@@ -63,17 +67,27 @@ public final class SanctuaryBannerCapeLayer extends RenderLayer<AvatarRenderStat
helper.register(new SanctuaryBannerCapeLayer(parent, context));
}
private static LayerDefinition createLayer() {
static LayerDefinition createLayer() {
MeshDefinition mesh = PlayerModel.createMesh(CubeDeformation.NONE, false);
PartDefinition root = mesh.getRoot().clearRecursively();
PartDefinition body = root.getChild("body");
body.addOrReplaceChild("cape", CubeListBuilder.create().texOffs(0, 0)
.addBox(-5.0F, 0.0F, -1.0F, 10.0F, 16.0F, 1.0F,
CubeDeformation.NONE, SHIELD_PATTERN_X_SCALE, SHIELD_PATTERN_Y_SCALE),
PartPose.offsetAndRotation(0.0F, 0.0F, 2.0F, 0.0F, (float) Math.PI, 0.0F));
PartDefinition cape = body.addOrReplaceChild("cape", CubeListBuilder.create(),
PartPose.offsetAndRotation(0.0F, 0.0F, 2.0F, 0.0F, (float) Math.PI, 0.0F)
.scaled(CAPE_WIDTH / SHIELD_FACE_WIDTH, CAPE_HEIGHT / SHIELD_FACE_HEIGHT, 1.0F));
cape.addOrReplaceChild("front", shieldPatternFace(), PartPose.offset(0.0F, 0.0F, -1.0F));
cape.addOrReplaceChild("back", shieldPatternFace(),
PartPose.offsetAndRotation(0.0F, 0.0F, -1.0F, 0.0F, (float) Math.PI, 0.0F));
return LayerDefinition.create(mesh, 64, 64);
}
private static CubeListBuilder shieldPatternFace() {
// Shield patterns paint this exact 12x22 UV island. Two opposed planes keep
// the cloth visible from both sides without rendering the shield thickness or handle.
return CubeListBuilder.create().texOffs(1, 1)
.addBox(-6.0F, 0.0F, 0.0F, SHIELD_FACE_WIDTH, SHIELD_FACE_HEIGHT, 0.0F,
Set.of(Direction.NORTH));
}
@Override
public void submit(PoseStack poseStack, SubmitNodeCollector collector, int packedLight,
AvatarRenderState state, float yRot, float xRot) {
@@ -538,6 +538,7 @@
"metric.sanctuary.ranking.broken": "%s broken",
"metric.sanctuary.ranking.picked_up": "%s picked up",
"metric.sanctuary.ranking.dropped": "%s dropped",
"metric.sanctuary.ranking.killed": "Killed: %s",
"metric.sanctuary.ranking.custom": "Custom statistic",
"metric.sanctuary.ranking.unknown": "Unknown statistic"
}
@@ -538,6 +538,7 @@
"metric.sanctuary.ranking.broken": "%s cassé",
"metric.sanctuary.ranking.picked_up": "%s ramassé",
"metric.sanctuary.ranking.dropped": "%s jeté",
"metric.sanctuary.ranking.killed": "Tués : %s",
"metric.sanctuary.ranking.custom": "Statistique personnalisée",
"metric.sanctuary.ranking.unknown": "Statistique inconnue"
}
@@ -494,6 +494,7 @@
"metric.sanctuary.ranking.broken": "Сломано: %s",
"metric.sanctuary.ranking.picked_up": "Подобрано: %s",
"metric.sanctuary.ranking.dropped": "Выброшено: %s",
"metric.sanctuary.ranking.killed": "Убито: %s",
"metric.sanctuary.ranking.custom": "Пользовательская статистика",
"metric.sanctuary.ranking.unknown": "Неизвестная статистика"
}
@@ -3,12 +3,22 @@ package fr.koka99cab.sanctuary26.sanctuary.api.ranking;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.minecraft.SharedConstants;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.Identifier;
import net.minecraft.server.Bootstrap;
import net.minecraft.stats.Stats;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.TypedEntityData;
/** Pure ranking smoke for ordering, tied ranks, saturation and newer-save protection. */
public final class SanctuaryRankingModelSmoke {
@@ -36,6 +46,12 @@ public final class SanctuaryRankingModelSmoke {
&& tied.podium().get(2).rank() == 3, "Equal values must retain the same competition rank");
require(tied.owner().rank() == 1 && tied.owner().value() == 10L,
"Panel owner placement must reflect the real tied rank");
for (int index = 4; index <= 9; index++) {
UUID player = UUID.fromString("00000000-0000-0000-0000-" + String.format("%012d", index));
data.recordValue(player, "Player" + index, metricId, 10L - index);
}
require(data.snapshot(metric, bob).podium().size() == RankingSnapshot.MAX_PODIUM_SIZE,
"The eight-line panel must expose exactly seven bounded ranking rows");
data.increment(chris, "Chris", metricId, Long.MAX_VALUE);
require(data.score(chris, metricId) == SanctuaryRankingData.MAX_VALUE,
@@ -57,6 +73,16 @@ public final class SanctuaryRankingModelSmoke {
require(VanillaRankingMetric.parse(vanillaId).orElseThrow().target().equals(Stats.PLAY_TIME)
&& VanillaRankingMetric.descriptor(vanillaId).unit() == RankingUnit.TICKS,
"Vanilla metric IDs and units must round-trip stably");
List<RankingMetric> zombieMetrics = VanillaRankingMetric.itemMetrics(
new ItemStack(new Holder.Direct<Item>(Items.ZOMBIE_SPAWN_EGG, DataComponentMap.builder()
.set(DataComponents.MAX_STACK_SIZE, 64)
.set(DataComponents.ENTITY_DATA, TypedEntityData.of(EntityTypes.ZOMBIE, new CompoundTag()))
.build())));
VanillaRankingMetric.Source zombieSource = VanillaRankingMetric.parse(
zombieMetrics.getFirst().id()).orElseThrow();
require(zombieMetrics.size() == 1 && zombieSource.type().getPath().equals("killed")
&& zombieSource.target().equals(Identifier.withDefaultNamespace("zombie")),
"A zombie spawn egg must configure the zombies-killed ranking");
Path temporary = Files.createTempDirectory("sanctuary-ranking-migration-");
Path oldStats = temporary.resolve(alex + ".json");
@@ -0,0 +1,80 @@
package fr.koka99cab.sanctuary26.sanctuary.client;
import com.mojang.blaze3d.vertex.PoseStack;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.client.model.geom.ModelPart;
import org.joml.Vector3f;
import org.joml.Vector3fc;
/** Verifies the shield-pattern UV island, cape proportions and double-sided plane. */
public final class SanctuaryBannerCapeModelSmoke {
private static final float EPSILON = 0.0001F;
private SanctuaryBannerCapeModelSmoke() {}
public static void main(String[] args) {
ModelPart cape = SanctuaryBannerCapeLayer.createLayer().bakeRoot()
.getChild("body").getChild("cape");
List<Float> normalZ = new ArrayList<>();
List<ModelPart.Vertex> vertices = new ArrayList<>();
cape.visit(new PoseStack(), (pose, path, cubeIndex, cube) -> {
for (ModelPart.Polygon polygon : cube.polygons) {
normalZ.add(pose.transformNormal(polygon.normal(), new Vector3f()).z());
vertices.addAll(List.of(polygon.vertices()));
}
});
require(normalZ.size() == 2 && normalZ.stream().anyMatch(value -> value < -0.99F)
&& normalZ.stream().anyMatch(value -> value > 0.99F),
"The banner cape must contain one visible plane for each culling direction");
require(close(minU(vertices), 1.0F / 64.0F) && close(maxU(vertices), 13.0F / 64.0F)
&& close(minV(vertices), 1.0F / 64.0F) && close(maxV(vertices), 23.0F / 64.0F),
"The cape no longer samples the shield plate's exact pattern UV island");
List<Vector3fc> extents = new ArrayList<>();
cape.getExtentsForGui(new PoseStack(), extents::add);
require(close(minX(extents), -5.0F / 16.0F) && close(maxX(extents), 5.0F / 16.0F)
&& close(minY(extents), 0.0F) && close(maxY(extents), 1.0F),
"The shield-pattern plane does not retain the vanilla 10x16 cape proportions");
}
private static float minU(List<ModelPart.Vertex> vertices) {
return (float) vertices.stream().mapToDouble(ModelPart.Vertex::u).min().orElseThrow();
}
private static float maxU(List<ModelPart.Vertex> vertices) {
return (float) vertices.stream().mapToDouble(ModelPart.Vertex::u).max().orElseThrow();
}
private static float minV(List<ModelPart.Vertex> vertices) {
return (float) vertices.stream().mapToDouble(ModelPart.Vertex::v).min().orElseThrow();
}
private static float maxV(List<ModelPart.Vertex> vertices) {
return (float) vertices.stream().mapToDouble(ModelPart.Vertex::v).max().orElseThrow();
}
private static float minX(List<Vector3fc> vertices) {
return (float) vertices.stream().mapToDouble(Vector3fc::x).min().orElseThrow();
}
private static float maxX(List<Vector3fc> vertices) {
return (float) vertices.stream().mapToDouble(Vector3fc::x).max().orElseThrow();
}
private static float minY(List<Vector3fc> vertices) {
return (float) vertices.stream().mapToDouble(Vector3fc::y).min().orElseThrow();
}
private static float maxY(List<Vector3fc> vertices) {
return (float) vertices.stream().mapToDouble(Vector3fc::y).max().orElseThrow();
}
private static boolean close(float actual, float expected) {
return Math.abs(actual - expected) <= EPSILON;
}
private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}