Apply block texture palettes to workshop sculptures (beta.116)
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -20,11 +20,12 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
La version **beta.114** ajoute une fuite chaotique aux animaux volants portés :
|
||||
un coup les fait décoller avec leur porteur, dans une direction aléatoire,
|
||||
pendant deux à trois secondes. Le [ticket](docs/carried-flight-panic-beta114.md)
|
||||
décrit le comportement et ses vérifications. La mise à jour est installée
|
||||
dans **Sanctuary Beta**.
|
||||
La version **beta.116** ouvre le Clay Workshop aux blocs de matière : les
|
||||
nuances de leur texture sont appliquées au modèle, avec aperçu et fabrication
|
||||
payée. Elle réunit aussi le correctif du Fourneau (cases décalées par trois,
|
||||
combustible stable et explication de la chauffe).
|
||||
Le [ticket de livraison](docs/sculpture-materials-beta116.md) conserve les
|
||||
contrôles et le suivi de distribution.
|
||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
@@ -35,11 +36,20 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.116 — Matières de sculpture et Fourneau
|
||||
|
||||
Un bloc de matière devient une sculpture aux couleurs de sa texture. Hotbar,
|
||||
orientation et ancrage conservés. Le Fourneau accepte les dépôts dans chacune
|
||||
de ses 27 cases et explique sa chauffe commune.
|
||||
[Contrat et vérifications](docs/sculpture-materials-beta116.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.116/Sanctuary-beta.116.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.116/Sanctuary-Test-beta.116.mrpack).
|
||||
|
||||
## beta.115 — Fourneau : dépôts et chauffe
|
||||
|
||||
Correctif du décalage par trois cases, combustible stable et progression
|
||||
par cuisson. [Diagnostic et essais](docs/fourneau-transfers-beta115.md).
|
||||
Préparation locale pour la prochaine livraison commune avec l’atelier.
|
||||
Étape locale intégrée à la livraison commune beta.116.
|
||||
|
||||
## beta.114 — Animaux portés et fuite en vol
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ relues dans `build/furnace115-evidence/`. Marqueur `FURNACE115_PASS` dans
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en 2 min 29 s, 125 tâches. Le panneau d'aide reçoit ensuite une dernière
|
||||
vérification client et une reconstruction ; la livraison commune beta.116
|
||||
relancera les contrôles complets sur l'ensemble des changements.
|
||||
repasse les contrôles complets en 2 min 23 s et le scénario Fourneau en 42 s.
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
Pas d'essai Windows ou de réseau distant avec latence simulée. Aucun monde
|
||||
personnel ouvert. Cette étape est préparée localement pour la livraison commune
|
||||
avec les matériaux de sculpture ; le canal publié reste beta.114.
|
||||
personnel ouvert. Cette étape reste locale et rejoint la livraison commune
|
||||
avec les matériaux de sculpture : [beta.116](sculpture-materials-beta116.md).
|
||||
|
||||
Archives locales vérifiées (même JAR Sanctuary, sources concordantes, aucune
|
||||
classe de test ni changement de production extérieur au ticket) :
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# beta.116 — Sculptures à partir des textures de blocs
|
||||
|
||||
Ticket sur `codex/sculpture-materials-beta116`, comprenant aussi le correctif
|
||||
Fourneau beta.115 dans la livraison commune.
|
||||
|
||||
Le Clay Workshop accepte les objets-blocs comme matière. Le client prélève une
|
||||
palette bornée dans la texture du bloc issue du pack actif, avec sa teinte, puis
|
||||
le modèle conserve sa géométrie et transpose ses valeurs claires/sombres dans
|
||||
cette palette. L'aperçu et le résultat utilisent le même calcul. Un bloc est
|
||||
consommé par sculpture ; les neuf cases de hotbar restent les seules accessibles.
|
||||
|
||||
## Contrat des données et du réseau
|
||||
|
||||
Les sculptures existantes conservent exactement leurs voxels sauvegardés :
|
||||
schéma 1 inchangé, identifiants `sanctuary:*` stables. Les nouvelles sculptures
|
||||
stockent elles aussi leurs couleurs finales dans ce format. Aucun chargement
|
||||
ne recolore les objets ou constructions existants. Orientation et ancrage
|
||||
beta.113 inchangés, sans migration ni modification d'un monde personnel.
|
||||
|
||||
La sélection du menu ajoute l'identité de la matière et une palette de 256
|
||||
couleurs opaques au maximum. Ce sont des données d'apparence fournies par le
|
||||
client, comme le modèle déjà importé ; elles n'octroient ni objets ni propriétés
|
||||
de bloc. Le serveur vérifie le menu, son jeton, l'accès à l'atelier, les limites
|
||||
et l'objet effectivement présent. Il applique lui-même la palette et facture
|
||||
chaque résultat. Un changement de matière invalide immédiatement l'ancien
|
||||
résultat jusqu'à réception de la sélection correspondante. Les deux côtés
|
||||
doivent utiliser beta.116. Aucun nouveau format de sauvegarde.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`Sculpture116ClientChecks` passe en 1 min 7 s, sur un nouveau monde plat de
|
||||
Minecraft 26.3, graine 116, puis la régression complète de l'atelier :
|
||||
|
||||
- Les 1 207 objets-blocs enregistrés ont une palette opaque, non vide et bornée.
|
||||
- Fabrication payée avec pierre, planches de chêne, or, laine rouge, verre,
|
||||
feuilles de chêne, argile et ardoise ; aperçu identique au résultat serveur.
|
||||
- Conservation des tons clairs/sombres du modèle et de ses coordonnées, sans
|
||||
convertir les 16 argiles colorées en une simple couleur unie.
|
||||
- Une matière différente efface aussitôt l'ancien résultat ; une sélection
|
||||
périmée ou un jeton incorrect ne fabrique rien et ne consomme aucun objet.
|
||||
- Les conteneurs contenant des objets sont refusés ; ils peuvent servir de
|
||||
matière une fois vidés. Les objets qui ne sont pas des blocs sont refusés.
|
||||
- Bornes et opacité des palettes, véritable rechargement des ressources,
|
||||
neuf cases de hotbar, stock plein, consommation exacte et restitution.
|
||||
- Poses selon les quatre regards, accroche au bord, grille de voxels,
|
||||
casse/repose, sauvegarde et reconnexion après suppression du fichier GLB.
|
||||
- Interface FR/EN aux échelles 2/3, génération de plan au Métabli inchangée.
|
||||
|
||||
Marqueurs `SCULPTURE116_PALETTES`, `SCULPTURE116_MATERIALS_PASS`,
|
||||
`SCULPTURE116_PASS` et régressions 106/111/112/113 dans
|
||||
`build/sculpture116-client.log`. Captures relues dans
|
||||
`build/sculpture116-evidence/`. Un libellé est raccourci en « Bloc » pour éviter
|
||||
le contact avec « Sculpture » dans le cadre compact.
|
||||
|
||||
La palette vient de la texture représentative du modèle de bloc du pack actif,
|
||||
avec la teinte locale ; une texture animée utilise sa première image. Les blocs
|
||||
sans texture lisible utilisent leur couleur de carte. Il s'agit d'un transfert
|
||||
de couleurs sur les voxels opaques, sans transparence, émission ou fonction
|
||||
spéciale du bloc d'origine. Les sculptures existantes gardent leurs couleurs.
|
||||
|
||||
Essais natifs macOS avec serveur intégré ; pas de client Windows ni de test
|
||||
LAN à plusieurs joueurs. Le serveur dédié de GameTest reste exclu selon le
|
||||
refus antérieur de son EULA. Aucun monde personnel ouvert.
|
||||
|
||||
La régression native du Fourneau passe également dans beta.116, en 42 s :
|
||||
`build/sculpture116-furnace.log`, captures FR/EN dans
|
||||
`build/sculpture116-furnace-evidence/`. Les 27 cases, le glisser-déposer, les
|
||||
combustibles et les états de cuisson restent conformes au ticket beta.115.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en 2 min 23 s : 125 tâches, dont 105 exécutées. Sources Java concordantes avec
|
||||
les JAR sources, versions alignées et intégrité des deux MRpack vérifiées.
|
||||
Le JAR conserve à l'identique le correctif Fourneau beta.115 ; seuls les
|
||||
classes de matière de sculpture et libellés FR/EN changent dans la production.
|
||||
Les classes de test et modèles GLB de test sont absents des archives.
|
||||
|
||||
- `Sanctuary-beta.116.mrpack` : 10189831 octets, SHA-256
|
||||
`7df04a6e2a57e76a55ca57bd7520b05cbb573392690b401b868837498807d252`.
|
||||
- `Sanctuary-Test-beta.116.mrpack` : 10208754 octets, SHA-256
|
||||
`f4c9293e20384b05366d092fc7ee1abea275940a02373ee4036f73f0caa43747`.
|
||||
- JAR Sanctuary :
|
||||
`76a9205783c8b18506c32c9eef6c481d95fd3ba0746a2bdd558a194534a51a32`.
|
||||
|
||||
Reçu local : `build/sculpture116-artifact.json`. Publication et synchronisation
|
||||
de l'instance en cours ; leurs résultats seront consignés après vérification.
|
||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.115
|
||||
pack_version=beta.115
|
||||
mod_version=beta.116
|
||||
pack_version=beta.116
|
||||
resource_pack_version=beta.090
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -93,6 +93,9 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def sculpture116Client = providers.gradleProperty('sanctuarySculpture116ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySculpture116ClientTests', sculpture116Client)
|
||||
if (sculpture116Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture116ClientChecks') }
|
||||
def furnace115Client = providers.gradleProperty('sanctuaryFurnace115ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryFurnace115ClientTests', furnace115Client)
|
||||
if (furnace115Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Furnace115ClientChecks') }
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.nio.file.*;
|
||||
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.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
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.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.ItemContainerContents;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** All registered block materials, native paid crafts, stale input rejection and pack reload. */
|
||||
public final class Sculpture116ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos WORKSHOP=new BlockPos(2,1,2);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);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){
|
||||
Starter039TestSupport.selectFirst(screen);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 net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
Path file=PlanClient.directory().resolve("sculpture116-"+UUID.randomUUID()+".glb");
|
||||
try{
|
||||
Files.createDirectories(file.getParent());Files.write(file,Statuary106ClientChecks.fixture());var asset=GlbModel.load(file);var source=VoxelSculpture.of(asset.id(),asset.name(),StatueVoxelizer.miniature(asset.faces()));
|
||||
try{new SculpturePalette(List.of());throw new AssertionError("Empty palette accepted");}catch(IllegalArgumentException expected){}
|
||||
try{new SculpturePalette(Collections.nCopies(257,-1));throw new AssertionError("Oversized palette accepted");}catch(IllegalArgumentException expected){}
|
||||
try{new SculpturePalette(List.of(0));throw new AssertionError("Transparent color accepted");}catch(IllegalArgumentException expected){}
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sculpture116 "+UUID.randomUUID());s.setSeed("116");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);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);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.onUpdateAbilities();p.getInventory().clearContent();p.teleportTo(.5,1,.5);for(int x=-4;x<8;x++)for(int z=-4;z<8;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());Statuary.open(p,WORKSHOP);});
|
||||
c.waitForScreen(ClayWorkshopScreen.class);c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();((ClayWorkshopScreen)m.gui.screen()).load(file);});
|
||||
c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);
|
||||
c.runOnClient(m->{int count=0;for(var item:BuiltInRegistries.ITEM)if(item instanceof BlockItem){var stack=new ItemStack(item);check(ClayWorkshopMenu.material(stack),"Empty registered block accepted: "+item);var palette=SculptureMaterialSampler.sample(stack);check(palette!=null&&palette.colors().size()<=256&&palette.colors().stream().allMatch(v->(v>>>24)==255),"Bounded opaque palette: "+item);count++;}check(count>1000,"Whole block registry sampled");System.out.println("SCULPTURE116_PALETTES "+count+" registered block items");
|
||||
var dark=new ItemStack(Items.SHULKER_BOX);dark.set(DataComponents.CONTAINER,ItemContainerContents.fromItems(List.of(new ItemStack(Items.DIAMOND))));check(!ClayWorkshopMenu.material(dark),"Filled container cannot lose contents");check(!ClayWorkshopMenu.material(new ItemStack(Items.DIAMOND)),"Non-block item refused");
|
||||
var shades=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++){int v=x*17;shades.add(new VoxelSculpture.Cell(x,0,0,0xFF000000|v<<16|v<<8|v));}var ramp=new VoxelSculpture(source.modelId(),"Ramp",16,1,1,shades);var palette=SculptureMaterialSampler.sample(new ItemStack(Items.OAK_PLANKS));var recolored=palette.apply(ramp);check(recolored.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>2,"Material retains multiple texture shades");for(int x=1;x<16;x++)check(SculpturePalette.lightness(recolored.cells().get(x).color())>=SculpturePalette.lightness(recolored.cells().get(x-1).color()),"Model shading order retained");});
|
||||
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)){
|
||||
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);
|
||||
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"));
|
||||
check(StatuaryEntity.decode(StatuaryEntity.encode(expected)).equals(expected),"Palette colors persist in unchanged schema 1");
|
||||
c.takeScreenshot("sculpture116-"+BuiltInRegistries.ITEM.getKey(item).getPath());
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getCarried().is(Statuary.ITEM)&&m.player.containerMenu.getSlot(0).getItem().getCount()==1,100);
|
||||
server.waitFor(s->{var menu=s.getPlayerList().getPlayers().getFirst().containerMenu;return menu.getCarried().is(Statuary.ITEM)&&menu.getSlot(0).getItem().getCount()==1;},100);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)s.getPlayerList().getPlayers().getFirst().containerMenu;check(expected.equals(StatuaryEntity.sculpture(menu.getCarried()))&&menu.getSlot(0).getItem().getCount()==1,"Exactly one actual block consumed per sculpture");});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,ClayWorkshopMenu.HOTBAR_START,0,ContainerInput.PICKUP,m.player));c.waitFor(m->m.player.containerMenu.getCarried().isEmpty(),100);server.waitFor(s->s.getPlayerList().getPlayers().getFirst().containerMenu.getCarried().isEmpty(),100);
|
||||
}
|
||||
check(rendered.size()>=7,"Distinct materials produce distinct sculptures");
|
||||
c.waitTicks(6);var stone=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(Items.STONE)));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var menu=(ClayWorkshopMenu)p.containerMenu;menu.getSlot(0).set(new ItemStack(Items.DIAMOND_BLOCK));menu.select(p,source,Items.STONE,stone);check(menu.getSlot(1).getItem().isEmpty(),"Stale material palette rejected by server");});
|
||||
var diamond=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(Items.DIAMOND_BLOCK)).apply(source));c.waitFor(m->diamond.equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())),180);
|
||||
c.runOnClient(m->{var menu=(ClayWorkshopMenu)m.player.containerMenu;ClientPlayNetworking.send(new ClayWorkshopMenu.Select(menu.containerId,UUID.randomUUID(),source,Items.DIAMOND_BLOCK,stone));});c.waitTicks(6);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)s.getPlayerList().getPlayers().getFirst().containerMenu;check(diamond.equals(StatuaryEntity.sculpture(menu.getSlot(1).getItem()))&&menu.getSlot(0).getItem().getCount()==1,"Forged menu token cannot change recipe or spend input");});
|
||||
var before=c.computeOnClient(m->m.getModelManager().getBlockStateModelSet());var reload=c.computeOnClient(m->m.reloadResourcePacks());c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(8);
|
||||
c.runOnClient(m->{check(before!=m.getModelManager().getBlockStateModelSet(),"Actual resource reload replaces sampling generation");check(diamond.equals(StatuaryEntity.sculpture(((ClayWorkshopScreen)m.gui.screen()).preview())),"Preview refreshes after pack reload");});
|
||||
System.out.println("SCULPTURE116_MATERIALS_PASS all block palettes, eight native paid materials, preview equality, shading, stale selections, input identity, token, palette bounds, containers and resource reload");
|
||||
}
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}finally{intro.set(false);try{Files.deleteIfExists(file);}catch(java.io.IOException ignored){}}
|
||||
new Statuary106ClientChecks().runTest(c);
|
||||
System.out.println("SCULPTURE116_PASS materials plus full workshop/hotbar, placement, anchoring, drops, reconnect, FR/EN and Metabli regression");
|
||||
}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+14
-11
@@ -46,8 +46,8 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try{
|
||||
System.out.println("STATUARY106_LIBRARY "+file);Files.createDirectories(file.getParent());byte[] fixture=fixture();Files.write(file,fixture);
|
||||
var asset=GlbModel.load(file);var pixels=StatueVoxelizer.miniature(asset.faces());var source=VoxelSculpture.of(asset.id(),asset.name(),pixels);
|
||||
MODEL.set(source.modelId());check(source.height()==16&&source.width()<16,"Proportions, 16 voxel longest axis");check(source.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>1,"Embedded PNG retains multiple colors");
|
||||
var asset=GlbModel.load(file);var pixels=StatueVoxelizer.miniature(asset.faces());var originalSource=VoxelSculpture.of(asset.id(),asset.name(),pixels);
|
||||
MODEL.set(originalSource.modelId());check(originalSource.height()==16&&originalSource.width()<16,"Proportions, 16 voxel longest axis");check(originalSource.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>1,"Embedded PNG retains multiple colors");
|
||||
check(CreationLibrary.files(CreationLibrary.Kind.MODEL).contains(file)&&!CreationLibrary.files(CreationLibrary.Kind.SCHEMATIC).contains(file),"One folder, different filters");
|
||||
var invalid=fixture.clone();ByteBuffer.wrap(invalid).order(ByteOrder.LITTLE_ENDIAN).putInt(8,1);reject(invalid,"GLB header length");
|
||||
var oversize=new byte[GlbModel.MAX_BYTES+1];reject(oversize,"File bound");
|
||||
@@ -57,11 +57,12 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
reject(edit(fixture,j->{var a=new JsonArray();a.add(0);j.getAsJsonArray("nodes").get(0).getAsJsonObject().add("children",a);}),"Cyclic scene");
|
||||
var interop=net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve("../../../../../build/statuary106-evidence/BoxTextured.glb").normalize();
|
||||
if(Files.exists(interop)){var box=GlbModel.read(interop);check(box.size()==12,"Independent Khronos indexed GLB");check(!StatueVoxelizer.miniature(box).isEmpty(),"Independent textured box voxelizes");System.out.println("STATUARY106_INTEROP_PASS Khronos BoxTextured GLB");}
|
||||
check(source.rotate().rotate().rotate().rotate().equals(source),"Four rotations preserve ID and data");
|
||||
try{new VoxelSculpture(source.modelId(),"bad",16,16,16,List.of(new VoxelSculpture.Cell(0,0,0,-1),new VoxelSculpture.Cell(0,0,0,-1)));throw new AssertionError("Duplicate cell accepted");}catch(IllegalArgumentException expected){}
|
||||
check(originalSource.rotate().rotate().rotate().rotate().equals(originalSource),"Four rotations preserve ID and data");
|
||||
try{new VoxelSculpture(originalSource.modelId(),"bad",16,16,16,List.of(new VoxelSculpture.Cell(0,0,0,-1),new VoxelSculpture.Cell(0,0,0,-1)));throw new AssertionError("Duplicate cell accepted");}catch(IllegalArgumentException expected){}
|
||||
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()){
|
||||
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 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());
|
||||
var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(Statuary.BLOCK.defaultBlockState());legacy.remove("properties");
|
||||
@@ -81,7 +82,7 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(ClayWorkshopMenu.HOTBAR_START).getItem().getCount()==3,100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,ClayWorkshopMenu.HOTBAR_START,0,ContainerInput.QUICK_MOVE,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);
|
||||
c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Clay Sculpture"),"Crafted sculpture has translated type name EN"));
|
||||
c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Sculpture"),"Crafted sculpture has translated type name EN"));
|
||||
c.takeScreenshot("statuary106-workshop-en");
|
||||
checkWorkshopLayout(c);
|
||||
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(3);checkWorkshopLayout(c);c.takeScreenshot("statuary111-workshop-en-scale3");
|
||||
@@ -97,8 +98,9 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
p.getInventory().setItem(18,ItemStack.EMPTY);p.getInventory().setItem(Inventory.SLOT_OFFHAND,ItemStack.EMPTY);fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,ItemStack.EMPTY);
|
||||
});
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;ORIGINAL.set(StatuaryEntity.sculpture(menu.getSlot(1).getItem()));check(ORIGINAL.get().equals(source),"Plain clay keeps exact colors and model ID");
|
||||
for(var family:ColoredBricks.families()){var colored=ClayWorkshopMenu.material(source,new ItemStack(family.clay()));check(colored.modelId().equals(source.modelId())&&colored.cells().size()==source.cells().size()&&colored.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()==1,"All 16 clays keep geometry and ID, recolor uniformly");}});
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;ORIGINAL.set(StatuaryEntity.sculpture(menu.getSlot(1).getItem()));check(ORIGINAL.get().equals(source),"Plain clay applies its sampled palette and preserves model ID");
|
||||
});
|
||||
c.runOnClient(m->{for(var family:ColoredBricks.families()){var palette=SculptureMaterialSampler.sample(new ItemStack(family.clay()));var colored=palette.apply(originalSource);check(colored.modelId().equals(source.modelId())&&colored.cells().size()==source.cells().size()&&colored.cells().stream().allMatch(cell->palette.colors().contains(cell.color())),"All 16 clays keep geometry and use actual texture shades");}});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getCarried().is(Statuary.ITEM),100);
|
||||
c.waitTicks(6);
|
||||
@@ -140,13 +142,14 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
server.waitFor(s->player(s).level().getBlockState(west).is(Statuary.BLOCK),100);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(west).getValue(Statuary.FACING)==Direction.WEST,"Dropped statue takes new gaze"));
|
||||
server.runOnServer(s->player(s).teleportTo(.5,1,.5));c.waitFor(m->m.player.distanceToSqr(.5,1,.5)<.05,100);
|
||||
// Reuse the same file with colored clay; server, rather than the client, chooses the tint.
|
||||
// Reuse the file with colored clay; server applies the palette bound to that actual material.
|
||||
server.runOnServer(s->player(s).getInventory().setItem(player(s).getInventory().getSelectedSlot(),ItemStack.EMPTY));c.waitTicks(4);open(c);
|
||||
c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow();menu.getSlot(0).set(new ItemStack(blue.clay(),2));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Sculpture d’argile"),"Crafted sculpture has translated type name FR"));c.takeScreenshot("statuary106-workshop-fr-blue");
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Sculpture"),"Crafted sculpture has translated type name FR"));c.takeScreenshot("statuary106-workshop-fr-blue");
|
||||
checkWorkshopLayout(c);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=StatuaryEntity.sculpture(menu.getSlot(1).getItem());check(blue.modelId().equals(MODEL.get())&&blue.cells().stream().allMatch(v->v.color()==0xFFA0A7BC),"Blue input overrides uploaded model colors");});
|
||||
var expectedBlue=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow().clay())).apply(originalSource));
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=StatuaryEntity.sculpture(menu.getSlot(1).getItem());check(blue.equals(expectedBlue),"Blue input uses sampled palette and model shading");});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);check(p.containerMenu.getSlot(0).getItem().isEmpty(),"Shift crafting consumes stock");check(count(p,Statuary.ITEM)==2,"Shift crafting produces exactly paid quantity");});close(c);
|
||||
// At the Metabli, the same source creates a full-sized construction plan, with no placement in survival.
|
||||
@@ -224,7 +227,7 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
byte[] text=json.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);int padded=(text.length+3)/4*4;byte[] binary=Arrays.copyOfRange(file,20+length,file.length);var out=ByteBuffer.allocate(20+padded+binary.length).order(ByteOrder.LITTLE_ENDIAN);out.putInt(0x46546C67).putInt(2).putInt(out.capacity()).putInt(padded).putInt(0x4E4F534A).put(text);while(out.position()<20+padded)out.put((byte)' ');out.put(binary);return out.array();
|
||||
}
|
||||
/** A tiny authored cat built from triangle boxes, embedded 2×2 texture and a translated scene node. */
|
||||
private static byte[] fixture()throws IOException{
|
||||
static byte[] fixture()throws IOException{
|
||||
var xyz=new ArrayList<Float>();var uv=new ArrayList<Float>();
|
||||
for(float[] box:List.of(new float[]{.2f,0,.1f,.8f,1,.65f},new float[]{.1f,1,.05f,.9f,1.7f,.7f},new float[]{.1f,1.7f,.1f,.3f,2.1f,.5f},new float[]{.7f,1.7f,.1f,.9f,2.1f,.5f},new float[]{.75f,.1f,.2f,1.1f,.65f,.5f})){
|
||||
float x=box[0],y=box[1],z=box[2],X=box[3],Y=box[4],Z=box[5];float[][] v={{x,y,z},{X,y,z},{X,Y,z},{x,Y,z},{x,y,Z},{X,y,Z},{X,Y,Z},{x,Y,Z}};
|
||||
|
||||
@@ -22,7 +22,8 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
||||
private static final Identifier SELECTION=fr.koka.sanctuary.SanctuaryMod.id("hud/hotbar_selection");
|
||||
private static final ExecutorService WORKER=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"Sanctuary clay models");t.setDaemon(true);return t;});
|
||||
private List<Path> files=List.of();private int page,generation,ticks;private boolean busy,pending;
|
||||
private VoxelSculpture source,previewSource;private ItemStack preview=ItemStack.EMPTY,lastClay=ItemStack.EMPTY;
|
||||
private VoxelSculpture source,previewSource;private ItemStack preview=ItemStack.EMPTY,lastMaterial=ItemStack.EMPTY;
|
||||
private SculpturePalette palette;private Object sampledResources;
|
||||
private Component status=text("choose_model");private Button rotate;
|
||||
public ClayWorkshopScreen(ClayWorkshopMenu menu,Inventory inv,Component title){super(menu,inv,title,310,202);}
|
||||
public static Component text(String key,Object...args){return Component.translatable("sanctuary.statuary."+key,args);}
|
||||
@@ -59,12 +60,22 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
||||
}
|
||||
@Override protected void containerTick(){
|
||||
super.containerTick();rotate.active=source!=null&&!busy;
|
||||
if(pending&&++ticks>=5){ticks=0;pending=false;ClientPlayNetworking.send(new ClayWorkshopMenu.Select(menu.containerId,menu.view.token(),source));}
|
||||
var clay=menu.getSlot(0).getItem();
|
||||
if(previewSource!=source||!ItemStack.matches(lastClay,clay)){
|
||||
previewSource=source;lastClay=clay.copy();var colored=ClayWorkshopMenu.material(source,clay);
|
||||
preview=source==null?ItemStack.EMPTY:StatuaryEntity.item(colored==null?source:colored);
|
||||
var material=menu.getSlot(0).getItem();var resources=minecraft.getModelManager().getBlockStateModelSet();
|
||||
if(previewSource!=source||!ItemStack.isSameItemSameComponents(lastMaterial,material)||sampledResources!=resources){
|
||||
previewSource=source;lastMaterial=material.copy();sampledResources=resources;
|
||||
palette=SculptureMaterialSampler.sample(material);
|
||||
var colored=source==null?null:palette==null?source:palette.apply(source);
|
||||
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());
|
||||
}
|
||||
// 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.
|
||||
boolean needsResult=source!=null&&palette!=null&&!ItemStack.matches(preview,menu.getSlot(1).getItem());
|
||||
if(++ticks>=5&&(pending||needsResult)){
|
||||
ticks=0;pending=false;
|
||||
if(source!=null&&palette!=null)ClientPlayNetworking.send(new ClayWorkshopMenu.Select(menu.containerId,menu.view.token(),source,material.getItem(),palette));
|
||||
}
|
||||
|
||||
}
|
||||
@Override public void onFilesDrop(List<Path> files){if(!files.isEmpty())load(files.getFirst());}
|
||||
@Override public void removed(){generation++;super.removed();}
|
||||
@@ -88,6 +99,7 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
||||
g.text(font,title,10,8,0xFF303030,false);g.text(font,text("models"),10,22,0xFF404040,false);
|
||||
g.text(font,text("input"),230-font.width(text("input"))/2,95,0xFF303030,false);g.text(font,text("output"),imageWidth-10-font.width(text("output")),95,0xFF303030,false);
|
||||
}
|
||||
ItemStack preview(){return preview;}
|
||||
public boolean busy(){return busy;}
|
||||
public VoxelSculpture selected(){return source;}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
/** Representative texture pixels from the active pack, including biome tint and animated first frame. */
|
||||
final class SculptureMaterialSampler {
|
||||
static SculpturePalette sample(ItemStack stack){
|
||||
if(!ClayWorkshopMenu.material(stack))return null;
|
||||
var state=((BlockItem)stack.getItem()).getBlock().defaultBlockState();var sample=PlanMaterials.sample(state);
|
||||
var mc=net.minecraft.client.Minecraft.getInstance();var tints=mc.getBlockColors().getTintSources(state);
|
||||
int tint=tints.isEmpty()?0xFFFFFFFF:tints.getFirst().colorInWorld(state,mc.level,mc.player.blockPosition());
|
||||
var pixels=new ArrayList<Integer>();
|
||||
for(int pixel:sample.pixels().argb()){
|
||||
int color=ARGB.multiply(pixel,tint);
|
||||
if((color>>>24)>=128)pixels.add(color|0xFF000000);
|
||||
}
|
||||
if(pixels.isEmpty())pixels.add(sample.average()|0xFF000000);
|
||||
pixels.sort(Comparator.comparingDouble(SculpturePalette::lightness).thenComparingInt(Integer::intValue));
|
||||
var palette=new ArrayList<Integer>();int count=Math.min(SculpturePalette.MAX_COLORS,pixels.size());
|
||||
for(int i=0;i<count;i++)palette.add(pixels.get((int)((i+.5)*pixels.size()/count)));
|
||||
return new SculpturePalette(palette);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package fr.koka.sanctuary.plans;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.building.ColoredBricks;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.core.*;
|
||||
@@ -16,23 +17,27 @@ import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native input/result slots. Server recomputes the color from the actual clay and charges each take. */
|
||||
/** Native input/result slots. Server validates the material, applies the bounded palette and charges each take. */
|
||||
public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
public static final int HOTBAR_START=2,HOTBAR_END=HOTBAR_START+Inventory.SELECTION_SIZE,HOTBAR_X=75,HOTBAR_Y=176;
|
||||
public record View(BlockPos pos,UUID token){
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,View> CODEC=StreamCodec.of((b,p)->{b.writeBlockPos(p.pos);b.writeUUID(p.token);},b->new View(b.readBlockPos(),b.readUUID()));
|
||||
}
|
||||
/** At most 24 KiB of cells in one atomic, bounded selection, scoped to one open menu. */
|
||||
public record Select(int menu,UUID token,VoxelSculpture sculpture) implements CustomPacketPayload {
|
||||
/** At most 24 KiB of cells plus 256 palette colors in one atomic, bounded selection, scoped to one open menu. */
|
||||
public record Select(int menu,UUID token,VoxelSculpture sculpture,Item material,SculpturePalette palette) implements CustomPacketPayload {
|
||||
public static final Type<Select> TYPE=new Type<>(SanctuaryMod.id("clay_model"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,Select> CODEC=StreamCodec.of((b,p)->{
|
||||
b.writeVarInt(p.menu);b.writeUUID(p.token);b.writeBoolean(p.sculpture!=null);
|
||||
b.writeVarInt(p.menu);b.writeUUID(p.token);b.writeVarInt(BuiltInRegistries.ITEM.getId(p.material));b.writeVarIntArray(p.palette==null?new int[0]:p.palette.colors().stream().mapToInt(Integer::intValue).toArray());b.writeBoolean(p.sculpture!=null);
|
||||
if(p.sculpture!=null){var s=p.sculpture;b.writeUtf(s.modelId(),128);b.writeUtf(s.name(),128);b.writeByte(s.width());b.writeByte(s.height());b.writeByte(s.depth());b.writeVarInt(s.cells().size());for(var c:s.cells()){b.writeShort(c.key());b.writeInt(c.color());}}
|
||||
},b->{
|
||||
int menu=b.readVarInt();var token=b.readUUID();if(!b.readBoolean())return new Select(menu,token,null);
|
||||
int menu=b.readVarInt();var token=b.readUUID();int itemId=b.readVarInt();
|
||||
if(itemId<0||itemId>=BuiltInRegistries.ITEM.size())throw new IllegalArgumentException("sculpture_material");
|
||||
var material=BuiltInRegistries.ITEM.byId(itemId);var colors=b.readVarIntArray(SculpturePalette.MAX_COLORS);
|
||||
var palette=colors.length==0?null:new SculpturePalette(Arrays.stream(colors).boxed().toList());
|
||||
if(!b.readBoolean())return new Select(menu,token,null,material,palette);
|
||||
String model=b.readUtf(128),name=b.readUtf(128);int w=b.readUnsignedByte(),h=b.readUnsignedByte(),d=b.readUnsignedByte(),n=b.readVarInt();
|
||||
if(n<1||n>VoxelSculpture.MAX_CELLS)throw new IllegalArgumentException("sculpture_limit");int[] packed=new int[n*2];for(int i=0;i<n;i++){packed[i*2]=b.readUnsignedShort();packed[i*2+1]=b.readInt();}
|
||||
return new Select(menu,token,VoxelSculpture.unpack(model,name,w,h,d,packed));
|
||||
return new Select(menu,token,VoxelSculpture.unpack(model,name,w,h,d,packed),material,palette);
|
||||
});
|
||||
public Type<? extends CustomPacketPayload> type(){return TYPE;}
|
||||
}
|
||||
@@ -41,41 +46,42 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
private final SimpleContainer input=new SimpleContainer(1){@Override public void setChanged(){super.setChanged();ClayWorkshopMenu.this.slotsChanged(this);}};
|
||||
private final SimpleContainer result=new SimpleContainer(1);
|
||||
private VoxelSculpture selected;
|
||||
private ItemStack lastClay=ItemStack.EMPTY;
|
||||
private ItemStack lastMaterial=ItemStack.EMPTY;
|
||||
private Item selectedMaterial=Items.AIR;
|
||||
private SculpturePalette selectedPalette;
|
||||
private long lastSelection=-100;
|
||||
public ClayWorkshopMenu(int id,Inventory inv,View view){this(id,inv,view,null);}
|
||||
public ClayWorkshopMenu(int id,Inventory inv,View view,Level level){
|
||||
super(Statuary.MENU,id);this.view=view;this.level=level;
|
||||
addSlot(new Slot(input,0,222,106){@Override public boolean mayPlace(ItemStack stack){return clay(stack);}});
|
||||
addSlot(new Slot(input,0,222,106){@Override public boolean mayPlace(ItemStack stack){return material(stack);}});
|
||||
addSlot(new Slot(result,0,282,106){
|
||||
@Override public boolean mayPlace(ItemStack stack){return false;}
|
||||
@Override public boolean mayPickup(Player p){return mayCraft(p)&&clay(input.getItem(0))&&!getItem().isEmpty();}
|
||||
@Override public boolean mayPickup(Player p){return mayCraft(p)&&material(input.getItem(0))&&(level==null||input.getItem(0).is(selectedMaterial))&&!getItem().isEmpty();}
|
||||
@Override public void onTake(Player p,ItemStack stack){input.removeItem(0,1);input.setChanged();super.onTake(p,stack);}
|
||||
});
|
||||
// Use the nine carried slots without registering a full inventory/overflow range.
|
||||
addInventoryHotbarSlots(inv,HOTBAR_X,HOTBAR_Y);
|
||||
}
|
||||
// Colors match the published clay palette (tools/colored-bricks-palette.json), in native dye order.
|
||||
private static final int[] CLAY_COLORS={0xC6C4C8,0xD3AA8D,0xD39FD0,0x91B0CA,0xC8BF98,0xA2C69A,0xDAC4DD,0x74767D,0xA9A8AC,0xA8C5C1,0xBE91E3,0xA0A7BC,0xA49A91,0xA0B499,0xB49A99,0x444541};
|
||||
public static boolean clay(ItemStack item){return item.is(Items.CLAY)||ColoredBricks.families().stream().anyMatch(f->item.is(f.clay().asItem()));}
|
||||
public static VoxelSculpture material(VoxelSculpture model,ItemStack clay){
|
||||
if(model==null||!clay(clay))return null;
|
||||
if(clay.is(Items.CLAY))return model;
|
||||
for(var family:ColoredBricks.families())if(clay.is(family.clay().asItem()))return model.color(0xFF000000|CLAY_COLORS[family.color().getId()]);
|
||||
return null;
|
||||
public static boolean material(ItemStack item){
|
||||
if(item.isEmpty()||!(item.getItem() instanceof BlockItem))return false;
|
||||
var contents=item.get(DataComponents.CONTAINER);
|
||||
return contents==null||!contents.nonEmptyItems().iterator().hasNext();
|
||||
}
|
||||
public void select(ServerPlayer p,VoxelSculpture sculpture){
|
||||
public void select(ServerPlayer p,VoxelSculpture sculpture,Item material,SculpturePalette palette){
|
||||
if(level==null||!stillValid(p)||level.getGameTime()-lastSelection<4)return;
|
||||
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;
|
||||
lastSelection=level.getGameTime();selected=sculpture;refresh();broadcastChanges();
|
||||
lastSelection=level.getGameTime();
|
||||
if(sculpture!=null&&(!material(input.getItem(0))||!input.getItem(0).is(material)||palette==null))return;
|
||||
selected=sculpture;selectedMaterial=material;selectedPalette=palette;refresh();broadcastChanges();
|
||||
}
|
||||
private void refresh(){
|
||||
if(level==null)return;var sculpture=material(selected,input.getItem(0));lastClay=input.getItem(0).copy();
|
||||
if(level==null)return;var item=input.getItem(0);
|
||||
var sculpture=selected!=null&&selectedPalette!=null&&material(item)&&item.is(selectedMaterial)?selectedPalette.apply(selected):null;lastMaterial=item.copy();
|
||||
result.setItem(0,sculpture==null?ItemStack.EMPTY:StatuaryEntity.item(sculpture));
|
||||
}
|
||||
@Override public void slotsChanged(Container container){if(container==input)refresh();super.slotsChanged(container);}
|
||||
@Override public void broadcastChanges(){if(level!=null&&!ItemStack.matches(lastClay,input.getItem(0)))refresh();super.broadcastChanges();}
|
||||
@Override public void broadcastChanges(){if(level!=null&&!ItemStack.matches(lastMaterial,input.getItem(0)))refresh();super.broadcastChanges();}
|
||||
@Override public boolean stillValid(Player p){return level==null?p.isAlive():Statuary.allowed(p,level,view.pos);}
|
||||
private boolean mayCraft(Player p){
|
||||
if(!stillValid(p))return false;
|
||||
@@ -94,7 +100,7 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
if(index<0||index>=slots.size()||!stillValid(p))return ItemStack.EMPTY;var slot=slots.get(index);if(!slot.mayPickup(p)||!slot.hasItem())return ItemStack.EMPTY;
|
||||
var stack=slot.getItem();var copy=stack.copy();
|
||||
if(index<HOTBAR_START){if(!moveItemStackTo(stack,HOTBAR_START,HOTBAR_END,true))return ItemStack.EMPTY;}
|
||||
else if(clay(stack)){if(!moveItemStackTo(stack,0,1,false))return ItemStack.EMPTY;}
|
||||
else if(material(stack)){if(!moveItemStackTo(stack,0,1,false))return ItemStack.EMPTY;}
|
||||
else return ItemStack.EMPTY;
|
||||
if(stack.isEmpty())slot.set(ItemStack.EMPTY);else slot.setChanged();
|
||||
if(index==1)slot.onTake(p,copy);return copy;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.koka.sanctuary.plans;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/** Bounded appearance data, applied identically by preview and authoritative crafting. */
|
||||
public record SculpturePalette(List<Integer> colors) {
|
||||
public static final int MAX_COLORS=256;
|
||||
public SculpturePalette {
|
||||
if(colors==null||colors.isEmpty()||colors.size()>MAX_COLORS||colors.stream().anyMatch(c->c==null||(c>>>24)!=255))
|
||||
throw new IllegalArgumentException("sculpture_palette");
|
||||
colors=colors.stream().sorted(Comparator.comparingDouble(SculpturePalette::lightness).thenComparingInt(Integer::intValue)).toList();
|
||||
}
|
||||
public static double lightness(int color){return StatueVoxelizer.colorSpace(color)[0];}
|
||||
public VoxelSculpture apply(VoxelSculpture source){
|
||||
if(source==null)return null;
|
||||
var shades=new HashMap<Integer,Double>();for(var cell:source.cells())shades.computeIfAbsent(cell.color(),SculpturePalette::lightness);
|
||||
double min=shades.values().stream().mapToDouble(Double::doubleValue).min().orElse(0),max=shades.values().stream().mapToDouble(Double::doubleValue).max().orElse(0);
|
||||
var matches=new HashMap<Integer,Integer>();
|
||||
shades.forEach((color,l)->{double fraction=max-min<.00001?.5:(l-min)/(max-min);matches.put(color,colors.get(Math.clamp((int)Math.round(fraction*(colors.size()-1)),0,colors.size()-1)));});
|
||||
return new VoxelSculpture(source.modelId(),source.name(),source.width(),source.height(),source.depth(),source.cells().stream()
|
||||
.map(c->new VoxelSculpture.Cell(c.x(),c.y(),c.z(),matches.get(c.color()))).toList());
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public final class Statuary {
|
||||
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.FUNCTIONAL_BLOCKS).register(out->out.accept(WORKSHOP_ITEM));
|
||||
PayloadTypeRegistry.serverboundPlay().register(ClayWorkshopMenu.Select.TYPE,ClayWorkshopMenu.Select.CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(ClayWorkshopMenu.Select.TYPE,(packet,c)->c.server().execute(()->{
|
||||
if(c.player().containerMenu instanceof ClayWorkshopMenu menu&&menu.containerId==packet.menu()&&menu.view.token().equals(packet.token()))menu.select(c.player(),packet.sculpture());
|
||||
if(c.player().containerMenu instanceof ClayWorkshopMenu menu&&menu.containerId==packet.menu()&&menu.view.token().equals(packet.token()))menu.select(c.player(),packet.sculpture(),packet.material(),packet.palette());
|
||||
}));
|
||||
}
|
||||
public static boolean allowed(Player p,Level l,BlockPos pos){
|
||||
|
||||
@@ -2151,23 +2151,23 @@
|
||||
"sanctuary.weather.season.summer": "Summer",
|
||||
"sanctuary.weather.season.autumn": "Autumn",
|
||||
"sanctuary.weather.season.winter": "Winter",
|
||||
"block.sanctuary.statuary": "Clay Sculpture",
|
||||
"block.sanctuary.statuary": "Sculpture",
|
||||
"block.sanctuary.clay_workshop": "Clay workshop",
|
||||
"sanctuary.plans.imported_models": "My 3D models",
|
||||
"sanctuary.plans.native_models": "Discovered creatures",
|
||||
"sanctuary.plans.imported_model": "3D model → block statue",
|
||||
"sanctuary.plans.model_files_help": "GLB models in schematics/sanctuary/, alongside your schematics. Create a full-block statue here. Craft 16³ miniatures at the clay workshop.",
|
||||
"sanctuary.plans.empty_models": "Place a .glb file in the shared folder, then refresh.",
|
||||
"sanctuary.statuary.choose_model": "Choose a GLB model, then add clay from your hotbar.",
|
||||
"sanctuary.statuary.choose_model": "Choose a GLB model, then a material block from your hotbar.",
|
||||
"sanctuary.statuary.folder": "Shared folder",
|
||||
"sanctuary.statuary.refresh": "Refresh",
|
||||
"sanctuary.statuary.models": "GLB models",
|
||||
"sanctuary.statuary.placement": "The sculpture faces your gaze and snaps to the aimed block edge, on the pixel grid. Rotate adjusts the model before crafting.",
|
||||
"sanctuary.statuary.rotate": "Rotate",
|
||||
"sanctuary.statuary.working": "Voxelizing to 16 × 16 × 16…",
|
||||
"sanctuary.statuary.ready": "1 clay = 1 sculpture. Plain: model colors. Colored: solid tint.",
|
||||
"sanctuary.statuary.ready": "1 block = 1 sculpture. Shades come from your material’s texture.",
|
||||
"sanctuary.statuary.empty": "No .glb files",
|
||||
"sanctuary.statuary.input": "Clay",
|
||||
"sanctuary.statuary.input": "Block",
|
||||
"sanctuary.statuary.output": "Sculpture",
|
||||
"sanctuary.statuary.model_name": "Model: %s",
|
||||
"sanctuary.statuary.error.file": "Cannot read this local file.",
|
||||
@@ -2203,5 +2203,7 @@
|
||||
"sanctuary.multiblock.state.cooking": "Cooking: %s %%",
|
||||
"sanctuary.multiblock.state.no_fuel": "Add fuel — %s %% progress kept",
|
||||
"sanctuary.multiblock.state.blocked": "Empty the matching slot in Results.",
|
||||
"sanctuary.multiblock.state.no_recipe": "This item has no furnace recipe."
|
||||
"sanctuary.multiblock.state.no_recipe": "This item has no furnace recipe.",
|
||||
"sanctuary.statuary.choose_material": "Add a block to apply its palette to the model. Containers must be empty.",
|
||||
"sanctuary.statuary.material_ready": "1 block = 1 sculpture. Palette: %s."
|
||||
}
|
||||
|
||||
@@ -2151,23 +2151,23 @@
|
||||
"sanctuary.weather.season.summer": "Été",
|
||||
"sanctuary.weather.season.autumn": "Automne",
|
||||
"sanctuary.weather.season.winter": "Hiver",
|
||||
"block.sanctuary.statuary": "Sculpture d’argile",
|
||||
"block.sanctuary.statuary": "Sculpture",
|
||||
"block.sanctuary.clay_workshop": "Atelier d’argile",
|
||||
"sanctuary.plans.imported_models": "Mes modèles 3D",
|
||||
"sanctuary.plans.native_models": "Créatures découvertes",
|
||||
"sanctuary.plans.imported_model": "Modèle 3D → statue en blocs",
|
||||
"sanctuary.plans.model_files_help": "Modèles GLB dans schematics/sanctuary/, avec vos schémas. Ici, créez une grande statue en blocs. Les miniatures 16³ se fabriquent à l’atelier d’argile.",
|
||||
"sanctuary.plans.empty_models": "Déposez un fichier .glb dans le dossier commun, puis actualisez.",
|
||||
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB, puis déposez de l’argile depuis votre barre rapide.",
|
||||
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB, puis un bloc de matière dans votre barre rapide.",
|
||||
"sanctuary.statuary.folder": "Dossier commun",
|
||||
"sanctuary.statuary.refresh": "Actualiser",
|
||||
"sanctuary.statuary.models": "Modèles GLB",
|
||||
"sanctuary.statuary.placement": "La sculpture suit votre regard et se cale contre le bord visé du bloc, sur la grille de pixels. Tourner ajuste le modèle avant fabrication.",
|
||||
"sanctuary.statuary.rotate": "Tourner",
|
||||
"sanctuary.statuary.working": "Voxelisation en 16 × 16 × 16…",
|
||||
"sanctuary.statuary.ready": "1 argile = 1 sculpture. Normale : couleurs du modèle. Colorée : teinte unie.",
|
||||
"sanctuary.statuary.ready": "1 bloc = 1 sculpture. Les nuances viennent de la texture de votre matière.",
|
||||
"sanctuary.statuary.empty": "Aucun fichier .glb",
|
||||
"sanctuary.statuary.input": "Argile",
|
||||
"sanctuary.statuary.input": "Bloc",
|
||||
"sanctuary.statuary.output": "Sculpture",
|
||||
"sanctuary.statuary.model_name": "Modèle : %s",
|
||||
"sanctuary.statuary.error.file": "Impossible de lire ce fichier local.",
|
||||
@@ -2203,5 +2203,7 @@
|
||||
"sanctuary.multiblock.state.cooking": "En cuisson : %s %%",
|
||||
"sanctuary.multiblock.state.no_fuel": "Ajouter du combustible — %s %% conservés",
|
||||
"sanctuary.multiblock.state.blocked": "Vider la case correspondante dans Résultats.",
|
||||
"sanctuary.multiblock.state.no_recipe": "Cet objet ne possède pas de recette au four."
|
||||
"sanctuary.multiblock.state.no_recipe": "Cet objet ne possède pas de recette au four.",
|
||||
"sanctuary.statuary.choose_material": "Ajoutez un bloc pour appliquer sa palette au modèle. Les conteneurs doivent être vides.",
|
||||
"sanctuary.statuary.material_ready": "1 bloc = 1 sculpture. Palette : %s."
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.115"
|
||||
version = "beta.116"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user