mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-07-05 19:42:54 +00:00

Here's a fun bug you can try at home: - Create a new world - Spawn in a pocket computer, turn it on, and place it in a chest. - Reload the world - the pocket computer in the chest should now be off. - Spawn in a new pocket computer, and turn it on. The computer in chest will also appear to be on! This bug has been present since pocket computers were added (27th March, 2024). When a pocket computer is added to a player's inventory, it is assigned a unique *per-session* "instance id" , which is used to find the associated computer. Note the "per-session" there - these ids will be reused if you reload the world (or restart the server). In the above bug, we see the following: - The first pocket computer is assigned an instance id of 0. - After reloading, the second pocket computer is assigned an instance id of 0. - If the first pocket computer was in our inventory, it'd be ticked and assigned a new instance id. However, because it's in an inventory, it keeps its old one. - Both computers look up their client-side computer state and get the same value, meaning the first pocket computer mirrors the second! To fix this, we now ensure instance ids are entirely unique (not just per-session). Rather than sequentially assigning an int, we now use a random UUID (we probably could get away with a random long, but this feels more idiomatic). This has a couple of user-visible changes: - /computercraft no longer lists instance ids outside of dumping an individual computer. - The @c[instance=...] selector uses UUIDs. We still use int instance ids for the legacy selector, but that'll be removed in a later MC version. - Pocket computers now store a UUID rather than an int. Related to this change (I made this change first, but then they got kinda mixed up together), we now only create PocketComputerData when receiving server data. This makes the code a little uglier in some places (the data may now be null), but means we don't populate the client-side pocket computer map with computers the server doesn't know about.
196 lines
7.1 KiB
Java
196 lines
7.1 KiB
Java
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
|
//
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package dan200.computercraft.shared.pocket.core;
|
|
|
|
import dan200.computercraft.api.peripheral.IPeripheral;
|
|
import dan200.computercraft.api.pocket.IPocketAccess;
|
|
import dan200.computercraft.api.pocket.IPocketUpgrade;
|
|
import dan200.computercraft.api.upgrades.UpgradeData;
|
|
import dan200.computercraft.core.computer.ComputerSide;
|
|
import dan200.computercraft.shared.common.IColouredItem;
|
|
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
|
import dan200.computercraft.shared.computer.core.ComputerState;
|
|
import dan200.computercraft.shared.computer.core.ServerComputer;
|
|
import dan200.computercraft.shared.config.Config;
|
|
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
|
|
import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage;
|
|
import dan200.computercraft.shared.network.server.ServerNetworking;
|
|
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
import net.minecraft.resources.ResourceLocation;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.entity.LivingEntity;
|
|
import net.minecraft.world.entity.item.ItemEntity;
|
|
import net.minecraft.world.entity.player.Player;
|
|
import net.minecraft.world.item.ItemStack;
|
|
|
|
import javax.annotation.Nullable;
|
|
import java.util.*;
|
|
|
|
public class PocketServerComputer extends ServerComputer implements IPocketAccess {
|
|
private @Nullable IPocketUpgrade upgrade;
|
|
private @Nullable Entity entity;
|
|
private ItemStack stack = ItemStack.EMPTY;
|
|
|
|
private int lightColour = -1;
|
|
|
|
// The state the previous tick, used to determine if the state needs to be sent to the client.
|
|
private int oldLightColour = -1;
|
|
private @Nullable ComputerState oldComputerState;
|
|
|
|
private final Set<ServerPlayer> tracking = new HashSet<>();
|
|
|
|
public PocketServerComputer(ServerLevel world, BlockPos position, int computerID, @Nullable String label, ComputerFamily family) {
|
|
super(world, position, computerID, label, family, Config.pocketTermWidth, Config.pocketTermHeight);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public Entity getEntity() {
|
|
var entity = this.entity;
|
|
if (entity == null || stack.isEmpty() || !entity.isAlive()) return null;
|
|
|
|
if (entity instanceof Player) {
|
|
var inventory = ((Player) entity).getInventory();
|
|
return inventory.items.contains(stack) || inventory.offhand.contains(stack) ? entity : null;
|
|
} else if (entity instanceof LivingEntity living) {
|
|
return living.getMainHandItem() == stack || living.getOffhandItem() == stack ? entity : null;
|
|
} else if (entity instanceof ItemEntity itemEntity) {
|
|
return itemEntity.getItem() == stack ? entity : null;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int getColour() {
|
|
return IColouredItem.getColourBasic(stack);
|
|
}
|
|
|
|
@Override
|
|
public void setColour(int colour) {
|
|
IColouredItem.setColourBasic(stack, colour);
|
|
updateUpgradeNBTData();
|
|
}
|
|
|
|
@Override
|
|
public int getLight() {
|
|
return lightColour;
|
|
}
|
|
|
|
@Override
|
|
public void setLight(int colour) {
|
|
if (colour < 0 || colour > 0xFFFFFF) colour = -1;
|
|
lightColour = colour;
|
|
}
|
|
|
|
@Override
|
|
public CompoundTag getUpgradeNBTData() {
|
|
return PocketComputerItem.getUpgradeInfo(stack);
|
|
}
|
|
|
|
@Override
|
|
public void updateUpgradeNBTData() {
|
|
if (entity instanceof Player player) player.getInventory().setChanged();
|
|
}
|
|
|
|
@Override
|
|
public void invalidatePeripheral() {
|
|
var peripheral = upgrade == null ? null : upgrade.createPeripheral(this);
|
|
setPeripheral(ComputerSide.BACK, peripheral);
|
|
}
|
|
|
|
@Override
|
|
@Deprecated(forRemoval = true)
|
|
public Map<ResourceLocation, IPeripheral> getUpgrades() {
|
|
return upgrade == null ? Map.of() : Collections.singletonMap(upgrade.getUpgradeID(), getPeripheral(ComputerSide.BACK));
|
|
}
|
|
|
|
public @Nullable UpgradeData<IPocketUpgrade> getUpgrade() {
|
|
return upgrade == null ? null : UpgradeData.of(upgrade, getUpgradeNBTData());
|
|
}
|
|
|
|
/**
|
|
* Set the upgrade for this pocket computer, also updating the item stack.
|
|
* <p>
|
|
* Note this method is not thread safe - it must be called from the server thread.
|
|
*
|
|
* @param upgrade The new upgrade to set it to, may be {@code null}.
|
|
*/
|
|
public void setUpgrade(@Nullable UpgradeData<IPocketUpgrade> upgrade) {
|
|
synchronized (this) {
|
|
PocketComputerItem.setUpgrade(stack, upgrade);
|
|
updateUpgradeNBTData();
|
|
this.upgrade = upgrade == null ? null : upgrade.upgrade();
|
|
invalidatePeripheral();
|
|
}
|
|
}
|
|
|
|
public synchronized void updateValues(@Nullable Entity entity, ItemStack stack, @Nullable IPocketUpgrade upgrade) {
|
|
if (entity != null) {
|
|
setLevel((ServerLevel) entity.getCommandSenderWorld());
|
|
setPosition(entity.blockPosition());
|
|
}
|
|
|
|
// If a new entity has picked it up then rebroadcast the terminal to them
|
|
if (entity != this.entity && entity instanceof ServerPlayer) markTerminalChanged();
|
|
|
|
this.entity = entity;
|
|
this.stack = stack;
|
|
|
|
if (this.upgrade != upgrade) {
|
|
this.upgrade = upgrade;
|
|
invalidatePeripheral();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void tickServer() {
|
|
super.tickServer();
|
|
|
|
// Find any players which have gone missing and remove them from the tracking list.
|
|
tracking.removeIf(player -> !player.isAlive() || player.level() != getLevel());
|
|
|
|
// And now find any new players, add them to the tracking list, and broadcast state where appropriate.
|
|
var state = getState();
|
|
if (oldLightColour != lightColour || oldComputerState != state) {
|
|
oldComputerState = state;
|
|
oldLightColour = lightColour;
|
|
|
|
// Broadcast the state to all players
|
|
tracking.addAll(getLevel().players());
|
|
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), tracking);
|
|
} else {
|
|
// Broadcast the state to new players.
|
|
List<ServerPlayer> added = new ArrayList<>();
|
|
for (var player : getLevel().players()) {
|
|
if (tracking.add(player)) added.add(player);
|
|
}
|
|
if (!added.isEmpty()) {
|
|
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), added);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onTerminalChanged() {
|
|
super.onTerminalChanged();
|
|
|
|
if (entity instanceof ServerPlayer player && entity.isAlive()) {
|
|
// Broadcast the terminal to the current player.
|
|
ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), player);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onRemoved() {
|
|
super.onRemoved();
|
|
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceUUID()), getLevel().getServer());
|
|
}
|
|
}
|