Initial commit

This commit is contained in:
KäseToatz
2025-08-22 23:54:52 +02:00
parent 3de8506524
commit 1403d6dbed
28 changed files with 1131 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package com.kasetoatz.jukeboxboat.interfaces;
import net.minecraft.entity.decoration.DisplayEntity;
import net.minecraft.item.ItemStack;
public interface IAbstractBoatEntity
{
boolean jukeboxBoat$isJukeboxBoat();
void jukeboxBoat$setJukeboxBoat(boolean value);
ItemStack jukeboxBoat$getStoredDisc();
DisplayEntity.BlockDisplayEntity jukeboxBoat$getJukebox();
void jukeboxBoat$setJukebox(DisplayEntity.BlockDisplayEntity jukebox);
}

View File

@@ -0,0 +1,8 @@
package com.kasetoatz.jukeboxboat.interfaces;
public interface IArmorStandEntity
{
boolean jukeboxBoat$isPlaceholder();
void jukeboxBoat$setPlaceholder(boolean value);
void jukeboxBoat$setMarker(boolean value);
}

View File

@@ -0,0 +1,204 @@
package com.kasetoatz.jukeboxboat.mixin;
import com.kasetoatz.jukeboxboat.interfaces.IAbstractBoatEntity;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.entity.Entity;
import net.minecraft.entity.decoration.DisplayEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.vehicle.AbstractBoatEntity;
import net.minecraft.entity.vehicle.BoatEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.network.packet.s2c.play.PositionFlag;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvent;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.EnumSet;
import java.util.UUID;
import static com.kasetoatz.jukeboxboat.util.Util.*;
@Mixin(AbstractBoatEntity.class)
public abstract class AbstractBoatEntityMixin implements IAbstractBoatEntity
{
@Shadow protected abstract boolean canAddPassenger(Entity passenger);
@Unique
private void setStoredDisc(ItemStack disc)
{
if ((Object)this instanceof BoatEntity boat)
{
if (disc.isEmpty())
{
boat.setComponent(DataComponentTypes.CUSTOM_DATA, boat.get(DataComponentTypes.CUSTOM_DATA).apply(nbtCompound -> nbtCompound.remove("stored_disc")));
}
else
{
boat.setComponent(DataComponentTypes.CUSTOM_DATA, boat.get(DataComponentTypes.CUSTOM_DATA).apply(nbtCompound -> nbtCompound.put("stored_disc", ItemStack.CODEC, disc)));
}
}
}
@Unique
private void setLastStateChange()
{
if ((Object)this instanceof BoatEntity boat)
{
boat.setComponent(DataComponentTypes.CUSTOM_DATA, boat.get(DataComponentTypes.CUSTOM_DATA).apply(nbtCompound -> nbtCompound.putLong("last_state_change", System.currentTimeMillis())));
}
}
@Unique
private boolean canExecute()
{
if ((Object)this instanceof BoatEntity boat)
{
return System.currentTimeMillis() - boat.get(DataComponentTypes.CUSTOM_DATA).copyNbt().getLong("last_state_change").orElse(0L) > 100;
}
return true;
}
@Override
public boolean jukeboxBoat$isJukeboxBoat()
{
if ((Object)this instanceof BoatEntity boat)
{
return boat.get(DataComponentTypes.CUSTOM_DATA).copyNbt().getBoolean("is_jukebox_boat").orElse(false);
}
return false;
}
@Override
public void jukeboxBoat$setJukeboxBoat(boolean value)
{
if ((Object)this instanceof BoatEntity boat)
{
boat.setComponent(DataComponentTypes.CUSTOM_DATA, boat.get(DataComponentTypes.CUSTOM_DATA).apply(nbtCompound -> nbtCompound.putBoolean("is_jukebox_boat", value)));
}
}
@Override
public ItemStack jukeboxBoat$getStoredDisc()
{
if ((Object)this instanceof BoatEntity boat)
{
return boat.get(DataComponentTypes.CUSTOM_DATA).copyNbt().get("stored_disc", ItemStack.CODEC).orElse(ItemStack.EMPTY);
}
return ItemStack.EMPTY;
}
@Override
public DisplayEntity.BlockDisplayEntity jukeboxBoat$getJukebox()
{
if ((Object)this instanceof BoatEntity boat)
{
String uuid = boat.get(DataComponentTypes.CUSTOM_DATA).copyNbt().getString("jukebox", "");
if (boat.getWorld().getEntity(UUID.fromString(uuid)) instanceof DisplayEntity.BlockDisplayEntity entity)
{
return entity;
}
}
return null;
}
@Override
public void jukeboxBoat$setJukebox(DisplayEntity.BlockDisplayEntity jukebox)
{
if ((Object)this instanceof BoatEntity boat)
{
if (jukebox == null)
{
boat.setComponent(DataComponentTypes.CUSTOM_DATA, boat.get(DataComponentTypes.CUSTOM_DATA).apply(nbtCompound -> nbtCompound.remove("jukebox")));
}
else
{
boat.setComponent(DataComponentTypes.CUSTOM_DATA, boat.get(DataComponentTypes.CUSTOM_DATA).apply(nbtCompound -> nbtCompound.putString("jukebox", jukebox.getUuidAsString())));
}
}
}
@ModifyReturnValue(method="interact", at=@At("RETURN"))
public ActionResult interact(ActionResult result, @Local(argsOnly=true) PlayerEntity player, @Local(argsOnly=true) Hand hand)
{
if ((Object)this instanceof BoatEntity boat)
{
if (result != ActionResult.PASS || !jukeboxBoat$isJukeboxBoat() || !canExecute())
{
return result;
}
else if (canAddPassenger(player) && !player.shouldCancelInteraction())
{
return ActionResult.PASS;
}
else if (!jukeboxBoat$getStoredDisc().isEmpty())
{
ItemStack stack = jukeboxBoat$getStoredDisc();
if (!stack.isEmpty())
{
setPlaying(boat, false);
setStoredDisc(ItemStack.EMPTY);
player.getInventory().insertStack(stack);
}
replaceJukebox(boat);
setLastStateChange();
}
else
{
ItemStack stack = player.getStackInHand(hand);
ItemStack storedDisc = stack.copy();
SoundEvent sound = getDiscSound(player, storedDisc);
if (sound == null)
{
return ActionResult.PASS;
}
stack.splitUnlessCreative(1, player);
DisplayEntity.BlockDisplayEntity jukebox = jukeboxBoat$getJukebox();
if (jukebox == null)
{
jukebox = createJukebox(boat);
}
player.getWorld().playSoundFromEntity(
null,
jukebox,
sound,
SoundCategory.RECORDS,
1.0f,
1.0f
);
setPlaying(boat, true);
setStoredDisc(storedDisc);
setLastStateChange();
}
return ActionResult.SUCCESS;
}
return ActionResult.PASS;
}
@Inject(method="tick", at=@At("HEAD"))
public void tick(CallbackInfo ci)
{
if ((Object)this instanceof BoatEntity boat)
{
IAbstractBoatEntity accessor = (IAbstractBoatEntity)boat;
DisplayEntity.BlockDisplayEntity jukebox = jukeboxBoat$getJukebox();
if (!accessor.jukeboxBoat$isJukeboxBoat() || jukebox == null)
{
return;
}
if (boat.getWorld() instanceof ServerWorld world)
{
jukebox.teleport(world, boat.getX(), boat.getY(), boat.getZ(), EnumSet.noneOf(PositionFlag.class), boat.getYaw() + 90, boat.getPitch(), false);
}
}
}
}

