feat(mod): apply server policy from the proxy
This commit is contained in:
parent
711fcafc3a
commit
33cff90c82
10 changed files with 455 additions and 1 deletions
|
|
@ -25,6 +25,7 @@ import dev.loki.lovisual.features.module.phase.Notifier;
|
|||
import dev.loki.lovisual.features.module.lifecycle.ModuleAutoLoader;
|
||||
import dev.loki.lovisual.features.module.core.ModuleManager;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.features.platform.serverpolicy.ServerPolicyClient;
|
||||
import dev.loki.lovisual.render.effects.NetherPortalRiftPass;
|
||||
import dev.loki.lovisual.render.effects.SleepOverlayPass;
|
||||
import dev.loki.lovisual.render.engine.LoVisualRenderEngineBootstrap;
|
||||
|
|
@ -60,6 +61,7 @@ public class LoVisual implements ClientModInitializer {
|
|||
AccountConfig.get();
|
||||
ProxyBackend.init();
|
||||
CommandManager.init();
|
||||
ServerPolicyClient.init();
|
||||
MediaSessionService.get().init();
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> MediaSessionService.get().shutdown());
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import dev.loki.lovisual.Lifecycle;
|
|||
import dev.loki.lovisual.addon.manager.ModuleExtensionManager;
|
||||
import dev.loki.lovisual.config.values.mode.EnumValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.features.command.util.CommandOutput;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.module.core.ModuleManager;
|
||||
import dev.loki.lovisual.features.module.lifecycle.ModuleActivationSource;
|
||||
import dev.loki.lovisual.features.platform.serverpolicy.ServerPolicyState;
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
|
||||
final class ModuleLifecycleHelper {
|
||||
private final Module owner;
|
||||
|
|
@ -28,6 +31,7 @@ final class ModuleLifecycleHelper {
|
|||
|
||||
void setEnabled(boolean state, ModuleActivationSource src, BooleanValue ev, EnumValue<ModuleActivationSource> av) {
|
||||
if (state && !Lifecycle.isActive()) return;
|
||||
if (state && !serverPolicyAllows()) return;
|
||||
ModuleActivationSource norm = normalize(state, src);
|
||||
if (enabled==state) {
|
||||
if (source==norm && ev.get()==enabled && av.get()==source) return;
|
||||
|
|
@ -47,7 +51,23 @@ final class ModuleLifecycleHelper {
|
|||
dev.loki.lovisual.features.module.module.ModuleConfigValues.requestSaveQuietly(owner);
|
||||
}
|
||||
|
||||
/** Server policy gate: deny in lock mode, warn-but-allow in suggest mode. */
|
||||
private boolean serverPolicyAllows() {
|
||||
String id = owner.name();
|
||||
if (ServerPolicyState.allows(id)) {
|
||||
if (ServerPolicyState.suggestWarn(id)) {
|
||||
CommandOutput.warning(I18n.get("lovisual.policy.suggest",
|
||||
owner.getDisplayName(), ServerPolicyState.reason()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
CommandOutput.warning(I18n.get("lovisual.policy.denied",
|
||||
owner.getDisplayName(), ServerPolicyState.reason()));
|
||||
return false;
|
||||
}
|
||||
|
||||
void setTransient(boolean state, BooleanValue ev, EnumValue<ModuleActivationSource> av) {
|
||||
if (state && !ServerPolicyState.allows(owner.name())) return;
|
||||
if (enabled==state) return; enabled=state;
|
||||
if (enabled) {
|
||||
if (ModuleExtensionManager.beforeEnable(owner)) { owner.onEnable(); ModuleExtensionManager.afterEnable(owner);} else enabled=false;
|
||||
|
|
@ -56,7 +76,7 @@ final class ModuleLifecycleHelper {
|
|||
}
|
||||
|
||||
void loadAndApply(BooleanValue ev, EnumValue<ModuleActivationSource> av) {
|
||||
enabled=ev.get(); source=normalize(enabled, av.get()); av.set(source);
|
||||
enabled=ev.get() && ServerPolicyState.allows(owner.name()); source=normalize(enabled, av.get()); av.set(source);
|
||||
if (enabled) {
|
||||
if (ModuleExtensionManager.beforeEnable(owner)) { owner.onEnable(); ModuleExtensionManager.afterEnable(owner);}
|
||||
else { enabled=false; source=ModuleActivationSource.NONE; ev.set(false); av.set(source); }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package dev.loki.lovisual.features.platform.serverpolicy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A parsed server policy from the {@code lovisual:policy} channel: which
|
||||
* modules the proxy denies ({@link Mode#LOCK} = refuse to enable and lock the
|
||||
* ClickGui card, {@link Mode#SUGGEST} = warn only).
|
||||
*/
|
||||
public record ServerPolicy(Mode mode, String reason, List<String> deny, String server) {
|
||||
|
||||
public enum Mode {
|
||||
LOCK,
|
||||
SUGGEST
|
||||
}
|
||||
|
||||
public ServerPolicy {
|
||||
mode = mode == null ? Mode.LOCK : mode;
|
||||
reason = reason == null ? "" : reason;
|
||||
deny = deny == null ? List.of() : List.copyOf(deny);
|
||||
server = server == null ? "" : server;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return mode == Mode.LOCK;
|
||||
}
|
||||
|
||||
public boolean denies(String moduleId) {
|
||||
return moduleId != null && deny.contains(moduleId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package dev.loki.lovisual.features.platform.serverpolicy;
|
||||
|
||||
import dev.loki.lovisual.features.command.util.CommandOutput;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.module.core.ModuleManager;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/**
|
||||
* Fabric side of the {@code lovisual:policy} channel: registers the payload,
|
||||
* asks the proxy for a policy on every join, applies incoming policies (force
|
||||
* disabling denied modules) and forgets everything on disconnect so
|
||||
* singleplayer has no policy.
|
||||
*/
|
||||
public final class ServerPolicyClient {
|
||||
|
||||
private ServerPolicyClient() {
|
||||
}
|
||||
|
||||
/** The one-field JSON payload travelling on {@code lovisual:policy}. */
|
||||
public record Payload(String json) implements CustomPacketPayload {
|
||||
|
||||
public static final CustomPacketPayload.Type<Payload> TYPE =
|
||||
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath("lovisual", "policy"));
|
||||
public static final StreamCodec<ByteBuf, Payload> CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.STRING_UTF8, Payload::json, Payload::new);
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called once from {@code LoVisual#onInitializeClient}. */
|
||||
public static void init() {
|
||||
PayloadTypeRegistry.serverboundPlay().register(Payload.TYPE, Payload.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(Payload.TYPE, Payload.CODEC);
|
||||
ClientPlayNetworking.registerReceiver(Payload.TYPE,
|
||||
(payload, context) -> onPolicy(payload.json()));
|
||||
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) ->
|
||||
ClientPlayNetworking.send(new Payload(ServerPolicyCodec.REQUEST_JSON)));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) ->
|
||||
ServerPolicyState.clear());
|
||||
}
|
||||
|
||||
private static void onPolicy(String json) {
|
||||
ServerPolicy policy = ServerPolicyCodec.parse(json);
|
||||
if (policy == null) {
|
||||
return;
|
||||
}
|
||||
ServerPolicyState.apply(policy);
|
||||
forceDisableDenied(policy);
|
||||
}
|
||||
|
||||
/** A lock policy must stop modules that are already running. */
|
||||
private static void forceDisableDenied(ServerPolicy policy) {
|
||||
if (!policy.isLocked()) {
|
||||
return;
|
||||
}
|
||||
boolean stoppedAny = false;
|
||||
for (String id : policy.deny()) {
|
||||
Module module = ModuleManager.get(id);
|
||||
if (module != null && module.isEnabled()) {
|
||||
module.setEnabledInternal(false);
|
||||
stoppedAny = true;
|
||||
}
|
||||
}
|
||||
if (stoppedAny) {
|
||||
CommandOutput.warning(I18n.get("lovisual.policy.applied", policy.reason()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package dev.loki.lovisual.features.platform.serverpolicy;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JSON half of the {@code lovisual:policy} channel. Parsing is deliberately
|
||||
* forgiving: anything that is not a well-formed {@code "type":"policy"}
|
||||
* object returns {@code null}, and an unknown mode degrades to the
|
||||
* conservative {@link ServerPolicy.Mode#LOCK} — a broken payload must never
|
||||
* silently unlock the client.
|
||||
*/
|
||||
public final class ServerPolicyCodec {
|
||||
|
||||
/** The C→S answer the proxy matches on to push the policy again. */
|
||||
public static final String REQUEST_JSON = "{\"type\":\"request\"}";
|
||||
|
||||
private ServerPolicyCodec() {
|
||||
}
|
||||
|
||||
public static ServerPolicy parse(String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JsonElement element = JsonParser.parseString(json);
|
||||
if (!element.isJsonObject()) {
|
||||
return null;
|
||||
}
|
||||
JsonObject object = element.getAsJsonObject();
|
||||
JsonElement type = object.get("type");
|
||||
if (type == null || !type.isJsonPrimitive() || !"policy".equals(type.getAsString())) {
|
||||
return null;
|
||||
}
|
||||
return new ServerPolicy(
|
||||
parseMode(object.get("mode")),
|
||||
string(object.get("reason")),
|
||||
parseDeny(object.get("deny")),
|
||||
string(object.get("server")));
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ServerPolicy.Mode parseMode(JsonElement raw) {
|
||||
if (raw == null || !raw.isJsonPrimitive()) {
|
||||
return ServerPolicy.Mode.LOCK;
|
||||
}
|
||||
String value = raw.getAsString();
|
||||
if ("suggest".equalsIgnoreCase(value)) {
|
||||
return ServerPolicy.Mode.SUGGEST;
|
||||
}
|
||||
return ServerPolicy.Mode.LOCK;
|
||||
}
|
||||
|
||||
private static List<String> parseDeny(JsonElement raw) {
|
||||
if (raw == null || !raw.isJsonArray()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> out = new ArrayList<>();
|
||||
for (JsonElement item : raw.getAsJsonArray()) {
|
||||
if (item != null && item.isJsonPrimitive()) {
|
||||
out.add(item.getAsString());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String string(JsonElement raw) {
|
||||
return raw != null && raw.isJsonPrimitive() ? raw.getAsString() : "";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package dev.loki.lovisual.features.platform.serverpolicy;
|
||||
|
||||
/**
|
||||
* Static holder for the active server policy. {@link #allows} is the single
|
||||
* gate the module lifecycle consults; ClickGui reads {@link #isLocked} for the
|
||||
* card lock and {@link #suggestWarn} for the warn-but-allow path.
|
||||
*/
|
||||
public final class ServerPolicyState {
|
||||
|
||||
private static volatile ServerPolicy current;
|
||||
private static volatile long receivedAtMs;
|
||||
|
||||
private ServerPolicyState() {
|
||||
}
|
||||
|
||||
/** Installs a parsed policy (replacing any previous one). */
|
||||
public static void apply(ServerPolicy policy) {
|
||||
if (policy == null) {
|
||||
return;
|
||||
}
|
||||
current = policy;
|
||||
receivedAtMs = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/** Drops the policy — called on disconnect so singleplayer is unaffected. */
|
||||
public static void clear() {
|
||||
current = null;
|
||||
receivedAtMs = 0L;
|
||||
}
|
||||
|
||||
public static ServerPolicy current() {
|
||||
return current;
|
||||
}
|
||||
|
||||
public static long receivedAtMs() {
|
||||
return receivedAtMs;
|
||||
}
|
||||
|
||||
public static boolean active() {
|
||||
return current != null;
|
||||
}
|
||||
|
||||
/** True when the module may be enabled: no policy, no lock, or not denied. */
|
||||
public static boolean allows(String moduleId) {
|
||||
ServerPolicy policy = current;
|
||||
return policy == null || !policy.isLocked() || !policy.denies(moduleId);
|
||||
}
|
||||
|
||||
/** True when the module is listed by a {@code lock} policy. */
|
||||
public static boolean isLocked(String moduleId) {
|
||||
ServerPolicy policy = current;
|
||||
return policy != null && policy.isLocked() && policy.denies(moduleId);
|
||||
}
|
||||
|
||||
/** True when the module is listed by a {@code suggest} policy. */
|
||||
public static boolean suggestWarn(String moduleId) {
|
||||
ServerPolicy policy = current;
|
||||
return policy != null && !policy.isLocked() && policy.denies(moduleId);
|
||||
}
|
||||
|
||||
/** The proxy-supplied reason text, for messages; empty when no policy. */
|
||||
public static String reason() {
|
||||
ServerPolicy policy = current;
|
||||
return policy == null ? "" : policy.reason();
|
||||
}
|
||||
}
|
||||
|
|
@ -3259,6 +3259,7 @@
|
|||
"command.iris.description": "Shows Iris and shader compatibility diagnostics.",
|
||||
"command.modules.description": "Lists modules by state or category.",
|
||||
"command.ping.description": "Shows the current server latency.",
|
||||
"command.policy.description": "Shows the policy the proxy sends to this client (mode, denied modules, reason).",
|
||||
"command.profiler.description": "Controls rendering and runtime profilers.",
|
||||
"command.serverinfo.description": "Shows information about the current world or server.",
|
||||
"command.staff.description": "Manages the client staff list.",
|
||||
|
|
@ -3266,6 +3267,10 @@
|
|||
"command.tps.description": "Shows estimated server ticks per second.",
|
||||
"command.username.description": "Shows the current Minecraft username and UUID.",
|
||||
"command.xaero.description": "Adds, lists, or removes Xaero waypoints.",
|
||||
"lovisual.policy.applied": "The server disabled modules by its rules: %s",
|
||||
"lovisual.policy.denied": "Module %s is disabled by server policy: %s",
|
||||
"lovisual.policy.locked": "Disabled by server: %s",
|
||||
"lovisual.policy.suggest": "The server does not recommend %s: %s",
|
||||
"module.animations.description": "Animates interface transitions.",
|
||||
"setting.animations.hotbar_raise": "Hotbar raise",
|
||||
"setting.animations.inventory": "Inventory open",
|
||||
|
|
|
|||
|
|
@ -3054,6 +3054,7 @@
|
|||
"command.iris.description": "Показывает диагностику совместимости Iris и шейдеров.",
|
||||
"command.modules.description": "Показывает модули по состоянию или категории.",
|
||||
"command.ping.description": "Показывает текущую задержку до сервера.",
|
||||
"command.policy.description": "Показывает политику, которую прокси отправляет клиенту (режим, запрещённые модули, причина).",
|
||||
"command.profiler.description": "Управляет профилировщиками рендера и среды выполнения.",
|
||||
"command.serverinfo.description": "Показывает информацию о текущем мире или сервере.",
|
||||
"command.staff.description": "Управляет списком персонала клиента.",
|
||||
|
|
@ -3061,6 +3062,10 @@
|
|||
"command.tps.description": "Показывает расчётное количество серверных тиков в секунду.",
|
||||
"command.username.description": "Показывает текущее имя Minecraft и UUID.",
|
||||
"command.xaero.description": "Добавляет, показывает или удаляет точки Xaero.",
|
||||
"lovisual.policy.applied": "Сервер отключил модули по своим правилам: %s",
|
||||
"lovisual.policy.denied": "Модуль %s отключён политикой сервера: %s",
|
||||
"lovisual.policy.locked": "Отключено сервером: %s",
|
||||
"lovisual.policy.suggest": "Сервер не рекомендует %s: %s",
|
||||
"module.animations.description": "Плавные анимации интерфейса.",
|
||||
"setting.animations.hotbar_raise": "Подъём хотбара",
|
||||
"setting.animations.inventory": "Открытие инвентаря",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue