1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-01-24 16:07:01 +00:00

Replace integer instance IDs with UUIDs

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.
This commit is contained in:
Jonathan Coates 2024-03-17 12:21:21 +00:00
parent 1a5dc92bd4
commit 5d8c46c7e6
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
20 changed files with 196 additions and 141 deletions

View File

@ -113,6 +113,8 @@ sourceSets.all {
option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull") option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
option("NullAway:CheckOptionalEmptiness") option("NullAway:CheckOptionalEmptiness")
option("NullAway:AcknowledgeRestrictiveAnnotations") option("NullAway:AcknowledgeRestrictiveAnnotations")
excludedPaths = ".*/jmh_generated/.*"
} }
} }
} }

View File

@ -22,6 +22,7 @@ import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.CommandComputerCraft; import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.common.IColouredItem; import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerContext; import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu; import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
@ -91,7 +92,10 @@ public final class ClientRegistry {
MenuScreens.<ViewComputerMenu, ComputerScreen<ViewComputerMenu>>register(ModRegistry.Menus.VIEW_COMPUTER.get(), ComputerScreen::new); MenuScreens.<ViewComputerMenu, ComputerScreen<ViewComputerMenu>>register(ModRegistry.Menus.VIEW_COMPUTER.get(), ComputerScreen::new);
registerItemProperty("state", registerItemProperty("state",
new UnclampedPropertyFunction((stack, world, player, random) -> ClientPocketComputers.get(stack).getState().ordinal()), new UnclampedPropertyFunction((stack, world, player, random) -> {
var computer = ClientPocketComputers.get(stack);
return (computer == null ? ComputerState.OFF : computer.getState()).ordinal();
}),
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
); );
registerItemProperty("coloured", registerItemProperty("coloured",
@ -155,17 +159,14 @@ public final class ClientRegistry {
} }
private static int getPocketColour(ItemStack stack, int layer) { private static int getPocketColour(ItemStack stack, int layer) {
switch (layer) { return switch (layer) {
case 0: default -> 0xFFFFFF;
default: case 1 -> IColouredItem.getColourBasic(stack); // Frame colour
return 0xFFFFFF; case 2 -> { // Light colour
case 1: // Frame colour var computer = ClientPocketComputers.get(stack);
return IColouredItem.getColourBasic(stack); yield computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
case 2: { // Light colour
var light = ClientPocketComputers.get(stack).getLightState();
return light == -1 ? Colour.BLACK.getHex() : light;
} }
} };
} }
private static int getTurtleColour(ItemStack stack, int layer) { private static int getTurtleColour(ItemStack stack, int layer) {

View File

@ -67,14 +67,12 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext {
} }
@Override @Override
public void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) { public void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal) {
var computer = ClientPocketComputers.get(instanceId, terminal.colour); ClientPocketComputers.setState(instanceId, state, lightState, terminal);
computer.setState(state, lightState);
if (terminal.hasTerminal()) computer.setTerminal(terminal);
} }
@Override @Override
public void handlePocketComputerDeleted(int instanceId) { public void handlePocketComputerDeleted(UUID instanceId) {
ClientPocketComputers.remove(instanceId); ClientPocketComputers.remove(instanceId);
} }

View File

@ -4,21 +4,26 @@
package dan200.computercraft.client.pocket; package dan200.computercraft.client.pocket;
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.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage; import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/** /**
* Maps {@link ServerComputer#getInstanceID()} to locals {@link PocketComputerData}. * Maps {@link ServerComputer#getInstanceUUID()} to locals {@link PocketComputerData}.
* <p> * <p>
* This is populated by {@link PocketComputerDataMessage} and accessed when rendering pocket computers * This is populated by {@link PocketComputerDataMessage} and accessed when rendering pocket computers
*/ */
public final class ClientPocketComputers { public final class ClientPocketComputers {
private static final Int2ObjectMap<PocketComputerData> instances = new Int2ObjectOpenHashMap<>(); private static final Map<UUID, PocketComputerData> instances = new HashMap<>();
private ClientPocketComputers() { private ClientPocketComputers() {
} }
@ -27,25 +32,32 @@ public final class ClientPocketComputers {
instances.clear(); instances.clear();
} }
public static void remove(int id) { public static void remove(UUID id) {
instances.remove(id); instances.remove(id);
} }
/** /**
* Get or create a pocket computer. * Set the state of a pocket computer.
* *
* @param instanceId The instance ID of the pocket computer. * @param instanceId The instance ID of the pocket computer.
* @param advanced Whether this computer has an advanced terminal. * @param state The computer state of the pocket computer.
* @return The pocket computer data. * @param lightColour The current colour of the modem light.
* @param terminalData The current terminal contents.
*/ */
public static PocketComputerData get(int instanceId, boolean advanced) { public static void setState(UUID instanceId, ComputerState state, int lightColour, TerminalState terminalData) {
var computer = instances.get(instanceId); var computer = instances.get(instanceId);
if (computer == null) instances.put(instanceId, computer = new PocketComputerData(advanced)); if (computer == null) {
return computer; var terminal = new NetworkedTerminal(terminalData.width, terminalData.height, terminalData.colour);
instances.put(instanceId, computer = new PocketComputerData(state, lightColour, terminal));
} else {
computer.setState(state, lightColour);
}
if (terminalData.hasTerminal()) terminalData.apply(computer.getTerminal());
} }
public static PocketComputerData get(ItemStack stack) { public static @Nullable PocketComputerData get(ItemStack stack) {
var family = stack.getItem() instanceof PocketComputerItem computer ? computer.getFamily() : ComputerFamily.NORMAL; var id = PocketComputerItem.getInstanceID(stack);
return get(PocketComputerItem.getInstanceID(stack), family != ComputerFamily.NORMAL); return id == null ? null : instances.get(id);
} }
} }