View File

@@ -0,0 +1,38 @@
package com.kasetoatz.jukeboxboat.mixin;
import com.kasetoatz.jukeboxboat.interfaces.IArmorStandEntity;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.entity.decoration.ArmorStandEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(ArmorStandEntity.class)
public abstract class ArmorStandEntityMixin implements IArmorStandEntity
{
@Shadow protected abstract void setMarker(boolean marker);
@Override
public boolean jukeboxBoat$isPlaceholder()
{
if ((Object)this instanceof ArmorStandEntity entity)
{
return entity.get(DataComponentTypes.CUSTOM_DATA).copyNbt().getBoolean("is_placeholder").orElse(false);
}
return false;
}
@Override
public void jukeboxBoat$setPlaceholder(boolean value)
{
if ((Object)this instanceof ArmorStandEntity entity)
{
entity.setComponent(DataComponentTypes.CUSTOM_DATA, entity.get(DataComponentTypes.CUSTOM_DATA).apply(nbtCompound -> nbtCompound.putBoolean("is_placeholder", value)));
}
}
@Override
public void jukeboxBoat$setMarker(boolean value)
{
setMarker(value);
}
}

View File

@@ -0,0 +1,44 @@
package com.kasetoatz.jukeboxboat.mixin;
import com.kasetoatz.jukeboxboat.interfaces.IAbstractBoatEntity;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.entity.vehicle.AbstractBoatEntity;
import net.minecraft.entity.vehicle.BoatEntity;
import net.minecraft.item.BoatItem;
import net.minecraft.item.ItemStack;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult;
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.CallbackInfoReturnable;
import static com.kasetoatz.jukeboxboat.util.Util.createJukebox;
import static com.kasetoatz.jukeboxboat.util.Util.spawnPlaceholder;
@Mixin(BoatItem.class)
public class BoatItemMixin
{
@Inject(method="use", at=@At(value="INVOKE", target="Lnet/minecraft/entity/vehicle/AbstractBoatEntity;setYaw(F)V"))
public void use(CallbackInfoReturnable<ActionResult> cir, @Local ItemStack stack, @Local AbstractBoatEntity abstractBoat)
{
if (abstractBoat instanceof BoatEntity boat)
{
boolean isJukeboxBoat = false;
Text name = stack.get(DataComponentTypes.ITEM_NAME);
if (name != null)
{
isJukeboxBoat = name.getString().equals("Boat with Jukebox");
}
IAbstractBoatEntity accessor = (IAbstractBoatEntity)boat;
accessor.jukeboxBoat$setJukeboxBoat(isJukeboxBoat);
if (!isJukeboxBoat)
{
return;
}
spawnPlaceholder(boat);
accessor.jukeboxBoat$setJukebox(createJukebox(boat));
}
}
}

