From 35dca6ea8869204776c44d97ef39f5937c596f6f Mon Sep 17 00:00:00 2001 From: maxryan008 Date: Tue, 22 Sep 2026 12:01:30 +1000 Subject: [PATCH] Add support for multiple named machine security access levels --- .../block/entity/ConfiguredBlockEntity.java | 16 +- .../configuration/SecuritySettings.java | 360 ++++++++++++++++-- 2 files changed, 339 insertions(+), 37 deletions(-) diff --git a/src/main/java/dev/galacticraft/machinelib/api/block/entity/ConfiguredBlockEntity.java b/src/main/java/dev/galacticraft/machinelib/api/block/entity/ConfiguredBlockEntity.java index 39b4d8e9..441eac66 100644 --- a/src/main/java/dev/galacticraft/machinelib/api/block/entity/ConfiguredBlockEntity.java +++ b/src/main/java/dev/galacticraft/machinelib/api/block/entity/ConfiguredBlockEntity.java @@ -54,7 +54,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.UUID; public abstract class ConfiguredBlockEntity extends BaseBlockEntity implements RenderDataBlockEntity { private final @NotNull IOConfig configuration; @@ -364,19 +363,8 @@ public void setPowered(boolean powered) { */ private class InternalSecuritySettings extends SecuritySettings { @Override - public void tryUpdate(@NotNull UUID uuid) { - if (this.owner == null) { - this.owner = uuid; - ConfiguredBlockEntity.this.setChanged(); - } - } - - @Override - public void setAccessLevel(@NotNull AccessLevel accessLevel) { - if (this.accessLevel != accessLevel) { - this.accessLevel = accessLevel; - ConfiguredBlockEntity.this.setChanged(); - } + protected void onChanged() { + ConfiguredBlockEntity.this.setChanged(); } } diff --git a/src/main/java/dev/galacticraft/machinelib/api/machine/configuration/SecuritySettings.java b/src/main/java/dev/galacticraft/machinelib/api/machine/configuration/SecuritySettings.java index 9c8576dc..be6c4efd 100644 --- a/src/main/java/dev/galacticraft/machinelib/api/machine/configuration/SecuritySettings.java +++ b/src/main/java/dev/galacticraft/machinelib/api/machine/configuration/SecuritySettings.java @@ -27,30 +27,61 @@ import dev.galacticraft.machinelib.api.misc.Serializable; import dev.galacticraft.machinelib.impl.Constant; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Objects; -import java.util.UUID; +import java.util.*; /** - * Represents a security setting of a machine. + * Represents the security settings of a machine. + * + *

A machine always has a default access level which determines whether a + * player may use the machine normally. Additional independent access levels + * may also be registered for machine-specific functionality.

+ * + *

All access levels share the same owner. For example, a machine may be + * configured as {@link AccessLevel#PRIVATE} for normal interaction while an + * additional access level is configured as + * {@link AccessLevel#PUBLIC}.

*/ public class SecuritySettings implements Serializable, DeltaPacketSerializable { + private static final String NBT_ADDITIONAL_ACCESS_LEVELS = "AccessLevels"; /** * The profile of the player who owns the linked machine. */ protected @Nullable UUID owner = null; /** - * The access level of the linked machine. + * The default access level of the linked machine. */ protected @NotNull AccessLevel accessLevel = AccessLevel.PUBLIC; + /** + * Additional registered access levels. + */ + protected final @NotNull Map accessLevels = new HashMap<>(); + /** + * Default values for additional registered access levels. + * + *

These are kept separately so missing or legacy NBT entries can fall + * back to the access level defined by the machine.

+ */ + protected final @NotNull Map defaultAccessLevels = new HashMap<>(); + StreamCodec CODEC = PacketSerializable.createCodec(SecuritySettings::new); + /** + * Called whenever a security setting is modified. + * + *

Subclasses may override this to notify their owning object that the + * security settings have changed.

+ */ + protected void onChanged() {} + /** * Updates the owner of the linked machine if it is not already set. * @@ -59,6 +90,7 @@ public class SecuritySettings implements Serializable, DeltaPacketS public void tryUpdate(@NotNull UUID uuid) { if (this.owner == null) { this.owner = uuid; + this.onChanged(); } } @@ -73,37 +105,195 @@ public boolean isOwner(@NotNull Player player) { } /** - * {@return whether the player has access to the linked machine} + * {@return whether the player has access to the linked machine using the + * default machine access level} * * @param player the player to check */ + @Contract(pure = true) public boolean hasAccess(@NotNull Player player) { - return switch (this.accessLevel) { + return this.hasAccess(player, this.accessLevel); + } + + /** + * Determines whether a player has access using the supplied access level. + * + *

This method contains the actual access-level behaviour and is shared + * by both the default machine access level and all additional registered + * access levels.

+ * + * @param player the player to check + * @param accessLevel the access level to check against + * @return whether the player has access + */ + @Contract(pure = true) + public boolean hasAccess( + @NotNull Player player, + @NotNull AccessLevel accessLevel + ) { + return switch (accessLevel) { case PUBLIC -> true; + case TEAM -> this.isOwner(player); // todo: teams + case PRIVATE -> this.isOwner(player); }; } /** - * {@return the access level of the linked machine} + * Determines whether a player has access to an additional registered + * access level. + * + * @param id the identifier of the registered access level + * @param player the player to check + * @return whether the player has access + * @throws IllegalArgumentException if the access level has not been + * registered */ + @Contract(pure = true) + public boolean hasAccess( + @NotNull ResourceLocation id, + @NotNull Player player + ) { + return this.hasAccess( + player, + this.getAccessLevel(id) + ); + } + + /** + * Registers an additional independent access level. + * + *

Registration defines that a machine supports the supplied security + * option. Registration should normally occur during block entity + * construction.

+ * + *

Registering an access level does not mark the security settings as + * changed because registration describes the structure of the machine, + * rather than modifying its saved state.

+ * + * @param id the unique identifier for the access level + * @param defaultLevel the default access level + * @throws IllegalArgumentException if this identifier has already been + * registered + */ + public void registerAccessLevel( + @NotNull ResourceLocation id, + @NotNull AccessLevel defaultLevel + ) { + if (this.defaultAccessLevels.containsKey(id)) { + throw new IllegalArgumentException("Security access level already registered: " + id); + } + + this.defaultAccessLevels.put(id, defaultLevel); + this.accessLevels.put(id, defaultLevel); + } + + /** + * {@return whether an additional access level has been registered} + * + * @param id the access level identifier + */ + @Contract(pure = true) + public boolean hasAccessLevel(@NotNull ResourceLocation id) { + return this.defaultAccessLevels.containsKey(id); + } + + /** + * {@return the default machine access level} + */ + @Contract(pure = true) public @NotNull AccessLevel getAccessLevel() { return this.accessLevel; } /** - * Sets the access level of the linked machine. + * Returns an additional registered access level. * - * @param accessLevel The access level to set. + * @param id the identifier of the registered access level + * @return the current access level + * @throws IllegalArgumentException if the access level has not been + * registered + */ + @Contract(pure = true) + public @NotNull AccessLevel getAccessLevel( + @NotNull ResourceLocation id + ) { + AccessLevel accessLevel = this.accessLevels.get(id); + + if (accessLevel == null) { + throw new IllegalArgumentException("Unknown security access level: " + id); + } + + return accessLevel; + } + + /** + * {@return an immutable view of all additional registered access levels} + */ + @Contract(pure = true) + public @NotNull Map getAccessLevels() { + return Collections.unmodifiableMap(this.accessLevels); + } + + /** + * Sets the default machine access level. + * + * @param accessLevel the access level to set */ public void setAccessLevel(@NotNull AccessLevel accessLevel) { - this.accessLevel = accessLevel; + if (this.accessLevel != accessLevel) { + this.accessLevel = accessLevel; + this.onChanged(); + } + } + + /** + * Sets an additional registered access level. + * + * @param id the identifier of the registered access level + * @param accessLevel the new access level + * @throws IllegalArgumentException if the access level has not been + * registered + */ + public void setAccessLevel( + @NotNull ResourceLocation id, + @NotNull AccessLevel accessLevel + ) { + AccessLevel previous = this.accessLevels.get(id); + + if (previous == null) { + throw new IllegalArgumentException("Unknown security access level: " + id); + } + + if (previous != accessLevel) { + this.accessLevels.put(id, accessLevel); + this.onChanged(); + } + } + + /** + * Resets an additional registered access level to the default value it was + * registered with. + * + * @param id the identifier of the registered access level + * @throws IllegalArgumentException if the access level has not been + * registered + */ + public void resetAccessLevel(@NotNull ResourceLocation id) { + AccessLevel defaultLevel = this.defaultAccessLevels.get(id); + + if (defaultLevel == null) { + throw new IllegalArgumentException("Unknown security access level: " + id); + } + + this.setAccessLevel(id, defaultLevel); } /** * {@return the uuid of the player that owns the linked machine} */ + @Contract(pure = true) public @Nullable UUID getOwner() { return this.owner; } @@ -111,10 +301,24 @@ public void setAccessLevel(@NotNull AccessLevel accessLevel) { @Override public @NotNull CompoundTag createTag() { CompoundTag nbt = new CompoundTag(); + if (this.owner != null) { nbt.putUUID(Constant.Nbt.OWNER, this.owner); } + nbt.putString(Constant.Nbt.ACCESS_LEVEL, this.accessLevel.getSerializedName()); + + if (!this.accessLevels.isEmpty()) { + CompoundTag additionalAccessLevels = new CompoundTag(); + + for (Map.Entry entry : this.accessLevels.entrySet()) { + + additionalAccessLevels.putString(entry.getKey().toString(), entry.getValue().getSerializedName()); + } + + nbt.put(NBT_ADDITIONAL_ACCESS_LEVELS, additionalAccessLevels); + } + return nbt; } @@ -122,70 +326,174 @@ public void setAccessLevel(@NotNull AccessLevel accessLevel) { public void readTag(@NotNull CompoundTag tag) { if (tag.contains(Constant.Nbt.OWNER)) { this.owner = tag.getUUID(Constant.Nbt.OWNER); + } else { + this.owner = null; } if (tag.contains(Constant.Nbt.ACCESS_LEVEL)) { this.accessLevel = AccessLevel.fromString(tag.getString(Constant.Nbt.ACCESS_LEVEL)); + } else { + this.accessLevel = AccessLevel.PUBLIC; + } + + /* + * Reset all registered additional access levels to their defaults + * before applying values from NBT. + */ + this.accessLevels.clear(); + this.accessLevels.putAll(this.defaultAccessLevels); + + if (tag.contains(NBT_ADDITIONAL_ACCESS_LEVELS, Tag.TAG_COMPOUND)) { + CompoundTag additionalAccessLevels = tag.getCompound(NBT_ADDITIONAL_ACCESS_LEVELS); + + for (String key : additionalAccessLevels.getAllKeys()) { + ResourceLocation id = ResourceLocation.tryParse(key); + + if (id == null || !this.defaultAccessLevels.containsKey(id)) { + continue; + } + + String value = additionalAccessLevels.getString(key); + + try { + this.accessLevels.put(id, AccessLevel.fromString(value)); + } catch (IllegalArgumentException ignored) { + /* + * Invalid values fall back to the registered default. + */ + } + } } } @Override public void writePacket(@NotNull FriendlyByteBuf buf) { buf.writeByte(this.accessLevel.ordinal()); + buf.writeBoolean(this.owner != null); + if (this.owner != null) { buf.writeUUID(this.owner); } + + buf.writeVarInt(this.accessLevels.size()); + + for (Map.Entry entry : this.accessLevels.entrySet()) { + buf.writeUtf(entry.getKey().toString()); + buf.writeByte(entry.getValue().ordinal()); + } } @Override public void readPacket(@NotNull FriendlyByteBuf buf) { this.accessLevel = AccessLevel.getByOrdinal(buf.readByte()); + if (buf.readBoolean()) { this.owner = buf.readUUID(); } else { this.owner = null; } + + /* + * Reset registered values before applying the packet. + */ + this.accessLevels.clear(); + this.accessLevels.putAll(this.defaultAccessLevels); + + int count = buf.readVarInt(); + + for (int i = 0; i < count; i++) { + ResourceLocation id = ResourceLocation.tryParse(buf.readUtf()); + + AccessLevel level = AccessLevel.getByOrdinal(buf.readByte()); + + if (id != null && this.defaultAccessLevels.containsKey(id)) { + this.accessLevels.put(id, level); + } + } } @Override - public void writeDeltaPacket(@NotNull FriendlyByteBuf buf, SecuritySettings previous) { - byte ref = 0b000; + public void writeDeltaPacket( + @NotNull FriendlyByteBuf buf, + SecuritySettings previous + ) { + byte ref = 0b0000; if (previous.accessLevel != this.accessLevel) { - ref |= 0b001; + ref |= 0b0001; } - if (previous.owner != this.owner) { + if (!Objects.equals(previous.owner, this.owner)) { if (this.owner != null) { - ref |= 0b010; + ref |= 0b0010; } else { - ref |= 0b100; + ref |= 0b0100; + } + } + + Map changedAccessLevels = new HashMap<>(); + + for (Map.Entry entry : this.accessLevels.entrySet()) { + + AccessLevel previousLevel = previous.accessLevels.get(entry.getKey()); + + if (previousLevel != entry.getValue()) { + changedAccessLevels.put(entry.getKey(), entry.getValue()); } } + if (!changedAccessLevels.isEmpty()) { + ref |= 0b1000; + } + buf.writeByte(ref); - if ((ref & 0b001) != 0) { + + if ((ref & 0b0001) != 0) { buf.writeByte(this.accessLevel.ordinal()); } - if ((ref & 0b010) != 0) { + + if ((ref & 0b0010) != 0) { buf.writeUUID(this.owner); } + + if ((ref & 0b1000) != 0) { + buf.writeVarInt(changedAccessLevels.size()); + + for (Map.Entry entry : changedAccessLevels.entrySet()) { + buf.writeUtf(entry.getKey().toString()); + buf.writeByte(entry.getValue().ordinal()); + } + } } @Override public void readDeltaPacket(@NotNull FriendlyByteBuf buf) { byte ref = buf.readByte(); - if ((ref & 0b001) != 0) { + if ((ref & 0b0001) != 0) { this.accessLevel = AccessLevel.getByOrdinal(buf.readByte()); } - if ((ref & 0b010) != 0) { + if ((ref & 0b0010) != 0) { this.owner = buf.readUUID(); - } else if ((ref & 0b100) != 0) { + } else if ((ref & 0b0100) != 0) { this.owner = null; } + + if ((ref & 0b1000) != 0) { + int count = buf.readVarInt(); + + for (int i = 0; i < count; i++) { + ResourceLocation id = ResourceLocation.tryParse(buf.readUtf()); + + AccessLevel level = AccessLevel.getByOrdinal(buf.readByte()); + + if (id != null && this.defaultAccessLevels.containsKey(id)) { + this.accessLevels.put(id, level); + } + } + } } @Override @@ -195,12 +503,18 @@ public SecuritySettings createEquivalent() { @Override public boolean hasChanged(SecuritySettings previous) { - return !Objects.equals(previous.owner, this.owner) || previous.accessLevel != this.accessLevel; + return !Objects.equals(previous.owner, this.owner) || previous.accessLevel != this.accessLevel || !Objects.equals(previous.accessLevels, this.accessLevels); } @Override public void copyInto(SecuritySettings other) { other.owner = this.owner; other.accessLevel = this.accessLevel; + + other.defaultAccessLevels.clear(); + other.defaultAccessLevels.putAll(this.defaultAccessLevels); + + other.accessLevels.clear(); + other.accessLevels.putAll(this.accessLevels); } -} +} \ No newline at end of file