View File

@ -4,11 +4,8 @@
package dan200.computercraft.client.pocket; package dan200.computercraft.client.pocket;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal; import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.pocket.core.PocketServerComputer; import dan200.computercraft.shared.pocket.core.PocketServerComputer;
/** /**
@ -21,20 +18,22 @@ import dan200.computercraft.shared.pocket.core.PocketServerComputer;
* @see ClientPocketComputers The registry which holds pocket computers. * @see ClientPocketComputers The registry which holds pocket computers.
* @see PocketServerComputer The server-side pocket computer. * @see PocketServerComputer The server-side pocket computer.
*/ */
public class PocketComputerData { public final class PocketComputerData {
private final NetworkedTerminal terminal; private final NetworkedTerminal terminal;
private ComputerState state = ComputerState.OFF; private ComputerState state;
private int lightColour = -1; private int lightColour;
public PocketComputerData(boolean colour) { PocketComputerData(ComputerState state, int lightColour, NetworkedTerminal terminal) {
terminal = new NetworkedTerminal(Config.pocketTermWidth, Config.pocketTermHeight, colour); this.state = state;
this.lightColour = lightColour;
this.terminal = terminal;
} }
public int getLightState() { public int getLightState() {
return state != ComputerState.OFF ? lightColour : -1; return state != ComputerState.OFF ? lightColour : -1;
} }
public Terminal getTerminal() { public NetworkedTerminal getTerminal() {
return terminal; return terminal;
} }
@ -42,12 +41,8 @@ public class PocketComputerData {
return state; return state;
} }
public void setState(ComputerState state, int lightColour) { void setState(ComputerState state, int lightColour) {
this.state = state; this.state = state;
this.lightColour = lightColour; this.lightColour = lightColour;
} }
public void setTerminal(TerminalState state) {
state.apply(terminal);
}
} }

View File

@ -11,6 +11,7 @@ import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer; import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.util.Colour; import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
@ -32,10 +33,16 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
@Override @Override
protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, ItemStack stack, int light) { protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, ItemStack stack, int light) {
var computer = ClientPocketComputers.get(stack); var computer = ClientPocketComputers.get(stack);
var terminal = computer.getTerminal(); var terminal = computer == null ? null : computer.getTerminal();
var termWidth = terminal.getWidth(); int termWidth, termHeight;
var termHeight = terminal.getHeight(); if (terminal == null) {
termWidth = Config.pocketTermWidth;
termHeight = Config.pocketTermHeight;
} else {
termWidth = terminal.getWidth();
termHeight = terminal.getHeight();
}
var width = termWidth * FONT_WIDTH + MARGIN * 2; var width = termWidth * FONT_WIDTH + MARGIN * 2;
var height = termHeight * FONT_HEIGHT + MARGIN * 2; var height = termHeight * FONT_HEIGHT + MARGIN * 2;
@ -60,14 +67,15 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
renderFrame(matrix, bufferSource, family, frameColour, light, width, height); renderFrame(matrix, bufferSource, family, frameColour, light, width, height);
// Render the light // Render the light
var lightColour = ClientPocketComputers.get(stack).getLightState(); var lightColour = computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
if (lightColour == -1) lightColour = Colour.BLACK.getHex();
renderLight(transform, bufferSource, lightColour, width, height); renderLight(transform, bufferSource, lightColour, width, height);
FixedWidthFontRenderer.drawTerminal( var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)), if (terminal == null) {
MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
); } else {
FixedWidthFontRenderer.drawTerminal(quadEmitter, MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN);
}
transform.popPose(); transform.popPose();
} }

