Run all sending of framebuffer data through a load balancer, so we have more control over bandwidth use.

Penalize projectors that a) send to multiple clients, b) are far away from players.
This commit is contained in:
Florian Nücke
2022-02-03 17:31:14 +01:00
parent ced5a0b5b4
commit 94318dceee
15 changed files with 461 additions and 120 deletions

View File

@@ -231,6 +231,8 @@ public final class ProjectorDepthRenderer {
renderProjectorDepthBuffer(minecraft, partialTicks, startNanos, viewModelStack);
storeProjectorColorBuffer(projectorIndex, projector);
projector.onRendering();
}
} finally {
finishDepthBufferRendering(minecraft, player);

View File

@@ -39,7 +39,7 @@ public final class Config {
@Path("gameplay") public static ResourceLocation blockOperationsModuleToolTier = TierSortingRegistry.getName(Tiers.DIAMOND);
@Path("admin") public static UUID fakePlayerUUID = UUID.fromString("e39dd9a7-514f-4a2d-aa5e-b6030621416d");
@Path("admin.blocks") public static int projectorMaxBytesPerTick = 8192;
@Path("admin.network") public static int projectorAverageMaxBytesPerSecond = 160 * 1024;
public static boolean robotsUseEnergy() {
return robotEnergyPerTick > 0 && robotEnergyStorage > 0;

View File

@@ -6,7 +6,8 @@ import li.cil.oc2.common.bus.device.vm.ProjectorVMDevice;
import li.cil.oc2.common.capabilities.Capabilities;
import li.cil.oc2.common.energy.FixedEnergyStorage;
import li.cil.oc2.common.network.Network;
import li.cil.oc2.common.network.message.ProjectorFramebufferMessage;
import li.cil.oc2.common.network.ProjectorLoadBalancer;
import li.cil.oc2.common.network.message.ProjectorRequestFramebufferMessage;
import li.cil.oc2.common.network.message.ProjectorStateMessage;
import li.cil.oc2.jcodec.codecs.h264.H264Decoder;
import li.cil.oc2.jcodec.codecs.h264.H264Encoder;
@@ -18,7 +19,6 @@ import net.minecraft.core.Direction;
import net.minecraft.core.SectionPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.AABB;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -27,17 +27,14 @@ import javax.annotation.Nullable;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
// TODO Only send frames to watching clients (have clients send "keepalive" packets when rendering this).
// TODO Throttle update speed by distance to closest player and max number any player watching this projector is watching in total.
public final class ProjectorBlockEntity extends ModBlockEntity implements TickableBlockEntity {
@FunctionalInterface
public interface FrameConsumer {
@@ -53,15 +50,13 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
public static final int MAX_WIDTH = MAX_GOOD_RENDER_DISTANCE + 1; // +1 To make it odd, so we can center.
public static final int MAX_HEIGHT = (MAX_GOOD_RENDER_DISTANCE * ProjectorVMDevice.HEIGHT / ProjectorVMDevice.WIDTH) + 1; // + 1 To match horizontal margin.
private static final int FRAME_EVERY_N_TICKS = 5;
private static final String ENERGY_TAG_NAME = "energy";
private static final String IS_PROJECTING_TAG_NAME = "projecting";
private static final ExecutorService FRAME_WORKERS = Executors.newCachedThreadPool(r -> {
private static final ExecutorService DECODER_WORKERS = Executors.newCachedThreadPool(r -> {
final Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("Projector Frame Encoder/Decoder");
thread.setName("Projector Frame Decoder");
return thread;
});
@@ -70,24 +65,21 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
private final ProjectorVMDevice projectorDevice = new ProjectorVMDevice(this);
private boolean isProjecting, hasEnergy;
private final FixedEnergyStorage energy = new FixedEnergyStorage(Config.projectorEnergyStorage);
// Video transfer.
private final H264Encoder encoder = new H264Encoder(new CQPRateControl(12));
@Nullable private Future<?> runningEncode; // To allow waiting for previous frame to finish.
private final ByteBuffer encoderBuffer = ByteBuffer.allocateDirect(1024 * 1024); // Re-used decompression buffer.
private final Picture picture = Picture.create(ProjectorVMDevice.WIDTH, ProjectorVMDevice.HEIGHT, ColorSpace.YUV420J);
// Video encoding.
private final H264Encoder encoder = new H264Encoder(new CQPRateControl(12));
private final ByteBuffer encoderBuffer = ByteBuffer.allocateDirect(1024 * 1024); // Re-used decompression buffer.
private boolean needsIDR = true; // Whether we need to send a keyframe next.
private final AtomicInteger sendBudget = new AtomicInteger(); // Remaining accumulated bandwidth budget.
private int nextFrameIn = 0; // Remaining cooldown before sending next frame.
// Client only data.
// Video decoding.
private final H264Decoder decoder = new H264Decoder();
@Nullable private Future<?> runningDecode; // Current decoding operation, if any, to avoid race conditions.
@Nullable private CompletableFuture<?> runningDecode; // Current decoding operation, if any, to avoid race conditions.
private final ByteBuffer decoderBuffer = ByteBuffer.allocateDirect(1024 * 1024); // Re-used decompression buffer.
@Nullable private FrameConsumer frameConsumer; // Where to throw received frames.
private AABB renderBounds; // Overall render bounds, disregarding projection surface, to allow growing if necessary.
private AABB renderBounds; // Maximum possible render bounds, assuming we project on furthest away surface.
private long lastKeepAliveSentAt;
///////////////////////////////////////////////////////////////
@@ -132,7 +124,11 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
needsIDR = true;
}
sendRunningState();
sendProjectorState();
}
public void setRequiresKeyframe() {
needsIDR = true;
}
public void setFrameConsumer(@Nullable final FrameConsumer consumer) {
@@ -147,6 +143,14 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
}
}
public void onRendering() {
final long now = System.currentTimeMillis();
if (now - lastKeepAliveSentAt > 1000) {
lastKeepAliveSentAt = now;
Network.sendToServer(new ProjectorRequestFramebufferMessage(this));
}
}
@Override
public void serverTick() {
if (!isProjecting()) {
@@ -156,63 +160,19 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
if (energy.extractEnergy(Config.projectorEnergyPerTick, true) < Config.projectorEnergyPerTick) {
if (hasEnergy) {
hasEnergy = false;
sendRunningState();
sendProjectorState();
}
return;
} else if (!hasEnergy) {
hasEnergy = true;
sendRunningState();
sendProjectorState();
}
sendBudget.updateAndGet(budget -> Math.min(Config.projectorMaxBytesPerTick * 10, budget + Config.projectorMaxBytesPerTick));
nextFrameIn = Math.max(0, nextFrameIn - 1);
if (sendBudget.get() < 0 || nextFrameIn > 0) {
if (!projectorDevice.hasChanges()) {
return;
}
if (runningEncode != null && !runningEncode.isDone()) {
return;
}
joinWorkerAndLogErrors(runningEncode);
nextFrameIn = FRAME_EVERY_N_TICKS;
if (level == null || !(level.getChunk(getBlockPos()) instanceof LevelChunk chunk)) {
return;
}
runningEncode = FRAME_WORKERS.submit(() -> {
final boolean hasChanges = projectorDevice.applyChanges(picture);
if (!hasChanges && !needsIDR) {
return;
}
encoderBuffer.clear();
final ByteBuffer frameData;
try {
if (needsIDR) {
frameData = encoder.encodeIDRFrame(picture, encoderBuffer);
needsIDR = false;
} else {
frameData = encoder.encodeFrame(picture, encoderBuffer).data();
}
} catch (final BufferOverflowException ignored) {
return; // Sad, frame encode failed for unknown reasons...
}
final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
deflater.setInput(frameData);
deflater.finish();
final ByteBuffer compressedFrameData = ByteBuffer.allocateDirect(1024 * 1024);
deflater.deflate(compressedFrameData, Deflater.FULL_FLUSH);
deflater.end();
compressedFrameData.flip();
sendBudget.accumulateAndGet(compressedFrameData.limit(), (budget, packetSize) -> budget - packetSize);
final ProjectorFramebufferMessage message = new ProjectorFramebufferMessage(this, compressedFrameData);
Network.sendToClientsTrackingChunk(message, chunk);
});
ProjectorLoadBalancer.offerFrame(this, this::encodeFrame);
}
@Override
@@ -260,10 +220,13 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
}
public void applyNextFrame(final ByteBuffer frameData) {
final Future<?> lastDecode = runningDecode;
runningDecode = FRAME_WORKERS.submit(() -> {
final CompletableFuture<?> lastDecode = runningDecode;
runningDecode = CompletableFuture.runAsync(() -> {
try {
joinWorkerAndLogErrors(lastDecode);
try {
if (lastDecode != null) lastDecode.join();
} catch (final CompletionException ignored) {
}
final Inflater inflater = new Inflater();
inflater.setInput(frameData);
@@ -281,7 +244,7 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
}
} catch (final DataFormatException ignored) {
}
});
}, DECODER_WORKERS);
}
///////////////////////////////////////////////////////////////
@@ -299,12 +262,43 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
return Config.projectorEnergyStorage > 0 && Config.projectorEnergyPerTick > 0;
}
private void sendRunningState() {
private void sendProjectorState() {
if (level != null && !level.isClientSide()) {
Network.sendToClientsTrackingBlockEntity(new ProjectorStateMessage(this, isProjecting && hasEnergy), this);
}
}
@Nullable
private ByteBuffer encodeFrame() {
final boolean hasChanges = projectorDevice.applyChanges(picture);
if (!hasChanges && !needsIDR) {
return null;
}
encoderBuffer.clear();
final ByteBuffer frameData;
try {
if (needsIDR) {
frameData = encoder.encodeIDRFrame(picture, encoderBuffer);
needsIDR = false;
} else {
frameData = encoder.encodeFrame(picture, encoderBuffer).data();
}
} catch (final BufferOverflowException ignored) {
return null;
}
final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
deflater.setInput(frameData);
deflater.finish();
final ByteBuffer compressedFrameData = ByteBuffer.allocateDirect(1024 * 1024);
deflater.deflate(compressedFrameData, Deflater.FULL_FLUSH);
deflater.end();
compressedFrameData.flip();
return compressedFrameData;
}
private void updateRenderBounds() {
final Direction blockFacing = getBlockState().getValue(ProjectorBlock.FACING);
final Direction canvasUp = Direction.UP;
@@ -319,17 +313,4 @@ public final class ProjectorBlockEntity extends ModBlockEntity implements Tickab
renderBounds = new AABB(getBlockPos()).minmax(new AABB(screenMinPos)).minmax(new AABB(screenMaxPos));
}
private static void joinWorkerAndLogErrors(@Nullable final Future<?> job) {
if (job == null) {
return;
}
try {
job.get();
} catch (final InterruptedException ignored) {
} catch (final ExecutionException e) {
LOGGER.error(e);
}
}
}

View File

@@ -44,13 +44,15 @@ public final class ProjectorVMDevice extends IdentityProxy<ProjectorBlockEntity>
///////////////////////////////////////////////////////////////
public boolean hasChanges() {
synchronized (deviceLock) {
return device != null && device.hasChanges();
}
}
public boolean applyChanges(final Picture picture) {
synchronized (deviceLock) {
if (device != null) {
return device.applyChanges(picture);
} else {
return false;
}
return device != null && device.applyChanges(picture);
}
}

View File

@@ -10,20 +10,30 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public final class MessageUtils {
@SuppressWarnings("unchecked")
public static <T extends BlockEntity> void withNearbyServerBlockEntityAt(final NetworkEvent.Context context, final BlockPos pos, final Class<T> type, final Consumer<T> callback) {
public static <T extends BlockEntity> void withNearbyServerBlockEntityForInteraction(final NetworkEvent.Context context, final BlockPos pos, final Class<T> type, final BiConsumer<ServerPlayer, T> callback) {
final ServerPlayer player = context.getSender();
if (player == null || !pos.closerThan(player.position(), 8)) {
return;
}
withNearbyServerBlockEntity(context, pos, type, callback);
}
@SuppressWarnings("unchecked")
public static <T extends BlockEntity> void withNearbyServerBlockEntity(final NetworkEvent.Context context, final BlockPos pos, final Class<T> type, final BiConsumer<ServerPlayer, T> callback) {
final ServerPlayer player = context.getSender();
if (player == null) {
return;
}
final ServerLevel level = player.getLevel();
final BlockEntity blockEntity = LevelUtils.getBlockEntityIfChunkExists(level, pos);
if (type.isInstance(blockEntity)) {
callback.accept((T) blockEntity);
callback.accept(player, (T) blockEntity);
}
}

View File

@@ -74,6 +74,7 @@ public final class Network {
registerMessage(NetworkInterfaceCardConfigurationMessage.class, NetworkInterfaceCardConfigurationMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(NetworkTunnelLinkMessage.class, NetworkTunnelLinkMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(ProjectorRequestFramebufferMessage.class, ProjectorRequestFramebufferMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(ProjectorFramebufferMessage.class, ProjectorFramebufferMessage::new, NetworkDirection.PLAY_TO_CLIENT);
registerMessage(ProjectorStateMessage.class, ProjectorStateMessage::new, NetworkDirection.PLAY_TO_CLIENT);
}

View File

@@ -0,0 +1,307 @@
package li.cil.oc2.common.network;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalNotification;
import li.cil.oc2.api.API;
import li.cil.oc2.common.Config;
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
import li.cil.oc2.common.network.message.ProjectorFramebufferMessage;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.server.ServerStoppedEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import static java.util.Objects.requireNonNull;
/**
* Mostly round-robin load balancer for allowing projectors to send data to clients.
* <p>
* We try to satisfy two limits here:
* <ul>
* <li>
* Server side, limiting overall bandwidth consumed by projectors.
* </li>
* <li>
* Client side, limiting per-client bandwidth consumed by projectors.
* </li>
* </ul>
* <p>
* To achieve this, there's a global budget and a per-projector skip count. The global budget
* controls overall data sent from the server. The skip counts modulate the round-robin behaviour
* of the load balancer. For example, projectors further away from their closest player will get
* a penalty, as will projectors with a large number of players watching them.
*/
@Mod.EventBusSubscriber(modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public final class ProjectorLoadBalancer {
private static final long CACHE_EXPIRES_AFTER = 2000; /* In milliseconds */
/**
* Maps projectors to their specific info, i.e. players watching them and sending state.
* <p>
* This is used to let projectors register new frames to be sent, and to register new players
* with already being tracked projectors.
* <p>
* Player watching state is expired manually, by checking the last update time for players in
* the info instance every tick. This is required in case two players start watching a projector
* but then only one keeps watching it. In that case, we want to still remove the other player
* from the projector info, but keep the info for the still watching player.
*/
private static final Cache<ProjectorBlockEntity, ProjectorInfo> PROJECTOR_INFO = CacheBuilder.newBuilder()
.weakKeys()
.expireAfterWrite(Duration.ofMillis(CACHE_EXPIRES_AFTER))
.removalListener(ProjectorLoadBalancer::handleProjectorInfoRemoved)
.build();
/**
* Global byte budget for sending stuff to clients. This is filled up every tick and consumed
* when sending packets to clients. We only send stuff when budget is non-negative.
*/
private static final AtomicInteger BUDGET = new AtomicInteger(getMaxBudget());
/**
* Pointer into our circular projector linked list pointing to the projector we last sent a packet for.
* <p>
* At the same time represents the head of our doubly-linked, circular list of projectors. This list is
* used to do the round-robin load balancing, by advancing it to the next projector until we find one
* that can send something every tick.
*/
@Nullable private static ProjectorInfo lastSender;
/**
* Updates timestamp of a player currently watching a projector.
*/
public static void updateWatcher(final ProjectorBlockEntity projector, final ServerPlayer player) {
try {
PROJECTOR_INFO.get(projector, () -> addProjectorInfo(projector))
.players.put(player, System.currentTimeMillis());
} catch (final ExecutionException e) {
throw new RuntimeException(e);
}
}
/**
* Notifies the load balancer that a projector has data to send.
* <p>
* Ignored if there are no players watching the projector.
*/
public static void offerFrame(final ProjectorBlockEntity projector, final Supplier<ByteBuffer> messageSupplier) {
final ProjectorInfo info = PROJECTOR_INFO.getIfPresent(projector);
if (info != null) {
info.nextFrameSupplier = messageSupplier;
}
}
/**
* Expires cached values. Checks if we can send something, and if so starts async
* generation of the package to send.
*/
@SubscribeEvent
public static void handleServerTick(final TickEvent.ServerTickEvent event) {
PROJECTOR_INFO.cleanUp();
removeExpiredPlayers();
if (BUDGET.updateAndGet(ProjectorLoadBalancer::replenishBudget) > 0) {
sendNextReadyPacket();
}
}
/**
* Cleanup when server stops.
*/
@SubscribeEvent
public static void handleServerStopped(final ServerStoppedEvent event) {
PROJECTOR_INFO.invalidateAll();
}
private static int getMaxBudget() {
// We allow over-budgeting projectors to some degree, to allow short bursts of larger frame changes.
// Otherwise, this would be divided by twenty, since we attempt to send every tick.
return Config.projectorAverageMaxBytesPerSecond / 2;
}
private static int replenishBudget(final int budget) {
return Math.min(getMaxBudget(), budget + Math.max(1, Config.projectorAverageMaxBytesPerSecond / 20));
}
private static ProjectorInfo addProjectorInfo(final ProjectorBlockEntity projector) {
projector.setRequiresKeyframe(); // When first watcher starts, immediately request keyframe.
final ProjectorInfo info = new ProjectorInfo(projector.getBlockPos());
if (lastSender == null) {
// No sender yet, start the circle.
lastSender = info;
} else {
// Just add after last sender.
lastSender.add(info);
}
return info;
}
private static void handleProjectorInfoRemoved(final RemovalNotification<ProjectorBlockEntity, ProjectorInfo> notification) {
final ProjectorInfo info = requireNonNull(notification.getValue());
if (lastSender == info) {
if (lastSender.next == lastSender) {
lastSender = null; // Last element in list, clear list.
} else {
lastSender = info.next; // Shift current entry to next.
}
}
info.remove();
}
private static void removeExpiredPlayers() {
if (lastSender == null) {
return;
}
final ProjectorInfo start = lastSender;
do {
lastSender.removeExpiredPlayers();
lastSender = lastSender.next;
} while (lastSender != start);
}
private static void sendNextReadyPacket() {
if (lastSender == null) {
return;
}
final ProjectorInfo start = lastSender;
do {
lastSender = lastSender.next;
if (lastSender.sendIfReady()) {
return;
}
} while (lastSender != start);
}
/**
* Tracks info for a single projector. This class is an entry in a circular double linked list,
* i.e. the last entry will always point back to the first entry, which makes looping over it
* for the round-robin load-balancing more comfortable.
*/
private static class ProjectorInfo {
private static final ExecutorService ENCODER_WORKERS = Executors.newCachedThreadPool(r -> {
final Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("Projector Frame Encoder");
return thread;
});
/**
* Pointers to next and previous entries in linked list. May point to this if it's the only entry.
*/
private ProjectorInfo next, previous;
/**
* Position of the projector this info is for in the world.
*/
private final BlockPos projectorPos;
/**
* The players watching this projector, to know where to send the data and how strongly to penalize.
*/
private final WeakHashMap<ServerPlayer, Long> players = new WeakHashMap<>();
/**
* The current penalty, in the form of rounds in the round-robin to skip.
*/
private int skipCount;
@Nullable private Supplier<ByteBuffer> nextFrameSupplier;
@Nullable private Future<?> runningEncode;
public ProjectorInfo(final BlockPos projectorPos) {
next = previous = this;
this.projectorPos = projectorPos;
}
public void add(final ProjectorInfo info) {
info.next = next;
next.previous = info;
next = info;
info.previous = this;
}
public void remove() {
if (previous == null) {
return;
}
previous.next = next;
next.previous = previous;
previous = null;
next = null;
}
public void removeExpiredPlayers() {
players.entrySet().removeIf(entry -> System.currentTimeMillis() - entry.getValue() > CACHE_EXPIRES_AFTER);
}
public boolean sendIfReady() {
if (skipCount > 0) {
skipCount--;
return false;
}
final boolean isReady = !players.isEmpty() && nextFrameSupplier != null && (runningEncode == null || runningEncode.isDone());
if (isReady) {
sendAsync();
updateSkipCount();
}
return isReady;
}
private void sendAsync() {
assert nextFrameSupplier != null;
final Supplier<ByteBuffer> frameSupplier = nextFrameSupplier;
nextFrameSupplier = null;
assert runningEncode == null || runningEncode.isDone();
runningEncode = ENCODER_WORKERS.submit(() -> {
final ByteBuffer frame = frameSupplier.get();
final int budgetCost = frame.limit() * players.size();
BUDGET.accumulateAndGet(budgetCost, (budget, cost) -> budget - cost);
final ProjectorFramebufferMessage message = new ProjectorFramebufferMessage(projectorPos, frame);
for (final ServerPlayer player : players.keySet()) {
Network.sendToClient(message, player);
}
});
}
private void updateSkipCount() {
skipCount = 0;
double closestPlayerDistanceSqr = Double.MAX_VALUE;
final Vec3 blockCenter = Vec3.atCenterOf(projectorPos);
for (final ServerPlayer player : players.keySet()) {
skipCount++;
final double distance = player.distanceToSqr(blockCenter);
closestPlayerDistanceSqr = Math.min(closestPlayerDistanceSqr, distance);
}
final double closestPlayerDistance = Math.sqrt(closestPlayerDistanceSqr);
if (closestPlayerDistance > 16) {
skipCount++;
}
}
}
}

View File

@@ -69,8 +69,8 @@ public abstract class BusInterfaceNameMessage extends AbstractMessage {
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withNearbyServerBlockEntityAt(context, pos, BusCableBlockEntity.class,
busCable -> busCable.setInterfaceName(side, value));
MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, BusCableBlockEntity.class,
(player, busCable) -> busCable.setInterfaceName(side, value));
}
}
}

View File

@@ -39,8 +39,8 @@ public final class ComputerPowerMessage extends AbstractMessage {
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withNearbyServerBlockEntityAt(context, pos, ComputerBlockEntity.class,
computer -> {
MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, ComputerBlockEntity.class,
(player, computer) -> {
if (power) {
computer.start();
} else {

View File

@@ -20,7 +20,7 @@ public final class ComputerTerminalInputMessage extends AbstractTerminalBlockMes
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withNearbyServerBlockEntityAt(context, pos, ComputerBlockEntity.class,
computer -> computer.getTerminal().putInput(ByteBuffer.wrap(data)));
MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, ComputerBlockEntity.class,
(player, computer) -> computer.getTerminal().putInput(ByteBuffer.wrap(data)));
}
}

View File

@@ -4,7 +4,6 @@ import li.cil.oc2.common.blockentity.ComputerBlockEntity;
import li.cil.oc2.common.network.MessageUtils;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
public final class OpenComputerInventoryMessage extends AbstractMessage {
@@ -35,10 +34,7 @@ public final class OpenComputerInventoryMessage extends AbstractMessage {
@Override
protected void handleMessage(final NetworkEvent.Context context) {
final ServerPlayer player = context.getSender();
if (player != null) {
MessageUtils.withNearbyServerBlockEntityAt(context, pos, ComputerBlockEntity.class,
computer -> computer.openInventoryScreen(player));
}
MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, ComputerBlockEntity.class,
(player, computer) -> computer.openInventoryScreen(player));
}
}

View File

@@ -4,7 +4,6 @@ import li.cil.oc2.common.blockentity.ComputerBlockEntity;
import li.cil.oc2.common.network.MessageUtils;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
public final class OpenComputerTerminalMessage extends AbstractMessage {
@@ -36,10 +35,7 @@ public final class OpenComputerTerminalMessage extends AbstractMessage {
@Override
protected void handleMessage(final NetworkEvent.Context context) {
final ServerPlayer player = context.getSender();
if (player != null) {
MessageUtils.withNearbyServerBlockEntityAt(context, pos, ComputerBlockEntity.class,
computer -> computer.openTerminalScreen(player));
}
MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, ComputerBlockEntity.class,
(player, computer) -> computer.openTerminalScreen(player));
}
}

View File

@@ -14,8 +14,8 @@ public final class ProjectorFramebufferMessage extends AbstractMessage {
///////////////////////////////////////////////////////////////////
public ProjectorFramebufferMessage(final ProjectorBlockEntity projector, final ByteBuffer frame) {
this.pos = projector.getBlockPos();
public ProjectorFramebufferMessage(final BlockPos projectorPos, final ByteBuffer frame) {
this.pos = projectorPos;
this.frame = frame;
}
@@ -44,8 +44,7 @@ public final class ProjectorFramebufferMessage extends AbstractMessage {
@Override
protected void handleMessage(final NetworkEvent.Context context) {
context.enqueueWork(() ->
MessageUtils.withClientBlockEntityAt(pos, ProjectorBlockEntity.class,
projector -> projector.applyNextFrame(frame)));
MessageUtils.withClientBlockEntityAt(pos, ProjectorBlockEntity.class,
projector -> projector.applyNextFrame(frame));
}
}

View File

@@ -0,0 +1,42 @@
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
import li.cil.oc2.common.network.MessageUtils;
import li.cil.oc2.common.network.ProjectorLoadBalancer;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
public final class ProjectorRequestFramebufferMessage extends AbstractMessage {
private BlockPos pos;
///////////////////////////////////////////////////////////////////
public ProjectorRequestFramebufferMessage(final ProjectorBlockEntity projector) {
this.pos = projector.getBlockPos();
}
public ProjectorRequestFramebufferMessage(final FriendlyByteBuf buffer) {
super(buffer);
}
///////////////////////////////////////////////////////////////////
@Override
public void fromBytes(final FriendlyByteBuf buffer) {
pos = buffer.readBlockPos();
}
@Override
public void toBytes(final FriendlyByteBuf buffer) {
buffer.writeBlockPos(pos);
}
///////////////////////////////////////////////////////////////////
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withNearbyServerBlockEntity(context, pos, ProjectorBlockEntity.class,
(player, projector) -> ProjectorLoadBalancer.updateWatcher(projector, player));
}
}

View File

@@ -35,6 +35,7 @@ public final class SimpleFramebufferDevice implements MemoryMappedDevice {
this.buffer = buffer.order(ByteOrder.LITTLE_ENDIAN);
this.dirtyLines = new BitSet(height / 2);
this.dirtyLines.set(0, height / 2);
}
///////////////////////////////////////////////////////////////
@@ -52,6 +53,10 @@ public final class SimpleFramebufferDevice implements MemoryMappedDevice {
return height;
}
public boolean hasChanges() {
return !dirtyLines.isEmpty();
}
public boolean applyChanges(final Picture picture) {
if (dirtyLines.isEmpty()) {
return false;