Add the default Sanctuary server and pack favicon (beta.295)
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-10-26 06:24:03 +01:00
parent 74dc1265a8
commit 2734c2383a
21 changed files with 413 additions and 18 deletions
+8
View File
@@ -7,6 +7,14 @@ correspondantes. Le module Sanctuary Test n’est pas requis.
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
## beta.295 — Serveur Sanctuary dans la liste multijoueur
[Serveur préenregistré et logo du pack](docs/default-server-beta295.md) :
ajout initial de **Sanctuary Beta** sans écraser les favoris personnels,
avec icône 64 × 64 également distribuée au serveur. Intègre la simplification
de `/sanctuary property`. Build, 18 tests serveur et parcours Vulkan FR/EN
réussis ; état de publication dans le contrat.
## beta.293 — mise à jour complète
[Livraison complète publiée](docs/release-beta293.md) : Happy Creepers de la
+52
View File
@@ -0,0 +1,52 @@
# PACK-03 — Serveur Sanctuary préenregistré et icône
Ticket du **26 octobre 2026**, branche `codex/default-server`, version `beta.295`.
Le premier démarrage du client ajoute **Sanctuary Beta**, adresse publique
`beta.sanctuary-minecraft.net`, dans la liste multijoueur vanilla. Le mécanisme
est livré dans le JAR : il fonctionne après import du ZIP Prism du site,
installation MRpack et mise à jour d'une instance packwiz existante.
`servers.dat` reste un fichier personnel, jamais géré par packwiz. L'ajout
conserve l'ordre, les noms, réglages et champs inconnus des autres favoris.
Un favori Sanctuary existant est conservé sans doublon, y compris avec le nom
DNS en majuscules, un point final ou les ports 25565/25567. Une entrée cachée
issue de connexion directe devient visible sans être renommée. Avant la
première écriture, une copie exacte est conservée dans
`servers.dat.before-sanctuary-beta`. Un fichier illisible reste intact.
Le marqueur local `.sanctuary/default-server-beta` indique que l'ajout initial
est terminé : le joueur peut ensuite renommer, déplacer ou supprimer le favori.
Il n'est pas recréé à chaque lancement. Aucun téléchargement ni connexion de
jeu automatique n'est ajouté.
Le logo 160 × 160 du pack est redimensionné en PNG 64 × 64, sans nouvelle
illustration. Cette même image sert d'icône initiale du favori et de
`server-icon.png` dans le profil serveur packwiz. Le serveur annonce ainsi
l'icône aux clients lors du ping de la liste multijoueur. Le profil client ne
possède jamais `servers.dat` ; le profil serveur possède explicitement cette
icône. Les mondes et réglages de jeu ne changent pas.
La livraison commune intègre le ticket terminé
[CLAIM-03](property-command-beta294.md) : seule la commande anglaise
`/sanctuary property` et ses sous-commandes sont conservées. Les deux fichiers
de production, les deux fichiers de GameTests et le contrat sont repris
exactement depuis sa préparation locale beta.294, après ses 18 tests serveur
réussis, sans modifier son checkout de travail.
## Vérifications
- `defaultServerListSmoke` : 15 contrôles, dont lecture par `ServerData`
vanilla, icône 64 × 64, favoris existants, sauvegarde exacte, suppression,
absence de doublon et fichier corrompu suivi d'une réparation.
- Parcours client **Vulkan FR/EN**, GUI 3 en 1280 × 800 : réussi en 38 s,
hook réel de démarrage, liste vanilla, nom et icône, ping public 0/20.
Deux captures inspectées ; aucun monde ouvert. Journal et captures sous
`build/release295/` et `mods/sanctuary/build/run/clientGameTest/screenshots/`.
- `./gradlew check build assemblePack assembleServerPack` : réussi en
**3 min 22 s**, avec les contrôles purs et **18 tests serveur ciblés**
`property270,claims272,claims277,claims278`, `sanctuaryAtlasOnly=true`.
Les tests vérifient à nouveau la commande unifiée sur la base commune.
Journal : `build/release295/check-build.log`.
- Publication Gitea, canal, CT 350, téléchargement Web et Prism : à consigner
après livraison ; cette préparation ne prouve pas leur mise à jour.
+17
View File
@@ -0,0 +1,17 @@
# Sanctuary beta.295
Le serveur **Sanctuary Beta** est enregistré automatiquement au premier
lancement du client, avec le logo du modpack. Il utilise l'adresse publique
`beta.sanctuary-minecraft.net`. Les autres favoris restent conservés ; vous
pouvez ensuite renommer, déplacer ou supprimer cette entrée.
Le serveur annonce aussi le logo Sanctuary dans la liste multijoueur.
Le même parcours s'applique au ZIP Prism du site, au MRpack et aux instances
existantes qui suivent le canal packwiz.
La commande de propriété est unifiée : **`/sanctuary property`**, avec
`claim`, `protect` et `duel`. Les alias `propriete` et `propriété` sont retirés.
Les règles, prix et protections des claims restent identiques.
Les sauvegardes et paramètres de génération sont conservés ; aucun nouveau
monde n'est nécessaire.
+5
View File
@@ -2194,6 +2194,11 @@ déjà branchée sur le canal stable n'a pas besoin d'être réimportée.
## Données locales et mondes
Depuis beta.295, le premier lancement du client ajoute le serveur Sanctuary
dans les favoris sans remplacer leur liste. Un marqueur local respecte ensuite
les suppressions volontaires ; le profil serveur distribue le logo 64 × 64.
Voir [le contrat](default-server-beta295.md).
Le pack livré possède uniquement les fichiers déclarés dans son index. Il ne
déclare ni `saves/`, ni `options.txt`, `servers.dat`, `screenshots/`, ni les packs
graphiques personnels. L'installation de packwiz et une mise à jour des mods
+60
View File
@@ -0,0 +1,60 @@
# CLAIM-03 — Une seule commande de propriété
Ticket local du **26 octobre 2026**, branche `codex/property-command`,
préparation locale `beta.294`.
## Demande et résultat attendu
Les trois variantes de propriété encombrent les commandes proposées au joueur.
Conserver uniquement le nom anglais `/sanctuary property`, cohérent avec les
autres commandes. Retirer les alias `propriete` et `propriété`, y compris pour
les duels, afin qu'aucun enregistrement secondaire ne les réintroduise.
Commandes conservées :
- `/sanctuary property` : ouvrir la carte de propriété ;
- `/sanctuary property claim` : revendiquer le chunk courant ;
- `/sanctuary property protect` : acheter sa protection ;
- `/sanctuary property duel <id>` : consulter le duel proposé.
Ce ticket remplace uniquement les alias de commande introduits en beta.270.
Les règles de revendication, prix, protections, interfaces FR/EN et sauvegardes
restent identiques. Aucun contrat de migration n'est nécessaire.
## Critères d'acceptation
- La complétion de `/sanctuary prop` propose uniquement `property`.
- Les deux alias retirés sont absents de l'arbre serveur et sont refusés,
seuls comme avec `claim`, `protect` ou `duel`.
- La branche anglaise et ses trois sous-commandes restent accessibles sans
permission opérateur. Revendication et protection fonctionnent via le
répartiteur réel des commandes.
- Les tests de propriétés et de duels passent ; le mod et le pack portent
ensemble `beta.294`.
## Vérifications
`check build assemblePack` réussit en **4 min 4 s**, sur Minecraft 26.3,
Fabric 0.19.5 et Java 25. Les **18 GameTests serveur ciblés** passent :
propriétés historiques, claims par chunks, priorité Demeure et duels Battle.
Le contrôle de commande vérifie la complétion unique, les anciens alias refusés
et les sous-commandes conservées. Le scénario natif de minage revendique et
protège désormais son terrain via les vraies commandes `property claim` et
`property protect`, en contrôlant le propriétaire et les huit saphirs débités.
Les **63 contrôles du registre de claims**, les **26 contrôles du journal de
duels** et les autres vérifications de `check` passent également.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=property270,claims272,claims277,claims278 \
-PsanctuaryAtlasOnly=true
```
Journal ignoré : `build/property294-check-build-pack.log`.
JAR local : `mods/sanctuary/build/libs/sanctuary-beta.294.jar`.
Les packs client et serveur sont assemblés et vérifiés dans `build/packwiz`
et `build/packwiz-server`. La validation porte sur l'arbre serveur et les
commandes exécutées ; aucun essai graphique n'est revendiqué pour ce ticket.
Préparation locale uniquement ; aucune publication ni synchronisation d'une
installation de jeu ou du serveur Beta demandée pour ce ticket.
+4
View File
@@ -56,6 +56,10 @@ avant de rendre le verrou de mise à jour.
L'API d'accueil et la communauté sont deux services existants distincts.
Le pack ne contient ni secret ni configuration de production.
Depuis beta.295, le profil serveur gère aussi `server-icon.png`, le logo du
pack en 64 × 64. La synchronisation le pose à la racine ; Minecraft l'annonce
aux clients au démarrage suivant. Le favori client est ajouté séparément au
premier lancement, sans indexer le fichier personnel `servers.dat`.
Pour l'accès/candidature, fournir dans l'environnement du service Minecraft :
+6
View File
@@ -69,6 +69,12 @@ les mods. [Contrat et maintien de l'outil](packwiz-logo-beta291.md). L'option
`--bootstrap-no-update` conserve cet installateur vérifié ; elle ne désactive
pas les mises à jour du pack. Ne pas ajouter de réglages ou sauvegardes personnels.
Depuis beta.295, le JAR client initialise une seule fois le favori Sanctuary,
pour les nouvelles installations comme pour les instances déjà reliées au
canal. `servers.dat` reste personnel et hors index. Le profil serveur distribue
en revanche `server-icon.png` (logo 64 × 64) ; vérifier son hash et l'icône
annoncée par le ping après redémarrage. [Contrat](default-server-beta295.md).
## Publier les artefacts et le canal
Committer et pousser la branche source vérifiée, puis exécuter depuis cette
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.293
pack_version=beta.293
mod_version=beta.295
pack_version=beta.295
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.4
+10
View File
@@ -152,6 +152,9 @@ tasks.named('runGameTest') {
}
tasks.named('processGametestResources') {
def defaultServer295 = providers.gradleProperty('sanctuaryDefaultServer295ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryDefaultServer295ClientTests', defaultServer295)
if (defaultServer295) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.DefaultServer295ClientChecks') }
def happyCreeper = providers.gradleProperty('sanctuaryHappyCreeperClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryHappyCreeperClientTests', happyCreeper)
if (happyCreeper) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.HappyCreeperClientChecks') }
@@ -1605,3 +1608,10 @@ tasks.register('claimDuel278Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.property.ClaimDuel278Smoke'
}
tasks.named('check') { dependsOn('claimDuel278Smoke') }
tasks.register('defaultServerListSmoke', JavaExec) {
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.client.DefaultServerListSmoke'
}
tasks.named('check') { dependsOn('defaultServerListSmoke') }
@@ -0,0 +1,42 @@
package fr.koka.sanctuary.client;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
/** Exercises the real startup hook and vanilla multiplayer list without opening a world. */
public final class DefaultServer295ClientChecks implements FabricClientGameTest {
@Override public void runTest(ClientGameTestContext c) {
c.runOnClient(m -> {
if (!com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo().backendName().equalsIgnoreCase("vulkan"))
throw new AssertionError("Vulkan required");
if (!java.nio.file.Files.exists(m.gameDirectory.toPath().resolve(".sanctuary/default-server-beta")))
throw new AssertionError("Client startup hook did not initialize the default");
});
for (String locale : new String[]{"fr_fr", "en_us"}) {
c.runOnClient(m -> {
m.getWindow().setWindowed(1280, 800);
m.options.guiScale().set(3); m.resizeGui();
m.getLanguageManager().setSelected(locale);
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
m.gui.setScreen(new JoinMultiplayerScreen(new TitleScreen()));
var list = ((JoinMultiplayerScreen) m.gui.screen()).getServers();
int count = 0;
for (int i = 0; i < list.size(); i++) {
var server = list.get(i);
if (server.ip.equals(DefaultServerList.ADDRESS)) {
count++;
if (!server.name.equals("Sanctuary Beta") || server.getIconBytes() == null)
throw new AssertionError("Missing default name or cached icon");
}
}
if (count != 1) throw new AssertionError("Expected one Sanctuary default, got " + count);
});
c.waitTicks(30);
c.takeScreenshot("default-server295-" + locale);
c.runOnClient(m -> m.gui.setScreen(new TitleScreen()));
}
System.out.println("DEFAULT_SERVER295_VULKAN_PASS: startup, vanilla list and logo FR/EN; no world opened");
}
}
@@ -56,10 +56,11 @@ public final class ChunkClaims272GameTests {
}
@GameTest public void nativeDurationUsesActualChunkAndKeepsInteractionsFree(GameTestHelper h)throws Exception{
var owner=admitted(h,340,340);var other=admitted(h,340,340);var s=ChunkPropertyService.store(owner.level().getServer());
h.assertTrue(act(owner,"claim",340,340).equals("claimed"),"Reserve chunk");
var dispatcher=owner.level().getServer().getCommands().getDispatcher();var source=owner.createCommandSourceStack();
h.assertTrue(dispatcher.execute("sanctuary property claim",source)==1&&s.get(new ChunkClaims.Key(340,340)).owner().equals(owner.getUUID()),"English command reserves the current chunk");
var pos=new BlockPos(340*16+15,80,340*16+2);var state=Blocks.STONE.defaultBlockState();owner.level().setBlockAndUpdate(pos,state);
float ownBase=state.getDestroyProgress(owner,owner.level(),pos),otherBase=state.getDestroyProgress(other,other.level(),pos);
owner.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),64));h.assertTrue(act(owner,"protect",340,340).equals("protected"),"Activate protection");
owner.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),64));h.assertTrue(dispatcher.execute("sanctuary property protect",source)==1&&ChunkPropertyService.sapphires(owner)==56,"English command activates protection for eight sapphires");
h.assertTrue(state.getDestroyProgress(owner,owner.level(),pos)==ownBase*2&&state.getDestroyProgress(other,other.level(),pos)==otherBase*.5f,"Real native mining duration hook multiplies once");
h.assertTrue(state.getDestroyProgress(other,other.level(),pos.east())==otherBase,"Adjacent chunk has no slowdown");
h.assertTrue(ChunkPropertyService.multiplier(owner,owner.level(),pos.above(100))==2&&ChunkPropertyService.multiplier(other,other.level(),pos.below(100))==.5f,"Chunk column covers all build heights");
@@ -66,7 +66,19 @@ public final class Property270GameTests {
PropertyService.recover(p);h.assertTrue(FamiliarExperience.playerXp(p)==250,"Repeated recovery does not debit again");
var reloaded=new PropertyStore(server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("sanctuary/properties-v1.json"),server.overworld().getSeed());
h.assertTrue(reloaded.get(p.getUUID()).equals(store.get(p.getUUID())),"Committed property survives disk reload");
for(String alias:java.util.List.of("property","propriete","propriété"))h.assertTrue(server.getCommands().getDispatcher().getRoot().getChild("sanctuary").getChild(alias).canUse(p.createCommandSourceStack()),"Property commands need no operator");
var dispatcher=server.getCommands().getDispatcher();var source=p.createCommandSourceStack();
var root=dispatcher.getRoot().getChild("sanctuary");var property=root.getChild("property");
h.assertTrue(property!=null&&property.canUse(source)&&property.getCommand()!=null,"English property command opens the menu without operator permissions");
for(String action:java.util.List.of("claim","protect","duel"))h.assertTrue(property.getChild(action)!=null&&property.getChild(action).canUse(source),"English property command retains "+action);
var suggestions=dispatcher.getCompletionSuggestions(dispatcher.parse("sanctuary prop",source)).join().getList().stream().map(s->s.getText()).toList();
h.assertTrue(suggestions.equals(java.util.List.of("property")),"Property completion offers only the English command");
for(String alias:java.util.List.of("propriete","propriété")){
h.assertTrue(root.getChild(alias)==null,"Removed property alias is absent: "+alias);
for(String suffix:java.util.List.of(""," claim"," protect"," duel 00000000-0000-0000-0000-000000000000")){
boolean rejected=false;try{dispatcher.execute("sanctuary "+alias+suffix,source);}catch(com.mojang.brigadier.exceptions.CommandSyntaxException expected){rejected=true;}
h.assertTrue(rejected,"Removed property alias cannot execute: "+alias+suffix);
}
}
h.succeed();
}
@GameTest public void spawnDimensionAndHeightCannotBeClaimed(GameTestHelper h)throws Exception{
@@ -0,0 +1,78 @@
package fr.koka.sanctuary.client;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Base64;
import java.util.Locale;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtIo;
/** One-time client default; servers.dat remains personal and outside the pack index. */
public final class DefaultServerList {
public static final String ADDRESS = "beta.sanctuary-minecraft.net";
public static final String ICON = "/assets/sanctuary/server-icon.png";
private DefaultServerList() {}
public static void install(Path gameDirectory) throws IOException {
Path marker = gameDirectory.resolve(".sanctuary/default-server-beta");
if (Files.exists(marker)) return;
Path file = gameDirectory.resolve("servers.dat");
CompoundTag root = NbtIo.read(file);
if (root == null) root = new CompoundTag();
if (root.contains("servers") && !(root.get("servers") instanceof ListTag)) {
throw new IOException("Unexpected server list format; preserving servers.dat");
}
ListTag servers = root.getListOrEmpty("servers");
CompoundTag existing = null;
for (var tag : servers) {
if (!(tag instanceof CompoundTag server)) {
throw new IOException("Unexpected server entry; preserving servers.dat");
}
if (isSanctuary(server.getStringOr("ip", ""))) existing = server;
}
boolean changed = existing == null || existing.getBooleanOr("hidden", false);
if (existing == null) {
CompoundTag server = new CompoundTag();
server.putString("name", "Sanctuary Beta");
server.putString("ip", ADDRESS);
try (var icon = DefaultServerList.class.getResourceAsStream(ICON)) {
if (icon == null) throw new IOException("Missing Sanctuary server icon");
server.putString("icon", Base64.getEncoder().encodeToString(icon.readAllBytes()));
}
servers.add(server);
} else if (changed) {
existing.putBoolean("hidden", false);
}
if (changed) {
root.put("servers", servers);
Path backup = gameDirectory.resolve("servers.dat.before-sanctuary-beta");
if (Files.exists(file) && !Files.exists(backup)) Files.copy(file, backup);
Path temporary = Files.createTempFile(gameDirectory, "sanctuary-servers-", ".dat");
try {
NbtIo.write(root, temporary);
try {
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException ignored) {
Files.move(temporary, file, StandardCopyOption.REPLACE_EXISTING);
}
} finally {
Files.deleteIfExists(temporary);
}
}
Files.createDirectories(marker.getParent());
Files.writeString(marker, ADDRESS + "\n");
}
private static boolean isSanctuary(String address) {
String normalized = address.strip().toLowerCase(Locale.ROOT);
int separator = normalized.indexOf(':');
String host = separator < 0 ? normalized : normalized.substring(0, separator);
String port = separator < 0 ? "" : normalized.substring(separator);
if (host.endsWith(".")) host = host.substring(0, host.length() - 1);
return host.equals(ADDRESS) && (port.isEmpty() || port.equals(":25565") || port.equals(":25567"));
}
}
@@ -17,6 +17,13 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
}
public static void registerClient() {
net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
try {
DefaultServerList.install(client.gameDirectory.toPath());
} catch (java.io.IOException | RuntimeException error) {
org.slf4j.LoggerFactory.getLogger("Sanctuary").warn("Could not add the default server; existing favorites preserved", error);
}
});
InsectRenderer.register();
net.fabricmc.fabric.api.client.rendering.v1.ModelLayerRegistry.registerModelLayer(FlyingWhaleModel.LAYER_LOCATION, FlyingWhaleModel::createBodyLayer);
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(fr.koka.sanctuary.whale.FlyingWhales.TYPE, FlyingWhaleRenderer::new);
@@ -68,12 +68,11 @@ public final class ChunkPropertyService {
view(p,key,result,false);
}));
CommandRegistrationCallback.EVENT.register((dispatcher,access,env)->{
var root=Commands.literal("sanctuary");for(String alias:List.of("property","propriete","propriété")){
var branch=Commands.literal(alias).executes(c->{open(c.getSource().getPlayerOrException());return 1;});
for(String action:List.of("claim","protect"))branch.then(Commands.literal(action).executes(c->{
var p=c.getSource().getPlayerOrException();var s=store(p.level().getServer());var result=act(p,action,at(p),s==null?-1:s.revision());p.sendSystemMessage(label(result));view(p,at(p),result,false);return result.equals("claimed")||result.equals("protected")?1:0;
}));root.then(branch);
}dispatcher.register(root);
var branch=Commands.literal("property").executes(c->{open(c.getSource().getPlayerOrException());return 1;});
for(String action:List.of("claim","protect"))branch.then(Commands.literal(action).executes(c->{
var p=c.getSource().getPlayerOrException();var s=store(p.level().getServer());var result=act(p,action,at(p),s==null?-1:s.revision());p.sendSystemMessage(label(result));view(p,at(p),result,false);return result.equals("claimed")||result.equals("protected")?1:0;
}));
dispatcher.register(Commands.literal("sanctuary").then(branch));
});
ClaimDuels.register();
}
@@ -33,7 +33,7 @@ public final class ClaimDuelNetwork {
ServerPlayConnectionEvents.DISCONNECT.register((h,s)->{WATCHERS.remove(h.player);REQUESTS.remove(h.player);});
CommandRegistrationCallback.EVENT.register((dispatcher,access,env)->{
var root=Commands.literal("sanctuary");
for(String alias:List.of("property","propriete","propriété"))root.then(Commands.literal(alias).then(Commands.literal("duel")
root.then(Commands.literal("property").then(Commands.literal("duel")
.then(Commands.argument("id",com.mojang.brigadier.arguments.StringArgumentType.word()).executes(c->{
var p=c.getSource().getPlayerOrException();UUID id;try{id=UUID.fromString(com.mojang.brigadier.arguments.StringArgumentType.getString(c,"id"));}catch(IllegalArgumentException e){return 0;}
var o=ClaimDuels.get(p.level().getServer(),id);if(o==null||!o.participant(p.getUUID()))return 0;
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

@@ -0,0 +1,86 @@
package fr.koka.sanctuary.client;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtIo;
public final class DefaultServerListSmoke {
private static int checks;
private static void check(boolean condition, String message) {
checks++;
if (!condition) throw new AssertionError(message);
}
public static void main(String[] args) throws Exception {
net.minecraft.SharedConstants.tryDetectVersion();
Path root = Files.createTempDirectory("sanctuary-server-default-");
try {
Path fresh = Files.createDirectory(root.resolve("fresh"));
DefaultServerList.install(fresh);
CompoundTag server = NbtIo.read(fresh.resolve("servers.dat")).getListOrEmpty("servers").getCompoundOrEmpty(0);
check(server.getStringOr("name", "").equals("Sanctuary Beta"), "Default display name");
check(server.getStringOr("ip", "").equals(DefaultServerList.ADDRESS), "Public SRV address");
byte[] icon = Base64.getDecoder().decode(server.getStringOr("icon", ""));
var image = javax.imageio.ImageIO.read(new java.io.ByteArrayInputStream(icon));
check(image.getWidth() == 64 && image.getHeight() == 64, "Minecraft favicon dimensions");
check(Arrays.equals(net.minecraft.client.multiplayer.ServerData.read(server).getIconBytes(), icon), "Vanilla reads the cached icon");
byte[] first = Files.readAllBytes(fresh.resolve("servers.dat"));
DefaultServerList.install(fresh);
check(Arrays.equals(first, Files.readAllBytes(fresh.resolve("servers.dat"))), "Second launch leaves favorites byte-identical");
CompoundTag empty = new CompoundTag(); empty.put("servers", new ListTag());
NbtIo.write(empty, fresh.resolve("servers.dat"));
DefaultServerList.install(fresh);
check(NbtIo.read(fresh.resolve("servers.dat")).equals(empty), "User may remove the default permanently");
Path personal = Files.createDirectory(root.resolve("personal"));
CompoundTag other = new CompoundTag(); other.putString("name", "Mes amis"); other.putString("ip", "friends.example:25568");
other.putBoolean("acceptTextures", true); other.putString("custom", "preserve me");
CompoundTag original = new CompoundTag(); original.putString("extra", "preserve root fields");
ListTag entries = new ListTag(); entries.add(other); original.put("servers", entries);
NbtIo.write(original, personal.resolve("servers.dat"));
byte[] originalBytes = Files.readAllBytes(personal.resolve("servers.dat"));
DefaultServerList.install(personal);
CompoundTag merged = NbtIo.read(personal.resolve("servers.dat"));
check(merged.getListOrEmpty("servers").size() == 2, "Default appends to personal list");
check(merged.getListOrEmpty("servers").getCompoundOrEmpty(0).equals(other)
&& merged.getStringOr("extra", "").equals("preserve root fields"), "Preserve order, settings and unknown fields");
check(Arrays.equals(originalBytes, Files.readAllBytes(personal.resolve("servers.dat.before-sanctuary-beta"))), "Original favorites backed up exactly");
for (String address : new String[]{DefaultServerList.ADDRESS, "BETA.SANCTUARY-MINECRAFT.NET.:25565", DefaultServerList.ADDRESS + ":25567"}) {
Path existing = Files.createTempDirectory(root, "existing-");
CompoundTag favorite = new CompoundTag(); favorite.putString("name", "Mon Sanctuary"); favorite.putString("ip", address);
ListTag list = new ListTag(); list.add(favorite); CompoundTag nbt = new CompoundTag(); nbt.put("servers", list);
NbtIo.write(nbt, existing.resolve("servers.dat"));
byte[] before = Files.readAllBytes(existing.resolve("servers.dat"));
DefaultServerList.install(existing);
check(Arrays.equals(before, Files.readAllBytes(existing.resolve("servers.dat"))), "Existing favorite preserved without duplicate: " + address);
}
Path hidden = Files.createDirectory(root.resolve("hidden"));
CompoundTag favorite = server.copy(); favorite.putBoolean("hidden", true); favorite.putString("name", "My Beta");
ListTag list = new ListTag(); list.add(favorite); CompoundTag nbt = new CompoundTag(); nbt.put("servers", list);
NbtIo.write(nbt, hidden.resolve("servers.dat"));
DefaultServerList.install(hidden);
ListTag shown = NbtIo.read(hidden.resolve("servers.dat")).getListOrEmpty("servers");
check(shown.size() == 1 && !shown.getCompoundOrEmpty(0).getBooleanOr("hidden", false)
&& shown.getCompoundOrEmpty(0).getStringOr("name", "").equals("My Beta"), "Promote direct-connect history without duplicate or rename");
Path corrupt = Files.createDirectory(root.resolve("corrupt"));
byte[] broken = new byte[]{10, 0, 0, 9}; Files.write(corrupt.resolve("servers.dat"), broken);
try { DefaultServerList.install(corrupt); throw new AssertionError("Corrupt file accepted"); }
catch (java.io.IOException | RuntimeException expected) { }
check(Arrays.equals(broken, Files.readAllBytes(corrupt.resolve("servers.dat")))
&& !Files.exists(corrupt.resolve(".sanctuary/default-server-beta")), "Corrupt favorites preserved; retry allowed");
NbtIo.write(empty, corrupt.resolve("servers.dat"));
DefaultServerList.install(corrupt);
check(NbtIo.read(corrupt.resolve("servers.dat")).getListOrEmpty("servers").size() == 1, "Retry after user repairs file");
System.out.println("DefaultServerList: " + checks + " checks passed");
} finally {
try (var files = Files.walk(root)) {
for (Path path : files.sorted(java.util.Comparator.reverseOrder()).toList()) Files.delete(path);
}
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.293"
version = "beta.295"
pack-format = "packwiz:1.1.0"
[index]
+10 -2
View File
@@ -198,6 +198,9 @@ def assemble(jar_url=None):
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(destination / entry['file'], target)
server_entries.append(entry)
icon = ROOT / 'mods/sanctuary/src/main/resources/assets/sanctuary/server-icon.png'
shutil.copy2(icon, server / 'server-icon.png')
server_entries.append({'file': 'server-icon.png', 'hash': sha256(icon)})
write_index(server, server_entries, name='Sanctuary Server')
verify_server(server)
print('Client pack staged at ' + str(destination))
@@ -230,8 +233,13 @@ def verify_server(base):
files = {entry['file'] for entry in entries}
sanctuary = 'mods/sanctuary.pw.toml' if 'mods/sanctuary.pw.toml' in files else 'mods/sanctuary-' + values['mod_version'] + '.jar'
require(files == {'mods/fabric-api.pw.toml', sanctuary, 'mods/lithium.pw.toml',
'mods/simple-voice-chat.pw.toml'},
'Server pack must contain Fabric API, Sanctuary, Lithium and Simple Voice Chat')
'mods/simple-voice-chat.pw.toml', 'server-icon.png'},
'Server pack must contain the shared mods and the Sanctuary server icon')
icon = (base / 'server-icon.png').read_bytes()
require(icon == (ROOT / 'mods/sanctuary/src/main/resources/assets/sanctuary/server-icon.png').read_bytes()
and icon[:8] == b'\x89PNG\r\n\x1a\n'
and int.from_bytes(icon[16:20], 'big') == 64
and int.from_bytes(icon[20:24], 'big') == 64, 'Server icon must be the verified 64x64 PNG')
for entry in entries:
if entry.get('metafile'):
require(read_toml(base / entry['file'])['side'] == 'both', 'Client mod in server pack')
+3 -3
View File
@@ -116,9 +116,9 @@ def remote_tag_commit(tag):
def channel_file(relative):
"""The indexed pack icon is the only non-TOML file allowed on the channel."""
"""Only generated metadata and the two verified pack/server icons are allowed."""
path = Path(relative)
return path.suffix == '.toml' or path.as_posix() == 'icon.png'
return path.suffix == '.toml' or path.as_posix() in ('icon.png', 'server/server-icon.png')
def publish_channel(origin, version, source_head):
@@ -130,7 +130,7 @@ def publish_channel(origin, version, source_head):
'Client and server must publish the same Sanctuary artifact')
files = [path for path in staged.rglob('*') if path.is_file()]
pack.require(all(channel_file(path.relative_to(staged)) for path in files),
'Distribution branch must contain only generated TOML metadata and the pack icon')
'Distribution branch must contain only generated TOML metadata and verified icons')
if (staged / 'icon.png').exists():
pack.require((staged / 'icon.png').read_bytes() == (ROOT / 'packwiz/icon.png').read_bytes(),
'Distribution icon differs from the verified source icon')