View File

@@ -0,0 +1,32 @@
package com.kasetoatz.jukeboxboat.mixin;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.item.ItemStack;
import net.minecraft.recipe.ShapelessRecipe;
import net.minecraft.recipe.input.CraftingRecipeInput;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(ShapelessRecipe.class)
public abstract class ShapelessRecipeMixin
{
@Shadow public abstract String getGroup();
@ModifyReturnValue(method="craft(Lnet/minecraft/recipe/input/CraftingRecipeInput;Lnet/minecraft/registry/RegistryWrapper$WrapperLookup;)Lnet/minecraft/item/ItemStack;", at=@At("RETURN"))
public ItemStack craft(ItemStack original, @Local(argsOnly = true) CraftingRecipeInput input)
{
if (!original.isEmpty() && input.getStacks().stream().anyMatch(stack -> stack.getName().getString().equals("Boat with Jukebox")))
{
return ItemStack.EMPTY;
}
if (getGroup().equals("jukebox_boat"))
{
original.set(DataComponentTypes.ITEM_NAME, Text.of("Boat with Jukebox"));
}
return original;
}
}

View File

@@ -0,0 +1,52 @@
package com.kasetoatz.jukeboxboat.mixin;
import com.kasetoatz.jukeboxboat.interfaces.IAbstractBoatEntity;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.entity.decoration.DisplayEntity;
import net.minecraft.entity.vehicle.BoatEntity;
import net.minecraft.entity.vehicle.VehicleEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
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;
import static com.kasetoatz.jukeboxboat.util.Util.removePlaceholder;
@Mixin(VehicleEntity.class)
public abstract class VehicleEntityMixin
{
@Inject(method="killAndDropItem", at=@At("HEAD"))
public void preKill(CallbackInfo ci)
{
if ((Object)this instanceof BoatEntity boat)
{
IAbstractBoatEntity accessor = (IAbstractBoatEntity)boat;
DisplayEntity.BlockDisplayEntity jukebox = accessor.jukeboxBoat$getJukebox();
if (jukebox != null)
{
jukebox.discard();
accessor.jukeboxBoat$setJukebox(null);
}
removePlaceholder(boat);
}
}
@Inject(method="killAndDropItem", at=@At(value="INVOKE", target="Lnet/minecraft/entity/vehicle/VehicleEntity;dropStack(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/item/ItemStack;)Lnet/minecraft/entity/ItemEntity;"))
public void killAndDropItem(CallbackInfo ci, @Local(argsOnly=true) ServerWorld world, @Local ItemStack stack)
{
if ((Object)this instanceof BoatEntity boat)
{
IAbstractBoatEntity accessor = (IAbstractBoatEntity)boat;
ItemStack disc = accessor.jukeboxBoat$getStoredDisc();
if (!disc.isEmpty())
{
boat.dropStack(world, disc);
}
stack.set(DataComponentTypes.ITEM_NAME, Text.of("Boat with Jukebox"));
}
}
}

