diff --git a/src/main/java/dan200/computercraft/client/ClientHooks.java b/src/main/java/dan200/computercraft/client/ClientHooks.java index 17e565f5b..87b60cc44 100644 --- a/src/main/java/dan200/computercraft/client/ClientHooks.java +++ b/src/main/java/dan200/computercraft/client/ClientHooks.java @@ -22,6 +22,7 @@ public static void onWorldUnload( WorldEvent.Unload event ) if( event.getWorld().isClientSide() ) { ClientMonitor.destroyAll(); + SoundManager.reset(); } } diff --git a/src/main/java/dan200/computercraft/client/SoundManager.java b/src/main/java/dan200/computercraft/client/SoundManager.java new file mode 100644 index 000000000..7e6a5605b --- /dev/null +++ b/src/main/java/dan200/computercraft/client/SoundManager.java @@ -0,0 +1,84 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.ISound; +import net.minecraft.client.audio.ITickableSound; +import net.minecraft.client.audio.LocatableSound; +import net.minecraft.client.audio.SoundHandler; +import net.minecraft.util.SoundCategory; +import net.minecraft.util.SoundEvent; +import net.minecraft.util.math.Vec3d; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class SoundManager +{ + private static final Map sounds = new HashMap<>(); + + public static void playSound( UUID source, Vec3d position, SoundEvent event, float volume, float pitch ) + { + SoundHandler soundManager = Minecraft.getInstance().getSoundManager(); + + MoveableSound oldSound = sounds.get( source ); + if( oldSound != null ) soundManager.stop( oldSound ); + + MoveableSound newSound = new MoveableSound( event, position, volume, pitch ); + sounds.put( source, newSound ); + soundManager.play( newSound ); + } + + public static void stopSound( UUID source ) + { + ISound sound = sounds.remove( source ); + if( sound == null ) return; + + Minecraft.getInstance().getSoundManager().stop( sound ); + } + + public static void moveSound( UUID source, Vec3d position ) + { + MoveableSound sound = sounds.get( source ); + if( sound != null ) sound.setPosition( position ); + } + + public static void reset() + { + sounds.clear(); + } + + private static class MoveableSound extends LocatableSound implements ITickableSound + { + protected MoveableSound( SoundEvent sound, Vec3d position, float volume, float pitch ) + { + super( sound, SoundCategory.RECORDS ); + setPosition( position ); + this.volume = volume; + this.pitch = pitch; + } + + void setPosition( Vec3d position ) + { + x = (float) position.x(); + y = (float) position.y(); + z = (float) position.z(); + } + + @Override + public boolean isStopped() + { + return false; + } + + @Override + public void tick() + { + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/Generator.java b/src/main/java/dan200/computercraft/core/asm/Generator.java index 051da8195..e68732f95 100644 --- a/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -11,7 +11,10 @@ import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.lua.*; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.MethodResult; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; diff --git a/src/main/java/dan200/computercraft/data/LootTables.java b/src/main/java/dan200/computercraft/data/LootTables.java index dc6ea8152..0fa458363 100644 --- a/src/main/java/dan200/computercraft/data/LootTables.java +++ b/src/main/java/dan200/computercraft/data/LootTables.java @@ -6,11 +6,11 @@ package dan200.computercraft.data; import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.CommonHooks; import dan200.computercraft.shared.Registry; import dan200.computercraft.shared.data.BlockNamedEntityLootCondition; import dan200.computercraft.shared.data.HasComputerIdLootCondition; import dan200.computercraft.shared.data.PlayerCreativeLootCondition; -import dan200.computercraft.shared.CommonHooks; import net.minecraft.block.Block; import net.minecraft.data.DataGenerator; import net.minecraft.util.ResourceLocation; diff --git a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java index d78dd991b..a2ac3451e 100644 --- a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java +++ b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java @@ -56,6 +56,9 @@ public static void setup() registerMainThread( 13, NetworkDirection.PLAY_TO_CLIENT, ComputerTerminalClientMessage::new ); registerMainThread( 14, NetworkDirection.PLAY_TO_CLIENT, PlayRecordClientMessage.class, PlayRecordClientMessage::new ); registerMainThread( 15, NetworkDirection.PLAY_TO_CLIENT, MonitorClientMessage.class, MonitorClientMessage::new ); + registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new ); + registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new ); + registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new ); } public static void sendToPlayer( PlayerEntity player, NetworkMessage packet ) diff --git a/src/main/java/dan200/computercraft/shared/network/client/SpeakerMoveClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/SpeakerMoveClientMessage.java new file mode 100644 index 000000000..9c41297e6 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/SpeakerMoveClientMessage.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.network.client; + +import dan200.computercraft.client.SoundManager; +import dan200.computercraft.shared.network.NetworkMessage; +import net.minecraft.network.PacketBuffer; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; +import java.util.UUID; + +/** + * Starts a sound on the client. + * + * Used by speakers to play sounds. + * + * @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker + */ +public class SpeakerMoveClientMessage implements NetworkMessage +{ + private final UUID source; + private final Vec3d pos; + + public SpeakerMoveClientMessage( UUID source, Vec3d pos ) + { + this.source = source; + this.pos = pos; + } + + public SpeakerMoveClientMessage( PacketBuffer buf ) + { + source = buf.readUUID(); + pos = new Vec3d( buf.readDouble(), buf.readDouble(), buf.readDouble() ); + } + + @Override + public void toBytes( @Nonnull PacketBuffer buf ) + { + buf.writeUUID( source ); + buf.writeDouble( pos.x() ); + buf.writeDouble( pos.y() ); + buf.writeDouble( pos.z() ); + } + + @Override + @OnlyIn( Dist.CLIENT ) + public void handle( NetworkEvent.Context context ) + { + SoundManager.moveSound( source, pos ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/SpeakerPlayClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/SpeakerPlayClientMessage.java new file mode 100644 index 000000000..9c27ca0e7 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/SpeakerPlayClientMessage.java @@ -0,0 +1,74 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.network.client; + +import dan200.computercraft.client.SoundManager; +import dan200.computercraft.shared.network.NetworkMessage; +import net.minecraft.network.PacketBuffer; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.SoundEvent; +import net.minecraft.util.math.Vec3d; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.network.NetworkEvent; +import net.minecraftforge.registries.ForgeRegistries; + +import javax.annotation.Nonnull; +import java.util.UUID; + +/** + * Starts a sound on the client. + * + * Used by speakers to play sounds. + * + * @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker + */ +public class SpeakerPlayClientMessage implements NetworkMessage +{ + private final UUID source; + private final Vec3d pos; + private final ResourceLocation sound; + private final float volume; + private final float pitch; + + public SpeakerPlayClientMessage( UUID source, Vec3d pos, ResourceLocation event, float volume, float pitch ) + { + this.source = source; + this.pos = pos; + sound = event; + this.volume = volume; + this.pitch = pitch; + } + + public SpeakerPlayClientMessage( PacketBuffer buf ) + { + source = buf.readUUID(); + pos = new Vec3d( buf.readDouble(), buf.readDouble(), buf.readDouble() ); + sound = buf.readResourceLocation(); + volume = buf.readFloat(); + pitch = buf.readFloat(); + } + + @Override + public void toBytes( @Nonnull PacketBuffer buf ) + { + buf.writeUUID( source ); + buf.writeDouble( pos.x() ); + buf.writeDouble( pos.y() ); + buf.writeDouble( pos.z() ); + buf.writeResourceLocation( sound ); + buf.writeFloat( volume ); + buf.writeFloat( pitch ); + } + + @Override + @OnlyIn( Dist.CLIENT ) + public void handle( NetworkEvent.Context context ) + { + SoundEvent sound = ForgeRegistries.SOUND_EVENTS.getValue( this.sound ); + if( sound != null ) SoundManager.playSound( source, pos, sound, volume, pitch ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/network/client/SpeakerStopClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/SpeakerStopClientMessage.java new file mode 100644 index 000000000..e1a10455f --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/network/client/SpeakerStopClientMessage.java @@ -0,0 +1,51 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.network.client; + +import dan200.computercraft.client.SoundManager; +import dan200.computercraft.shared.network.NetworkMessage; +import net.minecraft.network.PacketBuffer; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.network.NetworkEvent; + +import javax.annotation.Nonnull; +import java.util.UUID; + +/** + * Stops a sound on the client + * + * Called when a speaker is broken. + * + * @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker + */ +public class SpeakerStopClientMessage implements NetworkMessage +{ + private final UUID source; + + public SpeakerStopClientMessage( UUID source ) + { + this.source = source; + } + + public SpeakerStopClientMessage( PacketBuffer buf ) + { + source = buf.readUUID(); + } + + @Override + public void toBytes( @Nonnull PacketBuffer buf ) + { + buf.writeUUID( source ); + } + + @Override + @OnlyIn( Dist.CLIENT ) + public void handle( NetworkEvent.Context context ) + { + SoundManager.stopSound( source ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java index e51362843..bea861f05 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java @@ -87,7 +87,7 @@ public void setPlacedBy( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull B { TileMonitor monitor = (TileMonitor) entity; // Defer the block update if we're being placed by another TE. See #691 - if ( livingEntity == null || livingEntity instanceof FakePlayer ) + if( livingEntity == null || livingEntity instanceof FakePlayer ) { monitor.updateNeighborsDeferred(); return; diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java index ce7a3710f..408521ff0 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java @@ -10,17 +10,23 @@ import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.client.SpeakerMoveClientMessage; +import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage; import net.minecraft.network.play.server.SPlaySoundPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.state.properties.NoteBlockInstrument; import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocationException; import net.minecraft.util.SoundCategory; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import javax.annotation.Nonnull; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import static dan200.computercraft.api.lua.LuaValues.checkFinite; @@ -32,20 +38,44 @@ */ public abstract class SpeakerPeripheral implements IPeripheral { + private static final int MIN_TICKS_BETWEEN_SOUNDS = 1; + private long clock = 0; private long lastPlayTime = 0; private final AtomicInteger notesThisTick = new AtomicInteger(); + private long lastPositionTime; + private Vec3d lastPosition; + public void update() { clock++; notesThisTick.set( 0 ); + + // Push position updates to any speakers which have ever played a note, + // have moved by a non-trivial amount and haven't had a position update + // in the last second. + if( lastPlayTime > 0 && (clock - lastPositionTime) >= 20 ) + { + Vec3d position = getPosition(); + if( lastPosition == null || lastPosition.distanceToSqr( position ) >= 0.1 ) + { + lastPosition = position; + lastPositionTime = clock; + NetworkHandler.sendToAllTracking( + new SpeakerMoveClientMessage( getSource(), position ), + getWorld().getChunkAt( new BlockPos( position ) ) + ); + } + } } public abstract World getWorld(); public abstract Vec3d getPosition(); + protected abstract UUID getSource(); + public boolean madeSound( long ticks ) { return clock - lastPlayTime <= ticks; @@ -135,26 +165,37 @@ public final synchronized boolean playNote( ILuaContext context, String name, Op private synchronized boolean playSound( ILuaContext context, ResourceLocation name, float volume, float pitch, boolean isNote ) throws LuaException { - if( clock - lastPlayTime < TileSpeaker.MIN_TICKS_BETWEEN_SOUNDS && - (!isNote || clock - lastPlayTime != 0 || notesThisTick.get() >= ComputerCraft.maxNotesPerTick) ) + if( clock - lastPlayTime < MIN_TICKS_BETWEEN_SOUNDS ) { - // Rate limiting occurs when we've already played a sound within the last tick, or we've - // played more notes than allowable within the current tick. - return false; + // Rate limiting occurs when we've already played a sound within the last tick. + if( !isNote ) return false; + // Or we've played more notes than allowable within the current tick. + if( clock - lastPlayTime != 0 || notesThisTick.get() >= ComputerCraft.maxNotesPerTick ) return false; } World world = getWorld(); Vec3d pos = getPosition(); + float range = MathHelper.clamp( volume, 1.0f, 3.0f ) * 16; + context.issueMainThreadTask( () -> { MinecraftServer server = world.getServer(); if( server == null ) return null; - float adjVolume = Math.min( volume, 3.0f ); - server.getPlayerList().broadcast( - null, pos.x, pos.y, pos.z, adjVolume > 1.0f ? 16 * adjVolume : 16.0, world.dimension.getType(), - new SPlaySoundPacket( name, SoundCategory.RECORDS, pos, adjVolume, pitch ) - ); + if( isNote ) + { + server.getPlayerList().broadcast( + null, pos.x, pos.y, pos.z, range, world.dimension.getType(), + new SPlaySoundPacket( name, SoundCategory.RECORDS, pos, range, pitch ) + ); + } + else + { + NetworkHandler.sendToAllAround( + new SpeakerPlayClientMessage( getSource(), pos, name, range, pitch ), + world, pos, range + ); + } return null; } ); diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java index e06edc73e..600190867 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java @@ -7,6 +7,8 @@ import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.shared.common.TileGeneric; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.client.SpeakerStopClientMessage; import dan200.computercraft.shared.util.CapabilityUtil; import net.minecraft.tileentity.ITickableTileEntity; import net.minecraft.tileentity.TileEntityType; @@ -19,15 +21,15 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.UUID; import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL; public class TileSpeaker extends TileGeneric implements ITickableTileEntity { - public static final int MIN_TICKS_BETWEEN_SOUNDS = 1; - private final SpeakerPeripheral peripheral; private LazyOptional peripheralCap; + private final UUID source = UUID.randomUUID(); public TileSpeaker( TileEntityType type ) { @@ -41,6 +43,13 @@ public void tick() peripheral.update(); } + @Override + public void setRemoved() + { + super.setRemoved(); + NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) ); + } + @Nonnull @Override public LazyOptional getCapability( @Nonnull Capability cap, @Nullable Direction side ) @@ -83,6 +92,12 @@ public Vec3d getPosition() return new Vec3d( pos.getX(), pos.getY(), pos.getZ() ); } + @Override + protected UUID getSource() + { + return speaker.source; + } + @Override public boolean equals( @Nullable IPeripheral other ) { diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/UpgradeSpeakerPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/UpgradeSpeakerPeripheral.java new file mode 100644 index 000000000..cbe318845 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/UpgradeSpeakerPeripheral.java @@ -0,0 +1,33 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.shared.peripheral.speaker; + +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.shared.network.NetworkHandler; +import dan200.computercraft.shared.network.client.SpeakerStopClientMessage; + +import javax.annotation.Nonnull; +import java.util.UUID; + +/** + * A speaker peripheral which is used on an upgrade, and so is only attached to one computer. + */ +public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral +{ + private final UUID source = UUID.randomUUID(); + + @Override + protected final UUID getSource() + { + return source; + } + + @Override + public void detach( @Nonnull IComputerAccess computer ) + { + NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java index f518e4158..6588a76f0 100644 --- a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeakerPeripheral.java @@ -6,11 +6,11 @@ package dan200.computercraft.shared.pocket.peripherals; import dan200.computercraft.api.peripheral.IPeripheral; -import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral; +import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; -public class PocketSpeakerPeripheral extends SpeakerPeripheral +public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral { private World world = null; private Vec3d position = Vec3d.ZERO; diff --git a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index aeecf1d52..be0415416 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -715,7 +715,6 @@ public final MethodResult inspectDown() * more information about the item at the cost of taking longer to run. * @return The command result. * @throws LuaException If the slot is out of range. - * @see InventoryMethods#getItemDetail Describes the information returned by a detailed query. * @cc.treturn nil|table Information about the given slot, or {@code nil} if it is empty. * @cc.usage Print the current slot, assuming it contains 13 dirt. * @@ -726,6 +725,7 @@ public final MethodResult inspectDown() * -- count = 13, * -- } * } + * @see InventoryMethods#getItemDetail Describes the information returned by a detailed query. */ @LuaFunction public final MethodResult getItemDetail( ILuaContext context, Optional slot, Optional detailed ) throws LuaException diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java index d4f682766..0c77003d0 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java @@ -12,7 +12,7 @@ import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.turtle.TurtleUpgradeType; import dan200.computercraft.shared.Registry; -import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral; +import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral; import net.minecraft.client.renderer.model.ModelResourceLocation; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; @@ -28,7 +28,7 @@ public class TurtleSpeaker extends AbstractTurtleUpgrade private static final ModelResourceLocation leftModel = new ModelResourceLocation( "computercraft:turtle_speaker_upgrade_left", "inventory" ); private static final ModelResourceLocation rightModel = new ModelResourceLocation( "computercraft:turtle_speaker_upgrade_right", "inventory" ); - private static class Peripheral extends SpeakerPeripheral + private static class Peripheral extends UpgradeSpeakerPeripheral { ITurtleAccess turtle;