diff --git a/common/src/main/java/net/xolt/freecam/Freecam.java b/common/src/main/java/net/xolt/freecam/Freecam.java index 4acac43f7..cf3eb0655 100644 --- a/common/src/main/java/net/xolt/freecam/Freecam.java +++ b/common/src/main/java/net/xolt/freecam/Freecam.java @@ -11,6 +11,7 @@ import net.xolt.freecam.config.ModBindings; import net.xolt.freecam.config.ModConfig; import net.xolt.freecam.config.keys.Tickable; +import net.xolt.freecam.network.ServerPolicies; import net.xolt.freecam.tripod.TripodRegistry; import net.xolt.freecam.tripod.TripodSlot; import net.xolt.freecam.util.FreeCamera; @@ -47,7 +48,7 @@ public static void preTick(Minecraft mc) { // Disable if the previous tick asked us to, // or Freecam is restricted on the current server if ((disableNextTick || isRestrictedOnServer()) && isEnabled()) { - toggle(); + disable(); } disableNextTick = false; @@ -83,8 +84,9 @@ public static void postTick(Minecraft mc) { @ApiStatus.Internal public static void onDisconnect() { if (isEnabled()) { - toggle(); + disable(); } + ServerPolicies.reset(); tripods.clear(); } @@ -282,6 +284,18 @@ private static void onDisabled() { } } + @ApiStatus.Internal + public static void disable() { + if (tripodEnabled) { + onDisableTripod(); + } else if (freecamEnabled) { + onDisableFreecam(); + } + freecamEnabled = false; + tripodEnabled = false; + onDisabled(); + } + private static void resetCamera(TripodSlot tripod) { if (tripodEnabled && activeTripod != TripodSlot.NONE && activeTripod == tripod && freeCamera != null) { moveToPlayer(); @@ -371,11 +385,22 @@ public static boolean isPlayerControlEnabled() { return playerControlEnabled; } + @ApiStatus.Internal + public static boolean shouldPreventInteractions() { + if (!isEnabled()) { + return false; + } + if (!ServerPolicies.allowInteract()) { + return true; + } + return !isPlayerControlEnabled() && ModConfig.get().shouldPreventInteractions(); + } + @ApiStatus.Experimental @ApiStatus.AvailableSince("1.2.4") public static boolean isRestrictedOnServer() { ServerData server = MC.getCurrentServer(); return server != null && !MC.hasSingleplayerServer() - && ModConfig.get().isRestrictedOnServer(server.ip); + && (!ServerPolicies.allowFreecam() || ModConfig.get().isRestrictedOnServer(server.ip)); } } diff --git a/common/src/main/java/net/xolt/freecam/config/model/ModConfigDTO.java b/common/src/main/java/net/xolt/freecam/config/model/ModConfigDTO.java index 89a9f3604..301b5f7a0 100644 --- a/common/src/main/java/net/xolt/freecam/config/model/ModConfigDTO.java +++ b/common/src/main/java/net/xolt/freecam/config/model/ModConfigDTO.java @@ -3,6 +3,7 @@ import com.google.gson.JsonObject; import net.minecraft.world.level.block.Block; import net.xolt.freecam.config.MCAwareModConfig; +import net.xolt.freecam.network.ServerPolicies; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -50,17 +51,17 @@ public double getVerticalSpeed() { @Override public boolean ignoreAllCollision() { - return collision.ignoreAll; + return ServerPolicies.allowClipping() && collision.ignoreAll; } @Override public boolean shouldCheckInitialCollision() { - return collision.alwaysCheck || !collision.ignoreAll; + return !ServerPolicies.allowClipping() || collision.alwaysCheck || !collision.ignoreAll; } @Override public boolean ignoreCollisionWith(Block block) { - return collision.ignoreAll || collisionPredicate.shouldIgnore(block); + return ServerPolicies.allowClipping() && (collision.ignoreAll || collisionPredicate.shouldIgnore(block)); } @Override @@ -80,7 +81,7 @@ public boolean shouldShowHand() { @Override public boolean isFullBrightEnabled() { - return visual.fullBright; + return ServerPolicies.allowFullbright() && visual.fullBright; } @Override @@ -100,11 +101,11 @@ public boolean shouldFreezePlayer() { @Override public boolean shouldPreventInteractions() { - return !utility.allowInteract; + return !ServerPolicies.allowInteract() || !utility.allowInteract; } public boolean allowInteractionsFrom(InteractionMode mode) { - return utility.allowInteract && utility.interactionMode == mode; + return ServerPolicies.allowInteract() && utility.allowInteract && utility.interactionMode == mode; } @Override diff --git a/common/src/main/java/net/xolt/freecam/mixins/ClientPacketListenerMixin.java b/common/src/main/java/net/xolt/freecam/mixins/ClientPacketListenerMixin.java index 270968f77..b9c714555 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/ClientPacketListenerMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/ClientPacketListenerMixin.java @@ -7,6 +7,12 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//? if <1.20.5 { +/*import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; +import net.xolt.freecam.network.ServerPolicies; +*///? } + @Mixin(ClientPacketListener.class) public class ClientPacketListenerMixin { @@ -14,7 +20,21 @@ public class ClientPacketListenerMixin { @Inject(method = "handleRespawn", at = @At("TAIL")) private void onPlayerRespawn(CallbackInfo ci) { if (Freecam.isEnabled()) { - Freecam.toggle(); + Freecam.disable(); } } + + //? if <1.20.5 { + /*@Inject(method = "handleCustomPayload", at = @At("HEAD")) + private void onCustomPayload(ClientboundCustomPayloadPacket packet, CallbackInfo ci) { + if (!ServerPolicies.CHANNEL.equals(packet.getIdentifier().toString())) { + return; + } + + FriendlyByteBuf data = packet.getData(); + byte[] bytes = new byte[data.readableBytes()]; + data.getBytes(data.readerIndex(), bytes); + ServerPolicies.applyBytes(bytes); + } + *///? } } diff --git a/common/src/main/java/net/xolt/freecam/mixins/GameRendererMixin.java b/common/src/main/java/net/xolt/freecam/mixins/GameRendererMixin.java index 375429015..0eb2f2c04 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/GameRendererMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/GameRendererMixin.java @@ -27,7 +27,7 @@ private void onRenderItemInHand(CallbackInfo ci) { // Disables block outlines when allowInteract is disabled. @Inject(method = "shouldRenderBlockOutline", at = @At("HEAD"), cancellable = true) private void onShouldRenderBlockOutline(CallbackInfoReturnable cir) { - if (Freecam.isEnabled() && !Freecam.isPlayerControlEnabled() && ModConfig.get().shouldPreventInteractions()) { + if (Freecam.shouldPreventInteractions()) { cir.setReturnValue(false); } } diff --git a/common/src/main/java/net/xolt/freecam/mixins/MinecraftMixin.java b/common/src/main/java/net/xolt/freecam/mixins/MinecraftMixin.java index 83a69d076..25f3e7f94 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/MinecraftMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/MinecraftMixin.java @@ -28,7 +28,15 @@ private void onDoAttack(CallbackInfoReturnable ci) { } } - // Prevents item pick when allowInteract is disabled. + // Prevents item use when interactions are disabled. + @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) + private void onUseItem(CallbackInfo ci) { + if (freecam$disableInteract()) { + ci.cancel(); + } + } + + // Prevents item pick when interactions are disabled. //~ if >=26.1 pickBlock -> pickBlockOrEntity @Inject(method = "pickBlockOrEntity", at = @At("HEAD"), cancellable = true) private void onDoItemPick(CallbackInfo ci) { @@ -75,6 +83,6 @@ private void onDisconnect(CallbackInfo ci) { @Unique private static boolean freecam$disableInteract() { - return Freecam.isEnabled() && !Freecam.isPlayerControlEnabled() && ModConfig.get().shouldPreventInteractions(); + return Freecam.shouldPreventInteractions(); } } diff --git a/common/src/main/java/net/xolt/freecam/mixins/MultiPlayerGameModeMixin.java b/common/src/main/java/net/xolt/freecam/mixins/MultiPlayerGameModeMixin.java index f72f81793..42c6d593b 100644 --- a/common/src/main/java/net/xolt/freecam/mixins/MultiPlayerGameModeMixin.java +++ b/common/src/main/java/net/xolt/freecam/mixins/MultiPlayerGameModeMixin.java @@ -9,30 +9,28 @@ import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.EntityHitResult; import net.xolt.freecam.Freecam; -import net.xolt.freecam.config.ModConfig; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import static net.xolt.freecam.Freecam.MC; //? if <=1.18.2 { /*import net.minecraft.client.multiplayer.ClientLevel; *///? } -import static net.xolt.freecam.Freecam.MC; - @Mixin(MultiPlayerGameMode.class) public class MultiPlayerGameModeMixin { // Prevents interacting with blocks when allowInteract is disabled. @Inject(method = "useItemOn", at = @At("HEAD"), cancellable = true) private void onInteractBlock(LocalPlayer player, - //? if <= 1.18.2 - //ClientLevel level, - InteractionHand hand, - BlockHitResult hitResult, - CallbackInfoReturnable cir) { + //? if <=1.18.2 + //ClientLevel level, + InteractionHand hand, + BlockHitResult hitResult, + CallbackInfoReturnable cir) { if (freecam$disableInteract()) { cir.setReturnValue(InteractionResult.PASS); } @@ -65,13 +63,13 @@ private void onInteractEntityAtLocation(Player player, Entity entity, EntityHitR // Prevents attacking self. @Inject(method = "attack", at = @At("HEAD"), cancellable = true) private void onAttackEntity(Player player, Entity target, CallbackInfo ci) { - if (target == MC.player) { + if (target == MC.player || freecam$disableInteract()) { ci.cancel(); } } @Unique private static boolean freecam$disableInteract() { - return Freecam.isEnabled() && !Freecam.isPlayerControlEnabled() && ModConfig.get().shouldPreventInteractions(); + return Freecam.shouldPreventInteractions(); } } diff --git a/common/src/main/java/net/xolt/freecam/network/ServerPolicies.java b/common/src/main/java/net/xolt/freecam/network/ServerPolicies.java new file mode 100644 index 000000000..5d13ec202 --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/network/ServerPolicies.java @@ -0,0 +1,103 @@ +package net.xolt.freecam.network; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +public final class ServerPolicies { + public static final String CHANNEL = "freecam:server_config"; + private static final Logger LOGGER = LoggerFactory.getLogger(ServerPolicies.class); + private static final Policies ALLOW_ALL = new Policies(true, true, true, true); + private static volatile Policies current = ALLOW_ALL; + + public static boolean allowFreecam() { + return current.allowFreecam; + } + + public static boolean allowClipping() { + return current.allowClipping; + } + + public static boolean allowFullbright() { + return current.allowFullbright; + } + + public static boolean allowInteract() { + return current.allowInteract; + } + + public static void reset() { + current = ALLOW_ALL; + } + + public static boolean applyBytes(byte[] payload) { + return applyJson(new String(payload, StandardCharsets.UTF_8)); + } + + public static boolean applyJson(String json) { + try { + JsonElement rootElement = parse(json); + if (!rootElement.isJsonObject()) { + throw new JsonParseException("root must be a JSON object"); + } + + JsonObject root = rootElement.getAsJsonObject(); + Policies parsed = new Policies( + readBoolean(root, "allowFreecam"), + readBoolean(root, "allowClipping"), + readBoolean(root, "allowFullbright"), + readBoolean(root, "allowInteract") + ); + current = parsed; + LOGGER.debug("Applied server Freecam policies: {}", parsed); + return true; + } catch (JsonParseException | IllegalStateException | ClassCastException e) { + LOGGER.warn("Ignoring invalid {} payload: {}", CHANNEL, e.getMessage()); + return false; + } + } + + private static JsonElement parse(String json) { + return new JsonParser().parse(json); + } + + private static boolean readBoolean(JsonObject root, String key) { + JsonElement value = root.get(key); + if (value == null) { + return true; + } + if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) { + throw new JsonParseException("'" + key + "' must be a boolean"); + } + return value.getAsBoolean(); + } + + private static final class Policies { + private final boolean allowFreecam; + private final boolean allowClipping; + private final boolean allowFullbright; + private final boolean allowInteract; + + private Policies(boolean allowFreecam, boolean allowClipping, boolean allowFullbright, boolean allowInteract) { + this.allowFreecam = allowFreecam; + this.allowClipping = allowClipping; + this.allowFullbright = allowFullbright; + this.allowInteract = allowInteract; + } + + @Override + public String toString() { + return "Policies{" + + "allowFreecam=" + allowFreecam + + ", allowClipping=" + allowClipping + + ", allowFullbright=" + allowFullbright + + ", allowInteract=" + allowInteract + + '}'; + } + } +} diff --git a/common/src/main/java/net/xolt/freecam/network/ServerPolicyPayload.java b/common/src/main/java/net/xolt/freecam/network/ServerPolicyPayload.java new file mode 100644 index 000000000..3823481ab --- /dev/null +++ b/common/src/main/java/net/xolt/freecam/network/ServerPolicyPayload.java @@ -0,0 +1,43 @@ +//? if >=1.20.5 { +package net.xolt.freecam.network; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.EncoderException; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +//~ if >=1.21.11 ResourceLocation -> Identifier +import net.minecraft.resources.Identifier; +import net.xolt.freecam.Freecam; + +import java.nio.charset.StandardCharsets; + +public record ServerPolicyPayload(String json) implements CustomPacketPayload { + public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(identifier()); + public static final StreamCodec STREAM_CODEC = new StreamCodec<>() { + @Override + public ServerPolicyPayload decode(ByteBuf buffer) { + byte[] bytes = new byte[buffer.readableBytes()]; + buffer.readBytes(bytes); + return new ServerPolicyPayload(new String(bytes, StandardCharsets.UTF_8)); + } + + @Override + public void encode(ByteBuf buffer, ServerPolicyPayload payload) { + byte[] bytes = payload.json().getBytes(StandardCharsets.UTF_8); + buffer.writeBytes(bytes); + } + }; + + @Override + public CustomPacketPayload.Type type() { + return TYPE; + } + + private static Identifier identifier() { + //? if >=1.21 { + return Identifier.fromNamespaceAndPath(Freecam.MOD_ID, "server_config"); + //? } else + //return new ResourceLocation(Freecam.MOD_ID, "server_config"); + } +} +//? } diff --git a/fabric/src/main/java/net/xolt/freecam/fabric/FabricServerPolicyNetworking.java b/fabric/src/main/java/net/xolt/freecam/fabric/FabricServerPolicyNetworking.java new file mode 100644 index 000000000..9d06405cc --- /dev/null +++ b/fabric/src/main/java/net/xolt/freecam/fabric/FabricServerPolicyNetworking.java @@ -0,0 +1,30 @@ +//? if >=1.20.5 { +package net.xolt.freecam.fabric; + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.xolt.freecam.network.ServerPolicies; +import net.xolt.freecam.network.ServerPolicyPayload; + +public final class FabricServerPolicyNetworking { + + public static void register() { + /*? if >=26.1 {*/ + PayloadTypeRegistry.clientboundPlay().register( + ServerPolicyPayload.TYPE, + ServerPolicyPayload.STREAM_CODEC + ); + /*?} else {*/ + /*PayloadTypeRegistry.playS2C().register( + ServerPolicyPayload.TYPE, + ServerPolicyPayload.STREAM_CODEC + );*/ + /*?}*/ + + ClientPlayNetworking.registerGlobalReceiver( + ServerPolicyPayload.TYPE, + (payload, context) -> ServerPolicies.applyJson(payload.json()) + ); + } +} +//? } diff --git a/fabric/src/main/java/net/xolt/freecam/fabric/FreecamFabric.java b/fabric/src/main/java/net/xolt/freecam/fabric/FreecamFabric.java index dc07b37b1..4685ab46a 100644 --- a/fabric/src/main/java/net/xolt/freecam/fabric/FreecamFabric.java +++ b/fabric/src/main/java/net/xolt/freecam/fabric/FreecamFabric.java @@ -11,6 +11,8 @@ public class FreecamFabric implements ClientModInitializer { @Override public void onInitializeClient() { ModConfig.setup(); + //? if >=1.20.5 + FabricServerPolicyNetworking.register(); ModBindings.forEach(KeyMappingHelper::registerKeyMapping); ClientTickEvents.START_CLIENT_TICK.register(Freecam::preTick); ClientTickEvents.END_CLIENT_TICK.register(Freecam::postTick); diff --git a/neoforge/src/main/java/net/xolt/freecam/forge/FreecamNeoforge.java b/neoforge/src/main/java/net/xolt/freecam/forge/FreecamNeoforge.java index 59b2fe7f8..9cb4b8b05 100644 --- a/neoforge/src/main/java/net/xolt/freecam/forge/FreecamNeoforge.java +++ b/neoforge/src/main/java/net/xolt/freecam/forge/FreecamNeoforge.java @@ -11,10 +11,19 @@ import net.neoforged.neoforge.client.event.ClientTickEvent; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; import net.neoforged.neoforge.client.gui.IConfigScreenFactory; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; import net.xolt.freecam.Freecam; import net.xolt.freecam.config.ModBindings; import net.xolt.freecam.config.ModConfig; import net.xolt.freecam.config.gui.ConfigScreenProvider; +import net.xolt.freecam.network.ServerPolicies; +import net.xolt.freecam.network.ServerPolicyPayload; + +//? if >=1.21.11 +import net.neoforged.neoforge.client.network.event.RegisterClientPayloadHandlersEvent; + @Mod(value = Freecam.MOD_ID, dist = Dist.CLIENT) @EventBusSubscriber( @@ -43,6 +52,31 @@ public static void registerKeymappings(RegisterKeyMappingsEvent event) { ModBindings.forEach(event::register); } + @SubscribeEvent + public static void registerPayloads(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar("1").optional(); + //? if >=1.21.11 { + registrar.playToClient(ServerPolicyPayload.TYPE, ServerPolicyPayload.STREAM_CODEC); + //? } else { + /*registrar.playToClient( + ServerPolicyPayload.TYPE, + ServerPolicyPayload.STREAM_CODEC, + FreecamNeoforge::handleServerPolicy + ); + *///? } + } + + //? if >=1.21.11 { + @SubscribeEvent + public static void registerClientPayloads(RegisterClientPayloadHandlersEvent event) { + event.register(ServerPolicyPayload.TYPE, FreecamNeoforge::handleServerPolicy); + } + //? } + + private static void handleServerPolicy(ServerPolicyPayload payload, IPayloadContext context) { + ServerPolicies.applyJson(payload.json()); + } + //? neoforge: <21 { /*@EventBusSubscriber(bus = EventBusSubscriber.Bus.GAME, value = Dist.CLIENT) public static class GlobalEventHandler {