From e48de1b400e8a406c6a6fe91e995f89311f45106 Mon Sep 17 00:00:00 2001 From: koka Date: Tue, 8 Sep 2026 03:24:04 +0200 Subject: [PATCH] Initialize Sanctuary Fabric 26.3 prototype and pack --- .editorconfig | 15 + .gitattributes | 9 + .gitea/ISSUE_TEMPLATE/bug.md | 41 ++ .gitea/ISSUE_TEMPLATE/feature.md | 26 + .gitea/workflows/build.yml | 17 + .gitignore | 17 + AGENTS.md | 17 + CHANGELOG.md | 22 + CONTRIBUTING.md | 48 ++ LICENSE | 676 ++++++++++++++++++ README.md | 141 ++++ THIRD_PARTY_NOTICES.md | 25 + build.gradle | 20 + docs/backlog.md | 153 ++++ docs/migration-26.2.md | 154 ++++ docs/testing.md | 101 +++ docs/vision.md | 285 ++++++++ docs/worldgen.md | 123 ++++ gradle.properties | 13 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes gradle/wrapper/gradle-wrapper.properties | 8 + gradlew | 253 +++++++ gradlew.bat | 93 +++ mods/sanctuary/build.gradle | 66 ++ .../gametest/SanctuaryWorldGameTests.java | 109 +++ .../mixin/SanctuaryGameTestServerMixin.java | 37 + .../src/gametest/resources/fabric.mod.json | 16 + .../resources/sanctuary-gametest.mixins.json | 7 + .../java/fr/koka/sanctuary/SanctuaryMod.java | 24 + .../sanctuary/mixin/InitialSpawnMixin.java | 20 + .../koka/sanctuary/worldgen/IslandShape.java | 27 + .../sanctuary/worldgen/MainIslandDensity.java | 61 ++ .../sanctuary/worldgen/SanctuarySpawn.java | 73 ++ .../assets/sanctuary/lang/en_us.json | 4 + .../assets/sanctuary/lang/fr_fr.json | 4 + .../tags/worldgen/world_preset/normal.json | 6 + .../worldgen/biome/starter_forest.json | 175 +++++ .../density_function/base_3d_noise.json | 8 + .../density_function/final_density.json | 22 + .../floating_archipelago.json | 25 + .../material_rule/starter_island.json | 21 + .../sanctuary/worldgen/noise/island_edge.json | 9 + .../worldgen/noise_settings/sanctuary.json | 23 + .../worldgen/world_preset/sanctuary.json | 36 + .../src/main/resources/fabric.mod.json | 30 + .../src/main/resources/sanctuary.mixins.json | 11 + .../sanctuary/worldgen/WorldgenSmoke.java | 39 + packwiz/.packwizignore | 9 + packwiz/README.md | 45 ++ packwiz/index.toml | 6 + packwiz/mods/fabric-api.pw.toml | 13 + packwiz/pack.toml | 13 + ressources-pack/README.md | 9 + .../helloworld/assets/minecraft_title.png | Bin 0 -> 43647 bytes scripts/pack.py | 133 ++++ settings.gradle | 11 + shaders-pack/README.md | 5 + 57 files changed, 3354 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitea/ISSUE_TEMPLATE/bug.md create mode 100644 .gitea/ISSUE_TEMPLATE/feature.md create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 build.gradle create mode 100644 docs/backlog.md create mode 100644 docs/migration-26.2.md create mode 100644 docs/testing.md create mode 100644 docs/vision.md create mode 100644 docs/worldgen.md create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 mods/sanctuary/build.gradle create mode 100644 mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/SanctuaryWorldGameTests.java create mode 100644 mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/mixin/SanctuaryGameTestServerMixin.java create mode 100644 mods/sanctuary/src/gametest/resources/fabric.mod.json create mode 100644 mods/sanctuary/src/gametest/resources/sanctuary-gametest.mixins.json create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/InitialSpawnMixin.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/IslandShape.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/MainIslandDensity.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuarySpawn.java create mode 100644 mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json create mode 100644 mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json create mode 100644 mods/sanctuary/src/main/resources/data/minecraft/tags/worldgen/world_preset/normal.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/starter_forest.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/base_3d_noise.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/final_density.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/floating_archipelago.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/starter_island.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/island_edge.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary.json create mode 100644 mods/sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json create mode 100644 mods/sanctuary/src/main/resources/fabric.mod.json create mode 100644 mods/sanctuary/src/main/resources/sanctuary.mixins.json create mode 100644 mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WorldgenSmoke.java create mode 100644 packwiz/.packwizignore create mode 100644 packwiz/README.md create mode 100644 packwiz/index.toml create mode 100644 packwiz/mods/fabric-api.pw.toml create mode 100644 packwiz/pack.toml create mode 100644 ressources-pack/README.md create mode 100644 ressources-pack/helloworld/assets/minecraft_title.png create mode 100644 scripts/pack.py create mode 100644 settings.gradle create mode 100644 shaders-pack/README.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fac54e1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{json,yml,yaml,toml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2d1c8a1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +* text=auto +*.java text eol=lf +*.json text eol=lf +*.toml text eol=lf +*.md text eol=lf +gradlew text eol=lf +gradlew.bat text eol=crlf +*.png binary +*.jar binary diff --git a/.gitea/ISSUE_TEMPLATE/bug.md b/.gitea/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..0e65ead --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,41 @@ +--- +name: Bug +about: Signaler un comportement incorrect et le reproduire +title: "[Bug] " +--- + +## Résultat observé + +Décrire le problème et son impact sur le jeu. + +## Résultat attendu + +Décrire le comportement correct. + +## Étapes de reproduction + +1. Ouvrir… +2. Faire… +3. Observer… + +## Environnement + +- Version de Minecraft : +- Version de Sanctuary ou commit : +- Version de Fabric Loader et Fabric API : +- Client seul ou serveur dédié : +- Autres mods pertinents : +- Fréquence : toujours / parfois / une fois : + +## Monde concerné + +Pour un problème de terrain, d'entités ou de position : + +- Seed : +- Dimension et coordonnées : +- Monde nouvellement créé ou sauvegarde existante : +- Type de monde et paramètres Sanctuary : + +## Logs et captures utiles + +Ajouter uniquement les éléments nécessaires à la reproduction. Retirer les secrets et informations personnelles. Indiquer si le problème survient aussi dans une sauvegarde de développement neuve, si ce test a été fait. diff --git a/.gitea/ISSUE_TEMPLATE/feature.md b/.gitea/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..26306f5 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,26 @@ +--- +name: Fonctionnalité +about: Proposer un incrément jouable de Sanctuary +title: "[Feature] " +--- + +## Ce que le joueur doit pouvoir faire + +Décrire le résultat souhaité et son utilité en quelques phrases. + +## Périmètre + +Indiquer les systèmes concernés et les dépendances éventuelles. Faire un ticket distinct pour les idées qui peuvent être livrées séparément. + +## Critères d'acceptation + +- [ ] Un comportement observable et vérifiable. +- [ ] Un cas limite important, si nécessaire. + +## Contexte et références + +Exemples, croquis, liens vers la vision, ancien fichier ou commit utile. Pour la génération : seed, coordonnées, dimensions et configuration si elles sont connues. + +## Décisions encore ouvertes + +Valeurs d'équilibrage, règles, interface ou contraintes qui doivent être fixées pendant le ticket. diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..d215ade --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,17 @@ +name: Build Sanctuary +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '25' + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Verify and build + run: ./gradlew --no-daemon check build assemblePack diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a90e1ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.DS_Store +.gradle/ +**/build/ +**/run/ +**/out/ +.idea/ +*.iml +*.jar +!gradle/wrapper/gradle-wrapper.jar +*.mrpack +logs/ +crash-reports/ +saves/ +screenshots/ +.env +.env.* +!.env.example diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..036b01b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,17 @@ +# Sanctuary Beta — travail par tickets + +Ce dépôt contient le pack Sanctuary et ses mods. Lire `README.md`, puis le ticket concerné. +La vision est dans `docs/vision.md` ; elle décrit aussi des fonctionnalités futures. + +- Avant une modification, lire l'état Git et préserver les changements existants. +- Un ticket correspond à un résultat jouable ou vérifiable et à une branche `codex/`. +- Garder les changements ciblés. Le dépôt voisin `26.2` est une référence historique, pas une cible de modification ou de déploiement. +- Les sources Java sont dans `mods//`, le manifeste du pack dans `packwiz/`. +- Les règles de jeu et sauvegardes font autorité côté serveur. Les mods autonomes gardent leur propre responsabilité. +- Ne pas modifier un monde existant, régénérer des chunks, changer un format de sauvegarde ou activer une expansion sans contrat de migration explicite. +- Garder les identifiants `sanctuary:*` stables et documenter toute évolution de la génération avec graine et version. +- Ajouter les libellés FR/EN des nouvelles interfaces. Vérifier les dépendances pour la version Minecraft exacte ; aucune compatibilité supposée à partir du nom d'un mod. +- Lancer `./gradlew check build` pour livrer du code, et `./gradlew assemblePack` si la distribution change. Ajouter seulement les tests utiles au comportement touché. +- Ne pas versionner de secrets, mondes, JAR générés ou dépendances téléchargées. Le wrapper Gradle fait exception. +- Ne pas déployer dans une installation de jeu ou un serveur personnel sans demande correspondante. Les serveurs de test restent dans les dossiers de développement ignorés. +- Une livraison explique le résultat, les vérifications effectuées et les limites encore ouvertes. Ne pas marquer une intention comme implémentée. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..806d8b0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +## 0.1.0-alpha.1 — 2026-09-08 + +Première base indépendante de Sanctuary Beta pour Minecraft 26.3-pre-2 / Fabric. + +- Construction Java 25 / Gradle reproductible, CI Gitea et modèles de tickets. +- Vision du projet conservée et audit du générateur 26.2 documenté. +- Preset Sanctuary : île flottante finie issue du bruit historique, forêt de + départ utilisant les ressources vanilla et extérieur vide. +- Spawn initial près de l'origine, recherché sur une surface pleine et libre. +- Tests de forme et tests sur un vrai serveur Minecraft de développement. +- Manifeste packwiz avec Fabric API épinglé et assemblage du mod local. + +Validation : `./gradlew check build assemblePack` réussit. Le serveur de test +valide le spawn `(0, 118, 0)`, 12 chunks extérieurs entièrement vides et le +déterminisme du bruit compilé. Voir [Validation](docs/testing.md) pour les seeds, +les limites des tests et les vérifications manuelles restantes. + +Cette version prépare le terrain. L'hydrologie, les continents déverrouillables, +TerraMix, la progression, l'économie et les autres dimensions restent à venir. +Les mondes joués de 26.2 ne sont pas migrés par ce prototype. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3b9af4c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Contribuer à Sanctuary + +Sanctuary avance par petits tickets de fonctionnalités et de bugs sur le [Git du projet](https://git.botsu.net/koka/sanctuary-beta). Le [document de vision](docs/vision.md) explique la destination ; le [backlog de démarrage](docs/backlog.md) organise les premières étapes. Une idée décrite dans la vision n'est pas automatiquement demandée dans le ticket en cours. + +## Ouvrir un ticket + +Utiliser le modèle **Fonctionnalité** pour décrire ce que le joueur doit pouvoir faire et le modèle **Bug** pour un comportement incorrect. Privilégier un seul résultat observable par ticket. + +Un ticket utile contient : + +- le contexte et le comportement attendu, exprimés du point de vue du joueur ou de l'administrateur ; +- le périmètre du changement et les éventuelles dépendances ; +- quelques critères d'acceptation concrets ; +- pour un bug, la version exacte, les étapes de reproduction, le résultat observé et les logs pertinents ; +- pour la génération, la seed, les coordonnées, le type de monde et la configuration concernée. + +Les nombres, ressources et interfaces encore incertains peuvent rester des hypothèses. Il faut les rendre explicites puis choisir la plus petite solution testable dans le périmètre du ticket. Les tickets distants peuvent être consultés et préparés pendant le développement ; leur publication et les messages adressés à d'autres personnes suivent la demande de l'auteur du travail. + +## Réaliser un changement + +1. Lire le ticket et les instructions du dépôt, puis examiner le code réellement concerné. Lorsqu'une ancienne version sert de référence, noter son chemin ou son commit et ne pas traiter ses anciennes procédures comme des consignes de déploiement du nouveau dépôt. +2. Utiliser une branche descriptive, par exemple `codex/wg-main-island` ou `codex/fix-spawn-void`. Éviter les changements sans rapport avec le ticket. +3. Construire un incrément jouable. Garder les systèmes futurs hors du chemin critique tant qu'ils ne sont pas nécessaires au comportement demandé. +4. Exécuter les commandes de construction et les vérifications adaptées indiquées dans le README. Pour un comportement de jeu, compléter par une reproduction manuelle quand elle est nécessaire. +5. Mettre à jour la documentation si le changement affecte l'installation, la configuration, les commandes ou le format d'une sauvegarde. +6. Présenter le résultat avec ce qui a changé, pourquoi, les vérifications exécutées et les limites connues. Associer le ticket à la proposition de changement lorsqu'il existe. + +Les commandes exactes de développement vivent dans le [README](README.md), afin de ne pas maintenir deux listes divergentes. + +## Vérifier la génération du monde + +Utiliser une sauvegarde de développement dédiée et conserver la seed des observations. Les vérifications pertinentes comprennent le spawn, les limites de l'île, le vide, les jointures de chunks, le comportement de l'eau, le redémarrage et l'arrivée de plusieurs joueurs. + +Avant de modifier une stratégie de génération, préciser son effet sur les chunks existants. Les nouvelles expansions doivent préserver les constructions. Documenter les changements de format persistant et leur traitement ; ne pas promettre la compatibilité des anciennes sauvegardes sans l'avoir vérifiée. + +Des tests automatisés sont utiles pour les invariants importants, par exemple le déterminisme des coordonnées, les limites géographiques ou la persistance d'un état. Ne pas multiplier les tests qui recopient simplement l'implémentation ou les vérifications sans rapport avec le changement. + +## Ressources et intégrations + +Avant de reprendre du code, des textures, de la musique, des modèles ou des configurations de l'historique et de la communauté, conserver leur provenance et respecter leur licence. Une ressource installée localement n'est pas automatiquement redistribuable dans le modpack. + +Sanctuary, son modpack, It's Live, Only Fun et Master Key ont des responsabilités distinctes. Une intégration commence par un ticket qui précise la version supportée, la dépendance réelle et le comportement en son absence. L'ajout d'une dépendance ou d'un module doit répondre à un besoin livré. + +## Signaler les résultats + +Une description de changement doit être compréhensible sans lire la conversation de développement. Pour un bug, donner si possible un exemple avant/après. Indiquer ce qui a été testé réellement ; une compilation réussie ne prouve pas à elle seule qu'une génération est agréable ni qu'une session multijoueur fonctionne. + +Les captures, logs et sauvegardes partagées doivent être limités au contexte utile et ne pas inclure de jetons, données d'authentification ou informations personnelles inutiles. Ne pas ajouter les répertoires d'exécution, les caches ou les mondes complets au dépôt par défaut. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..53b7f17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,676 @@ +Sanctuary 26.2 is licensed under GPL-3.0-or-later. + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a77ddb0 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# Sanctuary + +Sanctuary est une extension gratuite de Minecraft qui transforme le jeu en un +monde flottant d'exploration, de production et de progression collective. +Les joueurs commencent ensemble sur Sanctuary Island et construisent les +infrastructures qui permettront progressivement d'ouvrir de nouveaux continents. + +> Sanctuary is a free expansion of Minecraft that reshapes the game around a +> floating world. Players begin together on Sanctuary Island, isolated in the +> void. By exploring, building infrastructure and producing resources, they +> progressively unlock new floating continents, each with its own geography. +> Rather than a linear campaign, the server itself becomes a world that grows +> through the action of its players. + +Ce dépôt démarre la nouvelle base Fabric. La [vision complète](docs/vision.md) +conserve les intentions ; le [backlog](docs/backlog.md) prépare les premiers +tickets. Les systèmes d'expansion, d'économie, de progression et les dimensions +décrits dans la vision ne sont pas encore implémentés. + +## Premier incrément + +- Mod `sanctuary` indépendant et pack Sanctuary construit avec packwiz. +- Preset **Sanctuary** sélectionnable à la création d'un monde, avec une île + flottante finie, un cœur de départ et un extérieur vide. +- Forêt de départ avec végétation, minerais et animaux vanilla. Les formes de + terrain reprennent l'approche du générateur 26.2, adaptée à l'API 26.3. +- Spawn recherché sur le sol de l'île lors de la création. Les mondes ordinaires + et les choix ultérieurs de spawn ne sont pas remplacés. + +L'île a une emprise nominale de 512 blocs de diamètre, avec une bordure déformée +par la seed. C'est un premier terrain technique à régler en jeu. Le catalogue +TerraMix, les continents déverrouillables, les lacs et rivières, les ruines et +les autres fonctionnalités restent à construire par tickets. + +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). + +## Versions et prérequis + +Au 8 septembre 2026, la cible disponible est **26.3-pre-2**. Le dépôt ne prétend +pas cibler une version finale 26.3 déjà sortie. Le passage à la version finale +sera une mise à jour explicite, avec vérification des API et des sauvegardes. + +| Composant | Version fixée | +| --- | --- | +| Minecraft Java | 26.3-pre-2 | +| Java JDK | 25 | +| Fabric Loader | 0.19.5 | +| 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.1 | + +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 +3.11–3.14 si `python3` désigne l'ancien Python fourni par macOS. Gradle est fourni +par le wrapper. `packwiz` est utile pour modifier les dépendances ou servir le +pack ; il n'est pas nécessaire pour le construire. + +Références vérifiées : [Minecraft 26.3-pre-2](https://www.minecraft.net/en-us/article/minecraft-26-3-pre-release-2), +[Fabric](https://fabricmc.net/develop/), +[version Fabric API](https://modrinth.com/mod/fabric-api/version/o9uChmGq). + +## Construire et lancer + +```sh +git clone https://git.botsu.net/koka/sanctuary-beta.git +cd sanctuary-beta +./gradlew check build assemblePack +``` + +`check` inclut les tests de forme et le serveur de test automatisé de Fabric, +qui charge le vrai preset Sanctuary dans un monde de développement neuf. +Les scénarios couverts et les essais multijoueurs restant à effectuer sont +décrits dans [Validation](docs/testing.md). + +Résultats : + +- `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.1.jar` : mod à installer avec + Fabric API sur la version Minecraft indiquée. +- `build/packwiz/` : pack de développement complet, avec le mod construit et + l'index vérifié. Voir [Installation packwiz](packwiz/README.md). + +Le pack peut aussi être exporté en `.mrpack` avec packwiz pour une importation +dans un lanceur compatible ; la commande est indiquée dans ce même guide. + +Lancer le client de développement : + +```sh +./gradlew :sanctuary:runClient +``` + +Créer un **nouveau monde**, puis sélectionner le type de monde **Sanctuary**. +L'installation du mod seule ne transforme pas un monde Minecraft ordinaire. + +Pour un serveur de développement, lancer `./gradlew :sanctuary:runServer`, puis +suivre les indications du jeu concernant son EULA. Utiliser un dossier de monde +neuf et choisir dans `mods/sanctuary/run/server.properties` : + +```properties +level-type=sanctuary:sanctuary +level-name=sanctuary-dev +level-seed=42 +``` + +Relancer ensuite la même commande. Ne pas remplacer le générateur d'une +sauvegarde 26.2 : aucun outil de migration de ses chunks n'est livré ici. + +## Organisation + +| Chemin | Responsabilité | +| --- | --- | +| `mods/sanctuary/` | Code, ressources et tests du mod Sanctuary | +| `packwiz/` | Manifeste source, dépendances distantes et hashes | +| `ressources-pack/` | Sources graphiques existantes et futurs resource packs | +| `shaders-pack/` | Sources et réglages de shaders à venir | +| `docs/` | Vision, backlog, migration, génération et validation | +| `.gitea/ISSUE_TEMPLATE/` | Modèles de tickets fonctionnalités et bugs | +| `.gitea/workflows/` | Vérification sur un runner Gitea compatible | + +It's Live, Only Fun et Master Key restent des modules autonomes prévus par la +vision. Leurs sources 26.2 ne sont pas copiées dans ce socle. Fabric API est la +seule dépendance de gameplay actuellement distribuée avec le mod. + +## Travailler par tickets + +Un ticket décrit un comportement à obtenir ou un bug à reproduire. On livre +un petit incrément, on le vérifie en jeu si nécessaire, puis on ajuste avec +le ticket suivant. Pour le terrain, joindre la seed et les coordonnées aide +à retrouver exactement le problème. Voir [Contribuer](CONTRIBUTING.md). + +Le [backlog initial](docs/backlog.md) contient des propositions locales, pas des +issues déjà publiées. Le prochain travail après ce socle est le réglage de +l'île, puis le placement de continents et leur ouverture sans écraser les +constructions des joueurs. + +## Licence et crédits + +Sanctuary conserve la licence **GPL-3.0-or-later** du projet 26.2. Voir +[LICENSE](LICENSE) et [provenance et crédits](THIRD_PARTY_NOTICES.md). +Projet communautaire indépendant de Mojang Studios et Microsoft. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..e7712fb --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,25 @@ +# Provenance et crédits + +Le code Sanctuary est distribué sous GPL-3.0-or-later, comme la version 26.2 dont +la génération est reprise. Voir `LICENSE` et `docs/migration-26.2.md` pour la +provenance détaillée et le périmètre du portage. + +Minecraft appartient à Mojang Studios / Microsoft. Sanctuary est un projet +communautaire indépendant. Le dépôt ne redistribue pas le jeu. + +Le biome de départ reprend la configuration de la forêt vanilla 26.3-pre-2, +adaptée pour une île finie. Les références de blocs, végétation et minerais +sont résolues par Minecraft ; leurs textures et modèles ne sont pas inclus. + +Fabric Loader : Apache-2.0. Fabric API : Apache-2.0. Fabric Loom et le wrapper +Gradle : Apache-2.0. Ces projets conservent leurs auteurs, notices et licences. +Les dépendances sont résolues depuis leurs distributions officielles. + +Le fichier préexistant `ressources-pack/helloworld/assets/minecraft_title.png` +est conservé comme source graphique fournie par le propriétaire du dépôt. +Il n'est pas encore installé dans le pack généré. Sa provenance et son adaptation +à l'écran titre 26.3 seront précisées dans le ticket d'identité visuelle. + +Les resource packs, shaders et mods communautaires envisagés dans la vision +ne sont pas inclus automatiquement. Chaque ajout aura une version, une source, +un hash et les crédits de sa distribution. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..e66d49d --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'base' + id 'net.fabricmc.fabric-loom' version "${loom_version}" apply false +} + +tasks.named('build') { dependsOn(':sanctuary:build') } +tasks.named('check') { dependsOn(':sanctuary:check', 'verifyPack') } + +tasks.register('verifyPack', Exec) { + group = 'verification' + description = 'Verify pinned pack dependencies, versions and index hashes.' + commandLine('python3', 'scripts/pack.py', 'check') +} + +tasks.register('assemblePack', Exec) { + group = 'distribution' + description = 'Assemble a local packwiz pack including the built Sanctuary mod.' + dependsOn(':sanctuary:build', 'verifyPack') + commandLine('python3', 'scripts/pack.py', 'assemble') +} diff --git a/docs/backlog.md b/docs/backlog.md new file mode 100644 index 0000000..a4919dd --- /dev/null +++ b/docs/backlog.md @@ -0,0 +1,153 @@ +# Backlog de démarrage + +Ce fichier prépare les premiers tickets à publier sur le Git du projet. **Aucun identifiant ci-dessous n'est un numéro d'issue distante et aucun ticket n'est présumé publié.** Les identifiants `BOOT-*` et `WG-*` servent seulement à relier les travaux tant que les issues n'existent pas. + +Le [document de vision](vision.md) conserve les idées à long terme. Ce backlog organise uniquement le socle et les premiers incréments de génération. Une fonctionnalité n'est considérée comme livrée que lorsque son résultat et sa validation figurent dans le dépôt ou dans son issue. + +## Premier jalon : un monde Sanctuary jouable + +État au 8 septembre 2026 : **BOOT-01 et WG-01 sont réalisés** dans le premier +commit. Le prototype WG-02 et l'initialisation du spawn WG-03 sont implémentés +et passent les tests serveur décrits dans [Validation](testing.md). Les essais +visuels, multijoueurs et de redémarrage indiqués dans ce document restent à +faire avant de clôturer tous leurs critères. WG-04 à WG-07 restent proposés. + +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. + +### BOOT-01 — Initialiser le dépôt et la construction Fabric + +**But :** disposer d'une base clonable et vérifiable pour travailler par tickets. + +**Critères d'acceptation :** + +- Les versions réellement utilisées de Minecraft, Java, Fabric Loader, Fabric API et de l'outillage sont fixées et documentées. +- La cible souhaitée 26.3 est distinguée de la version exécutée si sa disponibilité oblige à utiliser une version de développement ou à différer la migration. +- Une commande reproductible construit le mod ; le README explique le lancement et les prérequis. +- Les métadonnées identifient Sanctuary sans annoncer les fonctionnalités de la vision comme déjà présentes. +- Le dépôt contient les consignes de contribution et des modèles de tickets ; les binaires, caches et sauvegardes de jeu ne sont pas suivis par accident. + +### WG-01 — Auditer le générateur Sanctuary 26.2 + +**But :** identifier précisément le code et les ressources utiles avant de les adapter. + +**Critères d'acceptation :** + +- Les fichiers, commits ou références historiques consultés sont cités dans une note d'audit. +- La note explique l'algorithme de l'île, le vide, le point d'apparition, les dépendances et les paramètres structurants. +- Les éléments réutilisables, les défauts connus et les changements d'API de la cible sont séparés. +- La réutilisation de ressources est accompagnée de leur origine et de leur licence connue ; les inconnues sont explicitement notées. +- L'audit n'installe ni ne déploie l'ancienne version et ne modifie pas ses sauvegardes. + +### WG-02 — Enregistrer un monde Sanctuary avec île principale et vide + +**Dépendances :** BOOT-01, WG-01. + +**But :** créer le premier terrain identifiable comme Sanctuary. + +**Critères d'acceptation :** + +- Une procédure documentée permet de créer un monde utilisant le générateur Sanctuary sur la version testée. +- Une île principale est générée au point de départ prévu ; ses coordonnées, son altitude et ses dimensions sont explicites. +- Au-delà de son emprise, les chunks de contrôle sont vides, sans fondation ni terrain vanilla inattendu. +- Une même seed et une même configuration donnent les mêmes blocs aux positions de contrôle. +- Les jointures entre chunks voisins ne créent pas de fissures ou de parois artificielles dues à une discontinuité du calcul. +- Un monde Minecraft ordinaire reste créable sans sélectionner Sanctuary. + +### WG-03 — Assurer une arrivée commune et un redémarrage sûr + +**Dépendance :** WG-02. + +**But :** éviter qu'un nouveau joueur apparaisse dans le vide et vérifier le comportement multijoueur. + +**Critères d'acceptation :** + +- Le spawn du monde se situe sur une surface stable de l'île, avec l'espace nécessaire au joueur. +- Deux nouveaux joueurs arrivent dans la zone de départ commune, sans traverser le sol ni apparaître hors de l'île. +- Le comportement après une mort sans lit est vérifié ; l'ajout ultérieur des Backrooms n'est pas requis pour ce ticket. +- Après sauvegarde et redémarrage, le générateur, la seed et le spawn sont conservés. +- Un scénario manuel reproductible couvre le client et le serveur dédié, avec la version et la seed utilisées. + +## Deuxième jalon : des continents d'essai reproductibles + +Ce jalon fournit un outil de développement du terrain. L'interface d'expansion et son coût collectif viendront dans des tickets distincts, lorsque la génération sera satisfaisante. + +### WG-04 — Décrire et placer un continent d'essai + +**Dépendance :** WG-03. + +**But :** pouvoir générer une terre suspendue à une direction, une distance et une taille explicites. + +**Critères d'acceptation :** + +- Un schéma décrit au minimum l'identifiant, le centre ou la direction et la distance, les dimensions, l'altitude et la seed du continent. +- Une configuration ou commande de développement documentée crée un continent reproductible. +- Les paramètres invalides et les chevauchements interdits sont rejetés avec un message compréhensible. +- Des limites de taille et de coût de génération sont définies à partir d'une mesure réelle. +- Le continent et ses paramètres restent identiques après rechargement du monde. + +### WG-05 — Ouvrir une expansion sans écraser l'existant + +**Dépendance :** WG-04. + +**But :** garantir que l'évolution du monde préserve les constructions et l'exploration. + +**Critères d'acceptation :** + +- La politique envers les chunks déjà générés est décidée et documentée avant l'implémentation : refus, réservation préalable ou mécanisme explicite de modification. +- Une nouvelle expansion ne remplace aucun bloc joueur silencieusement. +- Répéter la même demande ne crée pas de duplicata et ne décale pas les continents existants. +- L'état des expansions survit à un redémarrage et contient une version de format permettant de prévoir les migrations. +- Une interruption entre réservation et génération est simulée ; la reprise ou le refus reste cohérent et expliqué. + +### WG-06 — Ajouter reliefs, perforations et eaux retenues + +**Dépendance :** WG-04 ; combiner avec WG-05 avant l'usage sur une sauvegarde jouée. + +**But :** donner aux continents une géographie reconnaissable au-delà d'une simple masse de pierre. + +**Critères d'acceptation :** + +- Un jeu de seeds de référence montre des reliefs, montagnes, ravins et perforations traversantes. +- Des bassins accueillent lacs ou océans, et un premier type de rivière flottante est démontré. +- L'eau ne se répand pas de manière incontrôlée dans le vide lors du chargement et des mises à jour de blocs du scénario testé. +- Les profils du dessous et les bords du continent sont inspectés visuellement depuis les airs. +- La génération est mesurée sur une emprise et une machine indiquées ; les valeurs observées sont consignées, sans annoncer un objectif de performance non mesuré. + +### WG-07 — Introduire un premier biome distinct et une structure + +**Dépendance :** WG-06. + +**But :** valider les points d'extension avant d'ajouter un grand catalogue de contenus. + +**Critères d'acceptation :** + +- Le ticket choisit un premier biome, par exemple le Black Desert, avec des règles de surface et d'ambiance explicites. +- Une petite structure de référence se place sur un terrain compatible, sans flotter accidentellement ni détruire une construction existante. +- Le lien entre biome, végétation, ressources et règles de placement est documenté. +- La reprise du catalogue TerraMix natif d'Another World 26.2 est évaluée séparément ; « plus de cent biomes » reste une ambition tant que son adaptation n'est pas vérifiée. +- Les limites des futures zones Lost Cities et des structures uniques sont identifiées sans imposer leur livraison dans ce ticket. + +## 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. + +| Thème | Contenus à découper plus tard | +| --- | --- | +| Distribution et apparence | packwiz, mods communautaires, resource packs, shaders, icône, chargement, crédits et attributions | +| Expansion collective | deposit boxes, objectifs de production, ordinateur d'expansion, ouvertures persistantes | +| Progression | capacités, XP, inventaire, prestiges, recettes et advancements, capes et familiers | +| Économie | gemmes, mailbox, shop, offres horaires, black market, catalogue, coffre-fort et drill | +| Groupes et métiers | couleurs, équipes temporaires, factions, cloches et bannières, villageois et copper golems | +| Production et construction | convoyeurs, stockage, terminaux, ordinateur 8 bits, vein mining/building, plans et prefabs | +| Dimensions | cavernes, Alpha, Backrooms, indoors, salles secrètes et récupération des objets perdus | +| Faune et combats | zombies, fantômes, baleine, creepers, poules rares, Mooblooms, noms, armes et explosifs | +| Mobilité | waystones, téléporteurs, ziplines, grappin, aéronefs, Magic Carpet et interactions physiques | +| Temps et histoire | temps réel, calendrier, événements, loterie, étoiles, constellations, cube et sept boules | +| Objets et surprises | caméra, disque blanc, chunky, particuleur, lucky blocks et lootboxes | +| It's Live | agriculture localisée, ustensiles, recettes, fermentation, affinage et pages secrètes | +| Only Fun | interactions potaches, chanvre, anniversaires et intégration aux événements | +| Master Key | permissions communes, configuration, diagnostic et réparation du serveur | + +## Définition pratique d'un ticket terminé + +Un ticket contient un résultat observable, un périmètre limité et des critères d'acceptation vérifiables. Sa conclusion indique le comportement livré, la version testée, les vérifications réellement exécutées et les limitations qui subsistent. Les nouveaux besoins découverts deviennent de nouveaux tickets plutôt que des ajouts implicites à tous les systèmes. diff --git a/docs/migration-26.2.md b/docs/migration-26.2.md new file mode 100644 index 0000000..770d3ac --- /dev/null +++ b/docs/migration-26.2.md @@ -0,0 +1,154 @@ +# Réutiliser la génération Sanctuary 26.2 + +Audit de source du 8 septembre 2026, effectué en lecture seule dans le dépôt voisin +`../26.2`, à la révision `da61a3b1ec4fe161a9b5ffaab0fa3111238a35b6` +(pack `26.2.0-alpha.242`). Les chemins ci-dessous sont relatifs à ce dépôt historique. +Ce document décrit ce qui existe dans 26.2 ; il ne prétend pas que ces éléments sont +déjà portés dans Sanctuary Beta. + +## Conclusion pour le premier lot + +La base de terrain est réutilisable : du bruit Minecraft lié à la seed, une enveloppe +verticale d'îles flottantes, puis des masques de densité. En revanche, la génération +26.2 n'est pas un monde initialement vide avec des continents déverrouillables. +L'archipel extérieur est généré automatiquement au-delà d'un petit anneau de vide. +Le premier lot doit donc borner explicitement l'île centrale et garantir un départ +sûr, avant de construire le système persistant d'expansion collective. + +## Carte des sources récupérables + +| Élément | Source 26.2 | Réutilisation | +| --- | --- | --- | +| Bruit de terrain | `sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/base_3d_noise.json` | `minecraft:old_blended_noise`, dépendant de la seed ; aucun bloc propre au mod. | +| Enveloppe flottante | `sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/floating_archipelago.json` | Gradients verticaux combinés au bruit précédent. | +| Composition finale | `sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/final_density.json` | Référence pour l'interpolation et le mélange ; remplacer le comportement extérieur infini. | +| Masques et relief | `sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/SanctuaryDensityFunctions.java` | `AnnularVoidMask`, `TectonicMassifDensity`, `TectonicVoidDensity` ; séparer les fonctions utiles du code inactif. | +| Réglages de dimension | `sanctuary/src/main/resources/data/sanctuary/dimension_type/sanctuary.json` | Plage verticale Y=0–383, lumière et ciel ; vérifier les codecs de la version cible. | +| Réglages de bruit | `sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary.json` | Air comme fluide par défaut, aquifères désactivés, niveau marin -64 ; ne pas importer ses règles de surface en bloc. | +| Preset | `sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json` | Générateur `minecraft:noise`, biomes `minecraft:multi_noise` ; garder seulement les dimensions nécessaires au lot. | +| Recherche du spawn | `sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/world/SanctuarySpawn.java` | Recherche d'un sol naturel autour de l'origine ; corriger les limites décrites plus bas. | +| Lacs | `sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/worldgen/feature/SanctuaryWaterLakeFeature.java` | Extension de `LakeFeature`, avec quelques cannes à sucre ; utilisable dans un lot hydrologie. | +| Ravins | `sanctuary/src/main/resources/data/sanctuary/worldgen/configured_carver/tectonic_ravine.json` | Configuration de carver ; revalider sur des îles finies. | +| Prévisualisation | `sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/worldgen/SanctuaryBiomePreviewScreen.java` | Référence ultérieure pour visualiser la seed et la distribution des biomes. | + +Le graphe actif 26.2 est approximativement : + +```text +old_blended_noise -> floating_archipelago + central_halo_mask + | + max(tectonic_massifs) + | + min(void_basins) + | + blend_density -> interpolated -> squeeze +``` + +`IslandSpawnDensity` et `RadialVoidMask` restent enregistrés en Java mais ne font +plus partie du graphe actif. L'ancienne forme sculptée utilise des bruits de hachage +sans seed de monde passée à la fonction ; elle n'est pas un substitut fidèle au +terrain procédural actuel. + +## Terramix et Black Desert + +Dans ce dépôt, **Terramix est le nom du catalogue natif de 100 biomes d'Another +World**, répartis en dix familles de dix. Ce n'est pas une dépendance Fabric externe +à ajouter automatiquement au pack. + +- Catalogue auteur : `anotherworld/src/main/resources/data/anotherworld/sanctuary_biome_catalog.json`. +- Lecture et familles climatiques : `anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/SanctuaryBiomeCatalog.java`. +- Fabrication des biomes, végétations et géologies : `anotherworld/build.gradle`, + notamment la section commençant par « Terramix is a native Sanctuary catalogue ». +- Mélange avec les biomes vanilla : + `anotherworld/src/main/java/fr/koka99cab/sanctuary26/anotherworld/mixin/MultiNoiseBiomeSourceParameterListPresetMixin.java`. +- Black Desert est un biome historique distinct, `anotherworld:black_desert`, avec + du sable noir et une géologie sombre. Ses règles de surface se trouvent aussi dans + `sanctuary/.../worldgen/noise_settings/sanctuary.json`. + +Le port du seul JSON de bruit complet échouerait sans les blocs, tags et biomes +`anotherworld:*` qu'il référence. Préférer d'abord une surface et des biomes vanilla, +puis un lot dédié qui préserve les identifiants du catalogue. Le mixin climatique +historique modifie le preset Overworld global : son périmètre doit être redéfini +pour éviter d'affecter d'autres mondes par simple installation du mod. + +Le changelog indique que Terralith a été retiré en alpha.143 et remplacé par le +catalogue natif en alpha.145. Ne pas confondre ces deux noms ni restaurer les anciennes +ressources Terralith lors d'un port de Terramix. + +## Limites et défauts à traiter + +1. **L'extérieur n'est pas vide.** Le masque annulaire actif protège un rayon nominal + de 96 blocs, entre dans le halo sur 160 blocs et quitte celui-ci à partir de + 256 blocs sur 96 blocs, avec une distorsion de 32 blocs. Il retrouve donc + l'archipel normal vers 352 blocs nominalement. Ce masque ne borne pas un monde. +2. **Aucune activation de continents trouvée.** La recherche des sources Java + Sanctuary, Another World et Master Key n'a trouvé ni registre de continents + déverrouillés, ni commande d'expansion directionnelle. Les « continents » du + changelog désignent le relief tectonique naturel. Un format de sauvegarde, + des limites de taille et une politique pour les chunks déjà explorés restent + à concevoir. +3. **Spawn non garanti.** `SanctuarySpawn` ne cherche que dans un rayon de trois + chunks autour de l'origine. En cas d'échec il conserve la suggestion vanilla, + sans créer de sol sûr. Il sélectionne à nouveau le spawn global à chaque + `SERVER_STARTED`, ce qui peut écraser le choix d'un administrateur. +4. **Reliefs extérieurs très éloignés.** Les massifs et bassins de vide actifs ont + `protected_radius=10000` et `protection_fade=2000`. Les copier autour d'une petite + île ne produit donc pas de montagne proche du départ. +5. **L'eau n'est pas une hydrologie continentale.** Le réglage emploie de l'air et + désactive les aquifères. Les lacs sont des features ponctuelles. Aucun réseau + de rivières flottantes ou d'océans suspendus contrôlés n'a été identifié dans + les sources examinées. +6. **La densité seule n'assure pas le vide final.** Les carvers, décorations et + structures constituent des étapes distinctes. Vérifier les chunks terminés, + y compris aux frontières de l'île, et borner aussi les placements qui pourraient + traverser la limite. +7. **Ordre des features sensible.** Le changelog alpha.105 rapporte un blocage + « Loading terrain » corrigé en ordonnant la végétation lush de plafond avant + celle au sol. Revalider les dépendances entre features lors du port des biomes. + +Ces constats sont issus de la lecture du code et du changelog. Aucun serveur 26.2 +n'a été lancé ni modifié pendant cet audit. + +## Historique exploitable + +L'historique Git local n'est pas shallow, mais commence par l'import +`94644a2` (« Import Sanctuary 26.2 collaboration workspace »). Le générateur principal +actuel est déjà présent dans cet import : les étapes plus anciennes ne correspondent +donc pas à des commits individuels disponibles dans ce dépôt. Leurs descriptions +restent dans `CHANGELOG.md` et `pack/migrations/`. + +| Repère | Intérêt | +| --- | --- | +| Alpha.104–106 | Introduction des massifs, lacs, ravins et hauteur de dimension 384. | +| Alpha.108–111 | Expériences de relief extrême, puis réduction des plateaux et pentes. | +| Alpha.116–120 | Protection centrale, passage à l'île procédurale et réduction du halo. | +| Alpha.143–148 | Retrait de Terralith, Terramix natif, correction des transitions et prévisualisation. | +| Alpha.189 | Provinces climatiques à deux échelles, points MultiNoise vanilla conservés. | +| Commit `9c09664` | Port de l'île Indev finie pour la dimension Alpha ; sujet distinct du monde Sanctuary. | + +Les profils extrêmes sont réellement présents dans +`sanctuary/design/worldgen/alpha108_mega_tectonics/` et +`sanctuary/design/worldgen/alpha110_steep_massifs/`. Les chemins `backups/` cités par +le changelog sont des repères historiques ; leur disponibilité n'a pas été vérifiée. + +## Provenance et licences déclarées + +Le `LICENSE` racine et le manifeste Fabric Sanctuary déclarent +`GPL-3.0-or-later`. Garder cette provenance avec les sources effectivement reprises. +La dimension Alpha possède en plus une notice BSD 3-Clause pour l'algorithme Indev +dans `sanctuary/THIRD_PARTY_NOTICES.md` ; elle concerne ce port spécifique. + +`anotherworld/THIRD_PARTY_NOTICES.md` signale des textures Flower Cows dont la +permission de redistribution publique est à clarifier. Elles ne sont pas nécessaires +au générateur minimal et ne doivent pas être importées avec un lot de terrain. + +## Lots proposés après l'initialisation + +1. Île principale finie dépendante de la seed, vide extérieur, spawn durable et sûr, + preset explicite ; validation sur plusieurs seeds et redémarrage serveur. +2. Modèle persistant de continents : identifiant, seed, centre, orientation, taille, + état verrouillé/déverrouillé et version de génération ; définir le traitement + des chunks vides déjà visités avant toute commande d'activation. +3. Hydrologie fermée et reliefs : bassins, lacs, rivières, cavités et ravins ; vérifier + les bordures de chunks, l'écoulement et la conservation des constructions. +4. Terramix et Black Desert avec leur géologie, leurs ressources et une distribution + climatique mesurable ; puis structures procédurales et continents thématiques. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..1182c1a --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,101 @@ +# Vérifier Sanctuary + +Utiliser Java 25 et les dépendances épinglées dans `gradle.properties`. + +```sh +./gradlew check build assemblePack --console=plain +``` + +`check` inclut les tests GameTest. Pour un diagnostic ciblé de la génération seule, +utiliser `./gradlew :sanctuary:runGameTest --console=plain`. + +## Tests de forme + +La tâche `:sanctuary:worldgenSmoke`, incluse dans `check`, vérifie les invariants de +l'enveloppe de l'île : noyau solide, limite extérieure, hauteur et stabilité de la +fonction géométrique. Elle complète les tests en jeu ; elle ne charge pas à elle +seule les ressources de génération de Minecraft. + +## Tests dans le moteur Minecraft + +`runGameTest` démarre le serveur de test headless officiel de Fabric dans +`mods/sanctuary/build/run/gameTest/`. La tâche `cleanGameTestWorld` supprime uniquement +son monde jetable avant chaque exécution afin de générer des chunks neufs. +Il ne déploie rien dans un serveur ou une installation de jeu personnels. +La configuration garde `eula=false` : aucun fichier d'acceptation n'est écrit par +Loom. Fabric dispose d'un chemin de démarrage propre à ses tests automatisés. + +Le source set `gametest` est un mod de test séparé, exclu du JAR Sanctuary distribué. +Deux adaptations y sont nécessaires, vérifiées contre les classes Minecraft +`26.3-pre-2` : + +- `GameTestServer` sélectionne normalement `minecraft:flat_all_dimensions`. + Le mixin de test sélectionne directement le preset de production + `sanctuary:sanctuary`, sans recopier ses JSON. +- 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 reste `0`. + +Le framework déplace aussi le spawn vers une grille de tests située loin du centre. +Le test capture donc le spawn juste avant ce déplacement, après l'initialisation +normale du monde, et examine les coordonnées absolues de l'île. + +Les trois tests vérifient : + +1. Le vrai générateur Sanctuary est chargé, le noyau de l'île existe et le spawn + collectif possède un sol plein de 3×3 blocs avec deux blocs libres et secs. +2. Douze chunks entièrement générés sont vides dans les quatre directions, juste + après l'enveloppe de décoration puis à environ 512 et 4 096 blocs. Cela couvre + notamment l'ancien retour automatique de l'archipel au loin. +3. La densité du datapack réellement chargé, compilée par `RandomState`, est + reproductible pour la seed `0` et change pour la seed `8675309`. + +Le chargement du serveur valide également les codecs et les références des +registres de biomes, densités, réglages et presets. Un échec de chargement ou un +test obligatoire en échec doit faire échouer la tâche Gradle. + +Le framework et les hooks de test sont spécifiques à la version épinglée. Lors +d'une mise à jour Minecraft, vérifier ces hooks avant de conclure que les tests +exercent toujours le preset de production. La vérification explicite du générateur +dans le premier test empêche un résultat positif sur un simple monde plat. + +Référence du workflow : [tests automatiques Fabric](https://docs.fabricmc.net/develop/automatic-testing). +Les signatures propres à `26.3-pre-2` ont été vérifiées dans les dépendances locales, +car la documentation publiée vise actuellement `26.2`. + +## Validation de la fondation — 8 septembre 2026 + +Commande `./gradlew check build assemblePack --console=plain` réussie en **22 secondes** +sur Java **25.0.3**, Minecraft **26.3-pre-2**, Fabric Loader **0.19.5**, Fabric API +**0.160.0+26.3** et Loom **1.17.20**, pour Sanctuary **0.1.0-alpha.1**. + +- Quatre tests requis exécutés par le framework, dont les trois tests Sanctuary + ci-dessus : tous réussis en **5,040 secondes**. +- Monde de test neuf, seed **0**, structures activées et spawn initial observé + à **(0, 118, 0)**. Les douze chunks extérieurs inspectés au statut `FULL` sont vides. +- Densité compilée reproductible pour **0**, différente pour **8675309** ; seuls + les chunks du monde de seed **0** ont été générés entièrement. +- `WorldgenSmoke`, vérification des versions et empreintes packwiz, compilation + du JAR et assemblage dans `build/packwiz/` réussis. +- Le JAR `mods/sanctuary/build/libs/sanctuary-0.1.0-alpha.1.jar` ne contient aucune + classe ni configuration de mixin GameTest. + +Les preuves locales sont dans +`mods/sanctuary/build/run/gameTest/logs/latest.log` et `debug.log` ; elles restent +ignorées par Git et sont remplacées lors des prochains tests. Les vérifications +manuelles ci-dessous n'ont pas été effectuées dans cette livraison. + +## Vérifications manuelles restantes + +- Créer un monde avec le preset Sanctuary dans le client et évaluer visuellement + la côte, les trous, les surplombs, la végétation et la lecture du vide. +- Rejoindre à plusieurs joueurs, mourir et réapparaître ; les tests headless ne + simulent pas de connexion client ni les offsets de réapparition des joueurs. +- Modifier le spawn administrateur, arrêter puis recharger le monde et vérifier + sa conservation. Le test automatique couvre le premier démarrage, pas un cycle + de sauvegarde et de redémarrage complet. +- Explorer plusieurs seeds dans le client. Les tests compilent deux seeds mais + ne génèrent des chunks complets que pour la seed `0`, et leur échantillonnage + extérieur ne constitue pas une inspection exhaustive de toutes les coordonnées. +- Tester les resource packs, shaders et mods communautaires après leur ajout et + vérification de compatibilité avec la version exacte du pack. diff --git a/docs/vision.md b/docs/vision.md new file mode 100644 index 0000000..82b4785 --- /dev/null +++ b/docs/vision.md @@ -0,0 +1,285 @@ +# Sanctuary — vision du projet + +Ce document conserve les intentions exprimées au démarrage de Sanctuary Beta. Il décrit une **destination de conception**, pas une liste de fonctionnalités déjà livrées. Le code, les tests et les notes de version font foi pour l'état réel du mod. Les valeurs d'équilibrage ci-dessous sont des propositions initiales à éprouver en jeu. + +La cible demandée est **Minecraft 26.3 avec Fabric**. Au démarrage du dépôt, le 8 septembre 2026, la base disponible retenue est **26.3-pre-2** ; les versions effectivement utilisées restent indiquées dans la configuration de construction. La première étape est d'initialiser proprement le projet, puis de retrouver et d'adapter les idées pertinentes du générateur de l'ancienne version 26.2. + +## Le projet en quelques mots + +> Sanctuary is a free expansion of Minecraft that reshapes the game around a floating world. Players begin together on Sanctuary Island, isolated in the void. By exploring, building infrastructure and producing resources, they progressively unlock new floating continents, each with its own geography. Rather than a linear campaign, the server itself becomes a world that grows through the action of its players. + +Sanctuary transforme Minecraft en un monde flottant d'exploration, de production et de progression collective. Tous les joueurs commencent sur l'île de Sanctuary, isolée dans le vide. Leurs constructions, leurs explorations et leurs chaînes de production permettent d'ouvrir de nouveaux continents suspendus. + +L'objectif mythologique est de progresser d'une condition très limitée, proche du hardcore, vers les possibilités du mode créatif, par la coopération. Le serveur conserve l'histoire de cette transformation : les continents ouverts, les ressources produites, les événements, les étoiles et les constellations. Battre l'Ender Dragon devient un événement parmi d'autres, et non la conclusion obligatoire d'une campagne. + +## Principes de conception + +- **Un monde construit par ses habitants.** Les efforts collectifs doivent avoir des effets durables et visibles sur le serveur. +- **Des objets et des gestes dans le monde.** Favoriser panneaux, ordinateurs, cloches, bannières, télescopes et infrastructures pour accéder aux systèmes. Ajouter un menu quand il sert réellement l'interaction. +- **Coopération et initiative individuelle.** Permettre les projets communs sans effacer les possessions, les collections, les voisinages ni les constructions personnelles. +- **Une progression par l'expérience et la production.** L'XP sert aux capacités, aux transports et aux actions créatives ; les ressources et les métiers soutiennent l'expansion. +- **Un monde aux ressources localisées.** Il faut partir explorer pour trouver certains biomes, cultures, structures et matériaux. +- **Humour, surprise et souvenirs.** Les noms d'entités, les événements, les animaux rares et les objets insolites comptent autant que les systèmes économiques. +- **Un développement par tickets.** Livrer une petite mécanique jouable et vérifiable, puis l'ajuster avec des tickets de fonctionnalités et de bugs. Ne pas construire tous les systèmes avant de pouvoir jouer. + +## Périmètre de départ + +Le premier incrément porte sur le socle Fabric et la génération du monde : retrouver le générateur 26.2, produire une île principale dans un vrai vide, assurer une arrivée commune et sûre, puis poser une manière reproductible de générer des continents à une distance, une direction et une taille choisies. + +La progression, l'économie, les nouveaux monstres, les interfaces de pack et les autres dimensions restent des intentions futures. Ils ne constituent pas des dépendances du premier générateur. Le [backlog](backlog.md) précise les premiers tickets et leurs critères d'acceptation. + +## Mod, modpack et modules autonomes + +| Ensemble | Rôle souhaité | +| --- | --- | +| **Sanctuary, le mod** | Monde flottant, expansion, progression collective et systèmes propres à cet univers. | +| **Sanctuary, le modpack** | Distribution avec packwiz, configurations, mods communautaires, resource packs et shader packs. | +| **It's Live** | Mod de cuisine et d'agriculture autonome, raccordable aux biomes, événements et déblocages de Sanctuary. Vérifier si l'historique utilise le nom « It's Alive » avant de fixer ses identifiants. | +| **Only Fun** | Mod autonome consacré aux interactions humoristiques, aux anniversaires et à des activités sociales. | +| **Master Key** | Bibliothèque d'outils et de permissions d'administration destinée aux différents mods, afin d'administrer et de réparer sans devoir jouer en créatif. | + +L'apparence du pack peut inclure les mentions affichées, les crédits, l'icône, l'écran de chargement et des adaptations visuelles. Les licences et attributions des composants restent attachées à leurs ayants droit ; choisir une licence pour le travail original est une décision distincte de personnaliser son habillage. + +Les intégrations communautaires envisagées comprennent Fabric API, Sodium, Iris, Vitrail, Indium, Just Enough Items, Simple Voice Chat, Xaero's Minimap et Dynamic Torches. Golden Days est envisagé pour l'apparence du monde Alpha, et Litematica pour les plans de construction. Ce sont des pistes d'intégration : la disponibilité sur la version cible, les noms exacts, les dépendances et les conditions de distribution doivent être vérifiés au moment de chaque ticket. + +**TerraMix** désigne ici le catalogue natif de 100 biomes d'**Another World**, retrouvé dans l'historique 26.2. Sa reprise et son extension sont une piste pour les continents ; ce nom ne désigne pas une nouvelle dépendance externe obligatoire. + +## Monde flottant et expansion + +### Sanctuary Island et continents + +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. + +Leur géographie doit rester variée : perforations et passages de vide, océans et lacs retenus dans le terrain, rivières flottantes, montagnes, ravins et reliefs lisibles depuis les airs. Le système doit pouvoir conserver les constructions et les chunks déjà explorés lorsqu'une nouvelle expansion est ouverte. + +L'ambition est de reprendre puis d'étendre la centaine de biomes du catalogue TerraMix d'Another World, avec des biomes originaux comme le **Black Desert**. Cette diversité sera introduite après la validation du terrain de base et de son adaptation technique. + +### Structures et territoires + +- **Lost Cities** : des zones traitées comme des biomes, où des bâtiments en ruine remplacent la végétation dominante. Le nom exprime ici un type de territoire souhaité, sans décider encore s'il sera produit par une intégration ou par du code Sanctuary. +- **Structures contemporaines** : parcs, parkings et autres vestiges. Les parkings sont souterrains et peuvent s'étendre comme une infestation. Ils n'apparaissent pas dans la dimension des cavernes. +- **Structures uniques et donjons** : d'abord sur Sanctuary Island, puis dans les continents. Ils peuvent contenir des clés de chunk et d'autres ressources rares. +- **Secret rooms et challenge rooms** : des accès présents dans le monde mènent à des indoors aménagés, avec des équipements, des épreuves ou des gisements de fer, d'or, d'argent ou de titane. + +### Dimensions + +| Dimension | Intention | +| --- | --- | +| **Cavernes** | Monde destiné au minage, avec des biomes souterrains, sans mineshafts ni parkings. Des cités anciennes particulièrement difficiles accueillent plusieurs types de squelettes et un nécromancien illageois qui les invoque. Il détient une boule de cristal. | +| **Alpha** | Monde inspiré de Minecraft Indev et des premières versions, avec les anciennes textures de Golden Days actives uniquement dans cette dimension. Notch y est un boss. | +| **Backrooms** | Monde sombre à plusieurs niveaux, exploré avec une source de lumière. Les joueurs y arrivent dans un lit. Il reçoit dans des coffres les objets perdus dans le vide ou brûlés ; c'est aussi un territoire d'exploration et un lieu de quête. | +| **Indoors** | Petits espaces associés à des objets, déclinés en classes de volume, avec une taille indicative allant de 5 × 5 × 5 à 100 × 100 × 100 blocs. Ils peuvent servir d'intérieurs, de salles secrètes ou de lieux d'événements. | + +La géométrie des indoors reste à décider : certains peuvent être des volumes précisément délimités, d'autres des espaces fermés qui se rebouclent en trois dimensions, de type tore 3D. Leurs objets d'accès sont liés à la mailbox de leur propriétaire et doivent être récupérables s'ils sont perdus. Les règles de propriété, de partage, de transfert et de sortie sûre seront définies avant leur implémentation. + +## Progression personnelle et collective + +### Capacités initiales + +Le joueur commence avec trois cœurs, trois icônes de nourriture, une capacité respiratoire réduite, une vitesse de minage très faible et l'impossibilité de courir. L'armure conserve son fonctionnement habituel. + +L'XP et les niveaux permettent d'acheter des améliorations. L'exemple de courbe souhaitée double les coûts : **1, 2, 4, 8, 16, 32, 64 niveaux**. Les valeurs doivent être testées séparément pour chaque capacité ; il reste notamment à confirmer si chaque achat consomme des niveaux entiers ou une quantité d'XP équivalente. + +Les capacités peuvent atteindre les valeurs usuelles de dix cœurs, dix icônes de nourriture et une respiration complète. Le nombre d'étapes, les plafonds et le lien avec le prestige seront arrêtés dans les tickets concernés. + +### Inventaire, prestige et déblocages + +L'intention finale exprimée est un inventaire qui commence à une rangée, peut s'étendre jusqu'à six, puis jusqu'à huit grâce aux prestiges. Une limite de quatre rangées a également été évoquée pendant la réflexion ; elle n'est pas retenue comme règle ferme. La place de la barre rapide dans ce comptage reste à préciser. + +La progression peut débloquer une table de fabrication dans l'inventaire, des capacités de minage et de construction, et l'accès à certaines fonctions de mods communautaires. Les joueurs peuvent également porter des capes et des familiers ; les spawn eggs correspondent à différents pouvoirs de familier. + +Les recettes de Just Enough Items sont révélées par paliers liés aux advancements. Les advancements Minecraft donnent accès aux recettes Minecraft ; ceux de Sanctuary ouvrent les blocs décoratifs, équipements et systèmes correspondants. Il faudra distinguer dans chaque ticket l'affichage des recettes, leur connaissance et l'autorisation réelle de fabriquer. + +### Construction et production + +- **Mining** améliore la vitesse de minage et ouvre le vein mining : extraction de blocs d'une même veine ou sur un plan défini par l'orientation du joueur. +- **Building** ouvre des outils de pose et de remplissage de surfaces, dont le vein building. +- **Plans et préfabriqués** : conserver et copier ses constructions, puis accéder à un catalogue de prefabs. Une intégration adaptée de Litematica est envisagée. Un ancien concept de « voxelier » dans un ordinateur permettait de créer des modèles en cubes ; cette piste est considérée comme complexe et n'est pas prioritaire. + +## Monnaies, propriétés et échanges + +### Les trois gemmes + +Les trois gemmes forment la palette et la symbolique triangulaire de Sanctuary. + +| Gemme | Fonction souhaitée | +| --- | --- | +| **Émeraude** | Économie locale des villageois, échanges et travail dans le monde. | +| **Rubis** | Monnaie des échanges passant par les services du serveur et ses boutiques. Nouveau minerai à extraire. | +| **Saphir** | Réservation et sauvegarde de quantités limitées d'objets dans le catalogue. Nouveau minerai à extraire. | + +### Mailbox, dépôts et boutiques + +Chaque joueur a une **mailbox** et le serveur dispose de **deposit boxes** pour les apports collectifs. Les achats autorisés sont livrés dans la mailbox. Les échanges doivent pouvoir être reliés à la progression du serveur et aux ressources qu'il conserve. + +Le shop propose des offres flash renouvelées toutes les heures. Le joueur peut débloquer jusqu'à neuf emplacements avec son XP. Le black market accueille les offres des joueurs ; le catalogue permet de réserver une quantité limitée d'un objet contre des saphirs. Des extensions et emplacements supplémentaires peuvent apparaître comme récompenses d'événements. + +Le shop pourrait devenir une infrastructure coûteuse à construire, accessible physiquement et située en fin de progression. Sa forme exacte reste ouverte : bâtiment joueur, service du serveur ou combinaison des deux. Des casinos événementiels peuvent être installés dans de grands indoors où les joueurs se retrouvent. + +### Coffre-fort, équipes et braquage + +Chaque joueur peut posséder un seul coffre-fort. Il contient plus de gemmes qu'un inventaire et permet un porte-monnaie utilisable en jeu. Le coffre doit pouvoir être caché et peut être percé avec une **drill en titane**. + +Chaque joueur a une couleur unique, qui sert de couleur d'équipe. Des joueurs peuvent se fédérer en équipes temporaires, factions ou voisinages et partager les gains d'une action, y compris d'un braquage. Les conditions d'accès, les protections et l'équilibrage entre coopération et conflit devront être explicités dans ces tickets. + +## Villageois et automatisation + +Une bannière placée au-dessus d'une cloche associe un village à une faction. Faire sonner la cloche permet de mettre à jour l'appartenance des villageois concernés à l'équipe associée. Le rayon, le choix de la bannière et les conflits de cloches restent à spécifier. + +Les villageois peuvent être payés en émeraudes pour réaliser des tâches cohérentes avec leur métier vanilla. Trois secteurs se complètent : **récolte**, **transformation** et **services**. Ils peuvent, par exemple, récolter du blé, le déposer dans une boîte ou le moudre. Les copper golems et la redstone participent au transport et aux chaînes de production. + +Les **conveyor belts**, fabriqués notamment avec du cuir de vache, déplacent les objets sous forme de drops dans le sens de pose du bloc. Ils se combinent avec les droppers pour acheminer les ressources sur des distances importantes. + +## Machines, équipements et redstone + +- **Ordinateur 8 bits** : bloc programmable avec six ports d'entrée et de sortie, permettant de construire ses propres comportements de redstone. Des évolutions en contrôleur et en ordinateur d'interface donnent accès, dans le monde, aux systèmes d'expansion. +- **Terminal et storage en titane** : stockage de fin de progression, avec jusqu'à 128 coffres connectés consultables par un terminal commun, afin de déposer et retrouver ses objets sans tri manuel constant. +- **Chunky et clé de chunk** : bloc fabriquable maintenant un chunk actif. Son activation requiert une clé trouvable dans les donjons et structures, afin que le chargement permanent ait une valeur d'exploration et d'échange. L'orthographe des noms sera fixée à partir de l'historique. +- **Particuleur** : émetteur de particules dont l'effet dépend de l'objet inséré et dont l'intensité dépend du signal de redstone. +- **Caméra** : photographie le jeu et transforme les images en cartes Minecraft utilisables dans les item frames. +- **Disque blanc** : renommé avec une référence à une vidéo YouTube, il permet d'en jouer le contenu dans un jukebox. La forme exacte de la référence et le comportement audiovisuel restent à décider. +- **Équipements en titane d'anomaly** : équipements impossibles à fabriquer, indestructibles, avec des enchantements exceptionnellement forts. Les machines fabriquables en titane forment une catégorie distincte. + +### Armes et explosifs + +L'argent, appelé aussi **silver**, est le métal des armes de précision et de certaines technologies : + +- Le **creeper lock** consomme de la poudre à canon pour une attaque explosive de courte portée. +- La **mitraillette** repousse les monstres à moyenne portée. Elle emploie des munitions métalliques de puissance variable ; le choix précis entre pépites et lingots reste à fixer. +- Le **mining rifle** est une arme de longue portée utilisant des minerais. + +Ces armes peuvent recevoir des enchantements adaptés. Les nouveaux explosifs comprennent une **Weather TNT** qui dissipe la pluie, une **Fragment TNT** qui libère plusieurs TNT, une **Randomizer TNT** qui redistribue les blocs autour d'elle, et une **Mega TNT** plus puissante. + +Les **lucky blocks** déclenchent volontairement un événement imprévisible, parfois très favorable, parfois franchement nuisible : récompenses rares, mort, poison, sorcières, lave ou dégâts causés par des explosifs. Leur risque et leur injustice font partie du concept ; la zone et les conditions de ces effets seront fixées dans le ticket avant leur ajout à un serveur partagé. + +## Déplacements et interactions + +| Système | Comportement souhaité | +| --- | --- | +| **Waystones horizontales** | Voyager dans une même dimension vers une waystone connue, avec un coût d'XP croissant selon la distance. | +| **Waystones verticales** | Ascenseurs : sauter pour monter, se baisser pour descendre. L'étage de destination n'a pas besoin d'avoir été découvert. | +| **Téléporteur longue distance** | Préparation plus lente, mais grandes distances possibles, avec une arrivée approximative. | +| **Zipline** | Corde reliant deux installations, jusqu'à une distance indicative de 512 blocs. Un clic droit lance le trajet, moins rapide qu'un minecart à pleine vitesse. | +| **Grappling rod** | Viser un point pour s'y tirer, avec un risque réel de chute. Consomme des leads qui ne sont pas récupérés. | +| **Bateau avec poule** | Aéronef simple obtenu en plaçant une poule dans un bateau, permettant notamment le transport de villageois. | +| **Biplan** | Aéronef à deux places, plus rapide que le bateau volant, mais plus lent que les elytras. | +| **Happy Ghast** | Monture volante à quatre places, avec une vitesse doublée. « Happy gust » a été prononcé dans la description ; l'identifiant exact est à vérifier. | +| **Totem de Notch / Magic Carpet** | En tenant le totem, faire apparaître une plateforme de verre sous ses pieds pour marcher dans les airs et faciliter la construction. La taille de la plateforme, évoquée comme trois blocs, reste à fixer. | + +Un sneak + clic droit permet de porter un mob sur sa tête, avec une limite d'un mob porté. Sur un autre joueur, le même geste permet de se placer sur sa tête. Un double sneak sans déplacement permet de s'asseoir. + +## Faune, créatures et personnages + +### Hostiles et fantômes + +Une famille de zombies inspirée de Left 4 Dead comprend le **Hunter**, le **Charger**, le **Spitter**, le **Boomer** et l'**Infecté**. Le **Tank** et le **zombie géant** sont des boss futurs. + +Le système d'insomnie et les phantoms vanilla disparaissent lorsque le monde passe au temps réel. Ils laissent place à d'autres fantômes, dont une **baleine volante, fantomatique et apaisante**, qui passe autour des joueurs et peut s'inviter dans le paysage comme un photobomb. + +Un **Happy Creeper**, bienveillant, porte une fleur sur la tête et un visage différent. Des creepers rares ont également des apparences variées. + +### Poules rares + +Une poule ordinaire peut très rarement pondre un œuf en or. Les quatre variantes rares sont classées ainsi : + +| Poule | Production | Cadence indicative | +| --- | --- | --- | +| Émeraude | Émeraudes | Deux fois moins vite qu'une poule ordinaire | +| Rubis | Rubis | Deux fois moins vite que la poule émeraude | +| Saphir | Saphirs | Deux fois moins vite que la poule rubis | +| Or | Œufs en or | Deux fois moins vite que la poule saphir | + +Ces quatre variantes **ne peuvent pas se reproduire**. Leur rareté doit préserver la valeur de la monnaie. Les joueurs les protègent avec des armures de poule inspirées des armures de loup, colorables, et peuvent les nommer. L'objectif est de créer de l'attachement et de l'attention à ces animaux. + +### Mooblooms et noms d'entités + +Les **Mooblooms** sont des variantes de vaches très rares associées aux fleurs, avec de petites et grandes versions. La liste initiale comprend hibiscus, narcisse, tulipes roses, orange, rouges et blanches, marguerite/oxeye daisy, tournesol, allium, houstonie/azure bluet, orchidée bleue, bleuet/cornflower, pissenlit, muguet, lilas, rose, rose bleue et coquelicot/poppy. Les libellés exacts, la variante « moobloom » générique et les équivalences linguistiques devront être alignés sur les ressources existantes. + +La progression peut débloquer l'affichage de noms pour toutes les familles d'entités. Chaque famille a un registre humoristique : pseudos « kikoolol Xbox 360 » pour les zombies, alphabet galactique Minecraft pour les Endermen, et des noms variés pour les animaux. + +### Végétation propre à Sanctuary + +Nouveaux bois envisagés : **lavande, ébène, mossy et blueberry**. De nouvelles fleurs comprennent les roses et les roses bleues. Les formes finales des arbres et les noms de registre restent à définir. + +## Temps réel, calendrier, événements et ciel + +L'heure du jeu suit l'heure réelle : à 8 h du matin sur le serveur, il est 8 h dans le monde. L'administrateur choisit le fuseau ou le décalage horaire. Le sommeil ne sert donc plus à faire avancer la nuit, ce qui libère un rôle pour le lit comme entrée dans les Backrooms. + +Un calendrier expose des nombres de jours simples : âge du serveur depuis sa création et nombre de jours écoulés depuis une origine historique de Minecraft. La date exacte de cette origine reste à choisir. Cette chronologie fait partie de la mythologie de Sanctuary. + +Des panneaux d'événements permettent de proposer une activité et de s'inscrire, en lien avec les factions. Des anniversaires issus d'Only Fun peuvent y être reliés. Des événements suivent un cycle évoqué comme six jours actifs et un septième jour de repos ou de fête ; la durée et l'ancrage hebdomadaire doivent être confirmés. + +Une **loterie du dimanche** permet de collecter des tickets pendant la semaine. Le nombre de tickets augmente les chances de récompense : lootboxes anomaly, emplacements du catalogue ou extensions du shop. Les probabilités et le financement des récompenses doivent préserver la rareté des ressources. + +### Étoiles et constellations + +Les premières nuits n'ont pas d'étoiles. Les advancements Minecraft et Sanctuary font naître progressivement des familles d'étoiles communes à tout le serveur. Avec une spyglass, le joueur peut observer une étoile, son nom et l'accomplissement qui l'a fait apparaître. + +Il peut tracer des constellations depuis son point de vue, directement dans le ciel, sans ouvrir une interface séparée. Ajouter ou retirer un point coûte un niveau dans l'intention initiale ; il faudra préciser la différence entre point et segment lors de l'implémentation. Les constellations sont visibles par tous. Le ciel tourne plus lentement que le cycle quotidien, de sorte que les nuits évoluent au fil des jours. + +## Quêtes, récompenses et cosmologie + +### Panneaux et lootboxes + +Les panneaux de quêtes utilisent les trois gemmes et leurs couleurs : vert émeraude, rouge rubis, bleu saphir, pour trois paliers de difficulté. Le nombre de quêtes réalisables est limité par heure. Le joueur voit les récompenses avant de choisir ; les quêtes donnent notamment de l'XP et des lootboxes. + +Quatre catégories de lootboxes sont prévues : **normales**, **capes**, **spawn eggs** et **anomaly**. Les anomaly peuvent contenir les équipements en titane impossibles à fabriquer. + +### Le cube originel et les sept boules + +Sanctuary naît d'un **cube originel**, auquel le joueur reste secrètement lié et dont découle sa progression. Une mythologie cachée, inspirée d'une alchimie et d'une cabale minecraftiennes, relie ce cube à sept boules de cristal : six directions ou pointes et un centre. La référence visuelle évoque à la fois une figure à six pointes et les sept Dragon Balls. + +Les objectifs déjà évoqués sont : + +- **Boule de la fortune** : réunir une grande quantité de richesse, à calibrer pour un serveur pensé, par exemple, pour un mois de jeu. +- **Boule du cauchemar** : accomplir dans les Backrooms une action consistant à enfermer un mob particulier. +- **Boule de Notch** : vaincre Notch dans le monde Alpha ; cette rencontre donne aussi le totem qui active la Magic Carpet. +- **Boule de la cité ancienne** : vaincre ou résoudre la rencontre du nécromancien dans les cavernes. + +Les trois autres boules, les noms définitifs et la manière de les réunir restent à concevoir. L'Ender Dragon, Notch, le zombie géant, le premier burger et le premier sushi peuvent tous devenir des événements de l'histoire commune, à des échelles différentes. + +## It's Live — cuisine et agriculture + +It's Live est un mod autonome de cultures, de préparation et de transformation alimentaire. Son intégration à Sanctuary apporte des raisons d'explorer, de commercer et de produire ensemble. + +Les cultures et ingrédients se trouvent dans des biomes appropriés à leur température et à leur humidité. Tout ne pousse pas et ne se trouve pas partout. Certaines variations de récolte suivent une logique comparable à la recherche de baies dans Cobblemon. + +### Contenus envisagés + +| Famille | Exemples issus de la vision initiale | +| --- | --- | +| Fruits et légumes | Fraise, tomate, ananas, maïs, concombre, poivron, aubergine, laitue, chou, raisin, haricot, piment, olive, citron, oignon, ail | +| Céréales, herbes et arômes | Riz, soja, basilic, menthe, coriandre, café, vanille, thé | +| Ingrédients transformés | Lait, tofu, beurre, crème, fromage, vinaigre, poudre — nature de cette dernière à préciser | +| Fermentations et conserves | Vin, bière, kimchi, cornichons, yaourt, saucisson | +| Plats et recettes | Chili, veloutés, soupes, omelettes, sautés, currys, salades, sushis, pain, sandwichs, burgers, gâteaux, préparations fourrées et pizzas | +| Outils | Poêle, moulin, marmite, cuisinière, tonneau de macération et séchoir | + +Le vin peut vieillir pour gagner en qualité. Le fromage et le saucisson peuvent également être affinés ; ce vieillissement n'est pas prévu pour la bière dans l'intention actuelle. + +Des **pages culinaires** reconstituent le livre de recettes perdu de Steve. Il ne contient pas toutes les recettes : des sorcières gardent des **secret pages**, obtenues en les combattant, qui révèlent notamment certaines techniques de crème, beurre, vin et bière. + +## Only Fun — interactions sociales et absurdes + +Only Fun rassemble des interactions volontairement potaches : faire pipi, faire caca, vomir, fumer, cultiver du chanvre et célébrer les anniversaires de ses amis. Il peut fonctionner seul et se raccorder au calendrier, aux événements et aux factions de Sanctuary. Son contenu exact doit rester séparé du socle nécessaire pour jouer à Sanctuary. + +## Master Key — administration + +Master Key fournit des permissions et des outils d'administration partagés aux différents mods. L'objectif est qu'un administrateur puisse configurer le serveur, diagnostiquer et réparer une situation sans devoir activer le mode créatif pour jouer. Les actions de maintenance doivent avoir un périmètre clair, être contrôlées par le serveur et laisser des traces exploitables pour comprendre un incident. + +## Décisions à prendre au fil des tickets + +Les inconnues ne bloquent pas l'initialisation ni le terrain de base. Elles sont traitées au moment où elles influencent une fonctionnalité : + +- Taille, altitude, relief, réserve de ressources et identité visuelle de Sanctuary Island. +- Forme des continents, distances d'expansion, coûts collectifs et contrôle de leur ouverture. +- Adaptation du catalogue TerraMix historique et compatibilité des mods communautaires avec la cible Fabric. +- Courbe d'XP, lignes d'inventaire, règles de prestige et distinction entre rangée et barre rapide. +- Conditions de perte et de récupération des objets, durée de conservation dans les Backrooms et garantie d'unicité. +- Nature et propriété des indoors, partage des accès et comportement à la déconnexion. +- Règles des boutiques, du coffre-fort, du braquage et des équipes temporaires. +- Identifiants définitifs, noms hérités, ressources réutilisables et licences associées. +- Géométrie du ciel partagé et comportement des constellations selon la position du joueur. +- Date d'origine du calendrier, rythme des événements et trois boules de cristal encore non décrites. + +La prochaine décision concrète reste la même : obtenir un monde flottant stable et intéressant, puis l'éprouver en jeu avant d'ajouter les systèmes qui le feront grandir. diff --git a/docs/worldgen.md b/docs/worldgen.md new file mode 100644 index 0000000..5cb599f --- /dev/null +++ b/docs/worldgen.md @@ -0,0 +1,123 @@ +# Premier monde Sanctuary + +Le preset `sanctuary:sanctuary` crée l'île principale à l'origine de l'Overworld, +entourée de vide sans répétition d'îles à grande distance. C'est une première +tranche de génération pour Minecraft **26.3-pre-2 / Fabric**, pas encore le +système d'expansion collective. + +## 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. + +Sur un nouveau serveur Fabric disposant du mod et de Fabric API : + +```properties +level-type=sanctuary:sanctuary +level-name=sanctuary +level-seed=0 +``` + +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. + +## Ce qui est généré + +- Le générateur reste celui de Minecraft, `minecraft:noise`. +- La forme reprend le champ de bruit flottant actif en 26.2 : `old_blended_noise` + avec `xz_scale=0.25`, `y_scale=0.25`, `xz_factor=80`, `y_factor=160`, `smear=4`, + ainsi que ses deux gradients verticaux. Reliefs, surplombs, trous et fragments + dépendent donc de la seed Minecraft. +- Une enveloppe limite l'île à un rayon nominal de **256 blocs**. Le bord varie + avec un second bruit lié à la seed, jusqu'à **288 blocs au maximum** pour la + roche. Les arbres peuvent dépasser ce bord de quelques blocs. +- Une masse centrale ellipsoïdale, centrée en `(0, 76, 0)`, garantit du sol + même quand le bruit historique creuse l'origine. Son rayon horizontal est de + 96 blocs et son rayon vertical de 42 blocs. Le reste du relief peut monter + au-dessus de cette masse. La géométrie du noyau est volontairement stable. +- La roche se trouve entre `Y=1` et `Y=255` ; il n'y a ni plancher de bedrock, + ni mer globale, ni reprise du terrain à distance. Le Nether et l'End restent + ceux de Minecraft. +- `sanctuary:starter_forest` reprend végétation, minerais et animaux de la forêt + vanilla 26.3. Ses blocs sont exclusivement vanilla. C'est un biome distinct + pour ne pas hériter des tags de structures vanilla et de leurs apparitions + possibles dans le vide. +- Les carvers, géodes, donjons, lacs de lave et sources sont retirés de cette + forêt initiale. Les minerais remplacent la roche existante et les arbres ont + besoin de sol. Aucun mod de biomes externe n'est nécessaire. + +Ce premier prototype ne fournit pas encore d'eau : hydrologie, répartition +complète des ressources de survie et variété de biomes constituent le prochain +ticket de terrain. Les minerais vanilla conservent leurs plages d'altitude ; +leur abondance n'est donc pas encore équilibrée pour une île flottante. + +## Apparition commune + +Une injection limitée à la création initiale cherche une surface de 3 × 3 blocs +stable, avec deux blocs libres en hauteur, à proximité de l'origine. Elle ne +s'applique qu'à l'Overworld utilisant les paramètres `sanctuary:sanctuary`. +Elle enregistre le spawn partagé dans la sauvegarde et ne le réinitialise pas +au redémarrage : les modifications administratives de `/setworldspawn` restent +conservées. Les lits et la dispersion habituelle du spawn suivent encore les +règles de Minecraft. + +## Adaptation 26.2 → 26.3 + +Les références historiques sont +`sanctuary/worldgen/density_function/base_3d_noise.json` et +`floating_archipelago.json` dans l'ancien module Sanctuary. L'ancien masque +annulaire n'est pas conservé : après son anneau de vide, il faisait réapparaître +un archipel infini. Les anciennes surcharges de `minecraft:normal`, dimensions +supplémentaires, intégrations TerraMix et injections globales de features ne +font pas partie de ce port. + +Minecraft 26.3 a changé l'API de génération : les fonctions de densité compilent +maintenant un `DensitySampler`, leurs opérateurs JSON utilisent +`left` / `right` / `input`, les gradients utilisent `from_coordinate` et les +surfaces passent par le registre `worldgen/material_rule`. Les données de cette +branche suivent ces formats réels ; copier directement les JSON 26.2 ne suffit +pas. Les bruits utilisent également `base_octave`, `octave_count` et +`amplitude_modifiers`. Le seul opérateur Java ajouté, `sanctuary:main_island`, compose les deux +champs de bruit et impose la limite extérieure après interpolation. + +## Vérification + +```sh +./gradlew :sanctuary:worldgenSmoke +./gradlew :sanctuary:runGameTest +``` + +Le smoke test vérifie le noyau garanti même avec un bruit entièrement négatif, +le vide dans les quatre directions même avec un bruit entièrement positif, les +limites verticales, la préservation de l'influence du champ de terrain et la +symétrie de l'enveloppe pour des entrées identiques. Les coordonnées testées incluent la limite du monde et +les grands entiers pour détecter débordements et répétitions involontaires. + +Les GameTests chargent le preset réel sur un serveur éphémère, vérifient le +spawn après décoration, puis inspectent des chunks complets à distance dans +les quatre directions. Ils comparent aussi la densité compilée avec deux +instances de la même seed et une seed différente. Cela détecte les références +JSON invalides et les blocs que des features pourraient ajouter après le calcul +de densité. Les injections du serveur de test sont isolées dans +`src/gametest` et ne sont pas distribuées dans le mod. + +## Contrat du prochain ticket : expansion + +Avant de créer des continents, définir une donnée **persistante par monde** +contenant pour chaque région son identifiant, centre, direction, emprise, +seed dérivée, version de générateur et état de déblocage. L'île principale +devient la première région de ce registre ; les nouvelles régions utilisent +des coordonnées absolues, sans cellules périodiques. + +L'activation doit être une action du serveur, reproductible et sauvegardée, +conditionnée ensuite par les contributions collectives. Elle doit tenir compte +du fait qu'un joueur peut déjà avoir chargé ou construit dans le vide : un +chunk existant ne se régénère pas simplement parce que la liste des continents +change. Le ticket doit choisir une réservation d'espace ou une matérialisation +contrôlée qui protège les constructions et ne remplace jamais aveuglément des +chunks sauvegardés. + +La version de génération et les emprises doivent être gelées avant les premières +sauvegardes destinées à durer. Ce prototype ne promet pas encore de migration +automatique des mondes entre deux versions de terrain. diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c75a533 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +org.gradle.jvmargs=-Xmx2G +org.gradle.parallel=true +org.gradle.configuration-cache=false + +# Verified 2026-09-08. 26.3 is still a pre-release; do not silently move worlds. +minecraft_version=26.3-pre-2 +loader_version=0.19.5 +loom_version=1.17.20 +fabric_api_version=0.160.0+26.3 + +mod_version=0.1.0-alpha.1 +pack_version=0.1.0-alpha.1 +maven_group=fr.koka.sanctuary diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b21b69e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,8 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..47792a5 --- /dev/null +++ b/gradlew @@ -0,0 +1,253 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +if [ -z "$JAVA_HOME" ] && [ -x "$APP_HOME/.jdk/current/bin/java" ] ; then + JAVA_HOME=$APP_HOME/.jdk/current + export JAVA_HOME +fi + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mods/sanctuary/build.gradle b/mods/sanctuary/build.gradle new file mode 100644 index 0000000..be88782 --- /dev/null +++ b/mods/sanctuary/build.gradle @@ -0,0 +1,66 @@ +plugins { + id 'net.fabricmc.fabric-loom' +} + +group = rootProject.maven_group +version = rootProject.mod_version +base { archivesName = 'sanctuary' } + +fabricApi { + configureTests { + createSourceSet = true + modId = 'sanctuary-gametest' + enableGameTests = true + enableClientGameTests = false + eula = false + } +} + +// Every integration run must exercise fresh generation, never stale saved chunks. +tasks.register('cleanGameTestWorld', Delete) { + delete(layout.buildDirectory.dir('run/gameTest/world')) +} +tasks.named('runGameTest') { dependsOn('cleanGameTestWorld') } + +dependencies { + minecraft "com.mojang:minecraft:${rootProject.minecraft_version}" + implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}" + implementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}" +} + +java { + toolchain.languageVersion = JavaLanguageVersion.of(25) + withSourcesJar() +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release = 25 +} + +processResources { + // The launcher uses 26.3-pre-2; Fabric's runtime normalizes it to 26.3-pre.2. + def values = [version: project.version, minecraft_version: rootProject.minecraft_version, + minecraft_dependency_version: rootProject.minecraft_version.replace('-pre-', '-pre.'), + loader_version: rootProject.loader_version, fabric_api_version: rootProject.fabric_api_version] + inputs.properties(values) + filesMatching('fabric.mod.json') { expand(values) } +} + +tasks.named('jar') { + from(rootProject.file('LICENSE')) { rename { 'LICENSE_sanctuary' } } + from(rootProject.file('THIRD_PARTY_NOTICES.md')) +} + +tasks.register('worldgenSmoke', JavaExec) { + group = 'verification' + description = 'Check the guaranteed island core, density bounds and the surrounding void.' + dependsOn('testClasses') + classpath = sourceSets.test.runtimeClasspath + mainClass = 'fr.koka.sanctuary.worldgen.WorldgenSmoke' +} + +tasks.named('check') { dependsOn('worldgenSmoke') } + +// src/test contains a JavaExec assertion harness; engine tests use Fabric's gametest source set. +tasks.named('test') { failOnNoDiscoveredTests = false } diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/SanctuaryWorldGameTests.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/SanctuaryWorldGameTests.java new file mode 100644 index 0000000..94dd95e --- /dev/null +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/SanctuaryWorldGameTests.java @@ -0,0 +1,109 @@ +package fr.koka.sanctuary.gametest; + +import fr.koka.sanctuary.worldgen.SanctuarySpawn; +import net.fabricmc.fabric.api.gametest.v1.GameTest; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.densityfunction.DensityFunction; +import net.minecraft.world.level.storage.LevelData; + +/** Assertions use absolute world positions, away from the framework's random fixture grid. */ +public final class SanctuaryWorldGameTests { + public static LevelData.RespawnData initialSpawn; + + @GameTest(maxTicks = 200) + public void generatedIslandAndInitialSpawn(GameTestHelper helper) { + ServerLevel level = helper.getLevel(); + helper.assertTrue(SanctuarySpawn.usesSanctuaryGenerator(level), + "GameTest must load the production Sanctuary preset, not a flat world"); + + for (int x : new int[]{-16, 0, 16}) { + for (int z : new int[]{-16, 0, 16}) { + BlockPos core = new BlockPos(x, 76, z); + helper.assertFalse(level.getBlockState(core).isAir(), + "The generated island needs solid ground around its origin: " + core); + } + } + + helper.assertTrue(initialSpawn != null, "Capture the world's initial spawn before the test grid moves it"); + BlockPos spawn = initialSpawn.pos(); + helper.assertTrue(Math.abs(spawn.getX()) <= 48 && Math.abs(spawn.getZ()) <= 48, + "Shared spawn must be near the island origin: " + spawn); + helper.assertTrue(spawn.getY() > 1 && spawn.getY() < level.getMaxY() - 1, + "Shared spawn must be inside the dimension height"); + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + BlockPos feet = spawn.offset(dx, 0, dz); + BlockPos floor = feet.below(); + helper.assertTrue(level.getBlockState(floor).isCollisionShapeFullBlock(level, floor), + "Every cell of the 3x3 arrival area needs a full solid floor: " + floor); + for (int height = 0; height < 2; height++) { + BlockPos space = feet.above(height); + helper.assertTrue(level.getBlockState(space).getCollisionShape(level, space).isEmpty() + && level.getFluidState(space).isEmpty(), + "The arrival area needs two dry unobstructed blocks: " + space); + } + } + } + helper.succeed(); + } + + @GameTest(maxTicks = 200) + public void completedExteriorChunksAreVoid(GameTestHelper helper) { + ServerLevel level = helper.getLevel(); + // First row is just beyond the declared terrain/decorations envelope. The last + // samples catch the legacy bug where the infinite archipelago returned farther out. + int[][] chunks = { + {20, 0}, {-21, 0}, {0, 20}, {0, -21}, + {32, 0}, {-33, 0}, {0, 32}, {0, -33}, + {256, 0}, {-257, 0}, {0, 256}, {0, -257} + }; + for (int[] coordinate : chunks) { + ChunkAccess chunk = level.getChunkSource().getChunk(coordinate[0], coordinate[1], ChunkStatus.FULL, true); + helper.assertTrue(chunk instanceof LevelChunk, "The test must inspect a fully generated chunk"); + for (LevelChunkSection section : chunk.getSections()) { + helper.assertTrue(section.hasOnlyAir(), + "Finished exterior chunk contains blocks after world generation: " + chunk.getPos()); + } + } + helper.succeed(); + } + + @GameTest(maxTicks = 200) + public void compiledWorldDensityUsesTheSeed(GameTestHelper helper) { + ServerLevel level = helper.getLevel(); + var registries = level.registryAccess(); + NoiseGeneratorSettings settings = registries.lookupOrThrow(Registries.NOISE_SETTINGS) + .getOrThrow(SanctuarySpawn.SETTINGS).value(); + var noises = registries.lookupOrThrow(Registries.NOISE); + RandomState first = RandomState.create(noises, 0L, settings); + RandomState replay = RandomState.create(noises, 0L, settings); + RandomState otherSeed = RandomState.create(noises, 8675309L, settings); + DensityFunction density = settings.noiseRouter().finalDensity(); + boolean observedSeedVariation = false; + for (int x : new int[]{112, 160, 208}) { + for (int z : new int[]{-96, -48, 0, 48, 96}) { + for (int y : new int[]{64, 96, 128, 160}) { + float value = first.sampleBlockValueUncached(density, x, y, z); + float repeated = replay.sampleBlockValueUncached(density, x, y, z); + float changed = otherSeed.sampleBlockValueUncached(density, x, y, z); + helper.assertTrue(Float.isFinite(value) && Float.isFinite(changed), + "Compiled world density must remain finite"); + helper.assertTrue(Float.floatToIntBits(value) == Float.floatToIntBits(repeated), + "The actual datapack density must replay identically for the same seed"); + observedSeedVariation |= Float.floatToIntBits(value) != Float.floatToIntBits(changed); + } + } + } + helper.assertTrue(observedSeedVariation, "Different world seeds must change the production density"); + helper.succeed(); + } +} diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/mixin/SanctuaryGameTestServerMixin.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/mixin/SanctuaryGameTestServerMixin.java new file mode 100644 index 0000000..3ed58a0 --- /dev/null +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/mixin/SanctuaryGameTestServerMixin.java @@ -0,0 +1,37 @@ +package fr.koka.sanctuary.gametest.mixin; + +import fr.koka.sanctuary.SanctuaryMod; +import fr.koka.sanctuary.gametest.SanctuaryWorldGameTests; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.GameTestServer; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.levelgen.presets.WorldPreset; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Test-only: run the real Sanctuary datapack instead of the framework's flat fixture. */ +@Mixin(GameTestServer.class) +public abstract class SanctuaryGameTestServerMixin { + @ModifyArg(method = "", at = @At(value = "INVOKE", target = + "Lnet/minecraft/world/level/levelgen/WorldOptions;(JZZ)V"), index = 1) + private static boolean sanctuary$enableStructureGeneration(boolean original) { + return true; + } + + @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 sanctuary$selectActualPreset() { + return ResourceKey.create(Registries.WORLD_PRESET, SanctuaryMod.id("sanctuary")); + } + + @Inject(method = "startTests", at = @At("HEAD")) + private void sanctuary$captureInitialSpawn(ServerLevel level, CallbackInfo ci) { + // The framework moves its own spawn to a random test grid after this point. + SanctuaryWorldGameTests.initialSpawn = level.getRespawnData(); + } +} diff --git a/mods/sanctuary/src/gametest/resources/fabric.mod.json b/mods/sanctuary/src/gametest/resources/fabric.mod.json new file mode 100644 index 0000000..9b5b792 --- /dev/null +++ b/mods/sanctuary/src/gametest/resources/fabric.mod.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "sanctuary-gametest", + "version": "1.0.0", + "name": "Sanctuary generation tests", + "environment": "*", + "license": "GPL-3.0-or-later", + "entrypoints": { + "fabric-gametest": ["fr.koka.sanctuary.gametest.SanctuaryWorldGameTests"] + }, + "mixins": ["sanctuary-gametest.mixins.json"], + "depends": { + "sanctuary": "*", + "fabric-gametest-api-v1": "*" + } +} diff --git a/mods/sanctuary/src/gametest/resources/sanctuary-gametest.mixins.json b/mods/sanctuary/src/gametest/resources/sanctuary-gametest.mixins.json new file mode 100644 index 0000000..2fa3f54 --- /dev/null +++ b/mods/sanctuary/src/gametest/resources/sanctuary-gametest.mixins.json @@ -0,0 +1,7 @@ +{ + "required": true, + "package": "fr.koka.sanctuary.gametest.mixin", + "compatibilityLevel": "JAVA_25", + "mixins": ["SanctuaryGameTestServerMixin"], + "injectors": {"defaultRequire": 1} +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java new file mode 100644 index 0000000..8708557 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/SanctuaryMod.java @@ -0,0 +1,24 @@ +package fr.koka.sanctuary; + +import fr.koka.sanctuary.worldgen.MainIslandDensity; +import net.fabricmc.api.ModInitializer; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class SanctuaryMod implements ModInitializer { + public static final String ID = "sanctuary"; + public static final Logger LOGGER = LoggerFactory.getLogger(ID); + + public static Identifier id(String path) { + return Identifier.fromNamespaceAndPath(ID, path); + } + + @Override + public void onInitialize() { + Registry.register(BuiltInRegistries.DENSITY_FUNCTION_TYPE, id("main_island"), MainIslandDensity.CODEC); + LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available."); + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/InitialSpawnMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/InitialSpawnMixin.java new file mode 100644 index 0000000..a010f9f --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/InitialSpawnMixin.java @@ -0,0 +1,20 @@ +package fr.koka.sanctuary.mixin; + +import fr.koka.sanctuary.worldgen.SanctuarySpawn; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.progress.LevelLoadListener; +import net.minecraft.world.level.storage.ServerLevelData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftServer.class) +public abstract class InitialSpawnMixin { + @Inject(method = "setInitialSpawn", at = @At("RETURN")) + private static void sanctuary$initialSpawn(ServerLevel level, ServerLevelData data, boolean bonusChest, + boolean debugWorld, LevelLoadListener listener, CallbackInfo ci) { + SanctuarySpawn.initialize(level); + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/IslandShape.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/IslandShape.java new file mode 100644 index 0000000..49a1535 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/IslandShape.java @@ -0,0 +1,27 @@ +package fr.koka.sanctuary.worldgen; + +/** Geometry around the legacy seeded floating-island field. Coordinates never wrap. */ +public final class IslandShape { + public static final int RADIUS = 256; + public static final int EDGE_WARP = 32; + public static final int TERRAIN_LIMIT = RADIUS + EDGE_WARP; + public static final int CORE_Y = 76; + + private IslandShape() {} + + public static float density(int x, int y, int z, float terrain, float distortion) { + double distance = Math.hypot((double) x, (double) z); + // This independent cutoff remains empty for every seed and every input field. + if (distance >= TERRAIN_LIMIT || y <= 0 || y >= 256) { + return -1.0F; + } + + double edge = (RADIUS + Math.clamp(distortion, -1.0F, 1.0F) * EDGE_WARP - distance) / 48.0; + // A modest central ellipsoid guarantees initial ground even for a seed with a hole at 0,0. + // The surrounding legacy noise remains free to form overhangs, holes and separate fragments. + double core = 1.0 - distance * distance / (96.0 * 96.0) + - (y - CORE_Y) * (double) (y - CORE_Y) / (42.0 * 42.0); + double value = Math.min(Math.max(terrain, core), edge); + return (float) Math.clamp(value, -1.0, 1.0); + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/MainIslandDensity.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/MainIslandDensity.java new file mode 100644 index 0000000..4f9414a --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/MainIslandDensity.java @@ -0,0 +1,61 @@ +package fr.koka.sanctuary.worldgen; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.util.Interval; +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; + +/** 26.3's compiled density API; both child functions receive Minecraft's world seed. */ +public record MainIslandDensity(DensityFunction terrain, DensityFunction distortion) implements DensityFunction { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + DensityFunction.CODEC.fieldOf("terrain").forGetter(MainIslandDensity::terrain), + DensityFunction.CODEC.fieldOf("distortion").forGetter(MainIslandDensity::distortion) + ).apply(instance, MainIslandDensity::new)); + + @Override + public DensitySampler compileSampler(CompileContext context) { + DensitySampler terrainSampler = terrain.compileSampler(context); + DensitySampler distortionSampler = distortion.compileSampler(context); + return new DensitySampler() { + @Override + public float sampleValue(SamplerContext samplerContext, int x, int y, int z) { + if (Math.hypot((double) x, (double) z) >= IslandShape.TERRAIN_LIMIT || y <= 0 || y >= 256) { + return -1.0F; + } + return IslandShape.density(x, y, z, + terrainSampler.sampleValue(samplerContext, x, y, z), + distortionSampler.sampleValue(samplerContext, x, 0, z)); + } + + @Override + public void sampleVolume(SamplerContext context, DensityBuffer buffer, DensityVolume volume) { + DensitySampler.sampleVolumeNaive(context, buffer, volume, this); + } + }; + } + + @Override + public DensityFunction rewriteChildren(DfRewriteRule rule) { + return new MainIslandDensity(rule.rewrite(terrain), rule.rewrite(distortion)); + } + + @Override + public Interval range() { + return Interval.of(-1.0F, 1.0F); + } + + @Override + public int domainAxes() { + return ALL_AXES; + } + + @Override + public MapCodec codec() { + return CODEC; + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuarySpawn.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuarySpawn.java new file mode 100644 index 0000000..fd35f43 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/worldgen/SanctuarySpawn.java @@ -0,0 +1,73 @@ +package fr.koka.sanctuary.worldgen; + +import fr.koka.sanctuary.SanctuaryMod; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; +import net.minecraft.world.level.storage.LevelData; + +public final class SanctuarySpawn { + public static final ResourceKey SETTINGS = + ResourceKey.create(Registries.NOISE_SETTINGS, SanctuaryMod.id("sanctuary")); + + private SanctuarySpawn() {} + + public static boolean usesSanctuaryGenerator(ServerLevel level) { + return level.dimension() == Level.OVERWORLD + && level.getChunkSource().getGenerator() instanceof NoiseBasedChunkGenerator generator + && generator.generatorSettings().is(SETTINGS); + } + + /** Called only during first creation; saved spawns and /setworldspawn survive subsequent loads. */ + public static void initialize(ServerLevel level) { + if (!usesSanctuaryGenerator(level)) { + return; + } + for (int radius = 0; radius <= 48; radius += 4) { + for (int x = -radius; x <= radius; x += 4) { + for (int z = -radius; z <= radius; z += 4) { + if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue; + // Level.getHeight returns minY for an unloaded chunk; request full terrain first. + level.getChunk(x >> 4, z >> 4); + int top = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z); + for (int y = top; y >= 32; y--) { + BlockPos pos = new BlockPos(x, y, z); + if (isSafe(level, pos)) { + level.setRespawnData(LevelData.RespawnData.of(Level.OVERWORLD, pos, 0.0F, 0.0F)); + SanctuaryMod.LOGGER.info("Sanctuary shared spawn: {}", pos); + return; + } + } + } + } + } + throw new IllegalStateException("Sanctuary could not find safe ground near the guaranteed island core"); + } + + private static boolean isSafe(ServerLevel level, BlockPos pos) { + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + BlockPos feet = pos.offset(dx, 0, dz); + BlockState floor = level.getBlockState(feet.below()); + // In 26.3 the dirt tag no longer includes grass_block. + if (!(floor.is(Blocks.GRASS_BLOCK) || floor.is(BlockTags.DIRT) || floor.is(BlockTags.BASE_STONE_OVERWORLD)) + || !floor.isCollisionShapeFullBlock(level, feet.below()) + || !level.getFluidState(feet).isEmpty() + || !level.getFluidState(feet.above()).isEmpty() + || !level.getBlockState(feet).getCollisionShape(level, feet).isEmpty() + || !level.getBlockState(feet.above()).getCollisionShape(level, feet.above()).isEmpty()) { + return false; + } + } + } + return true; + } +} diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json new file mode 100644 index 0000000..f0a26c3 --- /dev/null +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json @@ -0,0 +1,4 @@ +{ + "generator.sanctuary.sanctuary": "Sanctuary", + "biome.sanctuary.starter_forest": "Sanctuary Forest" +} diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json new file mode 100644 index 0000000..ad8ea94 --- /dev/null +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json @@ -0,0 +1,4 @@ +{ + "generator.sanctuary.sanctuary": "Sanctuary", + "biome.sanctuary.starter_forest": "For\u00eat de Sanctuary" +} diff --git a/mods/sanctuary/src/main/resources/data/minecraft/tags/worldgen/world_preset/normal.json b/mods/sanctuary/src/main/resources/data/minecraft/tags/worldgen/world_preset/normal.json new file mode 100644 index 0000000..ae679a4 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/minecraft/tags/worldgen/world_preset/normal.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "sanctuary:sanctuary" + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/starter_forest.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/starter_forest.json new file mode 100644 index 0000000..036ce59 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/biome/starter_forest.json @@ -0,0 +1,175 @@ +{ + "attributes": { + "minecraft:audio/background_music": { + "default": { + "max_delay": 24000, + "min_delay": 12000, + "sound": "minecraft:music.overworld.forest" + } + }, + "minecraft:gameplay/natural_mob_spawns": { + "argument": { + "spawn_costs": {}, + "spawns_by_category": { + "ambient": [ + { + "type": "minecraft:bat", + "count": 8, + "weight": 10 + } + ], + "creature": [ + { + "type": "minecraft:sheep", + "count": 4, + "weight": 12 + }, + { + "type": "minecraft:pig", + "count": 4, + "weight": 10 + }, + { + "type": "minecraft:chicken", + "count": 4, + "weight": 10 + }, + { + "type": "minecraft:cow", + "count": 4, + "weight": 8 + }, + { + "type": "minecraft:wolf", + "count": 4, + "weight": 5 + } + ], + "monster": [ + { + "type": "minecraft:spider", + "count": 4, + "weight": 100 + }, + { + "type": "minecraft:zombie", + "count": 4, + "weight": 95 + }, + { + "type": "minecraft:zombie_villager", + "count": 1, + "weight": 5 + }, + { + "type": "minecraft:skeleton", + "count": 4, + "weight": 100 + }, + { + "type": "minecraft:creeper", + "count": 4, + "weight": 100 + }, + { + "type": "minecraft:slime", + "count": 4, + "weight": 100 + }, + { + "type": "minecraft:enderman", + "count": { + "type": "minecraft:uniform", + "max_inclusive": 4, + "min_inclusive": 1 + }, + "weight": 10 + }, + { + "type": "minecraft:witch", + "count": 1, + "weight": 5 + } + ], + "underground_water_creature": [ + { + "type": "minecraft:glow_squid", + "count": { + "type": "minecraft:uniform", + "max_inclusive": 6, + "min_inclusive": 4 + }, + "weight": 10 + } + ] + } + }, + "modifier": "overlay" + }, + "minecraft:visual/sky_color": "#79a6ff" + }, + "carvers": [], + "downfall": 0.8, + "effects": { + "water_color": "#3f76e4" + }, + "features": [ + [], + [], + [], + [], + [], + [], + [ + "minecraft:ore_dirt", + "minecraft:ore_gravel", + "minecraft:ore_granite_upper", + "minecraft:ore_granite_lower", + "minecraft:ore_diorite_upper", + "minecraft:ore_diorite_lower", + "minecraft:ore_andesite_upper", + "minecraft:ore_andesite_lower", + "minecraft:ore_tuff", + "minecraft:ore_coal_upper", + "minecraft:ore_coal_lower", + "minecraft:ore_iron_upper", + "minecraft:ore_iron_middle", + "minecraft:ore_iron_small", + "minecraft:ore_gold", + "minecraft:ore_gold_lower", + "minecraft:ore_redstone", + "minecraft:ore_redstone_lower", + "minecraft:ore_diamond", + "minecraft:ore_diamond_medium", + "minecraft:ore_diamond_large", + "minecraft:ore_diamond_buried", + "minecraft:ore_lapis", + "minecraft:ore_lapis_buried", + "minecraft:ore_copper", + "minecraft:underwater_magma", + "minecraft:disk_sand", + "minecraft:disk_clay", + "minecraft:disk_gravel" + ], + [], + [], + [ + "minecraft:glow_lichen", + "minecraft:forest_flowers", + "minecraft:trees_birch_and_oak_leaf_litter", + "minecraft:patch_bush", + "minecraft:flower_default", + "minecraft:patch_grass_forest", + "minecraft:brown_mushroom_normal", + "minecraft:red_mushroom_normal", + "minecraft:patch_pumpkin", + "minecraft:patch_sugar_cane", + "minecraft:patch_firefly_bush_near_water" + ], + [ + "minecraft:freeze_top_layer" + ] + ], + "has_precipitation": true, + "temperature": 0.7 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/base_3d_noise.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/base_3d_noise.json new file mode 100644 index 0000000..88b808a --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/base_3d_noise.json @@ -0,0 +1,8 @@ +{ + "type": "minecraft:old_blended_noise", + "smear_scale_multiplier": 4.0, + "xz_factor": 80.0, + "xz_scale": 0.25, + "y_factor": 160.0, + "y_scale": 0.25 +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/final_density.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/final_density.json new file mode 100644 index 0000000..3492b0a --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/final_density.json @@ -0,0 +1,22 @@ +{ + "type": "sanctuary:main_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" + } + } + }, + "distortion": { + "type": "minecraft:noise", + "noise": "sanctuary:island_edge", + "xz_scale": 1.0, + "y_scale": 0.0 + } +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/floating_archipelago.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/floating_archipelago.json new file mode 100644 index 0000000..8411551 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/density_function/floating_archipelago.json @@ -0,0 +1,25 @@ +{ + "type": "minecraft:lerp", + "alpha": { + "type": "minecraft:gradient", + "axis": "y", + "from_coordinate": 4, + "to_coordinate": 32, + "from_value": 0.0, + "to_value": 1.0 + }, + "first": -0.234375, + "second": { + "type": "minecraft:lerp", + "alpha": { + "type": "minecraft:gradient", + "axis": "y", + "from_coordinate": 184, + "to_coordinate": 440, + "from_value": 1.0, + "to_value": 0.0 + }, + "first": -23.4375, + "second": "sanctuary:base_3d_noise" + } +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/starter_island.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/starter_island.json new file mode 100644 index 0000000..e37c587 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/material_rule/starter_island.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:sequence", + "sequence": [ + { + "type": "minecraft:condition", + "if_true": "minecraft:on_floor", + "then_run": { + "type": "minecraft:block", + "result_state": "minecraft:grass_block" + } + }, + { + "type": "minecraft:condition", + "if_true": "minecraft:under_floor", + "then_run": { + "type": "minecraft:block", + "result_state": "minecraft:dirt" + } + } + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/island_edge.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/island_edge.json new file mode 100644 index 0000000..f3cee41 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise/island_edge.json @@ -0,0 +1,9 @@ +{ + "base_octave": -5, + "octave_count": 3, + "amplitude_modifiers": [ + 1.0, + 0.5, + 0.25 + ] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary.json new file mode 100644 index 0000000..87ad804 --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/noise_settings/sanctuary.json @@ -0,0 +1,23 @@ +{ + "default_block": "minecraft:stone", + "default_fluid": "minecraft:air", + "disable_mob_generation": false, + "legacy_random_source": true, + "material_rule": "sanctuary:starter_island", + "noise": { + "height": 256, + "min_y": 0 + }, + "noise_router": { + "chunk_surface_level": 0.0, + "continents": 0.0, + "depth": 0.0, + "erosion": 0.0, + "final_density": "sanctuary:final_density", + "ridges": 0.0, + "temperature": 0.0, + "vegetation": 0.0 + }, + "sea_level": -64, + "spawn_target": [] +} diff --git a/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json new file mode 100644 index 0000000..e74b5ec --- /dev/null +++ b/mods/sanctuary/src/main/resources/data/sanctuary/worldgen/world_preset/sanctuary.json @@ -0,0 +1,36 @@ +{ + "dimensions": { + "minecraft:overworld": { + "type": "minecraft:overworld", + "generator": { + "type": "minecraft:noise", + "biome_source": { + "type": "minecraft:fixed", + "biome": "sanctuary:starter_forest" + }, + "settings": "sanctuary:sanctuary" + } + }, + "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" + } + } + } +} diff --git a/mods/sanctuary/src/main/resources/fabric.mod.json b/mods/sanctuary/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..35463dd --- /dev/null +++ b/mods/sanctuary/src/main/resources/fabric.mod.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "id": "sanctuary", + "version": "${version}", + "name": "Sanctuary", + "description": "A floating world that grows through the actions of its players.", + "authors": [ + "Koka" + ], + "contact": { + "sources": "https://git.botsu.net/koka/sanctuary-beta", + "issues": "https://git.botsu.net/koka/sanctuary-beta/issues" + }, + "license": "GPL-3.0-or-later", + "environment": "*", + "entrypoints": { + "main": [ + "fr.koka.sanctuary.SanctuaryMod" + ] + }, + "mixins": [ + "sanctuary.mixins.json" + ], + "depends": { + "fabricloader": ">=${loader_version}", + "minecraft": "${minecraft_dependency_version}", + "java": ">=25", + "fabric-api": ">=${fabric_api_version}" + } +} diff --git a/mods/sanctuary/src/main/resources/sanctuary.mixins.json b/mods/sanctuary/src/main/resources/sanctuary.mixins.json new file mode 100644 index 0000000..c6a65ac --- /dev/null +++ b/mods/sanctuary/src/main/resources/sanctuary.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "fr.koka.sanctuary.mixin", + "compatibilityLevel": "JAVA_25", + "mixins": [ + "InitialSpawnMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WorldgenSmoke.java b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WorldgenSmoke.java new file mode 100644 index 0000000..2446806 --- /dev/null +++ b/mods/sanctuary/src/test/java/fr/koka/sanctuary/worldgen/WorldgenSmoke.java @@ -0,0 +1,39 @@ +package fr.koka.sanctuary.worldgen; + +/** Run with :sanctuary:worldgenSmoke. No Minecraft client or test library is required. */ +public final class WorldgenSmoke { + public static void main(String[] args) { + // Even an entirely negative legacy noise field must leave a safe, broad starter core. + for (int x = -16; x <= 16; x++) { + for (int z = -16; z <= 16; z++) { + require(IslandShape.density(x, IslandShape.CORE_Y, z, -1000, -1) > 0, "missing spawn core"); + } + } + // Strongly positive children must never restore periodic islands in any direction. + int[] distances = {288, 320, 512, 1024, 4096, 10000, 1000000, 29999984, Integer.MAX_VALUE}; + for (int distance : distances) { + for (int y = -64; y <= 320; y += 4) { + for (int sign : new int[]{-1, 1}) { + require(IslandShape.density(sign * distance, y, 0, 1000, 1000) < 0, "terrain outside X limit"); + require(IslandShape.density(0, y, sign * distance, 1000, 1000) < 0, "terrain outside Z limit"); + } + } + } + require(IslandShape.density(0, 0, 0, 1000, 0) < 0, "island touches world bottom"); + require(IslandShape.density(0, 256, 0, 1000, 0) < 0, "island touches world top"); + require(IslandShape.density(160, 100, 20, 0.75F, 0) > 0, "positive legacy field lost"); + require(IslandShape.density(160, 100, 20, -0.75F, 0) < 0, "negative legacy field lost"); + for (int x = -320; x <= 320; x += 7) { + for (int z = -320; z <= 320; z += 7) { + float first = IslandShape.density(x, 100, z, 0.35F, 0.25F); + require(first == IslandShape.density(-z, 100, x, 0.35F, 0.25F), "envelope biases a direction"); + require(first >= -1 && first <= 1 && Float.isFinite(first), "invalid density range"); + } + } + System.out.println("WorldgenSmoke: core, input-field preservation, infinite void, symmetry and density bounds passed."); + } + + private static void require(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } +} diff --git a/packwiz/.packwizignore b/packwiz/.packwizignore new file mode 100644 index 0000000..8c76f0a --- /dev/null +++ b/packwiz/.packwizignore @@ -0,0 +1,9 @@ +.DS_Store +README.md +*.mrpack +logs/ +crash-reports/ +saves/ +screenshots/ +options.txt +servers.dat diff --git a/packwiz/README.md b/packwiz/README.md new file mode 100644 index 0000000..4de302c --- /dev/null +++ b/packwiz/README.md @@ -0,0 +1,45 @@ +# Pack Sanctuary + +Ce dossier contient le manifeste packwiz source et Fabric API, avec une version +exacte de Minecraft et du chargeur Fabric. Les autres mods communautaires sont +des intégrations futures à vérifier par ticket. + +Depuis la racine : + +```sh +./gradlew check build assemblePack +``` + +Le dossier `build/packwiz/` contient le pack complet de développement : manifeste, +index, dépendances distantes et JAR Sanctuary construit localement. Les sources +dans ce dossier `packwiz/` ne contiennent jamais de JAR Sanctuary généré. + +Pour le servir à un installateur packwiz depuis cette machine : + +```sh +cd build/packwiz +packwiz serve +``` + +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.1.mrpack +``` + +L'export contient le JAR Sanctuary local et la référence vérifiée de Fabric API. +Il ne publie rien sur Modrinth. + +Utiliser l'URL HTTP affichée de `pack.toml` dans packwiz-installer. Le serveur +local doit rester actif pendant l'installation. Ce n'est pas une publication +hébergée permanente. Une installation manuelle reste possible : copier le JAR +Sanctuary construit et Fabric API dans `mods/` d'une nouvelle instance +Minecraft 26.3-pre-2 / Fabric 0.19.5 / Java 25. + +Pour modifier les dépendances, utiliser packwiz ici puis `packwiz refresh`. +La cible Minecraft, Fabric et Fabric API doit rester synchronisée avec +`gradle.properties`. Le build vérifie versions et hashes de l'index. + +Les resource packs et shaders en préparation restent dans leurs dossiers de +sources tant que leur ticket de distribution n'est pas terminé. diff --git a/packwiz/index.toml b/packwiz/index.toml new file mode 100644 index 0000000..5473274 --- /dev/null +++ b/packwiz/index.toml @@ -0,0 +1,6 @@ +hash-format = "sha256" + +[[files]] +file = "mods/fabric-api.pw.toml" +hash = "583fbe8be73d0f2802623bb4467a7056070d09fe2f412c39fc601b9dcf7a6fd5" +metafile = true diff --git a/packwiz/mods/fabric-api.pw.toml b/packwiz/mods/fabric-api.pw.toml new file mode 100644 index 0000000..0c74465 --- /dev/null +++ b/packwiz/mods/fabric-api.pw.toml @@ -0,0 +1,13 @@ +name = "Fabric API" +filename = "fabric-api-0.160.0+26.3.jar" +side = "both" + +[download] +url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/o9uChmGq/fabric-api-0.160.0%2B26.3.jar" +hash-format = "sha512" +hash = "0f9fb41ec3e480e7680e1640f90fb82a9298f5848c049ded3be47605615c3117fc17fc7acab59ec145dd4653422b742df5c5f137608d243d9aa4bf2d9ba2427d" + +[update] +[update.modrinth] +mod-id = "P7dR8mSH" +version = "o9uChmGq" diff --git a/packwiz/pack.toml b/packwiz/pack.toml new file mode 100644 index 0000000..5a6951a --- /dev/null +++ b/packwiz/pack.toml @@ -0,0 +1,13 @@ +name = "Sanctuary" +author = "KOKA99CAB" +version = "0.1.0-alpha.1" +pack-format = "packwiz:1.1.0" + +[index] +file = "index.toml" +hash-format = "sha256" +hash = "b590dd2bc0267a4e551b559d0987179d64e3b90ccb73552d694db153e9e1b1f4" + +[versions] +fabric = "0.19.5" +minecraft = "26.3-pre-2" diff --git a/ressources-pack/README.md b/ressources-pack/README.md new file mode 100644 index 0000000..e058b36 --- /dev/null +++ b/ressources-pack/README.md @@ -0,0 +1,9 @@ +# Resource packs + +Sources des resource packs propres à Sanctuary. Le nom de ce dossier existant +est conservé ; le dossier installé dans Minecraft s'appelle `resourcepacks/`. + +Le visuel `helloworld/assets/minecraft_title.png` est présent à l'initialisation. +Il reste une source de travail, pas encore un resource pack installable. +Les mentions, crédits, icône, écran de chargement et activation conditionnelle +de Golden Days dans la dimension Alpha seront traités par tickets distincts. diff --git a/ressources-pack/helloworld/assets/minecraft_title.png b/ressources-pack/helloworld/assets/minecraft_title.png new file mode 100644 index 0000000000000000000000000000000000000000..7aa0da7f8f26a770c1329409287fd0d584d1675a GIT binary patch literal 43647 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 PBfv#1gY!k_?C$;_R!yZz literal 0 HcmV?d00001 diff --git a/scripts/pack.py b/scripts/pack.py new file mode 100644 index 0000000..8537447 --- /dev/null +++ b/scripts/pack.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Validate the source pack or stage an installable packwiz directory (stdlib only).""" +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import sys + +try: + import tomllib +except ImportError: + # macOS ships Python 3.9; use an installed modern Python without pip packages. + for name in ('python3.14', 'python3.13', 'python3.12', 'python3.11'): + candidate = shutil.which(name) + if candidate and Path(candidate).resolve() != Path(sys.executable).resolve(): + os.execv(candidate, [candidate, *sys.argv]) + raise SystemExit('Python 3.11+ is required for pack verification (tomllib).') + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / 'packwiz' + + +def read_toml(path): + return tomllib.loads(path.read_text(encoding='utf-8')) + + +def sha256(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def properties(): + return dict(line.split('=', 1) for line in + (ROOT / 'gradle.properties').read_text().splitlines() + if line.strip() and not line.lstrip().startswith('#')) + + +def require(condition, message): + if not condition: + raise SystemExit(message) + + +def indexed_path(base, relative): + path = (base / relative).resolve() + require(path.is_relative_to(base.resolve()), 'Index path escapes pack: ' + relative) + require(path.is_file(), 'Missing indexed file: ' + str(path)) + return path + + +def check_index(base): + pack = read_toml(base / 'pack.toml') + index_path = indexed_path(base, pack['index']['file']) + require(pack['index']['hash-format'] == 'sha256', 'Pack index must use sha256') + require(sha256(index_path) == pack['index']['hash'], 'Stale pack hash; run packwiz refresh') + index = read_toml(index_path) + require(index['hash-format'] == 'sha256', 'Index files must use sha256') + entries = index.get('files', []) + require(len({entry['file'] for entry in entries}) == len(entries), 'Duplicate index entry') + for entry in entries: + path = indexed_path(base, entry['file']) + require(sha256(path) == entry['hash'], 'Stale index hash: ' + entry['file']) + if entry.get('metafile'): + metadata = read_toml(path) + download = metadata['download'] + require(download['url'].startswith('https://'), 'Dependency URL must use HTTPS') + algorithm = download['hash-format'] + require(algorithm in ('sha256', 'sha512'), 'Dependency needs SHA-256/512') + length = 64 if algorithm == 'sha256' else 128 + require(re.fullmatch('[0-9a-f]{' + str(length) + '}', download['hash']), + 'Invalid dependency download hash') + return pack, entries + + +def check(): + values = properties() + pack, entries = check_index(SOURCE) + require(pack['versions']['minecraft'] == values['minecraft_version'], 'Minecraft version drift') + require(pack['versions']['fabric'] == values['loader_version'], 'Fabric Loader version drift') + require(pack['version'] == values['pack_version'], 'Pack version drift') + api = read_toml(SOURCE / 'mods/fabric-api.pw.toml') + require(api['filename'] == 'fabric-api-' + values['fabric_api_version'] + '.jar', + 'Fabric API version drift') + require(any(e['file'] == 'mods/fabric-api.pw.toml' and e.get('metafile') for e in entries), + 'Fabric API is missing from the index') + print('Pack versions and index hashes verified.') + return values, entries + + +def assemble(): + values, entries = check() + jar = ROOT / 'mods/sanctuary/build/libs' / ('sanctuary-' + values['mod_version'] + '.jar') + require(jar.is_file(), 'Build Sanctuary before assembling the pack: ' + str(jar)) + destination = ROOT / 'build/packwiz' + # Only this generated staging directory is replaced; never touch a game instance. + if destination.exists(): + shutil.rmtree(destination) + destination.mkdir(parents=True) + staged_entries = [] + for entry in entries: + source = indexed_path(SOURCE, entry['file']) + target = destination / entry['file'] + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + staged_entries.append(entry) + target = destination / 'mods' / jar.name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(jar, target) + staged_entries.append({'file': 'mods/' + jar.name, 'hash': sha256(target)}) + lines = ['hash-format = "sha256"', ''] + for entry in sorted(staged_entries, key=lambda e: e['file']): + lines.append('[[files]]') + for key, value in entry.items(): + lines.append(key + ' = ' + json.dumps(value)) + lines.append('') + index_path = destination / 'index.toml' + index_path.write_text('\n'.join(lines), encoding='utf-8') + manifest = (SOURCE / 'pack.toml').read_text(encoding='utf-8') + manifest, count = re.subn(r'(?m)^hash = "[a-f0-9]+"$', + 'hash = "' + sha256(index_path) + '"', manifest) + require(count == 1, 'Expected a single pack index hash') + (destination / 'pack.toml').write_text(manifest, encoding='utf-8') + check_index(destination) + print('Installable development pack staged at ' + str(destination)) + + +if __name__ == '__main__': + require(len(sys.argv) == 2 and sys.argv[1] in ('check', 'assemble'), + 'Usage: python3 scripts/pack.py check|assemble') + if sys.argv[1] == 'assemble': + assemble() + else: + check() diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..12a713f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + maven { url = 'https://maven.fabricmc.net/' } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'sanctuary-beta' +include('sanctuary') +project(':sanctuary').projectDir = file('mods/sanctuary') diff --git a/shaders-pack/README.md b/shaders-pack/README.md new file mode 100644 index 0000000..1edbf1d --- /dev/null +++ b/shaders-pack/README.md @@ -0,0 +1,5 @@ +# Shader packs + +Emplacement des sources et réglages propres à Sanctuary. Aucun shader n'est +encore distribué. Les shaders externes seront ajoutés au manifeste packwiz +avec version, URL, hash, licence et compatibilité vérifiée pour le jeu et Iris.