Add the default Sanctuary server and pack favicon (beta.295)
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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 :
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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') }
|
||||
|
||||
+42
@@ -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");
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -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");
|
||||
|
||||
+13
-1
@@ -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
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.293"
|
||||
version = "beta.295"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
+10
-2
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user