package one.oth3r.sit.utl; import com.google.gson.*; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.google.gson.stream.MalformedJsonException; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.minecraft.block.*; import net.minecraft.block.enums.BlockHalf; import net.minecraft.block.enums.SlabType; import net.minecraft.entity.EntityType; import net.minecraft.entity.decoration.DisplayEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.BlockItem; import net.minecraft.item.ItemStack; import net.minecraft.registry.Registries; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.text.TextColor; import net.minecraft.util.*; import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.hit.HitResult; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; import net.minecraft.world.RaycastContext; import net.minecraft.world.World; import one.oth3r.sit.file.*; import one.oth3r.sit.packet.SitPayloads; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; public class Utl { /** * check if a block is obstructed (no collision / custom list) * @return true if not obstructed */ public static boolean isNotObstructed(World world, BlockPos blockPos) { // get the block state at the blockPos BlockState state = world.getBlockState(blockPos); // make sure it doesn't have a collision return state.getCollisionShape(world,blockPos).isEmpty(); } public static class Num { public static boolean isInt(String string) { try { Integer.parseInt(string); } catch (NumberFormatException nfe) { return false; } return true; } public static Integer toInt(String s) { // return an int no matter what try { return Integer.parseInt(s); } catch (NumberFormatException e) { try { return (int) Double.parseDouble(s); } catch (NumberFormatException e2) { return 0; } } } public static boolean isNum(String s) { // checks if int or a double try { Integer.parseInt(s); return true; } catch (NumberFormatException e1) { try { Double.parseDouble(s); return true; } catch (NumberFormatException e2) { return false; } } } } public static final double HALF_BLOCK = 0.5; public static final double CARPET = 0.062; /** * checks if the provided itemstack is a valid one for the provided filter * @param filter the filter * @param itemStack itemstack to check * @return if true, the item isn't filtered out */ public static boolean checkItem(HandSetting.Filter filter, ItemStack itemStack) { // default to true if there's nothing if (itemStack.isEmpty()) return true; boolean itemcheck = filter.getCustomItems().checkItem(itemStack); // iif the item passes the checks, return true if (itemcheck) return true; // if none of the custom were met, try the default conditions // get the use actions for the filters ArrayList food = new ArrayList<>(); food.add(UseAction.EAT); food.add(UseAction.DRINK); ArrayList notUsable = new ArrayList<>(food); notUsable.add(UseAction.NONE); // try the default conditions if (filter.isBlock() && itemStack.getItem() instanceof BlockItem) return true; if (filter.isFood() && food.contains(itemStack.getUseAction())) return true; if (filter.isUsable() && !notUsable.contains(itemStack.getUseAction())) return true; // if nothing else is met, the item is filtered out return false; } /** * get a block ID (namespace, minecraft:air) from a blockstate. (it is easier with a block, but we are mostly working with block states * @return the block ID (minecraft:air) */ public static String getBlockID(BlockState blockState) { return Registries.BLOCK.getId(blockState.getBlock()).toString(); } /** * gets the sitting height for the provided blockstate, via memory loaded config from Data * @param blockState the state of the block * @param player the player to * @param blockPos the pos of the block * @param hit nullable, for the player interaction check * @return null if not a valid block */ public static Double getSittingHeight(BlockState blockState, ServerPlayerEntity player, BlockPos blockPos, @Nullable BlockHitResult hit) { ServerConfig config = FileData.getServerConfig(); Block block = blockState.getBlock(); // make sure that the block that is being sit on has no interaction when hand sitting if (hit != null && hasInteraction(blockState)) { return null; } // only if custom is enabled if (config.isCustomEnabled()) { // if the block is on the blacklist, false if (config.getBlacklistedBlocks().stream().anyMatch(c -> c.isValid(blockState))) return null; for (SittingBlock sittingBlock : config.getSittingBlocks()) { // if the block is valid, true if (sittingBlock.isValid(blockState)) return sittingBlock.getSittingHeight(); } } // add the default block types and check for them if (block instanceof StairsBlock && config.getPresetBlocks().isStairs() && blockState.get(StairsBlock.HALF) == BlockHalf.BOTTOM) return HALF_BLOCK; if (config.getPresetBlocks().isSlabs() && block instanceof SlabBlock && blockState.get(SlabBlock.TYPE) == SlabType.BOTTOM) return HALF_BLOCK; if (config.getPresetBlocks().isCarpets() && block instanceof CarpetBlock) return CARPET; if (config.getPresetBlocks().isFullBlocks() // make sure the block is a full cube && blockState.isFullCube(player.getWorld(),blockPos)) return 1.0; // at the end, return false return null; } /** * checks if a block has an interaction * @param blockState * @return */ public static boolean hasInteraction(BlockState blockState) { return isMethodOverridden(AbstractBlock.class, blockState.getBlock().getClass(), "onUse", BlockState.class, World.class, BlockPos.class, PlayerEntity.class, BlockHitResult.class); } /** * checks if a method in the base class has been overridden in the subclass(es) by: * checking the subclass and its supers till the original method is found or the original method is found * @param baseClass the base class * @param subclass the subclass to check * @param methodName the method to check for * @param parameterTypes the parameterTypes for the method, see {@link java.lang.Class#getDeclaredMethod(java.lang.String, java.lang.Class[])} * @return if the method is overridden or not */ public static boolean isMethodOverridden(Class baseClass, Class subclass, String methodName, Class... parameterTypes) { try { // get the original method Method superMethod = baseClass.getMethod(methodName, parameterTypes); // the current class to check, starting with the subclass Class currentClass = subclass; // while the class is null and the current class isn't the same as the baseclass. while (currentClass != null && !currentClass.equals(baseClass)) { try { // get the submethod Method subMethod = currentClass.getDeclaredMethod(methodName, parameterTypes); // check if the methods are different if (!superMethod.equals(subMethod)) { return true; } } catch (NoSuchMethodException ignored) { // method isnt in this class, bump up a class and check that one } currentClass = currentClass.getSuperclass(); } } catch (NoSuchMethodException e) { // method doesn't exist in the base class return false; } // an override wasn't found return false; } public static class Entity { /** * checks if the entity's block is still there, & is valid */ public static boolean isValid(ServerPlayerEntity player, @NotNull DisplayEntity.TextDisplayEntity entity) { BlockPos blockPos = getBlockPos(entity); // get the blockstate BlockState blockState = player.getWorld().getBlockState(blockPos); // check if the block is still there & the block is a valid sit block (by checking if there is a sit height for the block) return !blockState.isAir() && getSittingHeight(blockState,player,blockPos,null) != null; } /** * gets the bound block pos of the sit entity */ public static BlockPos getBlockPos(DisplayEntity.TextDisplayEntity entity) { // get the block pos BlockPos pos = new BlockPos(entity.getBlockX(),entity.getBlockY(),entity.getBlockZ()); // if above the block, subtract 1 if (isAboveBlockHeight(entity)) { pos = pos.add(0,-1,0); } return pos; } /** * using the entity's pitch, figure out if the player is above the block height or not */ public static boolean isAboveBlockHeight(DisplayEntity.TextDisplayEntity entity) { return entity.getPitch() > 0; } /** * creates the sit entity from the pos & sit height provided * @param world the world to make the entity in * @param blockPos the pos of the entity * @param sitHeight the height for the entity to be at * @return the entity at the correct height and position */ public static DisplayEntity.TextDisplayEntity create(World world, BlockPos blockPos, double sitHeight) { DisplayEntity.TextDisplayEntity entity = new DisplayEntity.TextDisplayEntity(EntityType.TEXT_DISPLAY,world); entity.setCustomName(Text.of(Data.ENTITY_NAME)); entity.setCustomNameVisible(false); entity.setInvulnerable(true); entity.setInvisible(true); entity.updatePositionAndAngles(blockPos.getX()+.5, blockPos.getY()+sitHeight, blockPos.getZ()+.5, 0, 0); // // 1.20.2 mounting pos change (shifts everything down by .25) // double oneTwentyTwo = .25; // entity.updatePositionAndAngles(entity.getX(),entity.getY()+oneTwentyTwo,entity.getZ(),0,0); // change pitch based on if player is sitting below block height or not (full block height only) if (entity.getY() == blockPos.getY() + 1) entity.setPitch(90); // below else entity.setPitch(-90); // above return entity; } /** * removes the entity from the entity map and world, dismounting any passengers */ public static void remove(DisplayEntity.TextDisplayEntity entity) { // dismount everyone entity.removeAllPassengers(); // remove the entity entity.setRemoved(net.minecraft.entity.Entity.RemovalReason.DISCARDED); // remove the entity from the data set if exists FileData.removeSitEntity(entity); } /** * spawns the entity and make the player sit on it */ public static void spawnSit(ServerPlayerEntity player, DisplayEntity.TextDisplayEntity entity) { Data.setSpawnList(player, entity); } /** * removes all sit entities loaded on the server */ public static void purge(ServerPlayerEntity player, boolean message) { // todo test if it can purge an entity from a disconnected player or unloaded chunks // get a list of sit entities List list = player.getServerWorld() .getEntitiesByType(TypeFilter.instanceOf(DisplayEntity.TextDisplayEntity.class), entity -> entity.getName().getString().equals(Data.ENTITY_NAME)); // amount of sit entities purged int count = 0; // remove each one & count for (DisplayEntity.TextDisplayEntity entity : list) { remove(entity); count++; } // send a message if needed if (message) { player.sendMessage(messageTag().append(Utl.lang("sit!.chat.purged",Utl.lang("sit!.chat.purged.total",count).styled( style -> style.withColor(Colors.LIGHT_GRAY).withItalic(true) )).styled( style -> style.withColor(Colors.GREEN) ))); } } } public static MutableText messageTag() { return Text.literal("[").append(Text.literal("Sit!").styled( style -> style.withColor(TextColor.parse("#c400ff").result().orElse(TextColor.fromFormatting(Formatting.DARK_PURPLE)))) ).append("] "); } /** * gets a MutableText using the language key, if on server, using the custom lang reader */ public static MutableText lang(String key, Object... args) { if (Data.isClient()) return Text.translatable(key, args); else return LangReader.of(key, args).getTxT(); } public static class Enum { public static > T get(Object enumString, Class enumType) { return get(enumString,enumType,enumType.getEnumConstants()[0]); } /** * gets an enum from a string without returning null * @param enumString the string of the enum * @param enumType the class of enums * @param defaultEnum the enum to return if a match isn't found * @return an enum, if there isn't a match, it returns the first enum */ public static > T get(Object enumString, Class enumType, T defaultEnum) { T[] values = enumType.getEnumConstants(); for (T all : values) { // check if there is a match for any of the enum names if (enumString.toString().equals(all.name())) return all; } // if there's no match return the first entry return defaultEnum; } } // todo call when editing a config on the client /** * sends the settings packets to the server, if client & in game */ public static void sendSettingsPackets() { if (Data.isClient() && Data.isInGame()) { ClientPlayNetworking.send(new SitPayloads.SettingsPayload(Utl.getGson().toJson(FileData.getSittingConfig()))); } } /** * gets a Gson with the LenientTypeAdapter */ public static Gson getGson() { return new GsonBuilder() .disableHtmlEscaping() .setPrettyPrinting() .registerTypeAdapterFactory(new LenientTypeAdapterFactory()) .create(); } /** * the LenientTypeAdapter, doesn't throw anything when reading a weird JSON entry, good for human entered JSONs */ @SuppressWarnings("unchecked") public static class LenientTypeAdapterFactory implements TypeAdapterFactory { public TypeAdapter create(Gson gson, TypeToken type) { final TypeAdapter delegate = gson.getDelegateAdapter(this, type); // Check if the type is a List, then run the custom list type adapter if (List.class.isAssignableFrom(type.getRawType())) { Type elementType = ((ParameterizedType) type.getType()).getActualTypeArguments()[0]; TypeAdapter elementAdapter = gson.getAdapter(TypeToken.get(elementType)); // the custom adapter return (TypeAdapter) new RemoveNullListTypeAdapter<>(elementAdapter); } return new TypeAdapter<>() { // normal writer public void write(JsonWriter out, T value) throws IOException { delegate.write(out, value); } // custom reader public T read(JsonReader in) throws IOException { try { //Try to read value using default TypeAdapter return delegate.read(in); } catch (JsonSyntaxException | MalformedJsonException e) { // don't throw anything if there's a weird JSON, just return null in.skipValue(); return null; } } }; } } /** * type adapter that doesnt allow null / bad entries */ private static class RemoveNullListTypeAdapter extends TypeAdapter> { private final TypeAdapter elementAdapter; RemoveNullListTypeAdapter(TypeAdapter elementAdapter) { this.elementAdapter = elementAdapter; } @Override public void write(JsonWriter out, List value) throws IOException { out.beginArray(); for (E element : value) { elementAdapter.write(out, element); } out.endArray(); } @Override public List read(JsonReader in) throws IOException { List list = new ArrayList<>(); in.beginArray(); while (in.hasNext()) { try { E element = elementAdapter.read(in); // skip null entry if (element == null) continue; list.add(element); } catch (Exception e) { // skip invalid entry in.skipValue(); } } in.endArray(); return list; } } public static BlockPos getBlockPosPlayerIsLookingAt(ServerWorld world, PlayerEntity player, double range) { // pos, adjusted to player eye level Vec3d rayStart = player.getPos().add(0, player.getEyeHeight(player.getPose()), 0); // extend ray by the range Vec3d rayEnd = rayStart.add(player.getRotationVector().multiply(range)); BlockHitResult hitResult = world.raycast(new RaycastContext(rayStart, rayEnd, RaycastContext.ShapeType.OUTLINE, RaycastContext.FluidHandling.NONE, ShapeContext.absent())); if (hitResult.getType() == HitResult.Type.BLOCK) { return hitResult.getBlockPos(); } return new BlockPos(player.getBlockPos()); } }