Add Sanctuary gems, original clay colors and shared discovery commands for beta.126
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-17 17:10:15 +02:00
parent 1547396c06
commit 99860af312
119 changed files with 1895 additions and 27 deletions
+15 -7
View File
@@ -20,13 +20,13 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
L’économie, les claims et les dimensions décrits dans la vision restent L’économie, les claims et les dimensions décrits dans la vision restent
à implémenter. à implémenter.
La version **beta.125** étend les sculptures au centre, aux bords et aux coins, La version **beta.126** ajoute le rubis et le saphir, leurs minerais et leurs
en bas, au milieu ou en haut du bloc. Les éclats reprennent la couleur des voxels blocs, rangés après l'émeraude. Leur génération est configurable et limitée
et le Clay Workshop propose les animaux déjà découverts, en plus des imports GLB. aux nouveaux chunks de Sanctuary. L'argile classique du Clay Workshop conserve
Le [ticket de livraison](docs/sculpture-placement-beta125.md) décrit le placement, les couleurs originales des modèles. La commande `/sanctuary discovery`
la compatibilité des anciennes sculptures et les vérifications. La release est enrichit les découvertes communes, globalement ou précisément, et actualise
publiée et linstance **Sanctuary Beta** synchronisée, sauvegardes et réglages les recettes et catalogues liés. Le [ticket de livraison](docs/gems-beta126.md)
conservés. Packs normal/Test et template sont aussi dans le dossier `build/`. décrit les règles, la compatibilité et les vérifications.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**. latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées. Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -37,6 +37,14 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres. l’étude de Xaero et des alternatives libres.
## beta.126 — Gemmes, argile et découvertes
Rubis et saphir avec filons configurables, couleurs originales avec l'argile,
commandes de découverte globales ou ciblées.
[Contrat et vérifications](docs/gems-beta126.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.126/Sanctuary-beta.126.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.126/Sanctuary-Test-beta.126.mrpack).
## beta.125 — Sculptures : placement, éclats et animaux ## beta.125 — Sculptures : placement, éclats et animaux
Coins et trois hauteurs sur la grille 1/16 ; particules issues des voxels ; Coins et trois hauteurs sur la grille 1/16 ; particules issues des voxels ;
+136
View File
@@ -0,0 +1,136 @@
# GEM-01 — Gemmes, argile et découvertes — beta.126
Contrat du 17 septembre 2026, branche `codex/gems-beta126`, Minecraft 26.3.
Deux gemmes natives complètent l'émeraude : `sanctuary:ruby` et
`sanctuary:sapphire`. Chaque famille comprend un bloc de stockage, un minerai
ordinaire et un minerai des abîmes. Les huit PNG du créateur sont copiés à
l'identique ; leurs empreintes figurent dans `tools/gems-textures-beta126.json`.
Les noms des fichiers français `saphir` sont associés aux identifiants anglais
stables `sapphire`, sans modifier les pixels. Les deux items utilisent les fichiers de remplacement
`Desktop/ruby.png` et `Desktop/saphir.png` fournis en fin de livraison.
L'ordre créatif est émeraude, rubis, saphir : ingrédients, blocs de stockage,
minerais ordinaires et minerais des abîmes dans leurs onglets natifs respectifs.
Minage à la pioche de fer ou mieux, résistance et expérience comme l'émeraude,
Fortune, Toucher de soie, cuisson/four à fusion et conversion 9 gemmes ↔ 1 bloc.
Les blocs peuvent former un socle de balise et les gemmes servir de paiement.
Les recettes rejoignent la collection actuelle de l'émeraude, sans ajouter de
transactions de villageois. Libellés français et anglais, tags communs `c:`.
## Génération procédurale et contrat de compatibilité
Profil initial **gemmes 1 / beta.126** : quatre tentatives par chunk et par
gemme, filons natifs de taille 3, hauteur uniforme Y0256, probabilité de rejet
au contact de l'air de 0,25. Ces nombres sont un réglage initial à équilibrer
lors de la future refonte ; aucun quota de gemmes par chunk n'est garanti.
Les variantes ordinaires remplacent les roches du tag vanilla
`stone_ore_replaceables`, les variantes des abîmes celles de
`deepslate_ore_replaceables`.
Les fichiers `worldgen/feature/ore_<gemme>.json`,
`worldgen/placed_feature/ore_<gemme>.json` et les tags de biomes
`has_ore/<gemme>` séparent les cibles, la forme, la fréquence, la hauteur et
les régions. Un datapack peut les remplacer sans recompiler le mod. Le code
réutilise `OreFeature` et le hasard natif issu de la graine ; il limite ces
filons aux mondes utilisant `SanctuaryChunkGenerator`. Les biomes vanilla
partagés avec Sanctuary restent compatibles sans ajouter les gemmes aux
mondes vanilla, au Nether ou à l'End.
Cette évolution additive est autorisée pour les **nouveaux chunks uniquement**.
Aucun remplissage rétrospectif, aucune régénération, aucun remplacement de chunk
sauvegardé ni activation d'expansion. Les codecs du relief, les dimensions et
le format des sauvegardes restent inchangés. Les filons suivent les protections
existantes de la décoration native. Les anciens profils expérimentaux précédant
le générateur Sanctuary unifié ne sont pas étendus.
## Argile spéciale dans le Clay Workshop
Le bloc d'argile vanilla conserve les couleurs échantillonnées sur le modèle
original, animal découvert ou import GLB. Les argiles colorées et les autres
blocs continuent à transférer leur palette de texture. Une même règle est
appliquée à l'aperçu client et au résultat serveur. Le changement de matériau
invalide le résultat même si les deux palettes sont identiques. Le texte de
l'atelier explique l'exception en français et en anglais.
Une sculpture coûte toujours un bloc ; aucune recette, découverte, donnée de
sculpture déjà fabriquée ni règle de placement n'est modifiée.
## Commande de découverte commune
`/sanctuary discovery all` découvre tous les blocs, objets et espèces enregistrés.
Les variantes `blocks all`, `items all` et `mobs all` ciblent une catégorie ;
`block <identifiant>`, `item <identifiant>` et `mob <identifiant>` ciblent une
entrée avec la complétion native. Un sélecteur de joueurs en fin de commande
est facultatif ; sans cible, elle s'applique au joueur qui l'exécute.
Exemples :
```mcfunction
/sanctuary discovery all
/sanctuary discovery mobs all
/sanctuary discovery block minecraft:stone
/sanctuary discovery item minecraft:diamond
/sanctuary discovery mob minecraft:cow
/sanctuary discovery all @a
```
La permission suit « Autoriser les commandes » en solo et les droits opérateur
sur serveur. La console doit préciser la cible. Les identifiants invalides,
l'air et les types d'entités sans espèce découvrable sont refusés.
Les blocs passent par le registre de découvertes et son événement existant ;
les objets passent par `RecipeService.discover`. Les collections et recettes
sont ensuite recalculées normalement. Les espèces rejoignent `seen_mobs`,
la même donnée que l'observation naturelle, consommée par les familiers,
le Métabli et le Clay Workshop. Il n'existe aucun drapeau de déblocage parallèle.
Les catalogues déjà ouverts sont à rouvrir après la commande.
La commande conserve les quantités possédées et les compteurs de combat,
minage, pose et fabrication. Elle ne donne pas d'objets, n'achète pas les skills
et n'attribue pas d'advancements. Les recettes réservées aux opérateurs gardent
leurs règles. Les historiques personnels sont enrichis sans changer leur
schéma ; la commande refuse les données illisibles plutôt que les remplacer.
## Vérifications
Client natif Minecraft 26.3 / Java 25, `Gems126ClientChecks`, réussi en
1 min 08 s. Les mondes sont jetables, sans sauvegarde personnelle ouverte :
- Les huit items et six blocs sont présents, avec les huit textures exactes.
Quatre groupes créatifs respectent émeraude → rubis → saphir, sans doublons.
- Douze recettes natives, découverte du bloc de stockage, outil fer minimum,
Fortune III, Toucher de soie et balises passent les contrôles serveur.
- Graine **126**, profil gemmes 1, 81 chunks autour de l'origine : 7 rubis dans
la pierre et 8 dans la roche des abîmes ; 15 saphirs dans la pierre et 10 dans
la roche des abîmes. La génération est refusée dans le monde plat vanilla.
Les minerais persistent à la reconnexion, et un minerai retiré ne réapparaît pas.
- L'argile conserve exactement les couleurs des voxels d'une vache et d'un GLB
texturé. Bois et argile colorée transfèrent toujours leur palette. Le retour
à l'argile, l'égalité aperçu/résultat et le coût d'un bloc en survie sont vérifiés.
- La commande native respecte « Autoriser les commandes », accepte une cible
et la console, refuse les identifiants invalides et permet une espèce, un
bloc, un objet, toutes les espèces ou tout le contenu. Les recettes liées
sont recalculées. La répétition est idempotente et les découvertes survivent
à la reconnexion sans inventer de compteurs de combat ou de possession.
Logs et captures dans `build/gems126-client.log` et
`mods/sanctuary/build/run/clientGameTest/screenshots/`. Le GameTest dédié
reste exclu conformément au refus antérieur de son EULA. Le build complet et
les archives sont vérifiés séparément avant publication.
Build final `./gradlew check build assemblePack assembleTestPack
-x :sanctuary:runGameTest` réussi en **2 min 10 s**, 126 tâches. Les contrôles
natifs de gameplay précèdent uniquement le remplacement final des deux PNG
d'items ; les huit fichiers livrés sont vérifiés à l'octet près contre les
sources, tous en 16 × 16. Les JAR de sources correspondent aux sources courantes.
L'audit entre les binaires beta.125 et beta.126 ne trouve que les 112 entrées
de production prévues. Les archives normal/Test contiennent le même mod et
le template reproduit exactement ses ressources exportables.
| Artefact | SHA-256 |
| --- | --- |
| `Sanctuary-beta.126.mrpack` | `716221ce24549781ab91f5437ad2310497b198a4b61c43c40c67a27984365ae5` |
| `Sanctuary-Test-beta.126.mrpack` | `bbf22d7dd72e6746538b4ec28686f699225d4a59372304e5db42140a80e88bd1` |
| `sanctuary-beta.126.jar` | `85dbe1b41d98751029bb62e87d259522f8609758896c797b1e02fa7656ec0342` |
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3 fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md). # Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.125 mod_version=beta.126
pack_version=beta.125 pack_version=beta.126
resource_pack_version=beta.121 resource_pack_version=beta.121
maven_group=fr.koka.sanctuary maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3 jei_version=30.32.0-sanctuary.3
+4
View File
@@ -93,6 +93,10 @@ tasks.named('runGameTest') {
} }
tasks.named('processGametestResources') { tasks.named('processGametestResources') {
def gems126Client = providers.gradleProperty('sanctuaryGems126ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryGems126ClientTests', gems126Client)
if (gems126Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Gems126ClientChecks') }
def sculpture125Client = providers.gradleProperty('sanctuarySculpture125ClientTests').map { it.toBoolean() }.getOrElse(false) def sculpture125Client = providers.gradleProperty('sanctuarySculpture125ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuarySculpture125ClientTests', sculpture125Client) inputs.property('sanctuarySculpture125ClientTests', sculpture125Client)
if (sculpture125Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture125ClientChecks') } if (sculpture125Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture125ClientChecks') }
@@ -0,0 +1,42 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.companion.CompanionService;
import fr.koka.sanctuary.plans.*;
import java.nio.file.*;
import java.util.*;
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.inventory.ContainerInput;
import net.minecraft.world.item.*;
import net.minecraft.world.level.GameType;
/** Native preview, server result and survival payment for the special plain-clay material. */
final class Clay126Checks {
static void exercise(ClientGameTestContext c,TestServerContext server){
var pos=new BlockPos(4,1,4);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(3,1,3);p.setGameMode(GameType.SURVIVAL);p.getAbilities().invulnerable=true;p.onUpdateAbilities();p.level().setBlockAndUpdate(pos,Statuary.WORKSHOP.defaultBlockState());CompanionService.save(p,CompanionService.record(p).note("seen_mobs","cow"));Statuary.open(p,pos);p.containerMenu.getSlot(0).set(new ItemStack(Items.CLAY,2));p.containerMenu.broadcastChanges();});
c.waitForScreen(ClayWorkshopScreen.class);c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();((ClayWorkshopScreen)m.gui.screen()).load(EntityTypes.COW);});
c.waitFor(m->{var screen=(ClayWorkshopScreen)m.gui.screen();return !screen.busy()&&screen.selected()!=null&&screen.selected().equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem()));},250);
var cow=c.computeOnClient(m->((ClayWorkshopScreen)m.gui.screen()).selected());check(cow.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>3,"Cow source retains texture colors");
for(var material:List.of(Items.OAK_PLANKS,fr.koka.sanctuary.building.ColoredBricks.families().getFirst().clay().asItem(),Items.CLAY)){
server.runOnServer(s->{var menu=s.getPlayerList().getPlayers().getFirst().containerMenu;menu.getSlot(0).set(new ItemStack(material,2));menu.broadcastChanges();});
var expected=c.computeOnClient(m->ClayWorkshopMenu.appearance(cow,material,SculptureMaterialSampler.sample(new ItemStack(material))));
check(material==Items.CLAY?expected.equals(cow):!expected.equals(cow),"Only plain clay retains original colors");
c.waitFor(m->expected.equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())),180);
c.runOnClient(m->check(ItemStack.matches(((ClayWorkshopScreen)m.gui.screen()).preview(),m.player.containerMenu.getSlot(1).getItem()),"Preview matches authoritative material switch"));
}
c.takeScreenshot("clay126-original-cow");
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
server.waitFor(s->{var menu=s.getPlayerList().getPlayers().getFirst().containerMenu;return cow.equals(StatuaryEntity.sculpture(menu.getCarried()))&&menu.getSlot(0).getItem().getCount()==1;},100);
c.waitFor(m->cow.equals(StatuaryEntity.sculpture(m.player.containerMenu.getCarried()))&&m.player.containerMenu.getSlot(0).getItem().getCount()==1,100);
Path file=PlanClient.directory().resolve("clay126-"+UUID.randomUUID()+".glb");
try{Files.createDirectories(file.getParent());Files.write(file,Statuary106ClientChecks.fixture());var id=GlbModel.load(file).id();c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));
c.waitFor(m->{var screen=(ClayWorkshopScreen)m.gui.screen();return !screen.busy()&&screen.selected()!=null&&screen.selected().modelId().equals(id)&&screen.selected().equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem()));},250);
c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();check(screen.selected().cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>1,"Imported textured GLB retains its original colors");check(ItemStack.matches(screen.preview(),m.player.containerMenu.getSlot(1).getItem()),"Imported preview equals native result");});
}catch(Exception e){throw new AssertionError(e);}finally{try{Files.deleteIfExists(file);}catch(Exception ignored){}}
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.closeContainer();p.setGameMode(GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();});c.waitFor(m->m.gui.screen()==null,100);
System.out.println("CLAY126_PASS original animal and GLB colors, oak and colored clay palette transfer, switch back, matching previews and one clay paid in survival");
}
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
}
@@ -0,0 +1,47 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.companion.*;
import fr.koka.sanctuary.knowledge.*;
import fr.koka.sanctuary.plans.*;
import fr.koka.sanctuary.recipes.*;
import java.util.*;
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.stats.Stats;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Blocks;
final class Discovery126Checks {
static void exercise(ClientGameTestContext c,TestServerContext server){
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var dispatcher=s.getCommands().getDispatcher();
var source=p.createCommandSourceStack();var root=dispatcher.getRoot().getChild("sanctuary").getChild("discovery");
s.getWorldData().setAllowCommands(false);check(!root.canUse(source),"Discovery commands respect native Allow Commands");
try{dispatcher.execute("sanctuary discovery all",source);throw new AssertionError("Denied command executed");}catch(com.mojang.brigadier.exceptions.CommandSyntaxException expected){}
s.getWorldData().setAllowCommands(true);
var before=CompanionService.record(p);var items=new TreeSet<>(RecipeService.knowledge(p).items);int kills=p.getStats().getValue(Stats.ENTITY_KILLED,EntityTypes.WITHER);
command(p,"mob minecraft:wither");check(StatueDiscoveries.known(p).contains("minecraft:wither"),"Precise species unlocks sculpture catalogue");check(CompanionService.record(p).notes().get("seen_mobs").contains("wither"),"Same actual seen_mobs record");check(RecipeService.knowledge(p).items.equals(items),"Mob discovery invents no unrelated item discovery");check(p.getStats().getValue(Stats.ENTITY_KILLED,EntityTypes.WITHER)==kills,"No fake kill stat");
command(p,"block minecraft:pink_glazed_terracotta");var block=BlockKnowledgeService.knowledge(p,Blocks.GLAZED_TERRACOTTA.pick(DyeColor.PINK));check(block.seen()&&!block.possessed()&&block.mined()==0&&block.placed()==0,"Precise block is seen without fabricated possession or stats");check(RecipeService.knowledge(p).items.contains("minecraft:pink_glazed_terracotta")&&RecipeService.knowledge(p).collections.contains("sanctuary:terracotta"),"Block event unlocks its normal recipe collection");
command(p,"item minecraft:amethyst_shard");check(RecipeService.knowledge(p).items.contains("minecraft:amethyst_shard")&&RecipeService.knowledge(p).recipes.contains("minecraft:spyglass"),"Precise item unlocks linked recipes");
String old=CompanionService.record(p).json();check(run(p,"mob minecraft:item")==0,"Nonmob type refused");check(CompanionService.record(p).json().equals(old),"Rejected request does not change discoveries");
try{dispatcher.execute("sanctuary discovery item sanctuary:missing_item",source);throw new AssertionError("Missing ID accepted");}catch(com.mojang.brigadier.exceptions.CommandSyntaxException expected){}
command(p,"mobs all");allSpecies(p);var pos=new BlockPos(4,1,4);Statuary.open(p,pos);check(((ClayWorkshopMenu)p.containerMenu).view.statues().equals(StatueDiscoveries.known(p)),"Workshop sees the shared full catalogue");p.closeContainer();
command(p,"all");all(p);String species=CompanionService.record(p).json(),recipes=RecipeService.knowledge(p).json();var blocks=BlockKnowledgeService.snapshot(p);command(p,"all @s");check(species.equals(CompanionService.record(p).json())&&recipes.equals(RecipeService.knowledge(p).json())&&blocks.equals(BlockKnowledgeService.snapshot(p)),"Repeated all with target selector is idempotent");
check(CompanionService.record(p).paid()==before.paid()&&CompanionService.record(p).unlocked()==before.unlocked(),"Discoveries do not fabricate paid skills");
try{check(dispatcher.execute("sanctuary discovery mob minecraft:cow "+p.getGameProfile().name(),s.createCommandSourceStack())==1,"Console can address an online player");}catch(Exception e){throw new AssertionError(e);}
System.out.println("DISCOVERY126_PASS native permissions, precise block/item/species, linked recipes, shared catalogue, all three categories, all, selector, console, idempotence and unchanged combat/ownership counters");
});
c.waitFor(m->m.gui.screen()==null,100);
}
static void all(ServerPlayer p){
allSpecies(p);check(BlockKnowledgeService.snapshot(p).stream().allMatch(BlockKnowledge::known),"All block discoveries");
var knowledge=RecipeService.knowledge(p);for(var item:BuiltInRegistries.ITEM)if(item!=Items.AIR)check(knowledge.items.contains(BuiltInRegistries.ITEM.getKey(item).toString()),"All registered items discovered");
check(knowledge.recipes.contains("minecraft:spyglass")&&knowledge.recipes.contains("sanctuary:ruby_block"),"Vanilla and Sanctuary recipes are derived from discoveries");
}
static void allSpecies(ServerPlayer p){check(StatueDiscoveries.known(p).size()==BuiltInRegistries.ENTITY_TYPE.stream().filter(t->SpawnEggItem.byId(t).isPresent()).count(),"All registered species discovered");}
static void command(ServerPlayer p,String args){check(run(p,args)==1,"Discovery command succeeds: "+args);}
private static int run(ServerPlayer p,String args){try{return p.level().getServer().getCommands().getDispatcher().execute("sanctuary discovery "+args,p.createCommandSourceStack());}catch(Exception e){throw new AssertionError(e);}}
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
}
@@ -0,0 +1,88 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.gems.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.core.*;
import net.minecraft.core.registries.*;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.*;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.*;
import net.minecraft.world.item.crafting.*;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.GenerationStep;
/** Actual native registration, recipes, drops, tab order and naturally decorated Sanctuary chunks. */
public final class Gems126ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){
var intro=new AtomicBoolean(true);
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);var enter=new KeyEvent(InputConstants.KEY_RETURN,0,0);
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter);
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter);intro.set(false);
}});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Gems126 registry "+UUID.randomUUID());s.setSeed("126");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
world.getServer().runOnServer(s->{
var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();var pos=new BlockPos(0,4,0);var iron=new ItemStack(Items.IRON_PICKAXE);var silk=iron.copy();silk.enchant(l.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.SILK_TOUCH),1);var fortune=iron.copy();fortune.enchant(l.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.FORTUNE),3);
for(var f:Gems.FAMILIES){
for(var b:List.of(f.block(),f.ore(),f.deepslateOre())){var state=b.defaultBlockState();check(state.is(BlockTags.MINEABLE_WITH_PICKAXE)&&state.is(BlockTags.NEEDS_IRON_TOOL),"Native mining tags "+b);check(iron.isCorrectToolForDrops(state)&&!new ItemStack(Items.STONE_PICKAXE).isCorrectToolForDrops(state),"Iron tier enforced "+b);}
drop(Block.getDrops(f.block().defaultBlockState(),l,pos,null,p,iron),f.block().asItem(),1);
for(var ore:List.of(f.ore(),f.deepslateOre())){
drop(Block.getDrops(ore.defaultBlockState(),l,pos,null,p,iron),f.gem(),1);drop(Block.getDrops(ore.defaultBlockState(),l,pos,null,p,silk),ore.asItem(),1);
int max=0;for(int i=0;i<48;i++){var drops=Block.getDrops(ore.defaultBlockState(),l,pos,null,p,fortune);check(drops.size()==1&&drops.getFirst().is(f.gem()),"Fortune gem type");int n=drops.getFirst().getCount();check(n>=1&&n<=4,"Native Fortune III range");max=Math.max(max,n);}check(max>1,"Fortune increases gem yield");
var input=new SingleRecipeInput(new ItemStack(ore));for(var type:List.of(RecipeType.SMELTING,RecipeType.BLASTING)){var recipe=s.getRecipeManager().getRecipeFor(type,input,l).orElseThrow().value();check(recipe.assemble(input).is(f.gem()),"Native furnace recipe");}
}
var input=CraftingInput.of(3,3,Collections.nCopies(9,new ItemStack(f.gem())));drop(List.of(s.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,input,l).orElseThrow().value().assemble(input)),f.block().asItem(),1);
input=CraftingInput.of(1,1,List.of(new ItemStack(f.block())));drop(List.of(s.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,input,l).orElseThrow().value().assemble(input)),f.gem(),9);
check(f.block().defaultBlockState().is(BlockTags.BEACON_BASE_BLOCKS)&&new ItemStack(f.gem()).is(ItemTags.BEACON_PAYMENT_ITEMS),"Native beacon compatibility");
var feature=l.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(ResourceKey.create(Registries.FEATURE,SanctuaryMod.id("ore_"+f.id()))).value();check(feature instanceof GemOreFeature,"Data feature decoded");
check(!feature.place(l,l.getChunkSource().getGenerator(),RandomSource.create(126),pos),"Gem veins do not run in ordinary flat/vanilla worlds");
fr.koka.sanctuary.recipes.RecipeService.discover(p,f.gem());fr.koka.sanctuary.recipes.RecipeService.refresh(p);check(fr.koka.sanctuary.recipes.RecipeService.knowledge(p).recipes.contains("sanctuary:"+f.id()+"_block"),"Native discovery unlocks storage recipe");
}
int x=0;for(var block:List.of(Blocks.EMERALD_BLOCK,Gems.RUBY.block(),Gems.SAPPHIRE.block(),Blocks.EMERALD_ORE,Gems.RUBY.ore(),Gems.SAPPHIRE.ore(),Blocks.DEEPSLATE_EMERALD_ORE,Gems.RUBY.deepslateOre(),Gems.SAPPHIRE.deepslateOre())){l.setBlockAndUpdate(new BlockPos(x%3,1,x/3),block.defaultBlockState());x++;}
p.teleportTo(1.5,3,7);p.getAbilities().flying=true;p.onUpdateAbilities();p.getInventory().setItem(0,new ItemStack(Items.EMERALD));p.getInventory().setItem(1,new ItemStack(Gems.RUBY.gem()));p.getInventory().setItem(2,new ItemStack(Gems.SAPPHIRE.gem()));p.inventoryMenu.broadcastFullState();
System.out.println("GEMS126_SERVER_PASS eight registered items, six blocks, native loot/Fortune/Silk Touch, twelve recipes, mining tiers, beacons, discoveries and vanilla generation exclusion");
});
c.runOnClient(m->{CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());order(CreativeModeTabs.INGREDIENTS,Items.EMERALD,Gems.RUBY.gem(),Gems.SAPPHIRE.gem());order(CreativeModeTabs.BUILDING_BLOCKS,Items.EMERALD_BLOCK,Gems.RUBY.block().asItem(),Gems.SAPPHIRE.block().asItem());order(CreativeModeTabs.NATURAL_BLOCKS,Items.EMERALD_ORE,Gems.RUBY.ore().asItem(),Gems.SAPPHIRE.ore().asItem());order(CreativeModeTabs.NATURAL_BLOCKS,Items.DEEPSLATE_EMERALD_ORE,Gems.RUBY.deepslateOre().asItem(),Gems.SAPPHIRE.deepslateOre().asItem());
for(var f:Gems.FAMILIES)for(var block:List.of(f.block(),f.ore(),f.deepslateOre())){var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(block.defaultBlockState()).collectParts(RandomSource.create(0),parts);check(!parts.isEmpty(),"Baked gem block model");for(var part:parts)for(var face:Direction.values())for(var quad:part.getQuads(face))check(!quad.materialInfo().sprite().contents().name().toString().contains("missingno"),"Native texture atlas resolves supplied gem PNG");}
System.out.println("GEMS126_CREATIVE_PASS emerald, ruby, sapphire adjacency in four groups; all six baked block models");
});
c.waitTicks(10);c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new net.minecraft.world.phys.Vec3(1.5,1.5,1.5)));c.waitTicks(4);c.takeScreenshot("gems126-gallery");
Clay126Checks.exercise(c,world.getServer());
Discovery126Checks.exercise(c,world.getServer());
}
intro.set(true);net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;var found=new HashMap<BlockPos,BlockState>();
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Gems126 natural "+UUID.randomUUID());s.setSeed("126");s.setAllowCommands(true);s.setGenerateStructures(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());}).create()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&ProgressionClient.active()&&!IntroSpawnFlash.active(),2400);
world.getServer().runOnServer(s->{var l=s.overworld();check(l.getChunkSource().getGenerator() instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator,"Actual Sanctuary generator");
for(int cx=-4;cx<=4;cx++)for(int cz=-4;cz<=4;cz++){var chunk=l.getChunk(cx,cz);var cursor=new BlockPos.MutableBlockPos();for(int x=0;x<16;x++)for(int z=0;z<16;z++)for(int y=0;y<=256;y++){cursor.set(cx*16+x,y,cz*16+z);var state=chunk.getBlockState(cursor);if(state.is(Gems.RUBY.ore())||state.is(Gems.RUBY.deepslateOre())||state.is(Gems.SAPPHIRE.ore())||state.is(Gems.SAPPHIRE.deepslateOre()))found.put(cursor.immutable(),state);}}
for(var f:Gems.FAMILIES){long ordinary=found.values().stream().filter(b->b.is(f.ore())).count(),deep=found.values().stream().filter(b->b.is(f.deepslateOre())).count();System.out.println("GEMS126_NATURAL "+f.id()+" stone="+ordinary+" deepslate="+deep+" seed=126 chunks=81");check(ordinary+deep>0,"Both gems generate naturally without structure placement: "+f.id());}
var biome=l.getBiome(found.keySet().iterator().next()).value();var features=biome.getGenerationSettings().features().get(GenerationStep.Decoration.UNDERGROUND_ORES.ordinal());for(var f:Gems.FAMILIES)check(features.stream().anyMatch(h->h.is(ResourceKey.create(Registries.PLACED_FEATURE,SanctuaryMod.id("ore_"+f.id())))),"Native biome includes configurable gem placement");
Discovery126Checks.command(s.getPlayerList().getPlayers().getFirst(),"all");
var mined=found.keySet().iterator().next();l.setBlockAndUpdate(mined,Blocks.AIR.defaultBlockState());found.put(mined,Blocks.AIR.defaultBlockState());
});saved=world.getWorldSave();
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,2400);world.getServer().runOnServer(s->{Discovery126Checks.all(s.getPlayerList().getPlayers().getFirst());for(var e:found.entrySet())check(s.overworld().getBlockState(e.getKey())==e.getValue(),"Generated ores persist on reload without repopulation");});
}
System.out.println("GEMS126_PASS registry, recipes, drops, creative order, native models, natural Sanctuary generation seed 126 and save/reconnect");
}
private static void order(ResourceKey<CreativeModeTab> tab,Item anchor,Item ruby,Item sapphire){var items=new ArrayList<>(BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(tab).value().getDisplayItems());int i=0;while(i<items.size()&&!items.get(i).is(anchor))i++;check(i+2<items.size()&&items.get(i+1).is(ruby)&&items.get(i+2).is(sapphire),"Emerald/ruby/sapphire order in "+tab);check(items.stream().filter(v->v.is(ruby)||v.is(sapphire)).count()==2,"No duplicate creative entries");}
private static void drop(List<ItemStack> drops,Item item,int n){check(drops.size()==1&&drops.getFirst().is(item)&&drops.getFirst().getCount()==n,"Native loot/craft expected "+n+" "+item+", got "+drops);}
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
}
@@ -48,7 +48,7 @@ public final class Sculpture116ClientChecks implements FabricClientGameTest {
var rendered=new HashSet<VoxelSculpture>(); var rendered=new HashSet<VoxelSculpture>();
for(var item:List.of(Items.STONE,Items.OAK_PLANKS,Items.GOLD_BLOCK,Items.WOOL.pick(DyeColor.RED),Items.GLASS,Items.OAK_LEAVES,Items.CLAY,Items.DEEPSLATE)){ for(var item:List.of(Items.STONE,Items.OAK_PLANKS,Items.GOLD_BLOCK,Items.WOOL.pick(DyeColor.RED),Items.GLASS,Items.OAK_LEAVES,Items.CLAY,Items.DEEPSLATE)){
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();var menu=(ClayWorkshopMenu)p.containerMenu;menu.getSlot(0).set(new ItemStack(item,2));check(menu.getSlot(1).getItem().isEmpty(),"Previous material result invalidates immediately");menu.broadcastChanges();}); server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();var menu=(ClayWorkshopMenu)p.containerMenu;menu.getSlot(0).set(new ItemStack(item,2));check(menu.getSlot(1).getItem().isEmpty(),"Previous material result invalidates immediately");menu.broadcastChanges();});
var expected=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(item)).apply(source));rendered.add(expected); var expected=c.computeOnClient(m->ClayWorkshopMenu.appearance(source,item,SculptureMaterialSampler.sample(new ItemStack(item))));rendered.add(expected);
c.waitFor(m->expected.equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())),180); c.waitFor(m->expected.equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())),180);
c.runOnClient(m->check(ItemStack.matches(((ClayWorkshopScreen)m.gui.screen()).preview(),m.player.containerMenu.getSlot(1).getItem()),"Preview equals server-owned result")); c.runOnClient(m->check(ItemStack.matches(((ClayWorkshopScreen)m.gui.screen()).preview(),m.player.containerMenu.getSlot(1).getItem()),"Preview equals server-owned result"));
check(StatuaryEntity.decode(StatuaryEntity.encode(expected)).equals(expected),"Palette colors persist in unchanged schema 1"); check(StatuaryEntity.decode(StatuaryEntity.encode(expected)).equals(expected),"Palette colors persist in unchanged schema 1");
@@ -62,7 +62,7 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Statuary106 "+UUID.randomUUID());s.setSeed("106");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE); try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Statuary106 "+UUID.randomUUID());s.setSeed("106");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){ s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200); saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
var source=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(Items.CLAY)).apply(originalSource)); var source=originalSource;
var server=world.getServer(); var server=world.getServer();
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();p.setGameMode(GameType.SURVIVAL);for(int x=-7;x<=9;x++)for(int z=-5;z<=9;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState()); server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();p.setGameMode(GameType.SURVIVAL);for(int x=-7;x<=9;x++)for(int z=-5;z<=9;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());
var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(Statuary.BLOCK.defaultBlockState());legacy.remove("properties"); var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(Statuary.BLOCK.defaultBlockState());legacy.remove("properties");
@@ -44,6 +44,7 @@ public final class SanctuaryMod implements ModInitializer {
@Override @Override
public void onInitialize() { public void onInitialize() {
fr.koka.sanctuary.building.ColoredBricks.register(); fr.koka.sanctuary.building.ColoredBricks.register();
fr.koka.sanctuary.gems.Gems.register();
fr.koka.sanctuary.sky.VortexClouds.register(); fr.koka.sanctuary.sky.VortexClouds.register();
fr.koka.sanctuary.loading.PreloadProgress.register(); fr.koka.sanctuary.loading.PreloadProgress.register();
fr.koka.sanctuary.progression.SanctuaryGameplay.register(); fr.koka.sanctuary.progression.SanctuaryGameplay.register();
@@ -14,6 +14,7 @@ import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
/** File selection is local; the two ordinary inventory slots and every crafted item are server-owned. */ /** File selection is local; the two ordinary inventory slots and every crafted item are server-owned. */
public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorkshopMenu> { public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorkshopMenu> {
@@ -91,9 +92,9 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
if(previewSource!=source||changedMaterial){ if(previewSource!=source||changedMaterial){
previewSource=source;lastMaterial=material.copy();sampledResources=resources; previewSource=source;lastMaterial=material.copy();sampledResources=resources;
if(changedMaterial)palette=SculptureMaterialSampler.sample(material); if(changedMaterial)palette=SculptureMaterialSampler.sample(material);
var colored=source==null?null:palette==null?source:palette.apply(source); var colored=source==null?null:palette==null?source:ClayWorkshopMenu.appearance(source,material.getItem(),palette);
preview=colored==null?ItemStack.EMPTY:StatuaryEntity.item(colored);pending=true; preview=colored==null?ItemStack.EMPTY:StatuaryEntity.item(colored);pending=true;
if(source!=null&&!busy)status=palette==null?text("choose_material"):text("material_ready",material.getHoverName()); if(source!=null&&!busy)status=palette==null?text("choose_material"):material.is(Items.CLAY)?text("clay_ready"):text("material_ready",material.getHoverName());
} }
// Retry a selection that raced the input packet or server rate limit. The result is the // Retry a selection that raced the input packet or server rate limit. The result is the
// acknowledgement; never predict consumption or manufacture an item on the client. // acknowledgement; never predict consumption or manufacture an item on the client.
@@ -34,13 +34,17 @@ public final class CompanionQueries {
&&familiar.egg().getItem() instanceof net.minecraft.world.item.SpawnEggItem?net.minecraft.world.item.SpawnEggItem.getType(familiar.egg()):entity.getType(); &&familiar.egg().getItem() instanceof net.minecraft.world.item.SpawnEggItem?net.minecraft.world.item.SpawnEggItem.getType(familiar.egg()):entity.getType();
var id=BuiltInRegistries.ENTITY_TYPE.getKey(type).getPath(); var id=BuiltInRegistries.ENTITY_TYPE.getKey(type).getPath();
if(CompanionType.find(id)==null)return; if(CompanionType.find(id)==null)return;
var seen=new TreeSet<>(Arrays.asList(record.notes().getOrDefault("seen_mobs","").split(",")));seen.remove(""); record=discoverSpecies(record,List.of(id));
if(seen.add(id))record=record.note("seen_mobs",String.join(",",seen));
if(entity instanceof net.minecraft.world.entity.animal.fish.TropicalFish fish){ if(entity instanceof net.minecraft.world.entity.animal.fish.TropicalFish fish){
var variants=fishVariants(record);variants.set(fishVariant(fish));record=record.note("seen_fish_v1",Base64.getEncoder().encodeToString(variants.toByteArray())); var variants=fishVariants(record);variants.set(fishVariant(fish));record=record.note("seen_fish_v1",Base64.getEncoder().encodeToString(variants.toByteArray()));
} }
if(!record.equals(CompanionService.record(p)))CompanionService.save(p,record); if(!record.equals(CompanionService.record(p)))CompanionService.save(p,record);
} }
/** The same encounter record is used by natural observation and administrative discoveries. */
public static CompanionRecord discoverSpecies(CompanionRecord record,Collection<String> ids){
var seen=new TreeSet<>(Arrays.asList(record.notes().getOrDefault("seen_mobs","").split(",")));seen.remove("");
return seen.addAll(ids)?record.note("seen_mobs",String.join(",",seen)):record;
}
private static int fishVariant(net.minecraft.world.entity.animal.fish.TropicalFish fish){ private static int fishVariant(net.minecraft.world.entity.animal.fish.TropicalFish fish){
int packed=fish.getPattern().getPackedId();return ((packed>>>8)*2+(packed&1))*256+fish.getBaseColor().getId()*16+fish.getPatternColor().getId(); int packed=fish.getPattern().getPackedId();return ((packed>>>8)*2+(packed&1))*256+fish.getBaseColor().getId()*16+fish.getPatternColor().getId();
} }
@@ -0,0 +1,19 @@
package fr.koka.sanctuary.gems;
import com.mojang.serialization.MapCodec;
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.*;
/** Native seeded ore veins, scoped to Sanctuary even when its biomes are shared with vanilla. */
public record GemOreFeature(OreFeature ore) implements Feature {
public static final MapCodec<GemOreFeature> CODEC=OreFeature.CODEC.xmap(GemOreFeature::new,GemOreFeature::ore);
@Override public MapCodec<GemOreFeature> codec(){return CODEC;}
@Override public boolean place(WorldGenLevel level,ChunkGenerator generator,RandomSource random,BlockPos origin){
if(!(level.getLevel().getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator))return false;
return ore.place(level,generator,random,origin);
}
}
@@ -0,0 +1,50 @@
package fr.koka.sanctuary.gems;
import fr.koka.sanctuary.SanctuaryMod;
import java.util.List;
import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.*;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.TagKey;
import net.minecraft.util.valueproviders.UniformInt;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.levelgen.GenerationStep;
/** Ordinary gem items and blocks; their distribution lives in data, not this registry. */
public final class Gems {
public record Family(String id,Item gem,Block block,Block ore,Block deepslateOre){}
public static final Family RUBY=family("ruby",DyeColor.RED);
public static final Family SAPPHIRE=family("sapphire",DyeColor.BLUE);
public static final List<Family> FAMILIES=List.of(RUBY,SAPPHIRE);
public static void register(){
Registry.register(BuiltInRegistries.FEATURE_TYPE,SanctuaryMod.id("gem_ore"),GemOreFeature.CODEC);
for(var f:FAMILIES){
var biomes=TagKey.create(Registries.BIOME,SanctuaryMod.id("has_ore/"+f.id));
BiomeModifications.addFeature(c->c.hasTag(biomes),GenerationStep.Decoration.UNDERGROUND_ORES,
ResourceKey.create(Registries.PLACED_FEATURE,SanctuaryMod.id("ore_"+f.id)));
}
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.INGREDIENTS).register(out->out.insertAfter(Items.EMERALD,RUBY.gem,SAPPHIRE.gem));
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.BUILDING_BLOCKS).register(out->out.insertAfter(Blocks.EMERALD_BLOCK,RUBY.block,SAPPHIRE.block));
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.NATURAL_BLOCKS).register(out->{
out.insertAfter(Blocks.EMERALD_ORE,RUBY.ore,SAPPHIRE.ore);
out.insertAfter(Blocks.DEEPSLATE_EMERALD_ORE,RUBY.deepslateOre,SAPPHIRE.deepslateOre);
});
}
private static Family family(String id,DyeColor color){
var key=SanctuaryMod.id(id);var item=Registry.register(BuiltInRegistries.ITEM,key,new Item(new Item.Properties().setId(ResourceKey.create(Registries.ITEM,key))));
var block=block(id+"_block",new Block(properties(id+"_block",Blocks.EMERALD_BLOCK).mapColor(color.getMapColor())));
var ore=block(id+"_ore",new DropExperienceBlock(UniformInt.of(3,7),properties(id+"_ore",Blocks.EMERALD_ORE)));
var deep=block("deepslate_"+id+"_ore",new DropExperienceBlock(UniformInt.of(3,7),properties("deepslate_"+id+"_ore",Blocks.DEEPSLATE_EMERALD_ORE)));
return new Family(id,item,block,ore,deep);
}
private static BlockBehaviour.Properties properties(String id,Block source){return BlockBehaviour.Properties.ofFullCopy(source).setId(ResourceKey.create(Registries.BLOCK,SanctuaryMod.id(id)));}
private static Block block(String id,Block block){
var key=SanctuaryMod.id(id);Registry.register(BuiltInRegistries.BLOCK,key,block);
Registry.register(BuiltInRegistries.ITEM,key,new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,key)).useBlockDescriptionPrefix()));return block;
}
private Gems(){}
}
@@ -47,6 +47,7 @@ public final class BlockKnowledgeService {
private BlockKnowledgeService() {} private BlockKnowledgeService() {}
public static void register() { public static void register() {
DiscoveryCommands.register();
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> observeInventory(handler.player)); ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> observeInventory(handler.player));
// Fabric also reaches DISCONNECT through Netty's channelInactive callback. // Fabric also reaches DISCONNECT through Netty's channelInactive callback.
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> server.execute(() -> { ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> server.execute(() -> {
@@ -97,6 +98,12 @@ public final class BlockKnowledgeService {
public static void recordSeen(ServerPlayer player, Block block) { public static void recordSeen(ServerPlayer player, Block block) {
if (block.defaultBlockState().isAir() || player.isSpectator()) return; if (block.defaultBlockState().isAir() || player.isSpectator()) return;
discover(player,block);
}
/** Explicit discoveries use the same record and events, including for a spectator command target. */
public static void discover(ServerPlayer player,Block block) {
if(block.defaultBlockState().isAir())return;
Session session = session(player); Session session = session(player);
if (session.failed || !session.memory.recordSeen(id(block))) return; if (session.failed || !session.memory.recordSeen(id(block))) return;
session.dirty = true; session.dirty = true;
@@ -0,0 +1,75 @@
package fr.koka.sanctuary.knowledge;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import fr.koka.sanctuary.companion.*;
import fr.koka.sanctuary.operator.OperatorService;
import fr.koka.sanctuary.progression.ProgressEffects;
import fr.koka.sanctuary.recipes.*;
import java.util.*;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.commands.*;
import net.minecraft.commands.arguments.*;
import net.minecraft.core.registries.*;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Block;
/** One command routes through the existing discovery services and their dependent recipe/catalogue rules. */
public final class DiscoveryCommands {
private record Selection(List<Block> blocks,List<Item> items,List<EntityType<?>> mobs){}
@FunctionalInterface private interface SelectionArgument {Selection get(CommandContext<CommandSourceStack> context)throws CommandSyntaxException;}
public static void register(){
CommandRegistrationCallback.EVENT.register((dispatcher,registries,environment)->{
var root=Commands.literal("discovery").requires(OperatorService::canCommand);
root.then(targets(Commands.literal("all"),c->all(true,true,true)));
root.then(Commands.literal("blocks").then(targets(Commands.literal("all"),c->all(true,false,false))));
root.then(Commands.literal("items").then(targets(Commands.literal("all"),c->all(false,true,false))));
root.then(Commands.literal("mobs").then(targets(Commands.literal("all"),c->all(false,false,true))));
root.then(Commands.literal("block").then(targets(Commands.argument("block",ResourceArgument.resource(registries,Registries.BLOCK)),c->new Selection(List.of(ResourceArgument.getResource(c,"block",Registries.BLOCK).value()),List.of(),List.of()))));
root.then(Commands.literal("item").then(targets(Commands.argument("item",ResourceArgument.resource(registries,Registries.ITEM)),c->new Selection(List.of(),List.of(ResourceArgument.getResource(c,"item",Registries.ITEM).value()),List.of()))));
root.then(Commands.literal("mob").then(targets(Commands.argument("mob",ResourceArgument.resource(registries,Registries.ENTITY_TYPE)),c->new Selection(List.of(),List.of(),List.of(ResourceArgument.getResource(c,"mob",Registries.ENTITY_TYPE).value())))));
dispatcher.register(Commands.literal("sanctuary").then(root));
});
}
private static <T extends ArgumentBuilder<CommandSourceStack,T>> T targets(T node,SelectionArgument selection){
return node.executes(c->discover(c,selection.get(c),List.of(c.getSource().getPlayerOrException())))
.then(Commands.argument("players",EntityArgument.players()).executes(c->discover(c,selection.get(c),EntityArgument.getPlayers(c,"players"))));
}
private static Selection all(boolean blocks,boolean items,boolean mobs){
return new Selection(blocks?BuiltInRegistries.BLOCK.stream().filter(b->!b.defaultBlockState().isAir()).toList():List.of(),
items?BuiltInRegistries.ITEM.stream().filter(i->i!=Items.AIR).toList():List.of(),
mobs?BuiltInRegistries.ENTITY_TYPE.stream().filter(t->SpawnEggItem.byId(t).isPresent()).toList():List.of());
}
private static int discover(CommandContext<CommandSourceStack> context,Selection selection,Collection<ServerPlayer> players){
var source=context.getSource();
if(selection.blocks.stream().anyMatch(b->b.defaultBlockState().isAir())||selection.items.contains(Items.AIR)||selection.mobs.stream().anyMatch(t->SpawnEggItem.byId(t).isEmpty())){
source.sendFailure(Component.translatable("sanctuary.discovery.invalid"));return 0;
}
var species=selection.mobs.stream().map(BuiltInRegistries.ENTITY_TYPE::getKey).map(id->id.getNamespace().equals("minecraft")?id.getPath():id.toString()).toList();
int changed=0;
for(var player:players){
var record=CompanionService.record(player);
if(ProgressEffects.get(player)==null||record==null||!BlockKnowledgeService.healthy(player)||!RecipeService.healthy(player)){
source.sendFailure(Component.translatable("sanctuary.discovery.unavailable",player.getDisplayName()));continue;
}
// Validate bounded records before any write; no new save schema or unlock flag.
CompanionRecord next;
var discoveredItems=new HashSet<String>(RecipeService.knowledge(player).items);
for(var item:selection.items)discoveredItems.add(BuiltInRegistries.ITEM.getKey(item).toString());
for(var block:selection.blocks)if(block.asItem()!=Items.AIR)discoveredItems.add(BuiltInRegistries.ITEM.getKey(block.asItem()).toString());
try{next=CompanionQueries.discoverSpecies(record,species);if(discoveredItems.size()>RecipeKnowledge.MAX_IDS)throw new IllegalArgumentException("Discovery capacity");}
catch(IllegalArgumentException invalid){source.sendFailure(Component.translatable("sanctuary.discovery.unavailable",player.getDisplayName()));continue;}
CompanionService.save(player,next);
for(var block:selection.blocks)BlockKnowledgeService.discover(player,block);
for(var item:selection.items){if(item instanceof BlockItem b)BlockKnowledgeService.discover(player,b.getBlock());RecipeService.discover(player,item);}
RecipeService.refresh(player);
source.sendSuccess(()->Component.translatable("sanctuary.discovery.granted",player.getDisplayName(),selection.blocks.size(),selection.items.size(),selection.mobs.size()),true);changed++;
}
return changed;
}
private DiscoveryCommands(){}
}
@@ -71,14 +71,18 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
var contents=item.get(DataComponents.CONTAINER); var contents=item.get(DataComponents.CONTAINER);
return contents==null||!contents.nonEmptyItems().iterator().hasNext(); return contents==null||!contents.nonEmptyItems().iterator().hasNext();
} }
/** Plain clay retains the model's sampled colors; other blocks transfer their texture palette. */
public static VoxelSculpture appearance(VoxelSculpture source,Item material,SculpturePalette palette){
return material==Items.CLAY?source:palette.apply(source);
}
public void select(ServerPlayer p,VoxelSculpture sculpture,Item material,SculpturePalette palette){ public void select(ServerPlayer p,VoxelSculpture sculpture,Item material,SculpturePalette palette){
if(level==null||!stillValid(p)||level.getGameTime()-lastSelection<4)return; if(level==null||!stillValid(p)||level.getGameTime()-lastSelection<4)return;
var hit=new BlockHitResult(Vec3.atCenterOf(view.pos),Direction.UP,view.pos,false); var hit=new BlockHitResult(Vec3.atCenterOf(view.pos),Direction.UP,view.pos,false);
if(UseBlockCallback.EVENT.invoker().interact(p,level,InteractionHand.MAIN_HAND,hit)!=InteractionResult.PASS||!stillValid(p))return; if(UseBlockCallback.EVENT.invoker().interact(p,level,InteractionHand.MAIN_HAND,hit)!=InteractionResult.PASS||!stillValid(p))return;
lastSelection=level.getGameTime(); lastSelection=level.getGameTime();
if(sculpture!=null&&(!StatueDiscoveries.allowedModel(sculpture.modelId(),view.statues())||!material(input.getItem(0))||!input.getItem(0).is(material)||palette==null))return; if(sculpture!=null&&(!StatueDiscoveries.allowedModel(sculpture.modelId(),view.statues())||!material(input.getItem(0))||!input.getItem(0).is(material)||palette==null))return;
if(!Objects.equals(selected,sculpture)||!Objects.equals(selectedPalette,palette)) if(!Objects.equals(selected,sculpture)||selectedMaterial!=material||!Objects.equals(selectedPalette,palette))
selectedResult=sculpture==null||palette==null?null:palette.apply(sculpture); selectedResult=sculpture==null||palette==null?null:appearance(sculpture,material,palette);
selected=sculpture;selectedMaterial=material;selectedPalette=palette;refresh();broadcastChanges(); selected=sculpture;selectedMaterial=material;selectedPalette=palette;refresh();broadcastChanges();
} }
private void refresh(){ private void refresh(){
@@ -35,6 +35,7 @@ public final class RecipeService {
} }
private static Session session(ServerPlayer p){return SESSIONS.computeIfAbsent(p,Session::new);} private static Session session(ServerPlayer p){return SESSIONS.computeIfAbsent(p,Session::new);}
public static RecipeKnowledge knowledge(ServerPlayer p){return session(p).data;} public static RecipeKnowledge knowledge(ServerPlayer p){return session(p).data;}
public static boolean healthy(ServerPlayer p){return !session(p).invalid;}
public static void discover(ServerPlayer p,Item item){ public static void discover(ServerPlayer p,Item item){
if(ProgressEffects.get(p)==null||item==Items.AIR)return;var s=session(p);if(s.invalid)return; if(ProgressEffects.get(p)==null||item==Items.AIR)return;var s=session(p);if(s.invalid)return;
s.dirty|=RecipeKnowledge.add(s.data.items,BuiltInRegistries.ITEM.getKey(item).toString()); s.dirty|=RecipeKnowledge.add(s.data.items,BuiltInRegistries.ITEM.getKey(item).toString());
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "sanctuary:block/deepslate_ruby_ore"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "sanctuary:block/deepslate_sapphire_ore"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "sanctuary:block/ruby_block"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "sanctuary:block/ruby_ore"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "sanctuary:block/sapphire_block"
}
}
}
@@ -0,0 +1,7 @@
{
"variants": {
"": {
"model": "sanctuary:block/sapphire_ore"
}
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:block/deepslate_ruby_ore"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:block/deepslate_sapphire_ore"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:item/ruby"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:block/ruby_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:block/ruby_ore"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:item/sapphire"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:block/sapphire_block"
}
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:block/sapphire_ore"
}
}
@@ -1,4 +1,5 @@
{ {
"sanctuary.statuary.clay_ready": "Clay: original model colors.",
"sanctuary.weather.selected": "Real Time weather: %s for %s (%s). The daily forecast resumes at midnight.", "sanctuary.weather.selected": "Real Time weather: %s for %s (%s). The daily forecast resumes at midnight.",
"sanctuary.weather.overcast": "Overcast", "sanctuary.weather.overcast": "Overcast",
"sanctuary.weather.fog": "Fog", "sanctuary.weather.fog": "Fog",
@@ -1972,7 +1973,7 @@
"sanctuary.discovery.item_filter": "Show carried items or every item already discovered.", "sanctuary.discovery.item_filter": "Show carried items or every item already discovered.",
"sanctuary.discovery.catalogue_required": "The Catalogue ability lets you browse discovered items you no longer carry.", "sanctuary.discovery.catalogue_required": "The Catalogue ability lets you browse discovered items you no longer carry.",
"sanctuary.discovery.loading": "Loading statistics…", "sanctuary.discovery.loading": "Loading statistics…",
"sanctuary.discovery.unavailable": "Statistics unavailable. Refresh to try again.", "sanctuary.discovery.unavailable": "Unable to update discoveries for %s; no changes made.",
"sanctuary.discovery.mobs_hint": "Mobs you have killed or that have killed you. Sightings alone are not recorded yet.", "sanctuary.discovery.mobs_hint": "Mobs you have killed or that have killed you. Sightings alone are not recorded yet.",
"sanctuary.discovery.count": "%s entries", "sanctuary.discovery.count": "%s entries",
"sanctuary.discovery.empty": "No entries match this filter.", "sanctuary.discovery.empty": "No entries match this filter.",
@@ -2245,5 +2246,15 @@
"sanctuary.statuary.animals": "Animals", "sanctuary.statuary.animals": "Animals",
"sanctuary.statuary.imports": "Imports", "sanctuary.statuary.imports": "Imports",
"sanctuary.statuary.no_animals": "Encounter creatures to unlock their sculptures.", "sanctuary.statuary.no_animals": "Encounter creatures to unlock their sculptures.",
"sanctuary.statuary.animal_error": "This creatures model could not be sculpted." "sanctuary.statuary.animal_error": "This creatures model could not be sculpted.",
"item.sanctuary.ruby": "Ruby",
"block.sanctuary.ruby_block": "Block of Ruby",
"block.sanctuary.ruby_ore": "Ruby Ore",
"block.sanctuary.deepslate_ruby_ore": "Deepslate Ruby Ore",
"item.sanctuary.sapphire": "Sapphire",
"block.sanctuary.sapphire_block": "Block of Sapphire",
"block.sanctuary.sapphire_ore": "Sapphire Ore",
"block.sanctuary.deepslate_sapphire_ore": "Deepslate Sapphire Ore",
"sanctuary.discovery.granted": "Discoveries added for %s: %s blocks, %s items, %s species. Linked recipes updated; reopen catalogues.",
"sanctuary.discovery.invalid": "Choose a discoverable block, item or species."
} }
@@ -1,4 +1,5 @@
{ {
"sanctuary.statuary.clay_ready": "Argile : couleurs originales du modèle.",
"sanctuary.weather.selected": "Météo Real Time : %s pour la journée du %s (%s). Le tirage quotidien reprend à minuit.", "sanctuary.weather.selected": "Météo Real Time : %s pour la journée du %s (%s). Le tirage quotidien reprend à minuit.",
"sanctuary.weather.overcast": "Ciel gris", "sanctuary.weather.overcast": "Ciel gris",
"sanctuary.weather.fog": "Brouillard", "sanctuary.weather.fog": "Brouillard",
@@ -1972,7 +1973,7 @@
"sanctuary.discovery.item_filter": "Afficher les objets portés ou tous les objets déjà découverts.", "sanctuary.discovery.item_filter": "Afficher les objets portés ou tous les objets déjà découverts.",
"sanctuary.discovery.catalogue_required": "Laptitude Catalogue permet de consulter les objets découverts que tu ne portes plus.", "sanctuary.discovery.catalogue_required": "Laptitude Catalogue permet de consulter les objets découverts que tu ne portes plus.",
"sanctuary.discovery.loading": "Chargement des statistiques…", "sanctuary.discovery.loading": "Chargement des statistiques…",
"sanctuary.discovery.unavailable": "Statistiques indisponibles. Actualise pour réessayer.", "sanctuary.discovery.unavailable": "Impossible de mettre à jour les découvertes de %s ; aucune modification effectuée.",
"sanctuary.discovery.mobs_hint": "Créatures déjà tuées ou responsables de ta mort. Les simples observations ne sont pas encore enregistrées.", "sanctuary.discovery.mobs_hint": "Créatures déjà tuées ou responsables de ta mort. Les simples observations ne sont pas encore enregistrées.",
"sanctuary.discovery.count": "%s entrées", "sanctuary.discovery.count": "%s entrées",
"sanctuary.discovery.empty": "Aucune entrée pour ce filtre.", "sanctuary.discovery.empty": "Aucune entrée pour ce filtre.",
@@ -2245,5 +2246,15 @@
"sanctuary.statuary.animals": "Animaux", "sanctuary.statuary.animals": "Animaux",
"sanctuary.statuary.imports": "Imports", "sanctuary.statuary.imports": "Imports",
"sanctuary.statuary.no_animals": "Rencontrez des créatures pour débloquer leurs sculptures.", "sanctuary.statuary.no_animals": "Rencontrez des créatures pour débloquer leurs sculptures.",
"sanctuary.statuary.animal_error": "Impossible de sculpter le modèle de cette créature." "sanctuary.statuary.animal_error": "Impossible de sculpter le modèle de cette créature.",
"item.sanctuary.ruby": "Rubis",
"block.sanctuary.ruby_block": "Bloc de rubis",
"block.sanctuary.ruby_ore": "Minerai de rubis",
"block.sanctuary.deepslate_ruby_ore": "Minerai de rubis des abîmes",
"item.sanctuary.sapphire": "Saphir",
"block.sanctuary.sapphire_block": "Bloc de saphir",
"block.sanctuary.sapphire_ore": "Minerai de saphir",
"block.sanctuary.deepslate_sapphire_ore": "Minerai de saphir des abîmes",
"sanctuary.discovery.granted": "Découvertes ajoutées pour %s : %s blocs, %s objets, %s espèces. Recettes liées actualisées ; rouvre les catalogues.",
"sanctuary.discovery.invalid": "Choisis un bloc, un objet ou une espèce découvrable."
} }
@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "sanctuary:block/deepslate_ruby_ore"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "sanctuary:block/deepslate_sapphire_ore"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "sanctuary:block/ruby_block"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "sanctuary:block/ruby_ore"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "sanctuary:block/sapphire_block"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "sanctuary:block/sapphire_ore"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "sanctuary:item/ruby"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "sanctuary:item/sapphire"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"#c:ores/ruby",
"#c:ores/sapphire"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"#sanctuary:ruby_ores"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"#sanctuary:sapphire_ores"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"sanctuary:deepslate_ruby_ore",
"sanctuary:deepslate_sapphire_ore"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"sanctuary:ruby_ore",
"sanctuary:sapphire_ore"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"#c:storage_blocks/ruby",
"#c:storage_blocks/sapphire"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"sanctuary:ruby_block"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"sanctuary:sapphire_block"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"#c:gems/ruby",
"#c:gems/sapphire"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"sanctuary:ruby"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"sanctuary:sapphire"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"#c:ores/ruby",
"#c:ores/sapphire"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"#sanctuary:ruby_ores"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"#sanctuary:sapphire_ores"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"sanctuary:deepslate_ruby_ore",
"sanctuary:deepslate_sapphire_ore"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"sanctuary:ruby_ore",
"sanctuary:sapphire_ore"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"#c:storage_blocks/ruby",
"#c:storage_blocks/sapphire"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"sanctuary:ruby_block"
]
}
@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"sanctuary:sapphire_block"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"sanctuary:ruby_block",
"sanctuary:sapphire_block"
]
}
@@ -64,6 +64,12 @@
"sanctuary:brown_brick_wall", "sanctuary:brown_brick_wall",
"sanctuary:green_brick_wall", "sanctuary:green_brick_wall",
"sanctuary:red_brick_wall", "sanctuary:red_brick_wall",
"sanctuary:black_brick_wall" "sanctuary:black_brick_wall",
"sanctuary:ruby_block",
"sanctuary:ruby_ore",
"sanctuary:deepslate_ruby_ore",
"sanctuary:sapphire_block",
"sanctuary:sapphire_ore",
"sanctuary:deepslate_sapphire_ore"
] ]
} }
@@ -0,0 +1,11 @@
{
"replace": false,
"values": [
"sanctuary:ruby_block",
"sanctuary:ruby_ore",
"sanctuary:deepslate_ruby_ore",
"sanctuary:sapphire_block",
"sanctuary:sapphire_ore",
"sanctuary:deepslate_sapphire_ore"
]
}
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"sanctuary:ruby",
"sanctuary:sapphire"
]
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_emerald": {
"conditions": {
"items": [
{
"items": "sanctuary:ruby"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:ruby_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_emerald"
]
],
"rewards": {
"recipes": [
"sanctuary:ruby_block"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_emerald": {
"conditions": {
"items": [
{
"items": "sanctuary:sapphire"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:sapphire_block"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_emerald"
]
],
"rewards": {
"recipes": [
"sanctuary:sapphire_block"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_emerald_block": {
"conditions": {
"items": [
{
"items": "sanctuary:ruby_block"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:ruby"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_emerald_block"
]
],
"rewards": {
"recipes": [
"sanctuary:ruby"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_deepslate_ruby_ore": {
"conditions": {
"items": [
{
"items": "sanctuary:deepslate_ruby_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:ruby_from_blasting_deepslate_ruby_ore"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_deepslate_ruby_ore"
]
],
"rewards": {
"recipes": [
"sanctuary:ruby_from_blasting_deepslate_ruby_ore"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_ruby_ore": {
"conditions": {
"items": [
{
"items": "sanctuary:ruby_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:ruby_from_blasting_ruby_ore"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_ruby_ore"
]
],
"rewards": {
"recipes": [
"sanctuary:ruby_from_blasting_ruby_ore"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_deepslate_ruby_ore": {
"conditions": {
"items": [
{
"items": "sanctuary:deepslate_ruby_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:ruby_from_smelting_deepslate_ruby_ore"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_deepslate_ruby_ore"
]
],
"rewards": {
"recipes": [
"sanctuary:ruby_from_smelting_deepslate_ruby_ore"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_ruby_ore": {
"conditions": {
"items": [
{
"items": "sanctuary:ruby_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:ruby_from_smelting_ruby_ore"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_ruby_ore"
]
],
"rewards": {
"recipes": [
"sanctuary:ruby_from_smelting_ruby_ore"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_emerald_block": {
"conditions": {
"items": [
{
"items": "sanctuary:sapphire_block"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:sapphire"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_emerald_block"
]
],
"rewards": {
"recipes": [
"sanctuary:sapphire"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_deepslate_sapphire_ore": {
"conditions": {
"items": [
{
"items": "sanctuary:deepslate_sapphire_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:sapphire_from_blasting_deepslate_sapphire_ore"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_deepslate_sapphire_ore"
]
],
"rewards": {
"recipes": [
"sanctuary:sapphire_from_blasting_deepslate_sapphire_ore"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_sapphire_ore": {
"conditions": {
"items": [
{
"items": "sanctuary:sapphire_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:sapphire_from_blasting_sapphire_ore"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_sapphire_ore"
]
],
"rewards": {
"recipes": [
"sanctuary:sapphire_from_blasting_sapphire_ore"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_deepslate_sapphire_ore": {
"conditions": {
"items": [
{
"items": "sanctuary:deepslate_sapphire_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:sapphire_from_smelting_deepslate_sapphire_ore"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_deepslate_sapphire_ore"
]
],
"rewards": {
"recipes": [
"sanctuary:sapphire_from_smelting_deepslate_sapphire_ore"
]
}
}
@@ -0,0 +1,32 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_sapphire_ore": {
"conditions": {
"items": [
{
"items": "sanctuary:sapphire_ore"
}
]
},
"trigger": "minecraft:inventory_changed"
},
"has_the_recipe": {
"conditions": {
"recipes": "sanctuary:sapphire_from_smelting_sapphire_ore"
},
"trigger": "minecraft:recipe_unlocked"
}
},
"requirements": [
[
"has_the_recipe",
"has_sapphire_ore"
]
],
"rewards": {
"recipes": [
"sanctuary:sapphire_from_smelting_sapphire_ore"
]
}
}
@@ -0,0 +1,35 @@
{
"type": "minecraft:block",
"pools": [
{
"entries": [
{
"type": "minecraft:alternatives",
"children": [
{
"type": "minecraft:item",
"condition": "minecraft:tool/can_silk_touch",
"name": "sanctuary:deepslate_ruby_ore"
},
{
"type": "minecraft:item",
"modifier": [
{
"type": "minecraft:apply_bonus",
"enchantment": "minecraft:fortune",
"formula": "minecraft:ore_drops"
},
{
"type": "minecraft:explosion_decay"
}
],
"name": "sanctuary:ruby"
}
]
}
],
"rolls": 1
}
],
"random_sequence": "sanctuary:blocks/deepslate_ruby_ore"
}
@@ -0,0 +1,35 @@
{
"type": "minecraft:block",
"pools": [
{
"entries": [
{
"type": "minecraft:alternatives",
"children": [
{
"type": "minecraft:item",
"condition": "minecraft:tool/can_silk_touch",
"name": "sanctuary:deepslate_sapphire_ore"
},
{
"type": "minecraft:item",
"modifier": [
{
"type": "minecraft:apply_bonus",
"enchantment": "minecraft:fortune",
"formula": "minecraft:ore_drops"
},
{
"type": "minecraft:explosion_decay"
}
],
"name": "sanctuary:sapphire"
}
]
}
],
"rolls": 1
}
],
"random_sequence": "sanctuary:blocks/deepslate_sapphire_ore"
}
@@ -0,0 +1,18 @@
{
"type": "minecraft:block",
"pools": [
{
"condition": {
"type": "minecraft:survives_explosion"
},
"entries": [
{
"type": "minecraft:item",
"name": "sanctuary:ruby_block"
}
],
"rolls": 1
}
],
"random_sequence": "sanctuary:blocks/ruby_block"
}
@@ -0,0 +1,35 @@
{
"type": "minecraft:block",
"pools": [
{
"entries": [
{
"type": "minecraft:alternatives",
"children": [
{
"type": "minecraft:item",
"condition": "minecraft:tool/can_silk_touch",
"name": "sanctuary:ruby_ore"
},
{
"type": "minecraft:item",
"modifier": [
{
"type": "minecraft:apply_bonus",
"enchantment": "minecraft:fortune",
"formula": "minecraft:ore_drops"
},
{
"type": "minecraft:explosion_decay"
}
],
"name": "sanctuary:ruby"
}
]
}
],
"rolls": 1
}
],
"random_sequence": "sanctuary:blocks/ruby_ore"
}
@@ -0,0 +1,18 @@
{
"type": "minecraft:block",
"pools": [
{
"condition": {
"type": "minecraft:survives_explosion"
},
"entries": [
{
"type": "minecraft:item",
"name": "sanctuary:sapphire_block"
}
],
"rolls": 1
}
],
"random_sequence": "sanctuary:blocks/sapphire_block"
}
@@ -0,0 +1,35 @@
{
"type": "minecraft:block",
"pools": [
{
"entries": [
{
"type": "minecraft:alternatives",
"children": [
{
"type": "minecraft:item",
"condition": "minecraft:tool/can_silk_touch",
"name": "sanctuary:sapphire_ore"
},
{
"type": "minecraft:item",
"modifier": [
{
"type": "minecraft:apply_bonus",
"enchantment": "minecraft:fortune",
"formula": "minecraft:ore_drops"
},
{
"type": "minecraft:explosion_decay"
}
],
"name": "sanctuary:sapphire"
}
]
}
],
"rolls": 1
}
],
"random_sequence": "sanctuary:blocks/sapphire_ore"
}
@@ -0,0 +1,10 @@
{
"type": "minecraft:crafting_shapeless",
"ingredients": [
"sanctuary:ruby_block"
],
"result": {
"count": 9,
"id": "sanctuary:ruby"
}
}
@@ -0,0 +1,15 @@
{
"type": "minecraft:crafting_shaped",
"category": "building",
"key": {
"#": "sanctuary:ruby"
},
"pattern": [
"###",
"###",
"###"
],
"result": {
"id": "sanctuary:ruby_block"
}
}
@@ -0,0 +1,10 @@
{
"type": "minecraft:blasting",
"cookingtime": 200,
"experience": 1.0,
"group": "ruby",
"ingredient": "sanctuary:deepslate_ruby_ore",
"result": {
"id": "sanctuary:ruby"
}
}
@@ -0,0 +1,10 @@
{
"type": "minecraft:blasting",
"cookingtime": 200,
"experience": 1.0,
"group": "ruby",
"ingredient": "sanctuary:ruby_ore",
"result": {
"id": "sanctuary:ruby"
}
}
@@ -0,0 +1,10 @@
{
"type": "minecraft:smelting",
"cookingtime": 200,
"experience": 1.0,
"group": "ruby",
"ingredient": "sanctuary:deepslate_ruby_ore",
"result": {
"id": "sanctuary:ruby"
}
}
@@ -0,0 +1,10 @@
{
"type": "minecraft:smelting",
"cookingtime": 200,
"experience": 1.0,
"group": "ruby",
"ingredient": "sanctuary:ruby_ore",
"result": {
"id": "sanctuary:ruby"
}
}
@@ -0,0 +1,10 @@
{
"type": "minecraft:crafting_shapeless",
"ingredients": [
"sanctuary:sapphire_block"
],
"result": {
"count": 9,
"id": "sanctuary:sapphire"
}
}
@@ -0,0 +1,15 @@
{
"type": "minecraft:crafting_shaped",
"category": "building",
"key": {
"#": "sanctuary:sapphire"
},
"pattern": [
"###",
"###",
"###"
],
"result": {
"id": "sanctuary:sapphire_block"
}
}
@@ -0,0 +1,10 @@
{
"type": "minecraft:blasting",
"cookingtime": 200,
"experience": 1.0,
"group": "sapphire",
"ingredient": "sanctuary:deepslate_sapphire_ore",
"result": {
"id": "sanctuary:sapphire"
}
}

Some files were not shown because too many files have changed in this diff Show More