Compare commits

...
Author SHA1 Message Date
koka 309d0ba6f3 Add permanent Smithing aptitude for anvils and smithing tables in beta.238
Build Sanctuary / build (push) Waiting to run
2026-10-06 16:12:32 +02:00
22 changed files with 299 additions and 21 deletions
+11
View File
@@ -6,6 +6,17 @@ Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
correspondantes. Le module Sanctuary Test n’est pas requis.
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
## beta.238 — Forgerie
[Aptitude Forgerie](docs/forgerie-beta238.md) dans Arts et métiers : quatre
niveaux pour débloquer les enclumes et la table de forge, achat définitif.
Les réparations, noms, améliorations en netherite et ornements conservent
leurs règles natives. L'achat est contrôlé côté serveur, y compris pour les
postes portés sur la tête. Icône fournie et libellés FR/EN intégrés.
Build, 21 GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
[MRpack local beta.238](build/Sanctuary-beta.238.mrpack) vérifié, sans
publication ni mise à jour personnelle.
## beta.237 — Aptitudes acquises définitivement
[Aptitudes par thème et achats permanents](docs/aptitudes-beta237.md) : seuls
+8
View File
@@ -1,5 +1,13 @@
# Backlog Sanctuary
## APT-238 — Forgerie — livré localement
Branche `codex/forgerie-beta238`, base beta.237. Aptitude permanente des Arts
et métiers ; quatre niveaux pour les trois variantes d'enclume et la table
de forge, y compris les postes portés. [Contrat et persistance](forgerie-beta238.md).
Réparations, noms, améliorations et ornements gardent leurs règles natives.
Build, 21 GameTests requis, Vulkan FR/EN GUI 2/3/4 et MRpack vérifiés.
## APT-237 — achats permanents et intégration Pause — livré localement
Branche `codex/progression-aptitudes`, travaux beta.235 et beta.236 réunis.
+95
View File
@@ -0,0 +1,95 @@
# APT-238 — Forgerie
Branche `codex/forgerie-beta238`, base beta.237 (`5dee981`).
## Contrat avant implémentation
Une aptitude Forgerie est ajoutée dans Arts et métiers de Progression, avec
l'icône `aptitudes_forgerie.png` fournie. Comme Enchantement et Alchimie,
elle coûte quatre niveaux, est achetée une seule fois et reste acquise
sans interrupteur. Elle débloque l'usage personnel de toutes les variantes
d'enclume et de la table de forge. Avant l'achat, les menus natifs de ces
postes sont refusés côté serveur, y compris les postes fonctionnels portés
sur la tête. Les réparations, noms, améliorations et ornements natifs restent
fonctionnels après achat. Équipements déjà obtenus et recettes conservés.
La meule, les fours et les autres postes restent sous leurs règles actuelles.
Les deux aides World Map / Mob Names restent les seuls choix désactivables.
Les interfaces ajoutées sont traduites en français et en anglais.
## Persistance et compatibilité
L'attachement booléen additif `sanctuary:smithing_learned` conserve l'achat.
Absent : false. Copie à la mort et conservation au New Game+. Aucun schéma,
aucune preuve ni aucun attachement existant n'est réécrit. Les personnages
existants avec progression doivent aussi acquérir Forgerie. Les joueurs
hors progression Sanctuary gardent l'accès vanilla. Aucun monde personnel,
aucune génération ni aucun terrain n'est modifié.
La capacité `sanctuary:smithing_v1` et l'état `sanctuary:smithing_state_v1`
transmettent l'achat sans modifier
les paquets v1 existants. Le serveur exige un client compatible et contrôle
la session, le joueur, la révision et les niveaux avant l'achat. Un retour
à une version ancienne peut perdre cet attachement inconnu à la sauvegarde ;
conserver une sauvegarde avant ce retour.
## Vérifications et livraison
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=forgerie238,aptitudes236,headblocks \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
```
`check build assemblePack` réussi en **2 min 44 s**, **21 GameTests requis**
réussis : cinq nouveaux scénarios Forgerie, six scénarios d'aptitudes et dix
scénarios de postes portés. Contrôles purs du dépôt réussis. Le banc Atlas
évite le chemin de génération par défaut déjà documenté en beta.232.
Log `build/forgerie238-check-build-pack.log`.
- Achat refusé avec trois niveaux ou une révision périmée ; quatre niveaux
dépensés une seule fois. Autres achats et preuves inchangés. Requête de
désactivation refusée ; conservation native NBT, à la mort et au New Game+.
- Trois variantes d'enclume refusées avant achat puis ouvertes après achat.
**Réparation et renommage réels d'une épée diamant** sur chacune ; XP et
diamant consommés selon le coût natif du menu.
- Table de forge refusée avant achat puis ouverte : **épée netherite et
plastron diamant avec ornement Sentry réellement produits**, entrées
consommées une fois, coût d'XP natif conservé et accès aux visites suivantes.
- Menus construits en dehors de l'ouverture normale : validité et prise de
résultat refusées avant acquisition, sans dépense ni perte d'entrées.
Joueurs sans progression Sanctuary autorisés comme en vanilla.
- Enclumes et table de forge sur la tête : contrôle de l'achat du visiteur,
indépendamment du porteur. La fixture générale des postes natifs possède
explicitement les trois aptitudes des Arts avant de tester leurs menus.
Sources et bytecode du **Minecraft 26.3 local** vérifiés pour les méthodes
héritées de `ItemCombinerMenu`, l'override de prise d'`AnvilMenu` et les recettes
natives de forge. Aucun changement de recette ni de dépendance externe.
Parcours graphique en **34 s**, Vulkan / MoltenVK 1.4.2 sur Apple M1,
FR/EN GUI 2/3/4. La suite d'aptitudes existante contrôle désormais treize
aptitudes, dont onze achats permanents. Trois Arts achetés depuis les boutons
natifs : douze niveaux au total, Forgerie synchronisée depuis le serveur et
présentée comme acquise sans interrupteur. Deux seules aides désactivables,
XP et progression conservées lors de douze paires de changement, disposition
Pause intégrée, défilement et retour Terminé conservés. Log
`build/forgerie238-client-vulkan.log`, marqueur `FORGERIE238_PASS`.
Vingt-quatre captures dans `build/forgerie238-screenshots/` ; Arts FR GUI 3
et EN GUI 4 inspectés, texte et icône contenus dans le cadre.
Versions du mod, du pack et du manifeste alignées sur **beta.238**.
[MRpack local](../build/Sanctuary-beta.238.mrpack) vérifié : 12802487 octets,
intégrité ZIP, Minecraft 26.3 / Fabric Loader 0.19.5, un seul JAR Sanctuary
identique au build final, sans classe de test, monde ou module de laboratoire.
Les trois icônes fournies des Arts sont identiques octet pour octet aux
originaux dans le JAR. Reçu `build/forgerie238-artifact.json`.
- SHA-256 JAR : `e29ea6f7f56c57290d0307076f641f2e714ec72a80be858a5fd9c5f08e27eb8e`.
- SHA-256 MRpack : `2fbd605acf555dd8b192c0c0ad7171414a13f5820e5d440ba54debcd4cb897a7`.
Tests dans des mondes de développement neufs. Pas de validation Windows ni
multijoueur distant. Les sauvegardes et installations personnelles restent
intactes ; aucune publication ni avancée du canal packwiz.
+2 -2
View File
@@ -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.237
pack_version=beta.237
mod_version=beta.238
pack_version=beta.238
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+1 -1
View File
@@ -228,7 +228,7 @@ tasks.named('processGametestResources') {
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
def focused = providers.gradleProperty('sanctuaryFocusedTests').getOrElse('').split(',').findAll { !it.isEmpty() }
def focusClasses = [aptitudes236: 'Aptitudes236GameTests', solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
def focusClasses = [forgerie238: 'Forgerie238GameTests', aptitudes236: 'Aptitudes236GameTests', solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
focused.each { if (!focusClasses.containsKey(it)) throw new GradleException('Unknown focused native test: ' + it) }
inputs.property('focusedNative', focused)
def yellowRock222Client = providers.gradleProperty('sanctuaryYellowRock222ClientTests').map { it.toBoolean() }.getOrElse(false)
@@ -25,7 +25,7 @@ public final class Aptitudes236ClientChecks implements FabricClientGameTest {
Map.entry("sorting","sanctuary.progression.sorting"),Map.entry("swimming","sanctuary.progression.swimming"),
Map.entry("resting","sanctuary.progression.resting"),Map.entry("active_bond","sanctuary.companion.link"),
Map.entry("familiar_study","sanctuary.companion.study"),Map.entry("catalogue","sanctuary.recipes.catalogue"),
Map.entry("zoom","sanctuary.zoom.title"),Map.entry("enchanting","sanctuary.progression.enchanting"),Map.entry("alchemy","sanctuary.progression.alchemy"));
Map.entry("zoom","sanctuary.zoom.title"),Map.entry("enchanting","sanctuary.progression.enchanting"),Map.entry("alchemy","sanctuary.progression.alchemy"),Map.entry("smithing","sanctuary.progression.smithing"));
@Override public void runTest(ClientGameTestContext c) {
var intro=new AtomicBoolean(true);fr.koka.sanctuary.testing.QuickTestWorld.enable();
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
@@ -34,17 +34,17 @@ public final class Aptitudes236ClientChecks implements FabricClientGameTest {
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
press(screen,"sanctuary.progression.enter");intro.set(false);
}});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Aptitudes237 "+UUID.randomUUID());s.setSeed("237");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Forgerie238 "+UUID.randomUUID());s.setSeed("238");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft: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.setExperienceLevels(100);ProgressionService.send(p,"");});
c.waitFor(m->m.player.experienceLevel==100,200);
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));check(!CraftAptitudes.learned(m.player,"enchanting") && !CraftAptitudes.learned(m.player,"alchemy"),"New workstations start locked");});
for(String craft:List.of("enchanting","alchemy")) {
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));check(!CraftAptitudes.learned(m.player,"enchanting") && !CraftAptitudes.learned(m.player,"alchemy") && !CraftAptitudes.learned(m.player,"smithing"),"New workstations start locked");});
for(String craft:List.of("enchanting","alchemy","smithing")) {
c.runOnClient(m->{var button=afterTitle(m.gui.screen(),TITLES.get(craft));check(key(button).equals("sanctuary.progression.unlock_cost")&&button.active,"Locked craft offers its normal purchase button");button.onPress(enter());});
c.waitFor(m->CraftAptitudes.learned(m.player,craft) && key(afterTitle(m.gui.screen(),TITLES.get(craft))).equals("sanctuary.progression.aptitude_acquired"),200);
}
c.runOnClient(m->check(m.player.experienceLevel==92,"Two UI purchases cost eight levels"));
c.runOnClient(m->check(m.player.experienceLevel==88,"Three UI purchases cost twelve levels"));
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(100);
for(String target:ProgressRecord.APTITUDES)check(ProgressionService.purchase(p,target,ProgressEffects.get(p).revision()),"Server acquires "+target);
check(CompanionService.buy(p),"Active Bond bought");check(CompanionStudy.buy(p),"Familiar Study bought");
@@ -81,14 +81,14 @@ public final class Aptitudes236ClientChecks implements FabricClientGameTest {
c.waitFor(m->AptitudeClient.enabled(target) && key(afterTitle(m.gui.screen(),TITLES.get(target))).equals("sanctuary.progression.aptitude_enabled"),200);
}
c.runOnClient(m->{m.gui.setScreen(new PauseScreen(true));pauseLayout(m.gui.screen(),true);});shot(c,"pause-map-enabled-"+language+"-"+scale);
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));reveal(m.gui.screen(),afterTitle(m.gui.screen(),TITLES.get("alchemy")));});
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));reveal(m.gui.screen(),afterTitle(m.gui.screen(),TITLES.get("smithing")));});
shot(c,"crafts-"+language+"-"+scale);
c.runOnClient(m->{var screen=m.gui.screen();var area=aptitudes(screen);area.setScrollAmount(100);});
c.runOnClient(m->{m.getWindow().setWindowed(1344,1008);m.resizeGui();});c.waitTicks(3);
c.runOnClient(m->{check(aptitudes(m.gui.screen()).scrollAmount()==100,"Resize retains aptitude scroll");layout(m.gui.screen());press(m.gui.screen(),"gui.done");check(m.gui.screen() instanceof PauseScreen,"Done returns to Pause");m.gui.setScreen(null);});
}
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.experienceLevel==xp && p.getAttached(ProgressionService.RECORD).equals(purchaseProof),"Twelve disable/reactivate pairs preserve XP and purchases");});
System.out.println("APTITUDES237_PASS: Vulkan FR/EN GUI 2/3/4, five themes, two real server toggles, ten permanent purchases, legacy preferences and merged Pause layout, two native UI purchases, preserved XP/purchases, scroll/resize and Done");
System.out.println("FORGERIE238_PASS: Vulkan FR/EN GUI 2/3/4, five themes, two real server toggles, eleven permanent purchases, legacy preferences and merged Pause layout, three native UI purchases, preserved XP/purchases, scroll/resize and Done");
}finally{intro.set(false);}
}
private static void pauseLayout(Screen screen,boolean enabled) {Pause233ClientChecks.pause(screen,enabled);}
@@ -101,5 +101,5 @@ public final class Aptitudes236ClientChecks implements FabricClientGameTest {
private static AbstractScrollArea aptitudes(Screen screen){return widgets(screen).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).max(Comparator.comparingInt(AbstractWidget::getX)).orElseThrow();}
private static void reveal(Screen screen,AbstractWidget widget){var area=aptitudes(screen);area.setScrollAmount(Math.clamp(area.scrollAmount()+widget.getBottom()-area.getBottom()+4,0,area.maxScrollAmount()));}
private static void layout(Screen screen){var area=aptitudes(screen);check(area.getX()>=0&&area.getRight()<=screen.width&&area.getBottom()<screen.height-26,"Aptitudes viewport stays inside native frame");for(var button:widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).toList())check(button.getX()>=0&&button.getRight()<=screen.width,"Buttons remain within the screen horizontally");}
private static void shot(ClientGameTestContext c,String name){var source=c.takeScreenshot("aptitudes237-"+name);try{var dir=FabricLoader.getInstance().getGameDir().resolve("../../../../../build/aptitudes237-screenshots").normalize();Files.createDirectories(dir);Files.copy(source,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);}catch(Exception e){throw new AssertionError(e);}}
private static void shot(ClientGameTestContext c,String name){var source=c.takeScreenshot("forgerie238-"+name);try{var dir=FabricLoader.getInstance().getGameDir().resolve("../../../../../build/forgerie238-screenshots").normalize();Files.createDirectories(dir);Files.copy(source,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);}catch(Exception e){throw new AssertionError(e);}}
}
@@ -0,0 +1,110 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.inventory.*;
import fr.koka.sanctuary.cosmetics.HeadBlockService;
import java.util.UUID;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.storage.*;
import net.minecraft.util.ProblemReporter;
/** Purchase persistence and real native repairs/upgrades, including worn workstations. */
public final class Forgerie238GameTests {
@GameTest public void purchaseIsPermanentAndSurvivesSaveDeathAndCycle(GameTestHelper h) {
var r=ProgressRecord.INITIAL;
for(String skill:ProgressRecord.SKILLS)while(r.rank(skill)<ProgressRecord.cap(skill))r=r.buy(skill,r.cost(skill,ProgressionConfig.DEFAULT));
var p=player(h,r);String proof=p.getAttached(ProgressionService.RECORD);
h.assertTrue(!p.hasAttached(CraftAptitudes.SMITHING) && !CraftAptitudes.learned(p,"smithing"),"Existing players start without the new purchase");
p.setExperienceLevels(3);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==3,"Three levels cannot unlock Forgerie");
p.setExperienceLevels(4);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()-1) && p.experienceLevel==4,"Stale purchase cannot spend XP");
h.assertTrue(CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==0,"Four levels acquire Forgerie once");
p.setExperienceLevels(8);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==8,"Repeat purchase costs nothing");
h.assertTrue(!AptitudeService.set(p,new AptitudeService.Set("smithing",false,0)) && CraftAptitudes.allowed(p,"smithing"),"Bought Forgerie has no disable request");
h.assertTrue(!CraftAptitudes.learned(p,"enchanting") && !CraftAptitudes.learned(p,"alchemy") && proof.equals(p.getAttached(ProgressionService.RECORD)),"Other purchases and progression proofs stay independent");
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
h.assertTrue(CraftAptitudes.learned(loaded,"smithing") && loaded.experienceLevel==8 && proof.equals(loaded.getAttached(ProgressionService.RECORD)),"Native NBT reload preserves the purchase and XP");
var respawn=Progression003GameTests.player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(p,false);
ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(p,respawn,false);
h.assertTrue(CraftAptitudes.learned(respawn,"smithing"),"Forgerie survives ordinary death");
bind(p,r.nextCycle());h.assertTrue(CraftAptitudes.learned(p,"smithing") && CraftAptitudes.allowed(p,"smithing"),"Forgerie survives New Game+");h.succeed();
}
@GameTest public void allAnvilsRequirePurchaseThenRepairAndRename(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at.below(),Blocks.STONE.defaultBlockState());
for(var block:new net.minecraft.world.level.block.Block[]{Blocks.ANVIL,Blocks.CHIPPED_ANVIL,Blocks.DAMAGED_ANVIL}) {
h.getLevel().setBlockAndUpdate(at,block.defaultBlockState());
h.assertTrue(p.openMenu(h.getLevel().getBlockState(at).getMenuProvider(h.getLevel(),at)).isEmpty(),"Every anvil variant is locked before purchase");
}
p.setExperienceLevels(40);h.assertTrue(CraftAptitudes.buy(p,"smithing",0) && p.experienceLevel==36,"Anvil unlock charges four levels");
for(var block:new net.minecraft.world.level.block.Block[]{Blocks.ANVIL,Blocks.CHIPPED_ANVIL,Blocks.DAMAGED_ANVIL}) {
h.getLevel().setBlockAndUpdate(at,block.defaultBlockState());
h.assertTrue(p.openMenu(h.getLevel().getBlockState(at).getMenuProvider(h.getLevel(),at)).isPresent(),"Every acquired anvil variant opens");
var menu=(AnvilMenu)p.containerMenu;var sword=new ItemStack(Items.DIAMOND_SWORD);sword.setDamageValue(100);
menu.getSlot(AnvilMenu.INPUT_SLOT).set(sword);menu.getSlot(AnvilMenu.ADDITIONAL_SLOT).set(new ItemStack(Items.DIAMOND));
h.assertTrue(menu.setItemName("Forgerie238") && menu.stillValid(p),"Native anvil accepts a repair and name");
int cost=menu.getCost(),xp=p.experienceLevel;
var result=menu.getSlot(AnvilMenu.RESULT_SLOT).safeTake(1,1,p);
h.assertTrue(result.is(Items.DIAMOND_SWORD) && result.getDamageValue()==0 && result.get(DataComponents.CUSTOM_NAME).getString().equals("Forgerie238"),"Native result is repaired and renamed");
h.assertTrue(cost>0 && p.experienceLevel==xp-cost && menu.getSlot(AnvilMenu.ADDITIONAL_SLOT).getItem().isEmpty(),"Native operation consumes its normal XP and repair material");p.closeContainer();
}
h.succeed();
}
@GameTest public void smithingTableUpgradesAndTrimsAfterPurchase(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at,Blocks.SMITHING_TABLE.defaultBlockState());
var provider=h.getLevel().getBlockState(at).getMenuProvider(h.getLevel(),at);
h.assertTrue(p.openMenu(provider).isEmpty(),"Smithing table is locked before purchase");
p.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(p,"smithing",0) && p.openMenu(provider).isPresent(),"Same purchase opens smithing table");
var menu=(SmithingMenu)p.containerMenu;
inputs(menu,Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE,Items.DIAMOND_SWORD,Items.NETHERITE_INGOT);
var upgrade=menu.getSlot(SmithingMenu.RESULT_SLOT).safeTake(1,1,p);
h.assertTrue(upgrade.is(Items.NETHERITE_SWORD) && p.experienceLevel==0,"Real netherite upgrade works without extra XP");
h.assertTrue(menu.getSlot(0).getItem().isEmpty() && menu.getSlot(1).getItem().isEmpty() && menu.getSlot(2).getItem().isEmpty(),"Upgrade consumes each native input once");
inputs(menu,Items.SENTRY_ARMOR_TRIM_SMITHING_TEMPLATE,Items.DIAMOND_CHESTPLATE,Items.REDSTONE);
var trimmed=menu.getSlot(SmithingMenu.RESULT_SLOT).safeTake(1,1,p);
h.assertTrue(trimmed.is(Items.DIAMOND_CHESTPLATE) && trimmed.has(DataComponents.TRIM),"Native armor trim remains available");
p.closeContainer();h.assertTrue(p.openMenu(provider).isPresent(),"Forgerie remains available on later visits");p.closeContainer();h.succeed();
}
@GameTest public void forgedMenusCannotYieldAnUnownedResult(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL);p.setExperienceLevels(20);var at=p.blockPosition();
h.getLevel().setBlockAndUpdate(at,Blocks.ANVIL.defaultBlockState());
var anvil=new AnvilMenu(7,p.getInventory(),ContainerLevelAccess.create(h.getLevel(),at));
anvil.getSlot(0).set(new ItemStack(Items.DIAMOND_SWORD));anvil.setItemName("Forbidden");
h.assertTrue(anvil.getSlot(AnvilMenu.RESULT_SLOT).hasItem() && !anvil.stillValid(p) && !anvil.getSlot(AnvilMenu.RESULT_SLOT).mayPickup(p),"Anvil constructed outside normal opening is still locked");
h.assertTrue(anvil.getSlot(AnvilMenu.RESULT_SLOT).safeTake(1,1,p).isEmpty() && p.experienceLevel==20 && anvil.getSlot(0).hasItem(),"Rejected anvil take changes neither XP nor inputs");anvil.removed(p);
h.getLevel().setBlockAndUpdate(at,Blocks.SMITHING_TABLE.defaultBlockState());
var forge=new SmithingMenu(8,p.getInventory(),ContainerLevelAccess.create(h.getLevel(),at));inputs(forge,Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE,Items.DIAMOND_SWORD,Items.NETHERITE_INGOT);
h.assertTrue(forge.getSlot(SmithingMenu.RESULT_SLOT).hasItem() && !forge.stillValid(p) && !forge.getSlot(SmithingMenu.RESULT_SLOT).mayPickup(p),"Smithing's inherited result gate rejects an unowned menu");
h.assertTrue(forge.getSlot(SmithingMenu.RESULT_SLOT).safeTake(1,1,p).isEmpty() && forge.getSlot(1).hasItem(),"Rejected smithing take never consumes its base item");forge.removed(p);
var vanilla=Progression003GameTests.player(h);h.assertTrue(CraftAptitudes.allowed(vanilla,"smithing"),"Players outside Sanctuary progression retain vanilla access");h.succeed();
}
@GameTest public void wornWorkstationsRespectTheVisitorsPurchase(GameTestHelper h) {
var owner=player(h,ProgressRecord.INITIAL);var actor=player(h,ProgressRecord.INITIAL);actor.setPos(owner.position().add(1,0,0));
Item[] items={Items.ANVIL,Items.CHIPPED_ANVIL,Items.DAMAGED_ANVIL,Items.SMITHING_TABLE};
for(var item:items) {
AccessoryAccess.of(owner).setItem(AccessoryContainer.HEAD,new ItemStack(item));
HeadBlockService.use(actor,owner,false);h.assertTrue(actor.containerMenu==actor.inventoryMenu,"Worn workstation cannot bypass the visitor's locked aptitude: "+item);
}
actor.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(actor,"smithing",0),"Visitor purchases Forgerie");
for(var item:items) {
AccessoryAccess.of(owner).setItem(AccessoryContainer.HEAD,new ItemStack(item));
h.assertTrue(HeadBlockService.use(actor,owner,false) && actor.containerMenu!=actor.inventoryMenu,"Visitor uses acquired worn workstation: "+item);
var menu=actor.containerMenu;h.assertTrue(HeadBlockService.validMenu(menu,actor,()->menu.stillValid(actor)),"Native worn menu stays valid for learned visitor");actor.closeContainer();
}
AccessoryAccess.of(owner).removeItemNoUpdate(AccessoryContainer.HEAD);h.succeed();
}
private static void inputs(SmithingMenu menu,Item template,Item base,Item addition) {menu.getSlot(0).set(new ItemStack(template));menu.getSlot(1).set(new ItemStack(base));menu.getSlot(2).set(new ItemStack(addition));}
private static ServerPlayer player(GameTestHelper h,ProgressRecord record) {
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());
try {var identities=ProgressionService.session(h.getLevel().getServer()).inhabitants;identities.create(p.getUUID(),"forge"+p.getUUID().toString().substring(0,8),"",identities.available().getFirst());}
catch(Exception e){throw new AssertionError(e);}
bind(p,record);return p;
}
private static void bind(ServerPlayer p,ProgressRecord record){p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);}
}
@@ -68,6 +68,7 @@ public class HeadBlocks043GameTests {
}
@GameTest public void nativeWorkstationsOpenAndCloseAfterMovement(GameTestHelper h){
var owner=player(h);var actor=player(h);actor.setPos(owner.position().add(1,0,0));
for(var target:new String[]{"enchanting","alchemy","smithing"}){actor.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(actor,target,0),"Native workstation fixture owns "+target);}
Item[] items={Items.CRAFTING_TABLE,Items.CHEST,Items.TRAPPED_CHEST,Items.BARREL,Items.ENDER_CHEST,Items.SHULKER_BOX,
Items.FURNACE,Items.BLAST_FURNACE,Items.SMOKER,Items.BREWING_STAND,Items.HOPPER,Items.DISPENSER,Items.DROPPER,
Items.CRAFTER,Items.STONECUTTER,Items.SMITHING_TABLE,Items.LOOM,Items.CARTOGRAPHY_TABLE,Items.GRINDSTONE,Items.ANVIL,Items.ENCHANTING_TABLE,Items.BEACON};
@@ -8,7 +8,7 @@
"entrypoints": {
"client": ["fr.koka.sanctuary.gametest.Starter039TestSupport"],
"preLaunch": ["fr.koka.sanctuary.gametest.Inventory010MixinAudit"],
"fabric-gametest": ["fr.koka.sanctuary.gametest.Aptitudes236GameTests","fr.koka.sanctuary.gametest.Diagonal231GameTests", "fr.koka.sanctuary.gametest.Diagonal230GameTests", "fr.koka.sanctuary.gametest.Diagonal229GameTests", "fr.koka.sanctuary.gametest.Diagonal228GameTests", "fr.koka.sanctuary.gametest.Diagonal227GameTests", "fr.koka.sanctuary.gametest.Diagonal226GameTests", "fr.koka.sanctuary.gametest.Diagonal225GameTests", "fr.koka.sanctuary.gametest.East224GameTests", "fr.koka.sanctuary.gametest.East223GameTests", "fr.koka.sanctuary.gametest.YellowRock222GameTests", "fr.koka.sanctuary.gametest.West221GameTests", "fr.koka.sanctuary.gametest.West220GameTests", "fr.koka.sanctuary.gametest.West219GameTests", "fr.koka.sanctuary.gametest.West218GameTests", "fr.koka.sanctuary.gametest.West217GameTests", "fr.koka.sanctuary.gametest.Base204GameTests", "fr.koka.sanctuary.gametest.North205GameTests", "fr.koka.sanctuary.gametest.Palace173GameTests", "fr.koka.sanctuary.gametest.Plans072GameTests", "fr.koka.sanctuary.gametest.Starter052GameTests", "fr.koka.sanctuary.gametest.Dispenser051GameTests", "fr.koka.sanctuary.gametest.Head050GameTests", "fr.koka.sanctuary.gametest.Poultry048GameTests", "fr.koka.sanctuary.gametest.StarInspection047GameTests", "fr.koka.sanctuary.gametest.SkyFlight046GameTests", "fr.koka.sanctuary.gametest.GravesFood038GameTests","fr.koka.sanctuary.gametest.Realtime037GameTests","fr.koka.sanctuary.gametest.Corrections036GameTests","fr.koka.sanctuary.gametest.Collections035GameTests","fr.koka.sanctuary.gametest.Notifications034GameTests","fr.koka.sanctuary.gametest.Menus033GameTests","${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.Drained307GameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests", "fr.koka.sanctuary.gametest.AncientExpeditions001GameTests", "fr.koka.sanctuary.gametest.Progression003GameTests", "fr.koka.sanctuary.gametest.Atlas005GameTests", "fr.koka.sanctuary.gametest.Demeure006GameTests", "fr.koka.sanctuary.gametest.Mining008GameTests", "fr.koka.sanctuary.gametest.Building009GameTests", "fr.koka.sanctuary.gametest.Inventory010GameTests", "fr.koka.sanctuary.gametest.Cycle011GameTests", "fr.koka.sanctuary.gametest.Inventory012GameTests", "fr.koka.sanctuary.gametest.Inventory014GameTests", "fr.koka.sanctuary.gametest.Tutorial016GameTests", "fr.koka.sanctuary.gametest.Movement017GameTests", "fr.koka.sanctuary.gametest.Accessory018GameTests", "fr.koka.sanctuary.gametest.Companion019GameTests", "fr.koka.sanctuary.gametest.Companion032GameTests", "fr.koka.sanctuary.gametest.Recipes024GameTests", "fr.koka.sanctuary.gametest.Carry027GameTests", "fr.koka.sanctuary.gametest.Familiar029GameTests", "fr.koka.sanctuary.gametest.Dynamic030GameTests", "fr.koka.sanctuary.gametest.GraveInventory039GameTests", "fr.koka.sanctuary.gametest.FoodKnowledge039GameTests", "fr.koka.sanctuary.gametest.OperatorStars040GameTests", "fr.koka.sanctuary.gametest.Starter039GameTests", "fr.koka.sanctuary.gametest.Horde167GameTests", "fr.koka.sanctuary.gametest.Horde168GameTests", "fr.koka.sanctuary.gametest.Horde169GameTests", "fr.koka.sanctuary.gametest.Horde170GameTests", "fr.koka.sanctuary.gametest.Horde171GameTests", "fr.koka.sanctuary.gametest.Horde172GameTests"],
"fabric-gametest": ["fr.koka.sanctuary.gametest.Forgerie238GameTests","fr.koka.sanctuary.gametest.Aptitudes236GameTests","fr.koka.sanctuary.gametest.Diagonal231GameTests", "fr.koka.sanctuary.gametest.Diagonal230GameTests", "fr.koka.sanctuary.gametest.Diagonal229GameTests", "fr.koka.sanctuary.gametest.Diagonal228GameTests", "fr.koka.sanctuary.gametest.Diagonal227GameTests", "fr.koka.sanctuary.gametest.Diagonal226GameTests", "fr.koka.sanctuary.gametest.Diagonal225GameTests", "fr.koka.sanctuary.gametest.East224GameTests", "fr.koka.sanctuary.gametest.East223GameTests", "fr.koka.sanctuary.gametest.YellowRock222GameTests", "fr.koka.sanctuary.gametest.West221GameTests", "fr.koka.sanctuary.gametest.West220GameTests", "fr.koka.sanctuary.gametest.West219GameTests", "fr.koka.sanctuary.gametest.West218GameTests", "fr.koka.sanctuary.gametest.West217GameTests", "fr.koka.sanctuary.gametest.Base204GameTests", "fr.koka.sanctuary.gametest.North205GameTests", "fr.koka.sanctuary.gametest.Palace173GameTests", "fr.koka.sanctuary.gametest.Plans072GameTests", "fr.koka.sanctuary.gametest.Starter052GameTests", "fr.koka.sanctuary.gametest.Dispenser051GameTests", "fr.koka.sanctuary.gametest.Head050GameTests", "fr.koka.sanctuary.gametest.Poultry048GameTests", "fr.koka.sanctuary.gametest.StarInspection047GameTests", "fr.koka.sanctuary.gametest.SkyFlight046GameTests", "fr.koka.sanctuary.gametest.GravesFood038GameTests","fr.koka.sanctuary.gametest.Realtime037GameTests","fr.koka.sanctuary.gametest.Corrections036GameTests","fr.koka.sanctuary.gametest.Collections035GameTests","fr.koka.sanctuary.gametest.Notifications034GameTests","fr.koka.sanctuary.gametest.Menus033GameTests","${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.Drained307GameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests", "fr.koka.sanctuary.gametest.AncientExpeditions001GameTests", "fr.koka.sanctuary.gametest.Progression003GameTests", "fr.koka.sanctuary.gametest.Atlas005GameTests", "fr.koka.sanctuary.gametest.Demeure006GameTests", "fr.koka.sanctuary.gametest.Mining008GameTests", "fr.koka.sanctuary.gametest.Building009GameTests", "fr.koka.sanctuary.gametest.Inventory010GameTests", "fr.koka.sanctuary.gametest.Cycle011GameTests", "fr.koka.sanctuary.gametest.Inventory012GameTests", "fr.koka.sanctuary.gametest.Inventory014GameTests", "fr.koka.sanctuary.gametest.Tutorial016GameTests", "fr.koka.sanctuary.gametest.Movement017GameTests", "fr.koka.sanctuary.gametest.Accessory018GameTests", "fr.koka.sanctuary.gametest.Companion019GameTests", "fr.koka.sanctuary.gametest.Companion032GameTests", "fr.koka.sanctuary.gametest.Recipes024GameTests", "fr.koka.sanctuary.gametest.Carry027GameTests", "fr.koka.sanctuary.gametest.Familiar029GameTests", "fr.koka.sanctuary.gametest.Dynamic030GameTests", "fr.koka.sanctuary.gametest.GraveInventory039GameTests", "fr.koka.sanctuary.gametest.FoodKnowledge039GameTests", "fr.koka.sanctuary.gametest.OperatorStars040GameTests", "fr.koka.sanctuary.gametest.Starter039GameTests", "fr.koka.sanctuary.gametest.Horde167GameTests", "fr.koka.sanctuary.gametest.Horde168GameTests", "fr.koka.sanctuary.gametest.Horde169GameTests", "fr.koka.sanctuary.gametest.Horde170GameTests", "fr.koka.sanctuary.gametest.Horde171GameTests", "fr.koka.sanctuary.gametest.Horde172GameTests"],
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
},
"mixins": ["sanctuary-gametest.mixins.json"],
@@ -18,6 +18,13 @@ public final class AptitudeClient {
public static void register() {
ClientConfigurationNetworking.registerGlobalReceiver(AptitudeService.Capability.TYPE,(p,c)->{});
ClientConfigurationNetworking.registerGlobalReceiver(AptitudeService.PermanentCapability.TYPE,(p,c)->{});
ClientConfigurationNetworking.registerGlobalReceiver(AptitudeService.SmithingCapability.TYPE,(p,c)->{});
ClientPlayNetworking.registerGlobalReceiver(AptitudeService.SmithingState.TYPE,(state,c)->c.client().execute(()->{
var player=c.client().player;
if(player==null || player.getId()!=state.entity())return;
player.setAttached(CraftAptitudes.SMITHING,state.learned());
if(c.client().gui.screen() instanceof ProgressionScreen screen)screen.accept("");
}));
ClientPlayNetworking.registerGlobalReceiver(AptitudeService.State.TYPE,(state,c)->c.client().execute(()->{
var player=c.client().player;
if(player==null || player.getId()!=state.entity())return;
@@ -97,7 +97,7 @@ public final class ProgressionScreen extends BlocodexFrame {
List.of("inventory_crafting","sorting","catalogue"),
List.of("familiars","active_bond","familiar_study"),
List.of("exploration","atlas","names","zoom"),
List.of("crafts","enchanting","alchemy"))) {
List.of("crafts","enchanting","alchemy","smithing"))) {
contents.addChild(new MultiLineTextWidget(ProgressionService.label("theme_"+theme.getFirst()).copy()
.withStyle(net.minecraft.ChatFormatting.AQUA),font).setMaxWidth(innerWidth));
for(String target:theme.subList(1,theme.size())) {
@@ -0,0 +1,17 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.progression.CraftAptitudes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.*;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** Anvil overrides the combiner pickup check; Smithing inherits it. */
@Mixin({ItemCombinerMenu.class,AnvilMenu.class})
abstract class CraftAptitudeCombinerMixin {
@Inject(method="mayPickup",at=@At("HEAD"),cancellable=true)
private void sanctuary$forgePickup(Player player,boolean hasItem,CallbackInfoReturnable<Boolean> ci) {
if(!CraftAptitudes.allowed(player,(AbstractContainerMenu)(Object)this))ci.setReturnValue(false);
}
}
@@ -7,7 +7,7 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin({EnchantmentMenu.class,BrewingStandMenu.class})
@Mixin({EnchantmentMenu.class,BrewingStandMenu.class,ItemCombinerMenu.class})
abstract class CraftAptitudeMenuMixin {
@Inject(method="stillValid",at=@At("HEAD"),cancellable=true)
private void sanctuary$craftAccess(Player player,CallbackInfoReturnable<Boolean> ci) {
@@ -6,7 +6,7 @@ import java.util.List;
public record AptitudePreferences(int disabledMask) {
public static final List<String> TOGGLEABLE=List.of("names","atlas");
public static final List<String> PERMANENT=List.of("sorting","swimming","resting",
"active_bond","familiar_study","catalogue","zoom","enchanting","alchemy","food_knowledge");
"active_bond","familiar_study","catalogue","zoom","enchanting","alchemy","smithing","food_knowledge");
public static final int MAX_MASK=2047;
public static final int ACTIVE_MASK=3;
public static final AptitudePreferences DEFAULT=new AptitudePreferences(0);
@@ -26,6 +26,17 @@ public final class AptitudeService {
public static final StreamCodec<FriendlyByteBuf,PermanentCapability> CODEC=StreamCodec.unit(new PermanentCapability());
public Type<PermanentCapability> type(){return TYPE;}
}
public record SmithingCapability() implements CustomPacketPayload {
public static final Type<SmithingCapability> TYPE=new Type<>(SanctuaryMod.id("smithing_v1"));
public static final StreamCodec<FriendlyByteBuf,SmithingCapability> CODEC=StreamCodec.unit(new SmithingCapability());
public Type<SmithingCapability> type(){return TYPE;}
}
public record SmithingState(int entity,boolean learned) implements CustomPacketPayload {
public static final Type<SmithingState> TYPE=new Type<>(SanctuaryMod.id("smithing_state_v1"));
public static final StreamCodec<RegistryFriendlyByteBuf,SmithingState> CODEC=CustomPacketPayload.codec(
(p,b)->{b.writeVarInt(p.entity);b.writeBoolean(p.learned);},b->new SmithingState(b.readVarInt(),b.readBoolean()));
public Type<SmithingState> type(){return TYPE;}
}
public record Set(String target,boolean enabled,int expectedMask) implements CustomPacketPayload {
public static final Type<Set> TYPE=new Type<>(SanctuaryMod.id("aptitude_set_v1"));
public static final StreamCodec<RegistryFriendlyByteBuf,Set> CODEC=CustomPacketPayload.codec(
@@ -64,12 +75,16 @@ public final class AptitudeService {
int mask=preferences(p).disabledMask();
if(p.getAttachedOrElse(PREFERENCES,0)!=mask)p.setAttached(PREFERENCES,mask);
if(ServerPlayNetworking.canSend(p,State.TYPE))ServerPlayNetworking.send(p,new State(p.getId(),preferences(p).disabledMask(),CraftAptitudes.learned(p,"enchanting"),CraftAptitudes.learned(p,"alchemy")));
if(ServerPlayNetworking.canSend(p,SmithingState.TYPE))ServerPlayNetworking.send(p,new SmithingState(p.getId(),CraftAptitudes.learned(p,"smithing")));
}
public static void register() {
java.util.Objects.requireNonNull(CraftAptitudes.ENCHANTING);
java.util.Objects.requireNonNull(CraftAptitudes.ALCHEMY);
java.util.Objects.requireNonNull(CraftAptitudes.SMITHING);
PayloadTypeRegistry.clientboundConfiguration().register(Capability.TYPE,Capability.CODEC);
PayloadTypeRegistry.clientboundConfiguration().register(PermanentCapability.TYPE,PermanentCapability.CODEC);
PayloadTypeRegistry.clientboundConfiguration().register(SmithingCapability.TYPE,SmithingCapability.CODEC);
PayloadTypeRegistry.clientboundPlay().register(SmithingState.TYPE,SmithingState.CODEC);
PayloadTypeRegistry.serverboundPlay().register(Set.TYPE,Set.CODEC);
PayloadTypeRegistry.clientboundPlay().register(State.TYPE,State.CODEC);
ServerPlayNetworking.registerGlobalReceiver(Set.TYPE,(request,c)->c.server().execute(()->{
@@ -14,13 +14,15 @@ public final class CraftAptitudes {
b->b.persistent(Codec.BOOL).copyOnDeath());
public static final AttachmentType<Boolean> ALCHEMY=AttachmentRegistry.create(SanctuaryMod.id("alchemy_learned"),
b->b.persistent(Codec.BOOL).copyOnDeath());
public static final AttachmentType<Boolean> SMITHING=AttachmentRegistry.create(SanctuaryMod.id("smithing_learned"),
b->b.persistent(Codec.BOOL).copyOnDeath());
private CraftAptitudes() {}
public static boolean target(String target){return target.equals("enchanting") || target.equals("alchemy");}
public static boolean target(String target){return target.equals("enchanting") || target.equals("alchemy") || target.equals("smithing");}
private static AttachmentType<Boolean> attachment(String target) {
return switch(target){case "enchanting"->ENCHANTING;case "alchemy"->ALCHEMY;default->throw new IllegalArgumentException("Unknown craft aptitude");};
return switch(target){case "enchanting"->ENCHANTING;case "alchemy"->ALCHEMY;case "smithing"->SMITHING;default->throw new IllegalArgumentException("Unknown craft aptitude");};
}
public static boolean learned(Player p,String target){return p.getAttachedOrElse(attachment(target),false);}
public static String target(AbstractContainerMenu menu){return menu instanceof EnchantmentMenu ? "enchanting" : menu instanceof BrewingStandMenu ? "alchemy" : "";}
public static String target(AbstractContainerMenu menu){return menu instanceof EnchantmentMenu ? "enchanting" : menu instanceof BrewingStandMenu ? "alchemy" : menu instanceof AnvilMenu || menu instanceof SmithingMenu ? "smithing" : "";}
public static boolean allowed(Player p,String target) {
if(ProgressEffects.get(p)==null)return true;
if(p instanceof ServerPlayer server) {
@@ -121,6 +121,9 @@ public final class ProgressionService {
|| !ServerConfigurationNetworking.canSend(handler,AptitudeService.PermanentCapability.TYPE)) {
handler.disconnect(label("aptitude_version_required"));return;
}
if (!ServerConfigurationNetworking.canSend(handler,AptitudeService.SmithingCapability.TYPE)) {
handler.disconnect(label("smithing_version_required"));return;
}
if (!ServerConfigurationNetworking.canSend(handler,ProgressionPayloads.SwimmingCapability.TYPE)) {
handler.disconnect(label("swimming_version_required"));return;
}
@@ -2639,5 +2639,9 @@
"sanctuary.progression.aptitude_disable_hint": "Click to disable.",
"sanctuary.progression.aptitude_enable_hint": "Click to enable.",
"sanctuary.progression.aptitude_acquired": "Acquired",
"sanctuary.progression.aptitude_acquired_hint": "This aptitude remains permanently available after purchase."
"sanctuary.progression.aptitude_acquired_hint": "This aptitude remains permanently available after purchase.",
"sanctuary.progression.smithing": "Smithing",
"sanctuary.progression.smithing_description": "Use anvils and the smithing table to repair, rename and upgrade equipment.",
"sanctuary.progression.smithing_locked": "Acquire Smithing in Progression to use an anvil or smithing table.",
"sanctuary.progression.smithing_version_required": "Smithing requires Sanctuary beta.238 or later on both client and server."
}
@@ -2639,5 +2639,9 @@
"sanctuary.progression.aptitude_disable_hint": "Cliquer pour désactiver.",
"sanctuary.progression.aptitude_enable_hint": "Cliquer pour activer.",
"sanctuary.progression.aptitude_acquired": "Acquise",
"sanctuary.progression.aptitude_acquired_hint": "Cette aptitude est acquise définitivement après achat."
"sanctuary.progression.aptitude_acquired_hint": "Cette aptitude est acquise définitivement après achat.",
"sanctuary.progression.smithing": "Forgerie",
"sanctuary.progression.smithing_description": "Utiliser les enclumes et la table de forge pour réparer, renommer et améliorer les équipements.",
"sanctuary.progression.smithing_locked": "Acquérez Forgerie dans Progression pour utiliser une enclume ou une table de forge.",
"sanctuary.progression.smithing_version_required": "Forgerie nécessite Sanctuary beta.238 ou plus récent côté client et serveur."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

@@ -6,6 +6,7 @@
"CraftAptitudeOpenMixin",
"CraftAptitudeMenuMixin",
"CraftAptitudeEnchantMixin",
"CraftAptitudeCombinerMixin",
"IslandChestLootMixin",
"BadlandsBands211Accessor",
"WestMonument217Accessor",
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.237"
version = "beta.238"
pack-format = "packwiz:1.1.0"
[index]