Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f816ffc640 | ||
|
|
f30a350240 |
@@ -1,5 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0-alpha.11 — 2026-09-09
|
||||
|
||||
- Deux choix à la création : **Petit** (512 blocs de diamètre nominal)
|
||||
et **Grand** (environ 724 blocs), Grand par défaut. Leur aire nominale varie
|
||||
d’un facteur deux, sur une hauteur de 384 blocs. Les repères internes 5/10
|
||||
ne désignent pas un nombre garanti de joueurs.
|
||||
- Nouveau réglage `sanctuary:population_10` pour Grand. Les anciens formats
|
||||
20/100 disparaissent des choix publics, mais restent chargeables avec leurs
|
||||
identifiants et leur relief enregistrés. Aucune île existante n’est réduite.
|
||||
- Cache des coins d’interpolation conservant l’arithmétique exacte du moteur,
|
||||
borné à 192 Kio par thread. Les plans régionaux déjà calculés restent lisibles
|
||||
pendant le calcul d’une nouvelle région ; le sampler est compilé une seule
|
||||
fois par plan.
|
||||
- À géométrie 20 identique, création et 289 chunks : 407,43 → 62,54 s
|
||||
(−84,65 %). Exploration de 80 chunks : 103,14 → 14,05 s. Le nouveau Grand
|
||||
atteint 62,67 s sur le protocole de départ. Une seule mesure par version,
|
||||
graine 0, M1/8 Gio : ce sont des temps serveur jusqu’à FULL, pas des FPS
|
||||
ni une garantie de délai en jeu.
|
||||
|
||||
Validation : `check build assemblePack`, douze tests moteur pour Petit et
|
||||
Grand, 1 800 ticks de fluides, persistance des quatre références et
|
||||
3 183 804 comparaisons bit à bit de densités réelles, plus 122 528 synthétiques.
|
||||
Les sources historiques de forme et les anciens réglages sont conservés.
|
||||
Voir [Validation](docs/testing.md) et [Distribution](docs/packwiz.md).
|
||||
Le canal stable est publié et la même instance Prism est synchronisée deux fois,
|
||||
avec les 364 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## 0.1.0-alpha.10 — 2026-09-09
|
||||
|
||||
- Trois tailles de départ : **Sanctuary (20 joueurs)** par défaut, plus les
|
||||
|
||||
@@ -86,9 +86,9 @@ anciennes sauvegardes alpha.5. Aucun lac, groupe de terrasses, geyser ou stock
|
||||
de ressources n’est garanti sur chaque graine. Voir le ticket local
|
||||
[WG-10](docs/backlog.md).
|
||||
|
||||
## Alpha.10 : îles pour 5, 20 ou 100 joueurs
|
||||
## Alpha.10, historique : îles pour 5, 20 ou 100 joueurs
|
||||
|
||||
Trois tailles de départ sont disponibles, avec **20 joueurs par défaut**.
|
||||
L’alpha.10 proposait trois tailles, avec **20 joueurs par défaut**.
|
||||
Leur aire nominale est proportionnelle à la capacité choisie, sur la base
|
||||
de l’île de 512 blocs pour cinq joueurs. La hauteur reste de 384 blocs.
|
||||
|
||||
@@ -128,6 +128,37 @@ en jeu reste à construire. Voir [Expansion et ressources](docs/expansion.md).
|
||||
Les détails du terrain et de ses limites sont dans [Génération](docs/worldgen.md),
|
||||
et la provenance dans [Audit 26.2](docs/migration-26.2.md).
|
||||
|
||||
## Alpha.11 : Petit, Grand et génération plus rapide
|
||||
|
||||
Les anciens formats sont trop grands et éloignent les constructions. Le
|
||||
nouveau choix de départ se limite à **Petit** et **Grand**, avec **Grand par
|
||||
défaut**. Petit garde un diamètre nominal de 512 blocs ; Grand mesure environ
|
||||
724 blocs, soit deux fois l’aire nominale de Petit. La hauteur reste de
|
||||
384 blocs. Les repères internes 5 et 10 servent au calcul de surface : ils
|
||||
ne sont ni les noms des choix ni une promesse de capacité multijoueur.
|
||||
|
||||
Grand utilise le nouveau réglage `sanctuary:population_10`. Les anciennes
|
||||
parties `population_5`, `population_20` et `population_100` gardent leurs
|
||||
paramètres ; les formats 20/100 sont retirés des choix publics de création,
|
||||
sans réduire les mondes déjà enregistrés. Aucun monde personnel n’est ouvert
|
||||
pour les essais.
|
||||
|
||||
Le ticket [WG-12](docs/backlog.md) réduit les calculs répétés de terrain et
|
||||
permet de lire un plan régional déjà prêt pendant le calcul du suivant.
|
||||
À géométrie 20 identique, la création puis 289 chunks autour du spawn passent
|
||||
de 407,43 à 62,54 secondes (−84,65 %) ; 80 chunks d’exploration passent de
|
||||
103,14 à 14,05 secondes. Le nouveau Grand atteint 62,67 secondes sur le même
|
||||
protocole de départ. Ces essais serveur sur une graine et une machine ne
|
||||
mesurent pas le rendu du client ni une charge multijoueur.
|
||||
|
||||
`check build assemblePack` passe, ainsi que douze tests moteur pour chacun
|
||||
des deux choix, avec 1 800 ticks de fluides. Plus de trois millions de
|
||||
comparaisons de densité confirment l’équivalence de calcul. Les mesures et
|
||||
limites sont dans [Validation](docs/testing.md) ; publication et synchronisation
|
||||
de la même instance sont consignées dans [Distribution](docs/packwiz.md).
|
||||
L’alpha.11 est publiée ; la même instance Prism a été synchronisée deux fois,
|
||||
avec les 364 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## Versions et prérequis
|
||||
|
||||
Au 8 septembre 2026, la cible disponible est **26.3-pre-2**. Le dépôt ne prétend
|
||||
@@ -142,7 +173,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.10 |
|
||||
| Sanctuary / pack | 0.1.0-alpha.11 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
|
||||
@@ -16,6 +16,8 @@ WG-09 est livré en alpha.8, avec mesures moteur et mise à jour Prism vérifié
|
||||
WG-10 est livré en alpha.9, avec mesures moteur et synchronisation Prism vérifiées.
|
||||
WG-11 est livré en alpha.10 : tailles 5/20/100 validées sur la graine 0,
|
||||
release publiée et même instance Prism synchronisée.
|
||||
WG-12 est livré en alpha.11 : Petit/Grand, génération accélérée et même instance
|
||||
Prism synchronisée deux fois ; voir [Distribution](packwiz.md).
|
||||
|
||||
Résultat visé : un client et un serveur Fabric compatibles peuvent ouvrir un monde Sanctuary, plusieurs joueurs y arrivent sur une même île sûre et les chunks extérieurs restent vides. La génération est reproductible et ses limites sont documentées.
|
||||
|
||||
@@ -359,6 +361,64 @@ Les anciens mondes restent inchangés. Les observations ne couvrent pas toute
|
||||
l’île de 100 joueurs, et le coût initial d’un plan régional reste notable.
|
||||
Ce ticket local n’est pas une issue distante publiée.
|
||||
|
||||
### WG-12 — Petit, Grand et génération plus rapide — alpha.11 livrée
|
||||
|
||||
**Branche :** `codex/worldgen-performance`.
|
||||
|
||||
**Problème :** les grands formats éloignent trop les constructions. Les essais
|
||||
signalent aussi plusieurs minutes de chargement puis des chunks difficiles à
|
||||
utiliser. Le ticket rapproche les deux choix de départ et optimise les calculs,
|
||||
avec des preuves distinctes pour la taille et la vitesse.
|
||||
|
||||
**Critères d’acceptation :**
|
||||
|
||||
- Proposer seulement **Petit** et **Grand** dans la création, avec **Grand par
|
||||
défaut**, et libellés FR/EN. Petit garde le diamètre nominal de 512 blocs ;
|
||||
Grand vise environ 724 blocs, soit une aire nominale deux fois supérieure.
|
||||
La hauteur reste de 384 blocs. Les nombres 5/10 ne servent que de repères
|
||||
internes de surface, sans capacité de joueurs garantie.
|
||||
- Associer Petit à `sanctuary:sanctuary_5` / `sanctuary:population_5` et Grand
|
||||
à `sanctuary:sanctuary` / nouveau réglage `sanctuary:population_10`.
|
||||
Garder les anciens réglages `population_20` et `population_100` chargeables,
|
||||
tout en cachant leurs formats dans les choix publics de création.
|
||||
- Ne pas modifier le relief d’une sauvegarde existante, son identifiant ou
|
||||
son format. Les changements du défaut de création ne convertissent pas
|
||||
un monde. Aucune sauvegarde personnelle n’est ouverte pendant les essais.
|
||||
- Pour l’optimisation, **conserver les résultats à paramètres identiques** :
|
||||
densités, plans et blocs utiles, ordre déterministe et protections de l’eau,
|
||||
des terrasses et de la lave. Le nouveau relief Grand a une clé distincte ;
|
||||
la réduction de surface ne compte pas comme gain de calcul à géométrie égale.
|
||||
- Mesurer séparément démarrage initial, planification et génération d’un lot
|
||||
explicite de chunks. Fixer machine, Java, mémoire, graine, paramètres,
|
||||
coordonnées et état des caches pour comparer alpha.10 et alpha.11.
|
||||
- Viser **120 secondes au plus de démarrage initial** pour les choix publics
|
||||
et **au moins 30 % de gain** sur les phases comparées à géométrie identique.
|
||||
Un temps de planificateur seul ne prouve pas l’entrée dans un monde jouable.
|
||||
Préciser les bornes de chronométrage et ne pas promettre ces seuils pour
|
||||
tout matériel ou pour une charge de cent joueurs.
|
||||
- Valider Petit et Grand, plus un témoin de compatibilité et de performance
|
||||
sur l’ancien profil 20. Les anciennes mesures 100 restent historiques ;
|
||||
ce format n’est plus un choix public ni le centre du plan de validation.
|
||||
Examiner le premier accès, le cache réutilisé et les chunks d’exploration.
|
||||
- Conserver les contrôles moteur de fluides et de réentrée sans marqueur,
|
||||
puis exécuter `check build assemblePack` sur les sources finales. Garder
|
||||
visibles les limites et toute cible de performance non atteinte.
|
||||
- Après validation seulement : artefacts immuables, canal packwiz stable,
|
||||
deux synchronisations isolées puis deux dans la même instance Prism. Les
|
||||
mondes, réglages et mods personnels, même désactivés, doivent être préservés.
|
||||
|
||||
**État :** livré. Artefacts immuables publiés, canal stable avancé, deux
|
||||
synchronisations isolées et deux dans la même instance Prism, avec 364 fichiers
|
||||
personnels et réglages suivis conservés. Tailles et code validés. `check build assemblePack` passe, ainsi
|
||||
que douze tests moteur sur Petit et Grand (graine 0, 1 800 ticks), les contrôles
|
||||
de persistance et l’équivalence bit à bit. Le benchmark ancien20 comparable
|
||||
baisse de 84,65 % au départ et de 86,38 % en exploration. Grand atteint
|
||||
62,67 secondes pour création + 289 chunks FULL. Cette mesure serveur ne
|
||||
valide pas le rendu ni un temps de chargement universel. Voir
|
||||
[Validation](testing.md), [résultats](benchmarks/alpha11-generation.json) et
|
||||
[Distribution](packwiz.md) pour le reçu de publication et de synchronisation.
|
||||
Ce ticket local n’est pas une issue distante publiée.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -0,0 +1,654 @@
|
||||
{
|
||||
"ticket": "WG-12",
|
||||
"version": "0.1.0-alpha.11",
|
||||
"date": "2026-09-09",
|
||||
"protocol": {
|
||||
"machine": "Apple M1, 8 GiB RAM, macOS",
|
||||
"minecraft": "26.3-pre-2",
|
||||
"fabric_loader": "0.19.5",
|
||||
"fabric_api": "0.160.0+26.3",
|
||||
"baseline_source_commit": "a26b72f",
|
||||
"production_files_changed_in_baseline": 0,
|
||||
"runs_per_configuration": 1,
|
||||
"world": "fresh ignored development world per run",
|
||||
"workload": "289 synchronous FULL chunks in concentric rings around natural spawn, then 80 FULL chunks in an eastward corridor. Minecraft world creation timed separately from startup log.",
|
||||
"scope": "Server generation up to FULL. Does not establish ticking readiness, postprocessing completion, client rendering, FPS, multiplayer capacity or a guaranteed user loading time.",
|
||||
"baseline_scope_correction": "The raw alpha.10 report incorrectly says postprocessing is included; FULL alone does not guarantee this. Workload code is identical; alpha.11 corrects only that explanatory string.",
|
||||
"build_time_excluded": true,
|
||||
"comparison": "Only legacy20 versus legacy20 measures the algorithm gain. Grand10 changes geometry; its exploration timing cannot be used for that comparison.",
|
||||
"frozen_runtime_files": 272,
|
||||
"frozen_runtime_manifest_sha256": "1c2b433417a5d9e6a219e9ee19781574538d2167c774e14ed2bf254d277d4482",
|
||||
"baseline_runtime_manifest_sha256": "d4860072d1896f0dfd784a5876f1253367b7818a2023f1910ba1612b6f0e7103",
|
||||
"benchmark_source_sha256": "408711c97d65de63af4e0a1eac90e6edb5bf2df2b70c43772a3fdef7347d75d1"
|
||||
},
|
||||
"runs": {
|
||||
"alpha10_legacy20": {
|
||||
"version": "0.1.0-alpha.10",
|
||||
"players": 20,
|
||||
"seed": 0,
|
||||
"spawn": [
|
||||
0,
|
||||
252,
|
||||
0
|
||||
],
|
||||
"java": "25.0.3",
|
||||
"processors": 8,
|
||||
"max_heap_bytes": 2147483648,
|
||||
"spawn_area_ms": 310664.163542,
|
||||
"spawn_area_full_chunks": 289,
|
||||
"spawn_area_nonempty_chunks": 289,
|
||||
"rings": [
|
||||
{
|
||||
"elapsed_ms": 2.609,
|
||||
"cumulative_chunks": 1,
|
||||
"radius": 0
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5468.709333,
|
||||
"cumulative_chunks": 9,
|
||||
"radius": 1
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 17601.560917,
|
||||
"cumulative_chunks": 25,
|
||||
"radius": 2
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 27397.566458,
|
||||
"cumulative_chunks": 49,
|
||||
"radius": 3
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 36562.853333,
|
||||
"cumulative_chunks": 81,
|
||||
"radius": 4
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 44462.775334,
|
||||
"cumulative_chunks": 121,
|
||||
"radius": 5
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 52423.80275,
|
||||
"cumulative_chunks": 169,
|
||||
"radius": 6
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 60416.705916,
|
||||
"cumulative_chunks": 225,
|
||||
"radius": 7
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 66303.128125,
|
||||
"cumulative_chunks": 289,
|
||||
"radius": 8
|
||||
}
|
||||
],
|
||||
"spawn_area_chunk_latency_ms": {
|
||||
"median": 993.3335,
|
||||
"p95": 1392.158958,
|
||||
"max": 2560.16575
|
||||
},
|
||||
"exploration_ms": 103141.834334,
|
||||
"exploration_full_chunks": 80,
|
||||
"exploration_steps": [
|
||||
{
|
||||
"elapsed_ms": 5146.88675,
|
||||
"chunk_x": 9
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5087.495583,
|
||||
"chunk_x": 10
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5229.782834,
|
||||
"chunk_x": 11
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5046.559,
|
||||
"chunk_x": 12
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4810.5415,
|
||||
"chunk_x": 13
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 28755.95025,
|
||||
"chunk_x": 14
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4763.415834,
|
||||
"chunk_x": 15
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4772.209166,
|
||||
"chunk_x": 16
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4826.460208,
|
||||
"chunk_x": 17
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4857.054416,
|
||||
"chunk_x": 18
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4947.188167,
|
||||
"chunk_x": 19
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5168.714916,
|
||||
"chunk_x": 20
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4897.048167,
|
||||
"chunk_x": 21
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 5286.23025,
|
||||
"chunk_x": 22
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4865.694458,
|
||||
"chunk_x": 23
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 4661.793875,
|
||||
"chunk_x": 24
|
||||
}
|
||||
],
|
||||
"exploration_chunk_latency_ms": {
|
||||
"median": 925.461875,
|
||||
"p95": 1273.829625,
|
||||
"max": 25131.068417
|
||||
},
|
||||
"regions_cached": 2,
|
||||
"passed": true,
|
||||
"world_creation_ms": 96766,
|
||||
"world_creation_plus_spawn_area_ms": 407430.163542,
|
||||
"raw_report_sha256": "d70c4ac49f37470d4db15b9200e26da8ac9d4d3b1b008e7ec03e67deafcc0f93",
|
||||
"log_sha256": "5b1df55f7a167003c089e80fe1fb824d087c137e3662bac84af3ae0b325b8392"
|
||||
},
|
||||
"alpha11_legacy20": {
|
||||
"version": "0.1.0-alpha.11",
|
||||
"players": 20,
|
||||
"seed": 0,
|
||||
"spawn": [
|
||||
0,
|
||||
252,
|
||||
0
|
||||
],
|
||||
"java": "25.0.3",
|
||||
"processors": 8,
|
||||
"max_heap_bytes": 2147483648,
|
||||
"spawn_area_ms": 43647.054375,
|
||||
"spawn_area_full_chunks": 289,
|
||||
"spawn_area_nonempty_chunks": 289,
|
||||
"rings": [
|
||||
{
|
||||
"cumulative_chunks": 1,
|
||||
"elapsed_ms": 1.965667,
|
||||
"radius": 0
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 9,
|
||||
"elapsed_ms": 870.4725,
|
||||
"radius": 1
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 25,
|
||||
"elapsed_ms": 2972.902334,
|
||||
"radius": 2
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 49,
|
||||
"elapsed_ms": 4480.355667,
|
||||
"radius": 3
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 81,
|
||||
"elapsed_ms": 5120.230375,
|
||||
"radius": 4
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 121,
|
||||
"elapsed_ms": 6290.425208,
|
||||
"radius": 5
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 169,
|
||||
"elapsed_ms": 7381.384417,
|
||||
"radius": 6
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 225,
|
||||
"elapsed_ms": 7839.425333,
|
||||
"radius": 7
|
||||
},
|
||||
{
|
||||
"cumulative_chunks": 289,
|
||||
"elapsed_ms": 8677.269209,
|
||||
"radius": 8
|
||||
}
|
||||
],
|
||||
"spawn_area_chunk_latency_ms": {
|
||||
"p95": 280.6,
|
||||
"median": 135.289625,
|
||||
"max": 699.612708
|
||||
},
|
||||
"exploration_ms": 14051.250167,
|
||||
"exploration_full_chunks": 80,
|
||||
"exploration_steps": [
|
||||
{
|
||||
"chunk_x": 9,
|
||||
"elapsed_ms": 750.152875
|
||||
},
|
||||
{
|
||||
"chunk_x": 10,
|
||||
"elapsed_ms": 680.650584
|
||||
},
|
||||
{
|
||||
"chunk_x": 11,
|
||||
"elapsed_ms": 729.180958
|
||||
},
|
||||
{
|
||||
"chunk_x": 12,
|
||||
"elapsed_ms": 725.223
|
||||
},
|
||||
{
|
||||
"chunk_x": 13,
|
||||
"elapsed_ms": 540.200125
|
||||
},
|
||||
{
|
||||
"chunk_x": 14,
|
||||
"elapsed_ms": 5554.177667
|
||||
},
|
||||
{
|
||||
"chunk_x": 15,
|
||||
"elapsed_ms": 473.982208
|
||||
},
|
||||
{
|
||||
"chunk_x": 16,
|
||||
"elapsed_ms": 471.05575
|
||||
},
|
||||
{
|
||||
"chunk_x": 17,
|
||||
"elapsed_ms": 513.109667
|
||||
},
|
||||
{
|
||||
"chunk_x": 18,
|
||||
"elapsed_ms": 533.284292
|
||||
},
|
||||
{
|
||||
"chunk_x": 19,
|
||||
"elapsed_ms": 503.983917
|
||||
},
|
||||
{
|
||||
"chunk_x": 20,
|
||||
"elapsed_ms": 513.126292
|
||||
},
|
||||
{
|
||||
"chunk_x": 21,
|
||||
"elapsed_ms": 508.7555
|
||||
},
|
||||
{
|
||||
"chunk_x": 22,
|
||||
"elapsed_ms": 587.331416
|
||||
},
|
||||
{
|
||||
"chunk_x": 23,
|
||||
"elapsed_ms": 467.628834
|
||||
},
|
||||
{
|
||||
"chunk_x": 24,
|
||||
"elapsed_ms": 494.771375
|
||||
}
|
||||
],
|
||||
"exploration_chunk_latency_ms": {
|
||||
"p95": 186.482333,
|
||||
"median": 103.648708,
|
||||
"max": 5153.6295
|
||||
},
|
||||
"regions_cached": 2,
|
||||
"passed": true,
|
||||
"world_creation_ms": 18888,
|
||||
"world_creation_plus_spawn_area_ms": 62535.054375,
|
||||
"raw_report_sha256": "4799180bd61ec4ffc93d6a1a7ccb5440eb880f7dc08a345429b77db10cc17ab0",
|
||||
"log_sha256": "a9347297608528e719d6780f3dd2f2588bb516e06ab771ca6da6ffdda404f7a4"
|
||||
},
|
||||
"alpha11_grand10": {
|
||||
"version": "0.1.0-alpha.11",
|
||||
"players": 10,
|
||||
"seed": 0,
|
||||
"spawn": [
|
||||
0,
|
||||
252,
|
||||
0
|
||||
],
|
||||
"java": "25.0.3",
|
||||
"processors": 8,
|
||||
"max_heap_bytes": 2147483648,
|
||||
"spawn_area_ms": 44291.159875,
|
||||
"spawn_area_full_chunks": 289,
|
||||
"spawn_area_nonempty_chunks": 289,
|
||||
"rings": [
|
||||
{
|
||||
"radius": 0,
|
||||
"cumulative_chunks": 1,
|
||||
"elapsed_ms": 42.387459
|
||||
},
|
||||
{
|
||||
"radius": 1,
|
||||
"cumulative_chunks": 9,
|
||||
"elapsed_ms": 1474.290584
|
||||
},
|
||||
{
|
||||
"radius": 2,
|
||||
"cumulative_chunks": 25,
|
||||
"elapsed_ms": 3180.279791
|
||||
},
|
||||
{
|
||||
"radius": 3,
|
||||
"cumulative_chunks": 49,
|
||||
"elapsed_ms": 4474.062208
|
||||
},
|
||||
{
|
||||
"radius": 4,
|
||||
"cumulative_chunks": 81,
|
||||
"elapsed_ms": 5043.704042
|
||||
},
|
||||
{
|
||||
"radius": 5,
|
||||
"cumulative_chunks": 121,
|
||||
"elapsed_ms": 6367.417708
|
||||
},
|
||||
{
|
||||
"radius": 6,
|
||||
"cumulative_chunks": 169,
|
||||
"elapsed_ms": 7285.252542
|
||||
},
|
||||
{
|
||||
"radius": 7,
|
||||
"cumulative_chunks": 225,
|
||||
"elapsed_ms": 7824.965875
|
||||
},
|
||||
{
|
||||
"radius": 8,
|
||||
"cumulative_chunks": 289,
|
||||
"elapsed_ms": 8577.641208
|
||||
}
|
||||
],
|
||||
"spawn_area_chunk_latency_ms": {
|
||||
"max": 665.612875,
|
||||
"p95": 274.9215,
|
||||
"median": 136.067375
|
||||
},
|
||||
"exploration_ms": 7684.81575,
|
||||
"exploration_full_chunks": 80,
|
||||
"exploration_steps": [
|
||||
{
|
||||
"elapsed_ms": 739.863,
|
||||
"chunk_x": 9
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 724.49625,
|
||||
"chunk_x": 10
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 720.754584,
|
||||
"chunk_x": 11
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 695.608958,
|
||||
"chunk_x": 12
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 522.1695,
|
||||
"chunk_x": 13
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 1112.798042,
|
||||
"chunk_x": 14
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 430.820667,
|
||||
"chunk_x": 15
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 416.082834,
|
||||
"chunk_x": 16
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 400.90425,
|
||||
"chunk_x": 17
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 414.387708,
|
||||
"chunk_x": 18
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 394.878458,
|
||||
"chunk_x": 19
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 393.643834,
|
||||
"chunk_x": 20
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 391.553625,
|
||||
"chunk_x": 21
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 279.306334,
|
||||
"chunk_x": 22
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 25.634541,
|
||||
"chunk_x": 23
|
||||
},
|
||||
{
|
||||
"elapsed_ms": 18.210167,
|
||||
"chunk_x": 24
|
||||
}
|
||||
],
|
||||
"exploration_chunk_latency_ms": {
|
||||
"max": 760.323917,
|
||||
"p95": 198.353916,
|
||||
"median": 85.88425
|
||||
},
|
||||
"regions_cached": 2,
|
||||
"passed": true,
|
||||
"world_creation_ms": 18376,
|
||||
"world_creation_plus_spawn_area_ms": 62667.159875,
|
||||
"raw_report_sha256": "5284e0ef88217f01d51f9040bf13f652de8276dc3f9844dc14569848b227d05a",
|
||||
"log_sha256": "3a4c84bb6ea1614f8fc9c85f472612750149996e2f7658c83afe398b43afe5f6"
|
||||
}
|
||||
},
|
||||
"same_geometry_reduction_percent": {
|
||||
"world_creation_plus_spawn_area_ms": 84.65134396742975,
|
||||
"spawn_area_ms": 85.95040577665497,
|
||||
"exploration_ms": 86.37676917641545
|
||||
},
|
||||
"targets": {
|
||||
"minimum_reduction_percent": 30,
|
||||
"grand_creation_plus_spawn_area_max_ms": 120000,
|
||||
"same_geometry_reduction_passed": true,
|
||||
"grand_generation_proxy_passed": true
|
||||
},
|
||||
"density_equivalence": {
|
||||
"comparison": "Float.floatToRawIntBits; no tolerance",
|
||||
"reference_commit": "a26b72f",
|
||||
"scope": "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes",
|
||||
"reference_source_sha256": "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
|
||||
"profiles": [
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 5,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "28ec59cba36174fd"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 5,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "efd1e8084800ed5c"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 5,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "91bea1db755d9aca"
|
||||
},
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 10,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "b7bab667baf73361"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 10,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "42aeaf160360e41c"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 10,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "cb85f95333e040c0"
|
||||
},
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 20,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "175524360755dd88"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 20,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "4cc8aec467a2d164"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 20,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "5a3f16e426e5a64e"
|
||||
},
|
||||
{
|
||||
"seed": 0,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 100,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "83cda17336772fa7"
|
||||
},
|
||||
{
|
||||
"seed": 42,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 100,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "e7aa7f52022cc6aa"
|
||||
},
|
||||
{
|
||||
"seed": 8675309,
|
||||
"bitwise_comparisons": 255678,
|
||||
"players": 100,
|
||||
"optimized_path_supported": true,
|
||||
"reference_density_digest": "de4b8f52d249a9bd"
|
||||
}
|
||||
],
|
||||
"concurrent_workers": 3,
|
||||
"concurrent_comparisons": 115668,
|
||||
"passed": true,
|
||||
"adversarial": {
|
||||
"aligned_scalar_path_preserved": true,
|
||||
"passed": true,
|
||||
"bitwise_comparisons": 122528,
|
||||
"concurrent_workers": 3,
|
||||
"repeated_cell_corner_batches": 1
|
||||
}
|
||||
},
|
||||
"persistence": {
|
||||
"default_players": 10,
|
||||
"profiles": [
|
||||
{
|
||||
"restored_settings": "sanctuary:population_5",
|
||||
"players": 5,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary_5"
|
||||
},
|
||||
{
|
||||
"restored_settings": "sanctuary:population_10",
|
||||
"players": 10,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary"
|
||||
},
|
||||
{
|
||||
"restored_settings": "sanctuary:population_20",
|
||||
"players": 20,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary_20"
|
||||
},
|
||||
{
|
||||
"restored_settings": "sanctuary:population_100",
|
||||
"players": 100,
|
||||
"disk_round_trip": true,
|
||||
"preset": "sanctuary:sanctuary_100"
|
||||
}
|
||||
],
|
||||
"passed": true,
|
||||
"visible_presets": [
|
||||
"sanctuary:sanctuary_5",
|
||||
"sanctuary:sanctuary"
|
||||
],
|
||||
"seed": 0,
|
||||
"legacy_settings_preserved": "sanctuary:sanctuary_cavern",
|
||||
"omitted_density_capacity_preserved": 20
|
||||
},
|
||||
"limits": [
|
||||
"Single seed and one timed run per configuration; repeat to estimate variance.",
|
||||
"A new cold region still has a synchronous planning cost; observed legacy20 maximum exploration chunk latency is about 5.15 s.",
|
||||
"No personal saved world was opened or regenerated."
|
||||
],
|
||||
"engine_checks": {
|
||||
"5": {
|
||||
"mod_version": "0.1.0-alpha.11",
|
||||
"seed": 0,
|
||||
"settings": "sanctuary:population_5",
|
||||
"inspected_full_chunks": 110,
|
||||
"retained_water_blocks": 5656,
|
||||
"fluid_ticks": 1800,
|
||||
"tested_springs": 1,
|
||||
"tested_spills": 1,
|
||||
"tested_lava_falls": 1,
|
||||
"postprocessing_reentry_witnesses": 3,
|
||||
"passed": true,
|
||||
"scope": "Selected basins, terraces, springs and ecology patches in explicit regions; no whole-island inventory or multiplayer load test",
|
||||
"raw_report_sha256": "cfaa0a532b50399ab28808f51a871363b198a67d8c096ac13f7a011bd87ff9d0"
|
||||
},
|
||||
"10": {
|
||||
"mod_version": "0.1.0-alpha.11",
|
||||
"seed": 0,
|
||||
"settings": "sanctuary:population_10",
|
||||
"inspected_full_chunks": 125,
|
||||
"retained_water_blocks": 5923,
|
||||
"fluid_ticks": 1800,
|
||||
"tested_springs": 1,
|
||||
"tested_spills": 1,
|
||||
"tested_lava_falls": 1,
|
||||
"postprocessing_reentry_witnesses": 3,
|
||||
"passed": true,
|
||||
"scope": "Selected basins, terraces, springs and ecology patches in explicit regions; no whole-island inventory or multiplayer load test",
|
||||
"raw_report_sha256": "aaed5b38821197c479a8acb7f2a201c64f290666c039d0b15047340e403e028c"
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-17
@@ -5,20 +5,19 @@ registre persistant des continents, leur déblocage et leur génération dans le
|
||||
huit directions restent à implémenter. Le relevé de ressources décrit plus bas
|
||||
est un outil de développement séparé, exécuté dans le monde jetable des GameTests.
|
||||
|
||||
## Capacité de l’île initiale — alpha.10
|
||||
## Taille de l’île initiale — alpha.11
|
||||
|
||||
Le choix de départ propose 5, 20 ou 100 joueurs, avec 20 par défaut. L’aire
|
||||
nominale est proportionnelle à la capacité : base de 512 blocs de diamètre
|
||||
pour cinq joueurs, puis 1 024 pour vingt et environ 2 290 pour cent.
|
||||
La hauteur reste de 384 blocs. Le contour réel et les stocks dépendent de
|
||||
la génération ; l’aire nominale ne garantit pas une surface constructible
|
||||
ou une quantité de minerai par joueur.
|
||||
Deux choix de départ sont proposés : **Petit** et **Grand**, Grand par défaut.
|
||||
Petit garde un diamètre nominal de 512 blocs ; Grand vise environ 724 blocs,
|
||||
soit deux fois l’aire nominale de Petit. La hauteur reste de 384 blocs. Les
|
||||
repères internes 5/10 servent au calcul de surface, sans nom de capacité affiché
|
||||
ni promesse sur le nombre de joueurs, les minerais ou l’aire constructible.
|
||||
|
||||
Cette taille est choisie pour un nouveau monde et ne remplace pas le futur
|
||||
déblocage collectif de continents. Elle ne redimensionne aucune sauvegarde
|
||||
existante et ne change pas la limite de connexions du serveur. Les contrôles
|
||||
de génération des trois tailles passent sur la graine `0`, dans les emprises
|
||||
consignées dans [Validation](testing.md), sans simuler cent joueurs simultanés.
|
||||
Cette taille se choisit pour un nouveau monde et ne remplace pas le futur
|
||||
déblocage collectif de continents. Grand utilise `population_10` ; les anciennes
|
||||
sauvegardes 5/20/100 conservent leurs réglages et leurs dimensions. Les formats
|
||||
20/100 sont cachés de la création. Les mesures de l’alpha.10 conservées plus bas
|
||||
restent historiques ; les contrôles alpha.11 sont dans [Validation](testing.md).
|
||||
|
||||
## Les huit directions climatiques
|
||||
|
||||
@@ -150,11 +149,13 @@ n'est inclus dans ces comptes de blocs.
|
||||
```
|
||||
|
||||
Les modes sont transmis à la propriété JVM de test
|
||||
`sanctuary.test.resourceSurvey`. La propriété Gradle `-PsanctuaryTestPlayers=5`,
|
||||
`20` ou `100` choisit le nouveau profil ; sa valeur par défaut est `20`.
|
||||
Le mode `island` calcule l’enveloppe de la capacité active, avec 16 blocs de
|
||||
marge de décoration et un halo de préparation d’un chunk. Les tailles ci-dessous
|
||||
sont les bornes calculées du travail demandé, pas des relevés déjà exécutés :
|
||||
`sanctuary.test.resourceSurvey`. La propriété Gradle `-PsanctuaryTestPlayers`
|
||||
choisit le profil interne. L’alpha.11 utilise `5` pour Petit et `10` pour Grand,
|
||||
ce dernier par défaut ; `20` et `100` servent à la compatibilité historique.
|
||||
Le mode `island` calcule l’enveloppe du profil actif, avec 16 blocs de marge de
|
||||
décoration et un halo de préparation d’un chunk. Les valeurs ci-dessous sont
|
||||
les bornes calculées lors de l’alpha.10, pas des relevés exécutés ni un nombre
|
||||
de chunks déjà validé pour le nouveau Grand :
|
||||
|
||||
| Profil | Chunks comptés par `island` | Chunks FULL préparés avec halo |
|
||||
| --- | --- | --- |
|
||||
|
||||
+24
-11
@@ -16,31 +16,33 @@ de l'ancien pack 26.2.
|
||||
## Livrer une mise à jour
|
||||
|
||||
Dernière livraison vérifiée le 9 septembre 2026 :
|
||||
[Sanctuary 0.1.0-alpha.10](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.10),
|
||||
depuis le commit source `ef5d1ea`. Le JAR, le `.mrpack` et le ZIP d’amorçage
|
||||
[Sanctuary 0.1.0-alpha.11](https://git.botsu.net/koka/sanctuary-beta/releases/tag/v0.1.0-alpha.11),
|
||||
depuis le commit source `f30a350`. Le JAR, le `.mrpack` et le ZIP d’amorçage
|
||||
Prism sont publiés et leurs téléchargements publics vérifiés. Le canal stable
|
||||
pointe sur cette version. SHA-256 du JAR :
|
||||
|
||||
```text
|
||||
261f727c6b7fdf8dfa81395d805967885b52143147a41709d1307cc881dc7d7b
|
||||
f075ad8a0b97cd53e4891273adbee5f79f04ab3984442950d5d85f58d2ded589
|
||||
```
|
||||
|
||||
Le véritable installateur packwiz a réussi deux fois dans un dossier de
|
||||
développement neuf, puis deux fois dans l’instance Sanctuary Beta existante.
|
||||
Le second passage laisse les fichiers gérés identiques. Un seul JAR Sanctuary
|
||||
alpha.10 est actif ; les 363 fichiers personnels et réglages suivis conservent
|
||||
leurs hashes. L’ancien JAR et les manifestes locaux sont sauvegardés hors de
|
||||
`mods/`, dans `sanctuary-backups/before-0.1.0-alpha.10/` de cette même instance.
|
||||
alpha.11 est actif ; les 364 fichiers personnels et réglages suivis conservent
|
||||
leurs hashes, y compris les fichiers des mods tiers et leurs configurations.
|
||||
L’ancien JAR et les manifestes locaux sont sauvegardés hors de `mods/`, dans
|
||||
`sanctuary-backups/before-0.1.0-alpha.11/` de cette même instance.
|
||||
Aucun monde existant n’a été ouvert, converti ou régénéré par la livraison.
|
||||
Les reçus locaux ignorés sont `build/alpha10-isolated-validation.json` et
|
||||
`build/alpha10-prism-validation.json` ; les listes de hashes avant et après
|
||||
figurent dans le dossier de sauvegarde.
|
||||
Les reçus locaux ignorés sont `build/alpha11-isolated-validation.json` et
|
||||
`build/alpha11-prism-validation.json` ; les listes de hashes avant et après
|
||||
figurent dans le dossier de sauvegarde. Les preuves du build, des deux tailles
|
||||
et de performance figurent dans [Validation](testing.md).
|
||||
|
||||
Après les vérifications, le commit et le push de la branche du ticket, le script
|
||||
de publication réalise les étapes de release et de canal ci-dessous :
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.10.mrpack
|
||||
python3 scripts/publish_pack.py --notes-file chemin/vers/notes.md --asset build/Sanctuary-0.1.0-alpha.11.mrpack
|
||||
```
|
||||
|
||||
Les options sont facultatives. Ce script **publie** sur le Git configuré dans
|
||||
@@ -59,7 +61,7 @@ La procédure complète, également utilisable manuellement :
|
||||
3. Préparer les métadonnées de la livraison, avec l'URL exacte du futur JAR :
|
||||
|
||||
```sh
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.10/sanctuary-0.1.0-alpha.10.jar
|
||||
python3 scripts/pack.py release https://git.botsu.net/koka/sanctuary-beta/releases/download/v0.1.0-alpha.11/sanctuary-0.1.0-alpha.11.jar
|
||||
```
|
||||
|
||||
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
|
||||
@@ -175,6 +177,17 @@ nécessite pas de réimporter le ZIP d’amorçage. Les fichiers de sauvegarde,
|
||||
captures, packs graphiques et réglages personnels suivis ont conservé leurs
|
||||
hashes lors des deux synchronisations de livraison.
|
||||
|
||||
L’alpha.11 propose deux choix : **Petit** et **Grand**, Grand par défaut.
|
||||
Le preset `sanctuary:sanctuary` crée un nouveau monde `population_10` ;
|
||||
Petit garde `sanctuary:sanctuary_5` / `population_5`. Les anciens mondes
|
||||
20/100 restent chargeables à leur taille enregistrée, alors que leurs formats
|
||||
sont cachés de la création. Les formats de sauvegarde sont inchangés.
|
||||
|
||||
L’optimisation conserve les résultats pour un même réglage. La livraison a
|
||||
préservé les fichiers personnels suivis, les mods tiers et leurs configurations.
|
||||
L’instance n’a pas été réimportée et aucun mod tiers n’a été retiré. Les
|
||||
sauvegardes personnelles sont restées fermées pendant les essais et la synchronisation.
|
||||
|
||||
Références officielles : [installation packwiz](https://packwiz.infra.link/tutorials/installing/packwiz-installer/),
|
||||
[commandes Prism](https://prismlauncher.org/wiki/help-pages/custom-commands/),
|
||||
[bootstrap v0.0.3](https://github.com/packwiz/packwiz-installer-bootstrap/releases/tag/v0.0.3).
|
||||
|
||||
+85
-3
@@ -54,9 +54,10 @@ Deux adaptations y sont nécessaires, vérifiées contre les classes Minecraft
|
||||
|
||||
- `GameTestServer` sélectionne normalement `minecraft:flat_all_dimensions`.
|
||||
Le mixin de test sélectionne directement le preset de production
|
||||
`sanctuary:sanctuary` pour 20 joueurs par défaut.
|
||||
`-PsanctuaryTestPlayers=5` ou `100` sélectionne sa variante de production,
|
||||
sans recopier ses JSON.
|
||||
`sanctuary:sanctuary`, sans recopier ses JSON. L’alpha.11 utilise le défaut
|
||||
Grand (`-PsanctuaryTestPlayers=10`) et Petit (`5`). Les profils historiques
|
||||
restent accessibles aux diagnostics de compatibilité, séparément de la liste
|
||||
publique de création.
|
||||
- Le framework désactive normalement les structures. Le test les active dans
|
||||
`WorldOptions` pour examiner les chunks après toutes les étapes de génération.
|
||||
La seed du serveur de test est `0` par défaut ; `-PsanctuaryTestSeed` est transmis
|
||||
@@ -193,6 +194,87 @@ Pour cet essai visuel en monde de développement créatif, comparer la même vue
|
||||
Y=63 ni assombrissement d'altitude au passage de Y=32. Les brouillards de l'eau,
|
||||
de la météo et des effets d'aveuglement ou d'obscurité doivent rester présents.
|
||||
|
||||
## Petit, Grand et optimisation alpha.11 — 9 septembre 2026
|
||||
|
||||
`check build assemblePack` réussit sur les sources finales avec Grand, puis
|
||||
la suite moteur réussit également sur Petit. Douze tests requis passent dans
|
||||
chaque monde neuf, graine `0`. Les mondes personnels restent fermés.
|
||||
|
||||
| Choix | Chunks FULL inspectés | Eau retenue | Ticks de fluides |
|
||||
| --- | --- | --- | --- |
|
||||
| Petit (`population_5`) | 110 | 5 656 | 1 800 |
|
||||
| Grand (`population_10`, défaut) | 125 | 5 923 | 1 800 |
|
||||
|
||||
Les deux essais vérifient les sources, déversoirs, coulées de lave et témoins
|
||||
de réentrée après post-traitement. La persistance réelle sur disque préserve
|
||||
les quatre références 5/10/20/100, le défaut Grand, les deux seuls choix publics
|
||||
et l’ancien défaut 20 lorsque le champ `players` est absent d’une densité sauvée.
|
||||
|
||||
L’oracle alpha.10 effectue **3 183 804 comparaisons bit à bit** de densités
|
||||
compilées réelles : références 5/10/20/100, graines `0`, `42`, `8675309`,
|
||||
volumes, coordonnées signées, frontières et accès concurrents. Pour 10,
|
||||
l’oracle utilise l’ancien algorithme paramétré à cette nouvelle taille ;
|
||||
aucun ancien preset 10 n’est supposé. Les tests synthétiques ajoutent
|
||||
122 528 comparaisons sur les arrondis, collisions et changements de contexte.
|
||||
Les expressions de datapacks non reconnues gardent le sampler vanilla.
|
||||
Un test de concurrence vérifie qu’un plan prêt reste lisible pendant un
|
||||
calcul lent, sans publier de plan incomplet ni conserver un échec en cache.
|
||||
|
||||
### Mesure à géométrie identique et nouveau Grand
|
||||
|
||||
Machine : Apple M1, 8 Gio de RAM, Java 25.0.3, heap 2 Gio, huit processeurs
|
||||
visibles, Minecraft 26.3-pre-2 / Fabric 0.19.5. **Une exécution par configuration**,
|
||||
graine `0`, nouveau monde à chaque fois, spawn `(0,252,0)`. La référence
|
||||
alpha.10 est le commit `a26b72f` avec uniquement le harnais de mesure ajouté :
|
||||
aucun fichier de production modifié. Aucun autre serveur ou build simultané.
|
||||
|
||||
| Phase serveur | Alpha.10, ancien 20 | Alpha.11, ancien 20 | Alpha.11, Grand 10 |
|
||||
| --- | --- | --- | --- |
|
||||
| Création du monde et spawn | 96,766 s | 18,888 s | 18,376 s |
|
||||
| 289 chunks autour du spawn | 310,664 s | 43,647 s | 44,291 s |
|
||||
| Somme création + zone | 407,430 s | 62,535 s | 62,667 s |
|
||||
| 80 chunks d’exploration | 103,142 s | 14,051 s | 7,685 s |
|
||||
|
||||
À géométrie 20 identique, la somme initiale baisse de **84,65 %** et
|
||||
l’exploration de **86,38 %**. Le gain minimal de 30 % est dépassé sur ce
|
||||
protocole. Grand respecte le proxy de 120 secondes pour créer le monde et
|
||||
produire la zone demandée. Sa surface différente ne compte pas comme preuve
|
||||
du gain algorithmique. La latence maximale d’un chunk d’exploration sur
|
||||
l’ancien 20 passe de 25,13 à 5,15 secondes : une région froide conserve donc
|
||||
un coût ponctuel. Le cache ne rend pas toute exploration instantanée.
|
||||
|
||||
Le benchmark demande 289 chunks `FULL` par anneaux jusqu’au rayon huit,
|
||||
puis 80 dans un corridor vers l’est. Il distingue la création relevée dans
|
||||
`Time elapsed` des phases mesurées dans le test. **FULL ne garantit pas le
|
||||
statut de simulation ni la fin du post-traitement.** Le libellé du rapport
|
||||
brut alpha.10 affirmait par erreur inclure ce dernier ; seul ce texte a été
|
||||
corrigé dans le harnais alpha.11, sans changer les demandes de chunks.
|
||||
Ni rendu client, ni réseau, ni FPS, ni délai d’entrée visuellement confirmé,
|
||||
ni capacité multijoueur ne sont mesurés. Une graine et une répétition ne
|
||||
mesurent pas la variance sur toutes les îles ou machines.
|
||||
|
||||
Les [résultats structurés et hashes](benchmarks/alpha11-generation.json)
|
||||
conservent les phases, anneaux, latences, périmètres et preuves moteur.
|
||||
Les journaux bruts locaux sont `build/alpha11-baseline-20.log`,
|
||||
`build/alpha11-optimized-20.log`, `build/alpha11-grand-10.log`,
|
||||
`build/alpha11-check-build.log` et `build/alpha11-small-5.log`.
|
||||
|
||||
Reproduire le benchmark sur cette version (utiliser `10` pour Grand) :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryBenchmark=true -PsanctuaryTestPlayers=20 -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
```
|
||||
|
||||
L’option sélectionne le harnais de performance à la place de la suite moteur,
|
||||
jamais dans le JAR livré. Le build obligatoire reste une exécution normale :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack -PsanctuaryTestPlayers=10 -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
./gradlew :sanctuary:runGameTest -PsanctuaryTestPlayers=5 -PsanctuaryTestSeed=0 -PsanctuaryResourceSurvey=off -PsanctuaryDensityMaps=false --console=plain
|
||||
```
|
||||
|
||||
Les tableaux alpha.10 suivants restent historiques.
|
||||
|
||||
## Contrôles et validation alpha.10 — 9 septembre 2026
|
||||
|
||||
Les profils `sanctuary:population_5`, `population_20` et `population_100`
|
||||
|
||||
+13
-9
@@ -50,15 +50,19 @@ Les intégrations communautaires envisagées comprennent Fabric API, Sodium, Iri
|
||||
|
||||
L'île de Sanctuary est le point de départ commun. Elle flotte dans un vide qui ne doit pas être rempli par la génération habituelle du monde. Les continents suivants sont de vastes terres suspendues, créées progressivement selon une taille et une direction maîtrisées.
|
||||
|
||||
La capacité prévue du groupe choisit la taille initiale : **5, 20 ou
|
||||
100 joueurs**, avec **20 par défaut**. L’aire nominale doit évoluer
|
||||
proportionnellement à cette capacité, en prenant l’île de 512 blocs de
|
||||
diamètre comme référence pour cinq joueurs. Cela donne 1 024 blocs pour
|
||||
vingt joueurs et environ 2 290 pour cent joueurs, sur une hauteur conservée
|
||||
de 384 blocs. Ce choix appartient à la création du monde ; il n’agrandit pas
|
||||
une sauvegarde existante ni ne suit automatiquement les connexions.
|
||||
Il s’agit d’un dimensionnement du terrain, sans quota de ressources imposé
|
||||
ou garantie de performances. L’alpha.10 met ces trois tailles à disposition ; leurs contrôles moteur portent sur la graine `0` et des emprises explicites.
|
||||
Le choix de taille initiale se limite à **Petit** et **Grand**, avec Grand
|
||||
par défaut. Petit prend une référence de 512 blocs de diamètre ; Grand vise
|
||||
environ 724 blocs, soit deux fois son aire nominale, sur une hauteur de
|
||||
384 blocs. Les repères internes 5/10 ne constituent ni les noms affichés ni
|
||||
une promesse de capacité multijoueur. Ce resserrement des tailles doit limiter
|
||||
les distances entre constructions.
|
||||
|
||||
L’alpha.11 implémente ces deux choix et l’optimisation des chargements. Les
|
||||
anciens formats 20/100 sont masqués à la création mais restent chargeables
|
||||
avec leurs paramètres enregistrés ; aucune sauvegarde n’est réduite.
|
||||
Les mesures serveur et leurs limites sont consignées dans [Validation](testing.md).
|
||||
Ce dimensionnement ne remplace pas l’expansion collective ni ne garantit
|
||||
une aire habitable ou un stock de ressources par joueur.
|
||||
|
||||
L'île initiale doit être **tempérée et légèrement humide**, avec quelques zones
|
||||
plus sèches, notamment de larges plages et dépôts irréguliers de sable, gravier,
|
||||
|
||||
+42
-24
@@ -5,22 +5,43 @@ entourée de vide sans répétition d'îles à grande distance. C'est une premi
|
||||
tranche de génération pour Minecraft **26.3-pre-2 / Fabric**, pas encore le
|
||||
système d'expansion collective.
|
||||
|
||||
L’alpha.10 propose trois tailles d’île pour 5, 20 ou 100 joueurs, avec
|
||||
20 joueurs par défaut. Les trois profils passent les contrôles moteur sur
|
||||
la graine `0`, dans des emprises explicitement inspectées. Les générations
|
||||
historiques conservent leurs réglages, notamment `sanctuary:sanctuary_cavern`
|
||||
pour l’alpha.9 et `sanctuary:sanctuary_rift` pour l’alpha.8. Les observations
|
||||
de chaque version restent séparées dans [Validation](testing.md).
|
||||
L’alpha.11 propose deux choix de création, **Petit** et **Grand**, avec Grand
|
||||
par défaut. Les anciennes tailles 20/100 restent chargeables dans les sauvegardes.
|
||||
Les observations de chaque version restent dans [Validation](testing.md).
|
||||
|
||||
## Petit et Grand — alpha.11
|
||||
|
||||
| Choix public | Diamètre nominal | Aire nominale relative | Preset | Réglage enregistré |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Petit | 512 blocs | 1 | `sanctuary:sanctuary_5` | `sanctuary:population_5` |
|
||||
| Grand, défaut | Environ 724 blocs | 2 | `sanctuary:sanctuary` | `sanctuary:population_10` |
|
||||
|
||||
La hauteur reste de 384 blocs. Les repères internes 5/10 décrivent le calcul
|
||||
de surface ; ils ne sont ni des noms affichés ni une capacité promise.
|
||||
Le relief réel garde ses trous, ses surplombs et sa bordure sculptée.
|
||||
|
||||
Le nouveau Grand reçoit sa propre clé `population_10`. Les anciens mondes
|
||||
`population_5`, `population_20` et `population_100` restent chargeables avec
|
||||
leurs paramètres ; les formats 20/100 sont cachés dans les choix publics.
|
||||
Changer la valeur du preset de création n’agrandit ni ne réduit une sauvegarde.
|
||||
Les générations antérieures restent distinctes, dont `sanctuary_cavern` et
|
||||
`sanctuary_rift`.
|
||||
|
||||
L’optimisation réutilise les coins d’interpolation avec la même arithmétique
|
||||
et borne ce cache à 192 Kio par thread, sans retenir fortement un monde.
|
||||
Les plans prêts restent accessibles pendant le calcul d’une autre région ;
|
||||
le sampler compilé est réutilisé dans ce calcul. Les contrôles d’équivalence,
|
||||
de persistance et les mesures à géométrie identique sont dans [Validation](testing.md).
|
||||
Les règles de forme, d’hydrologie et les anciens réglages restent inchangés.
|
||||
|
||||
## Créer un monde
|
||||
|
||||
En solo : créer un **nouveau** monde et choisir le type de monde **Sanctuary**
|
||||
dans les options de génération. Les types de monde vanilla restent disponibles.
|
||||
L’alpha.10 utilise ce choix pour 20 joueurs par défaut, avec des variantes
|
||||
5 et 100 joueurs. Les identifiants sont `sanctuary:sanctuary` (20),
|
||||
`sanctuary:sanctuary_5` (5) et `sanctuary:sanctuary_100` (100).
|
||||
La création solo propose **Sanctuary (Petit)** et **Sanctuary (Grand)**,
|
||||
ce dernier étant le défaut Sanctuary.
|
||||
Les types de monde vanilla restent disponibles.
|
||||
|
||||
Sur un nouveau serveur Fabric disposant du mod et de Fabric API :
|
||||
Sur un nouveau serveur Fabric disposant de cette version et de Fabric API,
|
||||
le choix Grand utilise :
|
||||
|
||||
```properties
|
||||
level-type=sanctuary:sanctuary
|
||||
@@ -28,14 +49,10 @@ level-name=sanctuary
|
||||
level-seed=0
|
||||
```
|
||||
|
||||
Pour créer une variante alpha.10, remplacer seulement
|
||||
`level-type` par `sanctuary:sanctuary_5` ou `sanctuary:sanctuary_100`, avec un
|
||||
nouveau `level-name`. Ces exemples concernent une création de monde, pas le
|
||||
changement de taille d’une sauvegarde existante.
|
||||
|
||||
La seed est libre. Modifier `level-type` ne convertit pas un monde existant :
|
||||
utiliser un nouveau `level-name` pour tester une autre génération. Les fichiers
|
||||
de test et les sauvegardes personnelles doivent rester distincts.
|
||||
Pour Petit, utiliser `level-type=sanctuary:sanctuary_5`. Choisir un nouveau
|
||||
`level-name` pour tester l’autre taille. La seed est libre ; ces exemples ne
|
||||
convertissent aucun monde déjà enregistré et n’autorisent aucune régénération.
|
||||
Les fichiers de test et les sauvegardes personnelles restent distincts.
|
||||
|
||||
## Socle de génération conservé
|
||||
|
||||
@@ -86,11 +103,12 @@ chaque seed. L’accès à chaque progression vanilla et les ressources renouvel
|
||||
nécessitent des vérifications concrètes. Les stocks réels sont relevés après la
|
||||
génération, sans corriger une île pour atteindre un quota.
|
||||
|
||||
## Capacité de l’île — génération v10
|
||||
## Capacité de l’île — génération v10 historique
|
||||
|
||||
L’alpha.10 propose trois tailles accessibles à la création d’un monde.
|
||||
Le choix **Sanctuary** vise 20 joueurs par défaut, accompagné de variantes
|
||||
pour 5 et 100 joueurs. Les presets et paramètres de bruit sont associés ainsi :
|
||||
Lors de l’alpha.10, trois tailles étaient accessibles à la création d’un monde.
|
||||
Le choix **Sanctuary** visait 20 joueurs par défaut, accompagné de variantes
|
||||
pour 5 et 100 joueurs. Le tableau suivant conserve cette association historique ;
|
||||
l’alpha.11 utilise le nouveau défaut Grand indiqué plus haut :
|
||||
|
||||
| Preset de création | Paramètres enregistrés dans le monde |
|
||||
| --- | --- |
|
||||
|
||||
+2
-2
@@ -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.10
|
||||
pack_version=0.1.0-alpha.10
|
||||
mod_version=0.1.0-alpha.11
|
||||
pack_version=0.1.0-alpha.11
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -33,11 +33,21 @@ tasks.register('cleanGameTestWorld', Delete) {
|
||||
tasks.named('runGameTest') {
|
||||
dependsOn('cleanGameTestWorld')
|
||||
systemProperty('sanctuary.test.seed', providers.gradleProperty('sanctuaryTestSeed').getOrElse('0'))
|
||||
systemProperty('sanctuary.test.players', providers.gradleProperty('sanctuaryTestPlayers').getOrElse('20'))
|
||||
systemProperty('sanctuary.test.players', providers.gradleProperty('sanctuaryTestPlayers').getOrElse('10'))
|
||||
systemProperty('sanctuary.test.resourceSurvey', providers.gradleProperty('sanctuaryResourceSurvey').getOrElse('sample'))
|
||||
systemProperty('sanctuary.test.densityMaps', providers.gradleProperty('sanctuaryDensityMaps').getOrElse('true'))
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def benchmark = providers.gradleProperty('sanctuaryBenchmark').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('benchmark', benchmark)
|
||||
filesMatching('fabric.mod.json') {
|
||||
expand(gametest_entrypoint: benchmark
|
||||
? 'fr.koka.sanctuary.gametest.WorldgenPerformanceGameTests'
|
||||
: 'fr.koka.sanctuary.gametest.SanctuaryWorldGameTests')
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
|
||||
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
|
||||
@@ -222,3 +232,12 @@ tasks.register('populationRegionPlanSmoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.PopulationRegionPlanSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('populationRegionPlanSmoke') }
|
||||
|
||||
tasks.register('populationPlanCacheSmoke', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Check that completed regions remain readable while new regions are planned.'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.PopulationPlanCacheSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('populationPlanCacheSmoke') }
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Frozen test oracle from a26b72f PopulationIslandDensity.java.
|
||||
// Original SHA-256: fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1
|
||||
// Only package/imports/class name differ; do not optimize this reference.
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.*;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.util.Interval;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
|
||||
/** Capacity changes the envelope; all natural noise is still sampled at world coordinates. */
|
||||
public record Alpha10PopulationDensityReference(DensityFunction terrain, DensityFunction distortion,
|
||||
DensityFunction sculpt, DensityFunction detail, DensityFunction underside,
|
||||
DensityFunction riftWarp, DensityFunction riftDetail, IslandCapacity capacity) implements DensityFunction {
|
||||
public static final MapCodec<Alpha10PopulationDensityReference> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
DensityFunction.CODEC.fieldOf("terrain").forGetter(Alpha10PopulationDensityReference::terrain),
|
||||
DensityFunction.CODEC.fieldOf("distortion").forGetter(Alpha10PopulationDensityReference::distortion),
|
||||
DensityFunction.CODEC.fieldOf("sculpt").forGetter(Alpha10PopulationDensityReference::sculpt),
|
||||
DensityFunction.CODEC.fieldOf("detail").forGetter(Alpha10PopulationDensityReference::detail),
|
||||
DensityFunction.CODEC.fieldOf("underside").forGetter(Alpha10PopulationDensityReference::underside),
|
||||
DensityFunction.CODEC.fieldOf("rift_warp").forGetter(Alpha10PopulationDensityReference::riftWarp),
|
||||
DensityFunction.CODEC.fieldOf("rift_detail").forGetter(Alpha10PopulationDensityReference::riftDetail),
|
||||
IslandCapacity.CODEC.optionalFieldOf("players", IslandCapacity.TWENTY).forGetter(Alpha10PopulationDensityReference::capacity)
|
||||
).apply(instance, Alpha10PopulationDensityReference::new));
|
||||
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
var raw = terrain.compileSampler(context); var edge = distortion.compileSampler(context);
|
||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||
var fractureDetail = riftDetail.compileSampler(context);
|
||||
long seed = context.createRandom(SanctuaryMod.id("population_rifts_alpha10")).nextLong();
|
||||
var layouts = new ConcurrentHashMap<IslandCapacity.Region, RiftShape.Layout>();
|
||||
return new DensitySampler() {
|
||||
@Override public float sampleValue(SamplerContext sample, int x, int y, int z) {
|
||||
if (Math.hypot((double) x, z) >= capacity.terrainLimit() || y <= 0 || y >= 384) return -1;
|
||||
int localY = y - 64;
|
||||
float natural = PopulationIslandShape.raisedDensity(x, y, z, raw.sampleValue(sample, x, localY, z),
|
||||
edge.sampleValue(sample, x, 0, z), coarse.sampleValue(sample, x, localY, z),
|
||||
fine.sampleValue(sample, x, localY, z), bottom.sampleValue(sample, x, localY, z), 64, 384, capacity);
|
||||
if (natural <= 0) return natural;
|
||||
var region = capacity.regionAt(x, z);
|
||||
var layout = layouts.computeIfAbsent(region, r -> RiftShape.layout(regionSeed(seed, r)));
|
||||
int rx = x - region.originX(), rz = z - region.originZ();
|
||||
if (!RiftShape.mayAffect(layout, rx, rz)) return natural;
|
||||
return RiftShape.density(layout, rx, y, rz, natural,
|
||||
warp.sampleValue(sample, x, y, z), fractureDetail.sampleValue(sample, x, y, z));
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext sample, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(sample, buffer, volume, this);
|
||||
}
|
||||
};
|
||||
}
|
||||
public static long regionSeed(long seed, IslandCapacity.Region region) {
|
||||
long value = seed ^ ((long) region.x() << 32) ^ (region.z() & 0xffffffffL) ^ 0xA10CA7E1L;
|
||||
value = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94d049bb133111ebL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) {
|
||||
return new Alpha10PopulationDensityReference(rule.rewrite(terrain), rule.rewrite(distortion), rule.rewrite(sculpt),
|
||||
rule.rewrite(detail), rule.rewrite(underside), rule.rewrite(riftWarp), rule.rewrite(riftDetail), capacity);
|
||||
}
|
||||
@Override public Interval range() { return Interval.of(-1, 1); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
@Override public MapCodec<Alpha10PopulationDensityReference> codec() { return CODEC; }
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.ExactInterpolationChecks;
|
||||
import fr.koka.sanctuary.worldgen.ExactInterpolationSampler;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityBuffer;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Compares production against the frozen alpha.10 implementation; no chunks are created. */
|
||||
final class PopulationDensityEquivalenceDiagnostics {
|
||||
private record Pair(int players, long seed, DensitySampler reference, DensitySampler actual) {}
|
||||
|
||||
static void verify(GameTestHelper helper) throws Exception {
|
||||
Map<String, Object> adversarial = ExactInterpolationChecks.verify();
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
var noises = registries.lookupOrThrow(Registries.NOISE);
|
||||
var densities = registries.lookupOrThrow(Registries.DENSITY_FUNCTION);
|
||||
var settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS);
|
||||
List<Map<String, Object>> reports = new ArrayList<>();
|
||||
List<Pair> pairs = new ArrayList<>();
|
||||
for (int players : new int[]{5, 10, 20, 100}) {
|
||||
var key = SanctuaryMod.id("population_" + players);
|
||||
var registered = densities.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, key)).value();
|
||||
helper.assertTrue(registered instanceof PopulationIslandDensity, "Compare the real registered Population density");
|
||||
var density = (PopulationIslandDensity) registered;
|
||||
helper.assertTrue(ExactInterpolationSampler.supportsTerrain(density.terrain()),
|
||||
"The shipped coordinate-only terrain must actually use the exact cache, not the fallback");
|
||||
var reference = new Alpha10PopulationDensityReference(density.terrain(), density.distortion(), density.sculpt(),
|
||||
density.detail(), density.underside(), density.riftWarp(), density.riftDetail(), density.capacity());
|
||||
for (long seed : new long[]{0, 42, 8675309}) {
|
||||
var state = RandomState.create(noises, seed,
|
||||
settings.getOrThrow(ResourceKey.create(Registries.NOISE_SETTINGS, key)).value());
|
||||
var pair = new Pair(players, seed, state.getSampler(reference), state.getSampler(density));
|
||||
pairs.add(pair);
|
||||
SamplerContext context = SamplerContext.builder().enableCaches().build();
|
||||
long comparisons = 0, digest = 0xcbf29ce484222325L;
|
||||
for (var volume : volumes(IslandCapacity.fromPlayers(players))) {
|
||||
DensityBuffer expected = DensityBuffer.createUnpooled(volume.size());
|
||||
DensityBuffer actual = DensityBuffer.createUnpooled(volume.size());
|
||||
pair.reference().sampleVolume(SamplerContext.EMPTY_UNCACHED, expected, volume);
|
||||
pair.actual().sampleVolume(context, actual, volume);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++) {
|
||||
int index = volume.indexUnchecked(x, y, z);
|
||||
int wx = volume.blockX(x), wy = volume.blockY(y), wz = volume.blockZ(z);
|
||||
same(pair, expected.get(index), actual.get(index), wx, wy, wz, "bulk");
|
||||
same(pair, expected.get(index), pair.actual().sampleValue(context, wx, wy, wz), wx, wy, wz, "scalar after bulk");
|
||||
comparisons += 2;
|
||||
digest = (digest ^ Float.floatToRawIntBits(expected.get(index))) * 0x100000001b3L;
|
||||
}
|
||||
}
|
||||
Random order = new Random(seed ^ players);
|
||||
int limit = density.capacity().terrainLimit();
|
||||
for (int i = 0; i < 512; i++) {
|
||||
int x = order.nextInt(limit * 2 + 33) - limit - 16;
|
||||
int y = order.nextInt(400) - 8;
|
||||
int z = order.nextInt(limit * 2 + 33) - limit - 16;
|
||||
float expected = pair.reference().sampleValue(SamplerContext.EMPTY_UNCACHED, x, y, z);
|
||||
same(pair, expected, pair.actual().sampleValue(SamplerContext.EMPTY_UNCACHED, x, y, z), x, y, z, "uncached context");
|
||||
same(pair, expected, pair.actual().sampleValue(context, x, y, z), x, y, z, "context switch");
|
||||
comparisons += 2;
|
||||
}
|
||||
reports.add(Map.of("players", players, "seed", seed, "bitwise_comparisons", comparisons,
|
||||
"reference_density_digest", Long.toUnsignedString(digest, 16), "optimized_path_supported", true));
|
||||
SanctuaryMod.LOGGER.info("Exact density equivalence: players {}, seed {}, {} bitwise comparisons", players, seed, comparisons);
|
||||
}
|
||||
}
|
||||
long concurrentComparisons = 0;
|
||||
try (var executor = Executors.newFixedThreadPool(3)) {
|
||||
List<Callable<Long>> tasks = new ArrayList<>();
|
||||
for (int worker = 0; worker < 3; worker++) {
|
||||
int id = worker;
|
||||
tasks.add(() -> {
|
||||
long count = 0;
|
||||
SamplerContext local = SamplerContext.builder().enableCaches().build();
|
||||
// All workers share the same samplers and alternate worlds/seeds.
|
||||
// Only their scratch buffers and sampler contexts are thread-local.
|
||||
for (int round = 0; round < 3; round++) for (var pair : pairs) {
|
||||
var volume = new DensityVolume(9, 17, 7, -13 + id, 89 + round, -7 - id);
|
||||
DensityBuffer actual = DensityBuffer.createUnpooled(volume.size());
|
||||
pair.actual().sampleVolume(local, actual, volume);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++) {
|
||||
int wx = volume.blockX(x), wy = volume.blockY(y), wz = volume.blockZ(z);
|
||||
same(pair, pair.reference().sampleValue(local, wx, wy, wz),
|
||||
actual.get(volume.indexUnchecked(x, y, z)), wx, wy, wz, "concurrent bulk");
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
});
|
||||
}
|
||||
for (var result : executor.invokeAll(tasks)) concurrentComparisons += result.get();
|
||||
}
|
||||
Path report = Path.of("diagnostics", "population-alpha10-density-equivalence.json");
|
||||
Files.createDirectories(report.getParent());
|
||||
Files.writeString(report, new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
"reference_commit", "a26b72f", "reference_source_sha256", "fcc5a8a39b7c4d181d74ce4f9183a1ca93646137955ac64bab9d22e6db0752a1",
|
||||
"comparison", "Float.floatToRawIntBits; no tolerance", "profiles", reports, "adversarial", adversarial,
|
||||
"concurrent_comparisons", concurrentComparisons, "concurrent_workers", 3, "passed", true,
|
||||
"scope", "Real compiled scalar/bulk densities, 4 capacities x 3 seeds (5/20/100 compatibility and new10 using the same parameterized algorithm), signed coordinates, region/coast/height boundaries, context switches and concurrency; no block changes")) + "\n");
|
||||
}
|
||||
|
||||
private static List<DensityVolume> volumes(IslandCapacity capacity) {
|
||||
int limit = capacity.terrainLimit(), middle = (int) capacity.radius() / 2;
|
||||
return List.of(
|
||||
new DensityVolume(16, 384, 16, -8, 0, -8),
|
||||
new DensityVolume(19, 11, 17, -263, 61, -259),
|
||||
new DensityVolume(17, 17, 17, 249, 124, 249),
|
||||
new DensityVolume(13, 21, 11, 761, 167, -9),
|
||||
new DensityVolume(13, 21, 11, -775, 103, 503),
|
||||
new DensityVolume(17, 49, 5, limit - 12, 96, -2),
|
||||
new DensityVolume(17, 49, 5, -limit - 4, 96, -2),
|
||||
new DensityVolume(12, 33, 12, middle - 7, 90, -middle / 2 - 5),
|
||||
new DensityVolume(5, 13, 5, -2, -4, -2),
|
||||
new DensityVolume(5, 13, 5, -2, 378, -2),
|
||||
new DensityVolume(9, 13, 7, -257, 61, -9, 3, 5, 2));
|
||||
}
|
||||
|
||||
private static void same(Pair pair, float expected, float actual, int x, int y, int z, String phase) {
|
||||
if (Float.floatToRawIntBits(expected) != Float.floatToRawIntBits(actual))
|
||||
throw new AssertionError("Density changed from alpha10 in " + phase + ": players=" + pair.players() + ", seed=" + pair.seed()
|
||||
+ ", position=" + x + "," + y + "," + z + ", expected=" + Float.toHexString(expected) + ", actual=" + Float.toHexString(actual));
|
||||
}
|
||||
}
|
||||
+20
-5
@@ -39,7 +39,7 @@ public final class PopulationDiagnostics {
|
||||
var level = helper.getLevel();
|
||||
var generator = (NoiseBasedChunkGenerator) level.getChunkSource().getGenerator();
|
||||
var capacity = IslandCapacity.forGenerator(generator);
|
||||
helper.assertTrue(capacity != null && capacity.players() == Integer.getInteger("sanctuary.test.players", 20),
|
||||
helper.assertTrue(capacity != null && capacity.players() == Integer.getInteger("sanctuary.test.players", 10),
|
||||
"The engine must create the requested production world preset");
|
||||
helper.assertFalse(HydrologyRuntime.enabled(generator) || NaturalHydrologyRuntime.enabled(generator)
|
||||
|| LayeredHydrologyRuntime.enabled(generator) || WoodlandHydrologyRuntime.enabled(generator)
|
||||
@@ -77,6 +77,7 @@ public final class PopulationDiagnostics {
|
||||
int step = Math.max(8, (int) Math.ceil(capacity.terrainLimit() / 32.0));
|
||||
int extent = (int) Math.ceil(capacity.terrainLimit() / (double) step) * step;
|
||||
int solidColumns = 0, outerColumns = 0, emptyColumns = 0;
|
||||
List<int[]> landCoordinates = new ArrayList<>();
|
||||
Set<Integer> bottomHeights = new TreeSet<>();
|
||||
StringBuilder map = new StringBuilder("x,z,bottom_y,top_y\n");
|
||||
for (int x = -extent; x <= extent; x += step) for (int z = -extent; z <= extent; z += step) {
|
||||
@@ -93,6 +94,7 @@ public final class PopulationDiagnostics {
|
||||
}
|
||||
if (top >= 0) {
|
||||
solidColumns++;
|
||||
landCoordinates.add(new int[]{x, z});
|
||||
bottomHeights.add(bottom);
|
||||
if (Math.hypot(x, z) > IslandShape.TERRAIN_LIMIT + 16) outerColumns++;
|
||||
helper.assertTrue(Math.hypot(x, z) < capacity.terrainLimit(), "Terrain stays inside its finite declared boundary");
|
||||
@@ -101,8 +103,20 @@ public final class PopulationDiagnostics {
|
||||
}
|
||||
helper.assertTrue(solidColumns > 100 && emptyColumns > 100 && bottomHeights.size() >= 4,
|
||||
"The sampled island must contain natural masses, holes and a varied underside");
|
||||
helper.assertTrue(capacity.players() == 5 ? outerColumns == 0 : outerColumns > 100,
|
||||
"The larger capacities must actually contain land beyond the old island boundary");
|
||||
int additionalColumns = 0;
|
||||
if (capacity == IslandCapacity.TEN) {
|
||||
var small = level.registryAccess().lookupOrThrow(Registries.DENSITY_FUNCTION).getOrThrow(
|
||||
ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_5"))).value();
|
||||
for (int[] coordinate : landCoordinates) {
|
||||
boolean presentInSmall = false;
|
||||
for (int y = 8; y < 384 && !presentInSmall; y += 8)
|
||||
presentInSmall = random.sampleBlockValueUncached(small, coordinate[0], y, coordinate[1]) > 0;
|
||||
if (!presentInSmall) additionalColumns++;
|
||||
}
|
||||
helper.assertTrue(additionalColumns > 0,
|
||||
"Large must add real land columns compared with Small on the same seed and sample grid");
|
||||
} else helper.assertTrue(capacity.players() == 5 ? outerColumns == 0 : outerColumns > 100,
|
||||
"The historical large capacities must contain land beyond the old island boundary");
|
||||
Path directory = Path.of("diagnostics");
|
||||
Files.createDirectories(directory);
|
||||
String prefix = "population-" + capacity.players() + "-seed-" + level.getSeed();
|
||||
@@ -110,7 +124,8 @@ public final class PopulationDiagnostics {
|
||||
Files.writeString(directory.resolve(prefix + "-density.json"), json(Map.of("players", capacity.players(),
|
||||
"seed", level.getSeed(), "step", step, "solid_columns", solidColumns, "empty_columns", emptyColumns,
|
||||
"solid_columns_beyond_old_boundary", outerColumns, "underside_heights", bottomHeights,
|
||||
"scope", "sparse actual density samples; no resource extrapolation", "passed", true)));
|
||||
"scope", "sparse actual density samples; no resource extrapolation", "passed", true,
|
||||
"additional_columns_vs_small", capacity == IslandCapacity.TEN ? additionalColumns : -1)));
|
||||
SanctuaryMod.LOGGER.info("Capacity {} density seed {}: {} land columns, {} beyond the old envelope",
|
||||
capacity.players(), level.getSeed(), solidColumns, outerColumns);
|
||||
}
|
||||
@@ -253,7 +268,7 @@ public final class PopulationDiagnostics {
|
||||
"ecology_patch_x",bestX,"ecology_patch_z",bestZ)));
|
||||
}
|
||||
helper.assertTrue(!expectedWater.isEmpty(), "The test must inspect retained production water");
|
||||
if (capacity.players() > 5) helper.assertTrue(regions.stream().skip(1).anyMatch(r -> !r.water().cells().isEmpty()),
|
||||
if (capacity.players() > 10) helper.assertTrue(regions.stream().skip(1).anyMatch(r -> !r.water().cells().isEmpty()),
|
||||
"Hydrology must occur on newly available terrain, not only around the old central island");
|
||||
SanctuaryMod.LOGGER.info("Capacity {}: preparing {} bounded FULL chunks",capacity.players(),requested.size());
|
||||
for (long packed : requested) {
|
||||
|
||||
+24
-6
@@ -1,12 +1,16 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationIslandDensity;
|
||||
import fr.koka.sanctuary.worldgen.SanctuarySpawn;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.tags.WorldPresetTags;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.WorldDimensions;
|
||||
import net.minecraft.world.level.levelgen.WorldGenSettings;
|
||||
@@ -19,6 +23,8 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Uses the real 26.3 saved-data writer and reader; never opens the user's saves. */
|
||||
final class PopulationPersistenceDiagnostics {
|
||||
@@ -28,14 +34,25 @@ final class PopulationPersistenceDiagnostics {
|
||||
var presets = access.lookupOrThrow(Registries.WORLD_PRESET);
|
||||
var standard = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET,
|
||||
SanctuaryMod.id("sanctuary"))).value();
|
||||
helper.assertTrue(IslandCapacity.forGenerator(standard.createWorldDimensions().overworld()) == IslandCapacity.TWENTY,
|
||||
"The public Sanctuary preset defaults to 20 planned players");
|
||||
helper.assertTrue(IslandCapacity.forGenerator(standard.createWorldDimensions().overworld()) == IslandCapacity.TEN,
|
||||
"The public Sanctuary preset defaults to the new large geometry");
|
||||
var visible = presets.getOrThrow(WorldPresetTags.NORMAL).stream().map(holder -> holder.getRegisteredName())
|
||||
.filter(name -> name.startsWith("sanctuary:")).collect(Collectors.toSet());
|
||||
helper.assertTrue(visible.equals(Set.of("sanctuary:sanctuary", "sanctuary:sanctuary_5")),
|
||||
"World creation must expose only small and large Sanctuary presets");
|
||||
var oldDensity = (PopulationIslandDensity) access.lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, SanctuaryMod.id("population_20"))).value();
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||
var serialized = PopulationIslandDensity.CODEC.codec().encodeStart(ops, oldDensity).getOrThrow().getAsJsonObject();
|
||||
serialized.remove("players");
|
||||
helper.assertTrue(PopulationIslandDensity.CODEC.codec().parse(ops, serialized).getOrThrow().capacity() == IslandCapacity.TWENTY,
|
||||
"An omitted alpha10 density field must keep its historical default, independently of the new world selector");
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
Path testRoot = Path.of("diagnostics", "population-persistence-seed-" + level.getSeed()).toAbsolutePath();
|
||||
Files.createDirectories(testRoot);
|
||||
LevelStorageSource storage = LevelStorageSource.createDefault(testRoot);
|
||||
for (int players : new int[]{5, 20, 100}) {
|
||||
String presetId = players == 20 ? "sanctuary" : "sanctuary_" + players;
|
||||
for (int players : new int[]{5, 10, 20, 100}) {
|
||||
String presetId = players == 10 ? "sanctuary" : "sanctuary_" + players;
|
||||
WorldDimensions dimensions = presets.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET,
|
||||
SanctuaryMod.id(presetId))).value().createWorldDimensions();
|
||||
helper.assertTrue(IslandCapacity.forGenerator(dimensions.overworld()) == IslandCapacity.fromPlayers(players),
|
||||
@@ -68,7 +85,8 @@ final class PopulationPersistenceDiagnostics {
|
||||
"An existing alpha.9 settings reference must not acquire the new default capacity");
|
||||
}
|
||||
Files.writeString(testRoot.resolve("round-trip.json"), new GsonBuilder().setPrettyPrinting().create().toJson(Map.of(
|
||||
"seed", level.getSeed(), "default_players", 20, "profiles", results,
|
||||
"legacy_settings_preserved", "sanctuary:sanctuary_cavern", "passed", true)) + "\n");
|
||||
"seed", level.getSeed(), "default_players", 10, "profiles", results,
|
||||
"legacy_settings_preserved", "sanctuary:sanctuary_cavern", "visible_presets", visible,
|
||||
"omitted_density_capacity_preserved", 20, "passed", true)) + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -32,6 +32,12 @@ import net.minecraft.world.level.storage.LevelData;
|
||||
public final class SanctuaryWorldGameTests {
|
||||
public static LevelData.RespawnData initialSpawn;
|
||||
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void optimizedDensityPreservesSavedTerrain(GameTestHelper helper) throws Exception {
|
||||
PopulationDensityEquivalenceDiagnostics.verify(helper);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 200)
|
||||
public void populationChoiceSurvivesSavingTheWorld(GameTestHelper helper) throws java.io.IOException {
|
||||
PopulationPersistenceDiagnostics.verify(helper);
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.worldgen.IslandCapacity;
|
||||
import fr.koka.sanctuary.worldgen.PopulationHydrologyRuntime;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
/** Opt-in, repeatable server generation workload; it does not measure client rendering or FPS. */
|
||||
public final class WorldgenPerformanceGameTests {
|
||||
@GameTest(maxTicks = 3000)
|
||||
public void spawnAreaAndExploration(GameTestHelper helper) throws Exception {
|
||||
ServerLevel level = helper.getLevel();
|
||||
var capacity = IslandCapacity.forGenerator(level.getChunkSource().getGenerator());
|
||||
var spawn = SanctuaryWorldGameTests.initialSpawn.pos();
|
||||
int cx = spawn.getX() >> 4, cz = spawn.getZ() >> 4;
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("version", FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow()
|
||||
.getMetadata().getVersion().getFriendlyString());
|
||||
report.put("players", capacity.players()); report.put("seed", level.getSeed());
|
||||
report.put("spawn", List.of(spawn.getX(), spawn.getY(), spawn.getZ()));
|
||||
report.put("java", System.getProperty("java.version"));
|
||||
report.put("processors", Runtime.getRuntime().availableProcessors());
|
||||
report.put("max_heap_bytes", Runtime.getRuntime().maxMemory());
|
||||
List<Map<String, Object>> rings = new ArrayList<>();
|
||||
List<Long> chunkTimes = new ArrayList<>();
|
||||
int count = 0, land = 0;
|
||||
long areaStart = System.nanoTime();
|
||||
// Centre outwards, 17x17 FULL chunks: a fixed eight-chunk radius around natural spawn.
|
||||
for (int radius = 0; radius <= 8; radius++) {
|
||||
long start = System.nanoTime();
|
||||
for (int x = -radius; x <= radius; x++) for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
long before = System.nanoTime();
|
||||
var chunk = full(helper, level, cx + x, cz + z);
|
||||
chunkTimes.add(System.nanoTime() - before);
|
||||
count++;
|
||||
if (java.util.Arrays.stream(chunk.getSections()).anyMatch(s -> !s.hasOnlyAir())) land++;
|
||||
}
|
||||
long elapsed = System.nanoTime() - start;
|
||||
rings.add(Map.of("radius", radius, "elapsed_ms", millis(elapsed), "cumulative_chunks", count));
|
||||
SanctuaryMod.LOGGER.info("Generation benchmark {} seed {} ring {}: {} FULL chunks, {} ms",
|
||||
capacity.players(), level.getSeed(), radius, count, millis(elapsed));
|
||||
}
|
||||
report.put("spawn_area_ms", millis(System.nanoTime() - areaStart));
|
||||
report.put("spawn_area_full_chunks", count); report.put("spawn_area_nonempty_chunks", land);
|
||||
report.put("rings", rings); report.put("spawn_area_chunk_latency_ms", latency(chunkTimes));
|
||||
|
||||
// Continue east through a regional boundary, with a five-chunk-wide corridor.
|
||||
// This exposes stalls during exploration instead of timing only the initial region.
|
||||
List<Map<String, Object>> steps = new ArrayList<>();
|
||||
chunkTimes.clear();
|
||||
long exploreStart = System.nanoTime();
|
||||
for (int x = 9; x <= 24; x++) {
|
||||
long start = System.nanoTime();
|
||||
for (int z = -2; z <= 2; z++) {
|
||||
long before = System.nanoTime();
|
||||
full(helper, level, cx + x, cz + z);
|
||||
chunkTimes.add(System.nanoTime() - before);
|
||||
}
|
||||
long elapsed = System.nanoTime() - start;
|
||||
steps.add(Map.of("chunk_x", cx + x, "elapsed_ms", millis(elapsed)));
|
||||
SanctuaryMod.LOGGER.info("Generation benchmark {} seed {} exploration column {}: {} ms",
|
||||
capacity.players(), level.getSeed(), cx + x, millis(elapsed));
|
||||
}
|
||||
report.put("exploration_ms", millis(System.nanoTime() - exploreStart));
|
||||
report.put("exploration_full_chunks", 80); report.put("exploration_steps", steps);
|
||||
report.put("exploration_chunk_latency_ms", latency(chunkTimes));
|
||||
report.put("regions_cached", PopulationHydrologyRuntime.cachedRegionCount(level.getChunkSource().randomState()));
|
||||
report.put("scope", "Fresh development world; 289 FULL chunks around natural spawn, then 80 eastward FULL chunks. "
|
||||
+ "Synchronous server requests include dependent generation up to FULL, not guaranteed ticking readiness, client GPU, network, FPS or player load. "
|
||||
+ "World creation before the test is timed separately by Minecraft's startup log.");
|
||||
helper.assertTrue(count == 289 && land > 100, "The benchmark must generate actual playable terrain");
|
||||
report.put("passed", true);
|
||||
Path output = Path.of("diagnostics", "generation-performance-players-" + capacity.players() + "-seed-" + level.getSeed() + ".json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static LevelChunk full(GameTestHelper helper, ServerLevel level, int x, int z) {
|
||||
var chunk = level.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true);
|
||||
helper.assertTrue(chunk instanceof LevelChunk, "Benchmark requires completed chunk " + new ChunkPos(x, z));
|
||||
return (LevelChunk) chunk;
|
||||
}
|
||||
|
||||
private static double millis(long nanos) { return nanos / 1_000_000.0; }
|
||||
|
||||
private static Map<String, Double> latency(List<Long> times) {
|
||||
var sorted = times.stream().sorted().toList();
|
||||
return Map.of("median", millis(sorted.get(sorted.size() / 2)),
|
||||
"p95", millis(sorted.get((int) Math.ceil(sorted.size() * .95) - 1)),
|
||||
"max", millis(sorted.getLast()));
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -32,12 +32,13 @@ public abstract class SanctuaryGameTestServerMixin {
|
||||
@Redirect(method = "lambda$create$1", at = @At(value = "FIELD", target =
|
||||
"Lnet/minecraft/world/level/levelgen/presets/WorldPresets;FLAT_ALL_DIMENSIONS:Lnet/minecraft/resources/ResourceKey;"))
|
||||
private static ResourceKey<WorldPreset> sanctuary$selectActualPreset() {
|
||||
int players = Integer.getInteger("sanctuary.test.players", 20);
|
||||
int players = Integer.getInteger("sanctuary.test.players", 10);
|
||||
String preset = switch (players) {
|
||||
case 5 -> "sanctuary_5";
|
||||
case 20 -> "sanctuary";
|
||||
case 10 -> "sanctuary";
|
||||
case 20 -> "sanctuary_20";
|
||||
case 100 -> "sanctuary_100";
|
||||
default -> throw new IllegalArgumentException("Test capacity must be 5, 20 or 100");
|
||||
default -> throw new IllegalArgumentException("Test geometry must be 5, 10, 20 or 100");
|
||||
};
|
||||
return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id(preset));
|
||||
}
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.Interval;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.util.context.ContextKey;
|
||||
import net.minecraft.util.context.ContextMap;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.*;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.InterpolatedFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.UnaryFunction;
|
||||
import net.minecraft.world.level.levelgen.synth.Noise;
|
||||
import net.minecraft.world.level.levelgen.synth.NormalNoise;
|
||||
|
||||
/** Adversarial checks against the actual vanilla scalar implementation, requiring no chunks. */
|
||||
public final class ExactInterpolationChecks {
|
||||
private static final ContextKey<Float> SHIFT = ContextKey.vanilla("sanctuary_test_interpolation_shift");
|
||||
private static final ContextKey<float[]> MUTABLE = ContextKey.vanilla("sanctuary_test_mutable_interpolation_shift");
|
||||
private static final float[] NO_MUTATION = {0};
|
||||
private static final DensityFunction.CompileContext COMPILE = new DensityFunction.CompileContext() {
|
||||
@Override public Noise createNoiseSampler(Holder<NormalNoise> noise) { throw new AssertionError("No noise needed"); }
|
||||
@Override public RandomSource createRandom(Identifier id) { throw new AssertionError("No random needed"); }
|
||||
@Override public RandomSource createEndIslandRandom() { throw new AssertionError("No End needed"); }
|
||||
};
|
||||
|
||||
private ExactInterpolationChecks() {}
|
||||
|
||||
public static Map<String, Object> verify() throws Exception {
|
||||
long checked = 0;
|
||||
for (int[] sizes : new int[][]{{8, 4}, {3, 5}, {1, 1}}) {
|
||||
var input = new Probe(31);
|
||||
DensityFunction expression = expression(input, sizes[0], sizes[1]);
|
||||
var reference = expression.compileSampler(COMPILE);
|
||||
var cached = cached(input, sizes[0], sizes[1]);
|
||||
SamplerContext first = context(0), second = context(0.0625f);
|
||||
var points = new ArrayList<int[]>();
|
||||
for (int x = -13; x <= 13; x++) for (int y = -7; y <= 11; y++) for (int z = -9; z <= 9; z++)
|
||||
points.add(new int[]{x, y, z});
|
||||
Collections.shuffle(points, new Random(73));
|
||||
for (var context : List.of(first, second, first)) for (var p : points) {
|
||||
same(reference.sampleValue(context, p[0], p[1], p[2]), cached.sampleValue(context, p[0], p[1], p[2]), "scalar/context");
|
||||
checked++;
|
||||
}
|
||||
var volume = new DensityVolume(13, 17, 11, -19, -9, -17, 3, 2, 5);
|
||||
DensityBuffer actual = DensityBuffer.createUnpooled(volume.size());
|
||||
cached.sampleVolume(first, actual, volume);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++) {
|
||||
same(reference.sampleValue(first, volume.blockX(x), volume.blockY(y), volume.blockZ(z)),
|
||||
actual.get(volume.indexUnchecked(x, y, z)), "non-unit volume");
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
|
||||
// A scalar and a bulk input intentionally differ in Probe. This catches accidental
|
||||
// replacement of the aligned scalar branch with one of the cached lattice corners.
|
||||
var counted = new Probe(91);
|
||||
var cached = cached(counted, 8, 4);
|
||||
var stable = context(0);
|
||||
for (int repeat = 0; repeat < 2; repeat++)
|
||||
for (int x = 1; x < 8; x++) for (int y = 1; y < 4; y++) for (int z = 1; z < 8; z++) cached.sampleValue(stable, x, y, z);
|
||||
require(counted.volumeCalls.get() == 1, "A reused cell must sample its eight corners only once");
|
||||
int alignedBefore = counted.scalarCalls.get();
|
||||
cached.sampleValue(stable, 0, 0, 0);
|
||||
require(counted.scalarCalls.get() == alignedBefore + 1, "Aligned nodes retain their separate scalar input path");
|
||||
|
||||
var reference = expression(counted, 8, 4).compileSampler(COMPILE);
|
||||
// More distinct cells than the bounded cache holds; collisions and replacement must
|
||||
// remain invisible to both forward and reverse traversals.
|
||||
for (int direction : new int[]{1, -1}) for (int index = 0; index < 9000; index++) {
|
||||
int i = direction > 0 ? index : 8999 - index;
|
||||
int x = (i % 113 - 56) * 8 + 1, y = ((i / 113) % 97 - 48) * 4 + 1, z = (i % 67 - 33) * 8 + 3;
|
||||
same(reference.sampleValue(stable, x, y, z), cached.sampleValue(stable, x, y, z), "cache replacement");
|
||||
checked++;
|
||||
}
|
||||
var other = cached(new Probe(999), 8, 4);
|
||||
var otherReference = expression(new Probe(999), 8, 4).compileSampler(COMPILE);
|
||||
for (int i = 0; i < 128; i++) {
|
||||
same(reference.sampleValue(stable, i - 67, 21, -3), cached.sampleValue(stable, i - 67, 21, -3), "sampler identity A");
|
||||
same(otherReference.sampleValue(stable, i - 67, 21, -3), other.sampleValue(stable, i - 67, 21, -3), "sampler identity B");
|
||||
checked += 2;
|
||||
}
|
||||
try (var executor = Executors.newFixedThreadPool(3)) {
|
||||
List<Callable<Integer>> tasks = new ArrayList<>();
|
||||
for (int worker = 0; worker < 3; worker++) {
|
||||
final int id = worker;
|
||||
tasks.add(() -> {
|
||||
var local = context(id * 0.03125f);
|
||||
for (int i = 0; i < 3000; i++) {
|
||||
int x = (i * 17 + id) % 251 - 125, y = i % 384, z = (i * 37 - id) % 257 - 128;
|
||||
same(reference.sampleValue(local, x, y, z), cached.sampleValue(local, x, y, z), "concurrent shared sampler");
|
||||
}
|
||||
return 3000;
|
||||
});
|
||||
}
|
||||
for (var result : executor.invokeAll(tasks)) checked += result.get();
|
||||
}
|
||||
var plain = new ProbeFunction(new Probe(42));
|
||||
require(ExactInterpolationSampler.compileTerrain(plain, COMPILE) == plain.probe,
|
||||
"Unrecognized expressions must preserve their original sampler");
|
||||
require(!ExactInterpolationSampler.supportsTerrain(expression(new Probe(42), 8, 4)),
|
||||
"An opaque/contextual input must not be cached merely because its outer expression matches");
|
||||
float[] payload = {0};
|
||||
var mutable = SamplerContext.builder().setUserFields(ContextMap.builder().set(MUTABLE, payload).build()).build();
|
||||
var fallback = ExactInterpolationSampler.compileTerrain(expression(new Probe(42), 8, 4), COMPILE);
|
||||
float before = fallback.sampleValue(mutable, 1, 2, 3);
|
||||
payload[0] = 0.125f;
|
||||
float after = fallback.sampleValue(mutable, 1, 2, 3);
|
||||
require(Float.floatToRawIntBits(before) != Float.floatToRawIntBits(after),
|
||||
"The fallback must observe mutable context payloads even when context identity stays the same");
|
||||
|
||||
var nestedInput = new Probe(817);
|
||||
var nested = cached(nestedInput, 8, 4);
|
||||
var nestedReference = expression(new Probe(817), 8, 4).compileSampler(COMPILE);
|
||||
var outerInput = new Probe(83);
|
||||
outerInput.nested = nested;
|
||||
var outer = cached(outerInput, 8, 4);
|
||||
var outerReference = expression(outerInput, 8, 4).compileSampler(COMPILE);
|
||||
for (int i = 0; i < 128; i++) {
|
||||
int x = i - 65;
|
||||
same(outerReference.sampleValue(stable, x, 23, -7), outer.sampleValue(stable, x, 23, -7), "nested outer request");
|
||||
same(nestedReference.sampleValue(stable, x, 23, -7), nested.sampleValue(stable, x, 23, -7), "nested cache identity");
|
||||
checked += 2;
|
||||
}
|
||||
return Map.of("bitwise_comparisons", checked, "repeated_cell_corner_batches", 1,
|
||||
"concurrent_workers", 3, "aligned_scalar_path_preserved", true, "passed", true);
|
||||
}
|
||||
|
||||
private static DensityFunction expression(Probe probe, int xz, int y) {
|
||||
return new UnaryFunction(UnaryFunction.Type.SQUEEZE, new InterpolatedFunction(new ProbeFunction(probe), xz, y));
|
||||
}
|
||||
|
||||
private static DensitySampler cached(Probe probe, int xz, int y) {
|
||||
return new UnaryFunction.SqueezeSampler(new ExactInterpolationSampler(probe, xz, y));
|
||||
}
|
||||
|
||||
private static SamplerContext context(float shift) {
|
||||
return SamplerContext.builder().setUserFields(ContextMap.builder().set(SHIFT, shift).build()).build();
|
||||
}
|
||||
|
||||
private record ProbeFunction(Probe probe) implements DensityFunction {
|
||||
@Override public DensitySampler compileSampler(CompileContext context) { return probe; }
|
||||
@Override public DensityFunction rewriteChildren(DfRewriteRule rule) { return this; }
|
||||
@Override public Interval range() { return Interval.of(-2, 2); }
|
||||
@Override public int domainAxes() { return ALL_AXES; }
|
||||
@Override public MapCodec<ProbeFunction> codec() { return MapCodec.unit(this); }
|
||||
}
|
||||
|
||||
private static final class Probe implements DensitySampler {
|
||||
final int seed;
|
||||
final AtomicInteger scalarCalls = new AtomicInteger(), volumeCalls = new AtomicInteger();
|
||||
DensitySampler nested;
|
||||
Probe(int seed) { this.seed = seed; }
|
||||
private float value(SamplerContext context, int x, int y, int z) {
|
||||
int hash = (x * 73428767 ^ y * 912931 ^ z * 4382893 ^ seed);
|
||||
hash ^= hash >>> 15;
|
||||
float[] mutable = context.getFieldOrDefault(MUTABLE, NO_MUTATION);
|
||||
return Float.intBitsToFloat(0x3e000000 | (hash & 0x007fffff)) - 0.19f
|
||||
+ context.getFieldOrDefault(SHIFT, 0f) + mutable[0];
|
||||
}
|
||||
@Override public float sampleValue(SamplerContext context, int x, int y, int z) {
|
||||
scalarCalls.incrementAndGet();
|
||||
return value(context, x, y, z) + 0.03125f;
|
||||
}
|
||||
@Override public void sampleVolume(SamplerContext context, DensityBuffer buffer, DensityVolume volume) {
|
||||
volumeCalls.incrementAndGet();
|
||||
if (nested != null) nested.sampleValue(context, volume.minBlockX() + 1, volume.minBlockY() + 1, volume.minBlockZ() + 1);
|
||||
for (int z = 0; z < volume.sizeZ(); z++) for (int x = 0; x < volume.sizeX(); x++) for (int y = 0; y < volume.sizeY(); y++)
|
||||
buffer.set(volume.indexUnchecked(x, y, z), value(context, volume.blockX(x), volume.blockY(y), volume.blockZ(z)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void same(float expected, float actual, String label) {
|
||||
if (Float.floatToRawIntBits(expected) != Float.floatToRawIntBits(actual))
|
||||
throw new AssertionError(label + ": expected " + Float.toHexString(expected) + ", actual " + Float.toHexString(actual));
|
||||
}
|
||||
private static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"environment": "*",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"entrypoints": {
|
||||
"fabric-gametest": ["fr.koka.sanctuary.gametest.SanctuaryWorldGameTests"],
|
||||
"fabric-gametest": ["${gametest_entrypoint}"],
|
||||
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
|
||||
},
|
||||
"mixins": ["sanctuary-gametest.mixins.json"],
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityBuffer;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.DfRewriteRule;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.generator.ConstantFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.generator.GradientFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.generator.NoiseFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.BinaryFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.ClampFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.InterpolatedFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.LerpFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.SliceFunction;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.op.UnaryFunction;
|
||||
import net.minecraft.world.level.levelgen.synth.BlendedNoise;
|
||||
|
||||
/** Reuses lattice corners without changing 26.3-pre-2's scalar interpolation arithmetic.
|
||||
* Vanilla's bulk interpolation changes operation order and accumulates Y increments, so it
|
||||
* cannot replace scalar sampling in a generator whose existing worlds must remain exact. */
|
||||
public final class ExactInterpolationSampler implements DensitySampler {
|
||||
private static final int CACHE_SIZE = 4096;
|
||||
// One numeric cache per worker, not one cache per world. Both identities are weak:
|
||||
// neither a sampler's captured noise graph nor a context's world fields are retained.
|
||||
private static final ThreadLocal<CornerCache> CORNERS = ThreadLocal.withInitial(CornerCache::new);
|
||||
|
||||
private final DensitySampler input;
|
||||
private final int cellSizeXz;
|
||||
private final int cellSizeY;
|
||||
|
||||
ExactInterpolationSampler(DensitySampler input, int cellSizeXz, int cellSizeY) {
|
||||
this.input = input;
|
||||
this.cellSizeXz = cellSizeXz;
|
||||
this.cellSizeY = cellSizeY;
|
||||
}
|
||||
|
||||
/** Only the known Population terrain expression is optimized. Other data-pack expressions
|
||||
* retain their own sampler, and the squeeze operation remains the actual vanilla class. */
|
||||
public static DensitySampler compileTerrain(DensityFunction terrain, DensityFunction.CompileContext context) {
|
||||
if (supportsTerrain(terrain)) {
|
||||
var interpolated = (InterpolatedFunction) ((UnaryFunction) terrain).input();
|
||||
return new UnaryFunction.SqueezeSampler(new ExactInterpolationSampler(
|
||||
interpolated.input().compileSampler(context), interpolated.cellSizeXz(), interpolated.cellSizeY()));
|
||||
}
|
||||
return terrain.compileSampler(context);
|
||||
}
|
||||
|
||||
/** Also used by the engine equivalence check to prove the shipped graphs take this path. */
|
||||
public static boolean supportsTerrain(DensityFunction terrain) {
|
||||
return terrain instanceof UnaryFunction unary && unary.type() == UnaryFunction.Type.SQUEEZE
|
||||
&& unary.input() instanceof InterpolatedFunction interpolated && coordinateOnly(interpolated.input(), 0);
|
||||
}
|
||||
|
||||
private static boolean coordinateOnly(DensityFunction function, int depth) {
|
||||
if (depth > 64) return false;
|
||||
function = DfRewriteRule.INLINE_REFERENCE.rewrite(function);
|
||||
if (function instanceof ConstantFunction || function instanceof GradientFunction || function instanceof BlendedNoise) return true;
|
||||
if (function instanceof NoiseFunction noise) return coordinateOnly(noise.shiftX(), depth + 1)
|
||||
&& coordinateOnly(noise.shiftY(), depth + 1) && coordinateOnly(noise.shiftZ(), depth + 1);
|
||||
if (function instanceof BinaryFunction binary) return coordinateOnly(binary.left(), depth + 1)
|
||||
&& coordinateOnly(binary.right(), depth + 1);
|
||||
if (function instanceof LerpFunction lerp) return coordinateOnly(lerp.alpha(), depth + 1)
|
||||
&& coordinateOnly(lerp.first(), depth + 1) && coordinateOnly(lerp.second(), depth + 1);
|
||||
if (function instanceof UnaryFunction unary) return coordinateOnly(unary.input(), depth + 1);
|
||||
if (function instanceof ClampFunction clamp) return coordinateOnly(clamp.input(), depth + 1);
|
||||
if (function instanceof SliceFunction slice) return coordinateOnly(slice.input(), depth + 1);
|
||||
// Contextual blending, prepared caches and third-party density functions are deliberately
|
||||
// opaque. Identity of a SamplerContext does not prove that its payload cannot mutate.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public float sampleValue(SamplerContext context, int x, int y, int z) {
|
||||
int offsetX = Math.floorMod(x, cellSizeXz);
|
||||
int offsetY = Math.floorMod(y, cellSizeY);
|
||||
int offsetZ = Math.floorMod(z, cellSizeXz);
|
||||
// The vanilla aligned path is scalar; a lattice value from sampleVolume is not
|
||||
// assumed to have the same bits as this separate input.sampleValue path.
|
||||
if (offsetX == 0 && offsetY == 0 && offsetZ == 0) return input.sampleValue(context, x, y, z);
|
||||
|
||||
int originX = x - offsetX, originY = y - offsetY, originZ = z - offsetZ;
|
||||
CornerCache cache = CORNERS.get();
|
||||
cache.select(this, context);
|
||||
int slot = cache.slot(originX, originY, originZ);
|
||||
if (!cache.contains(slot, originX, originY, originZ)) {
|
||||
DensityVolume lattice = new DensityVolume(2, 2, 2, originX, originY, originZ,
|
||||
cellSizeXz, cellSizeY, cellSizeXz);
|
||||
try (var buffer = context.acquireBuffer(lattice)) {
|
||||
// Keep the precise 2x2x2 request used by InterpolatedFunction.sampleValue:
|
||||
// batching a larger lattice can change the input noise's rounding too.
|
||||
input.sampleVolume(context, buffer, lattice);
|
||||
// A nested input sampler could itself use this thread's cache. Restore our
|
||||
// identity after it returns, before publishing our corners into the cache.
|
||||
cache.select(this, context);
|
||||
int first = slot * 8;
|
||||
for (int i = 0; i < 8; i++) cache.values[first + i] = buffer.get(i);
|
||||
cache.store(slot, originX, originY, originZ);
|
||||
}
|
||||
}
|
||||
int first = slot * 8;
|
||||
// DensityVolume packs Y fastest, then X, then Z. lerp3 expects the corners in
|
||||
// X, Y, Z order; retain vanilla's float divisions, not reciprocal multiplication.
|
||||
return Mth.lerp3((float) offsetX / cellSizeXz, (float) offsetY / cellSizeY, (float) offsetZ / cellSizeXz,
|
||||
cache.values[first], cache.values[first + 2], cache.values[first + 1], cache.values[first + 3],
|
||||
cache.values[first + 4], cache.values[first + 6], cache.values[first + 5], cache.values[first + 7]);
|
||||
}
|
||||
|
||||
@Override public void sampleVolume(SamplerContext context, DensityBuffer buffer, DensityVolume volume) {
|
||||
DensitySampler.sampleVolumeNaive(context, buffer, volume, this);
|
||||
}
|
||||
|
||||
/** Direct-mapped and bounded: 192 KiB of primitive arrays per participating thread.
|
||||
* A collision only recomputes a cell; it never substitutes a different cell's corners. */
|
||||
private static final class CornerCache {
|
||||
private WeakReference<ExactInterpolationSampler> sampler = new WeakReference<>(null);
|
||||
private WeakReference<SamplerContext> context = new WeakReference<>(null);
|
||||
private final int[] xs = new int[CACHE_SIZE], ys = new int[CACHE_SIZE], zs = new int[CACHE_SIZE];
|
||||
private final int[] generations = new int[CACHE_SIZE];
|
||||
private final float[] values = new float[CACHE_SIZE * 8];
|
||||
private int generation;
|
||||
|
||||
void select(ExactInterpolationSampler selected, SamplerContext selectedContext) {
|
||||
if (sampler.get() == selected && context.get() == selectedContext) return;
|
||||
sampler = new WeakReference<>(selected);
|
||||
context = new WeakReference<>(selectedContext);
|
||||
if (++generation == 0) {
|
||||
Arrays.fill(generations, 0);
|
||||
generation = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int slot(int x, int y, int z) {
|
||||
int hash = x * 0x9e3779b9 ^ Integer.rotateLeft(y * 0x85ebca6b, 11)
|
||||
^ Integer.rotateLeft(z * 0xc2b2ae35, 22);
|
||||
hash = (hash ^ (hash >>> 16)) * 0x7feb352d;
|
||||
return (hash ^ (hash >>> 15)) & (CACHE_SIZE - 1);
|
||||
}
|
||||
|
||||
boolean contains(int slot, int x, int y, int z) {
|
||||
return generations[slot] == generation && xs[slot] == x && ys[slot] == y && zs[slot] == z;
|
||||
}
|
||||
|
||||
void store(int slot, int x, int y, int z) {
|
||||
xs[slot] = x; ys[slot] = y; zs[slot] = z;
|
||||
generations[slot] = generation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,14 @@ import com.mojang.serialization.DataResult;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
|
||||
/** Versioned alpha.10 world geometry. Capacity describes area, not a login limit. */
|
||||
/** Saved geometry references. Small/large use FIVE/TEN; TWENTY/HUNDRED remain loadable. */
|
||||
public enum IslandCapacity {
|
||||
FIVE(5, 256), TWENTY(20, 512), HUNDRED(100, 256 * Math.sqrt(20));
|
||||
FIVE(5, 256), TEN(10, 256 * Math.sqrt(2)), TWENTY(20, 512), HUNDRED(100, 256 * Math.sqrt(20));
|
||||
|
||||
public static final int REGION_SIZE = 512;
|
||||
public static final Codec<IslandCapacity> CODEC = Codec.INT.comapFlatMap(players -> {
|
||||
for (var capacity : values()) if (capacity.players == players) return DataResult.success(capacity);
|
||||
return DataResult.error(() -> "Island capacity must be 5, 20 or 100");
|
||||
return DataResult.error(() -> "Unknown saved island size; supported references are 5, 10, 20 and 100");
|
||||
}, IslandCapacity::players);
|
||||
private final int players;
|
||||
private final double radius;
|
||||
@@ -22,12 +22,13 @@ public enum IslandCapacity {
|
||||
public int terrainLimit() { return (int) Math.ceil(radius + 32); }
|
||||
public static IslandCapacity fromPlayers(int players) {
|
||||
for (var capacity : values()) if (capacity.players == players) return capacity;
|
||||
throw new IllegalArgumentException("Island capacity must be 5, 20 or 100");
|
||||
throw new IllegalArgumentException("Unknown saved island size; supported references are 5, 10, 20 and 100");
|
||||
}
|
||||
public static IslandCapacity forGenerator(ChunkGenerator generator) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise)) return null;
|
||||
var settings = noise.generatorSettings();
|
||||
if (settings.is(SanctuarySpawn.POPULATION_5_SETTINGS)) return FIVE;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_10_SETTINGS)) return TEN;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_20_SETTINGS)) return TWENTY;
|
||||
if (settings.is(SanctuarySpawn.POPULATION_100_SETTINGS)) return HUNDRED;
|
||||
return null;
|
||||
|
||||
+12
-13
@@ -10,52 +10,51 @@ import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
|
||||
/** Alpha.10 plans only regions touched by generation or a requested protection volume. */
|
||||
public final class PopulationHydrologyRuntime {
|
||||
public record RegionPlan(IslandCapacity.Region region, PopulationHydrology.Plan water, PopulationLavaDeposit.Plan lava) {}
|
||||
private static final Map<RandomState, Map<IslandCapacity.Region, RegionPlan>> WORLDS =
|
||||
private static final Map<RandomState, PopulationPlanCache<IslandCapacity.Region, RegionPlan>> WORLDS =
|
||||
Collections.synchronizedMap(new WeakHashMap<>());
|
||||
private PopulationHydrologyRuntime() {}
|
||||
public static boolean enabled(NoiseBasedChunkGenerator generator) { return IslandCapacity.forGenerator(generator) != null; }
|
||||
public static IslandCapacity capacity(NoiseBasedChunkGenerator generator) {
|
||||
return Objects.requireNonNull(IslandCapacity.forGenerator(generator), "Population generation is not enabled");
|
||||
}
|
||||
private static Map<IslandCapacity.Region, RegionPlan> regions(RandomState state) {
|
||||
synchronized (WORLDS) { return WORLDS.computeIfAbsent(state, ignored -> new LinkedHashMap<>()); }
|
||||
private static PopulationPlanCache<IslandCapacity.Region, RegionPlan> regions(RandomState state) {
|
||||
synchronized (WORLDS) { return WORLDS.computeIfAbsent(state, ignored -> new PopulationPlanCache<>()); }
|
||||
}
|
||||
public static int cachedRegionCount(RandomState state) {
|
||||
Map<IslandCapacity.Region, RegionPlan> regions;
|
||||
PopulationPlanCache<IslandCapacity.Region, RegionPlan> regions;
|
||||
synchronized (WORLDS) { regions = WORLDS.get(state); }
|
||||
if (regions == null) return 0;
|
||||
synchronized (regions) { return regions.size(); }
|
||||
return regions.size();
|
||||
}
|
||||
public static RegionPlan regionPlan(NoiseBasedChunkGenerator generator, RandomState state, IslandCapacity.Region region) {
|
||||
var capacity = capacity(generator);
|
||||
var regions = regions(state);
|
||||
// One build per world at a time bounds the temporary sign-mask memory. No recursive
|
||||
// neighbouring plan is needed: each accepted assembly owns its complete protected extent.
|
||||
synchronized (regions) {
|
||||
RegionPlan existing = regions.get(region);
|
||||
if (existing != null) return existing;
|
||||
// Only cache misses wait for the bounded planner. Completed regions stay readable
|
||||
// while another region is being computed; no neighbouring plan is built recursively.
|
||||
return regions.getOrCreate(region, () -> {
|
||||
long start = System.nanoTime();
|
||||
var density = generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||
var sampler = state.getSampler(density);
|
||||
var memo = new SignMemo((x, y, z) -> {
|
||||
long wx = (long) x + region.originX(), wz = (long) z + region.originZ();
|
||||
if (Math.hypot((double) wx, wz) >= capacity.terrainLimit()) return -1;
|
||||
return state.sampleBlockValueUncached(density, (int) wx, y, (int) wz);
|
||||
return sampler.sampleValue(SamplerContext.EMPTY_UNCACHED, (int) wx, y, (int) wz);
|
||||
});
|
||||
long seed = PopulationIslandDensity.regionSeed(state.seed(), region);
|
||||
var water = PopulationHydrology.create(seed, memo).admitted();
|
||||
var lava = PopulationLavaDeposit.create(seed, memo, water).admitted();
|
||||
var result = new RegionPlan(region, water.translated(region.originX(), region.originZ()),
|
||||
lava.translated(region.originX(), region.originZ()));
|
||||
regions.put(region, result);
|
||||
SanctuaryMod.LOGGER.info("Population {} seed {} region {},{}: {} features, {} water columns, {} lava sources, {} density probes, {} ms",
|
||||
capacity.players(), state.seed(), region.x(), region.z(), water.features().size(), water.cells().size(),
|
||||
lava.lavaCells().size(), memo.samples, (System.nanoTime() - start) / 1_000_000);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
public static RegionPlan regionPlan(ServerLevel level, IslandCapacity.Region region) {
|
||||
return regionPlan((NoiseBasedChunkGenerator) level.getChunkSource().getGenerator(), level.getChunkSource().randomState(), region);
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ public record PopulationIslandDensity(DensityFunction terrain, DensityFunction d
|
||||
).apply(instance, PopulationIslandDensity::new));
|
||||
|
||||
@Override public DensitySampler compileSampler(CompileContext context) {
|
||||
var raw = terrain.compileSampler(context); var edge = distortion.compileSampler(context);
|
||||
var raw = ExactInterpolationSampler.compileTerrain(terrain, context); var edge = distortion.compileSampler(context);
|
||||
var coarse = sculpt.compileSampler(context); var fine = detail.compileSampler(context);
|
||||
var bottom = underside.compileSampler(context); var warp = riftWarp.compileSampler(context);
|
||||
var fractureDetail = riftDetail.compileSampler(context);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Completed plans remain readable while one new region uses temporary planning memory. */
|
||||
final class PopulationPlanCache<K, V> {
|
||||
private final ConcurrentHashMap<K, V> completed = new ConcurrentHashMap<>();
|
||||
|
||||
V getOrCreate(K key, Supplier<V> create) {
|
||||
V value = completed.get(key);
|
||||
if (value != null) return value;
|
||||
synchronized (this) {
|
||||
value = completed.get(key);
|
||||
if (value == null) {
|
||||
value = Objects.requireNonNull(create.get());
|
||||
completed.put(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
int size() { return completed.size(); }
|
||||
}
|
||||
@@ -38,6 +38,8 @@ public final class SanctuarySpawn {
|
||||
|
||||
public static final ResourceKey<NoiseGeneratorSettings> POPULATION_5_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_5"));
|
||||
public static final ResourceKey<NoiseGeneratorSettings> POPULATION_10_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_10"));
|
||||
public static final ResourceKey<NoiseGeneratorSettings> POPULATION_20_SETTINGS =
|
||||
ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("population_20"));
|
||||
public static final ResourceKey<NoiseGeneratorSettings> POPULATION_100_SETTINGS =
|
||||
@@ -56,6 +58,7 @@ public final class SanctuarySpawn {
|
||||
|| generator.generatorSettings().is(RIFT_SETTINGS)
|
||||
|| generator.generatorSettings().is(CAVERN_SETTINGS)
|
||||
|| generator.generatorSettings().is(POPULATION_5_SETTINGS)
|
||||
|| generator.generatorSettings().is(POPULATION_10_SETTINGS)
|
||||
|| generator.generatorSettings().is(POPULATION_20_SETTINGS)
|
||||
|| generator.generatorSettings().is(POPULATION_100_SETTINGS));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generator.sanctuary.sanctuary": "Sanctuary (20 players)",
|
||||
"generator.sanctuary.sanctuary": "Sanctuary (Large)",
|
||||
"biome.sanctuary.starter_forest": "Sanctuary Forest",
|
||||
"biome.sanctuary.starter_forest_hydrology": "Sanctuary Forest and Shores",
|
||||
"biome.sanctuary.temperate_forest": "Sanctuary Temperate Forest",
|
||||
@@ -38,7 +38,7 @@
|
||||
"biome.sanctuary.cavern_sulfur_depths": "Sulfur Depths",
|
||||
"biome.sanctuary.cavern_lush_caves": "Sanctuary Lush Caves",
|
||||
"biome.sanctuary.cavern_dripstone_caves": "Sanctuary Dripstone Caves",
|
||||
"generator.sanctuary.sanctuary_5": "Sanctuary (5 players)",
|
||||
"generator.sanctuary.sanctuary_5": "Sanctuary (Small)",
|
||||
"generator.sanctuary.sanctuary_100": "Sanctuary (100 players)",
|
||||
"biome.sanctuary.population_oak_forest": "Sanctuary Oak Woodland",
|
||||
"biome.sanctuary.population_birch_forest": "Sanctuary Birch Woodland",
|
||||
@@ -49,5 +49,6 @@
|
||||
"biome.sanctuary.population_bamboo_grove": "Bamboo Grove",
|
||||
"biome.sanctuary.population_sulfur_depths": "Sulfur Depths",
|
||||
"biome.sanctuary.population_lush_caves": "Sanctuary Lush Caves",
|
||||
"biome.sanctuary.population_dripstone_caves": "Sanctuary Dripstone Caves"
|
||||
"biome.sanctuary.population_dripstone_caves": "Sanctuary Dripstone Caves",
|
||||
"generator.sanctuary.sanctuary_20": "Sanctuary (20 players — legacy size)"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generator.sanctuary.sanctuary": "Sanctuary (20 joueurs)",
|
||||
"generator.sanctuary.sanctuary": "Sanctuary (Grand)",
|
||||
"biome.sanctuary.starter_forest": "Forêt de Sanctuary",
|
||||
"biome.sanctuary.starter_forest_hydrology": "Forêt et rives de Sanctuary",
|
||||
"biome.sanctuary.temperate_forest": "Forêt tempérée de Sanctuary",
|
||||
@@ -38,7 +38,7 @@
|
||||
"biome.sanctuary.cavern_sulfur_depths": "Profondeurs soufrées",
|
||||
"biome.sanctuary.cavern_lush_caves": "Grottes luxuriantes de Sanctuary",
|
||||
"biome.sanctuary.cavern_dripstone_caves": "Grottes de spéléothèmes de Sanctuary",
|
||||
"generator.sanctuary.sanctuary_5": "Sanctuary (5 joueurs)",
|
||||
"generator.sanctuary.sanctuary_5": "Sanctuary (Petit)",
|
||||
"generator.sanctuary.sanctuary_100": "Sanctuary (100 joueurs)",
|
||||
"biome.sanctuary.population_oak_forest": "Chênaie de Sanctuary",
|
||||
"biome.sanctuary.population_birch_forest": "Bétulaie de Sanctuary",
|
||||
@@ -49,5 +49,6 @@
|
||||
"biome.sanctuary.population_bamboo_grove": "Bosquet de bambous",
|
||||
"biome.sanctuary.population_sulfur_depths": "Profondeurs soufrées",
|
||||
"biome.sanctuary.population_lush_caves": "Grottes luxuriantes de Sanctuary",
|
||||
"biome.sanctuary.population_dripstone_caves": "Grottes de spéléothèmes de Sanctuary"
|
||||
"biome.sanctuary.population_dripstone_caves": "Grottes de spéléothèmes de Sanctuary",
|
||||
"generator.sanctuary.sanctuary_20": "Sanctuary (20 joueurs — ancien format)"
|
||||
}
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@
|
||||
"replace": false,
|
||||
"values": [
|
||||
"sanctuary:sanctuary",
|
||||
"sanctuary:sanctuary_5",
|
||||
"sanctuary:sanctuary_100"
|
||||
"sanctuary:sanctuary_5"
|
||||
]
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"type": "sanctuary:population_island",
|
||||
"terrain": {
|
||||
"type": "minecraft:squeeze",
|
||||
"input": {
|
||||
"type": "minecraft:interpolated",
|
||||
"cell_size_xz": 8,
|
||||
"cell_size_y": 4,
|
||||
"input": {
|
||||
"type": "minecraft:mul",
|
||||
"left": 0.64,
|
||||
"right": "sanctuary:floating_archipelago_384"
|
||||
}
|
||||
}
|
||||
},
|
||||
"distortion": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_edge",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 0.0
|
||||
},
|
||||
"sculpt": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_sculpt",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"underside": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:island_underside",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"players": 10,
|
||||
"rift_warp": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_warp",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
},
|
||||
"rift_detail": {
|
||||
"type": "minecraft:noise",
|
||||
"noise": "sanctuary:rift_detail",
|
||||
"xz_scale": 1.0,
|
||||
"y_scale": 1.0
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"default_block": "minecraft:stone",
|
||||
"default_fluid": "minecraft:air",
|
||||
"disable_mob_generation": false,
|
||||
"legacy_random_source": true,
|
||||
"material_rule": "sanctuary:population_island",
|
||||
"noise": {
|
||||
"height": 384,
|
||||
"min_y": 0
|
||||
},
|
||||
"noise_router": {
|
||||
"chunk_surface_level": 0.0,
|
||||
"continents": 0.0,
|
||||
"depth": 0.0,
|
||||
"erosion": 0.0,
|
||||
"final_density": "sanctuary:population_10",
|
||||
"ridges": 0.0,
|
||||
"temperature": "sanctuary:temperate_variation",
|
||||
"vegetation": "sanctuary:temperate_moisture"
|
||||
},
|
||||
"sea_level": -64,
|
||||
"spawn_target": []
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
"lush_caves": "sanctuary:population_lush_caves",
|
||||
"dripstone_caves": "sanctuary:population_dripstone_caves"
|
||||
},
|
||||
"settings": "sanctuary:population_20"
|
||||
"settings": "sanctuary:population_10"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"dimensions": {
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_384",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "sanctuary:population_island",
|
||||
"oak_forest": "sanctuary:population_oak_forest",
|
||||
"birch_forest": "sanctuary:population_birch_forest",
|
||||
"clearing": "sanctuary:population_clearing",
|
||||
"dry_woodland": "sanctuary:population_dry_woodland",
|
||||
"dark_grove": "sanctuary:population_dark_grove",
|
||||
"bamboo_grove": "sanctuary:population_bamboo_grove",
|
||||
"sulfur_depths": "sanctuary:population_sulfur_depths",
|
||||
"rocky_heath": "sanctuary:population_rocky_heath",
|
||||
"lush_caves": "sanctuary:population_lush_caves",
|
||||
"dripstone_caves": "sanctuary:population_dripstone_caves"
|
||||
},
|
||||
"settings": "sanctuary:population_20"
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:the_end"
|
||||
},
|
||||
"settings": "minecraft:end"
|
||||
}
|
||||
},
|
||||
"minecraft:the_nether": {
|
||||
"type": "minecraft:the_nether",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:multi_noise",
|
||||
"preset": "minecraft:nether"
|
||||
},
|
||||
"settings": "minecraft:nether"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -6,8 +6,8 @@ import com.google.gson.JsonPrimitive;
|
||||
/** Capacity and region boundaries are persistence contracts, including negative coordinates. */
|
||||
public final class PopulationCapacitySmoke {
|
||||
public static void main(String[] args) {
|
||||
require(IslandCapacity.values().length == 3, "Only the three validated sizes are exposed");
|
||||
for (int players : new int[]{5, 20, 100}) {
|
||||
require(IslandCapacity.values().length == 4, "New small/large and both historical geometries remain supported");
|
||||
for (int players : new int[]{5, 10, 20, 100}) {
|
||||
var capacity = IslandCapacity.fromPlayers(players);
|
||||
double ratio = Math.pow(capacity.radius() / IslandCapacity.FIVE.radius(), 2);
|
||||
require(Math.abs(ratio - players / 5.0) < 1e-10, "Area scales with planned population");
|
||||
@@ -34,7 +34,7 @@ public final class PopulationCapacitySmoke {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int invalid : new int[]{0, 1, 4, 6, 10, 21, 99, 101, -5}) {
|
||||
for (int invalid : new int[]{0, 1, 4, 6, 11, 21, 99, 101, -5}) {
|
||||
require(IslandCapacity.CODEC.parse(JsonOps.INSTANCE, new JsonPrimitive(invalid)).error().isPresent(),
|
||||
"Unsupported saved counts must fail, not silently become a different island");
|
||||
boolean rejected = false;
|
||||
@@ -42,7 +42,7 @@ public final class PopulationCapacitySmoke {
|
||||
catch (IllegalArgumentException expected) { rejected = true; }
|
||||
require(rejected, "Unsupported configuration is rejected");
|
||||
}
|
||||
System.out.println("Population capacity smoke passed: 5/20/100, codec, area and signed region ownership");
|
||||
System.out.println("Population capacity smoke passed: small/large, legacy sizes, codec and signed region ownership");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/** Detects the world-wide stall when a cold region holds the planning lock. */
|
||||
public final class PopulationPlanCacheSmoke {
|
||||
public static void main(String[] args) throws Exception {
|
||||
var cache = new PopulationPlanCache<String, Integer>();
|
||||
cache.getOrCreate("ready", () -> 10);
|
||||
var entered = new CountDownLatch(1);
|
||||
var release = new CountDownLatch(1);
|
||||
try (var workers = Executors.newFixedThreadPool(8)) {
|
||||
Future<Integer> cold = workers.submit(() -> cache.getOrCreate("cold", () -> {
|
||||
entered.countDown();
|
||||
try {
|
||||
if (!release.await(10, TimeUnit.SECONDS)) throw new AssertionError("Cold plan was not released");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return 20;
|
||||
}));
|
||||
try {
|
||||
require(entered.await(5, TimeUnit.SECONDS), "Cold plan did not start");
|
||||
Future<Integer> ready = workers.submit(() -> cache.getOrCreate("ready", () -> {
|
||||
throw new AssertionError("Completed plan must not be rebuilt");
|
||||
}));
|
||||
require(ready.get(5, TimeUnit.SECONDS) == 10, "Warm reads must not await unrelated planning");
|
||||
require(cache.size() == 1, "Unfinished plans must not be exposed");
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
require(cold.get(5, TimeUnit.SECONDS) == 20, "Cold result lost");
|
||||
|
||||
var builds = new AtomicInteger();
|
||||
var start = new CountDownLatch(1);
|
||||
var results = new ArrayList<Future<Integer>>();
|
||||
for (int i = 0; i < 8; i++) results.add(workers.submit(() -> {
|
||||
start.await();
|
||||
return cache.getOrCreate("shared", builds::incrementAndGet);
|
||||
}));
|
||||
start.countDown();
|
||||
for (var result : results) require(result.get(5, TimeUnit.SECONDS) == 1, "All callers need the same complete plan");
|
||||
require(builds.get() == 1, "Concurrent misses must compute their region once");
|
||||
}
|
||||
|
||||
try {
|
||||
cache.getOrCreate("failed", () -> { throw new IllegalStateException("Expected planner failure"); });
|
||||
throw new AssertionError("Planner failure was swallowed");
|
||||
} catch (IllegalStateException expected) {
|
||||
require(cache.size() == 3, "Failed plan must not be published");
|
||||
}
|
||||
require(cache.getOrCreate("failed", () -> 40) == 40, "A failed computation must remain retryable");
|
||||
System.out.println("Population plan cache smoke passed: warm reads, single publication and failed-plan retry");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -51,12 +51,15 @@ Pour produire un fichier importable par un lanceur compatible Modrinth :
|
||||
|
||||
```sh
|
||||
cd build/packwiz # depuis la racine du dépôt
|
||||
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.10.mrpack
|
||||
packwiz modrinth export --output ../Sanctuary-0.1.0-alpha.11.mrpack
|
||||
```
|
||||
|
||||
L’exemple vise l’alpha.11 et nécessite ses artefacts vérifiés.
|
||||
Cette version propose Petit/Grand, avec Grand par défaut, et l’optimisation ;
|
||||
les anciennes tailles restent attachées aux sauvegardes déjà créées.
|
||||
L'export contient le JAR Sanctuary local et la référence vérifiée de Fabric API.
|
||||
Il ne publie rien sur Modrinth.
|
||||
L’alpha.10 est publiée sur le canal stable. L’instance Sanctuary Beta existante
|
||||
L’alpha.11 est publiée sur le canal stable. L’instance Sanctuary Beta existante
|
||||
a été synchronisée deux fois et ses fichiers personnels suivis sont conservés.
|
||||
Les instances déjà raccordées reçoivent cette mise à jour sans nouvelle importation.
|
||||
Cet export reste destiné aux imports ponctuels.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "0.1.0-alpha.10"
|
||||
version = "0.1.0-alpha.11"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user