Rewrite speaker networking code

Speakers now play sounds using a custom set of packets.
 - When playing a sound, we send the resource id, position, volume,
   pitch and a UUID for the _speaker_ to all nearby clients.
 - This UUID is then used when we need to update the sound. When the
   speaker is moved or destroyed, we send a new packet to clients and
   update accordingly.

This does have one side effect, that speakers can now only play one
sound at a time. I think this is accceptable - otherwise it's possible
to spam ward in a loop.

Notes still use the old networking code, and so will not be affected.

Closes #823
This commit is contained in:
Jonathan Coates 2021-06-18 22:23:04 +01:00
parent 2fab1a3054
commit de6f27ceaf
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
15 changed files with 383 additions and 20 deletions

View File

@ -22,6 +22,7 @@ public static void onWorldUnload( WorldEvent.Unload event )
if( event.getWorld().isClientSide() )
{
ClientMonitor.destroyAll();
SoundManager.reset();
}
}

View File

@ -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<UUID, MoveableSound> 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()
{
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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 )

View File

@ -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 );
}
}

View File

@ -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 );
}
}

View File

@ -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 );
}
}

View File

@ -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;

View File

@ -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;
} );

View File

@ -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<IPeripheral> peripheralCap;
private final UUID source = UUID.randomUUID();
public TileSpeaker( TileEntityType<TileSpeaker> type )
{
@ -41,6 +43,13 @@ public void tick()
peripheral.update();
}
@Override
public void setRemoved()
{
super.setRemoved();
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) );
}
@Nonnull
@Override
public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> 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 )
{

View File

@ -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 ) );
}
}

View File

@ -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;

View File

@ -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,
* -- }
* }</pre>
* @see InventoryMethods#getItemDetail Describes the information returned by a detailed query.
*/
@LuaFunction
public final MethodResult getItemDetail( ILuaContext context, Optional<Integer> slot, Optional<Boolean> detailed ) throws LuaException

View File

@ -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;