Fix Windows expansion journal persistence in alpha.13.1
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-09 12:37:36 +02:00
parent 3569cf1f5f
commit b19e93dd21
10 changed files with 375 additions and 5 deletions
+16
View File
@@ -1,5 +1,21 @@
# Changelog
## 0.1.0-alpha.13.1 — 2026-09-09
- Corrige larrêt des expansions sous Windows : le journal ne tente plus
douvrir son dossier comme un fichier pour le synchroniser. Cette opération
reste exigée sur les systèmes de fichiers utilisant le séparateur Unix.
- Écriture du fichier synchronisée, remplacement atomique et relecture stricte
conservés. Les véritables erreurs de stockage arrêtent toujours les expansions.
- Après mise à jour et réouverture du même monde, une réservation valide déjà
enregistrée avant lerreur est reprise automatiquement. Aucun changement de
schéma du journal, de génération, de paramètres ou de chunks existants.
Le smoke du journal passe sur macOS, avec politique Windows simulée, erreurs
après remplacement et récupération au rechargement. Aucun essai Windows natif
na encore été réalisé. Voir le [dépannage](docs/alpha13-worldgen.md#correctif-alpha131--expansions-sous-windows)
et les [contrôles ciblés](docs/testing.md#correctif-alpha131--journal-dexpansion-sous-windows).
## 0.1.0-alpha.13 — 2026-09-09
- Un seul choix public **Sanctuary**, avec le bouton natif **Personnaliser** :
+12
View File
@@ -20,6 +20,18 @@ initiale et les outils dexpansion dans un seul choix de monde Sanctuary.
Les trois tailles et linterface native sont vérifiées. Lalpha.13 est publiée
et la même instance Prism est synchronisée ; les versions précédentes restent historiques.
## Alpha.13.1 — correctif des expansions sous Windows
Le journal dexpansion nessaie plus douvrir son dossier comme un fichier sous
Windows, ce qui provoquait `AccessDeniedException` et bloquait les expansions.
La synchronisation du fichier, son remplacement atomique et sa relecture restent
obligatoires. Le format du journal et la génération alpha.13 restent identiques.
Après installation du correctif, **rouvrir le même monde** : une réservation déjà
enregistrée est reprise après vérification du journal. Il ne faut pas supprimer
`sanctuary-world-v13` ni recréer le monde pour ce correctif. Voir le
[dépannage Windows](docs/alpha13-worldgen.md) et la [validation](docs/testing.md).
## Alpha.13 — un seul monde Sanctuary
À la création dun **nouveau monde**, sélectionner **Sanctuary**, puis utiliser
+31
View File
@@ -120,6 +120,37 @@ temples, cabanes de marais et camps abandonnés dépendent du biome, du diamètr
et des fondations ; aucune présence nest garantie. Les règles de progression
futures restent décrites dans [Expansion](expansion.md).
## Correctif alpha.13.1 — expansions sous Windows
Lerreur `AccessDeniedException` visant le **dossier**
`data/sanctuary-world-v13`, depuis `ExpansionJournal.persist`, vient de la
synchronisation du répertoire après l’écriture du journal. Java sous Windows
ne permet pas cette ouverture avec `FileChannel` ; ce message ne signifie pas
à lui seul que le joueur manque de droits sur sa sauvegarde.
1. Fermer Minecraft pour arrêter la session dont les expansions sont bloquées.
2. Mettre linstance Prism existante à jour vers **0.1.0-alpha.13.1** via son
canal packwiz, puis relancer le jeu.
3. Rouvrir **le même monde** et consulter `/sanctuary expansion list`.
Si linstance Windows a été importée depuis le MRpack sans auto-update, fermer
Minecraft puis remplacer seulement `sanctuary-0.1.0-alpha.13.jar` par le JAR
`sanctuary-0.1.0-alpha.13.1.jar` de la release dans son dossier `mods`.
Conserver un seul JAR Sanctuary actif ; les autres mods et les sauvegardes
restent dans la même instance. Le raccordement au canal stable est décrit dans
[Distribution](packwiz.md#raccorder-linstance-prism-existante).
Le remplacement du journal ayant précédé cette erreur, lexpansion demandée
peut déjà être enregistrée comme `reserved`. Au chargement, les réservations
valides reprennent automatiquement avec leur identifiant, leur graine et leur
position. Il nest pas nécessaire de recréer le monde ou de supprimer le dossier
du journal. La commande `resume` seule ne débloque pas une session dont le
journal a été déclaré indisponible : sa relecture exige de rouvrir le monde.
Le correctif conserve le format et le contenu du journal existant ; il ne
migre ni ne régénère les chunks. La [validation ciblée](testing.md#correctif-alpha131--journal-dexpansion-sous-windows)
distingue les contrôles locaux de lessai Windows natif restant à effectuer.
## Paramètres et sauvegardes
Le preset public `sanctuary:sanctuary` utilise le nouveau codec de générateur
+25
View File
@@ -509,6 +509,31 @@ réglages suivis conservés.
Les limites, dont labsence de preuve de camp dans lorigine unifiée des graines
inspectées, sont documentées dans [Validation](testing.md).
### WG-16 — Journal dexpansion sous Windows — alpha.13.1
**Branche :** `codex/windows-expansion-journal`.
**Problème :** après le remplacement atomique du journal, louverture de son
répertoire par `FileChannel` échoue sous Windows avec `AccessDeniedException`.
La session arrête alors toute expansion, même si la réservation a déjà été écrite.
- Écarter uniquement la synchronisation de répertoire non prise en charge par
le système de fichiers Windows ; conserver force du fichier, remplacement
atomique, relecture stricte et arrêt sur les véritables erreurs de stockage.
- Conserver le schéma, les identifiants, la géométrie et les journaux existants.
Après mise à jour et rechargement, reprendre les réservations déjà enregistrées
avec le mécanisme existant, sans suppression ni reconstruction de lhistorique.
- Tester le chemin Windows par injection, une panne après remplacement et la
reprise, puis le build et les scénarios moteur dans le développement local.
- Publier le correctif sur le canal stable. La machine Windows signalée nest
pas accessible depuis cet environnement Mac : ne pas prétendre lavoir testée.
**État :** correctif et tests vérifiés en alpha.13.1. `check build assemblePack`
passe, ainsi que le smoke du journal et les scénarios de création/rechargement
dans deux processus Minecraft distincts. Validation Windows par injection sur
macOS ; aucun essai Windows natif. Publication et synchronisation du canal en
cours ; preuves dans [Validation](testing.md).
## Réserve de thèmes futurs
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
+115
View File
@@ -0,0 +1,115 @@
{
"version": "0.1.0-alpha.13.1",
"date": "2026-09-09",
"minecraft": "26.3-pre-2",
"java": 25,
"host_os": "macOS",
"windows_native_tested": false,
"windows_policy_tested_by_injection": true,
"journal_contracts": [
"Windows policy skips only directory channel",
"file force and atomic replacement and strict read-back remain required",
"supported directory AccessDenied propagates after replacement",
"failed session refuses further writes",
"verified reload recovers persisted reservation",
"post-replacement corruption fails closed"
],
"jar_sha256": "b340dfc58f3d2d1a2a375dc527ac6721c61430600622046db26228e0da46061b",
"logs": {
"journal-smoke": {
"path": "build/alpha131-journal-smoke.log",
"sha256": "c02e94713cf3f9a3c76674700cc5c800aecd84b213b2be9ed53629f8eb9cd3d0",
"passed": true,
"required_game_tests": null
},
"check": {
"path": "build/alpha131-check.log",
"sha256": "68f6cb97e2f80669d39e37623076229e030a0d9826c13e9420f51f6ba23885bd",
"passed": true,
"required_game_tests": 8
},
"expansion-create": {
"path": "build/alpha131-expansion-create.log",
"sha256": "61fa4513aef71290a1419c3425a329047c8fe623a561544c6a8d916fe50e226c",
"passed": true,
"required_game_tests": 2
},
"expansion-reload": {
"path": "build/alpha131-expansion-reload.log",
"sha256": "44fafb4636ff90707a068f325a245dbf9820ccb1bdf785c912f7e636fe7cfa43",
"passed": true,
"required_game_tests": 2
}
},
"binary_comparison": {
"base_version": "0.1.0-alpha.13",
"version": "0.1.0-alpha.13.1",
"added": [
"fr/koka/sanctuary/expansion/ExpansionJournal$DirectorySync.class"
],
"removed": [],
"changed": [
"fabric.mod.json",
"fr/koka/sanctuary/expansion/ExpansionJournal$1.class",
"fr/koka/sanctuary/expansion/ExpansionJournal.class"
],
"synthetic_switch_instructions_identical": true,
"all_other_entries_identical": true
},
"expansion": {
"seed": 42,
"initial_diameter": 724,
"generation": 13,
"creation_process": 13148,
"reload_process": 13279,
"creation_passed": true,
"reload_passed": true,
"creation_islands": [
{
"landing_biome": "minecraft:jungle",
"landing": [
0,
202,
-1792
],
"climate": "tropical",
"upper_biome": "minecraft:jungle",
"diameter": 64,
"centre": [
0,
-1792
],
"id": "alpha",
"parent": "sanctuary"
},
{
"landing_biome": "minecraft:desert",
"landing": [
2048,
198,
-1792
],
"climate": "arid",
"upper_biome": "minecraft:desert",
"diameter": 64,
"centre": [
2048,
-1792
],
"id": "beta",
"parent": "alpha"
}
],
"reloaded_regions": 3,
"saved_generator_codec_decoded": true,
"after_reload_expansion": [
0,
1792
],
"final_regions": 4,
"native_structure_status": "not_observed_after_complete_scan",
"native_structure_witness_required": false,
"limitation": "Optional native camp witness absent after complete search on seed 42, as in alpha.13; no new structures claim."
},
"scope": "No generation or journal schema change. Test worlds only; no personal world opened or regenerated. Windows native runtime and power-loss durability not tested."
}
+74
View File
@@ -19,6 +19,80 @@ Pour générer de vrais chunks et vérifier le spawn sur d'autres graines :
La propriété ne s'applique qu'au mod de test, jamais au mod distribué ni à une
sauvegarde de joueur. Chaque commande repart d'un monde de développement neuf.
## Correctif alpha.13.1 — journal dexpansion sous Windows
Le 9 septembre 2026, le contrôle ciblé suivant passe sur macOS et Java 25 :
```sh
./gradlew :sanctuary:expansionJournalSmoke --console=plain
```
Le journal de cette exécution est `build/alpha131-journal-smoke.log` :
`BUILD SUCCESSFUL`, en 7 secondes. Le test écrit et relit de vrais fichiers
temporaires ; il nouvre aucune sauvegarde Minecraft personnelle.
Les contrôles existants de persistance, de corruption, dhistorique immuable
et de compatibilité des racines restent actifs. Trois scénarios couvrent le
correctif :
- La politique Windows, sélectionnée par le séparateur `\`, permet la création
du journal, une réservation, sa finalisation et leurs rechargements. Le
callback de synchronisation du dossier, qui lèverait `AccessDeniedException`,
nest jamais appelé. Les écritures, remplacements atomiques et relectures du
fichier ont réellement lieu sur le système de fichiers du Mac.
- La politique Unix conserve une erreur `AccessDeniedException` injectée après
le remplacement atomique. La session devient indisponible, garde son ancien
état en mémoire et refuse toute nouvelle écriture. Le journal sur disque
contient la réservation ; une nouvelle instance la relit et peut la terminer.
- Une corruption injectée après le remplacement mais avant la relecture
empêche la publication du nouveau plan et conserve le fichier endommagé.
Le rechargement refuse ce journal au lieu de créer un historique vide.
**La branche Windows est simulée ; aucun processus Java ou Minecraft Windows
natif na encore été exécuté pour cette validation.**
`check build assemblePack` passe sur les sources alpha.13.1, en 1 min 27 s,
avec les huit GameTests du monde Moyen de graine 0. Les scénarios dexpansion
passent ensuite dans deux processus Minecraft distincts, sur la graine 42 et
le diamètre 724 : création (1 min 32 s), puis réouverture et nouvelle expansion
(26 s). Deux tests requis passent à chaque étape. Commandes de reproduction :
```sh
./gradlew check build assemblePack -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
./gradlew :sanctuary:runGameTest -PsanctuaryExpansionTests=true -PsanctuaryOptionalStructureWitness=true -PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=42 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
./gradlew :sanctuary:runGameTest -PsanctuaryExpansionTests=true -PsanctuaryExpansionReload=true -PsanctuaryOptionalStructureWitness=true -PsanctuaryTestDiameter=724 -PsanctuaryTestSeed=42 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
```
Lancer les deux scénarios dexpansion consécutivement ; le second réutilise
uniquement leur monde de développement. Comme pour lalpha.13, le témoin de
camp naturel est facultatif et absent après recherche complète sur cette
graine ; aucune nouvelle preuve de structure nest revendiquée.
Les [résultats alpha.13.1](benchmarks/alpha131-journal.json) conservent les
hashes des journaux de test, les identifiants des deux processus et les régions.
La comparaison des JAR confirme que seuls le journal, sa classe de stratégie
et la version changent ; le switch synthétique ne change que ses numéros de
lignes. Toutes les autres classes et ressources restent identiques.
Les résultats des trois tailles et de linterface ci-dessous restent ceux de
la livraison alpha.13 ; ils nont pas été réexécutés pour ce correctif de stockage.
La cause est établie par les sources de
[WindowsChannelFactory dans OpenJDK 25](https://github.com/openjdk/jdk/blob/jdk-25-ga/src/java.base/windows/classes/sun/nio/fs/WindowsChannelFactory.java#L206-L272) :
`FileChannel.open` najoute pas le drapeau `FILE_FLAG_BACKUP_SEMANTICS`, exigé
pour ouvrir un répertoire selon la
[documentation Microsoft de CreateFile](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#directories).
Seule cette synchronisation du dossier est omise sous Windows ; les exceptions
d’écriture, de remplacement ou de relecture restent bloquantes.
Le fichier temporaire est toujours synchronisé par `force(true)`, puis remplacé
atomiquement et vérifié. Cette séquence ne garantit pas sous Windows la même
durabilité du renommage en cas de coupure secteur quune synchronisation du
répertoire sous Unix :
[OpenJDK utilise MoveFileEx sans WRITE_THROUGH](https://github.com/openjdk/jdk/blob/jdk-25-ga/src/java.base/windows/classes/sun/nio/fs/WindowsFileCopy.java#L279-L294),
et le contrat de
[FileChannel.force](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/channels/FileChannel.html#force(boolean))
porte sur les modifications du fichier. Aucun changement de format, de graine,
de générateur ou de réservation enregistrée nest introduit.
## Alpha.13 — génération unifiée
`check build assemblePack` passe, y compris le monde Moyen de graine 0 et les
+2 -2
View File
@@ -8,6 +8,6 @@ loader_version=0.19.5
loom_version=1.17.20
fabric_api_version=0.160.0+26.3
mod_version=0.1.0-alpha.13
pack_version=0.1.0-alpha.13
mod_version=0.1.0-alpha.13.1
pack_version=0.1.0-alpha.13.1
maven_group=fr.koka.sanctuary
@@ -40,6 +40,7 @@ public final class ExpansionJournal {
private final long seed;
private final int rootDiameter;
private final int rootGeneration;
private final DirectorySync directorySync;
private List<ExpansionIsland> islands;
private boolean failed;
@@ -47,10 +48,16 @@ public final class ExpansionJournal {
this(directory, seed, rootDiameter, ExpansionIsland.GENERATION);
}
public ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration) throws IOException {
this(directory, seed, rootDiameter, rootGeneration,
directorySyncFor(directory.getFileSystem().getSeparator(), ExpansionJournal::forceDirectory));
}
ExpansionJournal(Path directory, long seed, int rootDiameter, int rootGeneration, DirectorySync directorySync) throws IOException {
if (rootGeneration != 12 && rootGeneration != 13) throw new IOException("Unsupported root generation");
if (rootDiameter != 512 && rootDiameter != 724 && !(rootGeneration == 13 && rootDiameter == 1024))
throw new IOException("Unsupported expansion root diameter");
this.rootGeneration = rootGeneration;
this.directorySync = java.util.Objects.requireNonNull(directorySync);
this.directory = directory.toAbsolutePath().normalize();
this.file = this.directory.resolve(FILE_NAME);
this.seed = seed;
@@ -110,7 +117,7 @@ public final class ExpansionJournal {
// No fallback to a non-atomic replacement: failure must remain visible to the caller.
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
temporary = null;
try (FileChannel parent = FileChannel.open(directory, StandardOpenOption.READ)) { parent.force(true); }
directorySync.force(directory);
if (!read().equals(next)) throw new IOException("Expansion journal verification disagrees with the committed plan");
} catch (IOException | RuntimeException e) {
failed = true;
@@ -123,6 +130,19 @@ public final class ExpansionJournal {
}
}
@FunctionalInterface
interface DirectorySync { void force(Path directory) throws IOException; }
static DirectorySync directorySyncFor(String separator, DirectorySync supportedSync) {
// Windows FileChannel cannot open directory handles. Only this additional
// directory flush is omitted: file force, atomic replacement and read-back remain mandatory.
return "\\".equals(separator) ? directory -> {} : supportedSync;
}
private static void forceDirectory(Path directory) throws IOException {
try (FileChannel parent = FileChannel.open(directory, StandardOpenOption.READ)) { parent.force(true); }
}
private List<ExpansionIsland> read() throws IOException {
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Existing expansion directory has no regular journal file");
if (Files.size(file) > MAX_BYTES) throw new IOException("Expansion journal exceeds its size limit");
@@ -2,12 +2,14 @@ package fr.koka.sanctuary.expansion;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/** Persistence failures must never turn saved continents into a new empty history. No game world is opened. */
public final class ExpansionJournalSmoke {
@@ -120,10 +122,14 @@ public final class ExpansionJournalSmoke {
afterCrash.commit(List.of(root, north.ready(), onward));
require(new ExpansionJournal(resumeDirectory, SEED, 512).islands().size() == 3, "Resumed region lost its descendant");
windowsDirectoryPolicy(scratch.resolve("windows-policy"));
directoryFailureAfterReplacement(scratch.resolve("directory-failure"));
readbackFailureAfterReplacement(scratch.resolve("readback-failure"));
var tooMany = new ArrayList<ExpansionIsland>(); tooMany.add(root);
for (int i = 0; i < 64; i++) tooMany.add(island("overflow_" + i, "sanctuary", "north", 0, -(i + 1) * 1024));
reject(() -> afterCrash.commit(tooMany), "Region count limit");
System.out.println("Expansion journal smoke passed: atomic persistence, restart, immutable history, corruption, interrupted reservation and unified root identity");
System.out.println("Expansion journal smoke passed: atomic persistence, restart, immutable history, corruption, interrupted reservation, unified root identity, Windows directory policy and post-replacement failures");
} finally {
try (var paths = Files.walk(scratch)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(path);
@@ -131,6 +137,77 @@ public final class ExpansionJournalSmoke {
}
}
private static void windowsDirectoryPolicy(Path directory) throws Exception {
var directoryAttempts = new AtomicInteger();
var windowsSync = ExpansionJournal.directorySyncFor("\\", path -> {
directoryAttempts.incrementAndGet();
throw new AccessDeniedException(path.toString());
});
var root = ExpansionIsland.origin(724, SEED, 13);
var north = island("windows_north", "sanctuary", "north", 0, -1024);
var journal = new ExpansionJournal(directory, SEED, 724, 13, windowsSync);
require(journal.healthy() && journal.islands().equals(List.of(root)),
"Windows policy must bootstrap a verified journal without opening the directory as a file");
journal.commit(List.of(root, north));
var reserved = new ExpansionJournal(directory, SEED, 724, 13, windowsSync);
require(reserved.healthy() && reserved.islands().equals(List.of(root, north)),
"Windows policy must persist and reload the exact reservation");
reserved.commit(List.of(root, north.ready()));
var ready = new ExpansionJournal(directory, SEED, 724, 13, windowsSync);
require(ready.healthy() && ready.islands().equals(List.of(root, north.ready())),
"Windows policy must complete and reload the same reservation");
require(directoryAttempts.get() == 0,
"Windows policy must never attempt the unsupported directory channel, including bootstrap and completion");
}
private static void directoryFailureAfterReplacement(Path directory) throws Exception {
var root = ExpansionIsland.origin(724, SEED, 13);
var north = island("retained_north", "sanctuary", "north", 0, -1024);
new ExpansionJournal(directory, SEED, 724, 13);
Path state = directory.resolve(ExpansionJournal.FILE_NAME);
var attempts = new AtomicInteger();
var denied = new AccessDeniedException(directory.toString());
var requiredSync = ExpansionJournal.directorySyncFor("/", path -> {
attempts.incrementAndGet();
throw denied;
});
var failed = new ExpansionJournal(directory, SEED, 724, 13, requiredSync);
try {
failed.commit(List.of(root, north));
throw new AssertionError("A directory sync failure on a supported filesystem was ignored");
} catch (AccessDeniedException actual) {
require(actual == denied, "The original directory sync error must reach the caller unchanged");
}
require(attempts.get() == 1, "A supported filesystem must attempt directory synchronization");
require(!failed.healthy() && failed.islands().equals(List.of(root)),
"Uncertain persistence must fail closed without publishing the new reservation to world generation");
byte[] reservedBytes = Files.readAllBytes(state);
var recovered = new ExpansionJournal(directory, SEED, 724, 13);
require(recovered.healthy() && recovered.islands().equals(List.of(root, north)),
"A verified reload must recover the reservation already installed before directory sync failed");
reject(() -> failed.commit(List.of(root, north)), "Retrying a failed journal without reload");
require(attempts.get() == 1 && Arrays.equals(reservedBytes, Files.readAllBytes(state)),
"A failed session must neither retry storage operations nor overwrite its retained reservation");
recovered.commit(List.of(root, north.ready()));
require(new ExpansionJournal(directory, SEED, 724, 13).islands().equals(List.of(root, north.ready())),
"A recovered reservation must complete normally without changing its identity");
}
private static void readbackFailureAfterReplacement(Path directory) throws Exception {
var root = ExpansionIsland.origin(724, SEED, 13);
var north = island("readback_north", "sanctuary", "north", 0, -1024);
new ExpansionJournal(directory, SEED, 724, 13);
Path state = directory.resolve(ExpansionJournal.FILE_NAME);
var journal = new ExpansionJournal(directory, SEED, 724, 13,
path -> Files.writeString(state, "damaged after atomic replacement", StandardCharsets.UTF_8));
reject(() -> journal.commit(List.of(root, north)), "Corruption detected by post-replacement verification");
require(!journal.healthy() && journal.islands().equals(List.of(root)),
"Successful replacement alone must not publish an unverified reservation");
require(Files.readString(state).equals("damaged after atomic replacement"),
"A failed readback must preserve evidence instead of silently recreating the journal");
reject(() -> new ExpansionJournal(directory, SEED, 724, 13), "Reloading a journal damaged after replacement");
}
private static ExpansionIsland island(String id, String parent, String direction, int x, int z) {
return new ExpansionIsland(id, parent, direction, ExpansionIsland.directionalClimate(direction), 64, x, z,
ExpansionIsland.deriveSeed(SEED, id), "natural", "reserved", ExpansionIsland.GENERATION);
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "0.1.0-alpha.13"
version = "0.1.0-alpha.13.1"
pack-format = "packwiz:1.1.0"
[index]