diff --git a/build.gradle.kts b/build.gradle.kts index b7e6e49a..96a8c3f8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -188,6 +188,7 @@ dependencies { listOf( "fabric-api-base", "fabric-api-lookup-api-v1", + "fabric-command-api-v2", "fabric-data-attachment-api-v1", "fabric-gametest-api-v1", "fabric-item-api-v1", diff --git a/src/main/java/dev/galacticraft/machinelib/api/config/Config.java b/src/main/java/dev/galacticraft/machinelib/api/config/Config.java index 7d02945c..06e9e2c8 100644 --- a/src/main/java/dev/galacticraft/machinelib/api/config/Config.java +++ b/src/main/java/dev/galacticraft/machinelib/api/config/Config.java @@ -72,6 +72,18 @@ static Config loadFrom(File file) { */ void setFluidUnits(FluidUnits units); + /** + * {@return the id of the team system used by MachineLib security} + */ + String teamSystem(); + + /** + * Sets the team system used by MachineLib security. + * + * @param teamSystem the registered team system id + */ + void setTeamSystem(String teamSystem); + /** * Copies the state of the provided config into this config. * 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 be6c4efd..c312dcc6 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 @@ -22,6 +22,7 @@ package dev.galacticraft.machinelib.api.machine.configuration; +import dev.galacticraft.machinelib.api.machine.security.TeamSystems; import dev.galacticraft.machinelib.api.misc.DeltaPacketSerializable; import dev.galacticraft.machinelib.api.misc.PacketSerializable; import dev.galacticraft.machinelib.api.misc.Serializable; @@ -134,7 +135,9 @@ public boolean hasAccess( return switch (accessLevel) { case PUBLIC -> true; - case TEAM -> this.isOwner(player); // todo: teams + case TEAM -> this.isOwner(player) + || this.owner != null + && TeamSystems.areTeammates(this.owner, player); case PRIVATE -> this.isOwner(player); }; diff --git a/src/main/java/dev/galacticraft/machinelib/api/machine/security/TeamSystem.java b/src/main/java/dev/galacticraft/machinelib/api/machine/security/TeamSystem.java new file mode 100644 index 00000000..8aaeba0a --- /dev/null +++ b/src/main/java/dev/galacticraft/machinelib/api/machine/security/TeamSystem.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021-2026 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.machinelib.api.machine.security; + +import net.minecraft.world.entity.player.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Provides team membership information for MachineLib security checks. + * + *

Implementations are registered through {@link TeamSystems}. MachineLib + * selects one registered implementation using its {@code teamSystem} config + * option.

+ */ +@FunctionalInterface +public interface TeamSystem { + /** + * Determines whether the supplied player is considered a teammate of the + * machine owner. + * + *

The owner check itself is performed by MachineLib before this method is + * called. Implementations therefore only need to determine team membership.

+ * + * @param owner the UUID of the machine owner + * @param player the player attempting to access the machine + * @return whether the player is a teammate of the owner + */ + boolean areTeammates(@NotNull UUID owner, @NotNull Player player); +} \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/machinelib/api/machine/security/TeamSystems.java b/src/main/java/dev/galacticraft/machinelib/api/machine/security/TeamSystems.java new file mode 100644 index 00000000..3b479ec0 --- /dev/null +++ b/src/main/java/dev/galacticraft/machinelib/api/machine/security/TeamSystems.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021-2026 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.machinelib.api.machine.security; + +import dev.galacticraft.machinelib.impl.MachineLib; +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.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +/** + * Registry for team systems exposed to MachineLib. + * + *

A mod may register its team implementation during initialization and a + * modpack may then select it using MachineLib's {@code teamSystem} config + * option.

+ */ +public final class TeamSystems { + /** + * The identifier of MachineLib's built-in vanilla team implementation. + */ + public static final String MINECRAFT = "minecraft"; + + private static final Pattern VALID_ID = Pattern.compile("[a-z0-9_.-]+"); + private static final Map SYSTEMS = new LinkedHashMap<>(); + + private TeamSystems() {} + + /** + * Registers a team system. + * + *

It is recommended that mods use their mod id as the team system id.

+ * + * @param id the unique team system id + * @param system the team system implementation + * @throws IllegalArgumentException if the id is invalid or already registered + */ + public static void register(@NotNull String id, @NotNull TeamSystem system) { + if (!VALID_ID.matcher(id).matches()) { + throw new IllegalArgumentException("Invalid team system id: " + id); + } + + if (SYSTEMS.putIfAbsent(id, system) != null) { + throw new IllegalArgumentException("Team system already registered: " + id); + } + } + + /** + * {@return whether a team system with the supplied id is registered} + * + * @param id the team system id + */ + @Contract(pure = true) + public static boolean isRegistered(@NotNull String id) { + return SYSTEMS.containsKey(id); + } + + /** + * Returns a registered team system. + * + * @param id the team system id + * @return the registered implementation, or {@code null} if none exists + */ + @Contract(pure = true) + public static @Nullable TeamSystem get(@NotNull String id) { + return SYSTEMS.get(id); + } + + /** + * {@return an immutable ordered set containing all registered team system ids} + */ + @Contract(pure = true) + public static @NotNull Set getRegisteredIds() { + return Collections.unmodifiableSet(SYSTEMS.keySet()); + } + + /** + * {@return an immutable ordered view of all registered team systems} + */ + @Contract(pure = true) + public static @NotNull Map getRegistered() { + return Collections.unmodifiableMap(SYSTEMS); + } + + /** + * Determines whether the supplied player is a teammate of the owner using + * the currently configured team system. + * + *

If the configured implementation is unavailable, the built-in + * {@value #MINECRAFT} implementation is used. If that implementation is also + * unavailable, access is denied.

+ * + * @param owner the machine owner + * @param player the player attempting access + * @return whether the configured team system considers them teammates + */ + public static boolean areTeammates( + @NotNull UUID owner, + @NotNull Player player + ) { + TeamSystem system = getConfigured(); + + return system != null && system.areTeammates(owner, player); + } + + /** + * Returns the currently configured team system, falling back to vanilla. + * + * @return the active team system, or {@code null} if no usable implementation + * is registered + */ + public static @Nullable TeamSystem getConfigured() { + String configured = MachineLib.CONFIG.teamSystem(); + TeamSystem system = SYSTEMS.get(configured); + + if (system != null) { + return system; + } + + return SYSTEMS.get(MINECRAFT); + } +} \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/machinelib/client/impl/compat/ClothConfigScreen.java b/src/main/java/dev/galacticraft/machinelib/client/impl/compat/ClothConfigScreen.java index ad2af9b1..d4f0b179 100644 --- a/src/main/java/dev/galacticraft/machinelib/client/impl/compat/ClothConfigScreen.java +++ b/src/main/java/dev/galacticraft/machinelib/client/impl/compat/ClothConfigScreen.java @@ -23,13 +23,17 @@ package dev.galacticraft.machinelib.client.impl.compat; import dev.galacticraft.machinelib.api.config.Config; +import dev.galacticraft.machinelib.api.machine.security.TeamSystems; import dev.galacticraft.machinelib.impl.MachineLib; import me.shedaniel.clothconfig2.api.ConfigBuilder; import me.shedaniel.clothconfig2.api.ConfigCategory; import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; +import java.util.ArrayList; + public class ClothConfigScreen { public static Screen factory(Screen screen) { final ConfigBuilder builder = ConfigBuilder.create(); @@ -50,6 +54,16 @@ public static Screen factory(Screen screen) { .setEnumNameProvider(v -> ((Config.FluidUnits) v).getName()) .build() ); + String configuredTeamSystem = MachineLib.CONFIG.teamSystem(); + String displayedTeamSystem = TeamSystems.isRegistered(configuredTeamSystem) ? configuredTeamSystem : TeamSystems.MINECRAFT; + general.addEntry(entryBuilder.startDropdownMenu(Component.translatable("ui.machinelib.config.team_system"), DropdownMenuBuilder.TopCellElementBuilder.of(displayedTeamSystem, value -> value), DropdownMenuBuilder.CellCreatorBuilder.of(value -> Component.literal(value))) + .setSelections(new ArrayList<>(TeamSystems.getRegisteredIds())) + .setDefaultValue(Config.DEFAULT.teamSystem()) + .setSuggestionMode(false) + .setSaveConsumer(MachineLib.CONFIG::setTeamSystem) + .setTooltip(Component.translatable("ui.machinelib.config.team_system.tooltip")) + .build() + ); // ConfigCategory debug = builder.getOrCreateCategory(Component.translatable("ui.machinelib.config.category.debug")); return builder.build(); diff --git a/src/main/java/dev/galacticraft/machinelib/impl/MachineLib.java b/src/main/java/dev/galacticraft/machinelib/impl/MachineLib.java index 260b9b94..38c0b3fa 100644 --- a/src/main/java/dev/galacticraft/machinelib/impl/MachineLib.java +++ b/src/main/java/dev/galacticraft/machinelib/impl/MachineLib.java @@ -24,6 +24,9 @@ import dev.galacticraft.machinelib.api.component.MLDataComponents; import dev.galacticraft.machinelib.api.config.Config; +import dev.galacticraft.machinelib.api.machine.security.TeamSystems; +import dev.galacticraft.machinelib.impl.command.MachineLibCommands; +import dev.galacticraft.machinelib.impl.machine.security.VanillaTeamSystem; import dev.galacticraft.machinelib.impl.network.MachineLibPackets; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariantAttributes; @@ -39,6 +42,13 @@ public final class MachineLib implements ModInitializer { @Override public void onInitialize() { + TeamSystems.register( + TeamSystems.MINECRAFT, + VanillaTeamSystem.INSTANCE + ); + + MachineLibCommands.register(); + MachineLibPackets.registerChannels(); MachineLibPackets.registerServer(); MLDataComponents.init(); diff --git a/src/main/java/dev/galacticraft/machinelib/impl/command/MachineLibCommands.java b/src/main/java/dev/galacticraft/machinelib/impl/command/MachineLibCommands.java new file mode 100644 index 00000000..0a30f378 --- /dev/null +++ b/src/main/java/dev/galacticraft/machinelib/impl/command/MachineLibCommands.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021-2026 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.machinelib.impl.command; + +import dev.galacticraft.machinelib.api.machine.security.TeamSystems; +import dev.galacticraft.machinelib.impl.MachineLib; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class MachineLibCommands { + private MachineLibCommands() {} + + public static void register() { + CommandRegistrationCallback.EVENT.register( + (dispatcher, registryAccess, environment) -> dispatcher.register( + Commands.literal("machinelib").then( + Commands.literal("exposedTeamSystems").executes(context -> { + String configured = MachineLib.CONFIG.teamSystem(); + boolean configuredRegistered = TeamSystems.isRegistered(configured); + + context.getSource().sendSuccess( + () -> Component.literal("Registered MachineLib team systems:"), + false + ); + + for (String id : TeamSystems.getRegisteredIds()) { + boolean active = id.equals(configured) + || !configuredRegistered && id.equals(TeamSystems.MINECRAFT); + + Component message = Component.literal(" - " + id); + + if (active) { + message = message.copy().append( + Component.literal(" [ACTIVE]") + .withStyle(ChatFormatting.GREEN) + ); + } + + Component finalMessage = message; + + context.getSource().sendSuccess(() -> finalMessage, false); + } + + if (!configuredRegistered) { + context.getSource().sendSuccess( + () -> Component.literal( + String.format( + "Configured team system '%s' is not registered; using '%s'.", + configured, + TeamSystems.MINECRAFT + ) + ).withStyle(ChatFormatting.YELLOW), + false + ); + } + + return TeamSystems.getRegisteredIds().size(); + }) + ) + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/machinelib/impl/config/MachineLibConfig.java b/src/main/java/dev/galacticraft/machinelib/impl/config/MachineLibConfig.java index c90c5093..9da2f949 100644 --- a/src/main/java/dev/galacticraft/machinelib/impl/config/MachineLibConfig.java +++ b/src/main/java/dev/galacticraft/machinelib/impl/config/MachineLibConfig.java @@ -27,6 +27,7 @@ import com.google.gson.annotations.Expose; import dev.galacticraft.machinelib.api.config.Config; import dev.galacticraft.machinelib.impl.MachineLib; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; @@ -44,6 +45,9 @@ public class MachineLibConfig implements Config { @Expose public FluidUnits fluidUnits = FluidUnits.MILLIBUCKET; + @Expose + public String teamSystem = "minecraft"; + public MachineLibConfig(@Nullable File file) { this.file = file; @@ -77,10 +81,25 @@ public void setFluidUnits(FluidUnits units) { this.fluidUnits = units; } + @Override @NotNull + public String teamSystem() { + if (this.teamSystem == null || this.teamSystem.isBlank()) { + return "minecraft"; + } + + return this.teamSystem; + } + + @Override + public void setTeamSystem(@Nullable String teamSystem) { + this.teamSystem = ((teamSystem == null || teamSystem.isBlank()) ? "minecraft" : teamSystem); + } + @Override public void copyFrom(Config config) { this.enableColoredVanillaFluidNames = config.enableColoredVanillaFluidNames(); this.fluidUnits = config.fluidUnits(); + this.teamSystem = config.teamSystem(); } @Override diff --git a/src/main/java/dev/galacticraft/machinelib/impl/machine/security/VanillaTeamSystem.java b/src/main/java/dev/galacticraft/machinelib/impl/machine/security/VanillaTeamSystem.java new file mode 100644 index 00000000..e92e25f0 --- /dev/null +++ b/src/main/java/dev/galacticraft/machinelib/impl/machine/security/VanillaTeamSystem.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021-2026 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.machinelib.impl.machine.security; + +import com.mojang.authlib.GameProfile; +import dev.galacticraft.machinelib.api.machine.security.TeamSystem; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.GameProfileCache; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.scores.PlayerTeam; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.UUID; + +/** + * Team system backed by vanilla Minecraft scoreboard teams ({@code /team}). + */ +@ApiStatus.Internal +public final class VanillaTeamSystem implements TeamSystem { + public static final VanillaTeamSystem INSTANCE = new VanillaTeamSystem(); + + private VanillaTeamSystem() {} + + @Override + public boolean areTeammates( + @NotNull UUID owner, + @NotNull Player player + ) { + MinecraftServer server = player.getServer(); + + if (server == null) { + return false; + } + + GameProfileCache cache = server.getProfileCache(); + + if (cache == null) { + return false; + } + + Optional ownerProfile = cache.get(owner); + + if (ownerProfile.isEmpty()) { + return false; + } + + PlayerTeam ownerTeam = server.getScoreboard().getPlayersTeam( + ownerProfile.get().getName() + ); + + return ownerTeam != null && ownerTeam == player.getTeam(); + } +} \ No newline at end of file diff --git a/src/main/resources/assets/machinelib/lang/en_us.json b/src/main/resources/assets/machinelib/lang/en_us.json index 81dc9318..5f4946db 100644 --- a/src/main/resources/assets/machinelib/lang/en_us.json +++ b/src/main/resources/assets/machinelib/lang/en_us.json @@ -58,5 +58,7 @@ "ui.machinelib.config.enable_colored_vanilla_fluid_names": "Enable Colored Vanilla Fluid Names", "ui.machinelib.config.fluid_display_mode": "Fluid Display Mode", "ui.machinelib.config.fluid_display_mode.millibucket": "Millibucket", - "ui.machinelib.config.fluid_display_mode.raw": "Raw" + "ui.machinelib.config.fluid_display_mode.raw": "Raw", + "ui.machinelib.config.team_system": "Team System", + "ui.machinelib.config.team_system.tooltip": "Registered team system used by machines set to Team access." } \ No newline at end of file