Record completed beta.061 release and finish Git synchronization
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-15 09:31:32 +02:00
parent e13f3cc1b8
commit c8b0684460
28 changed files with 502 additions and 32 deletions
+3
View File
@@ -114,6 +114,9 @@ tasks.named('processGametestResources') {
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 arrivalMount061Client = providers.gradleProperty('sanctuaryArrivalMount061ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('arrivalMount061Client', arrivalMount061Client)
if (arrivalMount061Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ArrivalMount061ClientChecks') }
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') }
@@ -0,0 +1,115 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.cosmetics.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.inventory.*;
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.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.resources.sounds.SoundInstance;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.*;
import net.minecraft.world.*;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.item.*;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.*;
/** Real audio stream continuity and native player input across integrated-server mount physics. */
public final class ArrivalMount061ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){
var watching=new AtomicBoolean(true);var started=new AtomicBoolean();var submitted=new AtomicBoolean();
var sound=new AtomicReference<SoundInstance>();var white=new AtomicBoolean();var worldSeen=new AtomicBoolean();
c.runOnClient(m->{m.options.getSoundSourceOptionInstance(SoundSource.MASTER).set(1.0);m.options.getSoundSourceOptionInstance(SoundSource.MUSIC).set(.3);});
ClientTickEvents.END_CLIENT_TICK.register(m->{
if(!watching.get())return;
if(m.gui.screen() instanceof HelloWorldScreen screen&&submitted.compareAndSet(false,true)){
IntroClient.setQuickTestIntro(false);Starter039TestSupport.selectFirst(screen);
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
}
if(m.gui.screen() instanceof IntroScreen intro){
if(intro.waitingForMusic())check(music(m)==null,"No Minecraft music while letters reveal");
else if(!intro.isFinished()&&started.compareAndSet(false,true)){
sound.set(music(m));check(sound.get()!=null,"Portal reveal starts native music");
}
}
if(started.get()){
check(sound.get()==music(m),"The same music instance survives intro and world handoff");
check(m.getSoundManager().isActive(sound.get()),"Music channel remains active through handoff");
if(IntroSpawnFlash.active())white.set(true);
if(m.level!=null&&m.player!=null)worldSeen.set(true);
}
});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("ArrivalMount061 "+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&&m.gui.screen()==null&&ProgressionClient.active()&&!IntroSpawnFlash.active(),1600);
c.waitTicks(40);watching.set(false);check(started.get()&&white.get()&&worldSeen.get(),"Actual cinematic, white flash and world were all exercised");
System.out.println("ARRIVAL061_MUSIC_PASS: same active streamed SoundInstance before, during and after first world");
var server=world.getServer();
server.runOnServer(s->{var p=player(s);p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);
p.teleportTo(0.5,1,0.5);var origin=p.blockPosition();for(int x=-12;x<=12;x++)for(int z=-12;z<=12;z++)for(int y=-1;y<=10;y++)p.level().setBlockAndUpdate(origin.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
double base=p.getAttributeValue(Attributes.MOVEMENT_SPEED);var pet=FamiliarService.TYPE.create(p.level(),EntitySpawnReason.COMMAND);
for(var item:List.of(Items.COW_SPAWN_EGG,Items.ENDER_DRAGON_SPAWN_EGG))for(int size:new int[]{120,350,650,849,1000,1500,2499}){
pet.bind(p,egg(item,size));pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Carry familiar size "+size);
double expected=size<=350?1:size==1000?.75:FamiliarSize.carrySpeed(pet.egg());check(Math.abs(p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base-expected)<.00001,"Size changes real movement attribute even on baby models");
CarryService.release(pet,true);check(Math.abs(p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base-1)<.00001,"Release restores unladen speed");
}pet.discard();
var baby=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);baby.setBaby(true);baby.setPos(p.position().add(0,0,2));check(CarryService.interact(p,baby),"Native baby still carries");check(Math.abs(p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base-1)<.00001,"Native baby remains weightless");CarryService.release(baby,true);baby.discard();
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(Items.COW_SPAWN_EGG,3500));p.inventoryMenu.broadcastChanges();
});
c.waitTicks(40);
int id=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Real equipped egg spawns colossal familiar");pet.setPos(p.position().add(0,0,2));pet.setNoAi(true);return pet.getId();});
c.waitFor(m->m.level.getEntity(id)!=null&&m.player.getMainHandItem().isEmpty(),200);
c.getInput().holdKey(o->o.keyShift);c.waitTicks(5);click(c,id);c.waitTicks(8);
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(p.getVehicle()==pet&&!CarryService.carried(pet),"Actual sneak interaction mounts, never carries, a colossal");check(pet.getBbHeight()>.7&&pet.getControllingPassenger()==null,"Mount uses rendered scale and server authority");pet.setNoAi(false);});
c.getInput().releaseKey(o->o.keyShift);c.waitTicks(5);c.getInput().lookAt(0,0);
var before=server.computeOnServer(s->FamiliarService.active(player(s)).position());
c.getInput().holdKey(o->o.keyUp);c.waitTicks(25);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(5);
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(p.getVehicle()==pet,"Mount survives released sneak");check(pet.getZ()>before.z+1,"Forward input moves the actual server mount");check(Math.abs(pet.getX()-before.x)<1,"Movement follows rider yaw");});
c.runOnClient(m->{m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK);});c.waitTicks(3);c.takeScreenshot("sanctuary-beta061-colossal-mount");
c.getInput().holdKey(o->o.keyShift);c.waitTicks(8);c.getInput().releaseKey(o->o.keyShift);
server.runOnServer(s->{var p=player(s);check(!p.isPassenger(),"Release then sneak dismounts normally");var pet=FamiliarService.active(p);pet.setNoAi(true);pet.setPos(p.position().add(0,0,2));p.setShiftKeyDown(false);check(CarryService.interact(p,pet),"Remount: actor="+p.position()+" pet="+pet.position()+" asleep="+p.isSleeping()+" lying="+fr.koka.sanctuary.progression.LyingService.active(p)+" ground="+p.onGround()+" removed="+pet.isRemoved()+" alive="+pet.isAlive()+" rider="+pet.getFirstPassenger()+" line="+p.hasLineOfSight(pet));
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);check(!p.isPassenger()&&pet.isRemoved(),"Removing egg safely detaches player and despawns mount");
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(Items.ALLAY_SPAWN_EGG,6000));});
c.waitTicks(30);
server.runOnServer(s->{var p=player(s);p.setShiftKeyDown(false);var pet=FamiliarService.active(p);check(pet!=null,"Flying mount spawned");pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Colossal allay is rideable");});c.waitTicks(5);
double ground=server.computeOnServer(s->FamiliarService.active(player(s)).getY());c.getInput().holdKey(o->o.keyJump);c.waitTicks(20);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(8);
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()>ground+2,"Space lifts native flying familiar");check(p.getVehicle()==pet,"Flying pilot stays attached");});
double peak=server.computeOnServer(s->FamiliarService.active(player(s)).getY());
c.getInput().lookAt(0,65);c.getInput().holdKey(o->o.keyUp);c.waitTicks(15);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(5);
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()<peak-1,"Looking down and moving descends");});
int ceiling=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);int y=(int)Math.ceil(p.getBoundingBox().maxY)+1;var at=pet.blockPosition();for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(at.getX()+x,y,at.getZ()+z),Blocks.STONE.defaultBlockState());return y;});
c.getInput().holdKey(o->o.keyJump);c.waitTicks(40);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(5);
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(p.getVehicle()==pet,"Ceiling stops mount without ejecting its rider");check(p.getBoundingBox().maxY<=ceiling+.03&&p.level().noBlockCollision(p,p.getBoundingBox().deflate(.02)),"Rider cannot be driven through a low ceiling");
pet.ejectPassengers();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);p.teleportTo(.5,1,.5);
for(int x=4;x<=12;x++)for(int z=-1;z<=7;z++)for(int y=1;y<=8;y++)if(x==4||x==12||z==-1||z==7)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(x,y,z),Blocks.STONE.defaultBlockState());
for(int x=5;x<=11;x++)for(int z=0;z<=6;z++)for(int y=1;y<=7;y++)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(x,y,z),Blocks.WATER.defaultBlockState());
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(Items.COD_SPAWN_EGG,6000));});c.waitTicks(30);
var land=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Aquatic colossal spawns on dry land");pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Aquatic colossal is rideable");return pet.position();});
c.getInput().lookAt(0,0);c.getInput().holdKey(o->o.keyUp);c.waitTicks(15);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(3);
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(!pet.isInWater(),"Dry-land measurement is outside the contained pool");check(pet.position().distanceTo(land)<2,"Aquatic mount moves slowly on land: "+pet.position().distanceTo(land));pet.teleportTo(8.5,2,2.5);pet.setDeltaMovement(Vec3.ZERO);pet.positionRider(p);});c.waitTicks(10);
double wet=server.computeOnServer(s->{var pet=FamiliarService.active(player(s));check(pet.isInWater(),"Aquatic mount actually enters water");return pet.getY();});
c.getInput().holdKey(o->o.keyJump);c.waitTicks(15);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(3);
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()>wet+1,"Aquatic mount swims upward under native input");pet.ejectPassengers();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);});
c.runOnClient(m->{m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON);m.getSoundManager().stop();});c.waitTicks(3);
c.runOnClient(m->check(!m.getSoundManager().isActive(sound.get()),"Explicit sound stop remains effective outside arrival"));
}finally{watching.set(false);}
System.out.println("ARRIVAL_MOUNT061_PASS: audio continuity, size weights, native babies, real mount input, dismount, removal and flight");
}
private static SoundInstance music(net.minecraft.client.Minecraft m){try{var f=m.getMusicManager().getClass().getDeclaredField("currentMusic");f.setAccessible(true);return (SoundInstance)f.get(m.getMusicManager());}catch(ReflectiveOperationException e){throw new AssertionError(e);}}
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
private static ItemStack egg(Item item,int value){var stack=new ItemStack(item);var size=new CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);CustomData.update(DataComponents.CUSTOM_DATA,stack,t->t.put(FamiliarSize.KEY,size));return stack;}
private static KeyEvent enter(){return new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);}
private static void click(ClientGameTestContext c,int id){c.runOnClient(m->{var e=m.level.getEntity(id);m.gameMode.interact(m.player,e,new EntityHitResult(e,e.getEyePosition()),InteractionHand.MAIN_HAND);});}
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
}
@@ -49,7 +49,7 @@ public final class Carry027GameTests {
}finally{online.removeAll(added);cleanup(p);cow.discard();}h.succeed();
}
@GameTest public void familiarBabyAppearanceAddsNoWeightButMiniatureAdultsDo(GameTestHelper h){
@GameTest public void familiarWithoutSizeUsesOrdinaryWeightRegardlessOfBabyAppearance(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{
@@ -59,7 +59,7 @@ public final class Carry027GameTests {
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);
ratio(h,p[0],base,.5625);ratio(h,p[1],base,.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);
}
@@ -36,6 +36,9 @@ public final class CompanionTooltips {
if(xp>0)extra.add(fr.koka.sanctuary.familiar.FamiliarBattle.label("xp.tooltip",fr.koka.sanctuary.familiar.FamiliarLevels.level(xp),xp).copy().withStyle(ChatFormatting.GREEN));
if(fr.koka.sanctuary.cosmetics.FamiliarSize.value(stack)>0)extra.add(CompanionClient.label("tooltip_size",
CompanionClient.label("size."+fr.koka.sanctuary.cosmetics.FamiliarSize.category(stack))).copy().withStyle(ChatFormatting.GRAY));
if(fr.koka.sanctuary.cosmetics.FamiliarSize.value(stack)>0)extra.add(Component.translatable(
fr.koka.sanctuary.cosmetics.FamiliarSize.colossal(stack)?"sanctuary.familiar.mount.hint":"sanctuary.familiar.carry.hint",
Math.round(fr.koka.sanctuary.cosmetics.FamiliarSize.carrySpeed(stack)*100)).withStyle(ChatFormatting.GRAY));
var profile=fr.koka.sanctuary.familiar.FamiliarCatalog.bundled().forEgg(stack);
if(profile!=null){
if(FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals(profile.id()))profile=FamiliarClient.profile;
@@ -90,8 +90,7 @@ public final class FamiliarRenderer extends EntityRenderer<FamiliarEntity,Famili
}
state.renderer=renderer;state.appearance=appearance;
if(fr.koka.sanctuary.cosmetics.CarryService.carried(pet))CarryPose.apply(appearance);
state.size=Math.min(0.65F,Math.min(0.85F/Math.max(0.1F,mob.getBbHeight()),0.85F/Math.max(0.1F,mob.getBbWidth())));
state.size*=FamiliarSize.multiplier(pet.egg());
state.size=FamiliarSize.renderScale(mob,pet.egg());
float height=mob.getBbHeight()*state.size;
state.visualRadius=Math.max(mob.getBbWidth()*state.size,height)*.5F;
if(state.nameTagAttachment!=null)state.nameTagAttachment=new net.minecraft.world.phys.Vec3(0,height+.2,0);
@@ -6,13 +6,16 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworkin
public final class IntroClient {
private static boolean previewRequested;
private static boolean arrivalMusic;
static void keepArrivalMusic(){arrivalMusic=true;}
public static boolean consumeArrivalMusic(){boolean keep=arrivalMusic;arrivalMusic=false;return keep;}
private static boolean quickTestIntro;
public static void setQuickTestIntro(boolean quick) { quickTestIntro=quick; }
private IntroClient() { }
public static void register() {
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.INIT.register((handler,client) -> { quickTestIntro=false;IntroSpawnFlash.clear(); });
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;IntroSpawnFlash.clear(); });
net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;IntroSpawnFlash.clear(); });
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.INIT.register((handler,client) -> { quickTestIntro=false;arrivalMusic=false;IntroSpawnFlash.clear(); });
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;arrivalMusic=false;IntroSpawnFlash.clear(); });
net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;arrivalMusic=false;IntroSpawnFlash.clear(); });
net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (!previewRequested) return;
previewRequested=false;
@@ -29,7 +29,7 @@ public final class IntroScreen extends Screen {
private long started=-1;
private int nextBeat;
private net.minecraft.client.renderer.texture.TextureAtlasSprite portalSprite;
private boolean rumble, whisper, portal, finished, skipped;
private boolean rumble, whisper, portal, finished, skipped, musicStarted;
public IntroScreen(UUID uuid,java.util.function.Consumer<Boolean> complete) {
this(uuid,complete,System::nanoTime);
@@ -41,14 +41,18 @@ public final class IntroScreen extends Screen {
}
public UUID uuid() { return uuid; }
boolean isFinished() { return finished; }
public boolean waitingForMusic() { return !finished&&!musicStarted; }
@Override public net.minecraft.sounds.Music getBackgroundMusic() { return musicStarted?net.minecraft.sounds.Musics.GAME:null; }
protected void init() {
if (started < 0) started=clock.getAsLong();
if (started < 0) {started=clock.getAsLong();minecraft.getMusicManager().stopPlaying();}
}
private double seconds() { return started < 0 ? 0 : Math.max(0,(clock.getAsLong()-started)/1_000_000_000.0); }
public void tick() {
if (finished) return;
double time=seconds();
minecraft.getMusicManager().stopPlaying();
if(!musicStarted&&time>=IntroSequence.PORTAL_REVEAL){
musicStarted=true;minecraft.getMusicManager().startPlaying(net.minecraft.sounds.Musics.GAME);
}
if (!rumble && time>=0.7) {
rumble=true;
play(SoundEvents.AMBIENT_SOUL_SAND_VALLEY_LOOP.value(),0.32f,0.65f,true);
@@ -126,7 +130,7 @@ public final class IntroScreen extends Screen {
double opacity=IntroSequence.smooth(progress/0.06)*(1-IntroSequence.smooth((progress-0.93)/0.07));
glyph(graphics,i,x,y,unit*(float)(0.65+point.radius()*5.5),opacity,0xD1B9F1);
}
double aperture=IntroSequence.smooth((time-23.5)/5.5);
double aperture=IntroSequence.smooth((time-IntroSequence.PORTAL_REVEAL)/5.5);
// The destination moves toward the viewer until the doorway exceeds the entire viewport.
int extent=(int)(3*unit+Math.max(width,height)*0.68*Math.pow(aperture,2.2));
for(int glow=8;glow>=1;glow--) {
@@ -164,7 +168,9 @@ public final class IntroScreen extends Screen {
}
private void finish(boolean skipped) {
if (finished) return;
finished=true;this.skipped=skipped;stopSounds();complete.accept(skipped);
finished=true;this.skipped=skipped;stopSounds();
if(musicStarted&&minecraft.level==null)IntroClient.keepArrivalMusic();
complete.accept(skipped);
}
private void stopSounds() {
for (var sound:sounds) { sound.close();minecraft.getSoundManager().stop(sound); }
@@ -40,7 +40,9 @@ public final class CarryService {
public static boolean marked(Entity e){return e.getAttachedOrElse(CARRIED,false);}
public static boolean carried(Entity e){return marked(e)&&e.getVehicle() instanceof Player;}
public static boolean carrying(Entity e){return e instanceof Player&&e.getPassengers().stream().anyMatch(CarryService::carried);}
public static boolean pair(Entity a,Entity b){return a!=null&&b!=null&&a!=b&&(carried(a)||carried(b))&&root(a)==root(b);}
public static boolean pair(Entity a,Entity b){return a!=null&&b!=null&&a!=b&&(
(carried(a)||carried(b))&&root(a)==root(b)
||(a instanceof FamiliarEntity||b instanceof FamiliarEntity)&&a.getRootVehicle()==b.getRootVehicle());}
private static Entity root(Entity e){while(carried(e))e=e.getVehicle();return e;}
/** One seat per head; the chain includes babies even though they add no carrying penalty. */
private static Entity passenger(Entity e){
@@ -73,6 +75,7 @@ public final class CarryService {
for(var rider:new ArrayList<>(RIDES.keySet())){
var ride=RIDES.get(rider);if(ride==null||ride.carrier.level().getServer()!=s)continue;
var carrier=ride.carrier;
if(rider instanceof FamiliarEntity pet&&FamiliarSize.colossal(pet.egg())){release(rider,true);continue;}
if(!carried(rider)||rider.getVehicle()!=carrier||!rider.isAlive()||!active(carrier)||rider.level()!=carrier.level()
||!validCarrierChain(carrier)){
release(rider,true);continue;
@@ -102,6 +105,7 @@ public final class CarryService {
public static boolean interact(ServerPlayer actor,Entity target){
if(!active(actor)||target.level()!=actor.level()||!target.isAlive()||!actor.isWithinEntityInteractionRange(target,0)||!actor.hasLineOfSight(target))return false;
if(carried(target)&&target.getVehicle()==actor)return release(target,true);
if(target instanceof FamiliarEntity pet&&FamiliarSize.colossal(pet.egg()))return pet.mount(actor);
ServerPlayer carrier;Entity rider;
if(target instanceof ServerPlayer player){carrier=player;rider=actor;}
else if(animal(target)){carrier=actor;rider=target;}
@@ -213,9 +217,10 @@ public final class CarryService {
}
private static void speed(ServerPlayer p){
var attribute=p.getAttribute(Attributes.MOVEMENT_SPEED);if(attribute==null)return;
int players=0,animals=0;
int players=0;double animals=0;
for(Entity e=passenger(p);e!=null;e=passenger(e)){
if(e instanceof Player)players++;
else if(e instanceof FamiliarEntity pet)animals+=Math.log(FamiliarSize.carrySpeed(pet.egg()))/Math.log(1-SLOWDOWN);
else if(!baby(e))animals++;
}
var strength=p.getEffect(net.minecraft.world.effect.MobEffects.STRENGTH);
@@ -27,6 +27,7 @@ public final class FamiliarEntity extends PathfinderMob {
private int motionMode=-1;
private int recoilUntil;
private Mob voice;
private boolean mountShiftReleased;
public FamiliarEntity(EntityType<? extends PathfinderMob> type,Level level) {
super(type,level);setPermanentlyInvulnerable(true);setPersistenceRequired();setCanPickUpLoot(false);
getNavigation().setCanFloat(true);
@@ -41,7 +42,96 @@ public final class FamiliarEntity extends PathfinderMob {
public ServerPlayer owner() {
return owner;
}
public boolean mount(ServerPlayer player){
if(player!=owner()||!FamiliarSize.colossal(egg())||isPassenger()||isVehicle()
||player.isPassenger()||player.isVehicle()||player.isSleeping()||player.isFallFlying()
||fr.koka.sanctuary.progression.LyingService.active(player)
||fr.koka.sanctuary.familiar.FamiliarDuels.fighting(player))return false;
var at=position().add(0,mountHeight(),0).subtract(player.getVehicleAttachmentPoint(this));
if(!mountRoom(player,at)||!player.startRiding(this,true,true))return false;
mountShiftReleased=!player.isShiftKeyDown();getNavigation().stop();setTarget(null);setDeltaMovement(Vec3.ZERO);
positionRider(player);
player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetPassengersPacket(this));
player.sendOverlayMessage(net.minecraft.network.chat.Component.translatable("sanctuary.familiar.mount.controls"));return true;
}
public boolean mayDismount(){return level().isClientSide()||mountShiftReleased;}
private ServerPlayer pilot(){return getFirstPassenger() instanceof ServerPlayer p&&p==owner()?p:null;}
@Override protected boolean canAddPassenger(Entity rider){return !isVehicle()&&FamiliarSize.colossal(egg())&&rider==owner();}
// Like crew boats, native input packets drive the server simulation, never client vehicle positions.
@Override public LivingEntity getControllingPassenger(){return null;}
private double mountHeight(){return visualHeight()*.85;}
public float visualHeight(){return prepareVoice()?voice.getBbHeight()*FamiliarSize.renderScale(voice,egg()):getBbHeight();}
@Override protected Vec3 getPassengerAttachmentPoint(Entity rider,EntityDimensions dimensions,float scale){return new Vec3(0,mountHeight(),0);}
@Override protected EntityDimensions getDefaultDimensions(Pose pose){
// The colossal mount's collision shape follows the same miniature model as its renderer.
if(FamiliarSize.colossal(egg())&&prepareVoice()){
float scale=FamiliarSize.renderScale(voice,egg());
return EntityDimensions.scalable(Math.max(.25F,voice.getBbWidth()*scale),Math.max(.3F,voice.getBbHeight()*scale));
}
return super.getDefaultDimensions(pose);
}
@Override public void onSyncedDataUpdated(EntityDataAccessor<?> key){
super.onSyncedDataUpdated(key);if(EGG.equals(key))refreshDimensions();
}
private boolean mountRoom(LivingEntity rider,Vec3 at){
var box=rider.getDimensions(Pose.STANDING).makeBoundingBox(at).deflate(.01);
if(!level().getWorldBorder().isWithinBounds(box)||box.minY<level().getMinY()||box.maxY>level().getMaxY())return false;
for(int x=((int)Math.floor(box.minX))>>4;x<=((int)Math.floor(box.maxX))>>4;x++)
for(int z=((int)Math.floor(box.minZ))>>4;z<=((int)Math.floor(box.maxZ))>>4;z++)if(!level().hasChunk(x,z))return false;
return level().noBlockCollision(rider,box);
}
@Override public Vec3 getDismountLocationForPassenger(LivingEntity rider){
int edge=(int)Math.ceil(getBbWidth()/2+rider.getBbWidth()/2);
for(int radius=Math.max(1,edge);radius<=edge+3;radius++)for(int dy:new int[]{0,1,-1,2,-2})
for(int x=-radius;x<=radius;x++)for(int z=-radius;z<=radius;z++){
if(Math.max(Math.abs(x),Math.abs(z))!=radius)continue;
var at=position().add(x,dy+.05,z);var below=BlockPos.containing(at).below();
if(mountRoom(rider,at)&&level().getFluidState(BlockPos.containing(at)).isEmpty()
&&!level().getBlockState(below).getCollisionShape(level(),below).isEmpty())return at;
}
return rider.position();
}
@Override public void travel(Vec3 input){
var player=pilot();
if(player==null){super.travel(input);return;}
var keys=player.getLastClientInput();
setYRot(player.getYRot());setXRot(player.getXRot()*.5F);yBodyRot=getYRot();yHeadRot=getYRot();
double forward=(keys.forward()?1:0)-(keys.backward()?1:0),side=(keys.left()?1:0)-(keys.right()?1:0);
if(forward<0)forward*=.5;
boolean spatial=motion().flies()||motion().swims()&&isInWater();
if(spatial){
double pitch=Math.toRadians(player.getXRot());
input=new Vec3(side,keys.jump()?1:-Math.sin(pitch)*forward,Math.cos(pitch)*forward);
if(input.lengthSqr()>1)input=input.normalize();
// Bounded acceleration/inertia, native swept block collisions and a full stop at rest.
var desired=getInputVector(input,.32F,getYRot());
setDeltaMovement(getDeltaMovement().lerp(desired,.3));
move(MoverType.SELF,getDeltaMovement());setDeltaMovement(getDeltaMovement().scale(.9));resetFallDistance();player.resetFallDistance();
}else{
setSpeed(motion()==FamiliarMotion.AQUATIC?.05F:.28F);
if(onGround()&&(keys.jump()||motion().hops()&&tickCount%14==0&&(side!=0||forward!=0)))jumpFromGround();
input=new Vec3(side,0,forward);if(input.lengthSqr()>1)input=input.normalize();
super.travel(input);
}
}
/** A rider's head also collides; tall mounts cannot carry their player through a ceiling. */
@Override public void move(MoverType type,Vec3 delta){
var player=pilot();
if(player!=null&&!mountRoom(player,player.position().add(delta))){
double x=mountRoom(player,player.position().add(delta.x,0,0))?delta.x:0;
double y=mountRoom(player,player.position().add(x,delta.y,0))?delta.y:0;
double z=mountRoom(player,player.position().add(x,y,delta.z))?delta.z:0;
delta=new Vec3(x,y,z);setDeltaMovement(delta);
}
super.move(type,delta);
}
@Override protected void registerGoals() {
goalSelector.addGoal(-3,new Goal(){
{setFlags(EnumSet.of(Flag.MOVE,Flag.LOOK,Flag.JUMP));}
@Override public boolean canUse(){return isVehicle();}
@Override public boolean requiresUpdateEveryTick(){return true;}
@Override public void tick(){getNavigation().stop();setTarget(null);setSpeed(0);setXxa(0);setYya(0);setZza(0);}
});
goalSelector.addGoal(-2,new Goal(){
{setFlags(EnumSet.of(Flag.MOVE,Flag.JUMP));}
@Override public boolean canUse(){return tickCount<recoilUntil;}
@@ -81,6 +171,14 @@ public final class FamiliarEntity extends PathfinderMob {
}
var player=owner();
if(player==null || player.level()!=level || !player.isAlive() || player.isSpectator()) {discard();return;}
if(isVehicle()){
if(!FamiliarSize.colossal(egg())||pilot()==null||fr.koka.sanctuary.familiar.FamiliarDuels.fighting(player))ejectPassengers();
else {
if(!player.isShiftKeyDown())mountShiftReleased=true;getNavigation().stop();
// Manual techniques retain their cast cadence while movement is driven by the rider.
fr.koka.sanctuary.familiar.FamiliarTechniques.advance(this);return;
}
}
if(CarryService.carried(this)){getNavigation().stop();return;}
boolean obstructed=!level.noBlockCollision(this,getBoundingBox().deflate(0.02));
if(distanceToSqr(player)>(fr.koka.sanctuary.familiar.FamiliarBattle.fighting(player)?24*24:144) || getY()<level.getMinY() || obstructed) {
@@ -7,7 +7,7 @@ import net.minecraft.util.RandomSource;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomData;
/** A permanent visual trait of one equipped egg, using native item components. */
/** A permanent trait of one equipped egg, shared by its appearance, weight and mount. */
public final class FamiliarSize {
public static final String KEY="sanctuary:familiar_size";
private FamiliarSize(){}
@@ -31,6 +31,17 @@ public final class FamiliarSize {
int value=number.intValue();return value>=120&&value<=6000?value:0;
}
public static float multiplier(ItemStack egg){int value=value(egg);return value==0?1:value/1000F;}
public static boolean colossal(ItemStack egg){return value(egg)>=2500;}
public static double carrySpeed(ItemStack egg){
int size=value(egg);if(size==0)size=1000;
if(size<=350)return 1;
if(size<850)return 1-(size-350)*.1/500;
if(size<=1000)return .9-(size-850)*.15/150;
return Math.max(.25,.75-(size-1000)*.5/1500);
}
public static float renderScale(net.minecraft.world.entity.Mob model,ItemStack egg){
return Math.min(.65F,Math.min(.85F/Math.max(.1F,model.getBbHeight()),.85F/Math.max(.1F,model.getBbWidth())))*multiplier(egg);
}
public static String category(ItemStack egg){
int value=value(egg);
return value==0?"unknown":value<=350?"tiny":value<850?"small":value<=1200?"ordinary":value<2500?"large":"colossal";
@@ -6,6 +6,7 @@ import java.util.UUID;
public final class IntroSequence {
public static final int GLYPHS = 32;
public static final double SPIRAL = 22.2;
public static final double PORTAL_REVEAL = 23.5;
public static final double PORTAL = 26.8;
public static final double DURATION = 29.5;
private IntroSequence() { }
@@ -14,7 +14,8 @@ import org.spongepowered.asm.mixin.injection.callback.*;
public abstract class CarryEntityMixin {
@ModifyExpressionValue(method="startRiding(Lnet/minecraft/world/entity/Entity;ZZ)Z",at=@At(value="INVOKE",target="Lnet/minecraft/world/entity/EntityType;canSerialize()Z"))
private boolean sanctuary$playerCanCarry(boolean original,Entity vehicle,boolean force,boolean events){
return original||vehicle instanceof Player&&CarryService.marked((Entity)(Object)this);
return original||vehicle instanceof Player&&CarryService.marked((Entity)(Object)this)
||vehicle instanceof FamiliarEntity pet&&pet.owner()==(Object)this&&FamiliarSize.colossal(pet.egg());
}
@Inject(method="removeVehicle",at=@At("TAIL"))
private void sanctuary$finishCarry(CallbackInfo ci){
@@ -13,6 +13,7 @@ public abstract class CarryPlayerMixin {
private void sanctuary$releaseThenSneak(CallbackInfoReturnable<Boolean> ci){
var p=(Player)(Object)this;
if(CarryService.carried(p)&&!CarryService.mayDismount(p))ci.setReturnValue(false);
if(p.getVehicle() instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity pet&&!pet.mayDismount())ci.setReturnValue(false);
}
@Inject(method="attack",at=@At("HEAD"),cancellable=true)
private void sanctuary$noPassengerAttack(Entity target,CallbackInfo ci){if(CarryService.pair((Entity)(Object)this,target))ci.cancel();}
@@ -10,7 +10,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ServerPlayer.class)
public abstract class CarryServerPlayerMixin {
@Inject(method="saveParentVehicle",at=@At("HEAD"),cancellable=true)
private void sanctuary$omitTemporaryMount(ValueOutput output,CallbackInfo ci){if(CarryService.carried((ServerPlayer)(Object)this))ci.cancel();}
private void sanctuary$omitTemporaryMount(ValueOutput output,CallbackInfo ci){
var p=(ServerPlayer)(Object)this;
if(CarryService.carried(p)||p.getRootVehicle() instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity)ci.cancel();
}
@Inject(method="die",at=@At("HEAD"))
private void sanctuary$detachOnDeath(CallbackInfo ci){CarryService.detach((ServerPlayer)(Object)this);}
@Inject(method="teleport(Lnet/minecraft/world/level/portal/TeleportTransition;)Lnet/minecraft/server/level/ServerPlayer;",at=@At("HEAD"))
@@ -0,0 +1,17 @@
package fr.koka.sanctuary.mixin.client;
import fr.koka.sanctuary.client.IntroScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.sounds.MusicManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MusicManager.class)
abstract class IntroMusicManagerMixin {
@Inject(method="tick",at=@At("HEAD"),cancellable=true)
private void sanctuary$waitForPortal(CallbackInfo ci){
if(Minecraft.getInstance().gui.screen() instanceof IntroScreen intro&&intro.waitingForMusic())ci.cancel();
}
}
@@ -0,0 +1,26 @@
package fr.koka.sanctuary.mixin.client;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import fr.koka.sanctuary.client.IntroScreen;
import fr.koka.sanctuary.client.IntroSpawnFlash;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.sounds.SoundManager;
import net.minecraft.sounds.SoundSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/** Keep the same music stream when the apparition hands off to its first world. */
@Mixin(Minecraft.class)
abstract class IntroMusicMixin {
@WrapOperation(method="updateLevelInEngines(Lnet/minecraft/client/multiplayer/ClientLevel;Z)V",
at=@At(value="INVOKE",target="Lnet/minecraft/client/sounds/SoundManager;stop()V"))
private void sanctuary$continueArrivalMusic(SoundManager sounds,Operation<Void> original,ClientLevel next,boolean stopSounds) {
var client=(Minecraft)(Object)this;
boolean handoff=next!=null&&fr.koka.sanctuary.client.IntroClient.consumeArrivalMusic();
if(next!=null && (handoff || IntroSpawnFlash.active() || client.gui.screen() instanceof IntroScreen)) {
for(var source:SoundSource.values())if(source!=SoundSource.MUSIC)sounds.stop(null,source);
} else original.call(sounds);
}
}
@@ -1724,5 +1724,8 @@
"sanctuary.mob_head.removed": "Mob head item retrieved.",
"sanctuary.mob_head.occupied": "This mob already wears an item. Retrieve it with an empty hand first.",
"sanctuary.mob_head.unreadable": "This mobs head equipment data is unreadable and has been preserved.",
"sanctuary.mob_head.hint": "Sneak + right-click a mob: equip one item. Empty hand: retrieve it."
"sanctuary.mob_head.hint": "Sneak + right-click a mob: equip one item. Empty hand: retrieve it.",
"sanctuary.familiar.mount.controls": "Mount: movement keys · Space: jump/rise · Sneak: dismount after releasing it.",
"sanctuary.familiar.mount.hint": "Colossal: Sneak + empty-hand right-click to ride your familiar.",
"sanctuary.familiar.carry.hint": "Carried movement speed: %s%%"
}
@@ -1724,5 +1724,8 @@
"sanctuary.mob_head.removed": "Objet récupéré sur le mob.",
"sanctuary.mob_head.occupied": "Ce mob porte déjà un objet. Récupérez-le dabord à main vide.",
"sanctuary.mob_head.unreadable": "Les données de lobjet porté sont illisibles et ont été conservées.",
"sanctuary.mob_head.hint": "Maj + clic droit sur un mob : poser un objet. Main vide : le récupérer."
"sanctuary.mob_head.hint": "Maj + clic droit sur un mob : poser un objet. Main vide : le récupérer.",
"sanctuary.familiar.mount.controls": "Monture : déplacement · Espace : saut/montée · Maj : descendre après relâchement.",
"sanctuary.familiar.mount.hint": "Colossal : Maj + clic droit à main vide pour monter sur ton familier.",
"sanctuary.familiar.carry.hint": "Vitesse en le portant : %s %%"
}
@@ -219,7 +219,9 @@
"client.ShaderSaturationMixin",
"client.WeatherEnvironmentMixin",
"client.VanillaLightMixin",
"client.ButtonHoverTextMixin"
"client.ButtonHoverTextMixin",
"client.IntroMusicMixin",
"client.IntroMusicManagerMixin"
],
"injectors": {
"defaultRequire": 1