Record Sanctuary sources through beta.060 and Git validation
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Demeure — provenance du port beta.006
|
||||
|
||||
Auteur historique : KOKA99CAB. Licence : GPL-3.0-or-later. Sources locales `Structures/demeure`, citées par `26.2/inventory/README.md`, ciblant 26.1.2. Aucun JAR historique repris. Modèle des influences et états adapté ; événements et sauvegarde réécrits pour 26.3-pre-2, jours UTC réels, limites et séparation Atlas. AtomicJson et le hook de placement proviennent de Sanctuary (même licence).
|
||||
|
||||
Empreintes SHA-256 des sources de référence :
|
||||
|
||||
- `src/main/java/fr/koka99cab/demeure/DemeureMod.java` : `fef440779af58ca21a26dc79fd71676d27f79370239fa9b24ee691beb78becdd`
|
||||
- `src/main/java/fr/koka99cab/demeure/command/DemeureCommands.java` : `ecd8fea6b7811cfa3b682ea566af748c5338a5c702970badc5a183480eda5706`
|
||||
- `src/main/java/fr/koka99cab/demeure/network/DemeureFootprintPayload.java` : `621b9912560a27b1ef8c785be4d077ab5e153394c333833419d8b0a1e6c8b021`
|
||||
- `src/main/java/fr/koka99cab/demeure/network/DemeureFootprintSync.java` : `790ac0c26b13f23e7619d1fffaf389d16ef44e9c3180552c6b1f307487646bea`
|
||||
- `src/main/java/fr/koka99cab/demeure/server/DemeureEvents.java` : `983e1e0f417ca6ac71978ad4d6250067480425d7cfa6b5b14cdc9abbf7038a62`
|
||||
- `src/main/java/fr/koka99cab/demeure/server/DemeureGesture.java` : `c50ce2b5a5897453ee7fe4b7cf3db9f0d5343522666acd7b69b5f2399678f8d7`
|
||||
- `src/main/java/fr/koka99cab/demeure/server/DemeureTracker.java` : `bc362a3815a955fd576a481305a8006089242a3c4855be3607d549114373ea3c`
|
||||
- `src/main/java/fr/koka99cab/demeure/world/DemeureApi.java` : `4cf5f353ff4d1149ec8ccceae5af315a500ce3d977572eeecc51c511058f73dd`
|
||||
- `src/main/java/fr/koka99cab/demeure/world/DemeureChunkFootprint.java` : `fc85e880218522f4e535fc13e7b3895dca8425040d9028dd5d3d4232a1d39673`
|
||||
- `src/main/java/fr/koka99cab/demeure/world/DemeureChunkView.java` : `516cf5f349e3364ca2dfb845838127b760efb62ca9eefb408bbdf78c32214879`
|
||||
- `src/main/java/fr/koka99cab/demeure/world/DemeureInfluence.java` : `f9aa33bcf974b041f2ccd3edf1cb3975f2fc81d8b2bef6adb259c46461737c01`
|
||||
- `src/main/java/fr/koka99cab/demeure/world/DemeureInfluenceView.java` : `13315b2400fc82f33fd7f636f3e4505e480b27d782f7b039b1e3f187784c71e3`
|
||||
- `src/main/java/fr/koka99cab/demeure/world/DemeureSavedData.java` : `ce842b4062266d52b5778de315e18526ee3370056cab358ead6dc5e0630ef1c6`
|
||||
- `src/main/java/fr/koka99cab/demeure/world/DemeureSubject.java` : `501fa36c7544da51ec625d999bed0d98c8caf1382e62cfb5500da5f3b2fa820f`
|
||||
- `src/main/java/fr/koka99cab/demeure/world/DemeureTerritoryState.java` : `0753ae22d166ccd7283914349c327c9b92899e13ce82c3e2bce0ef5ffcd142a5`
|
||||
@@ -0,0 +1,32 @@
|
||||
# Demeure
|
||||
|
||||
Mod Fabric autonome pour Minecraft 26.3-pre-2, porté depuis les sources
|
||||
historiques référencées par Sanctuary 26.2. Il mesure les empreintes du vivant
|
||||
et l'attachement aux lieux ; il n'attribue ni propriété ni protection.
|
||||
|
||||
Sanctuary l'embarque dans son JAR et utilise son API pour la surcouche de
|
||||
l'Atlas. Demeure ne dépend pas de Sanctuary. L'hôte peut restreindre les joueurs
|
||||
éligibles avec `DemeureService.setEligibility` ; le pack limite ainsi les
|
||||
traces aux mondes dont la progression est active et saine.
|
||||
|
||||
Les gestes sont relevés après placement ou destruction réussis, lors d'un
|
||||
sommeil ou d'une mort, et par échantillonnage de l'activité. Les animaux,
|
||||
villageois et monstres proches sont regroupés par catégorie, jusqu'à
|
||||
128 entités chargées par habitant actif et par minute, dans un rayon de
|
||||
32 blocs (boîte englobante). Le mode spectateur ne contribue pas.
|
||||
|
||||
Chaque chunk conserve des influences, une présence naturelle de départ,
|
||||
un budget par sujet et jour UTC, et une décroissance après quatorze jours
|
||||
d'absence. La consultation est indépendante des règles de protection et
|
||||
ne crée jamais de chunk. Aucun point d'expérience n'est distribué.
|
||||
|
||||
Sauvegarde : `data/demeure/footprints-v1.json`, schéma 1 et graine du monde,
|
||||
écriture atomique, sauvegarde périodique, déconnexion et arrêt du serveur.
|
||||
La corruption ou une modification externe bloque l'écriture et produit
|
||||
une erreur serveur ; le fichier existant est conservé. Les limites sont
|
||||
explicitées dans [le contrat beta.006](../../docs/demeure-beta006.md).
|
||||
Les données de l'ancien Demeure ne sont pas importées automatiquement.
|
||||
|
||||
`./gradlew :demeure:check :demeure:build` depuis la racine vérifie le modèle
|
||||
et construit le module. Les tests natifs des événements et de l'Atlas vivent
|
||||
dans Sanctuary. Licence GPL-3.0-or-later ; [provenance](PROVENANCE.md).
|
||||
@@ -0,0 +1,27 @@
|
||||
plugins { id 'net.fabricmc.fabric-loom' }
|
||||
group = 'fr.koka.demeure'
|
||||
version = rootProject.mod_version
|
||||
base { archivesName = 'demeure' }
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
|
||||
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
|
||||
implementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
|
||||
}
|
||||
java { toolchain.languageVersion = JavaLanguageVersion.of(25); withSourcesJar() }
|
||||
tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8'; options.release = 25 }
|
||||
processResources {
|
||||
def values = [version: project.version, minecraft: rootProject.minecraft_version.replace('-pre-', '-pre.'), loader: rootProject.loader_version]
|
||||
inputs.properties(values)
|
||||
filesMatching('fabric.mod.json') { expand(values) }
|
||||
}
|
||||
tasks.named('jar') {
|
||||
from(rootProject.file('LICENSE')) { rename { 'LICENSE_demeure' } }
|
||||
from('PROVENANCE.md')
|
||||
}
|
||||
tasks.named('test') { failOnNoDiscoveredTests = false }
|
||||
tasks.register('demeureSmoke', JavaExec) {
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.demeure.DemeureSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('demeureSmoke') }
|
||||
@@ -0,0 +1,78 @@
|
||||
package fr.koka.demeure;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.Strictness;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Refuses corrupt, oversized or externally replaced state; never resets evidence on failure. */
|
||||
public final class AtomicJson {
|
||||
private AtomicJson() { }
|
||||
public static JsonElement parse(String text) {
|
||||
try {
|
||||
var reader = new JsonReader(new StringReader(text));
|
||||
reader.setStrictness(Strictness.STRICT);
|
||||
scan(reader, 0);
|
||||
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IllegalArgumentException("Trailing JSON");
|
||||
reader = new JsonReader(new StringReader(text));
|
||||
reader.setStrictness(Strictness.STRICT);
|
||||
JsonElement value = JsonParser.parseReader(reader);
|
||||
if (reader.peek() != JsonToken.END_DOCUMENT) throw new IllegalArgumentException("Trailing JSON");
|
||||
return value;
|
||||
} catch (IOException error) { throw new IllegalArgumentException(error); }
|
||||
}
|
||||
public static void fields(JsonElement value, String... expected) {
|
||||
if (!value.isJsonObject() || !value.getAsJsonObject().keySet().equals(java.util.Set.of(expected)))
|
||||
throw new IllegalArgumentException("Missing or unknown JSON fields");
|
||||
}
|
||||
private static void scan(JsonReader reader, int depth) throws IOException {
|
||||
if (depth > 32) throw new IllegalArgumentException("JSON nesting too deep");
|
||||
switch (reader.peek()) {
|
||||
case BEGIN_OBJECT -> {
|
||||
reader.beginObject();
|
||||
var keys = new java.util.HashSet<String>();
|
||||
while (reader.hasNext()) {
|
||||
if (!keys.add(reader.nextName())) throw new IllegalArgumentException("Duplicate JSON field");
|
||||
scan(reader, depth + 1);
|
||||
}
|
||||
reader.endObject();
|
||||
}
|
||||
case BEGIN_ARRAY -> { reader.beginArray(); while (reader.hasNext()) scan(reader, depth + 1); reader.endArray(); }
|
||||
default -> reader.skipValue();
|
||||
}
|
||||
}
|
||||
public static byte[] read(Path file, int limit) throws IOException {
|
||||
if (Files.notExists(file, LinkOption.NOFOLLOW_LINKS)) return null;
|
||||
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS) || Files.size(file) > limit)
|
||||
throw new IOException("Invalid or oversized file: " + file);
|
||||
byte[] bytes;
|
||||
try (var stream = Files.newInputStream(file)) { bytes = stream.readNBytes(limit + 1); }
|
||||
if (bytes.length > limit) throw new IOException("Oversized file: " + file);
|
||||
return bytes;
|
||||
}
|
||||
public static byte[] write(Path file, byte[] previous, String text, int limit) throws IOException {
|
||||
if (!Arrays.equals(previous, read(file, limit))) throw new IOException("File changed externally: " + file);
|
||||
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
|
||||
if (bytes.length > limit) throw new IOException("Oversized state: " + file);
|
||||
Files.createDirectories(file.getParent());
|
||||
Path temporary = Files.createTempFile(file.getParent(), ".demeure-", ".tmp");
|
||||
try {
|
||||
try (var channel = FileChannel.open(temporary, StandardOpenOption.WRITE)) {
|
||||
var buffer = ByteBuffer.wrap(bytes);
|
||||
while (buffer.hasRemaining()) channel.write(buffer);
|
||||
channel.force(true);
|
||||
}
|
||||
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
if (!Arrays.equals(bytes, read(file, limit))) throw new IOException("Read-back mismatch: " + file);
|
||||
return bytes;
|
||||
} finally { Files.deleteIfExists(temporary); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package fr.koka.demeure;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.EntitySleepEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.animal.Animal;
|
||||
import net.minecraft.world.entity.monster.Monster;
|
||||
import net.minecraft.world.entity.npc.villager.AbstractVillager;
|
||||
import net.minecraft.world.level.block.BedBlock;
|
||||
import net.minecraft.world.level.block.CropBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Autonomous server domain. Hosts may narrow activation; no client request can add influence. */
|
||||
public final class DemeureService implements ModInitializer {
|
||||
private static final org.slf4j.Logger LOG=LoggerFactory.getLogger("demeure");
|
||||
private static final Map<MinecraftServer,Session> SESSIONS=new WeakHashMap<>();
|
||||
private static Predicate<ServerPlayer> eligibility=player -> true;
|
||||
private static final class Session {
|
||||
FootprintStore store;boolean failed,capacityWarned;int lastSave;
|
||||
final Map<UUID,Trace> traces=new HashMap<>();
|
||||
}
|
||||
private static final class Trace {
|
||||
FootprintStore.Key chunk;BlockPos pos;int activeTick,minuteTick;
|
||||
}
|
||||
public static void setEligibility(Predicate<ServerPlayer> predicate) {eligibility=Objects.requireNonNull(predicate);}
|
||||
public static long day() {return LocalDate.now(ZoneOffset.UTC).toEpochDay();}
|
||||
public static boolean eligible(ServerPlayer player) {return player.isAlive() && !player.isSpectator() && eligibility.test(player);}
|
||||
public static FootprintStore.Key key(ServerPlayer player,BlockPos pos) {
|
||||
return new FootprintStore.Key(player.level().dimension().identifier().toString(),Math.floorDiv(pos.getX(),16),Math.floorDiv(pos.getZ(),16));
|
||||
}
|
||||
private static FootprintStore.Subject subject(ServerPlayer player) {
|
||||
return new FootprintStore.Subject("player",player.getUUID().toString(),player.getGameProfile().name());
|
||||
}
|
||||
private static Session session(MinecraftServer server) {
|
||||
return SESSIONS.computeIfAbsent(server,s -> {
|
||||
var session=new Session();session.lastSave=s.getTickCount();
|
||||
try {session.store=new FootprintStore(s.getWorldPath(LevelResource.ROOT).resolve("data/demeure/footprints-v1.json"),s.overworld().getSeed());}
|
||||
catch (Exception error) {session.failed=true;LOG.error("Demeure unavailable; original file preserved",error);}
|
||||
return session;
|
||||
});
|
||||
}
|
||||
public static FootprintStore.View view(MinecraftServer server,FootprintStore.Key key) {
|
||||
var session=session(server);return session.failed ? null : session.store.view(key,day());
|
||||
}
|
||||
public static boolean available(MinecraftServer server) {return !session(server).failed;}
|
||||
public static double record(ServerPlayer player,BlockPos pos,double amount) {
|
||||
if (!eligible(player)) return 0;
|
||||
return recordUnchecked(player,pos,amount);
|
||||
}
|
||||
private static double recordUnchecked(ServerPlayer player,BlockPos pos,double amount) {
|
||||
var session=session(player.level().getServer());if (session.failed) return 0;
|
||||
var trace=session.traces.computeIfAbsent(player.getUUID(),id -> new Trace());trace.activeTick=player.level().getServer().getTickCount();
|
||||
return session.store.touch(key(player,pos),subject(player),amount,day());
|
||||
}
|
||||
public static void placed(ServerPlayer player,BlockPos pos,BlockState state) {
|
||||
record(player,pos,state.getBlock() instanceof BedBlock ? .5 : .05);
|
||||
}
|
||||
@Override public void onInitialize() {
|
||||
PlayerBlockBreakEvents.AFTER.register((level,player,pos,state,entity) -> {
|
||||
if (player instanceof ServerPlayer sp) record(sp,pos,state.getBlock() instanceof CropBlock crop && crop.isMaxAge(state) ? .3 : .05);
|
||||
});
|
||||
EntitySleepEvents.START_SLEEPING.register((entity,pos) -> {if (entity instanceof ServerPlayer sp) record(sp,pos,2);});
|
||||
ServerLivingEntityEvents.AFTER_DEATH.register((entity,source) -> {
|
||||
if (entity instanceof ServerPlayer sp && !sp.isSpectator() && eligibility.test(sp)) recordUnchecked(sp,sp.blockPosition(),1);
|
||||
});
|
||||
ServerTickEvents.END_SERVER_TICK.register(DemeureService::tick);
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler,server) -> {
|
||||
var session=SESSIONS.get(server);if (session!=null) {session.traces.remove(handler.player.getUUID());save(session);}
|
||||
});
|
||||
ServerLifecycleEvents.SERVER_STOPPING.register(server -> {var session=SESSIONS.remove(server);if (session!=null) save(session);});
|
||||
}
|
||||
private static void tick(MinecraftServer server) {
|
||||
if (server.getTickCount()%20!=0) return;
|
||||
int tick=server.getTickCount();var seen=new HashSet<UUID>();
|
||||
for (var player:server.getPlayerList().getPlayers()) {
|
||||
if (!eligible(player)) continue;
|
||||
var session=session(server);if (session.failed) continue;
|
||||
var trace=session.traces.computeIfAbsent(player.getUUID(),id -> new Trace());
|
||||
var pos=player.blockPosition();var key=key(player,pos);
|
||||
if (!key.equals(trace.chunk)) {record(player,pos,.01);trace.chunk=key;}
|
||||
if (!pos.equals(trace.pos)) {trace.activeTick=tick;trace.pos=pos.immutable();}
|
||||
if (tick-trace.minuteTick>=1200) {
|
||||
boolean active=tick-trace.activeTick<1200;
|
||||
if (active) session.store.touch(key,subject(player),.1,day());
|
||||
trace.minuteTick=tick;
|
||||
if (!active) continue;
|
||||
// A bounded sample of loaded living neighbours; never searches or loads chunks.
|
||||
int budget=128;
|
||||
for (var entity:player.level().getEntitiesOfClass(LivingEntity.class,player.getBoundingBox().inflate(32))) {
|
||||
if (budget--<=0) break;
|
||||
if (!entity.isAlive() || !seen.add(entity.getUUID())) continue;
|
||||
String kind=entity instanceof Animal ? "animals" : entity instanceof AbstractVillager ? "villagers" : entity instanceof Monster ? "monsters" : null;
|
||||
if (kind==null) continue;
|
||||
var at=entity.blockPosition();
|
||||
session.store.touch(key(player,at),new FootprintStore.Subject(kind,kind,""),kind.equals("animals") ? .025 : kind.equals("villagers") ? .035 : .04,day());
|
||||
}
|
||||
}
|
||||
}
|
||||
var session=SESSIONS.get(server);
|
||||
if (session!=null && !session.failed) {
|
||||
if (!session.capacityWarned && session.store.atCapacity()) {session.capacityWarned=true;LOG.warn("Demeure capacity reached; existing footprints retained");}
|
||||
if (tick-session.lastSave>=1200) {save(session);session.lastSave=tick;}
|
||||
}
|
||||
}
|
||||
private static void save(Session session) {
|
||||
if (session.failed) return;
|
||||
try {session.store.save();} catch (Exception error) {session.failed=true;LOG.error("Demeure save refused; original file preserved",error);}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package fr.koka.demeure;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/** Adapted from KOKA99CAB's Demeure influence model. No ownership or protection is inferred. */
|
||||
public final class FootprintStore {
|
||||
public static final int MAX_CHUNKS=16384,MAX_SUBJECTS=31;
|
||||
private static final int MAX_BYTES=64*1024*1024;
|
||||
private static final Gson JSON=new Gson();
|
||||
public record Key(String dimension,int x,int z) {
|
||||
public Key {
|
||||
if (dimension==null || dimension.length()>128 || !dimension.matches("[a-z0-9_.-]+:[a-z0-9_./-]+")
|
||||
|| Math.abs((long)x)>1875000 || Math.abs((long)z)>1875000) throw new IllegalArgumentException("Demeure location");
|
||||
}
|
||||
}
|
||||
public record Subject(String kind,String id,String label) {
|
||||
public Subject {
|
||||
if (!Set.of("player","animals","villagers","monsters","nature").contains(kind)
|
||||
|| id==null || id.isBlank() || id.length()>64 || label==null || label.length()>64
|
||||
|| label.codePoints().anyMatch(c -> Character.isISOControl(c) || c==0xA7)) throw new IllegalArgumentException("Demeure subject");
|
||||
if (kind.equals("player")) UUID.fromString(id);
|
||||
}
|
||||
}
|
||||
public record Influence(Subject subject,double score,long lastDay,int activeDays,double daily) {
|
||||
public Influence {
|
||||
if (subject==null || !Double.isFinite(score) || score<0 || score>1000000 || lastDay<0
|
||||
|| activeDays<1 || activeDays>16 || !Double.isFinite(daily) || daily<0 || daily>8.000001)
|
||||
throw new IllegalArgumentException("Demeure influence");
|
||||
}
|
||||
public double base(long day) {return score*Math.pow(.985,Math.max(0,day-lastDay-14));}
|
||||
public double effective(long day) {return base(day)*(1+Math.min(1.2,(activeDays-1)*.08));}
|
||||
}
|
||||
public record Share(Subject subject,double score,int percent) { }
|
||||
public record View(String state,List<Share> influences) {public View {influences=List.copyOf(influences);}}
|
||||
private record Chunk(Key key,List<Influence> influences) { }
|
||||
private record Document(int schema,long seed,List<Chunk> chunks) { }
|
||||
private final Path path;
|
||||
private final long seed;
|
||||
private final Map<Key,LinkedHashMap<String,Influence>> chunks=new LinkedHashMap<>();
|
||||
private byte[] previous;
|
||||
private boolean dirty,atCapacity;
|
||||
public FootprintStore(Path path,long seed) throws IOException {
|
||||
this.path=path;this.seed=seed;previous=AtomicJson.read(path,MAX_BYTES);
|
||||
if (previous==null) return;
|
||||
var tree=AtomicJson.parse(new String(previous,StandardCharsets.UTF_8));
|
||||
AtomicJson.fields(tree,"schema","seed","chunks");
|
||||
for (var chunk:tree.getAsJsonObject().getAsJsonArray("chunks")) {
|
||||
AtomicJson.fields(chunk,"key","influences");
|
||||
AtomicJson.fields(chunk.getAsJsonObject().get("key"),"dimension","x","z");
|
||||
for (var inf:chunk.getAsJsonObject().getAsJsonArray("influences")) {
|
||||
AtomicJson.fields(inf,"subject","score","lastDay","activeDays","daily");
|
||||
AtomicJson.fields(inf.getAsJsonObject().get("subject"),"kind","id","label");
|
||||
}
|
||||
}
|
||||
var doc=JSON.fromJson(tree,Document.class);
|
||||
if (doc.schema!=1 || doc.seed!=seed || doc.chunks==null || doc.chunks.size()>MAX_CHUNKS) throw new IOException("Demeure schema/seed");
|
||||
for (var chunk:doc.chunks) {
|
||||
if (chunk.key==null || chunk.influences==null || chunk.influences.size()>MAX_SUBJECTS) throw new IOException("Demeure chunk");
|
||||
var entries=new LinkedHashMap<String,Influence>();
|
||||
for (var inf:chunk.influences) {
|
||||
if (inf==null || inf.subject.kind.equals("nature") || entries.put(subjectKey(inf.subject),inf)!=null) throw new IOException("Demeure duplicate subject");
|
||||
}
|
||||
if (chunks.put(chunk.key,entries)!=null) throw new IOException("Demeure duplicate chunk");
|
||||
}
|
||||
}
|
||||
private static String subjectKey(Subject subject) {return subject.kind+":"+subject.id;}
|
||||
/** Returns the accepted contribution. Caps survive reconnects and restarts. */
|
||||
public double touch(Key key,Subject subject,double amount,long day) {
|
||||
if (day<0 || !Double.isFinite(amount) || amount<=0 || subject.kind.equals("nature")) throw new IllegalArgumentException("Demeure gesture");
|
||||
var entries=chunks.get(key);
|
||||
if (entries==null) {
|
||||
if (chunks.size()>=MAX_CHUNKS) {atCapacity=true;return 0;}
|
||||
entries=new LinkedHashMap<>();chunks.put(key,entries);
|
||||
}
|
||||
String id=subjectKey(subject);var old=entries.get(id);
|
||||
if (old==null && entries.size()>=MAX_SUBJECTS) {atCapacity=true;return 0;}
|
||||
day=old==null ? day : Math.max(day,old.lastDay);
|
||||
double daily=old==null || day>old.lastDay ? 0 : old.daily;
|
||||
double accepted=Math.min(amount,Math.max(0,8-daily));
|
||||
if (accepted<=0) return 0;
|
||||
int days=old==null ? 1 : Math.min(16,old.activeDays+(day>old.lastDay ? 1 : 0));
|
||||
double score=Math.min(1000000,(old==null ? 0 : old.base(day))+accepted);
|
||||
entries.put(id,new Influence(subject,score,day,days,daily+accepted));dirty=true;
|
||||
return accepted;
|
||||
}
|
||||
public View view(Key key,long day) {
|
||||
var entries=chunks.get(key);if (entries==null) return null;
|
||||
var raw=new ArrayList<Share>();
|
||||
raw.add(new Share(new Subject("nature","nature",""),.35,0));
|
||||
for (var inf:entries.values()) raw.add(new Share(inf.subject,inf.effective(day),0));
|
||||
raw.sort(Comparator.comparingDouble(Share::score).reversed().thenComparing(s -> subjectKey(s.subject)));
|
||||
double total=raw.stream().mapToDouble(Share::score).sum();
|
||||
var shares=raw.stream().map(s -> new Share(s.subject,s.score,(int)Math.round(s.score*100/total))).toList();
|
||||
var top=shares.getFirst();double first=top.score*100/total;
|
||||
double second=shares.size()>1 ? shares.get(1).score*100/total : 0;
|
||||
boolean player=shares.stream().anyMatch(s -> s.subject.kind.equals("player") && s.score*100/total>=20);
|
||||
long last=entries.values().stream().filter(i -> !i.subject.kind.equals("animals")).mapToLong(Influence::lastDay).max().orElse(day);
|
||||
String state=total<.8 ? "neutral"
|
||||
: day-last>14 && shares.stream().anyMatch(s -> !Set.of("nature","animals").contains(s.subject.kind) && s.score*100/total>=20) ? "abandoned"
|
||||
: top.subject.kind.equals("monsters") && !player ? "abandoned"
|
||||
: Set.of("nature","animals").contains(top.subject.kind) && !player ? "wild"
|
||||
: second>=20 && first-second<=10 ? "contested"
|
||||
: second>=20 ? "shared" : first>=50 ? "dominated" : "inhabited";
|
||||
return new View(state,shares);
|
||||
}
|
||||
public int size() {return chunks.size();}
|
||||
public boolean atCapacity() {return atCapacity || chunks.size()>=MAX_CHUNKS;}
|
||||
public void save() throws IOException {
|
||||
if (!dirty) return;
|
||||
var entries=chunks.entrySet().stream().map(e -> new Chunk(e.getKey(),new ArrayList<>(e.getValue().values()))).toList();
|
||||
previous=AtomicJson.write(path,previous,JSON.toJson(new Document(1,seed,entries)),MAX_BYTES);dirty=false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package fr.koka.demeure.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import fr.koka.demeure.DemeureService;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Count the confirmed block state, including wall variants and merged slabs. */
|
||||
@Mixin(BlockItem.class)
|
||||
public abstract class PlacementMixin {
|
||||
@WrapOperation(method = "place", at = @At(value = "INVOKE", target =
|
||||
"Lnet/minecraft/world/level/block/Block;setPlacedBy(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;)V"))
|
||||
private void demeure$placed(Block block, Level level, BlockPos pos,
|
||||
BlockState state, LivingEntity placer, ItemStack stack, Operation<Void> original) {
|
||||
original.call(block, level, pos, state, placer, stack);
|
||||
if (placer instanceof ServerPlayer player) {
|
||||
DemeureService.placed(player,pos,state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"required":true,"package":"fr.koka.demeure.mixin","compatibilityLevel":"JAVA_25","mixins":["PlacementMixin"],"injectors":{"defaultRequire":1}}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "demeure",
|
||||
"version": "${version}",
|
||||
"name": "Demeure",
|
||||
"description": "Living footprints, activity and attachment to places. Sanctuary adaptation.",
|
||||
"authors": ["KOKA99CAB", "Koka"],
|
||||
"license": "GPL-3.0-or-later",
|
||||
"environment": "*",
|
||||
"entrypoints": {"main": ["fr.koka.demeure.DemeureService"]},
|
||||
"mixins": ["demeure.mixins.json"],
|
||||
"depends": {"fabricloader": ">=${loader}", "minecraft": "${minecraft}", "java": ">=25", "fabric-api": "*"}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package fr.koka.demeure;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class DemeureSmoke {
|
||||
public static void main(String[] args) throws Exception {
|
||||
var dir=Files.createTempDirectory("demeure-smoke");var path=dir.resolve("footprints.json");
|
||||
var store=new FootprintStore(path,42);
|
||||
var key=new FootprintStore.Key("minecraft:overworld",-2,3);
|
||||
var player=new FootprintStore.Subject("player",UUID.randomUUID().toString(),"Inhabitant");
|
||||
require(store.view(key,20000)==null,"Reading an unknown chunk does not invent habitation");
|
||||
require(store.touch(key,player,6,20000)==6 && store.touch(key,player,5,20000)==2,"Daily accepted budget is eight");
|
||||
require(store.touch(key,player,1,19999)==0,"Clock rollback cannot reset budget");
|
||||
store.save();store=new FootprintStore(path,42);
|
||||
require(store.touch(key,player,1,20000)==0,"Restart does not reset budget");
|
||||
var original=store.view(key,20000);
|
||||
require(original.state().equals("dominated"),"Strong inhabitant influence is represented without a claim");
|
||||
double decayed=score(store.view(key,20100),player);
|
||||
require(decayed<8 && score(store.view(key,20014),player)==8,"Fourteen real days of grace then decay");
|
||||
store.touch(key,player,.1,20100);
|
||||
require(Math.abs(score(store.view(key,20100),player)-(decayed+.1)*1.08)<1e-8,"Returning materializes decay instead of resurrecting stale influence");
|
||||
require(score(original,player)==8,"Snapshots are immutable");
|
||||
store.save();require(new FootprintStore(path,42).view(key,20100).equals(store.view(key,20100)),"Exact persistence round trip");
|
||||
require(store.view(new FootprintStore.Key("minecraft:the_nether",-2,3),20100)==null,"Dimensions are independent");
|
||||
var other=new FootprintStore.Subject("player",UUID.randomUUID().toString(),"Neighbour");
|
||||
var shared=new FootprintStore.Key("minecraft:overworld",1,1);
|
||||
store.touch(shared,player,2,20100);store.touch(shared,other,2,20100);
|
||||
require(store.view(shared,20100).state().equals("contested"),"Close equal influences remain contested, never owned");
|
||||
require(store.view(shared,20115).state().equals("abandoned"),"Absence is evaluated on real days");
|
||||
var animals=new FootprintStore.Key("minecraft:overworld",2,2);
|
||||
store.touch(animals,new FootprintStore.Subject("animals","animals",""),2,20100);
|
||||
require(store.view(animals,20100).state().equals("wild"),"Animal presence gives a wild footprint");
|
||||
for (int i=0;i<40;i++) store.touch(shared,new FootprintStore.Subject("player",new UUID(0,i+1).toString(),"Visitor"),.1,20100);
|
||||
require(store.atCapacity() && store.view(shared,20100).influences().size()==32,"Subject cap preserves existing contributors and nature");
|
||||
var bytes=Files.readAllBytes(path);Files.writeString(path,"{broken");
|
||||
var held=store;failure(held::save);failure(() -> new FootprintStore(path,42));
|
||||
require(Files.readString(path).equals("{broken"),"External corruption is preserved");
|
||||
Files.write(path,bytes);failure(() -> new FootprintStore(path,43));
|
||||
System.out.println("Demeure smoke passed: real-day budget, rollback, decay, persistence, dimensions, territories and capacity.");
|
||||
}
|
||||
private static double score(FootprintStore.View view,FootprintStore.Subject subject) {return view.influences().stream().filter(s -> s.subject().equals(subject)).findFirst().orElseThrow().score();}
|
||||
private static void require(boolean value,String message) {if (!value) throw new AssertionError(message);}
|
||||
private static void failure(Checked action) throws Exception {boolean failed=false;try {action.run();}catch (Exception expected) {failed=true;}require(failed,"Invalid state must be refused");}
|
||||
private interface Checked {void run() throws Exception;}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Just Enough Items — Sanctuary preview
|
||||
|
||||
Source officielle : https://github.com/mezz/JustEnoughItems/tree/26.2
|
||||
Commit : `aae2dfcfb82e6b5bce787ba72bb2eda7339b274f`.
|
||||
Version source 30.32.0 ; fork `30.32.0-sanctuary.2` pour **Minecraft 26.3-pre-2**
|
||||
(Fabric normalise ce nom en `26.3-pre.2`), Loader 0.19.5, API 0.160.0+26.3, Java 25.
|
||||
Licence MIT, copyright 2014–2015 mezz. `LICENSE.txt` est inclus sans modification.
|
||||
|
||||
Le fork adapte les entrées SDL, les pipelines RenderPearl, les recettes devenues
|
||||
un registre, les tags d’ingrédients et les recettes d’alchimie natives. Les valeurs
|
||||
contextuelles de combustible/compost ont une page descriptive ; une valeur numérique
|
||||
n’est exposée que si le composant est explicitement constant.
|
||||
|
||||
L’interface de pont `mezz.jei.api.sanctuary.CatalogueAccess` laisse Sanctuary gérer
|
||||
l’accès au catalogue, la visibilité des recettes, les favoris et l’inventaire étendu.
|
||||
Les recettes apprises sont consultables avant la découverte de leur poste de travail.
|
||||
Les recettes cachées restent cachées lorsqu’un favori tente de les rouvrir directement.
|
||||
Le vieux rectangle cliquable de l’inventaire 26.2 est retiré ; R/U et Blocodex restent
|
||||
les points d’entrée. Les autres interfaces et le transfert réseau restent ceux de JEI.
|
||||
|
||||
## Reconstruction
|
||||
|
||||
`./gradlew :jei:build` exécute `scripts/prepare_jei.py` à la configuration lorsque
|
||||
l’empreinte source/patch change. Le script télécharge le dépôt officiel seulement si
|
||||
le cache ignoré manque, extrait le commit fixé et applique `patches/26.3.patch` dans
|
||||
`build/jei-fork`. Aucun source tiers téléchargé ni JAR n’est versionné.
|
||||
La première reconstruction nécessite Internet, Git, patch et Python 3.
|
||||
`./gradlew :jei:sourcesJar` produit aussi les sources Java effectivement compilées.
|
||||
|
||||
Le mod JEI est imbriqué dans le JAR Sanctuary avec deux dépendances :
|
||||
|
||||
- `net.mezzdev:baked-substring-index:0.1.0`, MIT, James Mitchell.
|
||||
https://github.com/mezz/baked-substring-index — licence dans `licenses/baked-substring-index.txt`.
|
||||
- `net.mezzdev:suffixtree:1.1.0`, Apache-2.0, Alessandro Bahgat Shehata.
|
||||
https://github.com/mezz/generalized-suffix-tree — licence dans `licenses/suffixtree.txt`.
|
||||
|
||||
Amecs 26.2 sert uniquement à compiler le point d’intégration optionnel historique ;
|
||||
il n’est ni embarqué ni annoncé compatible 26.3. Le client vérifié utilise les touches
|
||||
natives sans Amecs. Les fichiers de licences des dépendances sont ajoutés au JAR JEI.
|
||||
Ce fork expérimental ne revendique pas une validation upstream de cette préversion.
|
||||
|
||||
La révision Sanctuary.2 retire le bouton de configuration du catalogue lorsque
|
||||
Sanctuary le demande (dessin, infobulle et interactions), et adapte les libellés
|
||||
FR/EN des favoris, de l’historique et des raccourcis. Les attributions, identifiants
|
||||
JEI, formats de réglages et mécanismes de recettes restent conservés.
|
||||
@@ -0,0 +1,36 @@
|
||||
plugins { id 'net.fabricmc.fabric-loom' }
|
||||
version='30.32.0-sanctuary.2'
|
||||
group='mezz.jei'
|
||||
base { archivesName='jei-sanctuary-26.3-pre-2' }
|
||||
repositories { mavenCentral(); maven { url='https://maven.siphalor.de/' }; maven { url='https://maven.blamejared.com/' } }
|
||||
providers.exec { workingDir rootProject.projectDir; commandLine 'python3', 'scripts/prepare_jei.py' }.result.get()
|
||||
def upstream=rootProject.file('build/jei-fork')
|
||||
sourceSets.main.java.setSrcDirs(['Common/src/main/java','Common/src/api/java','Fabric/src/main/java','Fabric/src/api/java','Library/src/main/java','Gui/src/main/java'].collect{new File(upstream,it)})
|
||||
sourceSets.main.resources.setSrcDirs(['Common/src/main/resources','Fabric/src/main/resources','Library/src/main/resources','Gui/src/main/resources'].collect{new File(upstream,it)})
|
||||
loom { accessWidenerPath=new File(upstream,'Fabric/src/main/resources/jei.accesswidener') }
|
||||
dependencies {
|
||||
minecraft 'com.mojang:minecraft:26.3-pre-2'
|
||||
implementation 'net.fabricmc:fabric-loader:0.19.5'
|
||||
implementation 'net.fabricmc.fabric-api:fabric-api:0.160.0+26.3'
|
||||
implementation 'org.jetbrains:annotations:26.0.2'
|
||||
implementation 'org.jspecify:jspecify:1.0.0'
|
||||
implementation 'net.mezzdev:baked-substring-index:0.1.0'
|
||||
include 'net.mezzdev:baked-substring-index:0.1.0'
|
||||
implementation 'net.mezzdev:suffixtree:1.1.0'
|
||||
include 'net.mezzdev:suffixtree:1.1.0'
|
||||
compileOnly 'de.siphalor.amecs.amecs-key-modifiers:amecs-key-modifiers-mc26.2.0:1.0.2'
|
||||
}
|
||||
java { toolchain { languageVersion=JavaLanguageVersion.of(25) } }
|
||||
tasks.withType(JavaCompile).configureEach { options.encoding='UTF-8'; options.compilerArgs+=['-Xmaxerrs','300'] }
|
||||
processResources {
|
||||
def values=[modId:'jei', version:project.version, modName:'Just Enough Items — Sanctuary preview', modDescription:'Experimental source port for Sanctuary, Minecraft 26.3-pre-2.', modAuthor:'mezz', curseHomepageUrl:'https://www.curseforge.com/minecraft/mc-mods/jei', githubUrl:'https://github.com/mezz/JustEnoughItems', fabricLoaderVersionRange:'>=0.19.5', fabricApiVersionRange:'>=0.160.0+26.3', modJavaVersion:'25', amecsVersionFabric:'1.0.2']
|
||||
inputs.properties(values)
|
||||
filesMatching('fabric.mod.json') { expand(values) }
|
||||
duplicatesStrategy=DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
tasks.withType(Jar).configureEach {
|
||||
from(new File(upstream,'LICENSE.txt'))
|
||||
from('PROVENANCE.md')
|
||||
from('licenses') { into 'licenses' }
|
||||
}
|
||||
java { withSourcesJar() }
|
||||
@@ -0,0 +1,16 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 James Mitchell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2012 Alessandro Bahgat Shehata
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
plugins { id 'net.fabricmc.fabric-loom' }
|
||||
group='fr.koka.sanctuarytest'
|
||||
version=rootProject.mod_version
|
||||
base { archivesName='sanctuary-test' }
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
|
||||
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
|
||||
implementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
|
||||
implementation project(':sanctuary')
|
||||
}
|
||||
java { toolchain.languageVersion=JavaLanguageVersion.of(25); withSourcesJar() }
|
||||
tasks.withType(JavaCompile).configureEach { options.encoding='UTF-8'; options.release=25 }
|
||||
processResources {
|
||||
def values=[version:project.version,minecraft:rootProject.minecraft_version.replace('-pre-','-pre.'),loader:rootProject.loader_version]
|
||||
inputs.properties(values)
|
||||
filesMatching('fabric.mod.json') { expand(values) }
|
||||
}
|
||||
loom { runs { client { runDir 'run/quick-test' } } }
|
||||
tasks.named('test') { failOnNoDiscoveredTests=false }
|
||||
tasks.named('jar') { from(rootProject.file('LICENSE')) { rename { 'LICENSE_sanctuary_test' } } }
|
||||
@@ -0,0 +1,24 @@
|
||||
package fr.koka.sanctuarytest;
|
||||
|
||||
import fr.koka.sanctuary.client.IntroClient;
|
||||
import java.util.*;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworking;
|
||||
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
|
||||
|
||||
public final class QuickTestClient implements ClientModInitializer {
|
||||
private final Set<Screen> initialized=Collections.newSetFromMap(new WeakHashMap<>());
|
||||
public void onInitializeClient(){
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(QuickTestMod.Mode.TYPE,(payload,context)->
|
||||
context.client().execute(()->IntroClient.setQuickTestIntro(payload.quick())));
|
||||
ScreenEvents.AFTER_INIT.register((minecraft,screen,width,height)->{
|
||||
if(screen instanceof CreateWorldScreen creation&&initialized.add(screen)){
|
||||
var state=creation.getUiState();
|
||||
state.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat"))
|
||||
.findFirst().ifPresent(state::setWorldType);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package fr.koka.sanctuarytest;
|
||||
|
||||
import fr.koka.sanctuary.testing.QuickTestWorld;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.networking.v1.*;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
public final class QuickTestMod implements ModInitializer {
|
||||
public record Mode(boolean quick) implements CustomPacketPayload {
|
||||
public static final Type<Mode> TYPE=new Type<>(Identifier.fromNamespaceAndPath("sanctuary_test","mode"));
|
||||
public static final StreamCodec<FriendlyByteBuf,Mode> CODEC=CustomPacketPayload.codec((p,b)->b.writeBoolean(p.quick),b->new Mode(b.readBoolean()));
|
||||
public Type<Mode> type(){return TYPE;}
|
||||
}
|
||||
public void onInitialize(){
|
||||
QuickTestWorld.enable();
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(Mode.TYPE,Mode.CODEC);
|
||||
ServerConfigurationConnectionEvents.CONFIGURE.register((handler,server)->{
|
||||
if(ServerConfigurationNetworking.canSend(handler,Mode.TYPE))
|
||||
ServerConfigurationNetworking.send(handler,new Mode(QuickTestWorld.active(server)));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"generator.sanctuary_test.flat": "Sanctuary — Quick Test"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"generator.sanctuary_test.flat": "Sanctuary — Test rapide"
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"sanctuary_test:flat"
|
||||
]
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"dimensions": {
|
||||
"minecraft:overworld": {
|
||||
"type": "minecraft:overworld",
|
||||
"generator": {
|
||||
"type": "minecraft:flat",
|
||||
"settings": {
|
||||
"biome": "minecraft:plains",
|
||||
"features": false,
|
||||
"lakes": false,
|
||||
"layers": [
|
||||
{
|
||||
"block": "minecraft:bedrock",
|
||||
"height": 1
|
||||
},
|
||||
{
|
||||
"block": "minecraft:stone",
|
||||
"height": 60
|
||||
},
|
||||
{
|
||||
"block": "minecraft:dirt",
|
||||
"height": 2
|
||||
},
|
||||
{
|
||||
"block": "minecraft:grass_block",
|
||||
"height": 1
|
||||
}
|
||||
],
|
||||
"structure_overrides": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:the_end"
|
||||
},
|
||||
"settings": "minecraft:end"
|
||||
}
|
||||
},
|
||||
"minecraft:the_nether": {
|
||||
"type": "minecraft:the_nether",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:multi_noise",
|
||||
"preset": "minecraft:nether"
|
||||
},
|
||||
"settings": "minecraft:nether"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "sanctuary_test",
|
||||
"version": "${version}",
|
||||
"name": "Sanctuary — Quick Test",
|
||||
"description": "Optional development profile: flat worlds, real Sanctuary systems, automatically skipped introduction.",
|
||||
"authors": ["KOKA99CAB"],
|
||||
"license": "GPL-3.0-or-later",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
"main": ["fr.koka.sanctuarytest.QuickTestMod"],
|
||||
"client": ["fr.koka.sanctuarytest.QuickTestClient"]
|
||||
},
|
||||
"depends": {"fabricloader": ">=${loader}", "minecraft": "${minecraft}", "java": ">=25", "fabric-api": "*", "sanctuary": "${version}"}
|
||||
}
|
||||
+288
-1
@@ -25,6 +25,11 @@ if (clientRenderTests) {
|
||||
java.srcDir('src/clientGametest/java')
|
||||
}
|
||||
tasks.named('runClientGameTest') {
|
||||
systemProperty('sanctuary.test.noVsync', providers.gradleProperty('sanctuaryClientNoVsync').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.quick', providers.gradleProperty('sanctuaryQuickTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.introVisualOnly', providers.gradleProperty('sanctuaryIntroVisualOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.atlasBaseline', providers.gradleProperty('sanctuaryAtlasBaseline').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.inventoryClient', providers.gradleProperty('sanctuaryInventoryClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.highland304', providers.gradleProperty('sanctuaryHighland304Visual').getOrElse('false'))
|
||||
}
|
||||
}
|
||||
@@ -90,9 +95,139 @@ tasks.named('runGameTest') {
|
||||
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 = [aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests']
|
||||
def focusClasses = [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 introClient = providers.gradleProperty('sanctuaryIntroClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('introClient', introClient)
|
||||
if (introClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Intro031ClientChecks') }
|
||||
def progressionClient = providers.gradleProperty('sanctuaryProgressionClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('progressionClient', progressionClient)
|
||||
def mapPerfClient = providers.gradleProperty('sanctuaryMapPerfClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('mapPerfClient', mapPerfClient)
|
||||
if (mapPerfClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Atlas026ClientChecks') }
|
||||
def lightsClient = providers.gradleProperty('sanctuaryLightsClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lightsClient', lightsClient)
|
||||
if (lightsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Dynamic030ClientChecks') }
|
||||
def skillsClient = providers.gradleProperty('sanctuarySkillsClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('skillsClient', skillsClient)
|
||||
if (skillsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Skills025ClientChecks') }
|
||||
def fogClient = providers.gradleProperty('sanctuaryFogClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
def sky056Client = providers.gradleProperty('sanctuarySky056ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
def mobHead060Client = providers.gradleProperty('sanctuaryMobHead060ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('mobHead060Client', mobHead060Client)
|
||||
if (mobHead060Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.MobHead060ClientChecks') }
|
||||
def atlas059Client = providers.gradleProperty('sanctuaryAtlas059ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('atlas059Client', atlas059Client)
|
||||
if (atlas059Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Atlas059ClientChecks') }
|
||||
def atlas058Client = providers.gradleProperty('sanctuaryAtlas058ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('atlas058Client', atlas058Client)
|
||||
if (atlas058Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Atlas058ClientChecks') }
|
||||
def weather057Client = providers.gradleProperty('sanctuaryWeather057ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('weather057Client', weather057Client)
|
||||
if (weather057Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Weather057ClientChecks') }
|
||||
inputs.property('sky056Client', sky056Client)
|
||||
if (sky056Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Sky056ClientChecks') }
|
||||
def strengthCarryClient = providers.gradleProperty('sanctuaryStrengthCarryClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('strengthCarryClient', strengthCarryClient)
|
||||
if (strengthCarryClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Strength055ClientChecks') }
|
||||
def combatClient = providers.gradleProperty('sanctuaryCombatClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('combatClient', combatClient)
|
||||
if (combatClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Combat054ClientChecks') }
|
||||
inputs.property('fogClient', fogClient)
|
||||
if (fogClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Fog053ClientChecks') }
|
||||
def starterNameClient = providers.gradleProperty('sanctuaryStarterNameClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('starterNameClient', starterNameClient)
|
||||
if (starterNameClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Starter052ClientChecks') }
|
||||
def head050Client = providers.gradleProperty('sanctuaryHead050ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('head050Client', head050Client)
|
||||
if (head050Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Head050ClientChecks') }
|
||||
def boatsHeadClient = providers.gradleProperty('sanctuaryBoatsHeadClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('boatsHeadClient', boatsHeadClient)
|
||||
if (boatsHeadClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.BoatsHead049ClientChecks') }
|
||||
def poultryClient = providers.gradleProperty('sanctuaryPoultryClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('poultryClient', poultryClient)
|
||||
if (poultryClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Poultry048ClientChecks') }
|
||||
def starInspectionClient = providers.gradleProperty('sanctuaryStarInspectionClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('starInspectionClient', starInspectionClient)
|
||||
if (starInspectionClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.StarInspection047ClientChecks') }
|
||||
def skyFlightClient = providers.gradleProperty('sanctuarySkyFlightClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('skyFlightClient', skyFlightClient)
|
||||
if (skyFlightClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.SkyFlight046ClientChecks') }
|
||||
def titleClient = providers.gradleProperty('sanctuaryTitleClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('titleClient', titleClient)
|
||||
if (titleClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Title045ClientChecks') }
|
||||
def headBlocksClient = providers.gradleProperty('sanctuaryHeadBlocksClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('headBlocksClient', headBlocksClient)
|
||||
if (headBlocksClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.HeadBlocks043ClientChecks') }
|
||||
def lanCarryClient = providers.gradleProperty('sanctuaryLanCarryClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('lanCarryClient', lanCarryClient)
|
||||
if (lanCarryClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.LanCarry042ClientChecks') }
|
||||
def operatorClient = providers.gradleProperty('sanctuaryOperatorClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('operatorClient', operatorClient)
|
||||
if (operatorClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Operator040ClientChecks') }
|
||||
def foodKnowledgeClient = providers.gradleProperty('sanctuaryFoodKnowledgeClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('foodKnowledgeClient', foodKnowledgeClient)
|
||||
if (foodKnowledgeClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.FoodPanorama039ClientChecks') }
|
||||
def gravesClient = providers.gradleProperty('sanctuaryGravesClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('gravesClient', gravesClient)
|
||||
if (gravesClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.GravesFood038ClientChecks') }
|
||||
def realtimeClient = providers.gradleProperty('sanctuaryRealtimeClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('realtimeClient', realtimeClient)
|
||||
if (realtimeClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Realtime037ClientChecks') }
|
||||
def blueprintClient = providers.gradleProperty('sanctuaryBlueprintClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('blueprintClient', blueprintClient)
|
||||
if (blueprintClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Blueprint036ClientChecks') }
|
||||
def correctionsClient = providers.gradleProperty('sanctuaryCorrectionsClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('correctionsClient', correctionsClient)
|
||||
if (correctionsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Corrections036ClientChecks') }
|
||||
def collectionsClient = providers.gradleProperty('sanctuaryCollectionsClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('collectionsClient', collectionsClient)
|
||||
if (collectionsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Collections035ClientChecks') }
|
||||
def notificationsClient = providers.gradleProperty('sanctuaryNotificationsClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('notificationsClient', notificationsClient)
|
||||
if (notificationsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Notifications034ClientChecks') }
|
||||
def menusClient = providers.gradleProperty('sanctuaryMenusClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('menusClient', menusClient)
|
||||
if (menusClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Menus033ClientChecks') }
|
||||
def recipesClient = providers.gradleProperty('sanctuaryRecipesClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('recipesClient', recipesClient)
|
||||
if (recipesClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Recipes024ClientChecks') }
|
||||
def cosmeticsClient = providers.gradleProperty('sanctuaryHeadCosmeticsClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('cosmeticsClient', cosmeticsClient)
|
||||
if (cosmeticsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.HeadCosmetic021ClientChecks') }
|
||||
def familiarClient = providers.gradleProperty('sanctuaryFamiliarClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('familiarClient', familiarClient)
|
||||
if (familiarClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Familiar020ClientChecks') }
|
||||
def refonteClient = providers.gradleProperty('sanctuaryRefonteClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('refonteClient', refonteClient)
|
||||
if (refonteClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Companion032ClientChecks') }
|
||||
def companionsClient = providers.gradleProperty('sanctuaryCompanionsClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('companionsClient', companionsClient)
|
||||
if (companionsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Companion019ClientChecks') }
|
||||
def accessoriesClient = providers.gradleProperty('sanctuaryAccessoriesClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('accessoriesClient', accessoriesClient)
|
||||
if (accessoriesClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Accessory018ClientChecks') }
|
||||
def movementClient = providers.gradleProperty('sanctuaryMovementClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('movementClient', movementClient)
|
||||
if (movementClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Movement017ClientChecks') }
|
||||
def tutorialClient = providers.gradleProperty('sanctuaryTutorialClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('tutorialClient', tutorialClient)
|
||||
if (tutorialClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Tutorial016ClientChecks') }
|
||||
def sortingClient = providers.gradleProperty('sanctuarySortingClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sortingClient', sortingClient)
|
||||
if (sortingClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Inventory014ClientChecks') }
|
||||
def inlineClient = providers.gradleProperty('sanctuaryInlineClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('inlineClient', inlineClient)
|
||||
if (inlineClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Progression013ClientChecks') }
|
||||
def inventoryClient = providers.gradleProperty('sanctuaryInventoryClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('inventoryClient', inventoryClient)
|
||||
def cycleClient = providers.gradleProperty('sanctuaryCycleClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('cycleClient', cycleClient)
|
||||
if (cycleClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Cycle011ClientChecks') }
|
||||
if (inventoryClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Inventory010ClientChecks') }
|
||||
|
||||
if (progressionClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Progression003ClientChecks') }
|
||||
|
||||
inputs.property('villageOnly', villageOnly)
|
||||
def atlasOnly = providers.gradleProperty('sanctuaryAtlasOnly').map { it.toBoolean() }.getOrElse(false)
|
||||
@@ -153,6 +288,10 @@ tasks.named('processGametestResources') {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':jei')
|
||||
include project(':jei')
|
||||
implementation project(':demeure')
|
||||
include project(':demeure')
|
||||
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
|
||||
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
|
||||
implementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
|
||||
@@ -609,6 +748,14 @@ tasks.register('expansionOcean23Smoke', JavaExec) {
|
||||
}
|
||||
tasks.named('check') { dependsOn('expansionOcean23Smoke') }
|
||||
|
||||
tasks.register('ancientExpeditions001Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.expansion.AncientExpeditions001Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('ancientExpeditions001Smoke') }
|
||||
|
||||
[
|
||||
secrets23Smoke: 'fr.koka.sanctuary.worldgen.mineral.Secrets23Smoke',
|
||||
structureCensusSmoke: 'fr.koka.sanctuary.knowledge.structures.StructureCensusSmoke',
|
||||
@@ -703,3 +850,143 @@ tasks.register('balancedRelief303Smoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.worldgen.BalancedRelief303Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('balancedRelief303Smoke') }
|
||||
|
||||
// PROG-01: pure contract checks plus native server/player tests.
|
||||
tasks.register('progression003Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.progression.ProgressionSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('progression003Smoke') }
|
||||
|
||||
// MAP-01: durable observation and progression migration contracts.
|
||||
tasks.register('atlas005Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.atlas.AtlasSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('atlas005Smoke') }
|
||||
|
||||
// CYCLE-01 / FACTION-01: durable charge and reset contract.
|
||||
tasks.register('cycle011Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.cycle.CycleSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('cycle011Smoke') }
|
||||
|
||||
// First tutorial actions: bounded rewards and independent durable claims.
|
||||
tasks.register('tutorial016Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.tutorial.TutorialSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('tutorial016Smoke') }
|
||||
|
||||
tasks.register('movement017Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.progression.Movement017Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('movement017Smoke') }
|
||||
|
||||
tasks.register('inventoryDrag018Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.client.InventoryDragPathSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('inventoryDrag018Smoke') }
|
||||
|
||||
tasks.register('companion019Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.companion.Companion019Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('companion019Smoke') }
|
||||
|
||||
// Visual-light spatial index, dirty budget and dropped-item stress checks.
|
||||
tasks.register('dynamic030Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.lighting.Dynamic030Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('dynamic030Smoke') }
|
||||
|
||||
// INTRO-01: reversible UUID encoding, accelerating score and durable first arrival.
|
||||
tasks.register('intro031Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.progression.Intro031Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('intro031Smoke') }
|
||||
|
||||
if (providers.gradleProperty('sanctuaryQuickTests').map { it.toBoolean() }.getOrElse(false)) {
|
||||
dependencies { gametestRuntimeOnly project(':sanctuary-test') }
|
||||
}
|
||||
|
||||
tasks.register('notifications034Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.notifications.NotificationFeedSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('notifications034Smoke') }
|
||||
|
||||
tasks.register('weather057Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.weather.Weather057Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('weather057Smoke') }
|
||||
|
||||
|
||||
tasks.register('collections035Catalogue', Exec) {
|
||||
group = 'verification'
|
||||
workingDir rootProject.projectDir
|
||||
commandLine 'python3', 'scripts/compile_collections.py'
|
||||
}
|
||||
tasks.named('check') { dependsOn('collections035Catalogue') }
|
||||
|
||||
tasks.register('notifications036Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.notifications.NotificationLayoutSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('notifications036Smoke') }
|
||||
|
||||
|
||||
tasks.register('realtime037Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.realtime.Realtime037Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('realtime037Smoke') }
|
||||
|
||||
tasks.register('poultry048Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.boats.FlightBalance048Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('poultry048Smoke') }
|
||||
|
||||
// Personal map markers: durable discoveries, independent identities and bounded data.
|
||||
tasks.register('atlas059Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.atlas.AtlasMarker059Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('atlas059Smoke') }
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
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.networking.v1.ClientConfigurationNetworking;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
public final class Intro031ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
UUID testUuid=UUID.fromString("01234567-89ab-cdef-fedc-ba9876543210");
|
||||
Screen previous=c.computeOnClient(m -> m.gui.screen());
|
||||
int scale=c.computeOnClient(m -> m.options.guiScale().get());
|
||||
String language=c.computeOnClient(m -> m.getLanguageManager().getSelected());
|
||||
var clock=new AtomicLong();var completed=new AtomicInteger();
|
||||
c.runOnClient(m -> m.gui.setScreen(new IntroScreen(testUuid,skipped -> completed.incrementAndGet(),clock::get)));
|
||||
c.waitTick();c.takeScreenshot("sanctuary-beta031-intro-00-black");
|
||||
c.runOnClient(m -> m.gui.screen().onClose());
|
||||
require(completed.get()==0,"Opening cannot be skipped accidentally");
|
||||
for (int seconds:new int[]{3,7,13,20,23,25,27,29}) {
|
||||
clock.set(seconds*1_000_000_000L);c.waitTicks(2);
|
||||
c.takeScreenshot("sanctuary-beta031-intro-"+seconds);
|
||||
}
|
||||
clock.set(24_800_000_000L);
|
||||
c.runOnClient(m -> { m.options.guiScale().set(0);m.resizeGui(); });
|
||||
c.waitTick();c.takeScreenshot("sanctuary-beta031-intro-small");
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
});
|
||||
c.waitTick();c.takeScreenshot("sanctuary-beta031-intro-fr");
|
||||
clock.set(29_600_000_000L);c.waitTicks(3);
|
||||
require(completed.get()==1,"Completion exactly once despite extra ticks");
|
||||
c.runOnClient(m -> m.gui.screen().onClose());require(completed.get()==1,"Skip cannot double-send completion");
|
||||
c.runOnClient(m -> {m.gui.setScreen(previous);m.options.guiScale().set(scale);m.resizeGui();});
|
||||
// Removing a screen (disconnect/replacement) must clean up without accepting the connection.
|
||||
c.runOnClient(m -> m.gui.setScreen(new IntroScreen(testUuid,skipped -> completed.incrementAndGet())));
|
||||
c.waitTicks(2);c.runOnClient(m -> m.gui.setScreen(previous));
|
||||
require(completed.get()==1,"Unexpected removal never acknowledges arrival");
|
||||
|
||||
if(Boolean.getBoolean("sanctuary.test.introVisualOnly")) {
|
||||
c.runOnClient(m -> { m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager()); });
|
||||
System.out.println("INTRO031_VISUAL_PASS: spectral glyphs, circular spiral, expanding portal, white frame, resize");
|
||||
return;
|
||||
}
|
||||
var armed=new AtomicBoolean(true);var submitted=new AtomicBoolean();var observed=new AtomicBoolean();
|
||||
var sawWorldUnderFlash=new AtomicBoolean();
|
||||
var count=new AtomicInteger();var introRef=new java.util.concurrent.atomic.AtomicReference<IntroScreen>();
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if(!armed.get())return;
|
||||
if(m.player!=null && IntroSpawnFlash.active()) sawWorldUnderFlash.set(true);
|
||||
if(m.gui.screen() instanceof HelloWorldScreen screen && submitted.compareAndSet(false,true)) {
|
||||
require(m.player==null && m.getSingleplayerServer().getPlayerCount()==0,"Form before spawn");
|
||||
// Early acknowledgements cannot bypass character creation.
|
||||
ClientConfigurationNetworking.send(new ProgressionPayloads.IntroFinished());
|
||||
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);
|
||||
}
|
||||
if(m.gui.screen() instanceof IntroScreen screen && !screen.isFinished()) {
|
||||
if(introRef.getAndSet(screen)!=screen)count.incrementAndGet();
|
||||
require(m.player==null && m.level==null,"No client world or player during the cinematic");
|
||||
require(m.getSingleplayerServer().getPlayerCount()==0,"No server player during the cinematic");
|
||||
if(observed.compareAndSet(false,true)) {
|
||||
require(screen.uuid().equals(m.getGameProfile().id()),"Server UUID reaches the real screen");
|
||||
m.getSingleplayerServer().execute(() -> {
|
||||
var identity=ProgressionService.session(m.getSingleplayerServer()).inhabitants.get(screen.uuid());
|
||||
require(identity!=null && !identity.arrived(),"Validated identity precedes hello announcement");
|
||||
});
|
||||
// A repeated form packet while waiting must not reopen the form or duplicate the identity.
|
||||
ClientConfigurationNetworking.send(new ProgressionPayloads.Create("duplicate",-1));
|
||||
}
|
||||
}
|
||||
});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave save;
|
||||
try {
|
||||
try(var world=c.worldBuilder().adjustSettings(settings -> {
|
||||
settings.setName("Sanctuary intro031 "+UUID.randomUUID());settings.setSeed("42");
|
||||
settings.setWorldType(settings.getNormalPresetList().stream()
|
||||
.filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary"))
|
||||
.findFirst().orElseThrow());
|
||||
settings.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
save=world.getWorldSave();world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && m.gui.screen()==null && ProgressionClient.active(),1200);
|
||||
require(observed.get() && count.get()==1,"Real first arrival plays one complete cinematic");
|
||||
require(sawWorldUnderFlash.get(),"White exposure bridges the real spawn");
|
||||
c.waitFor(m -> !IntroSpawnFlash.active(),400);
|
||||
c.takeScreenshot("sanctuary-beta031-spawn-after-white");
|
||||
world.getServer().runOnServer(server -> {
|
||||
var player=server.getPlayerList().getPlayers().getFirst();
|
||||
require(ProgressionService.session(server).inhabitants.get(player.getUUID()).arrived(),"Join consumes first arrival");
|
||||
require(!ProgressionService.session(server).inhabitants.get(player.getUUID()).bio().equals("duplicate"),"Repeated form ignored");
|
||||
server.saveEverything(true,true,true);
|
||||
});
|
||||
}
|
||||
try(var world=save.open()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && ProgressionClient.active(),1200);
|
||||
require(count.get()==1,"Reconnection bypasses cinematic");
|
||||
c.waitFor(m -> m.gui.screen()==null,1200);
|
||||
c.runOnClient(m -> require(!(m.gui.screen() instanceof IntroScreen),"Existing inhabitant goes directly to play"));
|
||||
armed.set(false);
|
||||
c.runOnClient(m -> {
|
||||
m.gui.setScreen(new net.minecraft.client.gui.screens.ChatScreen("/sanctuary intro",false));
|
||||
m.gui.screen().keyPressed(ENTER);
|
||||
});
|
||||
c.waitForScreen(IntroScreen.class);
|
||||
c.runOnClient(m -> {
|
||||
require(((IntroScreen)m.gui.screen()).uuid().equals(m.player.getUUID()),"Chat command replays the current UUID");
|
||||
require(!m.gui.screen().isPauseScreen(),"The audio score also advances in replay");
|
||||
});
|
||||
c.waitFor(m -> m.gui.screen()==null,1200);
|
||||
c.waitFor(m -> !IntroSpawnFlash.active(),400);
|
||||
c.runOnClient(m -> require(ProgressionClient.active(),"Preview returns to play without changing identity"));
|
||||
}
|
||||
} finally {
|
||||
armed.set(false);
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.options.guiScale().set(scale);m.resizeGui();
|
||||
});
|
||||
}
|
||||
System.out.println("INTRO031_CLIENT_PASS: renders, resize, finish, removal, pre-spawn handshake, duplicates, reconnect");
|
||||
}
|
||||
private static void require(boolean value,String message) { if(!value)throw new AssertionError(message); }
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.*;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.Pose;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class Accessory018ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if(armed.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);
|
||||
}
|
||||
});
|
||||
int scale=c.computeOnClient(m -> m.options.guiScale().get());var camera=c.computeOnClient(m -> m.options.getCameraType());
|
||||
String language=c.computeOnClient(m -> m.getLanguageManager().getSelected());
|
||||
var postureKey=c.computeOnClient(m -> InputConstants.getKey(KeyMapping.get("sanctuary.progression.lying_key").saveString()));
|
||||
var forward=c.computeOnClient(m -> InputConstants.getKey(m.options.keyUp.saveString()));
|
||||
int letterW=c.computeOnClient(m -> KeyMapping.get("sanctuary.progression.lying_key").getDefaultKey().getValue());
|
||||
try(var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Sanctuary beta018 accessories "+UUID.randomUUID());
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && ProgressionClient.active() && m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
BlockPos floor=server.computeOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().above(3);
|
||||
for(int x=-10;x<=10;x++)for(int z=-10;z<=10;z++)for(int y=-1;y<5;y++)p.level().setBlockAndUpdate(at.offset(x,y,z),(y==-1 ? Blocks.STONE : Blocks.AIR).defaultBlockState());
|
||||
p.teleportTo(at.getX()+.5,at.getY(),at.getZ()+.5);p.getInventory().clearContent();
|
||||
p.getInventory().setItem(0,new ItemStack(Items.WOLF_SPAWN_EGG,3));p.getInventory().setItem(1,new ItemStack(AccessoryService.ZERO_CAPE));p.getInventory().setItem(2,new ItemStack(Items.PISTON,10));
|
||||
p.inventoryMenu.broadcastChanges();return at;
|
||||
});
|
||||
c.waitFor(m -> m.player.onGround() && m.player.getInventory().getItem(0).is(Items.WOLF_SPAWN_EGG),200);
|
||||
c.runOnClient(m -> {m.options.guiScale().set(2);m.resizeGui();m.options.keyUp.setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_UP));KeyMapping.get("sanctuary.progression.lying_key").setKey(InputConstants.Type.KEYBOARD.getOrCreate(letterW));KeyMapping.resetMapping();m.gui.setScreen(new InventoryScreen(m.player));});c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
require(m.player.inventoryMenu.slots.size()==67,"Client receives three actual equipment slots after overflow");
|
||||
require(SanctuaryInventoryClient.header().getString().equals(net.minecraft.network.chat.Component.translatable("container.inventory").getString()),"Inventory heading has no rows suffix");
|
||||
for(int i=64;i<67;i++)require(m.player.inventoryMenu.getSlot(i).x==78 && m.player.inventoryMenu.getSlot(i).y==24+18*(66-i),"Visual order: head, cape, egg; storage indices are unchanged");
|
||||
require(m.player.inventoryMenu.getSlot(45).x==78 && m.player.inventoryMenu.getSlot(45).y==78,"Offhand is fourth and last");
|
||||
});c.takeScreenshot("sanctuary-beta018-empty-slots");
|
||||
click(c,36,true);click(c,37,true);
|
||||
c.waitFor(m -> AccessoryAccess.of(m.player).getItem(0).is(Items.WOLF_SPAWN_EGG) && AccessoryAccess.of(m.player).getItem(1).is(AccessoryService.ZERO_CAPE),200);
|
||||
click(c,38,false);click(c,66,false);click(c,38,false);
|
||||
c.waitFor(m -> AccessoryAccess.of(m.player).getItem(2).is(Items.PISTON) && m.player.inventoryMenu.getCarried().isEmpty(),200);
|
||||
c.waitFor(m -> m.player.getAttachedOrElse(AccessoryService.APPEARANCE,AccessoryService.Appearance.EMPTY).head().is(Items.PISTON),200);
|
||||
c.runOnClient(m -> {
|
||||
var state=avatar(m);var head=((HeadCosmeticState.Access)state).sanctuary$headCosmetic();
|
||||
require(!head.block.isEmpty() && head.pistonHead.isEmpty(),"Piston uses block model in resting state");
|
||||
require(state.skin.cape()!=null && state.skin.cape().texturePath().toString().contains("zero_cape"),"Cape attachment selects native cape texture");
|
||||
});c.takeScreenshot("sanctuary-beta018-equipped-slots");
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.setInvulnerableTime(0);p.hurtServer(p.level(),p.damageSources().generic(),1);});
|
||||
c.waitFor(m -> m.player.hurtTime>0 && !((HeadCosmeticState.Access)avatar(m)).sanctuary$headCosmetic().pistonHead.isEmpty(),100);
|
||||
for(Item cosmetic:List.of(Items.DIAMOND_SWORD,Items.PLAYER_HEAD,Items.CHEST,Items.PISTON)) {
|
||||
server.runOnServer(s -> AccessoryAccess.of(s.getPlayerList().getPlayers().getFirst()).setItem(2,new ItemStack(cosmetic)));
|
||||
c.waitFor(m -> m.player.getAttachedOrElse(AccessoryService.APPEARANCE,AccessoryService.Appearance.EMPTY).head().is(cosmetic),200);
|
||||
c.runOnClient(m -> {var head=((HeadCosmeticState.Access)avatar(m)).sanctuary$headCosmetic();require(!head.item.isEmpty() || !head.block.isEmpty(),"Renderable cosmetic: "+cosmetic);});
|
||||
}
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressEffects.get(p);for(int i=0;i<5;i++)r=r.buy("inventory",1);p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");});
|
||||
c.waitFor(m -> ProgressionClient.progress.inventoryRows()==6,200);
|
||||
for(String locale:List.of("en_us","fr_fr")) {
|
||||
c.runOnClient(m -> {m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new InventoryScreen(m.player));});c.waitTick();
|
||||
c.takeScreenshot("sanctuary-beta018-six-rows-"+locale);
|
||||
}
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);
|
||||
c.waitFor(m -> m.level.entitiesForRendering().iterator().hasNext() && clientPet(m)!=null,200);
|
||||
var before=server.computeOnServer(s -> FamiliarService.active(s.getPlayerList().getPlayers().getFirst()).position());
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(floor.getX()+6.5,floor.getY(),floor.getZ()+.5);});
|
||||
c.waitTicks(30);
|
||||
require(server.computeOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);return pet!=null && pet.position().distanceToSqr(before)>.1 && pet.distanceToSqr(p)<20;}),"Familiar really walks toward moving owner");
|
||||
for(Item egg:List.of(Items.WOLF_SPAWN_EGG,Items.COW_SPAWN_EGG,Items.PARROT_SPAWN_EGG,Items.CREEPER_SPAWN_EGG,Items.COD_SPAWN_EGG,Items.GHAST_SPAWN_EGG)) {
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(0,new ItemStack(egg));});
|
||||
c.waitFor(m -> clientPet(m)!=null && clientPet(m).egg().is(egg),200);
|
||||
c.runOnClient(m -> {var state=(FamiliarRenderer.State)m.getEntityRenderDispatcher().extractEntity(clientPet(m),0);require(state.renderer!=null && state.appearance!=null && state.size>0 && state.size<=.65,"Native appearance resolves for "+egg);});
|
||||
}
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(0,new ItemStack(Items.WOLF_SPAWN_EGG));});
|
||||
c.waitFor(m -> clientPet(m)!=null && clientPet(m).egg().is(Items.WOLF_SPAWN_EGG),200);
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);pet.teleportNearOwner();});c.waitTicks(5);
|
||||
int particles=c.computeOnClient(m -> particleCount(m));
|
||||
c.runOnClient(m -> {var pet=clientPet(m);m.gameMode.interact(m.player,pet,new net.minecraft.world.phys.EntityHitResult(pet,pet.position().add(0,.4,0)),net.minecraft.world.InteractionHand.MAIN_HAND);});
|
||||
c.waitFor(m -> particleCount(m)>particles,100);
|
||||
c.runOnClient(m -> {m.options.setCameraType(CameraType.THIRD_PERSON_BACK);m.player.setXRot(15);});c.waitTicks(3);c.takeScreenshot("sanctuary-beta018-cape-and-companion");
|
||||
c.getInput().pressKey(letterW);c.waitFor(m -> LyingService.posture(m.player)==1 && m.player.getPose()==Pose.SWIMMING,200);
|
||||
c.runOnClient(m -> require(avatar(m).pose==Pose.SWIMMING && !ProgressionClient.progress.lying(),"Free W gesture uses mobile low pose without purchase"));
|
||||
c.waitTicks(15);c.takeScreenshot("sanctuary-beta018-free-lying");c.getInput().pressKey(letterW);c.waitFor(m -> LyingService.posture(m.player)==0,200);
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(4);ProgressionService.send(p,"");});
|
||||
c.waitFor(m -> m.player.experienceLevel==4,200);
|
||||
c.runOnClient(m -> {m.options.guiScale().set(1);m.resizeGui();ProgressionClient.open(null);});c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
require(widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).noneMatch(w -> key(w,"sanctuary.progression.lying")),"Free gesture is absent from paid aptitudes");
|
||||
var name=widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w -> key(w,"sanctuary.progression.resting")).findFirst().orElseThrow();
|
||||
widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> b.getX()==name.getX() && b.getY()>name.getY()).min(Comparator.comparingInt(Button::getY)).orElseThrow().onPress(enter());
|
||||
});c.waitFor(m -> ProgressionClient.progress.resting() && m.player.experienceLevel==0,200);
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);c.waitTicks(3);
|
||||
c.getInput().holdKey(o -> o.keyShift);c.getInput().pressKey(letterW);c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> LyingService.posture(m.player)==2,200);
|
||||
c.runOnClient(m -> {var state=avatar(m);require(state.pose==Pose.SLEEPING && state.bedOrientation!=null && state.swimAmount==0 && !m.player.isSleeping(),"Rest uses bed-sleep animation without initiating server sleep");});
|
||||
c.waitTicks(15);c.takeScreenshot("sanctuary-beta018-rest-sleep-animation");
|
||||
c.getInput().holdKey(o -> o.keyUp);c.waitFor(m -> LyingService.posture(m.player)==0,200);c.getInput().releaseKey(o -> o.keyUp);
|
||||
InventoryControls018Checks.run(c,server);
|
||||
GroupWork018Checks.run(c,server);
|
||||
System.out.println("Accessory018 native client passed: exact slots, native clicks, synchronized cape/piston, 1/6 rows FR/EN, living follower, six mob renderers, free W and paid bed-sleep rest");
|
||||
} finally {
|
||||
armed.set(false);c.getInput().releaseKey(o -> o.keyUp);c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.runOnClient(m -> {m.options.keyUp.setKey(forward);KeyMapping.get("sanctuary.progression.lying_key").setKey(postureKey);KeyMapping.resetMapping();m.options.setCameraType(camera);m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();});
|
||||
}
|
||||
}
|
||||
private static int particleCount(Minecraft m) {String count=m.particleEngine.countParticles();return Integer.parseInt(count.substring(count.lastIndexOf(' ')+1));}
|
||||
private static FamiliarEntity clientPet(Minecraft m) {for(var e:m.level.entitiesForRendering())if(e instanceof FamiliarEntity pet && !pet.isRemoved())return pet;return null;}
|
||||
private static AvatarRenderState avatar(Minecraft m) {return (AvatarRenderState)m.getEntityRenderDispatcher().extractEntity(m.player,0);}
|
||||
private static void click(ClientGameTestContext c,int index,boolean shift) {
|
||||
c.runOnClient(m -> {var screen=(AbstractContainerScreen<?>)m.gui.screen();var a=(AbstractContainerScreenAccessor)screen;var slot=screen.getMenu().getSlot(index);var w=m.getWindow();
|
||||
m.mouseHandler.onMove(w.handle(),(a.sanctuary$getLeftPos()+slot.x+8.0)*w.getScreenWidth()/w.getGuiScaledWidth(),(a.sanctuary$getTopPos()+slot.y+8.0)*w.getScreenHeight()/w.getGuiScaledHeight(),0,0);m.mouseHandler.handleAccumulatedMovement();
|
||||
});c.waitTick();if(shift)c.getInput().holdKey(o -> o.keyShift);
|
||||
c.runOnClient(m -> {var w=m.getWindow();m.mouseHandler.onButton(w.handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,shift ? InputConstants.MOD_SHIFT : 0),1);m.mouseHandler.onButton(w.handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,shift ? InputConstants.MOD_SHIFT : 0),0);});
|
||||
if(shift)c.getInput().releaseKey(o -> o.keyShift);c.waitTicks(2);
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(GuiEventListener widget) {return Stream.concat(Stream.of(widget),widget instanceof ContainerEventHandler p ? p.children().stream().flatMap(Accessory018ClientChecks::widgets) : Stream.empty());}
|
||||
private static boolean key(AbstractWidget widget,String key) {return widget.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key);}
|
||||
private static Button button(Screen screen,String key) {return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> key(b,key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter() {return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.atlas.AtlasService;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.events.ContainerEventHandler;
|
||||
import net.minecraft.client.gui.components.events.GuiEventListener;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonInfo;
|
||||
import net.minecraft.client.server.IntegratedServer;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
/** Real packets, operator transitions and map interaction, inside the existing progression scenario. */
|
||||
final class Atlas005ClientChecks {
|
||||
static void run(ClientGameTestContext context,TestServerContext server) {
|
||||
server.runOnServer(s -> {
|
||||
((IntegratedServer)s).setWorldAllowCommands(false);
|
||||
var player=s.getPlayerList().getPlayers().getFirst();
|
||||
fr.koka.demeure.DemeureService.record(player,player.blockPosition(),2);
|
||||
});
|
||||
int[] transparencySample=server.computeOnServer(s -> {
|
||||
var player=s.getPlayerList().getPlayers().getFirst();var chunk=player.chunkPosition();
|
||||
int x=chunk.x()+4,z=chunk.z();
|
||||
// Deterministic network fixture outside the passive survey radius; no world block is changed.
|
||||
byte[] colors=new byte[256];java.util.Arrays.fill(colors,(byte)1);
|
||||
colors[0]=0;colors[2]=net.minecraft.world.level.material.MapColor.GOLD.getPackedId(net.minecraft.world.level.material.MapColor.Brightness.NORMAL);
|
||||
AtlasService.observations(player).observe(new fr.koka.sanctuary.atlas.AtlasStore.Tile(x,z,colors));
|
||||
return new int[]{x*16,z*16};
|
||||
});
|
||||
context.runOnClient(c -> ProgressionClient.open(null,"history"));
|
||||
|
||||
context.waitForScreen(SanctuaryAtlasScreen.class);
|
||||
context.waitFor(c -> c.gui.screen() instanceof SanctuaryAtlasScreen a && a.status().equals("locked"),400);
|
||||
context.runOnClient(c -> require(((SanctuaryAtlasScreen)c.gui.screen()).observedTiles()==0,"Locked map sends no terrain to the real client"));
|
||||
server.runOnServer(s -> require(AtlasService.observations(s.getPlayerList().getPlayers().getFirst()).size()>0,"Surveys predate the map purchase"));
|
||||
context.takeScreenshot("sanctuary-beta007-map-locked");
|
||||
context.runOnClient(c -> ProgressionClient.open(null,"progress"));
|
||||
context.runOnClient(c -> descendants(c.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals("sanctuary.progression.unlock_cost"))
|
||||
.findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0)));
|
||||
context.waitFor(c -> ProgressionClient.progress.atlas(),400);
|
||||
context.runOnClient(c -> require(c.player.experienceLevel==1,"Real atlas purchase costs exactly four levels"));
|
||||
context.runOnClient(c -> ProgressionClient.open(null,"history"));
|
||||
context.waitFor(c -> c.gui.screen() instanceof SanctuaryAtlasScreen a && a.observedTiles()>0 && a.isComplete(),600);
|
||||
context.waitTicks(3);context.takeScreenshot("sanctuary-beta007-map-inhabitant");
|
||||
context.waitFor(c -> ((SanctuaryAtlasScreen)c.gui.screen()).footprintCount()>0,400);
|
||||
context.runOnClient(c -> {
|
||||
var map=(SanctuaryAtlasScreen)c.gui.screen();
|
||||
require(map.knownPixels()<map.observedTiles()*256,"Real server packets contain partially explored chunks");
|
||||
int sx=transparencySample[0],sz=transparencySample[1];
|
||||
require(map.renderedPixelAt(sx,sz)==0 && map.renderedPixelAt(sx+1,sz)==0,
|
||||
"Unknown and surveyed void are transparent in the actual viewport texture");
|
||||
require((map.renderedPixelAt(sx+2,sz)>>>24)==255,"Surveyed terrain retains its opaque native map color");
|
||||
double oldZoom=map.zoom();move(c,map.width/2.,map.height/2.);
|
||||
c.mouseHandler.onScroll(c.getWindow().handle(),0,1);
|
||||
require(map.zoom()>oldZoom,"Native mouse wheel zooms map");
|
||||
drag(c,40,0,false);drag(c,-30,15,true);
|
||||
map.recenter(); // A new request must not lock subsequent navigation.
|
||||
drag(c,20,0,false);
|
||||
});
|
||||
context.waitTicks(12);
|
||||
context.runOnClient(c -> {drag(c,-20,0,false);((SanctuaryAtlasScreen)c.gui.screen()).recenter();});
|
||||
context.waitFor(c -> ((SanctuaryAtlasScreen)c.gui.screen()).isComplete(),600);
|
||||
nativeClick(context,"sanctuary.atlas.grid_off");nativeClick(context,"sanctuary.atlas.grid_on");
|
||||
context.runOnClient(c -> {
|
||||
var map=(SanctuaryAtlasScreen)c.gui.screen();
|
||||
dragFrom(c,2,map.height/2.,25,0); // Dark left edge outside the old frame.
|
||||
dragFrom(c,map.width-2,map.height/2.,-25,0);
|
||||
dragFrom(c,2,2,20,10); // Behind the header, away from widgets.
|
||||
dragFrom(c,2,map.height-2,20,-10); // Behind the footer.
|
||||
});
|
||||
nativeClick(context,"sanctuary.atlas.recenter");
|
||||
context.runOnClient(c -> {
|
||||
var map=(SanctuaryAtlasScreen)c.gui.screen();
|
||||
require(Math.abs(map.centerX()-c.player.getX())<.01,"Overlay button receives the click before the full-screen map");
|
||||
var mode=button(c.gui.screen(),"sanctuary.atlas.inhabitant");
|
||||
double oldX=map.centerX(),oldZoom=map.zoom();
|
||||
move(c,mode.getX()+4,mode.getY()+4);
|
||||
c.mouseHandler.onScroll(c.getWindow().handle(),0,1);
|
||||
require(map.zoom()==oldZoom,"Wheel over a button does not zoom the terrain underneath");
|
||||
press(c,true);move(c,mode.getX()+8,mode.getY()+8);press(c,false);
|
||||
require(map.centerX()==oldX,"Disabled operator button cannot start map dragging");
|
||||
});
|
||||
context.runOnClient(c -> move(c,c.gui.screen().width/2.,c.gui.screen().height/2.+12));
|
||||
context.waitTicks(3);context.takeScreenshot("sanctuary-beta007-demeure-fog");
|
||||
int oldWidth=context.computeOnClient(c -> c.getWindow().getScreenWidth()),oldHeight=context.computeOnClient(c -> c.getWindow().getScreenHeight());
|
||||
int oldScale=context.computeOnClient(c -> c.options.guiScale().get());
|
||||
for (int[] size:new int[][]{{640,480,0},{1280,720,0},{1920,1080,1},{1920,1080,3}}) {
|
||||
context.runOnClient(c -> {c.getWindow().setWindowed(size[0],size[1]);c.options.guiScale().set(size[2]);c.resizeGui();});
|
||||
context.waitFor(c -> c.gui.screen() instanceof SanctuaryAtlasScreen a && a.isComplete() && a.observedTiles()>0,600);
|
||||
context.waitTicks(3);
|
||||
context.runOnClient(c -> {
|
||||
Blocodex004ClientChecks.frame(c.gui.screen(),"history");
|
||||
var widgets=c.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast).toList();
|
||||
for (int i=0;i<widgets.size();i++) for (int j=i+1;j<widgets.size();j++) {
|
||||
var a=widgets.get(i);var b=widgets.get(j);
|
||||
require(a.getRight()<=b.getX() || b.getRight()<=a.getX() || a.getBottom()<=b.getY() || b.getBottom()<=a.getY(),
|
||||
"Floating controls never overlap across GUI scales");
|
||||
}
|
||||
dragFrom(c,2,c.gui.screen().height/2.,10,0);
|
||||
((SanctuaryAtlasScreen)c.gui.screen()).recenter();
|
||||
move(c,c.gui.screen().width-2,c.gui.screen().height-2);
|
||||
});
|
||||
context.waitTicks(14);
|
||||
context.takeScreenshot("sanctuary-beta007-map-gui-"+size[0]+"-scale"+size[2]);
|
||||
}
|
||||
context.runOnClient(c -> {c.getWindow().setWindowed(oldWidth,oldHeight);c.options.guiScale().set(oldScale);c.resizeGui();});
|
||||
context.waitTicks(5);
|
||||
Blocodex004ClientChecks.language(context,"fr_fr");
|
||||
context.waitTicks(3);context.takeScreenshot("sanctuary-beta007-carte-fr");
|
||||
Blocodex004ClientChecks.language(context,"en_us");
|
||||
int personal=server.computeOnServer(s -> AtlasService.observations(s.getPlayerList().getPlayers().getFirst()).size());
|
||||
server.runOnServer(s -> {((IntegratedServer)s).setWorldAllowCommands(true);require(AtlasService.canOperate(s.getPlayerList().getPlayers().getFirst()),"Allow commands grants actual operator inspection");});
|
||||
context.runOnClient(c -> ((SanctuaryAtlasScreen)c.gui.screen()).recenter());
|
||||
context.waitFor(c -> descendants(c.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.anyMatch(b -> b.active && b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals("sanctuary.atlas.inhabitant")),400);
|
||||
nativeClick(context,"sanctuary.atlas.inhabitant");
|
||||
try {
|
||||
context.waitFor(c -> c.gui.screen() instanceof SanctuaryAtlasScreen a && a.observedTiles()>personal && a.isComplete(),1600);
|
||||
} catch (AssertionError error) {
|
||||
context.takeScreenshot("sanctuary-beta007-operator-timeout");
|
||||
context.runOnClient(c -> {if (!(c.gui.screen() instanceof SanctuaryAtlasScreen a)) {System.out.println("Unexpected screen: "+c.gui.screen());return;}System.out.println("Atlas timeout: status="+a.status()+" tiles="+a.observedTiles()+" complete="+a.isComplete()+" personal="+personal);});
|
||||
server.runOnServer(s -> System.out.println("Atlas timeout server: loaded="+fr.koka.sanctuary.knowledge.census.LoadedChunks.full(s.overworld()).size()+" op="+AtlasService.canOperate(s.getPlayerList().getPlayers().getFirst())));
|
||||
throw error;
|
||||
}
|
||||
context.waitTicks(3);context.takeScreenshot("sanctuary-beta007-map-operator");
|
||||
server.runOnServer(s -> {
|
||||
require(AtlasService.observations(s.getPlayerList().getPlayers().getFirst()).size()==personal,"Inspecting the loaded world does not add personal discoveries");
|
||||
((IntegratedServer)s).setWorldAllowCommands(false);
|
||||
});
|
||||
context.runOnClient(c -> ((SanctuaryAtlasScreen)c.gui.screen()).recenter());
|
||||
context.waitFor(c -> c.gui.screen() instanceof SanctuaryAtlasScreen a && a.status().equals("denied") && a.observedTiles()==0,400);
|
||||
nativeClick(context,"sanctuary.atlas.operator");
|
||||
context.waitFor(c -> c.gui.screen() instanceof SanctuaryAtlasScreen a && a.observedTiles()>0 && a.isComplete(),600);
|
||||
context.runOnClient(c -> require(((SanctuaryAtlasScreen)c.gui.screen()).observedTiles()<=personal,"Returning from operator mode discards operator terrain"));
|
||||
nativeClick(context,"gui.done");context.waitForScreen(null);
|
||||
context.getInput().pressKey(InputConstants.KEY_B);context.waitForScreen(ProgressionScreen.class);
|
||||
context.runOnClient(c -> ProgressionClient.open(null,"progress"));
|
||||
System.out.println("Atlas beta007 native client passed: locked/pre-recorded, purchase, zoom/pan, GUI sizes, explicit operator mode, revocation and isolation.");
|
||||
}
|
||||
private static Button button(net.minecraft.client.gui.screens.Screen screen,String key) {
|
||||
return descendants(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key)).findFirst().orElseThrow();
|
||||
}
|
||||
private static void nativeClick(ClientGameTestContext context,String key) {
|
||||
context.runOnClient(c -> {
|
||||
var b=button(c.gui.screen(),key);require(b.active,"Native button is active: "+key);
|
||||
move(c,b.getX()+b.getWidth()/2.,b.getY()+b.getHeight()/2.);press(c,true);press(c,false);
|
||||
});
|
||||
}
|
||||
private static void press(net.minecraft.client.Minecraft c,boolean down) {
|
||||
c.mouseHandler.onButton(c.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0),down ? InputConstants.PRESS : InputConstants.RELEASE);
|
||||
}
|
||||
private static void dragFrom(net.minecraft.client.Minecraft c,double x,double y,double dx,double dy) {
|
||||
var map=(SanctuaryAtlasScreen)c.gui.screen();move(c,x,y);move(c,x,y);press(c,true);
|
||||
double before=map.centerX();move(c,x+dx,y+dy);press(c,false);
|
||||
require(Math.abs(map.centerX()-(before-dx/map.zoom()))<.01,"Map can be dragged in the dark screen margins");
|
||||
}
|
||||
private static void move(net.minecraft.client.Minecraft c,double x,double y) {
|
||||
var window=c.getWindow();double sx=x*window.getScreenWidth()/window.getGuiScaledWidth(),sy=y*window.getScreenHeight()/window.getGuiScaledHeight();
|
||||
c.mouseHandler.onMove(window.handle(),sx,sy,0,0);c.mouseHandler.handleAccumulatedMovement();
|
||||
}
|
||||
private static void drag(net.minecraft.client.Minecraft c,double dx,double dy,boolean releaseOutside) {
|
||||
var map=(SanctuaryAtlasScreen)c.gui.screen();double x=map.width/2.,y=map.height/2.;
|
||||
move(c,x,y);move(c,x,y);
|
||||
var button=new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0);
|
||||
c.mouseHandler.onButton(c.getWindow().handle(),button,InputConstants.PRESS);
|
||||
double before=map.centerX();move(c,x+dx,y+dy);
|
||||
require(Math.abs(map.centerX()-(before-dx/map.zoom()))<.01,"Native repeated drag tracks pointer displacement");
|
||||
if (releaseOutside) move(c,1,1);
|
||||
c.mouseHandler.onButton(c.getWindow().handle(),button,InputConstants.RELEASE);
|
||||
double released=map.centerX();move(c,x,y);
|
||||
require(Math.abs(map.centerX()-released)<.01,"Mouse release outside map ends the gesture");
|
||||
}
|
||||
private static void clickNested(ClientGameTestContext context,String key) {
|
||||
context.runOnClient(c -> descendants(c.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.active && b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key))
|
||||
.findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0)));
|
||||
}
|
||||
private static java.util.stream.Stream<GuiEventListener> descendants(ContainerEventHandler container) {
|
||||
return container.children().stream().flatMap(child -> java.util.stream.Stream.concat(java.util.stream.Stream.of(child),
|
||||
child instanceof ContainerEventHandler nested ? descendants(nested) : java.util.stream.Stream.empty()));
|
||||
}
|
||||
private static void require(boolean value,String message) {if (!value) throw new AssertionError(message);}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.atlas.*;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.input.MouseButtonInfo;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.network.protocol.game.ClientboundTrackedWaypointPacket;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.minecraft.world.waypoints.Waypoint;
|
||||
|
||||
/** Stable rendered terrain across real refresh packets, and native shared locator sprites. */
|
||||
final class Atlas013ClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server) {
|
||||
int[] sample=server.computeOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(20);p.giveExperiencePoints(7);
|
||||
require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Unlock map after prestige");ProgressionService.send(p,"");
|
||||
require(p.waypointIcon().color.orElseThrow()==(0xFF000000|ProgressionService.color(p)),"Server locator uses the inhabitant colour");
|
||||
int x=p.chunkPosition().x()+4,z=p.chunkPosition().z();
|
||||
byte[] colors=new byte[256];java.util.Arrays.fill(colors,MapColor.GOLD.getPackedId(MapColor.Brightness.NORMAL));
|
||||
AtlasService.observations(p).observe(new AtlasStore.Tile(x,z,colors));return new int[]{x*16,z*16};
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.atlas(),200);
|
||||
c.runOnClient(m -> {m.gui.setScreen(new SanctuaryAtlasScreen(null));Inventory012RenderProbe.reset();});
|
||||
c.waitFor(m -> m.gui.screen() instanceof SanctuaryAtlasScreen a && a.isComplete() && a.renderedPixelAt(sample[0],sample[1])!=0,600);
|
||||
int pixel=c.computeOnClient(m -> ((SanctuaryAtlasScreen)m.gui.screen()).renderedPixelAt(sample[0],sample[1]));
|
||||
var armed=new AtomicBoolean(true);var stable=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if(armed.get() && m.gui.screen() instanceof SanctuaryAtlasScreen a && a.renderedPixelAt(sample[0],sample[1])!=pixel)stable.set(false);
|
||||
});
|
||||
double released=c.computeOnClient(m -> {
|
||||
var a=(SanctuaryAtlasScreen)m.gui.screen();double x=a.width/2.,y=a.height/2.;
|
||||
var mouse=new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0);
|
||||
require(a.mouseClicked(new MouseButtonEvent(x,y,mouse),false),"Map starts a drag");
|
||||
require(a.mouseDragged(new MouseButtonEvent(x+40,y,mouse),40,0),"Map pans");
|
||||
a.mouseReleased(new MouseButtonEvent(x+40,y,mouse));return a.centerX();
|
||||
});
|
||||
c.waitTicks(150);
|
||||
c.runOnClient(m -> {
|
||||
var a=(SanctuaryAtlasScreen)m.gui.screen();
|
||||
require(a.centerX()==released && stable.get(),"Drag release and periodic refresh never erase known terrain or recenter");
|
||||
require(Inventory012RenderProbe.sprites.stream().anyMatch(v -> v.id().equals("minecraft:hud/locator_bar_dot/default_0")),"Map uses the native locator texture");
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta013-map-stable");armed.set(false);
|
||||
c.runOnClient(m -> {
|
||||
var a=(SanctuaryAtlasScreen)m.gui.screen();
|
||||
a.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals("sanctuary.atlas.inhabitant"))
|
||||
.findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
|
||||
});
|
||||
c.waitFor(m -> ((SanctuaryAtlasScreen)m.gui.screen()).observedTiles()>0,600);
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();s.getPlayerList().deop(p.nameAndId());});
|
||||
c.runOnClient(m -> ((SanctuaryAtlasScreen)m.gui.screen()).recenter());
|
||||
c.waitFor(m -> ((SanctuaryAtlasScreen)m.gui.screen()).status().equals("denied"),600);
|
||||
c.runOnClient(m -> {require(((SanctuaryAtlasScreen)m.gui.screen()).observedTiles()==0,"Permission revocation clears retained operator terrain");m.gui.screen().onClose();});
|
||||
UUID marker=UUID.randomUUID();
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var icon=new Waypoint.Icon();icon.color=java.util.Optional.of(0xFF36CEAA);
|
||||
p.connection.send(ClientboundTrackedWaypointPacket.addWaypointPosition(marker,icon,p.blockPosition().offset(0,0,20)));
|
||||
});
|
||||
c.waitFor(m -> m.player.connection.getWaypointManager().hasWaypoints(),200);
|
||||
c.runOnClient(m -> {m.player.setYRot(0);m.player.setXRot(0);Inventory012RenderProbe.reset();});
|
||||
c.waitTicks(150);c.takeScreenshot("sanctuary-beta013-locator-and-xp");
|
||||
c.runOnClient(m -> {
|
||||
var sprites=Inventory012RenderProbe.sprites;
|
||||
var markerDraw=sprites.stream().filter(v -> v.id().startsWith("minecraft:hud/locator_bar_dot/") && v.color()==0xFF36CEAA).findFirst().orElseThrow();
|
||||
require(sprites.stream().anyMatch(v -> v.id().equals("minecraft:hud/experience_bar_background")),"Experience background remains drawn beyond the native temporary XP priority");
|
||||
require(sprites.stream().noneMatch(v -> v.id().equals("minecraft:hud/locator_bar_background")),"Locator never replaces the experience background");
|
||||
var xp=sprites.stream().filter(v -> v.id().equals("minecraft:hud/experience_bar_background")).findFirst().orElseThrow();
|
||||
require(markerDraw.y()==xp.y()-2 && markerDraw.y()+markerDraw.height()>xp.y()+xp.height(),"Player marker overlays the XP bar at its native position");
|
||||
});
|
||||
server.runOnServer(s -> s.getPlayerList().getPlayers().getFirst().connection.send(ClientboundTrackedWaypointPacket.removeWaypoint(marker)));
|
||||
}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Real widgets, fonts and purchase packets, in a fresh ignored development world. */
|
||||
public final class Atlas026ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (armed.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);
|
||||
}
|
||||
});
|
||||
int scale=c.computeOnClient(m -> m.options.guiScale().get());
|
||||
String language=c.computeOnClient(m -> m.getLanguageManager().getSelected());
|
||||
try (var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Sanctuary beta026 atlas "+UUID.randomUUID());
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && ProgressionClient.active() && CycleClient.view!=null && m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(server -> {
|
||||
var player=server.getPlayerList().getPlayers().getFirst();player.setExperienceLevels(20);
|
||||
require(ProgressionService.purchase(player,"atlas",ProgressEffects.get(player).revision()),"Buy atlas");
|
||||
int cx=player.chunkPosition().x(),cz=player.chunkPosition().z();
|
||||
for(int z=cz-8;z<cz+8;z++)for(int x=cx-8;x<cx+8;x++) {
|
||||
byte[] colors=new byte[256];java.util.Arrays.fill(colors,net.minecraft.world.level.material.MapColor.GOLD.getPackedId(net.minecraft.world.level.material.MapColor.Brightness.NORMAL));
|
||||
colors[17]=0;colors[18]=1;
|
||||
fr.koka.sanctuary.atlas.AtlasService.observations(player).observe(new fr.koka.sanctuary.atlas.AtlasStore.Tile(x,z,colors));
|
||||
fr.koka.demeure.DemeureService.record(player,new net.minecraft.core.BlockPos(x*16,200,z*16),8);
|
||||
}
|
||||
ProgressionService.send(player,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.atlas(),200);
|
||||
c.runOnClient(m -> {m.getWindow().setWindowed(1280,800);m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null));});
|
||||
c.runOnClient(m -> button(m.gui.screen(),"sanctuary.atlas.demeure_off").onPress(enter()));
|
||||
c.waitFor(m -> m.gui.screen() instanceof SanctuaryAtlasScreen a && a.isComplete() && a.footprintCount()>=256,1200);
|
||||
c.runOnClient(m -> {var a=(SanctuaryAtlasScreen)m.gui.screen();for(int i=0;i<4;i++)a.mouseScrolled(a.width/2.,a.height/2.,0,-1);});
|
||||
c.waitTicks(45);
|
||||
c.runOnClient(m -> hold((SanctuaryAtlasScreen)m.gui.screen()));c.waitTicks(3);
|
||||
measure(c,"stationary");
|
||||
c.runOnClient(m -> {var a=(SanctuaryAtlasScreen)m.gui.screen();a.mouseDragged(mouse(a,10,0),10,0);});
|
||||
measure(c,"pan-held");
|
||||
c.takeScreenshot("sanctuary-beta026-demeure");
|
||||
c.runOnClient(m -> {Atlas026RenderProbe.active=false;button(m.gui.screen(),"sanctuary.atlas.demeure_on").onPress(enter());});
|
||||
measure(c,"disabled");
|
||||
c.runOnClient(m -> button(m.gui.screen(),"sanctuary.atlas.demeure_off").onPress(enter()));
|
||||
measure(c,"enabled-again");
|
||||
if(!Boolean.getBoolean("sanctuary.test.atlasBaseline"))verifyUpdates(c,world.getServer());
|
||||
System.out.println("ATLAS026 client checks passed");
|
||||
} finally {
|
||||
Atlas026RenderProbe.reset();armed.set(false);c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.options.guiScale().set(scale);m.resizeGui();
|
||||
});
|
||||
}
|
||||
}
|
||||
private static void measure(ClientGameTestContext c,String phase) {
|
||||
c.runOnClient(m -> {Atlas026RenderProbe.reset();Atlas026RenderProbe.active=true;});
|
||||
c.waitFor(m -> Atlas026RenderProbe.nanos.size()>=30,1200);
|
||||
c.runOnClient(m -> {
|
||||
Atlas026RenderProbe.active=false;System.out.println(Atlas026RenderProbe.report(phase));
|
||||
if(!Boolean.getBoolean("sanctuary.test.atlasBaseline")) {
|
||||
require(Atlas026RenderProbe.blits==(phase.equals("disabled") ? 0 : Atlas026RenderProbe.nanos.size()),"One cached layer per visible frame: "+phase);
|
||||
require(Atlas026RenderProbe.fills==0,"Overlay never submits per-block rectangles: "+phase);
|
||||
require(Atlas026RenderProbe.uploads==0,"Stable data never reuploads map textures: "+phase);
|
||||
}
|
||||
});
|
||||
}
|
||||
private static net.minecraft.client.input.MouseButtonEvent mouse(SanctuaryAtlasScreen a,double dx,double dy) {
|
||||
return new net.minecraft.client.input.MouseButtonEvent(a.width/2.+dx,a.height/2.+dy,new net.minecraft.client.input.MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0));
|
||||
}
|
||||
private static void hold(SanctuaryAtlasScreen a) {require(a.mouseClicked(mouse(a,0,0),false),"Hold map gesture");}
|
||||
private record Fixture(int token,fr.koka.sanctuary.atlas.AtlasStore.Key key,byte[] colors,fr.koka.sanctuary.atlas.AtlasDemeurePayload.Footprint home) {}
|
||||
private static void verifyUpdates(ClientGameTestContext c,net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext server) {
|
||||
var sample=c.computeOnClient(m -> {
|
||||
var a=(SanctuaryAtlasScreen)m.gui.screen();
|
||||
var key=new fr.koka.sanctuary.atlas.AtlasStore.Key(m.player.chunkPosition().x()+5,m.player.chunkPosition().z()+5);
|
||||
var surveys=(java.util.Map<fr.koka.sanctuary.atlas.AtlasStore.Key,byte[]>)field(a,"surveyed");
|
||||
var homes=(java.util.Map<fr.koka.sanctuary.atlas.AtlasStore.Key,fr.koka.sanctuary.atlas.AtlasDemeurePayload.Footprint>)field(a,"homes");
|
||||
return new Fixture((int)field(a,"token"),key,surveys.get(key).clone(),homes.get(key));
|
||||
});
|
||||
c.runOnClient(m -> {
|
||||
var a=(SanctuaryAtlasScreen)m.gui.screen();int x=sample.key.x()*16,z=sample.key.z()*16;
|
||||
require(pixel(a,x,z)==(0xA0000000|sample.home.color()),"Original border colour and opacity");
|
||||
require(pixel(a,x+3,z+1)==(0x25000000|sample.home.color()),"Original interior opacity");
|
||||
require(pixel(a,x+1,z+1)==0 && pixel(a,x+2,z+1)==0,"Unknown pixels and observed void have no overlay");
|
||||
Atlas026RenderProbe.reset();Atlas026RenderProbe.active=true;
|
||||
});
|
||||
send(server,sample,java.util.List.of(sample.home));c.waitTicks(5);
|
||||
c.runOnClient(m -> {require(Atlas026RenderProbe.uploads==0,"Identical network batches do not upload either texture");Atlas026RenderProbe.active=false;});
|
||||
var changed=new fr.koka.sanctuary.atlas.AtlasDemeurePayload.Footprint(sample.key.x(),sample.key.z(),"dominated","player","Updated",99,0x36CEAA);
|
||||
send(server,sample,java.util.List.of(changed));
|
||||
c.waitFor(m -> pixel((SanctuaryAtlasScreen)m.gui.screen(),sample.key.x()*16,sample.key.z()*16)==0xA036CEAA,200);
|
||||
c.runOnClient(m -> {
|
||||
var a=(SanctuaryAtlasScreen)m.gui.screen();require(pixel(a,sample.key.x()*16+1,sample.key.z()*16+1)==0,"Colour changes never reveal the fog");
|
||||
button(a,"sanctuary.atlas.demeure_on").onPress(enter());Atlas026RenderProbe.reset();Atlas026RenderProbe.active=true;
|
||||
});
|
||||
send(server,sample,java.util.List.of(sample.home));c.waitTicks(5);
|
||||
c.runOnClient(m -> {require(Atlas026RenderProbe.uploads==0,"Hidden changes wait until the layer is enabled");Atlas026RenderProbe.active=false;button(m.gui.screen(),"sanctuary.atlas.demeure_off").onPress(enter());});
|
||||
c.waitFor(m -> pixel((SanctuaryAtlasScreen)m.gui.screen(),sample.key.x()*16,sample.key.z()*16)==(0xA0000000|sample.home.color()),200);
|
||||
byte[] revealed=sample.colors.clone();revealed[17]=sample.colors[0];
|
||||
send(server,new Fixture(sample.token,sample.key,revealed,sample.home),java.util.List.of(sample.home));
|
||||
c.waitFor(m -> pixel((SanctuaryAtlasScreen)m.gui.screen(),sample.key.x()*16+1,sample.key.z()*16+1)!=0,200);
|
||||
send(server,sample,java.util.List.of());
|
||||
c.waitFor(m -> pixel((SanctuaryAtlasScreen)m.gui.screen(),sample.key.x()*16,sample.key.z()*16)==0,200);
|
||||
c.runOnClient(m -> require(pixel((SanctuaryAtlasScreen)m.gui.screen(),sample.key.x()*16+1,sample.key.z()*16+1)==0,"Removed footprint and restored fog clear stale pixels"));
|
||||
// Reprojection and real refresh restore authoritative data from the same saved observations.
|
||||
c.runOnClient(m -> {
|
||||
var a=(SanctuaryAtlasScreen)m.gui.screen();a.mouseReleased(mouse(a,0,0));a.recenter();
|
||||
m.getWindow().setWindowed(640,480);m.options.guiScale().set(0);m.resizeGui();
|
||||
});c.waitTicks(45);
|
||||
c.waitFor(m -> ((SanctuaryAtlasScreen)m.gui.screen()).isComplete() && pixel((SanctuaryAtlasScreen)m.gui.screen(),sample.key.x()*16,sample.key.z()*16)==(0xA0000000|sample.home.color()),600);
|
||||
c.takeScreenshot("sanctuary-beta026-demeure-compact");
|
||||
// Removing the map aptitude uses the actual server permission check on the next request.
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);ProgressionService.send(p,"");});
|
||||
c.runOnClient(m -> ((SanctuaryAtlasScreen)m.gui.screen()).recenter());
|
||||
c.waitFor(m -> ((SanctuaryAtlasScreen)m.gui.screen()).status().equals("locked"),600);
|
||||
c.runOnClient(m -> {
|
||||
var a=(SanctuaryAtlasScreen)m.gui.screen();require(a.footprintCount()==0 && pixel(a,sample.key.x()*16,sample.key.z()*16)==0,"Revocation clears both map layers");
|
||||
var image=((net.minecraft.client.renderer.texture.DynamicTexture)field(a,"footprintTexture")).getPixels();
|
||||
a.onClose();require(image.isClosed(),"Closing releases the overlay image");
|
||||
});
|
||||
System.out.println("ATLAS026 updates passed: identical batches, colour, fog, removal, hidden updates, reprojection, resize, revocation, disposal");
|
||||
}
|
||||
private static void send(net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext server,Fixture f,java.util.List<fr.koka.sanctuary.atlas.AtlasDemeurePayload.Footprint> homes) {
|
||||
server.runOnServer(s -> {
|
||||
var player=s.getPlayerList().getPlayers().getFirst();
|
||||
net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(player,new fr.koka.sanctuary.atlas.AtlasPayloads.Response(f.token,false,true,false,false,"ready",java.util.List.of(new fr.koka.sanctuary.atlas.AtlasStore.Tile(f.key.x(),f.key.z(),f.colors))));
|
||||
net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(player,new fr.koka.sanctuary.atlas.AtlasDemeurePayload(f.token,homes));
|
||||
});
|
||||
}
|
||||
private static Object field(Object target,String name) {
|
||||
try {var f=target.getClass().getDeclaredField(name);f.setAccessible(true);return f.get(target);}catch(ReflectiveOperationException e){throw new AssertionError(e);}
|
||||
}
|
||||
private static int pixel(SanctuaryAtlasScreen a,int x,int z) {
|
||||
int localX=x-(int)field(a,"originX"),localZ=z-(int)field(a,"originZ");
|
||||
return ((net.minecraft.client.renderer.texture.DynamicTexture)field(a,"footprintTexture")).getPixels().getPixel(localX,localZ);
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(GuiEventListener widget) {
|
||||
return Stream.concat(Stream.of(widget),widget instanceof ContainerEventHandler parent
|
||||
? parent.children().stream().flatMap(Atlas026ClientChecks::widgets) : Stream.empty());
|
||||
}
|
||||
private static boolean key(AbstractWidget widget,String key) {return widget.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key);}
|
||||
private static Button button(Screen screen,String key) {return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> key(b,key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter() {return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.atlas.*;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** Real 640-high world, sampling, packets, cached rendering and native controls. */
|
||||
public final class Atlas058ClientChecks implements FabricClientGameTest {
|
||||
private static final net.minecraft.world.level.block.Block[] BLOCKS={Blocks.STONE,Blocks.DIAMOND_BLOCK,Blocks.GOLD_BLOCK,Blocks.LAPIS_BLOCK,Blocks.EMERALD_BLOCK,Blocks.REDSTONE_BLOCK};
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.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);
|
||||
}});
|
||||
int scale=c.computeOnClient(m->m.options.guiScale().get());String language=c.computeOnClient(m->m.getLanguageManager().getSelected());
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Atlas058 "+UUID.randomUUID());s.setSeed("58");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
int[] fixture=server.computeOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;
|
||||
var level=p.level();check(level.getMaxY()==639,"Real Sanctuary dimension reaches 640 blocks");
|
||||
int x=p.chunkPosition().x()*16+4,z=p.chunkPosition().z()*16+4;
|
||||
for(int dx=0;dx<4;dx++)for(int dz=0;dz<4;dz++) {
|
||||
for(int y=level.getMinY();y<=level.getMaxY();y++)level.setBlock(new BlockPos(x+dx,y,z+dz),Blocks.AIR.defaultBlockState(),2);
|
||||
if(dx==3&&dz==3)continue;
|
||||
for(int i=0;i<6;i++)level.setBlock(new BlockPos(x+dx,Math.min(639,AtlasAltitude.LEVELS.get(i)),z+dz),BLOCKS[i].defaultBlockState(),2);
|
||||
}
|
||||
int cx=Math.floorDiv(x,16),cz=Math.floorDiv(z,16),pixel=Math.floorMod(z,16)*16+Math.floorMod(x,16);
|
||||
var all=AtlasSampler.sampleLayers(level,cx,cz);check(all.size()==6,"Six layers in one scan");
|
||||
for(int i=0;i<6;i++) {
|
||||
int altitude=AtlasAltitude.LEVELS.get(i);var tile=all.get(altitude);
|
||||
int color=Byte.toUnsignedInt(tile.colors()[pixel])/4;
|
||||
check(color==BLOCKS[i].defaultMapColor().id,"Topmost block below inclusive ceiling "+altitude);
|
||||
check(Arrays.equals(tile.colors(),AtlasSampler.sample(level,cx,cz,altitude).colors()),"Single-layer and shared descent agree "+altitude);
|
||||
check(tile.colors()[pixel+51]==1,"Empty column stays transparent void "+altitude);
|
||||
AtlasService.observations(p,altitude).observe(AtlasFog.reveal(tile,p.getX(),p.getZ()));
|
||||
}
|
||||
check(AtlasSampler.sample(level,1000000,1000000,280)==null&&AtlasSampler.sampleLayers(level,1000000,1000000).isEmpty()&&level.getChunkSource().getChunkNow(1000000,1000000)==null,"Sampling never loads unknown distant chunks");
|
||||
// A transparent block at the cut must not hide the colour just underneath.
|
||||
level.setBlock(new BlockPos(x,140,z),Blocks.GLASS.defaultBlockState(),2);level.setBlock(new BlockPos(x,139,z),Blocks.DIAMOND_BLOCK.defaultBlockState(),2);
|
||||
check(Byte.toUnsignedInt(AtlasSampler.sample(level,cx,cz,140).colors()[pixel])/4==Blocks.DIAMOND_BLOCK.defaultMapColor().id,"Transparent cut reveals support");
|
||||
p.setExperienceLevels(20);check(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Atlas purchased after recording lower layers");
|
||||
s.getWorldData().setAllowCommands(false);ProgressionService.send(p,"");
|
||||
return new int[]{x,z,cx,cz};
|
||||
});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas()&&!ProgressionClient.operator,200);
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,800);m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null));});
|
||||
waitLayer(c,640,fixture);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(!(boolean)field(a,"grid")&&!(boolean)field(a,"footprints"),"Chunk grid and Demeure off by default");
|
||||
for(String key:List.of("sanctuary.atlas.grid_off","sanctuary.atlas.demeure_off"))a.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow().onPress(enter());
|
||||
check((boolean)field(a,"grid")&&(boolean)field(a,"footprints"),"Optional overlays can be enabled");
|
||||
for(String key:List.of("sanctuary.atlas.grid_on","sanctuary.atlas.demeure_on"))a.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow().onPress(enter());
|
||||
});
|
||||
double[] position=c.computeOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();a.mouseScrolled(a.width/2.,a.height/2.,0,1);var info=new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0);a.mouseClicked(new MouseButtonEvent(a.width/2.,a.height/2.,info),false);a.mouseDragged(new MouseButtonEvent(a.width/2.+28,a.height/2.,info),28,0);a.mouseReleased(new MouseButtonEvent(a.width/2.+28,a.height/2.,info));return new double[]{a.centerX(),a.zoom()};});
|
||||
c.waitTicks(25);
|
||||
for(int i=5;i>=0;i--) {
|
||||
int ceiling=AtlasAltitude.LEVELS.get(i);
|
||||
if(i<5)c.runOnClient(m->click((SanctuaryAtlasScreen)m.gui.screen(),"↓"));
|
||||
waitLayer(c,ceiling,fixture);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(a.centerX()==position[0]&&a.zoom()==position[1],"Layer change preserves pan and zoom");check(button(a,"↑").active==(ceiling<640)&&button(a,"↓").active==(ceiling>0),"Buttons clamp both ends");});
|
||||
if(i==5||i==2||i==0)c.takeScreenshot("sanctuary-beta058-altitude-"+ceiling);
|
||||
}
|
||||
c.runOnClient(m->{
|
||||
var a=(SanctuaryAtlasScreen)m.gui.screen();int oldToken=(int)field(a,"token");
|
||||
for(int i=0;i<5;i++)click(a,"↑");for(int i=0;i<2;i++)click(a,"↓");
|
||||
check(a.altitude()==420,"Rapid clicks end at requested layer");
|
||||
byte[] bad=new byte[256];Arrays.fill(bad,(byte)100);
|
||||
a.accept(new AtlasPayloads.Response(oldToken,false,true,true,false,"ready",List.of(new AtlasStore.Tile(fixture[2],fixture[3],bad))));
|
||||
a.accept(new AtlasDemeurePayload(oldToken,List.of(new AtlasDemeurePayload.Footprint(fixture[2],fixture[3],"inhabited","nature","",50,0xFF00FF))));
|
||||
check(a.observedTiles()==0&&a.footprintCount()==0,"Old terrain and Demeure packets are ignored after layer switch");
|
||||
});waitLayer(c,420,fixture);
|
||||
int pixel=c.computeOnClient(m->((SanctuaryAtlasScreen)m.gui.screen()).renderedPixelAt(fixture[0],fixture[1]));
|
||||
c.waitTicks(130);c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(a.renderedPixelAt(fixture[0],fixture[1])==pixel&&a.centerX()==position[0],"Periodic refresh keeps displayed terrain and position");});
|
||||
// FR/EN and minimum GUI scale retain clickable controls without footer overlap.
|
||||
for(String lang:List.of("fr_fr","en_us"))c.runOnClient(m->{m.getLanguageManager().setSelected(lang);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(4);m.resizeGui();var a=(SanctuaryAtlasScreen)m.gui.screen();check(button(a,"↓").getBottom()<a.height-58,"Altitude buttons clear coordinate/footer overlay");check(!SanctuaryAtlasScreen.label("altitude",420).getString().contains("sanctuary."),"Translated altitude label");});
|
||||
c.takeScreenshot("sanctuary-beta058-small-gui");
|
||||
// Additive persistence and server-side permission checks on every layer.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var root=s.getWorldPath(LevelResource.ROOT).resolve("data/sanctuary-atlas");try{
|
||||
for(int altitude:AtlasAltitude.LEVELS){var survey=AtlasService.observations(p,altitude);survey.save();var file=altitude==640?root.resolve(p.getUUID()+".json"):root.resolve("layers/"+p.getUUID()+"/"+altitude+".json");var bytes=Files.readAllBytes(file);var read=new AtlasStore(file,p.getUUID(),s.overworld().getSeed());check(Arrays.equals(read.get(fixture[2],fixture[3]).colors(),survey.get(fixture[2],fixture[3]).colors()),"Layer survives disk reload "+altitude);read.save();check(Arrays.equals(bytes,Files.readAllBytes(file)),"Layer reload is idempotent");}
|
||||
check(!AtlasService.canRead(p,true)&&AtlasService.canRead(p,false),"Normal mode cannot inspect operator chunks");
|
||||
}catch(Exception e){throw new RuntimeException(e);}});
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();int next=AtlasClient.nextToken();setField(a,"token",next);AtlasClient.send(new AtlasPayloads.Request(next,fixture[2],fixture[3],true,420));});
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).status().equals("denied"),200);
|
||||
c.runOnClient(m->{check(((SanctuaryAtlasScreen)m.gui.screen()).observedTiles()==0,"Denied operator response clears all pixels");m.gui.screen().onClose();});
|
||||
protocol();System.out.println("ATLAS058_PASS: ceilings, actual colours, fog, persistence, packets, controls, optional overlays, pan, permissions, FR/EN");
|
||||
} finally {intro.set(false);c.runOnClient(m->{m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();});}
|
||||
}
|
||||
private static void waitLayer(ClientGameTestContext c,int altitude,int[] fixture) {
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen a&&a.altitude()==altitude&&a.isComplete()&&a.renderedPixelAt(fixture[0],fixture[1])!=0,800);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();int index=AtlasAltitude.LEVELS.indexOf(altitude);int color=a.renderedPixelAt(fixture[0],fixture[1]);boolean match=false;for(var shade:net.minecraft.world.level.material.MapColor.Brightness.values())match|=color==net.minecraft.world.level.material.MapColor.getColorFromPackedId(BLOCKS[index].defaultMapColor().getPackedId(shade));check(match,"Actual rendered pixel belongs to layer "+altitude);check(a.renderedPixelAt(fixture[0]+3,fixture[1]+3)==0,"Void pixels remain transparent");check(a.renderedPixelAt(fixture[0]+250,fixture[1]+250)==0,"Unknown territory remains hidden");});
|
||||
}
|
||||
private static void protocol(){for(int altitude:AtlasAltitude.LEVELS){var b=new net.minecraft.network.RegistryFriendlyByteBuf(io.netty.buffer.Unpooled.buffer(),net.minecraft.core.RegistryAccess.EMPTY);try{var r=new AtlasPayloads.Request(4,-2,3,false,altitude);AtlasPayloads.Request.CODEC.encode(b,r);check(AtlasPayloads.Request.CODEC.decode(b).equals(r),"Altitude round trip");}finally{b.release();}}for(int invalid:new int[]{-1,1,139,141,641,Integer.MAX_VALUE}){boolean failed=false;try{new AtlasPayloads.Request(1,0,0,false,invalid);}catch(IllegalArgumentException expected){failed=true;}check(failed,"Reject invalid altitude");}check(AtlasAltitude.step(640,1)==640&&AtlasAltitude.step(0,-1)==0&&AtlasAltitude.step(560,1)==640,"Bounded final 80-block step");}
|
||||
private static Button button(SanctuaryAtlasScreen a,String text){return a.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getString().equals(text)).findFirst().orElseThrow();}
|
||||
private static void click(SanctuaryAtlasScreen a,String text){var b=button(a,text);check(b.active,"Enabled altitude control");a.mouseClicked(new MouseButtonEvent(b.getX()+5,b.getY()+5,new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0)),false);a.mouseReleased(new MouseButtonEvent(b.getX()+5,b.getY()+5,new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0)));}
|
||||
private static Object field(Object o,String name){try{var f=o.getClass().getDeclaredField(name);f.setAccessible(true);return f.get(o);}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static void setField(Object o,String name,Object value){try{var f=o.getClass().getDeclaredField(name);f.setAccessible(true);f.set(o,value);}catch(Exception e){throw new RuntimeException(e);}}
|
||||
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);}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.atlas.*;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.worldgen.aerial.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.*;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.saveddata.maps.*;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.loot.*;
|
||||
import net.minecraft.world.level.storage.loot.parameters.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native banner interactions, actual loot-generated maps, packets and no operator knowledge leak. */
|
||||
public final class Atlas059ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(m.gui.screen() instanceof IntroScreen s)s.onClose();
|
||||
if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen s){Starter039TestSupport.selectFirst(s);s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());s.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);}
|
||||
});
|
||||
int scale=c.computeOnClient(m->m.options.guiScale().get());String lang=c.computeOnClient(m->m.getLanguageManager().getSelected());
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Atlas059 "+UUID.randomUUID());s.setSeed("42");s.setAllowCommands(true);s.setGenerateStructures(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
var banner=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.setExperienceLevels(20);check(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Unlock atlas");p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);p.inventoryMenu.broadcastChanges();
|
||||
var pos=p.blockPosition().offset(1,0,1);p.level().setBlock(pos.below(),Blocks.STONE.defaultBlockState(),2);p.level().setBlock(pos,Blocks.BANNER.red().defaultBlockState(),2);rename(p.level(),pos,"Port de Zoé");
|
||||
var sites=AtlasMarkers.treasureSites(p.level());check(sites.size()==2,"Existing medium-world plan contains both mining treasures; structures="+p.level().structureManager().shouldGenerateStructures()+" generator="+p.level().getChunkSource().getGenerator());
|
||||
check(AtlasMarkers.discoveries(p).isEmpty(),"No treasure known at arrival");s.getWorldData().setAllowCommands(false);ProgressionService.send(p,"");return pos;
|
||||
});
|
||||
c.waitFor(m->!ProgressionClient.operator&&m.player.getMainHandItem().isEmpty()&&m.level.getBlockState(banner).is(Blocks.BANNER.red()),200);
|
||||
useBanner(c,banner);c.waitTicks(12);server.runOnServer(s->check(AtlasMarkers.discoveries(s.getPlayerList().getPlayers().getFirst()).stream().anyMatch(m->m.name().equals("Port de Zoé")),"Real right-click records named red banner"));
|
||||
useBanner(c,banner);c.waitTicks(12);server.runOnServer(s->check(AtlasMarkers.discoveries(s.getPlayerList().getPlayers().getFirst()).isEmpty(),"Second right-click removes native-style marker"));
|
||||
useBanner(c,banner);c.waitTicks(12);
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,800);m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null));});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen a&&a.isComplete()&&a.markers().size()==1,600);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(a.markers().getFirst().kind().equals("banner"),"Normal atlas receives no undiscovered treasure coordinates");spriteCheck(m,a);});
|
||||
c.takeScreenshot("sanctuary-beta059-named-banner");
|
||||
server.runOnServer(s->rename(s.overworld(),banner,"Quai des îles"));c.runOnClient(m->((SanctuaryAtlasScreen)m.gui.screen()).recenter());
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).markers().stream().anyMatch(v->v.name().equals("Quai des îles")),300);
|
||||
c.runOnClient(m->m.gui.screen().onClose());
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var map=MapItem.create(p.level(),banner.getX(),banner.getZ(),(byte)1,true,true);MapItemSavedData.addTargetDecoration(map,banner.offset(80,0,80),"mansion",MapDecorationTypes.WOODLAND_MANSION);p.setItemInHand(InteractionHand.OFF_HAND,map);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.getOffhandItem().has(DataComponents.MAP_ID),200);
|
||||
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.OFF_HAND,new BlockHitResult(Vec3.atCenterOf(banner),Direction.UP,banner,false)));c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(MapItem.getSavedData(p.getOffhandItem(),p.level()).getBanners().size()==1,"Offhand physical map keeps native banner recording");});
|
||||
c.runOnClient(m->m.gameMode.useItem(m.player,InteractionHand.OFF_HAND));c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(AtlasMarkers.discoveries(p).stream().anyMatch(v->v.kind().equals("point")&&v.y()==null),"Native exploration point imported without invented altitude");check(AtlasMarkers.discoveries(p).stream().anyMatch(v->v.name().equals("Quai des îles")),"Named banner imported from the native physical map");});
|
||||
// Generate actual loot from both unmodified site identities; no synthetic clue grants.
|
||||
var target=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var sites=AtlasMarkers.treasureSites(p.level());var charts=new ArrayList<ItemStack>();
|
||||
for(int i=0;i<2;i++) {
|
||||
String table=i==0?"merchant_provisions":"merchant_tools";var loot=loot(p.level(),table,p.position());
|
||||
check(loot.stream().anyMatch(v->v.is(iItem(table))),"Original merchant loot remains");if(i==0)check(loot.stream().filter(v->v.is(Items.BREAD)).mapToInt(ItemStack::getCount).sum()>=24,"Declared provision quantities apply in the native modifier format");
|
||||
var map=loot.stream().filter(v->v.has(DataComponents.MAP_ID)).findFirst().orElseThrow();var expected=AtlasMarkers.treasure(sites.get(i));
|
||||
check(map.get(DataComponents.MAP_DECORATIONS).decorations().values().stream().anyMatch(d->d.x()==expected.x()&&d.z()==expected.z()&&d.type().equals(MapDecorationTypes.RED_X)),"Loot map targets the saved island chest "+i);charts.add(map);
|
||||
}
|
||||
var mineral=loot(p.level(),"mineral_islet",Vec3.atCenterOf(AerialGeometry24.chest(sites.getFirst())));var next=mineral.stream().filter(v->v.has(DataComponents.MAP_ID)).findFirst().orElseThrow();var nextPoint=AtlasMarkers.treasure(sites.get(1));check(next.get(DataComponents.MAP_DECORATIONS).decorations().values().stream().anyMatch(d->d.x()==nextPoint.x()&&d.z()==nextPoint.z()),"Mining chest leads to the next island");
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,charts.getFirst());p.inventoryMenu.broadcastChanges();return AtlasMarkers.treasure(sites.getFirst());
|
||||
});
|
||||
c.waitFor(m->m.player.getMainHandItem().has(DataComponents.MAP_ID),200);
|
||||
c.runOnClient(m->m.gameMode.useItem(m.player,InteractionHand.MAIN_HAND));c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(AtlasMarkers.discoveries(p).stream().filter(m->m.kind().equals("treasure")).count()==1,"Using physical map unlocks exactly its own treasure");check(p.getMainHandItem().has(DataComponents.MAP_ID)&&p.getMainHandItem().getCount()==1,"Chart remains shareable");check(AtlasMarkers.importMap(p,p.getMainHandItem())==0,"Repeated use does not duplicate markers");});
|
||||
c.runOnClient(m->m.gui.setScreen(new SanctuaryAtlasScreen(null)));c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).markers().stream().anyMatch(v->v.id().equals(target.id())),600);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();setField(a,"centerX",(double)target.x());setField(a,"centerZ",(double)target.z());spriteCheck(m,a);});c.takeScreenshot("sanctuary-beta059-treasure-chart");
|
||||
// The full operator list never changes normal discoveries, and revocation clears it.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.getWorldData().setAllowCommands(true);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).markers().stream().filter(v->v.kind().equals("treasure")).count()==2,600);
|
||||
c.takeScreenshot("sanctuary-beta059-operator-treasures");int opToken=c.computeOnClient(m->(int)field(m.gui.screen(),"token"));
|
||||
var opMarkers=c.computeOnClient(m->((SanctuaryAtlasScreen)m.gui.screen()).markers());
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(AtlasMarkers.discoveries(p).stream().filter(v->v.kind().equals("treasure")).count()==1,"Operator inspection never grants knowledge");s.getWorldData().setAllowCommands(false);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->!ProgressionClient.operator&&((SanctuaryAtlasScreen)m.gui.screen()).isComplete()&&((SanctuaryAtlasScreen)m.gui.screen()).markers().stream().filter(v->v.kind().equals("treasure")).count()==1,600);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();a.accept(new AtlasMarkerPayload(opToken,opMarkers));check(a.markers().stream().filter(v->v.kind().equals("treasure")).count()==1,"Delayed operator payload cannot restore hidden treasure");});
|
||||
server.runOnServer(s->s.overworld().setBlock(banner,Blocks.AIR.defaultBlockState(),2));c.runOnClient(m->((SanctuaryAtlasScreen)m.gui.screen()).recenter());
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).markers().stream().noneMatch(v->v.kind().equals("banner")),300);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();try{var file=s.getWorldPath(LevelResource.ROOT).resolve("data/sanctuary-atlas/markers/"+p.getUUID()+".json");var read=new AtlasMarkerStore(file,p.getUUID(),s.overworld().getSeed());check(read.all().equals(AtlasMarkers.discoveries(p)),"Discovery and banner deletion already survive disk reload");}catch(Exception e){throw new RuntimeException(e);}});
|
||||
for(String language:List.of("fr_fr","en_us"))c.runOnClient(m->{m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());check(!AtlasMarkers.label("treasure_chart").getString().contains("sanctuary."),"Localized chart and markers");});
|
||||
protocol(target);System.out.println("ATLAS059_PASS: native banners, treasure loot/maps, physical use, server filtering, operator revocation, native sprites and persistence");
|
||||
}finally{intro.set(false);c.runOnClient(m->{m.getLanguageManager().setSelected(lang);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();});}
|
||||
}
|
||||
private static Item iItem(String table){return table.equals("merchant_tools")?Items.IRON_PICKAXE:Items.BREAD;}
|
||||
private static List<ItemStack> loot(net.minecraft.server.level.ServerLevel level,String table,Vec3 origin){var key=ResourceKey.create(Registries.LOOT_TABLE,Identifier.fromNamespaceAndPath("sanctuary","chests/aerial24/"+table));return level.getServer().reloadableRegistries().getLootTable(key).getRandomItems(new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN,origin).create(LootContextParamSets.CHEST),59L);}
|
||||
private static void rename(net.minecraft.server.level.ServerLevel level,BlockPos pos,String name){var item=new ItemStack(Items.BANNER.red());item.set(DataComponents.CUSTOM_NAME,Component.literal(name));var be=level.getBlockEntity(pos);be.applyComponentsFromItemStack(item);be.setChanged();level.sendBlockUpdated(pos,level.getBlockState(pos),level.getBlockState(pos),3);}
|
||||
private static void useBanner(ClientGameTestContext c,BlockPos pos){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false)));}
|
||||
private static void spriteCheck(net.minecraft.client.Minecraft m,SanctuaryAtlasScreen a){for(var marker:a.markers()){var id=Identifier.parse(marker.sprite());check(m.getAtlasManager().getAtlasOrThrow(net.minecraft.data.AtlasIds.MAP_DECORATIONS).getSprite(id).contents().name().equals(id),"Real native decoration sprite exists: "+id);}}
|
||||
private static void protocol(AtlasMarker target){var b=new net.minecraft.network.RegistryFriendlyByteBuf(io.netty.buffer.Unpooled.buffer(),RegistryAccess.EMPTY);try{var p=new AtlasMarkerPayload(17,List.of(target,new AtlasMarker("point",0,null,0,"point","minecraft:mansion","")));AtlasMarkerPayload.CODEC.encode(b,p);check(AtlasMarkerPayload.CODEC.decode(b).equals(p),"Marker codec preserves unknown Y and identities");}finally{b.release();}}
|
||||
private static Object field(Object o,String name){try{var f=o.getClass().getDeclaredField(name);f.setAccessible(true);return f.get(o);}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static void setField(Object o,String name,Object v){try{var f=o.getClass().getDeclaredField(name);f.setAccessible(true);f.set(o,v);}catch(Exception e){throw new RuntimeException(e);}}
|
||||
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);}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.ProgressionScreen;
|
||||
import fr.koka.sanctuary.client.SanctuaryBlocodexScreen;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.components.ObjectSelectionList;
|
||||
import net.minecraft.client.gui.components.StringWidget;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
/** Exercises real GUI resizes and the live discovery catalogue inside the progression test world. */
|
||||
final class Blocodex004ClientChecks {
|
||||
static void run(ClientGameTestContext context) {
|
||||
int originalWidth=context.computeOnClient(c -> c.getWindow().getScreenWidth());
|
||||
int originalHeight=context.computeOnClient(c -> c.getWindow().getScreenHeight());
|
||||
int originalScale=context.computeOnClient(c -> c.options.guiScale().get());
|
||||
try {
|
||||
for(int[] size:new int[][]{{854,480},{960,720}}){
|
||||
context.runOnClient(c -> c.getWindow().setWindowed(size[0],size[1]));context.waitTicks(3);
|
||||
for(int scale:new int[]{1,2,3,0}){
|
||||
context.runOnClient(c -> {c.options.guiScale().set(scale);c.resizeGui();});
|
||||
for(String page:new String[]{"blocks","progress","history","identity"}){
|
||||
context.runOnClient(c -> fr.koka.sanctuary.client.ProgressionClient.open(null,page));context.waitTick();
|
||||
context.runOnClient(c -> frame(c.gui.screen(),page));
|
||||
}
|
||||
}
|
||||
}
|
||||
context.runOnClient(c -> fr.koka.sanctuary.client.ProgressionClient.open(null,"progress"));
|
||||
System.out.println("Native compact page headers and footers fit both window sizes and four GUI scales.");
|
||||
}finally{
|
||||
context.runOnClient(c -> {c.getWindow().setWindowed(originalWidth,originalHeight);c.options.guiScale().set(originalScale);c.resizeGui();});context.waitTicks(3);
|
||||
}
|
||||
}
|
||||
static void language(ClientGameTestContext context,String language) {
|
||||
if (context.computeOnClient(client -> client.getLanguageManager().getSelected().equals(language))) return;
|
||||
var reload=context.computeOnClient(client -> {
|
||||
client.getLanguageManager().setSelected(language);
|
||||
return client.reloadResourcePacks();
|
||||
});
|
||||
context.waitFor(client -> reload.isDone(),600);
|
||||
reload.join();
|
||||
context.waitFor(client -> client.gui.overlay()==null,600);
|
||||
context.runOnClient(client -> client.resizeGui());
|
||||
context.waitTick();
|
||||
}
|
||||
static void frame(Screen screen,String tab) {
|
||||
Button done=button(screen,"gui.done");
|
||||
require(done.getBottom()<=screen.height-5 && Math.abs(done.getX()+done.getWidth()/2-screen.width/2)<=1,"Native Done footer fits and stays centered");
|
||||
var title=screen.children().stream().filter(StringWidget.class::isInstance).map(StringWidget.class::cast)
|
||||
.filter(w -> key(w).equals("sanctuary.progression."+tab)).findFirst().orElseThrow();
|
||||
require(title.getY()>=0 && title.getBottom()<=24,"Compact page title fits its header");
|
||||
require(screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.noneMatch(b -> List.of("blocks","progress","history","identity").stream().anyMatch(k -> key(b).equals("sanctuary.progression."+k))),"Pages do not duplicate pause navigation");
|
||||
}
|
||||
private static String key(AbstractWidget widget) {
|
||||
return widget.getMessage().getContents() instanceof TranslatableContents t ? t.getKey() : "";
|
||||
}
|
||||
private static Button button(Screen screen,String key) {
|
||||
return screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> key(b).equals(key)).findFirst().orElseThrow();
|
||||
}
|
||||
private static ObjectSelectionList<?> list(Screen screen) {
|
||||
return screen.children().stream().filter(ObjectSelectionList.class::isInstance).map(ObjectSelectionList.class::cast)
|
||||
.findFirst().orElseThrow();
|
||||
}
|
||||
private static EditBox search(Screen screen) {
|
||||
return screen.children().stream().filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow();
|
||||
}
|
||||
private static void require(boolean condition,String message) { if (!condition) throw new AssertionError(message); }
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.recipes.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import mezz.jei.api.constants.*;
|
||||
import mezz.jei.api.recipe.RecipeIngredientRole;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
/** End-to-end catalogue lookup, not just presence in the server recipe set. */
|
||||
public final class Blueprint036ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(armed.get()&&m.gui.screen() instanceof HelloWorldScreen screen){widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary blueprint036 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&RecipeClient.ready&&SanctuaryJeiPlugin.runtime()!=null&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.giveExperienceLevels(4-p.experienceLevel);RecipeService.refresh(p);});c.waitFor(m->RecipeClient.xp>=4,150);
|
||||
c.runOnClient(m->RecipeClient.request("buy"));c.waitFor(m->RecipeClient.allowed()&&RecipeClient.knowledge.learned(),150);
|
||||
c.runOnClient(m->{var filter=SanctuaryJeiPlugin.runtime().getIngredientFilter();filter.setFilterText("");require(filter.getFilteredItemStacks().stream().noneMatch(i->i.is(Items.BOW)),"Undiscovered bow blueprint is initially hidden");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(String id:List.of("adventure/kill_a_mob","story/root","husbandry/fishy_business","adventure/trade")){var a=s.getAdvancements().get(Identifier.withDefaultNamespace(id));for(String criterion:a.value().criteria().keySet())p.getAdvancements().award(a,criterion);}RecipeService.refresh(p);});
|
||||
c.waitFor(m->RecipeClient.knowledge.collections.containsAll(List.of("sanctuary:archery","sanctuary:basic_wood","sanctuary:boats_and_rafts","sanctuary:books_and_storage")),200);
|
||||
c.runOnClient(m->{var r=SanctuaryJeiPlugin.runtime();r.getIngredientFilter().setFilterText("");var shown=r.getIngredientFilter().getFilteredItemStacks();
|
||||
for(Item item:List.of(Items.BOW,Items.CROSSBOW,Items.CRAFTING_TABLE,Items.OAK_BOAT,Items.BAMBOO_RAFT,Items.BOOK,Items.BOOKSHELF)){
|
||||
String id=net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(item).toString();require(!RecipeClient.knowledge.items.contains(id),"Blueprint does not invent possession: "+id);require(shown.stream().anyMatch(i->i.is(item)),"Known recipe output searchable in actual JEI catalogue: "+id);
|
||||
require(r.getRecipeManager().createRecipeLookup(RecipeTypes.CRAFTING).get().anyMatch(v->v.id().identifier().toString().equals(id)),"Corresponding recipe really visible: "+id);
|
||||
}
|
||||
require(shown.stream().noneMatch(i->i.is(Items.DIAMOND_BLOCK)||i.is(Items.STRING)),"Unrelated item and unknown input remain hidden");
|
||||
require(!RecipeClient.operator,"All checks use inhabitant mode");});
|
||||
for(Item item:List.of(Items.BOW,Items.CRAFTING_TABLE,Items.OAK_BOAT,Items.BOOK)){
|
||||
c.runOnClient(m->{m.gui.setScreen(new InventoryScreen(m.player));var r=SanctuaryJeiPlugin.runtime();r.getRecipesGui().show(r.getJeiHelpers().getFocusFactory().createFocus(RecipeIngredientRole.OUTPUT,VanillaTypes.ITEM_STACK,item.getDefaultInstance()));});
|
||||
c.waitFor(m->m.gui.screen() instanceof mezz.jei.gui.recipes.RecipesGui,100);c.waitTicks(3);c.takeScreenshot("sanctuary-beta036-blueprint-"+net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(item).getPath());c.runOnClient(m->m.gui.setScreen(null));
|
||||
}
|
||||
System.out.println("BLUEPRINT036 passed: Archery, Woodworking, Boats and Rafts, Books and Storage; real JEI searchable outputs + focused recipe screens; undiscovered inputs/other objects hidden; no invented possessions; inhabitant permissions.");
|
||||
}
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static Button button(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.boats.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.entity.CampfireBlockEntity;
|
||||
import net.minecraft.client.renderer.blockentity.state.*;
|
||||
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class BoatsHead049ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b,"sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{
|
||||
s.setName("BoatsHead049 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(true);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
for(String locale:new String[]{"en_us","fr_fr"})c.runOnClient(m->{
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for(var v:CrewBoats.variants()){
|
||||
var boat=v.type().create(m.level,EntitySpawnReason.COMMAND);
|
||||
require(m.getEntityRenderDispatcher().getRenderer(boat)!=null,"Native renderer for "+v.wood());
|
||||
var mesh=TiledBoatModel.hull(v.columns(),v.rows(),v.raft()).bakeRoot();
|
||||
require(mesh.getAllParts().size()==v.tiles()+3+(v.raft()?0:2*(v.columns()+v.rows())+4),"One floor per cell, only exterior rails and two paddles");
|
||||
require(mesh.getAllParts().stream().allMatch(part->part.xScale==1&&part.yScale==1&&part.zScale==1),"No stretched model part");
|
||||
require(mesh.hasChild("left_paddle")&&mesh.hasChild("right_paddle"),"Exactly one native pair of paddles");
|
||||
require(TiledBoatModel.waterMask(v.columns(),v.rows()).bakeRoot().getAllParts().size()==v.tiles()+1,"Water mask covers every floor tile");
|
||||
require(!new ItemStack(v.item()).getHoverName().getString().startsWith("item.sanctuary"),"Localized item name "+locale);
|
||||
}
|
||||
});
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.getInventory().clearContent();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
p.setPos(p.position().add(0,2,0));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.CAMPFIRE));
|
||||
var session=HeadBlockService.session(p);
|
||||
HeadBlockService.scoped(session,()->{((CampfireBlockEntity)session.entity).placeFood(p.level(),p,new ItemStack(Items.BEEF));return null;});
|
||||
});
|
||||
c.runOnClient(m->m.options.setCameraType(CameraType.THIRD_PERSON_FRONT));c.getInput().pressKey(InputConstants.KEY_F1);c.getInput().lookAt(0,0);c.waitTicks(15);
|
||||
c.runOnClient(m->{var h=head(m);require(h.blockEntity instanceof CampfireRenderState fire&&fire.items.stream().anyMatch(i->!i.isEmpty()),"Actual campfire renderer receives network food");});
|
||||
c.takeScreenshot("sanctuary-beta049-campfire-food");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.CHEST));HeadBlockService.session(p).event(1,1);});
|
||||
c.waitTicks(15);
|
||||
c.runOnClient(m->{var h=head(m);require(h.blockEntity instanceof ChestRenderState chest&&chest.open>.7,"Native lid animation opens on client from server event");});
|
||||
c.takeScreenshot("sanctuary-beta049-chest-open");
|
||||
server.runOnServer(s->HeadBlockService.session(s.getPlayerList().getPlayers().getFirst()).event(1,0));c.waitTicks(15);
|
||||
c.runOnClient(m->{var h=head(m);require(h.blockEntity instanceof ChestRenderState chest&&chest.open<.05,"Native lid closes without recreating the proxy");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.SHULKER_BOX));HeadBlockService.session(p).event(1,1);});
|
||||
c.waitTicks(15);
|
||||
c.runOnClient(m->require(head(m).blockEntity instanceof ShulkerBoxRenderState box&&box.progress>.9,"Shulker opens visually without running client block collisions"));
|
||||
c.takeScreenshot("sanctuary-beta049-shulker-open");
|
||||
server.runOnServer(s->HeadBlockService.session(s.getPlayerList().getPlayers().getFirst()).event(1,0));c.waitTicks(15);
|
||||
c.runOnClient(m->require(head(m).blockEntity instanceof ShulkerBoxRenderState box&&box.progress<.05,"Shulker lid closes"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.BELL));HeadBlockService.session(p).event(1,net.minecraft.core.Direction.NORTH.get3DDataValue());});
|
||||
c.waitTicks(12);c.runOnClient(m->require(head(m).blockEntity instanceof BellRenderState bell&&bell.ticks>1,"Bell ring animates over successive ticks"));
|
||||
c.waitTicks(50);c.runOnClient(m->require(head(m).blockEntity instanceof BellRenderState bell&&bell.shakeDirection==null,"Bell ringing stops"));
|
||||
for(var item:List.of(Items.FURNACE,Items.PLAYER_HEAD,Items.OAK_TRAPDOOR,CrewBoats.variant("oak",3,1).item())){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(item));HeadBlockService.session(p);});
|
||||
c.waitTicks(12);
|
||||
c.runOnClient(m->{var h=head(m);if(item instanceof net.minecraft.world.item.BoatItem)require(h.entity instanceof net.minecraft.client.renderer.entity.state.BoatRenderState,"Boat hat uses its entity model");else require(h.blockEntity!=null||!h.block.isEmpty(),"Native head visual exists");});
|
||||
c.takeScreenshot("sanctuary-beta049-head-"+net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(item).getPath());
|
||||
}
|
||||
server.runOnServer(s->AccessoryAccess.of(s.getPlayerList().getPlayers().getFirst()).clearContent());
|
||||
c.runOnClient(m->m.options.setCameraType(CameraType.THIRD_PERSON_BACK));
|
||||
for(var shape:List.of(new CrewBoats.Shape(3,1),new CrewBoats.Shape(1,3),new CrewBoats.Shape(2,3),new CrewBoats.Shape(3,2))){
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var old=p.getVehicle();p.stopRiding();if(old!=null)old.getSelfAndPassengers().toList().reversed().forEach(Entity::discard);
|
||||
var boat=CrewBoats.variant("oak",shape.columns(),shape.rows()).type().create(s.overworld(),EntitySpawnReason.COMMAND);boat.setPos(p.position().add(0,8,0));s.overworld().addFreshEntity(boat);
|
||||
var motor=EntityTypes.ALLAY.create(s.overworld(),EntitySpawnReason.COMMAND);motor.setPos(boat.position());motor.setNoAi(true);s.overworld().addFreshEntity(motor);motor.startRiding(boat);p.startRiding(boat,true,true);
|
||||
});
|
||||
c.waitFor(m->m.player.getVehicle() instanceof CrewBoat b&&b.columns()==shape.columns()&&b.rows()==shape.rows(),200);
|
||||
c.getInput().holdKey(o->o.keyJump);c.getInput().holdKey(o->o.keyUp);c.waitTicks(20);
|
||||
Vec3 from=c.computeOnClient(m->m.player.getVehicle().position());c.waitTicks(40);Vec3 to=c.computeOnClient(m->m.player.getVehicle().position());
|
||||
require(to.y>from.y+2&&to.subtract(from).horizontalDistance()>5,"Each rectangular boat flies from native pilot inputs");
|
||||
c.getInput().lookAt(25,20);c.waitTicks(5);c.takeScreenshot("sanctuary-beta049-boat-"+shape.columns()+"x"+shape.rows());
|
||||
c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyJump);
|
||||
}
|
||||
System.out.println("BOATS_HEAD049_CLIENT_PASS: 66 tiled models FR/EN, actual campfire food, chest and Shulker lids open/close, head models, four rectangular flights");
|
||||
}
|
||||
}
|
||||
private static HeadCosmeticState head(net.minecraft.client.Minecraft m){
|
||||
var renderer=m.getEntityRenderDispatcher().getRenderer(m.player);
|
||||
var state=renderer.createRenderState(m.player,1);
|
||||
return ((HeadCosmeticState.Access)state).sanctuary$headCosmetic();
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static boolean key(Button button,String key){return button.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean yes,String message){if(!yes)throw new AssertionError(message);}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.building.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.input.MouseButtonInfo;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Real native input and network packets, with a fresh development-only wall. */
|
||||
final class Building009ClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server) {
|
||||
server.runOnServer(s -> s.getPlayerList().getPlayers().getFirst().giveExperienceLevels(3));
|
||||
c.waitFor(m -> m.player.experienceLevel==3,400);
|
||||
for (int rank=1;rank<=2;rank++) {
|
||||
c.runOnClient(m -> {
|
||||
var button=m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals("sanctuary.progression.upgrade_cost"))
|
||||
.sorted(java.util.Comparator.comparingInt(Button::getY)).skip(3).findFirst().orElseThrow();
|
||||
require(button.active,"Construction is purchasable in the skill column");
|
||||
double x=(button.getX()+button.getWidth()/2.)*m.getWindow().getScreenWidth()/m.getWindow().getGuiScaledWidth();
|
||||
double y=(button.getY()+button.getHeight()/2.)*m.getWindow().getScreenHeight()/m.getWindow().getGuiScaledHeight();
|
||||
m.mouseHandler.onMove(m.getWindow().handle(),x,y,0,0);m.mouseHandler.handleAccumulatedMovement();
|
||||
var mouse=new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0);
|
||||
m.mouseHandler.onButton(m.getWindow().handle(),mouse,InputConstants.PRESS);
|
||||
m.mouseHandler.onButton(m.getWindow().handle(),mouse,InputConstants.RELEASE);
|
||||
});
|
||||
int expected=rank;c.waitFor(m -> ProgressionClient.progress.building()==expected,400);
|
||||
}
|
||||
c.waitFor(m -> m.player.experienceLevel==0,400);
|
||||
Blocodex004ClientChecks.language(c,"fr_fr");c.takeScreenshot("sanctuary-beta009-construction-fr");
|
||||
Blocodex004ClientChecks.language(c,"en_us");c.clickScreenButton("gui.done");c.waitForScreen(null);
|
||||
var origin=server.computeOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var base=p.blockPosition().above(14);
|
||||
for(int x=-3;x<=3;x++) for(int z=-1;z<=5;z++) {
|
||||
p.level().setBlockAndUpdate(base.offset(x,-1,z),Blocks.BEDROCK.defaultBlockState());
|
||||
for(int y=0;y<=6;y++) p.level().setBlockAndUpdate(base.offset(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
var target=base.offset(0,2,3);
|
||||
for(int x=-2;x<=2;x++) for(int y=-2;y<=2;y++) p.level().setBlockAndUpdate(target.offset(x,y,0),Blocks.STONE.defaultBlockState());
|
||||
p.teleportTo(base.getX()+.5,base.getY(),base.getZ()+.5);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.COBBLESTONE,32));p.inventoryMenu.broadcastChanges();
|
||||
return target;
|
||||
});
|
||||
c.waitFor(m -> m.level.getBlockState(origin).is(Blocks.STONE) && m.player.getMainHandItem().is(Items.COBBLESTONE)
|
||||
&& Math.abs(m.player.getY()-(origin.getY()-2))<.1,600);
|
||||
c.getInput().lookAt(origin);c.waitTicks(3);c.getInput().holdKey(InputConstants.KEY_R);
|
||||
c.waitFor(m -> BuildingClient.previewSize()==25 && MiningClient.previewSize()==0,200);
|
||||
c.takeScreenshot("sanctuary-beta009-building-preview-25");
|
||||
c.runOnClient(m -> {
|
||||
int slot=m.player.getInventory().getSelectedSlot();m.mouseHandler.onScroll(m.getWindow().handle(),0,-1);
|
||||
require(BuildingClient.selectedRank()==1 && m.player.getInventory().getSelectedSlot()==slot,"Shared modifier wheel selects a 3x3 surface without changing material");
|
||||
});
|
||||
c.waitFor(m -> BuildingClient.previewSize()==9,100);
|
||||
c.runOnClient(m -> m.mouseHandler.onScroll(m.getWindow().handle(),0,1));
|
||||
c.waitFor(m -> BuildingClient.previewSize()==25,100);
|
||||
c.getInput().holdKey(o -> o.keyUse);
|
||||
c.waitFor(m -> m.level.getBlockState(origin.north()).is(Blocks.COBBLESTONE),300);
|
||||
c.getInput().releaseKey(o -> o.keyUse);
|
||||
server.waitFor(s -> !BuildingManager.active(s.getPlayerList().getPlayers().getFirst()),200);
|
||||
int first=server.computeOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();int used=32-p.getMainHandItem().getCount();
|
||||
require(used>0 && used<25,"Releasing right click keeps only the paid partial surface");
|
||||
return used;
|
||||
});
|
||||
c.getInput().releaseKey(InputConstants.KEY_R);resetWall(c,server,origin);
|
||||
c.getInput().lookAt(origin);c.waitTicks(3);c.getInput().holdKey(InputConstants.KEY_R);
|
||||
c.waitFor(m -> BuildingClient.previewSize()==25,200);c.getInput().holdKey(o -> o.keyUse);
|
||||
server.waitFor(s -> s.getPlayerList().getPlayers().getFirst().getMainHandItem().getCount()==7,600);
|
||||
c.waitTicks(10); // Holding beyond completion must not create a second layer.
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();
|
||||
require(p.getMainHandItem().getCount()==7 && !BuildingManager.active(p),"One gesture creates exactly one 25-block surface");
|
||||
require(p.getStats().getValue(Stats.ITEM_USED.get(Items.COBBLESTONE))==25+first,"Native placement statistics count every paid block once");
|
||||
for(int x=-2;x<=2;x++) for(int y=-2;y<=2;y++) require(p.level().getBlockState(origin.offset(x,y,-1)).is(Blocks.COBBLESTONE),"Full surface matches the preview");
|
||||
});
|
||||
c.getInput().releaseKey(o -> o.keyUse);c.getInput().releaseKey(InputConstants.KEY_R);c.waitTicks(3);
|
||||
c.takeScreenshot("sanctuary-beta009-built-wall");resetWall(c,server,origin);c.getInput().lookAt(origin);c.waitTicks(3);
|
||||
c.runOnClient(m -> {sharedKey(m).setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_G));KeyMapping.resetMapping();});
|
||||
try {
|
||||
c.getInput().holdKey(InputConstants.KEY_G);c.waitFor(m -> BuildingClient.previewSize()==25,200);
|
||||
c.getInput().holdKey(o -> o.keyUse);
|
||||
server.waitFor(s -> BuildingManager.active(s.getPlayerList().getPlayers().getFirst()),200);
|
||||
c.getInput().pressKey(InputConstants.KEY_B);c.waitForScreen(ProgressionScreen.class);
|
||||
server.waitFor(s -> !BuildingManager.active(s.getPlayerList().getPlayers().getFirst()),200);
|
||||
} finally {
|
||||
c.getInput().releaseKey(o -> o.keyUse);c.getInput().releaseKey(InputConstants.KEY_R);c.getInput().releaseKey(InputConstants.KEY_G);
|
||||
c.runOnClient(m -> {sharedKey(m).setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_R));KeyMapping.resetMapping();});
|
||||
}
|
||||
System.out.println("Building beta009 native client passed: two real purchases, 25/9 previews, wheel, release, exact consumption, one frozen layer, key rebind and Blocodex interruption.");
|
||||
}
|
||||
private static void resetWall(ClientGameTestContext c,TestServerContext server,BlockPos origin) {
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();
|
||||
for(int x=-2;x<=2;x++) for(int y=-2;y<=2;y++) p.level().setBlockAndUpdate(origin.offset(x,y,-1),Blocks.AIR.defaultBlockState());
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.COBBLESTONE,32));p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m -> m.level.getBlockState(origin.north()).isAir() && m.player.getMainHandItem().getCount()==32,300);
|
||||
}
|
||||
private static KeyMapping sharedKey(net.minecraft.client.Minecraft m) {
|
||||
return java.util.Arrays.stream(m.options.keyMappings).filter(k -> k.getName().equals("sanctuary.mining.key")).findFirst().orElseThrow();
|
||||
}
|
||||
private static void require(boolean value,String message) {if(!value) throw new AssertionError(message);}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.recipes.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import mezz.jei.api.constants.RecipeTypes;
|
||||
import mezz.jei.api.sanctuary.CatalogueAccess;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
public final class Collections035ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
int scale=c.computeOnClient(m->m.options.guiScale().get());
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary beta035 recipes "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&RecipeClient.ready&&SanctuaryJeiPlugin.runtime()!=null&&m.gui.screen()==null,1200);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new InventoryScreen(m.player));});c.waitTicks(6);
|
||||
c.runOnClient(m->{require(!RecipeClient.allowed(),"Catalogue initially locked");require(!SanctuaryJeiPlugin.runtime().getIngredientListOverlay().isListDisplayed(),"Locked catalogue has no sidebar");SanctuaryJeiPlugin.runtime().getRecipesGui().showTypes(List.of(RecipeTypes.CRAFTING));require(m.gui.screen() instanceof InventoryScreen,"JEI cannot open while locked");});
|
||||
c.takeScreenshot("sanctuary-beta035-locked-inventory");
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.OAK_PLANKS,8));p.giveExperienceLevels(4-p.experienceLevel);RecipeService.refresh(p);});
|
||||
c.waitFor(m->RecipeClient.knowledge.recipes.contains("minecraft:crafting_table")&&RecipeClient.xp>=4,120);
|
||||
c.runOnClient(m->{require(!RecipeClient.knowledge.recipes.contains("minecraft:shield"),"Plank discovery does not teach shield");m.gui.setScreen(new RecipeScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));});c.waitTicks(3);c.takeScreenshot("sanctuary-beta035-collections-locked");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.recipes.buy").onPress(enter()));c.waitFor(m->RecipeClient.allowed()&&RecipeClient.knowledge.learned(),120);
|
||||
c.runOnClient(m->{var r=SanctuaryJeiPlugin.runtime();require(r.getIngredientFilter().getFilteredItemStacks().stream().noneMatch(i->i.is(Items.DIAMOND_BLOCK)),"Unknown item hidden");
|
||||
require(r.getIngredientFilter().getFilteredIngredients(mezz.jei.api.fabric.constants.FabricTypes.FLUID_STACK).isEmpty(),"Unknown fluids hidden too");
|
||||
require(r.getRecipeManager().createRecipeCategoryLookup().get().anyMatch(cat->cat.getRecipeType().equals(RecipeTypes.CRAFTING)),"Learned blueprints appear before discovering their crafting station");
|
||||
require(r.getRecipeManager().createRecipeLookup(RecipeTypes.CRAFTING).get().anyMatch(v->v.id().identifier().toString().equals("minecraft:crafting_table")),"Learned wood recipe visible");
|
||||
require(r.getRecipeManager().createRecipeLookup(RecipeTypes.CRAFTING).get().noneMatch(v->v.id().identifier().toString().equals("minecraft:shield")),"Unknown recipe hidden");
|
||||
m.gui.setScreen(new InventoryScreen(m.player));});c.waitTicks(5);c.takeScreenshot("sanctuary-beta035-personal-catalogue");
|
||||
hover(c,36);c.getInput().pressKey(InputConstants.KEY_U);c.waitFor(m->m.gui.screen() instanceof mezz.jei.gui.recipes.RecipesGui,60);
|
||||
c.runOnClient(m->{m.gui.setScreen(new RecipeScreen(null));button(m.gui.screen(),"sanctuary.recipes.open").onPress(enter());});c.waitFor(m->m.gui.screen() instanceof mezz.jei.gui.recipes.RecipesGui,60);c.waitTicks(5);c.takeScreenshot("sanctuary-beta035-wood-recipes");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.getPlayerList().op(p.nameAndId());RecipeService.refresh(p);});c.waitFor(m->RecipeClient.operatorAllowed,120);
|
||||
c.runOnClient(m->{require(!RecipeClient.operator,"Operator status alone does not enable full view");m.gui.setScreen(new RecipeScreen(null));RecipeClient.request("operator");});
|
||||
c.waitFor(m->RecipeClient.operator,120);c.waitTicks(3);c.takeScreenshot("sanctuary-beta035-operator-view");
|
||||
c.runOnClient(m->require(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilteredItemStacks().stream().anyMatch(i->i.is(Items.DIAMOND_BLOCK)),"Explicit operator sees unknown items"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.getPlayerList().deop(p.nameAndId());RecipeService.refresh(p);});
|
||||
c.waitFor(m->!RecipeClient.operatorAllowed&&!RecipeClient.operator,120);c.runOnClient(m->require(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilteredItemStacks().stream().noneMatch(i->i.is(Items.DIAMOND_BLOCK)),"Permission revocation removes unknown items"));
|
||||
// Use JEI's real transfer packet with ingredients available only in the final added row.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressEffects.get(p);for(int i=progress.inventory();i<5;i++)progress=progress.buy("inventory",1);
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);p.getInventory().clearContent();p.getInventory().setItem(SanctuaryInventoryCapacity.EXTRA_START+17,new ItemStack(Items.OAK_PLANKS,8));p.inventoryMenu.broadcastChanges();ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.inventoryRows()==6&&m.player.getInventory().getItem(SanctuaryInventoryCapacity.EXTRA_START+17).getCount()==8,120);
|
||||
c.runOnClient(m->{m.gui.setScreen(new InventoryScreen(m.player));var menu=m.player.inventoryMenu;var slots=CatalogueAccess.inventory.apply(menu,List.of());require(slots.size()==54,"JEI sees all six unlocked storage rows");
|
||||
var r=SanctuaryJeiPlugin.runtime();var manager=r.getRecipeManager();var category=manager.getRecipeCategory(RecipeTypes.CRAFTING);
|
||||
var recipe=manager.createRecipeLookup(RecipeTypes.CRAFTING).get().filter(v->v.id().identifier().toString().equals("minecraft:crafting_table")).findFirst().orElseThrow();
|
||||
var layout=manager.createRecipeLayoutDrawable(category,recipe,r.getJeiHelpers().getFocusFactory().createFocusGroup(List.of())).orElseThrow();
|
||||
var handler=r.getRecipeTransferManager().getRecipeTransferHandler(menu,category).orElseThrow();
|
||||
require(handler.transferRecipe(menu,recipe,layout.getRecipeSlotsView(),m.player,false,true)==null,"Real JEI transfer accepts overflow ingredients");});
|
||||
c.waitFor(m->m.player.inventoryMenu.getSlot(0).getItem().is(Items.CRAFTING_TABLE),120);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(p.getInventory().getItem(SanctuaryInventoryCapacity.EXTRA_START+17).getCount()==4,"Exactly four planks consumed from row six");
|
||||
for(int i=1;i<=4;i++)require(p.inventoryMenu.getSlot(i).getItem().is(Items.OAK_PLANKS)&&p.inventoryMenu.getSlot(i).getItem().getCount()==1,"Grid receives one plank per slot");});
|
||||
c.takeScreenshot("sanctuary-beta035-overflow-transfer");
|
||||
c.runOnClient(m->{m.player.closeContainer();NotificationClient.clearFeed();});c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
for(String id:List.of("minecraft:story/smelt_iron","minecraft:nether/brew_potion","minecraft:end/find_end_city")){
|
||||
var a=s.getAdvancements().get(net.minecraft.resources.Identifier.parse(id));for(String criterion:a.value().criteria().keySet())p.getAdvancements().award(a,criterion);
|
||||
}
|
||||
RecipeService.refresh(p);
|
||||
});
|
||||
c.waitFor(m->RecipeClient.knowledge.collections.containsAll(List.of("sanctuary:ironworking","sanctuary:brewing","sanctuary:shulker_storage")),200);
|
||||
c.runOnClient(m->{
|
||||
require(!RecipeClient.knowledge.items.contains("minecraft:iron_ingot")&&!RecipeClient.knowledge.items.contains("minecraft:shulker_shell"),"Advancement unlocks plans without revealing their ingredients as discovered");
|
||||
require(RecipeClient.knowledge.recipes.contains("minecraft:shield")&&RecipeClient.knowledge.recipes.contains("minecraft:shulker_box"),"Full families arrive over the actual personal network");
|
||||
require(SanctuaryJeiPlugin.runtime().getRecipeManager().createRecipeLookup(RecipeTypes.CRAFTING).get().anyMatch(r->r.id().identifier().toString().equals("minecraft:shulker_box")),"Shulker blueprint visible without shell possession");
|
||||
require(NotificationClient.messages().stream().anyMatch(e->e.value().text().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.notifications.collection_batch")),"New collection batch is announced");
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta035-collection-notifications");
|
||||
for(String language:List.of("fr_fr","en_us")){
|
||||
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected(language);m.options.languageCode=language;return m.reloadResourcePacks();});c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();
|
||||
c.runOnClient(m->{m.gui.setScreen(new RecipeScreen(null));require(RecipeClient.collectionName("sanctuary:ironworking").getString().equals(language.equals("fr_fr")?"Fer et forge de base":"Ironworking"),"Selected language is rendered");for(String id:RecipeClient.knowledge.collections)require(!RecipeClient.collectionName(id).getString().startsWith("sanctuary"),"Collections are translated");});
|
||||
c.waitTicks(5);c.takeScreenshot("sanctuary-beta035-collections-"+language);
|
||||
}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressEffects.get(p);
|
||||
for(String skill:ProgressRecord.SKILLS)while(progress.rank(skill)<ProgressRecord.cap(skill))progress=progress.buy(skill,1);
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);
|
||||
var action=new fr.koka.sanctuary.cycle.CyclePayloads.Action("ngp","","",ProgressionService.session(s).cycles.revision(),progress.revision());
|
||||
require(fr.koka.sanctuary.cycle.CycleService.perform(p,action).equals("done"),"Real New Game+ succeeds");RecipeService.refresh(p);});
|
||||
c.waitFor(m->ProgressionClient.progress.inventoryRows()==1&&ProgressionClient.progress.cycle()==1,120);
|
||||
c.runOnClient(m->{require(CatalogueAccess.inventory.apply(m.player.inventoryMenu,List.of()).size()==9,"After reset, locked recovery rows excluded from JEI autofill");require(RecipeClient.knowledge.learned(),"Catalogue persists after skill reset");
|
||||
m.options.guiScale().set(4);m.resizeGui();m.gui.setScreen(new RecipeScreen(null));});c.waitTicks(4);c.takeScreenshot("sanctuary-beta035-compact-blocodex");
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.hurtServer(p.level(),p.damageSources().fellOutOfWorld(),Float.MAX_VALUE);});
|
||||
c.waitForScreen(net.minecraft.client.gui.screens.DeathScreen.class);c.waitTicks(45);c.clickScreenButton("deathScreen.respawn");
|
||||
c.waitFor(m->m.player!=null&&m.player.isAlive()&&m.gui.screen()==null&&ProgressionClient.active(),1200);c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(RecipeKnowledge.parse(p.getAttached(RecipeService.RECORD)).learned(),"Real death preserves catalogue attachment");s.saveEverything(true,true,true);});
|
||||
System.out.println("COLLECTIONS035: locked UI, personal discoveries, recipes, operator revocation and real six-row transfer passed.");
|
||||
}finally{onboarding.set(false);c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();});}
|
||||
c.runOnClient(m->require(!RecipeClient.ready&&!RecipeClient.allowed(),"Recipe state cleared on world exit"));
|
||||
try(var reopened=saved.open()){
|
||||
reopened.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&RecipeClient.ready&&SanctuaryJeiPlugin.runtime()!=null,1200);
|
||||
c.runOnClient(m->{require(RecipeClient.knowledge.learned()&&RecipeClient.knowledge.recipes.contains("minecraft:crafting_table"),"Catalogue and recipes survive server restart");require(!RecipeClient.operator,"Operator inspection is not persisted");require(RecipeClient.knowledge.collections.containsAll(List.of("sanctuary:ironworking","sanctuary:brewing","sanctuary:shulker_storage"))&&RecipeClient.knowledge.recipes.contains("minecraft:shulker_box"),"New collections survive actual prestige, death and world restart");});
|
||||
System.out.println("COLLECTIONS035: actual prestige, death and server restart preserve recipe knowledge.");
|
||||
}
|
||||
}
|
||||
private static void hover(ClientGameTestContext c,int index){c.runOnClient(m->{var screen=(net.minecraft.client.gui.screens.inventory.AbstractContainerScreen<?>)m.gui.screen();var a=(fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor)screen;var slot=screen.getMenu().getSlot(index);var w=m.getWindow();double x=a.sanctuary$getLeftPos()+slot.x+8.0,y=a.sanctuary$getTopPos()+slot.y+8.0;double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();});c.waitTicks(2);}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?Stream.concat(Stream.of(w),widgets(nested)):Stream.of(w));}
|
||||
private static Button button(Screen screen,String key){return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(w->w.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow(()->new AssertionError("Missing "+key));}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
/** Integrated development world: native AI, networking and persistence, without a dedicated server EULA. */
|
||||
public final class Combat054ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.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());onboarding.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
var individual=new java.util.concurrent.atomic.AtomicReference<UUID>();
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Combat054 "+UUID.randomUUID());s.setSeed("54");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(true);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;
|
||||
require(FamiliarBattle.world(s)!=null&&FamiliarBattle.world(s).catalog.profiles().size()==88,"All 88 server profiles loaded");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set night");
|
||||
equip(p,"wolf");
|
||||
});
|
||||
c.waitTicks(25);
|
||||
world.getServer().runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
require(pet!=null,"Physical familiar spawns");individual.set(FamiliarBattle.bond(p).id());
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPos(pet.position().add(0,0,2));p.level().addFreshEntity(zombie);
|
||||
FamiliarBattle.engage(p,zombie,false);
|
||||
require(FamiliarBattle.state(p).target.equals(zombie.getUUID()),"Engage binds an enemy");
|
||||
});
|
||||
c.waitTicks(100);
|
||||
world.getServer().runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var target=FamiliarBattle.target(p);
|
||||
require(target!=null&&target.getHealth()<target.getMaxHealth(),"Native familiar navigation reaches and damages its enemy");
|
||||
var pet=FamiliarService.active(p);var b=FamiliarBattle.bond(p);
|
||||
pet.hurtServer(p.level(),pet.damageSources().mobAttack(target),3);
|
||||
require(FamiliarBattle.bond(p).health()<b.health(),"Hostile damage persists on individual");
|
||||
target.discard();FamiliarBattle.cancel(p);FamiliarBattle.flush(s);s.saveEverything(true,true,true);
|
||||
});
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());FamiliarClient.open(null);});
|
||||
c.waitTicks(6);c.takeScreenshot("sanctuary-beta054-familiar-"+locale);c.runOnClient(m->m.gui.setScreen(null));
|
||||
}
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(individual.get().equals(FamiliarBattle.bond(p).id()),"Same individual after reconnect");require(FamiliarBattle.bond(p).health()<FamiliarBattle.profile(p).health(),"Reconnect does not fully heal");});
|
||||
Duel054Checks.run(c,world.getServer());
|
||||
Experience054Checks.run(c,world.getServer());
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->Experience054Checks.reconnect(s.getPlayerList().getPlayers().getFirst()));
|
||||
}
|
||||
System.out.println("COMBAT054_CORE_PASS: physical autonomous attack, wound persistence, 88 profiles, FR/EN interface and real reconnect");
|
||||
}
|
||||
static void equip(ServerPlayer p,String id){
|
||||
FamiliarBattle.release(p);var type=net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getValue(net.minecraft.resources.Identifier.withDefaultNamespace(id));
|
||||
var egg=new ItemStack(SpawnEggItem.byId(type).orElseThrow());AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);
|
||||
CompanionService.tick(p);p.tickCount+=10;FamiliarService.tick(p);
|
||||
}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
static void require(boolean value,String text){if(!value)throw new AssertionError(text);}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Companion019ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());onboarding.set(false);
|
||||
}
|
||||
});
|
||||
int scale=c.computeOnClient(m->m.options.guiScale().get());
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary beta019 familiars "+UUID.randomUUID());s.setSeed("42");
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&CompanionClient.record!=null&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().above(5);
|
||||
for(int x=-9;x<=9;x++)for(int z=-9;z<=9;z++)for(int y=-1;y<8;y++)p.level().setBlockAndUpdate(at.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
p.teleportTo(at.getX()+.5,at.getY(),at.getZ()+.5);p.setYRot(0);p.setXRot(-8);
|
||||
p.getInventory().clearContent();p.giveExperienceLevels(10-p.experienceLevel);AccessoryAccess.of(p).setItem(0,new ItemStack(Items.CAT_SPAWN_EGG));CompanionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m->CompanionClient.species==CompanionType.CAT,200);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new ProgressionScreen(null));});c.waitTick();
|
||||
c.runOnClient(m->{require(!CompanionClient.record.unlocked(),"First session starts with active bond locked");button(m.gui.screen(),"sanctuary.companion.buy").onPress(enter());});
|
||||
c.waitFor(m->CompanionClient.record!=null&&CompanionClient.record.unlocked(),200);
|
||||
require(server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().experienceLevel)==6,"Purchase is synchronized and costs four levels");
|
||||
c.runOnClient(m->CompanionClient.openInhabitant(null));c.waitTick();
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.companion.consent_on").onPress(enter()));c.waitFor(m->!CompanionClient.record.acceptAllies(),200);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.companion.consent_off").onPress(enter()));c.waitFor(m->CompanionClient.record.acceptAllies(),200);
|
||||
for(int gui:new int[]{2,3,4}){
|
||||
c.runOnClient(m->{m.options.guiScale().set(gui);m.resizeGui();});c.waitTick();
|
||||
c.runOnClient(m->{for(var widget:widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).toList())
|
||||
require(widget.getWidth()>0&&widget.getX()>=0&&widget.getRight()<=m.gui.screen().width,"Familiar buttons fit GUI width");});
|
||||
c.takeScreenshot("sanctuary-beta019-familiar-gui-"+gui);
|
||||
}
|
||||
c.runOnClient(m->{m.gui.setScreen(null);m.options.guiScale().set(2);m.resizeGui();});c.waitTick();
|
||||
// Extract all native render states at an actual nonzero world position, at two animation times.
|
||||
c.runOnClient(m->{
|
||||
try{
|
||||
var field=FamiliarEntity.class.getDeclaredField("EGG");field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked") var eggData=(net.minecraft.network.syncher.EntityDataAccessor<ItemStack>)field.get(null);
|
||||
var pet=FamiliarService.TYPE.create(m.level,EntitySpawnReason.COMMAND);pet.setId(900000);pet.setPos(m.player.position().add(2,1,3));
|
||||
pet.xo=pet.getX();pet.yo=pet.getY();pet.zo=pet.getZ();pet.xOld=pet.getX();pet.yOld=pet.getY();pet.zOld=pet.getZ();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
for(var type:CompanionType.values()){
|
||||
pet.getEntityData().set(eggData,new ItemStack(BuiltInRegistries.ITEM.getValue(Identifier.withDefaultNamespace(type.id()+"_spawn_egg"))));
|
||||
pet.tickCount=100;pet.setDeltaMovement(.05,0,.1);pet.setYRot(30);
|
||||
var state=renderer.createRenderState();renderer.extractRenderState(pet,state,.5F);
|
||||
require(state.appearance!=null&&state.renderer!=null,"Native visual exists for "+type);
|
||||
require(net.minecraft.util.LightCoordsUtil.sky(state.appearance.lightCoords)==m.level.getBrightness(net.minecraft.world.level.LightLayer.SKY,pet.blockPosition()),"Native light follows familiar world position for "+type);
|
||||
pet.tickCount=110;pet.setYRot(65);renderer.extractRenderState(pet,state,.5F);
|
||||
require(Float.isFinite(state.size)&&state.size>0,"Native visual keeps finite scale for "+type);
|
||||
if(pet.motion().flies()){
|
||||
var camera=new net.minecraft.client.renderer.state.level.CameraRenderState();camera.pos=pet.position().add(1.5,.35,0);camera.orientation=new org.joml.Quaternionf();
|
||||
var collector=new net.minecraft.client.renderer.SubmitNodeStorage();
|
||||
renderer.submit(state,new com.mojang.blaze3d.vertex.PoseStack(),collector,camera);
|
||||
var submits=new ArrayList<net.minecraft.client.renderer.feature.ModelFeatureRenderer.Submit<?>>();
|
||||
collector.drainPhases(phase->phase.sortInto((node,sorted)->{if(node instanceof net.minecraft.client.renderer.feature.ModelFeatureRenderer.Submit<?> model)submits.add(model);}));
|
||||
require(!submits.isEmpty(),"Flying model submits translucent geometry: "+type);
|
||||
for(var model:submits)require(model.renderType().hasBlending()&&(model.tintedColor()>>>24)<100,"Body and eye layers both fade: "+type);
|
||||
require(FamiliarFade.current()==1,"Transparency never leaks into the next entity");
|
||||
}
|
||||
float ordinarySize=state.size;
|
||||
for(int perMille:new int[]{120,6000}){
|
||||
var sized=pet.egg().copy();net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,sized,tag->{
|
||||
var trait=new net.minecraft.nbt.CompoundTag();trait.putInt("schema",1);trait.putInt("per_mille",perMille);tag.put(fr.koka.sanctuary.cosmetics.FamiliarSize.KEY,trait);
|
||||
});pet.getEntityData().set(eggData,sized);renderer.extractRenderState(pet,state,.5F);
|
||||
require(Math.abs(state.size-ordinarySize*perMille/1000F)<.0001,"Smallest and largest egg scale applies to native model "+type);
|
||||
}
|
||||
}
|
||||
}catch(ReflectiveOperationException error){throw new AssertionError(error);}
|
||||
});
|
||||
for(var species:List.of(CompanionType.ENDER_DRAGON,CompanionType.WITHER,CompanionType.GHAST)){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(0,new ItemStack(BuiltInRegistries.ITEM.getValue(Identifier.withDefaultNamespace(species.id()+"_spawn_egg"))));});
|
||||
c.waitFor(m->CompanionClient.species==species,200);c.waitTicks(60);
|
||||
var geometry=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);return new double[]{pet.getY()-p.getY(),pet.getX(),pet.getZ()};});
|
||||
require(geometry[0]>.3,"Airborne familiar stays airborne: "+species);
|
||||
c.runOnClient(m->{var toward=new Vec3(geometry[1],m.player.getY()+geometry[0]+.35,geometry[2]).subtract(m.player.getEyePosition());
|
||||
m.player.setYRot((float)Math.toDegrees(Math.atan2(-toward.x,toward.z)));m.player.setXRot((float)-Math.toDegrees(Math.atan2(toward.y,toward.horizontalDistance())));
|
||||
});c.waitTick();c.takeScreenshot("sanctuary-beta019-"+species.id());
|
||||
}
|
||||
for(double distance:new double[]{4,2,.8}){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);pet.setNoAi(true);pet.getNavigation().stop();pet.setDeltaMovement(Vec3.ZERO);pet.setPos(p.getEyePosition().add(0,-.35,distance));});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});c.waitTicks(5);
|
||||
c.takeScreenshot("sanctuary-beta019-fade-"+distance);
|
||||
}
|
||||
// Real client key -> server activation -> durable cooldown, followed by an egg swap.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(0,new ItemStack(Items.SLIME_SPAWN_EGG));});
|
||||
c.waitFor(m->CompanionClient.species==CompanionType.SLIME&&CompanionClient.attunement()==0,400);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);
|
||||
c.waitFor(m->CompanionClient.seconds()>0,100);
|
||||
require(server.computeOnServer(s->CompanionEffects.has(s.getPlayerList().getPlayers().getFirst(),CompanionEffects.Kind.REBOUND)),"Configured key activates the actual equipped power");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(0,new ItemStack(Items.CAT_SPAWN_EGG));});
|
||||
c.waitFor(m->CompanionClient.species==CompanionType.CAT,100);
|
||||
require(CompanionClient.seconds()>0,"Changing egg does not reset the client cooldown");
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();AccessoryAccess.of(p).setItem(0,new ItemStack(Items.VILLAGER_SPAWN_EGG));CompanionService.tick(p);CompanionService.runtime(p).equippedAt=p.tickCount-200;
|
||||
var old=CompanionService.record(p);CompanionService.save(p,new CompanionRecord(1,old.unlocked(),old.paid(),old.purchasedAt(),0,0,0,old.acceptAllies(),old.notes()));
|
||||
p.getInventory().setItem(0,new ItemStack(Items.EMERALD,10));p.setYRot(0);p.setXRot(0);
|
||||
var v=EntityTypes.VILLAGER.create(p.level(),EntitySpawnReason.COMMAND);v.setNoAi(true);v.setPos(p.position().add(0,0,2));
|
||||
var offers=new net.minecraft.world.item.trading.MerchantOffers();offers.add(new net.minecraft.world.item.trading.MerchantOffer(new net.minecraft.world.item.trading.ItemCost(Items.EMERALD,2),new ItemStack(Items.BREAD),2,1,0));v.setOffers(offers);p.level().addFreshEntity(v);CompanionService.send(p,"");
|
||||
});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});
|
||||
c.waitFor(m->CompanionClient.species==CompanionType.VILLAGER&&CompanionClient.attunement()==0,100);c.waitTicks(15);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);
|
||||
c.waitFor(m->m.gui.screen() instanceof CompanionChoiceScreen||!CompanionClient.message.isEmpty(),100);
|
||||
require(c.computeOnClient(m->m.gui.screen() instanceof CompanionChoiceScreen),"Quote opens; server response: "+CompanionClient.message);
|
||||
c.takeScreenshot("sanctuary-beta019-trade-quote");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.companion.choose").onPress(enter()));
|
||||
c.waitFor(m->m.gui.screen()==null,100);
|
||||
require(server.computeOnServer(s->new CompanionInventory.Plan(s.getPlayerList().getPlayers().getFirst()).count(i->i.is(Items.BREAD)))==2,"The reviewed client quote produces two paid trades");
|
||||
require(!CompanionClient.hasChoice(),"A successful quote disappears from the menu");
|
||||
|
||||
}finally{c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();});}
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler container){return container.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?Stream.concat(Stream.of(w),widgets(nested)):Stream.of(w));}
|
||||
private static Button button(Screen screen,String key){return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow(()->new AssertionError("Missing button "+key));}
|
||||
private static KeyEvent enter(){return new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.effect.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Fresh-world real key, network and rendering checks for the v2 abilities. */
|
||||
public final class Companion032ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(screen).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());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary refonte032 "+UUID.randomUUID());s.setSeed("42");
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
BlockPos origin=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().above(80);
|
||||
for(int x=-12;x<=12;x++)for(int z=-12;z<=12;z++)for(int y=-1;y<7;y++)p.level().setBlockAndUpdate(at.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
p.teleportTo(at.getX()+.5,at.getY(),at.getZ()+.5);p.getInventory().clearContent();p.setYRot(0);p.setXRot(55);return at;});
|
||||
c.waitFor(m->m.player.getY()>origin.getY()-.2,200);c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(55);});c.waitTicks(20);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();equip(p,CompanionType.SHEEP);p.getInventory().setItem(0,new ItemStack(Items.WOOL.white(),2));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->CompanionClient.species==CompanionType.SHEEP&&CompanionClient.attunement()==0,200);
|
||||
c.getInput().pressKey(InputConstants.KEY_G);c.waitFor(m->CompanionClient.seconds()>0,100);
|
||||
require(server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();return p.getMainHandItem().getCount()==1&&CompanionZones.active(p.level()).stream().anyMatch(z->z.kind==CompanionZones.Kind.CUSHION);}),"Real G input spends one wool and creates the world cushion");
|
||||
c.waitTicks(10);c.takeScreenshot("sanctuary-beta032-wool-cushion");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();equip(p,CompanionType.WITCH);
|
||||
var potion=new ItemStack(Items.POTION);potion.set(DataComponents.POTION_CONTENTS,new net.minecraft.world.item.alchemy.PotionContents(Optional.empty(),Optional.empty(),List.of(new MobEffectInstance(MobEffects.SPEED,600,0)),Optional.empty()));p.setItemInHand(InteractionHand.MAIN_HAND,potion);p.setYRot(0);p.setXRot(0);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->CompanionClient.species==CompanionType.WITCH&&CompanionClient.attunement()==0,100);c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});c.waitTicks(10);
|
||||
c.getInput().pressKey(InputConstants.KEY_G);c.waitFor(m->CompanionClient.seconds()>0,100);c.waitTicks(10);c.takeScreenshot("sanctuary-beta032-potion-mist");
|
||||
require(server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();return p.getMainHandItem().is(Items.GLASS_BOTTLE)&&CompanionZones.active(p.level()).stream().anyMatch(z->z.kind==CompanionZones.Kind.MIST&&!z.spent);}),"A real dose becomes a visible unused mist and returns one bottle");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var mist=CompanionZones.active(p.level()).stream().filter(z->z.kind==CompanionZones.Kind.MIST).findFirst().orElseThrow();p.teleportTo(mist.center.x,mist.center.y-.8,mist.center.z);});
|
||||
c.waitFor(m->m.player.hasEffect(MobEffects.SPEED),100);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();equip(p,CompanionType.RABBIT);p.teleportTo(origin.getX()+.5,origin.getY(),origin.getZ()+.5);});
|
||||
c.waitFor(m->CompanionClient.species==CompanionType.RABBIT&&CompanionClient.attunement()==0,100);c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(-70);});c.waitTicks(20);
|
||||
c.getInput().pressKey(InputConstants.KEY_G);c.waitFor(m->m.player.getY()>origin.getY()+.2,100);c.takeScreenshot("sanctuary-beta032-rabbit-hop");
|
||||
c.waitTicks(45);require(server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();return p.getY()<origin.getY()+2.2;}),"Rabbit hop stays bounded and lands, even with a native speed potion");
|
||||
// Exercise all 88 FR/EN tooltip paths in the native client; no translation keys may leak.
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.options.languageCode=locale;m.reloadResourcePacks();});c.waitTicks(60);
|
||||
c.runOnClient(m->{for(var type:CompanionType.values()){
|
||||
String passive=type.passive().getString(),active=type.active().getString();
|
||||
require(!passive.contains("sanctuary.companion")&&!active.contains("sanctuary.companion"),"Native translation resolves both powers: "+locale+"/"+type);
|
||||
}});
|
||||
}
|
||||
c.takeScreenshot("sanctuary-beta032-native-world");
|
||||
}
|
||||
}
|
||||
private static void equip(net.minecraft.server.level.ServerPlayer p,CompanionType type){
|
||||
AccessoryAccess.of(p).setItem(0,new ItemStack(BuiltInRegistries.ITEM.getValue(Identifier.withDefaultNamespace(type.id()+"_spawn_egg"))));
|
||||
FamiliarService.tick(p);CompanionService.tick(p);CompanionService.request(p,new CompanionPayloads.Request("operator_ready",-1));CompanionService.send(p,"");
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler container){return container.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?Stream.concat(Stream.of(w),widgets(nested)):Stream.of(w));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledgeService;
|
||||
import fr.koka.sanctuary.notifications.NotificationLayout.*;
|
||||
import fr.koka.sanctuary.mixin.client.NotificationAdvancementTabAccess;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.advancements.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class Corrections036ClientChecks implements FabricClientGameTest {
|
||||
private static final Identifier DIET=Identifier.withDefaultNamespace("husbandry/balanced_diet");
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(armed.get()&&m.gui.screen() instanceof HelloWorldScreen screen){widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);}});
|
||||
c.runOnClient(m->{require(new NotificationOptions().position==Position.TOP_RIGHT,"Fresh installs default top right");var o=NotificationClient.options();o.position=Position.TOP_RIGHT;o.seconds=3;o.pinnedAdvancements=true;o.advancements=true;o.discoveries=true;o.collections=true;});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary corrections036 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&RecipeClient.ready&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
c.runOnClient(m->{m.player.setXRot(-90);m.options.guiScale().set(2);m.resizeGui();});c.waitTicks(30);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(String id:List.of("husbandry/root","husbandry/plant_seed")){var a=s.getAdvancements().get(Identifier.withDefaultNamespace(id));for(String k:a.value().criteria().keySet())p.getAdvancements().award(a,k);}var a=s.getAdvancements().get(DIET);p.getAdvancements().award(a,"rotten_flesh");});
|
||||
c.waitFor(m->!NotificationClient.tracked().isEmpty(),200);c.waitTicks(130);
|
||||
c.runOnClient(m->{require(NotificationClient.tracked().isEmpty(),"A Balanced Diet expires after rotten flesh without becoming an automatic permanent tracker");require(NotificationClient.messages().isEmpty(),"Messages expire as well");});
|
||||
openDiet(c);clickDiet(c,false);c.runOnClient(m->require(NotificationClient.isPinned(DIET),"Actual native advancement click pins objective"));c.takeScreenshot("sanctuary-beta036-pin-advancement");
|
||||
clickDiet(c,true);c.runOnClient(m->require(NotificationClient.isPinned(DIET),"Dragging tree never toggles tracking"));
|
||||
openDiet(c);clickDiet(c,false);c.runOnClient(m->require(!NotificationClient.isPinned(DIET),"Second click unpins"));clickDiet(c,false);
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(125);c.runOnClient(m->require(NotificationClient.isPinned(DIET)&&NotificationClient.tracked().size()==1,"Explicit follow survives transient expiry"));
|
||||
server.runOnServer(s->BlockKnowledgeService.recordSeen(s.getPlayerList().getPlayers().getFirst(),Blocks.AMETHYST_BLOCK));c.waitFor(m->!NotificationClient.messages().isEmpty(),100);c.takeScreenshot("sanctuary-beta036-top-right");
|
||||
c.runOnClient(m->{m.gui.toastManager().clear();m.debugEntries.setOverlayVisible(true);});c.waitTicks(5);c.runOnClient(m->{require(!NotificationObstacles.debug.isEmpty(),"Native F3 text captured");noOverlap();});c.takeScreenshot("sanctuary-beta036-f3");
|
||||
for(int scale:List.of(1,2,3,4))for(Position position:Position.values()){
|
||||
c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();NotificationClient.options().position=position;});c.waitTicks(2);c.runOnClient(m->noOverlap());
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();NotificationClient.options().position=Position.TOP_RIGHT;m.options.debugGuiScale().set(1);});c.waitTicks(3);c.runOnClient(m->noOverlap());c.takeScreenshot("sanctuary-beta036-f3-separate-scale");
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.options.debugGuiScale().set(-1);m.debugEntries.setOverlayVisible(false);});
|
||||
server.runOnServer(s->{var b=s.getCustomBossEvents().create(s.overworld().getRandom(),Identifier.withDefaultNamespace("sanctuary_test_boss"),Component.literal("HUD layout test"));b.addPlayer(s.getPlayerList().getPlayers().getFirst());b.setValue(50);});
|
||||
c.runOnClient(m->NotificationClient.options().position=Position.TOP_CENTER);c.waitTicks(5);c.runOnClient(m->{var b=NotificationObstacles.lastLayout;require(b.y()>20,"Top center reserves native boss bar");});c.takeScreenshot("sanctuary-beta036-boss");
|
||||
c.runOnClient(m->NotificationClient.options().position=Position.BOTTOM_CENTER);c.waitTicks(3);c.takeScreenshot("sanctuary-beta036-bottom-center");
|
||||
server.runOnServer(s->{var b=s.getCustomBossEvents().get(Identifier.withDefaultNamespace("sanctuary_test_boss"));b.removeAllPlayers();s.getCustomBossEvents().remove(b);});
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected(locale);m.options.languageCode=locale;return m.reloadResourcePacks();});c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();
|
||||
c.runOnClient(m->{var o=NotificationClient.options();o.position=Position.TOP_RIGHT;o.save();m.gui.setScreen(new NotificationOptionsScreen(null));require(NotificationOptions.load().position==Position.TOP_RIGHT,"Anchor saved");require(NotificationOptions.load().trackedByWorld.values().stream().anyMatch(ids->ids.contains(DIET.toString())),"Explicit tracks saved per world");
|
||||
require(NotificationClient.label("position").getString().equals("Position"),"Actual translated position label");});c.waitTicks(3);c.takeScreenshot("sanctuary-beta036-options-"+locale);c.runOnClient(m->m.gui.setScreen(null));
|
||||
}
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().hurtServer(s.overworld(),s.overworld().damageSources().fellOutOfWorld(),Float.MAX_VALUE));
|
||||
c.waitFor(m->m.gui.screen() instanceof DeathScreen,200);c.waitTicks(45);c.runOnClient(m->button(m.gui.screen(),"deathScreen.respawn").onPress(enter()));c.waitFor(m->m.player!=null&&m.player.isAlive()&&m.gui.screen()==null,300);c.waitTicks(30);
|
||||
c.runOnClient(m->{require(NotificationClient.isPinned(DIET)&&NotificationClient.tracked().size()==1,"Selected follow survives respawn");require(NotificationClient.messages().isEmpty(),"Respawn does not replay old notifications");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var a=s.getAdvancements().get(DIET);for(String k:a.value().criteria().keySet())p.getAdvancements().award(a,k);});c.waitFor(m->!NotificationClient.isPinned(DIET),200);c.runOnClient(m->require(NotificationClient.tracked().isEmpty(),"Completed objective automatically leaves tracker"));
|
||||
gliding(c,server);
|
||||
System.out.println("HUD036 client passed: rotten-flesh expiry; real click/unclick/drag follow; persistent per-world selection; respawn; completion; F3 collision at 36 layouts + independent scale; boss/hotbar; FR/EN; live carried-parrot descent and landing.");
|
||||
}finally{c.runOnClient(m->{m.debugEntries.setOverlayVisible(false);var o=NotificationClient.options();o.position=Position.TOP_RIGHT;o.seconds=5;o.save();});}
|
||||
}
|
||||
private static void gliding(ClientGameTestContext c,TestServerContext server){
|
||||
var landing=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var ground=p.blockPosition().above(3);for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++){p.level().setBlockAndUpdate(ground.offset(x,0,z),Blocks.STONE.defaultBlockState());for(int y=1;y<=15;y++)p.level().setBlockAndUpdate(ground.offset(x,y,z),Blocks.AIR.defaultBlockState());}p.teleportTo(ground.getX()+.5,ground.getY()+1,ground.getZ()+.5);p.setHealth(p.getMaxHealth());return ground;});
|
||||
c.waitFor(m->m.player.onGround()&&m.player.getY()>=landing.getY()+1,200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var bird=EntityTypes.PARROT.create(p.level(),EntitySpawnReason.COMMAND);bird.setPos(p.position().add(0,0,1));p.level().addFreshEntity(bird);require(CarryService.interact(p,bird),"Native parrot carry");p.teleportTo(p.getX(),landing.getY()+10,p.getZ());p.positionRider(bird);});
|
||||
c.waitFor(m->CarryGlide.active(m.player)&&m.player.getY()>landing.getY()+4,200);c.waitTicks(12);
|
||||
c.runOnClient(m->{require(m.player.getDeltaMovement().y>=-.121&&m.player.getDeltaMovement().y<0,"Native LocalPlayer physics glides");require(m.player.fallDistance<.2,"No stored fall damage");});c.takeScreenshot("sanctuary-beta036-parrot-gliding");
|
||||
c.waitFor(m->m.player.onGround(),300);c.runOnClient(m->require(m.player.getHealth()==m.player.getMaxHealth(),"Real landing survives without fall damage"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var bird=p.getFirstPassenger();CarryService.release(bird,true);bird.discard();});c.waitFor(m->!CarryGlide.supported(m.player),100);
|
||||
}
|
||||
private static void noOverlap(){var b=NotificationObstacles.lastLayout;for(var obstacle:NotificationObstacles.boxes())require(!b.intersects(obstacle),"Notification overlap "+b+" / "+obstacle);}
|
||||
private static void openDiet(ClientGameTestContext c){
|
||||
c.runOnClient(m->{var adv=m.getConnection().getAdvancements();m.gui.setScreen(new AdvancementsScreen(adv));var root=adv.progress().keySet().stream().filter(a->a.id().toString().equals("minecraft:husbandry/root")).findFirst().orElseThrow();adv.setSelectedTab(root,true);});c.waitTicks(2);
|
||||
c.runOnClient(m->{var tab=tab((AdvancementsScreen)m.gui.screen());var access=(NotificationAdvancementTabAccess)tab;var w=access.sanctuary$widgets().values().stream().filter(a->a.getAdvancement().id().equals(DIET)).findFirst().orElseThrow();tab.scroll(100-w.getX()-access.sanctuary$scrollX(),45-w.getY()-access.sanctuary$scrollY());});c.waitTicks(2);
|
||||
}
|
||||
private static void clickDiet(ClientGameTestContext c,boolean drag){c.runOnClient(m->{var screen=(AdvancementsScreen)m.gui.screen();var tab=(NotificationAdvancementTabAccess)tab(screen);var w=tab.sanctuary$widgets().values().stream().filter(a->a.getAdvancement().id().equals(DIET)).findFirst().orElseThrow();int x=(int)Math.floor(tab.sanctuary$scrollX())+w.getX()+12,y=(int)Math.floor(tab.sanctuary$scrollY())+w.getY()+12;require(x>0&&x<234&&y>0&&y<113,"Advancement icon reachable in native viewport");var e=new MouseButtonEvent(number(screen,"leftPos")+9+x,number(screen,"topPos")+18+y,new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0));screen.mouseClicked(e,false);if(drag)screen.mouseDragged(e,8,0);screen.mouseReleased(e);});c.waitTicks(2);}
|
||||
private static AdvancementTab tab(AdvancementsScreen screen){try{var f=AdvancementsScreen.class.getDeclaredField("selectedTab");f.setAccessible(true);return (AdvancementTab)f.get(screen);}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static int number(AdvancementsScreen screen,String key){try{var f=AdvancementsScreen.class.getDeclaredField(key);f.setAccessible(true);return f.getInt(screen);}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static Button button(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow(()->new AssertionError(key));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
/** Creates only an ignored development world; validates confirmation through real packets. */
|
||||
public final class Cycle011ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (armed.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
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");armed.set(false);
|
||||
}
|
||||
});
|
||||
int scale=c.computeOnClient(m -> m.options.guiScale().get());
|
||||
try (var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Sanctuary beta011 cycles "+UUID.randomUUID());
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && ProgressionClient.active() && CycleClient.view!=null && m.gui.screen()==null,1200);
|
||||
for(int gui:new int[]{2,0}) {
|
||||
c.runOnClient(m -> {m.options.guiScale().set(gui);m.resizeGui();CycleClient.open(new net.minecraft.client.gui.screens.PauseScreen(true),"cycle");});
|
||||
c.waitTick();
|
||||
c.runOnClient(m -> require(!button(m.gui.screen(),"sanctuary.cycle.ngp").active,"New character cannot begin NGP"));
|
||||
c.takeScreenshot("sanctuary-beta011-cycle-locked-gui-"+gui);
|
||||
}
|
||||
world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressRecord.INITIAL.buy("names",4).buy("atlas",4);
|
||||
for (String skill:ProgressRecord.SKILLS) while(progress.rank(skill)<ProgressRecord.cap(skill))progress=progress.buy(skill,1);
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);p.giveExperienceLevels(20);ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.complete(),200);
|
||||
c.runOnClient(m -> {press(m.gui.screen(),"sanctuary.cycle.ngp");require(CycleClient.view.prestige()==0,"Opening preview makes no server change");});
|
||||
c.takeScreenshot("sanctuary-beta011-ngp-confirmation");
|
||||
c.runOnClient(m -> {press(m.gui.screen(),"sanctuary.cycle.cancel");require(CycleClient.view.prestige()==0,"Cancel keeps cycle");press(m.gui.screen(),"sanctuary.cycle.ngp");press(m.gui.screen(),"sanctuary.cycle.confirm");});
|
||||
c.waitFor(m -> CycleClient.view.prestige()==1 && ProgressionClient.progress.cycle()==1 && m.player.experienceLevel==0,200);
|
||||
c.runOnClient(m -> require(ProgressionClient.progress.names() && ProgressionClient.progress.atlas() && CycleClient.view.charges()==1,"Server confirmation preserves aptitudes and grants one charge"));
|
||||
for (int i=0;i<6;i++) c.waitTick();
|
||||
world.getServer().runOnServer(server -> {var p=server.getPlayerList().getPlayers().getFirst();p.giveExperienceLevels(10);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->m.player.experienceLevel==10,100);
|
||||
c.runOnClient(m -> {CycleClient.open(new net.minecraft.client.gui.screens.PauseScreen(true),"faction");});c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
var screen=m.gui.screen();screen.children().stream().filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow().setValue("Les Veilleurs");
|
||||
press(screen,"sanctuary.cycle.create");press(screen,"sanctuary.cycle.confirm");
|
||||
});
|
||||
c.waitFor(m -> CycleClient.view.faction()!=null && CycleClient.view.charges()==1 && m.player.experienceLevel==0,200);
|
||||
c.takeScreenshot("sanctuary-beta011-faction-created");
|
||||
for(int gui:new int[]{2,0}) {
|
||||
c.runOnClient(m -> {m.options.guiScale().set(gui);m.resizeGui();CycleClient.open(new net.minecraft.client.gui.screens.PauseScreen(true),"log");});
|
||||
c.waitTick();c.takeScreenshot("sanctuary-beta011-log-gui-"+gui);
|
||||
}
|
||||
} finally {armed.set(false);c.runOnClient(m -> {m.options.guiScale().set(scale);m.resizeGui();});}
|
||||
}
|
||||
private static KeyEvent enter() {return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static Button button(Screen screen,String key) {
|
||||
return screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key)).findFirst().orElseThrow();
|
||||
}
|
||||
private static void press(Screen screen,String key) {var button=button(screen,key);require(button.active,"Active button: "+key);button.onPress(enter());}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.network.protocol.game.*;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.server.network.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Two actual ServerPlayer entities, native AI/projectiles, persistent escrow and client screens. */
|
||||
public final class Duel054Checks {
|
||||
private static void check(boolean value,String message){Combat054ClientChecks.require(value,message);}
|
||||
// The context API type is obtained from the public Fabric test world rather than a dedicated server.
|
||||
public static void run(ClientGameTestContext c,net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext server){
|
||||
var remote=new java.util.concurrent.atomic.AtomicReference<ServerPlayer>();
|
||||
ServerTickEvents.END_SERVER_TICK.register(s->{var p=remote.get();if(p!=null&&p.level().getServer()==s){p.tickCount++;CompanionService.tick(p);FamiliarService.tick(p);FamiliarBattle.tick(p);}});
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition();
|
||||
p.level().getGameRules().set(net.minecraft.world.level.gamerules.GameRules.PVP,false,s);
|
||||
for(int x=-10;x<=10;x++)for(int z=-10;z<=10;z++)for(int y=-1;y<8;y++)p.level().setBlockAndUpdate(at.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
var profile=new GameProfile(UUID.randomUUID(),"Rival054");var other=new ServerPlayer(s,p.level(),profile,ClientInformation.createDefault());
|
||||
var connection=new Connection(PacketFlow.SERVERBOUND);new io.netty.channel.embedded.EmbeddedChannel(connection);
|
||||
other.connection=new ServerGamePacketListenerImpl(s,connection,other,CommonListenerCookie.createInitial(profile,false));
|
||||
other.setPos(p.position().add(0,0,5));other.setYRot(180);other.setYHeadRot(180);
|
||||
other.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(other,ProgressRecord.INITIAL);other.setHealth(6);
|
||||
other.connection.handleAcceptPlayerLoad(new ServerboundPlayerLoadedPacket());p.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(other)));p.level().addNewPlayer(other);
|
||||
remote.set(other);p.getInventory().clearContent();other.getInventory().clearContent();fresh(p,"wolf");fresh(other,"wolf");
|
||||
p.getInventory().setItem(0,new ItemStack(Items.EMERALD,3));other.getInventory().setItem(0,new ItemStack(Items.DIAMOND,2));
|
||||
});
|
||||
c.waitTicks(20);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var q=remote.get();
|
||||
var invited=FamiliarDuels.invite(p,q);check(invited.equals("duel.sent"),"Invitation between available players: "+invited+" ready="+FamiliarDuels.ready(p)+"/"+FamiliarDuels.ready(q)+" combat="+FamiliarBattle.fighting(p)+"/"+FamiliarBattle.fighting(q)+" distance="+p.distanceTo(q)+" sight="+p.hasLineOfSight(q));var m=FamiliarDuels.match(p);
|
||||
check(Experience054Checks.transfer(p,"deposit_all").equals("xp.locked"),"XP transfers lock from the invitation, before stake consent");
|
||||
request(q,"accept",-1,0);request(p,"stake",0,3);request(q,"stake",0,2);
|
||||
request(p,"ready",-1,0);check(m.ready[0]&&!m.ready[1]&&!m.running(),"One confirmation cannot start combat");
|
||||
int old=m.revision;request(q,"stake",0,1);check(!m.ready[0]&&!m.ready[1],"Changing a stake invalidates both confirmations");
|
||||
p.tickCount+=4;FamiliarDuels.request(p,m.id.toString(),old,"ready",-1,0);check(!m.ready[0],"Stale consent cannot accept a changed offer");
|
||||
request(q,"stake",9,1);check(m.stakes[1].getCount()==1,"Locked inventory slots cannot be wagered");
|
||||
check(p.getInventory().getItem(0).getCount()==3&&q.getInventory().getItem(0).getCount()==2,"Offers do not remove items before mutual agreement");
|
||||
request(q,"stake",0,2);FamiliarDuels.open(p);
|
||||
});
|
||||
c.waitFor(m->m.gui.screen() instanceof FamiliarDuelScreen,100);
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new FamiliarDuelScreen());});
|
||||
c.waitTicks(4);c.takeScreenshot("sanctuary-beta054-duel-stakes-"+locale);
|
||||
}
|
||||
c.runOnClient(m->{
|
||||
var slot=widgets(m.gui.screen()).filter(net.minecraft.client.gui.components.AbstractWidget.class::isInstance).map(net.minecraft.client.gui.components.AbstractWidget.class::cast)
|
||||
.filter(w->w.getWidth()==20&&w.getHeight()==20)
|
||||
.filter(w->w.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.familiar.duel.own_stake")).findFirst().orElseThrow();
|
||||
boolean clicked=m.gui.screen().mouseClicked(new net.minecraft.client.input.MouseButtonEvent(slot.getX()+10,slot.getY()+10,new net.minecraft.client.input.MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_LEFT,0)),false);
|
||||
System.out.println("DUEL054_CLICK "+clicked+" x="+slot.getX()+" y="+slot.getY()+" phase="+FamiliarClient.duel.phase()+" revision="+FamiliarClient.duel.revision());
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.duel!=null&&FamiliarClient.duel.ownStake().isEmpty(),100);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var q=remote.get();check(FamiliarDuels.match(p).stakes[0].isEmpty(),"Native slot click reaches the authoritative offer");request(p,"stake",0,3);request(p,"ready",-1,0);request(q,"ready",-1,0);var m=FamiliarDuels.match(p);
|
||||
check(m!=null&&m.phase==FamiliarDuels.Phase.COUNTDOWN,"Two confirmations establish a durable escrow");
|
||||
check(p.getInventory().getItem(0).isEmpty()&&q.getInventory().getItem(0).isEmpty(),"Both real stakes removed exactly once");
|
||||
check(!FamiliarBattle.enemy(p,FamiliarService.active(q)),"Countdown blocks attacks");m.until=FamiliarBattle.now();
|
||||
});
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(25);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var q=remote.get();var a=FamiliarService.active(p);var b=FamiliarService.active(q);
|
||||
check(FamiliarBattle.enemy(p,b)&&!FamiliarBattle.enemy(p,q),"The agreement targets the rival familiar, never its player");
|
||||
check(!CompanionWorld.enemy(p,q),"The ordinary PvP prohibition remains in effect");
|
||||
check(!FamiliarTechniques.ally(q,a),"Opponent cannot supply healing assistance");
|
||||
check(!b.hurtServer(p.level(),b.damageSources().playerAttack(p),3),"Players cannot directly damage duelling familiars");
|
||||
a.setPos(p.position().add(1,0,2));b.setPos(p.position().add(1,0,3.5));FamiliarBattle.engage(p,b,false);FamiliarBattle.engage(q,a,false);
|
||||
});
|
||||
c.waitTicks(80);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var q=remote.get();var m=FamiliarDuels.match(p);check(m!=null&&m.damage[0]+m.damage[1]>0,"Autonomous duel produces measured physical hits");
|
||||
// A familiar knockout is the actual victory trigger; no /win command or player damage.
|
||||
var a=FamiliarService.active(p);var b=FamiliarService.active(q);var cast=new FamiliarTechniques.Cast(a,FamiliarBattle.profile(p).initiative(),false,b,b.position());
|
||||
FamiliarBattle.state(p).cast=null;FamiliarBattle.state(q).cast=null;
|
||||
for(int i=0;i<12&&FamiliarDuels.match(p)!=null;i++)FamiliarTechniques.hit(cast,b,4);
|
||||
check(FamiliarDuels.match(p)==null&&FamiliarBattle.bond(q).health()==0,"K.O. ends the match");
|
||||
check(count(p,Items.EMERALD)==3&&count(p,Items.DIAMOND)==2&&count(q,Items.DIAMOND)==0,"Winner receives both exact stakes");
|
||||
FamiliarDuels.ledger(s).claim(p);check(count(p,Items.DIAMOND)==2,"Repeated collection cannot duplicate a payout");
|
||||
check(!FamiliarTechniques.hit(cast,b,4),"An old duel cast is invalid after settlement");
|
||||
check(!FamiliarBattle.bond(p).memories().containsKey("signature"),"Duels do not farm familiar mastery");
|
||||
System.out.println("DUEL054_MATCH_PASS: consent, changed stakes, native hits, isolation, knockout and exact payout");
|
||||
});
|
||||
c.waitTicks(25);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var q=remote.get();fresh(p,"skeleton");fresh(q,"cow");
|
||||
p.getInventory().setItem(1,new ItemStack(Items.ARROW,12));check(FamiliarDuels.invite(p,q).equals("duel.sent"),"A rematch can be offered");
|
||||
request(q,"accept",-1,0);request(p,"ready",-1,0);request(q,"ready",-1,0);var m=FamiliarDuels.match(p);check(m.running(),"Duels may start without a wager");m.until=FamiliarBattle.now();FamiliarDuels.tick(s);
|
||||
var a=FamiliarService.active(p);var b=FamiliarService.active(q);a.setPos(p.position().add(1,0,2));b.setPos(p.position().add(1,0,5));b.setNoAi(true);
|
||||
var cast=new FamiliarTechniques.Cast(a,FamiliarBattle.profile(p).signature(),false,b,b.position());cast.supply=new ItemStack(Items.ARROW);FamiliarProjectiles.launch(cast);
|
||||
var shots=p.level().getEntitiesOfClass(net.minecraft.world.entity.projectile.Projectile.class,a.getBoundingBox().inflate(2),FamiliarProjectiles::managed);check(!shots.isEmpty()&&shots.stream().noneMatch(Entity::shouldBeSaved),"Managed native projectiles cannot resurrect after a world reload");
|
||||
for(var shot:shots)FamiliarProjectiles.impact(shot,new net.minecraft.world.phys.EntityHitResult(b));
|
||||
check(b.getHealth()<b.getMaxHealth(),"Native arrow impacts damage the rival familiar");float wounded=b.getHealth();request(p,"cancel",-1,0);
|
||||
check(FamiliarDuels.match(p)==null&&b.getHealth()==wounded,"Surrender ends the duel without resetting wounds");
|
||||
FamiliarProjectiles.launch(cast);for(var shot:p.level().getEntitiesOfClass(net.minecraft.world.entity.projectile.Projectile.class,a.getBoundingBox().inflate(2),FamiliarProjectiles::managed))FamiliarProjectiles.impact(shot,new net.minecraft.world.phys.EntityHitResult(b));
|
||||
check(b.getHealth()==wounded,"Late projectiles cannot damage after surrender");
|
||||
System.out.println("DUEL054_PROJECTILES_PASS: native projectiles, no save resurrection, surrender and late impact isolation");
|
||||
});
|
||||
// Exercise refunds and inventory-full claims using the same persisted transaction contract.
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var q=remote.get();var ledger=FamiliarDuels.ledger(s);
|
||||
p.getInventory().clearContent();q.getInventory().clearContent();fresh(p,"skeleton");fresh(q,"blaze");p.getInventory().setItem(0,new ItemStack(Items.EMERALD,3));q.getInventory().setItem(0,new ItemStack(Items.DIAMOND,2));
|
||||
var id=UUID.randomUUID();check(ledger.reserve(id,p,q,new ItemStack(Items.EMERALD,3),new ItemStack(Items.DIAMOND,2)),"Independent escrow established");
|
||||
for(int slot:CompanionInventory.slots(p,true))p.getInventory().setItem(slot,new ItemStack(Items.STONE,64));ledger.finish(id,0);ledger.claim(p);ledger.claim(q);
|
||||
check(ledger.pending(p)&&count(p,Items.DIAMOND)==0,"Full inventory retains payout without dropping it into the world");
|
||||
for(int slot:CompanionInventory.slots(p,true))p.getInventory().setItem(slot,ItemStack.EMPTY);ledger.claim(p);check(count(p,Items.DIAMOND)==2&&count(p,Items.EMERALD)==3&&!ledger.pending(p),"Freed space collects the complete pending payout");
|
||||
q.getInventory().setItem(0,new ItemStack(Items.DIAMOND,2));var interrupted=UUID.randomUUID();check(ledger.reserve(interrupted,p,q,new ItemStack(Items.EMERALD,3),new ItemStack(Items.DIAMOND,2)),"Interrupted escrow fixture");
|
||||
try{
|
||||
var reloaded=new FamiliarStakes(s);reloaded.claim(p);reloaded.claim(q);
|
||||
check(count(p,Items.EMERALD)==3&&count(q,Items.DIAMOND)==2,"Journal reload refunds both stakes after interruption");
|
||||
reloaded.claim(p);reloaded.claim(q);check(count(q,Items.DIAMOND)==2,"Recovery receipts prevent repeated refunds");
|
||||
var named=new ItemStack(Items.CHEST);named.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,net.minecraft.network.chat.Component.literal("Archive du duel"));
|
||||
named.set(net.minecraft.core.component.DataComponents.CONTAINER,net.minecraft.world.item.component.ItemContainerContents.fromItems(List.of(new ItemStack(Items.DIAMOND,7))));
|
||||
p.getInventory().setItem(3,named.copy());var custom=UUID.randomUUID();check(reloaded.reserve(custom,p,q,named,ItemStack.EMPTY),"Named container can be wagered");reloaded.finish(custom,0);reloaded.claim(p);reloaded.claim(q);
|
||||
check(CompanionInventory.slots(p,true).stream().anyMatch(slot->ItemStack.matches(named,p.getInventory().getItem(slot))),"All native item components and nested contents survive escrow");
|
||||
}catch(Exception e){throw new AssertionError(e);}
|
||||
System.out.println("DUEL054_ESCROW_PASS: durable receipts, full inventory and interrupted match recovery");
|
||||
// Reload uses a fresh journal owner: discard this fixture before the normal shutdown hook writes.
|
||||
FamiliarDuels.start(s);
|
||||
});
|
||||
c.runOnClient(m->m.gui.setScreen(new FamiliarOrdersScreen()));c.waitTicks(3);c.takeScreenshot("sanctuary-beta054-orders");c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var q=remote.getAndSet(null);FamiliarDuels.disconnect(q);FamiliarBattle.release(q);var pet=FamiliarService.active(q);if(pet!=null)pet.discard();q.discard();});
|
||||
}
|
||||
private static void request(ServerPlayer p,String action,int slot,int count){var m=FamiliarDuels.match(p);p.tickCount+=4;FamiliarDuels.request(p,m.id.toString(),m.revision,action,slot,count);}
|
||||
private static void fresh(ServerPlayer p,String species){FamiliarBattle.cancel(p);p.setAttached(FamiliarBattle.COMBAT_LOCK,0L);CompanionService.save(p,CompanionService.record(p).cooldown(0));Combat054ClientChecks.equip(p,species);p.tickCount=((p.tickCount/10)+1)*10;FamiliarService.tick(p);}
|
||||
private static int count(ServerPlayer p,Item item){return CompanionInventory.slots(p,true).stream().mapToInt(i->p.getInventory().getItem(i).is(item)?p.getInventory().getItem(i).getCount():0).sum();}
|
||||
private static java.util.stream.Stream<net.minecraft.client.gui.components.events.GuiEventListener> widgets(net.minecraft.client.gui.components.events.GuiEventListener root){
|
||||
return java.util.stream.Stream.concat(java.util.stream.Stream.of(root),root instanceof net.minecraft.client.gui.components.events.ContainerEventHandler container?container.children().stream().flatMap(Duel054Checks::widgets):java.util.stream.Stream.empty());
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.network.protocol.game.*;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.server.network.*;
|
||||
import net.minecraft.util.LightCoordsUtil;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.*;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
import net.minecraft.world.level.block.*;
|
||||
|
||||
/** Fresh enclosed room: native network synchronization, meshing and rendered evidence. */
|
||||
public final class Dynamic030ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(screen).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());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary beta030 lighting "+UUID.randomUUID());s.setSeed("42");
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
BlockPos origin=server.computeOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().above(80);
|
||||
for(int x=-12;x<=12;x++)for(int z=-12;z<=12;z++)for(int y=-1;y<=7;y++)p.level().setBlockAndUpdate(at.offset(x,y,z),
|
||||
(y==-1||y==7||Math.abs(x)==12||Math.abs(z)==12?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
for(int x=-4;x<=-2;x++)for(int z=3;z<=5;z++)p.level().setBlockAndUpdate(at.offset(x,0,z),Blocks.WATER.defaultBlockState());
|
||||
p.teleportTo(at.getX()+.5,at.getY(),at.getZ()-5.5);p.setYRot(0);p.setXRot(12);
|
||||
p.getInventory().clearContent();p.setItemInHand(InteractionHand.OFF_HAND,ItemStack.EMPTY);head(p,ItemStack.EMPTY);p.inventoryMenu.broadcastChanges();return at;
|
||||
});
|
||||
c.waitFor(m->m.player.getY()>=origin.getY()-.1,200);
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,800);m.options.guiScale().set(2);m.resizeGui();m.player.setYRot(0);m.player.setXRot(12);});
|
||||
c.waitTicks(100);
|
||||
c.runOnClient(m->{require(DynamicLightClient.snapshot(m.level).size()==0,"Empty room has no dynamic sources");
|
||||
require(m.level.getBrightness(LightLayer.BLOCK,origin.above())==0,"No real block light in test room");});
|
||||
c.takeScreenshot("sanctuary-beta030-room-dark");
|
||||
Dynamic030RenderProbe.active=true;
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.TORCH));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==1,200);c.waitTicks(50);
|
||||
c.runOnClient(m->{var pos=m.player.blockPosition().above();require((LightCoordsUtil.getLightCoords(m.level,pos)&255)>150,"Held torch reaches native light lookup");
|
||||
require(m.level.getBrightness(LightLayer.BLOCK,pos)==0,"World light engine remains untouched");
|
||||
require((m.getEntityRenderDispatcher().getRenderer(m.player).getPackedLightCoords(m.player,1)&255)>150,"Player and held-item extraction receive dynamic light");
|
||||
require(Dynamic030RenderProbe.litTerrainSamples.get()>0,"Actual terrain compilation consumes dynamic light");
|
||||
System.out.println("LIGHT-01 native meshing: "+Dynamic030RenderProbe.litTerrainSamples.get()+" lit samples");});
|
||||
c.takeScreenshot("sanctuary-beta030-hand-torch");
|
||||
c.waitFor(m->DynamicLightClient.pendingSections()==0,200);
|
||||
long samples=Dynamic030RenderProbe.litTerrainSamples.get();
|
||||
for(int i=0;i<20;i++){c.runOnClient(m->m.player.setYRot(m.player.getYRot()+18));c.waitTick();}
|
||||
c.runOnClient(m->require(DynamicLightClient.pendingSections()==0,"Looking around does not invalidate the source"));
|
||||
c.waitTicks(30);System.out.println("LIGHT-01 settled terrain samples before/after camera turn: "+samples+" / "+Dynamic030RenderProbe.litTerrainSamples.get());
|
||||
double oldX=c.computeOnClient(m->m.player.getX()),oldY=c.computeOnClient(m->m.player.getEyeY()),oldZ=c.computeOnClient(m->m.player.getZ());
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(origin.getX()+9.5,origin.getY(),origin.getZ()+9.5));
|
||||
c.waitFor(m->m.player.getZ()>origin.getZ()+9,100);c.waitTicks(40);
|
||||
c.runOnClient(m->{var light=DynamicLightClient.snapshot(m.level);
|
||||
require(light.apply(0,oldX,oldY,oldZ)==0,"Walking away removes the old pool of light");
|
||||
require((light.apply(0,m.player.getX(),m.player.getEyeY(),m.player.getZ())&255)>200,"Light follows the moving player");});
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(oldX,origin.getY(),oldZ));
|
||||
c.waitFor(m->m.player.getZ()<origin.getZ()-5,100);c.waitTicks(30);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);p.setItemInHand(InteractionHand.OFF_HAND,new ItemStack(Items.SOUL_LANTERN));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.getOffhandItem().is(Items.SOUL_LANTERN),100);c.waitTicks(40);
|
||||
c.runOnClient(m->require((DynamicLightClient.snapshot(m.level).apply(0,m.player.getX(),m.player.getEyeY(),m.player.getZ())&255)>=150,"Offhand light follows native soul intensity"));
|
||||
c.takeScreenshot("sanctuary-beta030-offhand");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemInHand(InteractionHand.OFF_HAND,ItemStack.EMPTY);head(p,new ItemStack(Items.GLOWSTONE));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->HeadCosmeticService.appearance(m.player).is(Items.GLOWSTONE),100);c.waitTicks(40);
|
||||
c.runOnClient(m->require((DynamicLightClient.snapshot(m.level).apply(0,m.player.getX(),m.player.getEyeY(),m.player.getZ())&255)>220,"Head block supplies its own light"));
|
||||
c.takeScreenshot("sanctuary-beta030-head-glowstone");
|
||||
server.runOnServer(s->head(s.getPlayerList().getPlayers().getFirst(),cake(true)));c.waitFor(m->HeadCosmeticData.lit(HeadCosmeticService.appearance(m.player)),100);c.waitTicks(40);
|
||||
c.runOnClient(m->require((DynamicLightClient.snapshot(m.level).apply(0,m.player.getX(),m.player.getEyeY(),m.player.getZ())&255)>25,"Lit cake produces its smaller pool of light"));
|
||||
c.takeScreenshot("sanctuary-beta030-head-candle-cake");
|
||||
server.runOnServer(s->head(s.getPlayerList().getPlayers().getFirst(),cake(false)));c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==0,100);c.waitTicks(40);
|
||||
c.runOnClient(m->require((LightCoordsUtil.getLightCoords(m.level,m.player.blockPosition().above())&255)==0,"Extinguishing removes dynamic light"));
|
||||
c.takeScreenshot("sanctuary-beta030-cake-extinguished");
|
||||
UUID remote=server.computeOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();head(p,ItemStack.EMPTY);
|
||||
var profile=new GameProfile(UUID.randomUUID(),"Luciole");var other=new ServerPlayer(s,p.level(),profile,ClientInformation.createDefault());
|
||||
var connection=new Connection(PacketFlow.SERVERBOUND);new io.netty.channel.embedded.EmbeddedChannel(connection);
|
||||
other.connection=new ServerGamePacketListenerImpl(s,connection,other,CommonListenerCookie.createInitial(profile,false));
|
||||
other.setPos(origin.getX()+3.5,origin.getY(),origin.getZ()+3.5);other.setYRot(180);other.setYHeadRot(180);other.setNoGravity(true);
|
||||
other.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.LANTERN));other.connection.handleAcceptPlayerLoad(new ServerboundPlayerLoadedPacket());
|
||||
p.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(other)));p.level().addNewPlayer(other);return other.getUUID();
|
||||
});
|
||||
c.waitFor(m->m.level.getPlayerByUUID(remote)!=null&&DynamicLightClient.snapshot(m.level).size()==1,200);c.waitTicks(40);
|
||||
c.runOnClient(m->{var other=m.level.getPlayerByUUID(remote);require((LightCoordsUtil.getLightCoords(m.level,other.blockPosition().above())&255)>180,"Other player's lantern lights terrain");});
|
||||
c.takeScreenshot("sanctuary-beta030-other-player-lantern");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var other=(ServerPlayer)p.level().getPlayerByUUID(remote);other.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);
|
||||
// The synthetic remote has no live connection ticking its inventory; deliver its normal equipment update.
|
||||
p.connection.send(new ClientboundSetEquipmentPacket(other.getId(),List.of(com.mojang.datafixers.util.Pair.of(net.minecraft.world.entity.EquipmentSlot.MAINHAND,ItemStack.EMPTY))));
|
||||
for(int i=0;i<200;i++){var item=new ItemEntity(p.level(),origin.getX()+.5,origin.getY()+.2,origin.getZ()+2.5,new ItemStack(Items.TORCH));
|
||||
item.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);item.setNoGravity(true);item.setDefaultPickUpDelay();p.level().addFreshEntity(item);}});
|
||||
c.waitFor(m->m.level.getPlayerByUUID(remote).getMainHandItem().isEmpty(),200);
|
||||
c.waitTicks(30);
|
||||
c.runOnClient(m->System.out.println("LIGHT-01 dropped field: "+DynamicLightClient.snapshot(m.level).size()+" sources; "+m.level.getEntitiesOfClass(ItemEntity.class,m.player.getBoundingBox().inflate(30)).size()+" item entities"));
|
||||
c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==1,200);c.waitTicks(20);
|
||||
c.runOnClient(m->{require(DynamicLightClient.snapshot(m.level).size()==1,"200 dropped torches form one source even while stacks merge");
|
||||
var dropped=m.level.getEntitiesOfClass(ItemEntity.class,m.player.getBoundingBox().inflate(30)).getFirst();
|
||||
// Flowing water carries the pile: sample where the items are now, not where they were thrown.
|
||||
int light=LightCoordsUtil.getLightCoords(m.level,dropped.blockPosition())&255;
|
||||
System.out.println("LIGHT-01 floating drop at "+dropped.position()+" has light "+light);
|
||||
require(light>180,"Dropped cluster follows the items moving in water");});
|
||||
c.takeScreenshot("sanctuary-beta030-dropped-cluster");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(var e:p.level().getEntitiesOfClass(ItemEntity.class,p.getBoundingBox().inflate(30)))e.discard();});
|
||||
c.waitFor(m->DynamicLightClient.snapshot(m.level).size()==0&&DynamicLightClient.pendingSections()==0,200);c.waitTicks(40);
|
||||
c.runOnClient(m->require((LightCoordsUtil.getLightCoords(m.level,origin.offset(0,0,2))&255)==0,"Despawn clears every old source"));
|
||||
c.takeScreenshot("sanctuary-beta030-cluster-removed");
|
||||
}finally{onboarding.set(false);Dynamic030RenderProbe.active=false;}
|
||||
c.runOnClient(m->require(DynamicLightClient.pendingSections()==0,"Disconnect clears queued light updates"));
|
||||
System.out.println("LIGHT-01 client passed: hands, head, cake, remote player, 200 drops, meshing, cleanup and unchanged world light.");
|
||||
}
|
||||
private static ItemStack cake(boolean lit) {var item=new ItemStack(Items.CAKE);var tag=new CompoundTag();tag.putString(HeadCosmeticData.CANDLE,"minecraft:candle");item.set(DataComponents.CUSTOM_DATA,CustomData.of(tag));item.set(DataComponents.BLOCK_STATE,BlockItemStateProperties.EMPTY.with(CandleBlock.LIT,lit));return item;}
|
||||
private static void head(ServerPlayer p,ItemStack stack) {AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,stack);AccessoryService.publish(p);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static Stream<GuiEventListener> widgets(GuiEventListener element){return Stream.concat(Stream.of(element),element instanceof ContainerEventHandler parent?parent.children().stream().flatMap(Dynamic030ClientChecks::widgets):Stream.empty());}
|
||||
private static void require(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
|
||||
/** Exact balances, real damage, native button requests and interrupted save recovery. */
|
||||
public final class Experience054Checks {
|
||||
private static void check(boolean ok,String message){Combat054ClientChecks.require(ok,message);}
|
||||
public static void run(ClientGameTestContext c,TestServerContext server){
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();peace(p);Combat054ClientChecks.equip(p,"wolf");p.tickCount=((p.tickCount/10)+1)*10;FamiliarService.tick(p);
|
||||
check(FamiliarService.active(p)!=null,"Physical familiar for XP combat test");
|
||||
check(FamiliarLevels.xpAt(16)==352&&FamiliarLevels.xpAt(17)==394&&FamiliarLevels.xpAt(31)==1507&&FamiliarLevels.xpAt(32)==1628&&FamiliarLevels.xpAt(40)==2920,"XP boundaries match Minecraft point costs");
|
||||
check(FamiliarLevels.previous(FamiliarLevels.xpAt(40)+7)==FamiliarLevels.xpAt(40)+7-FamiliarLevels.xpAt(39),"A level withdrawal removes the partial XP and reaches the lower whole level");
|
||||
for(int level:new int[]{0,1,15,16,17,30,31,32,40,120,100000,1000000}){
|
||||
long xp=FamiliarLevels.xpAt(level);check(FamiliarLevels.level(xp)==level,"Native XP boundary "+level);
|
||||
if(level>0)check(FamiliarLevels.level(xp-1)==level-1,"Native XP lower boundary "+level);
|
||||
}
|
||||
p.setExperienceLevels(Integer.MAX_VALUE);p.experienceProgress=.8F;
|
||||
check(FamiliarExperience.playerXp(p)>Integer.MAX_VALUE&&transfer(p,"deposit_all").equals("xp.native_limit"),"Extreme operator levels are refused without integer overflow");
|
||||
long source=FamiliarLevels.xpAt(120)+7;FamiliarExperience.setPlayerXp(p,source);p.totalExperience=1;
|
||||
check(FamiliarExperience.playerXp(p)==source,"Spending levels cannot confuse XP balance with stale totalExperience");
|
||||
var b=FamiliarBattle.bond(p);float health=FamiliarLevels.current(b);
|
||||
check(transfer(p,"deposit_all").equals("xp.transferred"),"Exact XP deposit");
|
||||
b=FamiliarBattle.bond(p);check(b.storedXp()==source&&FamiliarExperience.playerXp(p)==0,"All points debited and banked once");
|
||||
check(FamiliarLevels.level(b.storedXp())==120&&FamiliarLevels.health(b)>1.5F&&FamiliarLevels.damage(b.storedXp())>1.25F,"Power continues beyond the rejected caps");
|
||||
check(Math.abs(FamiliarLevels.current(b)-health)<.001,"Deposit cannot heal");
|
||||
check(FamiliarLevels.rarity("common",b.storedXp()).name().equals("LEGENDARY"),"Rarity rises to legendary");
|
||||
check(FamiliarBattle.world(s).experience.transfer(p,b.id(),0,"deposit").equals("xp.changed"),"Stale request rejected");
|
||||
check(transfer(p,"withdraw_all").equals("xp.transferred")&&FamiliarExperience.playerXp(p)==source,"Full withdrawal restores exactly the original points");
|
||||
check(FamiliarBattle.bond(p).storedXp()==0&&Math.abs(FamiliarLevels.current(FamiliarBattle.bond(p))-health)<.001,"Withdrawal removes bonuses without healing");
|
||||
check(transfer(p,"deposit").equals("xp.transferred")&&FamiliarBattle.bond(p).storedXp()==7,"One level uses the familiar XP curve");
|
||||
check(transfer(p,"withdraw").equals("xp.transferred")&&FamiliarExperience.playerXp(p)==source,"Single level round trip conserves XP");
|
||||
check(transfer(p,"deposit_all").equals("xp.transferred"),"Charge for physical damage check");
|
||||
var pet=FamiliarService.active(p);var target=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(pet.position().add(0,0,1.5));p.level().addFreshEntity(target);
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).initiative(),true,target,target.position());
|
||||
check(FamiliarTechniques.hit(cast,target,4)&&target.getHealth()<13,"Stored levels increase actual native outgoing damage");
|
||||
float before=FamiliarLevels.current(FamiliarBattle.bond(p));pet.hurtServer(p.level(),pet.damageSources().mobAttack(target),3);
|
||||
check(Math.abs(FamiliarLevels.current(FamiliarBattle.bond(p))-(before-3))<.001,"Incoming damage is applied against the expanded HP reserve");
|
||||
check(transfer(p,"withdraw_all").equals("xp.locked"),"Cannot drain combat strength during a fight");target.discard();peace(p);
|
||||
check(transfer(p,"withdraw_all").equals("xp.transferred"),"XP is recoverable after combat");
|
||||
recovery(p);trade(p);
|
||||
FamiliarExperience.setPlayerXp(p,FamiliarLevels.xpAt(40)+7);FamiliarBattle.send(p,"");
|
||||
});
|
||||
c.waitTicks(3);
|
||||
c.runOnClient(m->m.gui.setScreen(new FamiliarExperienceScreen(null)));
|
||||
c.runOnClient(m->{var button=m.gui.screen().children().stream().filter(net.minecraft.client.gui.components.Button.class::isInstance).map(net.minecraft.client.gui.components.Button.class::cast)
|
||||
.filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.familiar.xp.deposit_all")).findFirst().orElseThrow();
|
||||
check(button.active,"Deposit control is enabled");button.onPress(new net.minecraft.client.input.KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0));});
|
||||
c.waitFor(m->FamiliarClient.bond!=null&&FamiliarClient.bond.storedXp()==FamiliarLevels.xpAt(40)+7,100);
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new FamiliarExperienceScreen(null));});
|
||||
c.waitTicks(3);c.takeScreenshot("sanctuary-beta054-experience-"+locale);
|
||||
}
|
||||
c.runOnClient(m->{
|
||||
var egg=AccessoryAccess.of(m.player).getItem(AccessoryContainer.FAMILIAR);
|
||||
check(CompanionTooltips.rarity(egg).name().equals("LEGENDARY"),"Item rarity uses server individual balance");
|
||||
check(CompanionTooltips.color(egg)==net.minecraft.ChatFormatting.LIGHT_PURPLE,"Egg name uses the legendary color");m.gui.setScreen(null);
|
||||
});
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var b=FamiliarBattle.bond(p);
|
||||
// A copied object points to one canonical balance, not another reserve of XP.
|
||||
var copy=FamiliarBattle.egg(p).copy();p.getInventory().setItem(2,copy);
|
||||
check(FamiliarIdentity.get(copy).equals(b.id()),"An exchanged/copied egg retains its individual identity");
|
||||
FamiliarBattle.flush(s);s.saveEverything(true,true,true);
|
||||
});
|
||||
System.out.println("XP054_BALANCE_PASS: native XP conservation, uncapped health and damage, no transfer healing, combat lock, receipt recovery and FR/EN controls");
|
||||
}
|
||||
public static void reconnect(ServerPlayer p){
|
||||
var b=FamiliarBattle.bond(p);check(b!=null&&b.storedXp()==FamiliarLevels.xpAt(40)+7&&FamiliarExperience.playerXp(p)==0,"Reconnect preserves stored points and debited player balance");
|
||||
check(FamiliarIdentity.get(p.getInventory().getItem(2)).equals(b.id()),"Copied egg still references the same individual after reconnect");
|
||||
System.out.println("XP054_RECONNECT_PASS: real world reload preserves individual XP, player debit and shared egg identity");
|
||||
}
|
||||
static String transfer(ServerPlayer p,String action){var b=FamiliarBattle.bond(p);return FamiliarBattle.world(p.level().getServer()).experience.transfer(p,b.id(),b.storedXp(),action);}
|
||||
private static void peace(ServerPlayer p){
|
||||
FamiliarBattle.cancel(p);p.setAttached(FamiliarBattle.COMBAT_LOCK,0L);var b=FamiliarBattle.bond(p);
|
||||
if(b!=null){var json=FamiliarCatalog.JSON.toJsonTree(b).getAsJsonObject();json.addProperty("combatUntil",0);FamiliarBattle.update(p,FamiliarBondStore.parse(json));}
|
||||
}
|
||||
private static void trade(ServerPlayer p){
|
||||
var server=p.level().getServer();check(transfer(p,"deposit_all").equals("xp.transferred"),"Charge an egg before trading it");
|
||||
var egg=FamiliarBattle.egg(p).copy();long xp=FamiliarBattle.bond(p).storedXp();
|
||||
FamiliarBattle.release(p);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,net.minecraft.world.item.ItemStack.EMPTY);FamiliarService.tick(p);
|
||||
var identity=new com.mojang.authlib.GameProfile(UUID.randomUUID(),"XpTrader054");
|
||||
var q=new ServerPlayer(server,p.level(),identity,net.minecraft.server.level.ClientInformation.createDefault());
|
||||
var connection=new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);new io.netty.channel.embedded.EmbeddedChannel(connection);
|
||||
q.connection=new net.minecraft.server.network.ServerGamePacketListenerImpl(server,connection,q,net.minecraft.server.network.CommonListenerCookie.createInitial(identity,false));
|
||||
q.setPos(p.position().add(2,0,0));q.setAttached(fr.koka.sanctuary.progression.ProgressionService.RECORD,fr.koka.sanctuary.progression.ProgressRecord.INITIAL.json());
|
||||
fr.koka.sanctuary.progression.ProgressEffects.apply(q,fr.koka.sanctuary.progression.ProgressRecord.INITIAL);
|
||||
q.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());p.level().addNewPlayer(q);
|
||||
AccessoryAccess.of(q).setItem(AccessoryContainer.FAMILIAR,egg.copy());fr.koka.sanctuary.companion.CompanionService.tick(q);
|
||||
check(FamiliarBattle.prepare(q)&&FamiliarBattle.bond(q).storedXp()==xp,"Egg trade preserves its exact stored XP");
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg.copy());
|
||||
check(!FamiliarBattle.prepare(p)&&transfer(p,"withdraw_all").equals("unavailable"),"A retained copy cannot steal the active new owner's balance");
|
||||
check(transfer(q,"withdraw_all").equals("xp.transferred")&&FamiliarExperience.playerXp(q)==xp,"New owner can recover all stored XP");
|
||||
check(FamiliarExperience.playerXp(p)==0&&FamiliarBattle.bond(p).storedXp()==0,"Copies do not hold another XP balance");
|
||||
FamiliarBattle.release(q);AccessoryAccess.of(q).setItem(AccessoryContainer.FAMILIAR,net.minecraft.world.item.ItemStack.EMPTY);FamiliarService.tick(q);q.discard();
|
||||
check(FamiliarBattle.prepare(p),"Empty traded egg can return to its original owner");p.tickCount=((p.tickCount/10)+1)*10;FamiliarService.tick(p);
|
||||
System.out.println("XP054_TRADE_PASS: two players transfer a charged egg, recover its XP and reject a duplicated claim");
|
||||
}
|
||||
private static void recovery(ServerPlayer p){
|
||||
var s=p.level().getServer();var store=FamiliarBattle.world(s).store;
|
||||
try{
|
||||
var original=FamiliarBattle.bond(p);var legacy=FamiliarCatalog.JSON.toJsonTree(original).getAsJsonObject();legacy.addProperty("schema",1);legacy.remove("storedXp");
|
||||
check(FamiliarBondStore.parse(legacy).storedXp()==0,"Schema 1 migration starts with zero XP");
|
||||
var migrationFile=s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("sanctuary/xp-migration-test.json");
|
||||
var unknown=legacy.deepCopy();var unknownId=UUID.randomUUID();unknown.addProperty("id",unknownId.toString());unknown.addProperty("schema",99);
|
||||
var individuals=new com.google.gson.JsonObject();individuals.add(original.id().toString(),legacy);individuals.add(unknownId.toString(),unknown);
|
||||
var oldRoot=new com.google.gson.JsonObject();oldRoot.addProperty("schema",1);oldRoot.add("individuals",individuals);java.nio.file.Files.writeString(migrationFile,FamiliarCatalog.JSON.toJson(oldRoot));
|
||||
var migrating=new FamiliarBondStore(migrationFile);check(migrating.get(unknownId)==null,"Unknown individual schema is disabled");migrating.put(migrating.get(original.id()));migrating.flush();
|
||||
var migrated=com.google.gson.JsonParser.parseString(java.nio.file.Files.readString(migrationFile)).getAsJsonObject().getAsJsonObject("individuals");
|
||||
check(migrated.get(unknownId.toString()).equals(unknown)&&migrated.getAsJsonObject(original.id().toString()).get("schema").getAsInt()==2,"Migration preserves unknown data while upgrading known individuals");
|
||||
for(int phase:new int[]{0,1,2}){boolean paid=phase>0;
|
||||
var before=FamiliarBattle.bond(p);var after=before.charge(before.storedXp()+7,FamiliarBattle.profile(p).health());long balance=FamiliarExperience.playerXp(p);
|
||||
var transfer=new com.google.gson.JsonObject();String receipt=UUID.randomUUID().toString();transfer.addProperty("receipt",receipt);
|
||||
transfer.add("before",FamiliarCatalog.JSON.toJsonTree(before));transfer.add("after",FamiliarCatalog.JSON.toJsonTree(after));transfer.addProperty("playerBefore",balance);transfer.addProperty("playerAfter",balance-7);
|
||||
var entries=new com.google.gson.JsonObject();entries.add(p.getStringUUID(),transfer);var root=new com.google.gson.JsonObject();root.addProperty("schema",1);root.add("pending",entries);
|
||||
store.flush();java.nio.file.Files.writeString(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("sanctuary/familiar-xp-v1.json"),FamiliarCatalog.JSON.toJson(root));
|
||||
if(paid){FamiliarExperience.setPlayerXp(p,balance-7);p.setAttached(FamiliarExperience.RECEIPT,receipt);}
|
||||
if(phase==2){store.put(after);store.flush();}
|
||||
var reloaded=new FamiliarExperience(s,store);check(reloaded.blocked(before.id()),"Pending transfer freezes the individual");reloaded.recover(p);
|
||||
check(store.get(before.id()).storedXp()==before.storedXp()+(paid?7:0),"Recovery commits only a paid native receipt");
|
||||
reloaded.recover(p);check(FamiliarExperience.playerXp(p)==balance-(paid?7:0),"Repeated recovery never credits player XP twice");
|
||||
}
|
||||
// Original ledger resumes once the recovered journal is the same empty canonical document.
|
||||
check(transfer(p,"withdraw_all").equals("xp.transferred"),"Recovered bank can be withdrawn exactly once");
|
||||
}catch(Exception error){throw new AssertionError(error);}
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.*;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Familiar020ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
int scale=c.computeOnClient(m->m.options.guiScale().get());
|
||||
var forward=c.computeOnClient(m->m.options.keyUp.getDefaultKey());
|
||||
var posture=c.computeOnClient(m->KeyMapping.get("sanctuary.progression.lying_key").getDefaultKey());
|
||||
var camera=c.computeOnClient(m->m.options.getCameraType());
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary beta020 interactions "+UUID.randomUUID());s.setSeed("42");
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&CompanionClient.record!=null&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().above(6);
|
||||
for(int x=-10;x<=10;x++)for(int z=-10;z<=10;z++)for(int y=-1;y<9;y++)p.level().setBlockAndUpdate(at.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
p.teleportTo(at.getX()+.5,at.getY(),at.getZ()+.5);p.getInventory().clearContent();p.giveExperienceLevels(10-p.experienceLevel);
|
||||
var egg=new ItemStack(Items.CAT_SPAWN_EGG);egg.set(DataComponents.CUSTOM_NAME,Component.literal("Nuage"));AccessoryAccess.of(p).setItem(0,egg);
|
||||
p.getInventory().setItem(0,new ItemStack(Items.CAT_SPAWN_EGG));CompanionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m->CompanionClient.species==CompanionType.CAT&&clientPet(m)!=null&&FamiliarSize.value(clientPet(m).egg())>0,200);
|
||||
c.runOnClient(m->{
|
||||
require(!ProgressionClient.operator&&!CompanionClient.studied&&CompanionClient.record.acceptAllies(),"Ordinary inhabitant starts with assistance on and study locked");
|
||||
var lines=tooltip(m,new ItemStack(Items.CAT_SPAWN_EGG));
|
||||
require(lines.stream().anyMatch(t->t.getString().contains("Velvet paws"))&&lines.stream().noneMatch(t->t.getString().contains("20%")),"Unlearned tooltip only gives cryptic names");
|
||||
int distinct=(int)List.of(Items.CAT_SPAWN_EGG,Items.BEE_SPAWN_EGG,Items.BLAZE_SPAWN_EGG,Items.GHAST_SPAWN_EGG,Items.WITHER_SPAWN_EGG).stream()
|
||||
.map(i->new ItemStack(i).getStyledHoverName().getStyle().getColor().getValue()).distinct().count();require(distinct==5,"Five rarity colors reach native hover names");
|
||||
var pet=clientPet(m);var state=(FamiliarRenderer.State)m.getEntityRenderDispatcher().extractEntity(pet,.5F);
|
||||
require(((net.minecraft.client.renderer.entity.state.LivingEntityRenderState)state.appearance).isBaby,"Native baby cat model selected");
|
||||
m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new InventoryScreen(m.player));
|
||||
});c.waitTick();hover(c,36);c.takeScreenshot("sanctuary-beta020-egg-cryptic");
|
||||
c.runOnClient(m->m.gui.setScreen(new ProgressionScreen(null)));c.waitTick();
|
||||
c.runOnClient(m->{require(widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).noneMatch(w->key(w,"sanctuary.companion.open")||key(w,"sanctuary.companion.activate")),"Aptitudes have no familiar menu or use-power button");button(m.gui.screen(),"sanctuary.companion.study_buy").onPress(enter());});
|
||||
c.waitFor(m->CompanionClient.studied,200);require(server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().experienceLevel)==6,"Actual study purchase costs four levels");
|
||||
c.runOnClient(m->{var lines=tooltip(m,new ItemStack(Items.CAT_SPAWN_EGG));require(lines.stream().anyMatch(t->t.getString().contains("20%")),"Purchased study reveals actual mechanics");m.gui.setScreen(new InventoryScreen(m.player));});
|
||||
c.waitTick();hover(c,36);c.takeScreenshot("sanctuary-beta020-egg-deciphered");
|
||||
c.runOnClient(m->CompanionClient.openInhabitant(null));c.waitTick();
|
||||
for(int gui:new int[]{2,3,4}){
|
||||
c.runOnClient(m->{m.options.guiScale().set(gui);m.resizeGui();});c.waitTick();
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.companion.consent_on");for(var widget:widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).toList())
|
||||
require(widget.getX()>=0&&widget.getRight()<=m.gui.screen().width,"Inhabitant widgets fit at every GUI scale");});
|
||||
c.takeScreenshot("sanctuary-beta020-inhabitant-"+gui);
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();button(m.gui.screen(),"sanctuary.companion.consent_on").onPress(enter());});c.waitFor(m->!CompanionClient.record.acceptAllies(),200);
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(3);
|
||||
var at=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);pet.setNoAi(true);pet.setDeltaMovement(Vec3.ZERO);pet.setPos(p.position().add(0,0,2));p.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND,ItemStack.EMPTY);return pet.position().add(0,.35,0);});
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(3);face(c,at);c.waitTicks(2);
|
||||
clickWorld(c,InputConstants.MOUSE_BUTTON_RIGHT);
|
||||
c.waitFor(m->clientPet(m)!=null&&CarryService.carried(clientPet(m)),200);c.getInput().releaseKey(o->o.keyShift);c.waitTicks(3);
|
||||
require(server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();return CarryService.carrying(p)&&p.getFirstPassenger()==FamiliarService.active(p);}),"Actual sneak-click carries equipped familiar");
|
||||
c.runOnClient(m->{var state=(FamiliarRenderer.State)m.getEntityRenderDispatcher().extractEntity(clientPet(m),.5F);
|
||||
require(((net.minecraft.client.renderer.entity.state.CatRenderState)state.appearance).isSitting,"Carried cat sits on head");m.options.setCameraType(CameraType.THIRD_PERSON_BACK);m.player.setXRot(10);});
|
||||
c.waitTicks(3);c.takeScreenshot("sanctuary-beta020-carried-baby");
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(2);clickWorld(c,InputConstants.MOUSE_BUTTON_RIGHT);c.waitFor(m->!CarryService.carrying(m.player),200);c.getInput().releaseKey(o->o.keyShift);
|
||||
c.runOnClient(m->{m.options.setCameraType(CameraType.FIRST_PERSON);m.options.keyUp.setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_UP));KeyMapping.get("sanctuary.progression.lying_key").setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_F9));KeyMapping.resetMapping();m.player.setYRot(90);m.player.setXRot(0);});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressEffects.get(p).buy("hunger",1);p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);p.getFoodData().setFoodLevel(8);ProgressionService.send(p,"");
|
||||
// A small ledge leaves enough flight time to inspect the actual rendered dive.
|
||||
var launch=p.blockPosition();for(int x=-2;x<=2;x++)for(int z=-2;z<=2;z++)for(int y=0;y<3;y++)p.level().setBlockAndUpdate(launch.offset(x,y,z),Blocks.STONE.defaultBlockState());
|
||||
p.teleportTo(launch.getX()-1.5,launch.getY()+3,launch.getZ()+.5);
|
||||
});
|
||||
c.waitFor(m->ProgressionClient.progress.hunger()>0&&m.player.onGround(),200);
|
||||
c.runOnClient(m->m.options.setCameraType(CameraType.THIRD_PERSON_BACK));
|
||||
c.getInput().holdKey(o->o.keyUp);c.getInput().holdKey(o->o.keySprint);c.waitTicks(4);
|
||||
c.getInput().holdKey(o->o.keyJump);c.waitTicks(2);c.getInput().releaseKey(o->o.keyJump);
|
||||
c.getInput().pressKey(InputConstants.KEY_F9);
|
||||
c.waitFor(m->LyingService.posture(m.player)==3,100);
|
||||
c.runOnClient(m->{var state=(net.minecraft.client.renderer.entity.state.AvatarRenderState)m.getEntityRenderDispatcher().extractEntity(m.player,.5F);
|
||||
require(state.swimAmount==1&&state.isVisuallySwimming,"Dive immediately renders the body horizontally");});
|
||||
c.takeScreenshot("sanctuary-beta020-dive");c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keySprint);
|
||||
c.waitFor(m->m.player.onGround()&&LyingService.posture(m.player)==0,200);
|
||||
c.runOnClient(m->m.options.setCameraType(CameraType.FIRST_PERSON));
|
||||
for(int size:new int[]{120,6000}){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var egg=AccessoryAccess.of(p).getItem(0).copy();
|
||||
net.minecraft.world.item.component.CustomData.update(DataComponents.CUSTOM_DATA,egg,tag->{var trait=new net.minecraft.nbt.CompoundTag();trait.putInt("schema",1);trait.putInt("per_mille",size);tag.put(FamiliarSize.KEY,trait);});AccessoryAccess.of(p).setItem(0,egg);
|
||||
});c.waitFor(m->clientPet(m)!=null&&FamiliarSize.value(clientPet(m).egg())==size,200);
|
||||
var position=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);pet.setNoAi(true);pet.setDeltaMovement(Vec3.ZERO);pet.setPos(p.position().add(0,0,4));return pet.position().add(0,size==120?.1:1.2,0);});
|
||||
face(c,position);c.waitTicks(4);c.takeScreenshot("sanctuary-beta020-size-"+size);
|
||||
}
|
||||
System.out.println("Familiar020 client passed: native cryptic/revealed tooltips, 5 rarity colors, study purchase, inhabitant GUI 2/3/4, baby sitting, real sneak-right-click carry/drop, configured dive key and horizontal render, landing, egg size synchronization and extremes.");
|
||||
}finally{
|
||||
onboarding.set(false);c.getInput().releaseKey(o->o.keyShift);c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyJump);c.getInput().releaseKey(o->o.keySprint);
|
||||
c.runOnClient(m->{m.options.keyUp.setKey(forward);KeyMapping.get("sanctuary.progression.lying_key").setKey(posture);KeyMapping.resetMapping();m.options.setCameraType(camera);m.options.guiScale().set(scale);m.resizeGui();});
|
||||
}
|
||||
}
|
||||
private static List<Component> tooltip(Minecraft m,ItemStack stack){return stack.getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);}
|
||||
private static FamiliarEntity clientPet(Minecraft m){for(var e:m.level.entitiesForRendering())if(e instanceof FamiliarEntity pet&&!pet.isRemoved())return pet;return null;}
|
||||
private static void face(ClientGameTestContext c,Vec3 at){c.runOnClient(m->{var d=at.subtract(m.player.getEyePosition());m.player.setYRot((float)Math.toDegrees(Math.atan2(-d.x,d.z)));m.player.setXRot((float)-Math.toDegrees(Math.atan2(d.y,d.horizontalDistance())));});}
|
||||
private static void clickWorld(ClientGameTestContext c,int button){c.runOnClient(m->{var w=m.getWindow();m.mouseHandler.onButton(w.handle(),new MouseButtonInfo(button,0),1);});c.waitTick();c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(button,0),0));c.waitTick();}
|
||||
private static void hover(ClientGameTestContext c,int index){c.runOnClient(m->{var screen=(AbstractContainerScreen<?>)m.gui.screen();var a=(AbstractContainerScreenAccessor)screen;var slot=screen.getMenu().getSlot(index);var w=m.getWindow();m.mouseHandler.onMove(w.handle(),(a.sanctuary$getLeftPos()+slot.x+8.0)*w.getScreenWidth()/w.getGuiScaledWidth(),(a.sanctuary$getTopPos()+slot.y+8.0)*w.getScreenHeight()/w.getGuiScaledHeight(),0,0);m.mouseHandler.handleAccumulatedMovement();});c.waitTick();}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler container){return container.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?Stream.concat(Stream.of(w),widgets(nested)):Stream.of(w));}
|
||||
private static boolean key(AbstractWidget w,String key){return w.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key);}
|
||||
private static Button button(Screen screen,String key){return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(w->key(w,key)).findFirst().orElseThrow(()->new AssertionError("Missing "+key));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.client.shader.VanillaLight;
|
||||
import fr.koka.sanctuary.client.panorama.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.*;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.renderer.fog.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.effect.*;
|
||||
import net.minecraft.world.level.material.FogType;
|
||||
|
||||
public final class Fog053ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.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());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Fog053 "+UUID.randomUUID());s.setSeed("53");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(true);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
var base=p.blockPosition();int i=0;
|
||||
for(int d:new int[]{8,32,64,96}){int x=(i++%2==0?-1:1)*d/4;for(int dy=0;dy<5;dy++)for(int dx=-2;dx<=2;dx++)p.level().setBlockAndUpdate(base.offset(x+dx,dy,d),net.minecraft.world.level.block.Blocks.CONCRETE.red().defaultBlockState());}
|
||||
});
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.bobView().set(false);m.options.fov().set(70);m.player.setYRot(0);m.player.yRotO=0;m.player.setXRot(8);m.player.xRotO=8;VanillaLight.setEnabled(false);});
|
||||
c.waitTicks(35);
|
||||
c.runOnClient(m->{
|
||||
var nativeFog=setup(m,m.gameRenderer.mainCamera());require(nativeFog.renderDistanceStart>0,"Native distant start before enabling shader");
|
||||
VanillaLight.setEnabled(true);var graded=setup(m,m.gameRenderer.mainCamera());require(graded.renderDistanceStart==0&&graded.environmentalStart<=0,"Fog begins immediately at the viewpoint");
|
||||
sameEnds(nativeFog,graded);float prior=-1;
|
||||
for(float d:new float[]{0,1,4,16,32,64,96,128}){float f=PanoramaDepth.fog(new org.joml.Vector3f(0,0,d),PanoramaDepth.Fog.of(graded));require(f>=prior&&f>=0&&f<=1,"Continuous monotonic native GPU fog curve");if(d==0)require(f==0,"No veil at zero distance");if(d==1)require(f>0&&f<.02,"Very light fog starts close to the player");prior=f;}
|
||||
require(prior==1,"Fog fully hides the render boundary");VanillaLight.setEnabled(false);
|
||||
});
|
||||
c.waitTicks(6);c.takeScreenshot("sanctuary-beta053-fog-off");
|
||||
c.runOnClient(m->VanillaLight.setEnabled(true));c.waitFor(m->m.gameRenderer.getAppliedPostEffects().contains(VanillaLight.EFFECT),100);c.waitTicks(6);c.takeScreenshot("sanctuary-beta053-fog-on");
|
||||
var chain=c.computeOnClient(m->m.getShaderManager().getPostChain(VanillaLight.EFFECT,net.minecraft.client.renderer.LevelTargetBundle.MAIN_TARGETS));
|
||||
for(int saturation:new int[]{0,100,200}){
|
||||
c.runOnClient(m->VanillaLight.setSaturation(saturation));c.waitTicks(6);
|
||||
c.runOnClient(m->require(m.getShaderManager().getPostChain(VanillaLight.EFFECT,net.minecraft.client.renderer.LevelTargetBundle.MAIN_TARGETS)==chain,"Live saturation reuses the compiled shader chain"));
|
||||
c.takeScreenshot("sanctuary-beta053-saturation-"+saturation);
|
||||
}
|
||||
c.runOnClient(m->{
|
||||
VanillaLight.setSaturation(170);
|
||||
try{var pref=VanillaLight.class.getDeclaredField("preferences");pref.setAccessible(true);pref.set(null,null);}catch(ReflectiveOperationException e){throw new RuntimeException(e);}
|
||||
require(VanillaLight.saturation()==170,"Saturation preference survives reload from disk");
|
||||
VanillaLight.setSaturation(999);require(VanillaLight.saturation()==200,"Upper saturation limit");VanillaLight.setSaturation(-1);require(VanillaLight.saturation()==0,"Lower saturation limit");VanillaLight.setSaturation(100);
|
||||
});
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
VanillaLight.setSaturation(100);m.gui.setScreen(new fr.koka.sanctuary.client.shader.ShaderOptionsScreen(null));
|
||||
var slider=widgets(m.gui.screen()).filter(AbstractSliderButton.class::isInstance).map(AbstractSliderButton.class::cast).findFirst().orElseThrow();
|
||||
require(slider.getMessage().getString().contains("100")&&slider.getMessage().getString().contains("Saturation"),"Localized native saturation slider");
|
||||
slider.keyPressed(enter());for(int i=0;i<8;i++)slider.keyPressed(new KeyEvent(InputConstants.KEY_RIGHT,0,0));
|
||||
require(VanillaLight.saturation()>100,"Keyboard interaction immediately applies native slider value");
|
||||
});c.waitTicks(4);c.takeScreenshot("sanctuary-beta053-shader-options-"+locale);
|
||||
}
|
||||
c.runOnClient(m->{m.gui.setScreen(null);VanillaLight.setSaturation(100);});
|
||||
for(CameraType type:List.of(CameraType.FIRST_PERSON,CameraType.THIRD_PERSON_BACK))for(int fov:new int[]{30,110}){
|
||||
c.runOnClient(m->{m.options.setCameraType(type);m.options.fov().set(fov);m.player.setYRot(30);m.player.setXRot(-10);});c.waitTicks(5);
|
||||
c.runOnClient(m->{var f=m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData;require(f.renderDistanceStart==0&&f.renderDistanceEnd==128,"Camera and FOV preserve the native distance scale");});
|
||||
}
|
||||
c.runOnClient(m->{m.options.setCameraType(CameraType.FIRST_PERSON);m.options.fov().set(70);m.player.setYRot(0);m.player.setXRot(8);
|
||||
var live=m.gameRenderer.mainCamera();
|
||||
for(var medium:List.of(FogType.WATER,FogType.LAVA,FogType.POWDER_SNOW)){
|
||||
Camera camera=new MediumCamera(live,medium);VanillaLight.setEnabled(false);var before=setup(m,camera);VanillaLight.setEnabled(true);sameFog(before,setup(m,camera));
|
||||
}
|
||||
for(var effect:List.of(MobEffects.BLINDNESS,MobEffects.DARKNESS)){
|
||||
m.player.addEffect(new MobEffectInstance(effect,200));VanillaLight.setEnabled(false);var before=setup(m,live);VanillaLight.setEnabled(true);sameFog(before,setup(m,live));m.player.removeEffect(effect);
|
||||
}
|
||||
m.level.setRainLevel(1);m.level.setThunderLevel(1);VanillaLight.setEnabled(false);var before=setup(m,live);VanillaLight.setEnabled(true);sameEnds(before,setup(m,live));m.level.setRainLevel(0);m.level.setThunderLevel(0);
|
||||
});
|
||||
for(String locale:List.of("fr_fr","en_us"))c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());require(net.minecraft.network.chat.Component.translatable("sanctuary.shaders.help").getString().contains(locale.equals("fr_fr")?"brume progressive":"progressive fog"),"Localized shader tooltip");});
|
||||
var reload=c.computeOnClient(m->m.reloadResourcePacks());c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(6);
|
||||
c.runOnClient(m->require(m.gameRenderer.getAppliedPostEffects().contains(VanillaLight.EFFECT)&&setup(m,m.gameRenderer.mainCamera()).renderDistanceStart==0,"Native shader and fog survive resource reload"));
|
||||
c.runOnClient(m->{PanoramaOptions.get().resolution=256;PanoramaCapture.request(m);});
|
||||
c.waitFor(m->PanoramaCapture.lastCapture().isDone(),1200);var panorama=c.computeOnClient(m->PanoramaCapture.lastCapture().join());
|
||||
try{var data=fr.koka.sanctuary.progression.AtomicJson.parse(java.nio.file.Files.readString(panorama.resolve("capture.json"))).getAsJsonObject();
|
||||
for(var face:data.getAsJsonArray("faces"))require(face.getAsJsonObject().getAsJsonObject("fog").get("renderStart").getAsFloat()>0,"Technical panorama retains raw native fog matching its depth mask");
|
||||
}catch(java.io.IOException e){throw new RuntimeException(e);}
|
||||
c.waitTicks(5);c.runOnClient(m->require(setup(m,m.gameRenderer.mainCamera()).renderDistanceStart==0,"Normal progressive fog resumes after six panorama faces"));
|
||||
c.takeScreenshot("sanctuary-beta053-fog-restored");
|
||||
}
|
||||
System.out.println("FOG053_CLIENT_PASS: native progressive fog, toggle, near/far, camera/FOV, fluids/effects/weather, FR/EN, resource reload and raw panorama restoration");
|
||||
System.out.println("SATURATION053_CLIENT_PASS: 0/100/200 live GPU screenshots, native FR/EN keyboard slider, bounded saved preference, no shader reload per change");
|
||||
}
|
||||
private static FogData setup(Minecraft m,Camera camera){try{var field=net.minecraft.client.renderer.GameRenderer.class.getDeclaredField("fogRenderer");field.setAccessible(true);return ((FogRenderer)field.get(m.gameRenderer)).setupFog(camera,8,DeltaTracker.ZERO,0,m.level);}catch(ReflectiveOperationException e){throw new RuntimeException(e);}}
|
||||
private static void sameEnds(FogData a,FogData b){require(a.environmentalEnd==b.environmentalEnd&&a.renderDistanceEnd==b.renderDistanceEnd&&a.skyEnd==b.skyEnd&&a.cloudEnd==b.cloudEnd&&a.color.equals(b.color),"Native ends, sky/cloud limits and colour preserved");}
|
||||
private static void sameFog(FogData a,FogData b){sameEnds(a,b);require(a.environmentalStart==b.environmentalStart&&a.renderDistanceStart==b.renderDistanceStart,"Special visibility restriction remains entirely native");}
|
||||
private static final class MediumCamera extends Camera {
|
||||
private final Camera live;private final FogType medium;
|
||||
MediumCamera(Camera live,FogType medium){this.live=live;this.medium=medium;}
|
||||
@Override public FogType getFluidInCamera(){return medium;}
|
||||
@Override public net.minecraft.world.entity.Entity entity(){return live.entity();}
|
||||
@Override public net.minecraft.world.phys.Vec3 position(){return live.position();}
|
||||
@Override public net.minecraft.core.BlockPos blockPosition(){return live.blockPosition();}
|
||||
@Override public net.minecraft.world.attribute.EnvironmentAttributeProbe attributeProbe(){return live.attributeProbe();}
|
||||
@Override public org.joml.Vector3fc forwardVector(){return live.forwardVector();}
|
||||
}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static java.util.stream.Stream<net.minecraft.client.gui.components.events.GuiEventListener> widgets(net.minecraft.client.gui.components.events.ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof net.minecraft.client.gui.components.events.ContainerEventHandler child?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(child)):java.util.stream.Stream.of(w));}
|
||||
private static void require(boolean v,String text){if(!v)throw new AssertionError(text);}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.grave.*;
|
||||
import fr.koka.sanctuary.food.*;
|
||||
import fr.koka.sanctuary.client.panorama.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import java.nio.file.*;
|
||||
import java.nio.*;
|
||||
import javax.imageio.ImageIO;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.SkullBlockEntity;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class FoodPanorama039ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;
|
||||
var starterFrames=new java.util.concurrent.atomic.AtomicInteger();
|
||||
var starterOptions=new java.util.concurrent.atomic.AtomicReference<java.util.List<StarterCompanions.Choice>>();
|
||||
var starterChosen=new java.util.concurrent.atomic.AtomicReference<String>();
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(m.gui.screen() instanceof HelloWorldScreen screen&&!screen.starterChoices().isEmpty()){
|
||||
require(m.player==null&&m.level==null,"Starter chosen before any world or player appears");
|
||||
int frame=starterFrames.incrementAndGet();
|
||||
if(frame==1){starterOptions.set(screen.starterChoices());require(screen.starterChoices().size()==3,"Three starters displayed");require(!button(screen,"sanctuary.progression.enter").active,"Entering requires colour and starter");m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());}
|
||||
if(frame==3){var slot=widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("StarterSlot")).findFirst().orElseThrow();var w=m.getWindow();m.mouseHandler.onMove(w.handle(),(slot.getX()+11.0)*w.getScreenWidth()/w.getGuiScaledWidth(),(slot.getY()+11.0)*w.getScreenHeight()/w.getGuiScaledHeight(),0,0);m.mouseHandler.handleAccumulatedMovement();}
|
||||
if(frame==8){net.minecraft.client.Screenshot.grab(m,false);net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworking.send(new StarterPayloads.Create("",0,"ender_dragon"));}
|
||||
if(frame==18){require(screen.starterChoices().equals(starterOptions.get()),"Forged choice cannot reroll the offer");starterChosen.set(screen.starterChoices().getFirst().mob());Starter039TestSupport.selectFirst(screen);widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());button(screen,"sanctuary.progression.enter").onPress(enter());}
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave savedWorld;
|
||||
var armed=new AtomicBoolean(true);
|
||||
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary food panorama039 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
|
||||
savedWorld=world.getWorldSave();
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected("en_us");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var store=ProgressionService.session(s).starters;require(store.get(p.getUUID()).selected().equals(starterChosen.get()),"Server persists chosen starter");require(starterChosen.get().equals(p.getAttached(StarterCompanions.RECEIPT)),"Starter delivery receipt on first arrival");require(fr.koka.sanctuary.inventory.AccessoryAccess.of(p).getItem(fr.koka.sanctuary.inventory.AccessoryContainer.FAMILIAR).getItem()==StarterCompanions.egg(starterChosen.get()).getItem(),"Selected egg is really equipped");});
|
||||
server.runOnServer(s->{s.getGlobalGameRules().set(GameRules.NATURAL_HEALTH_REGENERATION,false,s);var p=s.getPlayerList().getPlayers().getFirst();p.getFoodData().setFoodLevel(1);p.getFoodData().setSaturation(0);p.getInventory().setItem(0,new ItemStack(Items.APPLE,12));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->FoodKnowledgeClient.ready()&&FoodDetailsClient.current()!=null,150);
|
||||
c.runOnClient(m->{require(!FoodKnowledgeClient.knows(new ItemStack(Items.APPLE)),"Untasted food hidden");require(new ItemStack(Items.APPLE).getTooltipImage().isEmpty(),"Unknown tooltip has no food metrics");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stack=new ItemStack(Items.APPLE);stack.get(DataComponents.CONSUMABLE).onConsume(p.level(),p,stack);});
|
||||
c.waitFor(m->FoodKnowledgeClient.knows(new ItemStack(Items.APPLE)),100);
|
||||
c.runOnClient(m->{require(new ItemStack(Items.APPLE).getTooltipImage().orElseThrow() instanceof FoodDetailsTooltip.Data,"Tasting reveals native icon tooltip");require(new ItemStack(Items.COOKED_BEEF).getTooltipImage().isEmpty(),"Untasted steak remains hidden");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(8);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->m.player.experienceLevel==8,100);
|
||||
c.runOnClient(m->m.gui.setScreen(new ProgressionScreen(null)));c.waitTicks(5);
|
||||
c.runOnClient(m->{var b=button(m.gui.screen(),"sanctuary.food_knowledge.buy");require(b.active,"Food study available with XP");b.onPress(enter());});
|
||||
c.waitFor(m->m.player.experienceLevel==4&&FoodKnowledgeClient.knows(new ItemStack(Items.COOKED_BEEF)),100);
|
||||
c.takeScreenshot("sanctuary-beta039-food-study");c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stack=GraveService.head(p,List.of(new ItemStack(Items.DIAMOND)));fr.koka.sanctuary.inventory.AccessoryAccess.of(p).setItem(fr.koka.sanctuary.inventory.AccessoryContainer.HEAD,stack);});
|
||||
c.waitFor(m->fr.koka.sanctuary.cosmetics.HeadCosmeticService.appearance(m.player).is(Items.PLAYER_HEAD),150);
|
||||
c.runOnClient(m->{var stack=fr.koka.sanctuary.cosmetics.HeadCosmeticService.appearance(m.player);var h=((HeadCosmeticState.Access)m.getEntityRenderDispatcher().extractEntity(m.player,.5f)).sanctuary$headCosmetic();require(h.blockEntity instanceof net.minecraft.client.renderer.blockentity.state.SkullBlockRenderState,"Skull uses native component-aware renderer");var skull=(net.minecraft.client.renderer.blockentity.state.SkullBlockRenderState)h.blockEntity;require(skull.renderType==m.playerSkinRenderCache().getOrDefault(stack.get(DataComponents.PROFILE)).renderType(),"Skull uses owner's skin cache, not a generic Steve texture");require(h.block.isEmpty(),"No generic block model over the skull");m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_FRONT);});
|
||||
c.waitTicks(5);c.takeScreenshot("sanctuary-beta039-head-skin");
|
||||
c.runOnClient(m->{m.gui.setScreen(new PanoramaOptionsScreen(null));});c.waitTicks(5);c.takeScreenshot("sanctuary-beta039-panorama-options");c.runOnClient(m->m.gui.setScreen(null));
|
||||
c.runOnClient(m->{m.player.setYRot(33);m.player.setXRot(17);m.player.yRotO=31;m.player.xRotO=15;});
|
||||
var before=c.computeOnClient(m->List.of(m.getWindow().getWidth(),m.getWindow().getHeight(),m.options.fov().get(),m.options.getCameraType()));
|
||||
c.runOnClient(m->{PanoramaOptions.get().enabled=true;PanoramaOptions.get().resolution=256;net.minecraft.client.Screenshot.grab(m,false);require(m.player.getYRot()==33&&m.player.getXRot()==17&&m.player.yRotO==31&&m.player.xRotO==15,"Camera angles restored synchronously");});
|
||||
c.waitFor(m->PanoramaCapture.lastCapture().isDone(),1200);
|
||||
Path first=c.computeOnClient(m->PanoramaCapture.lastCapture().join());
|
||||
require(c.computeOnClient(m->List.of(m.getWindow().getWidth(),m.getWindow().getHeight(),m.options.fov().get(),m.options.getCameraType())).equals(before),"Window, FOV and third-person choice restored");
|
||||
verifyCapture(first);
|
||||
c.waitFor(m->!PanoramaCapture.busy(),100);
|
||||
c.runOnClient(m->net.minecraft.client.Screenshot.grab(m,false));c.waitFor(m->PanoramaCapture.lastCapture().isDone(),1200);
|
||||
Path second=c.computeOnClient(m->PanoramaCapture.lastCapture().join());require(!first.equals(second),"Second panorama cannot overwrite first");verifyCapture(second);
|
||||
c.waitFor(m->!PanoramaCapture.busy(),100);
|
||||
c.runOnClient(m->{PanoramaOptions.get().enabled=false;PanoramaOptions.get().resolution=1024;net.minecraft.client.Screenshot.grab(m,false);require(PanoramaCapture.lastCapture().join().equals(second),"Normal screenshot does not invoke panorama");});
|
||||
c.waitTicks(5);c.takeScreenshot("sanctuary-beta039-after-panorama");
|
||||
server.runOnServer(sv->{
|
||||
var p=sv.getPlayerList().getPlayers().getFirst();var progress=ProgressRecord.INITIAL;for(int i=1;i<6;i++)progress=progress.buy("inventory",i);p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);
|
||||
p.getInventory().clearContent();fr.koka.sanctuary.inventory.AccessoryAccess.of(p).clearContent();fr.koka.sanctuary.inventory.HotbarRows.select(p,2);
|
||||
p.getInventory().setItem(0,new ItemStack(Items.DIAMOND,12));p.getInventory().setItem(10,new ItemStack(Items.EMERALD,3));fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,new ItemStack(Items.APPLE,5));
|
||||
p.setItemSlot(net.minecraft.world.entity.EquipmentSlot.HEAD,new ItemStack(Items.IRON_HELMET));fr.koka.sanctuary.inventory.AccessoryAccess.of(p).setItem(fr.koka.sanctuary.inventory.AccessoryContainer.CAPE,new ItemStack(fr.koka.sanctuary.cosmetics.AccessoryService.ZERO_CAPE));fr.koka.sanctuary.inventory.AccessoryAccess.of(p).setItem(fr.koka.sanctuary.inventory.AccessoryContainer.FAMILIAR,new ItemStack(Items.CHICKEN_SPAWN_EGG));
|
||||
var layout=GraveLayout.capture(p);for(int i=0;i<p.getInventory().getContainerSize();i++)layout.collect(p.getInventory().getItem(i));for(int i=0;i<3;i++)layout.collect(fr.koka.sanctuary.inventory.AccessoryAccess.of(p).getItem(i));
|
||||
var head=GraveService.head(p,layout.items());var data=head.get(DataComponents.CUSTOM_DATA).copyTag();var marker=data.getCompound(GraveService.MARKER).orElseThrow();layout.info.write(marker);data.put(GraveService.MARKER,marker);head.set(DataComponents.CUSTOM_DATA,net.minecraft.world.item.component.CustomData.of(data));
|
||||
p.getInventory().clearContent();fr.koka.sanctuary.inventory.AccessoryAccess.of(p).clearContent();fr.koka.sanctuary.inventory.HotbarRows.select(p,0);p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);ProgressionService.send(p,"");p.getInventory().setItem(0,head);GraveService.open(p,head);
|
||||
});
|
||||
c.waitForScreen(GraveInventoryScreen.class);c.waitFor(m->((GraveInventoryMenu)m.player.containerMenu).visibleRows()==6&&m.player.containerMenu.getSlot(18).hasItem(),150);c.waitTicks(5);
|
||||
c.runOnClient(m->{var screen=(GraveInventoryScreen)m.gui.screen();var menu=screen.getMenu();require(menu.getSlot(18).x<menu.getSlot(135).x,"Death cells are left of current inventory");require(menu.rows()==6&&ProgressionClient.progress.inventoryRows()==1,"Six death rows, one current row");require(!menu.getSlot(0).mayPlace(new ItemStack(Items.STONE)),"Empty death cells are read-only");});
|
||||
c.takeScreenshot("sanctuary-beta039-grave-inventories");
|
||||
for(int slot:new int[]{18,54,59,60}){final int cell=slot;c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,cell,0,net.minecraft.world.inventory.ContainerInput.QUICK_MOVE,m.player));c.waitFor(m->m.player.containerMenu.getSlot(cell).getItem().isEmpty(),100);}
|
||||
c.waitTicks(6);
|
||||
require(server.computeOnServer(sv->{var p=sv.getPlayerList().getPlayers().getFirst();System.out.println("GRAVE039 recovery state: "+java.util.stream.IntStream.range(0,9).mapToObj(i->p.getInventory().getItem(i).toString()).toList()+" helmet="+p.getItemBySlot(net.minecraft.world.entity.EquipmentSlot.HEAD)+" accessories="+java.util.stream.IntStream.range(0,3).mapToObj(i->fr.koka.sanctuary.inventory.AccessoryAccess.of(p).getItem(i).toString()).toList());return p.getInventory().contains(new ItemStack(Items.DIAMOND))&&p.getItemBySlot(net.minecraft.world.entity.EquipmentSlot.HEAD).is(Items.IRON_HELMET)&&fr.koka.sanctuary.inventory.AccessoryAccess.of(p).getItem(fr.koka.sanctuary.inventory.AccessoryContainer.CAPE).is(fr.koka.sanctuary.cosmetics.AccessoryService.ZERO_CAPE)&&fr.koka.sanctuary.inventory.AccessoryAccess.of(p).getItem(fr.koka.sanctuary.inventory.AccessoryContainer.FAMILIAR).is(Items.CHICKEN_SPAWN_EGG);}),"Real recovery packets route storage, helmet, cape and familiar correctly");
|
||||
c.waitTicks(5);c.takeScreenshot("sanctuary-beta039-grave-recovery");c.runOnClient(m->{m.options.guiScale().set(1);m.resizeGui();});c.waitTicks(5);c.takeScreenshot("sanctuary-beta039-grave-scale1");
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});c.waitTicks(5);c.takeScreenshot("sanctuary-beta039-grave-fr");
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON));
|
||||
server.runOnServer(sv->{var p=sv.getPlayerList().getPlayers().getFirst();p.getFoodData().setFoodLevel(3);p.getFoodData().setSaturation(0);p.getFoodData().addExhaustion(3.5f);});c.waitTicks(12);c.takeScreenshot("sanctuary-beta039-food-underlay");
|
||||
server.runOnServer(sv->{var p=sv.getPlayerList().getPlayers().getFirst();fr.koka.sanctuary.inventory.AccessoryAccess.of(p).clearContent();sv.saveEverything(true,true,true);});
|
||||
System.out.println("FOOD PANORAMA039 client passed: hidden/tasted/studied tooltips, real purchase, owner skull renderer, six colour/depth/fog faces, repeated capture, restored camera. Output: "+first);
|
||||
}
|
||||
int beforeReconnect=starterFrames.get();
|
||||
try(var world=savedWorld.open()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
require(starterFrames.get()==beforeReconnect,"Returning resident skips starter selection");
|
||||
world.getServer().runOnServer(sv->{var p=sv.getPlayerList().getPlayers().getFirst();require(starterChosen.get().equals(p.getAttached(StarterCompanions.RECEIPT))&&fr.koka.sanctuary.inventory.AccessoryAccess.of(p).getItem(fr.koka.sanctuary.inventory.AccessoryContainer.FAMILIAR).isEmpty(),"Reconnect never replaces a removed starter");});
|
||||
System.out.println("STARTER039_CLIENT_PASS: configuration choice, forged choice rejected, equipped once, reconnect without a second gift");
|
||||
}finally{armed.set(false);Starter039TestSupport.manual=false;PanoramaOptions.get().enabled=false;}
|
||||
}
|
||||
private static void verifyCapture(Path folder){try{
|
||||
var data=com.google.gson.JsonParser.parseString(Files.readString(folder.resolve("capture.json"))).getAsJsonObject();require(data.getAsJsonArray("faces").size()==6,"Metadata has six faces");
|
||||
boolean zeroToOne=data.get("clip_z_zero_to_one").getAsBoolean();
|
||||
for(int i=0;i<6;i++){
|
||||
for(String prefix:List.of("panorama_","depth_","fog_")){var image=ImageIO.read(folder.resolve(prefix+i+".png").toFile());require(image.getWidth()==256&&image.getHeight()==256,"Square export "+prefix+i);}
|
||||
byte[] raw=Files.readAllBytes(folder.resolve("depth_"+i+".f32"));require(raw.length==256*256*4,"Depth float32 size");
|
||||
var bytes=ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN);int surfaces=0;for(int n=0;n<256*256;n++){float z=bytes.getFloat();require(Float.isFinite(z)&&z>=0&&z<=1,"Valid reversed GPU depth");if(z>0)surfaces++;}
|
||||
if(i==5)require(surfaces>256*256/2,"Bottom face contains terrain depth, not a buffer cleared by HUD");
|
||||
var metadata=data.getAsJsonArray("faces").get(i).getAsJsonObject();var numbers=metadata.getAsJsonArray("inverse_view_projection_column_major");float[] inv=new float[16];for(int n=0;n<16;n++)inv[n]=numbers.get(n).getAsFloat();
|
||||
var numbers2=metadata.getAsJsonArray("view_projection_column_major");float[] vp=new float[16];for(int n=0;n<16;n++)vp[n]=numbers2.get(n).getAsFloat();
|
||||
var world=PanoramaDepth.position(new Matrix4f().set(inv),.2f,-.4f,.5f,zeroToOne);var clip=new Matrix4f().set(vp).transformProject(new Vector3f(world));require(Math.abs(clip.x-.2f)<.001&&Math.abs(clip.y+.4f)<.001&&Math.abs(clip.z-(zeroToOne?.5f:0))<.001,"Inverse projection matches backend depth convention");
|
||||
}
|
||||
}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static Button button(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.grave.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.SkullBlockEntity;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class GravesFood038ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(armed.get()&&m.gui.screen() instanceof HelloWorldScreen screen){widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary graves food038 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
server.runOnServer(s->{s.getGlobalGameRules().set(GameRules.NATURAL_HEALTH_REGENERATION,false,s);s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);var p=s.getPlayerList().getPlayers().getFirst();fr.koka.sanctuary.food.FoodKnowledgeService.tasted(p,new ItemStack(Items.COOKED_BEEF));p.getInventory().setItem(0,new ItemStack(Items.COOKED_BEEF,12));p.getFoodData().setFoodLevel(3);p.getFoodData().setSaturation(1.5f);p.getFoodData().addExhaustion(2);p.inventoryMenu.broadcastChanges();});
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.player.setXRot(10);});
|
||||
c.waitFor(m->FoodDetailsClient.current()!=null&&FoodDetailsClient.current().food()==3&&FoodDetailsClient.current().capacity()==6,150);
|
||||
c.waitTicks(5);c.runOnClient(m->require(FoodDetailsClient.renderedIcons()==3,"Native hunger hook renders exactly three icons"));c.takeScreenshot("sanctuary-beta038-food-three");
|
||||
c.runOnClient(m->{var data=(FoodDetailsTooltip.Data)new ItemStack(Items.COOKED_BEEF).getTooltipImage().orElseThrow();require(data.food()==8&&Math.abs(data.saturation()-12.8f)<.001&&data.foodGain()==3&&data.overflow(),"Native tooltip supplies food/saturation icons with capacity-aware gain");require(net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent.create(data) instanceof FoodDetailsTooltip,"Fabric maps the native item image to the food icon renderer");m.gui.setScreen(new InventoryScreen(m.player));});c.waitTicks(3);hover(c,36);c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-food-tooltip");c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressEffects.get(p);for(int i=0;i<7;i++)r=r.buy("hunger",1);for(int i=0;i<5;i++)r=r.buy("inventory",1);p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");p.getFoodData().setFoodLevel(13);p.getFoodData().setSaturation(7.5f);});
|
||||
c.waitFor(m->ProgressionClient.progress.inventoryRows()==6&&FoodDetailsClient.current()!=null&&FoodDetailsClient.current().capacity()==20&&FoodDetailsClient.renderedIcons()==10,150);c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-food-ten");
|
||||
server.runOnServer(s->Companion019GameTests.equip(s.getPlayerList().getPlayers().getFirst(),fr.koka.sanctuary.companion.CompanionType.PIG));
|
||||
c.waitFor(m->FoodDetailsClient.current()!=null&&FoodDetailsClient.current().foodMultiplier()>1,200);
|
||||
c.runOnClient(m->require(Math.abs(FoodDetailsClient.current().preview(new ItemStack(Items.APPLE)).saturationGain()-2.64f)<.01,"Server pig bonus reaches the actual client food preview"));
|
||||
server.runOnServer(s->fr.koka.sanctuary.inventory.AccessoryAccess.of(s.getPlayerList().getPlayers().getFirst()).setItem(0,ItemStack.EMPTY));c.waitFor(m->FoodDetailsClient.current().foodMultiplier()==1,100);
|
||||
c.runOnClient(m->m.gui.setScreen(new FoodDetailsOptionsScreen(null)));c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-food-options");c.runOnClient(m->m.gui.setScreen(null));
|
||||
BlockPos pos=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().east(2);p.level().setBlockAndUpdate(at,Blocks.PLAYER_HEAD.defaultBlockState());var skull=(SkullBlockEntity)p.level().getBlockEntity(at);var items=new ArrayList<ItemStack>();for(int i=0;i<64;i++)items.add(new ItemStack(i==54?Items.EMERALD:Items.DIAMOND,i+1));skull.applyComponentsFromItemStack(GraveService.head(p,items));skull.setChanged();p.level().sendBlockUpdated(at,skull.getBlockState(),skull.getBlockState(),3);return at;});
|
||||
c.waitFor(m->GraveService.isGrave(m.level.getBlockEntity(pos)),100);
|
||||
c.runOnClient(m->{var result=m.gameMode.useItemOn(m.player,net.minecraft.world.InteractionHand.MAIN_HAND,new net.minecraft.world.phys.BlockHitResult(net.minecraft.world.phys.Vec3.atCenterOf(pos),net.minecraft.core.Direction.UP,pos,false));require(result.consumesAction(),"Real client block interaction consumes the right-click");});
|
||||
c.waitForScreen(GraveScreen.class);c.waitFor(m->m.player.containerMenu.getSlot(0).getItem().is(Items.DIAMOND),100);c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-grave-page1");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.grave.page").onPress(enter()));c.waitFor(m->((GraveMenu)m.player.containerMenu).page()==1&&m.player.containerMenu.getSlot(0).getItem().is(Items.EMERALD),100);c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-grave-page2");
|
||||
click(c,0,true);c.waitFor(m->m.player.containerMenu.getSlot(0).getItem().isEmpty(),100);
|
||||
require(server.computeOnServer(s->GraveService.container((SkullBlockEntity)s.overworld().getBlockEntity(pos)).getItem(54).isEmpty()),"Real packet extraction removes second-page contents server-side");
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var skull=(SkullBlockEntity)p.level().getBlockEntity(pos);var head=Block.getDrops(skull.getBlockState(),p.level(),pos,skull).getFirst();p.level().removeBlock(pos,false);p.getInventory().setItem(2,head);GraveService.open(p,head);});
|
||||
c.waitForScreen(GraveScreen.class);c.waitFor(m->m.player.containerMenu.getSlot(0).getItem().is(Items.DIAMOND),100);c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-grave-portable");
|
||||
click(c,0,true);c.waitFor(m->m.player.containerMenu.getSlot(0).getItem().isEmpty(),100);c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);
|
||||
c.runOnClient(m->m.options.guiScale().set(1));c.runOnClient(m->m.resizeGui());
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();GraveService.open(p,p.getInventory().getItem(2));});c.waitForScreen(GraveScreen.class);c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-grave-scale1");
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);
|
||||
c.runOnClient(m->m.gui.setScreen(new InventoryScreen(m.player)));c.waitTicks(3);hover(c,38);c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-grave-memory");c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new FoodDetailsOptionsScreen(null));});c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-food-options-fr");
|
||||
c.runOnClient(m->{require(FoodDetailsClient.label("food_icons").getString().equals("Nourriture"),"French icon label is translated");FoodDetailsClient.options().tooltips=false;require(new ItemStack(Items.COOKED_BEEF).getTooltipImage().isEmpty(),"Tooltip display switch removes food icons");FoodDetailsClient.options().tooltips=true;m.gui.setScreen(new InventoryScreen(m.player));});c.waitTicks(3);hover(c,36);c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-food-tooltip-fr");c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(10);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->m.player.experienceLevel==10,100);c.runOnClient(m->CycleClient.open(null,"faction"));
|
||||
c.waitFor(m->CycleClient.view!=null&&CycleClient.view.prestige()==0&&m.gui.screen() instanceof CycleFactionScreen,100);c.waitTicks(5);c.takeScreenshot("sanctuary-beta038-faction-founding");
|
||||
c.runOnClient(m->{widgets(m.gui.screen()).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow().setValue("Les Veilleurs");button(m.gui.screen(),"sanctuary.cycle.create").onPress(enter());});c.waitTicks(2);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.cycle.confirm").onPress(enter()));
|
||||
c.waitFor(m->CycleClient.view.faction()!=null&&m.player.experienceLevel==0,100);c.waitTicks(5);
|
||||
c.runOnClient(m->require(CycleClient.view.faction().capacity()==1&&CycleClient.view.faction().owner().equals(m.player.getUUID())&&CycleClient.view.prestige()==0,"Real founding packet spends ten levels, no prestige, one founder place"));c.takeScreenshot("sanctuary-beta038-faction-founded");
|
||||
System.out.println("GRAVES FOOD038 client passed: real server food sync, three/ten icons, saturation tooltip, options, two-page chest, packet shift-click and portable grave.");
|
||||
}finally{armed.set(false);}
|
||||
}
|
||||
private static void hover(ClientGameTestContext c,int index){c.runOnClient(m->{var screen=(AbstractContainerScreen<?>)m.gui.screen();var a=(AbstractContainerScreenAccessor)screen;var slot=screen.getMenu().getSlot(index);var w=m.getWindow();m.mouseHandler.onMove(w.handle(),(a.sanctuary$getLeftPos()+slot.x+8.0)*w.getScreenWidth()/w.getGuiScaledWidth(),(a.sanctuary$getTopPos()+slot.y+8.0)*w.getScreenHeight()/w.getGuiScaledHeight(),0,0);m.mouseHandler.handleAccumulatedMovement();});}
|
||||
private static void click(ClientGameTestContext c,int index,boolean shift){hover(c,index);c.waitTick();if(shift)c.getInput().holdKey(o->o.keyShift);c.runOnClient(m->{var w=m.getWindow();m.mouseHandler.onButton(w.handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,shift?InputConstants.MOD_SHIFT:0),1);m.mouseHandler.onButton(w.handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,shift?InputConstants.MOD_SHIFT:0),0);});if(shift)c.getInput().releaseKey(o->o.keyShift);c.waitTicks(2);}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static Button button(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.mining.*;
|
||||
import fr.koka.sanctuary.building.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
|
||||
/** Real client gestures: a changed crosshair must never replace ongoing server work. */
|
||||
final class GroupWork018Checks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server) {
|
||||
var previous=c.computeOnClient(m -> InputConstants.getKey(key().saveString()));
|
||||
try {
|
||||
c.runOnClient(m -> {key().setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_R));KeyMapping.resetMapping();});
|
||||
BlockPos base=server.computeOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().above(8);
|
||||
for(int x=-6;x<=6;x++)for(int z=-6;z<=8;z++)for(int y=-1;y<=6;y++)p.level().setBlockAndUpdate(at.offset(x,y,z),(y==-1 ? Blocks.BEDROCK : Blocks.AIR).defaultBlockState());
|
||||
var record=ProgressEffects.get(p);while(record.mining()<2)record=record.buy("mining",1);while(record.building()<2)record=record.buy("building",1);
|
||||
p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);ProgressionService.send(p,"");
|
||||
p.teleportTo(at.getX()+.5,at.getY(),at.getZ()+.5);p.getInventory().clearContent();p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND_PICKAXE));
|
||||
for(int i=0;i<8;i++)p.level().setBlockAndUpdate(at.offset(i%4,1,3+i/4),Blocks.STONE.defaultBlockState());
|
||||
p.level().setBlockAndUpdate(at.offset(-3,1,0),Blocks.STONE.defaultBlockState());p.inventoryMenu.broadcastFullState();return at;
|
||||
});
|
||||
var origin=base.offset(0,1,3);var other=base.offset(-3,1,0);
|
||||
c.waitFor(m -> m.player.onGround() && Math.abs(m.player.getY()-base.getY())<.1 && m.player.getMainHandItem().is(Items.DIAMOND_PICKAXE) && ProgressionClient.progress.mining()==2 && m.level.getBlockState(origin).is(Blocks.STONE),600);
|
||||
c.getInput().lookAt(origin);c.waitTicks(3);c.getInput().holdKey(InputConstants.KEY_R);c.waitFor(m -> MiningClient.previewSize()==8,200);
|
||||
c.getInput().holdKey(o -> o.keyAttack);server.waitFor(s -> MiningManager.active(s.getPlayerList().getPlayers().getFirst()),200);
|
||||
c.getInput().lookAt(other);c.runOnClient(m -> m.mouseHandler.onScroll(m.getWindow().handle(),0,-1));
|
||||
c.waitTick();c.runOnClient(m -> require(MiningClient.previewSize()==8 && MiningClient.selectedRank()==2,"Looking at another one-block vein and scrolling keep the original eight-block selection"));
|
||||
c.getInput().holdKey(o -> o.keyRight);c.waitTicks(2);c.getInput().releaseKey(o -> o.keyRight);
|
||||
c.waitFor(m -> m.level.getBlockState(origin).isAir(),500);c.waitTicks(15);
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();
|
||||
for(int i=0;i<8;i++)require(p.level().getBlockState(origin.offset(i%4,0,i/4)).isAir(),"Every initially selected block is mined while looking away");
|
||||
require(p.level().getBlockState(other).is(Blocks.STONE) && p.getMainHandItem().getDamageValue()==8,"Holding after completion cannot start the newly aimed vein");
|
||||
});c.takeScreenshot("sanctuary-beta018-mining-locked-camera");
|
||||
c.getInput().releaseKey(o -> o.keyAttack);c.waitTicks(3);c.getInput().lookAt(other);c.waitTicks(3);c.getInput().holdKey(o -> o.keyAttack);
|
||||
c.waitFor(m -> m.level.getBlockState(other).isAir(),300);c.getInput().releaseKey(o -> o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);c.waitTicks(3);
|
||||
|
||||
var wall=base.offset(0,2,3);
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(base.getX()+.5,base.getY(),base.getZ()+.5);
|
||||
for(int x=-2;x<=2;x++)for(int y=-2;y<=2;y++) {p.level().setBlockAndUpdate(wall.offset(x,y,-1),Blocks.AIR.defaultBlockState());p.level().setBlockAndUpdate(wall.offset(x,y,0),Blocks.STONE.defaultBlockState());}
|
||||
p.level().setBlockAndUpdate(other,Blocks.STONE.defaultBlockState());p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.OAK_STAIRS,32));p.inventoryMenu.broadcastFullState();
|
||||
});
|
||||
c.waitFor(m -> m.player.getMainHandItem().is(Items.OAK_STAIRS) && m.level.getBlockState(wall).is(Blocks.STONE) && Math.abs(m.player.getX()-(base.getX()+.5))<.1,400);
|
||||
c.getInput().lookAt(wall);c.waitTicks(3);c.getInput().holdKey(InputConstants.KEY_R);c.waitFor(m -> BuildingClient.previewSize()==25,200);
|
||||
Direction initial=c.computeOnClient(m -> m.player.getDirection());c.getInput().holdKey(o -> o.keyUse);
|
||||
c.waitFor(m -> m.level.getBlockState(wall.north()).is(Blocks.OAK_STAIRS),200);
|
||||
c.getInput().lookAt(other);c.runOnClient(m -> m.mouseHandler.onScroll(m.getWindow().handle(),0,-1));
|
||||
c.getInput().holdKey(o -> o.keyRight);c.waitTicks(2);c.getInput().releaseKey(o -> o.keyRight);
|
||||
c.runOnClient(m -> require(BuildingClient.selectedRank()==2,"Wheel cannot resize the locked surface"));
|
||||
c.waitFor(m -> m.player.getMainHandItem().getCount()==7,400);c.waitTicks(15);
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();require(p.getMainHandItem().getCount()==7 && !BuildingManager.active(p),"A held group places one surface only, with exact native material consumption");
|
||||
for(int x=-2;x<=2;x++)for(int y=-2;y<=2;y++) {var state=p.level().getBlockState(wall.offset(x,y,-1));require(state.is(Blocks.OAK_STAIRS) && state.getValue(StairBlock.FACING)==initial,"Grid and stair orientation remain frozen while camera and player move");}
|
||||
});
|
||||
c.getInput().releaseKey(o -> o.keyUse);c.getInput().releaseKey(InputConstants.KEY_R);c.getInput().lookAt(wall);c.waitTicks(3);c.takeScreenshot("sanctuary-beta018-building-locked-camera");
|
||||
System.out.println("GroupWork018 native client passed: camera retarget, movement within reach, frozen ranks, exact initial vein and oriented surface, no automatic restart, release and new gesture");
|
||||
} finally {
|
||||
c.getInput().releaseKey(o -> o.keyAttack);c.getInput().releaseKey(o -> o.keyUse);c.getInput().releaseKey(o -> o.keyRight);c.getInput().releaseKey(InputConstants.KEY_R);
|
||||
c.runOnClient(m -> {key().setKey(previous);KeyMapping.resetMapping();});
|
||||
}
|
||||
}
|
||||
private static KeyMapping key() {return KeyMapping.get("sanctuary.mining.key");}
|
||||
private static void require(boolean ok,String text) {if(!ok)throw new AssertionError(text);}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.boats.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.entity.CampfireBlockEntity;
|
||||
import net.minecraft.client.renderer.blockentity.state.*;
|
||||
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Head050ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);var pose=new AtomicBoolean(false);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(pose.get()&&m.player!=null){m.player.setYHeadRot(0);m.player.yHeadRotO=0;m.player.setYBodyRot(0);m.player.yBodyRotO=0;}});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b,"sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{
|
||||
s.setName("Head050 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(true);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
for(String locale:new String[]{"en_us","fr_fr"})c.runOnClient(m->{
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for(var v:CrewBoats.variants()){
|
||||
var boat=v.type().create(m.level,EntitySpawnReason.COMMAND);
|
||||
require(m.getEntityRenderDispatcher().getRenderer(boat)!=null,"Native renderer for "+v.wood());
|
||||
var mesh=TiledBoatModel.hull(v.columns(),v.rows(),v.raft()).bakeRoot();
|
||||
require(mesh.getAllParts().size()==v.tiles()+3+(v.raft()?0:2*(v.columns()+v.rows())+4),"One floor per cell, only exterior rails and two paddles");
|
||||
require(mesh.getAllParts().stream().allMatch(part->part.xScale==1&&part.yScale==1&&part.zScale==1),"No stretched model part");
|
||||
require(mesh.hasChild("left_paddle")&&mesh.hasChild("right_paddle"),"Exactly one native pair of paddles");
|
||||
require(TiledBoatModel.waterMask(v.columns(),v.rows()).bakeRoot().getAllParts().size()==v.tiles()+1,"Water mask covers every floor tile");
|
||||
require(!new ItemStack(v.item()).getHoverName().getString().startsWith("item.sanctuary"),"Localized item name "+locale);
|
||||
}
|
||||
});
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.getInventory().clearContent();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
p.setPos(p.position().add(0,2,0));
|
||||
});
|
||||
c.runOnClient(m->m.options.setCameraType(CameraType.THIRD_PERSON_FRONT));c.getInput().pressKey(InputConstants.KEY_F1);c.getInput().lookAt(45,-25);pose.set(true);
|
||||
for(var item:List.of(Items.OAK_SHELF,Items.LECTERN,Items.BED.red(),Items.STONE_BUTTON,Items.LEVER,Items.FLOWER_POT,Items.CHISELED_BOOKSHELF)){
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(item));
|
||||
if(item==Items.OAK_SHELF)for(var contents:List.of(Items.DIAMOND,Items.POPPY,Items.GOLD_INGOT)){
|
||||
p.inventoryMenu.setCarried(new ItemStack(contents,3));require(HeadBlockService.use(p,p,true),"Insert real shelf item");
|
||||
}
|
||||
else if(item==Items.LECTERN){p.inventoryMenu.setCarried(new ItemStack(Items.WRITABLE_BOOK));require(HeadBlockService.use(p,p,true),"Insert real lectern book");}
|
||||
else if(item==Items.FLOWER_POT){p.inventoryMenu.setCarried(new ItemStack(Items.POPPY));require(HeadBlockService.use(p,p,true),"Plant real flower");}
|
||||
else if(item==Items.CHISELED_BOOKSHELF){p.inventoryMenu.setCarried(new ItemStack(Items.ENCHANTED_BOOK));require(HeadBlockService.use(p,p,true),"Insert enchanted book");}
|
||||
else if(item==Items.STONE_BUTTON||item==Items.LEVER)HeadBlockService.use(p,p,false);
|
||||
p.level().getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,p.getBoundingBox().inflate(8)).forEach(Entity::discard);
|
||||
});
|
||||
c.waitTicks(12);
|
||||
c.runOnClient(m->{
|
||||
var h=head(m);var state=HeadCosmeticData.state(HeadCosmeticService.appearance(m.player));
|
||||
if(item==Items.OAK_SHELF)require(h.blockEntity instanceof ShelfRenderState shelf&&Arrays.stream(shelf.items).filter(Objects::nonNull).count()==3,"All three shelf items reach actual native renderer");
|
||||
if(item==Items.LECTERN)require(h.blockEntity instanceof LecternRenderState book&&book.hasBook,"Lectern renderer shows its book");
|
||||
if(item==Items.BED.red())require(!h.block.isEmpty()&&!h.second.isEmpty()&&Math.abs(h.secondZ)+Math.abs(h.secondX)==1,"Complete bed has two adjoining native models");
|
||||
if(item==Items.STONE_BUTTON||item==Items.LEVER)require(h.baseY>=1/16F&&state.getValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.ATTACH_FACE)==net.minecraft.world.level.block.state.properties.AttachFace.FLOOR,"Switch lies on top");
|
||||
if(item==Items.FLOWER_POT)require(state.is(net.minecraft.world.level.block.Blocks.POTTED_POPPY),"Potted flower reaches display");
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta050-head-"+net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(item).getPath());
|
||||
}
|
||||
pose.set(false);
|
||||
server.runOnServer(s->AccessoryAccess.of(s.getPlayerList().getPlayers().getFirst()).clearContent());
|
||||
c.runOnClient(m->m.options.setCameraType(CameraType.THIRD_PERSON_BACK));
|
||||
for(var shape:List.of(new CrewBoats.Shape(1,2),new CrewBoats.Shape(2,1))){
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var old=p.getVehicle();p.stopRiding();if(old!=null)old.getSelfAndPassengers().toList().reversed().forEach(Entity::discard);
|
||||
var boat=CrewBoats.variant("oak",shape.columns(),shape.rows()).type().create(s.overworld(),EntitySpawnReason.COMMAND);boat.setPos(p.position().add(0,8,0));s.overworld().addFreshEntity(boat);
|
||||
var motor=EntityTypes.ALLAY.create(s.overworld(),EntitySpawnReason.COMMAND);motor.setPos(boat.position());motor.setNoAi(true);s.overworld().addFreshEntity(motor);motor.startRiding(boat);p.startRiding(boat,true,true);
|
||||
});
|
||||
c.waitFor(m->m.player.getVehicle() instanceof CrewBoat b&&b.columns()==shape.columns()&&b.rows()==shape.rows(),200);
|
||||
c.getInput().holdKey(o->o.keyJump);c.getInput().holdKey(o->o.keyUp);c.waitTicks(20);
|
||||
Vec3 from=c.computeOnClient(m->m.player.getVehicle().position());c.waitTicks(40);Vec3 to=c.computeOnClient(m->m.player.getVehicle().position());
|
||||
require(to.y>from.y+2&&to.subtract(from).horizontalDistance()>5,"Each rectangular boat flies from native pilot inputs");
|
||||
c.getInput().lookAt(25,20);c.waitTicks(5);c.takeScreenshot("sanctuary-beta050-boat-"+shape.columns()+"x"+shape.rows());
|
||||
c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyJump);
|
||||
}
|
||||
System.out.println("HEAD050_CLIENT_PASS: 88 tiled models FR/EN, shelf contents, lectern book, complete bed, switches on crown, potted flower, enchanted books and two new rectangular flights");
|
||||
}
|
||||
}
|
||||
private static HeadCosmeticState head(net.minecraft.client.Minecraft m){
|
||||
var renderer=m.getEntityRenderDispatcher().getRenderer(m.player);
|
||||
var state=renderer.createRenderState(m.player,1);
|
||||
return ((HeadCosmeticState.Access)state).sanctuary$headCosmetic();
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static boolean key(Button button,String key){return button.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean yes,String message){if(!yes)throw new AssertionError(message);}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.Container;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
public class HeadBlocks043ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getString().startsWith("Enter Sanctuary")).findFirst().orElseThrow().onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Head blocks043 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(true);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressRecord.INITIAL;for(int i=1;i<6;i++)r=r.buy("inventory",i);p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.CHEST));var head=HeadBlockService.session(p);HeadBlockService.scoped(head,()->{((Container)head.entity).setItem(0,new ItemStack(Items.DIAMOND,11));return null;});});
|
||||
c.waitTicks(5);c.runOnClient(m->{m.gui.setScreen(new InventoryScreen(m.player));ClientPlayNetworking.send(new HeadCosmeticPayloads.Interact(-1));});
|
||||
c.waitFor(m->m.gui.screen() instanceof ContainerScreen,150);c.waitTicks(5);c.takeScreenshot("sanctuary-beta043-head-chest-six-rows");
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,0,0,ContainerInput.QUICK_MOVE,m.player));
|
||||
c.waitFor(m->m.player.getInventory().countItem(Items.DIAMOND)==11,150);
|
||||
c.waitTicks(10);
|
||||
require(server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getInventory().countItem(Items.DIAMOND)==11),"Native client shift-click reaches server exactly once");
|
||||
server.runOnServer(s->AccessoryAccess.of(s.getPlayerList().getPlayers().getFirst()).removeItemNoUpdate(AccessoryContainer.HEAD));
|
||||
c.waitFor(m->m.player.containerMenu==m.player.inventoryMenu,150);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.FURNACE));HeadBlockService.use(p,p,false);});
|
||||
c.waitFor(m->m.gui.screen() instanceof FurnaceScreen,150);c.waitTicks(5);c.takeScreenshot("sanctuary-beta043-head-furnace");c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.COMMAND_BLOCK));require(HeadBlockService.use(p,p,false),"Native operator editor requested");});
|
||||
c.waitFor(m->m.gui.screen() instanceof CommandBlockEditScreen,150);c.waitTicks(4);c.takeScreenshot("sanctuary-beta043-head-operator-editor");c.getInput().pressKey(InputConstants.KEY_ESCAPE);
|
||||
System.out.println("HEAD043_CLIENT_PASS: inventory gesture, six rows, real shift-click packet, removal closes menu, furnace and native operator editor");
|
||||
}
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean yes,String message){if(!yes)throw new AssertionError(message);}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.*;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.client.renderer.blockentity.state.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.*;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.network.protocol.game.*;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.server.network.*;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class HeadCosmetic021ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
int scale=c.computeOnClient(m->m.options.guiScale().get());
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary beta021 cosmetics "+UUID.randomUUID());s.setSeed("42");
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
UUID remote=server.computeOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().above(6);
|
||||
for(int x=-8;x<=8;x++)for(int z=-8;z<=8;z++)for(int y=-1;y<9;y++)p.level().setBlockAndUpdate(at.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
p.teleportTo(at.getX()+.5,at.getY(),at.getZ()+.5);p.getInventory().clearContent();
|
||||
var profile=new GameProfile(UUID.randomUUID(),"Porteur");
|
||||
var other=new ServerPlayer(s,p.level(),profile,ClientInformation.createDefault());
|
||||
var connection=new Connection(PacketFlow.SERVERBOUND);new io.netty.channel.embedded.EmbeddedChannel(connection);
|
||||
other.connection=new ServerGamePacketListenerImpl(s,connection,other,CommonListenerCookie.createInitial(profile,false));
|
||||
other.setPos(p.position().add(0,0,2.5));other.setYRot(180);other.setYHeadRot(180);other.setNoGravity(true);
|
||||
other.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(other,ProgressRecord.INITIAL);
|
||||
other.connection.handleAcceptPlayerLoad(new ServerboundPlayerLoadedPacket());
|
||||
p.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(other)));p.level().addNewPlayer(other);
|
||||
equip(other,new ItemStack(Items.OAK_SIGN));return other.getUUID();
|
||||
});
|
||||
c.waitFor(m->m.level.getPlayerByUUID(remote)!=null&&HeadCosmeticService.appearance(m.level.getPlayerByUUID(remote)).is(Items.OAK_SIGN),240);
|
||||
var aim=c.computeOnClient(m->m.level.getPlayerByUUID(remote).getEyePosition().add(0,.45,0));face(c,aim);c.waitTicks(4);
|
||||
click(c);c.waitFor(m->m.gui.screen() instanceof AbstractSignEditScreen,100);
|
||||
c.runOnClient(m->{for(char ch:"Bonjour!".toCharArray())m.gui.screen().charTyped(new CharacterEvent(ch));});
|
||||
c.takeScreenshot("sanctuary-beta021-sign-editor");c.getInput().pressKey(InputConstants.KEY_ESCAPE);
|
||||
c.waitFor(m->{var stack=HeadCosmeticService.appearance(m.level.getPlayerByUUID(remote));return List.of(DataComponents.SIGN_TEXT_FRONT,DataComponents.SIGN_TEXT_BACK).stream().anyMatch(k->stack.getOrDefault(k,SignText.EMPTY).getMessages(false).getFirst().getString().equals("Bonjour!"));},200);
|
||||
c.runOnClient(m->{var h=render(m,m.level.getPlayerByUUID(remote));require(h.blockEntity instanceof SignRenderState&&!h.block.isEmpty(),"Sign has native board and native text render state");
|
||||
var sign=(SignRenderState)h.blockEntity;require(Stream.concat(sign.frontText.getMessages(false).stream(),sign.backText.getMessages(false).stream()).anyMatch(t->t.getString().equals("Bonjour!")),"Written text reaches native renderer");});
|
||||
c.waitTicks(3);c.takeScreenshot("sanctuary-beta021-sign-written");
|
||||
server.runOnServer(s->{var p=(ServerPlayer)s.getPlayerList().getPlayers().getFirst().level().getPlayerByUUID(remote);equip(p,new ItemStack(Items.OAK_HANGING_SIGN));});
|
||||
c.waitFor(m->HeadCosmeticService.appearance(m.level.getPlayerByUUID(remote)).is(Items.OAK_HANGING_SIGN),100);c.waitTicks(5);click(c);
|
||||
c.waitFor(m->m.gui.screen() instanceof HangingSignEditScreen,100);c.runOnClient(m->{for(char ch:"Suspendu".toCharArray())m.gui.screen().charTyped(new CharacterEvent(ch));});c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(6);
|
||||
// Native item models and components are tested through actual server synchronization and render extraction.
|
||||
List<Item> models=new ArrayList<>(List.of(Items.OAK_DOOR,Items.IRON_DOOR,Items.BED.red(),Items.STRAW_BED,
|
||||
Items.MINECART,Items.CHEST_MINECART,Items.HOPPER_MINECART,Items.FURNACE_MINECART,Items.TNT_MINECART,Items.COMMAND_BLOCK_MINECART,
|
||||
Items.COBWEB,Items.OAK_LEAVES,Items.GLASS,Items.STAINED_GLASS.purple(),Items.TINTED_GLASS,Items.COPPER_GOLEM_STATUE.waxed().oxidized(),Items.CAKE,Items.PISTON,Items.COW_SPAWN_EGG));
|
||||
models.addAll(Items.BANNER.asList());
|
||||
for(Item item:models) {
|
||||
server.runOnServer(s->equip((ServerPlayer)s.getPlayerList().getPlayers().getFirst().level().getPlayerByUUID(remote),new ItemStack(item)));
|
||||
c.waitFor(m->HeadCosmeticService.appearance(m.level.getPlayerByUUID(remote)).is(item),100);
|
||||
c.runOnClient(m->{var h=render(m,m.level.getPlayerByUUID(remote));var state=HeadCosmeticData.state(new ItemStack(item));
|
||||
if(state!=null&&(state.getBlock() instanceof DoorBlock||state.getBlock() instanceof BedBlock))require(!h.block.isEmpty()&&!h.second.isEmpty(),"Both halves render: "+item);
|
||||
if(item instanceof MinecartItem)require(h.entity!=null&&h.item.isEmpty(),"Minecart uses entity model: "+item);
|
||||
if(state!=null&&state.getBlock() instanceof AbstractBannerBlock)require(h.blockEntity instanceof BannerRenderState b&&b.baseColor==((BannerItem)item).getColor(),"Banner keeps native base color: "+item);
|
||||
if(HeadCosmeticData.wrapsHead(state))require(h.wraps,"Block encloses head: "+item);
|
||||
if(item instanceof SpawnEggItem)require(h.entity==null&&!h.item.isEmpty(),"Egg retains existing item rendering");
|
||||
});c.waitTicks(2);
|
||||
if(List.of(Items.OAK_DOOR,Items.BED.red(),Items.MINECART,Items.COBWEB,Items.OAK_LEAVES,Items.GLASS,Items.CAKE).contains(item))c.takeScreenshot("sanctuary-beta021-"+net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(item).getPath());
|
||||
}
|
||||
server.runOnServer(s->{var p=(ServerPlayer)s.getPlayerList().getPlayers().getFirst().level().getPlayerByUUID(remote);equip(p,net.minecraft.world.entity.raid.Raid.getOminousBannerInstance(p.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN)));});
|
||||
c.waitFor(m->HeadCosmeticService.appearance(m.level.getPlayerByUUID(remote)).getOrDefault(DataComponents.BANNER_PATTERNS,BannerPatternLayers.EMPTY).layers().size()>0,100);
|
||||
c.runOnClient(m->{var p=m.level.getPlayerByUUID(remote);var b=(BannerRenderState)render(m,p).blockEntity;
|
||||
require(b.patterns.equals(HeadCosmeticService.appearance(p).get(DataComponents.BANNER_PATTERNS))&&!b.patterns.layers().isEmpty(),"Ominous banner keeps every native pattern");});
|
||||
c.waitTicks(3);c.takeScreenshot("sanctuary-beta021-ominous-banner");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();equip(p,new ItemStack(Items.CAKE));
|
||||
p.getInventory().setItem(0,new ItemStack(Items.DYED_CANDLE.blue(),2));p.getInventory().setItem(1,new ItemStack(Items.FLINT_AND_STEEL));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->HeadCosmeticService.appearance(m.player).is(Items.CAKE)&&m.player.getInventory().getItem(0).is(Items.DYED_CANDLE.blue()),100);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new InventoryScreen(m.player));
|
||||
require(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->b.getMessage().getString().equalsIgnoreCase("Cosmetic")),"No large cosmetic button");});
|
||||
clickSlot(c,36,0);c.takeScreenshot("sanctuary-beta021-cursor-candle");c.waitFor(m->m.player.inventoryMenu.getCarried().is(Items.DYED_CANDLE.blue()),100);
|
||||
clickSlot(c,66,1);c.waitFor(m->HeadCosmeticData.candle(HeadCosmeticService.appearance(m.player))==Items.DYED_CANDLE.blue()&&m.player.inventoryMenu.getCarried().getCount()==1,100);
|
||||
clickSlot(c,36,0);clickSlot(c,37,0);c.waitFor(m->m.player.inventoryMenu.getCarried().is(Items.FLINT_AND_STEEL),100);
|
||||
clickSlot(c,66,1);c.waitFor(m->HeadCosmeticData.lit(HeadCosmeticService.appearance(m.player))&&m.player.inventoryMenu.getCarried().getDamageValue()==1,100);
|
||||
clickSlot(c,37,0);c.waitFor(m->m.player.inventoryMenu.getCarried().isEmpty(),100);
|
||||
c.takeScreenshot("sanctuary-beta021-inventory-candle");
|
||||
for(int slot:new int[]{5,6,7,8,45,64,65,66}){hover(c,slot);c.takeScreenshot("sanctuary-beta021-slot-"+slot);}
|
||||
clickSlot(c,66,1);c.waitFor(m->!HeadCosmeticData.lit(HeadCosmeticService.appearance(m.player)),100);
|
||||
c.waitTicks(5);clickSlot(c,66,1);c.waitFor(m->HeadCosmeticData.candle(HeadCosmeticService.appearance(m.player))==Items.AIR&&m.player.inventoryMenu.getCarried().is(Items.DYED_CANDLE.blue()),100);
|
||||
clickSlot(c,36,0);clickSlot(c,66,0);c.waitFor(m->m.player.inventoryMenu.getCarried().is(Items.CAKE)&&HeadCosmeticService.appearance(m.player).isEmpty(),100);
|
||||
clickSlot(c,38,0);c.waitFor(m->m.player.inventoryMenu.getCarried().isEmpty(),100);
|
||||
server.runOnServer(s->equip(s.getPlayerList().getPlayers().getFirst(),new ItemStack(Items.OAK_SIGN)));c.waitFor(m->HeadCosmeticService.appearance(m.player).is(Items.OAK_SIGN),100);c.waitTicks(3);
|
||||
clickSlot(c,66,1);c.waitFor(m->m.player.inventoryMenu.getCarried().is(Items.OAK_SIGN)&&m.gui.screen() instanceof InventoryScreen,100);
|
||||
clickSlot(c,39,0);c.waitFor(m->m.player.inventoryMenu.getCarried().isEmpty(),100);
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var other=(ServerPlayer)p.level().getPlayerByUUID(remote);
|
||||
equip(other,new ItemStack(Items.TNT));other.setHealth(6);other.hurtServer(other.level(),other.damageSources().generic(),1);
|
||||
});
|
||||
c.waitFor(m->{for(var entity:m.level.entitiesForRendering())if(entity instanceof net.minecraft.world.entity.item.PrimedTnt)return true;return false;},80);
|
||||
c.waitTicks(3);c.takeScreenshot("sanctuary-beta021-tnt-primed");
|
||||
// Move the observer out of the explosion; the armed avatar and native terrain remain in the laboratory.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(p.getX()+12,p.getY(),p.getZ());});
|
||||
c.waitTicks(90);
|
||||
c.runOnClient(m->{for(var entity:m.level.entitiesForRendering())require(!(entity instanceof net.minecraft.world.entity.item.PrimedTnt),"Native TNT completed its fuse and disappeared after explosion");});
|
||||
System.out.println("HeadCosmetic021 client passed: actual remote sign/hanging-sign clicks and text packets, 16 banner colors and ominous patterns, complete beds/doors, six minecarts, wrapping blocks, unchanged egg, inventory cake/candle actions and own-sign exclusion.");
|
||||
} finally {onboarding.set(false);c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();});}
|
||||
}
|
||||
private static HeadCosmeticState render(Minecraft m,Player p){return ((HeadCosmeticState.Access)m.getEntityRenderDispatcher().extractEntity(p,.5F)).sanctuary$headCosmetic();}
|
||||
private static void equip(ServerPlayer p,ItemStack item){AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,item);}
|
||||
private static void face(ClientGameTestContext c,Vec3 at){c.runOnClient(m->{var d=at.subtract(m.player.getEyePosition());m.player.setYRot((float)Math.toDegrees(Math.atan2(-d.x,d.z)));m.player.setXRot((float)-Math.toDegrees(Math.atan2(d.y,d.horizontalDistance())));});}
|
||||
private static void hover(ClientGameTestContext c,int index){c.runOnClient(m->{var screen=(AbstractContainerScreen<?>)m.gui.screen();var a=(fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor)screen;var slot=screen.getMenu().getSlot(index);var w=m.getWindow();double x=a.sanctuary$getLeftPos()+slot.x+8.0,y=a.sanctuary$getTopPos()+slot.y+8.0;double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();});c.waitTicks(2);}
|
||||
private static void clickSlot(ClientGameTestContext c,int slot,int button){hover(c,slot);c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(button==0?InputConstants.MOUSE_BUTTON_LEFT:InputConstants.MOUSE_BUTTON_RIGHT,0),1));c.waitTick();c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(button==0?InputConstants.MOUSE_BUTTON_LEFT:InputConstants.MOUSE_BUTTON_RIGHT,0),0));c.waitTicks(3);}
|
||||
private static void click(ClientGameTestContext c){c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_RIGHT,0),1));c.waitTick();c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_RIGHT,0),0));c.waitTick();}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?Stream.concat(Stream.of(w),widgets(nested)):Stream.of(w));}
|
||||
private static Button button(Screen screen,String key){return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(w->w.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow(()->new AssertionError("Missing "+key));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean value,String text){if(!value)throw new AssertionError(text);}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
/** Native screen/packet checks; exclusively creates an ignored development world. */
|
||||
public final class Inventory010ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if(armed.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
var press=new KeyEvent(InputConstants.KEY_RETURN,0,0);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(press);
|
||||
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(press);armed.set(false);
|
||||
}
|
||||
});
|
||||
int scale=c.computeOnClient(m -> m.options.guiScale().get());
|
||||
try(var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Sanctuary beta012 inventory "+UUID.randomUUID());
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m->m.player!=null && ProgressionClient.active() && m.gui.screen()==null,1200);
|
||||
for(int rank=0;rank<=5;rank++) {
|
||||
if(rank>0) world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.giveExperienceLevels(16);
|
||||
require(ProgressionService.purchase(p,"inventory",ProgressEffects.get(p).revision()),"Server purchase succeeds");ProgressionService.send(p,"");
|
||||
});
|
||||
int expected=rank;c.waitFor(m->ProgressionClient.progress.inventory()==expected,200);
|
||||
for(int gui:new int[]{1,0}) {
|
||||
c.runOnClient(m -> {m.options.guiScale().set(gui);m.resizeGui();m.gui.setScreen(new InventoryScreen(m.player));bounds((AbstractContainerScreen<?>)m.gui.screen());});
|
||||
c.waitTick();c.takeScreenshot("sanctuary-beta012-inventory-"+(rank+1)+"-gui-"+gui);
|
||||
c.runOnClient(m->m.player.closeContainer());
|
||||
}
|
||||
}
|
||||
for(var type:BuiltInRegistries.MENU) {
|
||||
boolean supported=world.getServer().computeOnServer(server -> {
|
||||
var p=server.getPlayerList().getPlayers().getFirst();
|
||||
var ext=(SanctuaryExtendedInventoryMenu)type.create(99,p.getInventory());ext.sanctuary$ensureExtendedInventory();
|
||||
return ext.sanctuary$hasExtendedInventory();
|
||||
});
|
||||
if(!supported)continue;
|
||||
c.runOnClient(m -> Inventory012RenderProbe.reset());
|
||||
String name=BuiltInRegistries.MENU.getKey(type).getPath();
|
||||
world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.openMenu(new SimpleMenuProvider((id,inv,owner)->type.create(id,inv),Component.literal(name)));
|
||||
if(p.containerMenu instanceof ChestMenu chest) {
|
||||
for(int i=0;i<chest.getRowCount()*9;i++) chest.getSlot(i).setByPlayer(new ItemStack(i%2==0 ? Items.WHEAT : Items.COAL,i%5+1));
|
||||
chest.broadcastChanges();
|
||||
}
|
||||
});
|
||||
c.waitFor(m->m.gui.screen() instanceof AbstractContainerScreen<?> screen && screen.getMenu().getType()==type,200);
|
||||
c.runOnClient(m -> {
|
||||
var screen=(AbstractContainerScreen<?>)m.gui.screen();bounds(screen);
|
||||
var ext=(SanctuaryExtendedInventoryMenu)screen.getMenu();
|
||||
if(ext.sanctuary$hasExtendedInventory()) {
|
||||
var canvas=(fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor)screen;
|
||||
screen.mouseScrolled(canvas.sanctuary$getLeftPos()+88,screen.height-30,0,10);
|
||||
bounds(screen);
|
||||
}
|
||||
});
|
||||
c.waitTick();c.takeScreenshot("sanctuary-beta012-"+name+"-minimum");
|
||||
c.runOnClient(m -> {
|
||||
var canvas=(fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor)m.gui.screen();
|
||||
require(Inventory012RenderProbe.captured && Inventory012RenderProbe.x==canvas.sanctuary$getLeftPos()
|
||||
&& Inventory012RenderProbe.y==canvas.sanctuary$getTopPos(),"Native background origin matches actual slot origin: "+name);
|
||||
});
|
||||
c.runOnClient(m->m.player.closeContainer());c.waitForScreen(null);
|
||||
}
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(60,new ItemStack(Items.DIAMOND,23));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.getInventory().getItem(60).getCount()==23,200);
|
||||
c.runOnClient(m->{m.gui.setScreen(new InventoryScreen(m.player));bounds((AbstractContainerScreen<?>)m.gui.screen());});
|
||||
c.takeScreenshot("sanctuary-beta012-last-row-synced");
|
||||
c.getInput().pressKey(InputConstants.KEY_TAB);
|
||||
c.waitFor(m->m.player.getInventory().getItem(60).isEmpty() && m.player.getInventory().getItem(51).getCount()==23,200);
|
||||
c.runOnClient(m -> {
|
||||
var screen=m.gui.screen();
|
||||
var backwards=new KeyEvent(InputConstants.KEY_TAB,0,1);
|
||||
net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents.allowKeyPress(screen).invoker().allowKeyPress(screen,backwards);
|
||||
net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents.afterKeyRelease(screen).invoker().afterKeyRelease(screen,backwards);
|
||||
});
|
||||
c.waitFor(m->m.player.getInventory().getItem(60).getCount()==23,200);
|
||||
c.runOnClient(m -> {
|
||||
var key=net.minecraft.client.KeyMapping.get("screen.sanctuary.inventory.rotate_key");
|
||||
key.setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_E));net.minecraft.client.KeyMapping.resetMapping();
|
||||
});
|
||||
c.getInput().pressKey(InputConstants.KEY_E);
|
||||
c.waitFor(m->m.gui.screen() instanceof InventoryScreen && m.player.getInventory().getItem(51).getCount()==23,200);
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);
|
||||
c.runOnClient(m -> {
|
||||
var key=net.minecraft.client.KeyMapping.get("screen.sanctuary.inventory.rotate_key");key.setKey(key.getDefaultKey());net.minecraft.client.KeyMapping.resetMapping();
|
||||
});
|
||||
world.getServer().runOnServer(server -> {var p=server.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(0);ProgressionService.send(p,"");});
|
||||
c.runOnClient(m -> ProgressionClient.open(null));c.waitTick();
|
||||
c.runOnClient(m -> require(buttons(m.gui.screen())
|
||||
.noneMatch(b -> b.visible && b.getMessage().getString().equals("+")),"No unaffordable plus button"));
|
||||
c.takeScreenshot("sanctuary-beta012-progression-no-xp");
|
||||
world.getServer().runOnServer(server -> {
|
||||
var p=server.getPlayerList().getPlayers().getFirst();server.getPlayerList().op(p.nameAndId());ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.operator,200);
|
||||
world.getServer().runOnServer(server -> {var p=server.getPlayerList().getPlayers().getFirst();p.giveExperienceLevels(30);ProgressionService.send(p,"");});
|
||||
c.waitFor(m -> m.player.experienceLevel==30,200);c.waitTick();c.takeScreenshot("sanctuary-beta012-progression-ready");
|
||||
c.runOnClient(m -> buttons(m.gui.screen())
|
||||
.filter(b -> b.visible && b.active && b.getMessage().getString().equals("+")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0)));
|
||||
c.waitFor(m -> ProgressionClient.progress.vitality()==1 && m.player.experienceLevel==29,200);
|
||||
c.takeScreenshot("sanctuary-beta012-progression-operator");
|
||||
c.runOnClient(m -> buttons(m.gui.screen())
|
||||
.filter(b -> b.visible && b.active && b.getMessage().getString().equals("−")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0)));
|
||||
c.waitFor(m -> ProgressionClient.progress.vitality()==0 && m.player.experienceLevel==30,200);
|
||||
c.runOnClient(m -> require(ProgressionClient.progress.history().getLast().target().equals("remove_vitality"),"Native UI refund is recorded by the server"));
|
||||
|
||||
c.runOnClient(m->m.player.closeContainer());
|
||||
} finally {armed.set(false);c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();});}
|
||||
}
|
||||
private static java.util.stream.Stream<Button> buttons(net.minecraft.client.gui.components.events.GuiEventListener widget) {
|
||||
if(widget instanceof Button button)return java.util.stream.Stream.of(button);
|
||||
if(widget instanceof net.minecraft.client.gui.components.events.ContainerEventHandler parent)
|
||||
return parent.children().stream().flatMap(Inventory010ClientChecks::buttons);
|
||||
return java.util.stream.Stream.empty();
|
||||
}
|
||||
private static void bounds(AbstractContainerScreen<?> screen) {
|
||||
var canvas=(fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor)screen;
|
||||
for(var slot:screen.getMenu().slots) if(slot.isActive() && slot.x>-1000 && slot.y>-1000) {
|
||||
int x=canvas.sanctuary$getLeftPos()+slot.x,y=canvas.sanctuary$getTopPos()+slot.y;
|
||||
require(x>=0 && y>=0 && x+16<=screen.width && y+16<=screen.height,"Visible slot stays on screen: "+screen.getClass().getSimpleName()+" #"+slot.index+" at "+x+","+y);
|
||||
}
|
||||
}
|
||||
private static void require(boolean condition,String message){if(!condition)throw new AssertionError(message);}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.ProgressionClient;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.gametest.mixin.Inventory013MerchantAccess;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.inventory.MerchantMenu;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.trading.*;
|
||||
|
||||
/** Native wheel dispatch distinguishes inventory rows, selected slots and merchant offers. */
|
||||
final class Inventory013WheelChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server) {
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(100);
|
||||
while(ProgressEffects.get(p).inventory()<5)require(ProgressionService.purchase(p,"inventory",ProgressEffects.get(p).revision()),"Unlock all wheel rows");
|
||||
p.getInventory().clearContent();
|
||||
int[] slots={0,9,18,27,43,52};Item[] items={Items.STONE,Items.DIRT,Items.COAL,Items.GOLD_INGOT,Items.IRON_INGOT,Items.DIAMOND};
|
||||
for(int i=0;i<slots.length;i++)p.getInventory().setItem(slots[i],new ItemStack(items[i],i+1));
|
||||
p.getInventory().setSelectedSlot(0);p.inventoryMenu.broadcastChanges();ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.inventory()==5 && m.player.getInventory().getItem(52).is(Items.DIAMOND),200);
|
||||
c.runOnClient(m -> {m.gui.setScreen(new InventoryScreen(m.player));overSlot(m,0,-1);});
|
||||
c.waitFor(m -> m.player.getInventory().getSelectedSlot()==1,200);
|
||||
// Selection is predicted locally; wait for the native packet before checking the server.
|
||||
c.waitTicks(3);
|
||||
require(server.computeOnServer(s -> s.getPlayerList().getPlayers().getFirst().getInventory().getSelectedSlot()==1),"Hotbar wheel selection reaches the server");
|
||||
c.runOnClient(m -> {require(m.player.getInventory().getItem(0).is(Items.STONE),"Hotbar wheel never rotates rows");overSlot(m,9,-1);});
|
||||
c.waitFor(m -> m.player.getInventory().getItem(0).is(Items.DIRT),200);c.waitTick();
|
||||
c.runOnClient(m -> overSlot(m,9,1));
|
||||
c.waitFor(m -> m.player.getInventory().getItem(0).is(Items.STONE),200);c.waitTick();
|
||||
c.runOnClient(m -> overSlot(m,9,-2));
|
||||
c.waitFor(m -> m.player.getInventory().getItem(0).is(Items.COAL),200);c.waitTick();
|
||||
c.runOnClient(m -> overSlot(m,9,2));
|
||||
c.waitFor(m -> m.player.getInventory().getItem(0).is(Items.STONE),200);c.waitTick();
|
||||
c.getInput().pressKey(InputConstants.KEY_TAB);
|
||||
c.waitFor(m -> m.player.getInventory().getItem(0).is(Items.DIAMOND),200);c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
var screen=m.gui.screen();var reverse=new KeyEvent(InputConstants.KEY_TAB,0,1);
|
||||
net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents.allowKeyPress(screen).invoker().allowKeyPress(screen,reverse);
|
||||
net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents.afterKeyRelease(screen).invoker().afterKeyRelease(screen,reverse);
|
||||
});
|
||||
c.waitFor(m -> m.player.getInventory().getItem(0).is(Items.STONE),200);c.waitTick();
|
||||
c.runOnClient(m -> m.player.closeContainer());
|
||||
// Let the close packet reach the server before opening the next menu.
|
||||
c.waitTicks(3);
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();
|
||||
p.openMenu(new SimpleMenuProvider((id,inv,owner)->MenuType.MERCHANT.create(id,inv),Component.literal("Villager")));
|
||||
var offers=new MerchantOffers();
|
||||
for(int i=0;i<20;i++)offers.add(new MerchantOffer(new ItemCost(Items.EMERALD,i+1),new ItemStack(Items.BREAD,i+1),12,1,0.05F));
|
||||
((MerchantMenu)p.containerMenu).setOffers(offers);p.sendMerchantOffers(p.containerMenu.containerId,offers,1,0,false,false);
|
||||
require(p.containerMenu.stillValid(p),"Merchant fixture stays valid for its player");
|
||||
});
|
||||
c.waitFor(m -> m.gui.screen() instanceof MerchantScreen screen && screen.getMenu().getOffers().size()==20,200);
|
||||
c.runOnClient(m -> {var canvas=(AbstractContainerScreenAccessor)m.gui.screen();scroll(m,canvas.sanctuary$getLeftPos()+20,canvas.sanctuary$getTopPos()+30,-1);});
|
||||
int offset=c.computeOnClient(m -> ((Inventory013MerchantAccess)m.gui.screen()).sanctuary$offerOffset());
|
||||
require(offset>0,"Wheel over merchant offers scrolls their native list");
|
||||
c.runOnClient(m -> {require(m.player.getInventory().getItem(0).is(Items.STONE),"Offers do not change hotbar rows");overSlot(m,9,-1);});
|
||||
c.waitFor(m -> m.player.getInventory().getItem(0).is(Items.DIRT),200);
|
||||
c.runOnClient(m -> {
|
||||
require(((Inventory013MerchantAccess)m.gui.screen()).sanctuary$offerOffset()==offset,"Wheel over player storage never scrolls offers");
|
||||
overSlot(m,0,-1);
|
||||
});
|
||||
c.waitFor(m -> m.player.getInventory().getSelectedSlot()==2,200);
|
||||
c.takeScreenshot("sanctuary-beta013-merchant-wheel-zones");
|
||||
c.runOnClient(m -> {require(((Inventory013MerchantAccess)m.gui.screen()).sanctuary$offerOffset()==offset,"Hotbar selection never scrolls offers");m.player.closeContainer();});
|
||||
}
|
||||
private static void overSlot(Minecraft m,int index,double amount) {
|
||||
var screen=(AbstractContainerScreen<?>)m.gui.screen();var canvas=(AbstractContainerScreenAccessor)screen;
|
||||
var slot=screen.getMenu().slots.stream().filter(s -> s.container==m.player.getInventory() && s.getContainerSlot()==index).findFirst().orElseThrow();
|
||||
scroll(m,canvas.sanctuary$getLeftPos()+slot.x+8,canvas.sanctuary$getTopPos()+slot.y+8,amount);
|
||||
}
|
||||
private static void scroll(Minecraft m,double x,double y,double amount) {
|
||||
var window=m.getWindow();
|
||||
m.mouseHandler.onMove(window.handle(),x*window.getScreenWidth()/window.getGuiScaledWidth(),y*window.getScreenHeight()/window.getGuiScaledHeight(),0,0);
|
||||
m.mouseHandler.handleAccumulatedMovement();m.mouseHandler.onScroll(window.handle(),0,amount);
|
||||
}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.cycle.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/** Actual UI purchase, middle-click packets and keyboard remapping in a fresh local test world. */
|
||||
public final class Inventory014ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if(armed.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);
|
||||
}
|
||||
});
|
||||
String language=c.computeOnClient(m -> m.getLanguageManager().getSelected());
|
||||
int gui=c.computeOnClient(m -> m.options.guiScale().get());
|
||||
var previousKey=c.computeOnClient(m -> InputConstants.getKey(KeyMapping.get("screen.sanctuary.inventory.sort_key").saveString()));
|
||||
try(var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Sanctuary beta014 sorting "+UUID.randomUUID());
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && ProgressionClient.active() && m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
c.runOnClient(m -> {KeyMapping.get("screen.sanctuary.inventory.sort_key").setKey(InputConstants.Type.MOUSE.getOrCreate(2));KeyMapping.resetMapping();});
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(3);ProgressionService.send(p,"");});
|
||||
c.waitFor(m -> m.player.experienceLevel==3,200);
|
||||
c.runOnClient(m -> {m.options.guiScale().set(1);m.resizeGui();ProgressionClient.open(null);});c.waitTick();
|
||||
c.runOnClient(m -> require(!sortingButton(m.gui.screen()).active,"Sorting aptitude cannot be bought with only three levels"));
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(4);ProgressionService.send(p,"");});
|
||||
c.waitFor(m -> m.player.experienceLevel==4,200);c.waitTick();
|
||||
for(String locale:new String[]{"en_us","fr_fr"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());ProgressionClient.open(null);
|
||||
});c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
require(sortingButton(m.gui.screen()).active,"Four levels enable the new aptitude in both languages");
|
||||
for(String aptitude:ProgressRecord.APTITUDES)require(!Component.translatable("sanctuary.progression."+aptitude+"_description").getString().contains("Game+"),"Persistence is a global rule, absent from aptitude descriptions");
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta014-aptitude-"+locale);
|
||||
}
|
||||
c.runOnClient(m -> sortingButton(m.gui.screen()).onPress(enter()));
|
||||
c.waitFor(m -> ProgressionClient.progress.sorting() && m.player.experienceLevel==0,200);
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressEffects.get(p);
|
||||
for(int i=0;i<5;i++)progress=progress.buy("inventory",1);
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);
|
||||
p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.STONE,5));p.getInventory().setItem(8,new ItemStack(Items.APPLE,2));
|
||||
p.getInventory().setItem(9,new ItemStack(Items.STONE,11));p.getInventory().setItem(43,new ItemStack(Items.COAL,40));p.getInventory().setItem(60,new ItemStack(Items.COAL,30));
|
||||
p.inventoryMenu.broadcastChanges();ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.inventory()==5 && m.player.getInventory().getItem(60).getCount()==30,200);
|
||||
c.runOnClient(m -> {m.gui.setScreen(new InventoryScreen(m.player));click(m,9,2);});
|
||||
c.waitFor(m -> m.player.getInventory().getItem(9).is(Items.COAL) && m.player.getInventory().getItem(9).getCount()==64,200);c.waitTicks(3);
|
||||
c.runOnClient(m -> {
|
||||
require(m.player.getInventory().getItem(0).is(Items.STONE) && m.player.getInventory().getItem(8).is(Items.APPLE),"Storage sort preserves arranged hotbar");
|
||||
click(m,0,2);
|
||||
});
|
||||
c.waitFor(m -> m.player.getInventory().getItem(0).is(Items.APPLE),200);c.waitTicks(3);
|
||||
c.takeScreenshot("sanctuary-beta014-middle-click-sorted");
|
||||
c.runOnClient(m -> m.player.closeContainer());c.waitTicks(3);
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(9,new ItemStack(Items.STONE,8));p.getInventory().setItem(60,new ItemStack(Items.APPLE,6));
|
||||
p.openMenu(new SimpleMenuProvider((id,inv,owner) -> MenuType.GENERIC_9x3.create(id,inv),Component.literal("Sorting chest")));
|
||||
p.containerMenu.getSlot(0).set(new ItemStack(Items.GOLD_INGOT,31));p.containerMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m -> m.gui.screen() instanceof ContainerScreen && m.player.getInventory().getItem(60).is(Items.APPLE),200);
|
||||
c.runOnClient(m -> {
|
||||
var key=KeyMapping.get("screen.sanctuary.inventory.sort_key");key.setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_E));KeyMapping.resetMapping();
|
||||
point(m,9);
|
||||
});
|
||||
c.getInput().pressKey(InputConstants.KEY_E);
|
||||
c.waitFor(m -> m.player.getInventory().getItem(9).is(Items.APPLE),200);c.waitTicks(3);
|
||||
c.runOnClient(m -> {
|
||||
require(m.gui.screen() instanceof ContainerScreen,"Remapped E sorts instead of closing the inventory");
|
||||
require(m.player.containerMenu.getSlot(0).getItem().is(Items.GOLD_INGOT) && m.player.containerMenu.getSlot(0).getItem().getCount()==31,"Opened chest contents are untouched");
|
||||
require(m.player.getInventory().getItem(0).is(Items.APPLE) && m.player.getInventory().getItem(0).getCount()==2,"Separate hotbar keeps its own apple count");
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta014-remapped-key-chest");
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitFor(m -> m.gui.screen()==null,200);
|
||||
require(server.computeOnServer(s -> s.getPlayerList().getPlayers().getFirst().getInventory().getItem(9).is(Items.APPLE)),"The final sorted inventory is authoritative on the server");
|
||||
mobileHotbar(c,server);
|
||||
factionProfile(c,server);
|
||||
} finally {
|
||||
armed.set(false);c.runOnClient(m -> {
|
||||
KeyMapping.get("screen.sanctuary.inventory.sort_key").setKey(previousKey);KeyMapping.resetMapping();
|
||||
m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(gui);m.resizeGui();
|
||||
});
|
||||
}
|
||||
}
|
||||
private static void mobileHotbar(ClientGameTestContext c,TestServerContext server) {
|
||||
c.waitTicks(3);
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();
|
||||
for(int i=0;i<54;i++) {
|
||||
var stack=new ItemStack(Items.STONE,1+i%4);
|
||||
stack.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,Component.literal("fixed-"+i));
|
||||
p.getInventory().setItem(HotbarRows.address(i),stack);
|
||||
}
|
||||
p.getInventory().setSelectedSlot(0);p.inventoryMenu.broadcastFullState();ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> m.player.getInventory().getItem(60).getHoverName().getString().equals("fixed-53"),200);
|
||||
c.runOnClient(m -> m.gui.setScreen(new InventoryScreen(m.player)));c.waitTick();
|
||||
var fixed=c.computeOnClient(Inventory014ClientChecks::grid);
|
||||
int baseY=c.computeOnClient(m -> nativeSlot(m,0).y);
|
||||
c.takeScreenshot("sanctuary-beta014-hotbar-base");
|
||||
for(int step=1;step<=6;step++) {
|
||||
int expected=Math.floorMod(-step,6);
|
||||
c.getInput().pressKey(InputConstants.KEY_TAB);
|
||||
c.waitFor(m -> HotbarRows.selected(m.player)==expected,200);c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
require(fixed.equals(grid(m)),"Tab moves only the frame: all 54 item coordinates remain exact at row "+expected);
|
||||
require(m.player.getInventory().getItem(0).getHoverName().getString().equals("fixed-"+expected*9),"Selected row supplies the native held item");
|
||||
if(expected==5)require(nativeSlot(m,0).y==baseY-18,"First Tab moves the frame one row up");
|
||||
});
|
||||
if(expected==5 || expected==3)c.takeScreenshot("sanctuary-beta014-hotbar-row-"+expected);
|
||||
}
|
||||
c.runOnClient(m -> {point(m,9);m.mouseHandler.onScroll(m.getWindow().handle(),0,1);});
|
||||
c.waitFor(m -> HotbarRows.selected(m.player)==5,200);c.waitTick();
|
||||
c.runOnClient(m -> {require(fixed.equals(grid(m)),"Wheel moves the frame without moving items");point(m,0);m.mouseHandler.onScroll(m.getWindow().handle(),0,-1);});
|
||||
c.waitFor(m -> m.player.getInventory().getSelectedSlot()==1,200);
|
||||
c.runOnClient(m -> {require(HotbarRows.selected(m.player)==5,"Wheel over active row selects a column only");m.player.closeContainer();});c.waitTicks(3);
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();
|
||||
p.openMenu(new SimpleMenuProvider((id,inv,owner) -> MenuType.GENERIC_9x3.create(id,inv),Component.literal("Mobile hotbar chest")));
|
||||
p.containerMenu.getSlot(0).set(new ItemStack(Items.GOLD_INGOT,31));p.containerMenu.broadcastFullState();
|
||||
});
|
||||
c.waitFor(m -> m.gui.screen() instanceof ContainerScreen,200);c.waitTick();
|
||||
var chestFixed=c.computeOnClient(Inventory014ClientChecks::grid);
|
||||
c.getInput().pressKey(InputConstants.KEY_TAB);c.waitFor(m -> HotbarRows.selected(m.player)==4,200);c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
require(chestFixed.equals(grid(m)),"Chest inventory has the same fixed-grid selection");
|
||||
require(m.player.containerMenu.getSlot(0).getItem().getCount()==31,"Selecting a row never touches chest slots");
|
||||
m.options.guiScale().set(2);m.resizeGui();
|
||||
});c.waitTick();
|
||||
c.runOnClient(m -> require(nativeSlot(m,0).y>=0,"Active hotbar stays reachable in a smaller GUI"));
|
||||
c.takeScreenshot("sanctuary-beta014-mobile-hotbar-chest");
|
||||
c.runOnClient(m -> {m.player.closeContainer();m.options.guiScale().set(1);m.resizeGui();});c.waitTicks(3);
|
||||
require(server.computeOnServer(s -> HotbarRows.selected(s.getPlayerList().getPlayers().getFirst())==4),"Closing the menu keeps the active row on the server");
|
||||
c.runOnClient(m -> m.gui.setScreen(new InventoryScreen(m.player)));c.waitTick();
|
||||
c.runOnClient(m -> {require(fixed.equals(grid(m)),"Reopening preserves every item position and the selected row");m.player.closeContainer();});c.waitTicks(3);
|
||||
}
|
||||
private static void factionProfile(ClientGameTestContext c,TestServerContext server) {
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressEffects.get(p);
|
||||
for(String skill:ProgressRecord.SKILLS)while(progress.rank(skill)<ProgressRecord.cap(skill))progress=progress.buy(skill,1);
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);
|
||||
require(cycle(p,"ngp","").equals("done"),"Complete skills allow New Game+");
|
||||
require(HotbarRows.selected(p)==0,"New Game+ resets the frame to the base");
|
||||
require(cycle(p,"create","Les Arpenteurs").equals("done"),"Earned charge creates faction");CycleService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.cycle()==1 && CycleClient.view!=null && CycleClient.view.faction()!=null,200);
|
||||
c.waitFor(m -> m.player.getDisplayName().getString().startsWith("Les Arpenteurs · "),200);
|
||||
c.runOnClient(m -> {ProgressionClient.open(null,"identity");});c.waitTick();
|
||||
String name=c.computeOnClient(m -> ProgressionClient.identity.name()+" I");
|
||||
c.runOnClient(m -> require(shows(m.gui.screen(),"Les Arpenteurs · "+name),"Inhabitant profile uses faction prefix and Roman prestige"));
|
||||
c.takeScreenshot("sanctuary-beta014-faction-inhabitant");
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();require(cycle(p,"dissolve","").equals("done"),"Faction dissolves");CycleService.send(p,"");});
|
||||
c.waitFor(m -> CycleClient.view.faction()==null && shows(m.gui.screen(),name) && !shows(m.gui.screen(),"Les Arpenteurs · "+name),200);
|
||||
c.runOnClient(m -> require(m.player.getDisplayName().getString().endsWith(" I") && !m.player.getDisplayName().getString().contains("Les Arpenteurs"),"Live display loses faction but keeps prestige"));
|
||||
c.takeScreenshot("sanctuary-beta014-inhabitant-after-dissolution");
|
||||
}
|
||||
private static String cycle(net.minecraft.server.level.ServerPlayer p,String action,String value) {
|
||||
return CycleService.perform(p,new CyclePayloads.Action(action,"",value,ProgressionService.session(p.level().getServer()).cycles.revision(),ProgressEffects.get(p).revision()));
|
||||
}
|
||||
private static boolean shows(Screen screen,String text) {return widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(w -> w.getMessage().getString().equals(text));}
|
||||
private static net.minecraft.world.inventory.Slot nativeSlot(Minecraft m,int index) {
|
||||
return ((AbstractContainerScreen<?>)m.gui.screen()).getMenu().slots.stream().filter(s -> s.container==m.player.getInventory() && s.getContainerSlot()==index).findFirst().orElseThrow();
|
||||
}
|
||||
private static java.util.Map<String,String> grid(Minecraft m) {
|
||||
var result=new java.util.TreeMap<String,String>();
|
||||
for(var slot:((AbstractContainerScreen<?>)m.gui.screen()).getMenu().slots) {
|
||||
if(slot.container==m.player.getInventory() && slot.getContainerSlot()<36 || slot.container instanceof SanctuaryOverflowContainer) {
|
||||
require(slot.x>=0 && slot.y>=0,"All six rows must be visible for the coordinate assertion");
|
||||
var stack=slot.getItem();result.put(slot.x+","+slot.y,stack.getHoverName().getString()+"/"+stack.getCount());
|
||||
}
|
||||
}
|
||||
require(result.size()==54,"Exactly 54 distinct physical cells");return result;
|
||||
}
|
||||
private static Button sortingButton(Screen screen) {
|
||||
var name=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w -> key(w,"sanctuary.progression.sorting")).findFirst().orElseThrow();
|
||||
return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> b.getX()==name.getX() && b.getY()>name.getY())
|
||||
.min(java.util.Comparator.comparingInt(Button::getY)).orElseThrow();
|
||||
}
|
||||
private static void click(Minecraft m,int index,int button) {point(m,index);m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(button,0),1);m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(button,0),0);}
|
||||
private static void point(Minecraft m,int index) {
|
||||
var screen=(AbstractContainerScreen<?>)m.gui.screen();var canvas=(AbstractContainerScreenAccessor)screen;
|
||||
var slot=screen.getMenu().slots.stream().filter(s -> s.container==m.player.getInventory() && s.getContainerSlot()==index).findFirst().orElseThrow();
|
||||
var window=m.getWindow();double x=canvas.sanctuary$getLeftPos()+slot.x+8,y=canvas.sanctuary$getTopPos()+slot.y+8;
|
||||
m.mouseHandler.onMove(window.handle(),x*window.getScreenWidth()/window.getGuiScaledWidth(),y*window.getScreenHeight()/window.getGuiScaledHeight(),0,0);m.mouseHandler.handleAccumulatedMovement();
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(GuiEventListener widget) {return Stream.concat(Stream.of(widget),widget instanceof ContainerEventHandler parent ? parent.children().stream().flatMap(Inventory014ClientChecks::widgets) : Stream.empty());}
|
||||
private static boolean key(AbstractWidget widget,String key) {return widget.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key);}
|
||||
private static Button button(Screen screen,String key) {return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> key(b,key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter() {return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.ProgressionClient;
|
||||
import fr.koka.sanctuary.cosmetics.AccessoryService;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import fr.koka.sanctuary.progression.ProgressionService;
|
||||
import java.util.List;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.minecraft.client.*;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.input.MouseButtonInfo;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
/** Uses the real mouse/keyboard pipeline and observes authoritative server inventories. */
|
||||
final class InventoryControls018Checks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server) {
|
||||
var previous=c.computeOnClient(m -> InputConstants.getKey(rotationKey().saveString()));
|
||||
try {
|
||||
c.runOnClient(m -> {rotationKey().setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_TAB));KeyMapping.resetMapping();});
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();HotbarRows.select(p,0);p.getInventory().clearContent();p.getInventory().setSelectedSlot(2);
|
||||
var types=List.of(Items.STONE,Items.DIRT,Items.COAL,Items.COPPER_INGOT,Items.IRON_INGOT,Items.GOLD_INGOT);
|
||||
for(int row=0;row<6;row++)for(int col=0;col<9;col++)p.getInventory().setItem(HotbarRows.address(row*9+col),new ItemStack(types.get(row),row+1));
|
||||
p.inventoryMenu.broadcastFullState();HotbarService.send(p);ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> m.gui.screen()==null && HotbarRows.selected(m.player)==0 && m.player.getInventory().getItem(0).is(Items.STONE),200);
|
||||
c.runOnClient(m -> {m.player.getInventory().setSelectedSlot(2);m.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket(2));});
|
||||
c.getInput().holdKey(o -> rotationKey());
|
||||
c.waitFor(m -> HotbarRows.selected(m.player)==5 && m.player.getInventory().getSelectedItem().is(Items.GOLD_INGOT),200);
|
||||
c.waitTicks(8);c.runOnClient(m -> require(HotbarRows.selected(m.player)==5 && !m.options.keyPlayerList.isDown(),"Holding Tab neither repeats the row nor opens its overlapping player list"));
|
||||
c.getInput().releaseKey(o -> rotationKey());c.waitTicks(2);
|
||||
c.getInput().holdKey(o -> o.keyShift);c.getInput().pressKey(InputConstants.KEY_TAB);
|
||||
c.waitFor(m -> HotbarRows.selected(m.player)==0 && m.player.getInventory().getSelectedItem().is(Items.STONE),200);c.getInput().releaseKey(o -> o.keyShift);c.waitTicks(2);
|
||||
c.runOnClient(m -> {rotationKey().setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_G));KeyMapping.resetMapping();});
|
||||
c.getInput().pressKey(InputConstants.KEY_G);c.waitFor(m -> HotbarRows.selected(m.player)==5,200);
|
||||
c.runOnClient(m -> m.gui.setScreen(new InventoryScreen(m.player)));c.waitTicks(3);
|
||||
c.runOnClient(m -> {
|
||||
var types=List.of(Items.STONE,Items.DIRT,Items.COAL,Items.COPPER_INGOT,Items.IRON_INGOT,Items.GOLD_INGOT);
|
||||
for(int row=0;row<6;row++)for(int col=0;col<9;col++)require(m.player.getInventory().getItem(HotbarRows.physicalAddress(m.player,row*9+col)).is(types.get(row)),"Reopening shows every item in its original physical row");
|
||||
require(m.player.getInventory().getSelectedSlot()==2,"Column selection is conserved when switching rows");
|
||||
});c.takeScreenshot("sanctuary-beta018-hotbar-from-world");close(c);
|
||||
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();HotbarRows.select(p,0);p.getInventory().clearContent();
|
||||
p.getInventory().setItem(9,new ItemStack(Items.STONE,8));p.getInventory().setItem(10,new ItemStack(Items.DIRT,9));p.getInventory().setItem(11,new ItemStack(Items.DIAMOND,10));
|
||||
p.openMenu(new SimpleMenuProvider((id,inv,owner) -> ChestMenu.threeRows(id,inv,new SimpleContainer(27)),Component.literal("Shift drag chest")));p.containerMenu.broadcastChanges();HotbarService.send(p);
|
||||
});
|
||||
c.waitFor(m -> m.gui.screen() instanceof ContainerScreen && m.player.getInventory().getItem(11).is(Items.DIAMOND),200);
|
||||
int first=c.computeOnClient(m -> playerSlot(m,9)),last=c.computeOnClient(m -> playerSlot(m,11));
|
||||
begin(c,first);move(c,last);c.waitFor(m -> m.player.getInventory().getItem(9).isEmpty() && m.player.getInventory().getItem(10).isEmpty() && m.player.getInventory().getItem(11).isEmpty(),200);
|
||||
require(server.computeOnServer(s -> {var m=s.getPlayerList().getPlayers().getFirst().containerMenu;return m.getSlot(0).getItem().getCount()==8 && m.getSlot(1).getItem().getCount()==9 && m.getSlot(2).getItem().getCount()==10;}),"One fast sweep transfers all three slots, including the intermediate slot");
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(9,new ItemStack(Items.EMERALD,2));p.containerMenu.broadcastChanges();});
|
||||
c.waitFor(m -> m.player.getInventory().getItem(9).is(Items.EMERALD),200);move(c,first);end(c);c.waitTicks(3);
|
||||
c.runOnClient(m -> require(m.player.getInventory().getItem(9).is(Items.EMERALD),"Revisiting a source in the same gesture does not transfer a server refill"));
|
||||
begin(c,0);move(c,2);end(c);c.waitFor(m -> m.player.containerMenu.getSlot(0).getItem().isEmpty() && m.player.containerMenu.getSlot(1).getItem().isEmpty() && m.player.containerMenu.getSlot(2).getItem().isEmpty(),200);
|
||||
c.takeScreenshot("sanctuary-beta018-shift-drag-chest");
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();for(int i=0;i<27;i++)p.containerMenu.getSlot(i).set(new ItemStack(Items.STONE,64));p.getInventory().setItem(9,new ItemStack(Items.EMERALD,2));p.containerMenu.broadcastChanges();});
|
||||
c.waitFor(m -> m.player.containerMenu.getSlot(26).getItem().getCount()==64,200);begin(c,first);move(c,last);end(c);c.waitTicks(3);
|
||||
require(server.computeOnServer(s -> s.getPlayerList().getPlayers().getFirst().getInventory().getItem(9).is(Items.EMERALD)),"Full destination preserves source stack");close(c);
|
||||
|
||||
server.runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();
|
||||
p.getInventory().setItem(9,new ItemStack(Items.IRON_ORE,8));p.getInventory().setItem(10,new ItemStack(Items.COAL,9));p.getInventory().setItem(11,new ItemStack(Items.STONE,10));
|
||||
p.openMenu(new SimpleMenuProvider((id,inv,owner) -> new FurnaceMenu(id,inv),Component.literal("Shift drag furnace")));p.containerMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m -> m.gui.screen() instanceof FurnaceScreen && m.player.getInventory().getItem(9).is(Items.IRON_ORE),200);
|
||||
first=c.computeOnClient(m -> playerSlot(m,9));last=c.computeOnClient(m -> playerSlot(m,11));
|
||||
begin(c,first);move(c,last);end(c);c.waitFor(m -> m.player.containerMenu.getSlot(0).getItem().is(Items.IRON_ORE) && m.player.containerMenu.getSlot(1).getItem().is(Items.COAL),200);
|
||||
require(server.computeOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();int stone=0;for(int i=0;i<36;i++)if(p.getInventory().getItem(i).is(Items.STONE))stone+=p.getInventory().getItem(i).getCount();return stone==10 && p.containerMenu.getSlot(0).getItem().getCount()==8 && p.containerMenu.getSlot(1).getItem().getCount()==9;}),"Furnace native routing separates ingredient, fuel and unsuitable items without loss");
|
||||
c.takeScreenshot("sanctuary-beta018-shift-drag-furnace");close(c);
|
||||
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.getInventory().setItem(9,new ItemStack(Items.COW_SPAWN_EGG));p.getInventory().setItem(10,new ItemStack(AccessoryService.ZERO_CAPE));p.getInventory().setItem(11,new ItemStack(Items.DIAMOND));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m -> m.player.getInventory().getItem(11).is(Items.DIAMOND),200);c.runOnClient(m -> m.gui.setScreen(new InventoryScreen(m.player)));c.waitTick();
|
||||
begin(c,9);move(c,10);c.getInput().releaseKey(o -> o.keyShift);move(c,11);end(c);c.waitTicks(3);
|
||||
require(server.computeOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();var a=AccessoryAccess.of(p);return a.getItem(0).is(Items.COW_SPAWN_EGG) && a.getItem(1).is(AccessoryService.ZERO_CAPE) && a.getItem(2).isEmpty() && p.getInventory().getItem(11).is(Items.DIAMOND);}),"Paint equips eggs/capes; releasing Shift stops it; ordinary items never auto-equip as cosmetics");close(c);
|
||||
System.out.println("InventoryControls018 native client passed: world Tab/reverse/remap, stable physical rows, continuous chest/furnace transfers, fast sweep, full destination, one visit per gesture and equipment rules");
|
||||
} finally {
|
||||
c.getInput().releaseKey(o -> rotationKey());c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.runOnClient(m -> {rotationKey().setKey(previous);KeyMapping.resetMapping();});
|
||||
}
|
||||
}
|
||||
private static KeyMapping rotationKey() {return KeyMapping.get("screen.sanctuary.inventory.rotate_key");}
|
||||
private static int playerSlot(Minecraft m,int index) {return m.player.containerMenu.slots.stream().filter(s -> s.container==m.player.getInventory() && s.getContainerSlot()==index).findFirst().orElseThrow().index;}
|
||||
private static void close(ClientGameTestContext c) {c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);c.waitTicks(3);}
|
||||
private static void begin(ClientGameTestContext c,int slot) {c.getInput().holdKey(o -> o.keyShift);move(c,slot);c.runOnClient(m -> m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,InputConstants.MOD_SHIFT),1));c.waitTick();}
|
||||
private static void end(ClientGameTestContext c) {c.runOnClient(m -> m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,InputConstants.MOD_SHIFT),0));c.getInput().releaseKey(o -> o.keyShift);c.waitTick();}
|
||||
private static void move(ClientGameTestContext c,int index) {
|
||||
c.runOnClient(m -> {
|
||||
var screen=(AbstractContainerScreen<?>)m.gui.screen();var a=(AbstractContainerScreenAccessor)screen;var slot=screen.getMenu().getSlot(index);var w=m.getWindow();
|
||||
double x=a.sanctuary$getLeftPos()+slot.x+8,y=a.sanctuary$getTopPos()+slot.y+8;
|
||||
double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();
|
||||
m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();
|
||||
});c.waitTick();
|
||||
}
|
||||
private static void require(boolean value,String message) {if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.CarryService;
|
||||
import fr.koka.sanctuary.operator.OperatorService;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
|
||||
public final class LanCarry042ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
click(s,"Enter Sanctuary");onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary LAN042 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(false);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
c.runOnClient(m->{m.gui.setScreen(new WorldOptionsScreen(null,m.level));System.out.println("LAN042_WIDGETS "+widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).map(w->w.getMessage().getString()).toList());click(m.gui.screen(),"LAN");click(m.gui.screen(),"Apply Changes");});
|
||||
c.waitFor(m->m.getSingleplayerServer().isPublished(),150);
|
||||
require(server.computeOnServer(s->!s.getWorldData().isAllowCommands()),"LAN opens with commands disabled");
|
||||
c.runOnClient(m->{m.gui.setScreen(new WorldOptionsScreen(null,m.level));click(m.gui.screen(),"Allow Commands");click(m.gui.screen(),"Apply Changes");});
|
||||
c.waitFor(m->ProgressionClient.operator,150);
|
||||
System.out.println("LAN042_PERMISSION "+server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();return "world="+s.getWorldData().isAllowCommands()+" owner="+s.isSingleplayerOwner(p.nameAndId())+" permissions="+p.permissions()+" sanctuary="+OperatorService.allowed(p);}));
|
||||
c.runOnClient(m->{System.out.println("LAN042_TREE "+m.getConnection().getCommands().getRoot().getChildren().stream().map(n->n.getName()).toList());require(m.getConnection().getCommands().getRoot().getChild("experience")!=null,"Experience command visible after late LAN activation");var parsed=m.getConnection().getCommands().parse("experience set @s 17 levels",m.getConnection().getSuggestionsProvider());require(!parsed.getReader().canRead(),"Command parses with current client permissions");require(m.player.chatAbilities().canSendCommands(),"Host chat permits commands");m.gui.openChatScreen(ChatComponent.ChatMethod.COMMAND);require(m.gui.screen() instanceof ChatScreen,"Native command chat opens");widgets(m.gui.screen()).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow().setValue("/experience set @s 17 levels");});
|
||||
c.waitTick();c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN);
|
||||
c.waitFor(m->m.player.experienceLevel==17,150);
|
||||
require(server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().experienceLevel==17),"Real client command changes server XP");
|
||||
c.runOnClient(m->{m.gui.openChatScreen(ChatComponent.ChatMethod.COMMAND);widgets(m.gui.screen()).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow().setValue("/sanctuary time vanilla");});
|
||||
c.waitTick();c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN);c.waitTicks(15);
|
||||
require(server.computeOnServer(s->!fr.koka.sanctuary.realtime.RealtimeService.active(s.overworld())),"Native chat /sanctuary time vanilla reaches the server after LAN activation");
|
||||
System.out.println("LAN044_TIME_PASS: native chat Sanctuary command bypasses no server authorization");
|
||||
require(server.computeOnServer(s->!s.getProfilePermissions(new net.minecraft.server.players.NameAndId(UUID.randomUUID(),"Guest042")).hasPermission(net.minecraft.server.permissions.Permissions.COMMANDS_GAMEMASTER)),"Host activation alone does not promote guests");
|
||||
c.runOnClient(m->{m.gui.setScreen(new WorldOptionsScreen(null,m.level));click(m.gui.screen(),"Allow Commands");click(m.gui.screen(),"Apply Changes");});
|
||||
c.waitFor(m->!ProgressionClient.operator&&m.getConnection().getCommands().getRoot().getChild("experience")==null,150);
|
||||
c.runOnClient(m->m.getConnection().sendCommand("experience set @s 29 levels"));c.waitTicks(5);
|
||||
require(server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().experienceLevel==17),"Commands disabled again are rejected by the server");
|
||||
c.runOnClient(m->{m.gui.openChatScreen(ChatComponent.ChatMethod.COMMAND);widgets(m.gui.screen()).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow().setValue("/sanctuary intro");});
|
||||
c.waitTick();c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN);
|
||||
c.waitFor(m->m.gui.screen() instanceof IntroScreen,150);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
System.out.println("LAN044_INTRO_PASS: ordinary inhabitant replays intro without intercepting server commands");
|
||||
System.out.println("LAN042_COMMAND_PASS: native LAN publication, late host activation, native chat XP command, guest isolation and revocation");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();try{fr.koka.sanctuary.realtime.RealtimeService.setMode(s,false);}catch(Exception e){throw new RuntimeException(e);}s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();});
|
||||
var display=new ArrayList<net.minecraft.client.player.RemotePlayer>();
|
||||
c.runOnClient(m->{
|
||||
m.gui.setScreen(null);m.player.setYRot(0);m.player.setXRot(-10);
|
||||
var at=m.player.position().add(0,0,6);
|
||||
for(int i=0;i<3;i++){
|
||||
var p=new net.minecraft.client.player.RemotePlayer(m.level,new com.mojang.authlib.GameProfile(UUID.randomUUID(),"Carry042_"+i));
|
||||
p.setId(-420-i);p.setPos(at);p.setYRot(180);p.yBodyRot=p.yBodyRotO=180;p.setYHeadRot(180);p.yHeadRotO=180;m.level.addEntity(p);
|
||||
if(i>0){p.setAttached(CarryService.CARRIED,true);require(p.startRiding(display.get(i-1),true,true),"Native visual passenger mounts");display.get(i-1).positionRider(p);}
|
||||
display.add(p);
|
||||
}
|
||||
validateModels(m,display);
|
||||
});
|
||||
c.waitTicks(10);c.takeScreenshot("sanctuary-beta042-seated-stack");
|
||||
c.runOnClient(m->{display.getFirst().setPose(net.minecraft.world.entity.Pose.CROUCHING);display.getFirst().getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.SCALE).setBaseValue(1.5);display.get(1).getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.SCALE).setBaseValue(.75);
|
||||
for(int i=1;i<display.size();i++)display.get(i-1).positionRider(display.get(i));validateModels(m,display);
|
||||
});
|
||||
c.waitTicks(6);c.takeScreenshot("sanctuary-beta042-scaled-crouched-stack");
|
||||
c.runOnClient(m->display.forEach(net.minecraft.world.entity.Entity::discard));
|
||||
System.out.println("CARRY042_CLIENT_PASS: native avatar models seat pelvis on head, complete stack and mixed scales");
|
||||
}
|
||||
}
|
||||
private static void validateModels(net.minecraft.client.Minecraft m,List<net.minecraft.client.player.RemotePlayer> players){
|
||||
for(int i=0;i<players.size();i++){
|
||||
var p=players.get(i);var state=(net.minecraft.client.renderer.entity.state.AvatarRenderState)m.getEntityRenderDispatcher().extractEntity(p,1);
|
||||
var renderer=m.getEntityRenderDispatcher().getRenderer(state);var model=renderer.getModel();model.setupAnim(state);
|
||||
if(i>0){require(state.isPassenger&&!state.isCrouching,"Carried player uses the native seated pose");require(model.rightLeg.xRot<-1.4,"Native bent leg pose is active");}
|
||||
if(i+1<players.size()){
|
||||
double head=p.getY()+renderer.getRenderOffset(state).y()+(1.501-(model.head.y-8)/16.0)*.9375*state.scale;
|
||||
var rider=players.get(i+1);var next=(net.minecraft.client.renderer.entity.state.AvatarRenderState)m.getEntityRenderDispatcher().extractEntity(rider,1);
|
||||
var nextRenderer=m.getEntityRenderDispatcher().getRenderer(next);var nextModel=nextRenderer.getModel();nextModel.setupAnim(next);
|
||||
double pelvis=rider.getY()+nextRenderer.getRenderOffset(next).y()+(1.501-(nextModel.body.y+12)/16.0)*.9375*next.scale;
|
||||
require(Math.abs(head-pelvis)<.0001,"Native model pelvis touches the head: gap="+(pelvis-head));
|
||||
}
|
||||
}
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static void click(Screen s,String prefix){var w=widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(b->b.getMessage().getString().startsWith(prefix)).findFirst().orElseThrow(()->new AssertionError("Missing "+prefix));require(w.visible&&w.active,"Available "+prefix);if(w instanceof Button b)b.onPress(enter());else if(w instanceof CycleButton<?> b)b.onPress(enter());else throw new AssertionError(prefix);}
|
||||
private static KeyEvent enter(){return new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.knowledge.*;
|
||||
import fr.koka.sanctuary.recipes.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
|
||||
/** Actual pause navigation, restricted server state, GUI sizes, tooltips and head-cake rendering. */
|
||||
public final class Menus033ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
boolean quick=Boolean.getBoolean("sanctuary.test.quick");long began=System.nanoTime();
|
||||
var introSeen=new AtomicBoolean(false);
|
||||
var quickDefault=new AtomicBoolean(false);
|
||||
net.fabricmc.fabric.api.client.screen.v1.ScreenEvents.AFTER_INIT.register((m,screen,w,h)->{
|
||||
if(screen instanceof net.minecraft.client.gui.screens.worldselection.CreateWorldScreen creation){var type=creation.getUiState().getWorldType();quickDefault.set(type.preset()!=null&&type.preset().getRegisteredName().equals("sanctuary_test:flat"));}
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(m.gui.screen() instanceof IntroScreen)introSeen.set(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
int scale=c.computeOnClient(m->m.options.guiScale().get());
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary menus033 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals(quick?"sanctuary_test:flat":"sanctuary:sanctuary")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&RecipeClient.ready&&SanctuaryJeiPlugin.runtime()!=null&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
if(quick){require(quickDefault.get(),"Quick profile is the default on the native world creation screen");require(!introSeen.get(),"Quick profile completes the real intro exchange without rendering its sequence");require(server.computeOnServer(s->s.overworld().getChunkSource().getGenerator() instanceof net.minecraft.world.level.levelgen.FlatLevelSource && ProgressionService.session(s)!=null),"Flat native world has the real Sanctuary progression session");System.out.println("QUICK033 admitted with identity on native flat world in "+((System.nanoTime()-began)/1_000_000)+" ms");}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().above(80);
|
||||
for(int x=-10;x<=10;x++)for(int z=-10;z<=10;z++)for(int y=-1;y<7;y++)p.level().setBlockAndUpdate(at.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
p.teleportTo(at.getX()+.5,at.getY(),at.getZ()+.5);p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.OAK_PLANKS,8));
|
||||
BlockKnowledgeService.observeInventory(p);BlockKnowledgeService.recordSeen(p,Blocks.OBSIDIAN);RecipeService.refresh(p);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(20);c.runOnClient(m->{require(!RecipeClient.operatorAllowed&&!RecipeClient.operator,"Survival without commands is not an operator");m.player.setXRot(-25);});
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitFor(m->m.gui.screen() instanceof PauseScreen,100);
|
||||
for(int value:List.of(1,2,3,4)){
|
||||
c.runOnClient(m->{m.options.guiScale().set(value);m.resizeGui();var screen=m.gui.screen();
|
||||
for(String key:List.of("gui.advancements","gui.stats","sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.progression.history","sanctuary.progression.identity","menu.options","options.worldOptions.button"))button(screen,key);
|
||||
for(String key:List.of("menu.reportBugs","menu.sendFeedback","menu.playerReporting","sanctuary.progression.title","sanctuary.cycle.faction","sanctuary.cycle.log"))require(!hasButton(screen,key),"Removed pause entry: "+key);
|
||||
require(widgets(screen).noneMatch(w->w.getClass().getSimpleName().equals("FriendsButton")),"No Friends icon row");
|
||||
for(var w:widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.visible).toList())require(w.getX()>=0&&w.getY()>=0&&w.getX()+w.getWidth()<=screen.width&&w.getY()+w.getHeight()<=screen.height,"Pause controls fit GUI scale "+value);
|
||||
int above=button(screen,"gui.advancements").getY(),below=button(screen,"menu.options").getY();
|
||||
for(String key:List.of("blocks","progress","history","identity")){int y=button(screen,"sanctuary.progression."+key).getY();require(y>above&&y<below,"Pages sit between achievements and options");}
|
||||
});c.waitTicks(3);if(value==2)c.takeScreenshot("sanctuary-beta033-pause");
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();button(m.gui.screen(),"sanctuary.progression.history").onPress(enter());});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen,100);c.waitTicks(5);c.takeScreenshot("sanctuary-beta033-world-direct");done(c);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.progression.progress").onPress(enter()));c.waitFor(m->m.gui.screen() instanceof ProgressionScreen,100);
|
||||
c.runOnClient(m->{require(button(m.gui.screen(),"sanctuary.cycle.entry").getMessage().getString().equals("Prestige"),"Prestige has a stable, readable name");require(!hasButton(m.gui.screen(),"sanctuary.progression.blocks"),"Pages no longer repeat section navigation");});c.takeScreenshot("sanctuary-beta033-progression");done(c);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.progression.blocks").onPress(enter()));c.waitFor(m->m.gui.screen() instanceof SanctuaryBlocodexScreen,100);c.waitTicks(12);
|
||||
c.runOnClient(m->{var filter=widgets(m.gui.screen()).filter(CycleButton.class::isInstance).map(CycleButton.class::cast).findFirst().orElseThrow();require(filter.getValue().toString().equals("POSSESSED"),"Discovery defaults to Owned");
|
||||
for(int i=0;i<12;i++){filter.onPress(enter());require(!filter.getValue().toString().equals("ALL"),"All Blocks is absent before purchase");}
|
||||
require(discoveries(m.gui.screen()).noneMatch(t->t.contains("Obsidian")||t.contains("Diamond")),"Unowned and unknown entries are hidden");
|
||||
button(m.gui.screen(),"sanctuary.recipes.title").onPress(enter());});c.waitFor(m->m.gui.screen() instanceof RecipeScreen,100);
|
||||
c.runOnClient(m->{require(!hasButton(m.gui.screen(),"sanctuary.recipes.show_operator"),"Discovery does not offer operator inspection");RecipeClient.request("operator");});c.waitTicks(25);
|
||||
c.runOnClient(m->require(!RecipeClient.operator&&!RecipeClient.operatorAllowed,"A direct non-operator request cannot enable operator mode"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.giveExperienceLevels(4-p.experienceLevel);RecipeService.refresh(p);});c.waitFor(m->RecipeClient.xp>=4,100);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.recipes.buy").onPress(enter()));c.waitFor(m->RecipeClient.knowledge.learned(),100);done(c);c.waitTicks(12);
|
||||
c.runOnClient(m->{var filter=widgets(m.gui.screen()).filter(CycleButton.class::isInstance).map(CycleButton.class::cast).findFirst().orElseThrow();for(int i=0;i<9&&!filter.getValue().toString().equals("ALL");i++)filter.onPress(enter());require(filter.getValue().toString().equals("ALL"),"Paid Catalogue unlocks All Blocks");widgets(m.gui.screen()).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow().setValue("obsidian");});c.waitTicks(45);
|
||||
c.runOnClient(m->require(discoveries(m.gui.screen()).anyMatch(t->t.contains("Obsidian")),"Seen-only block appears after the paid unlock"));c.takeScreenshot("sanctuary-beta033-discovery-unlocked");
|
||||
for(String locale:List.of("en_us","fr_fr")){
|
||||
c.runOnClient(m->{m.gui.setScreen(null);m.getLanguageManager().setSelected(locale);m.options.languageCode=locale;m.reloadResourcePacks();});c.waitTicks(60);
|
||||
c.runOnClient(m->{for(String key:List.of("jei.tooltip.bookmarks.enable","jei.tooltip.bookmarks.disable","jei.tooltip.lookupHistory.enable","jei.tooltip.lookupHistory.disable")){String value=Component.translatable(key).getString();require(!value.contains("JEI")&&!value.contains("Just Enough Items")&&!value.equals(key),"Clean native tooltip: "+locale+"/"+key);}
|
||||
require(Component.translatable("sanctuary.cycle.log").getString().equals(locale.equals("fr_fr")?"Historique":"History"),"History label matches the game language");
|
||||
require(!mezz.jei.api.sanctuary.CatalogueAccess.configButtonVisible.getAsBoolean(),"Configuration has no drawn or interactive overlay entry");m.gui.setScreen(new InventoryScreen(m.player));});c.waitTicks(8);c.takeScreenshot("sanctuary-beta033-catalogue-"+locale);
|
||||
}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var cake=new ItemStack(Items.CAKE);
|
||||
CustomData.update(DataComponents.CUSTOM_DATA,cake,tag->tag.putString(HeadCosmeticData.CANDLE,"minecraft:blue_candle"));cake.set(DataComponents.BLOCK_STATE,BlockItemStateProperties.EMPTY.with(CandleBlock.LIT,true));AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,cake);});
|
||||
c.waitFor(m->HeadCosmeticData.lit(HeadCosmeticService.appearance(m.player)),100);
|
||||
c.runOnClient(m->{m.gui.setScreen(null);m.options.setCameraType(CameraType.THIRD_PERSON_FRONT);m.player.setXRot(0);m.options.fov().set(35);});c.waitTicks(20);c.takeScreenshot("sanctuary-beta033-candle-cake-flame");
|
||||
c.runOnClient(m->{m.options.setCameraType(CameraType.FIRST_PERSON);m.options.fov().set(70);m.options.guiScale().set(scale);m.resizeGui();});
|
||||
System.out.println("UI033 client passed: native pause layout, direct pages, paid discovery, denied operator request, FR/EN tooltips and cake flame rendered.");
|
||||
}
|
||||
}
|
||||
private static void done(ClientGameTestContext c){c.runOnClient(m->button(m.gui.screen(),"gui.done").onPress(enter()));c.waitTicks(4);}
|
||||
private static Stream<String> discoveries(Screen screen){return widgets(screen).filter(ObjectSelectionList.class::isInstance).map(ObjectSelectionList.class::cast).flatMap(list->((List<ObjectSelectionList.Entry<?>>)list.children()).stream()).map(e->e.getNarration().getString());}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?Stream.concat(Stream.of(w),widgets(nested)):Stream.of(w));}
|
||||
private static Button button(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow(()->new AssertionError("Missing button: "+key));}
|
||||
private static boolean hasButton(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).anyMatch(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.mining.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.input.MouseButtonInfo;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Real player packets and native input; no direct call to MiningClient or the server's mining handler. */
|
||||
final class Mining008ClientChecks {
|
||||
static void run(ClientGameTestContext c,TestServerContext server) {
|
||||
server.runOnServer(s -> s.getPlayerList().getPlayers().getFirst().giveExperienceLevels(2));
|
||||
c.waitFor(m -> m.player.experienceLevel==3,400);
|
||||
for (int rank=1;rank<=2;rank++) {
|
||||
c.runOnClient(m -> {
|
||||
var button=m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals("sanctuary.progression.upgrade_cost"))
|
||||
.sorted(java.util.Comparator.comparingInt(Button::getY)).skip(2).findFirst().orElseThrow();
|
||||
require(button.active,"The mining purchase is available in the skills column");
|
||||
double x=(button.getX()+button.getWidth()/2.)*m.getWindow().getScreenWidth()/m.getWindow().getGuiScaledWidth();
|
||||
double y=(button.getY()+button.getHeight()/2.)*m.getWindow().getScreenHeight()/m.getWindow().getGuiScaledHeight();
|
||||
m.mouseHandler.onMove(m.getWindow().handle(),x,y,0,0);m.mouseHandler.handleAccumulatedMovement();
|
||||
var mouse=new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0);
|
||||
m.mouseHandler.onButton(m.getWindow().handle(),mouse,InputConstants.PRESS);
|
||||
m.mouseHandler.onButton(m.getWindow().handle(),mouse,InputConstants.RELEASE);
|
||||
});
|
||||
int expected=rank;c.waitFor(m -> ProgressionClient.progress.mining()==expected,400);
|
||||
}
|
||||
c.runOnClient(m -> require(m.player.experienceLevel==0,"Both ranks cost exactly three levels"));
|
||||
Blocodex004ClientChecks.language(c,"fr_fr");c.takeScreenshot("sanctuary-beta008-minage-fr");
|
||||
Blocodex004ClientChecks.language(c,"en_us");
|
||||
c.clickScreenButton("gui.done");c.waitForScreen(null);
|
||||
BlockPos origin=server.computeOnServer(s -> {
|
||||
var player=s.getPlayerList().getPlayers().getFirst();var base=player.blockPosition().above(14);
|
||||
for (int x=-2;x<=6;x++) for (int z=-2;z<=7;z++) {
|
||||
player.level().setBlockAndUpdate(base.offset(x,-1,z),Blocks.BEDROCK.defaultBlockState());
|
||||
for (int y=0;y<=4;y++) player.level().setBlockAndUpdate(base.offset(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
var target=base.offset(0,1,3);
|
||||
for (int i=0;i<8;i++) player.level().setBlockAndUpdate(target.offset(i%4,0,i/4),Blocks.STONE.defaultBlockState());
|
||||
player.teleportTo(base.getX()+.5,base.getY(),base.getZ()+.5);
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND_PICKAXE));player.inventoryMenu.broadcastChanges();
|
||||
return target;
|
||||
});
|
||||
c.waitFor(m -> m.level.getBlockState(origin).is(Blocks.STONE) && m.player.getMainHandItem().is(Items.DIAMOND_PICKAXE)
|
||||
&& Math.abs(m.player.getY()-(origin.getY()-1))<.1,600);
|
||||
c.getInput().lookAt(origin);c.waitTicks(3);
|
||||
c.getInput().holdKey(InputConstants.KEY_R);
|
||||
c.waitFor(m -> MiningClient.previewSize()==8,200);
|
||||
c.takeScreenshot("sanctuary-beta008-vein-preview-eight");
|
||||
c.runOnClient(m -> {
|
||||
int slot=m.player.getInventory().getSelectedSlot();
|
||||
m.mouseHandler.onScroll(m.getWindow().handle(),0,-1);
|
||||
require(MiningClient.selectedRank()==1 && m.player.getInventory().getSelectedSlot()==slot,"Modifier wheel selects a smaller vein without changing the held tool");
|
||||
});
|
||||
c.waitFor(m -> MiningClient.previewSize()==4,100);
|
||||
c.runOnClient(m -> m.mouseHandler.onScroll(m.getWindow().handle(),0,1));
|
||||
c.waitFor(m -> MiningClient.previewSize()==8,100);
|
||||
c.getInput().holdKey(o -> o.keyAttack);
|
||||
server.waitFor(s -> MiningManager.active(s.getPlayerList().getPlayers().getFirst()),200);
|
||||
c.waitTicks(3);c.getInput().releaseKey(o -> o.keyAttack);
|
||||
server.waitFor(s -> !MiningManager.active(s.getPlayerList().getPlayers().getFirst()),200);
|
||||
server.runOnServer(s -> require(s.overworld().getBlockState(origin).is(Blocks.STONE),"Releasing the physical attack gesture cancels before the vein breaks"));
|
||||
c.getInput().holdKey(o -> o.keyAttack);
|
||||
c.waitFor(m -> m.level.getBlockState(origin).isAir(),800);
|
||||
c.getInput().releaseKey(o -> o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);c.waitTicks(5);
|
||||
server.runOnServer(s -> {
|
||||
var player=s.getPlayerList().getPlayers().getFirst();
|
||||
require(player.getStats().getValue(Stats.BLOCK_MINED.get(Blocks.STONE))==8 && player.getMainHandItem().getDamageValue()==8,
|
||||
"The real client mines the full vein exactly once through server vanilla actions");
|
||||
for (int i=0;i<8;i++) require(player.level().getBlockState(origin.offset(i%4,0,i/4)).isAir(),"Every selected block is removed");
|
||||
for (int i=0;i<8;i++) player.level().setBlockAndUpdate(origin.offset(i%4,0,i/4),Blocks.STONE.defaultBlockState());
|
||||
});
|
||||
c.waitFor(m -> m.level.getBlockState(origin).is(Blocks.STONE),200);c.getInput().lookAt(origin);
|
||||
c.runOnClient(m -> {miningKey(m).setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_G));KeyMapping.resetMapping();});
|
||||
try {
|
||||
c.getInput().holdKey(InputConstants.KEY_G);c.waitFor(m -> MiningClient.previewSize()==8,200);
|
||||
c.getInput().holdKey(o -> o.keyAttack);
|
||||
server.waitFor(s -> MiningManager.active(s.getPlayerList().getPlayers().getFirst()),200);
|
||||
c.getInput().pressKey(InputConstants.KEY_B);c.waitForScreen(ProgressionScreen.class);
|
||||
server.waitFor(s -> !MiningManager.active(s.getPlayerList().getPlayers().getFirst()),200);
|
||||
c.getInput().releaseKey(o -> o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_G);
|
||||
c.takeScreenshot("sanctuary-beta008-mining-progression");
|
||||
} finally {
|
||||
c.getInput().releaseKey(o -> o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);c.getInput().releaseKey(InputConstants.KEY_G);
|
||||
c.runOnClient(m -> {miningKey(m).setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_R));KeyMapping.resetMapping();});
|
||||
}
|
||||
System.out.println("Mining beta008 native client passed: two real purchases, eight/four block previews, native wheel, release, repeated mining, configurable key and Blocodex interruption.");
|
||||
}
|
||||
private static KeyMapping miningKey(net.minecraft.client.Minecraft m) {
|
||||
return java.util.Arrays.stream(m.options.keyMappings).filter(k -> k.getName().equals("sanctuary.mining.key")).findFirst().orElseThrow();
|
||||
}
|
||||
private static void require(boolean value,String message) {if (!value) throw new AssertionError(message);}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.item.PrimedTnt;
|
||||
import net.minecraft.world.inventory.ChestMenu;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Real client packets and integrated-server lifecycle; no dedicated-server EULA. */
|
||||
public final class MobHead060ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(m.gui.screen() instanceof IntroScreen s)s.onClose();
|
||||
if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen s){Starter039TestSupport.selectFirst(s);s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);}
|
||||
});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("MobHead060 "+UUID.randomUUID());s.setSeed("42");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);var server=world.getServer();c.waitTicks(40);
|
||||
int id=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.getInventory().clearContent();s.getWorldData().setAllowCommands(false);fr.koka.sanctuary.progression.ProgressionService.send(p,"");p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.REDSTONE_TORCH,3));p.setShiftKeyDown(true);p.inventoryMenu.broadcastChanges();
|
||||
var mob=EntityTypes.CHICKEN.create(p.level(),EntitySpawnReason.COMMAND);mob.setPos(p.position().add(0,0,2));mob.setCustomName(Component.literal("Circuit chicken"));p.level().addFreshEntity(mob);check(!mob.isNoAi(),"Chicken keeps its native AI");mob.setNoAi(true);var at=BlockPos.containing(mob.getEyePosition().add(0,.25,0));p.level().setBlock(at.east(),Blocks.REDSTONE_LAMP.defaultBlockState(),3);p.level().setBlock(at.offset(1,0,3),Blocks.REDSTONE_LAMP.defaultBlockState(),3);return mob.getId();});
|
||||
c.waitFor(m->m.level.getEntity(id)!=null&&m.player.getMainHandItem().is(Items.REDSTONE_TORCH),200);c.getInput().holdKey(o->o.keyShift);c.waitTicks(5);
|
||||
click(c,id);c.waitFor(m->HeadCosmeticService.appearance((Mob)m.level.getEntity(id)).is(Items.REDSTONE_TORCH),200);
|
||||
server.runOnServer(s->{var p=player(s);var mob=mob(s,id);check(HeadEquipment.get(mob).getCount()==1&&p.getMainHandItem().getCount()==2,"Real shift-right-click transfers exactly one item");check(!mob.isPassenger(),"Equipment gesture never carries the chicken");check(mob.requiresCustomPersistence(),"Equipped mob cannot naturally despawn");
|
||||
var at=HeadBlockService.session(mob).pos();check(p.level().hasNeighborSignal(at.east()),"Chicken torch energizes actual neighboring cell");});
|
||||
c.getInput().releaseKey(o->o.keyShift);c.waitTicks(10);
|
||||
server.runOnServer(s->{var m=mob(s,id);var at=HeadBlockService.session(m).pos();check(m.level().getBlockState(at.east()).getValue(RedstoneLampBlock.LIT),"Native lamp lights from mobile source");m.setPos(m.position().add(0,0,3));});c.waitTicks(12);
|
||||
server.runOnServer(s->{var m=mob(s,id);var at=HeadBlockService.session(m).pos();check(!m.level().getBlockState(at.offset(1,0,-3)).getValue(RedstoneLampBlock.LIT),"Movement removes the old source and native lamp turns off");check(((ServerLevel)m.level()).hasNeighborSignal(at.east()),"New cell powers without scanning chunks");m.setPos(player(s).position().add(0,0,2));});c.waitTicks(8);
|
||||
c.runOnClient(m->{var mob=(Mob)m.level.getEntity(id);var state=m.getEntityRenderDispatcher().getRenderer(mob).createRenderState(mob,1);var hat=((MobHeadState.Access)state).sanctuary$mobHat();check(hat.active&&!hat.head.block.isEmpty(),"Native chicken renderer extracts torch model");m.getWindow().setWindowed(1280,800);m.options.guiScale().set(2);m.resizeGui();});
|
||||
c.getInput().lookAt(0,12);c.takeScreenshot("sanctuary-beta060-chicken-redstone");
|
||||
// Removal uses the same native interaction and restores empty-head carry behavior.
|
||||
server.runOnServer(s->{var p=player(s);p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);p.setShiftKeyDown(true);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.getMainHandItem().isEmpty(),200);c.getInput().holdKey(o->o.keyShift);c.waitTicks(5);click(c,id);c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);var mob=mob(s,id);check(HeadEquipment.get(mob).isEmpty(),"Empty-hand shift-right-click retrieves equipment");check(CarryService.interact(p,mob),"Empty-headed mob remains carryable");CarryService.release(mob,true);mob.setPos(p.position().add(0,0,2));p.setShiftKeyDown(false);});c.getInput().releaseKey(o->o.keyShift);
|
||||
// Shared native menus, persistence and item conservation use the actual mob slot/session.
|
||||
server.runOnServer(s->storage(s,id));c.waitTicks(10);
|
||||
c.runOnClient(m->{check(m.gui.screen() instanceof net.minecraft.client.gui.screens.inventory.ContainerScreen,"Mob chest opens a real client chest screen");});c.takeScreenshot("sanctuary-beta060-mob-chest");
|
||||
server.runOnServer(s->{var p=player(s);var mob=mob(s,id);HeadEquipment.set(mob,ItemStack.EMPTY);check(p.containerMenu==p.inventoryMenu,"Removing storage closes its visitor menu");check(drops(p,Items.DIAMOND)==7,"Ordinary chest content returned exactly once");});c.waitFor(m->m.gui.screen()==null,200);
|
||||
server.runOnServer(s->lifecycle(s,id));c.waitTicks(8);
|
||||
server.runOnServer(s->interactions(s,id));c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=player(s);var m=mob(s,id);check(((Container)HeadBlockService.session(m).entity).getItem(0).getCount()==1,"A hit fires one native dispenser arrow");
|
||||
HeadEquipment.set(m,new ItemStack(Items.FURNACE));var furnace=(Container)HeadBlockService.session(m).entity;furnace.setItem(0,new ItemStack(Items.RAW_IRON));furnace.setItem(1,new ItemStack(Items.COAL));});
|
||||
c.waitTicks(210);server.runOnServer(s->{var f=(Container)HeadBlockService.session(mob(s,id)).entity;check(f.getItem(2).is(Items.IRON_INGOT),"Carried furnace smelts with real recipe and fuel");});
|
||||
// Adult, baby, hostile, fish and non-block items all pass native model extraction.
|
||||
c.runOnClient(m->{for(var type:List.of(EntityTypes.CHICKEN,EntityTypes.COW,EntityTypes.ZOMBIE,EntityTypes.BEE,EntityTypes.SLIME,EntityTypes.COD,EntityTypes.WITHER,EntityTypes.ENDER_DRAGON)){
|
||||
var e=(Mob)type.create(m.level,EntitySpawnReason.COMMAND);e.setId(200000+type.hashCode()%100000);e.setPos(m.player.position());e.xo=e.xOld=e.getX();e.yo=e.yOld=e.getY();e.zo=e.zOld=e.getZ();
|
||||
for(boolean baby:List.of(false,true)){e.setBaby(baby);e.setAttached(AccessoryService.APPEARANCE,new AccessoryService.Appearance(ItemStack.EMPTY,new ItemStack(Items.CHEST)));var state=m.getEntityRenderDispatcher().getRenderer(e).createRenderState(e,1);check(((MobHeadState.Access)state).sanctuary$mobHat().active,"Mob and baby model supports equipment: "+type);}
|
||||
}
|
||||
var e=(Mob)m.level.getEntity(id);e.setAttached(AccessoryService.APPEARANCE,new AccessoryService.Appearance(ItemStack.EMPTY,new ItemStack(Items.DIAMOND)));var state=m.getEntityRenderDispatcher().getRenderer(e).createRenderState(e,1);check(!((MobHeadState.Access)state).sanctuary$mobHat().head.item.isEmpty(),"Any ordinary item has a native head render");});
|
||||
for(String language:List.of("fr_fr","en_us"))c.runOnClient(m->{m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());check(!Component.translatable("sanctuary.mob_head.equipped").getString().contains("sanctuary."),"Localized equipment hints");});
|
||||
System.out.println("MOB_HEAD060_PASS: native input, mobile redstone, inventory, persistence, death, Shulker, shared player blocks, models and FR/EN");
|
||||
}finally{intro.set(false);c.getInput().releaseKey(o->o.keyShift);}
|
||||
}
|
||||
private static void storage(net.minecraft.server.MinecraftServer s,int id){
|
||||
var p=player(s);var m=mob(s,id);clearDrops(p);p.setShiftKeyDown(false);p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);HeadEquipment.set(m,new ItemStack(Items.CHEST));
|
||||
var session=HeadBlockService.session(m);((Container)session.entity).setItem(0,new ItemStack(Items.DIAMOND,7));
|
||||
check(HeadCosmeticService.interact(p,m)&&p.containerMenu instanceof ChestMenu chest&&chest.getRowCount()==3,"Mob chest is the native three-row public container");
|
||||
var tag=save(m);HeadBlockService.suspend(m);check(HeadBlockService.session(m).entity instanceof ChestBlockEntity chest&&chest.getItem(0).getCount()==7,"Suspend and resume preserve contents");
|
||||
var copy=EntityTypes.CHICKEN.create(p.level(),EntitySpawnReason.COMMAND);copy.load(TagValueInput.create(ProblemReporter.DISCARDING,p.level().registryAccess(),tag));
|
||||
check(HeadEquipment.get(copy).is(Items.CHEST)&&((Container)HeadBlockService.session(copy).entity).getItem(0).getCount()==7,"Native entity save/load retains head and contents");HeadBlockService.suspend(copy);
|
||||
HeadBlockService.use(p,m,false);p.inventoryMenu.broadcastChanges();
|
||||
}
|
||||
private static void lifecycle(net.minecraft.server.MinecraftServer s,int id){
|
||||
var p=player(s);var m=mob(s,id);clearDrops(p);HeadEquipment.set(m,new ItemStack(Items.SHULKER_BOX));var ses=HeadBlockService.session(m);((Container)ses.entity).setItem(0,new ItemStack(Items.EMERALD,5));
|
||||
p.setShiftKeyDown(true);p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);check(MobHeadService.equip(p,m),"Retrieve Shulker from mob");
|
||||
check(drops(p,Items.EMERALD)==0,"Shulker retrieval does not also drop contents");
|
||||
var boxes=new ArrayList<ItemStack>();for(int i=0;i<p.getInventory().getContainerSize();i++)if(p.getInventory().getItem(i).is(Items.SHULKER_BOX))boxes.add(p.getInventory().getItem(i));check(boxes.size()==1&&boxes.getFirst().get(DataComponents.CONTAINER).itemCopies().mapToInt(ItemStack::getCount).sum()==5,"Exactly one Shulker with five emeralds");
|
||||
var dying=EntityTypes.CHICKEN.create(p.level(),EntitySpawnReason.COMMAND);dying.setPos(p.position().add(1,0,1));p.level().addFreshEntity(dying);HeadEquipment.set(dying,new ItemStack(Items.BARREL));((Container)HeadBlockService.session(dying).entity).setItem(0,new ItemStack(Items.GOLD_INGOT,4));dying.kill(p.level());
|
||||
check(drops(p,Items.BARREL)==1&&drops(p,Items.GOLD_INGOT)==4,"Death returns one barrel and its exact contents");MobHeadService.drop(dying);check(drops(p,Items.BARREL)==1,"Repeated cleanup cannot duplicate equipment");
|
||||
var invalid=new CompoundTag();invalid.putInt("schema",999);var tag=new CompoundTag();tag.put(MobHeadEquipment.KEY,invalid);var bad=EntityTypes.CHICKEN.create(p.level(),EntitySpawnReason.COMMAND);HeadEquipment.mob(bad).read(TagValueInput.create(ProblemReporter.DISCARDING,p.level().registryAccess(),tag));check(!HeadEquipment.mob(bad).readable(),"Invalid data is blocked");var out=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,p.level().registryAccess());HeadEquipment.mob(bad).write(out);check(out.buildResult().getCompound(MobHeadEquipment.KEY).orElseThrow().equals(invalid),"Invalid data is preserved verbatim");
|
||||
var loaded=EntityTypes.CHICKEN.create(p.level(),EntitySpawnReason.COMMAND);loaded.setPos(p.position().add(-2,0,1));loaded.setNoAi(true);p.level().addFreshEntity(loaded);HeadEquipment.set(loaded,new ItemStack(Items.REDSTONE_TORCH));HeadBlockService.session(loaded).tick();var source=HeadBlockService.session(loaded).pos();var saved=save(loaded);
|
||||
check(HeadRedstoneService.signal(p.level(),source)==15,"Loaded mob source active");loaded.remove(Entity.RemovalReason.UNLOADED_TO_CHUNK);check(HeadRedstoneService.signal(p.level(),source)==0,"Native entity unload removes its power immediately");
|
||||
var restored=EntityTypes.CHICKEN.create(p.level(),EntitySpawnReason.COMMAND);restored.load(TagValueInput.create(ProblemReporter.DISCARDING,p.level().registryAccess(),saved));p.level().addFreshEntity(restored);HeadBlockService.session(restored).tick();check(HeadRedstoneService.signal(p.level(),source)==15,"Loaded saved mob restores its source");MobHeadService.drop(restored);restored.discard();
|
||||
p.setShiftKeyDown(false);
|
||||
}
|
||||
private static void interactions(net.minecraft.server.MinecraftServer s,int id){
|
||||
var p=player(s);var m=mob(s,id);p.setShiftKeyDown(false);p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);
|
||||
HeadEquipment.set(m,new ItemStack(Items.LEVER));check(HeadBlockService.use(p,m,false),"Mob lever is usable");check(HeadBlockService.session(m).state.getValue(LeverBlock.POWERED),"Mob lever changes block state");
|
||||
HeadEquipment.set(m,new ItemStack(Items.STONE_BUTTON));check(HeadBlockService.use(p,m,false)&&HeadBlockService.session(m).emitsPower(),"Mob button emits native timed power");
|
||||
HeadEquipment.set(m,new ItemStack(Items.FLOWER_POT));p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.POPPY));check(HeadBlockService.use(p,m,false)&&HeadBlockService.session(m).state.is(Blocks.POTTED_POPPY),"Native transformations retain mob session");
|
||||
HeadEquipment.set(m,new ItemStack(Items.DISPENSER));((Container)HeadBlockService.session(m).entity).setItem(0,new ItemStack(Items.ARROW,2));check(m.hurtServer(p.level(),p.damageSources().generic(),1),"Real mob damage triggers its head block");
|
||||
// Player path still retains its original slot and native menu behavior after generalization.
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.CHEST));p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);check(HeadBlockService.use(p,p,false)&&p.containerMenu instanceof ChestMenu,"Player head chest remains functional");AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,ItemStack.EMPTY);
|
||||
var bomb=EntityTypes.CHICKEN.create(p.level(),EntitySpawnReason.COMMAND);bomb.setPos(p.position().add(-1,0,2));p.level().addFreshEntity(bomb);HeadEquipment.set(bomb,new ItemStack(Items.TNT));var tnt=HeadTntService.ignite(bomb,p.damageSources().generic());check(tnt!=null&&HeadEquipment.get(bomb).isEmpty(),"Mob TNT becomes one real fused entity");check(HeadTntService.ignite(bomb,p.damageSources().generic())==null,"TNT cannot prime twice");tnt.discard();bomb.discard();
|
||||
}
|
||||
private static CompoundTag save(Mob m){var out=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,m.level().registryAccess());m.saveWithoutId(out);return out.buildResult();}
|
||||
private static long drops(ServerPlayer p,Item item){return p.level().getEntitiesOfClass(ItemEntity.class,p.getBoundingBox().inflate(12)).stream().filter(e->e.getItem().is(item)).mapToLong(e->e.getItem().getCount()).sum();}
|
||||
private static void clearDrops(ServerPlayer p){p.level().getEntitiesOfClass(ItemEntity.class,p.getBoundingBox().inflate(12)).forEach(Entity::discard);}
|
||||
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static Mob mob(net.minecraft.server.MinecraftServer s,int id){return (Mob)s.overworld().getEntity(id);}
|
||||
private static void click(ClientGameTestContext c,int id){c.runOnClient(m->{var mob=m.level.getEntity(id);check(m.player.isShiftKeyDown(),"Native interaction packet carries held sneak modifier");m.gameMode.interact(m.player,mob,new EntityHitResult(mob,mob.getEyePosition()),InteractionHand.MAIN_HAND);});}
|
||||
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);}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.entity.Pose;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class Movement017ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if(armed.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);
|
||||
}
|
||||
});
|
||||
String language=c.computeOnClient(m -> m.getLanguageManager().getSelected());int scale=c.computeOnClient(m -> m.options.guiScale().get());
|
||||
var previous=c.computeOnClient(m -> InputConstants.getKey(KeyMapping.get("sanctuary.progression.lying_key").saveString()));
|
||||
var forward=c.computeOnClient(m -> InputConstants.getKey(m.options.keyUp.saveString()));
|
||||
int letterW=c.computeOnClient(m -> {
|
||||
var binding=KeyMapping.get("sanctuary.progression.lying_key");
|
||||
require(binding.getDefaultKey().getDisplayName().getString().equals("W"),"Default posture key is the letter W on the active keyboard layout");
|
||||
return binding.getDefaultKey().getValue();
|
||||
});
|
||||
var xpSounds=new java.util.concurrent.atomic.AtomicInteger();
|
||||
net.minecraft.client.sounds.SoundEventListener soundListener=(sound,event,distance) -> {
|
||||
if(sound.getIdentifier().equals(net.minecraft.sounds.SoundEvents.EXPERIENCE_ORB_PICKUP.location()))xpSounds.incrementAndGet();
|
||||
};
|
||||
c.runOnClient(m -> m.getSoundManager().addListener(soundListener));
|
||||
try(var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Sanctuary beta017 movement "+UUID.randomUUID());
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && ProgressionClient.active() && m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
// Isolate fixture movement from W without changing or depending on saved player controls.
|
||||
c.runOnClient(m -> {m.options.keyUp.setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_UP));KeyMapping.get("sanctuary.progression.lying_key").setKey(InputConstants.Type.KEYBOARD.getOrCreate(letterW));KeyMapping.resetMapping();});
|
||||
BlockPos floor=server.computeOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var base=p.blockPosition().above(8);
|
||||
for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++) {
|
||||
p.level().setBlockAndUpdate(base.offset(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int y=1;y<=5;y++)p.level().setBlockAndUpdate(base.offset(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
p.teleportTo(base.getX()+.5,base.getY()+1,base.getZ()+.5);
|
||||
var progress=ProgressRecord.INITIAL.buy("hunger",1);p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);p.getFoodData().setFoodLevel(8);ProgressionService.send(p,"");return base;
|
||||
});
|
||||
c.waitFor(m -> m.player.onGround() && m.player.getY()>=floor.getY()+1 && ProgressionClient.progress.hunger()==1,200);
|
||||
c.getInput().pressKey(letterW);c.waitTicks(5);
|
||||
c.runOnClient(m -> require(LyingService.posture(m.player)==0,"W cannot bypass an unpurchased aptitude"));
|
||||
// Real water input before purchasing the aptitude: slow movement and upward buoyancy remain.
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();for(int x=-2;x<=2;x++)for(int z=-2;z<=2;z++)for(int y=1;y<=4;y++)p.level().setBlockAndUpdate(floor.offset(x,y,z),Blocks.WATER.defaultBlockState());p.teleportTo(floor.getX()+.5,floor.getY()+1,floor.getZ()+.5);});
|
||||
c.waitFor(m -> m.player.isInWater(),200);var start=c.computeOnClient(m -> m.player.position());
|
||||
c.getInput().holdKey(o -> o.keyUp);c.getInput().holdKey(o -> o.keySprint);c.waitTicks(8);
|
||||
c.runOnClient(m -> require(!m.player.isSprinting() && !m.player.isSwimming() && Math.hypot(m.player.getX()-start.x,m.player.getZ()-start.z)>0.01,"Untrained player moves slowly without aquatic sprint"));
|
||||
c.getInput().releaseKey(o -> o.keyUp);c.getInput().releaseKey(o -> o.keySprint);
|
||||
double oldY=c.computeOnClient(m -> m.player.getY());c.getInput().holdKey(o -> o.keyJump);c.waitTicks(8);c.getInput().releaseKey(o -> o.keyJump);
|
||||
c.runOnClient(m -> require(m.player.getY()>oldY,"Jump can still bring an untrained player toward the surface"));
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++)for(int y=1;y<=5;y++)p.level().setBlockAndUpdate(floor.offset(x,y,z),Blocks.AIR.defaultBlockState());p.teleportTo(floor.getX()+.5,floor.getY()+1,floor.getZ()+.5);p.setExperienceLevels(8);p.setExperiencePoints(0);ProgressionService.send(p,"");});
|
||||
c.waitFor(m -> m.player.onGround() && !m.player.isInWater() && m.player.experienceLevel==8,200);
|
||||
c.runOnClient(m -> {m.options.guiScale().set(1);m.resizeGui();ProgressionClient.open(null);});c.waitTick();
|
||||
for(String aptitude:List.of("swimming","resting")) {
|
||||
c.runOnClient(m -> {
|
||||
var buy=purchase(m.gui.screen(),aptitude);require(buy.active,"Four-level purchase available: "+aptitude);
|
||||
buy.onPress(enter());
|
||||
});c.waitFor(m -> ProgressionClient.progress.rank(aptitude)==1,200);c.waitTick();
|
||||
}
|
||||
c.runOnClient(m -> require(m.player.experienceLevel==0,"Two paid aptitudes cost eight levels; lying is free"));
|
||||
for(String locale:List.of("en_us","fr_fr")) {
|
||||
c.runOnClient(m -> {m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());ProgressionClient.open(null);m.gui.screen().mouseScrolled(m.gui.screen().width-40,m.gui.screen().height/2.0,0,-40);});c.waitTick();
|
||||
c.runOnClient(m -> prices(m.gui.screen()));
|
||||
c.takeScreenshot("sanctuary-beta017-aptitudes-"+locale);
|
||||
}
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);c.waitTicks(3);
|
||||
c.getInput().pressKey(letterW);c.waitFor(m -> LyingService.posture(m.player)==1 && m.player.getPose()==Pose.SWIMMING,200);
|
||||
require(server.computeOnServer(s -> LyingService.posture(s.getPlayerList().getPlayers().getFirst())==1),"W posture is confirmed by server attachment");
|
||||
c.takeScreenshot("sanctuary-beta017-lying-W");c.getInput().pressKey(letterW);c.waitFor(m -> LyingService.posture(m.player)==0,200);
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.setHealth(2);p.getFoodData().setFoodLevel(6);p.getFoodData().setSaturation(0);});
|
||||
c.waitFor(m -> m.player.getHealth()==2,200);
|
||||
c.getInput().holdKey(o -> o.keyShift);c.getInput().pressKey(letterW);c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> LyingService.posture(m.player)==2,200);
|
||||
c.waitFor(m -> m.player.getHealth()==3 && m.player.getFoodData().getFoodLevel()==5,400);
|
||||
c.getInput().holdKey(o -> o.keyUp);c.waitFor(m -> LyingService.posture(m.player)==0,200);c.getInput().releaseKey(o -> o.keyUp);
|
||||
c.runOnClient(m -> {KeyMapping.get("sanctuary.progression.lying_key").setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_G));KeyMapping.resetMapping();});c.waitTicks(5);
|
||||
c.getInput().pressKey(InputConstants.KEY_G);c.waitFor(m -> LyingService.posture(m.player)==1,200);c.getInput().pressKey(InputConstants.KEY_G);c.waitFor(m -> LyingService.posture(m.player)==0,200);
|
||||
c.getInput().holdKey(o -> o.keyShift);c.getInput().pressKey(InputConstants.KEY_G);c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> LyingService.posture(m.player)==2,200);
|
||||
c.getInput().holdKey(o -> o.keyShift);c.getInput().pressKey(InputConstants.KEY_G);c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> LyingService.posture(m.player)==0,200);c.waitTicks(5);
|
||||
c.runOnClient(m -> require(!m.player.isPassenger(),"Repeated Shift+posture does not trigger the separate double-Sneak sitting gesture"));
|
||||
// Confirm the purchased aptitude restores actual native swimming, not just its server flag.
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.getFoodData().setFoodLevel(8);for(int x=-2;x<=2;x++)for(int z=-2;z<=2;z++)for(int y=1;y<=4;y++)p.level().setBlockAndUpdate(floor.offset(x,y,z),Blocks.WATER.defaultBlockState());p.teleportTo(floor.getX()+.5,floor.getY()+1,floor.getZ()+.5);});
|
||||
c.waitFor(m -> m.player.isUnderWater() && m.player.getFoodData().getFoodLevel()==8,200);
|
||||
c.getInput().holdKey(o -> o.keyUp);c.getInput().holdKey(o -> o.keySprint);c.waitFor(m -> m.player.isSwimming() && m.player.isSprinting(),200);
|
||||
c.getInput().releaseKey(o -> o.keyUp);c.getInput().releaseKey(o -> o.keySprint);
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++)for(int y=1;y<=5;y++)p.level().setBlockAndUpdate(floor.offset(x,y,z),Blocks.AIR.defaultBlockState());p.teleportTo(floor.getX()+.5,floor.getY()+1,floor.getZ()+.5);});
|
||||
c.waitFor(m -> m.player.onGround() && !m.player.isInWater(),200);c.waitTicks(5);
|
||||
// Native recipe data can remember an open book; every actual screen still omits it.
|
||||
xpSounds.set(0);
|
||||
c.runOnClient(m -> {for(var type:RecipeBookType.values())m.player.getRecipeBook().setOpen(type,true);m.gui.setScreen(new InventoryScreen(m.player));});c.waitTick();
|
||||
c.runOnClient(m -> bookRemoved(m.gui.screen()));c.takeScreenshot("sanctuary-beta017-inventory-no-book");
|
||||
c.waitFor(m -> (TutorialClient.completedMask&8)!=0 && xpSounds.get()==1,200);
|
||||
c.runOnClient(m -> {for(int i=0;i<20;i++)net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking.send(new fr.koka.sanctuary.tutorial.TutorialPayloads.InventoryOpened());});
|
||||
c.waitTicks(5);require(xpSounds.get()==1,"Replaying the first-step signal never replays its XP sound");
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.inventoryMenu.getSlot(1).set(new ItemStack(Items.OAK_LOG));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m -> m.player.inventoryMenu.getSlot(0).getItem().is(Items.OAK_PLANKS),200);
|
||||
c.runOnClient(m -> {
|
||||
var screen=(AbstractContainerScreen<?>)m.gui.screen();var a=(AbstractContainerScreenAccessor)screen;var slot=screen.getMenu().getSlot(0);var w=m.getWindow();
|
||||
m.mouseHandler.onMove(w.handle(),(a.sanctuary$getLeftPos()+slot.x+8.0)*w.getScreenWidth()/w.getGuiScaledWidth(),(a.sanctuary$getTopPos()+slot.y+8.0)*w.getScreenHeight()/w.getGuiScaledHeight(),0,0);m.mouseHandler.handleAccumulatedMovement();
|
||||
System.out.println("Recipe result pointer: "+(a.sanctuary$getLeftPos()+slot.x+8)+","+(a.sanctuary$getTopPos()+slot.y+8)+" slot="+slot.getItem());
|
||||
});c.waitTick();c.takeScreenshot("sanctuary-beta017-craft-hover");
|
||||
c.runOnClient(m -> {
|
||||
var w=m.getWindow();m.mouseHandler.onButton(w.handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0),1);m.mouseHandler.onButton(w.handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0),0);
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("sanctuary-beta017-craft-click");
|
||||
c.runOnClient(m -> System.out.println("Recipe click: carried="+m.player.inventoryMenu.getCarried()+" result="+m.player.inventoryMenu.getSlot(0).getItem()+" input="+m.player.inventoryMenu.getSlot(1).getItem()));
|
||||
c.waitFor(m -> m.player.inventoryMenu.getCarried().is(Items.OAK_PLANKS),200);
|
||||
c.waitFor(m -> xpSounds.get()==2,200);
|
||||
c.runOnClient(m -> m.player.closeContainer());c.waitTicks(3);
|
||||
for(var type:List.of(MenuType.CRAFTING,MenuType.FURNACE,MenuType.BLAST_FURNACE,MenuType.SMOKER)) {
|
||||
server.runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();var at=floor.east();p.level().setBlockAndUpdate(at,Blocks.CRAFTING_TABLE.defaultBlockState());
|
||||
p.openMenu(new SimpleMenuProvider((id,inv,owner) -> type==MenuType.CRAFTING ? new CraftingMenu(id,inv,ContainerLevelAccess.create(p.level(),at)) : type.create(id,inv),Component.literal("Recipe screen")));});
|
||||
c.waitFor(m -> m.gui.screen() instanceof AbstractRecipeBookScreen<?>,200);
|
||||
for(int gui:new int[]{0,1}) {c.runOnClient(m -> {m.options.guiScale().set(gui);m.resizeGui();});c.waitTick();c.runOnClient(m -> bookRemoved(m.gui.screen()));}
|
||||
c.takeScreenshot("sanctuary-beta017-no-book-"+type.toString().replaceAll("[^a-zA-Z0-9_-]","_"));
|
||||
c.runOnClient(m -> m.player.closeContainer());c.waitForScreen(null);c.waitTicks(3);
|
||||
}
|
||||
System.out.println("Movement017 native client passed: skill prices, slow water movement, buoyancy, purchases, layout-aware W/Shift+W, ten-second rest, remapping, five bookless screens, manual crafting and once-only first-step XP sounds");
|
||||
} finally {
|
||||
armed.set(false);c.getInput().releaseKey(o -> o.keyUp);c.getInput().releaseKey(o -> o.keySprint);c.getInput().releaseKey(o -> o.keyJump);c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.runOnClient(m -> m.getSoundManager().removeListener(soundListener));
|
||||
c.runOnClient(m -> {m.options.keyUp.setKey(forward);KeyMapping.get("sanctuary.progression.lying_key").setKey(previous);KeyMapping.resetMapping();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();});
|
||||
}
|
||||
}
|
||||
private static void prices(Screen screen) {
|
||||
var all=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).toList();
|
||||
for(String skill:ProgressRecord.SKILLS) {
|
||||
var name=all.stream().filter(w -> key(w,"sanctuary.progression."+skill)).findFirst().orElseThrow();
|
||||
var price=all.stream().filter(w -> w.getY()==name.getY() && (key(w,"sanctuary.progression.next_cost_short") || key(w,"sanctuary.progression.rank_max_short"))).findFirst().orElseThrow();
|
||||
int cost=ProgressionClient.progress.cost(skill,ProgressionClient.config);
|
||||
require(price.getMessage().getString().equals(ProgressionService.label(cost<0 ? "rank_max_short" : "next_cost_short",cost).getString()),"Each skill displays its next server price");
|
||||
require(all.stream().noneMatch(w -> w.getY()==name.getY() && w.getMessage().getString().matches("\\d+/\\d+")),"No redundant skill fraction");
|
||||
}
|
||||
}
|
||||
private static void bookRemoved(Screen screen) {
|
||||
require(screen instanceof AbstractRecipeBookScreen<?>,"Native recipe screen expected");
|
||||
require(widgets(screen).noneMatch(RecipeBookComponent.class::isInstance),"No focusable recipe panel");
|
||||
require(widgets(screen).noneMatch(ImageButton.class::isInstance),"No native recipe toggle button");
|
||||
try {var field=AbstractRecipeBookScreen.class.getDeclaredField("recipeBookComponent");field.setAccessible(true);var book=(RecipeBookComponent<?>)field.get(screen);book.toggleVisibility();require(!book.isVisible(),"Old state or toggle cannot reopen the native book");}
|
||||
catch(ReflectiveOperationException e) {throw new AssertionError(e);}
|
||||
}
|
||||
private static Button purchase(Screen screen,String aptitude) {
|
||||
var name=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w -> key(w,"sanctuary.progression."+aptitude)).findFirst().orElseThrow();
|
||||
return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> b.getX()==name.getX() && b.getY()>name.getY()).min(Comparator.comparingInt(Button::getY)).orElseThrow();
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(GuiEventListener widget) {return Stream.concat(Stream.of(widget),widget instanceof ContainerEventHandler parent ? parent.children().stream().flatMap(Movement017ClientChecks::widgets) : Stream.empty());}
|
||||
private static boolean key(AbstractWidget widget,String key) {return widget.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key);}
|
||||
private static Button button(Screen screen,String key) {return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> key(b,key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter() {return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.knowledge.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.recipes.*;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.inventory.*;
|
||||
import net.minecraft.client.gui.screens.options.OptionsScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/** Real HUD, options persistence, native advancement progress, and enlarged hotbar input. */
|
||||
public final class Notifications034ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(armed.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);
|
||||
}});
|
||||
c.runOnClient(m->{var config=NotificationClient.options();config.discoveries=true;config.collections=true;config.advancements=true;config.seconds=5;});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary notifications034 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&RecipeClient.ready&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();c.runOnClient(m->{m.player.setXRot(-90);m.options.guiScale().set(2);m.resizeGui();});c.waitTicks(25);c.runOnClient(m->NotificationClient.clearFeed());
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();BlockKnowledgeService.recordSeen(p,Blocks.AMETHYST_BLOCK);});
|
||||
c.waitFor(m->NotificationClient.messages().stream().anyMatch(e->e.key().equals("block:minecraft:amethyst_block")),100);
|
||||
c.runOnClient(m->{require(NotificationClient.visible(),"Discovery HUD is visible without F3");m.gui.setScreen(new PauseScreen(true));
|
||||
require(!hasButton(m.gui.screen(),"sanctuary.cycle.faction")&&!hasButton(m.gui.screen(),"sanctuary.cycle.log"),"Factions and History removed from pause");
|
||||
button(m.gui.screen(),"sanctuary.progression.progress").onPress(enter());
|
||||
button(m.gui.screen(),"sanctuary.cycle.entry").onPress(enter());});
|
||||
c.waitFor(m->m.gui.screen() instanceof CycleFactionScreen&&hasButton(m.gui.screen(),"sanctuary.cycle.faction"),100);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.cycle.faction").onPress(enter());button(m.gui.screen(),"sanctuary.cycle.log").onPress(enter());require(!button(m.gui.screen(),"sanctuary.cycle.log").active,"Both remain reachable through Progression");});
|
||||
c.waitTicks(125);c.runOnClient(m->{require(!NotificationClient.messages().isEmpty(),"Unread messages wait while a menu is open");m.gui.setScreen(null);});
|
||||
c.getInput().pressKey(InputConstants.KEY_F1);c.runOnClient(m->require(!NotificationClient.visible(),"F1 hides discovery HUD"));c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(0,new ItemStack(Items.AMETHYST_BLOCK,3));p.getInventory().setItem(1,new ItemStack(Items.OAK_PLANKS,32));p.getInventory().setItem(2,new ItemStack(Items.DIAMOND));BlockKnowledgeService.observeInventory(p);RecipeService.refresh(p);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->NotificationClient.messages().stream().anyMatch(e->e.key().equals("recipes")),150);
|
||||
c.runOnClient(m->{require(NotificationClient.messages().size()<=5,"HUD line count is bounded");require(NotificationClient.messages().stream().anyMatch(e->e.key().equals("item:minecraft:diamond")),"Non-block item has its discovery notice");});
|
||||
c.takeScreenshot("sanctuary-beta034-discoveries-en");
|
||||
// The real native advancement criteria feed the tracker; no synthetic quest is installed.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
for(String id:List.of("husbandry/root","husbandry/plant_seed")){var holder=s.getAdvancements().get(Identifier.withDefaultNamespace(id));for(String criterion:holder.value().criteria().keySet())p.getAdvancements().award(holder,criterion);}
|
||||
var holder=s.getAdvancements().get(Identifier.withDefaultNamespace("husbandry/balanced_diet"));holder.value().criteria().keySet().stream().sorted().limit(2).forEach(criterion->p.getAdvancements().award(holder,criterion));
|
||||
});
|
||||
c.waitFor(m->!NotificationClient.tracked().isEmpty(),200);c.runOnClient(m->require(NotificationClient.tracked().stream().allMatch(t->t.fraction()>0&&t.fraction()<1),"Tracker only shows native partially completed objectives"));c.takeScreenshot("sanctuary-beta034-advancements");
|
||||
c.runOnClient(m->m.gui.setScreen(new OptionsScreen(null,m.options)));
|
||||
for(int scale:List.of(1,2,3,4)){c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();var b=button(m.gui.screen(),"sanctuary.notifications.title");require(b.getY()>=0&&b.getY()+b.getHeight()<=m.gui.screen().height,"Options entry fits GUI scale");});c.waitTick();}
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();button(m.gui.screen(),"sanctuary.notifications.title").onPress(enter());});c.takeScreenshot("sanctuary-beta034-options-en");
|
||||
c.runOnClient(m->{option(m.gui.screen(),"sanctuary.notifications.discoveries").onPress(enter());require(!NotificationClient.options().discoveries&&!NotificationOptions.load().discoveries,"Option persists to disk");m.gui.setScreen(null);});
|
||||
server.runOnServer(s->BlockKnowledgeService.recordSeen(s.getPlayerList().getPlayers().getFirst(),Blocks.DIAMOND_BLOCK));c.waitTicks(30);
|
||||
c.runOnClient(m->{require(NotificationClient.messages().isEmpty(),"Disabled feed discards live notices");m.gui.setScreen(new NotificationOptionsScreen(null));option(m.gui.screen(),"sanctuary.notifications.discoveries").onPress(enter());m.gui.setScreen(null);});c.waitTicks(15);
|
||||
c.runOnClient(m->require(NotificationClient.messages().isEmpty(),"Re-enabling does not replay disabled notices"));
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.options.languageCode=locale;m.reloadResourcePacks();});c.waitTicks(60);
|
||||
c.runOnClient(m->{m.gui.setScreen(new NotificationOptionsScreen(null));for(var widget:widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).toList())require(!widget.getMessage().getString().contains("sanctuary.notifications."),"No untranslated options");});c.takeScreenshot("sanctuary-beta034-options-"+locale);c.runOnClient(m->m.gui.setScreen(null));
|
||||
}
|
||||
c.runOnClient(m->{NotificationClient.options().seconds=3;NotificationClient.clearFeed();});
|
||||
server.runOnServer(s->BlockKnowledgeService.recordSeen(s.getPlayerList().getPlayers().getFirst(),Blocks.EMERALD_BLOCK));c.waitFor(m->!NotificationClient.messages().isEmpty(),100);c.waitTicks(80);c.runOnClient(m->require(NotificationClient.messages().isEmpty(),"Visible messages automatically expire"));
|
||||
hotbar(c,server);
|
||||
System.out.println("HUD034 native passed: discoveries, grouped recipes, expiry, F1/menu hiding, persisted options, native advancement tracker, compact pause, enlarged hotbar input and six rows.");
|
||||
}finally{c.runOnClient(m->{var config=NotificationClient.options();config.discoveries=true;config.collections=true;config.advancements=true;config.seconds=5;config.save();});}
|
||||
}
|
||||
private static void hotbar(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->m.gui.setScreen(new InventoryScreen(m.player)));c.waitTick();
|
||||
c.runOnClient(m->geometry(m));c.takeScreenshot("sanctuary-beta034-hotbar-one-row");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressEffects.get(p);while(progress.inventory()<5)progress=progress.buy("inventory",1);p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);p.getInventory().setItem(0,new ItemStack(Items.STONE,5));p.getInventory().setItem(8,new ItemStack(Items.APPLE,16));p.inventoryMenu.broadcastChanges();ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.inventory()==5,200);c.runOnClient(m->m.gui.setScreen(new InventoryScreen(m.player)));c.waitTicks(3);c.runOnClient(m->geometry(m));c.takeScreenshot("sanctuary-beta034-hotbar-six-rows");
|
||||
// Click the newly enlarged right edge of the last cell, beyond the old 16px item box.
|
||||
c.runOnClient(m->edgeClick(m,44,0));c.waitFor(m->m.player.containerMenu.getCarried().is(Items.APPLE),100);c.runOnClient(m->edgeClick(m,44,0));c.waitFor(m->m.player.containerMenu.getCarried().isEmpty(),100);
|
||||
// Shift-click uses the same enlarged target and native quick transfer.
|
||||
c.runOnClient(m->edgeClick(m,44,1));c.waitFor(m->m.player.getInventory().getItem(8).isEmpty(),100);
|
||||
c.getInput().pressKey(InputConstants.KEY_TAB);c.waitTicks(12);c.runOnClient(m->geometry(m));c.takeScreenshot("sanctuary-beta034-hotbar-moved");
|
||||
c.runOnClient(m->{var screen=m.gui.screen();var reverse=new KeyEvent(InputConstants.KEY_TAB,0,1);
|
||||
net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents.allowKeyPress(screen).invoker().allowKeyPress(screen,reverse);
|
||||
net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents.afterKeyRelease(screen).invoker().afterKeyRelease(screen,reverse);});
|
||||
c.waitFor(m->fr.koka.sanctuary.inventory.HotbarRows.selected(m.player)==0,100);
|
||||
Inventory013WheelChecks.run(c,server);
|
||||
}
|
||||
private static void geometry(Minecraft m){
|
||||
var screen=(InventoryScreen)m.gui.screen();var menu=screen.getMenu();var hot=menu.slots.get(36);
|
||||
require(menu.slots.get(44).x-hot.x==160,"Nine hotbar cells use the native 20px spacing");
|
||||
var canvas=(AbstractContainerScreenAccessor)screen;
|
||||
require(hot.x>=0&&menu.slots.get(44).x+18<=208,"Expanded hotbar fits the inventory width");
|
||||
for(var slot:menu.slots)if(slot.x>=0&&slot.y>=120&&slot!=hot&&slot.y!=hot.y)require(slot.y+16<=hot.y-1||slot.y>=hot.y+19,"Moving row reserves space for the taller hotbar");
|
||||
}
|
||||
private static void edgeClick(Minecraft m,int index,int mods){
|
||||
var screen=(InventoryScreen)m.gui.screen();var canvas=(AbstractContainerScreenAccessor)screen;var slot=screen.getMenu().slots.get(index);
|
||||
var event=new MouseButtonEvent(canvas.sanctuary$getLeftPos()+slot.x+17.5,canvas.sanctuary$getTopPos()+slot.y+8,new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,mods));screen.mouseClicked(event,false);screen.mouseReleased(event);
|
||||
}
|
||||
private static CycleButton<?> option(Screen s,String key){String prefix=Component.translatable(key).getString();return widgets(s).filter(CycleButton.class::isInstance).map(CycleButton.class::cast).filter(b->b.getMessage().getString().startsWith(prefix)).findFirst().orElseThrow();}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static Button button(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow(()->new AssertionError("Missing "+key));}
|
||||
private static boolean hasButton(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).anyMatch(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.client.shader.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.operator.OperatorService;
|
||||
import fr.koka.sanctuary.stars.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
public final class Operator040ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
// Reproduce the report's exact cold-client state before any integrated server binds components.
|
||||
c.runOnClient(m->{
|
||||
require(m.level==null&&m.player==null,"Cold welcome precedes world admission");
|
||||
boolean unbound=false;try{new ItemStack(Items.COW_SPAWN_EGG);}catch(NullPointerException expected){unbound=expected.getMessage().equals("Components not bound yet");}
|
||||
require(unbound,"Cold client reproduces the old ItemStack failure");
|
||||
var choices=List.of(new StarterCompanions.Choice("cow",fr.koka.sanctuary.companion.CompanionType.Rarity.COMMON),new StarterCompanions.Choice("chicken",fr.koka.sanctuary.companion.CompanionType.Rarity.COMMON),new StarterCompanions.Choice("bee",fr.koka.sanctuary.companion.CompanionType.Rarity.UNCOMMON));
|
||||
var colors=java.util.stream.IntStream.range(0,20).map(i->0x604040+i*0x050709).boxed().toList();
|
||||
m.gui.setScreen(new HelloWorldScreen(new StarterPayloads.Welcome(new ProgressionPayloads.Hello("Cold multiplayer",colors,""),"",false,choices),()->{}));
|
||||
});
|
||||
c.waitTicks(12);c.takeScreenshot("sanctuary-beta040-cold-multiplayer-welcome");
|
||||
c.runOnClient(m->{require(m.gui.screen() instanceof HelloWorldScreen,"Unbound welcome renders without ItemStack access");Starter039TestSupport.selectFirst((HelloWorldScreen)m.gui.screen());m.gui.setScreen(new TitleScreen());});
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(s).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().ifPresent(b->b.setValue("Explorer les îles avec mon compagnon."));
|
||||
click(s,"Enter Sanctuary");onboarding.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
int[] remembered={0};String[] rememberedRecipes={null};
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary operator040 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(false);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&RecipeClient.ready&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
c.waitFor(m->StarClient.starCount()>=1,100);c.runOnClient(m->{require(!ProgressionClient.operator&&!RecipeClient.operator,"Commands OFF starts in normal mode");m.gui.setScreen(new ProgressionScreen(null));});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressRecord.INITIAL.buy("vitality",1);p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");p.setExperienceLevels(20);});
|
||||
c.waitTicks(6);c.runOnClient(m->require(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->b.visible&&b.getMessage().getString().equals("−")),"No operator refund controls in normal play"));
|
||||
c.takeScreenshot("sanctuary-beta040-normal-progression");
|
||||
c.runOnClient(m->m.gui.setScreen(new WorldOptionsScreen(new PauseScreen(true),m.level)));c.waitTicks(4);
|
||||
c.runOnClient(m->{require(!widget(m.gui.screen(),"Default Game Mode").visible,"Normal World Options hides operator game mode");click(m.gui.screen(),"Allow Commands");click(m.gui.screen(),"Apply Changes");});
|
||||
c.waitFor(m->ProgressionClient.operator&&RecipeClient.operator,150);
|
||||
require(server.computeOnServer(s->OperatorService.allowed(s.getPlayerList().getPlayers().getFirst())),"Native toggle authorizes server operator actions");
|
||||
c.runOnClient(m->m.gui.setScreen(new ProgressionScreen(null)));c.waitTicks(6);
|
||||
c.runOnClient(m->require(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).anyMatch(b->b.visible&&b.active&&b.getMessage().getString().equals("−")),"Operator refund control available immediately"));
|
||||
c.takeScreenshot("sanctuary-beta040-operator-progression");
|
||||
c.runOnClient(m->m.gui.setScreen(new SanctuaryBlocodexScreen(null)));c.waitTicks(30);c.takeScreenshot("sanctuary-beta040-operator-discovery");
|
||||
c.runOnClient(m->m.gui.setScreen(new SanctuaryAtlasScreen(null)));c.waitTicks(35);c.takeScreenshot("sanctuary-beta040-operator-atlas");
|
||||
c.runOnClient(m->m.gui.setScreen(new WorldOptionsScreen(m.gui.screen(),m.level)));c.waitTicks(4);
|
||||
c.runOnClient(m->{click(m.gui.screen(),"Allow Commands");click(m.gui.screen(),"Apply Changes");});
|
||||
c.waitFor(m->!ProgressionClient.operator&&!RecipeClient.operator,150);c.waitTicks(6);
|
||||
require(server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();return !OperatorService.allowed(p)&&!ProgressionService.refund(p,"vitality",ProgressEffects.get(p).revision());}),"Revoked operator cannot refund server-side");
|
||||
c.takeScreenshot("sanctuary-beta040-normal-atlas");
|
||||
for(var item:List.of(Items.CAT_SPAWN_EGG,Items.PARROT_SPAWN_EGG,Items.GHAST_SPAWN_EGG,Items.ENDER_DRAGON_SPAWN_EGG)){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var egg=new ItemStack(item);egg.set(DataComponents.CUSTOM_NAME,Component.literal("Azimut"));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->AccessoryAccess.of(m.player).getItem(AccessoryContainer.FAMILIAR).is(item),150);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();CompanionClient.openInhabitant(new PauseScreen(true));});c.waitTicks(8);
|
||||
c.runOnClient(m->{var s=m.gui.screen();require(widgets(s).anyMatch(InhabitantCompanionWidget.class::isInstance),"Native companion preview exists");var skin=widgets(s).filter(PlayerSkinWidget.class::isInstance).map(PlayerSkinWidget.class::cast).findFirst().orElseThrow();require(skin.getHeight()<=skin.getWidth()*4/3,"Portrait fit includes width, no cropped arms");var pet=widgets(s).filter(InhabitantCompanionWidget.class::isInstance).map(InhabitantCompanionWidget.class::cast).findFirst().orElseThrow();require(skin.getRight()<s.width/2&&pet.getX()>s.width/2,"Character left, familiar right, profile sections between");require(widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).noneMatch(w->w.getMessage().getString().contains("Rename its egg")),"No extra help prose on inhabitant page");require(widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(w->w.getMessage().getString().contains("Azimut")),"Custom familiar name appears on inhabitant sheet");});
|
||||
c.takeScreenshot("sanctuary-beta040-inhabitant-"+net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(item).getPath());
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(1);m.resizeGui();m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());CompanionClient.openInhabitant(null);});c.waitTicks(8);c.takeScreenshot("sanctuary-beta040-inhabitant-fr-scale1");
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
for(int armor:new int[]{1,7,20}){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);
|
||||
p.setItemSlot(net.minecraft.world.entity.EquipmentSlot.HEAD,new ItemStack(armor==20?Items.DIAMOND_HELMET:Items.LEATHER_HELMET));
|
||||
p.setItemSlot(net.minecraft.world.entity.EquipmentSlot.CHEST,armor==1?ItemStack.EMPTY:new ItemStack(armor==20?Items.DIAMOND_CHESTPLATE:Items.IRON_CHESTPLATE));
|
||||
p.setItemSlot(net.minecraft.world.entity.EquipmentSlot.LEGS,armor==20?new ItemStack(Items.DIAMOND_LEGGINGS):ItemStack.EMPTY);
|
||||
p.setItemSlot(net.minecraft.world.entity.EquipmentSlot.FEET,armor==20?new ItemStack(Items.DIAMOND_BOOTS):ItemStack.EMPTY);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.getArmorValue()==armor,100);c.runOnClient(m->Inventory012RenderProbe.reset());c.waitTicks(5);
|
||||
c.runOnClient(m->{var sprites=Inventory012RenderProbe.sprites;require(sprites.stream().noneMatch(p->p.id().equals("minecraft:hud/armor_empty")),"No empty armor icons");require(sprites.stream().filter(p->p.id().equals("minecraft:hud/armor_full")).count()==armor/2,"Exactly the full armor points");require(sprites.stream().filter(p->p.id().equals("minecraft:hud/armor_half")).count()==armor%2,"Half armor preserved");});
|
||||
c.takeScreenshot("sanctuary-beta040-armor-"+armor);
|
||||
}
|
||||
server.runOnServer(s->{try{fr.koka.sanctuary.realtime.RealtimeService.setMode(s,false);}catch(Exception e){throw new RuntimeException(e);}s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");var p=s.getPlayerList().getPlayers().getFirst();var at=p.blockPosition().south(7);
|
||||
var blocks=List.of("red_wool","oak_leaves","oak_log","white_wool","blue_wool").stream().map(id->net.minecraft.core.registries.BuiltInRegistries.BLOCK.getValue(net.minecraft.resources.Identifier.withDefaultNamespace(id))).toList();
|
||||
for(int i=0;i<blocks.size();i++)for(int y=0;y<3;y++)p.level().setBlockAndUpdate(at.offset(i-2,y,0),blocks.get(i).defaultBlockState());
|
||||
});
|
||||
c.runOnClient(m->{m.player.setXRot(5);m.player.setYRot(0);VanillaLight.setEnabled(true);m.gui.setScreen(new net.minecraft.client.gui.screens.options.OptionsScreen(null,m.options));});c.waitTicks(6);
|
||||
c.runOnClient(m->click(m.gui.screen(),"Shaders"));c.waitTicks(5);c.takeScreenshot("sanctuary-beta040-shader-options");
|
||||
c.runOnClient(m->{click(m.gui.screen(),"Sanctuary · Vanilla Light");require(!VanillaLight.enabled(),"Native shader option toggles off");m.gui.setScreen(null);});
|
||||
c.waitFor(m->!m.gameRenderer.getAppliedPostEffects().contains(VanillaLight.EFFECT),100);c.waitTicks(6);c.takeScreenshot("sanctuary-beta040-shader-off");
|
||||
c.runOnClient(m->{m.gui.setScreen(new ShaderOptionsScreen(null));click(m.gui.screen(),"Sanctuary · Vanilla Light");require(VanillaLight.enabled(),"Native shader option toggles on");m.gui.setScreen(null);});
|
||||
c.waitFor(m->m.gameRenderer.getAppliedPostEffects().contains(VanillaLight.EFFECT),100);c.waitTicks(6);c.takeScreenshot("sanctuary-beta040-shader-on");
|
||||
var reload=c.computeOnClient(m->m.reloadResourcePacks());c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();
|
||||
c.waitFor(m->m.gameRenderer.getAppliedPostEffects().contains(VanillaLight.EFFECT),100);
|
||||
server.runOnServer(s->{s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");var p=s.getPlayerList().getPlayers().getFirst();for(var holder:s.getAdvancements().getAllAdvancements())if(holder.value().display().isPresent())for(var key:holder.value().criteria().keySet())p.getAdvancements().award(holder,key);fr.koka.sanctuary.recipes.RecipeService.refresh(p);rememberedRecipes[0]=fr.koka.sanctuary.recipes.RecipeService.knowledge(p).json();remembered[0]=StarService.ledger(s).starCount();s.saveEverything(true,true,true);});
|
||||
c.waitFor(m->StarClient.starCount()==remembered[0],200);
|
||||
c.runOnClient(m->{m.player.setXRot(-75);m.player.setYRot(30);Star040RenderProbe.reset();});c.getInput().pressKey(InputConstants.KEY_F1);c.waitTicks(15);c.takeScreenshot("sanctuary-beta040-earned-stars");
|
||||
c.runOnClient(m->require(Star040RenderProbe.draws>0&&Star040RenderProbe.indices==StarClient.starCount()*6,"Native shader star pass renders exactly the earned geometry"));
|
||||
long revision=c.computeOnClient(m->StarClient.revision());c.waitTicks(35);c.runOnClient(m->{require(StarClient.revision()==revision,"Unchanged sky does not rebuild its mesh every frame");});
|
||||
c.runOnClient(m->{Star040RenderProbe.reset();StarClient.accept(new StarPayload(true,System.currentTimeMillis(),List.of()));});c.waitTicks(5);c.takeScreenshot("sanctuary-beta040-empty-sky");
|
||||
c.runOnClient(m->require(Star040RenderProbe.draws>0&&Star040RenderProbe.indices==0,"An empty sky keeps native pass hooks but draws no decorative stars"));c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&StarClient.starCount()==remembered[0],1200);
|
||||
c.runOnClient(m->require(!ProgressionClient.operator,"Commands OFF survives world reload"));
|
||||
require(world.getServer().computeOnServer(s->StarService.ledger(s).starCount()==remembered[0]),"Reconnect and advancement backfill do not duplicate stars");
|
||||
require(world.getServer().computeOnServer(s->fr.koka.sanctuary.recipes.RecipeService.knowledge(s.getPlayerList().getPlayers().getFirst()).json().equals(rememberedRecipes[0])),"Complete recipe catalogue survives native player save and reconnect");
|
||||
}
|
||||
System.out.println("OPERATOR040_CLIENT_PASS: native World Options toggle, immediate authorization/revocation, normal hidden controls, companion previews and saved shared sky");
|
||||
System.out.println("SHADER040_CLIENT_PASS: native options, GPU post effect on/off, resource reload, earned/empty native star pass");
|
||||
System.out.println("MULTIPLAYER040_CLIENT_PASS: cold unbound-component welcome; integrated reconnect preserves full recipe catalogue; dedicated network test not run");
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static AbstractWidget widget(Screen s,String prefix){return widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.getMessage().getString().startsWith(prefix)).findFirst().orElseThrow(()->new AssertionError("Missing "+prefix+": "+widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).map(w->w.getMessage().getString()).toList()));}
|
||||
private static void click(Screen s,String prefix){var w=widget(s,prefix);require(w.visible&&w.active,"Control available: "+prefix);if(w instanceof Button b)b.onPress(enter());else if(w instanceof CycleButton<?> b)b.onPress(enter());else throw new AssertionError("Not a button: "+prefix);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.boats.*;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Poultry048ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b,"sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{
|
||||
s.setName("Poultry048 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(true);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
for(String locale:new String[]{"en_us","fr_fr"})c.runOnClient(m->{
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
for(var v:CrewBoats.variants()){
|
||||
var boat=v.type().create(m.level,EntitySpawnReason.COMMAND);
|
||||
require(m.getEntityRenderDispatcher().getRenderer(boat)!=null,"Native renderer for "+v.wood());
|
||||
var mesh=TiledBoatModel.hull(v.columns(),v.rows(),v.raft()).bakeRoot();
|
||||
require(mesh.getAllParts().size()==v.tiles()+3+(v.raft()?0:2*(v.columns()+v.rows())+4),"One floor per cell, only exterior rails and two paddles");
|
||||
require(mesh.getAllParts().stream().allMatch(part->part.xScale==1&&part.yScale==1&&part.zScale==1),"No stretched model part");
|
||||
require(mesh.hasChild("left_paddle")&&mesh.hasChild("right_paddle"),"Exactly one native pair of paddles");
|
||||
require(TiledBoatModel.waterMask(v.columns(),v.rows()).bakeRoot().getAllParts().size()==v.tiles()+1,"Water mask covers every floor tile");
|
||||
require(!new ItemStack(v.item()).getHoverName().getString().startsWith("item.sanctuary"),"Localized item name "+locale);
|
||||
}
|
||||
});
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.getInventory().clearContent();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
var boat=CrewBoats.variant("oak",2).type().create(s.overworld(),EntitySpawnReason.COMMAND);boat.setPos(p.position().add(0,30,0));boat.setYRot(0);
|
||||
s.overworld().addFreshEntity(boat);var chicken=EntityTypes.CHICKEN.create(s.overworld(),EntitySpawnReason.COMMAND);chicken.setPos(boat.position());chicken.setNoAi(true);s.overworld().addFreshEntity(chicken);chicken.startRiding(boat);p.startRiding(boat,true,true);
|
||||
});
|
||||
c.waitFor(m->m.player.getVehicle() instanceof CrewBoat b&&b.columns()==2&&b.rows()==2,200);
|
||||
c.getInput().holdKey(o->o.keyJump);c.getInput().holdKey(o->o.keyUp);c.waitTicks(35);
|
||||
Vec3 initial=c.computeOnClient(m->m.player.getVehicle().position());c.waitTicks(110);Vec3 top=c.computeOnClient(m->m.player.getVehicle().position());
|
||||
require(top.y-initial.y>12&&top.y-initial.y<23,"Collective chicken boat climbs after native key inputs and sustained flight validation");
|
||||
require(top.subtract(initial).horizontalDistance()>25,"Native forward key moves server-authoritative boat");
|
||||
require(server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getVehicle() instanceof CrewBoat b&&b.getControllingPassenger()==null&&b.orders().forward()==1),"Server receives direct pilot input without a client owner");
|
||||
c.runOnClient(m->m.options.setCameraType(CameraType.THIRD_PERSON_BACK));c.getInput().lookAt(25,20);c.waitTicks(8);c.takeScreenshot("sanctuary-beta048-2x2-flight");
|
||||
c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(30);
|
||||
double downStart=c.computeOnClient(m->m.player.getVehicle().getY());c.waitTicks(70);double bottom=c.computeOnClient(m->m.player.getVehicle().getY());
|
||||
require(downStart-bottom>17,"Releasing Space descends under native server ticks");
|
||||
c.getInput().holdKey(o->o.keyJump);c.getInput().holdKey(o->o.keyRight);float yaw=c.computeOnClient(m->m.player.getVehicle().getYRot());c.waitTicks(30);
|
||||
require(Math.abs(c.computeOnClient(m->m.player.getVehicle().getYRot())-yaw)>45,"Native right key steers collective boat");c.getInput().releaseKey(o->o.keyRight);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var old=p.getVehicle();Vec3 at=old.position();p.stopRiding();old.getSelfAndPassengers().toList().reversed().forEach(Entity::discard);
|
||||
var boat=CrewBoats.variant("bamboo",3).type().create(s.overworld(),EntitySpawnReason.COMMAND);boat.setPos(at);s.overworld().addFreshEntity(boat);
|
||||
for(var type:List.of(EntityTypes.CHICKEN,EntityTypes.BAT,EntityTypes.PARROT,EntityTypes.BEE,EntityTypes.BLAZE,EntityTypes.ALLAY,EntityTypes.BREEZE)){
|
||||
var motor=type.create(s.overworld(),EntitySpawnReason.COMMAND);motor.setPos(at);((Mob)motor).setNoAi(true);s.overworld().addFreshEntity(motor);motor.startRiding(boat);
|
||||
}p.startRiding(boat,true,true);
|
||||
});
|
||||
c.waitFor(m->m.player.getVehicle() instanceof CrewBoat b&&b.columns()==3&&b.rows()==3&&b.getPassengers().size()==8,200);c.waitTicks(20);
|
||||
double mixedStart=c.computeOnClient(m->m.player.getVehicle().getY());c.waitTicks(40);
|
||||
require(c.computeOnClient(m->m.player.getVehicle().getY())-mixedStart>15,"Mixed motors lift 3x3 raft");
|
||||
c.getInput().lookAt(30,35);c.waitTicks(8);c.takeScreenshot("sanctuary-beta048-3x3-seven-motors");
|
||||
// The ordinary boat still uses native client ownership; exercise the separate motor-control path.
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var old=p.getVehicle();Vec3 at=old.position();p.stopRiding();old.getSelfAndPassengers().toList().reversed().forEach(Entity::discard);
|
||||
var boat=EntityTypes.OAK_BOAT.create(s.overworld(),EntitySpawnReason.COMMAND);boat.setPos(at);boat.setYRot(0);s.overworld().addFreshEntity(boat);
|
||||
var bat=EntityTypes.BAT.create(s.overworld(),EntitySpawnReason.COMMAND);bat.setPos(at);bat.setNoAi(true);s.overworld().addFreshEntity(bat);bat.startRiding(boat);p.startRiding(boat,true,true);
|
||||
});
|
||||
c.waitFor(m->m.player.getVehicle() instanceof net.minecraft.world.entity.vehicle.boat.AbstractBoat b&&!(b instanceof CrewBoat)&&b.getControllingPassenger()==m.player,200);
|
||||
c.getInput().holdKey(o->o.keyUp);c.waitTicks(30);Vec3 batStart=c.computeOnClient(m->m.player.getVehicle().position());c.waitTicks(60);Vec3 batEnd=c.computeOnClient(m->m.player.getVehicle().position());
|
||||
require(batEnd.y-batStart.y>8&&batEnd.subtract(batStart).horizontalDistance()>12,"Native ordinary bat boat receives forward and lift controls");
|
||||
c.getInput().releaseKey(o->o.keyJump);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var old=p.getVehicle();var at=old.position();p.stopRiding();old.getSelfAndPassengers().toList().reversed().forEach(Entity::discard);
|
||||
var ghast=EntityTypes.HAPPY_GHAST.create(s.overworld(),EntitySpawnReason.COMMAND);ghast.setPos(at);ghast.setYRot(0);
|
||||
ghast.setItemSlot(EquipmentSlot.BODY,new ItemStack(net.minecraft.core.registries.BuiltInRegistries.ITEM.getValue(net.minecraft.resources.Identifier.withDefaultNamespace("white_harness"))));
|
||||
s.overworld().addFreshEntity(ghast);p.startRiding(ghast,true,true);
|
||||
});
|
||||
c.waitFor(m->m.player.getVehicle() instanceof net.minecraft.world.entity.animal.happyghast.HappyGhast g&&g.getControllingPassenger()==m.player&&CrewBoats.boosted(g),300);
|
||||
c.getInput().lookAt(0,0);c.waitTicks(60);Vec3 ghastFastStart=c.computeOnClient(m->m.player.getVehicle().position());c.waitTicks(50);double fast=c.computeOnClient(m->m.player.getVehicle().position().subtract(ghastFastStart).horizontalDistance());
|
||||
server.runOnServer(s->{var g=(net.minecraft.world.entity.animal.happyghast.HappyGhast)s.getPlayerList().getPlayers().getFirst().getVehicle();g.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.FLYING_SPEED).removeModifier(fr.koka.sanctuary.SanctuaryMod.id("happy_ghast_speed"));});
|
||||
c.waitFor(m->m.player.getVehicle() instanceof net.minecraft.world.entity.animal.happyghast.HappyGhast g&&!CrewBoats.boosted(g),100);c.waitTicks(60);
|
||||
Vec3 ghastSlowStart=c.computeOnClient(m->m.player.getVehicle().position());c.waitTicks(50);double slow=c.computeOnClient(m->m.player.getVehicle().position().subtract(ghastSlowStart).horizontalDistance());
|
||||
require(slow>1&&fast/slow>1.85&&fast/slow<2.15,"Happy Ghast native ridden speed doubles after synced modifier: "+fast/slow);
|
||||
System.out.println("POULTRY048_CLIENT_PASS: 22 tiled native-texture renderers and FR/EN names, actual key packets, collective server-authoritative flight beyond watchdog, forward/steering/ascent/descent, mixed seven-motor raft, ordinary bat boat, Happy Ghast measured x2 speed and screenshots");
|
||||
}finally{onboarding.set(false);c.getInput().releaseKey(o->o.keyJump);c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyRight);}
|
||||
}
|
||||
private static boolean key(Button b,String key){return b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key);}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean yes,String message){if(!yes)throw new AssertionError(message);}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.PauseScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
|
||||
/** Native UI, real pre-spawn handshake and progression packets in a fresh ignored Sanctuary world. */
|
||||
public final class Progression003ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent PRESS=new KeyEvent(InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext context) {
|
||||
require(InhabitantColorNames.key(0x509DDB).equals("azure") && InhabitantColorNames.key(0xF49CAF).equals("grapefruit"),
|
||||
"Color names describe the offered hues");
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
int previousScale=context.computeOnClient(client -> client.options.guiScale().get());
|
||||
Screen previous=context.computeOnClient(client -> client.gui.screen());
|
||||
var offer=new ProgressionPayloads.Hello("poupoutain",java.util.stream.IntStream.range(0,20)
|
||||
.map(i -> InhabitantStore.color(42,i)).boxed().toList(),"");
|
||||
context.runOnClient(client -> {
|
||||
client.options.guiScale().set(0);client.resizeGui();
|
||||
client.gui.setScreen(new HelloWorldScreen(offer,() -> { }));
|
||||
bounds(client.gui.screen());
|
||||
});
|
||||
context.waitTick();context.takeScreenshot("sanctuary-beta003-hello-minimum");
|
||||
context.runOnClient(client -> {
|
||||
client.options.guiScale().set(2);client.resizeGui();bounds(client.gui.screen());
|
||||
});
|
||||
context.waitTick();context.takeScreenshot("sanctuary-beta003-hello-large");
|
||||
String initialLanguage=context.computeOnClient(client -> client.getLanguageManager().getSelected());
|
||||
Blocodex004ClientChecks.language(context,"fr_fr");
|
||||
context.runOnClient(client -> {
|
||||
var colorButtons=client.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).toList();
|
||||
require(colorButtons.size()==20 && colorButtons.stream().allMatch(b -> b.getMessage().getContents()
|
||||
instanceof net.minecraft.network.chat.contents.TranslatableContents t && t.getKey().startsWith("sanctuary.color.")),
|
||||
"All twenty native colour buttons display translated hue names");
|
||||
});
|
||||
context.takeScreenshot("sanctuary-beta005-couleurs-fr");
|
||||
Blocodex004ClientChecks.language(context,initialLanguage);
|
||||
context.runOnClient(client -> client.gui.setScreen(previous));
|
||||
var armed=new AtomicBoolean(true);var handled=new AtomicBoolean(false);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
if (armed.get() && client.gui.screen() instanceof HelloWorldScreen screen && handled.compareAndSet(false,true)) {
|
||||
require(client.player==null && client.level==null,"HELLO_WORLD precedes client spawn");
|
||||
require(client.getSingleplayerServer().getPlayerCount()==0,"No inhabitant has joined the server before validation");
|
||||
screen.children().stream().filter(EditBox.class::isInstance).map(EditBox.class::cast)
|
||||
.findFirst().orElseThrow().setValue("Exploratrice du vide");
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(PRESS);
|
||||
button(screen,"enter").onPress(PRESS);
|
||||
}
|
||||
});
|
||||
try (var world=context.worldBuilder().adjustSettings(settings -> {
|
||||
settings.setName("Sanctuary beta003 progression "+UUID.randomUUID());
|
||||
settings.setWorldType(settings.getNormalPresetList().stream()
|
||||
.filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary"))
|
||||
.findFirst().orElseThrow());
|
||||
settings.setSeed("42");settings.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
saved=world.getWorldSave();
|
||||
armed.set(false);
|
||||
require(handled.get(),"The real configuration screen appeared exactly on first arrival");
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
context.waitFor(client -> client.player != null && client.gui.screen()==null && ProgressionClient.active(),1200);
|
||||
context.runOnClient(client -> {
|
||||
require(client.player.getMaxHealth()==6 && client.player.getFoodData().getFoodLevel()==6
|
||||
&& client.player.getMaxAirSupply()==90,"3/3/3 reaches the real client");
|
||||
client.player.setSprinting(true);
|
||||
require(!client.player.isSprinting() && !client.player.getFoodData().hasEnoughFood(),
|
||||
"A new client cannot sprint at three full food icons");
|
||||
client.player.getFoodData().eat(20,20);client.player.setSprinting(true);
|
||||
require(!client.player.isSprinting(),"Eating prediction cannot bypass the initial sprint gate");
|
||||
require(ProgressionClient.identity.bio().equals("Exploratrice du vide"),"The submitted bio returns from the server");
|
||||
});
|
||||
context.waitTick();context.takeScreenshot("sanctuary-beta003-hud-333");
|
||||
world.getServer().runOnServer(server -> {
|
||||
var player=server.getPlayerList().getPlayers().getFirst();
|
||||
player.giveExperienceLevels(10);
|
||||
});
|
||||
world.getConnection().waitForClientboundPackets();context.waitTicks(3);
|
||||
context.getInput().pressKey(InputConstants.KEY_B);context.waitForScreen(ProgressionScreen.class);
|
||||
context.runOnClient(client -> {
|
||||
var screen=client.gui.screen();bounds(screen);
|
||||
long planned=descendants(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t
|
||||
&& t.getKey().equals("sanctuary.progression.planned") && !b.active).count();
|
||||
require(planned==0,"All six skills now have real purchases");
|
||||
var scroll=screen.children().stream().filter(net.minecraft.client.gui.components.AbstractScrollArea.class::isInstance)
|
||||
.map(net.minecraft.client.gui.components.AbstractScrollArea.class::cast).findFirst().orElseThrow();
|
||||
require(screen.mouseScrolled(scroll.getX()+10,scroll.getY()+10,0,-2) && scroll.scrollAmount()>0,
|
||||
"The aptitude list scrolls independently");
|
||||
scroll.setScrollAmount(0);
|
||||
button(screen,"upgrade_cost").onPress(PRESS);
|
||||
});
|
||||
context.waitFor(client -> ProgressionClient.progress.vitality()==1,400);
|
||||
context.runOnClient(client -> {
|
||||
require(client.player.experienceLevel==9,"The heart cost one level");
|
||||
button(client.gui.screen(),"unlock_cost").onPress(PRESS);
|
||||
});
|
||||
context.waitFor(client -> ProgressionClient.progress.names(),400);
|
||||
Blocodex004ClientChecks.run(context);
|
||||
Atlas005ClientChecks.run(context,world.getServer());
|
||||
Mining008ClientChecks.run(context,world.getServer());
|
||||
Building009ClientChecks.run(context,world.getServer());
|
||||
context.waitTick();context.takeScreenshot("sanctuary-beta003-first-aptitude");
|
||||
context.runOnClient(client -> {client.options.guiScale().set(0);client.resizeGui();bounds(client.gui.screen());});
|
||||
context.waitTick();context.takeScreenshot("sanctuary-beta003-progression-minimum");
|
||||
context.runOnClient(c -> ProgressionClient.open(null,"identity"));
|
||||
context.runOnClient(client -> bounds(client.gui.screen()));
|
||||
context.waitTick();context.takeScreenshot("sanctuary-beta003-inhabitant");
|
||||
context.runOnClient(c -> ProgressionClient.open(null,"history"));
|
||||
context.waitTick();context.takeScreenshot("sanctuary-beta003-history");
|
||||
context.getInput().pressKey(InputConstants.KEY_ESCAPE);context.waitForScreen(null);
|
||||
context.getInput().pressKey(InputConstants.KEY_ESCAPE);context.waitForScreen(PauseScreen.class);
|
||||
context.runOnClient(client -> {bounds(client.gui.screen());button(client.gui.screen(),"progress").onPress(PRESS);});
|
||||
context.waitForScreen(ProgressionScreen.class);
|
||||
context.runOnClient(c -> ProgressionClient.open(new PauseScreen(true),"blocks"));
|
||||
context.waitForScreen(SanctuaryBlocodexScreen.class);
|
||||
context.clickScreenButton("gui.done");
|
||||
context.waitForScreen(PauseScreen.class);
|
||||
context.runOnClient(client -> client.gui.setScreen(new SanctuaryAtlasScreen(client.gui.screen())));
|
||||
context.waitFor(client -> client.gui.screen() instanceof SanctuaryAtlasScreen a && a.isComplete() && a.observedTiles()>0,600);
|
||||
context.clickScreenButton("gui.done");context.waitForScreen(PauseScreen.class);
|
||||
context.runOnClient(client -> button(client.gui.screen(),"progress").onPress(PRESS));
|
||||
context.waitForScreen(ProgressionScreen.class);
|
||||
context.runOnClient(client -> {
|
||||
var cow=net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getValue(net.minecraft.resources.Identifier.withDefaultNamespace("cow")).create(client.level,EntitySpawnReason.TRIGGERED);
|
||||
client.hitResult=new EntityHitResult(cow);
|
||||
ProgressionClient.names=new MobNamesConfig(1,true,Map.of("minecraft:cow",new MobNamesConfig.Entry(false,List.of("Marguerite"))));
|
||||
require(SanctuaryMobNames.name(cow).getString().equals("Marguerite"),"Custom names reach the personal overlay");
|
||||
cow.setCustomName(Component.literal("Etiquette"));
|
||||
require(SanctuaryMobNames.name(cow)==null,"A player's name tag remains authoritative");
|
||||
});
|
||||
context.runOnClient(client -> client.gui.setScreen(null));
|
||||
world.getServer().runOnServer(server -> {
|
||||
var player=server.getPlayerList().getPlayers().getFirst();
|
||||
require(player.experienceLevel==0,"Server paid all seven purchases once");
|
||||
require(ProgressEffects.get(player).names() && ProgressEffects.get(player).vitality()==1,"Server owns earned progression");
|
||||
player.giveExperienceLevels(60);
|
||||
require(player.getExperienceReward(player.level(),null)==6069,"Vanilla death reward now contains 70 percent of level 60");
|
||||
player.hurtServer(player.level(),player.damageSources().fellOutOfWorld(),Float.MAX_VALUE);
|
||||
});
|
||||
context.waitForScreen(net.minecraft.client.gui.screens.DeathScreen.class);
|
||||
context.waitTicks(45);
|
||||
context.clickScreenButton("deathScreen.respawn");
|
||||
context.waitFor(client -> client.player != null && client.gui.screen()==null && client.player.isAlive() && client.player.getMaxHealth()==8
|
||||
&& ProgressionClient.active() && ProgressionClient.progress.names(),1200);
|
||||
context.getInput().pressKey(InputConstants.KEY_B);context.waitForScreen(ProgressionScreen.class);
|
||||
context.waitTick();context.takeScreenshot("sanctuary-beta003-after-death");
|
||||
world.getServer().runOnServer(server -> {
|
||||
var player=server.getPlayerList().getPlayers().getFirst();
|
||||
require(ProgressEffects.get(player).revision()==7 && ProgressEffects.get(player).mining()==2 && ProgressEffects.get(player).building()==2,"A real respawn preserves the seven purchases including mining and building");
|
||||
server.saveEverything(true,true,true);
|
||||
});
|
||||
} finally {
|
||||
armed.set(false);
|
||||
context.runOnClient(client -> {client.options.guiScale().set(previousScale);client.resizeGui();});
|
||||
}
|
||||
require(!ProgressionClient.active(),"Disconnect clears the previous server's profile");
|
||||
try (var reopened=saved.open()) {
|
||||
reopened.getConnection().waitForChunksDownload(2400);
|
||||
context.waitFor(client -> client.player != null && ProgressionClient.active(),1200);
|
||||
context.runOnClient(client -> {
|
||||
require(!(client.gui.screen() instanceof HelloWorldScreen),"An existing inhabitant bypasses character creation");
|
||||
require(ProgressionClient.progress.names() && ProgressionClient.progress.vitality()==1
|
||||
&& ProgressionClient.progress.atlas() && ProgressionClient.progress.mining()==2 && ProgressionClient.progress.building()==2 && ProgressionClient.progress.revision()==7,"Purchases survive server restart");
|
||||
require(ProgressionClient.identity.bio().equals("Exploratrice du vide"),"Identity survives server restart");
|
||||
});
|
||||
reopened.getServer().runOnServer(server -> {
|
||||
var player=server.getPlayerList().getPlayers().getFirst();
|
||||
require(fr.koka.sanctuary.atlas.AtlasService.observations(player).size()>0,"Personal exploration survives server restart");
|
||||
try {
|
||||
require(!ProgressionService.session(server).inhabitants.arrive(player.getUUID()),"Welcome stays consumed after restart");
|
||||
} catch (java.io.IOException error) { throw new AssertionError(error); }
|
||||
});
|
||||
}
|
||||
}
|
||||
private static Button button(Screen screen,String key) {
|
||||
return descendants(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t
|
||||
&& t.getKey().equals("sanctuary.progression."+key)).findFirst().orElseThrow();
|
||||
}
|
||||
private static java.util.stream.Stream<net.minecraft.client.gui.components.events.GuiEventListener> descendants(
|
||||
net.minecraft.client.gui.components.events.ContainerEventHandler container) {
|
||||
return container.children().stream().flatMap(child -> java.util.stream.Stream.concat(java.util.stream.Stream.of(child),
|
||||
child instanceof net.minecraft.client.gui.components.events.ContainerEventHandler nested
|
||||
? descendants(nested) : java.util.stream.Stream.empty()));
|
||||
}
|
||||
private static void bounds(Screen screen) {
|
||||
for (var child:screen.children()) if (child instanceof AbstractWidget w)
|
||||
require(w.getX()>=0 && w.getY()>=0 && w.getRight()<=screen.width && w.getBottom()<=screen.height,
|
||||
"Widget within screen: "+w.getMessage().getString()+" "+screen.width+"x"+screen.height);
|
||||
}
|
||||
private static void require(boolean condition,String message) { if (!condition) throw new AssertionError(message); }
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.inventory.SanctuaryOverflowContainer;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Real widgets, fonts and purchase packets, in a fresh ignored development world. */
|
||||
public final class Progression013ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (armed.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);
|
||||
}
|
||||
});
|
||||
int scale=c.computeOnClient(m -> m.options.guiScale().get());
|
||||
String language=c.computeOnClient(m -> m.getLanguageManager().getSelected());
|
||||
try (var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Sanctuary beta013 skills "+UUID.randomUUID());
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && ProgressionClient.active() && CycleClient.view!=null && m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(server -> server.getPlayerList().getPlayers().getFirst().setExperienceLevels(0));
|
||||
c.waitFor(m -> m.player.experienceLevel==0,200);
|
||||
c.runOnClient(m -> ProgressionClient.open(null));c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
aligned(m.gui.screen());
|
||||
require(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.noneMatch(b -> b.visible && b.getMessage().getString().equals("+")),"Zero XP hides all skill purchases");
|
||||
require(ProgressionClient.config.statCosts().getLast()==48,"Server sends the migrated default price");
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta013-skills-start");
|
||||
world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();s.getPlayerList().op(p.nameAndId());
|
||||
var progress=ProgressRecord.INITIAL;
|
||||
for(String skill:ProgressRecord.SKILLS) while(progress.rank(skill)<ProgressRecord.cap(skill)-1)
|
||||
progress=progress.buy(skill,progress.cost(skill,ProgressionConfig.DEFAULT));
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);
|
||||
p.setExperienceLevels(47);ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.operator && ProgressionClient.progress.vitality()==6 && m.player.experienceLevel==47,200);c.waitTick();
|
||||
c.runOnClient(m -> require(!adjust(m.gui.screen(),"vitality","+").visible,"47 levels cannot buy the last heart"));
|
||||
world.getServer().runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(48);ProgressionService.send(p,"");});
|
||||
c.waitFor(m -> m.player.experienceLevel==48,200);c.waitTick();
|
||||
for(String locale:new String[]{"en_us","fr_fr"}) {
|
||||
c.runOnClient(m -> {m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});
|
||||
for(int gui:new int[]{1,0}) {
|
||||
c.runOnClient(m -> {m.options.guiScale().set(gui);m.resizeGui();ProgressionClient.open(null);});
|
||||
c.waitTick();c.runOnClient(m -> aligned(m.gui.screen()));
|
||||
c.takeScreenshot("sanctuary-beta013-skills-"+locale+"-gui-"+gui);
|
||||
}
|
||||
}
|
||||
c.runOnClient(m -> m.getWindow().setWindowed(640,480));
|
||||
for(int i=0;i<5;i++)c.waitTick();
|
||||
c.runOnClient(m -> {m.resizeGui();ProgressionClient.open(null);});c.waitTick();
|
||||
c.runOnClient(m -> {aligned(m.gui.screen());System.out.println("beta013 compact GUI: "+m.gui.screen().width+"x"+m.gui.screen().height);});
|
||||
c.takeScreenshot("sanctuary-beta013-skills-fr-compact");
|
||||
c.runOnClient(m -> {var plus=adjust(m.gui.screen(),"vitality","+");require(plus.visible && plus.active,"48 levels enables the last heart");plus.onPress(enter());});
|
||||
c.waitFor(m -> ProgressionClient.progress.vitality()==7 && m.player.experienceLevel==0,200);c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
require(!adjust(m.gui.screen(),"vitality","+").visible,"Maximum hides the plus");
|
||||
var minus=adjust(m.gui.screen(),"vitality","−");require(minus.visible && minus.active,"Operator can refund the last heart");minus.onPress(enter());
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.vitality()==6 && m.player.experienceLevel==48,200);
|
||||
c.runOnClient(m -> require(ProgressionClient.progress.history().getLast().cost()==48,"Exact purchase price refunded through the server"));
|
||||
c.runOnClient(m -> CycleClient.open(null,"faction"));c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
var screen=m.gui.screen();require(!button(screen,"sanctuary.cycle.create").active,"Founding still requires a charge");
|
||||
require(widgets(screen).anyMatch(w -> w instanceof AbstractWidget a && key(a,"sanctuary.cycle.founding_path")),"Faction page explains charge acquisition");
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta013-faction-path");
|
||||
c.runOnClient(m -> button(m.gui.screen(),"sanctuary.cycle.cycle_progress").onPress(enter()));c.waitTick();
|
||||
c.runOnClient(m -> require(!button(m.gui.screen(),"sanctuary.cycle.ngp").active,"Guidance opens Cycle and keeps eligibility enforced"));
|
||||
String originalName=c.computeOnClient(m -> ProgressionClient.identity.name());
|
||||
world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(500);
|
||||
for(String skill:ProgressRecord.SKILLS) while(ProgressEffects.get(p).rank(skill)<ProgressRecord.cap(skill))
|
||||
require(ProgressionService.purchase(p,skill,ProgressEffects.get(p).revision()),"Complete skills for the prestige test");
|
||||
p.getInventory().setItem(0,new ItemStack(Items.DIAMOND,1));
|
||||
p.getInventory().setItem(9,new ItemStack(Items.WHEAT,2));
|
||||
p.getInventory().setItem(35,new ItemStack(Items.COAL,3));
|
||||
p.getInventory().setItem(43,new ItemStack(Items.IRON_INGOT,4));
|
||||
p.getInventory().setItem(60,new ItemStack(Items.EMERALD,5));
|
||||
p.inventoryMenu.broadcastChanges();ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.complete() && m.player.getInventory().getItem(60).getCount()==5,200);
|
||||
c.runOnClient(m -> {CycleClient.open(null,"cycle");});c.waitTick();
|
||||
c.runOnClient(m -> {button(m.gui.screen(),"sanctuary.cycle.ngp").onPress(enter());button(m.gui.screen(),"sanctuary.cycle.confirm").onPress(enter());});
|
||||
c.waitFor(m -> ProgressionClient.progress.cycle()==1 && ProgressionClient.progress.inventoryRows()==1 && CycleClient.view.prestige()==1,200);
|
||||
c.runOnClient(m -> {
|
||||
require(ProgressionClient.identity.name().equals(originalName),"Prestige never renames the inhabitant record");
|
||||
require(m.player.getInventory().getItem(9).getCount()==2 && m.player.getInventory().getItem(35).getCount()==3
|
||||
&& m.player.getInventory().getItem(43).getCount()==4 && m.player.getInventory().getItem(60).getCount()==5,"Recovery objects survive the actual New Game+");
|
||||
ProgressionClient.open(null,"identity");
|
||||
require(widgets(m.gui.screen()).anyMatch(w -> w instanceof StringWidget a && a.getMessage().getString().equals(originalName+" I")),"Inhabitant page displays first prestige as Roman I");
|
||||
});
|
||||
c.waitTick();c.takeScreenshot("sanctuary-beta013-inhabitant-prestige-I");
|
||||
for(int gui:new int[]{1,0}) {
|
||||
c.runOnClient(m -> {m.options.guiScale().set(gui);m.resizeGui();m.gui.setScreen(new InventoryScreen(m.player));Inventory012RenderProbe.reset();});
|
||||
c.waitTick();c.takeScreenshot("sanctuary-beta013-recovery-inventory-gui-"+gui);
|
||||
c.runOnClient(m -> {recoveryDrawn((AbstractContainerScreen<?>)m.gui.screen());require(Inventory012RenderProbe.recoveryCells.size()==45,"All 45 locked cells are shaded, including empty overflow cells");m.player.closeContainer();});
|
||||
}
|
||||
for(var type:java.util.List.of(MenuType.GENERIC_9x3,MenuType.GENERIC_9x6)) {
|
||||
world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.openMenu(new SimpleMenuProvider((id,inv,owner)->type.create(id,inv),Component.literal("Chest")));
|
||||
});
|
||||
c.waitFor(m -> m.gui.screen() instanceof AbstractContainerScreen<?> screen && screen.getMenu().getType()==type,200);
|
||||
for(int offset:new int[]{0,1}) {
|
||||
c.runOnClient(m -> {
|
||||
var screen=(AbstractContainerScreen<?>)m.gui.screen();
|
||||
var canvas=(fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor)screen;
|
||||
if(offset==1)for(int i=0;i<5;i++)screen.mouseScrolled(canvas.sanctuary$getLeftPos()+88,screen.height-35,0,-1);
|
||||
Inventory012RenderProbe.reset();
|
||||
});
|
||||
c.waitTick();c.takeScreenshot("sanctuary-beta013-recovery-chest-"+(type==MenuType.GENERIC_9x3 ? 3 : 6)+"-scroll-"+offset);
|
||||
c.runOnClient(m -> recoveryDrawn((AbstractContainerScreen<?>)m.gui.screen()));
|
||||
}
|
||||
c.runOnClient(m -> m.player.closeContainer());c.waitForScreen(null);
|
||||
}
|
||||
Atlas013ClientChecks.run(c,world.getServer());
|
||||
Inventory013WheelChecks.run(c,world.getServer());
|
||||
} finally {
|
||||
armed.set(false);c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.options.guiScale().set(scale);m.resizeGui();
|
||||
});
|
||||
}
|
||||
}
|
||||
private static void aligned(Screen screen) {
|
||||
var all=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).toList();
|
||||
int gaugeX=-1,minusX=-1,plusX=-1;
|
||||
for(String skill:ProgressRecord.SKILLS) {
|
||||
var name=all.stream().filter(w -> key(w,"sanctuary.progression."+skill)).findFirst().orElseThrow();
|
||||
var row=all.stream().filter(w -> Math.abs((w.getY()+w.getHeight()/2)-(name.getY()+name.getHeight()/2))<=1)
|
||||
.sorted(java.util.Comparator.comparingInt(AbstractWidget::getX)).toList();
|
||||
var icon=row.stream().filter(ImageWidget.class::isInstance).findFirst().orElseThrow();
|
||||
var gauge=row.stream().filter(SkillExperienceBar.class::isInstance).findFirst().orElseThrow();
|
||||
var minus=adjust(screen,skill,"−");var plus=adjust(screen,skill,"+");
|
||||
var columns=java.util.List.of(icon,name,gauge,minus,plus);
|
||||
for(int i=1;i<columns.size();i++) require(columns.get(i-1).getRight()<columns.get(i).getX(),"Columns never overlap: "+skill);
|
||||
require(icon.getX()>=12 && plus.getRight()<screen.width-12,"Skill stays inside the GUI: "+skill);
|
||||
if(gaugeX>=0) require(gauge.getX()==gaugeX && minus.getX()==minusX && plus.getX()==plusX,
|
||||
"Bar and buttons use identical columns across all six skills: "+skill);
|
||||
gaugeX=gauge.getX();minusX=minus.getX();plusX=plus.getX();
|
||||
}
|
||||
}
|
||||
private static void recoveryDrawn(AbstractContainerScreen<?> screen) {
|
||||
var inventory=net.minecraft.client.Minecraft.getInstance().player.getInventory();
|
||||
var expected=new java.util.HashSet<Inventory012RenderProbe.Cell>();
|
||||
for(var slot:screen.getMenu().slots) if(slot.x>-1000 && slot.y>-1000) {
|
||||
var cell=new Inventory012RenderProbe.Cell(slot.x,slot.y);
|
||||
if(slot.container instanceof SanctuaryOverflowContainer || slot.container==inventory && slot.getContainerSlot()>=9 && slot.getContainerSlot()<36)
|
||||
expected.add(cell);
|
||||
else require(!Inventory012RenderProbe.recoveryCells.contains(cell),"Active hotbar, equipment and chest storage are never shaded");
|
||||
}
|
||||
require(!expected.isEmpty() && Inventory012RenderProbe.recoveryCells.equals(expected),"Draw calls cover exactly every visible locked cell, empty or occupied");
|
||||
}
|
||||
private static Button adjust(Screen screen,String skill,String symbol) {
|
||||
var name=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast)
|
||||
.filter(w -> key(w,"sanctuary.progression."+skill)).findFirst().orElseThrow();
|
||||
return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getY()==name.getY() && b.getMessage().getString().equals(symbol)).findFirst().orElseThrow();
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(GuiEventListener widget) {
|
||||
return Stream.concat(Stream.of(widget),widget instanceof ContainerEventHandler parent
|
||||
? parent.children().stream().flatMap(Progression013ClientChecks::widgets) : Stream.empty());
|
||||
}
|
||||
private static boolean key(AbstractWidget widget,String key) {return widget.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key);}
|
||||
private static Button button(Screen screen,String key) {return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> key(b,key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter() {return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.realtime.*;
|
||||
import fr.koka.sanctuary.notifications.ObjectiveTracker;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.clock.WorldClocks;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.state.properties.BedPart;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class Realtime037ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(armed.get()&&m.gui.screen() instanceof HelloWorldScreen screen){widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary realtime037 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
server.runOnServer(s->{require(RealtimeService.active(s.overworld()),"Realtime active automatically in quick flat preset");s.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);});
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.player.setYRot(-90);m.player.setXRot(-12);});
|
||||
var zone=ZoneId.systemDefault();var model=new SolarTime(zone,SolarReference.resolve(zone,"auto",Instant.parse("2026-06-21T12:00:00Z")));
|
||||
var day=SolarTime.day(LocalDate.of(2026,6,21),SolarReference.resolve(zone,"auto",Instant.parse("2026-06-21T12:00:00Z")),zone);
|
||||
for(var scene:List.of(new Scene("sunrise",day.sunrise(),270),new Scene("noon",day.noon(),0),new Scene("sunset",day.sunset(),90))){
|
||||
server.runOnServer(s->at(s,scene.time));double phase=model.sample(scene.time).phase();
|
||||
c.waitFor(m->{var clock=m.level.clockManager().getInstance(m.level.registryAccess().lookupOrThrow(Registries.WORLD_CLOCK).getOrThrow(WorldClocks.OVERWORLD));return Math.abs(wrap(clock.totalTicks()+clock.partialTick()-phase))<2&&clock.rate()<.1;},150);
|
||||
c.runOnClient(m->{float angle=m.level.environmentAttributes().getDimensionValue(EnvironmentAttributes.SUN_ANGLE);require(Math.abs(degrees(angle-scene.angle))<.1,"Native sun reaches "+scene.name+" horizon/noon: "+angle);});
|
||||
c.runOnClient(m->{m.player.setYRot(scene.name.equals("sunset")?90:-90);m.player.setXRot(scene.name.equals("noon")?-80:-12);});c.waitTicks(5);c.takeScreenshot("sanctuary-beta037-"+scene.name);
|
||||
}
|
||||
// Synthetic quest source exists only in test code. Its bar shares the production HUD.
|
||||
c.runOnClient(m->{NotificationClient.clearFeed();NotificationClient.options().seconds=3;NotificationClient.options().advancements=true;NotificationClient.objectives("quest",List.of(objective("Test quest",0)));});
|
||||
c.runOnClient(m->NotificationClient.objectives("quest",List.of(objective("Test quest",.5F))));
|
||||
c.waitFor(m->NotificationClient.tracked().stream().anyMatch(t->t.title().getString().equals("Test quest")),80);
|
||||
c.waitTicks(5);c.runOnClient(m->{require(NotificationClient.visible(),"Objective HUD visible in normal play");require(NotificationObstacles.lastLayout.height()>=23,"Objective has an actual render rectangle: "+NotificationObstacles.lastLayout+" "+NotificationObstacles.boxes());});
|
||||
c.takeScreenshot("sanctuary-beta037-shared-objective");
|
||||
c.waitTicks(145);c.runOnClient(m->{NotificationClient.objectives("quest",List.of(objective("Test quest",.5F)));require(NotificationClient.tracked().stream().noneMatch(t->t.title().getString().equals("Test quest")),"Unchanged quest expires in the real HUD");NotificationClient.toggleObjective(new ObjectiveTracker.Key("quest","sanctuary:test_only"));});
|
||||
c.waitTicks(5);c.runOnClient(m->require(NotificationClient.tracked().size()==1&&NotificationObstacles.lastLayout.height()>=23,"Pinned quest actually rendered"));c.takeScreenshot("sanctuary-beta037-pinned-objective");
|
||||
c.runOnClient(m->{NotificationClient.objectives("quest",List.of(objective("Test quest",1)));require(NotificationClient.tracked().stream().noneMatch(t->t.title().getString().equals("Test quest")),"Completed quest unpins in native client");NotificationClient.clearFeed();});
|
||||
var night=day.sunset().plusSeconds(Duration.between(day.sunset(),SolarTime.day(day.date().plusDays(1),SolarReference.resolve(zone,"auto",day.noon()),zone).sunrise()).toSeconds()/2);
|
||||
server.runOnServer(s->{at(s,night);var p=s.getPlayerList().getPlayers().getFirst();var level=p.level();var foot=p.blockPosition();var head=foot.north();
|
||||
level.setBlockAndUpdate(foot.below(),Blocks.STONE.defaultBlockState());level.setBlockAndUpdate(head.below(),Blocks.STONE.defaultBlockState());
|
||||
var bed=Blocks.BED.red().defaultBlockState().setValue(BedBlock.FACING,Direction.NORTH);level.setBlockAndUpdate(foot,bed.setValue(BedBlock.PART,BedPart.FOOT));level.setBlockAndUpdate(head,bed.setValue(BedBlock.PART,BedPart.HEAD));
|
||||
var bedBlock=(AbstractBedBlock)bed.getBlock();var result=p.startSleepInBed(bedBlock,level.getBlockState(head),bedBlock.getBedRule(level,head),head);require(result.left().isEmpty(),"Native bed entry succeeds at solar night: "+result);
|
||||
});
|
||||
c.waitFor(m->m.player.isSleeping(),150);c.waitTicks(135);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(p.isSleeping()&&p.isSleepingLongEnough(),"Deep sleep does not skip real night or wake the player");require(Math.abs(wrap(s.overworld().getOverworldClockTime()-model.sample(night).phase()))<2,"Night remains at the wall-clock phase");p.stopSleepInBed(true,true);});
|
||||
c.waitFor(m->!m.player.isSleeping()&&m.gui.screen()==null,150);c.waitTicks(5);c.takeScreenshot("sanctuary-beta037-night-after-bed");
|
||||
server.runOnServer(s->{try{var field=RealtimeService.Session.class.getDeclaredField("clock");field.setAccessible(true);field.set(RealtimeService.session(s),Clock.systemUTC());RealtimeService.tick(s);}catch(Exception e){throw new RuntimeException(e);}});
|
||||
System.out.println("REALTIME037 client passed: automatic solo reference; native sun horizon/noon + fractional clock synchronization; shared quest feed expiry/pinning/completion; deep bed sleep leaves real night intact.");
|
||||
}finally{armed.set(false);}
|
||||
}
|
||||
private record Scene(String name,Instant time,float angle){}
|
||||
private static void at(net.minecraft.server.MinecraftServer s,Instant time){try{var field=RealtimeService.Session.class.getDeclaredField("clock");field.setAccessible(true);field.set(RealtimeService.session(s),Clock.fixed(time,ZoneOffset.UTC));RealtimeService.tick(s);s.getPlayerList().broadcastAll(s.clockManager().createFullSyncPacket());}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static ObjectiveTracker.Objective<NotificationClient.Track> objective(String name,float f){return new ObjectiveTracker.Objective<>("sanctuary:test_only",new NotificationClient.Track(Component.literal(name),Component.literal(Math.round(f*100)+"%"),f,0xF5D176),f,f==1);}
|
||||
private static double wrap(double ticks){return ((ticks+12000)%24000+24000)%24000-12000;}
|
||||
private static float degrees(float a){return ((a+180)%360+360)%360-180;}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static Button button(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.recipes.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import mezz.jei.api.constants.RecipeTypes;
|
||||
import mezz.jei.api.sanctuary.CatalogueAccess;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
public final class Recipes024ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
int scale=c.computeOnClient(m->m.options.guiScale().get());
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sanctuary beta024 recipes "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&RecipeClient.ready&&SanctuaryJeiPlugin.runtime()!=null&&m.gui.screen()==null,1200);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new InventoryScreen(m.player));});c.waitTicks(6);
|
||||
c.runOnClient(m->{require(!RecipeClient.allowed(),"Catalogue initially locked");require(!SanctuaryJeiPlugin.runtime().getIngredientListOverlay().isListDisplayed(),"Locked catalogue has no sidebar");SanctuaryJeiPlugin.runtime().getRecipesGui().showTypes(List.of(RecipeTypes.CRAFTING));require(m.gui.screen() instanceof InventoryScreen,"JEI cannot open while locked");});
|
||||
c.takeScreenshot("sanctuary-beta024-locked-inventory");
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.OAK_PLANKS,8));p.giveExperienceLevels(4-p.experienceLevel);RecipeService.refresh(p);});
|
||||
c.waitFor(m->RecipeClient.knowledge.recipes.contains("minecraft:crafting_table")&&RecipeClient.xp>=4,120);
|
||||
c.runOnClient(m->{require(!RecipeClient.knowledge.recipes.contains("minecraft:shield"),"Plank discovery does not teach shield");m.gui.setScreen(new RecipeScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));});c.waitTicks(3);c.takeScreenshot("sanctuary-beta024-collections-locked");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.recipes.buy").onPress(enter()));c.waitFor(m->RecipeClient.allowed()&&RecipeClient.knowledge.learned(),120);
|
||||
c.runOnClient(m->{var r=SanctuaryJeiPlugin.runtime();require(r.getIngredientFilter().getFilteredItemStacks().stream().noneMatch(i->i.is(Items.DIAMOND_BLOCK)),"Unknown item hidden");
|
||||
require(r.getIngredientFilter().getFilteredIngredients(mezz.jei.api.fabric.constants.FabricTypes.FLUID_STACK).isEmpty(),"Unknown fluids hidden too");
|
||||
require(r.getRecipeManager().createRecipeCategoryLookup().get().anyMatch(cat->cat.getRecipeType().equals(RecipeTypes.CRAFTING)),"Learned blueprints appear before discovering their crafting station");
|
||||
require(r.getRecipeManager().createRecipeLookup(RecipeTypes.CRAFTING).get().anyMatch(v->v.id().identifier().toString().equals("minecraft:crafting_table")),"Learned wood recipe visible");
|
||||
require(r.getRecipeManager().createRecipeLookup(RecipeTypes.CRAFTING).get().noneMatch(v->v.id().identifier().toString().equals("minecraft:shield")),"Unknown recipe hidden");
|
||||
m.gui.setScreen(new InventoryScreen(m.player));});c.waitTicks(5);c.takeScreenshot("sanctuary-beta024-personal-catalogue");
|
||||
hover(c,36);c.getInput().pressKey(InputConstants.KEY_U);c.waitFor(m->m.gui.screen() instanceof mezz.jei.gui.recipes.RecipesGui,60);
|
||||
c.runOnClient(m->{m.gui.setScreen(new RecipeScreen(null));button(m.gui.screen(),"sanctuary.recipes.open").onPress(enter());});c.waitFor(m->m.gui.screen() instanceof mezz.jei.gui.recipes.RecipesGui,60);c.waitTicks(5);c.takeScreenshot("sanctuary-beta024-wood-recipes");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.getPlayerList().op(p.nameAndId());RecipeService.refresh(p);});c.waitFor(m->RecipeClient.operatorAllowed,120);
|
||||
c.runOnClient(m->{require(!RecipeClient.operator,"Operator status alone does not enable full view");m.gui.setScreen(new RecipeScreen(null));RecipeClient.request("operator");});
|
||||
c.waitFor(m->RecipeClient.operator,120);c.waitTicks(3);c.takeScreenshot("sanctuary-beta024-operator-view");
|
||||
c.runOnClient(m->require(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilteredItemStacks().stream().anyMatch(i->i.is(Items.DIAMOND_BLOCK)),"Explicit operator sees unknown items"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.getPlayerList().deop(p.nameAndId());RecipeService.refresh(p);});
|
||||
c.waitFor(m->!RecipeClient.operatorAllowed&&!RecipeClient.operator,120);c.runOnClient(m->require(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilteredItemStacks().stream().noneMatch(i->i.is(Items.DIAMOND_BLOCK)),"Permission revocation removes unknown items"));
|
||||
// Use JEI's real transfer packet with ingredients available only in the final added row.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressEffects.get(p);for(int i=progress.inventory();i<5;i++)progress=progress.buy("inventory",1);
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);p.getInventory().clearContent();p.getInventory().setItem(SanctuaryInventoryCapacity.EXTRA_START+17,new ItemStack(Items.OAK_PLANKS,8));p.inventoryMenu.broadcastChanges();ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.inventoryRows()==6&&m.player.getInventory().getItem(SanctuaryInventoryCapacity.EXTRA_START+17).getCount()==8,120);
|
||||
c.runOnClient(m->{m.gui.setScreen(new InventoryScreen(m.player));var menu=m.player.inventoryMenu;var slots=CatalogueAccess.inventory.apply(menu,List.of());require(slots.size()==54,"JEI sees all six unlocked storage rows");
|
||||
var r=SanctuaryJeiPlugin.runtime();var manager=r.getRecipeManager();var category=manager.getRecipeCategory(RecipeTypes.CRAFTING);
|
||||
var recipe=manager.createRecipeLookup(RecipeTypes.CRAFTING).get().filter(v->v.id().identifier().toString().equals("minecraft:crafting_table")).findFirst().orElseThrow();
|
||||
var layout=manager.createRecipeLayoutDrawable(category,recipe,r.getJeiHelpers().getFocusFactory().createFocusGroup(List.of())).orElseThrow();
|
||||
var handler=r.getRecipeTransferManager().getRecipeTransferHandler(menu,category).orElseThrow();
|
||||
require(handler.transferRecipe(menu,recipe,layout.getRecipeSlotsView(),m.player,false,true)==null,"Real JEI transfer accepts overflow ingredients");});
|
||||
c.waitFor(m->m.player.inventoryMenu.getSlot(0).getItem().is(Items.CRAFTING_TABLE),120);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(p.getInventory().getItem(SanctuaryInventoryCapacity.EXTRA_START+17).getCount()==4,"Exactly four planks consumed from row six");
|
||||
for(int i=1;i<=4;i++)require(p.inventoryMenu.getSlot(i).getItem().is(Items.OAK_PLANKS)&&p.inventoryMenu.getSlot(i).getItem().getCount()==1,"Grid receives one plank per slot");});
|
||||
c.takeScreenshot("sanctuary-beta024-overflow-transfer");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressEffects.get(p);
|
||||
for(String skill:ProgressRecord.SKILLS)while(progress.rank(skill)<ProgressRecord.cap(skill))progress=progress.buy(skill,1);
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);
|
||||
var action=new fr.koka.sanctuary.cycle.CyclePayloads.Action("ngp","","",ProgressionService.session(s).cycles.revision(),progress.revision());
|
||||
require(fr.koka.sanctuary.cycle.CycleService.perform(p,action).equals("done"),"Real New Game+ succeeds");RecipeService.refresh(p);});
|
||||
c.waitFor(m->ProgressionClient.progress.inventoryRows()==1&&ProgressionClient.progress.cycle()==1,120);
|
||||
c.runOnClient(m->{require(CatalogueAccess.inventory.apply(m.player.inventoryMenu,List.of()).size()==9,"After reset, locked recovery rows excluded from JEI autofill");require(RecipeClient.knowledge.learned(),"Catalogue persists after skill reset");
|
||||
m.options.guiScale().set(4);m.resizeGui();m.gui.setScreen(new RecipeScreen(null));});c.waitTicks(4);c.takeScreenshot("sanctuary-beta024-compact-blocodex");
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.hurtServer(p.level(),p.damageSources().fellOutOfWorld(),Float.MAX_VALUE);});
|
||||
c.waitForScreen(net.minecraft.client.gui.screens.DeathScreen.class);c.waitTicks(45);c.clickScreenButton("deathScreen.respawn");
|
||||
c.waitFor(m->m.player!=null&&m.player.isAlive()&&m.gui.screen()==null&&ProgressionClient.active(),1200);c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(RecipeKnowledge.parse(p.getAttached(RecipeService.RECORD)).learned(),"Real death preserves catalogue attachment");s.saveEverything(true,true,true);});
|
||||
System.out.println("JEI024: locked UI, personal discoveries, recipes, operator revocation and real six-row transfer passed.");
|
||||
}finally{onboarding.set(false);c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();});}
|
||||
c.runOnClient(m->require(!RecipeClient.ready&&!RecipeClient.allowed(),"Recipe state cleared on world exit"));
|
||||
try(var reopened=saved.open()){
|
||||
reopened.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&RecipeClient.ready&&SanctuaryJeiPlugin.runtime()!=null,1200);
|
||||
c.runOnClient(m->{require(RecipeClient.knowledge.learned()&&RecipeClient.knowledge.recipes.contains("minecraft:crafting_table"),"Catalogue and recipes survive server restart");require(!RecipeClient.operator,"Operator inspection is not persisted");});
|
||||
System.out.println("JEI024: actual prestige, death and server restart preserve recipe knowledge.");
|
||||
}
|
||||
}
|
||||
private static void hover(ClientGameTestContext c,int index){c.runOnClient(m->{var screen=(net.minecraft.client.gui.screens.inventory.AbstractContainerScreen<?>)m.gui.screen();var a=(fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor)screen;var slot=screen.getMenu().getSlot(index);var w=m.getWindow();double x=a.sanctuary$getLeftPos()+slot.x+8.0,y=a.sanctuary$getTopPos()+slot.y+8.0;double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();});c.waitTicks(2);}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?Stream.concat(Stream.of(w),widgets(nested)):Stream.of(w));}
|
||||
private static Button button(Screen screen,String key){return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(w->w.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow(()->new AssertionError("Missing "+key));}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Real widgets, fonts and purchase packets, in a fresh ignored development world. */
|
||||
public final class Skills025ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (armed.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);
|
||||
}
|
||||
});
|
||||
int scale=c.computeOnClient(m -> m.options.guiScale().get());
|
||||
String language=c.computeOnClient(m -> m.getLanguageManager().getSelected());
|
||||
try (var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Sanctuary beta025 skills "+UUID.randomUUID());
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && ProgressionClient.active() && CycleClient.view!=null && m.gui.screen()==null,1200);
|
||||
// First Steps can already award XP during admission; make the zero-XP case explicit.
|
||||
world.getServer().runOnServer(server -> server.getPlayerList().getPlayers().getFirst().setExperienceLevels(0));
|
||||
c.waitFor(m -> m.player.experienceLevel==0,200);
|
||||
c.runOnClient(m -> ProgressionClient.open(null));c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
aligned(m.gui.screen());
|
||||
require(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.noneMatch(b -> b.visible && b.getMessage().getString().equals("+")),"Zero XP hides all skill purchases");
|
||||
require(ProgressionClient.config.statCosts().getLast()==48,"Server sends the migrated default price");
|
||||
});
|
||||
c.runOnClient(m -> {
|
||||
m.getWindow().setWindowed(1280,800);m.options.guiScale().set(2);m.resizeGui();ProgressionClient.open(null);
|
||||
});
|
||||
for(int i=0;i<5;i++)c.waitTick();
|
||||
c.takeScreenshot("sanctuary-beta025-skills-start");
|
||||
world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();s.getPlayerList().op(p.nameAndId());
|
||||
var progress=ProgressRecord.INITIAL;
|
||||
for(String skill:ProgressRecord.SKILLS) while(progress.rank(skill)<ProgressRecord.cap(skill)-1)
|
||||
progress=progress.buy(skill,progress.cost(skill,ProgressionConfig.DEFAULT));
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);
|
||||
p.setExperienceLevels(47);ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.operator && ProgressionClient.progress.vitality()==6 && m.player.experienceLevel==47,200);c.waitTick();
|
||||
c.runOnClient(m -> require(!adjust(m.gui.screen(),"vitality","+").visible,"47 levels cannot buy the last heart"));
|
||||
world.getServer().runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(48);ProgressionService.send(p,"");});
|
||||
c.waitFor(m -> m.player.experienceLevel==48,200);c.waitTick();
|
||||
for(String locale:new String[]{"en_us","fr_fr"}) {
|
||||
c.runOnClient(m -> {m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});
|
||||
for(int gui:new int[]{1,2,0}) {
|
||||
c.runOnClient(m -> {m.options.guiScale().set(gui);m.resizeGui();ProgressionClient.open(null);});
|
||||
c.waitTick();c.runOnClient(m -> aligned(m.gui.screen()));
|
||||
c.takeScreenshot("sanctuary-beta025-skills-"+locale+"-gui-"+gui);
|
||||
}
|
||||
}
|
||||
c.runOnClient(m -> m.getWindow().setWindowed(640,480));
|
||||
for(int i=0;i<5;i++)c.waitTick();
|
||||
c.runOnClient(m -> {m.resizeGui();ProgressionClient.open(null);});c.waitTick();
|
||||
c.runOnClient(m -> {aligned(m.gui.screen());System.out.println("beta025 compact GUI: "+m.gui.screen().width+"x"+m.gui.screen().height);});
|
||||
c.takeScreenshot("sanctuary-beta025-skills-fr-compact");
|
||||
c.runOnClient(m -> {
|
||||
var bar=widgets(m.gui.screen()).filter(SkillExperienceBar.class::isInstance).map(SkillExperienceBar.class::cast).findFirst().orElseThrow();
|
||||
m.gui.screen().mouseScrolled(bar.getX()+5,bar.getY()+10,0,-20);
|
||||
});c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
var last=widgets(m.gui.screen()).filter(SkillExperienceBar.class::isInstance).map(SkillExperienceBar.class::cast).toList().getLast();
|
||||
require(last.getY()>=88 && last.getBottom()<=m.gui.screen().height-53,"Scrolling brings the inventory bar into the visible panel");
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta025-skills-fr-compact-scrolled");
|
||||
c.runOnClient(m -> ProgressionClient.open(null));c.waitTick();
|
||||
c.runOnClient(m -> {var plus=adjust(m.gui.screen(),"vitality","+");require(plus.visible && plus.active,"48 levels enables the last heart");plus.onPress(enter());});
|
||||
c.waitFor(m -> ProgressionClient.progress.vitality()==7 && m.player.experienceLevel==0,200);c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
require(!adjust(m.gui.screen(),"vitality","+").visible,"Maximum hides the plus");
|
||||
var minus=adjust(m.gui.screen(),"vitality","−");require(minus.visible && minus.active,"Operator can refund the last heart");minus.onPress(enter());
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.vitality()==6 && m.player.experienceLevel==48,200);
|
||||
c.runOnClient(m -> require(ProgressionClient.progress.history().getLast().cost()==48,"Exact purchase price refunded through the server"));
|
||||
world.getServer().runOnServer(server -> {
|
||||
var player=server.getPlayerList().getPlayers().getFirst();
|
||||
var complete=ProgressRecord.INITIAL;
|
||||
for(String skill:ProgressRecord.SKILLS)while(complete.rank(skill)<ProgressRecord.cap(skill))
|
||||
complete=complete.buy(skill,complete.cost(skill,ProgressionConfig.DEFAULT));
|
||||
player.setAttached(ProgressionService.RECORD,complete.json());ProgressEffects.apply(player,complete);
|
||||
ProgressionService.send(player,"");
|
||||
});
|
||||
c.waitFor(m -> ProgressionClient.progress.complete(),200);c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
aligned(m.gui.screen());
|
||||
require(ProgressionClient.progress.inventoryRows()==6,"Inventory keeps its six rows");
|
||||
require(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.noneMatch(b -> b.visible && b.getMessage().getString().equals("+")),"Full bars have no further purchase");
|
||||
});
|
||||
c.runOnClient(m -> {m.getWindow().setWindowed(1280,800);m.options.guiScale().set(2);m.resizeGui();ProgressionClient.open(null);});
|
||||
for(int i=0;i<5;i++)c.waitTick();
|
||||
c.takeScreenshot("sanctuary-beta025-skills-full");
|
||||
c.runOnClient(m -> ProgressionClient.open(null,"identity"));c.waitTick();
|
||||
c.runOnClient(m -> ProgressionClient.open(null,"progress"));c.waitTick();
|
||||
c.runOnClient(m -> aligned(m.gui.screen()));
|
||||
System.out.println("beta025 skills: FR/EN, responsive XP bars, 47/48 purchase, refund, caps and tab return passed");
|
||||
} finally {
|
||||
armed.set(false);c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.options.guiScale().set(scale);m.resizeGui();
|
||||
});
|
||||
}
|
||||
}
|
||||
private static void aligned(Screen screen) {
|
||||
var all=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).toList();
|
||||
int gaugeX=-1,minusX=-1,plusX=-1;
|
||||
require(all.stream().filter(SkillExperienceBar.class::isInstance).count()==6,"All six skills have an XP bar");
|
||||
require(all.stream().noneMatch(w -> key(w,"sanctuary.progression.rank_max_short") || key(w,"sanctuary.progression.next_cost_short")),"No separate Max or price columns");
|
||||
for(String skill:ProgressRecord.SKILLS) {
|
||||
var name=all.stream().filter(w -> key(w,"sanctuary.progression."+skill)).findFirst().orElseThrow();
|
||||
var row=all.stream().filter(w -> Math.abs((w.getY()+w.getHeight()/2)-(name.getY()+name.getHeight()/2))<=1)
|
||||
.sorted(java.util.Comparator.comparingInt(AbstractWidget::getX)).toList();
|
||||
var icon=row.stream().filter(ImageWidget.class::isInstance).findFirst().orElseThrow();
|
||||
var gauge=row.stream().filter(SkillExperienceBar.class::isInstance).findFirst().orElseThrow();
|
||||
var minus=adjust(screen,skill,"−");var plus=adjust(screen,skill,"+");
|
||||
require(gauge.getWidth()>=48,"Even compact bars leave room for the price");
|
||||
System.out.println("beta025 bar "+skill+" gui="+screen.width+"x"+screen.height+" bounds="+gauge.getX()+","+gauge.getY()+","+gauge.getWidth()+","+gauge.getHeight());
|
||||
var columns=java.util.List.of(icon,name,gauge,minus,plus);
|
||||
for(int i=1;i<columns.size();i++) require(columns.get(i-1).getRight()<columns.get(i).getX(),"Columns never overlap: "+skill);
|
||||
require(icon.getX()>=12 && plus.getRight()<screen.width-12,"Skill stays inside the GUI: "+skill);
|
||||
if(gaugeX>=0) require(gauge.getX()==gaugeX && minus.getX()==minusX && plus.getX()==plusX,
|
||||
"Bar and buttons use identical columns across all six skills: "+skill);
|
||||
gaugeX=gauge.getX();minusX=minus.getX();plusX=plus.getX();
|
||||
}
|
||||
}
|
||||
private static Button adjust(Screen screen,String skill,String symbol) {
|
||||
var name=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast)
|
||||
.filter(w -> key(w,"sanctuary.progression."+skill)).findFirst().orElseThrow();
|
||||
return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getY()==name.getY() && b.getMessage().getString().equals(symbol)).findFirst().orElseThrow();
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(GuiEventListener widget) {
|
||||
return Stream.concat(Stream.of(widget),widget instanceof ContainerEventHandler parent
|
||||
? parent.children().stream().flatMap(Skills025ClientChecks::widgets) : Stream.empty());
|
||||
}
|
||||
private static boolean key(AbstractWidget widget,String key) {return widget.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key);}
|
||||
private static Button button(Screen screen,String key) {return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> key(b,key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter() {return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.client.shader.*;
|
||||
import fr.koka.sanctuary.stars.*;
|
||||
import fr.koka.sanctuary.stars.ConstellationLedger.Ref;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.item.*;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
public final class Sky056ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
geometry();
|
||||
c.runOnClient(m->preferences());
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(s).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());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{
|
||||
s.setName("Sky056 "+UUID.randomUUID());s.setSeed("56");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();var expected=new AtomicInteger();
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.setExperienceLevels(12);p.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND,new ItemStack(Items.SPYGLASS));
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
addCatalogue(s,0);expected.set(StarService.ledger(s).starCount());
|
||||
});
|
||||
c.waitFor(m->StarClient.starCount()==expected.get()&&m.player.experienceLevel==12,200);
|
||||
c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(142);m.options.fov().set(90);});
|
||||
var first=c.computeOnClient(m->Map.copyOf(StarClient.markers()));
|
||||
c.getInput().lookAt(0,-45);c.waitTicks(8);c.takeScreenshot("sanctuary-beta056-one-catalogue");
|
||||
server.runOnServer(s->{addCatalogue(s,1);addCatalogue(s,2);expected.set(StarService.ledger(s).starCount());});
|
||||
c.waitFor(m->StarClient.starCount()==expected.get(),200);c.waitTicks(6);
|
||||
c.runOnClient(m->{
|
||||
require(StarClient.markers().entrySet().containsAll(first.entrySet()),"More inhabitants add density without changing previous identities");
|
||||
var points=StarClient.markers().entrySet().stream().flatMap(e->StarGeometry.points(e.getKey(),e.getValue()).stream()).toList();coverage(points);
|
||||
require(StarClient.indexCount()==points.size()*6,"Native GPU mesh contains every earned star");
|
||||
require(m.gameRenderer.getAppliedPostEffects().contains(VanillaLight.EFFECT)&&VanillaLight.saturation()==142,"Live shader uses the requested saturation");
|
||||
System.out.println("SKY056_CATALOGUE: one="+first.values().stream().mapToInt(Integer::intValue).sum()+", three="+points.size());
|
||||
});
|
||||
var mesh=c.computeOnClient(m->StarClient.vertexBuffer(null));long revision=c.computeOnClient(m->StarClient.revision());
|
||||
for(int yaw:new int[]{0,90,180,270}){c.getInput().lookAt(yaw,-45);c.waitTicks(4);c.takeScreenshot("sanctuary-beta056-three-catalogues-"+yaw);}
|
||||
c.runOnClient(m->require(StarClient.vertexBuffer(null)==mesh&&StarClient.revision()==revision,"Camera movement reuses the star buffer"));
|
||||
|
||||
var refs=c.computeOnClient(m->StarClient.markers().entrySet().stream().filter(e->e.getValue()==5).flatMap(e->java.util.stream.IntStream.range(1,5).mapToObj(i->new Ref(e.getKey(),i))).filter(r->direction(r).y>.4).limit(2).toList());
|
||||
require(refs.size()==2,"Visible dispersed bonus stars");
|
||||
aim(c,refs.getFirst());c.runOnClient(m->require(refs.getFirst().equals(StarTargeting.pick(.2)),"Targeting matches a dispersed bonus star"));
|
||||
c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_LEFT);c.waitTicks(4);
|
||||
aim(c,refs.getLast());c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_LEFT);
|
||||
c.waitFor(m->ConstellationClient.edges().size()==1&&m.player.experienceLevel==9,200);
|
||||
c.getInput().holdMouse(InputConstants.MOUSE_BUTTON_RIGHT);
|
||||
c.waitFor(m->m.player.isScoping()&&StarInspectionClient.detail()!=null,200);
|
||||
c.runOnClient(m->{var detail=StarInspectionClient.detail();require(detail.star().equals(refs.getLast())&&detail.total()==1&&detail.name().getString().contains("Aster"),"Native spyglass resolves origin and membership at new positions");});
|
||||
c.takeScreenshot("sanctuary-beta056-bonus-star-inspection");
|
||||
c.getInput().releaseMouse(InputConstants.MOUSE_BUTTON_RIGHT);
|
||||
server.runOnServer(s->{try{
|
||||
Path data=s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data");
|
||||
var stars=StarService.ledger(s);var links=ConstellationService.ledger(s);stars.save();
|
||||
byte[] starBytes=Files.readAllBytes(data.resolve("sanctuary-stars.json")),linkBytes=Files.readAllBytes(data.resolve("sanctuary-constellations.json"));
|
||||
var loaded=new StarLedger(data.resolve("sanctuary-stars.json"),s.overworld().getSeed());
|
||||
var loadedLinks=new ConstellationLedger(data.resolve("sanctuary-constellations.json"),loaded.world());
|
||||
require(loaded.events().equals(stars.events())&&loadedLinks.edges().equals(links.edges()),"Existing ledger schemas retain all memories and links");
|
||||
for(var event:stars.events())require(loaded.positionSeed(event)==stars.positionSeed(event),"Directions survive reloading their journal");
|
||||
require(Arrays.equals(starBytes,Files.readAllBytes(data.resolve("sanctuary-stars.json")))&&Arrays.equals(linkBytes,Files.readAllBytes(data.resolve("sanctuary-constellations.json"))),"Reading existing ledgers does not rewrite them");
|
||||
}catch(Exception e){throw new RuntimeException(e);}});
|
||||
var reload=c.computeOnClient(m->m.reloadResourcePacks());c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(6);
|
||||
c.runOnClient(m->require(StarClient.starCount()==expected.get()&&StarClient.indexCount()==expected.get()*6&&ConstellationClient.edges().size()==1,"Stars and constellation survive native resource reload"));
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new ShaderOptionsScreen(null));
|
||||
var slider=widgets(m.gui.screen()).filter(AbstractSliderButton.class::isInstance).map(AbstractSliderButton.class::cast).findFirst().orElseThrow();
|
||||
require(slider.getMessage().getString().contains("142"),"Native localized slider shows 142 percent");
|
||||
require(net.minecraft.network.chat.Component.translatable("sanctuary.shaders.saturation_help").getString().contains("142"),"Localized help identifies the default");});
|
||||
c.waitTicks(3);c.takeScreenshot("sanctuary-beta056-default-"+locale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
System.out.println("SKY056_PASS: full native advancement catalogue for three residents, whole sphere, stable references, cached mesh, bonus-star selection, constellation, inspection, ledger/resource reload, default and saved saturation, FR/EN");
|
||||
}finally{onboarding.set(false);c.getInput().releaseMouse(InputConstants.MOUSE_BUTTON_RIGHT);}
|
||||
}
|
||||
private static void addCatalogue(net.minecraft.server.MinecraftServer s,int index){
|
||||
var ledger=StarService.ledger(s);var actor=UUID.nameUUIDFromBytes(("sky056:"+index).getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
for(var holder:s.getAdvancements().getAllAdvancements()){
|
||||
var display=holder.value().display();if(!holder.id().getNamespace().equals("minecraft")||display.isEmpty()||!display.get().announceToChat())continue;
|
||||
int count=switch(display.get().type()){case TASK->1;case GOAL->3;case CHALLENGE->5;};
|
||||
ledger.add(new StarLedger.Event("advancement:"+actor+":"+holder.id(),"advancement",actor,"Aster "+index,holder.id().toString(),"2026-09-15T12:00:00Z",count));
|
||||
}
|
||||
try{ledger.save();}catch(Exception e){throw new RuntimeException(e);}
|
||||
}
|
||||
private static void geometry(){
|
||||
var all=new ArrayList<StarGeometry.Point>();
|
||||
for(long seed=0;seed<2000;seed++){
|
||||
var points=StarGeometry.points(seed,5);require(points.equals(StarGeometry.points(seed,5)),"Reproducible positions");all.addAll(points);
|
||||
require(!points.equals(StarGeometry.points(seed^(1L<<48),5)),"All seed bits distinguish event directions");
|
||||
for(int n=1;n<=5;n++)require(points.subList(0,n).equals(StarGeometry.points(seed,n)),"Stable prefix for constellation references");
|
||||
for(int i=0;i<5;i++){
|
||||
var a=points.get(i);double norm=Math.sqrt(a.x()*a.x()+a.y()*a.y()+a.z()*a.z());require(Math.abs(norm-100)<.001,"Celestial sphere radius");
|
||||
require(a.equals(new Ref(seed,i).point()),"Shared renderer and constellation geometry");
|
||||
for(int j=0;j<i;j++){var b=points.get(j);double dot=(a.x()*b.x()+a.y()*b.y()+a.z()*b.z())/10000.;require(dot<Math.cos(Math.toRadians(29.99)),"Same-event stars no longer form tight clusters");}
|
||||
}
|
||||
}
|
||||
coverage(all);
|
||||
}
|
||||
private static void coverage(List<StarGeometry.Point> points){
|
||||
int[] cells=new int[72];double x=0,y=0,z=0;boolean north=false,south=false;
|
||||
for(var p:points){int band=Math.clamp((int)((p.y()/100.+1)*3),0,5),sector=Math.clamp((int)((Math.atan2(p.z(),p.x())+Math.PI)/(2*Math.PI)*12),0,11);cells[band*12+sector]++;x+=p.x()/100.;y+=p.y()/100.;z+=p.z()/100.;north|=p.y()>95;south|=p.y()<-95;}
|
||||
require(Arrays.stream(cells).filter(n->n>0).count()>64&&north&&south,"Stars cover equal-area sectors and both polar caps");
|
||||
require(Math.abs(x/points.size())<.1&&Math.abs(y/points.size())<.1&&Math.abs(z/points.size())<.1,"No preferred celestial direction");
|
||||
}
|
||||
private static void preferences(){
|
||||
Path path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");
|
||||
try{
|
||||
byte[] prior=Files.exists(path)?Files.readAllBytes(path):null;
|
||||
try{
|
||||
Files.deleteIfExists(path);resetPreference();require(VanillaLight.saturation()==142,"Fresh installation uses 142");
|
||||
Files.createDirectories(path.getParent());Files.writeString(path,"{\"enabled\":false}");resetPreference();require(VanillaLight.saturation()==142&&!VanillaLight.enabled(),"Older preference gets missing saturation without changing enabled");
|
||||
for(int explicit:new int[]{0,100,170,200}){Files.writeString(path,"{\"enabled\":true,\"saturation\":"+explicit+"}");resetPreference();require(VanillaLight.saturation()==explicit,"An explicit saved preference remains respected");}
|
||||
VanillaLight.setSaturation(142);resetPreference();require(VanillaLight.saturation()==142,"142 persists on disk");
|
||||
}finally{if(prior==null)Files.deleteIfExists(path);else Files.write(path,prior);resetPreference();}
|
||||
}catch(Exception e){throw new RuntimeException(e);}
|
||||
}
|
||||
private static void resetPreference()throws Exception{var field=VanillaLight.class.getDeclaredField("preferences");field.setAccessible(true);field.set(null,null);}
|
||||
private static Vector3f direction(Ref r){var p=r.point();return new Vector3f(p.x(),p.y(),p.z()).normalize().rotateX(StarClient.angle()).rotateY(-(float)Math.PI/2);}
|
||||
private static void aim(ClientGameTestContext c,Ref ref){var d=c.computeOnClient(m->direction(ref));c.getInput().lookAt((float)Math.toDegrees(Math.atan2(-d.x,d.z)),-(float)Math.toDegrees(Math.asin(d.y)));c.waitTicks(4);}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof ContainerEventHandler child?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(child)):java.util.stream.Stream.of(w));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean yes,String message){if(!yes)throw new AssertionError(message);}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.stars.*;
|
||||
import fr.koka.sanctuary.stars.ConstellationLedger.Ref;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.boats.ChickenBoat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
|
||||
import net.minecraft.world.item.*;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
public final class SkyFlight046ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected("en_us");m.options.languageCode="en_us";return m.reloadResourcePacks();});
|
||||
c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();
|
||||
c.runOnClient(m->{
|
||||
m.updateTitle();
|
||||
require(org.lwjgl.sdl.SDLVideo.SDL_GetWindowTitle(m.getWindow().handle()).equals(ApplicationBrand.title()),"Native window title uses Sanctuary");
|
||||
require("Sanctuary".equals(org.lwjgl.sdl.SDLInit.SDL_GetAppMetadataProperty(org.lwjgl.sdl.SDLInit.SDL_PROP_APP_METADATA_NAME_STRING)),"Native app metadata uses Sanctuary");
|
||||
});
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b,"sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{
|
||||
s.setName("Sky flight046 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(true);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.giveExperienceLevels(20);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->m.player.experienceLevel==20,100);
|
||||
c.getInput().holdKey(InputConstants.KEY_C);c.waitTicks(3);
|
||||
c.runOnClient(m->require(!ZoomClient.active(),"Zoom key does nothing before purchasing aptitude"));c.getInput().releaseKey(InputConstants.KEY_C);
|
||||
c.runOnClient(m->ProgressionClient.open(null));c.waitTicks(3);
|
||||
c.runOnClient(m->{
|
||||
var buy=widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b,"sanctuary.zoom.buy")).findFirst().orElseThrow();
|
||||
require(buy.active,"Zoom is purchasable in the aptitude list");buy.onPress(enter());
|
||||
});
|
||||
c.waitFor(m->ZoomClient.learned()&&m.player.experienceLevel==16,200);
|
||||
for(String locale:new String[]{"en_us","fr_fr"}){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());ProgressionClient.open(null);});
|
||||
c.waitTicks(3);
|
||||
c.takeScreenshot("sanctuary-beta046-aptitudes-"+locale+"-top");
|
||||
for(int page=1;page<=14;page++){
|
||||
c.runOnClient(m->{var screen=m.gui.screen();screen.mouseScrolled(screen.width-30,screen.height/2,0,-12);});
|
||||
c.waitTicks(3);c.takeScreenshot("sanctuary-beta046-aptitudes-"+locale+"-"+page);
|
||||
}
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(3);
|
||||
int normal=c.computeOnClient(m->m.options.fov().get());
|
||||
c.getInput().holdKey(InputConstants.KEY_C);c.waitFor(m->ZoomClient.active(),100);
|
||||
c.getInput().scroll(100);c.waitTicks(5);
|
||||
c.runOnClient(m->require(Math.abs(m.gameRenderer.mainCamera().getFov()-5)<.01,"Wheel reaches actual narrow projection"));
|
||||
c.takeScreenshot("sanctuary-beta046-zoom-5");
|
||||
c.getInput().scroll(-200);c.waitTicks(5);
|
||||
c.runOnClient(m->require(Math.abs(m.gameRenderer.mainCamera().getFov()-110)<.01,"Wheel reaches actual Quake Pro projection"));
|
||||
c.takeScreenshot("sanctuary-beta046-zoom-110");
|
||||
c.getInput().releaseKey(InputConstants.KEY_C);c.waitTicks(5);
|
||||
c.runOnClient(m->require(!ZoomClient.active()&&m.options.fov().get()==normal&&Math.abs(m.gameRenderer.mainCamera().getFov()-normal)<.5,"Releasing restores normal FOV without writing preferences"));
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND,new ItemStack(Items.SPYGLASS));
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
var stars=StarService.ledger(s);
|
||||
for(int i=0;i<24;i++)stars.add(new StarLedger.Event("client046:"+i,"advancement",p.getUUID(),"Test","minecraft:test","2026-09-14T12:00:00Z",1));
|
||||
try{stars.save();}catch(java.io.IOException e){throw new RuntimeException(e);}
|
||||
});
|
||||
c.waitFor(m->StarClient.starCount()>=24&&ConstellationClient.editing(),200);c.waitTicks(5);
|
||||
var pair=c.computeOnClient(m->{
|
||||
var refs=StarClient.markers().keySet().stream().map(seed->new Ref(seed,0)).filter(r->direction(r).y>.3).toList();
|
||||
for(var a:refs)for(var b:refs){float dot=direction(a).dot(direction(b));if(!a.equals(b)&&dot>.4&&dot<.9)return List.of(a,b);}
|
||||
throw new AssertionError("Two visible stars for the gesture test");
|
||||
});
|
||||
aim(c,pair.get(0));c.runOnClient(m->require(pair.get(0).equals(ConstellationClient.pick()),"Cursor finds the mother star without scoping"));
|
||||
c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_LEFT);c.waitFor(m->pair.get(0).equals(ConstellationClient.root()),100);
|
||||
aim(c,pair.get(1));c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_LEFT);
|
||||
c.waitFor(m->ConstellationClient.edges().size()==1&&m.player.experienceLevel==13,200);
|
||||
require(server.computeOnServer(s->ConstellationService.ledger(s).edges().size()==1),"Native click created shared server link");
|
||||
var middle=c.computeOnClient(m->direction(pair.get(0)).add(direction(pair.get(1))).normalize());aim(c,middle);c.waitTicks(8);c.takeScreenshot("sanctuary-beta046-constellation");
|
||||
c.getInput().holdKey(o->o.keyShift);c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_RIGHT);c.getInput().releaseKey(o->o.keyShift);c.waitFor(m->ConstellationClient.edges().isEmpty()&&m.player.experienceLevel==13,200);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND,ItemStack.EMPTY);
|
||||
var boat=EntityTypes.OAK_BOAT.create(s.overworld(),EntitySpawnReason.COMMAND);boat.setPos(p.position().add(0,12,0));
|
||||
var chicken=EntityTypes.CHICKEN.create(s.overworld(),EntitySpawnReason.COMMAND);chicken.setPos(boat.position());
|
||||
s.overworld().addFreshEntity(boat);s.overworld().addFreshEntity(chicken);chicken.startRiding(boat,true,true);p.startRiding(boat,true,true);
|
||||
});
|
||||
c.waitFor(m->m.player.getVehicle() instanceof AbstractBoat b&&b.getControllingPassenger()==m.player,200);
|
||||
c.getInput().holdKey(o->o.keyJump);c.waitTicks(20);double initial=c.computeOnClient(m->m.player.getVehicle().getY());
|
||||
c.waitTicks(100);double top=c.computeOnClient(m->m.player.getVehicle().getY());
|
||||
require(top-initial>20&&top-initial<30,"Chicken-first boat ascends at about five blocks/sec for over the flight watchdog interval");
|
||||
c.takeScreenshot("sanctuary-beta046-chicken-flight");
|
||||
c.getInput().releaseKey(o->o.keyJump);c.waitTicks(20);double downStart=c.computeOnClient(m->m.player.getVehicle().getY());
|
||||
c.waitTicks(80);double bottom=c.computeOnClient(m->m.player.getVehicle().getY());
|
||||
require(downStart-bottom>16&&downStart-bottom<24,"Released Space descends at the same pace");
|
||||
require(server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getVehicle() instanceof AbstractBoat b&&b.isFlyingVehicle()),"Server retains pilot and accepts sustained native flight");
|
||||
c.runOnClient(m->{m.updateTitle();require(org.lwjgl.sdl.SDLVideo.SDL_GetWindowTitle(m.getWindow().handle()).equals(ApplicationBrand.title()),"Title stays Sanctuary while playing");});
|
||||
System.out.println("SKYFLIGHT046_CLIENT_PASS: native app identity, aptitude purchase, held-key FOV limits and release, first-person paid drawing/undo, chicken-first boat ascent/descent and flight watchdog");
|
||||
}finally{onboarding.set(false);c.getInput().releaseKey(InputConstants.KEY_C);c.getInput().releaseKey(o->o.keyJump);}
|
||||
}
|
||||
private static Vector3f direction(Ref r){var p=r.point();return new Vector3f(p.x(),p.y(),p.z()).normalize().rotateX(StarClient.angle()).rotateY(-(float)Math.PI/2);}
|
||||
private static void aim(ClientGameTestContext c,Ref ref){Vector3f v=c.computeOnClient(m->direction(ref));aim(c,v);}
|
||||
private static void aim(ClientGameTestContext c,Vector3f direction){
|
||||
float yaw=(float)Math.toDegrees(Math.atan2(-direction.x,direction.z));float pitch=-(float)Math.toDegrees(Math.asin(direction.y));
|
||||
c.getInput().lookAt(yaw,pitch);c.waitTicks(4);
|
||||
}
|
||||
private static boolean key(Button b,String key){return b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key);}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean yes,String message){if(!yes)throw new AssertionError(message);}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.stars.*;
|
||||
import fr.koka.sanctuary.stars.ConstellationLedger.Ref;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.item.*;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
public final class StarInspection047ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected("en_us");m.options.languageCode="en_us";return m.reloadResourcePacks();});
|
||||
c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();
|
||||
c.runOnClient(m->m.gui.setScreen(new TitleScreen(false)));c.waitTicks(3);
|
||||
c.runOnClient(m->{var b=titleButton(m.gui.screen());move(m,b.getX()+b.getWidth()/2,b.getY()+10);});c.waitTicks(4);
|
||||
c.runOnClient(m->require(titleButton(m.gui.screen()).getMessage().getStyle().getColor().getValue()==0xFFFF00,"Native active button label is vivid yellow on hover"));
|
||||
c.takeScreenshot("sanctuary-beta047-hover-yellow");
|
||||
c.runOnClient(m->titleButton(m.gui.screen()).active=false);c.waitTicks(2);
|
||||
c.runOnClient(m->require(titleButton(m.gui.screen()).getMessage().getStyle().getColor().getValue()!=0xFFFF00,"Disabled button retains its native inactive color"));
|
||||
c.runOnClient(m->{titleButton(m.gui.screen()).active=true;move(m,2,2);});c.waitTicks(2);
|
||||
c.runOnClient(m->{var color=titleButton(m.gui.screen()).getMessage().getStyle().getColor();require(color==null||color.getValue()!=0xFFFF00,"Leaving hover restores original label color");});
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b,"sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{
|
||||
s.setName("Star inspection047 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(true);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(12);p.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND,new ItemStack(Items.SPYGLASS));
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
var stars=StarService.ledger(s);
|
||||
for(int i=0;i<24;i++)stars.add(new StarLedger.Event("client047:"+i,"advancement",p.getUUID(),"Aster "+i,"minecraft:story/mine_stone","2026-09-14T12:34:00Z",3));
|
||||
try{stars.save();}catch(java.io.IOException e){throw new RuntimeException(e);}ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m->StarClient.starCount()>=72&&ConstellationClient.editing()&&m.player.experienceLevel==12,200);c.waitTicks(5);
|
||||
var refs=c.computeOnClient(m->StarClient.markers().entrySet().stream().filter(e->e.getValue()==3).map(e->new Ref(e.getKey(),0)).filter(r->direction(r).y>.3).limit(3).toList());
|
||||
require(refs.size()==3,"Three visible named event stars for inspection");
|
||||
aim(c,refs.get(0));c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_LEFT);c.waitTicks(4);
|
||||
aim(c,refs.get(1));c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_LEFT);c.waitFor(m->ConstellationClient.edges().size()==1&&m.player.experienceLevel==9,200);
|
||||
c.getInput().holdMouse(InputConstants.MOUSE_BUTTON_RIGHT);
|
||||
try{c.waitFor(m->m.player.isScoping()&&StarInspectionClient.detail()!=null,200);}
|
||||
catch(AssertionError e){diagnose(c,refs.get(1));server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();System.out.println("STAR047_DIAGNOSTIC server scope="+p.isScoping()+" description="+StarInspectionService.inspect(p,new StarInspectionPayloads.Query(refs.get(1),0)));});c.takeScreenshot("sanctuary-beta047-failure");throw e;}
|
||||
c.runOnClient(m->{var d=StarInspectionClient.detail();require(d.star().equals(refs.get(1))&&d.name().getString().contains("Aster")&&d.origin().getString().contains("Stone Age")&&d.total()==1,"Scoped star has name, native advancement origin, and actual membership");require(ConstellationClient.edges().size()==1,"Normal right click observes without deleting the selected constellation");});
|
||||
c.takeScreenshot("sanctuary-beta047-star-en");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var ledger=ConstellationService.ledger(s);
|
||||
require(ConstellationService.perform(p,new ConstellationPayloads.Request(false,refs.get(2),refs.get(2),refs.get(1),ledger.revision())).equals("added"),"Add a second real membership");});
|
||||
try{c.waitFor(m->StarInspectionClient.detail()!=null&&StarInspectionClient.detail().total()==2&&m.player.experienceLevel==6,200);}
|
||||
catch(AssertionError e){
|
||||
diagnose(c,refs.get(1));c.takeScreenshot("sanctuary-beta047-failure");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();System.out.println("STAR047_DIAGNOSTIC server scope="+p.isScoping()+" xp="+p.experienceLevel+" description="+StarInspectionService.inspect(p,new StarInspectionPayloads.Query(refs.get(1),0)));});throw e;
|
||||
}
|
||||
c.getInput().scroll(-1);c.waitFor(m->StarInspectionClient.detail()!=null&&StarInspectionClient.detail().page()==1,200);
|
||||
c.runOnClient(m->require(m.player.isScoping()&&m.player.getMainHandItem().is(Items.SPYGLASS),"Paging the card does not change the selected hotbar item"));
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});c.waitTicks(4);c.takeScreenshot("sanctuary-beta047-star-fr-page-2");
|
||||
c.runOnClient(m->{var title=net.minecraft.network.chat.Component.translatable("advancements.story.mine_stone.title").getString();require(!title.equals("Stone Age")&&StarInspectionClient.detail().origin().getString().contains(title),"Native advancement title follows the client language");});
|
||||
c.getInput().releaseMouse(InputConstants.MOUSE_BUTTON_RIGHT);c.waitFor(m->!m.player.isScoping()&&StarInspectionClient.detail()==null,100);
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(2);c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_RIGHT);c.getInput().releaseKey(o->o.keyShift);
|
||||
c.waitFor(m->ConstellationClient.edges().size()==1,200);
|
||||
c.runOnClient(m->require(m.player.experienceLevel==6,"Reading and undo never spend extra XP or refund drawing"));
|
||||
c.getInput().holdMouse(InputConstants.MOUSE_BUTTON_RIGHT);aim(c,refs.get(1));c.waitFor(m->StarInspectionClient.detail()!=null&&StarInspectionClient.detail().total()==1,200);
|
||||
c.getInput().lookAt(0,45);c.waitFor(m->StarInspectionClient.detail()==null,100);
|
||||
c.getInput().releaseMouse(InputConstants.MOUSE_BUTTON_RIGHT);
|
||||
System.out.println("STAR047_CLIENT_PASS: native yellow hover, scope gestures, localized source and star name, live constellation membership, paging, no XP debit and prompt clearing");
|
||||
}finally{onboarding.set(false);c.getInput().releaseMouse(InputConstants.MOUSE_BUTTON_RIGHT);c.getInput().releaseKey(o->o.keyShift);}
|
||||
}
|
||||
private static void diagnose(ClientGameTestContext c,Ref expected){c.runOnClient(m->System.out.println("STAR047_DIAGNOSTIC client scope="+m.player.isScoping()+" use="+m.options.keyUse.isDown()+" hand="+m.player.getMainHandItem()+" observing="+StarInspectionClient.observing()+" xp="+m.player.experienceLevel+" links="+ConstellationClient.edges().size()+" rev="+ConstellationClient.revision()+" target="+StarTargeting.pick(.2)+" detail="+StarInspectionClient.detail()+" focus="+m.isWindowActive()+" hit="+m.hitResult.getType()+" expected="+direction(expected)+" angle="+Math.toDegrees(Math.acos(Math.clamp(direction(expected).dot(new Vector3f((float)m.player.getLookAngle().x,(float)m.player.getLookAngle().y,(float)m.player.getLookAngle().z)),-1,1)))+" wider="+StarTargeting.pick(2.5)+" fov="+m.gameRenderer.mainCamera().getFov()+" look="+m.player.getLookAngle()+" stars="+m.gameRenderer.mainCamera().attributeProbe().getValue(net.minecraft.world.attribute.EnvironmentAttributes.STAR_BRIGHTNESS,1f)));}
|
||||
private static void move(net.minecraft.client.Minecraft m,double x,double y){var w=m.getWindow();double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();}
|
||||
private static Button titleButton(net.minecraft.client.gui.screens.Screen s){return s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b,"menu.singleplayer")).findFirst().orElseThrow();}
|
||||
private static Vector3f direction(Ref r){var p=r.point();return new Vector3f(p.x(),p.y(),p.z()).normalize().rotateX(StarClient.angle()).rotateY(-(float)Math.PI/2);}
|
||||
private static void aim(ClientGameTestContext c,Ref ref){Vector3f d=c.computeOnClient(m->direction(ref));c.getInput().lookAt((float)Math.toDegrees(Math.atan2(-d.x,d.z)),-(float)Math.toDegrees(Math.asin(d.y)));c.waitTicks(4);}
|
||||
private static boolean key(Button b,String key){return b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key);}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler root){return root.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean yes,String message){if(!yes)throw new AssertionError(message);}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.client.HelloWorldScreen;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import java.util.*;
|
||||
|
||||
/** Existing scenarios select one starter before their normal colour/Enter steps. Tests only. */
|
||||
public final class Starter039TestSupport implements ClientModInitializer {
|
||||
public static boolean manual;
|
||||
private static final Set<HelloWorldScreen> handled=Collections.newSetFromMap(new WeakHashMap<>());
|
||||
public void onInitializeClient(){ClientTickEvents.END_CLIENT_TICK.register(m->{if(!manual&&m.gui.screen() instanceof HelloWorldScreen s&&!s.starterChoices().isEmpty()&&handled.add(s))selectFirst(s);});}
|
||||
public static void selectFirst(HelloWorldScreen s){
|
||||
var enter=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("StarterSlot")).findFirst().orElseThrow().onPress(enter);
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.companion.CompanionType;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
public final class Starter052ClientChecks implements FabricClientGameTest {
|
||||
private static final String NAME="Étoile de lune";
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;
|
||||
var choices=List.of("cat","chicken","pig").stream().map(id->new StarterCompanions.Choice(id,CompanionType.Rarity.COMMON)).toList();
|
||||
var welcome=new StarterPayloads.Welcome(new ProgressionPayloads.Hello("Poupoutain",java.util.stream.IntStream.range(0,20).map(i->0xAAAA00+i*100).boxed().toList(),""),"",false,choices);
|
||||
c.runOnClient(m->{require(m.player==null&&m.level==null,"Cold welcome before any player or level");m.gui.setScreen(new HelloWorldScreen(welcome,()->{}));});
|
||||
for(String locale:List.of("en_us","fr_fr"))for(int scale:List.of(1,2)){
|
||||
c.runOnClient(m->{
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
var screen=(HelloWorldScreen)m.gui.screen();name(screen).setValue(NAME);Starter039TestSupport.selectFirst(screen);color(screen).onPress(enter());
|
||||
m.options.guiScale().set(scale);m.resizeGui();screen=(HelloWorldScreen)m.gui.screen();
|
||||
require(name(screen).getValue().equals(NAME),"Draft survives GUI resize");
|
||||
screen.accept(new StarterPayloads.Welcome(new ProgressionPayloads.Hello("Poupoutain",welcome.identity().colors(),"color_taken"),"",false,choices));
|
||||
require(name(screen).getValue().equals(NAME)&&screen.starterChoices().equals(choices),"Rejected colour preserves name and original eggs");
|
||||
screen.accept(welcome);
|
||||
var slots=screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("StarterSlot")).toList();
|
||||
require(slots.getFirst().getX()==name(screen).getX()&&slots.getFirst().getX()==color(screen).getX(),"Egg choices align with the name field and colour controls on the left");
|
||||
require(name(screen).getY()>slots.getFirst().getBottom()&&button(screen,"sanctuary.progression.enter").getY()>name(screen).getBottom(),"Name beneath eggs and above actions");
|
||||
for(var child:screen.children())if(child instanceof AbstractWidget w)require(w.getX()>=0&&w.getRight()<=screen.width&&w.getY()>=0&&w.getBottom()<=screen.height,"Widget fits "+locale+" "+screen.width+"x"+screen.height);
|
||||
require(!name(screen).getMessage().getString().startsWith("sanctuary."),"Localized name field");
|
||||
System.out.println("STARTER052_LAYOUT "+locale+" gui="+screen.width+"x"+screen.height);
|
||||
});c.waitTicks(5);c.takeScreenshot("sanctuary-beta052-hello-"+locale+"-"+scale);
|
||||
}
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.gui.setScreen(new TitleScreen(false));m.options.guiScale().set(2);m.resizeGui();});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
require(m.player==null&&m.level==null,"Real naming happens in configuration");name(s).setValue(NAME);Starter039TestSupport.selectFirst(s);color(s).onPress(enter());button(s,"sanctuary.progression.enter").onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Starter052 "+UUID.randomUUID());s.setSeed("52");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(ProgressionService.session(s).starters.get(p.getUUID()).name().equals(NAME),"Name stored by real configuration handler");require(AccessoryAccess.of(p).getItem(AccessoryContainer.FAMILIAR).getCustomName().getString().equals(NAME),"Named starter delivered");FamiliarService.tick(p);s.saveEverything(true,true,true);});
|
||||
c.waitFor(m->AccessoryClientName(m),200);
|
||||
c.runOnClient(m->ProgressionClient.open(null,"identity"));c.waitTicks(5);c.takeScreenshot("sanctuary-beta052-inhabitant");c.runOnClient(m->m.gui.setScreen(null));
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(ProgressionService.session(s).starters.get(p.getUUID()).name().equals(NAME),"Starter registry survives restart");require(AccessoryAccess.of(p).getItem(AccessoryContainer.FAMILIAR).getCustomName().getString().equals(NAME),"Player save retains custom name");require(!StarterCompanions.deliver(p,ProgressionService.session(s).starters),"No duplicate on reconnect");});
|
||||
c.waitFor(m->AccessoryClientName(m),200);
|
||||
}finally{Starter039TestSupport.manual=false;}
|
||||
System.out.println("STARTER052_CLIENT_PASS: cold welcome, FR/EN, compact layout, draft retention, real named admission and saved reconnect");
|
||||
}
|
||||
private static boolean AccessoryClientName(net.minecraft.client.Minecraft m){var egg=AccessoryAccess.of(m.player).getItem(AccessoryContainer.FAMILIAR);return egg.getCustomName()!=null&&egg.getCustomName().getString().equals(NAME);}
|
||||
private static EditBox name(Screen s){return (EditBox)widget(s,"sanctuary.starter.name");}
|
||||
private static Button button(Screen s,String key){return (Button)widget(s,key);}
|
||||
private static AbstractWidget widget(Screen s,String key){return s.children().stream().filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
|
||||
private static Button color(Screen s){return s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean v,String m){if(!v)throw new AssertionError(m);}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.CarryService;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.effect.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Real carry links, potion lifecycle and movement-attribute synchronization in a development client. */
|
||||
public final class Strength055ClientChecks implements FabricClientGameTest {
|
||||
private static final net.minecraft.resources.Identifier MODIFIER=fr.koka.sanctuary.SanctuaryMod.id("carrying_speed");
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);var base=new AtomicReference<Double>();var upper=new ArrayList<ServerPlayer>();
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
var enter=new net.minecraft.client.input.KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter);onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Strength055 "+UUID.randomUUID());s.setSeed("55");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setAllowCommands(true);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.removeAllEffects();
|
||||
var origin=p.blockPosition();for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++)for(int y=-1;y<12;y++)p.level().setBlockAndUpdate(origin.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
base.set(p.getAttributeValue(Attributes.MOVEMENT_SPEED));
|
||||
var q=guest(p,"Passenger055");upper.add(q);q.setPos(p.position().add(0,0,1.5));
|
||||
check(CarryService.interact(q,p),"A player can climb the host");ratio(p,base.get(),.75);
|
||||
p.addEffect(new MobEffectInstance(MobEffects.STRENGTH,600,0));
|
||||
});
|
||||
c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();ratio(p,base.get(),Math.sqrt(.75));p.addEffect(new MobEffectInstance(MobEffects.STRENGTH,600,1));});
|
||||
c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();ratio(p,base.get(),Math.cbrt(.75));p.addEffect(new MobEffectInstance(MobEffects.STRENGTH,600,255));});
|
||||
c.waitTicks(3);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();ratio(p,base.get(),Math.pow(.75,1.0/257));check(p.getAttributeValue(Attributes.MOVEMENT_SPEED)<=base.get(),"Even very high Strength never speeds carrying beyond unladen movement");
|
||||
p.removeEffect(MobEffects.STRENGTH);var q=upper.getFirst();q.addEffect(new MobEffectInstance(MobEffects.STRENGTH,600,1));
|
||||
var r=guest(p,"Top055");upper.add(r);r.setPos(q.position().add(0,0,1.5));check(CarryService.interact(r,q),"Second player joins the stack");
|
||||
ratio(p,base.get(),.5625);ratio(q,base.get(),Math.cbrt(.75));
|
||||
p.addEffect(new MobEffectInstance(MobEffects.STRENGTH,600,0));
|
||||
});
|
||||
c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();ratio(p,base.get(),.75);p.addEffect(new MobEffectInstance(MobEffects.SPEED,600,0));});
|
||||
c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();ratio(p,base.get(),.75*1.2);check(p.getAttribute(Attributes.MOVEMENT_SPEED).getModifiers().stream().filter(m->m.id().equals(MODIFIER)).count()==1,"Only one carrying modifier, no accumulation across potion changes");});
|
||||
c.waitFor(m->Math.abs(m.player.getAttributeValue(Attributes.MOVEMENT_SPEED)/base.get()-.9)<.0001,100);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.removeEffect(MobEffects.SPEED);p.removeEffect(MobEffects.STRENGTH);p.addEffect(new MobEffectInstance(MobEffects.STRENGTH,8,0));});
|
||||
c.waitTicks(12);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();check(!p.hasEffect(MobEffects.STRENGTH),"Temporary Strength naturally expires");ratio(p,base.get(),.5625);
|
||||
CarryService.detach(p);for(var q:upper){CarryService.detach(q);q.discard();}upper.clear();ratio(p,base.get(),1);
|
||||
check(p.getAttribute(Attributes.MOVEMENT_SPEED).getModifier(MODIFIER)==null,"Dismount removes the carrying modifier");
|
||||
p.addEffect(new MobEffectInstance(MobEffects.STRENGTH,600,1));
|
||||
});
|
||||
c.waitTicks(3);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();ratio(p,base.get(),1);
|
||||
var cow=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);cow.setNoAi(true);cow.setPos(p.position().add(0,0,1.5));p.level().addFreshEntity(cow);
|
||||
check(CarryService.interact(p,cow),"Adult animal can be carried");ratio(p,base.get(),.75);CarryService.release(cow,true);
|
||||
cow.setBaby(true);cow.setPos(p.position().add(0,0,1.5));check(CarryService.interact(p,cow),"Baby animal can be carried");ratio(p,base.get(),1);
|
||||
CarryService.release(cow,true);cow.discard();p.removeAllEffects();ratio(p,base.get(),1);
|
||||
System.out.println("STRENGTH055_PASS: carrier Strength I/II/256, whole stack, no rider-effect sharing, native expiry, Speed composition, client sync, adult/baby weight and dismount cleanup");
|
||||
});
|
||||
}
|
||||
}
|
||||
private static ServerPlayer guest(ServerPlayer host,String name){
|
||||
var s=host.level().getServer();var profile=new com.mojang.authlib.GameProfile(UUID.randomUUID(),name);
|
||||
var p=new ServerPlayer(s,host.level(),profile,net.minecraft.server.level.ClientInformation.createDefault());
|
||||
var connection=new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);new io.netty.channel.embedded.EmbeddedChannel(connection);
|
||||
p.connection=new net.minecraft.server.network.ServerGamePacketListenerImpl(s,connection,p,net.minecraft.server.network.CommonListenerCookie.createInitial(profile,false));
|
||||
p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);p.getAbilities().invulnerable=true;
|
||||
p.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());host.level().addNewPlayer(p);return p;
|
||||
}
|
||||
private static void ratio(ServerPlayer p,double base,double expected){double actual=p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base;check(Math.abs(actual-expected)<.0001,"Movement ratio for "+p.getName().getString()+": expected="+expected+" actual="+actual);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import java.util.List;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.FriendsButton;
|
||||
import net.minecraft.client.gui.components.SpriteIconButton;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import net.minecraft.client.gui.screens.options.OptionsScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
/** Native menu navigation and sizing, without creating a world or changing friend settings. */
|
||||
public final class Title045ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
c.runOnClient(m -> m.gui.setScreen(new TitleScreen(false)));
|
||||
for (String locale : List.of("en_us","fr_fr")) {
|
||||
String label=locale.equals("fr_fr")?"Amis":"Friends";
|
||||
var reload=c.computeOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);
|
||||
m.options.languageCode=locale;
|
||||
return m.reloadResourcePacks();
|
||||
});
|
||||
c.waitFor(m -> reload.isDone() && m.gui.overlay()==null,1200);
|
||||
reload.join();
|
||||
c.waitFor(m -> Component.translatable("sanctuary.menu.friends").getString().equals(label),300);
|
||||
for (int scale : List.of(1,2,3,4)) {
|
||||
c.runOnClient(m -> {
|
||||
m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new TitleScreen(false));
|
||||
var screen=(TitleScreen)m.gui.screen();
|
||||
var solo=button(screen,"menu.singleplayer");
|
||||
var multi=button(screen,"menu.multiplayer");
|
||||
var friends=button(screen,"sanctuary.menu.friends");
|
||||
require(friends.getWidth()==200 && friends.getHeight()==20,"Friends uses a normal full-width Minecraft button");
|
||||
require(solo.getX()==multi.getX() && multi.getX()==friends.getX(),"Main buttons align");
|
||||
require(multi.getY()==solo.getY()+24 && friends.getY()==multi.getY()+24,"Friends is directly below Multiplayer");
|
||||
require(button(screen,"menu.options").getY()==friends.getY()+24,"Options follows without the empty icon row");
|
||||
require(friends.active==(!m.isDemo()&&!m.isOfflineDeveloperMode()),"Native friend account restrictions are preserved");
|
||||
var children=screen.children();
|
||||
require(children.indexOf(solo)<children.indexOf(multi) && children.indexOf(multi)<children.indexOf(friends)
|
||||
&& children.indexOf(friends)<children.indexOf(button(screen,"menu.options")),"Keyboard order follows the visible menu");
|
||||
for (var child : children) if (child instanceof AbstractWidget widget) {
|
||||
require(!(widget instanceof FriendsButton) && !(widget instanceof SpriteIconButton),"Compact icon buttons are absent");
|
||||
require(!key(widget,"menu.online")&&!key(widget,"options.language")&&!key(widget,"options.accessibility"),"Removed entries are absent");
|
||||
require(widget.getY()>=0 && widget.getY()+widget.getHeight()<=screen.height,"Widgets fit at GUI scale "+scale);
|
||||
}
|
||||
String version=FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow().getMetadata().getVersion().getFriendlyString();
|
||||
require(Component.translatable("sanctuary.menu.version",version).getString().equals("Sanctuary "+version),"Version label follows the installed mod");
|
||||
System.out.println("TITLE045_LAYOUT "+locale+" requestedScale="+scale+" gui="+screen.width+"x"+screen.height);
|
||||
});
|
||||
c.waitTicks(5);
|
||||
if (scale==2 || scale==4) c.takeScreenshot("sanctuary-beta045-title-"+locale+"-"+scale);
|
||||
}
|
||||
}
|
||||
c.runOnClient(m -> button((TitleScreen)m.gui.screen(),"menu.options").onPress(enter()));
|
||||
c.waitFor(m -> m.gui.screen() instanceof OptionsScreen,100);
|
||||
c.runOnClient(m -> m.gui.setScreen(new TitleScreen(false)));
|
||||
// Inspect the native route without accepting or modifying online friend settings.
|
||||
c.runOnClient(m -> button((TitleScreen)m.gui.screen(),"sanctuary.menu.friends").onPress(enter()));
|
||||
c.waitFor(m -> m.gui.screen() instanceof net.minecraft.client.gui.screens.friends.FriendsOverlayScreen
|
||||
|| (m.gui.screen()!=null && m.gui.screen().getClass().getSimpleName().equals("FriendsListConfirmScreen")),100);
|
||||
c.runOnClient(m -> { m.gui.setScreen(new TitleScreen(false));m.options.guiScale().set(2);m.resizeGui(); });
|
||||
System.out.println("TITLE045_CLIENT_PASS: FR/EN, four GUI scales, full-width Friends, native options/friends routes and no compact/Realms entries");
|
||||
}
|
||||
private static Button button(TitleScreen screen,String name) {
|
||||
return screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b -> key(b,name)).findFirst().orElseThrow();
|
||||
}
|
||||
private static boolean key(AbstractWidget widget,String name) {
|
||||
return widget.getMessage().getContents() instanceof TranslatableContents text && text.getKey().equals(name);
|
||||
}
|
||||
private static KeyEvent enter() { return new KeyEvent(InputConstants.KEY_RETURN,0,0); }
|
||||
private static void require(boolean value,String message) { if(!value) throw new AssertionError(message); }
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.tutorial.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
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.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
/** Native E opening -> bounded server award -> spendable XP and rendered skill icons. */
|
||||
public final class Tutorial016ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if(armed.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());armed.set(false);
|
||||
}
|
||||
});
|
||||
String language=c.computeOnClient(m -> m.getLanguageManager().getSelected());
|
||||
int gui=c.computeOnClient(m -> m.options.guiScale().get());
|
||||
var key=c.computeOnClient(m -> InputConstants.getKey(m.options.keyInventory.saveString()));
|
||||
try(var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Sanctuary beta016 tutorial "+UUID.randomUUID());
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset()!=null && e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());
|
||||
s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
}).create()) {
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m -> m.player!=null && ProgressionClient.active() && TutorialClient.completedMask>=0 && m.gui.screen()==null,1200);
|
||||
c.runOnClient(m -> {m.options.keyInventory.setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_E));KeyMapping.resetMapping();});
|
||||
world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();
|
||||
// Isolate inventory opening from the fresh world's automatic tree observation.
|
||||
var progress=TutorialProgress.EMPTY;for(String step:TutorialProgress.STEPS)if(!step.equals("open_inventory"))progress=progress.complete(step,8);
|
||||
p.setAttached(TutorialService.RECORD,progress.json());TutorialService.initialize(p);
|
||||
p.setExperienceLevels(0);p.setExperiencePoints(0);p.totalExperience=0;ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m -> TutorialClient.completedMask==23 && m.player.experienceLevel==0,200);
|
||||
c.getInput().pressKey(InputConstants.KEY_E);
|
||||
c.waitFor(m -> m.gui.screen() instanceof InventoryScreen && TutorialClient.completedMask==31 && m.player.experienceLevel==1,200);
|
||||
require(world.getServer().computeOnServer(s -> s.getPlayerList().getPlayers().getFirst().totalExperience==8),"Native E triggers exactly eight server XP");
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);c.waitTicks(3);
|
||||
c.getInput().pressKey(InputConstants.KEY_E);c.waitFor(m -> m.gui.screen() instanceof InventoryScreen,200);
|
||||
c.runOnClient(m -> {for(int i=0;i<20;i++)ClientPlayNetworking.send(new TutorialPayloads.InventoryOpened());});c.waitTicks(25);
|
||||
require(world.getServer().computeOnServer(s -> s.getPlayerList().getPlayers().getFirst().totalExperience==8),"Reopening and repeated client packets never pay twice");
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);c.waitTicks(3);
|
||||
for(String locale:new String[]{"en_us","fr_fr"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.options.guiScale().set(0);m.resizeGui();ProgressionClient.open(null);
|
||||
require(!TutorialService.label("reward",TutorialService.label("open_inventory"),8,1,5).getString().contains("sanctuary."),"Reward is translated in both languages");
|
||||
});c.waitTick();
|
||||
c.runOnClient(m -> {
|
||||
require(widgets(m.gui.screen()).filter(ImageWidget.class::isInstance).count()==6,"Six new skill icons are rendered");
|
||||
require(plus(m.gui.screen(),"vitality").active && plus(m.gui.screen(),"vitality").visible,"One tutorial reward makes a skill affordable");
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta016-tutorial-xp-"+locale);
|
||||
}
|
||||
c.runOnClient(m -> plus(m.gui.screen(),"vitality").onPress(enter()));
|
||||
c.waitFor(m -> ProgressionClient.progress.vitality()==1 && m.player.experienceLevel==0,200);
|
||||
require(world.getServer().computeOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();return p.getMaxHealth()==8 && TutorialService.progress(p).done("open_inventory");}),"Real purchase spends tutorial XP and leaves the claim intact");
|
||||
System.out.println("Tutorial016 native client passed: E, repeat protection, FR/EN, six icons, funded purchase");
|
||||
} finally {
|
||||
armed.set(false);c.runOnClient(m -> {
|
||||
m.options.keyInventory.setKey(key);KeyMapping.resetMapping();
|
||||
m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(gui);m.resizeGui();
|
||||
});
|
||||
}
|
||||
}
|
||||
private static Button plus(Screen screen,String skill) {
|
||||
var name=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast)
|
||||
.filter(w -> key(w,"sanctuary.progression."+skill)).findFirst().orElseThrow();
|
||||
return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getY()==name.getY() && b.getMessage().getString().equals("+")).findFirst().orElseThrow();
|
||||
}
|
||||
private static Stream<GuiEventListener> widgets(GuiEventListener widget) {
|
||||
return Stream.concat(Stream.of(widget),widget instanceof ContainerEventHandler parent
|
||||
? parent.children().stream().flatMap(Tutorial016ClientChecks::widgets) : Stream.empty());
|
||||
}
|
||||
private static boolean key(AbstractWidget widget,String key) {return widget.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key);}
|
||||
private static Button button(Screen screen,String key) {return widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b -> key(b,key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter() {return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String message) {if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.realtime.*;
|
||||
import fr.koka.sanctuary.weather.*;
|
||||
import java.nio.file.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
|
||||
public final class Weather057ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);byte[] prior;
|
||||
try{prior=Files.exists(WeatherService.CONFIG)?Files.readAllBytes(WeatherService.CONFIG):null;Files.createDirectories(WeatherService.CONFIG.getParent());Files.writeString(WeatherService.CONFIG,WeatherConfig.DEFAULT.json());}catch(Exception e){throw new RuntimeException(e);}
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.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());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Weather057 "+UUID.randomUUID());s.setSeed("57");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();var day=new AtomicReference<>(LocalDate.of(2026,6,21));
|
||||
server.runOnServer(s->{
|
||||
check(WeatherService.active(s.overworld())&&RealtimeService.active(s.overworld()),"Daily weather and real sun active by default");
|
||||
check(!WeatherService.active(s.getLevel(net.minecraft.world.level.Level.NETHER))&&!WeatherService.active(s.getLevel(net.minecraft.world.level.Level.END)),"Weatherless dimensions remain unmanaged");
|
||||
s.getGlobalGameRules().set(GameRules.ADVANCE_WEATHER,true,s);s.getPlayerList().getPlayers().getFirst().getAbilities().invulnerable=true;
|
||||
checked(()->{
|
||||
var commands=s.getCommands().getDispatcher();var source=s.createCommandSourceStack();
|
||||
check(commands.execute("sanctuary weather",source)==1,"Status command registered with other Sanctuary branches");
|
||||
check(commands.execute("sanctuary time vanilla",source)==1&&WeatherService.active(s.overworld()),"Weather independent from sun mode");
|
||||
check(commands.execute("sanctuary time realtime",source)==1,"Sun mode restored");
|
||||
var host=s.getPlayerList().getPlayers().getFirst();var branch=commands.getRoot().getChild("sanctuary").getChild("weather");
|
||||
s.getWorldData().setAllowCommands(false);
|
||||
try{check(branch.canUse(host.createCommandSourceStack()),"Weather status visible normally");for(String mode:List.of("realtime","vanilla","reload"))check(!branch.getChild(mode).canUse(host.createCommandSourceStack()),"Weather mutation hidden without Allow Commands: "+mode);}
|
||||
finally{s.getWorldData().setAllowCommands(true);}
|
||||
});
|
||||
});
|
||||
c.getInput().lookAt(0,-5);
|
||||
for(var kind:List.of(DailyWeather.Kind.RAIN,DailyWeather.Kind.THUNDER,DailyWeather.Kind.OVERCAST,DailyWeather.Kind.FOG,DailyWeather.Kind.CLEAR)){
|
||||
server.runOnServer(s->{
|
||||
LocalDate next=day.get().plusDays(1);while(DailyWeather.at(s.overworld().getSeed(),next,WeatherConfig.DEFAULT)!=kind)next=next.plusDays(1);day.set(next);at(s,next.atTime(12,0).atZone(RealtimeService.civilTime(s).getZone()).toInstant());
|
||||
});
|
||||
c.waitTicks(25);
|
||||
c.waitFor(m->WeatherClient.state().active()&&WeatherClient.state().kind()==kind,100);
|
||||
server.runOnServer(s->{var d=s.overworld().getWeatherData();check(d.isRaining()==kind.raining()&&d.isThundering()==kind.thundering(),"Native state matches daily "+kind);check(WeatherService.session(s).state().date()==day.get().toEpochDay(),"Day boundary committed");d.setClearWeatherTime(1);d.setRainTime(1);d.setThunderTime(1);});
|
||||
c.waitFor(m->(kind.raining()?m.level.getRainLevel(1)>.95:m.level.getRainLevel(1)<.01)&&(kind.thundering()?m.level.getThunderLevel(1)>.95:m.level.getThunderLevel(1)<.01),300);
|
||||
c.waitTicks(25);
|
||||
if(kind==DailyWeather.Kind.FOG||kind==DailyWeather.Kind.OVERCAST||kind==DailyWeather.Kind.CLEAR)c.waitTicks(205);
|
||||
c.runOnClient(m->{
|
||||
float end=m.level.environmentAttributes().getDimensionValue(net.minecraft.world.attribute.EnvironmentAttributes.FOG_END_DISTANCE);
|
||||
check(kind==DailyWeather.Kind.FOG?end<=80.01:end>80.01,"Native environment fog distance follows the server atmosphere");
|
||||
if(kind==DailyWeather.Kind.OVERCAST){var color=m.level.environmentAttributes().getDimensionValue(net.minecraft.world.attribute.EnvironmentAttributes.SKY_COLOR);check(Math.max(color.x(),Math.max(color.y(),color.z()))-Math.min(color.x(),Math.min(color.y(),color.z()))<.12,"Cloudy sky is gray without fake rain");}
|
||||
if(kind==DailyWeather.Kind.FOG){float gpu=m.gameRenderer.gameRenderState().levelRenderState.cameraRenderState.fogData.environmentalEnd;check(gpu<=80.01,"Native rendered fog actually uses the shorter weather distance");}
|
||||
});
|
||||
c.takeScreenshot("sanctuary-beta057-daylight-"+kind.key());
|
||||
server.runOnServer(s->{var d=s.overworld().getWeatherData();check(d.getRainTime()==1&&d.getThunderTime()==1&&d.getClearWeatherTime()==1,"Native random countdown is suspended while interpolation continues");check(s.getGlobalGameRules().get(GameRules.ADVANCE_WEATHER),"Saved native weather rule remains untouched");
|
||||
checked(()->{var commands=s.getCommands().getDispatcher();var source=s.createCommandSourceStack();for(String name:List.of("clear","rain","thunder"))check(commands.execute("weather "+name+" 10",source)==0,"Native command explains managed weather");});
|
||||
s.overworld().resetWeatherCycle();check(d.isRaining()==kind.raining()&&d.isThundering()==kind.thundering(),"Sleep cannot clear daily forecast");
|
||||
at(s,day.get().atTime(23,59,59).atZone(RealtimeService.civilTime(s).getZone()).toInstant());
|
||||
});c.waitTicks(25);
|
||||
server.runOnServer(s->check(WeatherService.session(s).state().date()==day.get().toEpochDay()&&WeatherService.session(s).state().weather()==kind,"The whole civil day retains the same weather"));
|
||||
c.takeScreenshot("sanctuary-beta057-daily-"+kind.key());
|
||||
System.out.println("WEATHER057_NATIVE: "+kind+" all day, client interpolation and native effects");
|
||||
}
|
||||
var wetTime=new AtomicReference<Instant>();var dryTime=new AtomicReference<Instant>();
|
||||
server.runOnServer(s->{LocalDate next=day.get().plusDays(1);while(DailyWeather.at(s.overworld().getSeed(),next,WeatherConfig.DEFAULT)!=DailyWeather.Kind.SHOWERS)next=next.plusDays(1);day.set(next);var first=next.atStartOfDay(RealtimeService.civilTime(s).getZone());
|
||||
for(int i=0;i<180;i++){var moment=first.plusMinutes(i);if(DailyWeather.raining(DailyWeather.Kind.SHOWERS,s.overworld().getSeed(),moment))wetTime.compareAndSet(null,moment.toInstant());else dryTime.compareAndSet(null,moment.toInstant());}
|
||||
check(wetTime.get()!=null&&dryTime.get()!=null,"Shower day has both wet and dry moments");at(s,wetTime.get());
|
||||
});c.waitFor(m->WeatherClient.state().kind()==DailyWeather.Kind.SHOWERS&&m.level.getRainLevel(1)>.95,350);c.takeScreenshot("sanctuary-beta057-showers-wet");
|
||||
server.runOnServer(s->at(s,dryTime.get()));c.waitFor(m->WeatherClient.state().kind()==DailyWeather.Kind.SHOWERS&&m.level.getRainLevel(1)<.01,350);c.takeScreenshot("sanctuary-beta057-showers-dry");
|
||||
server.runOnServer(s->checked(()->{WeatherService.reload(s);check(WeatherService.session(s).state().weather()==DailyWeather.Kind.SHOWERS&&!s.overworld().getWeatherData().isRaining(),"Reload retains both the daily profile and its current dry interval");}));
|
||||
server.runOnServer(s->checked(()->{
|
||||
var before=WeatherService.session(s).state();var commands=s.getCommands().getDispatcher();var source=s.createCommandSourceStack();
|
||||
check(commands.execute("sanctuary weather vanilla",source)==1&&!WeatherService.active(s.overworld())&&RealtimeService.active(s.overworld()),"Vanilla weather independent from solar mode");
|
||||
commands.execute("weather thunder 600",source);check(s.overworld().getWeatherData().isThundering(),"Native command works again");
|
||||
check(commands.execute("sanctuary weather realtime",source)==1&&WeatherService.session(s).state().equals(before),"Toggling cannot reroll today's draw");
|
||||
Path path=s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-weather.json");byte[] bytes=Files.readAllBytes(path);
|
||||
WeatherService.reload(s);check(WeatherService.session(s).state().equals(before)&&Arrays.equals(bytes,Files.readAllBytes(path)),"Server-side reload preserves daily journal");
|
||||
Files.writeString(WeatherService.CONFIG,new WeatherConfig(true,0,0,0,100,0).json());WeatherService.reload(s);check(WeatherService.session(s).state().equals(before),"Changing probabilities does not interrupt today's forecast");
|
||||
Files.writeString(WeatherService.CONFIG,"{broken");boolean rejected=false;try{WeatherService.reload(s);}catch(Exception expected){rejected=true;}check(rejected&&WeatherService.active(s.overworld())&&Files.readString(WeatherService.CONFIG).equals("{broken"),"Invalid config preserves previous working session");
|
||||
Files.writeString(WeatherService.CONFIG,new WeatherConfig(false,15,5,10,3,2).json());WeatherService.reload(s);check(!WeatherService.active(s.overworld()),"Global disable restores vanilla");
|
||||
WeatherService.setMode(s,false);Files.writeString(WeatherService.CONFIG,WeatherConfig.DEFAULT.json());WeatherService.reload(s);check(!WeatherService.active(s.overworld()),"Explicit world opt-out survives config re-enable");
|
||||
WeatherService.setMode(s,true);WeatherService.setMode(s,false);s.overworld().resetWeatherCycle();check(!s.overworld().getWeatherData().isRaining(),"Native reset available again");
|
||||
s.getCommands().performPrefixedCommand(source,"weather rain 600");
|
||||
}));c.waitTicks(25);
|
||||
server.runOnServer(s->{var d=s.overworld().getWeatherData();check(d.getRainTime()>0&&d.getRainTime()<12000,"Native durations resume after leaving daily mode");});
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());for(String key:List.of("status","vanilla","clear","overcast","fog","showers","rain","thunder","inactive","invalid","managed"))check(!WeatherService.label(key,"Pluie","2026-09-15","Europe/Paris").getString().contains("sanctuary.weather."),"Localized weather text "+locale+"/"+key);});
|
||||
}
|
||||
System.out.println("WEATHER057_PASS: daily clear/overcast/fog/showers/rain/thunder, wet/dry alternation, full civil days, date rollover, native interpolation and client sync, cycle/sleep suspension, independent modes, commands and operator gates, reloads, configuration errors, vanilla return, FR/EN");
|
||||
}finally{
|
||||
onboarding.set(false);
|
||||
checked(()->{if(prior==null)Files.deleteIfExists(WeatherService.CONFIG);else Files.write(WeatherService.CONFIG,prior);});
|
||||
}
|
||||
}
|
||||
private static void at(MinecraftServer s,Instant instant){checked(()->{var field=RealtimeService.Session.class.getDeclaredField("clock");field.setAccessible(true);field.set(RealtimeService.session(s),Clock.fixed(instant,ZoneOffset.UTC));RealtimeService.tick(s);s.getPlayerList().broadcastAll(s.clockManager().createFullSyncPacket());});}
|
||||
private interface Checked{void run()throws Exception;}
|
||||
private static void checked(Checked action){try{action.run();}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean value,String why){if(!value)throw new AssertionError(why);}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
@Mixin(Minecraft.class)
|
||||
abstract class ClientVsync046Mixin {
|
||||
@Inject(method="renderFrame",at=@At("HEAD"))
|
||||
private void sanctuary$testPresentation(CallbackInfo ci){
|
||||
if(Boolean.getBoolean("sanctuary.test.noVsync")){
|
||||
var m=(Minecraft)(Object)this;if(m.options.enableVsync().get())m.options.enableVsync().set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.cycle.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Accessory018GameTests {
|
||||
@GameTest public void typedEquipmentAndShiftClickAtEveryCapacity(GameTestHelper h) {
|
||||
for(int rank=0;rank<=5;rank++) {
|
||||
var p=player(h,rank);var menu=p.inventoryMenu;var e=(SanctuaryExtendedInventoryMenu)menu;var a=AccessoryAccess.of(p);
|
||||
h.assertTrue(e.sanctuary$accessoryStart()==64 && e.sanctuary$overflowEnd()==64 && menu.slots.size()==67,"Stable storage addresses with three appended slots");
|
||||
p.getInventory().setItem(0,new ItemStack(Items.COW_SPAWN_EGG,5));menu.quickMoveStack(p,36);
|
||||
h.assertTrue(a.getItem(0).getCount()==1 && p.getInventory().getItem(0).getCount()==4,"Shift equips exactly one egg");
|
||||
menu.quickMoveStack(p,64);h.assertTrue(a.isEmpty() && p.getInventory().getItem(0).getCount()==5,"Shift unequips by merging into existing egg pile");
|
||||
p.getInventory().setItem(1,new ItemStack(AccessoryService.ZERO_CAPE));menu.quickMoveStack(p,37);
|
||||
h.assertTrue(a.getItem(1).is(AccessoryService.ZERO_CAPE),"Cape auto-equips");
|
||||
h.assertTrue(!menu.getSlot(64).mayPlace(new ItemStack(Items.STONE)) && !menu.getSlot(65).mayPlace(new ItemStack(Items.ELYTRA)),"Typed slots reject unrelated items");
|
||||
menu.setCarried(new ItemStack(Items.PISTON,12));menu.clicked(66,0,ContainerInput.PICKUP,p);
|
||||
h.assertTrue(a.getItem(2).getCount()==1 && a.getItem(2).is(Items.PISTON) && menu.getCarried().getCount()==11,"Any block can be deliberately equipped, one unit only");
|
||||
menu.setCarried(ItemStack.EMPTY);
|
||||
for(int i=0;i<36;i++)p.getInventory().setItem(i,new ItemStack(Items.STONE,64));
|
||||
for(int i=43;i<=60;i++)p.getInventory().setItem(i,new ItemStack(Items.STONE,64));
|
||||
menu.quickMoveStack(p,65);h.assertTrue(a.getItem(1).is(AccessoryService.ZERO_CAPE),"Full storage never deletes equipped cape");
|
||||
p.getInventory().setItem(8,ItemStack.EMPTY);menu.quickMoveStack(p,65);
|
||||
h.assertTrue(a.getItem(1).isEmpty() && p.getInventory().getItem(8).is(AccessoryService.ZERO_CAPE),"An eligible free hotbar slot accepts unequip at every capacity");
|
||||
var equipped=a.getItems().stream().map(ItemStack::copy).toList();bind(p,ProgressEffects.get(p).buy("sorting",4));
|
||||
InventorySortService.sort(p,new InventorySortPayloads.Sort(1,menu.containerId,menu.getStateId(),ProgressEffects.get(p).revision(),InventorySortPayloads.STORAGE));
|
||||
h.assertTrue(ItemStack.listMatches(equipped,a.getItems()),"Storage sorting never touches accessory equipment");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void storageMigrationAppearanceAndDeath(GameTestHelper h) {
|
||||
var p=player(h,5);var a=AccessoryAccess.of(p);equip(a);
|
||||
p.setItemSlot(EquipmentSlot.HEAD,new ItemStack(Items.IRON_HELMET));p.doTick();
|
||||
h.assertTrue(p.getArmorValue()==2,"Cosmetic netherite helmet grants no armor over native iron helmet");
|
||||
AccessoryService.publish(p);
|
||||
h.assertTrue(p.getAttachedOrThrow(AccessoryService.APPEARANCE).head().is(Items.NETHERITE_HELMET),"Server appearance attachment has exact equipped item");
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
|
||||
var saved=output.buildResult();var loaded=player(h,0);
|
||||
loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),saved));
|
||||
h.assertTrue(ItemStack.listMatches(a.getItems(),AccessoryAccess.of(loaded).getItems()),"Save reload preserves all three separate stacks");
|
||||
var keep=player(h,0);keep.restoreFrom(p,true);
|
||||
h.assertTrue(ItemStack.listMatches(a.getItems(),AccessoryAccess.of(keep).getItems()),"Native keepInventory copies accessories");
|
||||
var old=saved.copy();old.remove("sanctuary:accessories");
|
||||
var migrated=player(h,0);migrated.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),old));
|
||||
h.assertTrue(AccessoryAccess.of(migrated).isEmpty(),"Beta017 player receives empty slots without rearranging inventory");
|
||||
var before=h.getLevel().getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,p.getBoundingBox().inflate(5)).size();
|
||||
p.getInventory().dropAll();
|
||||
h.assertTrue(a.isEmpty() && h.getLevel().getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,p.getBoundingBox().inflate(5)).size()==before+4,"Death drops three accessories plus native helmet once");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void futureAccessoryDataIsRetained(GameTestHelper h) {
|
||||
var p=player(h,0);var input=new CompoundTag();var future=new CompoundTag();future.putInt("schema",99);future.putString("future","do not erase");input.put("sanctuary:accessories",future);
|
||||
var a=AccessoryAccess.of(p);a.read(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),input));
|
||||
h.assertTrue(!a.readable() && !p.inventoryMenu.getSlot(64).mayPlace(new ItemStack(Items.COW_SPAWN_EGG)),"Unknown schema cannot be modified");
|
||||
var out=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());a.write(out);
|
||||
h.assertTrue(out.buildResult().getCompound("sanctuary:accessories").orElseThrow().equals(future),"Unknown data round-trips unchanged");h.succeed();
|
||||
}
|
||||
@GameTest public void prestigePreservesAccessories(GameTestHelper h) throws Exception {
|
||||
var p=player(h,5);equip(AccessoryAccess.of(p));
|
||||
var s=ProgressionService.session(p.level().getServer());s.inhabitants.create(p.getUUID(),"pet"+p.getUUID().toString().substring(0,8),"",s.inhabitants.available().getFirst());
|
||||
var progress=ProgressEffects.get(p);for(String skill:ProgressRecord.SKILLS)while(progress.rank(skill)<ProgressRecord.cap(skill))progress=progress.buy(skill,1);
|
||||
bind(p,progress);var before=AccessoryAccess.of(p).getItems().stream().map(ItemStack::copy).toList();
|
||||
h.assertTrue(CycleService.perform(p,new CyclePayloads.Action("ngp","","",s.cycles.revision(),progress.revision())).equals("done"),"Actual prestige accepted");
|
||||
h.assertTrue(ProgressEffects.get(p).inventoryRows()==1 && ItemStack.listMatches(before,AccessoryAccess.of(p).getItems()),"Prestige keeps accessories while inventory capacity returns to one row");h.succeed();
|
||||
}
|
||||
@GameTest public void familiarIsHarmlessAndEscapesWalls(GameTestHelper h) {
|
||||
var p=player(h,0);platform(h,p.blockPosition());AccessoryAccess.of(p).setItem(0,new ItemStack(Items.CREEPER_SPAWN_EGG));
|
||||
FamiliarService.tick(p);var pet=FamiliarService.active(p);h.assertTrue(pet!=null,"Dedicated moving familiar spawns");
|
||||
h.assertTrue(!pet.isNoAi() && pet.getTarget()==null && !pet.shouldBeSaved(),"Navigation runs without destructive mob AI or chunk persistence");
|
||||
float health=pet.getHealth();h.assertTrue(!pet.hurtServer(h.getLevel(),p.damageSources().genericKill(),10000) && pet.getHealth()==health,"Even bypass damage cannot injure familiar");
|
||||
h.assertTrue(!pet.isPushable() && !pet.canBeCollidedWith(p) && !pet.canCollideWith(p),"No entity collision or pushing");
|
||||
var pos=pet.position();pet.push(p);h.assertTrue(pet.position().equals(pos),"Contact does not shove pet");
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.LEAD));
|
||||
h.assertTrue(pet.interact(p,InteractionHand.MAIN_HAND,Vec3.ZERO).consumesAction() && p.getMainHandItem().getCount()==1 && !pet.isLeashed(),"Right-click pets without consuming a lead or adding its native behavior");
|
||||
var wall=p.blockPosition().offset(2,0,0);h.getLevel().setBlockAndUpdate(wall,Blocks.STONE.defaultBlockState());pet.setPos(Vec3.atBottomCenterOf(wall));
|
||||
h.assertTrue(pet.teleportNearOwner() && h.getLevel().noBlockCollision(pet,pet.getBoundingBox()),"Safe fallback exits a wall into free supported space");
|
||||
var saved=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());h.assertTrue(!pet.save(saved),"Temporary pet is excluded from saves");
|
||||
FamiliarService.tick(p);h.assertTrue(FamiliarService.active(p)==pet,"Repeated service tick never duplicates pet");
|
||||
AccessoryAccess.of(p).setItem(0,ItemStack.EMPTY);FamiliarService.tick(p);h.assertTrue(pet.isRemoved() && FamiliarService.active(p)==null,"Removing egg removes familiar");h.succeed();
|
||||
}
|
||||
@GameTest public void familiarWalksAroundObstacle(GameTestHelper h) {
|
||||
var p=player(h,0);platform(h,p.blockPosition());var origin=p.blockPosition();
|
||||
AccessoryAccess.of(p).setItem(0,new ItemStack(Items.WOLF_SPAWN_EGG));FamiliarService.tick(p);var pet=FamiliarService.active(p);
|
||||
h.assertTrue(pet!=null,"Walking fixture has a pet");
|
||||
pet.setPos(Vec3.atBottomCenterOf(origin.offset(-3,0,0)));pet.setOnGround(true);p.setPos(Vec3.atBottomCenterOf(origin.offset(3,0,0)));
|
||||
for(int z=-1;z<=1;z++)for(int y=0;y<2;y++)h.getLevel().setBlockAndUpdate(origin.offset(0,y,z),Blocks.STONE.defaultBlockState());
|
||||
h.assertTrue(pet.getNavigation().moveTo(p,1.15) && pet.getNavigation().getPath()!=null,"Native navigator finds a route around a two-block wall");
|
||||
var route=pet.getNavigation().getPath();boolean around=false;
|
||||
for(int i=0;i<route.getNodeCount();i++)if(Math.abs(route.getNode(i).z-origin.getZ())>=2)around=true;
|
||||
h.assertTrue(around,"Route leaves the blocked straight line before reaching owner");pet.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void changingHotbarInPlayStopsUsingOldItem(GameTestHelper h) {
|
||||
var p=player(h,5);p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.BOW));p.startUsingItem(InteractionHand.MAIN_HAND);
|
||||
h.assertTrue(p.isUsingItem(),"Fixture is drawing a bow");
|
||||
var menu=p.inventoryMenu;
|
||||
h.assertTrue(HotbarService.rotate(p,new HotbarPayloads.Rotate(1,menu.containerId,menu.getStateId(),ProgressEffects.get(p).revision(),1)),"Same native inventory menu can rotate while playing");
|
||||
h.assertTrue(!p.isUsingItem() && HotbarRows.selected(p)==5 && p.getInventory().getItem(52).is(Items.BOW),"Row changes atomically and cancels using the old item without firing");h.succeed();
|
||||
}
|
||||
private static void platform(GameTestHelper h,BlockPos at) {
|
||||
for(int x=-6;x<=6;x++)for(int z=-6;z<=6;z++)for(int y=-1;y<=3;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),(y==-1 ? Blocks.STONE : Blocks.AIR).defaultBlockState());
|
||||
}
|
||||
private static void equip(AccessoryContainer a) {a.setItem(0,new ItemStack(Items.COW_SPAWN_EGG));a.setItem(1,new ItemStack(AccessoryService.ZERO_CAPE));a.setItem(2,new ItemStack(Items.NETHERITE_HELMET));}
|
||||
private static ServerPlayer player(GameTestHelper h,int rank) {
|
||||
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());var r=ProgressRecord.INITIAL;
|
||||
for(int i=0;i<rank;i++)r=r.buy("inventory",1);bind(p,r);p.getInventory().clearContent();
|
||||
((SanctuaryExtendedInventoryMenu)p.inventoryMenu).sanctuary$ensureExtendedInventory();return p;
|
||||
}
|
||||
private static void bind(ServerPlayer p,ProgressRecord record) {p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.expansion.*;
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** Real beta worlds: settings persistence, native landscapes and second-process player-edit witnesses. */
|
||||
public final class AncientExpeditions001GameTests {
|
||||
@GameTest(maxTicks = 12000)
|
||||
public void initialLayoutAndSettingsPersist(GameTestHelper helper) throws Exception {
|
||||
var host = (SanctuaryChunkGenerator) helper.getLevel().getChunkSource().getGenerator();
|
||||
if (!host.ancientExpeditions()) { helper.succeed(); return; }
|
||||
var level = helper.getLevel();
|
||||
var session = ExpansionRuntime.session(level);
|
||||
var expected = (host.expeditionRevision() == 2 ? AncientExpeditions002.plan(level.getSeed(), host.initialDiameter(), host.aerialPlan24())
|
||||
: AncientExpeditions001.plan(level.getSeed(), host.initialDiameter(), host.aerialPlan24()));
|
||||
helper.assertTrue(session.islands().equals(expected), "All five regions exist before exploration");
|
||||
Path directory = level.getServer().getWorldPath(LevelResource.ROOT).resolve("data/" + host.journalDirectory());
|
||||
byte[] before = Files.readAllBytes(directory.resolve(ExpansionJournal.FILE_NAME));
|
||||
var reloaded = new ExpansionJournal(directory, level.getSeed(), host.initialDiameter(), 24, "", true, host.expeditionRevision());
|
||||
helper.assertTrue(reloaded.islands().equals(expected), "Every region field survives journal reload");
|
||||
reloaded.commit(reloaded.islands());
|
||||
helper.assertTrue(Arrays.equals(before, Files.readAllBytes(directory.resolve(ExpansionJournal.FILE_NAME))), "Loading and no-op commits preserve bytes");
|
||||
UnifiedWorldSettingsDiagnostics.verify(helper);
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 16000)
|
||||
public void landscapesAndSavedEditsSurviveExploration(GameTestHelper helper) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var host = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
if (!host.ancientExpeditions()) { helper.succeed(); return; }
|
||||
var session = ExpansionRuntime.session(level);
|
||||
Path directory = level.getServer().getWorldPath(LevelResource.ROOT).resolve("data/" + host.journalDirectory());
|
||||
Path output = Path.of("diagnostics/expeditions00" + host.expeditionRevision(), host.initialDiameter() + "-" + level.getSeed() + "-native.json");
|
||||
Files.createDirectories(output.getParent());
|
||||
String journalHash = hash(Files.readAllBytes(directory.resolve(ExpansionJournal.FILE_NAME)));
|
||||
var ops = RegistryOps.create(JsonOps.INSTANCE, level.registryAccess());
|
||||
var codec = ChunkGenerator.CODEC.encodeStart(ops, host).getOrThrow();
|
||||
if (Boolean.getBoolean("sanctuary.test.expansionReload")) {
|
||||
var saved = JsonParser.parseString(Files.readString(output)).getAsJsonObject();
|
||||
helper.assertTrue(saved.get("passed").getAsBoolean() && saved.get("process_id").getAsLong() != ProcessHandle.current().pid(), "Reload uses a successful earlier process");
|
||||
helper.assertTrue(saved.get("journal_sha256").getAsString().equals(journalHash), "Process reload never moves or recreates an expedition");
|
||||
helper.assertTrue(saved.get("generator_codec").equals(codec), "Saved generator options remain exact");
|
||||
for (var entry : saved.getAsJsonArray("regions")) {
|
||||
var row = entry.getAsJsonObject();
|
||||
var point = row.getAsJsonArray("player_edit");
|
||||
var at = new BlockPos(point.get(0).getAsInt(), point.get(1).getAsInt(), point.get(2).getAsInt());
|
||||
level.getChunk(at.getX() >> 4, at.getZ() >> 4);
|
||||
helper.assertTrue(level.getBlockState(at).is(Blocks.GOLD_BLOCK), "Player edit survives second process: " + row.get("id"));
|
||||
}
|
||||
saved.addProperty("reload_process_id", ProcessHandle.current().pid());
|
||||
saved.addProperty("reload_passed", true);
|
||||
Files.writeString(output.resolveSibling(output.getFileName().toString().replace("-native", "-reload")), new GsonBuilder().setPrettyPrinting().create().toJson(saved));
|
||||
helper.succeed(); return;
|
||||
}
|
||||
var rows = new ArrayList<Map<String, Object>>();
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("seed", Long.toString(level.getSeed())); report.put("diameter", host.initialDiameter());
|
||||
report.put("version", net.fabricmc.loader.api.FabricLoader.getInstance().getModContainer("sanctuary")
|
||||
.orElseThrow().getMetadata().getVersion().getFriendlyString()); report.put("layout_version", host.expeditionRevision());
|
||||
report.put("process_id", ProcessHandle.current().pid()); report.put("journal_sha256", journalHash);
|
||||
report.put("generator_codec", codec); report.put("regions", rows);
|
||||
var iterator = session.islands().stream().filter(i -> !i.origin()).iterator();
|
||||
Runnable[] step = new Runnable[1];
|
||||
step[0] = () -> {
|
||||
try {
|
||||
if (!iterator.hasNext()) {
|
||||
helper.assertTrue(level.getServer().saveEverything(true, true, true), "Flush player edits before writing restart evidence");
|
||||
report.put("passed", true);
|
||||
Files.writeString(output, new GsonBuilder().setPrettyPrinting().create().toJson(report));
|
||||
helper.succeed(); return;
|
||||
}
|
||||
var island = iterator.next();
|
||||
var landing = session.landing(island.id());
|
||||
helper.assertTrue(ExpansionRuntime.safeLanding(level, landing), "Native visit finds real supported air");
|
||||
helper.assertTrue(host.islandForChunk(landing.getX() >> 4, landing.getZ() >> 4).id().equals(island.id()), "Correct island owns the landing chunk");
|
||||
var climatePoint = new BlockPos(island.centerX(), 320, island.centerZ());
|
||||
level.getChunk(climatePoint.getX() >> 4, climatePoint.getZ() >> 4);
|
||||
String biome = level.getBiome(climatePoint).unwrapKey().orElseThrow().identifier().getPath();
|
||||
var allowed = switch (island.climate()) {
|
||||
case "boreal" -> Set.of("taiga", "grove", "snowy_taiga", "snowy_plains", "ice_spikes", "frozen_peaks");
|
||||
case "glacial" -> Set.of("snowy_plains", "ice_spikes", "frozen_peaks");
|
||||
case "tropical" -> Set.of("mangrove_swamp", "jungle", "sparse_jungle", "bamboo_jungle");
|
||||
case "arid" -> Set.of("desert", "badlands", "eroded_badlands", "wooded_badlands");
|
||||
default -> host.expeditionRevision() == 2 ? Set.of("warm_ocean") : Set.of("ocean", "deep_ocean");
|
||||
};
|
||||
helper.assertTrue(allowed.contains(biome), "Actual chunk biome matches expedition climate: " + island.id() + " / " + biome);
|
||||
var row = new LinkedHashMap<String, Object>();
|
||||
row.put("id", island.id()); row.put("direction", island.direction()); row.put("climate", island.climate()); row.put("relief", island.relief());
|
||||
row.put("center", List.of(island.centerX(), island.centerZ())); row.put("diameter", island.diameter()); row.put("island_seed", Long.toString(island.seed()));
|
||||
row.put("landing", coordinates(landing)); row.put("upper_biome", biome);
|
||||
row.put("landing_biome", level.getBiome(landing).unwrapKey().orElseThrow().identifier().toString());
|
||||
if (ExpansionOcean23.active(island)) {
|
||||
var ocean = ExpansionOcean23.column(island, island.centerX(), island.centerZ());
|
||||
helper.assertTrue(ocean.water(), "Ocean has a real central basin");
|
||||
var water = new BlockPos(island.centerX(), ocean.waterLevel() - 4, island.centerZ());
|
||||
helper.assertTrue(level.getBlockState(water).is(Blocks.WATER), "Actual ocean chunk contains water");
|
||||
row.put("ocean_water", coordinates(water)); row.put("ocean_depth", ocean.depth());
|
||||
}
|
||||
var root = session.islands().getFirst();
|
||||
int dx = Integer.signum(island.centerX()), dz = Integer.signum(island.centerZ());
|
||||
double distance = ExpansionPlacement.distanceFromParent(island, root);
|
||||
int rootEdge = edge(host, root, dx, dz, level);
|
||||
int facingEdge = edge(host, island, -dx, -dz, level);
|
||||
row.put("center_distance", distance); row.put("axis_terrain_gap_sampled_every_4_blocks", distance - rootEdge - facingEdge);
|
||||
var old = ExpansionPlacement.candidates(level.getSeed(), List.of(root), island.id(), root.id(), island.direction(), island.climate(), 512, island.relief(), 0).getFirst();
|
||||
row.put("previous_placement_center_distance", ExpansionPlacement.distanceFromParent(old, root));
|
||||
var edit = landing.above(5);
|
||||
helper.assertTrue(level.getBlockState(edit).isAir(), "Player witness uses empty space");
|
||||
helper.assertTrue(level.setBlock(edit, Blocks.GOLD_BLOCK.defaultBlockState(), 3), "Write a development-only player edit");
|
||||
row.put("player_edit", coordinates(edit)); rows.add(row);
|
||||
boolean occupied = false;
|
||||
try { ExpansionAdmission.verify(level, island); }
|
||||
catch (ExpansionAdmission.OccupiedException expected) { occupied = true; }
|
||||
helper.assertTrue(occupied, "Close placement never bypasses loaded-chunk admission");
|
||||
// GameTest removes a callback after execution; rescheduling the same map key loses it.
|
||||
helper.runAfterDelay(10, () -> step[0].run());
|
||||
} catch (Exception failure) { throw new IllegalStateException(failure); }
|
||||
};
|
||||
step[0].run();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 24000)
|
||||
public void revisedPalettesTemplesAndLivingCoral(GameTestHelper helper) throws Exception {
|
||||
var level = helper.getLevel();
|
||||
var host = (SanctuaryChunkGenerator) level.getChunkSource().getGenerator();
|
||||
if (!host.ancientExpeditions() || host.expeditionRevision() != 2) { helper.succeed(); return; }
|
||||
var rows = new ArrayList<Map<String, Object>>();
|
||||
if (level.structureManager().shouldGenerateStructures()) {
|
||||
helper.assertTrue(host.expeditionTemples002().size() == 2, "Both requested temples are reserved before exploration");
|
||||
for (var temple : host.expeditionTemples002()) {
|
||||
var planned = temple.start(); var cp = planned.getChunkPos();
|
||||
var box = ExpeditionTemples002.footprint(planned);
|
||||
for (int cx = box.minX() >> 4; cx <= box.maxX() >> 4; cx++)
|
||||
for (int cz = box.minZ() >> 4; cz <= box.maxZ() >> 4; cz++) level.getChunk(cx, cz);
|
||||
var actual = level.structureManager().getStartForStructure(temple.structure().value(), level.getChunk(cp.x(), cp.z()));
|
||||
helper.assertTrue(actual != null && actual.isValid(), "Requested temple has a real native start");
|
||||
var serialization = net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext.fromLevel(level);
|
||||
var nbt = actual.createTag(serialization, cp);
|
||||
var restored = net.minecraft.world.level.levelgen.structure.StructureStart.loadStaticStart(serialization, nbt, level.getSeed());
|
||||
helper.assertTrue(restored != null && restored.isValid() && restored.createTag(serialization, cp).equals(nbt), "Temple native pieces and flags round-trip");
|
||||
String id = temple.structure().unwrapKey().orElseThrow().identifier().toString();
|
||||
int chests = 0, explosives = 0, dispensers = 0, masonry = 0;
|
||||
for (var at : BlockPos.betweenClosed(box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ())) {
|
||||
var block = level.getBlockState(at);
|
||||
if (block.is(Blocks.CHEST)) chests++;
|
||||
if (block.is(Blocks.TNT)) explosives++;
|
||||
if (block.is(Blocks.DISPENSER)) dispensers++;
|
||||
if (block.is(Blocks.SANDSTONE) || block.is(Blocks.COBBLESTONE) || block.is(Blocks.MOSSY_COBBLESTONE)) masonry++;
|
||||
}
|
||||
boolean desert = id.endsWith("desert_pyramid");
|
||||
helper.assertTrue(chests >= (desert ? 4 : 2) && masonry > 100, "Complete native temple masonry and loot chests: " + id);
|
||||
helper.assertTrue(desert ? explosives == 9 : dispensers == 2, "Native temple traps: " + id);
|
||||
var located = host.findNearestMapStructure(level, net.minecraft.core.HolderSet.direct(temple.structure()),
|
||||
actual.getBoundingBox().getCenter(), 1, false);
|
||||
helper.assertTrue(located != null && located.getSecond().equals(temple.structure()), "Native locate discovers the reserved temple");
|
||||
rows.add(Map.of("id", id, "chunk", List.of(cp.x(), cp.z()), "box", box.toString(), "chests", chests,
|
||||
"tnt", explosives, "dispensers", dispensers, "masonry", masonry, "native_round_trip", true, "locate", true));
|
||||
}
|
||||
} else helper.assertTrue(host.expeditionTemples002().isEmpty(), "Structures-off never adds a temple");
|
||||
var north = host.source().islands().stream().filter(i -> i.id().equals("noor_vigil")).findFirst().orElseThrow();
|
||||
var palette = new HashSet<String>();
|
||||
var sampler = host.randomState(north).createClimateSampler(net.minecraft.world.level.levelgen.densityfunction.SamplerContext.builder().enableCaches().build());
|
||||
var resolver = host.source().createResolver(sampler);
|
||||
for (int x = -144; x <= 144; x += 16) for (int z = -144; z <= 144; z += 16) for (int y : new int[]{210, 250, 280})
|
||||
palette.add(resolver.getNoiseBiome((north.centerX() + x) >> 2, y >> 2, (north.centerZ() + z) >> 2).unwrapKey().orElseThrow().identifier().getPath());
|
||||
helper.assertTrue(palette.contains("taiga") && palette.contains("snowy_taiga")
|
||||
&& palette.stream().anyMatch(Set.of("ice_spikes", "frozen_peaks", "snowy_plains")::contains), "Boreal preserves forest and adds genuine ice biomes");
|
||||
var ocean = host.source().islands().stream().filter(i -> i.id().equals("ari_waters")).findFirst().orElseThrow();
|
||||
int coral = 0, visited = 0;
|
||||
var point = new BlockPos.MutableBlockPos();
|
||||
outer: for (int dz = -4; dz <= 4; dz += 2) for (int dx = -4; dx <= 4; dx += 2) {
|
||||
int cx = (ocean.centerX() >> 4) + dx, cz = (ocean.centerZ() >> 4) + dz;
|
||||
level.getChunk(cx, cz); visited++;
|
||||
for (int x = cx * 16; x < cx * 16 + 16; x++) for (int z = cz * 16; z < cz * 16 + 16; z++) {
|
||||
var column = ExpansionOcean23.column(ocean, x, z);
|
||||
if (!column.water()) continue;
|
||||
helper.assertTrue(level.getBiome(point.set(x, column.floorY() + 4, z)).is(net.minecraft.world.level.biome.Biomes.WARM_OCEAN), "Actual reef basin uses warm_ocean");
|
||||
for (int y = column.floorY() + 1; y < column.waterLevel(); y++) {
|
||||
var state = level.getBlockState(point.set(x, y, z));
|
||||
String name = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||
if (name.contains("coral") && !name.startsWith("dead_")) coral++;
|
||||
}
|
||||
}
|
||||
if (coral > 0 && visited >= 3) break outer;
|
||||
}
|
||||
helper.assertTrue(coral > 0, "Native warm-ocean decoration produces living coral");
|
||||
Path output = Path.of("diagnostics/expeditions002", host.initialDiameter() + "-" + level.getSeed() + "-temples-reef"
|
||||
+ (Boolean.getBoolean("sanctuary.test.expansionReload") ? "-reload" : "") + ".json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, new GsonBuilder().setPrettyPrinting().create().toJson(Map.of("seed", Long.toString(level.getSeed()),
|
||||
"temples", rows, "boreal_biomes", palette, "living_coral_blocks", coral, "ocean_chunks_inspected", visited, "passed", true)));
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static int edge(SanctuaryChunkGenerator host, ExpansionIsland island, int dx, int dz, net.minecraft.server.level.ServerLevel level) {
|
||||
int edge = 0;
|
||||
for (int distance = 0; distance <= island.terrainRadius(); distance += 4) {
|
||||
int x = island.centerX() + dx * distance, z = island.centerZ() + dz * distance;
|
||||
if (host.getBaseHeight(x, z, Heightmap.Types.WORLD_SURFACE_WG, level, host.randomState(island)) > 1) edge = distance;
|
||||
}
|
||||
return edge;
|
||||
}
|
||||
private static List<Integer> coordinates(BlockPos point) { return List.of(point.getX(), point.getY(), point.getZ()); }
|
||||
private static String hash(byte[] bytes) throws Exception { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.atlas.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
public final class Atlas005GameTests {
|
||||
@GameTest
|
||||
public void mapPurchaseAndSeparation(GameTestHelper helper) throws Exception {
|
||||
ServerPlayer player=(ServerPlayer)helper.makeMockServerPlayer(GameType.SURVIVAL);
|
||||
player.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(player,ProgressRecord.INITIAL);
|
||||
helper.assertTrue(!AtlasService.canRead(player,false) && !AtlasService.canRead(player,true),"No map or operator access by default");
|
||||
helper.assertTrue(!ProgressionService.purchase(player,"atlas",0),"Insufficient levels cannot unlock map");
|
||||
var store=AtlasService.observations(player);byte[] known=new byte[256];java.util.Arrays.fill(known,(byte)1);store.observe(new AtlasStore.Tile(-12,7,known));store.save();
|
||||
helper.assertTrue(store.size()==1 && !ProgressEffects.get(player).atlas(),"Discovery persists independently before aptitude purchase");
|
||||
player.giveExperienceLevels(8);
|
||||
helper.assertTrue(ProgressionService.purchase(player,"atlas",0) && player.experienceLevel==4,"Server charges configurable map cost");
|
||||
helper.assertTrue(AtlasService.canRead(player,false) && !AtlasService.canRead(player,true),"Map purchase never grants operator inspection");
|
||||
helper.assertTrue(!ProgressionService.purchase(player,"atlas",0) && !ProgressionService.purchase(player,"atlas",1)
|
||||
&& player.experienceLevel==4,"Duplicate or replayed map purchase cannot charge twice");
|
||||
helper.assertTrue(!ProgressEffects.get(player).names() && ProgressEffects.get(player).maxFood()==6,"Aptitude does not unlock other abilities or skills");
|
||||
ServerPlayer other=(ServerPlayer)helper.makeMockServerPlayer(GameType.SURVIVAL);
|
||||
helper.assertTrue(AtlasService.observations(other).size()==0,"An inhabitant cannot inherit another's discovery");
|
||||
helper.succeed();
|
||||
}
|
||||
@GameTest
|
||||
public void samplerReadsSurfaceWithoutLoading(GameTestHelper helper) {
|
||||
var level=helper.getLevel();var origin=helper.absolutePos(BlockPos.ZERO);
|
||||
int x=origin.getX()>>4,z=origin.getZ()>>4;
|
||||
var chunk=level.getChunkSource().getChunkNow(x,z);
|
||||
helper.assertTrue(chunk!=null,"Fixture chunk is loaded");
|
||||
int dx=5,dz=5,wx=x*16+dx,wz=z*16+dz;
|
||||
int y=Math.min(level.getMaxY()-2,chunk.getHeight(Heightmap.Types.WORLD_SURFACE,dx,dz)+10);
|
||||
var pos=new BlockPos(wx,y,wz);var previous=level.getBlockState(pos);
|
||||
level.setBlock(pos,Blocks.GOLD_BLOCK.defaultBlockState(),3);
|
||||
try {
|
||||
var tile=AtlasSampler.sample(level,x,z);
|
||||
helper.assertTrue(Byte.toUnsignedInt(tile.colors()[dz*16+dx])/4==MapColor.GOLD.id,"Top-down survey uses the actual visible surface material");
|
||||
int far=1000000;
|
||||
helper.assertTrue(level.getChunkSource().getChunkNow(far,far)==null,"Distant chunk is absent before consultation");
|
||||
helper.assertTrue(AtlasSampler.sample(level,far,far)==null && level.getChunkSource().getChunkNow(far,far)==null,
|
||||
"Atlas sampling does not load or generate unexplored terrain");
|
||||
} finally {level.setBlock(pos,previous,3);}
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/** Test-only accounting of actual native overlay draws and texture uploads. */
|
||||
public final class Atlas026RenderProbe {
|
||||
public static boolean active,inside;
|
||||
public static int fills,uploads,blits;
|
||||
public static long started;
|
||||
public static final ArrayList<Long> nanos=new ArrayList<>();
|
||||
public static void reset() {active=false;inside=false;fills=0;uploads=0;blits=0;nanos.clear();}
|
||||
public static void start() {if(active){inside=true;started=System.nanoTime();}}
|
||||
public static void end() {if(inside){nanos.add(System.nanoTime()-started);inside=false;}}
|
||||
public static String report(String phase) {
|
||||
var ordered=nanos.stream().sorted().toList();
|
||||
return "ATLAS026 "+phase+" frames="+ordered.size()+" fills="+fills+" uploads="+uploads
|
||||
+" blits="+blits+" median_us="+(ordered.isEmpty()?0:ordered.get(ordered.size()/2)/1000.)
|
||||
+" p95_us="+(ordered.isEmpty()?0:ordered.get((ordered.size()-1)*95/100)/1000.);
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import fr.koka.sanctuary.boats.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.network.protocol.game.*;
|
||||
import net.minecraft.sounds.*;
|
||||
public final class BoatsHead049GameTests {
|
||||
@GameTest public void trapdoorBroadcastIncludesActorWearerAndObserversExactlyOnce(GameTestHelper h){
|
||||
var wearer=player(h);var actor=player(h);actor.setPos(wearer.position().add(1,0,0));
|
||||
equip(wearer,Items.OAK_TRAPDOOR);var s=HeadBlockService.session(wearer);
|
||||
Head049SoundProbe.capture=new ArrayList<>();
|
||||
try {
|
||||
for(boolean open:new boolean[]{true,false}){
|
||||
Head049SoundProbe.capture.clear();h.assertTrue(HeadBlockService.use(actor,wearer,false),"Remote trapdoor use accepted");
|
||||
var sounds=Head049SoundProbe.capture.stream().filter(b->b.packet() instanceof ClientboundSoundPacket).toList();
|
||||
h.assertTrue(sounds.size()==1&&sounds.getFirst().excluded()==null,"One native broadcast includes every nearby player, including actor");
|
||||
var packet=(ClientboundSoundPacket)sounds.getFirst().packet();
|
||||
h.assertTrue(packet.getSound().value()==(open?SoundEvents.WOODEN_TRAPDOOR_OPEN:SoundEvents.WOODEN_TRAPDOOR_CLOSE),"Correct native open/close sound");
|
||||
AccessoryService.publish(wearer);h.assertTrue(HeadCosmeticData.state(HeadCosmeticService.appearance(wearer)).getValue(TrapDoorBlock.OPEN)==open,"Same open state sent to every client");
|
||||
}
|
||||
Head049SoundProbe.capture.clear();
|
||||
h.getLevel().playSound(actor,s.pos(),SoundEvents.LEVER_CLICK,SoundSource.BLOCKS,1,1);
|
||||
h.assertTrue(Head049SoundProbe.capture.size()==1&&Head049SoundProbe.capture.getFirst().excluded()==actor,"Normal world sound retains vanilla prediction exclusion");
|
||||
Head049SoundProbe.capture.clear();HeadBlockService.scoped(s,()->{s.level().levelEvent(actor,1000,s.pos(),0);return null;});
|
||||
h.assertTrue(Head049SoundProbe.capture.size()==1&&Head049SoundProbe.capture.getFirst().excluded()==null,"Native level-event sounds include the actor too");
|
||||
}finally{Head049SoundProbe.capture=null;cleanup(wearer,actor);}h.succeed();
|
||||
}
|
||||
@GameTest public void visibleFoodAndLidEventsSyncWithoutExposingChestContents(GameTestHelper h){
|
||||
var p=player(h);var a=player(h);equip(p,Items.CAMPFIRE);var s=HeadBlockService.session(p);
|
||||
a.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.BEEF,2));
|
||||
h.assertTrue(HeadBlockService.use(a,p,false)&&a.getMainHandItem().getCount()==1,"Real campfire accepts one ingredient");
|
||||
AccessoryService.publish(p);var shown=HeadCosmeticService.appearance(p);
|
||||
var proxy=new CampfireBlockEntity(s.pos(),s.state);proxy.setLevel(h.getLevel());
|
||||
var data=shown.get(DataComponents.CUSTOM_DATA).copyTag().getCompoundOrEmpty(HeadBlockService.VISUAL).getCompoundOrEmpty("data");
|
||||
proxy.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,h.getLevel().registryAccess(),data));
|
||||
h.assertTrue(proxy.getItems().getFirst().is(Items.BEEF),"Remote render proxy receives actual visible food");
|
||||
h.assertTrue(!AccessoryAccess.of(p).getItem(AccessoryContainer.HEAD).has(DataComponents.CONTAINER),"Render data never creates a portable storage copy");
|
||||
equip(p,Items.CHEST);s=HeadBlockService.session(p);((Container)s.entity).setItem(0,new ItemStack(Items.DIAMOND,17));
|
||||
a.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);HeadBlockService.use(a,p,false);AccessoryService.publish(p);
|
||||
shown=HeadCosmeticService.appearance(p);data=shown.get(DataComponents.CUSTOM_DATA).copyTag().getCompoundOrEmpty(HeadBlockService.VISUAL);
|
||||
h.assertTrue(!shown.has(DataComponents.CONTAINER)&&!data.contains("data")&&data.getIntOr("value",0)==1,"Open lid travels; seventeen hidden diamonds do not");
|
||||
a.closeContainer();AccessoryService.publish(p);
|
||||
h.assertTrue(HeadCosmeticService.appearance(p).get(DataComponents.CUSTOM_DATA).copyTag().getCompoundOrEmpty(HeadBlockService.VISUAL).getIntOr("value",-1)==0,"Closing lid also synchronized");
|
||||
cleanup(p,a);h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=50) public void damagePulseActivatesNativeDispenserLampAndLocalPiston(GameTestHelper h){
|
||||
var p=player(h);equip(p,Items.DISPENSER);var dispenser=HeadBlockService.session(p);((Container)dispenser.entity).setItem(0,new ItemStack(Items.COBBLESTONE,2));
|
||||
var at=dispenser.pos();var terrain=h.getLevel().getBlockState(at);
|
||||
HeadBlockService.pulse(p);h.assertTrue(dispenser.state.getValue(DispenserBlock.TRIGGERED),"Native rising edge arms dispenser");
|
||||
h.runAfterDelay(5,()->{
|
||||
dispenser.tick();h.assertTrue(((Container)dispenser.entity).getItem(0).getCount()==1,"Scheduled dispenser really emits one item");
|
||||
equip(p,Items.REDSTONE_LAMP);HeadBlockService.pulse(p);var lamp=HeadBlockService.session(p);
|
||||
h.assertTrue(lamp.state.getValue(RedstoneLampBlock.LIT),"Lamp is lit by the hit pulse");
|
||||
h.runAfterDelay(20,()->{
|
||||
lamp.tick();h.runAfterDelay(5,()->{
|
||||
lamp.tick();h.assertTrue(!lamp.state.getValue(RedstoneLampBlock.LIT),"Lamp resets after native delayed shutoff");
|
||||
equip(p,Items.PISTON);HeadBlockService.pulse(p);
|
||||
h.assertTrue(HeadBlockService.session(p).state.getValue(net.minecraft.world.level.block.piston.PistonBaseBlock.EXTENDED),"Local piston extends");
|
||||
h.assertTrue(h.getLevel().getBlockState(at)==terrain,"No virtual head block overwrites world terrain");
|
||||
cleanup(p);h.succeed();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@GameTest public void rectangularFootprintsRotateAndEverySeatIsEquipable(GameTestHelper h){
|
||||
for(var v:CrewBoats.variants()){
|
||||
var b=v.type().create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
h.assertTrue(AccessoryContainer.accepts(AccessoryContainer.HEAD,new ItemStack(v.item())),"Every new boat fits cosmetic slot");
|
||||
h.assertTrue(b.seats()==2*v.tiles(),"Two people per module");
|
||||
var primary=new HashSet<String>();
|
||||
for(int i=0;i<v.tiles();i++){
|
||||
var first=b.seatOffset(i);var second=b.seatOffset(i+v.tiles());
|
||||
primary.add(first.x+":"+first.z);
|
||||
h.assertTrue(first.x==second.x&&Math.abs(first.z-second.z-.8)<.0001,"Secondary sits beside the primary of its module");
|
||||
}
|
||||
h.assertTrue(primary.size()==v.tiles(),"Primary places occupy every module before secondaries");
|
||||
if(v.columns()!=v.rows()){
|
||||
b.setYRot(0);double x=b.getBoundingBox().getXsize(),z=b.getBoundingBox().getZsize();
|
||||
h.assertTrue((x>z)==(v.columns()>v.rows()),"Dimensions distinguish transposed recipes");
|
||||
b.setYRot(90);h.assertTrue(Math.abs(x-b.getBoundingBox().getZsize())<.0001&&Math.abs(z-b.getBoundingBox().getXsize())<.0001,"Collision bounds follow orientation");
|
||||
var e=FlightBalance.Engines.NONE.add(FlightBalance.Kind.ALLAY,false);
|
||||
h.assertTrue(FlightBalance.profile(e,2,v.columns(),v.rows()).equals(FlightBalance.profile(e,2,v.rows(),v.columns())),"Transpose has identical engine balance");
|
||||
}b.discard();
|
||||
}h.succeed();
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h){var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);return p;}
|
||||
private static void equip(ServerPlayer p,Item item){AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(item));}
|
||||
private static void cleanup(ServerPlayer... ps){for(var p:ps){p.closeContainer();AccessoryAccess.of(p).clearContent();}}
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.building.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.knowledge.BlockKnowledgeService;
|
||||
import fr.koka.demeure.DemeureService;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.commands.arguments.EntityAnchorArgument;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class Building009GameTests {
|
||||
private static final Set<UUID> DENIED=new HashSet<>(),MUTATED=new HashSet<>();
|
||||
static {
|
||||
UseBlockCallback.EVENT.register((player,level,hand,hit) -> {
|
||||
if (DENIED.contains(player.getUUID())) return InteractionResult.FAIL;
|
||||
if (MUTATED.contains(player.getUUID())) level.setBlockAndUpdate(hit.getBlockPos().relative(hit.getDirection()),Blocks.CHEST.defaultBlockState());
|
||||
return InteractionResult.PASS;
|
||||
});
|
||||
}
|
||||
@GameTest public void purchasesLimitsAndPlaneOrientations(GameTestHelper h) {
|
||||
var p=player(h,0,Items.STONE,64);p.giveExperienceLevels(111);
|
||||
double reach=p.blockInteractionRange(),attack=p.entityInteractionRange();
|
||||
for (int rank=1;rank<=7;rank++) {
|
||||
h.assertTrue(ProgressionService.purchase(p,"building",rank-1),"Seven Building ranks can be bought");
|
||||
h.assertTrue(ProgressEffects.get(p).building()==rank && BuildingLimits.diameter(rank)==1+2*rank,"Each rank grows the diameter");
|
||||
}
|
||||
h.assertTrue(p.experienceLevel==0 && !ProgressionService.purchase(p,"building",7),"111 levels pay seven ranks, no eighth purchase");
|
||||
h.assertTrue(p.blockInteractionRange()==reach && p.entityInteractionRange()==attack,"Neither block nor attack reach is changed");
|
||||
for (Direction face:Direction.values()) for (Direction look:List.of(Direction.NORTH,Direction.EAST,Direction.SOUTH,Direction.WEST)) {
|
||||
var plane=BuildPlane.positions(BlockPos.ZERO,face,look,7);
|
||||
h.assertTrue(plane.size()==225 && new HashSet<>(plane).size()==225 && plane.getFirst().equals(BlockPos.ZERO),"Every orientation makes 225 unique positions, centre first");
|
||||
h.assertTrue(plane.stream().allMatch(pos -> pos.get(face.getAxis())==0),"Every position stays in the clicked plane");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void nativeFloorsWallsAndCeilings(GameTestHelper h) {
|
||||
for (Direction face:Direction.values()) {
|
||||
var origin=fixture(h,face,1);var p=player(h,1,Items.COBBLESTONE,16);aim(p,origin,face);
|
||||
var hit=BuildSelection.aimed(p,origin);h.assertTrue(hit!=null && hit.getDirection()==face,"Server ray sees the expected face: "+face);
|
||||
start(h,p,origin);BuildingManager.tick(p);
|
||||
h.assertTrue(p.getMainHandItem().getCount()==16,"Preview and first tick consume no blocks");
|
||||
finish(h,p);
|
||||
h.assertTrue(p.getMainHandItem().getCount()==7,"A 3x3 surface consumes exactly nine blocks: "+face);
|
||||
h.assertTrue(p.getStats().getValue(Stats.ITEM_USED.get(Items.COBBLESTONE))==9
|
||||
&& BlockKnowledgeService.knowledge(p,Blocks.COBBLESTONE).placed()==9,"Native stats and Blocodex count nine actual placements");
|
||||
for (var pos:BuildPlane.positions(origin.relative(face),face,p.getDirection(),1)) h.assertTrue(h.getLevel().getBlockState(pos).is(Blocks.COBBLESTONE),"Every previewed position is filled");
|
||||
BuildingManager.handle(p,new BuildingPayload(1,true,origin,1));
|
||||
h.assertTrue(!BuildingManager.active(p),"A completed heartbeat cannot extend a new layer");
|
||||
BuildingManager.forget(p);
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void survivalStockAndCreative(GameTestHelper h) {
|
||||
var origin=fixture(h,Direction.UP,1);var p=player(h,1,Items.COBBLESTONE,4);aim(p,origin,Direction.UP);
|
||||
double before=footprint(p,origin);start(h,p,origin);finish(h,p);
|
||||
h.assertTrue(p.getMainHandItem().isEmpty() && p.getStats().getValue(Stats.ITEM_USED.get(Items.COBBLESTONE))==4,"Stock exhaustion stops after exactly four paid blocks");
|
||||
h.assertTrue(Math.abs(footprint(p,origin)-before-.2)<1e-8,"Four completed placements contribute exactly four Demeure traces");
|
||||
BuildingManager.forget(p);
|
||||
fixture(h,Direction.UP,1);var creative=player(h,1,Items.COBBLESTONE,1,GameType.CREATIVE);aim(creative,origin,Direction.UP);
|
||||
start(h,creative,origin);finish(h,creative);
|
||||
h.assertTrue(creative.getMainHandItem().getCount()==1 && creative.getStats().getValue(Stats.ITEM_USED.get(Items.COBBLESTONE))==9,"Creative retains native infinite materials");
|
||||
BuildingManager.forget(creative);h.succeed();
|
||||
}
|
||||
@GameTest public void slabsStairsAndOccupiedPositions(GameTestHelper h) {
|
||||
var origin=fixture(h,Direction.UP,1);var p=player(h,1,Items.STONE_SLAB,32);aim(p,origin,Direction.UP);
|
||||
h.getLevel().setBlockAndUpdate(origin.above().east(),Blocks.CHEST.defaultBlockState());
|
||||
var chest=h.getLevel().getBlockEntity(origin.above().east());
|
||||
start(h,p,origin);finish(h,p);
|
||||
h.assertTrue(p.getMainHandItem().getCount()==24 && h.getLevel().getBlockEntity(origin.above().east())==chest,"Occupied chest stays intact and costs no slab");
|
||||
h.assertTrue(h.getLevel().getBlockState(origin.above()).getValue(SlabBlock.TYPE)==SlabType.BOTTOM,"Floor slabs use native bottom orientation");
|
||||
BuildingManager.forget(p);
|
||||
for (Direction look:List.of(Direction.NORTH,Direction.EAST,Direction.SOUTH,Direction.WEST)) {
|
||||
fixture(h,Direction.UP,1);p=player(h,1,Items.OAK_STAIRS,16);
|
||||
p.setPos(origin.getX()+.5-look.getStepX()*3,origin.getY()+1,origin.getZ()+.5-look.getStepZ()*3);
|
||||
p.lookAt(EntityAnchorArgument.Anchor.EYES,Vec3.atCenterOf(origin).add(0,.5,0));
|
||||
start(h,p,origin);finish(h,p);
|
||||
var stairs=h.getLevel().getBlockState(origin.above());
|
||||
h.assertTrue(stairs.is(Blocks.OAK_STAIRS) && stairs.getValue(StairBlock.FACING)==look && stairs.getValue(StairBlock.HALF)==Half.BOTTOM,"Stairs follow all four player directions");
|
||||
BuildingManager.forget(p);
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void refusalsAndCallbackWorldChanges(GameTestHelper h) {
|
||||
var origin=fixture(h,Direction.UP,1);
|
||||
for (var set:List.of(DENIED,MUTATED)) {
|
||||
fixture(h,Direction.UP,1);var p=player(h,1,Items.COBBLESTONE,16);aim(p,origin,Direction.UP);
|
||||
set.add(p.getUUID());double before=footprint(p,origin);
|
||||
try {
|
||||
start(h,p,origin);finish(h,p);
|
||||
h.assertTrue(p.getMainHandItem().getCount()==16 && p.getStats().getValue(Stats.ITEM_USED.get(Items.COBBLESTONE))==0
|
||||
&& footprint(p,origin)==before,"Refusal or callback mutation consumes and credits nothing");
|
||||
} finally {set.remove(p.getUUID());BuildingManager.forget(p);}
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void slabMergingCeilingAndEntityCollisions(GameTestHelper h) {
|
||||
var origin=fixture(h,Direction.UP,1);
|
||||
for(var pos:BuildPlane.positions(origin,Direction.UP,Direction.SOUTH,1))
|
||||
h.getLevel().setBlockAndUpdate(pos,Blocks.STONE_SLAB.defaultBlockState());
|
||||
var p=player(h,1,Items.STONE_SLAB,16);aim(p,origin,Direction.UP);
|
||||
p.lookAt(EntityAnchorArgument.Anchor.EYES,Vec3.atLowerCornerOf(origin).add(.5,.5,.5));
|
||||
start(h,p,origin);finish(h,p);
|
||||
h.assertTrue(p.getMainHandItem().getCount()==7 && h.getLevel().getBlockState(origin).getValue(SlabBlock.TYPE)==SlabType.DOUBLE,"Existing slabs merge through native contexts and consume one item each");
|
||||
BuildingManager.forget(p);
|
||||
fixture(h,Direction.DOWN,1);p=player(h,1,Items.STONE_SLAB,16);aim(p,origin,Direction.DOWN);
|
||||
start(h,p,origin);finish(h,p);
|
||||
h.assertTrue(h.getLevel().getBlockState(origin.below()).getValue(SlabBlock.TYPE)==SlabType.TOP,"Ceiling slabs retain top orientation");
|
||||
BuildingManager.forget(p);
|
||||
fixture(h,Direction.UP,1);p=player(h,1,Items.COBBLESTONE,16);aim(p,origin,Direction.UP);
|
||||
var cow=net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getValue(net.minecraft.resources.Identifier.withDefaultNamespace("cow")).create(h.getLevel(),net.minecraft.world.entity.EntitySpawnReason.TRIGGERED);
|
||||
cow.setPos(origin.getX()+1.5,origin.getY()+1,origin.getZ()+.5);cow.setNoGravity(true);
|
||||
h.getLevel().addFreshEntity(cow);
|
||||
var plan=BuildSelection.plan(p,BuildSelection.aimed(p,origin),1);
|
||||
h.assertTrue(plan!=null && plan.targets().size()<9 && plan.targets().stream().noneMatch(t -> new AABB(t.pos()).intersects(cow.getBoundingBox())),"Native collision validation excludes cells occupied by a living entity");
|
||||
start(h,p,origin);finish(h,p);
|
||||
h.assertTrue(h.getLevel().getBlockState(origin.above().east()).isAir(),"Bulk construction does not enclose the cow in a new block");
|
||||
cow.discard();BuildingManager.forget(p);h.succeed();
|
||||
}
|
||||
@GameTest public void interruptionsPreserveAlreadyPlacedBlocks(GameTestHelper h) {
|
||||
for (int reason=0;reason<4;reason++) {
|
||||
var origin=fixture(h,Direction.UP,1);var p=player(h,1,Items.COBBLESTONE,16);aim(p,origin,Direction.UP);
|
||||
start(h,p,origin);BuildingManager.tick(p);BuildingManager.tick(p);
|
||||
h.assertTrue(p.getMainHandItem().getCount()==15,"First placement is paid before cancellation");
|
||||
switch(reason) {
|
||||
case 0 -> BuildingManager.handle(p,new BuildingPayload(1,false,origin,1));
|
||||
case 1 -> p.setItemInHand(InteractionHand.MAIN_HAND,p.getMainHandItem().copy());
|
||||
case 2 -> p.setPos(p.getX()+30,p.getY(),p.getZ());
|
||||
case 3 -> p.setHealth(0);
|
||||
}
|
||||
BuildingManager.tick(p);h.assertTrue(!BuildingManager.active(p) && h.getLevel().getBlockState(origin.above()).is(Blocks.COBBLESTONE),"Interruption stops future work, retains completed placement: "+reason);
|
||||
h.assertTrue(p.getMainHandItem().getCount()==15,"No additional material consumed after interruption");
|
||||
BuildingManager.forget(p);
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void concurrentBuildersAndUntrustedRanks(GameTestHelper h) {
|
||||
var origin=fixture(h,Direction.UP,1);var a=player(h,1,Items.COBBLESTONE,16);var b=player(h,1,Items.COBBLESTONE,16);
|
||||
aim(a,origin,Direction.UP);aim(b,origin,Direction.UP);
|
||||
BuildingManager.handle(a,new BuildingPayload(1,true,origin,7));
|
||||
h.assertTrue(BuildingManager.selectedCount(a)==9,"Forged high rank is clamped to the purchased surface");
|
||||
start(h,b,origin);
|
||||
for (int i=0;i<40;i++) {BuildingManager.tick(a);BuildingManager.tick(b);}
|
||||
h.assertTrue(a.getMainHandItem().getCount()+b.getMainHandItem().getCount()==23,"Two builders consume only nine items for one shared surface");
|
||||
h.assertTrue(a.getStats().getValue(Stats.ITEM_USED.get(Items.COBBLESTONE))+b.getStats().getValue(Stats.ITEM_USED.get(Items.COBBLESTONE))==9,"No duplicated placement credit");
|
||||
BuildingManager.forget(a);BuildingManager.forget(b);
|
||||
var novice=player(h,0,Items.COBBLESTONE,16);aim(novice,origin,Direction.UP);
|
||||
BuildingManager.handle(novice,new BuildingPayload(1,true,origin,7));h.assertTrue(!BuildingManager.active(novice),"Rank zero cannot start group work");
|
||||
BuildingManager.handle(a,new BuildingPayload(2,true,origin,8));h.assertTrue(!BuildingManager.active(a),"Invalid rank rejected");
|
||||
var far=new BlockPos(29000000,100,29000000);
|
||||
h.assertTrue(!h.getLevel().isLoaded(far) && BuildSelection.plan(a,new BlockHitResult(Vec3.atCenterOf(far),Direction.UP,far,false),7)==null
|
||||
&& !h.getLevel().isLoaded(far),"Preview never loads a missing chunk");
|
||||
BuildingManager.forget(novice);BuildingManager.forget(a);h.succeed();
|
||||
}
|
||||
@GameTest public void containerSupportsAndUnsupportedItems(GameTestHelper h) {
|
||||
var origin=fixture(h,Direction.UP,1);h.getLevel().setBlockAndUpdate(origin,Blocks.CHEST.defaultBlockState());
|
||||
var chest=(net.minecraft.world.level.block.entity.ChestBlockEntity)h.getLevel().getBlockEntity(origin);
|
||||
chest.setItem(0,new ItemStack(Items.DIAMOND,3));
|
||||
var p=player(h,1,Items.COBBLESTONE,16);aim(p,origin,Direction.UP);
|
||||
start(h,p,origin);finish(h,p);
|
||||
h.assertTrue(chest.getItem(0).getCount()==3 && p.containerMenu==p.inventoryMenu,"Building on a container never opens it or changes its contents");
|
||||
BuildingManager.forget(p);
|
||||
for(var item:List.of(Items.OAK_DOOR,Items.BED.red(),Items.SCAFFOLDING,Items.LILY_PAD)) {
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(item,4));
|
||||
h.assertTrue(!BuildSelection.usable(p),"Multi-cell and relocated placements keep individual native use: "+item);
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=50) public void missingHeartbeatExpires(GameTestHelper h) {
|
||||
var origin=fixture(h,Direction.UP,2);var p=player(h,2,Items.COBBLESTONE,64);aim(p,origin,Direction.UP);start(h,p,origin);
|
||||
h.runAfterDelay(16,() -> {
|
||||
h.assertTrue(!BuildingManager.active(p),"A lost heartbeat expires a running surface");
|
||||
h.assertTrue(p.getMainHandItem().getCount()>39,"Lost connection cannot finish the full surface");
|
||||
BuildingManager.forget(p);h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest public void lockedSurfaceKeepsItsFacingWhileLookingAround(GameTestHelper h) {
|
||||
var origin=fixture(h,Direction.UP,1);var p=player(h,1,Items.OAK_STAIRS,16);aim(p,origin,Direction.UP);
|
||||
var look=p.getDirection();var hit=BuildSelection.aimed(p,origin);var plan=BuildSelection.plan(p,hit,1);
|
||||
h.assertTrue(plan.targets().size()==9,"Nine valid stairs are captured before starting");start(h,p,origin);
|
||||
BuildingManager.tick(p);BuildingManager.tick(p);
|
||||
p.setYRot(p.getYRot()+180);p.setXRot(-65);p.setPos(p.getX()+.1,p.getY(),p.getZ());finish(h,p);
|
||||
h.assertTrue(p.getMainHandItem().getCount()==7,"Turning and moving within reach still place exactly the original surface");
|
||||
for(var target:plan.targets()) {
|
||||
var state=h.getLevel().getBlockState(target.pos());
|
||||
h.assertTrue(state.is(Blocks.OAK_STAIRS) && state.getValue(StairBlock.FACING)==look,"All stairs retain the initial orientation");
|
||||
}
|
||||
var ordinary=new net.minecraft.world.item.context.UseOnContext(p,InteractionHand.MAIN_HAND,hit);
|
||||
h.assertTrue(ordinary.getHorizontalDirection()==p.getDirection() && ordinary.getHorizontalDirection()!=look,"Frozen orientation never leaks into ordinary item use");
|
||||
BuildingManager.handle(p,new BuildingPayload(1,true,origin,1));h.assertTrue(!BuildingManager.active(p),"A completed surface cannot restart from held heartbeats");
|
||||
BuildingManager.forget(p);h.succeed();
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h,int rank,Item item,int count) {return player(h,rank,item,count,GameType.SURVIVAL);}
|
||||
private static ServerPlayer player(GameTestHelper h,int rank,Item item,int count,GameType mode) {
|
||||
var p=Progression003GameTests.player(h,mode);var record=ProgressRecord.INITIAL;
|
||||
for(int i=0;i<rank;i++) record=record.buy("building",ProgressionConfig.DEFAULT.statCosts().get(i));
|
||||
p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(item,count));p.setOnGround(true);return p;
|
||||
}
|
||||
private static BlockPos fixture(GameTestHelper h,Direction face,int radius) {
|
||||
var origin=h.absolutePos(new BlockPos(3,5,3));
|
||||
for(int x=-3;x<=3;x++) for(int y=-3;y<=3;y++) for(int z=-3;z<=3;z++) h.getLevel().setBlockAndUpdate(origin.offset(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
for(var pos:BuildPlane.positions(origin,face,Direction.SOUTH,radius)) h.getLevel().setBlockAndUpdate(pos,Blocks.STONE.defaultBlockState());
|
||||
return origin;
|
||||
}
|
||||
private static void aim(ServerPlayer p,BlockPos origin,Direction face) {
|
||||
if(face==Direction.UP) p.setPos(origin.getX()+.5,origin.getY()+1,origin.getZ()-2.5);
|
||||
else if(face==Direction.DOWN) p.setPos(origin.getX()+.5,origin.getY()-3,origin.getZ()+.5);
|
||||
else p.setPos(origin.getX()+.5+face.getStepX()*3,origin.getY()-1,origin.getZ()+.5+face.getStepZ()*3);
|
||||
p.lookAt(EntityAnchorArgument.Anchor.EYES,Vec3.atCenterOf(origin).add(face.getStepX()*.5,face.getStepY()*.5,face.getStepZ()*.5));
|
||||
p.setOnGround(true);
|
||||
}
|
||||
private static void start(GameTestHelper h,ServerPlayer p,BlockPos origin) {
|
||||
BuildingManager.handle(p,new BuildingPayload(1,true,origin,ProgressEffects.get(p).building()));
|
||||
h.assertTrue(BuildingManager.active(p),"Valid native target starts a build session");
|
||||
}
|
||||
private static void finish(GameTestHelper h,ServerPlayer p) {
|
||||
for(int i=0;i<600 && BuildingManager.active(p);i++) BuildingManager.tick(p);
|
||||
h.assertTrue(!BuildingManager.active(p),"Surface finishes within a bounded tick budget");
|
||||
}
|
||||
private static double footprint(ServerPlayer p,BlockPos origin) {
|
||||
var keys=new HashSet<fr.koka.demeure.FootprintStore.Key>();
|
||||
for(int x=-2;x<=2;x++) for(int z=-2;z<=2;z++) keys.add(DemeureService.key(p,origin.offset(x,0,z)));
|
||||
return keys.stream().map(k -> DemeureService.view(p.level().getServer(),k)).filter(Objects::nonNull).flatMap(v -> v.influences().stream())
|
||||
.filter(s -> s.subject().id().equals(p.getUUID().toString())).mapToDouble(fr.koka.demeure.FootprintStore.Share::score).sum();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.cosmetics.CarryService;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.TagValueOutput;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native player mounts, attributes, lifecycle callbacks and collision checks, without a graphical client. */
|
||||
public final class Carry027GameTests {
|
||||
@GameTest public void seatedPelvisTouchesHeadAtEveryLayerAndScale(GameTestHelper h){
|
||||
var p=players(h,3);
|
||||
try{
|
||||
tower(h,p);
|
||||
for(int i=1;i<p.length;i++)h.assertTrue(Math.abs(p[i].getY()-p[i-1].getY()-1.171875)<.00001,
|
||||
"Seated players are separated by 20 model pixels at 15/16 scale, without a standing leg gap");
|
||||
p[0].getAttribute(Attributes.SCALE).setBaseValue(1.5);
|
||||
p[1].getAttribute(Attributes.SCALE).setBaseValue(.75);
|
||||
p[0].setPose(Pose.CROUCHING);
|
||||
p[0].positionRider(p[1]);p[1].positionRider(p[2]);
|
||||
double headTop=p[0].getY()+1.50484375*1.5;
|
||||
h.assertTrue(Math.abs(p[1].getY()+.7040625*.75-headTop)<.00001,"Scaled rider pelvis meets crouched carrier's rendered head");
|
||||
h.assertTrue(Math.abs(p[2].getY()+.7040625-(p[1].getY()+1.8759375*.75))<.00001,"Next layer uses the seated carrier's head and its scale");
|
||||
}finally{cleanup(p);}h.succeed();
|
||||
}
|
||||
@GameTest public void babiesAddNoWeightAndGrowingUpUpdatesTheWholeStack(GameTestHelper h){
|
||||
var p=players(h,2);double base=speed(p[0]);var cow=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
var online=h.getLevel().getServer().getPlayerList().getPlayers();var added=new java.util.ArrayList<ServerPlayer>();
|
||||
try{
|
||||
for(var player:p)if(!online.contains(player)){online.add(player);added.add(player);}
|
||||
tower(h,p);cow.setBaby(true);cow.setNoAi(true);cow.setPos(p[1].position().add(0,0,2));h.getLevel().addFreshEntity(cow);
|
||||
h.assertTrue(cow.isBaby()&&CarryService.interact(p[1],cow),"Native baby is carried on the upper player's head");
|
||||
ratio(h,p[0],base,.75);ratio(h,p[1],base,1);
|
||||
cow.setBaby(false);ServerTickEvents.END_SERVER_TICK.invoker().onEndTick(h.getLevel().getServer());
|
||||
ratio(h,p[0],base,.5625);ratio(h,p[1],base,.75);
|
||||
cow.setBaby(true);ServerTickEvents.END_SERVER_TICK.invoker().onEndTick(h.getLevel().getServer());
|
||||
ratio(h,p[0],base,.75);ratio(h,p[1],base,1);
|
||||
h.assertTrue(cow.getVehicle()==p[1],"Age changes do not drop or replace the animal");
|
||||
}finally{online.removeAll(added);cleanup(p);cow.discard();}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void familiarBabyAppearanceAddsNoWeightButMiniatureAdultsDo(GameTestHelper h){
|
||||
var p=players(h,2);double base=speed(p[0]);
|
||||
var pet=fr.koka.sanctuary.cosmetics.FamiliarService.TYPE.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
try{
|
||||
tower(h,p);
|
||||
for(var egg:java.util.List.of(net.minecraft.world.item.Items.COW_SPAWN_EGG,net.minecraft.world.item.Items.ZOMBIE_SPAWN_EGG,net.minecraft.world.item.Items.ENDER_DRAGON_SPAWN_EGG)){
|
||||
pet.bind(p[1],new net.minecraft.world.item.ItemStack(egg));pet.setPos(p[1].position().add(0,0,2));
|
||||
boolean baby=egg!=net.minecraft.world.item.Items.ENDER_DRAGON_SPAWN_EGG;
|
||||
h.assertTrue(pet.babyAppearance()==baby,"Familiar uses the same baby support as its native model");
|
||||
h.assertTrue(CarryService.interact(p[1],pet),"The familiar can be carried");
|
||||
ratio(h,p[0],base,baby?.75:.5625);ratio(h,p[1],base,baby?1:.75);
|
||||
h.assertTrue(CarryService.release(pet,true),"The same familiar is put down");
|
||||
ratio(h,p[0],base,.75);ratio(h,p[1],base,1);
|
||||
}
|
||||
}finally{cleanup(p);pet.discard();}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void stackPersistsAcrossTicksAndProtectsEveryMember(GameTestHelper h){
|
||||
var p=players(h,4);double base=speed(p[0]);
|
||||
try{
|
||||
tower(h,p);
|
||||
ratio(h,p[0],base,.421875);ratio(h,p[1],base,.5625);ratio(h,p[2],base,.75);
|
||||
for(int tick=0;tick<20;tick++){
|
||||
p[0].setPos(p[0].position().add(.03,0,0));
|
||||
for(int i=1;i<p.length;i++){p[i].setShiftKeyDown(false);p[i-1].positionRider(p[i]);}
|
||||
ServerTickEvents.END_SERVER_TICK.invoker().onEndTick(h.getLevel().getServer());
|
||||
for(int i=1;i<p.length;i++)h.assertTrue(p[i].getVehicle()==p[i-1]&&CarryService.mayDismount(p[i]),"Every layer stays mounted and can dismount after releasing sneak");
|
||||
}
|
||||
float health=p[0].getHealth();p[3].attack(p[0]);
|
||||
h.assertTrue(p[0].getHealth()==health&&!p[0].hurtServer(h.getLevel(),p[0].damageSources().playerAttack(p[3]),2)
|
||||
&&!p[3].hurtServer(h.getLevel(),p[3].damageSources().playerAttack(p[0]),2),"Damage is refused across the entire stack in both directions");
|
||||
p[3].stopRiding();ratio(h,p[0],base,.5625);ratio(h,p[2],base,1);
|
||||
h.assertTrue(!CarryService.marked(p[3])&&!CarryService.pair(p[0],p[3]),"Native dismount immediately removes carry status and friendly protection");
|
||||
}finally{cleanup(p);}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void aCarrierCanClimbWithItsWholeUpperGroup(GameTestHelper h){
|
||||
var p=players(h,4);double base=speed(p[0]);
|
||||
try{
|
||||
p[1].setPos(p[0].position().add(2,0,0));mount(h,p[2],p[1]);mount(h,p[3],p[2]);
|
||||
h.assertTrue(CarryService.interact(p[1],p[0]),"A player already carrying two people can climb a free head");
|
||||
for(int i=1;i<p.length;i++)h.assertTrue(p[i].getVehicle()==p[i-1]
|
||||
&&p[i].position().distanceTo(CarryService.head(p[i-1],p[i]))<.001,"All upper passengers immediately follow the new base");
|
||||
ratio(h,p[0],base,.421875);
|
||||
h.assertTrue(CarryService.release(p[1],true),"The lower carrier can put down the whole upper group");
|
||||
h.assertTrue(!p[1].isPassenger()&&p[2].getVehicle()==p[1]&&p[3].getVehicle()==p[2],"The detached group keeps its own hierarchy");
|
||||
for(int i=2;i<p.length;i++)h.assertTrue(p[i].position().distanceTo(CarryService.head(p[i-1],p[i]))<.001,"Safe placement moves every layer together");
|
||||
ratio(h,p[0],base,1);ratio(h,p[1],base,.5625);
|
||||
}finally{cleanup(p);}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void rejectsCyclesOccupiedHeadsAndForeignMounts(GameTestHelper h){
|
||||
var p=players(h,4);var pig=EntityTypes.PIG.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
try{
|
||||
tower(h,new ServerPlayer[]{p[0],p[1],p[2]});
|
||||
p[0].getAttribute(Attributes.ENTITY_INTERACTION_RANGE).setBaseValue(20);
|
||||
h.assertTrue(!CarryService.interact(p[0],p[2]),"The bottom player cannot climb their own upper passenger");
|
||||
h.assertTrue(!CarryService.interact(p[3],p[3]),"A player cannot ride themselves");
|
||||
p[3].setPos(p[0].position().add(0,0,2));
|
||||
h.assertTrue(!CarryService.interact(p[3],p[0])&&p[0].getPassengers().size()==1,"Each head still has exactly one seat");
|
||||
CarryService.detach(p[0]);CarryService.detach(p[1]);
|
||||
pig.setPos(p[0].position());h.getLevel().addFreshEntity(pig);
|
||||
h.assertTrue(p[0].startRiding(pig,true,true),"Native foreign mount fixture");
|
||||
h.assertTrue(!CarryService.interact(p[3],p[0]),"A normal animal mount is not a player stack");
|
||||
}finally{p[0].stopRiding();cleanup(p);pig.discard();}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void checksTheWholeGroupAgainstACeilingBeforeMounting(GameTestHelper h){
|
||||
var p=players(h,3);
|
||||
try{
|
||||
p[1].setPos(p[0].position().add(2,0,0));mount(h,p[2],p[1]);
|
||||
var ceiling=p[0].blockPosition().above(4);h.getLevel().setBlockAndUpdate(ceiling,Blocks.STONE.defaultBlockState());
|
||||
h.assertTrue(!CarryService.interact(p[1],p[0])&&!p[1].isPassenger()&&p[2].getVehicle()==p[1],"The first rider fits but the upper rider would hit the ceiling, so nothing moves");
|
||||
h.getLevel().setBlockAndUpdate(ceiling,Blocks.AIR.defaultBlockState());
|
||||
h.assertTrue(CarryService.interact(p[1],p[0]),"Removing that ceiling permits the same group");
|
||||
}finally{cleanup(p);}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void deathInTheMiddleSeparatesTheSurvivingGroups(GameTestHelper h){
|
||||
var p=players(h,4);double base=speed(p[0]);
|
||||
try{
|
||||
tower(h,p);p[1].die(p[1].damageSources().generic());
|
||||
h.assertTrue(!p[1].isPassenger()&&!p[1].isVehicle()&&!CarryService.marked(p[1]),"Death removes both carry links around the lost player");
|
||||
h.assertTrue(!p[2].isPassenger()&&p[3].getVehicle()==p[2]&&!CarryService.pair(p[0],p[3]),"The upper group is put down as a separate surviving stack");
|
||||
ratio(h,p[0],base,1);ratio(h,p[2],base,.75);
|
||||
}finally{cleanup(p);}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void unloadAndSaveNeverLeaveAPersistentTower(GameTestHelper h){
|
||||
var p=players(h,3);double base=speed(p[0]);
|
||||
try{
|
||||
tower(h,p);
|
||||
for(var player:p){
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());player.saveWithoutId(output);
|
||||
h.assertTrue(!output.buildResult().contains("RootVehicle")&&!output.buildResult().contains("Passengers"),"No player saves another inhabitant in their native data");
|
||||
}
|
||||
ServerEntityEvents.ENTITY_UNLOAD.invoker().onUnload(p[1],h.getLevel());
|
||||
for(var player:p)h.assertTrue(!player.isPassenger()&&!player.isVehicle()&&!CarryService.marked(player),"Unloading the middle player clears every affected link");
|
||||
ratio(h,p[0],base,1);ratio(h,p[1],base,1);
|
||||
}finally{cleanup(p);}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void carriedAnimalAddsWeightAndKeepsItsIdentity(GameTestHelper h){
|
||||
var p=players(h,2);double base=speed(p[0]);var cow=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);var id=cow.getUUID();
|
||||
try{
|
||||
tower(h,p);cow.setPos(p[1].position().add(0,0,2));h.getLevel().addFreshEntity(cow);
|
||||
h.assertTrue(CarryService.interact(p[1],cow)&&cow.getVehicle()==p[1],"A seated player can carry an animal on their head");
|
||||
ratio(h,p[0],base,.5625);ratio(h,p[1],base,.75);
|
||||
CarryService.release(cow,true);ratio(h,p[0],base,.75);ratio(h,p[1],base,1);
|
||||
h.assertTrue(!cow.isRemoved()&&cow.getUUID().equals(id)&&!CarryService.marked(cow),"Putting down the animal preserves the original entity");
|
||||
}finally{cleanup(p);cow.discard();}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void largeStacksStayMobileAndPreserveOtherSpeedModifiers(GameTestHelper h){
|
||||
var p=players(h,8);double base=speed(p[0]);var extra=SanctuaryMod.id("test_carry_other_speed");
|
||||
try{
|
||||
p[0].getAttribute(Attributes.MOVEMENT_SPEED).addTransientModifier(new AttributeModifier(extra,.5,AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL));
|
||||
tower(h,p);ratio(h,p[0],base,1.5*.25);
|
||||
for(int i=p.length-1;i>0;i--){
|
||||
p[i].stopRiding();double expected=1.5*Math.max(.25,Math.pow(.75,i-1));ratio(h,p[0],base,expected);
|
||||
}
|
||||
h.assertTrue(p[0].getAttribute(Attributes.MOVEMENT_SPEED).getModifier(extra)!=null,"Dismounting removes only Sanctuary's carrying penalty");
|
||||
}finally{cleanup(p);p[0].getAttribute(Attributes.MOVEMENT_SPEED).removeModifier(extra);}h.succeed();
|
||||
}
|
||||
|
||||
private static ServerPlayer[] players(GameTestHelper h,int count){
|
||||
var players=new ServerPlayer[count];
|
||||
for(int i=0;i<count;i++){
|
||||
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());
|
||||
p.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());
|
||||
p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);p.setHealth(6);players[i]=p;
|
||||
}
|
||||
var at=players[0].blockPosition();
|
||||
for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++)for(int y=-1;y<=19;y++)
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
return players;
|
||||
}
|
||||
private static void mount(GameTestHelper h,ServerPlayer rider,ServerPlayer carrier){
|
||||
rider.setPos(carrier.position().add(0,0,1.5));rider.setShiftKeyDown(true);
|
||||
h.assertTrue(CarryService.interact(rider,carrier),"Mount a free player head, including an already seated player");
|
||||
}
|
||||
private static void tower(GameTestHelper h,ServerPlayer[] p){for(int i=1;i<p.length;i++)mount(h,p[i],p[i-1]);}
|
||||
private static double speed(ServerPlayer p){return p.getAttributeValue(Attributes.MOVEMENT_SPEED);}
|
||||
private static void ratio(GameTestHelper h,ServerPlayer p,double base,double ratio){h.assertTrue(Math.abs(speed(p)-base*ratio)<.00001,"Expected speed factor "+ratio+", got "+speed(p)/base);}
|
||||
private static void cleanup(ServerPlayer[] p){for(int i=p.length-1;i>=0;i--)CarryService.detach(p[i]);}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.google.gson.*;
|
||||
import fr.koka.sanctuary.knowledge.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.recipes.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
public final class Collections035GameTests {
|
||||
@GameTest public void exhaustiveCatalogueMatchesLoadedVanilla(GameTestHelper h)throws Exception{
|
||||
var server=h.getLevel().getServer();var entries=RecipeCollections.entries(server);var primary=new HashSet<String>();var members=new HashSet<String>();int itemCount=0,blockCount=0;
|
||||
check(h,entries.size()==67,"67 collection definitions loaded");
|
||||
for(var entry:entries){
|
||||
var resource=server.getResourceManager().getResource(Identifier.fromNamespaceAndPath("sanctuary","sanctuary_recipe_collections/"+Identifier.parse(entry.id()).getPath()+".json")).orElseThrow();
|
||||
JsonObject raw;try(var reader=resource.openAsReader()){raw=JsonParser.parseReader(reader).getAsJsonObject();}
|
||||
for(var id:raw.getAsJsonArray("recipes"))check(h,primary.add(id.getAsString()),"Each vanilla recipe has one primary collection: "+id);
|
||||
var content=raw.getAsJsonObject("members");
|
||||
for(var id:content.getAsJsonArray("items")){itemCount++;members.add(id.getAsString());check(h,BuiltInRegistries.ITEM.containsKey(Identifier.parse(id.getAsString())),"Real item: "+id);}
|
||||
for(var id:content.getAsJsonArray("blocks")){blockCount++;members.add(id.getAsString());check(h,BuiltInRegistries.BLOCK.containsKey(Identifier.parse(id.getAsString())),"Real block: "+id);}
|
||||
for(String id:entry.items())check(h,BuiltInRegistries.ITEM.containsKey(Identifier.parse(id)),"Real discovery seed: "+id);
|
||||
for(String id:entry.advancements())check(h,server.getAdvancements().get(Identifier.parse(id))!=null,"Real advancement: "+id);
|
||||
}
|
||||
var actual=new HashSet<String>();for(var recipe:server.getRecipeManager().getRecipes())if(recipe.id().identifier().getNamespace().equals("minecraft"))actual.add(recipe.id().identifier().toString());
|
||||
check(h,primary.size()==2042&&primary.equals(actual),"All 2042 loaded vanilla recipes classified exactly once");
|
||||
check(h,itemCount==1658&&blockCount==1286&&members.size()==1815,"Complete document inventory uses real registry IDs");h.succeed();
|
||||
}
|
||||
@GameTest public void everyAdvancementTeachesItsCollectionsWithoutIngredients(GameTestHelper h){
|
||||
var server=h.getLevel().getServer();var entries=RecipeCollections.entries(server);var all=new TreeSet<String>();entries.forEach(e->all.addAll(e.advancements()));
|
||||
check(h,all.size()==126,"All progression advancements configured");
|
||||
for(String id:all){
|
||||
var p=player(h);var advancement=server.getAdvancements().get(Identifier.parse(id));for(String criterion:advancement.value().criteria().keySet())p.getAdvancements().award(advancement,criterion);
|
||||
var data=new RecipeKnowledge();int xp=p.totalExperience;RecipeCollections.apply(p,data);
|
||||
for(var collection:entries)if(collection.advancements().contains(id))check(h,data.collections.contains(collection.id())&&data.recipes.containsAll(collection.recipes()),"Advancement alone teaches whole family: "+id+" → "+collection.id());
|
||||
check(h,!data.learned()&&data.items.isEmpty()&&p.totalExperience==xp,"No purchase, ingredient possession or bonus XP fabricated");
|
||||
check(h,!RecipeCollections.apply(p,data),"Repeated evaluation is silent");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void everySurvivalFamilyHasDiscoveryAccessAndTechnicalHasNone(GameTestHelper h){
|
||||
var p=player(h);int families=0;
|
||||
for(var collection:RecipeCollections.entries(h.getLevel().getServer())){
|
||||
if(collection.operatorOnly()){check(h,collection.items().isEmpty()&&collection.advancements().isEmpty()&&collection.recipes().isEmpty(),"Technical content never grants survival recipes");continue;}
|
||||
families++;var data=new RecipeKnowledge();check(h,!collection.items().isEmpty(),"A discovery route exists before the late advancement");data.items.add(collection.items().getFirst());RecipeCollections.apply(p,data);
|
||||
check(h,data.collections.contains(collection.id())&&data.recipes.containsAll(collection.recipes()),"First family item teaches the family: "+collection.id());
|
||||
}
|
||||
var technical=new RecipeKnowledge();technical.items.add("minecraft:zombie_spawn_egg");technical.items.add("minecraft:barrier");RecipeCollections.apply(p,technical);
|
||||
check(h,families==66&&!technical.collections.contains("sanctuary:technical_catalogue"),"66 survival collections; familiars never unlock the technical catalogue");h.succeed();
|
||||
}
|
||||
@GameTest public void discoveryBoundariesAndCircularDependencies(GameTestHelper h){
|
||||
var p=player(h);var wood=new RecipeKnowledge();wood.items.add("minecraft:oak_planks");RecipeCollections.apply(p,wood);
|
||||
check(h,wood.recipes.contains("minecraft:crafting_table")&&wood.recipes.contains("minecraft:oak_boat")&&wood.recipes.contains("minecraft:crimson_door"),"Wood and historical boats/Nether woodworking remain available");
|
||||
check(h,!wood.recipes.contains("minecraft:piston")&&!wood.recipes.contains("minecraft:shield"),"A plank is not every recipe containing planks");
|
||||
for(var example:Map.of("white_wool","white_bed","blaze_rod","brewing_stand","redstone","crafter","clay_ball","brush").entrySet()){
|
||||
var data=new RecipeKnowledge();data.items.add("minecraft:"+example.getKey());RecipeCollections.apply(p,data);check(h,data.recipes.contains("minecraft:"+example.getValue()),"Starter teaches required tool before dependent advancement: "+example);
|
||||
}
|
||||
BlockKnowledgeService.recordSeen(p,Blocks.AMETHYST_BLOCK);RecipeService.refresh(p);
|
||||
check(h,RecipeService.knowledge(p).collections.contains("sanctuary:amethyst_and_optics")&&!RecipeService.knowledge(p).items.contains("minecraft:spyglass"),"Discovery feeds collection knowledge without creating possession");h.succeed();
|
||||
}
|
||||
@GameTest public void legacyRecordsReloadAndPlayersRemainIndependent(GameTestHelper h){
|
||||
var p=player(h);var old=new RecipeKnowledge();old.collections.add("sanctuary:basic_wood");old.recipes.add("minecraft:shield");old.recipes.add("another_pack:old_recipe");old.paid=4;old.purchasedAt=1;
|
||||
String saved=old.json();var data=RecipeKnowledge.parse(saved);check(h,RecipeCollections.apply(p,data)&&data.recipes.contains("minecraft:poplar_door"),"Existing collection gains new plans without rediscovering ingredients");
|
||||
String applied=data.json();for(int i=0;i<100;i++)check(h,!RecipeCollections.apply(p,data),"Stable record receives no repeated additions");
|
||||
RecipeCollections.clear(h.getLevel().getServer());check(h,!RecipeCollections.apply(p,data)&&data.json().equals(applied),"Reload reconciles once and retains every old recipe including absent IDs");
|
||||
p.setAttached(RecipeService.RECORD,data.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);RecipeService.refresh(p);
|
||||
check(h,RecipeService.knowledge(p).learned()&&RecipeService.knowledge(p).recipes.contains("another_pack:old_recipe"),"Progress reset preserves schema-1 recipe knowledge");
|
||||
var other=new RecipeKnowledge();RecipeCollections.apply(player(h),other);check(h,!other.collections.contains("sanctuary:basic_wood")&&!other.recipes.contains("minecraft:shield"),"Personal collection access is isolated");h.succeed();
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h){var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);p.getInventory().clearContent();return p;}
|
||||
private static void check(GameTestHelper h,boolean value,String message){h.assertTrue(value,message);}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.mixin.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
import net.minecraft.world.effect.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
|
||||
/** Behavioral checks exercise native hooks, resource mutations, authority and locomotion. */
|
||||
public final class Companion019GameTests {
|
||||
@GameTest public void catalogMatchesEveryNativeEgg(GameTestHelper h){
|
||||
var found=new HashSet<CompanionType>();
|
||||
for(var item:BuiltInRegistries.ITEM)if(item instanceof SpawnEggItem){
|
||||
var type=CompanionType.find(BuiltInRegistries.ENTITY_TYPE.getKey(SpawnEggItem.getType(new ItemStack(item))).toString());
|
||||
h.assertTrue(type!=null,"Each native egg has a configured power definition");found.add(type);
|
||||
}
|
||||
h.assertTrue(found.size()==88&&found.size()==CompanionType.values().length,"Exactly 88 native familiar species");h.succeed();
|
||||
}
|
||||
@GameTest public void activePurchaseChargesOnceAndCooldownPersists(GameTestHelper h){
|
||||
var p=player(h,CompanionType.ZOMBIE);p.giveExperienceLevels(10);
|
||||
h.assertTrue(CompanionService.buy(p)&&p.experienceLevel==6,"Link costs four actual levels");
|
||||
h.assertTrue(!CompanionService.buy(p)&&p.experienceLevel==6,"Duplicate purchase does not charge");
|
||||
long until=CompanionService.now()+120000;CompanionService.save(p,CompanionService.record(p).cooldown(until));
|
||||
CompanionService.clear(p);h.assertTrue(CompanionService.record(p).unlocked()&&CompanionService.record(p).readyAt()==until,"Lifecycle cache clearing preserves purchase and absolute cooldown");
|
||||
h.assertTrue(CompanionService.activate(p,-1).equals("cooldown"),"Operator and ordinary activation both respect the shared cooldown");h.succeed();
|
||||
}
|
||||
@GameTest public void malformedStateIsPreserved(GameTestHelper h){
|
||||
var p=player(h,CompanionType.CAT);String bad="{\"schema\":99}";p.setAttached(CompanionService.RECORD,bad);
|
||||
h.assertTrue(!CompanionService.available(p)&&CompanionService.record(p)==null,"Future state disables only powers");
|
||||
h.assertTrue(p.getAttached(CompanionService.RECORD).equals(bad),"Future data is not silently reset");h.succeed();
|
||||
}
|
||||
@GameTest public void switchingEggRemovesPassiveAndActive(GameTestHelper h){
|
||||
var p=player(h,CompanionType.CAT);CompanionPassives.tick(p);
|
||||
h.assertTrue(Math.abs(p.getAttributeValue(Attributes.FALL_DAMAGE_MULTIPLIER)-.8)<.001,"Cat applies native fall attribute");
|
||||
CompanionEffects.add(p,p,CompanionEffects.Kind.DEFENCE,100,.25F);
|
||||
AccessoryAccess.of(p).setItem(0,new ItemStack(Items.ZOMBIE_SPAWN_EGG));CompanionService.tick(p);
|
||||
h.assertTrue(!CompanionEffects.has(p,CompanionEffects.Kind.DEFENCE)&&p.getAttributeValue(Attributes.FALL_DAMAGE_MULTIPLIER)==1,"Switch clears owned active and passive modifiers");
|
||||
h.assertTrue(CompanionService.passive(p)==null,"New egg has its ten-second attunement");h.succeed();
|
||||
}
|
||||
@GameTest public void poisonDurationAndMilkUseNativeHooks(GameTestHelper h){
|
||||
var p=player(h,CompanionType.CAVE_SPIDER);p.addEffect(new MobEffectInstance(MobEffects.POISON,100,0));
|
||||
h.assertTrue(p.getEffect(MobEffects.POISON).getDuration()==80,"New poison is shortened by native addEffect hook");
|
||||
equip(p,CompanionType.COW);p.addEffect(new MobEffectInstance(MobEffects.SPEED,300,0));
|
||||
CompanionPassives.consume(new ItemStack(Items.MILK_BUCKET),p::removeAllEffects);
|
||||
h.assertTrue(p.hasEffect(MobEffects.SPEED)&&!p.hasEffect(MobEffects.POISON),"Selective milk preserves beneficial effects while native clear is called");h.succeed();
|
||||
}
|
||||
@GameTest public void sharedMilkSpendsOneBucket(GameTestHelper h){
|
||||
var p=player(h,CompanionType.COW);p.addEffect(new MobEffectInstance(MobEffects.POISON,100,0));p.getInventory().setItem(0,new ItemStack(Items.MILK_BUCKET));
|
||||
var action=CompanionActions.prepare(p,CompanionType.COW,-1);h.assertTrue(action.success(),"Milk has an actual condition to clean");action.commit().run();
|
||||
h.assertTrue(!p.hasEffect(MobEffects.POISON)&&p.getInventory().getItem(0).is(Items.BUCKET),"One milk consumed, one empty bucket returned");
|
||||
h.assertTrue(!CompanionActions.prepare(p,CompanionType.COW,-1).success(),"No-op cure cannot spend again");h.succeed();
|
||||
}
|
||||
@GameTest public void healingCostsFoodAndStaysBelowThreeHearts(GameTestHelper h){
|
||||
var p=player(h,CompanionType.ZOMBIE);p.setHealth(4);p.getFoodData().setFoodLevel(6);p.getFoodData().setSaturation(0);
|
||||
var action=CompanionActions.prepare(p,CompanionType.ZOMBIE,-1);h.assertTrue(action.success(),"Second wind can start");action.commit().run();
|
||||
h.assertTrue(p.getFoodData().getFoodLevel()==5,"One real food point spent up front");
|
||||
h.assertTrue(CompanionEffects.canRecover(p,false),"A scheduled small heal leaves shared capacity until delivered");
|
||||
CompanionEffects.tick(h.getLevel().getServer());h.assertTrue(p.getHealth()>4&&p.getHealth()<5.1&&p.getMaxHealth()==6,"Healing starts gradually without granting maximum health");
|
||||
h.assertTrue(p.hurtServer(h.getLevel(),p.damageSources().generic(),1),"Fixture takes real native damage");CompanionEffects.tick(h.getLevel().getServer());
|
||||
h.assertTrue(!CompanionEffects.has(p,CompanionEffects.Kind.HEAL),"Received damage interrupts recovery");h.succeed();
|
||||
}
|
||||
@GameTest public void airCannotExceedPurchasedCapacity(GameTestHelper h){
|
||||
var p=player(h,CompanionType.COD);var at=p.blockPosition();
|
||||
for(int y=0;y<3;y++)h.getLevel().setBlockAndUpdate(at.above(y),Blocks.WATER.defaultBlockState());p.baseTick();
|
||||
p.setAirSupply(p.getMaxAirSupply()-5);var action=CompanionActions.prepare(p,CompanionType.COD,-1);
|
||||
h.assertTrue(action.success(),"Bubble starts in water");action.commit().run();
|
||||
h.assertTrue(p.getAirSupply()==p.getMaxAirSupply()&&p.getMaxAirSupply()==90,"One bubble clamps to the three-bubble capacity");
|
||||
h.assertTrue(Math.abs(CompanionBudget.remaining(p,CompanionBudget.Pool.AIR)-25)<.001,"Only five air actually restored are charged to the beneficiary");h.succeed();
|
||||
}
|
||||
@GameTest public void allyCanRevokeAnExistingEffect(GameTestHelper h){
|
||||
var p=player(h,CompanionType.POLAR_BEAR);var ally=player(h,CompanionType.CAT);
|
||||
CompanionService.save(ally,CompanionService.record(ally).consent(true));CompanionEffects.add(p,ally,CompanionEffects.Kind.MELEE_DEFENCE,100,.25F);
|
||||
h.assertTrue(CompanionEffects.has(ally,CompanionEffects.Kind.MELEE_DEFENCE),"Consent permits finite effect");
|
||||
CompanionService.save(ally,CompanionService.record(ally).consent(false));h.assertTrue(!CompanionEffects.has(ally,CompanionEffects.Kind.MELEE_DEFENCE),"Revocation immediately removes effect applicability");h.succeed();
|
||||
}
|
||||
@GameTest public void damageBudgetAndNoTerrainExplosion(GameTestHelper h){
|
||||
var p=player(h,CompanionType.WITHER);var target=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(p.position().add(0,0,2));h.getLevel().addFreshEntity(target);
|
||||
var cast=new CompanionCombat.Cast(p,CompanionType.WITHER);float health=target.getHealth();
|
||||
h.assertTrue(CompanionCombat.hit(cast,target,12),"A visible hostile target can be hit");
|
||||
target.setInvulnerableTime(0);h.assertTrue(!CompanionCombat.hit(cast,target,12),"Three skulls share one twelve-point budget per PvE target");
|
||||
h.assertTrue(health-target.getHealth()<=12&&cast.spent.get(target.getUUID())==12,"Native armour and budget apply");
|
||||
var block=p.blockPosition().below();h.assertTrue(h.getLevel().getBlockState(block).is(Blocks.STONE),"No terrain explosion");target.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void lockedInventoryCannotSupplyConsumables(GameTestHelper h){
|
||||
var p=player(h,CompanionType.SHEEP);p.setXRot(85);p.getInventory().setItem(9,new ItemStack(Items.WOOL.white(),10));
|
||||
h.assertTrue(!CompanionActions.prepare(p,CompanionType.SHEEP,-1).success(),"Locked rows cannot fund powers");
|
||||
p.getInventory().setItem(0,new ItemStack(Items.WOOL.white(),1));var action=CompanionActions.prepare(p,CompanionType.SHEEP,-1);h.assertTrue(action.success(),"Hotbar can fund a real cushion");action.commit().run();
|
||||
h.assertTrue(p.getInventory().getItem(0).isEmpty()&&p.getInventory().getItem(9).getCount()==10,"Only the unlocked material is consumed");
|
||||
h.assertTrue(CompanionEffects.fall(p,10)==6&&CompanionEffects.fall(p,10)==10,"Shared cushion saves at most four points, exactly once");h.succeed();
|
||||
}
|
||||
@GameTest public void creatureProfilesActuallyMove(GameTestHelper h){
|
||||
var p=player(h,CompanionType.ENDER_DRAGON);var pet=FamiliarService.active(p);var start=pet.position();
|
||||
for(int i=0;i<120;i++)pet.tick();
|
||||
h.assertTrue(pet.isNoGravity()&&pet.motion()==FamiliarMotion.DRAGON,"Dragon uses aerial navigation");
|
||||
h.assertTrue(pet.position().distanceToSqr(start)>1&&pet.getY()>p.getY()+.5,"Native navigation produces an actual airborne displacement");
|
||||
h.assertTrue(h.getLevel().noBlockCollision(pet,pet.getBoundingBox()),"Flying familiar stays out of solid blocks");
|
||||
equip(p,CompanionType.GHAST);pet=FamiliarService.active(p);pet.tick();h.assertTrue(pet.isNoGravity(),"Ghast hovers rather than walking");
|
||||
equip(p,CompanionType.WITHER);pet=FamiliarService.active(p);pet.tick();h.assertTrue(pet.isNoGravity(),"Wither hovers rather than walking");h.succeed();
|
||||
}
|
||||
@GameTest public void aquaticPetFlopsAndThenSwims(GameTestHelper h){
|
||||
var p=player(h,CompanionType.SALMON);var pet=FamiliarService.active(p);pet.tick();
|
||||
h.assertTrue(!pet.isNoGravity()&&pet.getAttributeValue(Attributes.MOVEMENT_SPEED)<=.06,"Fish struggles on dry ground");
|
||||
var at=p.blockPosition();for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),Blocks.WATER.defaultBlockState());
|
||||
pet.setPos(p.position().add(2,0,0));p.baseTick();for(int i=0;i<15;i++)pet.tick();
|
||||
h.assertTrue(pet.isInWater()&&pet.isNoGravity()&&pet.getAttributeValue(Attributes.MOVEMENT_SPEED)>.3,"Fish switches to buoyant native swimming in water");h.succeed();
|
||||
}
|
||||
@GameTest public void projectileBudgetAndFriendlyTargets(GameTestHelper h){
|
||||
var p=player(h,CompanionType.WITHER);var other=player(h,CompanionType.CAT);other.setPos(p.position().add(0,0,2));
|
||||
var cast=new CompanionCombat.Cast(p,CompanionType.WITHER);
|
||||
h.assertTrue(CompanionCombat.hit(cast,other,12),"Native PvP hit is accepted");other.setInvulnerableTime(0);
|
||||
h.assertTrue(!CompanionCombat.hit(cast,other,12)&&other.getHealth()>=4,"All three skulls share a two-point PvP budget");
|
||||
var wolf=EntityTypes.WOLF.create(h.getLevel(),EntitySpawnReason.COMMAND);wolf.setTame(true,true);wolf.setPos(p.position().add(2,0,0));h.getLevel().addFreshEntity(wolf);
|
||||
h.assertTrue(!CompanionCombat.hit(new CompanionCombat.Cast(p,CompanionType.WITHER),wolf,12),"Tamed animals are never life-steal targets");wolf.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void potionSharingRestoresIndependentEffects(GameTestHelper h){
|
||||
var p=player(h,CompanionType.WITCH);p.setYRot(0);p.setXRot(0);var ally=player(h,CompanionType.CAT);ally.setPos(p.position().add(0,0,4));h.getLevel().addNewPlayer(ally);
|
||||
try{
|
||||
CompanionService.save(ally,CompanionService.record(ally).consent(true));ally.addEffect(new MobEffectInstance(MobEffects.SPEED,1000,0));
|
||||
var potion=new ItemStack(Items.POTION);potion.set(net.minecraft.core.component.DataComponents.POTION_CONTENTS,
|
||||
new net.minecraft.world.item.alchemy.PotionContents(Optional.empty(),Optional.empty(),List.of(new MobEffectInstance(MobEffects.SPEED,200,1)),Optional.empty()));p.setItemInHand(InteractionHand.MAIN_HAND,potion);
|
||||
var action=CompanionActions.prepare(p,CompanionType.WITCH,-1);h.assertTrue(action.success(),"A stronger real potion can be shared");action.commit().run();
|
||||
CompanionZones.tick(h.getLevel().getServer());
|
||||
h.assertTrue(p.getMainHandItem().is(Items.GLASS_BOTTLE)&&ally.getEffect(MobEffects.SPEED).getDuration()==200&&ally.getEffect(MobEffects.SPEED).getAmplifier()==1,"One full native dose, native strength, and one empty bottle");
|
||||
ally.addEffect(new MobEffectInstance(MobEffects.SPEED,600,2));CompanionWork.cancel(p);
|
||||
h.assertTrue(ally.getEffect(MobEffects.SPEED).getAmplifier()==2&&ally.getEffect(MobEffects.SPEED).getDuration()==600,"Later ordinary potion survives familiar cancellation");
|
||||
}finally{CompanionWork.cancel(p);ally.discard();}h.succeed();
|
||||
}
|
||||
@GameTest public void villagerBatchUsesNativeCostsAndStock(GameTestHelper h){
|
||||
var p=player(h,CompanionType.VILLAGER);p.setYRot(0);p.setXRot(0);var villager=EntityTypes.VILLAGER.create(h.getLevel(),EntitySpawnReason.COMMAND);villager.setNoAi(true);villager.setPos(p.position().add(0,0,2));
|
||||
var offer=new net.minecraft.world.item.trading.MerchantOffer(new net.minecraft.world.item.trading.ItemCost(Items.EMERALD,2),new ItemStack(Items.BREAD),2,1,0);
|
||||
var offers=new net.minecraft.world.item.trading.MerchantOffers();offers.add(offer);villager.setOffers(offers);h.getLevel().addFreshEntity(villager);p.getInventory().setItem(0,new ItemStack(Items.EMERALD,10));
|
||||
try{
|
||||
h.assertTrue(CompanionTrades.batch(p,-1).reason().equals("choose_offer"),"A real merchant creates a server-held quote");
|
||||
var action=CompanionTrades.batch(p,0);h.assertTrue(action.success(),"Quoted trade can execute");action.commit().run();
|
||||
var inventory=new CompanionInventory.Plan(p);h.assertTrue(inventory.count(s->s.is(Items.EMERALD))==6&&inventory.count(s->s.is(Items.BREAD))==2&&offer.getUses()==2,"Native stock caps the batch at two trades with exact payment and output");
|
||||
h.assertTrue(!CompanionTrades.batch(p,0).success(),"The consumed quote cannot be replayed");
|
||||
}finally{CompanionTrades.cancel(p);villager.discard();}h.succeed();
|
||||
}
|
||||
@GameTest public void cropCyclesStopAfterOwnerLeaves(GameTestHelper h){
|
||||
var p=player(h,CompanionType.BEE);h.getLevel().addNewPlayer(p);var at=p.blockPosition().offset(2,0,0);h.getLevel().setBlockAndUpdate(at.below(),Blocks.FARMLAND.defaultBlockState());h.getLevel().setBlockAndUpdate(at,Blocks.WHEAT.defaultBlockState());
|
||||
try{
|
||||
var state=h.getLevel().getBlockState(at);var random=net.minecraft.util.RandomSource.create(42);
|
||||
CompanionProduction.cropRoll(h.getLevel(),at,state,random,25,1);String raw=h.getLevel().getAttached(CompanionProduction.CROPS);
|
||||
h.assertTrue(raw!=null&&raw.contains(p.getUUID().toString()),"Only a present bee owner can start a crop cycle");
|
||||
CompanionProduction.interrupt(p);h.assertTrue(h.getLevel().getAttached(CompanionProduction.CROPS).contains("\"valid\":false"),"Leaving or changing the bond invalidates the cycle");
|
||||
int changed=0;for(int i=0;i<200;i++)if(CompanionProduction.cropRoll(h.getLevel(),at,state,random,25,1)==0)changed++;
|
||||
h.assertTrue(changed==0,"Returning cannot harvest stored growth credit");
|
||||
}finally{p.discard();}h.succeed();
|
||||
}
|
||||
@GameTest public void furnaceBonusConsumesFuelAndKeepsOneOutput(GameTestHelper h){
|
||||
var p=player(h,CompanionType.BLAZE);h.getLevel().addNewPlayer(p);
|
||||
try{
|
||||
var a=p.blockPosition().offset(2,0,0);var b=p.blockPosition().offset(6,0,0);
|
||||
h.getLevel().setBlockAndUpdate(a,Blocks.FURNACE.defaultBlockState());h.getLevel().setBlockAndUpdate(b,Blocks.FURNACE.defaultBlockState());
|
||||
var boosted=(net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity)h.getLevel().getBlockEntity(a);
|
||||
var ordinary=(net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity)h.getLevel().getBlockEntity(b);
|
||||
for(var furnace:List.of(boosted,ordinary)){furnace.setItem(0,new ItemStack(Items.RAW_IRON,2));furnace.setItem(1,new ItemStack(Items.COAL));}
|
||||
for(int i=0;i<100;i++)for(var pos:List.of(a,b))net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity.serverTick(h.getLevel(),pos,h.getLevel().getBlockState(pos),(net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity)h.getLevel().getBlockEntity(pos));
|
||||
var fast=(CompanionFurnaceAccess)boosted;var slow=(CompanionFurnaceAccess)ordinary;
|
||||
h.assertTrue(fast.sanctuary$cooking()>slow.sanctuary$cooking()&&fast.sanctuary$fuel()<slow.sanctuary$fuel(),"Extra cooking progress consumes matching fuel");
|
||||
for(int i=0;i<100;i++)net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity.serverTick(h.getLevel(),a,h.getLevel().getBlockState(a),boosted);
|
||||
h.assertTrue(boosted.getItem(2).getCount()==1&&boosted.getItem(0).getCount()==1,"Bonus changes speed without duplicating smelted output");
|
||||
}finally{CompanionProduction.interrupt(p);p.discard();}h.succeed();
|
||||
}
|
||||
@GameTest public void temporaryFangsCannotDealNativeDamageOrSave(GameTestHelper h){
|
||||
var p=player(h,CompanionType.EVOKER);var fangs=new net.minecraft.world.entity.projectile.EvokerFangs(h.getLevel(),p.getX(),p.getY(),p.getZ(),0,0,p);
|
||||
fangs.setAttached(CompanionProjectiles.VISUAL_FANGS,true);h.getLevel().addFreshEntity(fangs);
|
||||
h.assertTrue(!fangs.shouldBeSaved(),"Telegraph cannot become an autonomous damaging entity after reload");fangs.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void movementStopsRootAndSupportRemovalStopsSuspension(GameTestHelper h){
|
||||
var p=player(h,CompanionType.CREAKING);CompanionEffects.add(p,p,CompanionEffects.Kind.ROOT,120,.25F);CompanionEffects.add(p,p,CompanionEffects.Kind.KNOCKBACK,120,.5F);
|
||||
p.setPos(p.position().add(1,0,0));CompanionEffects.tick(h.getLevel().getServer());
|
||||
h.assertTrue(!CompanionEffects.has(p,CompanionEffects.Kind.ROOT)&&!CompanionEffects.has(p,CompanionEffects.Kind.KNOCKBACK),"Rooted resistance ends with root movement");
|
||||
equip(p,CompanionType.SHULKER);var at=p.blockPosition().offset(0,1,2);h.getLevel().setBlockAndUpdate(at,Blocks.STONE.defaultBlockState());
|
||||
CompanionEffects.add(p,p,CompanionEffects.Kind.SUSPEND,120,0).support=at;CompanionEffects.tick(h.getLevel().getServer());h.assertTrue(CompanionEffects.has(p,CompanionEffects.Kind.SUSPEND),"A solid support permits the finite suspension");
|
||||
h.getLevel().setBlockAndUpdate(at,Blocks.AIR.defaultBlockState());CompanionEffects.tick(h.getLevel().getServer());h.assertTrue(!CompanionEffects.has(p,CompanionEffects.Kind.SUSPEND),"Removing support ends the suspension");h.succeed();
|
||||
}
|
||||
static ServerPlayer player(GameTestHelper h,CompanionType type){
|
||||
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());var record=ProgressRecord.INITIAL;
|
||||
p.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());
|
||||
p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);p.setHealth(6);p.getInventory().clearContent();
|
||||
var at=p.blockPosition();for(int x=-7;x<=7;x++)for(int z=-7;z<=7;z++)for(int y=-1;y<8;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
p.setOnGround(true);equip(p,type);return p;
|
||||
}
|
||||
static void equip(ServerPlayer p,CompanionType type){
|
||||
var item=BuiltInRegistries.ITEM.getValue(net.minecraft.resources.Identifier.withDefaultNamespace(type.id()+"_spawn_egg"));
|
||||
AccessoryAccess.of(p).setItem(0,new ItemStack(item));p.tickCount=((p.tickCount/10)+1)*10;FamiliarService.tick(p);CompanionService.tick(p);
|
||||
CompanionService.runtime(p).equippedAt=p.tickCount-200;
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.effect.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
public final class Companion032GameTests {
|
||||
@GameTest public void sharedHealingAllowsSmallComplementAndSurvivesReload(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.AXOLOTL);p.setHealth(2);
|
||||
h.assertTrue(Math.abs(CompanionBudget.heal(p,.3F)-.3)<.0001,"A small heal is delivered fractionally");
|
||||
h.assertTrue(Math.abs(CompanionBudget.remaining(p,CompanionBudget.Pool.HEALTH)-.9)<.0001,"Small heal leaves a complement");
|
||||
CompanionService.clear(p);Companion019GameTests.equip(p,CompanionType.ZOMBIE);
|
||||
h.assertTrue(Math.abs(CompanionBudget.heal(p,4)-.9)<.0001,"Changing egg and clearing caches cannot refill shared budget");
|
||||
h.assertTrue(CompanionBudget.heal(p,1)<.0001&&Math.abs(p.getHealth()-3.2)<.0001,"All sources together restore at most 20 percent of six HP");h.succeed();
|
||||
}
|
||||
@GameTest public void ledgerExpiryAndMalformedData(GameTestHelper h){
|
||||
var state=CompanionBudget.EMPTY.debit(CompanionBudget.Pool.HEALTH,.25,1001).debit(CompanionBudget.Pool.HEALTH,.5,1100);
|
||||
h.assertTrue(state.health().size()==1&&Math.abs(state.remaining(CompanionBudget.Pool.HEALTH,1.2,21001)-.45)<1e-9,"Bucket expires conservatively");
|
||||
h.assertTrue(state.remaining(CompanionBudget.Pool.HEALTH,1.2,21250)==1.2,"Expired spending recovers");
|
||||
h.assertTrue(CompanionBudget.State.parse(state.json()).equals(state),"Budget has exact persistence round trip");
|
||||
var p=Companion019GameTests.player(h,CompanionType.CAT);String raw="{\"schema\":99}";p.setAttached(CompanionBudget.RECORD,raw);
|
||||
h.assertTrue(!CompanionService.available(p)&&p.getAttached(CompanionBudget.RECORD).equals(raw),"Invalid budget blocks powers and retains original data");h.succeed();
|
||||
}
|
||||
@GameTest public void legacyDeadlineRemainsHonored(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.AXOLOTL);p.setHealth(2);
|
||||
CompanionService.save(p,CompanionService.record(p).recovery(false,CompanionService.now()+60000));
|
||||
h.assertTrue(CompanionBudget.heal(p,1)==0,"Migration must not shorten an existing legacy recovery deadline");h.succeed();
|
||||
}
|
||||
@GameTest public void airSavingsAreCountedPerBeneficiary(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.NAUTILUS);
|
||||
h.assertTrue(CompanionBudget.air(p,12)==12&&CompanionBudget.air(p,50)==18&&CompanionBudget.air(p,1)==0,"Three-bubble capacity yields only thirty active air ticks");
|
||||
CompanionService.clear(p);Companion019GameTests.equip(p,CompanionType.COD);
|
||||
h.assertTrue(CompanionBudget.air(p,1)==0,"Changing air source cannot refill recipient allowance");h.succeed();
|
||||
}
|
||||
@GameTest public void potionMistMovesWithoutCopyingDose(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.WITCH);var ally=Companion019GameTests.player(h,CompanionType.CAT);
|
||||
ally.setPos(p.position().add(0,0,4));h.getLevel().addNewPlayer(ally);
|
||||
try{
|
||||
var effect=new MobEffectInstance(MobEffects.SPEED,600,0);
|
||||
var zone=CompanionZones.add(p,CompanionZones.Kind.MIST,ally.position(),1.5,120,List.of(effect));
|
||||
int end=zone.end;zone.velocity=new Vec3(.1,0,0);CompanionZones.tick(h.getLevel().getServer());
|
||||
h.assertTrue(zone.end==end&&zone.spent&&zone.beneficiaries.size()==1,"Movement preserves TTL and only one dose is served");
|
||||
h.assertTrue(ally.hasEffect(MobEffects.SPEED)&&!p.hasEffect(MobEffects.SPEED),"Exactly one recipient receives the consumed potion");
|
||||
CompanionZones.tick(h.getLevel().getServer());CompanionService.clear(p);
|
||||
h.assertTrue(ally.getEffect(MobEffects.SPEED).getDuration()==600,"Real paid potion remains native after familiar removal");
|
||||
}finally{ally.discard();CompanionZones.cancel(p);}h.succeed();
|
||||
}
|
||||
@GameTest public void cushionsServeFourDistinctPlayersOnlyOnce(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.SHEEP);var zone=CompanionZones.add(p,CompanionZones.Kind.CUSHION,p.position(),1,160);
|
||||
h.assertTrue(CompanionEffects.fall(p,10)==6&&CompanionEffects.fall(p,10)==10,"Cushion saves half up to four HP once");
|
||||
for(int i=0;i<4;i++){
|
||||
var q=Companion019GameTests.player(h,CompanionType.CAT);q.setPos(p.position());
|
||||
h.assertTrue(CompanionEffects.fall(q,10)==(i<3?6:10),"Zone cannot recycle its four lifetime recipients");
|
||||
}
|
||||
CompanionZones.cancel(p);h.succeed();
|
||||
}
|
||||
@GameTest public void aerialBudgetCannotResetOnPlayerOrMagicalSupport(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.RABBIT);
|
||||
h.assertTrue(CompanionBudget.movement(p,3,2,true),"First hop fits shared envelope");
|
||||
h.assertTrue(!CompanionBudget.movement(p,1,0,true),"Only one rabbit hop per flight");
|
||||
h.assertTrue(CompanionBudget.movement(p,5,2,false)&&!CompanionBudget.movement(p,.01,0,false),"Different eggs share eight horizontal and four rising blocks");
|
||||
p.setOnGround(false);for(int i=0;i<8;i++)CompanionBudget.tick(p);
|
||||
h.assertTrue(CompanionBudget.state(p).horizontal()==8,"Air time does not reset movement");
|
||||
p.setOnGround(true);for(int i=0;i<4;i++)CompanionBudget.tick(p);
|
||||
h.assertTrue(CompanionBudget.state(p).horizontal()==0&&!CompanionBudget.state(p).rabbit(),"Actual stable terrain rearms flight");h.succeed();
|
||||
}
|
||||
@GameTest public void axolotlFoodDurationAndGolemKnockbackUseNativeHooks(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.AXOLOTL);
|
||||
h.assertTrue(new ItemStack(Items.BREAD).getUseDuration(p)==29,"Food is ten percent faster on land");
|
||||
Companion019GameTests.equip(p,CompanionType.IRON_GOLEM);
|
||||
h.assertTrue(Math.abs(CompanionPassives.knockback(p,1,null)-.8)<.00001,"Golem grants constant twenty percent knockback reduction");h.succeed();
|
||||
}
|
||||
@GameTest public void invalidAimedCushionDoesNotConsumeWool(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.SHEEP);p.setXRot(-80);p.getInventory().setItem(0,new ItemStack(Items.WOOL.white(),2));
|
||||
h.assertTrue(!CompanionActions.prepare(p,CompanionType.SHEEP,-1).success()&&p.getMainHandItem().getCount()==2,"Invalid target never consumes wool");h.succeed();
|
||||
}
|
||||
@GameTest public void defaultCatalogKeepsAllRaritiesAndNewCooldowns(GameTestHelper h){
|
||||
h.assertTrue(CompanionType.values().length==88&&CompanionConfig.DEFAULT.species().size()==88,"Eighty-eight species remain configured");
|
||||
h.assertTrue(CompanionType.RABBIT.cooldown==8&&CompanionType.WITCH.cooldown==30&&CompanionType.NAUTILUS.cooldown==45,"Precise prototype cooldowns");
|
||||
long common=Arrays.stream(CompanionType.values()).filter(t->t.rarity==CompanionType.Rarity.COMMON).count();h.assertTrue(common==22,"Existing rarity distribution remains stable");h.succeed();
|
||||
}
|
||||
@GameTest public void guardsAndPotionsDoNotMultiply(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.IRON_GOLEM);p.setYRot(0);p.setXRot(0);
|
||||
var zombie=net.minecraft.world.entity.EntityTypes.ZOMBIE.create(h.getLevel(),net.minecraft.world.entity.EntitySpawnReason.COMMAND);zombie.setPos(p.position().add(0,0,2));h.getLevel().addFreshEntity(zombie);
|
||||
var source=p.damageSources().mobAttack(zombie);CompanionZones.add(p,CompanionZones.Kind.GUARD,p.position(),3,80);
|
||||
h.assertTrue(Math.abs(CompanionEffects.incoming(p,source,10)-6)<.001,"Frontal guard reduces forty percent");
|
||||
zombie.setPos(p.position().add(0,0,-2));h.assertTrue(CompanionEffects.incoming(p,source,10)==10,"Guard leaves the back exposed");
|
||||
zombie.setPos(p.position().add(0,0,2));p.addEffect(new MobEffectInstance(MobEffects.RESISTANCE,200,1));
|
||||
h.assertTrue(CompanionEffects.incoming(p,source,10)==10,"Equal native Resistance receives no extra familiar multiplication");
|
||||
p.removeEffect(MobEffects.RESISTANCE);CompanionBudget.spend(p,CompanionBudget.Pool.GUARD,6000);
|
||||
h.runAfterDelay(1,()->{h.assertTrue(Math.abs(CompanionEffects.incoming(p,source,10)-7.5)<.001,"Exhausted shared guard falls back to twenty-five percent");zombie.discard();CompanionZones.cancel(p);h.succeed();});
|
||||
}
|
||||
@GameTest(maxTicks=100) public void controlledMonstersGainResistanceAndPlayersAreNotRooted(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.CREAKING);
|
||||
var mob=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setNoAi(true);mob.setPos(p.position().add(0,0,2));h.getLevel().addFreshEntity(mob);
|
||||
CompanionEffects.root(p,mob,100);var root=CompanionEffects.get(mob,CompanionEffects.Kind.ROOTED);
|
||||
h.assertTrue(root!=null&&root.end-root.start==60,"Hard control is capped at three seconds");
|
||||
CompanionEffects.root(p,p,100);h.assertTrue(!CompanionEffects.has(p,CompanionEffects.Kind.ROOTED),"No hard crowd control on players");
|
||||
h.runAfterDelay(61,()->{CompanionEffects.root(p,mob,60);h.assertTrue(!CompanionEffects.has(mob,CompanionEffects.Kind.ROOTED),"Another controller cannot bypass post-control resistance");mob.discard();h.succeed();});
|
||||
}
|
||||
@GameTest public void aimPriorityStillKeepsOwnerAndFourRecipientLimit(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.SKELETON);p.setYRot(0);p.setXRot(0);
|
||||
var people=new ArrayList<ServerPlayer>();
|
||||
try{for(int i=0;i<5;i++){
|
||||
var q=Companion019GameTests.player(h,CompanionType.CAT);q.setPos(p.position().add(i==0?0:i,0,3));h.getLevel().addNewPlayer(q);people.add(q);
|
||||
}
|
||||
var recipients=CompanionWorld.allies(p,16,4,true);
|
||||
h.assertTrue(recipients.size()==4&&recipients.getFirst()==people.getFirst()&&recipients.get(1)==p,"Aimed ally, owner, then nearest within the four-person cap");
|
||||
}finally{people.forEach(Entity::discard);}h.succeed();
|
||||
}
|
||||
@GameTest public void barrelUnloadingConservesStockAndProtectsHotbar(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.DONKEY);
|
||||
var progress=ProgressEffects.get(p).buy("inventory",1);ProgressEffects.apply(p,progress);
|
||||
var at=p.blockPosition().offset(0,0,2);h.getLevel().setBlockAndUpdate(at,Blocks.BARREL.defaultBlockState());
|
||||
var container=CompanionGestures.container(p,at);container.setItem(0,new ItemStack(Items.COBBLESTONE,60));
|
||||
p.getInventory().setItem(0,new ItemStack(Items.COBBLESTONE,10));p.getInventory().setItem(9,new ItemStack(Items.COBBLESTONE,20));p.getInventory().setItem(10,new ItemStack(Items.DIRT,5));
|
||||
var action=CompanionInventory.transfer(p,container,18,true);h.assertTrue(action!=null,"Barrel accepts existing types");action.run();
|
||||
int total=0;for(int i=0;i<container.getContainerSize();i++)if(container.getItem(i).is(Items.COBBLESTONE))total+=container.getItem(i).getCount();
|
||||
h.assertTrue(total==80&&p.getInventory().getItem(0).getCount()==10&&p.getInventory().getItem(9).isEmpty()&&p.getInventory().getItem(10).getCount()==5,"Exactly twenty matching items move; hotbar and other types stay");h.succeed();
|
||||
}
|
||||
@GameTest public void everyEggHasARealActionPath(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.CAT);
|
||||
for(var type:CompanionType.values()){
|
||||
Companion019GameTests.equip(p,type);var action=CompanionActions.prepare(p,type,-1);
|
||||
h.assertTrue(action!=null&&(action.success()||!action.reason().isEmpty()),"Every egg offers an action or a contextual refusal: "+type);
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void migrationPreservesCustomConfigAndRunsOnce(GameTestHelper h){
|
||||
try{
|
||||
java.nio.file.Files.createDirectories(java.nio.file.Path.of("build"));var dir=java.nio.file.Files.createTempDirectory(java.nio.file.Path.of("build"),"companion032-config-");var file=dir.resolve("powers.json");
|
||||
var entries=new TreeMap<>(CompanionConfig.DEFAULT.species());
|
||||
entries.put("rabbit",new CompanionConfig.Entry(true,CompanionType.Rarity.COMMON,25));
|
||||
entries.put("witch",new CompanionConfig.Entry(false,CompanionType.Rarity.RARE,37));
|
||||
var original=new CompanionConfig(1,true,4,10,2,entries).json();java.nio.file.Files.writeString(file,original);
|
||||
var migrated=CompanionConfig.load(file);h.assertTrue(migrated.entry(CompanionType.RABBIT).cooldownSeconds()==8,"Legacy default cooldown migrates");
|
||||
h.assertTrue(migrated.entry(CompanionType.WITCH).cooldownSeconds()==37&&!migrated.entry(CompanionType.WITCH).enabled(),"Custom cooldown, rarity and disable flag survive");
|
||||
java.nio.file.Files.writeString(file,original);h.assertTrue(CompanionConfig.load(file).entry(CompanionType.RABBIT).cooldownSeconds()==25,"The marker makes migration one-time");
|
||||
h.assertTrue(java.nio.file.Files.readString(file.resolveSibling("powers.json.pre-v2")).equals(original),"Original config is backed up exactly");
|
||||
}catch(Exception e){throw new AssertionError(e);}h.succeed();
|
||||
}
|
||||
@GameTest public void budgetsSurviveNativeSaveDeathAndPrestige(GameTestHelper h) throws Exception {
|
||||
var p=Companion019GameTests.player(h,CompanionType.AXOLOTL);p.setHealth(2);CompanionBudget.heal(p,.4F);CompanionBudget.air(p,12);CompanionBudget.movement(p,3,2,true);
|
||||
String expected=p.getAttached(CompanionBudget.RECORD);
|
||||
var saved=net.minecraft.world.level.storage.TagValueOutput.createWithContext(net.minecraft.util.ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(saved);
|
||||
var loaded=Companion019GameTests.player(h,CompanionType.CAT);loaded.load(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,h.getLevel().registryAccess(),saved.buildResult()));
|
||||
h.assertTrue(expected.equals(loaded.getAttached(CompanionBudget.RECORD)),"Native player save/load retains exact v2 accounting");
|
||||
var respawn=Companion019GameTests.player(h,CompanionType.CAT);respawn.setUUID(p.getUUID());respawn.restoreFrom(p,false);
|
||||
net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(p,respawn,false);
|
||||
h.assertTrue(expected.equals(respawn.getAttached(CompanionBudget.RECORD)),"Death copies all pending budgets");
|
||||
var session=ProgressionService.session(respawn.level().getServer());session.inhabitants.create(respawn.getUUID(),"budget"+respawn.getUUID().toString().substring(0,8),"Budget fixture",session.inhabitants.available().getFirst());
|
||||
var progress=ProgressEffects.get(respawn);for(String skill:ProgressRecord.SKILLS)while(progress.rank(skill)<ProgressRecord.cap(skill))progress=progress.buy(skill,1);
|
||||
respawn.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(respawn,progress);
|
||||
String answer=fr.koka.sanctuary.cycle.CycleService.perform(respawn,new fr.koka.sanctuary.cycle.CyclePayloads.Action("ngp","","",session.cycles.revision(),progress.revision()));
|
||||
h.assertTrue(answer.equals("done")&&expected.equals(respawn.getAttached(CompanionBudget.RECORD)),"Actual New Game Plus preserves budgets despite reduced capacities");h.succeed();
|
||||
}
|
||||
@GameTest public void thirtyRecipientsKeepBoundedAccounting(GameTestHelper h){
|
||||
long began=System.nanoTime();var people=new CompanionBudget.State[30];Arrays.fill(people,CompanionBudget.EMPTY);
|
||||
for(int tick=0;tick<1200;tick++)for(int i=0;i<people.length;i++){
|
||||
long now=100000+tick*50L;var state=people[i];
|
||||
for(var pool:CompanionBudget.Pool.values()){
|
||||
double cap=pool==CompanionBudget.Pool.HEALTH?1.2:pool==CompanionBudget.Pool.AIR?30:6000;
|
||||
double amount=Math.min(pool==CompanionBudget.Pool.HEALTH?.03:pool==CompanionBudget.Pool.AIR?1:50,state.remaining(pool,cap,now));
|
||||
if(amount>1e-8)state=state.debit(pool,amount,now);
|
||||
h.assertTrue(state.remaining(pool,cap,now)<=cap+1e-8,"Thirty overlapping sources cannot exceed a recipient cap");
|
||||
}
|
||||
people[i]=state;
|
||||
}
|
||||
for(var state:people)h.assertTrue(state.json().length()<24576&&CompanionBudget.State.parse(state.json()).equals(state),"Rolling ledgers remain bounded and serializable");
|
||||
System.out.println("FAM032: thirty-recipient, sixty-second budget simulation in "+((System.nanoTime()-began)/1_000_000)+" ms; no FPS claim");h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void breezeMovesAnotherPlayersPotionWithoutRenewingIt(GameTestHelper h){
|
||||
var witch=Companion019GameTests.player(h,CompanionType.WITCH);
|
||||
var breeze=Companion019GameTests.player(h,CompanionType.BREEZE);breeze.setYRot(0);breeze.setXRot(0);
|
||||
var at=breeze.getEyePosition().add(0,0,3);
|
||||
var zone=CompanionZones.add(witch,CompanionZones.Kind.MIST,at,1.5,120,List.of(new MobEffectInstance(MobEffects.SPEED,600,0)));
|
||||
int end=zone.end;
|
||||
try{
|
||||
var wind=CompanionGestures.wind(breeze);h.assertTrue(wind.success(),"Wind has a native action");wind.commit().run();
|
||||
CompanionZones.tick(h.getLevel().getServer());
|
||||
h.assertTrue(zone.center.z>at.z&&zone.end==end&&zone.owner==witch&&!zone.spent&&zone.potion.size()==1,"Another player's gust moves the same paid dose, owner and expiry intact");
|
||||
}finally{CompanionZones.cancel(witch);CompanionService.clear(breeze);}h.succeed();
|
||||
}
|
||||
@GameTest public void beeHarvestUsesARealSeedAndWearsTheHoe(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.BEE);p.getInventory().setItem(0,new ItemStack(Items.IRON_HOE));
|
||||
var at=p.blockPosition().offset(0,0,2);h.getLevel().setBlockAndUpdate(at.below(),Blocks.FARMLAND.defaultBlockState());
|
||||
var wheat=(CropBlock)Blocks.WHEAT;h.getLevel().setBlockAndUpdate(at,wheat.getStateForAge(wheat.getMaxAge()));
|
||||
h.assertTrue(!CompanionHarvest.harvest(p,at)&&wheat.isMaxAge(h.getLevel().getBlockState(at)),"No owned seed means no harvest or free replant");
|
||||
p.getInventory().setItem(1,new ItemStack(Items.WHEAT_SEEDS,2));
|
||||
h.assertTrue(CompanionHarvest.harvest(p,at),"One authorized ripe crop is harvested natively");
|
||||
h.assertTrue(h.getLevel().getBlockState(at).equals(wheat.getStateForAge(0))&&p.getInventory().getItem(1).getCount()==1&&p.getMainHandItem().getDamageValue()==1,"Exactly one real seed and one hoe durability pay for replanting");h.succeed();
|
||||
}
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.effect.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public class Companion044GameTests {
|
||||
@GameTest(maxTicks=120) public void slimePlacesRealBlockUnderPlayerThenExpires(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.SLIME);var pos=p.blockPosition();var ground=h.getLevel().getBlockState(pos.below());
|
||||
var action=CompanionActions.prepare(p,CompanionType.SLIME,-1);h.assertTrue(action.success(),"Ordinary ground permits a block below the player");action.commit().run();
|
||||
var state=h.getLevel().getBlockState(pos);h.assertTrue(state.is(CompanionSlimeBlock.BLOCK),"A real world block is placed");
|
||||
h.assertTrue(p.getY()==pos.getY()+1&&!state.getCollisionShape(h.getLevel(),pos).isEmpty()&&state.getBlock().getBounceRestitution()==1,"Full native slime collision and bounce");
|
||||
h.assertTrue(h.getLevel().getBlockTicks().hasScheduledTick(pos,CompanionSlimeBlock.BLOCK),"Lifetime uses persisted native block ticks");
|
||||
h.runAfterDelay(102,()->{h.assertTrue(h.getLevel().getBlockState(pos).isAir(),"Block disappears after five seconds");h.assertTrue(h.getLevel().getBlockState(pos.below())==ground,"Original ground preserved");h.succeed();});
|
||||
}
|
||||
@GameTest public void slimeRefusesLowCeilingAndCannotDropPermanentSlime(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.SLIME);var pos=p.blockPosition();h.getLevel().setBlockAndUpdate(pos.above(2),Blocks.STONE.defaultBlockState());
|
||||
var before=p.position();h.assertTrue(!CompanionActions.prepare(p,CompanionType.SLIME,-1).success()&&p.position().equals(before),"No suffocation beneath a ceiling");
|
||||
h.getLevel().setBlockAndUpdate(pos.above(2),Blocks.AIR.defaultBlockState());CompanionActions.prepare(p,CompanionType.SLIME,-1).commit().run();
|
||||
h.getLevel().destroyBlock(pos,true);h.assertTrue(h.getLevel().getEntitiesOfClass(ItemEntity.class,p.getBoundingBox().inflate(3)).stream().noneMatch(e->e.getItem().is(Items.SLIME_BLOCK)),"Temporary block cannot be harvested as a permanent block");h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=120) public void vexMiningDoesNotRequireBuildingRank(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.VEX);progress(p,ProgressRecord.INITIAL.buy("mining",1));
|
||||
p.setShiftKeyDown(true);p.setPose(Pose.CROUCHING);p.setYRot(0);p.setXRot(20);p.getInventory().setItem(0,new ItemStack(Items.DIAMOND_PICKAXE));
|
||||
var at=p.blockPosition().offset(0,0,3);h.getLevel().setBlockAndUpdate(at,Blocks.STONE.defaultBlockState());
|
||||
var action=CompanionActions.prepare(p,CompanionType.VEX,-1);h.assertTrue(action.success(),"Mining-only progression unlocks spectral mining");action.commit().run();h.runAfterDelay(100,()->{h.assertTrue(p.level().getBlockState(at).isAir(),"Spectral mining actually finishes without Building");h.succeed();});
|
||||
}
|
||||
@GameTest public void fallbackOfferingWithoutTargetDoesNotDemandGold(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIFIED_PIGLIN);var action=CompanionActions.prepare(p,CompanionType.ZOMBIFIED_PIGLIN,-1);
|
||||
h.assertTrue(action.success(),"Documented no-target fallback needs no gold");action.commit().run();
|
||||
h.assertTrue(CompanionEffects.value(p,CompanionEffects.Kind.KNOCKBACK)==.25F,"Fallback actually grants the described resistance");h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=200) public void everyEggCommitsWithActualRequiredContext(GameTestHelper h){
|
||||
var errors=new ArrayList<String>();int passed=0;
|
||||
for(var type:CompanionType.values()){
|
||||
var p=Companion019GameTests.player(h,type);var spawned=new ArrayList<Entity>();int selection=-1;
|
||||
try{
|
||||
progress(p,ProgressRecord.INITIAL.buy("inventory",1).buy("hunger",1).buy("swimming",1).buy("building",1).buy("mining",1));
|
||||
p.getFoodData().setFoodLevel(8);p.setHealth(3);p.setYRot(0);p.setXRot(0);var at=p.blockPosition();
|
||||
switch(type){
|
||||
case CHICKEN,PHANTOM->{p.setPos(p.position().add(0,2,0));p.setOnGround(false);p.fallDistance=25;}
|
||||
case COD,SALMON,DOLPHIN,NAUTILUS,ZOMBIE_NAUTILUS->{
|
||||
for(int x=-1;x<=1;x++)for(int y=0;y<5;y++)for(int z=-1;z<=5;z++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),Blocks.WATER.defaultBlockState());
|
||||
p.baseTick();p.setAirSupply(20);
|
||||
}
|
||||
case COW->{p.addEffect(new MobEffectInstance(MobEffects.POISON,100));p.getInventory().setItem(0,new ItemStack(Items.MILK_BUCKET));}
|
||||
case DONKEY,COPPER_GOLEM->{
|
||||
h.getLevel().setBlockAndUpdate(at.offset(0,1,2),Blocks.BARREL.defaultBlockState());var c=(Container)h.getLevel().getBlockEntity(at.offset(0,1,2));c.setItem(0,new ItemStack(Items.STONE));
|
||||
p.getInventory().setItem(9,new ItemStack(Items.STONE,12));p.setItemInHand(InteractionHand.OFF_HAND,new ItemStack(Items.STONE));
|
||||
}
|
||||
case MULE->{
|
||||
var ally=Companion019GameTests.player(h,CompanionType.CAT);progress(ally,ProgressRecord.INITIAL.buy("inventory",1));ally.setPos(p.position().add(0,0,2));h.getLevel().addNewPlayer(ally);spawned.add(ally);
|
||||
p.getInventory().setItem(9,new ItemStack(Items.STONE,12));p.setItemInHand(InteractionHand.OFF_HAND,new ItemStack(Items.STONE));
|
||||
}
|
||||
case PIG,SNIFFER->{h.getLevel().setBlockAndUpdate(at.offset(2,0,0),Blocks.OAK_LOG.defaultBlockState());fr.koka.sanctuary.knowledge.BlockKnowledgeService.recordSeen(p,Blocks.OAK_LOG);}
|
||||
case SHEEP->{p.setXRot(85);p.getInventory().setItem(0,new ItemStack(Items.WOOL.white()));}
|
||||
case SKELETON,BOGGED,PARCHED,STRAY->{p.getInventory().setItem(0,new ItemStack(Items.BOW));p.getInventory().setItem(1,new ItemStack(Items.ARROW,8));}
|
||||
case PILLAGER->p.getInventory().setItem(0,new ItemStack(Items.CROSSBOW));
|
||||
case CAVE_SPIDER,PIGLIN_BRUTE,WITHER_SKELETON->p.getInventory().setItem(0,new ItemStack(Items.DIAMOND_SWORD));
|
||||
case VINDICATOR->p.getInventory().setItem(0,new ItemStack(Items.DIAMOND_AXE));
|
||||
case SPIDER,SILVERFISH,TROPICAL_FISH,GLOW_SQUID,PARROT->{h.getLevel().setBlockAndUpdate(at.offset(0,1,1),Blocks.STONE.defaultBlockState());}
|
||||
case TADPOLE->{h.getLevel().setBlockAndUpdate(at.offset(1,0,0),Blocks.WATER.defaultBlockState());}
|
||||
case BEE->{h.getLevel().setBlockAndUpdate(at.offset(1,-1,1),Blocks.FARMLAND.defaultBlockState());h.getLevel().setBlockAndUpdate(at.offset(1,0,1),Blocks.WHEAT.defaultBlockState());p.getInventory().setItem(0,new ItemStack(Items.BONE_MEAL,4));}
|
||||
case SNOW_GOLEM->p.getInventory().setItem(0,new ItemStack(Items.SNOWBALL,3));
|
||||
case STRIDER->h.getLevel().setBlockAndUpdate(at.offset(2,-1,0),Blocks.LAVA.defaultBlockState());
|
||||
case SKELETON_HORSE->h.getLevel().setBlockAndUpdate(at.offset(2,-1,0),Blocks.WATER.defaultBlockState());
|
||||
case MOOSHROOM->{p.getInventory().setItem(0,new ItemStack(Items.MUSHROOM_STEW));p.getFoodData().setFoodLevel(2);}
|
||||
case FROG,ALLAY->{var item=new ItemEntity(h.getLevel(),p.getX(),p.getEyeY(),p.getZ()+3,new ItemStack(Items.STONE,5));h.getLevel().addFreshEntity(item);spawned.add(item);}
|
||||
case VEX->{h.getLevel().setBlockAndUpdate(at.offset(0,1,3),Blocks.STONE.defaultBlockState());p.getInventory().setItem(0,new ItemStack(Items.COBBLESTONE,2));}
|
||||
case WITCH->{var potion=new ItemStack(Items.POTION);potion.set(DataComponents.POTION_CONTENTS,new net.minecraft.world.item.alchemy.PotionContents(net.minecraft.world.item.alchemy.Potions.SWIFTNESS));p.getInventory().setItem(0,potion);}
|
||||
case VILLAGER,WANDERING_TRADER->{
|
||||
var v=EntityTypes.VILLAGER.create(h.getLevel(),EntitySpawnReason.COMMAND);v.setNoAi(true);v.setPos(p.position().add(0,0,2));
|
||||
var offers=new net.minecraft.world.item.trading.MerchantOffers();offers.add(new net.minecraft.world.item.trading.MerchantOffer(new net.minecraft.world.item.trading.ItemCost(Items.EMERALD,2),new ItemStack(Items.BREAD),4,1,0));v.setOffers(offers);h.getLevel().addFreshEntity(v);spawned.add(v);p.getInventory().setItem(0,new ItemStack(Items.EMERALD,10));
|
||||
if(type==CompanionType.VILLAGER){CompanionActions.prepare(p,type,-1);selection=0;}
|
||||
else{v.setTradingPlayer(p);p.tickCount=100;CompanionService.runtime(p).equippedAt=-100;CompanionTrades.observe(p,type);v.setTradingPlayer(null);}
|
||||
}
|
||||
case PIGLIN->{var v=EntityTypes.PIGLIN.create(h.getLevel(),EntitySpawnReason.COMMAND);v.setNoAi(true);v.setPos(p.position().add(0,0,2));h.getLevel().addFreshEntity(v);spawned.add(v);p.getInventory().setItem(0,new ItemStack(Items.GOLD_INGOT,2));}
|
||||
case ZOMBIE_HORSE,TRADER_LLAMA->{
|
||||
var horse=EntityTypes.DONKEY.create(h.getLevel(),EntitySpawnReason.COMMAND);horse.setTamed(true);horse.setOwner(p);horse.setPos(p.position().add(0,0,2));h.getLevel().addFreshEntity(horse);spawned.add(horse);
|
||||
if(type==CompanionType.ZOMBIE_HORSE)p.startRiding(horse,true,true);else p.setXRot(45);
|
||||
}
|
||||
case BAT,CAT,GUARDIAN,WOLF,SHULKER,WARDEN->{var v=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);v.setNoAi(true);v.setPos(p.position().add(0,0,2));h.getLevel().addFreshEntity(v);spawned.add(v);}
|
||||
default->{}
|
||||
}
|
||||
var action=CompanionActions.prepare(p,type,selection);h.assertTrue(action.success(),type+": valid fixture refused: "+action.reason());action.commit().run();
|
||||
switch(type){
|
||||
case COW->h.assertTrue(p.getMainHandItem().is(Items.BUCKET)&&!p.hasEffect(MobEffects.POISON),"Milk consumed and poison removed");
|
||||
case SLIME->h.assertTrue(h.getLevel().getBlockState(at).is(CompanionSlimeBlock.BLOCK),"Real slime block under player");
|
||||
case DONKEY,COPPER_GOLEM,MULE->h.assertTrue(p.getInventory().getItem(9).isEmpty(),"Real stock leaves source storage");
|
||||
case SHEEP->h.assertTrue(p.getMainHandItem().isEmpty(),"One wool paid");
|
||||
case SNOW_GOLEM->h.assertTrue(p.getMainHandItem().isEmpty(),"Three snowballs paid");
|
||||
case COD->h.assertTrue(p.getAirSupply()==50,"Thirty air supplied");
|
||||
case VILLAGER->h.assertTrue(p.getInventory().countItem(Items.BREAD)==3,"Three real trades completed");
|
||||
case PIGLIN->h.assertTrue(p.getMainHandItem().isEmpty()&&!p.level().getEntitiesOfClass(ItemEntity.class,p.getBoundingBox().inflate(3)).isEmpty(),"Two gold buy real barter drops");
|
||||
case VEX->h.assertTrue(p.getMainHandItem().getCount()==1,"Spectral placement consumes a real block");
|
||||
case CHICKEN,PHANTOM->h.assertTrue(p.fallDistance==0&&CompanionEffects.has(p,CompanionEffects.Kind.GLIDE),"Fall accumulator reset and glide active");
|
||||
case WITCH->h.assertTrue(p.getMainHandItem().is(Items.GLASS_BOTTLE),"Potion dose converted into one mist");
|
||||
default->{}
|
||||
}
|
||||
passed++;
|
||||
}catch(Throwable e){errors.add(type+": "+e.getMessage());}
|
||||
finally{p.stopRiding();CompanionService.clear(p);var pet=FamiliarService.active(p);if(pet!=null)pet.discard();spawned.forEach(Entity::discard);p.discard();}
|
||||
}
|
||||
fr.koka.sanctuary.SanctuaryMod.LOGGER.info("EGG044_POSITIVE_CONTEXT: {}/88, failures={}",passed,errors);
|
||||
h.assertTrue(errors.isEmpty(),String.join(" | ",errors));h.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks=80) public void creeperDestroysTerrainAfterFuse(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.CREEPER);var at=p.blockPosition().below();
|
||||
for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)h.getLevel().setBlockAndUpdate(at.offset(x,0,z),Blocks.DIRT.defaultBlockState());
|
||||
CompanionActions.prepare(p,CompanionType.CREEPER,-1).commit().run();
|
||||
h.assertTrue(h.getLevel().getBlockState(at).is(Blocks.DIRT),"Fuse does not explode immediately");
|
||||
h.runAfterDelay(35,()->{h.assertTrue(h.getLevel().getBlockState(at).isAir(),"Real creeper explosion excavates the floor");h.succeed();});
|
||||
}
|
||||
@GameTest public void nativeFireballsHaveRealWorldImpacts(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.BLAZE);var level=p.level();var wall=p.blockPosition().offset(0,1,4);
|
||||
level.setBlockAndUpdate(wall,Blocks.STONE.defaultBlockState());level.setBlockAndUpdate(wall.north().below(),Blocks.STONE.defaultBlockState());
|
||||
var blaze=CompanionNativePowers.launch(p,CompanionType.BLAZE,new Vec3(0,0,1));
|
||||
impact(blaze,new BlockHitResult(Vec3.atCenterOf(wall).add(0,0,-.5),Direction.NORTH,wall,false));
|
||||
h.assertTrue(level.getBlockState(wall.north()).is(Blocks.FIRE),"Blaze ignites an actual fire block");
|
||||
var ghast=CompanionNativePowers.launch(p,CompanionType.GHAST,new Vec3(0,0,1));var target=wall.offset(3,-1,0);
|
||||
level.setBlockAndUpdate(target,Blocks.DIRT.defaultBlockState());ghast.setPos(Vec3.atCenterOf(target));
|
||||
impact(ghast,new BlockHitResult(ghast.position(),Direction.UP,target,false));
|
||||
h.assertTrue(level.getBlockState(target).isAir()||level.getBlockState(target).is(Blocks.FIRE),"Ghast explosion destroys actual terrain");
|
||||
var dragon=CompanionNativePowers.launch(p,CompanionType.ENDER_DRAGON,new Vec3(0,0,1));dragon.setPos(Vec3.atCenterOf(wall));
|
||||
impact(dragon,new BlockHitResult(dragon.position(),Direction.NORTH,wall,false));
|
||||
var clouds=level.getEntitiesOfClass(AreaEffectCloud.class,new AABB(wall).inflate(8));
|
||||
h.assertTrue(clouds.stream().anyMatch(c->c.getDuration()==600&&c.getRadius()>=3),"Dragon creates a real lingering breath cloud");
|
||||
clouds.forEach(Entity::discard);h.succeed();
|
||||
}
|
||||
@GameTest public void nativeProjectilesInflictStatusEffects(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.WITHER);var level=p.level();var target=EntityTypes.PIG.create(level,EntitySpawnReason.COMMAND);
|
||||
target.setNoAi(true);target.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);
|
||||
target.setPos(p.position().add(0,0,4));level.addFreshEntity(target);
|
||||
var skull=CompanionNativePowers.launch(p,CompanionType.WITHER,new Vec3(0,0,1));skull.setPos(target.getEyePosition());
|
||||
impact(skull,new EntityHitResult(target));
|
||||
h.assertTrue(target.getHealth()<100&&target.hasEffect(MobEffects.WITHER),"Wither skull causes native damage and wither");
|
||||
target.discard();var floating=EntityTypes.PIG.create(level,EntitySpawnReason.COMMAND);floating.setNoAi(true);floating.setPos(p.position().add(0,0,4));level.addFreshEntity(floating);
|
||||
Companion019GameTests.equip(p,CompanionType.SHULKER);
|
||||
var bullet=new net.minecraft.world.entity.projectile.ShulkerBullet(level,p,floating,Direction.Axis.Y);level.addFreshEntity(bullet);
|
||||
impact(bullet,new EntityHitResult(floating));
|
||||
h.assertTrue(floating.hasEffect(MobEffects.LEVITATION),"Real shulker bullet levitates a fresh victim");
|
||||
floating.discard();
|
||||
target.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void nativeArrowAndMeleeEffectsSurviveRealHits(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.BOGGED);
|
||||
for(var type:List.of(CompanionType.BOGGED,CompanionType.PARCHED,CompanionType.STRAY,CompanionType.CAVE_SPIDER,CompanionType.WITHER_SKELETON)){
|
||||
Companion019GameTests.equip(p,type);var target=EntityTypes.PIG.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(p.position().add(0,0,3));p.level().addFreshEntity(target);
|
||||
var effect=switch(type){case BOGGED,CAVE_SPIDER->MobEffects.POISON;case PARCHED->MobEffects.WEAKNESS;case STRAY->MobEffects.SLOWNESS;default->MobEffects.WITHER;};
|
||||
boolean bow=type==CompanionType.BOGGED||type==CompanionType.PARCHED||type==CompanionType.STRAY;
|
||||
p.getInventory().setItem(0,new ItemStack(bow?Items.BOW:Items.DIAMOND_SWORD));p.getInventory().setItem(1,new ItemStack(Items.ARROW,5));
|
||||
var a=CompanionActions.prepare(p,type,-1);h.assertTrue(a.success(),"Attack can be armed: "+type);a.commit().run();
|
||||
if(bow){
|
||||
var arrow=new net.minecraft.world.entity.projectile.arrow.Arrow(p.level(),p,new ItemStack(Items.ARROW),p.getMainHandItem());arrow.setDeltaMovement(0,0,1);p.level().addFreshEntity(arrow);CompanionArrows.fired(arrow);
|
||||
impact(arrow,new EntityHitResult(target));arrow.discard();
|
||||
}else target.hurtServer(p.level(),p.damageSources().playerAttack(p),1);
|
||||
h.assertTrue(target.hasEffect(effect),"Native hit applies "+effect+" for "+type);target.discard();
|
||||
}h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=80) public void sonicBoomCrossesWall(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.WARDEN);p.setYRot(0);p.setXRot(0);
|
||||
var target=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(p.position().add(0,0,4));p.level().addFreshEntity(target);
|
||||
var wall=p.blockPosition().offset(0,0,2);for(int y=0;y<3;y++)p.level().setBlockAndUpdate(wall.above(y),Blocks.STONE.defaultBlockState());
|
||||
var a=CompanionActions.prepare(p,CompanionType.WARDEN,-1);h.assertTrue(a.success(),"Sonic boom can target through solid wall");a.commit().run();
|
||||
h.runAfterDelay(38,()->{h.assertTrue(target.getHealth()<=10,"Sonic damage crosses wall and bypasses zombie armour");target.discard();h.succeed();});
|
||||
}
|
||||
@GameTest(maxTicks=80) public void evokerFangsActuallyBite(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.EVOKER);p.setYRot(0);p.setXRot(0);
|
||||
var target=EntityTypes.PIG.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(p.position().add(0,0,3));p.level().addFreshEntity(target);
|
||||
CompanionActions.prepare(p,CompanionType.EVOKER,-1).commit().run();h.assertTrue(target.getHealth()==10,"Fangs wait for their native bite");
|
||||
h.runAfterDelay(40,()->{h.assertTrue(target.getHealth()<=4,"Native fangs deal six actual magic damage");target.discard();h.succeed();});
|
||||
}
|
||||
@GameTest public void copperCannotDistributeThroughAnInterveningWall(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.COPPER_GOLEM);progress(p,ProgressRecord.INITIAL.buy("inventory",1));
|
||||
var at=p.blockPosition().offset(0,1,3);p.level().setBlockAndUpdate(at,Blocks.BARREL.defaultBlockState());((Container)p.level().getBlockEntity(at)).setItem(0,new ItemStack(Items.STONE));
|
||||
p.getInventory().setItem(9,new ItemStack(Items.STONE,12));p.setItemInHand(InteractionHand.OFF_HAND,new ItemStack(Items.STONE));
|
||||
p.level().setBlockAndUpdate(at.north(),Blocks.STONE.defaultBlockState());
|
||||
h.assertTrue(!CompanionActions.prepare(p,CompanionType.COPPER_GOLEM,-1).success()&&p.getInventory().getItem(9).getCount()==12,"Intervening wall still blocks stock transfer");h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void windSpitAndSnowUseNativeImpacts(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.LLAMA);p.setYRot(0);p.setXRot(0);
|
||||
var pig=EntityTypes.PIG.create(p.level(),EntitySpawnReason.COMMAND);pig.setNoAi(true);pig.setPos(p.position().add(0,0,3));p.level().addFreshEntity(pig);
|
||||
CompanionActions.prepare(p,CompanionType.LLAMA,-1).commit().run();
|
||||
var spit=p.level().getEntitiesOfClass(net.minecraft.world.entity.projectile.LlamaSpit.class,p.getBoundingBox().inflate(4)).getFirst();impact(spit,new EntityHitResult(pig));
|
||||
h.assertTrue(pig.getHealth()==9,"Real spit causes one native damage");pig.discard();spit.discard();
|
||||
Companion019GameTests.equip(p,CompanionType.SNOW_GOLEM);p.getInventory().setItem(0,new ItemStack(Items.SNOWBALL,3));
|
||||
var blaze=EntityTypes.BLAZE.create(p.level(),EntitySpawnReason.COMMAND);blaze.setNoAi(true);blaze.setPos(p.position().add(0,0,3));p.level().addFreshEntity(blaze);
|
||||
CompanionActions.prepare(p,CompanionType.SNOW_GOLEM,-1).commit().run();
|
||||
var balls=p.level().getEntitiesOfClass(net.minecraft.world.entity.projectile.throwableitemprojectile.Snowball.class,p.getBoundingBox().inflate(4));
|
||||
h.assertTrue(balls.size()==3&&p.getMainHandItem().isEmpty(),"Three real snowballs consume three items");impact(balls.getFirst(),new EntityHitResult(blaze));
|
||||
h.assertTrue(blaze.getHealth()==17,"Native snowball deals three damage to Blaze");balls.forEach(Entity::discard);blaze.discard();
|
||||
Companion019GameTests.equip(p,CompanionType.BREEZE);var target=EntityTypes.PIG.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(p.position().add(0,0,3));p.level().addFreshEntity(target);
|
||||
CompanionActions.prepare(p,CompanionType.BREEZE,-1).commit().run();
|
||||
var charge=p.level().getEntitiesOfClass(net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.BreezeWindCharge.class,p.getBoundingBox().inflate(4)).getFirst();
|
||||
charge.setPos(target.position().add(0,.5,-1));impact(charge,new EntityHitResult(target));
|
||||
h.assertTrue(target.getDeltaMovement().lengthSqr()>0,"Native wind explosion pushes a real entity");target.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void mobGriefingStillControlsNativeExplosionTerrain(GameTestHelper h){
|
||||
var p=Companion019GameTests.player(h,CompanionType.GHAST);var rule=net.minecraft.world.level.gamerules.GameRules.MOB_GRIEFING;
|
||||
var rules=p.level().getGameRules();boolean before=rules.get(rule);var at=p.blockPosition().offset(0,0,4);
|
||||
try{
|
||||
rules.set(rule,false,p.level().getServer());p.level().setBlockAndUpdate(at,Blocks.DIRT.defaultBlockState());
|
||||
var fireball=CompanionNativePowers.launch(p,CompanionType.GHAST,new Vec3(0,0,1));fireball.setPos(Vec3.atCenterOf(at));
|
||||
impact(fireball,new BlockHitResult(fireball.position(),Direction.UP,at,false));
|
||||
h.assertTrue(p.level().getBlockState(at).is(Blocks.DIRT),"Native server rule can disable terrain destruction");
|
||||
}finally{rules.set(rule,before,p.level().getServer());}
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
private static void impact(net.minecraft.world.entity.projectile.Projectile projectile,HitResult hit){
|
||||
try{
|
||||
var method=net.minecraft.world.entity.projectile.Projectile.class.getDeclaredMethod("hitTargetOrDeflectSelf",HitResult.class);
|
||||
method.setAccessible(true);method.invoke(projectile,hit);
|
||||
}catch(ReflectiveOperationException failure){throw new AssertionError("Native projectile impact failed",failure);}
|
||||
}
|
||||
private static void progress(ServerPlayer p,ProgressRecord r){p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.targeting.TargetingConditions;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
public final class Corrections036GameTests {
|
||||
@GameTest public void carriedBirdGlidesWithoutStoringPastFallDamage(GameTestHelper h){
|
||||
var p=player(h);var bird=EntityTypes.PARROT.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
try{
|
||||
bird.setPos(p.position().add(0,0,1));h.getLevel().addFreshEntity(bird);
|
||||
h.assertTrue(CarryService.interact(p,bird),"Pick up a real parrot");p.setOnGround(false);p.setDeltaMovement(.2,-2,.1);p.fallDistance=32;
|
||||
CarryGlide.tick(p);h.assertTrue(CarryGlide.active(p)&&p.getDeltaMovement().y==-.12&&p.fallDistance==0,"Descent capped and accumulated fall cleared");
|
||||
h.assertTrue(p.getDeltaMovement().x==.2&&p.getDeltaMovement().z==.1,"Horizontal movement preserved");
|
||||
p.setDeltaMovement(0,.3,0);CarryGlide.tick(p);h.assertTrue(p.getDeltaMovement().y==.3,"Jump has no upward boost");
|
||||
p.getAbilities().flying=true;p.setDeltaMovement(0,-2,0);CarryGlide.tick(p);h.assertTrue(p.getDeltaMovement().y==-2,"Creative flight untouched");p.getAbilities().flying=false;
|
||||
CarryService.release(bird,true);p.setOnGround(false);p.setDeltaMovement(0,-2,0);p.fallDistance=8;CarryGlide.tick(p);
|
||||
h.assertTrue(!CarryGlide.supported(p)&&p.getDeltaMovement().y==-2&&p.fallDistance==8,"Release immediately restores ordinary fall without lingering potion");
|
||||
}finally{CarryService.detach(p);bird.discard();}h.succeed();
|
||||
}
|
||||
@GameTest public void flyingSpeciesAndNonFlyersUseNativeClassification(GameTestHelper h){
|
||||
var p=player(h);
|
||||
for(var type:new EntityType<?>[]{EntityTypes.BAT,EntityTypes.BEE,EntityTypes.CHICKEN,EntityTypes.GHAST,EntityTypes.WITHER,EntityTypes.ENDER_DRAGON}){
|
||||
var e=type.create(h.getLevel(),EntitySpawnReason.COMMAND);h.assertTrue(CarryGlide.flyer(e),"Flying carrier species "+type);e.discard();
|
||||
}
|
||||
var cow=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
try{cow.setPos(p.position().add(0,0,1));h.getLevel().addFreshEntity(cow);h.assertTrue(CarryService.interact(p,cow),"Cow picked up normally");p.setOnGround(false);p.setDeltaMovement(0,-2,0);CarryGlide.tick(p);h.assertTrue(!CarryGlide.supported(p)&&p.getDeltaMovement().y==-2,"A ground animal gives no gliding");}
|
||||
finally{CarryService.detach(p);cow.discard();}h.succeed();
|
||||
}
|
||||
@GameTest public void proneReducesNativeVisualDetectionWithoutInvulnerability(GameTestHelper h){
|
||||
var p=player(h);var skeleton=EntityTypes.SKELETON.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
var nativeSight=TargetingConditions.forNonCombat().range(16).ignoreLineOfSight();skeleton.setPos(p.position().add(12,0,0));p.setOnGround(true);
|
||||
h.assertTrue(nativeSight.test(h.getLevel(),skeleton,p),"Standing is visible at 12 blocks");
|
||||
h.assertTrue(LyingService.request(p,new LyingService.Request(1,ProgressEffects.get(p).revision())),"Free lying request");
|
||||
h.assertTrue(p.getVisibilityPercent(h.getLevel(),skeleton)==.5&&!nativeSight.test(h.getLevel(),skeleton,p),"Prone halves native visual acquisition range");
|
||||
skeleton.setPos(p.position().add(3,0,0));h.assertTrue(nativeSight.test(h.getLevel(),skeleton,p),"Nearby mobs still see you");
|
||||
float health=p.getHealth();h.assertTrue(p.hurtServer(h.getLevel(),p.damageSources().mobAttack(skeleton),1)&&p.getHealth()<health,"Lying is not damage immunity");
|
||||
p.setAttached(LyingService.STATE,3);h.assertTrue(p.getVisibilityPercent(h.getLevel(),skeleton)==1,"Diving is not ground stealth");
|
||||
p.setAttached(LyingService.STATE,2);h.assertTrue(p.getVisibilityPercent(h.getLevel(),skeleton)==.5,"Ground rest shares low visual profile");
|
||||
p.setOnGround(false);h.assertTrue(p.getVisibilityPercent(h.getLevel(),skeleton)==1,"No stealth while airborne");
|
||||
p.setOnGround(true);LyingService.request(p,new LyingService.Request(0,ProgressEffects.get(p).revision()));h.assertTrue(p.getVisibilityPercent(h.getLevel(),skeleton)==1,"Standing restores visibility");skeleton.discard();h.succeed();
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h){
|
||||
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());p.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());
|
||||
p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);p.setHealth(6);
|
||||
var at=p.blockPosition();for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)for(int y=-1;y<10;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());return p;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.cycle.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.atlas.*;
|
||||
import fr.koka.demeure.DemeureService;
|
||||
import java.util.*;
|
||||
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.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.stats.Stats;
|
||||
|
||||
public final class Cycle011GameTests {
|
||||
@GameTest public void sixSkillsRequiredButAptitudesOptional(GameTestHelper h) throws Exception {
|
||||
ProgressRecord five=ProgressRecord.INITIAL.buy("names",4).buy("atlas",4);
|
||||
for (String skill:ProgressRecord.SKILLS) if (!skill.equals("inventory"))
|
||||
while(five.rank(skill)<ProgressRecord.cap(skill)) five=five.buy(skill,1);
|
||||
var p=player(h,five,GameType.SURVIVAL);var session=session(p);
|
||||
h.assertTrue(action(p,"ngp","","").equals("incomplete") && session.cycles.account(p.getUUID()).prestige()==0,"Five completed skills cannot trigger NGP even with every aptitude");
|
||||
var ready=full(ProgressRecord.INITIAL);bind(p,ready);
|
||||
h.assertTrue(action(p,"ngp","","").equals("done") && session.cycles.account(p.getUUID()).prestige()==1,"All six skills suffice without either aptitude");
|
||||
h.assertTrue(!ProgressEffects.get(p).names() && !ProgressEffects.get(p).atlas(),"NGP does not invent aptitudes");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void resetPreservesFullInventoryAndCommunity(GameTestHelper h) throws Exception {
|
||||
var before=full(ProgressRecord.INITIAL.buy("names",4).buy("atlas",4));
|
||||
var p=player(h,before,GameType.SURVIVAL);var identity=session(p).inhabitants.get(p.getUUID());
|
||||
for(int slot=0;slot<36;slot++)p.getInventory().setItem(slot,new ItemStack(Items.DIAMOND,64));
|
||||
for(int slot=0;slot<18;slot++)SanctuaryOverflowAccess.of(p).setItem(slot,new ItemStack(Items.DIAMOND,64));
|
||||
HotbarRows.select(p,5);
|
||||
p.getInventory().setItem(36,new ItemStack(Items.DIAMOND_BOOTS));
|
||||
p.getEnderChestInventory().setItem(0,new ItemStack(Items.EMERALD,19));
|
||||
p.awardStat(Stats.JUMP,3);int jumps=p.getStats().getValue(Stats.CUSTOM.get(Stats.JUMP));
|
||||
byte[] colors=new byte[256];Arrays.fill(colors,(byte)42);
|
||||
var atlas=AtlasService.observations(p);atlas.observe(new AtlasStore.Tile(-220,300,colors));atlas.save();int tiles=atlas.size();
|
||||
var pos=p.blockPosition();DemeureService.record(p,pos,2);var footprint=DemeureService.view(p.level().getServer(),DemeureService.key(p,pos));
|
||||
p.giveExperienceLevels(60);p.setHealth(5);p.getFoodData().setFoodLevel(18);p.setAirSupply(300);
|
||||
int oldRevision=before.revision();int journalRevision=session(p).cycles.revision();
|
||||
h.assertTrue(action(p,"ngp","","").equals("done"),"Full inventory does not prevent NGP");
|
||||
var after=ProgressEffects.get(p);
|
||||
h.assertTrue(after.complete()==false && after.cycle()==1 && after.names() && after.atlas() && after.inventoryRows()==1,"Six skills reset and aptitudes remain");
|
||||
h.assertTrue(p.getMaxHealth()==6 && p.getHealth()==5 && p.getFoodData().getFoodLevel()==6 && p.getAirSupply()==90
|
||||
&& p.experienceLevel==0 && p.totalExperience==0 && p.experienceProgress==0,"3/3/3 capacities, zero XP and no free healing");
|
||||
h.assertTrue(count(p)==54*64 && SanctuaryInventoryCapacity.visibleRows(p)==6,"All 54 full rows of items remain visible for recovery");
|
||||
h.assertTrue(HotbarRows.selected(p)==0,"New Game+ returns a formerly extended hotbar to the unlocked base");
|
||||
var last=p.inventoryMenu.getSlot(63);
|
||||
h.assertTrue(last.mayPickup(p) && !last.mayPlace(new ItemStack(Items.DIAMOND)),"Last locked row is withdrawal-only after NGP");
|
||||
p.inventoryMenu.clicked(63,0,ContainerInput.PICKUP,p);
|
||||
h.assertTrue(p.inventoryMenu.getCarried().getCount()==64,"Real menu retrieves the last locked stack");
|
||||
p.inventoryMenu.clicked(63,0,ContainerInput.PICKUP,p);
|
||||
h.assertTrue(p.inventoryMenu.getCarried().getCount()==64 && last.getItem().isEmpty(),"Forged insertion cannot reuse a locked row");
|
||||
p.inventoryMenu.setCarried(ItemStack.EMPTY);last.set(new ItemStack(Items.DIAMOND,64));
|
||||
h.assertTrue(p.getInventory().getItem(36).is(Items.DIAMOND_BOOTS) && p.getEnderChestInventory().getItem(0).getCount()==19,"Equipment and Ender chest preserved");
|
||||
h.assertTrue(session(p).inhabitants.get(p.getUUID()).equals(identity) && atlas.size()==tiles && AtlasService.canRead(p,false)
|
||||
&& Objects.equals(footprint,DemeureService.view(p.level().getServer(),DemeureService.key(p,pos)))
|
||||
&& p.getStats().getValue(Stats.CUSTOM.get(Stats.JUMP))==jumps,"Identity, Atlas, Demeure and native stats stay attached to the inhabitant");
|
||||
p.giveExperienceLevels(10);
|
||||
h.assertTrue(!ProgressionService.purchase(p,"vitality",oldRevision) && p.experienceLevel==10,"Old-cycle purchase packet is rejected");
|
||||
h.assertTrue(ProgressionService.purchase(p,"vitality",after.revision()) && p.experienceLevel==9,"New-cycle purchase remains playable");
|
||||
h.assertTrue(CycleService.perform(p,new CyclePayloads.Action("ngp","","",journalRevision,oldRevision)).equals("stale")
|
||||
&& session(p).cycles.account(p.getUUID()).charges()==1,"Replayed NGP grants nothing");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void durableDecisionRecoversBeforeAndAfterPlayerSave(GameTestHelper h) throws Exception {
|
||||
var full=full(ProgressRecord.INITIAL.buy("atlas",4));var p=player(h,full,GameType.SURVIVAL);p.giveExperienceLevels(40);
|
||||
SanctuaryOverflowAccess.of(p).setItem(17,new ItemStack(Items.DIAMOND,17));
|
||||
var old=save(p,h);
|
||||
var session=session(p);session.cycles.complete(p.getUUID(),full,session.cycles.revision(),session.cycleConfig);
|
||||
var disk=new CycleStore(h.getLevel().getServer().getWorldPath(LevelResource.ROOT).resolve("data/sanctuary-cycle-factions.json"),h.getLevel().getSeed());
|
||||
h.assertTrue(disk.account(p.getUUID()).prestige()==1,"Completion is on disk before player mutation");
|
||||
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),old.buildResult()));
|
||||
// Native Player.load restores the authenticated profile UUID, not the NBT UUID.
|
||||
// Each helper mock has a fresh profile; represent the same authenticated inhabitant.
|
||||
loaded.setUUID(p.getUUID());
|
||||
var recovered=CycleService.reconcile(loaded,ProgressRecord.parse(loaded.getAttachedOrThrow(ProgressionService.RECORD)));
|
||||
h.assertTrue(recovered.cycle()==1 && recovered.atlas() && loaded.experienceLevel==0 && SanctuaryOverflowAccess.of(loaded).getItem(17).getCount()==17,"Crash before reset recovers once without touching items");
|
||||
loaded.giveExperienceLevels(12);h.assertTrue(ProgressionService.purchase(loaded,"hunger",recovered.revision()),"Purchases work after crash recovery");
|
||||
var saved=save(loaded,h);var again=Progression003GameTests.player(h);
|
||||
again.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),saved.buildResult()));
|
||||
again.setUUID(p.getUUID());
|
||||
var kept=CycleService.reconcile(again,ProgressRecord.parse(again.getAttachedOrThrow(ProgressionService.RECORD)));
|
||||
h.assertTrue(kept.hunger()==1 && again.experienceLevel==11 && session.cycles.account(p.getUUID()).charges()==1,"Reload after reset keeps newly earned XP, purchases and single reward");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void ordinaryDeathKeepsNewCycleAndFaction(GameTestHelper h) throws Exception {
|
||||
var p=player(h,full(ProgressRecord.INITIAL.buy("names",4)),GameType.SURVIVAL);
|
||||
h.assertTrue(action(p,"ngp","","").equals("done"),"NGP accepted");
|
||||
p.giveExperienceLevels(10);h.assertTrue(action(p,"create","","F"+p.getUUID().toString().substring(0,12)).equals("done"),"Faction founded");
|
||||
p.giveExperienceLevels(10);h.assertTrue(ProgressionService.purchase(p,"building",ProgressEffects.get(p).revision()),"A new-cycle skill is purchased");
|
||||
var before=ProgressEffects.get(p);var account=session(p).cycles.account(p.getUUID());var faction=session(p).cycles.membership(p.getUUID());
|
||||
var respawn=Progression003GameTests.player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(p,false);
|
||||
ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(p,respawn,false);
|
||||
h.assertTrue(before.equals(ProgressEffects.get(respawn)) && session(p).cycles.account(p.getUUID()).equals(account)
|
||||
&& session(p).cycles.membership(p.getUUID()).equals(faction),"Ordinary death keeps cycle, purchased skills, aptitude, prestige and faction");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void serverPermissionsInvitationsAndContributions(GameTestHelper h) throws Exception {
|
||||
var a=player(h,full(ProgressRecord.INITIAL),GameType.SURVIVAL);var b=player(h,full(ProgressRecord.INITIAL),GameType.SURVIVAL);
|
||||
var c=player(h,ProgressRecord.INITIAL,GameType.SURVIVAL);
|
||||
h.assertTrue(action(a,"ngp","","").equals("done"),"Founder earns charge");
|
||||
a.giveExperienceLevels(10);h.assertTrue(action(a,"create","","F"+a.getUUID().toString().substring(0,12)).equals("done"),"Founder pays levels for two places");
|
||||
var faction=session(a).cycles.membership(a.getUUID());
|
||||
h.assertTrue(action(a,"invite",session(a).inhabitants.get(b.getUUID()).name(),"").equals("done"),"Registered offline-capable name resolves to stable UUID");
|
||||
h.assertTrue(action(b,"accept",faction.id().toString(),"").equals("done"),"Invited member joins free");
|
||||
h.assertTrue(action(b,"invite",c.getUUID().toString(),"").equals("owner_only"),"Ordinary member cannot invite");
|
||||
h.assertTrue(action(b,"ngp","","").equals("done") && action(b,"expand","","").equals("done"),"Member's NGP charge funds the common capacity");
|
||||
h.assertTrue(session(a).cycles.membership(a.getUUID()).capacity()==3 && session(a).cycles.account(b.getUUID()).charges()==0,"Capacity and spent personal balance agree");
|
||||
h.assertTrue(action(a,"transfer",b.getUUID().toString(),"").equals("done") && action(a,"dissolve","","").equals("owner_only"),"Transferred authority immediately applies");
|
||||
h.assertTrue(action(b,"invite",c.getUUID().toString(),"").equals("done") && CycleService.view(c,"").invitations().size()==1,"Target can read its server-issued invitation");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void pendingContainersSpectatorsAndPartialRestoreRefused(GameTestHelper h) throws Exception {
|
||||
var p=player(h,full(ProgressRecord.INITIAL),GameType.SURVIVAL);
|
||||
p.inventoryMenu.setCarried(new ItemStack(Items.DIAMOND));
|
||||
h.assertTrue(action(p,"ngp","","").equals("close_container"),"Cursor items require closing inventory first");p.inventoryMenu.setCarried(ItemStack.EMPTY);
|
||||
p.containerMenu=ChestMenu.threeRows(9,p.getInventory(),new SimpleContainer(27));
|
||||
h.assertTrue(action(p,"ngp","","").equals("close_container"),"Open container blocks reset");p.containerMenu=p.inventoryMenu;
|
||||
var spectator=player(h,full(ProgressRecord.INITIAL),GameType.SPECTATOR);
|
||||
h.assertTrue(action(spectator,"ngp","","").equals("unavailable") && session(p).cycles.account(spectator.getUUID()).prestige()==0,"Spectators cannot trigger NGP");
|
||||
boolean refused=false;try {CycleService.reconcile(p,full(ProgressRecord.INITIAL).nextCycle());} catch(java.io.IOException expected) {refused=true;}
|
||||
h.assertTrue(refused && session(p).cycles.account(p.getUUID()).prestige()==0,"A player snapshot ahead of the journal fails closed");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void configuredXpPreservationSurvivesRecovery(GameTestHelper h) throws Exception {
|
||||
var p=player(h,full(ProgressRecord.INITIAL),GameType.SURVIVAL);p.giveExperienceLevels(27);
|
||||
var config=new CycleConfig(3,2,10,2,1,100,true);var session=session(p);
|
||||
session.cycles.complete(p.getUUID(),ProgressEffects.get(p),session.cycles.revision(),config);
|
||||
CycleService.reconcile(p,ProgressEffects.get(p));
|
||||
h.assertTrue(p.experienceLevel==27 && session.cycles.account(p.getUUID()).charges()==2,"Recovery uses the XP/reward policy recorded in the transaction");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void factionPrefixFollowsMembershipAndPrestige(GameTestHelper h) throws Exception {
|
||||
var owner=player(h,full(ProgressRecord.INITIAL),GameType.SURVIVAL);
|
||||
String accountName=owner.getName().getString();var identity=session(owner).inhabitants.get(owner.getUUID());
|
||||
h.assertTrue(action(owner,"ngp","","").equals("done"),"Prestige reached");owner.giveExperienceLevels(10);
|
||||
h.assertTrue(action(owner,"create","","Les Arpenteurs").equals("done")&&action(owner,"expand","","").equals("done"),"Faction founded and prestige charge buys third place");
|
||||
String display="Les Arpenteurs · "+accountName+" I";
|
||||
h.assertTrue(owner.getDisplayName().getString().equals(display) && owner.getTabListDisplayName().getString().equals(display),"Chat/server and Tab use prefix, name and Roman prestige");
|
||||
var member=player(h,ProgressRecord.INITIAL,GameType.SURVIVAL);
|
||||
h.assertTrue(action(owner,"invite",member.getUUID().toString(),"").equals("done"),"Member invited");
|
||||
var faction=session(owner).cycles.membership(owner.getUUID());
|
||||
h.assertTrue(action(member,"accept",faction.id().toString(),"").equals("done"),"Member joins without prestige");
|
||||
var shown=member.getDisplayName();
|
||||
h.assertTrue(shown.getString().equals("Les Arpenteurs · "+member.getName().getString()),"New member gets prefix without a prestige suffix");
|
||||
h.assertTrue(shown.getSiblings().get(0).getStyle().getColor().getValue()==faction.color()
|
||||
&& shown.getSiblings().get(2).getStyle().getColor().getValue()==session(member).inhabitants.get(member.getUUID()).color(),"Faction and personal colours stay distinct");
|
||||
var packet=new net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket(
|
||||
net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME,member);
|
||||
h.assertTrue(packet.entries().getFirst().displayName().getString().equals(shown.getString()),"Native player-list packet carries the display projection");
|
||||
h.assertTrue(action(member,"leave","","").equals("done") && member.getDisplayName().getString().equals(member.getName().getString()),"Leaving removes prefix immediately");
|
||||
h.assertTrue(action(owner,"dissolve","","").equals("done") && owner.getDisplayName().getString().equals(accountName+" I"),"Dissolution removes prefix and retains prestige");
|
||||
h.assertTrue(owner.getName().getString().equals(accountName) && session(owner).inhabitants.get(owner.getUUID()).equals(identity),"Account and inhabitant identity never change");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void foundingBeforePrestigeCostsTenLevels(GameTestHelper h)throws Exception{
|
||||
var p=player(h,ProgressRecord.INITIAL,GameType.SURVIVAL);var store=session(p).cycles;int revision=store.revision();
|
||||
h.assertTrue(action(p,"create","","F"+p.getUUID().toString().substring(0,12)).equals("levels_missing")&&store.revision()==revision,"Zero-level novice cannot found for free");
|
||||
p.giveExperienceLevels(12);p.experienceProgress=.35f;
|
||||
h.assertTrue(action(p,"create","","F"+p.getUUID().toString().substring(0,12)).equals("done"),"Novice can found before any prestige");
|
||||
var faction=store.membership(p.getUUID());
|
||||
h.assertTrue(p.experienceLevel==2&&p.experienceProgress==.35f&&faction.capacity()==2&&faction.owner().equals(p.getUUID())&&store.account(p.getUUID()).prestige()==0&&store.account(p.getUUID()).charges()==0,"Exactly ten whole levels, two places including the founder, no prestige spent");
|
||||
p.giveExperienceLevels(10);int duplicateRevision=store.revision();
|
||||
h.assertTrue(action(p,"create","","Autre faction").equals("already_member")&&p.experienceLevel==12&&store.revision()==duplicateRevision,"Refused duplicate creation costs nothing");
|
||||
var invited=player(h,ProgressRecord.INITIAL,GameType.SURVIVAL);
|
||||
h.assertTrue(action(p,"invite",invited.getUUID().toString(),"").equals("done")&&action(invited,"accept",faction.id().toString(),"").equals("done"),"Second player joins immediately without prestige or XP payment");
|
||||
var third=player(h,ProgressRecord.INITIAL,GameType.SURVIVAL);
|
||||
h.assertTrue(store.membership(p.getUUID()).members().size()==2&&CycleService.view(p,"").faction().capacity()==2&&invited.experienceLevel==0,"Both occupied places are sent in the client view and joining is free");
|
||||
h.assertTrue(action(p,"invite",third.getUUID().toString(),"").equals("capacity_full")&&action(p,"expand","","").equals("charges_missing"),"Third player waits for prestige funding");
|
||||
bind(p,full(ProgressRecord.INITIAL));h.assertTrue(action(p,"ngp","","").equals("done")&&action(p,"expand","","").equals("done"),"First prestige buys the third place");
|
||||
h.assertTrue(action(p,"invite",third.getUUID().toString(),"").equals("done")&&action(third,"accept",faction.id().toString(),"").equals("done"),"Third player joins after the charge is spent");
|
||||
h.assertTrue(store.membership(p.getUUID()).capacity()==3&&store.membership(p.getUUID()).members().size()==3&&store.account(p.getUUID()).charges()==0&&p.getAttachedOrElse(CycleService.XP_PAID,0)==10,"NGP retains the founding receipt and exactly one charge buys one place");h.succeed();
|
||||
}
|
||||
@GameTest public void foundingPaymentRecoversExactlyOnce(GameTestHelper h)throws Exception{
|
||||
var p=player(h,ProgressRecord.INITIAL,GameType.SURVIVAL);p.giveExperienceLevels(12);var old=save(p,h);var session=session(p);
|
||||
session.cycles.act(p.getUUID(),"create","","F"+p.getUUID().toString().substring(0,12),0x445566,session.cycles.revision(),session.cycleConfig);
|
||||
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),old.buildResult()));loaded.setUUID(p.getUUID());
|
||||
CycleService.reconcile(loaded,ProgressRecord.INITIAL);h.assertTrue(loaded.experienceLevel==2&&loaded.getAttachedOrElse(CycleService.XP_PAID,0)==10,"Committed faction debits an unsaved pre-purchase player snapshot");
|
||||
loaded.giveExperienceLevels(5);var saved=save(loaded,h);var again=Progression003GameTests.player(h);again.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),saved.buildResult()));again.setUUID(p.getUUID());
|
||||
CycleService.reconcile(again,ProgressRecord.INITIAL);h.assertTrue(again.experienceLevel==7,"Reloading paid receipt keeps subsequently earned levels");
|
||||
var respawn=Progression003GameTests.player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(again,false);ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(again,respawn,false);
|
||||
h.assertTrue(respawn.getAttachedOrElse(CycleService.XP_PAID,0)==10,"Death preserves payment receipt");
|
||||
again.setAttached(CycleService.XP_PAID,20);boolean refused=false;try{CycleService.reconcile(again,ProgressRecord.INITIAL);}catch(java.io.IOException expected){refused=true;}
|
||||
h.assertTrue(refused&&again.experienceLevel==7,"Partial restoration cannot silently discard an ahead-of-journal receipt");h.succeed();
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h,ProgressRecord progress,GameType mode) throws Exception {
|
||||
var p=Progression003GameTests.player(h,mode);p.setUUID(UUID.randomUUID());
|
||||
var session=session(p);var identities=session.inhabitants;
|
||||
identities.create(p.getUUID(),"cycle"+p.getUUID().toString().substring(0,8),"Cycle fixture",identities.available().getFirst());
|
||||
bind(p,progress);return p;
|
||||
}
|
||||
private static ProgressionService.Session session(ServerPlayer p) {return ProgressionService.session(p.level().getServer());}
|
||||
private static void bind(ServerPlayer p,ProgressRecord progress) {p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);}
|
||||
private static String action(ServerPlayer p,String action,String subject,String value) {
|
||||
return CycleService.perform(p,new CyclePayloads.Action(action,subject,value,session(p).cycles.revision(),ProgressEffects.get(p).revision()));
|
||||
}
|
||||
private static ProgressRecord full(ProgressRecord value) {for(String skill:ProgressRecord.SKILLS)while(value.rank(skill)<ProgressRecord.cap(skill))value=value.buy(skill,1);return value;}
|
||||
private static int count(ServerPlayer p) {int total=0;for(int i=0;i<36;i++)total+=p.getInventory().getItem(i).getCount();for(int i=0;i<18;i++)total+=SanctuaryOverflowAccess.of(p).getItem(i).getCount();return total;}
|
||||
private static TagValueOutput save(ServerPlayer p,GameTestHelper h) {var out=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(out);return out;}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.demeure.*;
|
||||
import fr.koka.sanctuary.atlas.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class Demeure006GameTests {
|
||||
@GameTest
|
||||
public void confirmedGesturesAndPersonalAtlasBoundary(GameTestHelper helper) {
|
||||
var level=helper.getLevel();var server=level.getServer();
|
||||
var player=(ServerPlayer)helper.makeMockServerPlayer(GameType.SURVIVAL);
|
||||
player.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(player,ProgressRecord.INITIAL);
|
||||
player.setPos(helper.absoluteVec(new Vec3(5.5,3,5.5)));
|
||||
var support=helper.absolutePos(new BlockPos(1,1,1));
|
||||
level.setBlockAndUpdate(support,Blocks.STONE.defaultBlockState());
|
||||
level.setBlockAndUpdate(support.above(),Blocks.AIR.defaultBlockState());
|
||||
var key=DemeureService.key(player,support);
|
||||
double before=score(player,key);
|
||||
helper.assertTrue(place(player,Items.COBBLESTONE,support),"Real block placement succeeds");
|
||||
helper.assertTrue(Math.abs(score(player,key)-before-.05)<1e-8,"Confirmed placement adds one footprint contribution");
|
||||
level.setBlockAndUpdate(support.above(),Blocks.AIR.defaultBlockState());
|
||||
helper.assertTrue(!place(player,Items.OAK_SAPLING,support),"Invalid sapling placement fails");
|
||||
helper.assertTrue(Math.abs(score(player,key)-before-.05)<1e-8,"Failed placement adds no footprint");
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND_PICKAXE));
|
||||
helper.assertTrue(player.gameMode.destroyBlock(support),"Native break succeeds");
|
||||
helper.assertTrue(Math.abs(score(player,key)-before-.1)<1e-8,"Native successful break adds one contribution");
|
||||
byte[] colors=new byte[256];java.util.Arrays.fill(colors,(byte)42);
|
||||
var tile=new AtlasStore.Tile(key.x(),key.z(),colors);
|
||||
AtlasService.observations(player).observe(tile);
|
||||
helper.assertTrue(AtlasDemeurePayload.forTiles(player,false,java.util.List.of(tile)).isEmpty(),"Locked map never transmits Demeure metadata");
|
||||
var progress=ProgressRecord.INITIAL.buy("atlas",4);
|
||||
player.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(player,progress);
|
||||
helper.assertTrue(AtlasDemeurePayload.forTiles(player,false,java.util.List.of(tile)).size()==1,"Known chunk has a footprint after map unlock");
|
||||
var hidden=new AtlasStore.Tile(key.x()+100,key.z()+100,colors);
|
||||
helper.assertTrue(AtlasDemeurePayload.forTiles(player,false,java.util.List.of(hidden)).isEmpty(),"Caller-supplied tiles cannot disclose unobserved footprints");
|
||||
helper.assertTrue(AtlasDemeurePayload.forTiles(player,true,java.util.List.of(tile)).isEmpty(),"Operator view requires actual permission");
|
||||
var spectator=(ServerPlayer)helper.makeMockServerPlayer(GameType.SPECTATOR);
|
||||
spectator.setAttached(ProgressionService.RECORD,progress.json());
|
||||
helper.assertTrue(DemeureService.record(spectator,support,2)==0,"Spectators leave no habitation influence");
|
||||
helper.succeed();
|
||||
}
|
||||
private static double score(ServerPlayer player,FootprintStore.Key key) {
|
||||
var view=DemeureService.view(player.level().getServer(),key);
|
||||
return view==null ? 0 : view.influences().stream().filter(s -> s.subject().id().equals(player.getUUID().toString())).mapToDouble(FootprintStore.Share::score).sum();
|
||||
}
|
||||
private static boolean place(ServerPlayer player,Item item,BlockPos support) {
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(item));
|
||||
return ((BlockItem)item).place(new BlockPlaceContext(new UseOnContext(player,InteractionHand.MAIN_HAND,
|
||||
new BlockHitResult(Vec3.atCenterOf(support),Direction.UP,support,false)))).consumesAction();
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.dispenser.*;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.projectile.Projectile;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Dispenser051GameTests {
|
||||
@GameTest public void allNativeProjectileItemsFollowYawAndPitch(GameTestHelper h){
|
||||
var p=player(h);equip(p,Items.DISPENSER);var session=HeadBlockService.session(p);var inventory=(DispenserBlockEntity)session.entity;
|
||||
var ammunition=DispenserBlock.DISPENSER_REGISTRY.entrySet().stream().filter(e->e.getValue() instanceof ProjectileDispenseBehavior).map(Map.Entry::getKey).toList();
|
||||
h.assertTrue(ammunition.size()==13,"All thirteen native projectile items covered");
|
||||
for(var item:ammunition)for(var angle:new float[][]{{0,0},{90,0},{180,0},{-90,0},{37,-48},{-126,64},{0,-90},{0,90}}){
|
||||
p.setYRot(angle[0]);p.setXRot(angle[1]);p.setPos(p.position().add(.013,0,.017));
|
||||
int loaded=Math.min(2,new ItemStack(item).getMaxStackSize());
|
||||
inventory.setItem(0,new ItemStack(item,loaded));
|
||||
HeadBlockService.scoped(session,()->{session.state.tick(h.getLevel(),session.pos(),h.getLevel().getRandom());return null;});
|
||||
var shots=shots(p);h.assertTrue(shots.size()==1,"Exactly one native projectile for "+item);
|
||||
var shot=shots.getFirst();var look=p.getLookAngle();
|
||||
h.assertTrue(shot.getDeltaMovement().normalize().dot(look)>.9,"Aim follows yaw and pitch for "+item+" at "+Arrays.toString(angle));
|
||||
var muzzle=p.getEyePosition().add(0,.4925,0).add(look.scale(.35));
|
||||
h.assertTrue(shot.position().distanceTo(muzzle)<.00001,"Spawn follows sub-block head position");
|
||||
h.assertTrue(shot.getOwner()==p,"Wearer owns the shot: "+item);
|
||||
h.assertTrue(inventory.getItem(0).getCount()==loaded-1,"One ammunition consumed: "+item);
|
||||
float power=((ProjectileItem)item).createDispenseConfig().power();
|
||||
h.assertTrue(shot.getDeltaMovement().length()>power*.65&&shot.getDeltaMovement().length()<power*1.35,"Native launch speed retained");
|
||||
shot.discard();
|
||||
}
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=20) public void delayedDamageTriggerUsesTheNewLookAndPosition(GameTestHelper h){
|
||||
var p=player(h);equip(p,Items.DISPENSER);var session=HeadBlockService.session(p);var inventory=(DispenserBlockEntity)session.entity;
|
||||
inventory.setItem(0,new ItemStack(Items.ARROW,2));p.setYRot(180);p.setXRot(0);HeadBlockService.pulse(p);
|
||||
h.assertTrue(shots(p).isEmpty(),"Native four-tick launch delay retained");
|
||||
p.setYRot(57);p.setXRot(-61);p.setPos(p.position().add(.42,0,-.38));
|
||||
h.runAfterDelay(5,()->{
|
||||
session.tick();var shots=shots(p);h.assertTrue(shots.size()==1,"Damage pulse emits one projectile");
|
||||
h.assertTrue(shots.getFirst().getDeltaMovement().normalize().dot(p.getLookAngle())>.97,"Uses current look at firing time");
|
||||
h.assertTrue(inventory.getItem(0).getCount()==1,"Single ammunition consumed after delay");
|
||||
shots.forEach(Entity::discard);cleanup(p);h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest public void placedDispenserKeepsItsOwnFacingEvenInsideAnotherHeadScope(GameTestHelper h){
|
||||
var p=player(h);equip(p,Items.DISPENSER);p.setYRot(90);p.setXRot(-60);var session=HeadBlockService.session(p);
|
||||
var at=session.pos().east(3);var state=Blocks.DISPENSER.defaultBlockState().setValue(DispenserBlock.FACING,Direction.SOUTH);
|
||||
h.getLevel().setBlockAndUpdate(at,state);var inventory=(DispenserBlockEntity)h.getLevel().getBlockEntity(at);
|
||||
for(boolean scoped:new boolean[]{false,true}){
|
||||
inventory.setItem(0,new ItemStack(Items.ARROW,2));
|
||||
if(scoped)HeadBlockService.scoped(session,()->{state.tick(h.getLevel(),at,h.getLevel().getRandom());return null;});
|
||||
else state.tick(h.getLevel(),at,h.getLevel().getRandom());
|
||||
var shots=shots(p);h.assertTrue(shots.size()==1,"Placed dispenser emits one projectile");var shot=shots.getFirst();
|
||||
h.assertTrue(shot.getOwner()==null&&shot.getDeltaMovement().normalize().dot(new Vec3(0,0,1))>.97,"Ordinary dispenser stays ownerless and follows its block facing");
|
||||
h.assertTrue(shot.getX()>p.getX()+2,"Origin stays at the placed dispenser");shot.discard();
|
||||
}
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void dropperStillDropsArrowsAndDownwardShotDoesNotHitWearer(GameTestHelper h){
|
||||
var p=player(h);equip(p,Items.DROPPER);var dropper=HeadBlockService.session(p);((DispenserBlockEntity)dropper.entity).setItem(0,new ItemStack(Items.ARROW,2));
|
||||
HeadBlockService.scoped(dropper,()->{dropper.state.tick(h.getLevel(),dropper.pos(),h.getLevel().getRandom());return null;});
|
||||
var drops=h.getLevel().getEntitiesOfClass(ItemEntity.class,p.getBoundingBox().inflate(8));
|
||||
h.assertTrue(shots(p).isEmpty()&&drops.stream().filter(e->e.getItem().is(Items.ARROW)).mapToInt(e->e.getItem().getCount()).sum()==1,"Dropper ejects one arrow item, never a shot");drops.forEach(Entity::discard);
|
||||
equip(p,Items.DISPENSER);p.setXRot(90);
|
||||
var session=HeadBlockService.session(p);((DispenserBlockEntity)session.entity).setItem(0,new ItemStack(Items.ARROW));
|
||||
HeadBlockService.scoped(session,()->{session.state.tick(h.getLevel(),session.pos(),h.getLevel().getRandom());return null;});
|
||||
var shot=shots(p).getFirst();float health=p.getHealth();shot.tick();
|
||||
h.assertTrue(p.getHealth()==health,"Native owner immunity prevents immediate self-hit downwards");
|
||||
shot.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
private static List<Projectile> shots(ServerPlayer p){return p.level().getEntitiesOfClass(Projectile.class,p.getBoundingBox().inflate(8));}
|
||||
private static ServerPlayer player(GameTestHelper h){var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);p.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());h.getLevel().addNewPlayer(p);return p;}
|
||||
private static void equip(ServerPlayer p,Item item){AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(item));}
|
||||
private static void cleanup(ServerPlayer p){p.closeContainer();AccessoryAccess.of(p).clearContent();p.discard();}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.lighting.ItemLightEmission;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.BlockItemStateProperties;
|
||||
import net.minecraft.world.level.block.*;
|
||||
|
||||
public final class Dynamic030GameTests {
|
||||
@GameTest public void allRegisteredBlockItemsUseTheirNativeEmission(GameTestHelper h) {
|
||||
int count=0;
|
||||
for(Item item:BuiltInRegistries.ITEM)if(item instanceof BlockItem block) {
|
||||
var stack=new ItemStack(item);
|
||||
h.assertTrue(ItemLightEmission.of(stack)==block.getBlock().defaultBlockState().getLightEmission(),"Native emission: "+item);count++;
|
||||
}
|
||||
h.assertTrue(count>500,"Checked complete block-item registry");h.succeed();
|
||||
}
|
||||
@GameTest public void litStateAndStackCountControlEmission(GameTestHelper h) {
|
||||
var candle=new ItemStack(Items.CANDLE,64);
|
||||
h.assertTrue(ItemLightEmission.of(candle)==0,"Unlit candles stay dark regardless of stack count");
|
||||
candle.set(DataComponents.BLOCK_STATE,BlockItemStateProperties.EMPTY.with(CandleBlock.LIT,true).with(CandleBlock.CANDLES,4));
|
||||
h.assertTrue(ItemLightEmission.of(candle)==12,"Four lit candles use actual block-state emission");
|
||||
candle.setCount(1);h.assertTrue(ItemLightEmission.of(candle)==12,"Stack quantity never changes light");
|
||||
candle.set(DataComponents.BLOCK_STATE,candle.get(DataComponents.BLOCK_STATE).with(CandleBlock.LIT,false));
|
||||
h.assertTrue(ItemLightEmission.of(candle)==0,"Extinguishing is immediately reflected, despite cached default");
|
||||
h.assertTrue(ItemLightEmission.of(new ItemStack(Items.SOUL_TORCH))==10 && ItemLightEmission.of(new ItemStack(Items.LANTERN))==15,"Native soul and lantern intensities");
|
||||
h.assertTrue(ItemLightEmission.of(new ItemStack(Items.LAVA_BUCKET))==15 && ItemLightEmission.of(new ItemStack(Items.DIAMOND))==0,"Luminous non-block mapping excludes ordinary items");h.succeed();
|
||||
}
|
||||
@GameTest public void cosmeticCakeFollowsServerLightingAndAppearance(GameTestHelper h) {
|
||||
var p=Progression003GameTests.player(h);p.setPos(h.absoluteVec(new net.minecraft.world.phys.Vec3(1,2,1)));
|
||||
p.setUUID(java.util.UUID.randomUUID());p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());
|
||||
ProgressEffects.apply(p,ProgressRecord.INITIAL);
|
||||
((SanctuaryExtendedInventoryMenu)p.inventoryMenu).sanctuary$ensureExtendedInventory();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.CAKE));AccessoryService.publish(p);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.CANDLE));
|
||||
h.assertTrue(HeadCosmeticService.interact(p,p),"Place cosmetic candle");
|
||||
h.assertTrue(ItemLightEmission.of(HeadCosmeticService.appearance(p))==0,"Unlit composite appearance is dark");
|
||||
h.startSequence().thenIdle(5).thenExecute(()->{
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.FLINT_AND_STEEL));
|
||||
h.assertTrue(HeadCosmeticService.interact(p,p),"Server lights candle");
|
||||
h.assertTrue(ItemLightEmission.of(HeadCosmeticService.appearance(p))==3,"Synced cosmetic cake emits native light");
|
||||
var drop=HeadCosmeticService.appearance(p).copy();
|
||||
h.assertTrue(ItemLightEmission.of(drop)==3,"Dropping preserves lit composite stack");
|
||||
h.assertTrue(!p.isOnFire(),"Visual light creates no player fire");
|
||||
}).thenIdle(5).thenExecute(()->{
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);HeadCosmeticService.interact(p,p);
|
||||
h.assertTrue(ItemLightEmission.of(HeadCosmeticService.appearance(p))==0,"Extinguished appearance stops emitting");
|
||||
}).thenSucceed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public final class Dynamic030RenderProbe {
|
||||
public static volatile boolean active;
|
||||
public static final AtomicLong litTerrainSamples=new AtomicLong();
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
import net.fabricmc.fabric.api.event.player.AttackEntityCallback;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Familiar020GameTests {
|
||||
@GameTest public void eggSizeSurvivesStorageAndChangesOfOwner(GameTestHelper h){
|
||||
var p=player(h);var egg=AccessoryAccess.of(p).getItem(0).copy();int size=FamiliarSize.value(egg);
|
||||
h.assertTrue(size>=120&&size<=6000&&FamiliarSize.value(FamiliarService.active(p).egg())==size,"First equipment rolls and synchronizes an egg-specific size");
|
||||
var random=net.minecraft.util.RandomSource.create(42);
|
||||
h.assertTrue(ItemStack.isSameItemSameComponents(egg,FamiliarSize.initialize(egg,random)),"Reequipping cannot reroll the trait");
|
||||
var output=output(h);p.saveWithoutId(output);var loaded=player(h);
|
||||
loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
|
||||
var stored=AccessoryAccess.of(loaded).getItem(0);
|
||||
h.assertTrue(ItemStack.isSameItemSameComponents(egg,stored),"Native inventory save/load preserves the exact egg and size");
|
||||
var other=player(h);stored.set(DataComponents.CUSTOM_NAME,Component.literal("Géant de poche"));
|
||||
AccessoryAccess.of(other).setItem(0,stored);other.tickCount+=10;FamiliarService.tick(other);
|
||||
h.assertTrue(FamiliarSize.value(FamiliarService.active(other).egg())==size,"Name changes and transfer to another inhabitant keep the same size");h.succeed();
|
||||
}
|
||||
@GameTest public void sizeDistributionAndUnknownDataAreBounded(GameTestHelper h){
|
||||
var random=net.minecraft.util.RandomSource.create(42);int tiny=0,ordinary=0,giant=0;
|
||||
for(int i=0;i<10000;i++){int n=FamiliarSize.roll(random);h.assertTrue(n>=120&&n<=6000,"Every size lies in the visual range");if(n<=350)tiny++;else if(n>=2500)giant++;else ordinary++;}
|
||||
h.assertTrue(tiny>300&&tiny<700&&giant>300&&giant<700&&ordinary>8500,"Ordinary sizes dominate, with rare tiny and giant eggs");
|
||||
var egg=new ItemStack(Items.CAT_SPAWN_EGG);net.minecraft.world.item.component.CustomData.update(DataComponents.CUSTOM_DATA,egg,tag->{
|
||||
tag.putString("other_mod","preserved");var unknown=new net.minecraft.nbt.CompoundTag();unknown.putInt("schema",99);unknown.putInt("per_mille",100000);tag.put(FamiliarSize.KEY,unknown);
|
||||
});
|
||||
var before=egg.copy();h.assertTrue(FamiliarSize.multiplier(egg)==1&&ItemStack.isSameItemSameComponents(before,FamiliarSize.initialize(egg,random)),"Unknown size data stays intact with neutral rendering");h.succeed();
|
||||
}
|
||||
@GameTest public void carriesPlayerOnHeadWithoutFriendlyDamage(GameTestHelper h){
|
||||
var carrier=player(h);var rider=player(h);rider.setPos(carrier.position().add(0,0,1.5));rider.setShiftKeyDown(true);
|
||||
double speed=carrier.getAttributeValue(Attributes.MOVEMENT_SPEED);
|
||||
h.assertTrue(CarryService.interact(rider,carrier),"Sneak interaction mounts native player on player");
|
||||
h.assertTrue(rider.getVehicle()==carrier&&Math.abs((rider.getY()+.7040625)-(carrier.getY()+1.8759375))<.00001,
|
||||
"The seated pelvis rests on the head; feet hang below it");
|
||||
h.assertTrue(Math.abs(carrier.getAttributeValue(Attributes.MOVEMENT_SPEED)-speed*.75)<.0001,"Carrier is slowed by 25 percent");
|
||||
h.assertTrue(!CarryService.mayDismount(rider),"The initial held sneak does not immediately dismount");
|
||||
h.assertTrue(!SanctuarySitting.request(carrier),"Carrying cannot accidentally turn a double-sneak gesture into sitting");
|
||||
var saved=output(h);rider.saveWithoutId(saved);
|
||||
h.assertTrue(!saved.buildResult().contains("RootVehicle"),"A carried player never saves the other player as its vehicle");
|
||||
float before=carrier.getHealth();rider.attack(carrier);
|
||||
h.assertTrue(carrier.getHealth()==before&&!carrier.hurtServer(h.getLevel(),carrier.damageSources().playerAttack(rider),2),"Direct and indirect rider damage refused");
|
||||
h.assertTrue(!rider.hurtServer(h.getLevel(),rider.damageSources().playerAttack(carrier),2),"Carrier cannot hurt its passenger");
|
||||
CarryService.detach(rider);
|
||||
h.assertTrue(!rider.isPassenger()&&!carrier.isRemoved()&&Math.abs(carrier.getAttributeValue(Attributes.MOVEMENT_SPEED)-speed)<.0001,"Detaching never removes the other player and restores speed");h.succeed();
|
||||
}
|
||||
@GameTest public void carriedAnimalSavesOnceWithNativeIdentity(GameTestHelper h){
|
||||
var p=player(h);var cow=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);cow.setPos(p.position().add(0,0,2));
|
||||
cow.setCustomName(Component.literal("Pamplemousse"));cow.setNoAi(true);cow.setHealth(7);h.getLevel().addFreshEntity(cow);
|
||||
try{
|
||||
h.assertTrue(CarryService.interact(p,cow)&&cow.shouldBeSaved(),"Original animal remains eligible for world saving while carried");
|
||||
var output=output(h);h.assertTrue(cow.save(output),"Native entity storage can save a carried animal as a root");
|
||||
var playerOutput=output(h);p.saveWithoutId(playerOutput);
|
||||
h.assertTrue(!playerOutput.buildResult().contains("Passengers"),"No duplicate animal is embedded in player data");
|
||||
var loaded=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
|
||||
h.assertTrue(loaded.getUUID().equals(cow.getUUID())&&loaded.getCustomName().getString().equals("Pamplemousse")&&loaded.getHealth()==7
|
||||
&&loaded.isNoAi()&&!loaded.isPassenger()&&!CarryService.marked(loaded),"UUID, name, health and prior AI survive without a saved carry state");
|
||||
h.assertTrue(CarryService.interact(p,cow)&&!cow.isPassenger()&&!cow.isRemoved(),"Second interaction puts the same animal down");
|
||||
}finally{CarryService.detach(p);cow.discard();}h.succeed();
|
||||
}
|
||||
@GameTest public void carryRejectsCrowdedHeadAndSecondSeat(GameTestHelper h){
|
||||
var p=player(h);var cow=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);cow.setPos(p.position().add(2,0,0));h.getLevel().addFreshEntity(cow);
|
||||
try{
|
||||
h.getLevel().setBlockAndUpdate(p.blockPosition().above(2),Blocks.STONE.defaultBlockState());
|
||||
h.assertTrue(!CarryService.interact(p,cow)&&!cow.isPassenger(),"An occupied head space refuses carrying");
|
||||
h.getLevel().setBlockAndUpdate(p.blockPosition().above(2),Blocks.AIR.defaultBlockState());
|
||||
h.assertTrue(CarryService.interact(p,cow),"Clear head space accepts carrying");
|
||||
var other=player(h);other.setPos(p.position().add(0,0,2));
|
||||
h.assertTrue(!CarryService.interact(other,p),"An occupied head cannot acquire a second direct passenger");
|
||||
}finally{CarryService.detach(p);cow.discard();}h.succeed();
|
||||
}
|
||||
@GameTest public void tappingAndRenamingFamiliarUsesNoDamage(GameTestHelper h){
|
||||
var p=player(h);var pet=FamiliarService.active(p);pet.setPos(p.position().add(0,0,1.5));float health=pet.getHealth();
|
||||
var sword=new ItemStack(Items.DIAMOND_SWORD);p.setItemInHand(InteractionHand.MAIN_HAND,sword);
|
||||
var result=AttackEntityCallback.EVENT.invoker().interact(p,h.getLevel(),InteractionHand.MAIN_HAND,pet,null);
|
||||
h.assertTrue(result.consumesAction()&&pet.hurtTime>0&&pet.getHealth()==health&&sword.getDamageValue()==0,"Tap animates without damage or tool wear");
|
||||
var tag=new ItemStack(Items.NAME_TAG,2);tag.set(DataComponents.CUSTOM_NAME,Component.literal("Nuage"));p.setItemInHand(InteractionHand.MAIN_HAND,tag);
|
||||
pet.interact(p,InteractionHand.MAIN_HAND,Vec3.ZERO);
|
||||
h.assertTrue(tag.getCount()==1&&pet.getCustomName().getString().equals("Nuage")&&AccessoryAccess.of(p).getItem(0).getCustomName().getString().equals("Nuage"),"Native name tag renames both pet and equipped egg");
|
||||
var named=AccessoryAccess.of(p).getItem(0).copy();AccessoryAccess.of(p).setItem(0,ItemStack.EMPTY);FamiliarService.tick(p);
|
||||
h.assertTrue(pet.isRemoved()&&FamiliarService.active(p)==null,"Removing egg despawns the familiar");
|
||||
AccessoryAccess.of(p).setItem(0,named);p.tickCount+=10;FamiliarService.tick(p);
|
||||
h.assertTrue(FamiliarService.active(p).getCustomName().getString().equals("Nuage"),"Reequipped named egg keeps the name");h.succeed();
|
||||
}
|
||||
@GameTest public void studyChargesOnceAndSurvivesDeathAndPrestige(GameTestHelper h) throws Exception {
|
||||
var p=player(h);p.giveExperienceLevels(10);
|
||||
h.assertTrue(CompanionStudy.buy(p)&&p.experienceLevel==6&&!CompanionStudy.buy(p)&&p.experienceLevel==6,"Study purchase charges exactly once");
|
||||
var output=output(h);p.saveWithoutId(output);var loaded=player(h);
|
||||
loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
|
||||
h.assertTrue(CompanionStudy.learned(loaded.getAttached(CompanionStudy.RECEIPT)),"Study receipt survives native save/load");
|
||||
var respawn=player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(p,false);ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(p,respawn,false);
|
||||
h.assertTrue(CompanionStudy.learned(respawn.getAttached(CompanionStudy.RECEIPT)),"Study remains unlocked after death");
|
||||
p=respawn; // The old life has been retired by the actual respawn callbacks.
|
||||
var session=ProgressionService.session(p.level().getServer());
|
||||
session.inhabitants.create(p.getUUID(),"study"+p.getUUID().toString().substring(0,8),"Study fixture",session.inhabitants.available().getFirst());
|
||||
var progress=ProgressEffects.get(p);for(String skill:ProgressRecord.SKILLS)while(progress.rank(skill)<ProgressRecord.cap(skill))progress=progress.buy(skill,1);
|
||||
p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);
|
||||
String receipt=p.getAttached(CompanionStudy.RECEIPT);
|
||||
var result=fr.koka.sanctuary.cycle.CycleService.perform(p,new fr.koka.sanctuary.cycle.CyclePayloads.Action("ngp","","",session.cycles.revision(),progress.revision()));
|
||||
h.assertTrue(result.equals("done")&&receipt.equals(p.getAttached(CompanionStudy.RECEIPT)),"A real prestige preserves the exact study purchase receipt");h.succeed();
|
||||
}
|
||||
@GameTest public void studyCorruptionAndConsentDefaults(GameTestHelper h){
|
||||
var p=player(h);h.assertTrue(CompanionService.record(p).acceptAllies(),"New inhabitants accept familiar assistance");
|
||||
CompanionService.save(p,CompanionService.record(p).consent(false));CompanionService.clear(p);
|
||||
h.assertTrue(!CompanionService.record(p).acceptAllies(),"An existing refusal is respected");
|
||||
String raw="{\"schema\":99}";p.setAttached(CompanionStudy.RECEIPT,raw);p.giveExperienceLevels(10);
|
||||
h.assertTrue(!CompanionStudy.buy(p)&&p.experienceLevel==10&&p.getAttached(CompanionStudy.RECEIPT).equals(raw),"Bad study receipt is preserved and never charged");h.succeed();
|
||||
}
|
||||
@GameTest public void divingRequiresSprintTakeoffAndCannotRepeat(GameTestHelper h){
|
||||
var p=player(h);p.setOnGround(false);p.setDeltaMovement(0,.3,.3);
|
||||
h.assertTrue(!posture(p,3),"A regular fall cannot start a dive");
|
||||
sprintJump(p);
|
||||
double oldSpeed=p.getDeltaMovement().horizontalDistance();double oldY=p.getDeltaMovement().y;
|
||||
h.assertTrue(posture(p,3)&&LyingService.posture(p)==3,"W after sprint takeoff starts dive");
|
||||
h.assertTrue(p.getDeltaMovement().horizontalDistance()>oldSpeed&&p.getDeltaMovement().y<=oldY,"Dive is a forward impulse without extra height");
|
||||
h.assertTrue(!posture(p,0)&&LyingService.posture(p)==3&&!posture(p,3),"Repeated posture requests cannot cancel the dive or grant a second airborne impulse");
|
||||
p.setOnGround(true);LyingService.tick(p);h.assertTrue(LyingService.posture(p)==0,"Landing finishes dive");h.succeed();
|
||||
}
|
||||
@GameTest public void divingEndsInWaterAndKeepsSwimmingLocked(GameTestHelper h){
|
||||
var p=player(h);sprintJump(p);h.assertTrue(posture(p,3),"Sprint jump dive begins");
|
||||
var at=p.blockPosition();for(int y=0;y<3;y++)h.getLevel().setBlockAndUpdate(at.above(y),Blocks.WATER.defaultBlockState());
|
||||
p.baseTick();LyingService.tick(p);p.updateSwimming();
|
||||
h.assertTrue(p.isInWater()&&LyingService.posture(p)==0&&!p.isSwimming(),"Water ends dry dive without unlocking swim movements");h.succeed();
|
||||
}
|
||||
@GameTest public void longDiveKeepsItsNativePoseUntilContact(GameTestHelper h) throws Exception {
|
||||
var p=airborneDive(h);p.fallDistance=12;p.setDeltaMovement(0,-1,0);
|
||||
for(int tick=0;tick<240;tick++){
|
||||
p.tickCount++;p.setShiftKeyDown(tick%2==0);p.setXRot(tick%90);p.setYRot(tick*5);
|
||||
h.assertTrue(!posture(p,0)&&!posture(p,2)&&!posture(p,3),"Posture keys cannot cancel or restart a dive before contact");
|
||||
LyingService.tick(p);updatePose(p);
|
||||
h.assertTrue(LyingService.posture(p)==3&&p.getPose()==Pose.SWIMMING,"The native low pose survives well beyond the former 30-tick limit");
|
||||
}
|
||||
h.assertTrue(p.fallDistance==12&&p.getDeltaMovement().equals(new Vec3(0,-1,0)),"Holding the posture adds no impulse and does not erase falling distance");
|
||||
p.setOnGround(true);LyingService.tick(p);h.succeed();
|
||||
}
|
||||
@GameTest public void diveEndsOnANativeWallCollision(GameTestHelper h) throws Exception {
|
||||
var p=airborneDive(h);var at=p.blockPosition();
|
||||
for(int x=-1;x<=1;x++)for(int y=0;y<=2;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,2),Blocks.STONE.defaultBlockState());
|
||||
p.move(MoverType.SELF,new Vec3(0,0,3));
|
||||
h.assertTrue(p.horizontalCollision&&!p.onGround(),"Native movement hits the wall while still airborne");
|
||||
LyingService.tick(p);h.assertTrue(LyingService.posture(p)==0&&!posture(p,3),"A wall ends the posture without granting another airborne impulse");h.succeed();
|
||||
}
|
||||
@GameTest public void diveEndsOnANativeCeilingCollision(GameTestHelper h) throws Exception {
|
||||
var p=airborneDive(h);var at=p.blockPosition();
|
||||
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)h.getLevel().setBlockAndUpdate(at.offset(x,1,z),Blocks.STONE.defaultBlockState());
|
||||
p.move(MoverType.SELF,new Vec3(0,2,0));
|
||||
h.assertTrue(p.verticalCollision&&!p.onGround(),"Native movement hits the ceiling rather than the ground");
|
||||
LyingService.tick(p);h.assertTrue(LyingService.posture(p)==0,"An upward collision ends the dive too");h.succeed();
|
||||
}
|
||||
@GameTest public void diveEndsOnNativeLandingAndCanStartAfterANewJump(GameTestHelper h) throws Exception {
|
||||
var p=airborneDive(h);p.move(MoverType.SELF,new Vec3(0,-5,0));
|
||||
h.assertTrue(p.onGround()&&p.verticalCollision,"Native movement really reaches the arena floor");
|
||||
LyingService.tick(p);updatePose(p);
|
||||
h.assertTrue(LyingService.posture(p)==0&&p.getPose()==Pose.STANDING,"Landing restores the standing pose in clear space");
|
||||
p.verticalCollision=false;sprintJump(p);
|
||||
h.assertTrue(posture(p,3),"A new grounded sprint jump permits a new dive");
|
||||
p.setOnGround(true);LyingService.tick(p);h.succeed();
|
||||
}
|
||||
@GameTest public void diveStillStopsOnDeathOrFlight(GameTestHelper h) throws Exception {
|
||||
var dead=airborneDive(h);dead.setHealth(0);LyingService.tick(dead);
|
||||
h.assertTrue(LyingService.posture(dead)==0,"Removing the timer never preserves a dive after death");
|
||||
var flying=airborneDive(h);flying.getAbilities().flying=true;LyingService.tick(flying);
|
||||
h.assertTrue(LyingService.posture(flying)==0,"Creative flight exits the temporary dive posture");h.succeed();
|
||||
}
|
||||
@GameTest public void slowFallClearsPreviouslyAccumulatedFall(GameTestHelper h){
|
||||
var p=player(h);Companion019GameTests.equip(p,CompanionType.CHICKEN);p.setOnGround(false);p.fallDistance=30;p.setDeltaMovement(0,-1,0);
|
||||
var action=CompanionActions.prepare(p,CompanionType.CHICKEN,-1);h.assertTrue(action.success(),"Chicken can cushion an existing fall");action.commit().run();
|
||||
CompanionEffects.tick(h.getLevel().getServer());
|
||||
h.assertTrue(p.fallDistance==0&&p.getDeltaMovement().y>=-.101,"Slow descent discards the old dangerous fall accumulation");
|
||||
CompanionEffects.removeOwner(p);p.fallDistance=8;float health=p.getHealth();p.causeFallDamage(p.fallDistance,1,p.damageSources().fall());
|
||||
h.assertTrue(p.getHealth()<health,"A new unprotected fall still causes native damage");h.succeed();
|
||||
}
|
||||
@GameTest public void teleportCanCrossWallButNeverLandInsideIt(GameTestHelper h){
|
||||
var p=player(h);Companion019GameTests.equip(p,CompanionType.ENDERMITE);p.setYRot(0);p.setXRot(0);
|
||||
var origin=p.blockPosition();for(int x=-2;x<=2;x++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(origin.offset(x,y,1),Blocks.STONE.defaultBlockState());
|
||||
var destination=CompanionWorld.teleportDestination(p,3);
|
||||
h.assertTrue(destination!=null&&destination.z>p.getZ()+2,"An unseen free landing beyond the wall is found");
|
||||
var action=CompanionActions.prepare(p,CompanionType.ENDERMITE,-1);h.assertTrue(action.success(),"Endermite teleport accepts wall crossing");action.commit().run();
|
||||
h.assertTrue(p.position().distanceToSqr(destination)<.001&&h.getLevel().noCollision(p),"Teleport arrives in free space");
|
||||
h.getLevel().setBlockAndUpdate(p.blockPosition(),Blocks.STONE.defaultBlockState());h.assertTrue(!CompanionWorld.safe(p,p.position(),true),"A filled landing is refused");h.succeed();
|
||||
}
|
||||
@GameTest public void sonarDetectsLivingMobBehindWall(GameTestHelper h){
|
||||
var p=player(h);var cow=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);cow.setNoAi(true);cow.setPos(p.position().add(0,0,3));h.getLevel().addFreshEntity(cow);
|
||||
for(int x=-2;x<=2;x++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(p.blockPosition().offset(x,y,1),Blocks.STONE.defaultBlockState());
|
||||
try{h.assertTrue(!CompanionWorld.visible(p,cow)&&CompanionQueries.sonar(p,12,3,100).success(),"Sonar works behind solid walls without turning ordinary targeting into wall vision");}finally{cow.discard();}h.succeed();
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h){return Companion019GameTests.player(h,CompanionType.CAT);}
|
||||
private static TagValueOutput output(GameTestHelper h){return TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());}
|
||||
private static boolean posture(ServerPlayer p,int state){return LyingService.request(p,new LyingService.Request(state,ProgressEffects.get(p).revision()));}
|
||||
private static void updatePose(ServerPlayer p) throws Exception {
|
||||
var method=net.minecraft.world.entity.player.Player.class.getDeclaredMethod("updatePlayerPose");method.setAccessible(true);method.invoke(p);
|
||||
}
|
||||
private static ServerPlayer airborneDive(GameTestHelper h) throws Exception {
|
||||
var p=player(h);sprintJump(p);p.setPos(p.position().add(0,3,0));
|
||||
h.assertTrue(posture(p,3),"A valid sprint jump starts a dive in clear air");updatePose(p);return p;
|
||||
}
|
||||
private static void sprintJump(ServerPlayer p){
|
||||
var progress=ProgressRecord.INITIAL.buy("hunger",1);p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);
|
||||
p.getFoodData().setFoodLevel(8);p.setOnGround(true);p.setSprinting(true);DiveService.track(p);p.setYRot(0);p.setXRot(0);p.setOnGround(false);p.setDeltaMovement(0,.3,.3);
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.CompanionType;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.mixin.FamiliarVoiceAccess;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import java.util.List;
|
||||
import net.fabricmc.fabric.api.event.player.AttackEntityCallback;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.protocol.game.ClientboundSoundPacket;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.network.ServerCommonPacketListenerImpl;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Familiar029GameTests {
|
||||
@GameTest public void everyEggBroadcastsItsNativeHurtSound(GameTestHelper h) throws Exception {
|
||||
var p=player(h,CompanionType.CAT);var channel=channel(p);var players=h.getLevel().getServer().getPlayerList().getPlayers();players.add(p);
|
||||
try{
|
||||
for(var type:CompanionType.values()){
|
||||
Companion019GameTests.equip(p,type);var pet=FamiliarService.active(p);pet.setPos(p.position().add(0,0,1.5));
|
||||
var nativeMob=(Mob)SpawnEggItem.getType(pet.egg()).create(h.getLevel(),EntitySpawnReason.MOB_SUMMONED);nativeMob.setBaby(true);
|
||||
var access=(FamiliarVoiceAccess)nativeMob;var expected=access.sanctuary$hurtSound(p.damageSources().playerAttack(p));
|
||||
long before=entities(h.getLevel());channel.outboundMessages().clear();pet.tap(p);var sounds=sounds(channel);
|
||||
h.assertTrue(expected==null?sounds.isEmpty():sounds.size()==1,"Exactly one native sound packet per voiced egg: "+type);
|
||||
if(expected!=null){var packet=sounds.getFirst();h.assertTrue(packet.getSound().value()==expected
|
||||
&&packet.getSource()==nativeMob.getSoundSource()&&packet.getVolume()==access.sanctuary$soundVolume()
|
||||
&&Float.isFinite(packet.getPitch())&&packet.getPitch()>0,"Species, sound category, volume and valid pitch: "+type);}
|
||||
h.assertTrue(entities(h.getLevel())==before&&pet.getHealth()==pet.getMaxHealth(),"Voice lookup never spawns another entity or hurts the familiar: "+type);
|
||||
}
|
||||
}finally{players.remove(p);}h.succeed();
|
||||
}
|
||||
@GameTest public void directionalImpulseMovesThePetAndCooldownPreventsSpam(GameTestHelper h) throws Exception {
|
||||
var p=player(h,CompanionType.CAT);var pet=FamiliarService.active(p);var sword=new ItemStack(Items.DIAMOND_SWORD);p.setItemInHand(InteractionHand.MAIN_HAND,sword);
|
||||
var channel=channel(p);var players=h.getLevel().getServer().getPlayerList().getPlayers();players.add(p);
|
||||
try{
|
||||
for(var direction:List.of(new Vec3(1,0,0),new Vec3(-1,0,0),new Vec3(0,0,1),new Vec3(0,0,-1))){
|
||||
pet.setPos(p.position().add(direction.scale(1.5)));pet.setDeltaMovement(Vec3.ZERO);pet.setOnGround(true);pet.hurtTime=0;
|
||||
var from=pet.position();channel.outboundMessages().clear();tap(p,pet);
|
||||
var impulse=pet.getDeltaMovement();h.assertTrue(impulse.dot(direction)>.39&&impulse.y>.39&&pet.syncVelocity,"Hit pushes away from the attacker, with a native grounded lift and network synchronization");
|
||||
tap(p,pet);h.assertTrue(pet.getDeltaMovement().equals(impulse)&&sounds(channel).size()==1,"Immediate repeated hits neither stack velocity nor repeat the cry");
|
||||
tick(pet);h.assertTrue(pet.position().subtract(from).dot(direction)>0,"Native ticking actually moves the pet away");
|
||||
h.assertTrue(pet.hurtTime>0&&pet.getHealth()==pet.getMaxHealth()&&pet.getTarget()==null&&sword.getDamageValue()==0,"Hurt animation has no health, aggro or weapon cost");
|
||||
}
|
||||
h.assertTrue(sounds(channel).getFirst().getPitch()>=1.3F&&sounds(channel).getFirst().getPitch()<=1.7F,"Baby cat uses its native higher voice");
|
||||
}finally{players.remove(p);}h.succeed();
|
||||
}
|
||||
@GameTest public void flyingAndAquaticFollowCannotEraseTheImpulse(GameTestHelper h){
|
||||
var p=player(h,CompanionType.GHAST);
|
||||
for(var type:List.of(CompanionType.GHAST,CompanionType.WITHER,CompanionType.ENDER_DRAGON,CompanionType.COD)){
|
||||
Companion019GameTests.equip(p,type);var pet=FamiliarService.active(p);
|
||||
pet.setPos(p.position().add(0,type==CompanionType.COD?0:2,1.5));pet.setOnGround(type==CompanionType.COD);pet.setDeltaMovement(Vec3.ZERO);pet.tickCount=23;
|
||||
var from=pet.position();pet.tap(p);tick(pet);
|
||||
h.assertTrue(pet.getZ()>from.z&&pet.getHealth()==pet.getMaxHealth(),"Flight following or a dry fish flop cannot cancel the initial recoil: "+type);
|
||||
}h.succeed();
|
||||
}
|
||||
@GameTest public void guardianUsesWaterVariantAndSilentPetsStaySilent(GameTestHelper h) throws Exception {
|
||||
var p=player(h,CompanionType.GUARDIAN);var pet=FamiliarService.active(p);pet.setPos(p.position().add(0,0,1.5));
|
||||
var players=h.getLevel().getServer().getPlayerList().getPlayers();var channel=channel(p);players.add(p);
|
||||
try{
|
||||
channel.outboundMessages().clear();pet.tap(p);h.assertTrue(sounds(channel).getFirst().getSound().value()==SoundEvents.GUARDIAN_HURT_LAND,"Dry guardian cries on land");
|
||||
var at=pet.blockPosition();for(int y=0;y<3;y++)h.getLevel().setBlockAndUpdate(at.above(y),Blocks.WATER.defaultBlockState());pet.baseTick();
|
||||
h.assertTrue(pet.isInWater(),"Native water fixture");pet.hurtTime=0;channel.outboundMessages().clear();pet.tap(p);
|
||||
h.assertTrue(sounds(channel).getFirst().getSound().value()==SoundEvents.GUARDIAN_HURT,"The same cached voice switches to its underwater variant");
|
||||
pet.hurtTime=0;pet.setSilent(true);channel.outboundMessages().clear();pet.tap(p);
|
||||
h.assertTrue(sounds(channel).isEmpty()&&pet.hurtTime>0,"Native Silent suppresses audio without removing visual feedback");
|
||||
}finally{players.remove(p);}h.succeed();
|
||||
}
|
||||
@GameTest public void carriedPairAndInvulnerabilityStillApply(GameTestHelper h){
|
||||
var p=player(h,CompanionType.CAT);var pet=FamiliarService.active(p);pet.setPos(p.position().add(0,0,1.5));pet.setDeltaMovement(Vec3.ZERO);
|
||||
try{
|
||||
h.assertTrue(CarryService.interact(p,pet),"Carrier fixture");tap(p,pet);pet.tap(p);
|
||||
h.assertTrue(pet.hurtTime==0&&pet.getDeltaMovement().equals(Vec3.ZERO),"A carried familiar cannot be struck by its own stack");
|
||||
CarryService.release(pet,true);float hp=pet.getHealth();
|
||||
h.assertTrue(!pet.hurtServer(h.getLevel(),pet.damageSources().playerAttack(p),100)&&pet.getHealth()==hp,"Feedback never enables real damage");
|
||||
}finally{CarryService.detach(p);}h.succeed();
|
||||
}
|
||||
@GameTest public void recoilRespectsWallsThenNormalFollowingResumes(GameTestHelper h){
|
||||
var p=player(h,CompanionType.CAT);var pet=FamiliarService.active(p);pet.setPos(p.position().add(0,0,1.5));pet.setDeltaMovement(Vec3.ZERO);pet.setOnGround(true);
|
||||
var wall=p.blockPosition().offset(0,0,3);
|
||||
for(int x=-1;x<=1;x++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(wall.offset(x,y,0),Blocks.STONE.defaultBlockState());
|
||||
pet.tap(p);for(int i=0;i<12;i++)tick(pet);
|
||||
h.assertTrue(pet.getBoundingBox().maxZ<=wall.getZ()+.001&&h.getLevel().noBlockCollision(pet,pet.getBoundingBox().deflate(.02)),"Recoil follows physical movement and cannot push the pet through a solid wall");
|
||||
pet.setPos(p.position().add(4,0,0));pet.setDeltaMovement(Vec3.ZERO);double distance=pet.distanceToSqr(p);
|
||||
for(int i=0;i<24;i++)tick(pet);
|
||||
h.assertTrue(pet.distanceToSqr(p)<distance&&pet.getHealth()==pet.getMaxHealth(),"Normal following resumes after the short recoil pause: before="+distance+", after="+pet.distanceToSqr(p)+", velocity="+pet.getDeltaMovement()+", speed="+pet.getSpeed()+", ground="+pet.onGround()+", pathDone="+pet.getNavigation().isDone()+", tick="+pet.tickCount);h.succeed();
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h,CompanionType type){return Companion019GameTests.player(h,type);}
|
||||
private static void tap(ServerPlayer p,FamiliarEntity pet){AttackEntityCallback.EVENT.invoker().interact(p,p.level(),InteractionHand.MAIN_HAND,pet,null);}
|
||||
private static EmbeddedChannel channel(ServerPlayer p) throws Exception {
|
||||
var field=ServerCommonPacketListenerImpl.class.getDeclaredField("connection");field.setAccessible(true);var connection=(Connection)field.get(p.connection);
|
||||
var channel=Connection.class.getDeclaredField("channel");channel.setAccessible(true);return (EmbeddedChannel)channel.get(connection);
|
||||
}
|
||||
private static List<ClientboundSoundPacket> sounds(EmbeddedChannel channel){channel.runPendingTasks();return channel.outboundMessages().stream().filter(ClientboundSoundPacket.class::isInstance).map(ClientboundSoundPacket.class::cast).toList();}
|
||||
private static long entities(net.minecraft.server.level.ServerLevel level){long count=0;for(var e:level.getAllEntities())count++;return count;}
|
||||
private static void tick(FamiliarEntity pet){pet.commonTick();pet.tick();}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.food.*;
|
||||
import fr.koka.sanctuary.grave.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.cycle.*;
|
||||
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.server.level.ServerPlayer;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.stats.Stats;
|
||||
import java.util.*;
|
||||
|
||||
public final class FoodKnowledge039GameTests {
|
||||
private static ServerPlayer player(GameTestHelper h){var p=GravesFood038GameTests.player(h,1);p.setUUID(UUID.randomUUID());return p;}
|
||||
private static void eat(ServerPlayer p,Item item){var stack=new ItemStack(item,2);stack.get(DataComponents.CONSUMABLE).onConsume(p.level(),p,stack);}
|
||||
@GameTest public void actualConsumptionLearnsOnlyTheEatenFood(GameTestHelper h){
|
||||
var p=player(h);var apple=new ItemStack(Items.APPLE);p.getInventory().setItem(0,apple);p.getFoodData().setFoodLevel(0);
|
||||
h.assertTrue(!FoodKnowledgeService.knows(p,apple),"Holding is not tasting");p.startUsingItem(InteractionHand.MAIN_HAND);p.stopUsingItem();
|
||||
h.assertTrue(!FoodKnowledgeService.knows(p,apple),"Cancelled eating is not tasting");p.awardStat(Stats.ITEM_USED.get(Items.CARROT));
|
||||
h.assertTrue(!FoodKnowledgeService.knows(p,new ItemStack(Items.CARROT)),"Planting/use counters cannot reveal nutrition");eat(p,Items.APPLE);
|
||||
h.assertTrue(FoodKnowledgeService.knows(p,apple),"Native completed food listener learns apple");eat(p,Items.BEEF);
|
||||
h.assertTrue(!FoodKnowledgeService.knows(p,new ItemStack(Items.COOKED_BEEF)),"Raw and cooked foods are distinct");
|
||||
h.assertTrue(!FoodKnowledgeService.knows(player(h),apple),"Knowledge belongs to the eater");h.succeed();
|
||||
}
|
||||
@GameTest public void studyCostsExactlyFourAndCannotBeRepurchased(GameTestHelper h){
|
||||
var p=player(h);p.setExperienceLevels(3);h.assertTrue(!FoodKnowledgeService.buy(p)&&p.experienceLevel==3,"Insufficient XP retained");p.setExperienceLevels(9);
|
||||
h.assertTrue(FoodKnowledgeService.buy(p)&&p.experienceLevel==5,"Buy four levels");var raw=p.getAttached(FoodKnowledgeService.RECORD);
|
||||
h.assertTrue(!FoodKnowledgeService.buy(p)&&p.experienceLevel==5&&raw.equals(p.getAttached(FoodKnowledgeService.RECORD)),"Duplicate buy neither charges nor changes receipt");
|
||||
h.assertTrue(FoodKnowledgeService.knows(p,new ItemStack(Items.GOLDEN_CARROT)),"Study reveals untasted food");h.succeed();
|
||||
}
|
||||
@GameTest public void knowledgeAndReceiptSurviveSaveDeathAndPrestige(GameTestHelper h)throws Exception{
|
||||
var p=player(h);eat(p,Items.APPLE);p.setExperienceLevels(12);h.assertTrue(FoodKnowledgeService.buy(p),"Study purchased");
|
||||
var raw=p.getAttached(FoodKnowledgeService.RECORD);var out=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(out);
|
||||
var loaded=player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),out.buildResult()));loaded.setUUID(p.getUUID());
|
||||
h.assertTrue(raw.equals(loaded.getAttached(FoodKnowledgeService.RECORD))&&loaded.experienceLevel==8,"Knowledge and payment save together");
|
||||
var respawn=player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(loaded,false);ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(loaded,respawn,false);
|
||||
h.assertTrue(raw.equals(respawn.getAttached(FoodKnowledgeService.RECORD)),"Death preserves attachment");
|
||||
var session=ProgressionService.session(p.level().getServer());session.inhabitants.create(p.getUUID(),"food"+p.getUUID().toString().substring(0,8),"",session.inhabitants.available().getFirst());
|
||||
var r=ProgressRecord.INITIAL;for(var skill:ProgressRecord.SKILLS)while(r.rank(skill)<ProgressRecord.cap(skill))r=r.buy(skill,1);p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);
|
||||
h.assertTrue(CycleService.perform(p,new CyclePayloads.Action("ngp","","",session.cycles.revision(),r.revision())).equals("done"),"Real prestige accepted");
|
||||
h.assertTrue(raw.equals(p.getAttached(FoodKnowledgeService.RECORD))&&FoodKnowledgeService.knows(p,new ItemStack(Items.APPLE)),"Prestige preserves knowledge and receipt");h.succeed();
|
||||
}
|
||||
@GameTest public void oldBalancedDietProofSeedsButCorruptRecordsStayUntouched(GameTestHelper h){
|
||||
var p=player(h);var advancement=p.level().getServer().getAdvancements().get(Identifier.withDefaultNamespace("husbandry/balanced_diet"));p.getAdvancements().award(advancement,"apple");
|
||||
h.assertTrue(FoodKnowledgeService.knows(p,new ItemStack(Items.APPLE))&&!FoodKnowledgeService.knows(p,new ItemStack(Items.CARROT)),"Only confirmed legacy eating is imported");
|
||||
p.setAttached(FoodKnowledgeService.RECORD,"{broken");p.setExperienceLevels(12);eat(p,Items.CARROT);
|
||||
h.assertTrue(!FoodKnowledgeService.buy(p)&&p.experienceLevel==12&&p.getAttached(FoodKnowledgeService.RECORD).equals("{broken"),"Invalid data stays untouched without charging");h.succeed();
|
||||
}
|
||||
@GameTest public void gravesUseDeathOrdinalAndPreserveOwnerProfile(GameTestHelper h){
|
||||
var p=player(h);p.awardStat(Stats.DEATHS,13);var head=GraveService.head(p,List.of(new ItemStack(Items.DIAMOND)));
|
||||
var marker=head.get(DataComponents.CUSTOM_DATA).copyTag().getCompound(GraveService.MARKER).orElseThrow();
|
||||
h.assertTrue(marker.getIntOr("death_number",0)==14,"Next death uses native lifetime counter");
|
||||
var name=(net.minecraft.network.chat.contents.TranslatableContents)head.get(DataComponents.CUSTOM_NAME).getContents();
|
||||
h.assertTrue(name.getKey().equals("sanctuary.grave.numbered_name")&&name.getArgs()[1].equals("14"),"Name contains only player and death ordinal");
|
||||
h.assertTrue(head.get(DataComponents.PROFILE).partialProfile().id().equals(p.getGameProfile().id()),"Cosmetic retains real profile");
|
||||
var empty=head.copy();empty.set(DataComponents.CONTAINER,net.minecraft.world.item.component.ItemContainerContents.EMPTY);
|
||||
h.assertTrue(empty.get(DataComponents.CUSTOM_NAME).equals(head.get(DataComponents.CUSTOM_NAME)),"Empty head keeps death number");h.succeed();
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.grave.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.cosmetics.AccessoryService;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.*;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.SkullBlockEntity;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import java.util.*;
|
||||
|
||||
public final class GraveInventory039GameTests {
|
||||
@GameTest public void nativeDeathKeepsPhysicalCellsGearAndSize(GameTestHelper h){
|
||||
var p=GravesFood038GameTests.player(h,6);HotbarRows.select(p,2);
|
||||
p.getInventory().setItem(0,new ItemStack(Items.DIAMOND,3));p.getInventory().setItem(11,new ItemStack(Items.EMERALD,5));SanctuaryOverflowAccess.of(p).setItem(17,new ItemStack(Items.APPLE,2));
|
||||
p.setItemSlot(EquipmentSlot.HEAD,new ItemStack(Items.IRON_HELMET));p.setItemSlot(EquipmentSlot.OFFHAND,new ItemStack(Items.SHIELD));AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,new ItemStack(Items.GLASS));AccessoryAccess.of(p).setItem(AccessoryContainer.CAPE,new ItemStack(AccessoryService.ZERO_CAPE));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,new ItemStack(Items.CHICKEN_SPAWN_EGG));
|
||||
p.inventoryMenu.setCarried(new ItemStack(Items.GOLD_INGOT,4));p.inventoryMenu.getSlot(1).set(new ItemStack(Items.OAK_LOG,2));
|
||||
var level=h.getLevel();var at=h.absolutePos(new BlockPos(1,2,1));level.setBlockAndUpdate(at.below(),Blocks.STONE.defaultBlockState());p.setPos(at.getX()+.5,at.getY(),at.getZ()+.5);
|
||||
boolean keep=level.getGameRules().get(GameRules.KEEP_INVENTORY);level.getGameRules().set(GameRules.KEEP_INVENTORY,false,level.getServer());
|
||||
try{
|
||||
p.die(p.damageSources().genericKill());var skull=(SkullBlockEntity)level.getBlockEntity(at);var head=GraveService.head(skull);var info=GraveLayout.read(head);var source=GraveService.container(skull);
|
||||
h.assertTrue(info.rows()==6&&info.visibleRows()==6&&info.hotbar()==2,"Snapshot retains six rows and selected physical hotbar");
|
||||
h.assertTrue(source.getItem(18).is(Items.DIAMOND)&&source.getItem(18).getCount()==3&&source.getItem(11).is(Items.EMERALD)&&source.getItem(53).is(Items.APPLE),"Storage stays in physical cells despite shifted hotbar");
|
||||
h.assertTrue(source.getItem(54).is(Items.IRON_HELMET)&&source.getItem(58).is(Items.GLASS)&&source.getItem(59).is(AccessoryService.ZERO_CAPE)&&source.getItem(60).is(Items.CHICKEN_SPAWN_EGG)&&source.getItem(61).is(Items.SHIELD),"Armour and all accessories keep their dedicated slots");
|
||||
h.assertTrue(source.getItem(62).is(Items.OAK_LOG)&&source.getItem(66).is(Items.GOLD_INGOT),"Crafting and cursor remain separately recoverable");
|
||||
source.removeItem(18,1);h.assertTrue(source.getItem(18).getCount()==2&&source.getItem(0).isEmpty()&&source.getItem(11).is(Items.EMERALD),"Partial withdrawal never compacts cells");
|
||||
var broken=Block.getDrops(skull.getBlockState(),level,at,skull).getFirst();h.assertTrue(GraveLayout.read(broken).equals(info),"Breaking preserves the recorded layout");
|
||||
}finally{level.getGameRules().set(GameRules.KEEP_INVENTORY,keep,level.getServer());}h.succeed();
|
||||
}
|
||||
@GameTest public void withdrawalOnlyAndEquipmentRoutingWithTwoLooters(GameTestHelper h){
|
||||
var owner=GravesFood038GameTests.player(h,1);var a=GravesFood038GameTests.player(h,1);var b=GravesFood038GameTests.player(h,1);
|
||||
var items=new ArrayList<ItemStack>(Collections.nCopies(108,ItemStack.EMPTY));items.set(18,new ItemStack(Items.DIAMOND,12));items.set(54,new ItemStack(Items.IRON_HELMET));items.set(58,new ItemStack(Items.GLASS));items.set(59,new ItemStack(AccessoryService.ZERO_CAPE));items.set(60,new ItemStack(Items.CHICKEN_SPAWN_EGG));
|
||||
var source=new GraveContainer(ItemContainerContents.fromItems(items),()->true,s->{});var info=new GraveLayout.Info(6,6,2);
|
||||
var ma=new GraveInventoryMenu(10,a.getInventory(),source,ItemStack.EMPTY,()->true,info);var mb=new GraveInventoryMenu(11,b.getInventory(),source,ItemStack.EMPTY,()->true,info);
|
||||
((SanctuaryExtendedInventoryMenu)(Object)ma).sanctuary$ensureExtendedInventory();((SanctuaryExtendedInventoryMenu)(Object)mb).sanctuary$ensureExtendedInventory();
|
||||
for(int i=0;i<108;i++)h.assertTrue(!ma.getSlot(i).mayPlace(new ItemStack(Items.STONE)),"Every death cell rejects deposits");
|
||||
ma.setCarried(new ItemStack(Items.DIRT,3));ma.clicked(0,0,ContainerInput.PICKUP,a);h.assertTrue(source.getItem(0).isEmpty()&&ma.getCarried().getCount()==3,"Real click cannot insert into an empty death cell");ma.setCarried(ItemStack.EMPTY);
|
||||
ma.quickMoveStack(a,18);mb.quickMoveStack(b,18);h.assertTrue(a.getInventory().contains(new ItemStack(Items.DIAMOND))&&b.getInventory().isEmpty(),"Shared withdrawal cannot duplicate");
|
||||
ma.quickMoveStack(a,54);ma.quickMoveStack(a,59);ma.quickMoveStack(a,60);ma.quickMoveStack(a,58);
|
||||
h.assertTrue(a.getItemBySlot(EquipmentSlot.HEAD).is(Items.IRON_HELMET)&&AccessoryAccess.of(a).getItem(AccessoryContainer.CAPE).is(AccessoryService.ZERO_CAPE)&&AccessoryAccess.of(a).getItem(AccessoryContainer.FAMILIAR).is(Items.CHICKEN_SPAWN_EGG),"Shift recovery equips helmet, cape and egg");
|
||||
h.assertTrue(AccessoryAccess.of(a).getItem(AccessoryContainer.HEAD).isEmpty()&&a.getInventory().contains(new ItemStack(Items.GLASS)),"Cosmetic is stored, never automatically equipped");h.succeed();
|
||||
}
|
||||
@GameTest public void legacyLayoutIsNotInventedAndPrestigeOverflowStaysVisible(GameTestHelper h){
|
||||
var p=GravesFood038GameTests.player(h,1);SanctuaryOverflowAccess.of(p).setItem(17,new ItemStack(Items.EMERALD));var layout=GraveLayout.capture(p);
|
||||
h.assertTrue(layout.info.rows()==1&&layout.info.visibleRows()==6,"Locked occupied rows remain visible after prestige");
|
||||
h.assertTrue(GraveLayout.read(GraveService.head(p,List.of(new ItemStack(Items.DIAMOND))))==null,"Old compact heads remain legacy recovery chests");h.succeed();
|
||||
}
|
||||
@GameTest public void currentInventoryCraftingAndCloseRemainNative(GameTestHelper h){
|
||||
var p=GravesFood038GameTests.player(h,1);var source=new GraveContainer(ItemContainerContents.EMPTY,()->true,s->{});
|
||||
var menu=new GraveInventoryMenu(12,p.getInventory(),source,ItemStack.EMPTY,()->true,new GraveLayout.Info(1,1,0));((SanctuaryExtendedInventoryMenu)(Object)menu).sanctuary$ensureExtendedInventory();p.containerMenu=menu;
|
||||
menu.getSlot(GraveInventoryMenu.CRAFT+1).setByPlayer(new ItemStack(Items.OAK_LOG,2));
|
||||
h.assertTrue(menu.getSlot(GraveInventoryMenu.CRAFT).getItem().is(Items.OAK_PLANKS),"Current player uses the native crafting grid and recipes");
|
||||
menu.clicked(GraveInventoryMenu.CRAFT,0,ContainerInput.QUICK_MOVE,p);
|
||||
int planks=0;for(int i=0;i<9;i++)if(p.getInventory().getItem(i).is(Items.OAK_PLANKS))planks+=p.getInventory().getItem(i).getCount();
|
||||
h.assertTrue(planks==8&&source.isEmpty(),"Shift crafting stays in current inventory without depositing into the grave");
|
||||
h.assertTrue(menu.getSlot(GraveInventoryMenu.CRAFT).isFake(),"Native crafting output stays excluded from inventory drag transfers");
|
||||
var crafted=net.minecraft.stats.Stats.ITEM_CRAFTED.get(Items.OAK_PLANKS);int before=p.getStats().getValue(crafted);
|
||||
menu.getSlot(GraveInventoryMenu.CRAFT+1).setByPlayer(new ItemStack(Items.OAK_LOG));menu.clicked(GraveInventoryMenu.CRAFT,0,ContainerInput.PICKUP,p);
|
||||
h.assertTrue(menu.getCarried().is(Items.OAK_PLANKS)&&menu.getCarried().getCount()==4&&p.getStats().getValue(crafted)==before+4,"Ordinary pickup retains native crafted-item statistics");
|
||||
p.getInventory().add(menu.getCarried());menu.setCarried(ItemStack.EMPTY);
|
||||
menu.getSlot(GraveInventoryMenu.CRAFT+1).setByPlayer(new ItemStack(Items.OAK_LOG));menu.clicked(GraveInventoryMenu.CRAFT,4,ContainerInput.SWAP,p);
|
||||
h.assertTrue(p.getInventory().getItem(4).is(Items.OAK_PLANKS)&&p.getStats().getValue(crafted)==before+8,"Number-key craft retains native result callbacks");
|
||||
menu.getSlot(GraveInventoryMenu.CRAFT+1).setByPlayer(new ItemStack(Items.DIAMOND,3));menu.removed(p);
|
||||
h.assertTrue(p.inventoryMenu.getSlot(1).getItem().isEmpty()&&p.getInventory().contains(new ItemStack(Items.DIAMOND)),"Closing returns unfinished crafting inputs");h.succeed();
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user