Extend golden wrench to native block states and persistent texture rotation (beta.120)
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -20,12 +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
|
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||||
à implémenter.
|
à implémenter.
|
||||||
|
|
||||||
La version **beta.119** permet de centrer une sculpture en visant le milieu
|
La version **beta.120** étend la clé dorée aux états des blocs : clic gauche
|
||||||
du bloc, ou de la caler contre un bord en visant autour. La grille de 1/16
|
pour choisir une propriété, clic droit pour changer sa valeur, Maj pour inverser.
|
||||||
reste respectée, même pour les modèles de taille impaire.
|
La terre et les modèles de blocs peuvent aussi tourner leurs textures.
|
||||||
Le [ticket de livraison](docs/sculpture-center-beta119.md) conserve les contrôles
|
Le [ticket de livraison](docs/golden-wrench-beta120.md) détaille les contrôles,
|
||||||
et le suivi de distribution. La release est publiée et l’instance **Sanctuary Beta**
|
la compatibilité et la distribution. Tests natifs, build complet et deux packs
|
||||||
est synchronisée, sauvegardes et réglages conservés.
|
vérifiés localement.
|
||||||
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à
|
||||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
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.
|
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||||
@@ -36,6 +36,11 @@ 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.120 — Clé dorée et états de blocs
|
||||||
|
|
||||||
|
Gestes du debug stick, rotation de textures et assemblage des machines conservé.
|
||||||
|
[Contrat et vérifications](docs/golden-wrench-beta120.md).
|
||||||
|
|
||||||
## beta.119 — Sculptures au centre ou au bord
|
## beta.119 — Sculptures au centre ou au bord
|
||||||
|
|
||||||
Pose selon le point visé, centrage sur la grille entière et ancrage conservé
|
Pose selon le point visé, centrage sur la grille entière et ancrage conservé
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# TOOL-01 — Clé dorée et états de blocs — beta.120
|
||||||
|
|
||||||
|
Contrat du 17 septembre 2026, branche `codex/golden-wrench-beta120`.
|
||||||
|
Le créateur valide les gestes du debug stick : clic gauche choisit la propriété,
|
||||||
|
clic droit parcourt ses valeurs, Maj inverse le parcours. L'orientation est
|
||||||
|
proposée en premier, puis les propriétés de forme et les autres états natifs.
|
||||||
|
La sélection est personnelle, par type de bloc, pour la session courante.
|
||||||
|
Une indication dans la barre d'action affiche la propriété et sa valeur.
|
||||||
|
La clé ne casse pas le bloc, y compris en créatif, et ne remplace pas son type.
|
||||||
|
|
||||||
|
Les propriétés natives sont disponibles en survie avec la clé, sans condition
|
||||||
|
opérateur, dans le respect des droits de construction, protections, portée et
|
||||||
|
verrous de conteneurs. Comme avec le debug stick, les formes choisies restent
|
||||||
|
en place immédiatement ; les mises à jour ultérieures du jeu peuvent recalculer
|
||||||
|
les états dynamiques (redstone, connexions, cultures…).
|
||||||
|
|
||||||
|
Les multiblocs assemblés gardent leurs commandes d'ouverture/dissociation et
|
||||||
|
leurs propriétés techniques ne sont pas éditées individuellement. Un assemblage
|
||||||
|
complet garde priorité au clic droit ; Maj permet de régler les composants
|
||||||
|
non assemblés. Hors d'un assemblage complet, même un four, un baril, un piston
|
||||||
|
ou un établi reçoit l'action générique.
|
||||||
|
|
||||||
|
## Rotation de texture et contrat de stockage
|
||||||
|
|
||||||
|
La terre n'a pas d'orientation dans son blockstate vanilla. Une propriété
|
||||||
|
supplémentaire de la clé, « Rotation des textures », parcourt 0°, 90°, 180° et
|
||||||
|
270° sur les faces des modèles de blocs. La géométrie, l'éclairage, les teintes,
|
||||||
|
les ressources actives, le bloc, son inventaire et son butin restent natifs.
|
||||||
|
Les rendus spéciaux d'entités de blocs ne sont pas transformés par ce réglage
|
||||||
|
des faces ; leur orientation demeure modifiable par leurs états natifs.
|
||||||
|
|
||||||
|
Nouvelle donnée facultative `sanctuary:wrench_textures_v1`, attachée au chunk :
|
||||||
|
table immuable position entière → quart de tour (1 à 3). Une absence signifie
|
||||||
|
0°. Elle n'est créée qu'après une action de clé ; revenir à 0° retire l'entrée,
|
||||||
|
puis l'attachement s'il est vide. Aucun état vanilla ni format existant n'est
|
||||||
|
modifié ; aucun parcours, conversion ou régénération des anciens mondes.
|
||||||
|
Les modifications de blockstate du même type conservent la rotation. Casser,
|
||||||
|
remplacer ou déplacer par piston retire la rotation à l'ancienne position ;
|
||||||
|
les objets récupérés restent ordinaires. Les nouveaux clients la reçoivent
|
||||||
|
après le chunk natif, par lots bornés de 1 024 entrées au maximum.
|
||||||
|
Déconnexion, changement de dimension et déchargement libèrent le cache visuel.
|
||||||
|
Revenir à une ancienne version du mod supprime seulement cet effet visuel ;
|
||||||
|
les blocs conservent leurs identifiants, données et états natifs.
|
||||||
|
|
||||||
|
## Vérifications et livraison
|
||||||
|
|
||||||
|
Le parcours `Wrench120ClientChecks` passe en **47 s** sur Minecraft 26.3,
|
||||||
|
client macOS et serveur intégré, dans un monde plat jetable de graine 120 :
|
||||||
|
|
||||||
|
- **2 522 propriétés natives** parcourues, avec valeurs inverses et type conservé.
|
||||||
|
- Vrais clics sur les escaliers : orientation, moitié supérieure et forme,
|
||||||
|
sans casser le bloc en créatif ; gestes inverses et sélection distincte.
|
||||||
|
Les doubles événements créatifs d'un même clic sont filtrés à la cadence
|
||||||
|
native de cinq ticks.
|
||||||
|
- Terre en survie : rotation réelle des UV émis, positions des sommets et nombre
|
||||||
|
de faces identiques ; retour exact après quatre quarts de tour ou un inverse.
|
||||||
|
- Rechargement des ressources conservant les UV tournés ; capture de terre relue.
|
||||||
|
- Coffre tourné avec ses 23 diamants intacts, sans ouverture accidentelle.
|
||||||
|
- Assemblage/dissociation par vrais clics des quatre familles : Métabli, Fût,
|
||||||
|
Fourneau et super piston. Un clic gauche sur un assemblage ne modifie pas
|
||||||
|
les numéros de composants ; barils et pistons isolés tournent normalement.
|
||||||
|
- Refus en aventure, spectateur ou hors portée ; retrait des données visuelles
|
||||||
|
après remplacement, maintien lors d'un changement d'état du même bloc.
|
||||||
|
- Sauvegarde/reconnexion : rotation de terre côté serveur et client, état
|
||||||
|
natif de l'escalier et inventaire du coffre conservés.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew :sanctuary:runClientGameTest \
|
||||||
|
-PsanctuaryClientTests=true -PsanctuaryWrench120ClientTests=true \
|
||||||
|
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Marqueurs `WRENCH120_PASS` et `WRENCH120_PROPERTIES 2522` dans
|
||||||
|
`build/wrench120-client.log`, captures dans `build/wrench120-evidence/`.
|
||||||
|
Aucun monde personnel ouvert ; pas de validation Windows ni depuis deux
|
||||||
|
ordinateurs. Les rendus de faces natifs et le rechargement du pack sont vérifiés ;
|
||||||
|
pas de validation spécifique d'un shader tiers.
|
||||||
|
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||||
|
|
||||||
|
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||||
|
passe en **2 min 12 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||||
|
aux sources du dépôt. La comparaison avec beta.119 limite les différences de
|
||||||
|
production aux classes de clé/assemblage, aux deux nouveaux hooks, au rendu
|
||||||
|
et aux libellés prévus ; toutes les autres ressources sont identiques.
|
||||||
|
Les packs normal/Test contiennent le même JAR Sanctuary et aucune sauvegarde
|
||||||
|
ni fixture de test.
|
||||||
|
|
||||||
|
- Sanctuary-beta.120.mrpack : 10237973 octets, SHA-256
|
||||||
|
`86b5e480a12ba20b5c1e308c77dbb54bdf8a56806915fb32ca2d16f9a72cc9c9`.
|
||||||
|
- Sanctuary-Test-beta.120.mrpack : 10256895 octets, SHA-256
|
||||||
|
`2e3818483ba7dfa6e5652b97933e105a44e7cc4b269fc567116bf895d54bd8d9`.
|
||||||
|
- JAR Sanctuary : SHA-256
|
||||||
|
`8e35ef0f134a46b7760604175ad768264946d31fdd0694982e049e7a928fde2a`.
|
||||||
|
|
||||||
|
Reçu local : `build/wrench120-artifact.json`. Publication et synchronisation
|
||||||
|
Prism à consigner après leur réussite.
|
||||||
+2
-2
@@ -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.119
|
mod_version=beta.120
|
||||||
pack_version=beta.119
|
pack_version=beta.120
|
||||||
resource_pack_version=beta.090
|
resource_pack_version=beta.090
|
||||||
maven_group=fr.koka.sanctuary
|
maven_group=fr.koka.sanctuary
|
||||||
jei_version=30.32.0-sanctuary.3
|
jei_version=30.32.0-sanctuary.3
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ tasks.named('runGameTest') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('processGametestResources') {
|
tasks.named('processGametestResources') {
|
||||||
|
def wrench120Client = providers.gradleProperty('sanctuaryWrench120ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||||
|
inputs.property('sanctuaryWrench120ClientTests', wrench120Client)
|
||||||
|
if (wrench120Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Wrench120ClientChecks') }
|
||||||
|
|
||||||
def sculpture116Client = providers.gradleProperty('sanctuarySculpture116ClientTests').map { it.toBoolean() }.getOrElse(false)
|
def sculpture116Client = providers.gradleProperty('sanctuarySculpture116ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||||
inputs.property('sanctuarySculpture116ClientTests', sculpture116Client)
|
inputs.property('sanctuarySculpture116ClientTests', sculpture116Client)
|
||||||
if (sculpture116Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture116ClientChecks') }
|
if (sculpture116Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture116ClientChecks') }
|
||||||
|
|||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
package fr.koka.sanctuary.client;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.platform.InputConstants;
|
||||||
|
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||||
|
import fr.koka.sanctuary.multiblock.*;
|
||||||
|
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.fabricmc.fabric.api.client.renderer.v1.Renderer;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
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.BuiltInRegistries;
|
||||||
|
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.InteractionHand;
|
||||||
|
import net.minecraft.world.item.*;
|
||||||
|
import net.minecraft.world.level.GameType;
|
||||||
|
import net.minecraft.world.level.block.*;
|
||||||
|
import net.minecraft.world.level.block.entity.*;
|
||||||
|
import net.minecraft.world.level.block.piston.PistonBaseBlock;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.*;
|
||||||
|
import net.minecraft.world.phys.*;
|
||||||
|
|
||||||
|
/** Real client gestures and persisted server state; rendered UVs must change without moving geometry. */
|
||||||
|
public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||||
|
private static final BlockPos TARGET=new BlockPos(0,1,0),CHEST=new BlockPos(2,1,0),DIRT=new BlockPos(-2,1,0);
|
||||||
|
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 TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||||
|
}});
|
||||||
|
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||||
|
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Wrench120 "+UUID.randomUUID());s.setSeed("120");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);var server=world.getServer();
|
||||||
|
server.runOnServer(s->{var p=player(s);var l=p.level();p.teleportTo(.5,1,3.2);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.WRENCH));p.inventoryMenu.broadcastFullState();
|
||||||
|
l.setBlockAndUpdate(TARGET,Blocks.OAK_STAIRS.defaultBlockState());l.setBlockAndUpdate(CHEST,Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(CHEST)).setItem(0,new ItemStack(Items.DIAMOND,23));l.setBlockAndUpdate(DIRT,Blocks.DIRT.defaultBlockState());
|
||||||
|
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||||
|
int tested=0;for(var block:BuiltInRegistries.BLOCK)for(var property:block.getStateDefinition().getProperties()){
|
||||||
|
var before=block.defaultBlockState();var after=GoldenWrench.cycle(before,property,false);
|
||||||
|
check(after.is(block)&&GoldenWrench.cycle(after,property,true)==before,"Every registered native property has reversible values");tested++;
|
||||||
|
}
|
||||||
|
check(tested>1000,"Broad block property coverage");System.out.println("WRENCH120_PROPERTIES "+tested);
|
||||||
|
});
|
||||||
|
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
|
||||||
|
var stairs=c.computeOnClient(m->m.level.getBlockState(TARGET));click(c,TARGET,false,false);
|
||||||
|
server.waitFor(s->player(s).level().getBlockState(TARGET)!=stairs,100);c.waitFor(m->m.level.getBlockState(TARGET)!=stairs,100);
|
||||||
|
server.runOnServer(s->{var actual=player(s).level().getBlockState(TARGET);check(actual.getValue(StairBlock.FACING)!=stairs.getValue(StairBlock.FACING)&&actual.getValue(StairBlock.HALF)==stairs.getValue(StairBlock.HALF),"Right click changes facing only");});
|
||||||
|
click(c,TARGET,false,true);server.waitFor(s->player(s).level().getBlockState(TARGET)==stairs,100);
|
||||||
|
click(c,TARGET,true,false);server.runOnServer(s->check(GoldenWrench.selected(player(s),stairs).equals("half"),"Left click selects half without breaking creative block: "+GoldenWrench.selected(player(s),stairs)));
|
||||||
|
click(c,TARGET,false,false);server.waitFor(s->player(s).level().getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,100);
|
||||||
|
click(c,TARGET,true,false);server.runOnServer(s->check(GoldenWrench.selected(player(s),stairs).equals("shape"),"Next property is stair shape"));
|
||||||
|
click(c,TARGET,false,false);server.waitFor(s->player(s).level().getBlockState(TARGET).getValue(StairBlock.SHAPE)!=StairsShape.STRAIGHT,100);
|
||||||
|
c.takeScreenshot("wrench120-stairs");
|
||||||
|
// A container rotates in place; inventory and block entity survive the full native interaction.
|
||||||
|
click(c,CHEST,false,false);server.runOnServer(s->{var p=player(s);var chest=(ChestBlockEntity)p.level().getBlockEntity(CHEST);check(chest.getItem(0).is(Items.DIAMOND)&&chest.getItem(0).getCount()==23,"Chest contents retained");check(p.containerMenu==p.inventoryMenu,"Wrench does not open ordinary chest");check(chest.getBlockState().getValue(ChestBlock.FACING)!=Blocks.CHEST.defaultBlockState().getValue(ChestBlock.FACING),"Chest rotates");});
|
||||||
|
// Survival uses exactly the same gestures; dirt keeps its native ID, shape and loot.
|
||||||
|
server.runOnServer(s->player(s).setGameMode(GameType.SURVIVAL));c.waitFor(m->!m.player.isCreative(),100);
|
||||||
|
var original=c.computeOnClient(m->geometry(m,DIRT));c.takeScreenshot("wrench120-dirt-before");
|
||||||
|
click(c,DIRT,false,false);server.waitFor(s->WrenchTextures.rotation(player(s).level().getChunkAt(DIRT),DIRT)==1,100);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||||
|
var rotated=c.computeOnClient(m->geometry(m,DIRT));checkUvs(original,rotated);c.takeScreenshot("wrench120-dirt-90");
|
||||||
|
server.runOnServer(s->check(player(s).level().getBlockState(DIRT)==Blocks.DIRT.defaultBlockState(),"Dirt type and native state unchanged"));
|
||||||
|
click(c,DIRT,true,false);server.runOnServer(s->check(player(s).level().getBlockState(DIRT).is(Blocks.DIRT),"Left click never breaks dirt in survival"));
|
||||||
|
click(c,DIRT,false,true);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==0,100);checkGeometry(original,c.computeOnClient(m->geometry(m,DIRT)),"Reverse restores exact UVs");
|
||||||
|
for(int i=1;i<=4;i++){click(c,DIRT,false,false);int expected=i%4;c.waitFor(m->WrenchTextureModels.rotation(DIRT)==expected,100);}
|
||||||
|
checkGeometry(original,c.computeOnClient(m->geometry(m,DIRT)),"Four turns restore exact original geometry and UVs");
|
||||||
|
click(c,DIRT,false,false);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||||
|
var reload=c.computeOnClient(Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();checkGeometry(rotated,c.computeOnClient(m->geometry(m,DIRT)),"Resource reload retains texture rotation");
|
||||||
|
// Replacing a block clears the visual receipt; blockstate edits retain it.
|
||||||
|
server.runOnServer(s->{var l=player(s).level();var q=new BlockPos(-3,1,0);l.setBlockAndUpdate(q,Blocks.OAK_LOG.defaultBlockState());WrenchTextures.turn(l,q,1);l.setBlock(q,Blocks.OAK_LOG.defaultBlockState().setValue(RotatedPillarBlock.AXIS,Direction.Axis.X),18);check(WrenchTextures.rotation(l.getChunkAt(q),q)==1,"State edit preserves texture rotation");l.removeBlock(q,false);l.setBlockAndUpdate(q,Blocks.OAK_LOG.defaultBlockState());check(WrenchTextures.rotation(l.getChunkAt(q),q)==0,"Break/replacement clears rotation");
|
||||||
|
var p=player(s);p.setGameMode(GameType.ADVENTURE);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Adventure cannot edit");p.setGameMode(GameType.SPECTATOR);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Spectator cannot edit");p.setGameMode(GameType.SURVIVAL);check(!GoldenWrench.allowed(p,TARGET.offset(100,0,0),InteractionHand.MAIN_HAND,Direction.UP),"Out-of-range edit refused");
|
||||||
|
});
|
||||||
|
// A full Metabli still assembles with two normal right clicks, then separates with sneak-use.
|
||||||
|
var table=new BlockPos(6,1,0);server.runOnServer(s->{var p=player(s);for(var q:Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());p.teleportTo(7.5,1,4);});
|
||||||
|
c.waitFor(m->m.player.distanceToSqr(7.5,1,4)<.1&&m.level.getBlockState(table).is(Blocks.CRAFTING_TABLE),100);
|
||||||
|
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||||
|
server.waitFor(s->player(s).level().getBlockState(table).is(Metatable.BLOCK),100);c.waitFor(m->m.level.getBlockState(table).is(Metatable.BLOCK),100);
|
||||||
|
click(c,table.offset(1,0,2),true,false);server.runOnServer(s->check(Metatable.state(player(s).level(),table,true)==1,"Left click cannot corrupt a multiblock's part states"));
|
||||||
|
click(c,table.offset(1,0,2),false,true);server.waitFor(s->Metatable.state(player(s).level(),table,false)==1,100);
|
||||||
|
// Both container assemblers and the piston assembler must win over generic state editing.
|
||||||
|
for(var component:List.of(Blocks.BARREL,Blocks.FURNACE)){
|
||||||
|
server.runOnServer(s->{var l=player(s).level();for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++)l.setBlockAndUpdate(table.offset(x,y,z),component.defaultBlockState());});
|
||||||
|
c.waitFor(m->m.level.getBlockState(table.offset(1,0,2)).is(component),100);
|
||||||
|
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||||
|
server.runOnServer(s->check(MultiblockData.get(player(s).level()).at(table)!=null,"Complete container machine assembles before generic rotation"));
|
||||||
|
click(c,table.offset(1,0,2),false,true);
|
||||||
|
server.runOnServer(s->check(MultiblockData.get(player(s).level()).at(table)==null,"Container machine still separates"));
|
||||||
|
}
|
||||||
|
server.runOnServer(s->{var l=player(s).level();for(int y=2;y>=0;y--)for(int z=0;z<3;z++)for(int x=0;x<3;x++)l.setBlockAndUpdate(table.offset(x,y,z),y==0?Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.FACING,Direction.UP):Blocks.AIR.defaultBlockState());});
|
||||||
|
c.waitFor(m->m.level.getBlockState(table.offset(1,0,2)).is(Blocks.PISTON),100);
|
||||||
|
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||||
|
server.waitFor(s->SuperPistons.status(player(s).level(),table.offset(1,0,1),Direction.UP,false)==1,100);
|
||||||
|
c.waitFor(m->m.level.getBlockState(table.offset(1,0,2)).is(SuperPistons.NORMAL),100);
|
||||||
|
click(c,table.offset(1,0,2),false,true);
|
||||||
|
server.runOnServer(s->check(player(s).level().getBlockState(table.offset(1,0,2)).is(Blocks.PISTON),"Super piston still separates before editing its parts"));
|
||||||
|
// An isolated barrel and piston now use generic states instead of an incomplete-structure error.
|
||||||
|
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,3.2);p.level().setBlockAndUpdate(TARGET,Blocks.BARREL.defaultBlockState());});c.waitFor(m->m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.BARREL),100);
|
||||||
|
click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(BarrelBlock.FACING)!=Blocks.BARREL.defaultBlockState().getValue(BarrelBlock.FACING),"Isolated barrel rotates"));
|
||||||
|
server.runOnServer(s->player(s).level().setBlockAndUpdate(TARGET,Blocks.PISTON.defaultBlockState()));c.waitFor(m->m.level.getBlockState(TARGET).is(Blocks.PISTON),100);
|
||||||
|
click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(PistonBaseBlock.FACING)!=Blocks.PISTON.defaultBlockState().getValue(PistonBaseBlock.FACING),"Isolated piston rotates"));
|
||||||
|
server.runOnServer(s->player(s).level().setBlock(TARGET,stairs.setValue(StairBlock.HALF,Half.TOP),18));saved=world.getWorldSave();
|
||||||
|
}
|
||||||
|
try(var world=saved.open()){
|
||||||
|
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);world.getServer().runOnServer(s->{var l=player(s).level();check(WrenchTextures.rotation(l.getChunkAt(DIRT),DIRT)==1,"Server persisted dirt texture");check(l.getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,"Server persisted native edited state");check(((ChestBlockEntity)l.getBlockEntity(CHEST)).getItem(0).getCount()==23,"Saved chest keeps inventory");});
|
||||||
|
c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,150);c.getInput().lookAt(DIRT);c.takeScreenshot("wrench120-reconnect");
|
||||||
|
}
|
||||||
|
System.out.println("WRENCH120_PASS native creative/survival clicks, reverse, properties, textures and resource reload, unchanged geometry, filled chest, multiblock assembly priority, permissions, cleanup and save/reconnect");
|
||||||
|
}
|
||||||
|
private static void click(ClientGameTestContext c,BlockPos pos,boolean left,boolean sneak){
|
||||||
|
if(sneak){c.getInput().holdKey(o->o.keyShift);c.waitTicks(3);}
|
||||||
|
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5,pos.getY()+.3,pos.getZ()+.5)));c.waitTicks(3);
|
||||||
|
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(pos),"Native cursor hits target "+pos+" instead of "+m.hitResult));
|
||||||
|
if(left){c.getInput().holdKey(o->o.keyAttack);c.waitTicks(1);c.getInput().releaseKey(o->o.keyAttack);}else{c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);}
|
||||||
|
c.waitTicks(5);if(sneak){c.getInput().releaseKey(o->o.keyShift);c.waitTicks(3);}
|
||||||
|
}
|
||||||
|
private static List<float[]> geometry(Minecraft m,BlockPos pos){
|
||||||
|
var result=new ArrayList<float[]>();var state=m.level.getBlockState(pos);
|
||||||
|
var emitter=Renderer.get().quadEmitter(q->{var data=new float[20];for(int i=0;i<4;i++){data[i*5]=q.x(i);data[i*5+1]=q.y(i);data[i*5+2]=q.z(i);data[i*5+3]=q.u(i);data[i*5+4]=q.v(i);}result.add(data);});
|
||||||
|
m.getModelManager().getBlockStateModelSet().get(state).emitQuads(emitter,m.level,pos,state,RandomSource.create(state.getSeed(pos)),face->false);check(!result.isEmpty(),"Native model emits real textured geometry");return result;
|
||||||
|
}
|
||||||
|
private static void checkUvs(List<float[]> before,List<float[]> after){
|
||||||
|
check(before.size()==after.size(),"Same face count");boolean changed=false;
|
||||||
|
for(int q=0;q<before.size();q++)for(int v=0;v<4;v++){for(int axis=0;axis<3;axis++)check(before.get(q)[v*5+axis]==after.get(q)[v*5+axis],"Texture turn never moves geometry");changed|=before.get(q)[v*5+3]!=after.get(q)[v*5+3]||before.get(q)[v*5+4]!=after.get(q)[v*5+4];}
|
||||||
|
check(changed,"Dirt UVs actually change");
|
||||||
|
}
|
||||||
|
private static void checkGeometry(List<float[]> a,List<float[]> b,String message){check(a.size()==b.size(),message);for(int i=0;i<a.size();i++)check(Arrays.equals(a.get(i),b.get(i)),message);}
|
||||||
|
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||||
|
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||||
|
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
|||||||
FoodKnowledgeClient.register();
|
FoodKnowledgeClient.register();
|
||||||
MultiblockScreen.register();
|
MultiblockScreen.register();
|
||||||
FourneauModels.register();
|
FourneauModels.register();
|
||||||
|
WrenchTextureModels.register();
|
||||||
MetatableClient.register();
|
MetatableClient.register();
|
||||||
ClayWorkshopScreen.register();
|
ClayWorkshopScreen.register();
|
||||||
GraveScreen.register();
|
GraveScreen.register();
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package fr.koka.sanctuary.client;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.multiblock.WrenchTextures;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.*;
|
||||||
|
import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
|
||||||
|
import net.fabricmc.fabric.api.client.model.loading.v1.wrapper.WrapperBlockStateModel;
|
||||||
|
import net.fabricmc.fabric.api.client.networking.v1.*;
|
||||||
|
import net.fabricmc.fabric.api.client.renderer.v1.mesh.*;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.renderer.block.BlockAndTintGetter;
|
||||||
|
import net.minecraft.client.renderer.block.dispatch.BlockStateModel;
|
||||||
|
import net.minecraft.core.*;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
|
||||||
|
/** Immutable visual snapshots, usable from mesh workers and rebuilt after resource reloads. */
|
||||||
|
public final class WrenchTextureModels {
|
||||||
|
private static volatile Map<Long,Map<BlockPos,Integer>> chunks=Map.of();
|
||||||
|
public static int rotation(BlockPos pos){return chunks.getOrDefault(ChunkPos.pack(pos),Map.of()).getOrDefault(pos,0);}
|
||||||
|
public static void register(){
|
||||||
|
ClientPlayNetworking.registerGlobalReceiver(WrenchTextures.Update.TYPE,(packet,context)->context.client().execute(()->apply(packet)));
|
||||||
|
ClientPlayConnectionEvents.DISCONNECT.register((handler,client)->chunks=Map.of());
|
||||||
|
ClientLevelEvents.AFTER_CLIENT_LEVEL_CHANGE.register((client,level)->chunks=Map.of());
|
||||||
|
ClientChunkEvents.CHUNK_UNLOAD.register((level,chunk)->{var next=new HashMap<>(chunks);next.remove(chunk.getPos().pack());chunks=Map.copyOf(next);});
|
||||||
|
ModelLoadingPlugin.register(context->context.modifyBlockModelAfterBake().register((model,bake)->new Rotated(model)));
|
||||||
|
}
|
||||||
|
private static void apply(WrenchTextures.Update packet){
|
||||||
|
var client=Minecraft.getInstance();if(client.level==null||!client.level.dimension().identifier().equals(packet.dimension()))return;
|
||||||
|
long key=packet.chunk().pack();var old=chunks.getOrDefault(key,Map.of());var dirty=new HashSet<BlockPos>();
|
||||||
|
var entries=packet.clear()?new HashMap<BlockPos,Integer>():new HashMap<>(old);if(packet.clear())dirty.addAll(old.keySet());
|
||||||
|
for(var entry:packet.entries()){
|
||||||
|
var before=entry.rotation()==0?entries.remove(entry.pos()):entries.put(entry.pos(),entry.rotation());
|
||||||
|
if(!Objects.equals(before,entry.rotation()))dirty.add(entry.pos());
|
||||||
|
}
|
||||||
|
var next=new HashMap<>(chunks);if(entries.isEmpty())next.remove(key);else next.put(key,Map.copyOf(entries));chunks=Map.copyOf(next);
|
||||||
|
var sections=new HashSet<SectionPos>();for(var pos:dirty)sections.add(SectionPos.of(pos));
|
||||||
|
for(var section:sections)client.levelExtractor.setSectionDirty(section.x(),section.y(),section.z());
|
||||||
|
}
|
||||||
|
static boolean rotate(MutableQuadView quad,int turns){
|
||||||
|
// Rotate texture coordinates only: geometry, light, tints and materials stay native.
|
||||||
|
for(int turn=0;turn<turns;turn++){
|
||||||
|
float u=quad.u(0),v=quad.v(0);
|
||||||
|
for(int vertex=0;vertex<3;vertex++)quad.uv(vertex,quad.u(vertex+1),quad.v(vertex+1));
|
||||||
|
quad.uv(3,u,v);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private record Geometry(Object original,int rotation){}
|
||||||
|
private static final class Rotated extends WrapperBlockStateModel {
|
||||||
|
Rotated(BlockStateModel original){super(original);}
|
||||||
|
@Override public void emitQuads(QuadEmitter emitter,BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random,Predicate<Direction> cull){
|
||||||
|
int turns=rotation(pos);if(turns==0){wrapped.emitQuads(emitter,view,pos,state,random,cull);return;}
|
||||||
|
emitter.pushTransform(quad->rotate(quad,turns));
|
||||||
|
try{wrapped.emitQuads(emitter,view,pos,state,random,cull);}finally{emitter.popTransform();}
|
||||||
|
}
|
||||||
|
@Override public Object createGeometryKey(BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random){
|
||||||
|
var key=wrapped.createGeometryKey(view,pos,state,random);int turns=rotation(pos);
|
||||||
|
return turns==0||key==null?key:new Geometry(key,turns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private WrenchTextureModels(){}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.multiblock.WrenchTextures;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(LevelChunk.class)
|
||||||
|
public abstract class WrenchTextureEditMixin {
|
||||||
|
@Inject(method="setBlockState(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Lnet/minecraft/world/level/block/state/BlockState;",at=@At("RETURN"))
|
||||||
|
private void sanctuary$clearTexture(BlockPos pos,BlockState state,int flags,CallbackInfoReturnable<BlockState> ci){
|
||||||
|
var before=ci.getReturnValue();
|
||||||
|
if(before!=null&&!before.is(state.getBlock())&&((LevelChunk)(Object)this).getLevel() instanceof ServerLevel)
|
||||||
|
WrenchTextures.forget((LevelChunk)(Object)this,pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package fr.koka.sanctuary.mixin;
|
||||||
|
|
||||||
|
import fr.koka.sanctuary.multiblock.WrenchTextures;
|
||||||
|
import net.minecraft.server.network.*;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(PlayerChunkSender.class)
|
||||||
|
public abstract class WrenchTextureSyncMixin {
|
||||||
|
@Inject(method="sendChunk",at=@At("RETURN"))
|
||||||
|
private static void sanctuary$textures(ServerGamePacketListenerImpl connection,ServerLevel level,LevelChunk chunk,CallbackInfo ci){
|
||||||
|
WrenchTextures.sendChunk(connection.player,chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package fr.koka.sanctuary.multiblock;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
|
||||||
|
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||||
|
import net.minecraft.core.*;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.*;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.item.*;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.Property;
|
||||||
|
|
||||||
|
/** Server-owned debug-stick gestures without changing a block's type or its inventory. */
|
||||||
|
public final class GoldenWrench {
|
||||||
|
public static final String TEXTURE="sanctuary_texture";
|
||||||
|
private static final Map<ServerPlayer,Map<Block,String>> SELECTED=new WeakHashMap<>();
|
||||||
|
private static final Map<ServerPlayer,Integer> LAST_SELECTION=new WeakHashMap<>();
|
||||||
|
public static final class Tool extends Item {
|
||||||
|
public Tool(Properties properties){super(properties);}
|
||||||
|
@Override public boolean canDestroyBlock(ItemStack stack,BlockState state,Level level,BlockPos pos,LivingEntity entity){return false;}
|
||||||
|
}
|
||||||
|
public static void register(){
|
||||||
|
WrenchTextures.register();
|
||||||
|
AttackBlockCallback.EVENT.register((player,level,hand,pos,face)->{
|
||||||
|
if(!player.getItemInHand(hand).is(Multiblocks.WRENCH))return InteractionResult.PASS;
|
||||||
|
if(player.isSpectator())return InteractionResult.FAIL;
|
||||||
|
if(player instanceof ServerPlayer p){if(!allowed(p,pos,hand,face))return InteractionResult.FAIL;select(p,pos);}
|
||||||
|
return InteractionResult.SUCCESS;
|
||||||
|
});
|
||||||
|
ServerPlayConnectionEvents.DISCONNECT.register((handler,server)->{SELECTED.remove(handler.player);LAST_SELECTION.remove(handler.player);});
|
||||||
|
}
|
||||||
|
public static boolean allowed(ServerPlayer p,BlockPos pos,InteractionHand hand,Direction face){
|
||||||
|
return p.isAlive()&&!p.isSpectator()&&p.getAbilities().mayBuild&&p.level().hasChunkAt(pos)
|
||||||
|
&&p.isWithinBlockInteractionRange(pos,0)&&p.level().mayInteract(p,pos)
|
||||||
|
&&p.mayUseItemAt(pos,face,p.getItemInHand(hand))
|
||||||
|
&&!p.blockActionRestricted(p.level(),pos,p.gameMode.getGameModeForPlayer())
|
||||||
|
&&(!(p.level().getBlockEntity(pos) instanceof BaseContainerBlockEntity container)||container.canOpen(p));
|
||||||
|
}
|
||||||
|
private static boolean assembled(ServerPlayer p,BlockPos pos,BlockState state){
|
||||||
|
return state.is(Metatable.BLOCK)||state.getBlock() instanceof SuperPistonBlocks.Base
|
||||||
|
||state.getBlock() instanceof SuperPistonBlocks.Part||MultiblockData.get(p.level()).at(pos)!=null;
|
||||||
|
}
|
||||||
|
public static List<String> properties(BlockState state){
|
||||||
|
var result=new ArrayList<>(state.getProperties().stream().map(Property::getName).sorted(Comparator.comparingInt(GoldenWrench::priority).thenComparing(s->s)).toList());
|
||||||
|
result.add(TEXTURE);return List.copyOf(result);
|
||||||
|
}
|
||||||
|
private static int priority(String name){return switch(name){case "facing","axis","rotation","orientation"->0;case "half","type","shape"->1;default->2;};}
|
||||||
|
public static String selected(ServerPlayer p,BlockState state){
|
||||||
|
var choices=properties(state);var value=SELECTED.getOrDefault(p,Map.of()).get(state.getBlock());
|
||||||
|
return value!=null&&choices.contains(value)?value:choices.getFirst();
|
||||||
|
}
|
||||||
|
private static void select(ServerPlayer p,BlockPos pos){
|
||||||
|
var state=p.level().getBlockState(pos);if(state.isAir())return;
|
||||||
|
if(assembled(p,pos,state)){p.sendSystemMessage(text("assembled"),true);return;}
|
||||||
|
// Creative start/continue callbacks can describe the same click twice. Match its native 5-tick repeat cadence.
|
||||||
|
int tick=p.level().getServer().getTickCount();var last=LAST_SELECTION.get(p);
|
||||||
|
if(last!=null&&tick-last<5)return;LAST_SELECTION.put(p,tick);
|
||||||
|
var choices=properties(state);var selected=SELECTED.computeIfAbsent(p,key->new HashMap<>());
|
||||||
|
var before=selected.get(state.getBlock());int previous=before==null?-1:choices.indexOf(before);
|
||||||
|
int next=previous<0?0:Math.floorMod(previous+(p.isShiftKeyDown()?-1:1),choices.size());
|
||||||
|
var property=choices.get(next);selected.put(state.getBlock(),property);
|
||||||
|
p.sendSystemMessage(text("selected",label(property),value(p,pos,state,property)),true);
|
||||||
|
}
|
||||||
|
public static void change(ServerPlayer p,BlockPos pos){
|
||||||
|
var state=p.level().getBlockState(pos);if(state.isAir()||assembled(p,pos,state))return;
|
||||||
|
var name=selected(p,state);SELECTED.computeIfAbsent(p,key->new HashMap<>()).put(state.getBlock(),name);
|
||||||
|
if(name.equals(TEXTURE))WrenchTextures.turn(p.level(),pos,p.isShiftKeyDown()?-1:1);
|
||||||
|
else {
|
||||||
|
var property=state.getBlock().getStateDefinition().getProperty(name);
|
||||||
|
// Same flags as the vanilla debug stick: synchronize, preserve deliberately chosen shapes.
|
||||||
|
p.level().setBlock(pos,cycle(state,property,p.isShiftKeyDown()),Block.UPDATE_CLIENTS|Block.UPDATE_KNOWN_SHAPE);
|
||||||
|
}
|
||||||
|
p.sendSystemMessage(text("changed",label(name),value(p,pos,p.level().getBlockState(pos),name)),true);
|
||||||
|
}
|
||||||
|
public static <T extends Comparable<T>> BlockState cycle(BlockState state,Property<T> property,boolean reverse){
|
||||||
|
var values=new ArrayList<>(property.getPossibleValues());int index=values.indexOf(state.getValue(property));
|
||||||
|
return state.setValue(property,values.get(Math.floorMod(index+(reverse?-1:1),values.size())));
|
||||||
|
}
|
||||||
|
private static Object value(ServerPlayer p,BlockPos pos,BlockState state,String name){
|
||||||
|
return name.equals(TEXTURE)?(WrenchTextures.rotation(p.level().getChunkAt(pos),pos)*90)+"°":propertyValue(state,state.getBlock().getStateDefinition().getProperty(name));
|
||||||
|
}
|
||||||
|
private static <T extends Comparable<T>> String propertyValue(BlockState state,Property<T> property){return property.getName(state.getValue(property));}
|
||||||
|
private static Component label(String name){return name.equals(TEXTURE)?text("texture"):Component.literal(name);}
|
||||||
|
private static Component text(String name,Object...args){return Component.translatable("sanctuary.wrench."+name,args);}
|
||||||
|
private GoldenWrench(){}
|
||||||
|
}
|
||||||
@@ -80,10 +80,11 @@ public final class Metatable {
|
|||||||
if(p.isShiftKeyDown()){dissolve(level,origin);p.sendSystemMessage(Multiblocks.text("separated"),true);}else open(p,origin);
|
if(p.isShiftKeyDown()){dissolve(level,origin);p.sendSystemMessage(Multiblocks.text("separated"),true);}else open(p,origin);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if(!s.is(Blocks.CRAFTING_TABLE))return false;
|
if(!s.is(Blocks.CRAFTING_TABLE)||p.isShiftKeyDown())return false;
|
||||||
var candidates=new ArrayList<BlockPos>();
|
var candidates=new ArrayList<BlockPos>();
|
||||||
for(int z=0;z<3;z++)for(int x=0;x<3;x++){var o=pos.offset(-x,0,-z);if(state(level,o,false)==1)candidates.add(o);}
|
for(int z=0;z<3;z++)for(int x=0;x<3;x++){var o=pos.offset(-x,0,-z);if(state(level,o,false)==1)candidates.add(o);}
|
||||||
if(candidates.size()!=1){previews.remove(p);p.sendSystemMessage(Multiblocks.text(candidates.isEmpty()?"incomplete":"ambiguous"),true);return true;}
|
if(candidates.isEmpty()){previews.remove(p);return false;}
|
||||||
|
if(candidates.size()!=1){previews.remove(p);p.sendSystemMessage(Multiblocks.text("ambiguous"),true);return true;}
|
||||||
var origin=candidates.getFirst();var before=previews.remove(p);
|
var origin=candidates.getFirst();var before=previews.remove(p);
|
||||||
if(before!=null&&before.level==level&&before.origin.equals(origin)&&before.until>=level.getGameTime()){
|
if(before!=null&&before.level==level&&before.origin.equals(origin)&&before.until>=level.getGameTime()){
|
||||||
p.sendSystemMessage(Multiblocks.text(assemble(p,origin)?"assembled":"unavailable"),true);return true;
|
p.sendSystemMessage(Multiblocks.text(assemble(p,origin)?"assembled":"unavailable"),true);return true;
|
||||||
|
|||||||
@@ -17,14 +17,13 @@ import net.minecraft.world.inventory.*;
|
|||||||
import net.minecraft.world.item.*;
|
import net.minecraft.world.item.*;
|
||||||
import net.minecraft.world.level.block.*;
|
import net.minecraft.world.level.block.*;
|
||||||
import net.minecraft.world.level.block.entity.*;
|
import net.minecraft.world.level.block.entity.*;
|
||||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
|
||||||
import net.minecraft.world.phys.Vec3;
|
import net.minecraft.world.phys.Vec3;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import static fr.koka.sanctuary.multiblock.MultiblockData.Machine;
|
import static fr.koka.sanctuary.multiblock.MultiblockData.Machine;
|
||||||
|
|
||||||
public final class Multiblocks {
|
public final class Multiblocks {
|
||||||
public static final Item WRENCH=Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id("golden_wrench"),
|
public static final Item WRENCH=Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id("golden_wrench"),
|
||||||
new Item(new Item.Properties().stacksTo(1).setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_wrench")))));
|
new GoldenWrench.Tool(new Item.Properties().stacksTo(1).setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_wrench")))));
|
||||||
public static final MenuType<MultiblockMenu> MENU=Registry.register(BuiltInRegistries.MENU,SanctuaryMod.id("multiblock"),
|
public static final MenuType<MultiblockMenu> MENU=Registry.register(BuiltInRegistries.MENU,SanctuaryMod.id("multiblock"),
|
||||||
new ExtendedMenuType<>(MultiblockMenu::new,MultiblockMenu.View.CODEC));
|
new ExtendedMenuType<>(MultiblockMenu::new,MultiblockMenu.View.CODEC));
|
||||||
private record Preview(ServerLevel level,BlockPos origin,boolean furnace,long until){}
|
private record Preview(ServerLevel level,BlockPos origin,boolean furnace,long until){}
|
||||||
@@ -32,6 +31,7 @@ public final class Multiblocks {
|
|||||||
public static Component text(String key,Object...args){return Component.translatable("sanctuary.multiblock."+key,args);}
|
public static Component text(String key,Object...args){return Component.translatable("sanctuary.multiblock."+key,args);}
|
||||||
public static void register(){
|
public static void register(){
|
||||||
FourneauAppearance.register();
|
FourneauAppearance.register();
|
||||||
|
GoldenWrench.register();
|
||||||
SuperPistons.register();
|
SuperPistons.register();
|
||||||
Metatable.register();
|
Metatable.register();
|
||||||
net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.TOOLS_AND_UTILITIES).register(out->out.accept(WRENCH));
|
net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.TOOLS_AND_UTILITIES).register(out->out.accept(WRENCH));
|
||||||
@@ -44,7 +44,9 @@ public final class Multiblocks {
|
|||||||
// Preserve vanilla sneak-placement (for example, attaching a hopper to the facade).
|
// Preserve vanilla sneak-placement (for example, attaching a hopper to the facade).
|
||||||
if(!wrench&&p.isShiftKeyDown()&&(!p.getMainHandItem().isEmpty()||!p.getOffhandItem().isEmpty()))return InteractionResult.PASS;
|
if(!wrench&&p.isShiftKeyDown()&&(!p.getMainHandItem().isEmpty()||!p.getOffhandItem().isEmpty()))return InteractionResult.PASS;
|
||||||
if(!level.mayInteract(p,pos)||!p.mayUseItemAt(pos,hit.getDirection(),p.getItemInHand(hand)))return InteractionResult.FAIL;
|
if(!level.mayInteract(p,pos)||!p.mayUseItemAt(pos,hit.getDirection(),p.getItemInHand(hand)))return InteractionResult.FAIL;
|
||||||
if(wrench){if(!Metatable.wrench(p,pos)&&!SuperPistons.wrench(p,pos,hit.getDirection()))useWrench(p,pos,hit.getDirection());return InteractionResult.SUCCESS;}
|
if(wrench){if(!GoldenWrench.allowed(p,pos,hand,hit.getDirection()))return InteractionResult.FAIL;
|
||||||
|
if(!Metatable.wrench(p,pos)&&!SuperPistons.wrench(p,pos,hit.getDirection())&&!useWrench(p,pos,hit.getDirection()))GoldenWrench.change(p,pos);
|
||||||
|
return InteractionResult.SUCCESS;}
|
||||||
if(ready(level,machine)&&accessible(p,machine))open(p,machine,pos,0,"",0);
|
if(ready(level,machine)&&accessible(p,machine))open(p,machine,pos,0,"",0);
|
||||||
else p.sendSystemMessage(text("unavailable"),true);
|
else p.sendSystemMessage(text("unavailable"),true);
|
||||||
return InteractionResult.SUCCESS;
|
return InteractionResult.SUCCESS;
|
||||||
@@ -102,31 +104,28 @@ public final class Multiblocks {
|
|||||||
if(!MultiblockData.get(p.level()).add(m))return false;
|
if(!MultiblockData.get(p.level()).add(m))return false;
|
||||||
closeViewers(p.level(),m);FourneauAppearance.changed(p.level(),m,true);return true;
|
closeViewers(p.level(),m);FourneauAppearance.changed(p.level(),m,true);return true;
|
||||||
}
|
}
|
||||||
private static void useWrench(ServerPlayer p,BlockPos pos,Direction face){
|
private static boolean useWrench(ServerPlayer p,BlockPos pos,Direction face){
|
||||||
var level=p.level();var data=MultiblockData.get(level);var m=data.at(pos);
|
var level=p.level();var data=MultiblockData.get(level);var m=data.at(pos);
|
||||||
if(m!=null){
|
if(m!=null){
|
||||||
if(!ready(level,m)||!accessible(p,m)){p.sendSystemMessage(text("unavailable"),true);return;}
|
if(!ready(level,m)||!accessible(p,m)){p.sendSystemMessage(text("unavailable"),true);return true;}
|
||||||
if(p.isShiftKeyDown()){dissolve(level,m);p.sendSystemMessage(text("separated"),true);}
|
if(p.isShiftKeyDown()){dissolve(level,m);p.sendSystemMessage(text("separated"),true);}
|
||||||
else open(p,m,pos,0,"",0);
|
else open(p,m,pos,0,"",0);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
var state=level.getBlockState(pos);
|
var state=level.getBlockState(pos);
|
||||||
if(!state.is(Blocks.BARREL)&&!state.is(Blocks.FURNACE)){p.sendSystemMessage(text("hint"),true);return;}
|
if(!state.is(Blocks.BARREL)&&!state.is(Blocks.FURNACE))return false;
|
||||||
if(!(level.getBlockEntity(pos) instanceof BaseContainerBlockEntity be)||!be.canOpen(p))return;
|
if(!(level.getBlockEntity(pos) instanceof BaseContainerBlockEntity be)||!be.canOpen(p))return true;
|
||||||
if(p.isShiftKeyDown()){
|
if(p.isShiftKeyDown())return false;
|
||||||
var property=state.hasProperty(BlockStateProperties.HORIZONTAL_FACING)?BlockStateProperties.HORIZONTAL_FACING:BlockStateProperties.FACING;
|
|
||||||
var current=state.getValue(property);var next=current.getAxis().isVertical()?p.getDirection():current.getClockWise();
|
|
||||||
level.setBlockAndUpdate(pos,state.setValue(property,next));p.sendSystemMessage(text("rotated"),true);return;
|
|
||||||
}
|
|
||||||
boolean furnace=state.is(Blocks.FURNACE);var candidates=new ArrayList<Machine>();
|
boolean furnace=state.is(Blocks.FURNACE);var candidates=new ArrayList<Machine>();
|
||||||
for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++){
|
for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++){
|
||||||
var candidate=new Machine(UUID.randomUUID(),pos.offset(-x,-y,-z),furnace,p.getDirection().getOpposite());
|
var candidate=new Machine(UUID.randomUUID(),pos.offset(-x,-y,-z),furnace,p.getDirection().getOpposite());
|
||||||
if(state(level,candidate)==1&&candidate.members().stream().noneMatch(q->data.at(q)!=null))candidates.add(candidate);
|
if(state(level,candidate)==1&&candidate.members().stream().noneMatch(q->data.at(q)!=null))candidates.add(candidate);
|
||||||
}
|
}
|
||||||
if(candidates.size()!=1){PREVIEWS.remove(p);p.sendSystemMessage(text(candidates.isEmpty()?"incomplete":"ambiguous"),true);return;}
|
if(candidates.isEmpty()){PREVIEWS.remove(p);return false;}
|
||||||
|
if(candidates.size()!=1){PREVIEWS.remove(p);p.sendSystemMessage(text("ambiguous"),true);return true;}
|
||||||
var candidate=candidates.getFirst();var previous=PREVIEWS.remove(p);
|
var candidate=candidates.getFirst();var previous=PREVIEWS.remove(p);
|
||||||
if(previous!=null&&previous.level()==level&&previous.origin().equals(candidate.origin())&&previous.furnace()==furnace&&previous.until()>=level.getGameTime()){
|
if(previous!=null&&previous.level()==level&&previous.origin().equals(candidate.origin())&&previous.furnace()==furnace&&previous.until()>=level.getGameTime()){
|
||||||
p.sendSystemMessage(text(assemble(p,candidate)?"assembled":"unavailable"),true);return;
|
p.sendSystemMessage(text(assemble(p,candidate)?"assembled":"unavailable"),true);return true;
|
||||||
}
|
}
|
||||||
PREVIEWS.put(p,new Preview(level,candidate.origin(),furnace,level.getGameTime()+200));
|
PREVIEWS.put(p,new Preview(level,candidate.origin(),furnace,level.getGameTime()+200));
|
||||||
for(int axis=0;axis<3;axis++)for(int a=0;a<=3;a+=3)for(int b=0;b<=3;b+=3)for(int t=0;t<=6;t++){
|
for(int axis=0;axis<3;axis++)for(int a=0;a<=3;a+=3)for(int b=0;b<=3;b+=3)for(int t=0;t<=6;t++){
|
||||||
@@ -134,6 +133,7 @@ public final class Multiblocks {
|
|||||||
level.sendParticles(p,ParticleTypes.END_ROD,false,false,candidate.origin().getX()+px,candidate.origin().getY()+py,candidate.origin().getZ()+pz,1,0,0,0,0);
|
level.sendParticles(p,ParticleTypes.END_ROD,false,false,candidate.origin().getX()+px,candidate.origin().getY()+py,candidate.origin().getZ()+pz,1,0,0,0,0);
|
||||||
}
|
}
|
||||||
p.sendSystemMessage(text("confirm",text(furnace?"furnace":"barrel")),false);
|
p.sendSystemMessage(text("confirm",text(furnace?"furnace":"barrel")),false);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
public static void open(ServerPlayer p,Machine m,BlockPos anchor,int page,String query,int section){
|
public static void open(ServerPlayer p,Machine m,BlockPos anchor,int page,String query,int section){
|
||||||
open(p,m,anchor,page,query,section,new int[0]);
|
open(p,m,anchor,page,query,section,new int[0]);
|
||||||
|
|||||||
@@ -79,11 +79,10 @@ public final class SuperPistons {
|
|||||||
}
|
}
|
||||||
if(!s.is(Blocks.PISTON)&&!s.is(Blocks.STICKY_PISTON))return false;
|
if(!s.is(Blocks.PISTON)&&!s.is(Blocks.STICKY_PISTON))return false;
|
||||||
var f=s.getValue(FACING);boolean sticky=s.is(Blocks.STICKY_PISTON);
|
var f=s.getValue(FACING);boolean sticky=s.is(Blocks.STICKY_PISTON);
|
||||||
if(p.isShiftKeyDown()){
|
if(p.isShiftKeyDown())return false;
|
||||||
if(!s.getValue(EXTENDED)&&!l.hasNeighborSignal(clicked))l.setBlockAndUpdate(clicked,s.setValue(FACING,face));return true;
|
|
||||||
}
|
|
||||||
var found=new ArrayList<BlockPos>();for(int a=-1;a<=1;a++)for(int b=-1;b<=1;b++){var c=clicked.relative(u(f),a).relative(v(f),b);if(candidates(l,c,f,sticky))found.add(c);}
|
var found=new ArrayList<BlockPos>();for(int a=-1;a<=1;a++)for(int b=-1;b<=1;b++){var c=clicked.relative(u(f),a).relative(v(f),b);if(candidates(l,c,f,sticky))found.add(c);}
|
||||||
if(found.size()!=1){previews.remove(p);message(p,found.isEmpty()?"incomplete":"ambiguous");return true;}
|
if(found.isEmpty()){previews.remove(p);return false;}
|
||||||
|
if(found.size()!=1){previews.remove(p);message(p,"ambiguous");return true;}
|
||||||
var c=found.getFirst();for(var pos:tiles(c,f,0))if(!l.mayInteract(p,pos)||!p.mayUseItemAt(pos,face,p.getMainHandItem())){previews.remove(p);return true;}
|
var c=found.getFirst();for(var pos:tiles(c,f,0))if(!l.mayInteract(p,pos)||!p.mayUseItemAt(pos,face,p.getMainHandItem())){previews.remove(p);return true;}
|
||||||
var old=previews.remove(p);
|
var old=previews.remove(p);
|
||||||
if(old!=null&&old.level()==l&&old.center().equals(c)&&old.facing()==f&&old.sticky()==sticky&&old.until()>=l.getGameTime()){
|
if(old!=null&&old.level()==l&&old.center().equals(c)&&old.facing()==f&&old.sticky()==sticky&&old.until()>=l.getGameTime()){
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package fr.koka.sanctuary.multiblock;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import fr.koka.sanctuary.SanctuaryMod;
|
||||||
|
import java.util.*;
|
||||||
|
import net.fabricmc.fabric.api.attachment.v1.*;
|
||||||
|
import net.fabricmc.fabric.api.networking.v1.*;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.level.*;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
|
||||||
|
/** Optional per-chunk presentation data. Absent means original vanilla textures. */
|
||||||
|
public final class WrenchTextures {
|
||||||
|
public static final AttachmentType<Map<String,Integer>> ROTATIONS=AttachmentRegistry.createPersistent(
|
||||||
|
SanctuaryMod.id("wrench_textures_v1"),Codec.unboundedMap(Codec.STRING,Codec.intRange(1,3)).xmap(Map::copyOf,map->map));
|
||||||
|
public record Entry(BlockPos pos,int rotation){}
|
||||||
|
public record Update(Identifier dimension,ChunkPos chunk,boolean clear,List<Entry> entries) implements CustomPacketPayload {
|
||||||
|
public Update{entries=List.copyOf(entries);if(entries.size()>1024)throw new IllegalArgumentException("Texture batch too large");}
|
||||||
|
public static final Type<Update> TYPE=new Type<>(SanctuaryMod.id("wrench_textures_v1"));
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf,Update> CODEC=StreamCodec.of((b,p)->{
|
||||||
|
b.writeIdentifier(p.dimension);b.writeChunkPos(p.chunk);b.writeBoolean(p.clear);b.writeVarInt(p.entries.size());
|
||||||
|
for(var entry:p.entries){b.writeBlockPos(entry.pos);b.writeByte(entry.rotation);}
|
||||||
|
},b->{
|
||||||
|
var dimension=b.readIdentifier();var chunk=b.readChunkPos();boolean clear=b.readBoolean();int n=b.readVarInt();
|
||||||
|
if(n<0||n>1024)throw new IllegalArgumentException("Texture batch size");var entries=new ArrayList<Entry>(n);
|
||||||
|
for(int i=0;i<n;i++){var pos=b.readBlockPos();int rotation=b.readUnsignedByte();
|
||||||
|
if(rotation>3||ChunkPos.pack(pos)!=chunk.pack())throw new IllegalArgumentException("Invalid texture entry");entries.add(new Entry(pos,rotation));}
|
||||||
|
return new Update(dimension,chunk,clear,entries);
|
||||||
|
});
|
||||||
|
@Override public Type<? extends CustomPacketPayload> type(){return TYPE;}
|
||||||
|
}
|
||||||
|
public static void register(){PayloadTypeRegistry.clientboundPlay().register(Update.TYPE,Update.CODEC);}
|
||||||
|
public static int rotation(LevelChunk chunk,BlockPos pos){var map=chunk.getAttached(ROTATIONS);return map==null?0:map.getOrDefault(Long.toString(pos.asLong()),0);}
|
||||||
|
public static void turn(ServerLevel level,BlockPos pos,int delta){
|
||||||
|
var chunk=level.getChunkAt(pos);set(chunk,pos,Math.floorMod(rotation(chunk,pos)+delta,4));
|
||||||
|
}
|
||||||
|
public static void forget(LevelChunk chunk,BlockPos pos){if(rotation(chunk,pos)!=0)set(chunk,pos,0);}
|
||||||
|
private static void set(LevelChunk chunk,BlockPos pos,int value){
|
||||||
|
if(!(chunk.getLevel() instanceof ServerLevel level))return;
|
||||||
|
var old=chunk.getAttached(ROTATIONS);var next=old==null?new HashMap<String,Integer>():new HashMap<>(old);
|
||||||
|
if(value==0)next.remove(Long.toString(pos.asLong()));else next.put(Long.toString(pos.asLong()),value);
|
||||||
|
if(next.isEmpty())chunk.removeAttached(ROTATIONS);else chunk.setAttached(ROTATIONS,Map.copyOf(next));chunk.markUnsaved();
|
||||||
|
var packet=new Update(level.dimension().identifier(),chunk.getPos(),false,List.of(new Entry(pos.immutable(),value)));
|
||||||
|
for(var player:PlayerLookup.tracking(level,chunk.getPos()))send(player,packet);
|
||||||
|
}
|
||||||
|
public static void sendChunk(ServerPlayer player,LevelChunk chunk){
|
||||||
|
var entries=chunk.getAttached(ROTATIONS);var batch=new ArrayList<Entry>(1024);boolean clear=true;
|
||||||
|
if(entries!=null)for(var entry:entries.entrySet()){
|
||||||
|
BlockPos pos;try{pos=BlockPos.of(Long.parseLong(entry.getKey()));}catch(NumberFormatException ignored){continue;}
|
||||||
|
if(ChunkPos.pack(pos)!=chunk.getPos().pack()||chunk.getBlockState(pos).isAir())continue;
|
||||||
|
batch.add(new Entry(pos,entry.getValue()));
|
||||||
|
if(batch.size()==1024){send(player,new Update(player.level().dimension().identifier(),chunk.getPos(),clear,batch));clear=false;batch.clear();}
|
||||||
|
}
|
||||||
|
if(clear||!batch.isEmpty())send(player,new Update(player.level().dimension().identifier(),chunk.getPos(),clear,batch));
|
||||||
|
}
|
||||||
|
private static void send(ServerPlayer p,Update packet){if(ServerPlayNetworking.canSend(p,Update.TYPE))ServerPlayNetworking.send(p,packet);}
|
||||||
|
private WrenchTextures(){}
|
||||||
|
}
|
||||||
@@ -1979,7 +1979,7 @@
|
|||||||
"sanctuary.multiblock.outputs": "Results",
|
"sanctuary.multiblock.outputs": "Results",
|
||||||
"sanctuary.multiblock.heat": "Reserve: %s s",
|
"sanctuary.multiblock.heat": "Reserve: %s s",
|
||||||
"sanctuary.multiblock.progress": "Cooking: %s %%",
|
"sanctuary.multiblock.progress": "Cooking: %s %%",
|
||||||
"sanctuary.multiblock.hint": "Right click: assemble / open. Sneak: rotate / separate.",
|
"sanctuary.multiblock.hint": "Left click: select a property. Right click: change its value. Sneak: reverse. Complete multiblock: assemble / open; sneak: separate.",
|
||||||
"sanctuary.multiblock.unavailable": "Machine inaccessible, incomplete or partly unloaded.",
|
"sanctuary.multiblock.unavailable": "Machine inaccessible, incomplete or partly unloaded.",
|
||||||
"sanctuary.multiblock.separated": "Machine separated. Components retain their contents.",
|
"sanctuary.multiblock.separated": "Machine separated. Components retain their contents.",
|
||||||
"sanctuary.multiblock.rotated": "Component rotated.",
|
"sanctuary.multiblock.rotated": "Component rotated.",
|
||||||
@@ -2221,5 +2221,9 @@
|
|||||||
"block.sanctuary.brown_brick_wall": "Brown Brick Wall",
|
"block.sanctuary.brown_brick_wall": "Brown Brick Wall",
|
||||||
"block.sanctuary.green_brick_wall": "Green Brick Wall",
|
"block.sanctuary.green_brick_wall": "Green Brick Wall",
|
||||||
"block.sanctuary.red_brick_wall": "Red Brick Wall",
|
"block.sanctuary.red_brick_wall": "Red Brick Wall",
|
||||||
"block.sanctuary.black_brick_wall": "Black Brick Wall"
|
"block.sanctuary.black_brick_wall": "Black Brick Wall",
|
||||||
|
"sanctuary.wrench.selected": "Selected property: %s = %s · right click to change",
|
||||||
|
"sanctuary.wrench.changed": "%s = %s",
|
||||||
|
"sanctuary.wrench.texture": "Texture rotation",
|
||||||
|
"sanctuary.wrench.assembled": "Assembled multiblock: right click to open, sneak-right click to separate before editing its components."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1979,7 +1979,7 @@
|
|||||||
"sanctuary.multiblock.outputs": "Résultats",
|
"sanctuary.multiblock.outputs": "Résultats",
|
||||||
"sanctuary.multiblock.heat": "Réserve : %s s",
|
"sanctuary.multiblock.heat": "Réserve : %s s",
|
||||||
"sanctuary.multiblock.progress": "Cuisson : %s %%",
|
"sanctuary.multiblock.progress": "Cuisson : %s %%",
|
||||||
"sanctuary.multiblock.hint": "Clic droit : assembler / ouvrir. Maj : tourner / dissocier.",
|
"sanctuary.multiblock.hint": "Clic gauche : choisir un état. Clic droit : changer sa valeur. Maj : sens inverse. Multibloc complet : assembler / ouvrir ; Maj : dissocier.",
|
||||||
"sanctuary.multiblock.unavailable": "Machine inaccessible, incomplète ou partiellement déchargée.",
|
"sanctuary.multiblock.unavailable": "Machine inaccessible, incomplète ou partiellement déchargée.",
|
||||||
"sanctuary.multiblock.separated": "Machine dissociée. Les composants conservent leur contenu.",
|
"sanctuary.multiblock.separated": "Machine dissociée. Les composants conservent leur contenu.",
|
||||||
"sanctuary.multiblock.rotated": "Composant tourné.",
|
"sanctuary.multiblock.rotated": "Composant tourné.",
|
||||||
@@ -2221,5 +2221,9 @@
|
|||||||
"block.sanctuary.brown_brick_wall": "Muret de briques marron",
|
"block.sanctuary.brown_brick_wall": "Muret de briques marron",
|
||||||
"block.sanctuary.green_brick_wall": "Muret de briques vertes",
|
"block.sanctuary.green_brick_wall": "Muret de briques vertes",
|
||||||
"block.sanctuary.red_brick_wall": "Muret de briques rouges",
|
"block.sanctuary.red_brick_wall": "Muret de briques rouges",
|
||||||
"block.sanctuary.black_brick_wall": "Muret de briques noires"
|
"block.sanctuary.black_brick_wall": "Muret de briques noires",
|
||||||
|
"sanctuary.wrench.selected": "État choisi : %s = %s · clic droit pour modifier",
|
||||||
|
"sanctuary.wrench.changed": "%s = %s",
|
||||||
|
"sanctuary.wrench.texture": "Rotation des textures",
|
||||||
|
"sanctuary.wrench.assembled": "Multibloc assemblé : clic droit pour ouvrir, Maj-clic droit pour dissocier avant de modifier ses composants."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
"package": "fr.koka.sanctuary.mixin",
|
"package": "fr.koka.sanctuary.mixin",
|
||||||
"compatibilityLevel": "JAVA_25",
|
"compatibilityLevel": "JAVA_25",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
|
"WrenchTextureEditMixin",
|
||||||
|
"WrenchTextureSyncMixin",
|
||||||
"SuperPistonPickMixin",
|
"SuperPistonPickMixin",
|
||||||
"SuperPistonPickDataMixin",
|
"SuperPistonPickDataMixin",
|
||||||
"BoatPassengerRotationMixin",
|
"BoatPassengerRotationMixin",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "Sanctuary"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "beta.119"
|
version = "beta.120"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
Reference in New Issue
Block a user