View File

@ -134,7 +134,7 @@ public final class CommandComputerCraft {
} else if (b.getLevel() == world) { } else if (b.getLevel() == world) {
return 1; return 1;
} else { } else {
return Integer.compare(a.getInstanceID(), b.getInstanceID()); return a.getInstanceUUID().compareTo(b.getInstanceUUID());
} }
}); });
@ -159,7 +159,8 @@ public final class CommandComputerCraft {
*/ */
private static int dumpComputer(CommandSourceStack source, ServerComputer computer) { private static int dumpComputer(CommandSourceStack source, ServerComputer computer) {
var table = new TableBuilder("Dump"); var table = new TableBuilder("Dump");
table.row(header("Instance"), text(Integer.toString(computer.getInstanceID()))); table.row(header("Instance ID"), text(Integer.toString(computer.getInstanceID())));
table.row(header("Instance UUID"), text(computer.getInstanceUUID().toString()));
table.row(header("Id"), text(Integer.toString(computer.getID()))); table.row(header("Id"), text(Integer.toString(computer.getID())));
table.row(header("Label"), text(computer.getLabel())); table.row(header("Label"), text(computer.getLabel()));
table.row(header("On"), bool(computer.isOn())); table.row(header("On"), bool(computer.isOn()));
@ -338,11 +339,7 @@ public final class CommandComputerCraft {
if (computer == null) { if (computer == null) {
out.append("#" + computerId + " ").append(coloured("(unloaded)", ChatFormatting.GRAY)); out.append("#" + computerId + " ").append(coloured("(unloaded)", ChatFormatting.GRAY));
} else { } else {
out.append(link( out.append(makeComputerDumpCommand(computer));
text("#" + computerId + " ").append(coloured("(instance " + computer.getInstanceID() + ")", ChatFormatting.GRAY)),
makeComputerCommand("dump", computer),
Component.translatable("commands.computercraft.dump.action")
));
} }
// And, if we're a player, some useful links // And, if we're a player, some useful links
@ -372,10 +369,6 @@ public final class CommandComputerCraft {
return out; return out;
} }
private static String makeComputerCommand(String command, ServerComputer computer) {
return String.format("/computercraft %s @c[instance=%d]", command, computer.getInstanceID());
}
private static Component linkPosition(CommandSourceStack context, ServerComputer computer) { private static Component linkPosition(CommandSourceStack context, ServerComputer computer) {
if (ModRegistry.Permissions.PERMISSION_TP.test(context)) { if (ModRegistry.Permissions.PERMISSION_TP.test(context)) {
return link( return link(

View File

@ -16,7 +16,10 @@ import dan200.computercraft.shared.computer.core.ServerContext;
import net.minecraft.advancements.critereon.MinMaxBounds; import net.minecraft.advancements.critereon.MinMaxBounds;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.commands.arguments.UuidArgument;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentContents;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
@ -30,17 +33,21 @@ import java.util.stream.Stream;
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer; import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
import static dan200.computercraft.shared.command.Exceptions.*; import static dan200.computercraft.shared.command.Exceptions.*;
import static dan200.computercraft.shared.command.arguments.ArgumentParserUtils.consume; import static dan200.computercraft.shared.command.arguments.ArgumentParserUtils.consume;
import static dan200.computercraft.shared.command.text.ChatHelpers.makeComputerDumpCommand;
public record ComputerSelector( public record ComputerSelector(
String selector, String selector,
OptionalInt instanceId, OptionalInt instanceId,
@Nullable UUID instanceUuid,
OptionalInt computerId, OptionalInt computerId,
@Nullable String label, @Nullable String label,
@Nullable ComputerFamily family, @Nullable ComputerFamily family,
@Nullable AABB bounds, @Nullable AABB bounds,
@Nullable MinMaxBounds.Doubles range @Nullable MinMaxBounds.Doubles range
) { ) {
private static final ComputerSelector all = new ComputerSelector("@c[]", OptionalInt.empty(), OptionalInt.empty(), null, null, null, null); private static final ComputerSelector all = new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null);
private static UuidArgument uuidArgument = UuidArgument.uuid();
/** /**
* A {@link ComputerSelector} which matches all computers. * A {@link ComputerSelector} which matches all computers.
@ -59,8 +66,13 @@ public record ComputerSelector(
*/ */
public Stream<ServerComputer> find(CommandSourceStack source) { public Stream<ServerComputer> find(CommandSourceStack source) {
var context = ServerContext.get(source.getServer()); var context = ServerContext.get(source.getServer());
if (instanceId.isPresent()) { if (instanceId().isPresent()) {
var computer = context.registry().get(instanceId.getAsInt()); var computer = context.registry().get(instanceId().getAsInt());
return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
}
if (instanceUuid() != null) {
var computer = context.registry().get(instanceUuid());
return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of(); return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
} }
@ -79,7 +91,7 @@ public record ComputerSelector(
if (computers.isEmpty()) throw COMPUTER_ARG_NONE.create(selector); if (computers.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
if (computers.size() == 1) return computers.iterator().next(); if (computers.size() == 1) return computers.iterator().next();
var builder = new StringBuilder(); var builder = MutableComponent.create(ComponentContents.EMPTY);
var first = true; var first = true;
for (var computer : computers) { for (var computer : computers) {
if (first) { if (first) {
@ -88,12 +100,12 @@ public record ComputerSelector(
builder.append(", "); builder.append(", ");
} }
builder.append(computer.getInstanceID()); builder.append(makeComputerDumpCommand(computer));
} }
// We have an incorrect number of computers: throw an error // We have an incorrect number of computers: throw an error
throw COMPUTER_ARG_MANY.create(selector, builder.toString()); throw COMPUTER_ARG_MANY.create(selector, builder);
} }
/** /**
@ -105,6 +117,7 @@ public record ComputerSelector(
*/ */
public boolean matches(CommandSourceStack source, ServerComputer computer) { public boolean matches(CommandSourceStack source, ServerComputer computer) {
return (instanceId().isEmpty() || computer.getInstanceID() == instanceId().getAsInt()) return (instanceId().isEmpty() || computer.getInstanceID() == instanceId().getAsInt())
&& (instanceUuid() == null || computer.getInstanceUUID().equals(instanceUuid()))
&& (computerId().isEmpty() || computer.getID() == computerId().getAsInt()) && (computerId().isEmpty() || computer.getID() == computerId().getAsInt())
&& (label == null || Objects.equals(computer.getLabel(), label)) && (label == null || Objects.equals(computer.getLabel(), label))
&& (family == null || computer.getFamily() == family) && (family == null || computer.getFamily() == family)
@ -127,6 +140,7 @@ public record ComputerSelector(
if (consume(reader, "@c[")) { if (consume(reader, "@c[")) {
parseSelector(builder, reader); parseSelector(builder, reader);
} else { } else {
// TODO(1.20.5): Only parse computer ids here.
var kind = reader.peek(); var kind = reader.peek();
if (kind == '@') { if (kind == '@') {
reader.skip(); reader.skip();
@ -143,7 +157,7 @@ public record ComputerSelector(
} }
var selector = reader.getString().substring(start, reader.getCursor()); var selector = reader.getString().substring(start, reader.getCursor());
return new ComputerSelector(selector, builder.instanceId, builder.computerId, builder.label, builder.family, builder.bounds, builder.range); return new ComputerSelector(selector, builder.instanceId, builder.instanceUuid, builder.computerId, builder.label, builder.family, builder.bounds, builder.range);
} }
private static void parseSelector(Builder builder, StringReader reader) throws CommandSyntaxException { private static void parseSelector(Builder builder, StringReader reader) throws CommandSyntaxException {
@ -260,6 +274,7 @@ public record ComputerSelector(
private static final class Builder { private static final class Builder {
private OptionalInt instanceId = OptionalInt.empty(); private OptionalInt instanceId = OptionalInt.empty();
private @Nullable UUID instanceUuid = null;
private OptionalInt computerId = OptionalInt.empty(); private OptionalInt computerId = OptionalInt.empty();
private @Nullable String label; private @Nullable String label;
private @Nullable ComputerFamily family; private @Nullable ComputerFamily family;
@ -282,8 +297,8 @@ public record ComputerSelector(
var optionList = new Option[]{ var optionList = new Option[]{
new Option( new Option(
"instance", "instance",
(reader, builder) -> builder.instanceId = OptionalInt.of(reader.readInt()), (reader, builder) -> builder.instanceUuid = uuidArgument.parse(reader),
suggestComputers(c -> Integer.toString(c.getInstanceID())) suggestComputers(c -> c.getInstanceUUID().toString())
), ),
new Option( new Option(
"id", "id",

View File

@ -4,6 +4,7 @@
package dan200.computercraft.shared.command.text; package dan200.computercraft.shared.command.text;
import dan200.computercraft.shared.computer.core.ServerComputer;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.ClickEvent;
@ -73,4 +74,16 @@ public final class ChatHelpers {
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("gui.computercraft.tooltip.copy"))) .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("gui.computercraft.tooltip.copy")))
); );
} }
public static String makeComputerCommand(String command, ServerComputer computer) {
return String.format("/computercraft %s @c[instance=%s]", command, computer.getInstanceUUID());
}
public static Component makeComputerDumpCommand(ServerComputer computer) {
return link(
text("#" + computer.getID()),
makeComputerCommand("dump", computer),
Component.translatable("commands.computercraft.dump.action")
);
}
} }

View File

@ -36,13 +36,14 @@ import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
public abstract class AbstractComputerBlockEntity extends BlockEntity implements IComputerBlockEntity, Nameable, MenuProvider { public abstract class AbstractComputerBlockEntity extends BlockEntity implements IComputerBlockEntity, Nameable, MenuProvider {
private static final String NBT_ID = "ComputerId"; private static final String NBT_ID = "ComputerId";
private static final String NBT_LABEL = "Label"; private static final String NBT_LABEL = "Label";
private static final String NBT_ON = "On"; private static final String NBT_ON = "On";
private int instanceID = -1; private @Nullable UUID instanceID = null;
private int computerID = -1; private int computerID = -1;
protected @Nullable String label = null; protected @Nullable String label = null;
private boolean on = false; private boolean on = false;
@ -66,7 +67,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
var computer = getServerComputer(); var computer = getServerComputer();
if (computer != null) computer.close(); if (computer != null) computer.close();
instanceID = -1; instanceID = null;
} }
@Override @Override
@ -401,7 +402,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
} }
protected void transferStateFrom(AbstractComputerBlockEntity copy) { protected void transferStateFrom(AbstractComputerBlockEntity copy) {
if (copy.computerID != computerID || copy.instanceID != instanceID) { if (copy.computerID != computerID || !Objects.equals(copy.instanceID, instanceID)) {
unload(); unload();
instanceID = copy.instanceID; instanceID = copy.instanceID;
computerID = copy.computerID; computerID = copy.computerID;
@ -411,7 +412,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
lockCode = copy.lockCode; lockCode = copy.lockCode;
BlockEntityHelpers.updateBlock(this); BlockEntityHelpers.updateBlock(this);
} }
copy.instanceID = -1; copy.instanceID = null;
} }
@Override @Override

View File

@ -26,11 +26,13 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function; import java.util.function.Function;
public class ServerComputer implements InputHandler, ComputerEnvironment { public class ServerComputer implements InputHandler, ComputerEnvironment {
private final int instanceID; private final int instanceID;
private final UUID instanceUUID = UUID.randomUUID();
private ServerLevel level; private ServerLevel level;
private BlockPos position; private BlockPos position;
@ -119,9 +121,9 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
return computer.pollAndResetChanges(); return computer.pollAndResetChanges();
} }
public int register() { public UUID register() {
ServerContext.get(level.getServer()).registry().add(instanceID, this); ServerContext.get(level.getServer()).registry().add(this);
return instanceID; return instanceUUID;
} }
void unload() { void unload() {
@ -130,7 +132,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
public void close() { public void close() {
unload(); unload();
ServerContext.get(level.getServer()).registry().remove(instanceID); ServerContext.get(level.getServer()).registry().remove(this);
} }
private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage<ClientNetworkContext>> createPacket) { private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage<ClientNetworkContext>> createPacket) {
@ -150,6 +152,10 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
return instanceID; return instanceID;
} }
public UUID getInstanceUUID() {
return instanceUUID;
}
public int getID() { public int getID() {
return computer.getID(); return computer.getID();
} }

View File

@ -8,14 +8,14 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collection; import java.util.*;
import java.util.Random;
public class ServerComputerRegistry { public class ServerComputerRegistry {
private static final Random RANDOM = new Random(); private static final Random RANDOM = new Random();
private final int sessionId = RANDOM.nextInt(); private final int sessionId = RANDOM.nextInt();
private final Int2ObjectMap<ServerComputer> computers = new Int2ObjectOpenHashMap<>(); private final Int2ObjectMap<ServerComputer> computersByInstanceId = new Int2ObjectOpenHashMap<>();
private final Map<UUID, ServerComputer> computersByInstanceUuid = new HashMap<>();
private int nextInstanceId; private int nextInstanceId;
public int getSessionID() { public int getSessionID() {
@ -28,11 +28,16 @@ public class ServerComputerRegistry {
@Nullable @Nullable
public ServerComputer get(int instanceID) { public ServerComputer get(int instanceID) {
return instanceID >= 0 ? computers.get(instanceID) : null; return instanceID >= 0 ? computersByInstanceId.get(instanceID) : null;
} }
@Nullable @Nullable
public ServerComputer get(int sessionId, int instanceId) { public ServerComputer get(@Nullable UUID instanceID) {
return instanceID != null ? computersByInstanceUuid.get(instanceID) : null;
}
@Nullable
public ServerComputer get(int sessionId, @Nullable UUID instanceId) {
return sessionId == this.sessionId ? get(instanceId) : null; return sessionId == this.sessionId ? get(instanceId) : null;
} }
@ -50,28 +55,36 @@ public class ServerComputerRegistry {
} }
} }
void add(int instanceID, ServerComputer computer) { void add(ServerComputer computer) {
remove(instanceID); var instanceID = computer.getInstanceID();
computers.put(instanceID, computer); var instanceUUID = computer.getInstanceUUID();
nextInstanceId = Math.max(nextInstanceId, instanceID + 1);
}
void remove(int instanceID) { if (computersByInstanceId.containsKey(instanceID)) {
var computer = get(instanceID); throw new IllegalStateException("Duplicate computer " + instanceID);
if (computer != null) {
computer.unload();
computer.onRemoved();
} }
computers.remove(instanceID); if (computersByInstanceUuid.containsKey(instanceUUID)) {
throw new IllegalStateException("Duplicate computer " + instanceUUID);
}
computersByInstanceId.put(instanceID, computer);
computersByInstanceUuid.put(instanceUUID, computer);
}
void remove(ServerComputer computer) {
computer.unload();
computer.onRemoved();
computersByInstanceId.remove(computer.getInstanceID());
computersByInstanceUuid.remove(computer.getInstanceUUID());
} }
void close() { void close() {
for (var computer : getComputers()) computer.unload(); for (var computer : getComputers()) computer.unload();
computers.clear(); computersByInstanceId.clear();
computersByInstanceUuid.clear();
} }
public Collection<ServerComputer> getComputers() { public Collection<ServerComputer> getComputers() {
return computers.values(); return computersByInstanceId.values();
} }
} }

View File

@ -25,7 +25,7 @@ public class ViewComputerMenu extends ComputerMenuWithoutInventory {
private static boolean canInteractWith(ServerComputer computer, Player player) { private static boolean canInteractWith(ServerComputer computer, Player player) {
// If this computer no longer exists then discard it. // If this computer no longer exists then discard it.
if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceID()) != computer) { if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceUUID()) != computer) {
return false; return false;
} }

View File

@ -30,9 +30,9 @@ public interface ClientNetworkContext {
void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name); void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name);
void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal); void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal);
void handlePocketComputerDeleted(int instanceId); void handlePocketComputerDeleted(UUID instanceId);
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio audio); void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio audio);

View File

@ -13,24 +13,26 @@ import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.pocket.core.PocketServerComputer; import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import java.util.UUID;
/** /**
* Provides additional data about a client computer, such as its ID and current state. * Provides additional data about a client computer, such as its ID and current state.
*/ */
public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkContext> { public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkContext> {
private final int instanceId; private final UUID clientId;
private final ComputerState state; private final ComputerState state;
private final int lightState; private final int lightState;
private final TerminalState terminal; private final TerminalState terminal;
public PocketComputerDataMessage(PocketServerComputer computer, boolean sendTerminal) { public PocketComputerDataMessage(PocketServerComputer computer, boolean sendTerminal) {
instanceId = computer.getInstanceID(); clientId = computer.getInstanceUUID();
state = computer.getState(); state = computer.getState();
lightState = computer.getLight(); lightState = computer.getLight();
terminal = sendTerminal ? computer.getTerminalState() : new TerminalState((NetworkedTerminal) null); terminal = sendTerminal ? computer.getTerminalState() : new TerminalState((NetworkedTerminal) null);
} }
public PocketComputerDataMessage(FriendlyByteBuf buf) { public PocketComputerDataMessage(FriendlyByteBuf buf) {
instanceId = buf.readVarInt(); clientId = buf.readUUID();
state = buf.readEnum(ComputerState.class); state = buf.readEnum(ComputerState.class);
lightState = buf.readVarInt(); lightState = buf.readVarInt();
terminal = new TerminalState(buf); terminal = new TerminalState(buf);
@ -38,7 +40,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
@Override @Override
public void write(FriendlyByteBuf buf) { public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId); buf.writeUUID(clientId);
buf.writeEnum(state); buf.writeEnum(state);
buf.writeVarInt(lightState); buf.writeVarInt(lightState);
terminal.write(buf); terminal.write(buf);
@ -46,7 +48,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
@Override @Override
public void handle(ClientNetworkContext context) { public void handle(ClientNetworkContext context) {
context.handlePocketComputerData(instanceId, state, lightState, terminal); context.handlePocketComputerData(clientId, state, lightState, terminal);
} }
@Override @Override

View File

@ -9,21 +9,23 @@ import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages; import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import java.util.UUID;
public class PocketComputerDeletedClientMessage implements NetworkMessage<ClientNetworkContext> { public class PocketComputerDeletedClientMessage implements NetworkMessage<ClientNetworkContext> {
private final int instanceId; private final UUID instanceId;
public PocketComputerDeletedClientMessage(int instanceId) { public PocketComputerDeletedClientMessage(UUID instanceId) {
this.instanceId = instanceId; this.instanceId = instanceId;
} }
public PocketComputerDeletedClientMessage(FriendlyByteBuf buffer) { public PocketComputerDeletedClientMessage(FriendlyByteBuf buffer) {
instanceId = buffer.readVarInt(); instanceId = buffer.readUUID();
} }
@Override @Override
public void write(FriendlyByteBuf buf) { public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId); buf.writeUUID(instanceId);
} }
@Override @Override

View File

@ -190,6 +190,6 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
@Override @Override
protected void onRemoved() { protected void onRemoved() {
super.onRemoved(); super.onRemoved();
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer()); ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceUUID()), getLevel().getServer());
} }
} }

View File

@ -43,6 +43,7 @@ import net.minecraft.world.level.Level;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
public class PocketComputerItem extends Item implements IComputerItem, IMedia, IColouredItem { public class PocketComputerItem extends Item implements IComputerItem, IMedia, IColouredItem {
private static final String NBT_UPGRADE = "Upgrade"; private static final String NBT_UPGRADE = "Upgrade";
@ -188,10 +189,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
} }
public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) { public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) {
var sessionID = getSessionID(stack);
var registry = ServerContext.get(level.getServer()).registry(); var registry = ServerContext.get(level.getServer()).registry();
var computer = (PocketServerComputer) registry.get(sessionID, getInstanceID(stack)); var computer = (PocketServerComputer) registry.get(getSessionID(stack), getInstanceID(stack));
if (computer == null) { if (computer == null) {
var computerID = getComputerID(stack); var computerID = getComputerID(stack);
if (computerID < 0) { if (computerID < 0) {
@ -201,8 +201,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
computer = new PocketServerComputer(level, entity.blockPosition(), getComputerID(stack), getLabel(stack), getFamily()); computer = new PocketServerComputer(level, entity.blockPosition(), getComputerID(stack), getLabel(stack), getFamily());
setInstanceID(stack, computer.register()); var tag = stack.getOrCreateTag();
setSessionID(stack, registry.getSessionID()); tag.putInt(NBT_SESSION, registry.getSessionID());
tag.putUUID(NBT_INSTANCE, computer.register());
var upgrade = getUpgrade(stack); var upgrade = getUpgrade(stack);
@ -267,13 +268,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
return null; return null;
} }
public static int getInstanceID(ItemStack stack) { public static @Nullable UUID getInstanceID(ItemStack stack) {
var nbt = stack.getTag(); var nbt = stack.getTag();
return nbt != null && nbt.contains(NBT_INSTANCE) ? nbt.getInt(NBT_INSTANCE) : -1; return nbt != null && nbt.hasUUID(NBT_INSTANCE) ? nbt.getUUID(NBT_INSTANCE) : null;
}
private static void setInstanceID(ItemStack stack, int instanceID) {
stack.getOrCreateTag().putInt(NBT_INSTANCE, instanceID);
} }
private static int getSessionID(ItemStack stack) { private static int getSessionID(ItemStack stack) {
@ -281,10 +278,6 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
return nbt != null && nbt.contains(NBT_SESSION) ? nbt.getInt(NBT_SESSION) : -1; return nbt != null && nbt.contains(NBT_SESSION) ? nbt.getInt(NBT_SESSION) : -1;
} }
private static void setSessionID(ItemStack stack, int sessionID) {
stack.getOrCreateTag().putInt(NBT_SESSION, sessionID);
}
private static boolean isMarkedOn(ItemStack stack) { private static boolean isMarkedOn(ItemStack stack) {
var nbt = stack.getTag(); var nbt = stack.getTag();
return nbt != null && nbt.getBoolean(NBT_ON); return nbt != null && nbt.getBoolean(NBT_ON);

View File

@ -22,6 +22,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.OptionalInt; import java.util.OptionalInt;
import java.util.UUID;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
@ -39,19 +40,19 @@ class ComputerSelectorTest {
public static Arguments[] getArgumentTestCases() { public static Arguments[] getArgumentTestCases() {
return new Arguments[]{ return new Arguments[]{
// Legacy selectors // Legacy selectors
Arguments.of("@some_label", new ComputerSelector("@some_label", OptionalInt.empty(), OptionalInt.empty(), "some_label", null, null, null)), Arguments.of("@some_label", new ComputerSelector("@some_label", OptionalInt.empty(), null, OptionalInt.empty(), "some_label", null, null, null)),
Arguments.of("~normal", new ComputerSelector("~normal", OptionalInt.empty(), OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)), Arguments.of("~normal", new ComputerSelector("~normal", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
Arguments.of("#123", new ComputerSelector("#123", OptionalInt.empty(), OptionalInt.of(123), null, null, null, null)), Arguments.of("#123", new ComputerSelector("#123", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
Arguments.of("123", new ComputerSelector("123", OptionalInt.of(123), OptionalInt.empty(), null, null, null, null)), Arguments.of("123", new ComputerSelector("123", OptionalInt.of(123), null, OptionalInt.empty(), null, null, null, null)),
// New selectors // New selectors
Arguments.of("@c[]", new ComputerSelector("@c[]", OptionalInt.empty(), OptionalInt.empty(), null, null, null, null)), Arguments.of("@c[]", new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null)),
Arguments.of("@c[instance=123]", new ComputerSelector("@c[instance=123]", OptionalInt.of(123), OptionalInt.empty(), null, null, null, null)), Arguments.of("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", new ComputerSelector("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", OptionalInt.empty(), UUID.fromString("5e18f505-62f7-46f8-83f3-792f03224724"), OptionalInt.empty(), null, null, null, null)),
Arguments.of("@c[id=123]", new ComputerSelector("@c[id=123]", OptionalInt.empty(), OptionalInt.of(123), null, null, null, null)), Arguments.of("@c[id=123]", new ComputerSelector("@c[id=123]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
Arguments.of("@c[label=\"foo\"]", new ComputerSelector("@c[label=\"foo\"]", OptionalInt.empty(), OptionalInt.empty(), "foo", null, null, null)), Arguments.of("@c[label=\"foo\"]", new ComputerSelector("@c[label=\"foo\"]", OptionalInt.empty(), null, OptionalInt.empty(), "foo", null, null, null)),
Arguments.of("@c[family=normal]", new ComputerSelector("@c[family=normal]", OptionalInt.empty(), OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)), Arguments.of("@c[family=normal]", new ComputerSelector("@c[family=normal]", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
// Complex selectors // Complex selectors
Arguments.of("@c[ id = 123 , ]", new ComputerSelector("@c[ id = 123 , ]", OptionalInt.empty(), OptionalInt.of(123), null, null, null, null)), Arguments.of("@c[ id = 123 , ]", new ComputerSelector("@c[ id = 123 , ]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
Arguments.of("@c[id=123,family=normal]", new ComputerSelector("@c[id=123,family=normal]", OptionalInt.empty(), OptionalInt.of(123), null, ComputerFamily.NORMAL, null, null)), Arguments.of("@c[id=123,family=normal]", new ComputerSelector("@c[id=123,family=normal]", OptionalInt.empty(), null, OptionalInt.of(123), null, ComputerFamily.NORMAL, null, null)),
}; };
} }

View File

@ -38,7 +38,7 @@ class Pocket_Computer_Test {
// And ensure its synced to the client. // And ensure its synced to the client.
thenIdle(4) thenIdle(4)
thenOnClient { thenOnClient {
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem) val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
assertEquals(ComputerState.ON, pocketComputer.state) assertEquals(ComputerState.ON, pocketComputer.state)
val term = pocketComputer.terminal val term = pocketComputer.terminal
@ -54,7 +54,7 @@ class Pocket_Computer_Test {
// And ensure the new computer state and terminal are sent. // And ensure the new computer state and terminal are sent.
thenIdle(4) thenIdle(4)
thenOnClient { thenOnClient {
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem) val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
assertEquals(ComputerState.BLINKING, pocketComputer.state) assertEquals(ComputerState.BLINKING, pocketComputer.state)
val term = pocketComputer.terminal val term = pocketComputer.terminal