View File

@@ -0,0 +1,104 @@
package com.kasetoatz.jukeboxboat.util;
import com.kasetoatz.jukeboxboat.interfaces.IAbstractBoatEntity;
import com.kasetoatz.jukeboxboat.interfaces.IArmorStandEntity;
import net.minecraft.block.Blocks;
import net.minecraft.block.JukeboxBlock;
import net.minecraft.block.jukebox.JukeboxSong;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.JukeboxPlayableComponent;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.decoration.ArmorStandEntity;
import net.minecraft.entity.decoration.DisplayEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.vehicle.BoatEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.sound.SoundEvent;
import net.minecraft.util.math.AffineTransformation;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import java.util.Optional;
public class Util
{
public static SoundEvent getDiscSound(PlayerEntity player, ItemStack disc)
{
JukeboxPlayableComponent component = disc.get(DataComponentTypes.JUKEBOX_PLAYABLE);
Optional<Registry<JukeboxSong>> key = player.getWorld().getRegistryManager().getOptional(RegistryKeys.JUKEBOX_SONG);
if (component == null || key.isEmpty())
{
return null;
}
return component.song().resolveValue(key.get()).map(song -> song.soundEvent().value()).orElse(null);
}
public static void setPlaying(BoatEntity boat, boolean playing)
{
DisplayEntity.BlockDisplayEntity jukebox = ((IAbstractBoatEntity)boat).jukeboxBoat$getJukebox();
if (jukebox != null)
{
if (playing)
{
jukebox.setBlockState(Blocks.JUKEBOX.getDefaultState().with(JukeboxBlock.HAS_RECORD, true));
}
else
{
jukebox.setBlockState(Blocks.JUKEBOX.getDefaultState());
}
}
}
public static DisplayEntity.BlockDisplayEntity createJukebox(BoatEntity boat)
{
DisplayEntity.BlockDisplayEntity jukebox = new DisplayEntity.BlockDisplayEntity(EntityType.BLOCK_DISPLAY, boat.getWorld());
jukebox.setBlockState(Blocks.JUKEBOX.getDefaultState());
jukebox.setTransformation(new AffineTransformation(
new Vector3f(-0.8f, 0.2f, -0.4f),
new Quaternionf(),
new Vector3f(0.8f, 0.8f, 0.8f),
new Quaternionf()
));
boat.getWorld().spawnEntity(jukebox);
return jukebox;
}
public static void spawnPlaceholder(BoatEntity boat)
{
ArmorStandEntity placeholder = new ArmorStandEntity(boat.getWorld(), boat.getX(), boat.getY(), boat.getZ());
IArmorStandEntity accessor = (IArmorStandEntity)placeholder;
accessor.jukeboxBoat$setPlaceholder(true);
accessor.jukeboxBoat$setMarker(true);
placeholder.setHideBasePlate(true);
placeholder.setInvisible(true);
placeholder.setInvulnerable(true);
boat.getWorld().spawnEntity(placeholder);
placeholder.startRiding(boat);
}
public static void removePlaceholder(BoatEntity boat)
{
for (Entity entity : boat.getPassengerList())
{
if (entity instanceof ArmorStandEntity armorStand && ((IArmorStandEntity)armorStand).jukeboxBoat$isPlaceholder())
{
armorStand.discard();
}
}
}
public static void replaceJukebox(BoatEntity boat)
{
IAbstractBoatEntity accessor = (IAbstractBoatEntity)boat;
DisplayEntity.BlockDisplayEntity old = accessor.jukeboxBoat$getJukebox();
if (old != null)
{
old.discard();
}
DisplayEntity.BlockDisplayEntity jukebox = createJukebox(boat);
accessor.jukeboxBoat$setJukebox(jukebox);
}
}