2424 */
2525package org .spongepowered .common .mixin .core .server .level ;
2626
27+ import com .google .common .collect .ImmutableMap ;
2728import com .google .common .collect .ImmutableSet ;
2829import com .llamalad7 .mixinextras .injector .ModifyExpressionValue ;
2930import com .llamalad7 .mixinextras .injector .wrapmethod .WrapMethod ;
3839import net .kyori .adventure .text .Component ;
3940import net .minecraft .advancements .CriteriaTriggers ;
4041import net .minecraft .core .BlockPos ;
42+ import net .minecraft .core .GlobalPos ;
43+ import net .minecraft .core .registries .Registries ;
4144import net .minecraft .nbt .CompoundTag ;
4245import net .minecraft .network .chat .ChatType ;
4346import net .minecraft .network .chat .OutgoingChatMessage ;
4447import net .minecraft .network .protocol .Packet ;
4548import net .minecraft .network .protocol .game .ClientGamePacketListener ;
4649import net .minecraft .network .protocol .game .ClientboundSetEntityLinkPacket ;
50+ import net .minecraft .resources .Identifier ;
4751import net .minecraft .resources .ResourceKey ;
4852import net .minecraft .server .MinecraftServer ;
4953import net .minecraft .server .ServerScoreboard ;
7074import net .minecraft .world .level .border .WorldBorder ;
7175import net .minecraft .world .level .gamerules .GameRules ;
7276import net .minecraft .world .level .portal .TeleportTransition ;
77+ import net .minecraft .world .level .storage .LevelData ;
78+ import net .minecraft .world .level .storage .ValueInput ;
7379import net .minecraft .world .phys .Vec3 ;
7480import net .minecraft .world .scores .PlayerTeam ;
7581import org .checkerframework .checker .nullness .qual .Nullable ;
100106import org .spongepowered .api .registry .RegistryTypes ;
101107import org .spongepowered .api .scoreboard .Scoreboard ;
102108import org .spongepowered .api .service .permission .PermissionService ;
109+ import org .spongepowered .api .util .RespawnLocation ;
103110import org .spongepowered .api .util .Tristate ;
104111import org .spongepowered .api .util .locale .Locales ;
105112import org .spongepowered .api .world .server .ServerWorld ;
106113import org .spongepowered .asm .mixin .Final ;
107114import org .spongepowered .asm .mixin .Mixin ;
108115import org .spongepowered .asm .mixin .Overwrite ;
109116import org .spongepowered .asm .mixin .Shadow ;
117+ import org .spongepowered .asm .mixin .Unique ;
110118import org .spongepowered .asm .mixin .injection .At ;
111119import org .spongepowered .asm .mixin .injection .Inject ;
112120import org .spongepowered .asm .mixin .injection .ModifyVariable ;
123131import org .spongepowered .common .accessor .world .level .portal .TeleportTransitionAccessor ;
124132import org .spongepowered .common .adventure .SpongeAdventure ;
125133import org .spongepowered .common .bridge .data .DataCompoundHolder ;
134+ import org .spongepowered .common .bridge .data .SpongeDataHolderBridge ;
126135import org .spongepowered .common .bridge .data .TransientBridge ;
127136import org .spongepowered .common .bridge .permissions .SubjectBridge ;
128137import org .spongepowered .common .bridge .server .ServerScoreboardBridge ;
129138import org .spongepowered .common .bridge .server .level .ServerPlayerBridge ;
130139import org .spongepowered .common .bridge .world .BossEventBridge ;
140+ import org .spongepowered .common .bridge .world .entity .player .BedLocationHolderBridge ;
131141import org .spongepowered .common .bridge .world .entity .player .PlayerBridge ;
132142import org .spongepowered .common .data .DataUtil ;
133143import org .spongepowered .common .data .type .SpongeSkinPart ;
139149import org .spongepowered .common .network .packet .SpongePacketHandler ;
140150import org .spongepowered .common .util .LocaleCache ;
141151import org .spongepowered .common .world .border .PlayerOwnBorderListener ;
152+ import org .spongepowered .math .vector .Vector3d ;
142153
154+ import java .util .HashMap ;
143155import java .util .Locale ;
156+ import java .util .Map ;
144157import java .util .Objects ;
145158import java .util .Set ;
146159
147160// See also: SubjectMixin_API and SubjectMixin
148161@ SuppressWarnings ("ConstantConditions" )
149162@ Mixin (net .minecraft .server .level .ServerPlayer .class )
150- public abstract class ServerPlayerMixin extends PlayerMixin implements SubjectBridge , ServerPlayerBridge {
163+ public abstract class ServerPlayerMixin extends PlayerMixin implements SubjectBridge , ServerPlayerBridge , BedLocationHolderBridge {
151164
152165 // @formatter:off
153166 @ Shadow public ServerGamePacketListenerImpl connection ;
154167 @ Shadow @ Final public ServerPlayerGameMode gameMode ;
155168 @ Shadow @ Final private MinecraftServer server ;
156169 @ Shadow private int lastRecordedExperience ;
170+ @ Shadow @ javax .annotation .Nullable private net .minecraft .server .level .ServerPlayer .RespawnConfig respawnConfig ;
157171
158172 @ Shadow public abstract ServerLevel shadow$level ();
159173 @ Shadow public abstract void shadow$doCloseContainer ();
160174 @ Shadow public abstract boolean shadow$setGameMode (GameType param0 );
161175 @ Shadow public abstract void shadow$setCamera (@ org .jetbrains .annotations .Nullable final Entity $$0 );
176+ @ Shadow public abstract void shadow$setRespawnPosition (@ javax .annotation .Nullable net .minecraft .server .level .ServerPlayer .RespawnConfig config , boolean sendMessage );
162177 // @formatter:on
163178
164179 private net .minecraft .network .chat .@ Nullable Component impl$connectionMessage ;
@@ -179,6 +194,8 @@ public abstract class ServerPlayerMixin extends PlayerMixin implements SubjectBr
179194 @ Nullable
180195 private WorldBorder impl$worldBorder ;
181196 private ServerLevel impl$respawnLevel ;
197+ private final Map <org .spongepowered .api .ResourceKey , RespawnLocation > impl$bedLocations = new HashMap <>();
198+ private boolean impl$syncingRespawn ;
182199
183200 @ Override
184201 public net .minecraft .network .chat .@ Nullable Component bridge$getConnectionMessageToSend () {
@@ -763,6 +780,20 @@ private boolean isPvpAllowed() {
763780 final Operation <net .minecraft .server .level .ServerPlayer .RespawnConfig > original ,
764781 @ Share ("sponge:overridden-respawn" ) final LocalRef <ResourceKey <Level >> dimension
765782 ) {
783+ // Prefer a forced Keys.RESPAWN_LOCATIONS entry for the world the player died in, even if vanilla's single
784+ // slot points elsewhere. Mirror it into vanilla's respawnConfig up front so findRespawnPositionAndUseSpawnBlock
785+ // reads the correct position when locating the bed/anchor.
786+ final org .spongepowered .api .ResourceKey dyingWorldKey = (org .spongepowered .api .ResourceKey ) (Object ) player .level ().dimension ().identifier ();
787+ final RespawnLocation forcedInDyingWorld = this .impl$bedLocations .get (dyingWorldKey );
788+ if (forcedInDyingWorld != null && forcedInDyingWorld .isForced ()) {
789+ this .impl$syncingRespawn = true ;
790+ try {
791+ this .shadow$setRespawnPosition (this .impl$toRespawnConfig (forcedInDyingWorld ), false );
792+ } finally {
793+ this .impl$syncingRespawn = false ;
794+ }
795+ }
796+
766797 final var config = original .call (player );
767798 final var defaulted = config == null ? Level .OVERWORLD : config .respawnData ().dimension ();
768799
@@ -818,4 +849,124 @@ private boolean isPvpAllowed() {
818849 }
819850 original .call (instance , packet );
820851 }
852+
853+ @ Override
854+ public Map <org .spongepowered .api .ResourceKey , RespawnLocation > bridge$getBedlocations () {
855+ return new HashMap <>(this .impl$bedLocations );
856+ }
857+
858+ @ Override
859+ public boolean bridge$setBedLocations (final Map <org .spongepowered .api .ResourceKey , RespawnLocation > value ) {
860+ for (final Map .Entry <org .spongepowered .api .ResourceKey , RespawnLocation > entry : value .entrySet ()) {
861+ if (!Objects .equals (entry .getKey (), entry .getValue ().worldKey ())) {
862+ throw new IllegalArgumentException ("RespawnLocation world key " + entry .getValue ().worldKey ()
863+ + " does not match map key " + entry .getKey ());
864+ }
865+ }
866+ this .impl$bedLocations .clear ();
867+ this .impl$bedLocations .putAll (value );
868+ this .impl$syncingRespawn = true ;
869+ try {
870+ final RespawnLocation active = this .impl$pickActiveRespawn (value );
871+ this .shadow$setRespawnPosition (active == null ? null : this .impl$toRespawnConfig (active ), false );
872+ } finally {
873+ this .impl$syncingRespawn = false ;
874+ }
875+ if (!((SpongeDataHolderBridge ) this ).brigde$isDeserializing ()) {
876+ if (value .isEmpty ()) {
877+ ((SpongeDataHolderBridge ) this ).bridge$remove (Keys .RESPAWN_LOCATIONS );
878+ } else {
879+ ((SpongeDataHolderBridge ) this ).bridge$offer (Keys .RESPAWN_LOCATIONS , new HashMap <>(value ));
880+ }
881+ }
882+ return true ;
883+ }
884+
885+ @ Override
886+ public ImmutableMap <org .spongepowered .api .ResourceKey , RespawnLocation > bridge$removeAllBeds () {
887+ final ImmutableMap <org .spongepowered .api .ResourceKey , RespawnLocation > snapshot = ImmutableMap .copyOf (this .impl$bedLocations );
888+ this .impl$bedLocations .clear ();
889+ this .impl$syncingRespawn = true ;
890+ try {
891+ this .shadow$setRespawnPosition (null , false );
892+ } finally {
893+ this .impl$syncingRespawn = false ;
894+ }
895+ ((SpongeDataHolderBridge ) this ).bridge$remove (Keys .RESPAWN_LOCATIONS );
896+ return snapshot ;
897+ }
898+
899+ @ Unique
900+ private @ Nullable RespawnLocation impl$pickActiveRespawn (final Map <org .spongepowered .api .ResourceKey , RespawnLocation > map ) {
901+ if (map .isEmpty ()) {
902+ return null ;
903+ }
904+ if (this .respawnConfig != null ) {
905+ final org .spongepowered .api .ResourceKey vanillaKey = (org .spongepowered .api .ResourceKey ) (Object ) this .respawnConfig .respawnData ().dimension ().identifier ();
906+ final RespawnLocation forVanilla = map .get (vanillaKey );
907+ if (forVanilla != null ) {
908+ return forVanilla ;
909+ }
910+ }
911+ final org .spongepowered .api .ResourceKey currentLevelKey = (org .spongepowered .api .ResourceKey ) (Object ) this .shadow$level ().dimension ().identifier ();
912+ final RespawnLocation forCurrent = map .get (currentLevelKey );
913+ if (forCurrent != null ) {
914+ return forCurrent ;
915+ }
916+ return map .values ().iterator ().next ();
917+ }
918+
919+ @ Unique
920+ private net .minecraft .server .level .ServerPlayer .RespawnConfig impl$toRespawnConfig (final RespawnLocation location ) {
921+ final ResourceKey <Level > dim = ResourceKey .create (Registries .DIMENSION ,
922+ (Identifier ) (Object ) location .worldKey ());
923+ final Vector3d pos = location .position ();
924+ // Vector3d -> BlockPos via floor (BlockPos.containing uses floor semantics).
925+ // RespawnLocation has no yaw/pitch — keep the player's current orientation so sleep angle isn't clobbered.
926+ final LevelData .RespawnData data = LevelData .RespawnData .of (
927+ dim , BlockPos .containing (pos .x (), pos .y (), pos .z ()), this .shadow$getYRot (), this .shadow$getXRot ());
928+ return new net .minecraft .server .level .ServerPlayer .RespawnConfig (data , location .isForced ());
929+ }
930+
931+ @ Inject (method = "setRespawnPosition(Lnet/minecraft/server/level/ServerPlayer$RespawnConfig;Z)V" , at = @ At ("RETURN" ))
932+ private void impl$mirrorVanillaRespawnToBedLocations (
933+ final @ javax .annotation .Nullable net .minecraft .server .level .ServerPlayer .RespawnConfig config ,
934+ final boolean sendMessage , final CallbackInfo ci ) {
935+ if (this .impl$syncingRespawn ) {
936+ return ;
937+ }
938+ if (config == null ) {
939+ // Vanilla cleared respawn — drop every entry (vanilla only tracks one slot anyway)
940+ this .impl$bedLocations .clear ();
941+ } else {
942+ final GlobalPos globalPos = config .respawnData ().globalPos ();
943+ final BlockPos pos = globalPos .pos ();
944+ final org .spongepowered .api .ResourceKey apiKey = (org .spongepowered .api .ResourceKey ) (Object ) globalPos .dimension ().identifier ();
945+ this .impl$bedLocations .put (apiKey , RespawnLocation .builder ()
946+ .world (apiKey )
947+ .position (new Vector3d (pos .getX () + 0.5D , pos .getY (), pos .getZ () + 0.5D ))
948+ .forceSpawn (config .forced ())
949+ .build ());
950+ }
951+ if (this .impl$bedLocations .isEmpty ()) {
952+ ((SpongeDataHolderBridge ) this ).bridge$remove (Keys .RESPAWN_LOCATIONS );
953+ } else {
954+ ((SpongeDataHolderBridge ) this ).bridge$offer (Keys .RESPAWN_LOCATIONS , new HashMap <>(this .impl$bedLocations ));
955+ }
956+ }
957+
958+ @ Inject (method = "readAdditionalSaveData" , at = @ At ("RETURN" ))
959+ private void impl$seedBedLocationsFromVanilla (final ValueInput input , final CallbackInfo ci ) {
960+ if (this .respawnConfig == null || !this .impl$bedLocations .isEmpty ()) {
961+ return ;
962+ }
963+ final GlobalPos globalPos = this .respawnConfig .respawnData ().globalPos ();
964+ final BlockPos pos = globalPos .pos ();
965+ final org .spongepowered .api .ResourceKey apiKey = (org .spongepowered .api .ResourceKey ) (Object ) globalPos .dimension ().identifier ();
966+ this .impl$bedLocations .put (apiKey , RespawnLocation .builder ()
967+ .world (apiKey )
968+ .position (new Vector3d (pos .getX () + 0.5D , pos .getY (), pos .getZ () + 0.5D ))
969+ .forceSpawn (this .respawnConfig .forced ())
970+ .build ());
971+ }
821972}
0 commit comments