mirror of
https://github.com/Xiao-MoMi/craft-engine.git
synced 2025-12-31 12:56:28 +00:00
dev
- 修复可堆叠方块行为 - 尝试向 packetevents 注入自定义方块
This commit is contained in:
@@ -13,6 +13,7 @@ import net.momirealms.craftengine.bukkit.compatibility.model.modelengine.ModelEn
|
||||
import net.momirealms.craftengine.bukkit.compatibility.model.modelengine.ModelEngineUtils;
|
||||
import net.momirealms.craftengine.bukkit.compatibility.mythicmobs.MythicItemDropListener;
|
||||
import net.momirealms.craftengine.bukkit.compatibility.mythicmobs.MythicSkillHelper;
|
||||
import net.momirealms.craftengine.bukkit.compatibility.packetevents.WrappedBlockStateHelper;
|
||||
import net.momirealms.craftengine.bukkit.compatibility.papi.PlaceholderAPIUtils;
|
||||
import net.momirealms.craftengine.bukkit.compatibility.permission.LuckPermsEventListeners;
|
||||
import net.momirealms.craftengine.bukkit.compatibility.quickshop.QuickShopItemExpressionHandler;
|
||||
@@ -141,6 +142,22 @@ public class BukkitCompatibilityManager implements CompatibilityManager<org.bukk
|
||||
new QuickShopItemExpressionHandler(this.plugin).register();
|
||||
logHook("QuickShop-Hikari");
|
||||
}
|
||||
if (this.isPluginEnabled("packetevents")) {
|
||||
try {
|
||||
WrappedBlockStateHelper.register(null);
|
||||
} catch (Throwable e) {
|
||||
this.plugin.logger().warn("Failed to register block to WrappedBlockState", e);
|
||||
}
|
||||
logHook("packetevents");
|
||||
}
|
||||
if (this.isPluginEnabled("GrimAC")) {
|
||||
try {
|
||||
WrappedBlockStateHelper.register("ac{}grim{}grimac{}shaded{}com{}github{}retrooper{}packetevents");
|
||||
} catch (Throwable e) {
|
||||
this.plugin.logger().warn("Failed to register block to WrappedBlockState", e);
|
||||
}
|
||||
logHook("GrimAC");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.momirealms.craftengine.bukkit.compatibility.packetevents;
|
||||
|
||||
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
|
||||
import net.momirealms.craftengine.core.plugin.config.Config;
|
||||
import net.momirealms.craftengine.core.util.ReflectionUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
public final class WrappedBlockStateHelper {
|
||||
private static MethodHandle methodHandle$WrappedBlockState$BY_STRING$getter;
|
||||
private static MethodHandle methodHandle$WrappedBlockState$BY_ID$getter;
|
||||
private static MethodHandle methodHandle$WrappedBlockState$INTO_STRING$getter;
|
||||
private static MethodHandle methodHandle$WrappedBlockState$INTO_ID$getter;
|
||||
private static MethodHandle methodHandle$WrappedBlockState$DEFAULT_STATES$getter;
|
||||
private static MethodHandle methodHandle$WrappedBlockState$loadMappings;
|
||||
private static MethodHandle methodHandle$WrappedBlockState$constructor;
|
||||
private static MethodHandle methodHandle$StateTypes$builder;
|
||||
private static MethodHandle methodHandle$StateTypes$builder$name;
|
||||
private static MethodHandle methodHandle$StateTypes$builder$isBlocking;
|
||||
private static MethodHandle methodHandle$StateTypes$builder$setMaterial;
|
||||
private static MethodHandle methodHandle$StateTypes$builder$build;
|
||||
private static MethodHandle methodHandle$StateTypes$REGISTRY$getter;
|
||||
private static MethodHandle methodHandle$StateTypes$REGISTRY$getTypesBuilder;
|
||||
private static MethodHandle methodHandle$StateTypes$REGISTRY$load;
|
||||
private static MethodHandle methodHandle$StateTypes$REGISTRY$unloadFileMappings;
|
||||
private static Object instance$MaterialType$STONE;
|
||||
private static Object clientVersion;
|
||||
|
||||
private WrappedBlockStateHelper() {}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void register(@Nullable String packageName) throws Throwable {
|
||||
init(packageName);
|
||||
byte mappingsIndex = (byte) methodHandle$WrappedBlockState$loadMappings.invoke(clientVersion);
|
||||
Map<String, Object>[] BY_STRING = (Map<String, Object>[]) methodHandle$WrappedBlockState$BY_STRING$getter.invoke();
|
||||
Map<Integer, Object>[] BY_ID = (Map<Integer, Object>[]) methodHandle$WrappedBlockState$BY_ID$getter.invoke();
|
||||
Map<Object, String>[] INTO_STRING = (Map<Object, String>[]) methodHandle$WrappedBlockState$INTO_STRING$getter.invoke();
|
||||
Map<Object, Integer>[] INTO_ID = (Map<Object, Integer>[]) methodHandle$WrappedBlockState$INTO_ID$getter.invoke();
|
||||
Map<Object, Object>[] DEFAULT_STATES = (Map<Object, Object>[]) methodHandle$WrappedBlockState$DEFAULT_STATES$getter.invoke();
|
||||
Map<String, Object> stringWrappedBlockStateMap = BY_STRING[mappingsIndex];
|
||||
Map<Integer, Object> integerWrappedBlockStateMap = BY_ID[mappingsIndex];
|
||||
Map<Object, String> wrappedBlockStateStringMap = INTO_STRING[mappingsIndex];
|
||||
Map<Object, Integer> wrappedBlockStateIntegerMap = INTO_ID[mappingsIndex];
|
||||
Map<Object, Object> stateTypeWrappedBlockStateMap = DEFAULT_STATES[mappingsIndex];
|
||||
Object typesBuilder = methodHandle$StateTypes$REGISTRY$getTypesBuilder.invoke(methodHandle$StateTypes$REGISTRY$getter.invoke());
|
||||
methodHandle$StateTypes$REGISTRY$load.invoke(typesBuilder);
|
||||
for (int i = 0; i < Config.serverSideBlocks(); i++) {
|
||||
String blockId = "craftengine:custom_" + i;
|
||||
int id = BlockStateUtils.vanillaBlockStateCount() + i;
|
||||
Object stateType = methodHandle$StateTypes$builder$build.invoke(
|
||||
methodHandle$StateTypes$builder$setMaterial.invoke(
|
||||
methodHandle$StateTypes$builder$isBlocking.invoke(
|
||||
methodHandle$StateTypes$builder$name.invoke(
|
||||
methodHandle$StateTypes$builder.invoke(),
|
||||
blockId
|
||||
), true
|
||||
), instance$MaterialType$STONE
|
||||
)
|
||||
);
|
||||
Object wrappedBlockState = methodHandle$WrappedBlockState$constructor.invoke(stateType, Collections.emptyMap(), id, mappingsIndex);
|
||||
stringWrappedBlockStateMap.put(blockId, wrappedBlockState);
|
||||
integerWrappedBlockStateMap.put(id, wrappedBlockState);
|
||||
wrappedBlockStateStringMap.put(wrappedBlockState, blockId);
|
||||
wrappedBlockStateIntegerMap.put(wrappedBlockState, id);
|
||||
stateTypeWrappedBlockStateMap.put(stateType, wrappedBlockState);
|
||||
}
|
||||
methodHandle$StateTypes$REGISTRY$unloadFileMappings.invoke(typesBuilder);
|
||||
}
|
||||
|
||||
private static void init(@Nullable String packageName) throws Throwable {
|
||||
packageName = (packageName != null ? packageName : "com{}github{}retrooper{}packetevents").replace("{}", ".");
|
||||
Class<?> clazz$WrappedBlockState = Class.forName(packageName + ".protocol.world.states.WrappedBlockState");
|
||||
Class<?> clazz$PacketEvents = Class.forName(packageName + ".PacketEvents");
|
||||
Class<?> clazz$PacketEventsAPI = Class.forName(packageName + ".PacketEventsAPI");
|
||||
Class<?> clazz$ServerManager = Class.forName(packageName + ".manager.server.ServerManager");
|
||||
Class<?> clazz$ServerVersion = Class.forName(packageName + ".manager.server.ServerVersion");
|
||||
Class<?> clazz$ClientVersion = Class.forName(packageName + ".protocol.player.ClientVersion");
|
||||
Class<?> clazz$StateType = Class.forName(packageName + ".protocol.world.states.type.StateType");
|
||||
Class<?> clazz$StateTypes = Class.forName(packageName + ".protocol.world.states.type.StateTypes");
|
||||
Class<?> clazz$StateTypes$Builder = Class.forName(packageName + ".protocol.world.states.type.StateTypes$Builder");
|
||||
Class<?> clazz$MaterialType = Class.forName(packageName + ".protocol.world.MaterialType");
|
||||
Class<?> clazz$VersionedRegistry = Class.forName(packageName + ".util.mappings.VersionedRegistry");
|
||||
Class<?> clazz$TypesBuilder = Class.forName(packageName + ".util.mappings.TypesBuilder");
|
||||
MethodHandle methodHandle$PacketEvents$getAPI = ReflectionUtils.unreflectMethod(clazz$PacketEvents.getDeclaredMethod("getAPI"));
|
||||
MethodHandle methodHandle$PacketEventsAPI$getServerManager = ReflectionUtils.unreflectMethod(clazz$PacketEventsAPI.getDeclaredMethod("getServerManager"));
|
||||
MethodHandle methodHandle$ServerManager$getVersion = ReflectionUtils.unreflectMethod(clazz$ServerManager.getDeclaredMethod("getVersion"));
|
||||
MethodHandle methodHandle$ServerVersion$toClientVersion = ReflectionUtils.unreflectMethod(clazz$ServerVersion.getDeclaredMethod("toClientVersion"));
|
||||
methodHandle$WrappedBlockState$BY_STRING$getter = ReflectionUtils.unreflectGetter(clazz$WrappedBlockState.getDeclaredField("BY_STRING"));
|
||||
methodHandle$WrappedBlockState$BY_ID$getter = ReflectionUtils.unreflectGetter(clazz$WrappedBlockState.getDeclaredField("BY_ID"));
|
||||
methodHandle$WrappedBlockState$INTO_STRING$getter = ReflectionUtils.unreflectGetter(clazz$WrappedBlockState.getDeclaredField("INTO_STRING"));
|
||||
methodHandle$WrappedBlockState$INTO_ID$getter = ReflectionUtils.unreflectGetter(clazz$WrappedBlockState.getDeclaredField("INTO_ID"));
|
||||
methodHandle$WrappedBlockState$DEFAULT_STATES$getter = ReflectionUtils.unreflectGetter(clazz$WrappedBlockState.getDeclaredField("DEFAULT_STATES"));
|
||||
methodHandle$WrappedBlockState$loadMappings = ReflectionUtils.unreflectMethod(clazz$WrappedBlockState.getDeclaredMethod("loadMappings", clazz$ClientVersion));
|
||||
methodHandle$WrappedBlockState$constructor = ReflectionUtils.unreflectConstructor(clazz$WrappedBlockState.getDeclaredConstructor(clazz$StateType, Map.class, int.class, byte.class));
|
||||
methodHandle$StateTypes$builder = ReflectionUtils.unreflectMethod(clazz$StateTypes.getDeclaredMethod("builder"));
|
||||
methodHandle$StateTypes$builder$name = ReflectionUtils.unreflectMethod(clazz$StateTypes$Builder.getDeclaredMethod("name", String.class));
|
||||
methodHandle$StateTypes$builder$isBlocking = ReflectionUtils.unreflectMethod(clazz$StateTypes$Builder.getDeclaredMethod("isBlocking", boolean.class));
|
||||
methodHandle$StateTypes$builder$setMaterial = ReflectionUtils.unreflectMethod(clazz$StateTypes$Builder.getDeclaredMethod("setMaterial", clazz$MaterialType));
|
||||
methodHandle$StateTypes$builder$build = ReflectionUtils.unreflectMethod(clazz$StateTypes$Builder.getDeclaredMethod("build"));
|
||||
methodHandle$StateTypes$REGISTRY$getter = ReflectionUtils.unreflectGetter(clazz$StateTypes.getDeclaredField("REGISTRY"));
|
||||
methodHandle$StateTypes$REGISTRY$getTypesBuilder = ReflectionUtils.unreflectMethod(clazz$VersionedRegistry.getDeclaredMethod("getTypesBuilder"));
|
||||
methodHandle$StateTypes$REGISTRY$load = ReflectionUtils.unreflectMethod(clazz$TypesBuilder.getDeclaredMethod("load"));
|
||||
methodHandle$StateTypes$REGISTRY$unloadFileMappings = ReflectionUtils.unreflectMethod(clazz$TypesBuilder.getDeclaredMethod("unloadFileMappings"));
|
||||
instance$MaterialType$STONE = clazz$MaterialType.getDeclaredField("STONE").get(null);
|
||||
clientVersion = methodHandle$ServerVersion$toClientVersion.invoke(methodHandle$ServerManager$getVersion.invoke(methodHandle$PacketEventsAPI$getServerManager.invoke(methodHandle$PacketEvents$getAPI.invoke())));
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,10 @@ paper {
|
||||
register("ViaVersion") { required = false }
|
||||
register("QuickShop-Hikari") { required = false }
|
||||
|
||||
// PacketEvents
|
||||
register("GrimAC") { required = false }
|
||||
register("packetevents") { required = false }
|
||||
|
||||
// Geyser
|
||||
register("Geyser-Spigot") { required = false }
|
||||
register("floodgate") { required = false }
|
||||
|
||||
@@ -9,6 +9,7 @@ import net.momirealms.craftengine.core.block.BlockBehavior;
|
||||
import net.momirealms.craftengine.core.block.CustomBlock;
|
||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||
import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory;
|
||||
import net.momirealms.craftengine.core.block.behavior.CanBeReplacedBlockBehavior;
|
||||
import net.momirealms.craftengine.core.block.behavior.IsPathFindableBlockBehavior;
|
||||
import net.momirealms.craftengine.core.block.properties.Property;
|
||||
import net.momirealms.craftengine.core.block.properties.type.SlabType;
|
||||
@@ -25,7 +26,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class SlabBlockBehavior extends BukkitBlockBehavior implements IsPathFindableBlockBehavior {
|
||||
public class SlabBlockBehavior extends BukkitBlockBehavior implements IsPathFindableBlockBehavior, CanBeReplacedBlockBehavior {
|
||||
public static final Factory FACTORY = new Factory();
|
||||
private final Property<SlabType> typeProperty;
|
||||
|
||||
|
||||
@@ -1,100 +1,87 @@
|
||||
package net.momirealms.craftengine.bukkit.block.behavior;
|
||||
|
||||
import net.momirealms.craftengine.bukkit.nms.FastNMS;
|
||||
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
|
||||
import net.momirealms.craftengine.bukkit.util.LocationUtils;
|
||||
import net.momirealms.craftengine.core.block.BlockBehavior;
|
||||
import net.momirealms.craftengine.core.block.CustomBlock;
|
||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||
import net.momirealms.craftengine.core.block.UpdateOption;
|
||||
import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory;
|
||||
import net.momirealms.craftengine.core.block.behavior.CanBeReplacedBlockBehavior;
|
||||
import net.momirealms.craftengine.core.block.properties.IntegerProperty;
|
||||
import net.momirealms.craftengine.core.entity.player.InteractionHand;
|
||||
import net.momirealms.craftengine.core.entity.player.InteractionResult;
|
||||
import net.momirealms.craftengine.core.entity.player.Player;
|
||||
import net.momirealms.craftengine.core.block.properties.Property;
|
||||
import net.momirealms.craftengine.core.item.Item;
|
||||
import net.momirealms.craftengine.core.item.context.UseOnContext;
|
||||
import net.momirealms.craftengine.core.item.context.BlockPlaceContext;
|
||||
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
|
||||
import net.momirealms.craftengine.core.sound.SoundData;
|
||||
import net.momirealms.craftengine.core.util.ItemUtils;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import net.momirealms.craftengine.core.util.MiscUtils;
|
||||
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
|
||||
import net.momirealms.craftengine.core.world.BlockPos;
|
||||
import net.momirealms.craftengine.core.world.Vec3d;
|
||||
import net.momirealms.craftengine.core.world.World;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class StackableBlockBehavior extends BukkitBlockBehavior {
|
||||
public class StackableBlockBehavior extends BukkitBlockBehavior implements CanBeReplacedBlockBehavior {
|
||||
public static final Factory FACTORY = new Factory();
|
||||
private final IntegerProperty amountProperty;
|
||||
private final List<Key> items;
|
||||
private final SoundData stackSound;
|
||||
private final String propertyName;
|
||||
|
||||
public StackableBlockBehavior(CustomBlock block, IntegerProperty amountProperty, List<Key> items, SoundData stackSound) {
|
||||
public StackableBlockBehavior(CustomBlock block, IntegerProperty amountProperty, List<Key> items, String propertyName) {
|
||||
super(block);
|
||||
this.amountProperty = amountProperty;
|
||||
this.items = items;
|
||||
this.stackSound = stackSound;
|
||||
this.propertyName = propertyName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public InteractionResult useOnBlock(UseOnContext context, ImmutableBlockState state) {
|
||||
Player player = context.getPlayer();
|
||||
if (player == null || player.isSecondaryUseActive()) {
|
||||
return InteractionResult.PASS;
|
||||
public boolean canBeReplaced(BlockPlaceContext context, ImmutableBlockState state) {
|
||||
if (super.canBeReplaced(context, state)) {
|
||||
return true;
|
||||
}
|
||||
Item<ItemStack> item = (Item<ItemStack>) context.getItem();
|
||||
if (context.isSecondaryUseActive()) {
|
||||
return false;
|
||||
}
|
||||
Item<?> item = context.getItem();
|
||||
if (ItemUtils.isEmpty(item)) {
|
||||
return InteractionResult.PASS;
|
||||
return false;
|
||||
}
|
||||
if (!this.items.contains(item.id())) {
|
||||
return InteractionResult.PASS;
|
||||
return false;
|
||||
}
|
||||
BlockPos pos = context.getClickedPos();
|
||||
World world = context.getLevel();
|
||||
if (state.get(this.amountProperty) >= this.amountProperty.max) {
|
||||
return InteractionResult.SUCCESS_AND_CANCEL;
|
||||
Property<?> property = state.owner().value().getProperty(this.propertyName);
|
||||
if (property == null || property.valueClass() != Integer.class) {
|
||||
return false;
|
||||
}
|
||||
updateStackableBlock(state, pos, world, item, player, context.getHand());
|
||||
return InteractionResult.SUCCESS_AND_CANCEL;
|
||||
return (Integer) state.get(property) < this.amountProperty.max;
|
||||
}
|
||||
|
||||
private void updateStackableBlock(ImmutableBlockState state, BlockPos pos, World world, Item<ItemStack> item, Player player, InteractionHand hand) {
|
||||
ImmutableBlockState nextStage = state.cycle(this.amountProperty);
|
||||
Location location = new Location((org.bukkit.World) world.platformWorld(), pos.x(), pos.y(), pos.z());
|
||||
FastNMS.INSTANCE.method$LevelWriter$setBlock(world.serverWorld(), LocationUtils.toBlockPos(pos), nextStage.customBlockState().literalObject(), UpdateOption.UPDATE_ALL.flags());
|
||||
if (this.stackSound != null) {
|
||||
world.playBlockSound(new Vec3d(location.getX(), location.getY(), location.getZ()), this.stackSound);
|
||||
@Override
|
||||
public ImmutableBlockState updateStateForPlacement(BlockPlaceContext context, ImmutableBlockState state) {
|
||||
Object world = context.getLevel().serverWorld();
|
||||
Object pos = LocationUtils.toBlockPos(context.getClickedPos());
|
||||
ImmutableBlockState blockState = BlockStateUtils.getOptionalCustomBlockState(FastNMS.INSTANCE.method$BlockGetter$getBlockState(world, pos)).orElse(null);
|
||||
if (blockState == null) {
|
||||
return state;
|
||||
}
|
||||
if (!player.isCreativeMode()) {
|
||||
item.count(item.count() - 1);
|
||||
Property<?> property = blockState.owner().value().getProperty(this.propertyName);
|
||||
if (property == null || property.valueClass() != Integer.class) {
|
||||
return state;
|
||||
}
|
||||
player.swingHand(hand);
|
||||
return blockState.cycle(property);
|
||||
}
|
||||
|
||||
public static class Factory implements BlockBehaviorFactory {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public BlockBehavior create(CustomBlock block, Map<String, Object> arguments) {
|
||||
String propertyName = String.valueOf(arguments.getOrDefault("property", "amount"));
|
||||
IntegerProperty amount = (IntegerProperty) ResourceConfigUtils.requireNonNullOrThrow(block.getProperty(propertyName), () -> {
|
||||
throw new LocalizedResourceConfigException("warning.config.block.behavior.stackable.missing_property", propertyName);
|
||||
});
|
||||
Map<String, Object> sounds = (Map<String, Object>) arguments.get("sounds");
|
||||
SoundData stackSound = null;
|
||||
if (sounds != null) {
|
||||
stackSound = Optional.ofNullable(sounds.get("stack")).map(obj -> SoundData.create(obj, SoundData.SoundValue.FIXED_1, SoundData.SoundValue.FIXED_1)).orElse(null);
|
||||
}
|
||||
Object itemsObj = ResourceConfigUtils.requireNonNullOrThrow(arguments.get("items"), "warning.config.block.behavior.stackable.missing_items");
|
||||
List<Key> items = MiscUtils.getAsStringList(itemsObj).stream().map(Key::of).toList();
|
||||
return new StackableBlockBehavior(block, amount, items, stackSound);
|
||||
return new StackableBlockBehavior(block, amount, items, propertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
public class UnsafeCompositeBlockBehavior extends BukkitBlockBehavior
|
||||
implements FallOnBlockBehavior, PlaceLiquidBlockBehavior, IsPathFindableBlockBehavior {
|
||||
implements FallOnBlockBehavior, PlaceLiquidBlockBehavior, IsPathFindableBlockBehavior, CanBeReplacedBlockBehavior {
|
||||
private final AbstractBlockBehavior[] behaviors;
|
||||
|
||||
public UnsafeCompositeBlockBehavior(CustomBlock customBlock, List<AbstractBlockBehavior> behaviors) {
|
||||
@@ -258,8 +258,8 @@ public class UnsafeCompositeBlockBehavior extends BukkitBlockBehavior
|
||||
@Override
|
||||
public boolean canBeReplaced(BlockPlaceContext context, ImmutableBlockState state) {
|
||||
for (AbstractBlockBehavior behavior : this.behaviors) {
|
||||
if (!behavior.canBeReplaced(context, state)) {
|
||||
return false;
|
||||
if (behavior instanceof CanBeReplacedBlockBehavior canBeReplacedBlockBehavior) {
|
||||
return canBeReplacedBlockBehavior.canBeReplaced(context, state);
|
||||
}
|
||||
}
|
||||
return super.canBeReplaced(context, state);
|
||||
|
||||
Reference in New Issue
Block a user