Reorganized Imports/Packages

This commit is contained in:
Frank 2022-05-18 23:56:23 +02:00
parent cb9459f176
commit 3ee10482ab
721 changed files with 34873 additions and 33558 deletions

View file

@ -0,0 +1,65 @@
package org.betterx.bclib;
import net.minecraft.resources.ResourceLocation;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import org.betterx.bclib.api.WorldDataAPI;
import org.betterx.bclib.api.dataexchange.DataExchangeAPI;
import org.betterx.bclib.api.dataexchange.handler.autosync.*;
import org.betterx.bclib.api.tag.TagAPI;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.recipes.AnvilRecipe;
import org.betterx.bclib.recipes.CraftingRecipes;
import org.betterx.bclib.registry.BaseBlockEntities;
import org.betterx.bclib.registry.BaseRegistry;
import org.betterx.bclib.util.Logger;
import org.betterx.bclib.world.generator.BCLibEndBiomeSource;
import org.betterx.bclib.world.generator.BCLibNetherBiomeSource;
import org.betterx.bclib.world.generator.GeneratorOptions;
import java.util.List;
public class BCLib implements ModInitializer {
public static final String MOD_ID = "bclib";
public static final Logger LOGGER = new Logger(MOD_ID);
@Override
public void onInitialize() {
BaseRegistry.register();
GeneratorOptions.init();
BaseBlockEntities.register();
BCLibEndBiomeSource.register();
BCLibNetherBiomeSource.register();
TagAPI.init();
CraftingRecipes.init();
WorldDataAPI.registerModCache(MOD_ID);
DataExchangeAPI.registerMod(MOD_ID);
AnvilRecipe.register();
DataExchangeAPI.registerDescriptors(List.of(
HelloClient.DESCRIPTOR,
HelloServer.DESCRIPTOR,
RequestFiles.DESCRIPTOR,
SendFiles.DESCRIPTOR,
Chunker.DESCRIPTOR
));
BCLibPatch.register();
Configs.save();
}
public static boolean isDevEnvironment() {
return FabricLoader.getInstance().isDevelopmentEnvironment();
}
public static boolean isClient() {
return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT;
}
public static ResourceLocation makeID(String path) {
return new ResourceLocation(MOD_ID, path);
}
}

View file

@ -0,0 +1,95 @@
package org.betterx.bclib;
import net.minecraft.nbt.CompoundTag;
import org.betterx.bclib.api.datafixer.DataFixerAPI;
import org.betterx.bclib.api.datafixer.ForcedLevelPatch;
import org.betterx.bclib.api.datafixer.MigrationProfile;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.world.generator.GeneratorOptions;
public final class BCLibPatch {
public static void register() {
// TODO separate values in config on client side (config screen)
if (Configs.MAIN_CONFIG.repairBiomes() && (GeneratorOptions.fixEndBiomeSource() || GeneratorOptions.fixNetherBiomeSource())) {
DataFixerAPI.registerPatch(BiomeSourcePatch::new);
}
}
}
final class BiomeSourcePatch extends ForcedLevelPatch {
private static final String NETHER_BIOME_SOURCE = "bclib:nether_biome_source";
private static final String END_BIOME_SOURCE = "bclib:end_biome_source";
private static final String MC_NETHER = "minecraft:the_nether";
private static final String MC_END = "minecraft:the_end";
protected BiomeSourcePatch() {
super(BCLib.MOD_ID, "1.2.1");
}
@Override
protected Boolean runLevelDatPatch(CompoundTag root, MigrationProfile profile) {
CompoundTag worldGenSettings = root.getCompound("Data").getCompound("WorldGenSettings");
CompoundTag dimensions = worldGenSettings.getCompound("dimensions");
long seed = worldGenSettings.getLong("seed");
boolean result = false;
if (GeneratorOptions.fixNetherBiomeSource()) {
if (!dimensions.contains(MC_NETHER) || !isBCLibEntry(dimensions.getCompound(MC_NETHER))) {
CompoundTag dimRoot = new CompoundTag();
dimRoot.put("generator", makeNetherGenerator(seed));
dimRoot.putString("type", MC_NETHER);
dimensions.put(MC_NETHER, dimRoot);
result = true;
}
}
if (GeneratorOptions.fixEndBiomeSource()) {
if (!dimensions.contains(MC_END) || !isBCLibEntry(dimensions.getCompound(MC_END))) {
CompoundTag dimRoot = new CompoundTag();
dimRoot.put("generator", makeEndGenerator(seed));
dimRoot.putString("type", MC_END);
dimensions.put(MC_END, dimRoot);
result = true;
}
}
return result;
}
private boolean isBCLibEntry(CompoundTag dimRoot) {
String type = dimRoot.getCompound("generator").getCompound("biome_source").getString("type");
if (type.isEmpty() || type.length() < 5) {
return false;
}
return type.startsWith("bclib");
}
public static CompoundTag makeNetherGenerator(long seed) {
CompoundTag generator = new CompoundTag();
generator.putString("type", "minecraft:noise");
generator.putString("settings", "minecraft:nether");
generator.putLong("seed", seed);
CompoundTag biomeSource = new CompoundTag();
biomeSource.putString("type", NETHER_BIOME_SOURCE);
biomeSource.putLong("seed", seed);
generator.put("biome_source", biomeSource);
return generator;
}
public static CompoundTag makeEndGenerator(long seed) {
CompoundTag generator = new CompoundTag();
generator.putString("type", "minecraft:noise");
generator.putString("settings", "minecraft:end");
generator.putLong("seed", seed);
CompoundTag biomeSource = new CompoundTag();
biomeSource.putString("type", END_BIOME_SOURCE);
biomeSource.putLong("seed", seed);
generator.put("biome_source", biomeSource);
return generator;
}
}

View file

@ -0,0 +1,142 @@
package org.betterx.bclib.api;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.Block;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.betterx.bclib.util.WeightedList;
import java.util.Map;
import java.util.Set;
public class BonemealAPI {
private static final Map<ResourceLocation, Map<Block, WeightedList<Block>>> WATER_GRASS_BIOMES = Maps.newHashMap();
private static final Map<ResourceLocation, Map<Block, WeightedList<Block>>> LAND_GRASS_BIOMES = Maps.newHashMap();
private static final Map<Block, WeightedList<Block>> WATER_GRASS_TYPES = Maps.newHashMap();
private static final Map<Block, WeightedList<Block>> LAND_GRASS_TYPES = Maps.newHashMap();
private static final Map<Block, Block> SPREADABLE_BLOCKS = Maps.newHashMap();
private static final Set<Block> TERRAIN_TO_SPREAD = Sets.newHashSet();
private static final Set<Block> TERRAIN = Sets.newHashSet();
public static void addSpreadableBlock(Block spreadableBlock, Block surfaceForSpread) {
SPREADABLE_BLOCKS.put(spreadableBlock, surfaceForSpread);
TERRAIN_TO_SPREAD.add(surfaceForSpread);
TERRAIN.add(surfaceForSpread);
}
public static boolean isTerrain(Block block) {
return TERRAIN.contains(block);
}
public static boolean isSpreadableTerrain(Block block) {
return TERRAIN_TO_SPREAD.contains(block);
}
public static Block getSpreadable(Block block) {
return SPREADABLE_BLOCKS.get(block);
}
public static void addLandGrass(Block plant, Block... terrain) {
for (Block block : terrain) {
addLandGrass(plant, block, 1F);
}
}
public static void addLandGrass(ResourceLocation biome, Block plant, Block... terrain) {
for (Block block : terrain) {
addLandGrass(biome, plant, block, 1F);
}
}
public static void addLandGrass(Block plant, Block terrain, float chance) {
WeightedList<Block> list = LAND_GRASS_TYPES.get(terrain);
if (list == null) {
list = new WeightedList<Block>();
LAND_GRASS_TYPES.put(terrain, list);
}
TERRAIN.add(terrain);
list.add(plant, chance);
}
public static void addLandGrass(ResourceLocation biome, Block plant, Block terrain, float chance) {
Map<Block, WeightedList<Block>> map = LAND_GRASS_BIOMES.get(biome);
if (map == null) {
map = Maps.newHashMap();
LAND_GRASS_BIOMES.put(biome, map);
}
WeightedList<Block> list = map.get(terrain);
if (list == null) {
list = new WeightedList<Block>();
map.put(terrain, list);
}
TERRAIN.add(terrain);
list.add(plant, chance);
}
public static void addWaterGrass(Block plant, Block... terrain) {
for (Block block : terrain) {
addWaterGrass(plant, block, 1F);
}
}
public static void addWaterGrass(ResourceLocation biome, Block plant, Block... terrain) {
for (Block block : terrain) {
addWaterGrass(biome, plant, block, 1F);
}
}
public static void addWaterGrass(Block plant, Block terrain, float chance) {
WeightedList<Block> list = WATER_GRASS_TYPES.get(terrain);
if (list == null) {
list = new WeightedList<Block>();
WATER_GRASS_TYPES.put(terrain, list);
}
TERRAIN.add(terrain);
list.add(plant, chance);
}
public static void addWaterGrass(ResourceLocation biome, Block plant, Block terrain, float chance) {
Map<Block, WeightedList<Block>> map = WATER_GRASS_BIOMES.get(biome);
if (map == null) {
map = Maps.newHashMap();
WATER_GRASS_BIOMES.put(biome, map);
}
WeightedList<Block> list = map.get(terrain);
if (list == null) {
list = new WeightedList<Block>();
map.put(terrain, list);
}
TERRAIN.add(terrain);
list.add(plant, chance);
}
public static Block getLandGrass(ResourceLocation biomeID, Block terrain, RandomSource random) {
Map<Block, WeightedList<Block>> map = LAND_GRASS_BIOMES.get(biomeID);
WeightedList<Block> list = null;
if (map != null) {
list = map.get(terrain);
if (list == null) {
list = LAND_GRASS_TYPES.get(terrain);
}
} else {
list = LAND_GRASS_TYPES.get(terrain);
}
return list == null ? null : list.get(random);
}
public static Block getWaterGrass(ResourceLocation biomeID, Block terrain, RandomSource random) {
Map<Block, WeightedList<Block>> map = WATER_GRASS_BIOMES.get(biomeID);
WeightedList<Block> list = null;
if (map != null) {
list = map.get(terrain);
if (list == null) {
list = WATER_GRASS_TYPES.get(terrain);
}
} else {
list = WATER_GRASS_TYPES.get(terrain);
}
return list == null ? null : list.get(random);
}
}

View file

@ -0,0 +1,23 @@
package org.betterx.bclib.api;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Block;
import org.betterx.bclib.mixin.common.ComposterBlockAccessor;
public class ComposterAPI {
public static Block allowCompost(float chance, Block block) {
if (block != null) {
allowCompost(chance, block.asItem());
}
return block;
}
public static Item allowCompost(float chance, Item item) {
if (item != null && item != Items.AIR) {
ComposterBlockAccessor.callAdd(chance, item);
}
return item;
}
}

View file

@ -0,0 +1,141 @@
package org.betterx.bclib.api;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.progress.ChunkProgressListener;
import net.minecraft.world.level.CustomSpawner;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.storage.ServerLevelData;
import org.betterx.bclib.api.datafixer.DataFixerAPI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* provides some lifetime hooks for a Minecraft instance
*/
public class LifeCycleAPI {
private final static List<LevelLoadBiomesCall> onLoadLevelBiomes = new ArrayList<>(2);
private final static List<LevelLoadCall> onLoadLevel = new ArrayList<>(2);
private final static List<BeforeLevelLoadCall> beforeLoadLevel = new ArrayList<>(2);
/**
* A callback function that is used for each new ServerLevel instance
*/
public interface BeforeLevelLoadCall {
void beforeLoad();
}
/**
* A callback function that is used for each new ServerLevel instance
*/
public interface LevelLoadBiomesCall {
void onLoad(ServerLevel world, long seed, Registry<Biome> registry);
}
/**
* A callback function that is used for each new ServerLevel instance
*/
public interface LevelLoadCall {
void onLoad(
ServerLevel world,
MinecraftServer minecraftServer,
Executor executor,
LevelStorageSource.LevelStorageAccess levelStorageAccess,
ServerLevelData serverLevelData,
ResourceKey<Level> resourceKey,
ChunkProgressListener chunkProgressListener,
boolean bl,
long l,
List<CustomSpawner> list,
boolean bl2);
}
/**
* Register a callback that is called before a level is loaded or created,
* but after the {@link WorldDataAPI} was initialized and patches from
* the {@link DataFixerAPI} were applied.
*
* @param call The callback Method
*/
public static void beforeLevelLoad(BeforeLevelLoadCall call) {
beforeLoadLevel.add(call);
}
/**
* Register a callback that is called when a new {@code ServerLevel is instantiated}.
* This callback will receive the world seed as well as it's biome registry.
*
* @param call The calbback Method
*/
public static void onLevelLoad(LevelLoadBiomesCall call) {
onLoadLevelBiomes.add(call);
}
/**
* Register a callback that is called when a new {@code ServerLevel is instantiated}.
* This callbacl will receiv all parameters that were passed to the ServerLevel's constructor
*
* @param call The calbback Method
*/
public static void onLevelLoad(LevelLoadCall call) {
onLoadLevel.add(call);
}
/**
* For internal use, You should not call this method!
*/
public static void _runBeforeLevelLoad() {
beforeLoadLevel.forEach(c -> c.beforeLoad());
}
/**
* For internal use, You should not call this method!
*
* @param minecraftServer
* @param executor
* @param levelStorageAccess
* @param serverLevelData
* @param resourceKey
* @param chunkProgressListener
* @param bl
* @param l
* @param list
* @param bl2
*/
public static void _runLevelLoad(ServerLevel world,
MinecraftServer minecraftServer,
Executor executor,
LevelStorageSource.LevelStorageAccess levelStorageAccess,
ServerLevelData serverLevelData,
ResourceKey<Level> resourceKey,
ChunkProgressListener chunkProgressListener,
boolean bl,
long l,
List<CustomSpawner> list,
boolean bl2) {
onLoadLevel.forEach(c -> c.onLoad(
world,
minecraftServer,
executor,
levelStorageAccess,
serverLevelData,
resourceKey,
chunkProgressListener,
bl,
l,
list,
bl2)
);
final long seed = world.getSeed();
final Registry<Biome> biomeRegistry = world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY);
onLoadLevelBiomes.forEach(c -> c.onLoad(world, seed, biomeRegistry));
}
}

View file

@ -0,0 +1,48 @@
package org.betterx.bclib.api;
import net.fabricmc.loader.api.FabricLoader;
import com.google.common.collect.Lists;
import org.betterx.bclib.integration.ModIntegration;
import java.util.List;
public class ModIntegrationAPI {
private static final List<ModIntegration> INTEGRATIONS = Lists.newArrayList();
private static final boolean HAS_CANVAS = FabricLoader.getInstance().isModLoaded("canvas");
/**
* Registers mod integration
*
* @param integration
* @return
*/
public static ModIntegration register(ModIntegration integration) {
INTEGRATIONS.add(integration);
return integration;
}
/**
* Get all registered mod integrations.
*
* @return {@link List} of {@link ModIntegration}.
*/
public static List<ModIntegration> getIntegrations() {
return INTEGRATIONS;
}
/**
* Initialize all integrations, only for internal usage.
*/
public static void registerAll() {
INTEGRATIONS.forEach(integration -> {
if (integration.modIsInstalled()) {
integration.init();
}
});
}
public static boolean hasCanvas() {
return HAS_CANVAS;
}
}

View file

@ -0,0 +1,148 @@
package org.betterx.bclib.api;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.core.Registry;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
import com.google.common.collect.Lists;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.biomes.BiomeAPI;
import org.betterx.bclib.api.tag.NamedMineableTags;
import org.betterx.bclib.api.tag.TagAPI;
import org.betterx.bclib.blocks.BaseBarrelBlock;
import org.betterx.bclib.blocks.BaseChestBlock;
import org.betterx.bclib.blocks.BaseFurnaceBlock;
import org.betterx.bclib.blocks.BaseSignBlock;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.client.render.BaseChestBlockEntityRenderer;
import org.betterx.bclib.client.render.BaseSignBlockEntityRenderer;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.interfaces.PostInitable;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import org.betterx.bclib.interfaces.TagProvider;
import org.betterx.bclib.interfaces.tools.*;
import org.betterx.bclib.registry.BaseBlockEntities;
import java.util.List;
import java.util.function.Consumer;
public class PostInitAPI {
private static List<Consumer<Boolean>> postInitFunctions = Lists.newArrayList();
private static List<TagKey<Block>> blockTags = Lists.newArrayList();
private static List<TagKey<Item>> itemTags = Lists.newArrayList();
/**
* Register a new function which will be called after all mods are initiated. Will be called on both client and server.
*
* @param function {@link Consumer} with {@code boolean} parameter ({@code true} for client, {@code false} for server).
*/
public static void register(Consumer<Boolean> function) {
postInitFunctions.add(function);
}
/**
* Called in proper BCLib entry points, for internal usage only.
*
* @param isClient {@code boolean}, {@code true} for client, {@code false} for server.
*/
public static void postInit(boolean isClient) {
if (postInitFunctions == null) {
return;
}
postInitFunctions.forEach(function -> function.accept(isClient));
Registry.BLOCK.forEach(block -> {
processBlockCommon(block);
if (isClient) {
processBlockClient(block);
}
});
Registry.ITEM.forEach(item -> {
processItemCommon(item);
});
postInitFunctions = null;
blockTags = null;
itemTags = null;
BiomeAPI.loadFabricAPIBiomes();
Configs.BIOMES_CONFIG.saveChanges();
}
@Environment(EnvType.CLIENT)
private static void processBlockClient(Block block) {
if (block instanceof RenderLayerProvider) {
BCLRenderLayer layer = ((RenderLayerProvider) block).getRenderLayer();
if (layer == BCLRenderLayer.CUTOUT) BlockRenderLayerMap.INSTANCE.putBlock(block, RenderType.cutout());
else if (layer == BCLRenderLayer.TRANSLUCENT)
BlockRenderLayerMap.INSTANCE.putBlock(block, RenderType.translucent());
}
if (block instanceof BaseChestBlock) {
BaseChestBlockEntityRenderer.registerRenderLayer(block);
} else if (block instanceof BaseSignBlock) {
BaseSignBlockEntityRenderer.registerRenderLayer(block);
}
}
private static void processItemCommon(Item item) {
if (item instanceof TagProvider provider) {
try {
provider.addTags(null, itemTags);
} catch (NullPointerException ex) {
BCLib.LOGGER.error(item + " probably tried to access blockTags.", ex);
}
itemTags.forEach(tag -> TagAPI.addItemTag(tag, item));
itemTags.clear();
}
}
private static void processBlockCommon(Block block) {
if (block instanceof PostInitable) {
((PostInitable) block).postInit();
}
if (block instanceof BaseChestBlock) {
BaseBlockEntities.CHEST.registerBlock(block);
} else if (block instanceof BaseSignBlock) {
BaseBlockEntities.SIGN.registerBlock(block);
} else if (block instanceof BaseBarrelBlock) {
BaseBlockEntities.BARREL.registerBlock(block);
} else if (block instanceof BaseFurnaceBlock) {
BaseBlockEntities.FURNACE.registerBlock(block);
}
if (!(block instanceof PreventMineableAdd)) {
if (block instanceof AddMineableShears) {
TagAPI.addBlockTags(block, NamedMineableTags.SHEARS);
}
if (block instanceof AddMineableAxe) {
TagAPI.addBlockTags(block, NamedMineableTags.AXE);
}
if (block instanceof AddMineablePickaxe) {
TagAPI.addBlockTags(block, NamedMineableTags.PICKAXE);
}
if (block instanceof AddMineableShovel) {
TagAPI.addBlockTags(block, NamedMineableTags.SHOVEL);
}
if (block instanceof AddMineableHoe) {
TagAPI.addBlockTags(block, NamedMineableTags.HOE);
}
if (block instanceof AddMineableSword) {
TagAPI.addBlockTags(block, NamedMineableTags.SWORD);
}
if (block instanceof AddMineableHammer) {
TagAPI.addBlockTags(block, NamedMineableTags.HAMMER);
}
}
if (block instanceof TagProvider) {
((TagProvider) block).addTags(blockTags, itemTags);
blockTags.forEach(tag -> TagAPI.addBlockTag(tag, block));
itemTags.forEach(tag -> TagAPI.addItemTag(tag, block));
blockTags.clear();
itemTags.clear();
}
}
}

View file

@ -0,0 +1,22 @@
package org.betterx.bclib.api;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import org.betterx.bclib.mixin.common.ShovelItemAccessor;
import java.util.Map;
public class ShovelAPI {
/**
* Will add left-click behaviour to shovel: when it is targeting cetrain {@link Block} it will be converting to new
* {@link BlockState} on usage. Example: grass converting to path.
*
* @param target {@link Block} that will be converted.
* @param convert {@link BlockState} to convert block into.
*/
public static void addShovelBehaviour(Block target, BlockState convert) {
Map<Block, BlockState> map = ShovelItemAccessor.bclib_getFlattenables();
map.put(target, convert);
}
}

View file

@ -0,0 +1,146 @@
package org.betterx.bclib.api;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.datafixer.DataFixerAPI;
import org.betterx.bclib.util.ModUtil;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
/**
* Mod-specifix data-storage for a world.
* <p>
* This class provides the ability for mod to store persistent data inside a world. The Storage for the world is
* currently initialized as part of the {@link DataFixerAPI} in {@link DataFixerAPI#fixData(LevelStorageAccess, boolean, Consumer)}
* or {@link DataFixerAPI#initializeWorldData(File, boolean)}
*/
public class WorldDataAPI {
private static final Map<String, CompoundTag> TAGS = Maps.newHashMap();
private static final List<String> MODS = Lists.newArrayList();
private static File dataDir;
public static void load(File dataDir) {
WorldDataAPI.dataDir = dataDir;
MODS.stream()
.parallel()
.forEach(modID -> {
File file = new File(dataDir, modID + ".nbt");
CompoundTag root = new CompoundTag();
if (file.exists()) {
try {
root = NbtIo.readCompressed(file);
} catch (IOException e) {
BCLib.LOGGER.error("World data loading failed", e);
}
TAGS.put(modID, root);
} else {
Optional<ModContainer> optional = FabricLoader.getInstance()
.getModContainer(modID);
if (optional.isPresent()) {
ModContainer modContainer = optional.get();
if (BCLib.isDevEnvironment()) {
root.putString("version", "255.255.9999");
} else {
root.putString("version", modContainer.getMetadata()
.getVersion()
.toString());
}
TAGS.put(modID, root);
saveFile(modID);
}
}
});
}
/**
* Register mod cache, world cache is located in world data folder.
*
* @param modID - {@link String} modID.
*/
public static void registerModCache(String modID) {
MODS.add(modID);
}
/**
* Get root {@link CompoundTag} for mod cache in world data folder.
*
* @param modID - {@link String} modID.
* @return {@link CompoundTag}
*/
public static CompoundTag getRootTag(String modID) {
CompoundTag root = TAGS.get(modID);
if (root == null) {
root = new CompoundTag();
TAGS.put(modID, root);
}
return root;
}
/**
* Get {@link CompoundTag} with specified path from mod cache in world data folder.
*
* @param modID - {@link String} path to tag, dot-separated.
* @return {@link CompoundTag}
*/
public static CompoundTag getCompoundTag(String modID, String path) {
String[] parts = path.split("\\.");
CompoundTag tag = getRootTag(modID);
for (String part : parts) {
if (tag.contains(part)) {
tag = tag.getCompound(part);
} else {
CompoundTag t = new CompoundTag();
tag.put(part, t);
tag = t;
}
}
return tag;
}
/**
* Forces mod cache file to be saved.
*
* @param modID {@link String} mod ID.
*/
public static void saveFile(String modID) {
try {
if (!dataDir.exists()) {
dataDir.mkdirs();
}
NbtIo.writeCompressed(getRootTag(modID), new File(dataDir, modID + ".nbt"));
} catch (IOException e) {
BCLib.LOGGER.error("World data saving failed", e);
}
}
/**
* Get stored mod version (only for mods with registered cache).
*
* @return {@link String} mod version.
*/
public static String getModVersion(String modID) {
return getRootTag(modID).getString("version");
}
/**
* Get stored mod version as integer (only for mods with registered cache).
*
* @return {@code int} mod version.
*/
public static int getIntModVersion(String modID) {
return ModUtil.convertModVersion(getModVersion(modID));
}
}

View file

@ -0,0 +1,806 @@
package org.betterx.bclib.api.biomes;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.data.worldgen.BiomeDefaultFeatures;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.Music;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.tags.TagKey;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.level.biome.*;
import net.minecraft.world.level.biome.Biome.BiomeBuilder;
import net.minecraft.world.level.biome.Biome.Precipitation;
import net.minecraft.world.level.biome.MobSpawnSettings.SpawnerData;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.GenerationStep.Decoration;
import net.minecraft.world.level.levelgen.Noises;
import net.minecraft.world.level.levelgen.SurfaceRules;
import net.minecraft.world.level.levelgen.carver.ConfiguredWorldCarver;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
import com.google.common.collect.Lists;
import org.betterx.bclib.api.surface.SurfaceRuleBuilder;
import org.betterx.bclib.entity.BCLEntityWrapper;
import org.betterx.bclib.mixin.common.BiomeGenerationSettingsAccessor;
import org.betterx.bclib.util.CollectionsUtil;
import org.betterx.bclib.util.ColorUtil;
import org.betterx.bclib.util.Pair;
import org.betterx.bclib.util.TriFunction;
import org.betterx.bclib.world.biomes.BCLBiome;
import org.betterx.bclib.world.biomes.BCLBiomeSettings;
import org.betterx.bclib.world.features.BCLFeature;
import org.betterx.bclib.world.structures.BCLStructure;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
public class BCLBiomeBuilder {
@FunctionalInterface
public interface BiomeSupplier<T> extends TriFunction<ResourceLocation, Biome, BCLBiomeSettings, T> {
}
private static final BCLBiomeBuilder INSTANCE = new BCLBiomeBuilder();
private static final SurfaceRules.ConditionSource SURFACE_NOISE = SurfaceRules.noiseCondition(Noises.SOUL_SAND_LAYER,
-0.012);
private final List<TagKey<Biome>> structureTags = new ArrayList<>(8);
private final List<Pair<GenerationStep.Carving, Holder<? extends ConfiguredWorldCarver<?>>>> carvers = new ArrayList<>(1);
private BiomeGenerationSettings.Builder generationSettings;
private BiomeSpecialEffects.Builder effectsBuilder;
private MobSpawnSettings.Builder spawnSettings;
private SurfaceRules.RuleSource surfaceRule;
private Precipitation precipitation;
private ResourceLocation biomeID;
private final List<Climate.ParameterPoint> parameters = Lists.newArrayList();
//BiomeTags.IS_NETHER
private float temperature;
private float fogDensity;
private float genChance;
private float downfall;
private float height;
private int edgeSize;
private BCLBiome edge;
private boolean vertical;
/**
* Starts new biome building process.
*
* @param biomeID {@link ResourceLocation} biome identifier.
* @return prepared {@link BCLBiomeBuilder} instance.
*/
public static BCLBiomeBuilder start(ResourceLocation biomeID) {
INSTANCE.biomeID = biomeID;
INSTANCE.precipitation = Precipitation.NONE;
INSTANCE.generationSettings = null;
INSTANCE.effectsBuilder = null;
INSTANCE.spawnSettings = null;
INSTANCE.structureTags.clear();
INSTANCE.temperature = 1.0F;
INSTANCE.fogDensity = 1.0F;
INSTANCE.edgeSize = 0;
INSTANCE.downfall = 1.0F;
INSTANCE.genChance = 1.0F;
INSTANCE.height = 0.1F;
INSTANCE.vertical = false;
INSTANCE.edge = null;
INSTANCE.carvers.clear();
INSTANCE.parameters.clear();
return INSTANCE;
}
public BCLBiomeBuilder addNetherClimateParamater(float temperature, float humidity) {
parameters.add(Climate.parameters(temperature, humidity, 0, 0, 0, 0, 0));
return this;
}
/**
* Set biome {@link Precipitation}. Affect biome visual effects (rain, snow, none).
*
* @param precipitation {@link Precipitation}
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder precipitation(Precipitation precipitation) {
this.precipitation = precipitation;
return this;
}
/**
* Set biome temperature, affect plant color, biome generation and ice formation.
*
* @param temperature biome temperature.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder temperature(float temperature) {
this.temperature = temperature;
return this;
}
/**
* Set biome wetness (same as downfall). Affect plant color and biome generation.
*
* @param wetness biome wetness (downfall).
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder wetness(float wetness) {
this.downfall = wetness;
return this;
}
/**
* Adds mob spawning to biome.
*
* @param entityType {@link EntityType} mob type.
* @param weight spawn weight.
* @param minGroupCount minimum mobs in group.
* @param maxGroupCount maximum mobs in group.
* @return same {@link BCLBiomeBuilder} instance.
*/
public <M extends Mob> BCLBiomeBuilder spawn(EntityType<M> entityType,
int weight,
int minGroupCount,
int maxGroupCount) {
getSpawns().addSpawn(entityType.getCategory(),
new SpawnerData(entityType, weight, minGroupCount, maxGroupCount));
return this;
}
/**
* Adds mob spawning to biome.
*
* @param wrapper {@link BCLEntityWrapper} mob type.
* @param weight spawn weight.
* @param minGroupCount minimum mobs in group.
* @param maxGroupCount maximum mobs in group.
* @return same {@link BCLBiomeBuilder} instance.
*/
public <M extends Mob> BCLBiomeBuilder spawn(BCLEntityWrapper<M> wrapper,
int weight,
int minGroupCount,
int maxGroupCount) {
if (wrapper.canSpawn()) {
return spawn(wrapper.type(), weight, minGroupCount, maxGroupCount);
}
return this;
}
/**
* Adds ambient particles to thr biome.
*
* @param particle {@link ParticleOptions} particles (or {@link net.minecraft.core.particles.ParticleType}).
* @param probability particle spawn probability, should have low value (example: 0.01F).
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder particles(ParticleOptions particle, float probability) {
getEffects().ambientParticle(new AmbientParticleSettings(particle, probability));
return this;
}
/**
* Sets sky color for the biome. Color is in ARGB int format.
*
* @param color ARGB color as integer.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder skyColor(int color) {
getEffects().skyColor(color);
return this;
}
/**
* Sets sky color for the biome. Color represented as red, green and blue channel values.
*
* @param red red color component [0-255]
* @param green green color component [0-255]
* @param blue blue color component [0-255]
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder skyColor(int red, int green, int blue) {
red = Mth.clamp(red, 0, 255);
green = Mth.clamp(green, 0, 255);
blue = Mth.clamp(blue, 0, 255);
return skyColor(ColorUtil.color(red, green, blue));
}
/**
* Sets fog color for the biome. Color is in ARGB int format.
*
* @param color ARGB color as integer.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder fogColor(int color) {
getEffects().fogColor(color);
return this;
}
/**
* Sets fog color for the biome. Color represented as red, green and blue channel values.
*
* @param red red color component [0-255]
* @param green green color component [0-255]
* @param blue blue color component [0-255]
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder fogColor(int red, int green, int blue) {
red = Mth.clamp(red, 0, 255);
green = Mth.clamp(green, 0, 255);
blue = Mth.clamp(blue, 0, 255);
return fogColor(ColorUtil.color(red, green, blue));
}
/**
* Sets fog density for the biome.
*
* @param density fog density as a float, default value is 1.0F.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder fogDensity(float density) {
this.fogDensity = density;
return this;
}
/**
* Sets generation chance for this biome.
*
* @param genChance
* @return same {@link BCLBiomeBuilder}.
*/
public BCLBiomeBuilder genChance(float genChance) {
this.genChance = genChance;
return this;
}
/**
* Sets edge size for this biome.
*
* @param edgeSize size of the Edge (in Blocks)
* @return same {@link BCLBiomeBuilder}.
*/
public BCLBiomeBuilder edgeSize(int edgeSize) {
this.edgeSize = edgeSize;
return this;
}
/**
* Sets edge-Biome for this biome.
*
* @param edge The Edge Biome
* @return same {@link BCLBiomeBuilder}.
*/
public BCLBiomeBuilder edge(BCLBiome edge) {
this.edge = edge;
return this;
}
/**
* Sets edge-Biome for this biome.
*
* @param edge The Edge Biome
* @param edgeSize size of the Edge (in Blocks)
* @return same {@link BCLBiomeBuilder}.
*/
public BCLBiomeBuilder edge(BCLBiome edge, int edgeSize) {
this.edge(edge);
this.edgeSize(edgeSize);
return this;
}
/**
* Sets water color for the biome. Color is in ARGB int format.
*
* @param color ARGB color as integer.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder waterColor(int color) {
getEffects().waterColor(color);
return this;
}
/**
* Sets water color for the biome. Color represented as red, green and blue channel values.
*
* @param red red color component [0-255]
* @param green green color component [0-255]
* @param blue blue color component [0-255]
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder waterColor(int red, int green, int blue) {
red = Mth.clamp(red, 0, 255);
green = Mth.clamp(green, 0, 255);
blue = Mth.clamp(blue, 0, 255);
return waterColor(ColorUtil.color(red, green, blue));
}
/**
* Sets underwater fog color for the biome. Color is in ARGB int format.
*
* @param color ARGB color as integer.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder waterFogColor(int color) {
getEffects().waterFogColor(color);
return this;
}
/**
* Sets underwater fog color for the biome. Color represented as red, green and blue channel values.
*
* @param red red color component [0-255]
* @param green green color component [0-255]
* @param blue blue color component [0-255]
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder waterFogColor(int red, int green, int blue) {
red = Mth.clamp(red, 0, 255);
green = Mth.clamp(green, 0, 255);
blue = Mth.clamp(blue, 0, 255);
return waterFogColor(ColorUtil.color(red, green, blue));
}
/**
* Sets water and underwater fig color for the biome. Color is in ARGB int format.
*
* @param color ARGB color as integer.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder waterAndFogColor(int color) {
return waterColor(color).waterFogColor(color);
}
/**
* Sets water and underwater fig color for the biome. Color is in ARGB int format.
*
* @param red red color component [0-255]
* @param green green color component [0-255]
* @param blue blue color component [0-255]
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder waterAndFogColor(int red, int green, int blue) {
red = Mth.clamp(red, 0, 255);
green = Mth.clamp(green, 0, 255);
blue = Mth.clamp(blue, 0, 255);
return waterAndFogColor(ColorUtil.color(red, green, blue));
}
/**
* Sets grass color for the biome. Color is in ARGB int format.
*
* @param color ARGB color as integer.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder grassColor(int color) {
getEffects().grassColorOverride(color);
return this;
}
/**
* Sets grass color for the biome. Color represented as red, green and blue channel values.
*
* @param red red color component [0-255]
* @param green green color component [0-255]
* @param blue blue color component [0-255]
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder grassColor(int red, int green, int blue) {
red = Mth.clamp(red, 0, 255);
green = Mth.clamp(green, 0, 255);
blue = Mth.clamp(blue, 0, 255);
return grassColor(ColorUtil.color(red, green, blue));
}
/**
* Sets leaves and plants color for the biome. Color is in ARGB int format.
*
* @param color ARGB color as integer.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder foliageColor(int color) {
getEffects().foliageColorOverride(color);
return this;
}
/**
* Sets leaves and plants color for the biome. Color represented as red, green and blue channel values.
*
* @param red red color component [0-255]
* @param green green color component [0-255]
* @param blue blue color component [0-255]
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder foliageColor(int red, int green, int blue) {
red = Mth.clamp(red, 0, 255);
green = Mth.clamp(green, 0, 255);
blue = Mth.clamp(blue, 0, 255);
return foliageColor(ColorUtil.color(red, green, blue));
}
/**
* Sets grass, leaves and all plants color for the biome. Color is in ARGB int format.
*
* @param color ARGB color as integer.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder plantsColor(int color) {
return grassColor(color).foliageColor(color);
}
/**
* Sets grass, leaves and all plants color for the biome. Color represented as red, green and blue channel values.
*
* @param red red color component [0-255]
* @param green green color component [0-255]
* @param blue blue color component [0-255]
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder plantsColor(int red, int green, int blue) {
red = Mth.clamp(red, 0, 255);
green = Mth.clamp(green, 0, 255);
blue = Mth.clamp(blue, 0, 255);
return plantsColor(ColorUtil.color(red, green, blue));
}
/**
* Sets biome music, used for biomes in the Nether and End.
*
* @param music {@link Music} to use.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder music(Music music) {
getEffects().backgroundMusic(music);
return this;
}
/**
* Sets biome music, used for biomes in the Nether and End.
*
* @param music {@link SoundEvent} to use.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder music(SoundEvent music) {
return music(new Music(music, 600, 2400, true));
}
/**
* Sets biome ambient loop sound. Can be used for biome environment.
*
* @param loopSound {@link SoundEvent} to use as a loop.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder loop(SoundEvent loopSound) {
getEffects().ambientLoopSound(loopSound);
return this;
}
/**
* Sets biome mood sound. Can be used for biome environment.
*
* @param mood {@link SoundEvent} to use as a mood.
* @param tickDelay delay between sound events in ticks.
* @param blockSearchExtent block search radius (for area available for sound).
* @param soundPositionOffset offset in sound.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder mood(SoundEvent mood, int tickDelay, int blockSearchExtent, float soundPositionOffset) {
getEffects().ambientMoodSound(new AmbientMoodSettings(mood, tickDelay, blockSearchExtent, soundPositionOffset));
return this;
}
/**
* Sets biome mood sound. Can be used for biome environment.
*
* @param mood {@link SoundEvent} to use as a mood.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder mood(SoundEvent mood) {
return mood(mood, 6000, 8, 2.0F);
}
/**
* Sets biome additionsl ambient sounds.
*
* @param additions {@link SoundEvent} to use.
* @param intensity sound intensity. Default is 0.0111F.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder additions(SoundEvent additions, float intensity) {
getEffects().ambientAdditionsSound(new AmbientAdditionsSettings(additions, intensity));
return this;
}
/**
* Sets biome additionsl ambient sounds.
*
* @param additions {@link SoundEvent} to use.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder additions(SoundEvent additions) {
return additions(additions, 0.0111F);
}
/**
* Adds new feature to the biome.
*
* @param decoration {@link Decoration} feature step.
* @param feature {@link PlacedFeature}.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder feature(Decoration decoration, Holder<PlacedFeature> feature) {
getGeneration().addFeature(decoration, feature);
return this;
}
/**
* Adds vanilla Mushrooms.
*
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder defaultMushrooms() {
return feature(BiomeDefaultFeatures::addDefaultMushrooms);
}
/**
* Adds vanilla Nether Ores.
*
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder netherDefaultOres() {
return feature(BiomeDefaultFeatures::addNetherDefaultOres);
}
/**
* Will add features into biome, used for vanilla feature adding functions.
*
* @param featureAdd {@link Consumer} with {@link BiomeGenerationSettings.Builder}.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder feature(Consumer<BiomeGenerationSettings.Builder> featureAdd) {
featureAdd.accept(getGeneration());
return this;
}
/**
* Adds new feature to the biome.
*
* @param feature {@link BCLFeature}.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder feature(BCLFeature feature) {
return feature(feature.getDecoration(), feature.getPlacedFeature());
}
/**
* Adds new structure feature into the biome.
*
* @param structureTag {@link TagKey} to add.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder structure(TagKey<Biome> structureTag) {
structureTags.add(structureTag);
return this;
}
/**
* Adds new structure feature into thr biome. Will add building biome into the structure list.
*
* @param structure {@link BCLStructure} to add.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder structure(BCLStructure structure) {
structure.addInternalBiome(biomeID);
return structure(structure.biomeTag);
}
/**
* Adds new world carver into the biome.
*
* @param carver {@link ConfiguredWorldCarver} to add.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder carver(GenerationStep.Carving step, Holder<? extends ConfiguredWorldCarver<?>> carver) {
final ResourceLocation immutableID = biomeID;
var oKey = carver.unwrapKey();
if (oKey.isPresent()) {
BiomeModifications.addCarver(ctx -> ctx.getBiomeKey().location().equals(immutableID),
step,
(ResourceKey<ConfiguredWorldCarver<?>>) oKey.get());
}
//carvers.add(new Pair<>(step, carver));
return this;
}
/**
* Adds new world surface rule for the given block
*
* @param surfaceBlock {@link Block} to use.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder surface(Block surfaceBlock) {
return surface(surfaceBlock.defaultBlockState());
}
/**
* Adds new world surface rule for the given block
*
* @param surfaceBlock {@link BlockState} to use.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder surface(BlockState surfaceBlock) {
return surface(SurfaceRuleBuilder.start().surface(surfaceBlock).build());
}
/**
* Adds blocks to the biome surface and below it (with specified depth).
*
* @param surfaceBlock {@link Block} that will cover biome.
* @param subterrainBlock {@link Block} below it with specified depth.
* @param depth thickness of bottom block layer.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder surface(Block surfaceBlock, Block subterrainBlock, int depth) {
return surface(SurfaceRuleBuilder
.start()
.surface(surfaceBlock.defaultBlockState())
.subsurface(subterrainBlock.defaultBlockState(), depth)
.build());
}
/**
* Adds surface rule to this biome.
*
* @param newSurfaceRule {link SurfaceRules.RuleSource} surface rule.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder surface(SurfaceRules.RuleSource newSurfaceRule) {
this.surfaceRule = newSurfaceRule;
return this;
}
/**
* Set terrain height for the biome. Can be used in custom generators, doesn't change vanilla biome distribution or generation.
*
* @param height a relative float terrain height value.
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder terrainHeight(float height) {
this.height = height;
return this;
}
/**
* Make this a vertical Biome
*
* @return same {@link BCLBiomeBuilder} instance.
*/
public BCLBiomeBuilder vertical() {
this.vertical = vertical;
return this;
}
/**
* Finalize biome creation.
*
* @return created {@link BCLBiome} instance.
*/
public BCLBiome build() {
return build((BiomeSupplier<BCLBiome>) BCLBiome::new);
}
/**
* Finalize biome creation.
*
* @param biomeConstructor {@link BiFunction} biome constructor.
* @return created {@link BCLBiome} instance.
*/
@Deprecated(forRemoval = true)
public <T extends BCLBiome> T build(BiFunction<ResourceLocation, Biome, T> biomeConstructor) {
return build((id, biome, settings) -> biomeConstructor.apply(id, biome));
}
private static BiomeGenerationSettings fixGenerationSettings(BiomeGenerationSettings settings) {
//Fabric Biome Modification API can not handle an empty carver map, thus we will create one with
//an empty HolderSet for every possible step:
//https://github.com/FabricMC/fabric/issues/2079
//TODO: Remove, once fabric gets fixed
if (settings instanceof BiomeGenerationSettingsAccessor acc) {
Map<GenerationStep.Carving, HolderSet<ConfiguredWorldCarver<?>>> carvers = CollectionsUtil.getMutable(acc.bclib_getCarvers());
for (GenerationStep.Carving step : GenerationStep.Carving.values()) {
carvers.computeIfAbsent(step, __ -> HolderSet.direct(Lists.newArrayList()));
}
acc.bclib_setCarvers(carvers);
}
return settings;
}
/**
* Finalize biome creation.
*
* @param biomeConstructor {@link BiomeSupplier} biome constructor.
* @return created {@link BCLBiome} instance.
*/
public <T extends BCLBiome> T build(BiomeSupplier<T> biomeConstructor) {
BiomeBuilder builder = new BiomeBuilder()
.precipitation(precipitation)
.temperature(temperature)
.downfall(downfall);
builder.mobSpawnSettings(getSpawns().build());
builder.specialEffects(getEffects().build());
builder.generationSettings(fixGenerationSettings(getGeneration().build()));
BCLBiomeSettings settings = BCLBiomeSettings.createBCL()
.setTerrainHeight(height)
.setFogDensity(fogDensity)
.setGenChance(genChance)
.setEdgeSize(edgeSize)
.setEdge(edge)
.setVertical(vertical)
.build();
final Biome biome = builder.build();
final T res = biomeConstructor.apply(biomeID, biome, settings);
res.attachStructures(structureTags);
res.setSurface(surfaceRule);
res.addClimateParameters(parameters);
//carvers.forEach(cfg -> BiomeAPI.addBiomeCarver(biome, cfg.second, cfg.first));
return res;
}
/**
* Get or create {@link BiomeSpecialEffects.Builder} for biome visual effects.
* For internal usage only.
* For internal usage only.
*
* @return new or same {@link BiomeSpecialEffects.Builder} instance.
*/
private BiomeSpecialEffects.Builder getEffects() {
if (effectsBuilder == null) {
effectsBuilder = new BiomeSpecialEffects.Builder();
}
return effectsBuilder;
}
/**
* Get or create {@link MobSpawnSettings.Builder} for biome mob spawning.
* For internal usage only.
*
* @return new or same {@link MobSpawnSettings.Builder} instance.
*/
private MobSpawnSettings.Builder getSpawns() {
if (spawnSettings == null) {
spawnSettings = new MobSpawnSettings.Builder();
}
return spawnSettings;
}
/**
* Get or create {@link BiomeGenerationSettings.Builder} for biome features and generation.
* For internal usage only.
*
* @return new or same {@link BiomeGenerationSettings.Builder} instance.
*/
private BiomeGenerationSettings.Builder getGeneration() {
if (generationSettings == null) {
generationSettings = new BiomeGenerationSettings.Builder();
}
return generationSettings;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,109 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
public abstract class BaseDataHandler {
private final boolean originatesOnServer;
@NotNull
private final ResourceLocation identifier;
protected BaseDataHandler(ResourceLocation identifier, boolean originatesOnServer) {
this.originatesOnServer = originatesOnServer;
this.identifier = identifier;
}
final public boolean getOriginatesOnServer() {
return originatesOnServer;
}
final public ResourceLocation getIdentifier() {
return identifier;
}
@Environment(EnvType.CLIENT)
abstract void receiveFromServer(Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender);
private ServerPlayer lastMessageSender;
void receiveFromClient(MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
lastMessageSender = player;
}
final protected boolean reply(BaseDataHandler message, MinecraftServer server) {
if (lastMessageSender == null) return false;
message.sendToClient(server, lastMessageSender);
return true;
}
abstract void sendToClient(MinecraftServer server);
abstract void sendToClient(MinecraftServer server, ServerPlayer player);
@Environment(EnvType.CLIENT)
abstract void sendToServer(Minecraft client);
protected boolean isBlocking() {
return false;
}
@Override
public String toString() {
return "BasDataHandler{" + "originatesOnServer=" + originatesOnServer + ", identifier=" + identifier + '}';
}
/**
* Write a String to a buffer (Convenience Method)
*
* @param buf The buffer to write to
* @param s The String you want to write
*/
public static void writeString(FriendlyByteBuf buf, String s) {
buf.writeByteArray(s.getBytes(StandardCharsets.UTF_8));
}
/**
* Read a string from a buffer (Convenience Method)
*
* @param buf Thea buffer to read from
* @return The received String
*/
public static String readString(FriendlyByteBuf buf) {
byte[] data = buf.readByteArray();
return new String(data, StandardCharsets.UTF_8);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BaseDataHandler)) return false;
BaseDataHandler that = (BaseDataHandler) o;
return originatesOnServer == that.originatesOnServer && identifier.equals(that.identifier);
}
@Override
public int hashCode() {
return Objects.hash(originatesOnServer, identifier);
}
}

View file

@ -0,0 +1,19 @@
package org.betterx.bclib.api.dataexchange;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
import java.util.Set;
abstract class Connector {
protected final DataExchange api;
Connector(DataExchange api) {
this.api = api;
}
public abstract boolean onClient();
protected Set<DataHandlerDescriptor> getDescriptors() {
return api.getDescriptors();
}
}

View file

@ -0,0 +1,77 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.FriendlyByteBuf;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
/**
* This is an internal class that handles a Clienetside players Connection to a Server
*/
@Environment(EnvType.CLIENT)
public class ConnectorClientside extends Connector {
private Minecraft client;
ConnectorClientside(DataExchange api) {
super(api);
this.client = null;
}
@Override
public boolean onClient() {
return true;
}
public void onPlayInit(ClientPacketListener handler, Minecraft client) {
if (this.client != null && this.client != client) {
BCLib.LOGGER.warning("Client changed!");
}
this.client = client;
for (DataHandlerDescriptor desc : getDescriptors()) {
ClientPlayNetworking.registerReceiver(desc.IDENTIFIER, (_client, _handler, _buf, _responseSender) -> {
receiveFromServer(desc, _client, _handler, _buf, _responseSender);
});
}
}
public void onPlayReady(ClientPacketListener handler, PacketSender sender, Minecraft client) {
for (DataHandlerDescriptor desc : getDescriptors()) {
if (desc.sendOnJoin) {
BaseDataHandler h = desc.JOIN_INSTANCE.get();
if (!h.getOriginatesOnServer()) {
h.sendToServer(client);
}
}
}
}
public void onPlayDisconnect(ClientPacketListener handler, Minecraft client) {
for (DataHandlerDescriptor desc : getDescriptors()) {
ClientPlayNetworking.unregisterReceiver(desc.IDENTIFIER);
}
}
void receiveFromServer(DataHandlerDescriptor desc,
Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
BaseDataHandler h = desc.INSTANCE.get();
h.receiveFromServer(client, handler, buf, responseSender);
}
public void sendToServer(BaseDataHandler h) {
if (client == null) {
throw new RuntimeException("[internal error] Client not initialized yet!");
}
h.sendToServer(this.client);
}
}

View file

@ -0,0 +1,82 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
/**
* This is an internal class that handles a Serverside Connection to a Client-Player
*/
public class ConnectorServerside extends Connector {
private MinecraftServer server;
ConnectorServerside(DataExchange api) {
super(api);
server = null;
}
@Override
public boolean onClient() {
return false;
}
public void onPlayInit(ServerGamePacketListenerImpl handler, MinecraftServer server) {
if (this.server != null && this.server != server) {
BCLib.LOGGER.warning("Server changed!");
}
this.server = server;
for (DataHandlerDescriptor desc : getDescriptors()) {
ServerPlayNetworking.registerReceiver(handler,
desc.IDENTIFIER,
(_server, _player, _handler, _buf, _responseSender) -> {
receiveFromClient(desc,
_server,
_player,
_handler,
_buf,
_responseSender);
});
}
}
public void onPlayReady(ServerGamePacketListenerImpl handler, PacketSender sender, MinecraftServer server) {
for (DataHandlerDescriptor desc : getDescriptors()) {
if (desc.sendOnJoin) {
BaseDataHandler h = desc.JOIN_INSTANCE.get();
if (h.getOriginatesOnServer()) {
h.sendToClient(server, handler.player);
}
}
}
}
public void onPlayDisconnect(ServerGamePacketListenerImpl handler, MinecraftServer server) {
for (DataHandlerDescriptor desc : getDescriptors()) {
ServerPlayNetworking.unregisterReceiver(handler, desc.IDENTIFIER);
}
}
void receiveFromClient(DataHandlerDescriptor desc,
MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
BaseDataHandler h = desc.INSTANCE.get();
h.receiveFromClient(server, player, handler, buf, responseSender);
}
public void sendToClient(BaseDataHandler h) {
if (server == null) {
throw new RuntimeException("[internal error] Server not initialized yet!");
}
h.sendToClient(this.server);
}
}

View file

@ -0,0 +1,214 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import com.google.common.collect.Lists;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSync;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSyncID;
import org.betterx.bclib.config.Config;
import org.betterx.bclib.util.ModUtil;
import java.io.File;
import java.util.List;
import java.util.function.BiConsumer;
public class DataExchangeAPI extends DataExchange {
private final static List<String> MODS = Lists.newArrayList();
/**
* You should never need to create a custom instance of this Object.
*/
public DataExchangeAPI() {
super();
}
@Environment(EnvType.CLIENT)
protected ConnectorClientside clientSupplier(DataExchange api) {
return new ConnectorClientside(api);
}
protected ConnectorServerside serverSupplier(DataExchange api) {
return new ConnectorServerside(api);
}
/**
* Register a mod to participate in the DataExchange.
*
* @param modID - {@link String} modID.
*/
public static void registerMod(String modID) {
if (!MODS.contains(modID)) MODS.add(modID);
}
/**
* Register a mod dependency to participate in the DataExchange.
*
* @param modID - {@link String} modID.
*/
public static void registerModDependency(String modID) {
if (ModUtil.getModInfo(modID, false) != null && !"0.0.0".equals(ModUtil.getModVersion(modID))) {
registerMod(modID);
} else {
BCLib.LOGGER.info("Mod Dependency '" + modID + "' not found. This is probably OK.");
}
}
/**
* Returns the IDs of all registered Mods.
*
* @return List of modIDs
*/
public static List<String> registeredMods() {
return MODS;
}
/**
* Add a new Descriptor for a {@link DataHandler}.
*
* @param desc The Descriptor you want to add.
*/
public static void registerDescriptor(DataHandlerDescriptor desc) {
DataExchange api = DataExchange.getInstance();
api.getDescriptors()
.add(desc);
}
/**
* Bulk-Add a Descriptors for your {@link DataHandler}-Objects.
*
* @param desc The Descriptors you want to add.
*/
public static void registerDescriptors(List<DataHandlerDescriptor> desc) {
DataExchange api = DataExchange.getInstance();
api.getDescriptors()
.addAll(desc);
}
/**
* Sends the Handler.
* <p>
* Depending on what the result of {@link DataHandler#getOriginatesOnServer()}, the Data is sent from the server
* to the client (if {@code true}) or the other way around.
* <p>
* The method {@link DataHandler#serializeData(FriendlyByteBuf, boolean)} is called just before the data is sent. You should
* use this method to add the Data you need to the communication.
*
* @param h The Data that you want to send
*/
public static void send(BaseDataHandler h) {
if (h.getOriginatesOnServer()) {
DataExchangeAPI.getInstance().server.sendToClient(h);
} else {
DataExchangeAPI.getInstance().client.sendToServer(h);
}
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param fileName The name of the File
*/
public static void addAutoSyncFile(String modID, File fileName) {
AutoSync.addAutoSyncFileData(modID, fileName, false, SyncFileHash.NEED_TRANSFER);
}
/**
* Registers a File for automatic client syncing.
* <p>
* The file is synced of the {@link SyncFileHash} on client and server are not equal. This method will not copy the
* configs content from the client to the server.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link SyncFileHash#uniqueID} for
* Details
* @param fileName The name of the File
*/
public static void addAutoSyncFile(String modID, String uniqueID, File fileName) {
AutoSync.addAutoSyncFileData(modID, uniqueID, fileName, false, SyncFileHash.NEED_TRANSFER);
}
/**
* Registers a File for automatic client syncing.
* <p>
* The content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* if the File needs to be copied. Normally using the {@link SyncFileHash}
* for comparison is sufficient.
*
* @param modID The ID of the calling Mod
* @param fileName The name of the File
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
*/
public static void addAutoSyncFile(String modID, File fileName, AutoSync.NeedTransferPredicate needTransfer) {
AutoSync.addAutoSyncFileData(modID, fileName, true, needTransfer);
}
/**
* Registers a File for automatic client syncing.
* <p>
* The content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* if the File needs to be copied. Normally using the {@link SyncFileHash}
* for comparison is sufficient.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link SyncFileHash#uniqueID} for
* Details
* @param fileName The name of the File
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
*/
public static void addAutoSyncFile(String modID,
String uniqueID,
File fileName,
AutoSync.NeedTransferPredicate needTransfer) {
AutoSync.addAutoSyncFileData(modID, uniqueID, fileName, true, needTransfer);
}
/**
* Register a function that is called whenever the client receives a file from the server and replaced toe local
* file with the new content.
* <p>
* This callback is usefull if you need to reload the new content before the game is quit.
*
* @param callback A Function that receives the AutoSyncID as well as the Filename.
*/
public static void addOnWriteCallback(BiConsumer<AutoSyncID, File> callback) {
AutoSync.addOnWriteCallback(callback);
}
/**
* Returns the sync-folder for a given Mod.
* <p>
* BCLib will ensure that the contents of sync-folder on the client is the same as the one on the server.
*
* @param modID ID of the Mod
* @return The path to the sync-folder
*/
public static File getModSyncFolder(String modID) {
File fl = AutoSync.SYNC_FOLDER.localFolder.resolve(modID.replace(".", "-")
.replace(":", "-")
.replace("\\", "-")
.replace("/", "-"))
.normalize()
.toFile();
if (!fl.exists()) {
fl.mkdirs();
}
return fl;
}
static {
addOnWriteCallback(Config::reloadSyncedConfig);
}
}

View file

@ -0,0 +1,315 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.world.entity.player.Player;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.handler.autosync.Chunker;
import org.betterx.bclib.api.dataexchange.handler.autosync.Chunker.PacketChunkSender;
import java.util.Collection;
import java.util.List;
public abstract class DataHandler extends BaseDataHandler {
public abstract static class WithoutPayload extends DataHandler {
protected WithoutPayload(ResourceLocation identifier, boolean originatesOnServer) {
super(identifier, originatesOnServer);
}
@Override
protected boolean prepareData(boolean isClient) {
return true;
}
@Override
protected void serializeData(FriendlyByteBuf buf, boolean isClient) {
}
@Override
protected void deserializeIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean isClient) {
}
}
protected DataHandler(ResourceLocation identifier, boolean originatesOnServer) {
super(identifier, originatesOnServer);
}
protected boolean prepareData(boolean isClient) {
return true;
}
abstract protected void serializeData(FriendlyByteBuf buf, boolean isClient);
abstract protected void deserializeIncomingData(FriendlyByteBuf buf, PacketSender responseSender, boolean isClient);
abstract protected void runOnGameThread(Minecraft client, MinecraftServer server, boolean isClient);
@Environment(EnvType.CLIENT)
@Override
void receiveFromServer(Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
deserializeIncomingData(buf, responseSender, true);
final Runnable runner = () -> runOnGameThread(client, null, true);
if (isBlocking()) client.executeBlocking(runner);
else client.execute(runner);
}
@Override
void receiveFromClient(MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
super.receiveFromClient(server, player, handler, buf, responseSender);
deserializeIncomingData(buf, responseSender, false);
final Runnable runner = () -> runOnGameThread(null, server, false);
if (isBlocking()) server.executeBlocking(runner);
else server.execute(runner);
}
@Override
void sendToClient(MinecraftServer server) {
if (prepareData(false)) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeData(buf, false);
_sendToClient(getIdentifier(), server, PlayerLookup.all(server), buf);
}
}
@Override
void sendToClient(MinecraftServer server, ServerPlayer player) {
if (prepareData(false)) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeData(buf, false);
_sendToClient(getIdentifier(), server, List.of(player), buf);
}
}
public static void _sendToClient(ResourceLocation identifier,
MinecraftServer server,
Collection<ServerPlayer> players,
FriendlyByteBuf buf) {
if (buf.readableBytes() > Chunker.MAX_PACKET_SIZE) {
final PacketChunkSender sender = new PacketChunkSender(buf, identifier);
sender.sendChunks(players);
} else {
for (ServerPlayer player : players) {
ServerPlayNetworking.send(player, identifier, buf);
}
}
}
@Environment(EnvType.CLIENT)
@Override
void sendToServer(Minecraft client) {
if (prepareData(true)) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeData(buf, true);
ClientPlayNetworking.send(getIdentifier(), buf);
}
}
/**
* A Message that always originates on the Client
*/
public abstract static class FromClient extends BaseDataHandler {
public abstract static class WithoutPayload extends FromClient {
protected WithoutPayload(ResourceLocation identifier) {
super(identifier);
}
@Override
protected boolean prepareDataOnClient() {
return true;
}
@Override
protected void serializeDataOnClient(FriendlyByteBuf buf) {
}
@Override
protected void deserializeIncomingDataOnServer(FriendlyByteBuf buf,
Player player,
PacketSender responseSender) {
}
}
protected FromClient(ResourceLocation identifier) {
super(identifier, false);
}
@Environment(EnvType.CLIENT)
protected boolean prepareDataOnClient() {
return true;
}
@Environment(EnvType.CLIENT)
abstract protected void serializeDataOnClient(FriendlyByteBuf buf);
protected abstract void deserializeIncomingDataOnServer(FriendlyByteBuf buf,
Player player,
PacketSender responseSender);
protected abstract void runOnServerGameThread(MinecraftServer server, Player player);
@Environment(EnvType.CLIENT)
@Override
void receiveFromServer(Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the client!");
}
@Override
void receiveFromClient(MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
super.receiveFromClient(server, player, handler, buf, responseSender);
deserializeIncomingDataOnServer(buf, player, responseSender);
final Runnable runner = () -> runOnServerGameThread(server, player);
if (isBlocking()) server.executeBlocking(runner);
else server.execute(runner);
}
@Override
void sendToClient(MinecraftServer server) {
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the client!");
}
@Override
void sendToClient(MinecraftServer server, ServerPlayer player) {
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the client!");
}
@Environment(EnvType.CLIENT)
@Override
void sendToServer(Minecraft client) {
if (prepareDataOnClient()) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeDataOnClient(buf);
ClientPlayNetworking.send(getIdentifier(), buf);
}
}
}
/**
* A Message that always originates on the Server
*/
public abstract static class FromServer extends BaseDataHandler {
public abstract static class WithoutPayload extends FromServer {
protected WithoutPayload(ResourceLocation identifier) {
super(identifier);
}
@Override
protected boolean prepareDataOnServer() {
return true;
}
@Override
protected void serializeDataOnServer(FriendlyByteBuf buf) {
}
@Override
protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender) {
}
}
protected FromServer(ResourceLocation identifier) {
super(identifier, true);
}
protected boolean prepareDataOnServer() {
return true;
}
abstract protected void serializeDataOnServer(FriendlyByteBuf buf);
@Environment(EnvType.CLIENT)
abstract protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender);
@Environment(EnvType.CLIENT)
abstract protected void runOnClientGameThread(Minecraft client);
@Environment(EnvType.CLIENT)
@Override
final void receiveFromServer(Minecraft client,
ClientPacketListener handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
deserializeIncomingDataOnClient(buf, responseSender);
final Runnable runner = () -> runOnClientGameThread(client);
if (isBlocking()) client.executeBlocking(runner);
else client.execute(runner);
}
@Override
final void receiveFromClient(MinecraftServer server,
ServerPlayer player,
ServerGamePacketListenerImpl handler,
FriendlyByteBuf buf,
PacketSender responseSender) {
super.receiveFromClient(server, player, handler, buf, responseSender);
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the server!");
}
public void receiveFromMemory(FriendlyByteBuf buf) {
receiveFromServer(Minecraft.getInstance(), null, buf, null);
}
@Override
final void sendToClient(MinecraftServer server) {
if (prepareDataOnServer()) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeDataOnServer(buf);
_sendToClient(getIdentifier(), server, PlayerLookup.all(server), buf);
}
}
@Override
final void sendToClient(MinecraftServer server, ServerPlayer player) {
if (prepareDataOnServer()) {
FriendlyByteBuf buf = PacketByteBufs.create();
serializeDataOnServer(buf);
_sendToClient(getIdentifier(), server, List.of(player), buf);
}
}
@Environment(EnvType.CLIENT)
@Override
final void sendToServer(Minecraft client) {
BCLib.LOGGER.error("[Internal Error] The message '" + getIdentifier() + "' must originate from the server!");
}
}
}

View file

@ -0,0 +1,57 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.resources.ResourceLocation;
import java.util.Objects;
import java.util.function.Supplier;
import org.jetbrains.annotations.NotNull;
public class DataHandlerDescriptor {
public DataHandlerDescriptor(@NotNull ResourceLocation identifier, @NotNull Supplier<BaseDataHandler> instancer) {
this(identifier, instancer, instancer, false, false);
}
public DataHandlerDescriptor(@NotNull ResourceLocation identifier,
@NotNull Supplier<BaseDataHandler> instancer,
boolean sendOnJoin,
boolean sendBeforeEnter) {
this(identifier, instancer, instancer, sendOnJoin, sendBeforeEnter);
}
public DataHandlerDescriptor(@NotNull ResourceLocation identifier,
@NotNull Supplier<BaseDataHandler> receiv_instancer,
@NotNull Supplier<BaseDataHandler> join_instancer,
boolean sendOnJoin,
boolean sendBeforeEnter) {
this.INSTANCE = receiv_instancer;
this.JOIN_INSTANCE = join_instancer;
this.IDENTIFIER = identifier;
this.sendOnJoin = sendOnJoin;
this.sendBeforeEnter = sendBeforeEnter;
}
public final boolean sendOnJoin;
public final boolean sendBeforeEnter;
@NotNull
public final ResourceLocation IDENTIFIER;
@NotNull
public final Supplier<BaseDataHandler> INSTANCE;
@NotNull
public final Supplier<BaseDataHandler> JOIN_INSTANCE;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof ResourceLocation) {
return o.equals(IDENTIFIER);
}
if (!(o instanceof DataHandlerDescriptor that)) return false;
return IDENTIFIER.equals(that.IDENTIFIER);
}
@Override
public int hashCode() {
return Objects.hash(IDENTIFIER);
}
}

View file

@ -0,0 +1,160 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.BCLib;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
public class FileHash {
private static final int ERR_DOES_NOT_EXIST = -10;
private static final int ERR_IO_ERROR = -20;
/**
* The md5-hash of the file
*/
@NotNull
public final byte[] md5;
/**
* The size (in bytes) of the input.
*/
public final int size;
/**
* a value that is directly calculated from defined byte positions.
*/
public final int value;
FileHash(byte[] md5, int size, int value) {
Objects.nonNull(md5);
this.md5 = md5;
this.size = size;
this.value = value;
}
static FileHash createForEmpty(int errCode) {
return new FileHash(new byte[0], 0, errCode);
}
public boolean noFile() {
return md5.length == 0;
}
/**
* Serializes the Object to a buffer
*
* @param buf The buffer to write to
*/
public void serialize(FriendlyByteBuf buf) {
buf.writeInt(size);
buf.writeInt(value);
buf.writeByteArray(md5);
}
/**
* Deserialize a Buffer to a new {@link SyncFileHash}-Object
*
* @param buf Thea buffer to read from
* @return The received String
*/
public static FileHash deserialize(FriendlyByteBuf buf) {
final int size = buf.readInt();
final int value = buf.readInt();
final byte[] md5 = buf.readByteArray();
return new FileHash(md5, size, value);
}
/**
* Convert the md5-hash to a human readable string
*
* @return The converted String
*/
public String getMd5String() {
return toHexString(md5);
}
/**
* Converts a byte-array to a hex-string representation
*
* @param bytes The source array
* @return The resulting string, or an empty String if the input was {@code null}
*/
public static String toHexString(byte[] bytes) {
if (bytes == null) return "";
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* Create a new {@link FileHash}.
*
* @param file The input file
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static FileHash create(File file) {
if (!file.exists()) return createForEmpty(ERR_DOES_NOT_EXIST);
final Path path = file.toPath();
int size = 0;
byte[] md5 = new byte[0];
int value = 0;
try {
byte[] data = Files.readAllBytes(path);
size = data.length;
value = size > 0 ? (data[size / 3] | (data[size / 2] << 8) | (data[size / 5] << 16)) : -1;
if (size > 20) value |= data[20] << 24;
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(data);
md5 = md.digest();
return new FileHash(md5, size, value);
} catch (IOException e) {
BCLib.LOGGER.error("Failed to read file: " + file);
return null;
} catch (NoSuchAlgorithmException e) {
BCLib.LOGGER.error("Unable to build hash for file: " + file);
}
return createForEmpty(ERR_IO_ERROR);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FileHash)) return false;
FileHash fileHash = (FileHash) o;
return size == fileHash.size && value == fileHash.value && Arrays.equals(md5, fileHash.md5);
}
@Override
public int hashCode() {
int result = Objects.hash(size, value);
result = 31 * result + Arrays.hashCode(md5);
return result;
}
@Override
public String toString() {
return String.format("%08x", size) + "-" + String.format("%08x", value) + "-" + getMd5String();
}
}

View file

@ -0,0 +1,113 @@
package org.betterx.bclib.api.dataexchange;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSync;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSyncID;
import java.io.File;
import java.util.Objects;
/**
* Calculates a hash based on the contents of a File.
* <p>
* A File-Hash contains the md5-sum of the File, as well as its size and byte-values from defined positions
* <p>
* You can compare instances using {@link #equals(Object)} to determine if two files are
* identical.
*/
public class SyncFileHash extends AutoSyncID {
public final FileHash hash;
SyncFileHash(String modID, File file, byte[] md5, int size, int value) {
this(modID, file.getName(), md5, size, value);
}
SyncFileHash(String modID, String uniqueID, byte[] md5, int size, int value) {
this(modID, uniqueID, new FileHash(md5, size, value));
}
SyncFileHash(String modID, File file, FileHash hash) {
this(modID, file.getName(), hash);
}
SyncFileHash(String modID, String uniqueID, FileHash hash) {
super(modID, uniqueID);
this.hash = hash;
}
final static AutoSync.NeedTransferPredicate NEED_TRANSFER = (clientHash, serverHash, content) -> !clientHash.equals(
serverHash);
@Override
public String toString() {
return super.toString() + ": " + hash.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SyncFileHash)) return false;
if (!super.equals(o)) return false;
SyncFileHash that = (SyncFileHash) o;
return hash.equals(that.hash);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), hash);
}
/**
* Serializes the Object to a buffer
*
* @param buf The buffer to write to
*/
public void serialize(FriendlyByteBuf buf) {
hash.serialize(buf);
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
}
/**
* Deserialize a Buffer to a new {@link SyncFileHash}-Object
*
* @param buf Thea buffer to read from
* @return The received String
*/
public static SyncFileHash deserialize(FriendlyByteBuf buf) {
final FileHash hash = FileHash.deserialize(buf);
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
return new SyncFileHash(modID, uniqueID, hash);
}
/**
* Create a new {@link SyncFileHash}.
* <p>
* Will call {@link #create(String, File, String)} using the name of the File as {@code uniqueID}.
*
* @param modID ID of the calling Mod
* @param file The input file
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static SyncFileHash create(String modID, File file) {
return create(modID, file, file.getName());
}
/**
* Create a new {@link SyncFileHash}.
*
* @param modID ID of the calling Mod
* @param file The input file
* @param uniqueID The unique ID that is used for this File (see {@link SyncFileHash#uniqueID} for Details.
* @return A new Instance. You can compare instances using {@link #equals(Object)} to determine if two files are
* identical. Will return {@code null} when an error occurs or the File does not exist
*/
public static SyncFileHash create(String modID, File file, String uniqueID) {
return new SyncFileHash(modID, uniqueID, FileHash.create(file));
}
}

View file

@ -0,0 +1,111 @@
package org.betterx.bclib.api.dataexchange.handler;
import net.minecraft.resources.ResourceLocation;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import org.betterx.bclib.api.dataexchange.*;
import java.util.HashSet;
import java.util.Set;
abstract public class DataExchange {
private static DataExchangeAPI instance;
protected static DataExchangeAPI getInstance() {
if (instance == null) {
instance = new DataExchangeAPI();
}
return instance;
}
protected ConnectorServerside server;
protected ConnectorClientside client;
protected final Set<DataHandlerDescriptor> descriptors;
private final boolean didLoadSyncFolder = false;
abstract protected ConnectorClientside clientSupplier(DataExchange api);
abstract protected ConnectorServerside serverSupplier(DataExchange api);
protected DataExchange() {
descriptors = new HashSet<>();
}
public Set<DataHandlerDescriptor> getDescriptors() {
return descriptors;
}
public static DataHandlerDescriptor getDescriptor(ResourceLocation identifier) {
return getInstance().descriptors.stream().filter(d -> d.equals(identifier)).findFirst().orElse(null);
}
@Environment(EnvType.CLIENT)
protected void initClientside() {
if (client != null) return;
client = clientSupplier(this);
ClientPlayConnectionEvents.INIT.register(client::onPlayInit);
ClientPlayConnectionEvents.JOIN.register(client::onPlayReady);
ClientPlayConnectionEvents.DISCONNECT.register(client::onPlayDisconnect);
}
protected void initServerSide() {
if (server != null) return;
server = serverSupplier(this);
ServerPlayConnectionEvents.INIT.register(server::onPlayInit);
ServerPlayConnectionEvents.JOIN.register(server::onPlayReady);
ServerPlayConnectionEvents.DISCONNECT.register(server::onPlayDisconnect);
}
/**
* Initializes all datastructures that need to exist in the client component.
* <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/
@Environment(EnvType.CLIENT)
public static void prepareClientside() {
DataExchange api = DataExchange.getInstance();
api.initClientside();
}
/**
* Initializes all datastructures that need to exist in the server component.
* <p>
* This is automatically called by BCLib. You can register {@link DataHandler}-Objects before this Method is called
*/
public static void prepareServerside() {
DataExchange api = DataExchange.getInstance();
api.initServerSide();
}
/**
* Automatically called before the player enters the world.
* <p>
* This is automatically called by BCLib. It will send all {@link DataHandler}-Objects that have {@link DataHandlerDescriptor#sendBeforeEnter} set to*
* {@code true},
*/
@Environment(EnvType.CLIENT)
public static void sendOnEnter() {
getInstance().descriptors.forEach((desc) -> {
if (desc.sendBeforeEnter) {
BaseDataHandler h = desc.JOIN_INSTANCE.get();
if (!h.getOriginatesOnServer()) {
getInstance().client.sendToServer(h);
}
}
});
}
}

View file

@ -0,0 +1,249 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.SyncFileHash;
import org.betterx.bclib.util.ModUtil;
import org.betterx.bclib.util.ModUtil.ModInfo;
import org.betterx.bclib.util.Pair;
import org.betterx.bclib.util.PathUtil;
import org.betterx.bclib.util.Triple;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class AutoFileSyncEntry extends AutoSyncID {
static class ForDirectFileRequest extends AutoFileSyncEntry {
final File relFile;
ForDirectFileRequest(String syncID, File relFile, File absFile) {
super(AutoSyncID.ForDirectFileRequest.MOD_ID, syncID, absFile, false, (a, b, c) -> false);
this.relFile = relFile;
}
@Override
public int serializeContent(FriendlyByteBuf buf) {
int res = super.serializeContent(buf);
DataHandler.writeString(buf, relFile.toString());
return res;
}
static AutoFileSyncEntry.ForDirectFileRequest finishDeserializeContent(String syncID, FriendlyByteBuf buf) {
final String relFile = DataHandler.readString(buf);
SyncFolderDescriptor desc = AutoSync.getSyncFolderDescriptor(syncID);
if (desc != null) {
//ensures that the file is not above the base-folder
if (desc.acceptChildElements(desc.mapAbsolute(relFile))) {
return new AutoFileSyncEntry.ForDirectFileRequest(syncID,
new File(relFile),
desc.localFolder.resolve(relFile)
.normalize()
.toFile());
}
}
return null;
}
@Override
public String toString() {
return uniqueID + " - " + relFile;
}
}
static class ForModFileRequest extends AutoFileSyncEntry {
public static File getLocalPathForID(String modID, boolean matchLocalVersion) {
ModInfo mi = ModUtil.getModInfo(modID, matchLocalVersion);
if (mi != null) {
return mi.jarPath.toFile();
}
return null;
}
public final String version;
ForModFileRequest(String modID, boolean matchLocalVersion, String version) {
super(modID,
AutoSyncID.ForModFileRequest.UNIQUE_ID,
getLocalPathForID(modID, matchLocalVersion),
false,
(a, b, c) -> false);
if (this.fileName == null && matchLocalVersion) {
BCLib.LOGGER.error("Unknown mod '" + modID + "'.");
}
if (version == null)
this.version = ModUtil.getModVersion(modID);
else
this.version = version;
}
@Override
public int serializeContent(FriendlyByteBuf buf) {
final int res = super.serializeContent(buf);
buf.writeInt(ModUtil.convertModVersion(version));
return res;
}
static AutoFileSyncEntry.ForModFileRequest finishDeserializeContent(String modID, FriendlyByteBuf buf) {
final String version = ModUtil.convertModVersion(buf.readInt());
return new AutoFileSyncEntry.ForModFileRequest(modID, false, version);
}
@Override
public String toString() {
return "Mod " + modID + " (v" + version + ")";
}
}
public final AutoSync.NeedTransferPredicate needTransfer;
public final File fileName;
public final boolean requestContent;
private SyncFileHash hash;
AutoFileSyncEntry(String modID,
File fileName,
boolean requestContent,
AutoSync.NeedTransferPredicate needTransfer) {
this(modID, fileName.getName(), fileName, requestContent, needTransfer);
}
AutoFileSyncEntry(String modID,
String uniqueID,
File fileName,
boolean requestContent,
AutoSync.NeedTransferPredicate needTransfer) {
super(modID, uniqueID);
this.needTransfer = needTransfer;
this.fileName = fileName;
this.requestContent = requestContent;
}
public SyncFileHash getFileHash() {
if (hash == null) {
hash = SyncFileHash.create(modID, fileName, uniqueID);
}
return hash;
}
public byte[] getContent() {
if (!fileName.exists()) return new byte[0];
final Path path = fileName.toPath();
try {
return Files.readAllBytes(path);
} catch (IOException e) {
}
return new byte[0];
}
public int serializeContent(FriendlyByteBuf buf) {
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
return serializeFileContent(buf);
}
public static Triple<AutoFileSyncEntry, byte[], AutoSyncID> deserializeContent(FriendlyByteBuf buf) {
final String modID = DataHandler.readString(buf);
final String uniqueID = DataHandler.readString(buf);
byte[] data = deserializeFileContent(buf);
AutoFileSyncEntry entry;
if (AutoSyncID.ForDirectFileRequest.MOD_ID.equals(modID)) {
entry = AutoFileSyncEntry.ForDirectFileRequest.finishDeserializeContent(uniqueID, buf);
} else if (AutoSyncID.ForModFileRequest.UNIQUE_ID.equals(uniqueID)) {
entry = AutoFileSyncEntry.ForModFileRequest.finishDeserializeContent(modID, buf);
} else {
entry = AutoFileSyncEntry.findMatching(modID, uniqueID);
}
return new Triple<>(entry, data, new AutoSyncID(modID, uniqueID));
}
public void serialize(FriendlyByteBuf buf) {
getFileHash().serialize(buf);
buf.writeBoolean(requestContent);
if (requestContent) {
serializeFileContent(buf);
}
}
public static AutoSync.AutoSyncTriple deserializeAndMatch(FriendlyByteBuf buf) {
Pair<SyncFileHash, byte[]> e = deserialize(buf);
AutoFileSyncEntry match = findMatching(e.first);
return new AutoSync.AutoSyncTriple(e.first, e.second, match);
}
public static Pair<SyncFileHash, byte[]> deserialize(FriendlyByteBuf buf) {
SyncFileHash hash = SyncFileHash.deserialize(buf);
boolean withContent = buf.readBoolean();
byte[] data = null;
if (withContent) {
data = deserializeFileContent(buf);
}
return new Pair(hash, data);
}
private int serializeFileContent(FriendlyByteBuf buf) {
if (!PathUtil.isChildOf(PathUtil.GAME_FOLDER, fileName.toPath())) {
BCLib.LOGGER.error(fileName + " is not within game folder " + PathUtil.GAME_FOLDER + ". Pretending it does not exist.");
buf.writeInt(0);
return 0;
}
byte[] content = getContent();
buf.writeInt(content.length);
buf.writeByteArray(content);
return content.length;
}
private static byte[] deserializeFileContent(FriendlyByteBuf buf) {
byte[] data;
int size = buf.readInt();
data = buf.readByteArray(size);
return data;
}
public static AutoFileSyncEntry findMatching(SyncFileHash hash) {
return findMatching(hash.modID, hash.uniqueID);
}
public static AutoFileSyncEntry findMatching(AutoSyncID aid) {
if (aid instanceof AutoSyncID.ForDirectFileRequest) {
AutoSyncID.ForDirectFileRequest freq = (AutoSyncID.ForDirectFileRequest) aid;
SyncFolderDescriptor desc = AutoSync.getSyncFolderDescriptor(freq.uniqueID);
if (desc != null) {
SyncFolderDescriptor.SubFile subFile = desc.getLocalSubFile(freq.relFile.toString());
if (subFile != null) {
final File absPath = desc.localFolder.resolve(subFile.relPath)
.normalize()
.toFile();
return new AutoFileSyncEntry.ForDirectFileRequest(freq.uniqueID,
new File(subFile.relPath),
absPath);
}
}
return null;
} else if (aid instanceof AutoSyncID.ForModFileRequest) {
AutoSyncID.ForModFileRequest mreq = (AutoSyncID.ForModFileRequest) aid;
return new AutoFileSyncEntry.ForModFileRequest(mreq.modID, true, null);
}
return findMatching(aid.modID, aid.uniqueID);
}
public static AutoFileSyncEntry findMatching(String modID, String uniqueID) {
return AutoSync.getAutoSyncFiles()
.stream()
.filter(asf -> asf.modID.equals(modID) && asf.uniqueID.equals(uniqueID))
.findFirst()
.orElse(null);
}
}

View file

@ -0,0 +1,199 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.fabricmc.loader.api.FabricLoader;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataExchangeAPI;
import org.betterx.bclib.api.dataexchange.SyncFileHash;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.config.ServerConfig;
import org.betterx.bclib.util.PathUtil;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
public class AutoSync {
public static final String SYNC_CATEGORY = "auto_sync";
public final static SyncFolderDescriptor SYNC_FOLDER = new SyncFolderDescriptor("BCLIB-SYNC",
FabricLoader.getInstance()
.getGameDir()
.resolve("bclib-sync")
.normalize()
.toAbsolutePath(),
true);
@FunctionalInterface
public interface NeedTransferPredicate {
boolean test(SyncFileHash clientHash, SyncFileHash serverHash, FileContentWrapper content);
}
final static class AutoSyncTriple {
public final SyncFileHash serverHash;
public final byte[] serverContent;
public final AutoFileSyncEntry localMatch;
public AutoSyncTriple(SyncFileHash serverHash, byte[] serverContent, AutoFileSyncEntry localMatch) {
this.serverHash = serverHash;
this.serverContent = serverContent;
this.localMatch = localMatch;
}
@Override
public String toString() {
return serverHash.modID + "." + serverHash.uniqueID;
}
}
// ##### File Syncing
protected final static List<BiConsumer<AutoSyncID, File>> onWriteCallbacks = new ArrayList<>(2);
/**
* Register a function that is called whenever the client receives a file from the server and replaced toe local
* file with the new content.
* <p>
* This callback is usefull if you need to reload the new content before the game is quit.
*
* @param callback A Function that receives the AutoSyncID as well as the Filename.
*/
public static void addOnWriteCallback(BiConsumer<AutoSyncID, File> callback) {
onWriteCallbacks.add(callback);
}
private static final List<AutoFileSyncEntry> autoSyncFiles = new ArrayList<>(4);
public static List<AutoFileSyncEntry> getAutoSyncFiles() {
return autoSyncFiles;
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
* @param fileName The name of the File
* @param requestContent When {@code true} the content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* If the File needs to be copied. Normally using the {@link SyncFileHash}
* for comparison is sufficient.
*/
public static void addAutoSyncFileData(String modID,
File fileName,
boolean requestContent,
NeedTransferPredicate needTransfer) {
if (!PathUtil.isChildOf(PathUtil.GAME_FOLDER, fileName.toPath())) {
BCLib.LOGGER.error(fileName + " is outside of Game Folder " + PathUtil.GAME_FOLDER);
} else {
autoSyncFiles.add(new AutoFileSyncEntry(modID, fileName, requestContent, needTransfer));
}
}
/**
* Registers a File for automatic client syncing.
*
* @param modID The ID of the calling Mod
* @param uniqueID A unique Identifier for the File. (see {@link SyncFileHash#uniqueID} for
* Details
* @param needTransfer If the predicate returns true, the file needs to get copied to the server.
* @param fileName The name of the File
* @param requestContent When {@code true} the content of the file is requested for comparison. This will copy the
* entire file from the client to the server.
* <p>
* You should only use this option, if you need to compare parts of the file in order to decide
* If the File needs to be copied. Normally using the {@link SyncFileHash}
* for comparison is sufficient.
*/
public static void addAutoSyncFileData(String modID,
String uniqueID,
File fileName,
boolean requestContent,
NeedTransferPredicate needTransfer) {
if (!PathUtil.isChildOf(PathUtil.GAME_FOLDER, fileName.toPath())) {
BCLib.LOGGER.error(fileName + " is outside of Game Folder " + PathUtil.GAME_FOLDER);
} else {
autoSyncFiles.add(new AutoFileSyncEntry(modID, uniqueID, fileName, requestContent, needTransfer));
}
}
/**
* Called when {@code SendFiles} received a File on the Client and wrote it to the FileSystem.
* <p>
* This is the place where reload Code should go.
*
* @param aid The ID of the received File
* @param file The location of the FIle on the client
*/
static void didReceiveFile(AutoSyncID aid, File file) {
onWriteCallbacks.forEach(fkt -> fkt.accept(aid, file));
}
// ##### Folder Syncing
static final List<SyncFolderDescriptor> syncFolderDescriptions = Arrays.asList(SYNC_FOLDER);
private List<String> syncFolderContent;
protected List<String> getSyncFolderContent() {
if (syncFolderContent == null) {
return new ArrayList<>(0);
}
return syncFolderContent;
}
private static boolean didRegisterAdditionalMods = false;
//we call this from HelloClient on the Server to prepare transfer
protected static void loadSyncFolder() {
if (Configs.SERVER_CONFIG.isOfferingFiles()) {
syncFolderDescriptions.forEach(desc -> desc.loadCache());
}
if (!didRegisterAdditionalMods && Configs.SERVER_CONFIG.isOfferingMods()) {
didRegisterAdditionalMods = true;
List<String> modIDs = Configs.SERVER_CONFIG.get(ServerConfig.ADDITIONAL_MODS);
if (modIDs != null) {
modIDs.stream().forEach(modID -> DataExchangeAPI.registerModDependency(modID));
}
}
}
protected static SyncFolderDescriptor getSyncFolderDescriptor(String folderID) {
return syncFolderDescriptions.stream()
.filter(d -> d.equals(folderID))
.findFirst()
.orElse(null);
}
protected static Path localBasePathForFolderID(String folderID) {
final SyncFolderDescriptor desc = getSyncFolderDescriptor(folderID);
if (desc != null) {
return desc.localFolder;
} else {
BCLib.LOGGER.warning("Unknown Sync-Folder ID '" + folderID + "'");
return null;
}
}
public static void registerSyncFolder(String folderID, Path localBaseFolder, boolean removeAdditionalFiles) {
localBaseFolder = localBaseFolder.normalize();
if (PathUtil.isChildOf(PathUtil.GAME_FOLDER, localBaseFolder)) {
final SyncFolderDescriptor desc = new SyncFolderDescriptor(folderID,
localBaseFolder,
removeAdditionalFiles);
if (syncFolderDescriptions.contains(desc)) {
BCLib.LOGGER.warning("Tried to override Folder Sync '" + folderID + "' again.");
} else {
syncFolderDescriptions.add(desc);
}
} else {
BCLib.LOGGER.error(localBaseFolder + " (from " + folderID + ") is outside the game directory " + PathUtil.GAME_FOLDER + ". Sync is not allowed.");
}
}
}

View file

@ -0,0 +1,144 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.config.Config;
import org.betterx.bclib.util.ModUtil;
import java.io.File;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
public class AutoSyncID {
static class WithContentOverride extends AutoSyncID {
final FileContentWrapper contentWrapper;
final File localFile;
WithContentOverride(String modID, String uniqueID, FileContentWrapper contentWrapper, File localFile) {
super(modID, uniqueID);
this.contentWrapper = contentWrapper;
this.localFile = localFile;
}
@Override
public String toString() {
return super.toString() + " (Content override)";
}
}
static class ForDirectFileRequest extends AutoSyncID {
public final static String MOD_ID = "bclib::FILE";
final File relFile;
ForDirectFileRequest(String syncID, File relFile) {
super(ForDirectFileRequest.MOD_ID, syncID);
this.relFile = relFile;
}
@Override
void serializeData(FriendlyByteBuf buf) {
super.serializeData(buf);
DataHandler.writeString(buf, relFile.toString());
}
static ForDirectFileRequest finishDeserialize(String modID, String uniqueID, FriendlyByteBuf buf) {
final File fl = new File(DataHandler.readString(buf));
return new ForDirectFileRequest(uniqueID, fl);
}
@Override
public String toString() {
return super.uniqueID + " (" + this.relFile + ")";
}
}
static class ForModFileRequest extends AutoSyncID {
public final static String UNIQUE_ID = "bclib::MOD";
private final String version;
ForModFileRequest(String modID, String version) {
super(modID, ForModFileRequest.UNIQUE_ID);
this.version = version;
}
@Override
void serializeData(FriendlyByteBuf buf) {
super.serializeData(buf);
buf.writeInt(ModUtil.convertModVersion(version));
}
static ForModFileRequest finishDeserialize(String modID, String uniqueID, FriendlyByteBuf buf) {
final String version = ModUtil.convertModVersion(buf.readInt());
return new ForModFileRequest(modID, version);
}
@Override
public String toString() {
return super.modID + " (v" + this.version + ")";
}
}
/**
* A Unique ID for the referenced File.
* <p>
* Files with the same {@link #modID} need to have a unique IDs. Normally the filename from FileHash(String, File, byte[], int, int)
* is used to generated that ID, but you can directly specify one using FileHash(String, String, byte[], int, int).
*/
@NotNull
public final String uniqueID;
/**
* The ID of the Mod that is registering the File
*/
@NotNull
public final String modID;
public AutoSyncID(String modID, String uniqueID) {
Objects.nonNull(modID);
Objects.nonNull(uniqueID);
this.modID = modID;
this.uniqueID = uniqueID;
}
@Override
public String toString() {
return modID + "." + uniqueID;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AutoSyncID)) return false;
AutoSyncID that = (AutoSyncID) o;
return uniqueID.equals(that.uniqueID) && modID.equals(that.modID);
}
@Override
public int hashCode() {
return Objects.hash(uniqueID, modID);
}
void serializeData(FriendlyByteBuf buf) {
DataHandler.writeString(buf, modID);
DataHandler.writeString(buf, uniqueID);
}
static AutoSyncID deserializeData(FriendlyByteBuf buf) {
String modID = DataHandler.readString(buf);
String uID = DataHandler.readString(buf);
if (ForDirectFileRequest.MOD_ID.equals(modID)) {
return ForDirectFileRequest.finishDeserialize(modID, uID, buf);
} else if (ForModFileRequest.UNIQUE_ID.equals(uID)) {
return ForModFileRequest.finishDeserialize(modID, uID, buf);
} else {
return new AutoSyncID(modID, uID);
}
}
public boolean isConfigFile() {
return this.uniqueID.startsWith(Config.CONFIG_SYNC_PREFIX);
}
}

View file

@ -0,0 +1,276 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.ProgressListener;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.BaseDataHandler;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.api.dataexchange.handler.DataExchange;
import java.util.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Used to seperate large data transfers into multiple smaller messages.
* <p>
* {@link DataHandler} will automatically convert larger messages into Chunks on the Server
* and assemble the original message from those chunks on the client.
*/
public class Chunker extends DataHandler.FromServer {
/**
* Responsible for assembling the original ByteBuffer created by {@link PacketChunkSender} on the
* receiving end. Automatically created from the header {@link Chunker}-Message (where the serialNo==-1)
*/
static class PacketChunkReceiver {
@NotNull
public final UUID uuid;
public final int chunkCount;
@NotNull
private final FriendlyByteBuf networkedBuf;
@Nullable
private final DataHandlerDescriptor descriptor;
private static final List<PacketChunkReceiver> active = new ArrayList<>(1);
private static PacketChunkReceiver newReceiver(@NotNull UUID uuid, int chunkCount, ResourceLocation origin) {
DataHandlerDescriptor desc = DataExchange.getDescriptor(origin);
final PacketChunkReceiver r = new PacketChunkReceiver(uuid, chunkCount, desc);
active.add(r);
return r;
}
private static PacketChunkReceiver getOrCreate(@NotNull UUID uuid, int chunkCount, ResourceLocation origin) {
return active.stream()
.filter(r -> r.uuid.equals(uuid))
.findFirst()
.orElse(newReceiver(uuid, chunkCount, origin));
}
public static PacketChunkReceiver get(@NotNull UUID uuid) {
return active.stream().filter(r -> r.uuid.equals(uuid)).findFirst().orElse(null);
}
private PacketChunkReceiver(@NotNull UUID uuid, int chunkCount, @Nullable DataHandlerDescriptor descriptor) {
this.uuid = uuid;
this.chunkCount = chunkCount;
networkedBuf = PacketByteBufs.create();
this.descriptor = descriptor;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PacketChunkReceiver)) return false;
PacketChunkReceiver that = (PacketChunkReceiver) o;
return uuid.equals(that.uuid);
}
@Override
public int hashCode() {
return Objects.hash(uuid);
}
public boolean testFinished() {
ProgressListener listener = ChunkerProgress.getProgressListener();
if (listener != null) {
listener.progressStagePercentage((100 * receivedCount) / chunkCount);
}
if (incomingBuffer == null) {
return true;
}
if (lastReadSerial >= chunkCount - 1) {
onFinish();
return true;
}
return false;
}
private void addBuffer(FriendlyByteBuf input) {
final int size = input.readableBytes();
final int cap = networkedBuf.capacity() - networkedBuf.writerIndex();
if (cap < size) {
networkedBuf.capacity(networkedBuf.writerIndex() + size);
}
input.readBytes(networkedBuf, size);
input.clear();
}
protected void onFinish() {
incomingBuffer.clear();
incomingBuffer = null;
final BaseDataHandler baseHandler = descriptor.INSTANCE.get();
if (baseHandler instanceof DataHandler.FromServer handler) {
handler.receiveFromMemory(networkedBuf);
}
}
Map<Integer, FriendlyByteBuf> incomingBuffer = new HashMap<>();
int lastReadSerial = -1;
int receivedCount = 0;
public void processReceived(FriendlyByteBuf buf, int serialNo, int size) {
receivedCount++;
if (lastReadSerial == serialNo - 1) {
addBuffer(buf);
lastReadSerial = serialNo;
} else {
//not sure if order is guaranteed by the underlying system!
boolean haveAll = true;
for (int nr = lastReadSerial + 1; nr < serialNo - 1; nr++) {
if (incomingBuffer.get(nr) == null) {
haveAll = false;
break;
}
}
if (haveAll) {
for (int nr = lastReadSerial + 1; nr < serialNo - 1; nr++) {
addBuffer(incomingBuffer.get(nr));
incomingBuffer.put(nr, null);
}
addBuffer(buf);
lastReadSerial = serialNo;
} else {
incomingBuffer.put(serialNo, buf);
}
}
}
}
/**
* Responsible for splitting an outgoing ByteBuffer into several smaller Chunks and
* send them as seperate messages to the {@link Chunker}-Channel
*/
public static class PacketChunkSender {
private final FriendlyByteBuf networkedBuf;
public final UUID uuid;
public final int chunkCount;
public final int size;
public final ResourceLocation origin;
public PacketChunkSender(FriendlyByteBuf buf, ResourceLocation origin) {
networkedBuf = buf;
size = buf.readableBytes();
chunkCount = (int) Math.ceil((double) size / MAX_PAYLOAD_SIZE);
uuid = UUID.randomUUID();
this.origin = origin;
}
public void sendChunks(Collection<ServerPlayer> players) {
BCLib.LOGGER.info("Sending Request in " + chunkCount + " Packet-Chunks");
for (int i = -1; i < chunkCount; i++) {
Chunker c = new Chunker(i, uuid, networkedBuf, chunkCount, origin);
FriendlyByteBuf buf = PacketByteBufs.create();
c.serializeDataOnServer(buf);
for (ServerPlayer player : players) {
ServerPlayNetworking.send(player, DESCRIPTOR.IDENTIFIER, buf);
}
}
}
}
//header = version + UUID + serialNo + size, see serializeDataOnServer
private static final int HEADER_SIZE = 1 + 16 + 4 + 4;
public static final int MAX_PACKET_SIZE = 1024 * 1024;
private static final int MAX_PAYLOAD_SIZE = MAX_PACKET_SIZE - HEADER_SIZE;
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"chunker"),
Chunker::new,
false,
false);
private int serialNo;
private UUID uuid;
private int chunkCount;
private FriendlyByteBuf networkedBuf;
private ResourceLocation origin;
protected Chunker(int serialNo, UUID uuid, FriendlyByteBuf networkedBuf, int chunkCount, ResourceLocation origin) {
super(DESCRIPTOR.IDENTIFIER);
this.serialNo = serialNo;
this.uuid = uuid;
this.networkedBuf = networkedBuf;
this.chunkCount = chunkCount;
this.origin = origin;
}
protected Chunker() {
super(DESCRIPTOR.IDENTIFIER);
}
@Override
protected void serializeDataOnServer(FriendlyByteBuf buf) {
//Sending Header. Make sure to change HEADER_SIZE if you change this!
buf.writeByte(0);
buf.writeLong(uuid.getMostSignificantBits());
buf.writeLong(uuid.getLeastSignificantBits());
buf.writeInt(serialNo);
//sending Payload
if (serialNo == -1) {
//this is our header-Chunk that transports status information
buf.writeInt(chunkCount);
writeString(buf, origin.getNamespace());
writeString(buf, origin.getPath());
} else {
//this is an actual payload chunk
buf.capacity(MAX_PACKET_SIZE);
final int size = Math.min(MAX_PAYLOAD_SIZE, networkedBuf.readableBytes());
buf.writeInt(size);
networkedBuf.readBytes(buf, size);
}
}
private PacketChunkReceiver receiver;
@Override
protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender) {
final int version = buf.readByte();
uuid = new UUID(buf.readLong(), buf.readLong());
serialNo = buf.readInt();
if (serialNo == -1) {
chunkCount = buf.readInt();
final String namespace = readString(buf);
final String path = readString(buf);
ResourceLocation ident = new ResourceLocation(namespace, path);
BCLib.LOGGER.info("Receiving " + chunkCount + " + Packet-Chunks for " + ident);
receiver = PacketChunkReceiver.getOrCreate(uuid, chunkCount, ident);
} else {
receiver = PacketChunkReceiver.get(uuid);
if (receiver != null) {
final int size = buf.readInt();
receiver.processReceived(buf, serialNo, size);
} else {
BCLib.LOGGER.error("Unknown Packet-Chunk Transfer for " + uuid);
}
}
}
@Override
protected void runOnClientGameThread(Minecraft client) {
if (receiver != null) {
receiver.testFinished();
}
}
}

View file

@ -0,0 +1,28 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.util.ProgressListener;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import org.betterx.bclib.gui.screens.ProgressScreen;
@Environment(EnvType.CLIENT)
public class ChunkerProgress {
private static ProgressScreen progressScreen;
@Environment(EnvType.CLIENT)
public static void setProgressScreen(ProgressScreen scr) {
progressScreen = scr;
}
@Environment(EnvType.CLIENT)
public static ProgressScreen getProgressScreen() {
return progressScreen;
}
@Environment(EnvType.CLIENT)
public static ProgressListener getProgressListener() {
return progressScreen;
}
}

View file

@ -0,0 +1,75 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import org.betterx.bclib.BCLib;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class FileContentWrapper {
private byte[] rawContent;
private ByteArrayOutputStream outputStream;
FileContentWrapper(byte[] content) {
this.rawContent = content;
this.outputStream = null;
}
public byte[] getOriginalContent() {
return rawContent;
}
public byte[] getRawContent() {
if (outputStream != null) {
return outputStream.toByteArray();
}
return rawContent;
}
private void invalidateOutputStream() {
if (this.outputStream != null) {
try {
this.outputStream.close();
} catch (IOException e) {
BCLib.LOGGER.debug(e);
}
}
this.outputStream = null;
}
public void setRawContent(byte[] rawContent) {
this.rawContent = rawContent;
invalidateOutputStream();
}
public void syncWithOutputStream() {
if (outputStream != null) {
try {
outputStream.flush();
} catch (IOException e) {
BCLib.LOGGER.error(e.getMessage());
e.printStackTrace();
}
setRawContent(getRawContent());
invalidateOutputStream();
}
}
public ByteArrayInputStream getInputStream() {
if (rawContent == null) return new ByteArrayInputStream(new byte[0]);
return new ByteArrayInputStream(rawContent);
}
public ByteArrayOutputStream getOrCreateOutputStream() {
if (this.outputStream == null) {
return this.getEmptyOutputStream();
}
return this.outputStream;
}
public ByteArrayOutputStream getEmptyOutputStream() {
invalidateOutputStream();
this.outputStream = new ByteArrayOutputStream(this.rawContent.length);
return this.outputStream;
}
}

View file

@ -0,0 +1,509 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.fabricmc.loader.api.metadata.ModEnvironment;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataExchangeAPI;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.config.ServerConfig;
import org.betterx.bclib.gui.screens.ModListScreen;
import org.betterx.bclib.gui.screens.ProgressScreen;
import org.betterx.bclib.gui.screens.SyncFilesScreen;
import org.betterx.bclib.gui.screens.WarnBCLibVersionMismatch;
import org.betterx.bclib.util.ModUtil;
import org.betterx.bclib.util.ModUtil.ModInfo;
import org.betterx.bclib.util.PathUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
/**
* Sent from the Server to the Client.
* <p>
* For Details refer to {@link HelloServer}
*/
public class HelloClient extends DataHandler.FromServer {
public record OfferedModInfo(String version, int size, boolean canDownload) {
}
public interface IServerModMap extends Map<String, OfferedModInfo> {
}
public static class ServerModMap extends HashMap<String, OfferedModInfo> implements IServerModMap {
}
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"hello_client"),
HelloClient::new,
false,
false);
public HelloClient() {
super(DESCRIPTOR.IDENTIFIER);
}
static String getBCLibVersion() {
return ModUtil.getModVersion(BCLib.MOD_ID);
}
@Override
protected boolean prepareDataOnServer() {
if (!Configs.SERVER_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the server.");
return false;
}
AutoSync.loadSyncFolder();
return true;
}
@Override
protected void serializeDataOnServer(FriendlyByteBuf buf) {
final String vbclib = getBCLibVersion();
BCLib.LOGGER.info("Sending Hello to Client. (server=" + vbclib + ")");
//write BCLibVersion (=protocol version)
buf.writeInt(ModUtil.convertModVersion(vbclib));
if (Configs.SERVER_CONFIG.isOfferingMods() || Configs.SERVER_CONFIG.isOfferingInfosForMods()) {
List<String> mods = DataExchangeAPI.registeredMods();
final List<String> inmods = mods;
if (Configs.SERVER_CONFIG.isOfferingAllMods() || Configs.SERVER_CONFIG.isOfferingInfosForMods()) {
mods = new ArrayList<>(inmods.size());
mods.addAll(inmods);
mods.addAll(ModUtil
.getMods()
.entrySet()
.stream()
.filter(entry -> entry.getValue().metadata.getEnvironment() != ModEnvironment.SERVER && !inmods.contains(
entry.getKey()))
.map(entry -> entry.getKey())
.collect(Collectors.toList())
);
}
mods = mods
.stream()
.filter(entry -> !Configs.SERVER_CONFIG.get(ServerConfig.EXCLUDED_MODS).contains(entry))
.collect(Collectors.toList());
//write Plugin Versions
buf.writeInt(mods.size());
for (String modID : mods) {
final String ver = ModUtil.getModVersion(modID);
int size = 0;
final ModInfo mi = ModUtil.getModInfo(modID);
if (mi != null) {
try {
size = (int) Files.size(mi.jarPath);
} catch (IOException e) {
BCLib.LOGGER.error("Unable to get File Size: " + e.getMessage());
}
}
writeString(buf, modID);
buf.writeInt(ModUtil.convertModVersion(ver));
buf.writeInt(size);
final boolean canDownload = size > 0 && Configs.SERVER_CONFIG.isOfferingMods() && (Configs.SERVER_CONFIG.isOfferingAllMods() || inmods.contains(
modID));
buf.writeBoolean(canDownload);
BCLib.LOGGER.info(" - Listing Mod " + modID + " v" + ver + " (size: " + PathUtil.humanReadableFileSize(
size) + ", download=" + canDownload + ")");
}
} else {
BCLib.LOGGER.info("Server will not list Mods.");
buf.writeInt(0);
}
if (Configs.SERVER_CONFIG.isOfferingFiles() || Configs.SERVER_CONFIG.isOfferingConfigs()) {
//do only include files that exist on the server
final List<AutoFileSyncEntry> existingAutoSyncFiles = AutoSync.getAutoSyncFiles()
.stream()
.filter(e -> e.fileName.exists())
.filter(e -> (e.isConfigFile() && Configs.SERVER_CONFIG.isOfferingConfigs()) || (e instanceof AutoFileSyncEntry.ForDirectFileRequest && Configs.SERVER_CONFIG.isOfferingFiles()))
.collect(Collectors.toList());
//send config Data
buf.writeInt(existingAutoSyncFiles.size());
for (AutoFileSyncEntry entry : existingAutoSyncFiles) {
entry.serialize(buf);
BCLib.LOGGER.info(" - Offering " + (entry.isConfigFile() ? "Config " : "File ") + entry);
}
} else {
BCLib.LOGGER.info("Server will neither offer Files nor Configs.");
buf.writeInt(0);
}
if (Configs.SERVER_CONFIG.isOfferingFiles()) {
buf.writeInt(AutoSync.syncFolderDescriptions.size());
AutoSync.syncFolderDescriptions.forEach(desc -> {
BCLib.LOGGER.info(" - Offering Folder " + desc.localFolder + " (allowDelete=" + desc.removeAdditionalFiles + ")");
desc.serialize(buf);
});
} else {
BCLib.LOGGER.info("Server will not offer Sync Folders.");
buf.writeInt(0);
}
buf.writeBoolean(Configs.SERVER_CONFIG.isOfferingInfosForMods());
}
String bclibVersion = "0.0.0";
IServerModMap modVersion = new ServerModMap();
List<AutoSync.AutoSyncTriple> autoSyncedFiles = null;
List<SyncFolderDescriptor> autoSynFolders = null;
boolean serverPublishedModInfo = false;
@Environment(EnvType.CLIENT)
@Override
protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender) {
//read BCLibVersion (=protocol version)
bclibVersion = ModUtil.convertModVersion(buf.readInt());
//read Plugin Versions
modVersion = new ServerModMap();
int count = buf.readInt();
for (int i = 0; i < count; i++) {
final String id = readString(buf);
final String version = ModUtil.convertModVersion(buf.readInt());
final int size;
final boolean canDownload;
//since v0.4.1 we also send the size of the mod-File
size = buf.readInt();
canDownload = buf.readBoolean();
modVersion.put(id, new OfferedModInfo(version, size, canDownload));
}
//read config Data
count = buf.readInt();
autoSyncedFiles = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
//System.out.println("Deserializing ");
AutoSync.AutoSyncTriple t = AutoFileSyncEntry.deserializeAndMatch(buf);
autoSyncedFiles.add(t);
//System.out.println(t.first);
}
autoSynFolders = new ArrayList<>(1);
//since v0.4.1 we also send the sync folders
final int folderCount = buf.readInt();
for (int i = 0; i < folderCount; i++) {
SyncFolderDescriptor desc = SyncFolderDescriptor.deserialize(buf);
autoSynFolders.add(desc);
}
serverPublishedModInfo = buf.readBoolean();
}
@Environment(EnvType.CLIENT)
private void processAutoSyncFolder(final List<AutoSyncID> filesToRequest,
final List<AutoSyncID.ForDirectFileRequest> filesToRemove) {
if (!Configs.CLIENT_CONFIG.isAcceptingFiles()) {
return;
}
if (autoSynFolders.size() > 0) {
BCLib.LOGGER.info("Folders offered by Server:");
}
autoSynFolders.forEach(desc -> {
//desc contains the fileCache sent from the server, load the local version to get hold of the actual file cache on the client
SyncFolderDescriptor localDescriptor = AutoSync.getSyncFolderDescriptor(desc.folderID);
if (localDescriptor != null) {
BCLib.LOGGER.info(" - " + desc.folderID + " (" + desc.localFolder + ", allowRemove=" + desc.removeAdditionalFiles + ")");
localDescriptor.invalidateCache();
desc.relativeFilesStream()
.filter(desc::discardChildElements)
.forEach(subFile -> {
BCLib.LOGGER.warning(" * " + subFile.relPath + " (REJECTED)");
});
if (desc.removeAdditionalFiles) {
List<AutoSyncID.ForDirectFileRequest> additionalFiles = localDescriptor.relativeFilesStream()
.filter(subFile -> !desc.hasRelativeFile(
subFile))
.map(desc::mapAbsolute)
.filter(desc::acceptChildElements)
.map(absPath -> new AutoSyncID.ForDirectFileRequest(
desc.folderID,
absPath.toFile()))
.collect(Collectors.toList());
additionalFiles.forEach(aid -> BCLib.LOGGER.info(" * " + desc.localFolder.relativize(aid.relFile.toPath()) + " (missing on server)"));
filesToRemove.addAll(additionalFiles);
}
desc.relativeFilesStream()
.filter(desc::acceptChildElements)
.forEach(subFile -> {
SyncFolderDescriptor.SubFile localSubFile = localDescriptor.getLocalSubFile(subFile.relPath);
if (localSubFile != null) {
//the file exists locally, check if the hashes match
if (!localSubFile.hash.equals(subFile.hash)) {
BCLib.LOGGER.info(" * " + subFile.relPath + " (changed)");
filesToRequest.add(new AutoSyncID.ForDirectFileRequest(desc.folderID,
new File(subFile.relPath)));
} else {
BCLib.LOGGER.info(" * " + subFile.relPath);
}
} else {
//the file is missing locally
BCLib.LOGGER.info(" * " + subFile.relPath + " (missing on client)");
filesToRequest.add(new AutoSyncID.ForDirectFileRequest(desc.folderID,
new File(subFile.relPath)));
}
});
//free some memory
localDescriptor.invalidateCache();
} else {
BCLib.LOGGER.info(" - " + desc.folderID + " (Failed to find)");
}
});
}
@Environment(EnvType.CLIENT)
private void processSingleFileSync(final List<AutoSyncID> filesToRequest) {
final boolean debugHashes = Configs.CLIENT_CONFIG.shouldPrintDebugHashes();
if (autoSyncedFiles.size() > 0) {
BCLib.LOGGER.info("Files offered by Server:");
}
//Handle single sync files
//Single files need to be registered for sync on both client and server
//There are no restrictions to the target folder, but the client decides the final
//location.
for (AutoSync.AutoSyncTriple e : autoSyncedFiles) {
String actionString = "";
FileContentWrapper contentWrapper = new FileContentWrapper(e.serverContent);
if (e.localMatch == null) {
actionString = "(unknown source -> omitting)";
//filesToRequest.add(new AutoSyncID(e.serverHash.modID, e.serverHash.uniqueID));
} else if (e.localMatch.needTransfer.test(e.localMatch.getFileHash(), e.serverHash, contentWrapper)) {
actionString = "(prepare update)";
//we did not yet receive the new content
if (contentWrapper.getRawContent() == null) {
filesToRequest.add(new AutoSyncID(e.serverHash.modID, e.serverHash.uniqueID));
} else {
filesToRequest.add(new AutoSyncID.WithContentOverride(e.serverHash.modID,
e.serverHash.uniqueID,
contentWrapper,
e.localMatch.fileName));
}
}
BCLib.LOGGER.info(" - " + e + ": " + actionString);
if (debugHashes) {
BCLib.LOGGER.info(" * " + e.serverHash + " (Server)");
BCLib.LOGGER.info(" * " + e.localMatch.getFileHash() + " (Client)");
BCLib.LOGGER.info(" * local Content " + (contentWrapper.getRawContent() == null));
}
}
}
@Environment(EnvType.CLIENT)
private void processModFileSync(final List<AutoSyncID> filesToRequest, final Set<String> mismatchingMods) {
for (Entry<String, OfferedModInfo> e : modVersion.entrySet()) {
final String localVersion = ModUtil.convertModVersion(ModUtil.convertModVersion(ModUtil.getModVersion(e.getKey())));
final OfferedModInfo serverInfo = e.getValue();
ModInfo nfo = ModUtil.getModInfo(e.getKey());
final boolean clientOnly = nfo != null && nfo.metadata.getEnvironment() == ModEnvironment.CLIENT;
final boolean requestMod = !clientOnly && !serverInfo.version.equals(localVersion) && serverInfo.size > 0 && serverInfo.canDownload;
BCLib.LOGGER.info(" - " + e.getKey() + " (client=" + localVersion + ", server=" + serverInfo.version + ", size=" + PathUtil.humanReadableFileSize(
serverInfo.size) + (requestMod ? ", requesting" : "") + (serverInfo.canDownload
? ""
: ", not offered") + (clientOnly ? ", client only" : "") + ")");
if (requestMod) {
filesToRequest.add(new AutoSyncID.ForModFileRequest(e.getKey(), serverInfo.version));
}
if (!serverInfo.version.equals(localVersion)) {
mismatchingMods.add(e.getKey());
}
}
mismatchingMods.addAll(ModListScreen.localMissing(modVersion));
mismatchingMods.addAll(ModListScreen.serverMissing(modVersion));
}
@Override
protected boolean isBlocking() {
return true;
}
@Environment(EnvType.CLIENT)
@Override
protected void runOnClientGameThread(Minecraft client) {
if (!Configs.CLIENT_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the client.");
return;
}
final String localBclibVersion = getBCLibVersion();
BCLib.LOGGER.info("Received Hello from Server. (client=" + localBclibVersion + ", server=" + bclibVersion + ")");
if (ModUtil.convertModVersion(localBclibVersion) != ModUtil.convertModVersion(bclibVersion)) {
showBCLibError(client);
return;
}
final List<AutoSyncID> filesToRequest = new ArrayList<>(2);
final List<AutoSyncID.ForDirectFileRequest> filesToRemove = new ArrayList<>(2);
final Set<String> mismatchingMods = new HashSet<>(2);
processModFileSync(filesToRequest, mismatchingMods);
processSingleFileSync(filesToRequest);
processAutoSyncFolder(filesToRequest, filesToRemove);
//Handle folder sync
//Both client and server need to know about the folder you want to sync
//Files can only get placed within that folder
if ((filesToRequest.size() > 0 || filesToRemove.size() > 0) && (Configs.CLIENT_CONFIG.isAcceptingMods() || Configs.CLIENT_CONFIG.isAcceptingConfigs() || Configs.CLIENT_CONFIG.isAcceptingFiles())) {
showSyncFilesScreen(client, filesToRequest, filesToRemove);
return;
} else if (serverPublishedModInfo && mismatchingMods.size() > 0 && Configs.CLIENT_CONFIG.isShowingModInfo()) {
client.setScreen(new ModListScreen(client.screen,
Component.translatable("title.bclib.modmissmatch"),
Component.translatable("message.bclib.modmissmatch"),
CommonComponents.GUI_PROCEED,
ModUtil.getMods(),
modVersion));
return;
}
}
@Environment(EnvType.CLIENT)
protected void showBCLibError(Minecraft client) {
BCLib.LOGGER.error("BCLib differs on client and server.");
client.setScreen(new WarnBCLibVersionMismatch((download) -> {
if (download) {
requestBCLibDownload();
this.onCloseSyncFilesScreen();
} else {
Minecraft.getInstance()
.setScreen(null);
}
}));
}
@Environment(EnvType.CLIENT)
protected void showSyncFilesScreen(Minecraft client,
List<AutoSyncID> files,
final List<AutoSyncID.ForDirectFileRequest> filesToRemove) {
int configFiles = 0;
int singleFiles = 0;
int folderFiles = 0;
int modFiles = 0;
for (AutoSyncID aid : files) {
if (aid.isConfigFile()) {
configFiles++;
} else if (aid instanceof AutoSyncID.ForModFileRequest) {
modFiles++;
} else if (aid instanceof AutoSyncID.ForDirectFileRequest) {
folderFiles++;
} else {
singleFiles++;
}
}
client.setScreen(new SyncFilesScreen(modFiles,
configFiles,
singleFiles,
folderFiles,
filesToRemove.size(),
modVersion,
(downloadMods, downloadConfigs, downloadFiles, removeFiles) -> {
if (downloadMods || downloadConfigs || downloadFiles) {
BCLib.LOGGER.info("Updating local Files:");
List<AutoSyncID.WithContentOverride> localChanges = new ArrayList<>(
files.toArray().length);
List<AutoSyncID> requestFiles = new ArrayList<>(files.toArray().length);
files.forEach(aid -> {
if (aid.isConfigFile() && downloadConfigs) {
processOfferedFile(requestFiles, aid);
} else if (aid instanceof AutoSyncID.ForModFileRequest && downloadMods) {
processOfferedFile(requestFiles, aid);
} else if (downloadFiles) {
processOfferedFile(requestFiles, aid);
}
});
requestFileDownloads(requestFiles);
}
if (removeFiles) {
filesToRemove.forEach(aid -> {
BCLib.LOGGER.info(" - " + aid.relFile + " (removing)");
aid.relFile.delete();
});
}
this.onCloseSyncFilesScreen();
}));
}
@Environment(EnvType.CLIENT)
private void onCloseSyncFilesScreen() {
Minecraft.getInstance()
.setScreen(ChunkerProgress.getProgressScreen());
}
private void processOfferedFile(List<AutoSyncID> requestFiles, AutoSyncID aid) {
if (aid instanceof AutoSyncID.WithContentOverride) {
final AutoSyncID.WithContentOverride aidc = (AutoSyncID.WithContentOverride) aid;
BCLib.LOGGER.info(" - " + aid + " (updating Content)");
SendFiles.writeSyncedFile(aid, aidc.contentWrapper.getRawContent(), aidc.localFile);
} else {
requestFiles.add(aid);
BCLib.LOGGER.info(" - " + aid + " (requesting)");
}
}
private void requestBCLibDownload() {
BCLib.LOGGER.warning("Starting download of BCLib");
requestFileDownloads(List.of(new AutoSyncID.ForModFileRequest(BCLib.MOD_ID, bclibVersion)));
}
@Environment(EnvType.CLIENT)
private void requestFileDownloads(List<AutoSyncID> files) {
BCLib.LOGGER.info("Starting download of Files:" + files.size());
final ProgressScreen progress = new ProgressScreen(null,
Component.translatable("title.bclib.filesync.progress"),
Component.translatable("message.bclib.filesync.progress"));
progress.progressStart(Component.translatable("message.bclib.filesync.progress.stage.empty"));
ChunkerProgress.setProgressScreen(progress);
DataExchangeAPI.send(new RequestFiles(files));
}
}

View file

@ -0,0 +1,115 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.player.Player;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataExchangeAPI;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.util.ModUtil;
import java.io.File;
/**
* This message is sent once a player enters the world. It initiates a sequence of Messages that will sync files between both
* client and server.
* <table>
* <caption>Description</caption>
* <tr>
* <th>Server</th>
* <th></th>
* <th>Client</th>
* <th></th>
* </tr>
* <tr>
* <td colspan="4">Player enters World</td>
* </tr>
* <tr>
* <td></td>
* <td>&lt;--</td>
* <td>{@link HelloServer}</td>
* <td>Sends the current BLib-Version installed on the Client</td>
* </tr>
* <tr>
* <td>{@link HelloClient}</td>
* <td>--&gt;</td>
* <td></td>
* <td>Sends the current BClIb-Version, the Version of all Plugins and data for all AutpoSync-Files
* ({@link DataExchangeAPI#addAutoSyncFile(String, File)} on the Server</td>
* </tr>
* <tr>
* <td></td>
* <td>&lt;--</td>
* <td>{@link RequestFiles}</td>
* <td>Request missing or out of sync Files from the Server</td>
* </tr>
* <tr>
* <td>{@link SendFiles}</td>
* <td>--&gt;</td>
* <td></td>
* <td>Send Files from the Server to the Client</td>
* </tr>
* </table>
*/
public class HelloServer extends DataHandler.FromClient {
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"hello_server"),
HelloServer::new,
true,
false);
protected String bclibVersion = "0.0.0";
public HelloServer() {
super(DESCRIPTOR.IDENTIFIER);
}
@Environment(EnvType.CLIENT)
@Override
protected boolean prepareDataOnClient() {
if (!Configs.CLIENT_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the client.");
return false;
}
return true;
}
@Environment(EnvType.CLIENT)
@Override
protected void serializeDataOnClient(FriendlyByteBuf buf) {
BCLib.LOGGER.info("Sending hello to server.");
buf.writeInt(ModUtil.convertModVersion(HelloClient.getBCLibVersion()));
}
@Override
protected void deserializeIncomingDataOnServer(FriendlyByteBuf buf, Player player, PacketSender responseSender) {
bclibVersion = ModUtil.convertModVersion(buf.readInt());
}
@Override
protected void runOnServerGameThread(MinecraftServer server, Player player) {
if (!Configs.SERVER_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the server.");
return;
}
String localBclibVersion = HelloClient.getBCLibVersion();
BCLib.LOGGER.info("Received Hello from Client. (server=" + localBclibVersion + ", client=" + bclibVersion + ")");
if (!server.isPublished()) {
BCLib.LOGGER.info("Auto-Sync is disabled for Singleplayer worlds.");
return;
}
reply(new HelloClient(), server);
}
}

View file

@ -0,0 +1,105 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.player.Player;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.config.Configs;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class RequestFiles extends DataHandler.FromClient {
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"request_files"),
RequestFiles::new,
false,
false);
static String currentToken = "";
protected List<AutoSyncID> files;
private RequestFiles() {
this(null);
}
public RequestFiles(List<AutoSyncID> files) {
super(DESCRIPTOR.IDENTIFIER);
this.files = files;
}
@Environment(EnvType.CLIENT)
@Override
protected boolean prepareDataOnClient() {
if (!Configs.CLIENT_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the client.");
return false;
}
return true;
}
@Environment(EnvType.CLIENT)
@Override
protected void serializeDataOnClient(FriendlyByteBuf buf) {
newToken();
writeString(buf, currentToken);
buf.writeInt(files.size());
for (AutoSyncID a : files) {
a.serializeData(buf);
}
}
String receivedToken = "";
@Override
protected void deserializeIncomingDataOnServer(FriendlyByteBuf buf, Player player, PacketSender responseSender) {
receivedToken = readString(buf);
int size = buf.readInt();
files = new ArrayList<>(size);
BCLib.LOGGER.info("Client requested " + size + " Files:");
for (int i = 0; i < size; i++) {
AutoSyncID asid = AutoSyncID.deserializeData(buf);
files.add(asid);
BCLib.LOGGER.info(" - " + asid);
}
}
@Override
protected void runOnServerGameThread(MinecraftServer server, Player player) {
if (!Configs.SERVER_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the server.");
return;
}
List<AutoFileSyncEntry> syncEntries = files.stream()
.map(asid -> AutoFileSyncEntry.findMatching(asid))
.filter(e -> e != null)
.collect(Collectors.toList());
reply(new SendFiles(syncEntries, receivedToken), server);
}
public static void newToken() {
currentToken = UUID.randomUUID()
.toString();
}
static {
newToken();
}
}

View file

@ -0,0 +1,219 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.DataHandlerDescriptor;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.gui.screens.ConfirmRestartScreen;
import org.betterx.bclib.util.Pair;
import org.betterx.bclib.util.PathUtil;
import org.betterx.bclib.util.Triple;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SendFiles extends DataHandler.FromServer {
public static final DataHandlerDescriptor DESCRIPTOR = new DataHandlerDescriptor(new ResourceLocation(BCLib.MOD_ID,
"send_files"),
SendFiles::new,
false,
false);
protected List<AutoFileSyncEntry> files;
private String token;
public SendFiles() {
this(null, "");
}
public SendFiles(List<AutoFileSyncEntry> files, String token) {
super(DESCRIPTOR.IDENTIFIER);
this.files = files;
this.token = token;
}
@Override
protected boolean prepareDataOnServer() {
if (!Configs.SERVER_CONFIG.isAllowingAutoSync()) {
BCLib.LOGGER.info("Auto-Sync was disabled on the server.");
return false;
}
return true;
}
@Override
protected void serializeDataOnServer(FriendlyByteBuf buf) {
List<AutoFileSyncEntry> existingFiles = files.stream()
.filter(e -> e != null && e.fileName != null && e.fileName.exists())
.collect(Collectors.toList());
/*
//this will try to send a file that was not registered or requested by the client
existingFiles.add(new AutoFileSyncEntry("none", new File("D:\\MinecraftPlugins\\BetterNether\\run\\server.properties"),true,(a, b, content) -> {
System.out.println("Got Content:" + content.length);
return true;
}));*/
/*//this will try to send a folder-file that was not registered or requested by the client
existingFiles.add(new AutoFileSyncEntry.ForDirectFileRequest(DataExchange.SYNC_FOLDER.folderID, new File("test.json"), DataExchange.SYNC_FOLDER.mapAbsolute("test.json").toFile()));*/
/*//this will try to send a folder-file that was not registered or requested by the client and is outside the base-folder
existingFiles.add(new AutoFileSyncEntry.ForDirectFileRequest(DataExchange.SYNC_FOLDER.folderID, new File("../breakout.json"), DataExchange.SYNC_FOLDER.mapAbsolute("../breakout.json").toFile()));*/
writeString(buf, token);
buf.writeInt(existingFiles.size());
BCLib.LOGGER.info("Sending " + existingFiles.size() + " Files to Client:");
for (AutoFileSyncEntry entry : existingFiles) {
int length = entry.serializeContent(buf);
BCLib.LOGGER.info(" - " + entry + " (" + PathUtil.humanReadableFileSize(length) + ")");
}
}
private List<Pair<AutoFileSyncEntry, byte[]>> receivedFiles;
@Environment(EnvType.CLIENT)
@Override
protected void deserializeIncomingDataOnClient(FriendlyByteBuf buf, PacketSender responseSender) {
if (Configs.CLIENT_CONFIG.isAcceptingConfigs() || Configs.CLIENT_CONFIG.isAcceptingFiles() || Configs.CLIENT_CONFIG.isAcceptingMods()) {
token = readString(buf);
if (!token.equals(RequestFiles.currentToken)) {
RequestFiles.newToken();
BCLib.LOGGER.error("Unrequested File Transfer!");
receivedFiles = new ArrayList<>(0);
return;
}
RequestFiles.newToken();
int size = buf.readInt();
receivedFiles = new ArrayList<>(size);
BCLib.LOGGER.info("Server sent " + size + " Files:");
for (int i = 0; i < size; i++) {
Triple<AutoFileSyncEntry, byte[], AutoSyncID> p = AutoFileSyncEntry.deserializeContent(buf);
if (p.first != null) {
final String type;
if (p.first.isConfigFile() && Configs.CLIENT_CONFIG.isAcceptingConfigs()) {
receivedFiles.add(p);
type = "Accepted Config ";
} else if (p.first instanceof AutoFileSyncEntry.ForModFileRequest && Configs.CLIENT_CONFIG.isAcceptingMods()) {
receivedFiles.add(p);
type = "Accepted Mod ";
} else if (Configs.CLIENT_CONFIG.isAcceptingFiles()) {
receivedFiles.add(p);
type = "Accepted File ";
} else {
type = "Ignoring ";
}
BCLib.LOGGER.info(" - " + type + p.first + " (" + PathUtil.humanReadableFileSize(p.second.length) + ")");
} else {
BCLib.LOGGER.error(" - Failed to receive File " + p.third + ", possibly sent from a Mod that is not installed on the client.");
}
}
}
}
@Environment(EnvType.CLIENT)
@Override
protected void runOnClientGameThread(Minecraft client) {
if (Configs.CLIENT_CONFIG.isAcceptingConfigs() || Configs.CLIENT_CONFIG.isAcceptingFiles() || Configs.CLIENT_CONFIG.isAcceptingMods()) {
BCLib.LOGGER.info("Writing Files:");
for (Pair<AutoFileSyncEntry, byte[]> entry : receivedFiles) {
final AutoFileSyncEntry e = entry.first;
final byte[] data = entry.second;
writeSyncedFile(e, data, e.fileName);
}
showConfirmRestart(client);
}
}
@Environment(EnvType.CLIENT)
static void writeSyncedFile(AutoSyncID e, byte[] data, File fileName) {
if (fileName != null && !PathUtil.isChildOf(PathUtil.GAME_FOLDER, fileName.toPath())) {
BCLib.LOGGER.error(fileName + " is not within game folder " + PathUtil.GAME_FOLDER);
return;
}
if (!PathUtil.MOD_BAK_FOLDER.toFile().exists()) {
PathUtil.MOD_BAK_FOLDER.toFile().mkdirs();
}
Path path = fileName != null ? fileName.toPath() : null;
Path removeAfter = null;
if (e instanceof AutoFileSyncEntry.ForModFileRequest mase) {
removeAfter = path;
int count = 0;
final String prefix = "_bclib_synced_";
String name = prefix + mase.modID + "_" + mase.version.replace(".", "_") + ".jar";
do {
if (path != null) {
//move to the same directory as the existing Mod
path = path.getParent()
.resolve(name);
} else {
//move to the default mode location
path = PathUtil.MOD_FOLDER.resolve(name);
}
count++;
name = prefix + mase.modID + "_" + mase.version.replace(".", "_") + "__" + String.format("%03d",
count) + ".jar";
} while (path.toFile().exists());
}
BCLib.LOGGER.info(" - Writing " + path + " (" + PathUtil.humanReadableFileSize(data.length) + ")");
try {
final File parentFile = path.getParent()
.toFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
Files.write(path, data);
if (removeAfter != null) {
final String bakFileName = removeAfter.toFile().getName();
String collisionFreeName = bakFileName;
Path targetPath;
int count = 0;
do {
targetPath = PathUtil.MOD_BAK_FOLDER.resolve(collisionFreeName);
count++;
collisionFreeName = String.format("%03d", count) + "_" + bakFileName;
} while (targetPath.toFile().exists());
BCLib.LOGGER.info(" - Moving " + removeAfter + " to " + targetPath);
removeAfter.toFile().renameTo(targetPath.toFile());
}
AutoSync.didReceiveFile(e, fileName);
} catch (IOException ioException) {
BCLib.LOGGER.error(" --> Writing " + fileName + " failed: " + ioException);
}
}
@Environment(EnvType.CLIENT)
protected void showConfirmRestart(Minecraft client) {
client.setScreen(new ConfirmRestartScreen(() -> {
Minecraft.getInstance()
.setScreen(null);
client.stop();
}));
}
}

View file

@ -0,0 +1,214 @@
package org.betterx.bclib.api.dataexchange.handler.autosync;
import net.minecraft.network.FriendlyByteBuf;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.dataexchange.DataHandler;
import org.betterx.bclib.api.dataexchange.FileHash;
import org.betterx.bclib.api.dataexchange.handler.autosync.AutoSyncID.ForDirectFileRequest;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.util.PathUtil;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.jetbrains.annotations.NotNull;
public class SyncFolderDescriptor {
static class SubFile {
public final String relPath;
public final FileHash hash;
SubFile(String relPath, FileHash hash) {
this.relPath = relPath;
this.hash = hash;
}
@Override
public String toString() {
return relPath;
}
public void serialize(FriendlyByteBuf buf) {
DataHandler.writeString(buf, relPath);
hash.serialize(buf);
}
public static SubFile deserialize(FriendlyByteBuf buf) {
final String relPath = DataHandler.readString(buf);
FileHash hash = FileHash.deserialize(buf);
return new SubFile(relPath, hash);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof String) return relPath.equals(o);
if (!(o instanceof SubFile)) return false;
SubFile subFile = (SubFile) o;
return relPath.equals(subFile.relPath);
}
@Override
public int hashCode() {
return relPath.hashCode();
}
}
@NotNull
public final String folderID;
public final boolean removeAdditionalFiles;
@NotNull
public final Path localFolder;
private List<SubFile> fileCache;
public SyncFolderDescriptor(String folderID, Path localFolder, boolean removeAdditionalFiles) {
this.removeAdditionalFiles = removeAdditionalFiles;
this.folderID = folderID;
this.localFolder = localFolder;
fileCache = null;
}
@Override
public String toString() {
return "SyncFolderDescriptor{" + "folderID='" + folderID + '\'' + ", removeAdditionalFiles=" + removeAdditionalFiles + ", localFolder=" + localFolder + ", files=" + (
fileCache == null
? "?"
: fileCache.size()) + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof String) {
return folderID.equals(o);
}
if (o instanceof ForDirectFileRequest) {
return folderID.equals(((ForDirectFileRequest) o).uniqueID);
}
if (!(o instanceof SyncFolderDescriptor)) return false;
SyncFolderDescriptor that = (SyncFolderDescriptor) o;
return folderID.equals(that.folderID);
}
@Override
public int hashCode() {
return folderID.hashCode();
}
public int fileCount() {
return fileCache == null ? 0 : fileCache.size();
}
public void invalidateCache() {
fileCache = null;
}
public void loadCache() {
if (fileCache == null) {
fileCache = new ArrayList<>(8);
PathUtil.fileWalker(localFolder.toFile(), p -> fileCache.add(new SubFile(localFolder.relativize(p)
.toString(),
FileHash.create(p.toFile()))));
/*//this tests if we can trick the system to load files that are not beneath the base-folder
if (!BCLib.isClient()) {
fileCache.add(new SubFile("../breakout.json", FileHash.create(mapAbsolute("../breakout.json").toFile())));
}*/
}
}
public void serialize(FriendlyByteBuf buf) {
final boolean debugHashes = Configs.CLIENT_CONFIG.getBoolean(AutoSync.SYNC_CATEGORY, "debugHashes", false);
loadCache();
DataHandler.writeString(buf, folderID);
buf.writeBoolean(removeAdditionalFiles);
buf.writeInt(fileCache.size());
fileCache.forEach(fl -> {
BCLib.LOGGER.info(" - " + fl.relPath);
if (debugHashes) {
BCLib.LOGGER.info(" " + fl.hash);
}
fl.serialize(buf);
});
}
public static SyncFolderDescriptor deserialize(FriendlyByteBuf buf) {
final String folderID = DataHandler.readString(buf);
final boolean remAddFiles = buf.readBoolean();
final int count = buf.readInt();
SyncFolderDescriptor localDescriptor = AutoSync.getSyncFolderDescriptor(folderID);
final SyncFolderDescriptor desc;
if (localDescriptor != null) {
desc = new SyncFolderDescriptor(folderID,
localDescriptor.localFolder,
localDescriptor.removeAdditionalFiles && remAddFiles);
desc.fileCache = new ArrayList<>(count);
} else {
BCLib.LOGGER.warning(BCLib.isClient()
? "Client"
: "Server" + " does not know Sync-Folder ID '" + folderID + "'");
desc = null;
}
for (int i = 0; i < count; i++) {
SubFile relPath = SubFile.deserialize(buf);
if (desc != null) desc.fileCache.add(relPath);
}
return desc;
}
//Note: make sure loadCache was called before using this
boolean hasRelativeFile(String relFile) {
return fileCache.stream()
.filter(sf -> sf.equals(relFile))
.findFirst()
.isPresent();
}
//Note: make sure loadCache was called before using this
boolean hasRelativeFile(SubFile subFile) {
return hasRelativeFile(subFile.relPath);
}
//Note: make sure loadCache was called before using this
SubFile getLocalSubFile(String relPath) {
return fileCache.stream()
.filter(sf -> sf.relPath.equals(relPath))
.findFirst()
.orElse(null);
}
Stream<SubFile> relativeFilesStream() {
loadCache();
return fileCache.stream();
}
public Path mapAbsolute(String relPath) {
return this.localFolder.resolve(relPath)
.normalize();
}
public Path mapAbsolute(SubFile subFile) {
return this.localFolder.resolve(subFile.relPath)
.normalize();
}
public boolean acceptChildElements(Path absPath) {
return PathUtil.isChildOf(this.localFolder, absPath);
}
public boolean acceptChildElements(SubFile subFile) {
return acceptChildElements(mapAbsolute(subFile));
}
public boolean discardChildElements(SubFile subFile) {
return !acceptChildElements(subFile);
}
}

View file

@ -0,0 +1,632 @@
package org.betterx.bclib.api.datafixer;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.worldselection.EditWorldScreen;
import net.minecraft.nbt.*;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.storage.RegionFile;
import net.minecraft.world.level.storage.LevelResource;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.WorldDataAPI;
import org.betterx.bclib.config.Configs;
import org.betterx.bclib.gui.screens.AtomicProgressListener;
import org.betterx.bclib.gui.screens.ConfirmFixScreen;
import org.betterx.bclib.gui.screens.LevelFixErrorScreen;
import org.betterx.bclib.gui.screens.LevelFixErrorScreen.Listener;
import org.betterx.bclib.gui.screens.ProgressScreen;
import org.betterx.bclib.util.Logger;
import java.io.*;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.zip.ZipException;
import org.jetbrains.annotations.NotNull;
/**
* API to manage Patches that need to get applied to a world
*/
public class DataFixerAPI {
static final Logger LOGGER = new Logger("DataFixerAPI");
static class State {
public boolean didFail = false;
protected ArrayList<String> errors = new ArrayList<>();
public void addError(String s) {
errors.add(s);
}
public boolean hasError() {
return errors.size() > 0;
}
public String getErrorMessage() {
return errors.stream().reduce("", (a, b) -> a + " - " + b + "\n");
}
public String[] getErrorMessages() {
String[] res = new String[errors.size()];
return errors.toArray(res);
}
}
@FunctionalInterface
public interface Callback {
void call();
}
private static boolean wrapCall(LevelStorageSource levelSource,
String levelID,
Function<LevelStorageAccess, Boolean> runWithLevel) {
LevelStorageSource.LevelStorageAccess levelStorageAccess;
try {
levelStorageAccess = levelSource.createAccess(levelID);
} catch (IOException e) {
BCLib.LOGGER.warning("Failed to read level {} data", levelID, e);
SystemToast.onWorldAccessFailure(Minecraft.getInstance(), levelID);
Minecraft.getInstance().setScreen(null);
return true;
}
boolean returnValue = runWithLevel.apply(levelStorageAccess);
try {
levelStorageAccess.close();
} catch (IOException e) {
BCLib.LOGGER.warning("Failed to unlock access to level {}", levelID, e);
}
return returnValue;
}
/**
* Will apply necessary Patches to the world.
*
* @param levelSource The SourceStorage for this Minecraft instance, You can get this using
* {@code Minecraft.getInstance().getLevelSource()}
* @param levelID The ID of the Level you want to patch
* @param showUI {@code true}, if you want to present the user with a Screen that offers to backup the world
* before applying the patches
* @param onResume When this method retursn {@code true}, this function will be called when the world is ready
* @return {@code true} if the UI was displayed. The UI is only displayed if {@code showUI} was {@code true} and
* patches were enabled in the config and the Guardian did find any patches that need to be applied to the world.
*/
public static boolean fixData(LevelStorageSource levelSource,
String levelID,
boolean showUI,
Consumer<Boolean> onResume) {
return wrapCall(levelSource, levelID, (levelStorageAccess) -> fixData(levelStorageAccess, showUI, onResume));
}
/**
* Will apply necessary Patches to the world.
*
* @param levelStorageAccess The access class of the level you want to patch
* @param showUI {@code true}, if you want to present the user with a Screen that offers to backup the world
* before applying the patches
* @param onResume When this method retursn {@code true}, this function will be called when the world is ready
* @return {@code true} if the UI was displayed. The UI is only displayed if {@code showUI} was {@code true} and
* patches were enabled in the config and the Guardian did find any patches that need to be applied to the world.
*/
public static boolean fixData(LevelStorageSource.LevelStorageAccess levelStorageAccess,
boolean showUI,
Consumer<Boolean> onResume) {
File levelPath = levelStorageAccess.getLevelPath(LevelResource.ROOT).toFile();
File levelDat = levelStorageAccess.getLevelPath(LevelResource.LEVEL_DATA_FILE).toFile();
boolean newWorld = false;
if (!levelDat.exists()) {
BCLib.LOGGER.info("Creating a new World, no fixes needed");
newWorld = true;
}
initializeWorldData(levelPath, newWorld);
if (newWorld) return false;
return fixData(levelPath, levelStorageAccess.getLevelId(), showUI, onResume);
}
/**
* Initializes the DataStorage for this world. If the world is new, the patch registry is initialized to the
* current versions of the plugins.
* <p>
* This implementation will create a new {@link LevelStorageAccess} and call {@link #initializeWorldData(File, boolean)}
* using the provided root path.
*
* @param levelSource The SourceStorage for this Minecraft instance, You can get this using
* {@code Minecraft.getInstance().getLevelSource()}
* @param levelID The ID of the Level you want to patch
* @param newWorld {@code true} if this is a fresh world
*/
public static void initializeWorldData(LevelStorageSource levelSource, String levelID, boolean newWorld) {
wrapCall(levelSource, levelID, (levelStorageAccess) -> {
initializeWorldData(levelStorageAccess, newWorld);
return true;
});
}
/**
* Initializes the DataStorage for this world. If the world is new, the patch registry is initialized to the
* current versions of the plugins.
*
* @param access levelAccess for the worldd
* @param newWorld {@code true} if this is a fresh world
*/
public static void initializeWorldData(LevelStorageAccess access, boolean newWorld) {
initializeWorldData(access.getLevelPath(LevelResource.ROOT).toFile(), newWorld);
}
/**
* Initializes the DataStorage for this world. If the world is new, the patch registry is initialized to the
* current versions of the plugins.
*
* @param levelBaseDir Folder of the world
* @param newWorld {@code true} if this is a fresh world
*/
public static void initializeWorldData(File levelBaseDir, boolean newWorld) {
WorldDataAPI.load(new File(levelBaseDir, "data"));
if (newWorld) {
getMigrationProfile().markApplied();
WorldDataAPI.saveFile(BCLib.MOD_ID);
}
}
@Environment(EnvType.CLIENT)
private static AtomicProgressListener showProgressScreen() {
ProgressScreen ps = new ProgressScreen(Minecraft.getInstance().screen,
Component.translatable("title.bclib.datafixer.progress"),
Component.translatable("message.bclib.datafixer.progress"));
Minecraft.getInstance().setScreen(ps);
return ps;
}
private static boolean fixData(File dir, String levelID, boolean showUI, Consumer<Boolean> onResume) {
MigrationProfile profile = loadProfileIfNeeded(dir);
BiConsumer<Boolean, Boolean> runFixes = (createBackup, applyFixes) -> {
final AtomicProgressListener progress;
if (applyFixes) {
if (showUI) {
progress = showProgressScreen();
} else {
progress = new AtomicProgressListener() {
private long timeStamp = Util.getMillis();
private AtomicInteger counter = new AtomicInteger(0);
@Override
public void incAtomic(int maxProgress) {
int percentage = (100 * counter.incrementAndGet()) / maxProgress;
if (Util.getMillis() - this.timeStamp >= 1000L) {
this.timeStamp = Util.getMillis();
BCLib.LOGGER.info("Patching... {}%", percentage);
}
}
@Override
public void resetAtomic() {
counter = new AtomicInteger(0);
}
public void stop() {
}
public void progressStage(Component component) {
BCLib.LOGGER.info("Patcher Stage... {}%", component.getString());
}
};
}
} else {
progress = null;
}
Supplier<State> runner = () -> {
if (createBackup) {
progress.progressStage(Component.translatable("message.bclib.datafixer.progress.waitbackup"));
EditWorldScreen.makeBackupAndShowToast(Minecraft.getInstance().getLevelSource(), levelID);
}
if (applyFixes) {
return runDataFixes(dir, profile, progress);
}
return new State();
};
if (showUI) {
Thread fixerThread = new Thread(() -> {
final State state = runner.get();
Minecraft.getInstance()
.execute(() -> {
if (profile != null && showUI) {
//something went wrong, show the user our error
if (state.didFail || state.hasError()) {
showLevelFixErrorScreen(state, (markFixed) -> {
if (markFixed) {
profile.markApplied();
}
onResume.accept(applyFixes);
});
} else {
onResume.accept(applyFixes);
}
}
});
});
fixerThread.start();
} else {
State state = runner.get();
if (state.hasError()) {
LOGGER.error("There were Errors while fixing the Level:");
LOGGER.error(state.getErrorMessage());
}
}
};
//we have some migrations
if (profile != null) {
//display the confirm UI.
if (showUI) {
showBackupWarning(levelID, runFixes);
return true;
} else {
BCLib.LOGGER.warning("Applying Fixes on Level");
runFixes.accept(false, true);
}
}
return false;
}
@Environment(EnvType.CLIENT)
private static void showLevelFixErrorScreen(State state, Listener onContinue) {
Minecraft.getInstance()
.setScreen(new LevelFixErrorScreen(Minecraft.getInstance().screen,
state.getErrorMessages(),
onContinue));
}
private static MigrationProfile loadProfileIfNeeded(File levelBaseDir) {
if (!Configs.MAIN_CONFIG.applyPatches()) {
LOGGER.info("World Patches are disabled");
return null;
}
MigrationProfile profile = getMigrationProfile();
profile.runPrePatches(levelBaseDir);
if (!profile.hasAnyFixes()) {
LOGGER.info("Everything up to date");
return null;
}
return profile;
}
@NotNull
private static MigrationProfile getMigrationProfile() {
final CompoundTag patchConfig = WorldDataAPI.getCompoundTag(BCLib.MOD_ID, Configs.MAIN_PATCH_CATEGORY);
MigrationProfile profile = Patch.createMigrationData(patchConfig);
return profile;
}
@Environment(EnvType.CLIENT)
static void showBackupWarning(String levelID, BiConsumer<Boolean, Boolean> whenFinished) {
Minecraft.getInstance().setScreen(new ConfirmFixScreen(null, whenFinished::accept));
}
private static State runDataFixes(File dir, MigrationProfile profile, AtomicProgressListener progress) {
State state = new State();
progress.resetAtomic();
progress.progressStage(Component.translatable("message.bclib.datafixer.progress.reading"));
List<File> players = getAllPlayers(dir);
List<File> regions = getAllRegions(dir, null);
final int maxProgress = players.size() + regions.size() + 4;
progress.incAtomic(maxProgress);
progress.progressStage(Component.translatable("message.bclib.datafixer.progress.players"));
players.parallelStream().forEach((file) -> {
fixPlayer(profile, state, file);
progress.incAtomic(maxProgress);
});
progress.progressStage(Component.translatable("message.bclib.datafixer.progress.level"));
fixLevel(profile, state, dir);
progress.incAtomic(maxProgress);
progress.progressStage(Component.translatable("message.bclib.datafixer.progress.worlddata"));
try {
profile.patchWorldData();
} catch (PatchDidiFailException e) {
state.didFail = true;
state.addError("Failed fixing worldconfig (" + e.getMessage() + ")");
BCLib.LOGGER.error(e.getMessage());
}
progress.incAtomic(maxProgress);
progress.progressStage(Component.translatable("message.bclib.datafixer.progress.regions"));
regions.parallelStream().forEach((file) -> {
fixRegion(profile, state, file);
progress.incAtomic(maxProgress);
});
if (!state.didFail) {
progress.progressStage(Component.translatable("message.bclib.datafixer.progress.saving"));
profile.markApplied();
WorldDataAPI.saveFile(BCLib.MOD_ID);
}
progress.incAtomic(maxProgress);
progress.stop();
return state;
}
private static void fixLevel(MigrationProfile profile, State state, File levelBaseDir) {
try {
LOGGER.info("Inspecting level.dat in " + levelBaseDir);
//load the level (could already contain patches applied by patchLevelDat)
CompoundTag level = profile.getLevelDat(levelBaseDir);
boolean[] changed = {profile.isLevelDatChanged()};
if (profile.getPrePatchException() != null) {
throw profile.getPrePatchException();
}
if (level.contains("Data")) {
CompoundTag dataTag = (CompoundTag) level.get("Data");
if (dataTag.contains("Player")) {
CompoundTag player = (CompoundTag) dataTag.get("Player");
fixPlayerNbt(player, changed, profile);
}
}
if (changed[0]) {
LOGGER.warning("Writing '{}'", profile.getLevelDatFile());
NbtIo.writeCompressed(level, profile.getLevelDatFile());
}
} catch (Exception e) {
BCLib.LOGGER.error("Failed fixing Level-Data.");
state.addError("Failed fixing Level-Data in level.dat (" + e.getMessage() + ")");
state.didFail = true;
e.printStackTrace();
}
}
private static void fixPlayer(MigrationProfile data, State state, File file) {
try {
LOGGER.info("Inspecting " + file);
CompoundTag player = readNbt(file);
boolean[] changed = {false};
fixPlayerNbt(player, changed, data);
if (changed[0]) {
LOGGER.warning("Writing '{}'", file);
NbtIo.writeCompressed(player, file);
}
} catch (Exception e) {
BCLib.LOGGER.error("Failed fixing Player-Data.");
state.addError("Failed fixing Player-Data in " + file.getName() + " (" + e.getMessage() + ")");
state.didFail = true;
e.printStackTrace();
}
}
private static void fixPlayerNbt(CompoundTag player, boolean[] changed, MigrationProfile data) {
//Checking Inventory
ListTag inventory = player.getList("Inventory", Tag.TAG_COMPOUND);
fixItemArrayWithID(inventory, changed, data, true);
//Checking EnderChest
ListTag enderitems = player.getList("EnderItems", Tag.TAG_COMPOUND);
fixItemArrayWithID(enderitems, changed, data, true);
//Checking ReceipBook
if (player.contains("recipeBook")) {
CompoundTag recipeBook = player.getCompound("recipeBook");
changed[0] |= fixStringIDList(recipeBook, "recipes", data);
changed[0] |= fixStringIDList(recipeBook, "toBeDisplayed", data);
}
}
static boolean fixStringIDList(CompoundTag root, String name, MigrationProfile data) {
boolean _changed = false;
if (root.contains(name)) {
ListTag items = root.getList(name, Tag.TAG_STRING);
ListTag newItems = new ListTag();
for (Tag tag : items) {
final StringTag str = (StringTag) tag;
final String replace = data.replaceStringFromIDs(str.getAsString());
if (replace != null) {
_changed = true;
newItems.add(StringTag.valueOf(replace));
} else {
newItems.add(tag);
}
}
if (_changed) {
root.put(name, newItems);
}
}
return _changed;
}
private static void fixRegion(MigrationProfile data, State state, File file) {
try {
Path path = file.toPath();
LOGGER.info("Inspecting " + path);
boolean[] changed = new boolean[1];
RegionFile region = new RegionFile(path, path.getParent(), true);
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
ChunkPos pos = new ChunkPos(x, z);
changed[0] = false;
if (region.hasChunk(pos) && !state.didFail) {
DataInputStream input = region.getChunkDataInputStream(pos);
CompoundTag root = NbtIo.read(input);
// if ((root.toString().contains("betternether:chest") || root.toString().contains("bclib:chest"))) {
// NbtIo.write(root, new File(file.toString() + "-" + x + "-" + z + ".nbt"));
// }
input.close();
//Checking TileEntities
ListTag tileEntities = root.getCompound("Level")
.getList("TileEntities", Tag.TAG_COMPOUND);
fixItemArrayWithID(tileEntities, changed, data, true);
//Checking Entities
ListTag entities = root.getList("Entities", Tag.TAG_COMPOUND);
fixItemArrayWithID(entities, changed, data, true);
//Checking Block Palette
ListTag sections = root.getCompound("Level")
.getList("Sections", Tag.TAG_COMPOUND);
sections.forEach((tag) -> {
ListTag palette = ((CompoundTag) tag).getList("Palette", Tag.TAG_COMPOUND);
palette.forEach((blockTag) -> {
CompoundTag blockTagCompound = ((CompoundTag) blockTag);
changed[0] |= data.replaceStringFromIDs(blockTagCompound, "Name");
});
try {
changed[0] |= data.patchBlockState(palette,
((CompoundTag) tag).getList("BlockStates",
Tag.TAG_LONG));
} catch (PatchDidiFailException e) {
BCLib.LOGGER.error("Failed fixing BlockState in " + pos);
state.addError("Failed fixing BlockState in " + pos + " (" + e.getMessage() + ")");
state.didFail = true;
changed[0] = false;
e.printStackTrace();
}
});
if (changed[0]) {
LOGGER.warning("Writing '{}': {}/{}", file, x, z);
// NbtIo.write(root, new File(file.toString() + "-" + x + "-" + z + "-changed.nbt"));
DataOutputStream output = region.getChunkDataOutputStream(pos);
NbtIo.write(root, output);
output.close();
}
}
}
}
region.close();
} catch (Exception e) {
BCLib.LOGGER.error("Failed fixing Region.");
state.addError("Failed fixing Region in " + file.getName() + " (" + e.getMessage() + ")");
state.didFail = true;
e.printStackTrace();
}
}
static CompoundTag patchConfTag = null;
static CompoundTag getPatchData() {
if (patchConfTag == null) {
patchConfTag = WorldDataAPI.getCompoundTag(BCLib.MOD_ID, Configs.MAIN_PATCH_CATEGORY);
}
return patchConfTag;
}
static void fixItemArrayWithID(ListTag items, boolean[] changed, MigrationProfile data, boolean recursive) {
items.forEach(inTag -> {
fixID((CompoundTag) inTag, changed, data, recursive);
});
}
static void fixID(CompoundTag inTag, boolean[] changed, MigrationProfile data, boolean recursive) {
final CompoundTag tag = inTag;
changed[0] |= data.replaceStringFromIDs(tag, "id");
if (tag.contains("Item")) {
CompoundTag item = (CompoundTag) tag.get("Item");
fixID(item, changed, data, recursive);
}
if (recursive && tag.contains("Items")) {
fixItemArrayWithID(tag.getList("Items", Tag.TAG_COMPOUND), changed, data, true);
}
if (recursive && tag.contains("Inventory")) {
ListTag inventory = tag.getList("Inventory", Tag.TAG_COMPOUND);
fixItemArrayWithID(inventory, changed, data, true);
}
if (tag.contains("tag")) {
CompoundTag entityTag = (CompoundTag) tag.get("tag");
if (entityTag.contains("BlockEntityTag")) {
CompoundTag blockEntityTag = (CompoundTag) entityTag.get("BlockEntityTag");
fixID(blockEntityTag, changed, data, recursive);
/*ListTag items = blockEntityTag.getList("Items", Tag.TAG_COMPOUND);
fixItemArrayWithID(items, changed, data, recursive);*/
}
}
}
private static List<File> getAllPlayers(File dir) {
List<File> list = new ArrayList<>();
dir = new File(dir, "playerdata");
if (!dir.exists() || !dir.isDirectory()) {
return list;
}
for (File file : dir.listFiles()) {
if (file.isFile() && file.getName().endsWith(".dat")) {
list.add(file);
}
}
return list;
}
private static List<File> getAllRegions(File dir, List<File> list) {
if (list == null) {
list = new ArrayList<>();
}
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
getAllRegions(file, list);
} else if (file.isFile() && file.getName().endsWith(".mca")) {
list.add(file);
}
}
return list;
}
/**
* register a new Patch
*
* @param patch A #Supplier that will instantiate the new Patch Object
*/
public static void registerPatch(Supplier<Patch> patch) {
Patch.getALL().add(patch.get());
}
private static CompoundTag readNbt(File file) throws IOException {
try {
return NbtIo.readCompressed(file);
} catch (ZipException | EOFException e) {
return NbtIo.read(file);
}
}
}

View file

@ -0,0 +1,57 @@
package org.betterx.bclib.api.datafixer;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import org.betterx.bclib.interfaces.PatchBiFunction;
import org.betterx.bclib.interfaces.PatchFunction;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
/**
* A Patch for level.dat that is always executed no matter what Patchlevel is set in a world.
*/
public abstract class ForcedLevelPatch extends Patch {
protected ForcedLevelPatch(@NotNull String modID, String version) {
super(modID, version, true);
}
@Override
public final Map<String, String> getIDReplacements() {
return new HashMap<String, String>();
}
@Override
public final PatchFunction<CompoundTag, Boolean> getWorldDataPatcher() {
return null;
}
@Override
public final PatchBiFunction<ListTag, ListTag, Boolean> getBlockStatePatcher() {
return null;
}
@Override
public final List<String> getWorldDataIDPaths() {
return null;
}
@Override
public PatchFunction<CompoundTag, Boolean> getLevelDatPatcher() {
return this::runLevelDatPatch;
}
/**
* Called with the contents of level.dat in {@code root}
*
* @param root The contents of level.dat
* @param profile The active migration profile
* @return true, if the run did change the contents of root
*/
abstract protected Boolean runLevelDatPatch(CompoundTag root, MigrationProfile profile);
}

View file

@ -0,0 +1,372 @@
package org.betterx.bclib.api.datafixer;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.Tag;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.WorldDataAPI;
import org.betterx.bclib.interfaces.PatchBiFunction;
import org.betterx.bclib.interfaces.PatchFunction;
import org.betterx.bclib.util.ModUtil;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
public class MigrationProfile {
final Set<String> mods;
final Map<String, String> idReplacements;
final List<PatchFunction<CompoundTag, Boolean>> levelPatchers;
final List<PatchBiFunction<ListTag, ListTag, Boolean>> statePatchers;
final List<Patch> worldDataPatchers;
final Map<String, List<String>> worldDataIDPaths;
private final CompoundTag config;
private CompoundTag level;
private File levelBaseDir;
private boolean prePatchChangedLevelDat;
private boolean didRunPrePatch;
private Exception prePatchException;
MigrationProfile(CompoundTag config, boolean applyAll) {
this.config = config;
this.mods = Collections.unmodifiableSet(Patch.getALL()
.stream()
.map(p -> p.modID)
.collect(Collectors.toSet()));
HashMap<String, String> replacements = new HashMap<String, String>();
List<PatchFunction<CompoundTag, Boolean>> levelPatches = new LinkedList<>();
List<Patch> worldDataPatches = new LinkedList<>();
List<PatchBiFunction<ListTag, ListTag, Boolean>> statePatches = new LinkedList<>();
HashMap<String, List<String>> worldDataIDPaths = new HashMap<>();
for (String modID : mods) {
Patch.getALL()
.stream()
.filter(p -> p.modID.equals(modID))
.forEach(patch -> {
List<String> paths = patch.getWorldDataIDPaths();
if (paths != null) worldDataIDPaths.put(modID, paths);
if (applyAll || currentPatchLevel(modID) < patch.level || patch.alwaysApply) {
replacements.putAll(patch.getIDReplacements());
if (patch.getLevelDatPatcher() != null)
levelPatches.add(patch.getLevelDatPatcher());
if (patch.getWorldDataPatcher() != null)
worldDataPatches.add(patch);
if (patch.getBlockStatePatcher() != null)
statePatches.add(patch.getBlockStatePatcher());
DataFixerAPI.LOGGER.info("Applying " + patch);
} else {
DataFixerAPI.LOGGER.info("Ignoring " + patch);
}
});
}
this.worldDataIDPaths = Collections.unmodifiableMap(worldDataIDPaths);
this.idReplacements = Collections.unmodifiableMap(replacements);
this.levelPatchers = Collections.unmodifiableList(levelPatches);
this.worldDataPatchers = Collections.unmodifiableList(worldDataPatches);
this.statePatchers = Collections.unmodifiableList(statePatches);
}
/**
* This method is supposed to be used by developers to apply id-patches to custom nbt structures. It is only
* available in Developer-Mode
*/
public static void fixCustomFolder(File dir) {
if (!BCLib.isDevEnvironment()) return;
MigrationProfile profile = Patch.createMigrationData();
List<File> nbts = getAllNbts(dir, null);
nbts.parallelStream().forEach((file) -> {
DataFixerAPI.LOGGER.info("Loading NBT " + file);
try {
CompoundTag root = NbtIo.readCompressed(file);
boolean[] changed = {false};
int spawnerIdx = -1;
if (root.contains("palette")) {
ListTag items = root.getList("palette", Tag.TAG_COMPOUND);
for (int idx = 0; idx < items.size(); idx++) {
final CompoundTag tag = (CompoundTag) items.get(idx);
if (tag.contains("Name") && tag.getString("Name").equals("minecraft:spawner"))
spawnerIdx = idx;
if (tag.contains("Name") && (tag.getString("Name").equals("minecraft:") || tag.getString("Name")
.equals(""))) {
System.out.println("Empty Name");
}
if (tag.contains("id") && (tag.getString("id").equals("minecraft:") || tag.getString("id")
.equals(""))) {
System.out.println("Empty ID");
}
changed[0] |= profile.replaceStringFromIDs(tag, "Name");
}
}
if (spawnerIdx >= 0 && root.contains("blocks")) {
ListTag items = root.getList("blocks", Tag.TAG_COMPOUND);
for (int idx = 0; idx < items.size(); idx++) {
final CompoundTag blockTag = (CompoundTag) items.get(idx);
if (blockTag.contains("state") && blockTag.getInt("state") == spawnerIdx && blockTag.contains(
"nbt")) {
CompoundTag nbt = blockTag.getCompound("nbt");
if (nbt.contains("SpawnData")) {
final CompoundTag entity = nbt.getCompound("SpawnData");
if (!entity.contains("entity")) {
CompoundTag data = new CompoundTag();
data.put("entity", entity);
nbt.put("SpawnData", data);
changed[0] = true;
}
}
if (nbt.contains("SpawnPotentials")) {
ListTag pots = nbt.getList("SpawnPotentials", Tag.TAG_COMPOUND);
for (Tag potItemIn : pots) {
final CompoundTag potItem = (CompoundTag) potItemIn;
if (potItem.contains("Weight")) {
int weight = potItem.getInt("Weight");
potItem.putInt("weight", weight);
potItem.remove("Weight");
changed[0] = true;
}
if (potItem.contains("Entity")) {
CompoundTag entity = potItem.getCompound("Entity");
CompoundTag data = new CompoundTag();
data.put("entity", entity);
potItem.put("data", data);
potItem.remove("Entity");
changed[0] = true;
}
}
}
}
}
}
if (changed[0]) {
DataFixerAPI.LOGGER.info("Writing NBT " + file);
NbtIo.writeCompressed(root, file);
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
private static List<File> getAllNbts(File dir, List<File> list) {
if (list == null) {
list = new ArrayList<>();
}
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
getAllNbts(file, list);
} else if (file.isFile() && file.getName().endsWith(".nbt")) {
list.add(file);
}
}
return list;
}
final public CompoundTag getLevelDat(File levelBaseDir) {
if (level == null || this.levelBaseDir == null || !this.levelBaseDir.equals(levelBaseDir)) {
runPrePatches(levelBaseDir);
}
return level;
}
final public boolean isLevelDatChanged() {
return prePatchChangedLevelDat;
}
final public File getLevelDatFile() {
return new File(levelBaseDir, "level.dat");
}
final public Exception getPrePatchException() {
return prePatchException;
}
final public void runPrePatches(File levelBaseDir) {
if (didRunPrePatch) {
BCLib.LOGGER.warning("Already did run PrePatches for " + this.levelBaseDir + ".");
}
BCLib.LOGGER.info("Running Pre Patchers on " + levelBaseDir);
this.levelBaseDir = levelBaseDir;
this.level = null;
this.prePatchException = null;
didRunPrePatch = true;
this.prePatchChangedLevelDat = runPreLevelPatches(getLevelDatFile());
}
private boolean runPreLevelPatches(File levelDat) {
try {
level = NbtIo.readCompressed(levelDat);
boolean changed = patchLevelDat(level);
return changed;
} catch (IOException | PatchDidiFailException e) {
prePatchException = e;
return false;
}
}
final public void markApplied() {
for (String modID : mods) {
DataFixerAPI.LOGGER.info("Updating Patch-Level for '{}' from {} to {}",
modID,
ModUtil.convertModVersion(currentPatchLevel(modID)),
ModUtil.convertModVersion(Patch.maxPatchLevel(modID)));
if (config != null)
config.putString(modID, Patch.maxPatchVersion(modID));
}
}
public String currentPatchVersion(@NotNull String modID) {
if (config == null || !config.contains(modID)) return "0.0.0";
return config.getString(modID);
}
public int currentPatchLevel(@NotNull String modID) {
return ModUtil.convertModVersion(currentPatchVersion(modID));
}
public boolean hasAnyFixes() {
boolean hasLevelDatPatches;
if (didRunPrePatch != false) {
hasLevelDatPatches = prePatchChangedLevelDat;
} else {
hasLevelDatPatches = levelPatchers.size() > 0;
}
return idReplacements.size() > 0 || hasLevelDatPatches || worldDataPatchers.size() > 0;
}
public String replaceStringFromIDs(@NotNull String val) {
final String replace = idReplacements.get(val);
return replace;
}
public boolean replaceStringFromIDs(@NotNull CompoundTag tag, @NotNull String key) {
if (!tag.contains(key)) return false;
final String val = tag.getString(key);
final String replace = idReplacements.get(val);
if (replace != null) {
DataFixerAPI.LOGGER.warning("Replacing ID '{}' with '{}'.", val, replace);
tag.putString(key, replace);
return true;
}
return false;
}
private boolean replaceIDatPath(@NotNull ListTag list, @NotNull String[] parts, int level) {
boolean[] changed = {false};
if (level == parts.length - 1) {
DataFixerAPI.fixItemArrayWithID(list, changed, this, true);
} else {
list.forEach(inTag -> changed[0] |= replaceIDatPath((CompoundTag) inTag, parts, level + 1));
}
return changed[0];
}
private boolean replaceIDatPath(@NotNull CompoundTag tag, @NotNull String[] parts, int level) {
boolean changed = false;
for (int i = level; i < parts.length - 1; i++) {
final String part = parts[i];
if (tag.contains(part)) {
final byte type = tag.getTagType(part);
if (type == Tag.TAG_LIST) {
ListTag list = tag.getList(part, Tag.TAG_COMPOUND);
return replaceIDatPath(list, parts, i);
} else if (type == Tag.TAG_COMPOUND) {
tag = tag.getCompound(part);
}
} else {
return false;
}
}
if (tag != null && parts.length > 0) {
final String key = parts[parts.length - 1];
final byte type = tag.getTagType(key);
if (type == Tag.TAG_LIST) {
final ListTag list = tag.getList(key, Tag.TAG_COMPOUND);
final boolean[] _changed = {false};
if (list.size() == 0) {
_changed[0] = DataFixerAPI.fixStringIDList(tag, key, this);
} else {
DataFixerAPI.fixItemArrayWithID(list, _changed, this, true);
}
return _changed[0];
} else if (type == Tag.TAG_STRING) {
return replaceStringFromIDs(tag, key);
} else if (type == Tag.TAG_COMPOUND) {
final CompoundTag cTag = tag.getCompound(key);
boolean[] _changed = {false};
DataFixerAPI.fixID(cTag, _changed, this, true);
return _changed[0];
}
}
return false;
}
public boolean replaceIDatPath(@NotNull CompoundTag root, @NotNull String path) {
String[] parts = path.split("\\.");
return replaceIDatPath(root, parts, 0);
}
public boolean patchLevelDat(@NotNull CompoundTag level) throws PatchDidiFailException {
boolean changed = false;
for (PatchFunction<CompoundTag, Boolean> f : levelPatchers) {
changed |= f.apply(level, this);
}
return changed;
}
public void patchWorldData() throws PatchDidiFailException {
for (Patch patch : worldDataPatchers) {
CompoundTag root = WorldDataAPI.getRootTag(patch.modID);
boolean changed = patch.getWorldDataPatcher().apply(root, this);
if (changed) {
WorldDataAPI.saveFile(patch.modID);
}
}
for (Map.Entry<String, List<String>> entry : worldDataIDPaths.entrySet()) {
CompoundTag root = WorldDataAPI.getRootTag(entry.getKey());
boolean[] changed = {false};
entry.getValue().forEach(path -> {
changed[0] |= replaceIDatPath(root, path);
});
if (changed[0]) {
WorldDataAPI.saveFile(entry.getKey());
}
}
}
public boolean patchBlockState(ListTag palette, ListTag states) throws PatchDidiFailException {
boolean changed = false;
for (PatchBiFunction<ListTag, ListTag, Boolean> f : statePatchers) {
changed |= f.apply(palette, states, this);
}
return changed;
}
}

View file

@ -0,0 +1,238 @@
package org.betterx.bclib.api.datafixer;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import org.betterx.bclib.api.WorldDataAPI;
import org.betterx.bclib.interfaces.PatchBiFunction;
import org.betterx.bclib.interfaces.PatchFunction;
import org.betterx.bclib.util.ModUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
public abstract class Patch {
private static final List<Patch> ALL = new ArrayList<>(10);
/**
* The Patch-Level derived from {@link #version}
*/
public final int level;
/**
* The Patch-Version string
*/
public final String version;
/**
* The Mod-ID that registered this Patch
*/
@NotNull
public final String modID;
/**
* This Mod is tested for each level start
*/
public final boolean alwaysApply;
static List<Patch> getALL() {
return ALL;
}
/**
* Returns the highest Patch-Version that is available for the given mod. If no patches were
* registerd for the mod, this will return 0.0.0
*
* @param modID The ID of the mod you want to query
* @return The highest Patch-Version that was found
*/
public static String maxPatchVersion(@NotNull String modID) {
return ALL.stream().filter(p -> p.modID.equals(modID)).map(p -> p.version).reduce((p, c) -> c).orElse("0.0.0");
}
/**
* Returns the highest patch-level that is available for the given mod. If no patches were
* registerd for the mod, this will return 0
*
* @param modID The ID of the mod you want to query
* @return The highest Patch-Level that was found
*/
public static int maxPatchLevel(@NotNull String modID) {
return ALL.stream().filter(p -> p.modID.equals(modID)).mapToInt(p -> p.level).max().orElse(0);
}
/**
* Called by inheriting classes.
* <p>
* Performs some sanity checks on the values and might throw a #RuntimeException if any
* inconsistencies are found.
*
* @param modID The ID of the Mod you want to register a patch for. This should be your
* ModID only. The ModID can not be {@code null} or an empty String.
* @param version The mod-version that introduces the patch. This needs Semantic-Version String
* like x.x.x. Developers are responsible for registering their patches in the correct
* order (with increasing versions). You are not allowed to register a new
* Patch with a version lower or equal than
* {@link Patch#maxPatchVersion(String)}
*/
protected Patch(@NotNull String modID, String version) {
this(modID, version, false);
}
/**
* Internal Constructor used to create patches that can allways run (no matter what patchlevel a level has)
*
* @param modID The ID of the Mod
* @param version The mod-version that introduces the patch. When {@Code runAllways} is set, this version will
* determine the patchlevel that is written to the level
* @param alwaysApply When true, this patch is always active, no matter the patchlevel of the world.
* This should be used sparingly and just for patches that apply to level.dat (as they only take
* effect when changes are detected). Use {@link ForcedLevelPatch} to instatiate.
*/
Patch(@NotNull String modID, String version, boolean alwaysApply) {
//Patchlevels need to be unique and registered in ascending order
if (modID == null || modID.isEmpty()) {
throw new RuntimeException("[INTERNAL ERROR] Patches need a valid modID!");
}
if (version == null || version.isEmpty()) {
throw new RuntimeException("Invalid Mod-Version");
}
this.version = version;
this.alwaysApply = alwaysApply;
this.level = ModUtil.convertModVersion(version);
if (!ALL.stream().filter(p -> p.modID.equals(modID)).noneMatch(p -> p.level >= this.level) || this.level <= 0) {
throw new RuntimeException(
"[INTERNAL ERROR] Patch-levels need to be created in ascending order beginning with 1.");
}
this.modID = modID;
}
@Override
public String toString() {
return "Patch{" + modID + ':' + version + ':' + level + '}';
}
/**
* Return block data fixes. Fixes will be applied on world load if current patch-level for
* the linked mod is lower than the {@link #level}.
* <p>
* The default implementation of this method returns an empty map.
*
* @return The returned Map should contain the replacements. All occurences of the
* {@code KeySet} are replaced with the associated value.
*/
public Map<String, String> getIDReplacements() {
return new HashMap<String, String>();
}
/**
* Return a {@link PatchFunction} that is called with the content of <i>level.dat</i>.
* <p>
* The function needs to return {@code true}, if changes were made to the data.
* If an error occurs, the method should throw a {@link PatchDidiFailException}
* <p>
* The default implementation of this method returns null.
*
* @return {@code true} if changes were applied and we need to save the data
*/
public PatchFunction<CompoundTag, Boolean> getLevelDatPatcher() {
return null;
}
/**
* Return a {@link PatchFunction} that is called with the content from the
* {@link WorldDataAPI} for this Mod.
* The function needs to return {@code true}, if changes were made to the data.
* If an error occurs, the method should throw a {@link PatchDidiFailException}
* <p>
* The default implementation of this method returns null.
*
* @return {@code true} if changes were applied and we need to save the data
*/
public PatchFunction<CompoundTag, Boolean> getWorldDataPatcher() {
return null;
}
/**
* Return a {@link PatchBiFunction} that is called with pallette and blockstate of
* each chunk in every region. This method is called AFTER all ID replacements
* from {@link #getIDReplacements()} were applied to the pallete.
* <p>
* The first parameter is the palette and the second is the blockstate.
* <p>
* The function needs to return {@code true}, if changes were made to the data.
* If an error occurs, the method should throw a {@link PatchDidiFailException}
* <p>
* The default implementation of this method returns null.
*
* @return {@code true} if changes were applied and we need to save the data
*/
public PatchBiFunction<ListTag, ListTag, Boolean> getBlockStatePatcher() {
return null;
}
/**
* Generates ready to use data for all currently registered patches. The list of
* patches is selected by the current patch-level of the world.
* <p>
* A {@link #Patch} with a given {@link #level} is only included if the patch-level of the
* world is less
*
* @param config The current patch-level configuration*
* @return a new {@link MigrationProfile} Object.
*/
static MigrationProfile createMigrationData(CompoundTag config) {
return new MigrationProfile(config, false);
}
/**
* This method is supposed to be used by developers to apply id-patches to custom nbt structures. It is only
* available in Developer-Mode
*/
static MigrationProfile createMigrationData() {
return new MigrationProfile(null, true);
}
/**
* Returns a list of paths where your mod stores IDs in your {@link WorldDataAPI}-File.
* <p>
* {@link DataFixerAPI} will use information from the latest patch that returns a non-null-result. This list is used
* to automatically fix changed IDs from all active patches (see {@link Patch#getIDReplacements()}
* <p>
* The end of the path can either be a {@link net.minecraft.nbt.StringTag}, a {@link net.minecraft.nbt.ListTag} or
* a {@link CompoundTag}. If the Path contains a non-leaf {@link net.minecraft.nbt.ListTag}, all members of that
* list will be processed. For example:
* <pre>
* - global +
* | - key (String)
* | - items (List) +
* | - { id (String) }
* | - { id (String) }
* </pre>
* The path <b>global.items.id</b> will fix all <i>id</i>-entries in the <i>items</i>-list, while the path
* <b>global.key</b> will only fix the <i>key</i>-entry.
* <p>
* if the leaf-entry (= the last part of the path, which would be <i>items</i> in <b>global.items</b>) is a
* {@link CompoundTag}, the system will fix any <i>id</i> entry. If the {@link CompoundTag} contains an <i>item</i>
* or <i>tag.BlockEntityTag</i> entry, the system will recursivley continue with those. If an <i>items</i>
* or <i>inventory</i>-{@link net.minecraft.nbt.ListTag} was found, the system will continue recursivley with
* every item of that list.
* <p>
* if the leaf-entry is a {@link net.minecraft.nbt.ListTag}, it is handle the same as a child <i>items</i> entry
* of a {@link CompoundTag}.
*
* @return {@code null} if nothing changes or a list of Paths in your {@link WorldDataAPI}-File.
* Paths are dot-seperated (see {@link WorldDataAPI#getCompoundTag(String, String)}).
*/
public List<String> getWorldDataIDPaths() {
return null;
}
}

View file

@ -0,0 +1,11 @@
package org.betterx.bclib.api.datafixer;
public class PatchDidiFailException extends Exception {
public PatchDidiFailException() {
super();
}
public PatchDidiFailException(Exception e) {
super(e);
}
}

View file

@ -0,0 +1,150 @@
package org.betterx.bclib.api.features;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.levelgen.GenerationStep.Decoration;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration;
import net.minecraft.world.level.levelgen.feature.configurations.OreConfiguration;
import net.minecraft.world.level.levelgen.placement.PlacementModifier;
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockMatchTest;
import org.betterx.bclib.world.features.BCLFeature;
public class BCLCommonFeatures {
/**
* Will create a basic plant feature.
*
* @param id {@link ResourceLocation} feature ID.
* @param feature {@link Feature} with {@link NoneFeatureConfiguration} config.
* @param density iterations per chunk.
* @return new BCLFeature instance.
*/
public static BCLFeature makeVegetationFeature(ResourceLocation id,
Feature<NoneFeatureConfiguration> feature,
int density) {
return makeVegetationFeature(id, feature, density, false);
}
/**
* Will create a basic plant feature.
*
* @param id {@link ResourceLocation} feature ID.
* @param feature {@link Feature} with {@link NoneFeatureConfiguration} config.
* @param density iterations per chunk.
* @param allHeight if {@code true} will generate plant on all layers, if {@code false} - only on surface.
* @return new BCLFeature instance.
*/
public static BCLFeature makeVegetationFeature(ResourceLocation id,
Feature<NoneFeatureConfiguration> feature,
int density,
boolean allHeight) {
if (allHeight) {
return BCLFeatureBuilder.start(id, feature).countLayers(density).squarePlacement().onlyInBiome().build();
} else {
return BCLFeatureBuilder
.start(id, feature)
.countMax(density)
.squarePlacement()
.heightmap()
.onlyInBiome()
.build();
}
}
/**
* Will create feature which will be generated once in each chunk.
*
* @param id {@link ResourceLocation} feature ID.
* @param decoration {@link Decoration} feature step.
* @param feature {@link Feature} with {@link NoneFeatureConfiguration} config.
* @return new BCLFeature instance.
*/
public static BCLFeature makeChunkFeature(ResourceLocation id,
Decoration decoration,
Feature<NoneFeatureConfiguration> feature) {
return BCLFeatureBuilder.start(id, feature).decoration(decoration).count(1).onlyInBiome().build();
}
/**
* Will create feature with chanced decoration, chance for feature to generate per chunk is 1 / chance.
*
* @param id {@link ResourceLocation} feature ID.
* @param decoration {@link Decoration} feature step.
* @param feature {@link Feature} with {@link NoneFeatureConfiguration} config.
* @param chance chance for feature to be generated in.
* @return new BCLFeature instance.
*/
public static BCLFeature makeChancedFeature(ResourceLocation id,
Decoration decoration,
Feature<NoneFeatureConfiguration> feature,
int chance) {
return BCLFeatureBuilder.start(id, feature)
.decoration(decoration)
.oncePerChunks(chance)
.squarePlacement()
.onlyInBiome()
.build();
}
/**
* Will create feature with specified generation iterations per chunk.
*
* @param id {@link ResourceLocation} feature ID.
* @param decoration {@link Decoration} feature step.
* @param feature {@link Feature} with {@link NoneFeatureConfiguration} config.
* @param count iterations steps.
* @return new BCLFeature instance.
*/
public static BCLFeature makeCountFeature(ResourceLocation id,
Decoration decoration,
Feature<NoneFeatureConfiguration> feature,
int count) {
return BCLFeatureBuilder.start(id, feature)
.decoration(decoration)
.count(count)
.squarePlacement()
.onlyInBiome()
.build();
}
/**
* Will create a basic ore feature.
*
* @param id {@link ResourceLocation} feature ID.
* @param blockOre {@link Decoration} feature step.
* @param hostBlock {@link Block} to generate feature in.
* @param veins iterations per chunk.
* @param veinSize size of ore vein.
* @param airDiscardChance chance that this orge gets discarded when it is exposed to air
* @param placement {@link net.minecraft.world.level.levelgen.placement.PlacementModifier} for the ore distribution,
* for example {@code PlacementUtils.FULL_RANGE}, {@code PlacementUtils.RANGE_10_10}
* @param rare when true, this is placed as a rare resource
* @return new BCLFeature instance.
*/
public static BCLFeature makeOreFeature(ResourceLocation id,
Block blockOre,
Block hostBlock,
int veins,
int veinSize,
float airDiscardChance,
PlacementModifier placement,
boolean rare) {
BCLFeatureBuilder builder = BCLFeatureBuilder.start(id, Feature.ORE).decoration(Decoration.UNDERGROUND_ORES);
if (rare) {
builder.oncePerChunks(veins);
} else {
builder.count(veins);
}
builder.modifier(placement).squarePlacement().onlyInBiome();
return builder.build(new OreConfiguration(
new BlockMatchTest(hostBlock),
blockOre.defaultBlockState(),
veinSize,
airDiscardChance
));
}
}

View file

@ -0,0 +1,162 @@
package org.betterx.bclib.api.features;
import net.minecraft.data.worldgen.placement.PlacementUtils;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.valueproviders.UniformInt;
import net.minecraft.world.level.levelgen.GenerationStep.Decoration;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration;
import net.minecraft.world.level.levelgen.placement.*;
import org.betterx.bclib.world.features.BCLFeature;
import java.util.ArrayList;
import java.util.List;
public class BCLFeatureBuilder<FC extends FeatureConfiguration, F extends Feature<FC>> {
private static final BCLFeatureBuilder INSTANCE = new BCLFeatureBuilder();
private final List<PlacementModifier> modifications = new ArrayList<>(16);
private ResourceLocation featureID;
private Decoration decoration;
private F feature;
private BCLFeatureBuilder() {
}
/**
* Starts a new {@link BCLFeature} builder.
*
* @param featureID {@link ResourceLocation} feature identifier.
* @param feature {@link Feature} to construct.
* @return {@link BCLFeatureBuilder} instance.
*/
public static BCLFeatureBuilder start(ResourceLocation featureID, Feature<?> feature) {
INSTANCE.decoration = Decoration.VEGETAL_DECORATION;
INSTANCE.modifications.clear();
INSTANCE.featureID = featureID;
INSTANCE.feature = feature;
return INSTANCE;
}
/**
* Set generation step for the feature. Default is {@code VEGETAL_DECORATION}.
*
* @param decoration {@link Decoration} step.
* @return same {@link BCLFeatureBuilder} instance.
*/
public BCLFeatureBuilder decoration(Decoration decoration) {
this.decoration = decoration;
return this;
}
/**
* Add feature placement modifier. Used as a condition for feature how to generate.
*
* @param modifier {@link PlacementModifier}.
* @return same {@link BCLFeatureBuilder} instance.
*/
public BCLFeatureBuilder modifier(PlacementModifier modifier) {
modifications.add(modifier);
return this;
}
public BCLFeatureBuilder modifier(List<PlacementModifier> modifiers) {
modifications.addAll(modifiers);
return this;
}
/**
* Generate feature in certain iterations (per chunk).
*
* @param count how many times feature will be generated in chunk.
* @return same {@link BCLFeatureBuilder} instance.
*/
public BCLFeatureBuilder count(int count) {
return modifier(CountPlacement.of(count));
}
/**
* Generate feature in certain iterations (per chunk). Count can be between 0 and max value.
*
* @param count maximum amount of iterations per chunk.
* @return same {@link BCLFeatureBuilder} instance.
*/
public BCLFeatureBuilder countMax(int count) {
return modifier(CountPlacement.of(UniformInt.of(0, count)));
}
/**
* Generate feature in certain iterations (per chunk).
* Feature will be generated on all layers (example - Nether plants).
*
* @param count how many times feature will be generated in chunk layers.
* @return same {@link BCLFeatureBuilder} instance.
*/
@SuppressWarnings("deprecation")
public BCLFeatureBuilder countLayers(int count) {
return modifier(CountOnEveryLayerPlacement.of(count));
}
/**
* Generate feature in certain iterations (per chunk). Count can be between 0 and max value.
* Feature will be generated on all layers (example - Nether plants).
*
* @param count maximum amount of iterations per chunk layers.
* @return same {@link BCLFeatureBuilder} instance.
*/
@SuppressWarnings("deprecation")
public BCLFeatureBuilder countLayersMax(int count) {
return modifier(CountOnEveryLayerPlacement.of(UniformInt.of(0, count)));
}
/**
* Will place feature once in certain amount of chunks (in average).
*
* @param chunks amount of chunks.
* @return same {@link BCLFeatureBuilder} instance.
*/
public BCLFeatureBuilder oncePerChunks(int chunks) {
return modifier(RarityFilter.onAverageOnceEvery(chunks));
}
/**
* Restricts feature generation only to biome where feature was added.
*
* @return same {@link BCLFeatureBuilder} instance.
*/
public BCLFeatureBuilder onlyInBiome() {
return modifier(BiomeFilter.biome());
}
// Are these two things required in 1.18.1?
// TODO - add information
public BCLFeatureBuilder squarePlacement() {
return modifier(InSquarePlacement.spread());
}
// TODO - add information
public BCLFeatureBuilder heightmap() {
return modifier(PlacementUtils.HEIGHTMAP);
}
/**
* Builds a new {@link BCLFeature} instance. Features will be registered during this process.
*
* @param configuration any {@link FeatureConfiguration} for provided {@link Feature}.
* @return created {@link BCLFeature} instance.
*/
public BCLFeature build(FC configuration) {
PlacementModifier[] modifiers = modifications.toArray(new PlacementModifier[modifications.size()]);
return new BCLFeature(featureID, feature, decoration, configuration, modifiers);
}
/**
* Builds a new {@link BCLFeature} instance with {@code NONE} {@link FeatureConfiguration}.
* Features will be registered during this process.
*
* @return created {@link BCLFeature} instance.
*/
public BCLFeature build() {
return build((FC) FeatureConfiguration.NONE);
}
}

View file

@ -0,0 +1,361 @@
package org.betterx.bclib.api.spawning;
import net.minecraft.core.BlockPos;
import net.minecraft.world.Difficulty;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.SpawnPlacements.SpawnPredicate;
import net.minecraft.world.entity.SpawnPlacements.Type;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.levelgen.Heightmap.Types;
import net.minecraft.world.phys.AABB;
import net.fabricmc.fabric.mixin.object.builder.SpawnRestrictionAccessor;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.betterx.bclib.entity.BCLEntityWrapper;
import org.betterx.bclib.interfaces.SpawnRule;
import java.util.*;
import java.util.function.Supplier;
public class SpawnRuleBuilder<M extends Mob> {
private static final Map<String, SpawnRuleEntry> RULES_CACHE = Maps.newHashMap();
private static final SpawnRuleBuilder INSTANCE = new SpawnRuleBuilder();
private final List<SpawnRuleEntry> rules = Lists.newArrayList();
private SpawnRuleEntry entryInstance;
private EntityType<M> entityType;
private SpawnRuleBuilder() {
}
/**
* Starts new rule building process.
*
* @param entityType The entity you want to build a rule for
* @return prepared {@link SpawnRuleBuilder} instance.
*/
public static SpawnRuleBuilder start(EntityType<? extends Mob> entityType) {
INSTANCE.entityType = entityType;
INSTANCE.rules.clear();
return INSTANCE;
}
/**
* Starts new rule building process.
*
* @param wrapper The entity you want to build a rule for
* @return prepared {@link SpawnRuleBuilder} instance.
*/
public static SpawnRuleBuilder start(BCLEntityWrapper<? extends Mob> wrapper) {
SpawnRuleBuilder builder = start(wrapper.type());
if (!wrapper.canSpawn()) {
builder.preventSpawn();
}
return builder;
}
/**
* Stop entity spawn entierly
*
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder preventSpawn() {
entryInstance = getFromCache("prevent", () -> {
return new SpawnRuleEntry(-1, (type, world, spawnReason, pos, random) -> false);
});
rules.add(entryInstance);
return this;
}
/**
* Stop entity spawn on peaceful {@link Difficulty}
*
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder notPeaceful() {
entryInstance = getFromCache("not_peaceful", () -> {
return new SpawnRuleEntry(0,
(type, world, spawnReason, pos, random) -> world.getDifficulty() != Difficulty.PEACEFUL);
});
rules.add(entryInstance);
return this;
}
/**
* Restricts entity spawn above world surface (flying mobs).
*
* @param minHeight minimal spawn height.
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder aboveGround(int minHeight) {
entryInstance = getFromCache("above_ground", () -> {
return new SpawnRuleEntry(0, (type, world, spawnReason, pos, random) -> {
if (pos.getY() < world.getMinBuildHeight() + 2) {
return false;
}
return pos.getY() > world.getHeight(Types.WORLD_SURFACE, pos.getX(), pos.getZ()) + minHeight;
});
});
rules.add(entryInstance);
return this;
}
/**
* Restricts entity spawn below world logical height (useful for Nether mobs).
*
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder belowMaxHeight() {
entryInstance = getFromCache("below_max_height", () -> {
return new SpawnRuleEntry(0,
(type, world, spawnReason, pos, random) -> pos.getY() < world.dimensionType()
.logicalHeight());
});
rules.add(entryInstance);
return this;
}
/**
* Restricts spawning only to vanilla valid blocks.
*
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder onlyOnValidBlocks() {
entryInstance = getFromCache("only_on_valid_blocks", () -> {
return new SpawnRuleEntry(0, (type, world, spawnReason, pos, random) -> {
BlockPos below = pos.below();
return world.getBlockState(below).isValidSpawn(world, below, type);
});
});
rules.add(entryInstance);
return this;
}
/**
* Restricts spawning only to specified blocks.
*
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder onlyOnBlocks(Block... blocks) {
final Block[] floorBlocks = blocks;
Arrays.sort(floorBlocks, Comparator.comparing(Block::getDescriptionId));
StringBuilder builder = new StringBuilder("only_on_blocks");
for (Block block : floorBlocks) {
builder.append('_');
builder.append(block.getDescriptionId());
}
entryInstance = getFromCache(builder.toString(), () -> {
return new SpawnRuleEntry(0, (type, world, spawnReason, pos, random) -> {
Block below = world.getBlockState(pos.below()).getBlock();
for (Block floor : floorBlocks) {
if (floor == below) {
return true;
}
}
return false;
});
});
rules.add(entryInstance);
return this;
}
/**
* Will spawn entity with 1 / chance probability (randomly).
*
* @param chance probability limit.
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder withChance(int chance) {
entryInstance = getFromCache("with_chance_" + chance, () -> {
return new SpawnRuleEntry(1, (type, world, spawnReason, pos, random) -> random.nextInt(chance) == 0);
});
rules.add(entryInstance);
return this;
}
/**
* Will spawn entity only below specified brightness value.
*
* @param lightLevel light level upper limit.
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder belowBrightness(int lightLevel) {
entryInstance = getFromCache("below_brightness_" + lightLevel, () -> {
return new SpawnRuleEntry(2,
(type, world, spawnReason, pos, random) -> world.getMaxLocalRawBrightness(pos) <= lightLevel);
});
rules.add(entryInstance);
return this;
}
/**
* Will spawn entity only above specified brightness value.
*
* @param lightLevel light level lower limit.
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder aboveBrightness(int lightLevel) {
entryInstance = getFromCache("above_brightness_" + lightLevel, () -> {
return new SpawnRuleEntry(2,
(type, world, spawnReason, pos, random) -> world.getMaxLocalRawBrightness(pos) >= lightLevel);
});
rules.add(entryInstance);
return this;
}
/**
* Entity spawn will follow common vanilla spawn rules - spawn in darkness and not on peaceful level.
*
* @param lightLevel light level upper limit.
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder hostile(int lightLevel) {
return notPeaceful().belowBrightness(lightLevel);
}
/**
* Entity spawn will follow common vanilla spawn rules - spawn in darkness (below light level 7) and not on peaceful level.
*
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder vanillaHostile() {
return hostile(7);
}
/**
* Will spawn entity only if count of nearby entities will be lower than specified.
*
* @param selectorType selector {@link EntityType} to search.
* @param count max entity count.
* @param side side of box to search in.
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder maxNearby(EntityType<?> selectorType, int count, int side) {
final Class<? extends Entity> baseClass = selectorType.getBaseClass();
entryInstance = getFromCache("max_nearby_" + selectorType.getDescriptionId() + "_" + count + "_" + side, () -> {
return new SpawnRuleEntry(3, (type, world, spawnReason, pos, random) -> {
try {
final AABB box = new AABB(pos).inflate(side, world.getHeight(), side);
final List<?> list = world.getEntitiesOfClass(baseClass, box, (entity) -> true);
return list.size() < count;
} catch (Exception e) {
return true;
}
});
});
rules.add(entryInstance);
return this;
}
/**
* Will spawn entity only if count of nearby entities with same type will be lower than specified.
*
* @param count max entity count.
* @param side side of box to search in.
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder maxNearby(int count, int side) {
return maxNearby(entityType, count, side);
}
/**
* Will spawn entity only if count of nearby entities with same type will be lower than specified.
*
* @param count max entity count.
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder maxNearby(int count) {
return maxNearby(entityType, count, 256);
}
/**
* Allows to add custom spawning rule for specific entities.
*
* @param rule {@link SpawnRule} rule, can be a lambda expression.
* @return same {@link SpawnRuleBuilder} instance.
*/
public SpawnRuleBuilder customRule(SpawnRule rule) {
rules.add(new SpawnRuleEntry(7, rule));
return this;
}
/**
* Finalize spawning rule creation.
*
* @param spawnType {@link Type} of spawn.
* @param heightmapType {@link Types} heightmap type.
*/
public void build(Type spawnType, Types heightmapType) {
final List<SpawnRuleEntry> rulesCopy = Lists.newArrayList(this.rules);
Collections.sort(rulesCopy);
SpawnPredicate<M> predicate = (entityType, serverLevelAccessor, mobSpawnType, blockPos, random) -> {
for (SpawnRuleEntry rule : rulesCopy) {
if (!rule.canSpawn(entityType, serverLevelAccessor, mobSpawnType, blockPos, random)) {
return false;
}
}
return true;
};
SpawnRestrictionAccessor.callRegister(entityType, spawnType, heightmapType, predicate);
}
/**
* Finalize spawning rule creation with No Restrictions spawn type, useful for flying entities.
*
* @param heightmapType {@link Types} heightmap type.
*/
public void buildNoRestrictions(Types heightmapType) {
build(Type.NO_RESTRICTIONS, heightmapType);
}
/**
* Finalize spawning rule creation with On Ground spawn type, useful for common entities.
*
* @param heightmapType {@link Types} heightmap type.
*/
public void buildOnGround(Types heightmapType) {
build(Type.ON_GROUND, heightmapType);
}
/**
* Finalize spawning rule creation with In Water spawn type, useful for water entities.
*
* @param heightmapType {@link Types} heightmap type.
*/
public void buildInWater(Types heightmapType) {
build(Type.IN_WATER, heightmapType);
}
/**
* Finalize spawning rule creation with In Lava spawn type, useful for lava entities.
*
* @param heightmapType {@link Types} heightmap type.
*/
public void buildInLava(Types heightmapType) {
build(Type.IN_LAVA, heightmapType);
}
/**
* Internal function, will take entry from cache or create it if necessary.
*
* @param name {@link String} entry internal name.
* @param supplier {@link Supplier} for {@link SpawnRuleEntry}.
* @return new or existing {@link SpawnRuleEntry}.
*/
private static SpawnRuleEntry getFromCache(String name, Supplier<SpawnRuleEntry> supplier) {
SpawnRuleEntry entry = RULES_CACHE.get(name);
if (entry == null) {
entry = supplier.get();
RULES_CACHE.put(name, entry);
}
return entry;
}
}

View file

@ -0,0 +1,35 @@
package org.betterx.bclib.api.spawning;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.world.level.LevelAccessor;
import org.betterx.bclib.interfaces.SpawnRule;
import org.jetbrains.annotations.NotNull;
public class SpawnRuleEntry<M extends Mob> implements Comparable<SpawnRuleEntry> {
private final SpawnRule rule;
private final byte priority;
public SpawnRuleEntry(int priority, SpawnRule rule) {
this.priority = (byte) priority;
this.rule = rule;
}
protected boolean canSpawn(EntityType<M> type,
LevelAccessor world,
MobSpawnType spawnReason,
BlockPos pos,
RandomSource random) {
return rule.canSpawn(type, world, spawnReason, pos, random);
}
@Override
public int compareTo(@NotNull SpawnRuleEntry entry) {
return Integer.compare(priority, entry.priority);
}
}

View file

@ -0,0 +1,301 @@
package org.betterx.bclib.api.surface;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.SurfaceRules;
import net.minecraft.world.level.levelgen.SurfaceRules.RuleSource;
import net.minecraft.world.level.levelgen.placement.CaveSurface;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.betterx.bclib.api.biomes.BiomeAPI;
import org.betterx.bclib.api.surface.rules.NoiseCondition;
import org.betterx.bclib.world.surface.DoubleBlockSurfaceNoiseCondition;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
public class SurfaceRuleBuilder {
private static final Map<String, SurfaceRuleEntry> RULES_CACHE = Maps.newHashMap();
private static final SurfaceRuleBuilder INSTANCE = new SurfaceRuleBuilder();
private final List<SurfaceRuleEntry> rules = Lists.newArrayList();
private SurfaceRuleEntry entryInstance;
private ResourceKey<Biome> biomeKey;
private SurfaceRuleBuilder() {
}
public static SurfaceRuleBuilder start() {
INSTANCE.biomeKey = null;
INSTANCE.rules.clear();
return INSTANCE;
}
/**
* Restricts surface to only one biome.
*
* @param biomeKey {@link ResourceKey} for the {@link Biome}.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder biome(ResourceKey<Biome> biomeKey) {
this.biomeKey = biomeKey;
return this;
}
/**
* Restricts surface to only one biome.
*
* @param biome {@link Biome}.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder biome(Biome biome) {
return biome(BiomeAPI.getBiomeKey(biome));
}
/**
* Set biome surface with specified {@link BlockState}. Example - block of grass in the Overworld biomes
*
* @param state {@link BlockState} for the ground cover.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder surface(BlockState state) {
entryInstance = getFromCache("surface_" + state.toString(), () -> {
RuleSource rule = SurfaceRules.state(state);
rule = SurfaceRules.ifTrue(SurfaceRules.ON_FLOOR, rule);
return new SurfaceRuleEntry(2, rule);
});
rules.add(entryInstance);
return this;
}
/**
* Set biome subsurface with specified {@link BlockState}. Example - dirt in the Overworld biomes.
*
* @param state {@link BlockState} for the subterrain layer.
* @param depth block layer depth.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder subsurface(BlockState state, int depth) {
entryInstance = getFromCache("subsurface_" + depth + "_" + state.toString(), () -> {
RuleSource rule = SurfaceRules.state(state);
rule = SurfaceRules.ifTrue(SurfaceRules.stoneDepthCheck(depth, false, CaveSurface.FLOOR), rule);
return new SurfaceRuleEntry(3, rule);
});
rules.add(entryInstance);
return this;
}
/**
* Set biome filler with specified {@link BlockState}. Example - stone in the Overworld biomes. The rule is added with priority 10.
*
* @param state {@link BlockState} for filling.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder filler(BlockState state) {
entryInstance = getFromCache("fill_" + state.toString(),
() -> new SurfaceRuleEntry(10, SurfaceRules.state(state)));
rules.add(entryInstance);
return this;
}
/**
* Set biome floor with specified {@link BlockState}. Example - underside of a gravel floor. The rule is added with priority 3.
*
* @param state {@link BlockState} for the ground cover.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder floor(BlockState state) {
entryInstance = getFromCache("floor_" + state.toString(), () -> {
RuleSource rule = SurfaceRules.state(state);
return new SurfaceRuleEntry(3, SurfaceRules.ifTrue(SurfaceRules.ON_FLOOR, rule));
});
rules.add(entryInstance);
return this;
}
/**
* Set biome floor material with specified {@link BlockState} and height. The rule is added with priority 3.
*
* @param state {@link BlockState} for the subterrain layer.
* @param height block layer height.
* @param noise The noise object that is applied
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder belowFloor(BlockState state, int height, NoiseCondition noise) {
entryInstance = getFromCache("below_floor_" + height + "_" + state.toString() + "_" + noise.getClass()
.getSimpleName(),
() -> {
RuleSource rule = SurfaceRules.state(state);
rule = SurfaceRules.ifTrue(SurfaceRules.stoneDepthCheck(height,
false,
CaveSurface.FLOOR),
SurfaceRules.ifTrue(noise, rule));
return new SurfaceRuleEntry(3, rule);
});
rules.add(entryInstance);
return this;
}
/**
* Set biome floor material with specified {@link BlockState} and height. The rule is added with priority 3.
*
* @param state {@link BlockState} for the subterrain layer.
* @param height block layer height.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder belowFloor(BlockState state, int height) {
entryInstance = getFromCache("below_floor_" + height + "_" + state.toString(), () -> {
RuleSource rule = SurfaceRules.state(state);
rule = SurfaceRules.ifTrue(SurfaceRules.stoneDepthCheck(height, false, CaveSurface.FLOOR), rule);
return new SurfaceRuleEntry(3, rule);
});
rules.add(entryInstance);
return this;
}
/**
* Set biome ceiling with specified {@link BlockState}. Example - block of sandstone in the Overworld desert in air pockets. The rule is added with priority 3.
*
* @param state {@link BlockState} for the ground cover.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder ceil(BlockState state) {
entryInstance = getFromCache("ceil_" + state.toString(), () -> {
RuleSource rule = SurfaceRules.state(state);
return new SurfaceRuleEntry(3, SurfaceRules.ifTrue(SurfaceRules.ON_CEILING, rule));
});
rules.add(entryInstance);
return this;
}
/**
* Set biome ceiling material with specified {@link BlockState} and height. Example - sandstone in the Overworld deserts. The rule is added with priority 3.
*
* @param state {@link BlockState} for the subterrain layer.
* @param height block layer height.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder aboveCeil(BlockState state, int height) {
entryInstance = getFromCache("above_ceil_" + height + "_" + state.toString(), () -> {
RuleSource rule = SurfaceRules.state(state);
rule = SurfaceRules.ifTrue(SurfaceRules.stoneDepthCheck(height, false, CaveSurface.CEILING), rule);
return new SurfaceRuleEntry(3, rule);
});
rules.add(entryInstance);
return this;
}
/**
* Will cover steep areas (with large terrain angle). Example - Overworld mountains.
*
* @param state {@link BlockState} for the steep layer.
* @param depth layer depth
* @return
*/
public SurfaceRuleBuilder steep(BlockState state, int depth) {
entryInstance = getFromCache("steep_" + depth + "_" + state.toString(), () -> {
RuleSource rule = SurfaceRules.state(state);
rule = SurfaceRules.ifTrue(SurfaceRules.stoneDepthCheck(depth, false, CaveSurface.FLOOR), rule);
rule = SurfaceRules.ifTrue(SurfaceRules.steep(), rule);
int priority = depth < 1 ? 0 : 1;
return new SurfaceRuleEntry(priority, rule);
});
rules.add(entryInstance);
return this;
}
/**
* Allows to add custom rule.
*
* @param priority rule priority, lower values = higher priority (rule will be applied before others).
* @param rule custom {@link SurfaceRules.RuleSource}.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder rule(int priority, SurfaceRules.RuleSource rule) {
rules.add(new SurfaceRuleEntry(priority, rule));
return this;
}
/**
* Allows to add custom rule.
*
* @param rule custom {@link SurfaceRules.RuleSource}.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder rule(SurfaceRules.RuleSource rule) {
return rule(7, rule);
}
/**
* Set biome floor with specified {@link BlockState} and the {@link DoubleBlockSurfaceNoiseCondition}. The rule is added with priority 3.
*
* @param surfaceBlockA {@link BlockState} for the ground cover.
* @param surfaceBlockB {@link BlockState} for the alternative ground cover.
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder chancedFloor(BlockState surfaceBlockA, BlockState surfaceBlockB) {
return chancedFloor(surfaceBlockA, surfaceBlockB, DoubleBlockSurfaceNoiseCondition.CONDITION);
}
/**
* Set biome floor with specified {@link BlockState} and the given Noise Function. The rule is added with priority 3.
*
* @param surfaceBlockA {@link BlockState} for the ground cover.
* @param surfaceBlockB {@link BlockState} for the alternative ground cover.
* @param noise The {@link NoiseCondition}
* @return same {@link SurfaceRuleBuilder} instance.
*/
public SurfaceRuleBuilder chancedFloor(BlockState surfaceBlockA, BlockState surfaceBlockB, NoiseCondition noise) {
entryInstance = getFromCache("chancedFloor_" + surfaceBlockA + "_" + surfaceBlockB + "_" + noise.getClass()
.getSimpleName(),
() -> {
RuleSource rule =
SurfaceRules.ifTrue(SurfaceRules.ON_FLOOR,
SurfaceRules.sequence(
SurfaceRules.ifTrue(noise,
SurfaceRules.state(
surfaceBlockA)),
SurfaceRules.state(surfaceBlockB)
)
);
return new SurfaceRuleEntry(4, rule);
});
rules.add(entryInstance);
return this;
}
/**
* Finalise rule building process.
*
* @return {@link SurfaceRules.RuleSource}.
*/
public SurfaceRules.RuleSource build() {
Collections.sort(rules);
List<SurfaceRules.RuleSource> ruleList = rules.stream().map(entry -> entry.getRule()).toList();
SurfaceRules.RuleSource[] ruleArray = ruleList.toArray(new SurfaceRules.RuleSource[ruleList.size()]);
SurfaceRules.RuleSource rule = SurfaceRules.sequence(ruleArray);
if (biomeKey != null) {
rule = SurfaceRules.ifTrue(SurfaceRules.isBiome(biomeKey), rule);
}
return rule;
}
/**
* Internal function, will take entry from cache or create it if necessary.
*
* @param name {@link String} entry internal name.
* @param supplier {@link Supplier} for {@link SurfaceRuleEntry}.
* @return new or existing {@link SurfaceRuleEntry}.
*/
private static SurfaceRuleEntry getFromCache(String name, Supplier<SurfaceRuleEntry> supplier) {
SurfaceRuleEntry entry = RULES_CACHE.get(name);
if (entry == null) {
entry = supplier.get();
RULES_CACHE.put(name, entry);
}
return entry;
}
}

View file

@ -0,0 +1,25 @@
package org.betterx.bclib.api.surface;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.level.levelgen.SurfaceRules;
import org.jetbrains.annotations.NotNull;
public class SurfaceRuleEntry<M extends Mob> implements Comparable<SurfaceRuleEntry> {
private final SurfaceRules.RuleSource rule;
private final byte priority;
public SurfaceRuleEntry(int priority, SurfaceRules.RuleSource rule) {
this.priority = (byte) priority;
this.rule = rule;
}
protected SurfaceRules.RuleSource getRule() {
return rule;
}
@Override
public int compareTo(@NotNull SurfaceRuleEntry entry) {
return Integer.compare(priority, entry.priority);
}
}

View file

@ -0,0 +1,9 @@
package org.betterx.bclib.api.surface.rules;
import net.minecraft.world.level.levelgen.SurfaceRules;
import org.betterx.bclib.mixin.common.SurfaceRulesContextAccessor;
public interface NoiseCondition extends SurfaceRules.ConditionSource {
boolean test(SurfaceRulesContextAccessor context);
}

View file

@ -0,0 +1,29 @@
package org.betterx.bclib.api.surface.rules;
import net.minecraft.core.Registry;
import com.mojang.serialization.Codec;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.interfaces.NumericProvider;
import org.betterx.bclib.mixin.common.SurfaceRulesContextAccessor;
import org.betterx.bclib.util.MHelper;
public record RandomIntProvider(int range) implements NumericProvider {
public static final Codec<RandomIntProvider> CODEC = Codec.INT.fieldOf("range")
.xmap(RandomIntProvider::new, obj -> obj.range)
.codec();
@Override
public int getNumber(SurfaceRulesContextAccessor context) {
return MHelper.RANDOM.nextInt(range);
}
@Override
public Codec<? extends NumericProvider> pcodec() {
return CODEC;
}
static {
Registry.register(NumericProvider.NUMERIC_PROVIDER, BCLib.makeID("rnd_int"), RandomIntProvider.CODEC);
}
}

View file

@ -0,0 +1,31 @@
package org.betterx.bclib.api.surface.rules;
import net.minecraft.world.level.levelgen.SurfaceRules.Condition;
import net.minecraft.world.level.levelgen.SurfaceRules.Context;
import net.minecraft.world.level.levelgen.SurfaceRules.LazyXZCondition;
import org.betterx.bclib.mixin.common.SurfaceRulesContextAccessor;
public abstract class SurfaceNoiseCondition implements NoiseCondition {
@Override
public final Condition apply(Context context2) {
final SurfaceNoiseCondition self = this;
class Generator extends LazyXZCondition {
Generator() {
super(context2);
}
@Override
protected boolean compute() {
final SurfaceRulesContextAccessor context = SurfaceRulesContextAccessor.class.cast(this.context);
if (context == null) return false;
return self.test(context);
}
}
return new Generator();
}
public abstract boolean test(SurfaceRulesContextAccessor context);
}

View file

@ -0,0 +1,52 @@
package org.betterx.bclib.api.surface.rules;
import net.minecraft.core.Registry;
import net.minecraft.util.KeyDispatchDataCodec;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.SurfaceRules.Context;
import net.minecraft.world.level.levelgen.SurfaceRules.RuleSource;
import net.minecraft.world.level.levelgen.SurfaceRules.SurfaceRule;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import org.betterx.bclib.interfaces.NumericProvider;
import org.betterx.bclib.mixin.common.SurfaceRulesContextAccessor;
import java.util.List;
import org.jetbrains.annotations.Nullable;
//
public record SwitchRuleSource(NumericProvider selector, List<RuleSource> collection) implements RuleSource {
public static final Codec<SwitchRuleSource> CODEC = RecordCodecBuilder.create(instance -> instance.group(
NumericProvider.CODEC.fieldOf("selector").forGetter(SwitchRuleSource::selector),
RuleSource.CODEC.listOf().fieldOf("collection").forGetter(SwitchRuleSource::collection)
)
.apply(instance,
SwitchRuleSource::new));
private static final KeyDispatchDataCodec<? extends RuleSource> KEY_CODEC = KeyDispatchDataCodec.of(SwitchRuleSource.CODEC);
@Override
public KeyDispatchDataCodec<? extends RuleSource> codec() {
return KEY_CODEC;
}
@Override
public SurfaceRule apply(Context context) {
return new SurfaceRule() {
@Nullable
@Override
public BlockState tryApply(int x, int y, int z) {
final SurfaceRulesContextAccessor ctx = SurfaceRulesContextAccessor.class.cast(context);
int nr = Math.max(0, selector.getNumber(ctx)) % collection.size();
return collection.get(nr).apply(context).tryApply(x, y, z);
}
};
}
static {
Registry.register(Registry.RULE, "bclib_switch_rule", SwitchRuleSource.CODEC);
}
}

View file

@ -0,0 +1,41 @@
package org.betterx.bclib.api.surface.rules;
import net.minecraft.util.KeyDispatchDataCodec;
import net.minecraft.world.level.levelgen.SurfaceRules.Condition;
import net.minecraft.world.level.levelgen.SurfaceRules.ConditionSource;
import net.minecraft.world.level.levelgen.SurfaceRules.Context;
import net.minecraft.world.level.levelgen.SurfaceRules.LazyCondition;
import org.betterx.bclib.mixin.common.SurfaceRulesContextAccessor;
public abstract class VolumeNoiseCondition implements NoiseCondition {
public abstract KeyDispatchDataCodec<? extends ConditionSource> codec();
@Override
public final Condition apply(Context context2) {
final VolumeNoiseCondition self = this;
class Generator extends LazyCondition {
Generator() {
super(context2);
}
@Override
protected long getContextLastUpdate() {
final SurfaceRulesContextAccessor ctx = SurfaceRulesContextAccessor.class.cast(this.context);
return ctx.getLastUpdateY() + ctx.getLastUpdateXZ();
}
@Override
protected boolean compute() {
final SurfaceRulesContextAccessor context = SurfaceRulesContextAccessor.class.cast(this.context);
if (context == null) return false;
return self.test(context);
}
}
return new Generator();
}
public abstract boolean test(SurfaceRulesContextAccessor context);
}

View file

@ -1,8 +1,7 @@
package ru.bclib.api.tag;
package org.betterx.bclib.api.tag;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
public class CommonBiomeTags {
public static final TagKey<Biome> IN_NETHER = TagAPI.makeCommonBiomeTag("in_nether");

View file

@ -0,0 +1,27 @@
package org.betterx.bclib.api.tag;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
public class CommonBlockTags {
public static final TagKey<Block> BARREL = TagAPI.makeCommonBlockTag("barrel");
public static final TagKey<Block> BOOKSHELVES = TagAPI.makeCommonBlockTag("bookshelves");
public static final TagKey<Block> CHEST = TagAPI.makeCommonBlockTag("chest");
public static final TagKey<Block> END_STONES = TagAPI.makeCommonBlockTag("end_stones");
public static final TagKey<Block> GEN_END_STONES = END_STONES;
public static final TagKey<Block> IMMOBILE = TagAPI.makeCommonBlockTag("immobile");
public static final TagKey<Block> LEAVES = TagAPI.makeCommonBlockTag("leaves");
public static final TagKey<Block> NETHERRACK = TagAPI.makeCommonBlockTag("netherrack");
public static final TagKey<Block> NETHER_MYCELIUM = TagAPI.makeCommonBlockTag("nether_mycelium");
public static final TagKey<Block> NETHER_PORTAL_FRAME = TagAPI.makeCommonBlockTag("nether_pframe");
public static final TagKey<Block> NETHER_STONES = TagAPI.makeCommonBlockTag("nether_stones");
public static final TagKey<Block> SAPLINGS = TagAPI.makeCommonBlockTag("saplings");
public static final TagKey<Block> SOUL_GROUND = TagAPI.makeCommonBlockTag("soul_ground");
public static final TagKey<Block> WOODEN_BARREL = TagAPI.makeCommonBlockTag("wooden_barrels");
public static final TagKey<Block> WOODEN_CHEST = TagAPI.makeCommonBlockTag("wooden_chests");
public static final TagKey<Block> WORKBENCHES = TagAPI.makeCommonBlockTag("workbench");
public static final TagKey<Block> DRAGON_IMMUNE = TagAPI.makeCommonBlockTag("dragon_immune");
public static final TagKey<Block> MINABLE_WITH_HAMMER = TagAPI.makeCommonBlockTag("mineable/hammer");
}

View file

@ -0,0 +1,19 @@
package org.betterx.bclib.api.tag;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
public class CommonItemTags {
public final static TagKey<Item> HAMMERS = TagAPI.makeCommonItemTag("hammers");
public static final TagKey<Item> BARREL = TagAPI.makeCommonItemTag("barrel");
public static final TagKey<Item> CHEST = TagAPI.makeCommonItemTag("chest");
public static final TagKey<Item> SHEARS = TagAPI.makeCommonItemTag("shears");
public static final TagKey<Item> FURNACES = TagAPI.makeCommonItemTag("furnaces");
public static final TagKey<Item> IRON_INGOTS = TagAPI.makeCommonItemTag("iron_ingots");
public static final TagKey<Item> LEAVES = TagAPI.makeCommonItemTag("leaves");
public static final TagKey<Item> SAPLINGS = TagAPI.makeCommonItemTag("saplings");
public static final TagKey<Item> SOUL_GROUND = TagAPI.makeCommonItemTag("soul_ground");
public static final TagKey<Item> WOODEN_BARREL = TagAPI.makeCommonItemTag("wooden_barrels");
public static final TagKey<Item> WOODEN_CHEST = TagAPI.makeCommonItemTag("wooden_chests");
public static final TagKey<Item> WORKBENCHES = TagAPI.makeCommonItemTag("workbench");
}

View file

@ -0,0 +1,39 @@
package org.betterx.bclib.api.tag;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
public class NamedBlockTags {
public static final TagKey<Block> ANVIL = BlockTags.ANVIL;
public static final TagKey<Block> BUTTONS = BlockTags.BUTTONS;
public static final TagKey<Block> CLIMBABLE = BlockTags.CLIMBABLE;
public static final TagKey<Block> DOORS = BlockTags.DOORS;
public static final TagKey<Block> FENCES = BlockTags.FENCES;
public static final TagKey<Block> FENCE_GATES = BlockTags.FENCE_GATES;
public static final TagKey<Block> LEAVES = BlockTags.LEAVES;
public static final TagKey<Block> LOGS = BlockTags.LOGS;
public static final TagKey<Block> LOGS_THAT_BURN = BlockTags.LOGS_THAT_BURN;
public static final TagKey<Block> NYLIUM = BlockTags.NYLIUM;
public static final TagKey<Block> PLANKS = BlockTags.PLANKS;
public static final TagKey<Block> PRESSURE_PLATES = BlockTags.PRESSURE_PLATES;
public static final TagKey<Block> SAPLINGS = BlockTags.SAPLINGS;
public static final TagKey<Block> SIGNS = BlockTags.SIGNS;
public static final TagKey<Block> SLABS = BlockTags.SLABS;
public static final TagKey<Block> STAIRS = BlockTags.STAIRS;
public static final TagKey<Block> STONE_PRESSURE_PLATES = BlockTags.STONE_PRESSURE_PLATES;
public static final TagKey<Block> TRAPDOORS = BlockTags.TRAPDOORS;
public static final TagKey<Block> WALLS = BlockTags.WALLS;
public static final TagKey<Block> WOODEN_BUTTONS = BlockTags.WOODEN_BUTTONS;
public static final TagKey<Block> WOODEN_DOORS = BlockTags.WOODEN_DOORS;
public static final TagKey<Block> WOODEN_FENCES = BlockTags.WOODEN_FENCES;
public static final TagKey<Block> WOODEN_PRESSURE_PLATES = BlockTags.WOODEN_PRESSURE_PLATES;
public static final TagKey<Block> WOODEN_SLABS = BlockTags.WOODEN_SLABS;
public static final TagKey<Block> WOODEN_STAIRS = BlockTags.WOODEN_STAIRS;
public static final TagKey<Block> WOODEN_TRAPDOORS = BlockTags.WOODEN_TRAPDOORS;
public static final TagKey<Block> SOUL_FIRE_BASE_BLOCKS = BlockTags.SOUL_FIRE_BASE_BLOCKS;
public static final TagKey<Block> SOUL_SPEED_BLOCKS = BlockTags.SOUL_SPEED_BLOCKS;
public static final TagKey<Block> BEACON_BASE_BLOCKS = BlockTags.BEACON_BASE_BLOCKS;
public static final TagKey<Block> STONE_BRICKS = BlockTags.STONE_BRICKS;
}

View file

@ -0,0 +1,32 @@
package org.betterx.bclib.api.tag;
import net.minecraft.tags.ItemTags;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
public class NamedItemTags {
public static final TagKey<Item> BUTTONS = ItemTags.BUTTONS;
public static final TagKey<Item> DOORS = ItemTags.DOORS;
public static final TagKey<Item> FENCES = ItemTags.FENCES;
public static final TagKey<Item> LEAVES = ItemTags.LEAVES;
public static final TagKey<Item> LOGS = ItemTags.LOGS;
public static final TagKey<Item> LOGS_THAT_BURN = ItemTags.LOGS_THAT_BURN;
public static final TagKey<Item> PLANKS = ItemTags.PLANKS;
public static final TagKey<Item> SAPLINGS = ItemTags.SAPLINGS;
public static final TagKey<Item> SIGNS = ItemTags.SIGNS;
public static final TagKey<Item> SLABS = ItemTags.SLABS;
public static final TagKey<Item> STAIRS = ItemTags.STAIRS;
public static final TagKey<Item> TRAPDOORS = ItemTags.TRAPDOORS;
public static final TagKey<Item> WOODEN_BUTTONS = ItemTags.WOODEN_BUTTONS;
public static final TagKey<Item> WOODEN_DOORS = ItemTags.WOODEN_DOORS;
public static final TagKey<Item> WOODEN_FENCES = ItemTags.WOODEN_FENCES;
public static final TagKey<Item> WOODEN_PRESSURE_PLATES = ItemTags.WOODEN_PRESSURE_PLATES;
public static final TagKey<Item> WOODEN_SLABS = ItemTags.WOODEN_SLABS;
public static final TagKey<Item> WOODEN_STAIRS = ItemTags.WOODEN_STAIRS;
public static final TagKey<Item> WOODEN_TRAPDOORS = ItemTags.WOODEN_TRAPDOORS;
public static final TagKey<Item> BEACON_PAYMENT_ITEMS = ItemTags.BEACON_PAYMENT_ITEMS;
public static final TagKey<Item> STONE_BRICKS = ItemTags.STONE_BRICKS;
public static final TagKey<Item> STONE_CRAFTING_MATERIALS = ItemTags.STONE_CRAFTING_MATERIALS;
public static final TagKey<Item> STONE_TOOL_MATERIALS = ItemTags.STONE_TOOL_MATERIALS;
}

View file

@ -0,0 +1,16 @@
package org.betterx.bclib.api.tag;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
public class NamedMineableTags {
public static final TagKey<Block> AXE = BlockTags.MINEABLE_WITH_AXE;
public static final TagKey<Block> HOE = BlockTags.MINEABLE_WITH_HOE;
public static final TagKey<Block> PICKAXE = BlockTags.MINEABLE_WITH_PICKAXE;
public static final TagKey<Block> SHEARS = TagAPI.makeBlockTag("fabric", "mineable/shears");
public static final TagKey<Block> SHOVEL = BlockTags.MINEABLE_WITH_SHOVEL;
public static final TagKey<Block> SWORD = TagAPI.makeBlockTag("fabric", "mineable/sword");
public static final TagKey<Block> HAMMER = TagAPI.makeCommonBlockTag("mineable/hammer");
}

View file

@ -0,0 +1,14 @@
package org.betterx.bclib.api.tag;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
public class NamedToolTags {
public static final TagKey<Item> FABRIC_AXES = TagAPI.makeItemTag("fabric", "axes");
public static final TagKey<Item> FABRIC_HOES = TagAPI.makeItemTag("fabric", "hoes");
public static final TagKey<Item> FABRIC_PICKAXES = TagAPI.makeItemTag("fabric", "pickaxes");
public static final TagKey<Item> FABRIC_SHEARS = TagAPI.makeItemTag("fabric", "shears");
public static final TagKey<Item> FABRIC_SHOVELS = TagAPI.makeItemTag("fabric", "shovels");
public static final TagKey<Item> FABRIC_SWORDS = TagAPI.makeItemTag("fabric", "swords");
}

View file

@ -0,0 +1,277 @@
package org.betterx.bclib.api.tag;
import net.minecraft.core.DefaultedRegistry;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.tags.Tag;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import com.google.common.collect.Maps;
import org.betterx.bclib.api.biomes.BiomeAPI;
import org.betterx.bclib.mixin.common.DiggerItemAccessor;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
public class TagAPI {
private static final Map<String, TagType<?>> TYPES = Maps.newHashMap();
public static TagType.RegistryBacked<Block> BLOCKS = registerType(Registry.BLOCK);
public static TagType.RegistryBacked<Item> ITEMS = registerType(Registry.ITEM);
public static TagType.Simple<Biome> BIOMES = registerType(Registry.BIOME_REGISTRY,
"tags/worldgen/biome",
b -> BiomeAPI.getBiomeID(b));
private static <T> TagType.RegistryBacked<T> registerType(DefaultedRegistry<T> registry) {
TagType<T> type = new TagType.RegistryBacked<>(registry);
return (TagType.RegistryBacked<T>) TYPES.computeIfAbsent(type.directory, (dir) -> type);
}
public static <T> TagType.Simple<T> registerType(ResourceKey<? extends Registry<T>> registry,
String directory,
Function<T, ResourceLocation> locationProvider) {
return (TagType.Simple<T>) TYPES.computeIfAbsent(directory,
(dir) -> new TagType.Simple<>(registry,
dir,
locationProvider));
}
public static <T> TagType.UnTyped<T> registerType(ResourceKey<? extends Registry<T>> registry, String directory) {
return (TagType.UnTyped<T>) TYPES.computeIfAbsent(directory, (dir) -> new TagType.UnTyped<>(registry, dir));
}
/**
* Get or create {@link Block} {@link TagKey} with mod namespace.
*
* @param modID - {@link String} mod namespace (mod id);
* @param name - {@link String} tag name.
* @return {@link Block} {@link TagKey}.
*/
public static TagKey<Biome> makeBiomeTag(String modID, String name) {
return BIOMES.makeTag(new ResourceLocation(modID, name));
}
/**
* Get or create {@link Block} {@link TagKey} with mod namespace.
*
* @param modID - {@link String} mod namespace (mod id);
* @param name - {@link String} tag name.
* @return {@link Block} {@link TagKey}.
*/
public static TagKey<Block> makeBlockTag(String modID, String name) {
return BLOCKS.makeTag(new ResourceLocation(modID, name));
}
/**
* Get or create {@link Block} {@link TagKey} with mod namespace.
*
* @param id - {@link String} id for the tag;
* @return {@link Block} {@link TagKey}.
*/
public static TagKey<Block> makeBlockTag(ResourceLocation id) {
return BLOCKS.makeTag(id);
}
/**
* Get or create {@link Item} {@link TagKey} with mod namespace.
*
* @param modID - {@link String} mod namespace (mod id);
* @param name - {@link String} tag name.
* @return {@link Item} {@link TagKey}.
*/
public static TagKey<Item> makeItemTag(String modID, String name) {
return ITEMS.makeTag(new ResourceLocation(modID, name));
}
/**
* Get or create {@link Item} {@link TagKey} with mod namespace.
*
* @param id - {@link String} id for the tag;
* @return {@link Item} {@link TagKey}.
*/
public static TagKey<Item> makeItemTag(ResourceLocation id) {
return ITEMS.makeTag(id);
}
/**
* Get or create {@link Block} {@link TagKey}.
*
* @param name - {@link String} tag name.
* @return {@link Block} {@link TagKey}.
* @see <a href="https://fabricmc.net/wiki/tutorial:tags">Fabric Wiki (Tags)</a>
*/
public static TagKey<Block> makeCommonBlockTag(String name) {
return BLOCKS.makeCommonTag(name);
}
/**
* Get or create {@link Item} {@link TagKey}.
*
* @param name - {@link String} tag name.
* @return {@link Item} {@link TagKey}.
* @see <a href="https://fabricmc.net/wiki/tutorial:tags">Fabric Wiki (Tags)</a>
*/
public static TagKey<Item> makeCommonItemTag(String name) {
return ITEMS.makeCommonTag(name);
}
public static TagKey<Biome> makeCommonBiomeTag(String name) {
return BIOMES.makeCommonTag(name);
}
/**
* Initializes basic tags. Should be called only in BCLib main class.
*/
public static void init() {
addBlockTag(CommonBlockTags.BOOKSHELVES, Blocks.BOOKSHELF);
addBlockTag(CommonBlockTags.CHEST, Blocks.CHEST);
addItemTag(CommonItemTags.CHEST, Items.CHEST);
addItemTag(CommonItemTags.IRON_INGOTS, Items.IRON_INGOT);
addItemTag(CommonItemTags.FURNACES, Blocks.FURNACE);
}
/**
* Adds multiple Tags to one Biome.
*
* @param tagIDs array of {@link TagKey<Biome>} tag IDs.
* @param biome The {@link Biome} to add tag.
*/
@SafeVarargs
@Deprecated(forRemoval = true)
public static void addBiomeTags(Biome biome, TagKey<Biome>... tagIDs) {
BIOMES.add(biome, tagIDs);
}
/**
* Adds one Tag to multiple Biomes.
*
* @param tagID {@link TagKey<Biome>} tag ID.
* @param biomes array of {@link Biome} to add into tag.
*/
public static void addBiomeTag(TagKey<Biome> tagID, Biome... biomes) {
BIOMES.add(tagID, biomes);
}
/**
* Adds multiple Tags to one Block.
*
* @param tagIDs array of {@link TagKey<Block>} tag IDs.
* @param block The {@link Block} to add tag.
*/
@SafeVarargs
@Deprecated(forRemoval = true)
public static void addBlockTags(Block block, TagKey<Block>... tagIDs) {
BLOCKS.add(block, tagIDs);
}
/**
* Adds one Tag to multiple Blocks.
*
* @param tagID {@link TagKey<Block>} tag ID.
* @param blocks array of {@link Block} to add into tag.
*/
public static void addBlockTag(TagKey<Block> tagID, Block... blocks) {
BLOCKS.add(tagID, blocks);
}
/**
* Adds multiple Tags to one Item.
*
* @param tagIDs array of {@link TagKey<Item>} tag IDs.
* @param item The {@link Item} to add tag.
*/
@Deprecated(forRemoval = true)
@SafeVarargs
public static void addItemTags(ItemLike item, TagKey<Item>... tagIDs) {
ITEMS.add(item.asItem(), tagIDs);
}
/**
* Adds one Tag to multiple Items.
*
* @param tagID {@link TagKey<Item>} tag ID.
* @param items array of {@link ItemLike} to add into tag.
*/
public static void addItemTag(TagKey<Item> tagID, ItemLike... items) {
for (ItemLike i : items) {
ITEMS.add(i.asItem(), tagID);
}
}
/**
* Adds one Tag to multiple Items.
*
* @param tagID {@link TagKey<Item>} tag ID.
* @param items array of {@link ItemLike} to add into tag.
*/
public static void addItemTag(TagKey<Item> tagID, Item... items) {
ITEMS.add(tagID, items);
}
/**
* Automatically called in {@link net.minecraft.tags.TagLoader#loadAndBuild(ResourceManager)}.
* <p>
* In most cases there is no need to call this Method manually.
*
* @param directory The name of the Tag-directory. Should be either <i>"tags/blocks"</i> or
* <i>"tags/items"</i>.
* @param tagsMap The map that will hold the registered Tags
* @return The {@code tagsMap} Parameter.
*/
public static <T> Map<ResourceLocation, Tag.Builder> apply(String directory,
Map<ResourceLocation, Tag.Builder> tagsMap) {
TagType<?> type = TYPES.get(directory);
if (type != null) {
type.apply(tagsMap);
}
// final BiConsumer<ResourceLocation, Set<ResourceLocation>> consumer;
// consumer = (id, ids) -> apply(tagsMap.computeIfAbsent(id, key -> Tag.Builder.tag()), ids);
//
// if ("tags/blocks".equals(directory)) {
// TAGS_BLOCK.forEach(consumer);
// }
// else if ("tags/items".equals(directory)) {
// TAGS_ITEM.forEach(consumer);
// }
// else if ("tags/worldgen/biome".equals(directory)) {
// TAGS_BIOME.forEach(consumer);
// }
return tagsMap;
}
/**
* Adds all {@code ids} to the {@code builder}.
*
* @param builder
* @param ids
* @return The Builder passed as {@code builder}.
*/
public static Tag.Builder apply(Tag.Builder builder, Set<ResourceLocation> ids) {
ids.forEach(value -> builder.addElement(value, "BCLib Code"));
return builder;
}
public static boolean isToolWithMineableTag(ItemStack stack, TagKey<Block> tag) {
if (stack.getItem() instanceof DiggerItemAccessor dig) {
return dig.bclib_getBlockTag().equals(tag);
}
return false;
}
}

View file

@ -1,4 +1,4 @@
package ru.bclib.api.tag;
package org.betterx.bclib.api.tag;
import net.minecraft.core.DefaultedRegistry;
import net.minecraft.core.Registry;
@ -11,8 +11,8 @@ import net.minecraft.world.level.biome.Biome;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import ru.bclib.BCLib;
import ru.bclib.api.biomes.BiomeAPI;
import org.betterx.bclib.BCLib;
import org.betterx.bclib.api.biomes.BiomeAPI;
import java.util.Map;
import java.util.Set;
@ -21,7 +21,8 @@ import java.util.function.Function;
public class TagType<T> {
boolean isFrozen = false;
public static class RegistryBacked<T> extends Simple<T>{
public static class RegistryBacked<T> extends Simple<T> {
private final DefaultedRegistry<T> registry;
RegistryBacked(DefaultedRegistry<T> registry) {
@ -78,9 +79,12 @@ public class TagType<T> {
public static class UnTyped<T> extends TagType<T> {
UnTyped(ResourceKey<? extends Registry<T>> registry,
String directory) {
super(registry, directory, (t)->{throw new RuntimeException("Using Untyped TagType with Type-Dependant access. ");});
super(registry, directory, (t) -> {
throw new RuntimeException("Using Untyped TagType with Type-Dependant access. ");
});
}
}
public final String directory;
private final Map<ResourceLocation, Set<ResourceLocation>> tags = Maps.newConcurrentMap();
public final ResourceKey<? extends Registry<T>> registryKey;
@ -142,6 +146,7 @@ public class TagType<T> {
/**
* Adds one Tag to multiple Elements.
*
* @param tagID {@link TagKey< Biome >} tag ID.
* @param elements array of Elements to add into tag.
*/
@ -184,7 +189,8 @@ public class TagType<T> {
public void forEach(BiConsumer<ResourceLocation, Set<ResourceLocation>> consumer) {
tags.forEach(consumer);
}
public void apply(Map<ResourceLocation, Tag.Builder> tagsMap){
public void apply(Map<ResourceLocation, Tag.Builder> tagsMap) {
if (Registry.BIOME_REGISTRY.equals(registryKey)) BiomeAPI._runTagAdders();
//this.isFrozen = true;

View file

@ -0,0 +1,149 @@
package org.betterx.bclib.blockentities;
import net.minecraft.core.BlockPos;
import net.minecraft.core.NonNullList;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.BarrelBlock;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.betterx.bclib.blocks.BaseBarrelBlock;
import org.betterx.bclib.registry.BaseBlockEntities;
public class BaseBarrelBlockEntity extends RandomizableContainerBlockEntity {
private NonNullList<ItemStack> inventory;
private int viewerCount;
private BaseBarrelBlockEntity(BlockEntityType<?> type, BlockPos blockPos, BlockState blockState) {
super(type, blockPos, blockState);
this.inventory = NonNullList.withSize(27, ItemStack.EMPTY);
}
public BaseBarrelBlockEntity(BlockPos blockPos, BlockState blockState) {
this(BaseBlockEntities.BARREL, blockPos, blockState);
}
@Override
public void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
if (!this.trySaveLootTable(tag)) {
ContainerHelper.saveAllItems(tag, this.inventory);
}
//return tag;
}
public void load(CompoundTag tag) {
super.load(tag);
this.inventory = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY);
if (!this.tryLoadLootTable(tag)) {
ContainerHelper.loadAllItems(tag, this.inventory);
}
}
public int getContainerSize() {
return 27;
}
protected NonNullList<ItemStack> getItems() {
return this.inventory;
}
protected void setItems(NonNullList<ItemStack> list) {
this.inventory = list;
}
protected Component getDefaultName() {
return Component.translatable("container.barrel");
}
protected AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) {
return ChestMenu.threeRows(syncId, playerInventory, this);
}
public void startOpen(Player player) {
if (!player.isSpectator()) {
if (viewerCount < 0) {
viewerCount = 0;
}
++viewerCount;
BlockState blockState = this.getBlockState();
if (!blockState.getValue(BarrelBlock.OPEN)) {
playSound(blockState, SoundEvents.BARREL_OPEN);
setOpen(blockState, true);
}
if (level != null) {
scheduleUpdate();
}
}
}
private void scheduleUpdate() {
level.scheduleTick(getBlockPos(), getBlockState().getBlock(), 5);
}
public void tick() {
if (level != null) {
viewerCount = ChestBlockEntity.getOpenCount(level, worldPosition);
if (viewerCount > 0) {
scheduleUpdate();
} else {
BlockState blockState = getBlockState();
if (!(blockState.getBlock() instanceof BaseBarrelBlock)) {
setRemoved();
return;
}
if (blockState.getValue(BarrelBlock.OPEN)) {
playSound(blockState, SoundEvents.BARREL_CLOSE);
setOpen(blockState, false);
}
}
}
}
public void stopOpen(Player player) {
if (!player.isSpectator()) {
--this.viewerCount;
}
}
private void setOpen(BlockState state, boolean open) {
if (level != null) {
level.setBlock(this.getBlockPos(), state.setValue(BarrelBlock.OPEN, open), 3);
}
}
private void playSound(BlockState blockState, SoundEvent soundEvent) {
if (level != null) {
Vec3i vec3i = blockState.getValue(BarrelBlock.FACING).getNormal();
double d = (double) this.worldPosition.getX() + 0.5D + (double) vec3i.getX() / 2.0D;
double e = (double) this.worldPosition.getY() + 0.5D + (double) vec3i.getY() / 2.0D;
double f = (double) this.worldPosition.getZ() + 0.5D + (double) vec3i.getZ() / 2.0D;
level.playSound(
null,
d,
e,
f,
soundEvent,
SoundSource.BLOCKS,
0.5F,
this.level.random.nextFloat() * 0.1F + 0.9F
);
}
}
}

View file

@ -0,0 +1,13 @@
package org.betterx.bclib.blockentities;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.betterx.bclib.registry.BaseBlockEntities;
public class BaseChestBlockEntity extends ChestBlockEntity {
public BaseChestBlockEntity(BlockPos blockPos, BlockState blockState) {
super(BaseBlockEntities.CHEST, blockPos, blockState);
}
}

View file

@ -0,0 +1,26 @@
package org.betterx.bclib.blockentities;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.FurnaceMenu;
import net.minecraft.world.item.crafting.RecipeType;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.betterx.bclib.registry.BaseBlockEntities;
public class BaseFurnaceBlockEntity extends AbstractFurnaceBlockEntity {
public BaseFurnaceBlockEntity(BlockPos blockPos, BlockState blockState) {
super(BaseBlockEntities.FURNACE, blockPos, blockState, RecipeType.SMELTING);
}
protected Component getDefaultName() {
return Component.translatable("container.furnace");
}
protected AbstractContainerMenu createMenu(int syncId, Inventory playerInventory) {
return new FurnaceMenu(syncId, playerInventory, this, this.dataAccess);
}
}

View file

@ -0,0 +1,19 @@
package org.betterx.bclib.blockentities;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.betterx.bclib.registry.BaseBlockEntities;
public class BaseSignBlockEntity extends SignBlockEntity {
public BaseSignBlockEntity(BlockPos blockPos, BlockState blockState) {
super(blockPos, blockState);
}
@Override
public BlockEntityType<?> getType() {
return BaseBlockEntities.SIGN;
}
}

View file

@ -0,0 +1,44 @@
package org.betterx.bclib.blockentities;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
public class DynamicBlockEntityType<T extends BlockEntity> extends BlockEntityType<T> {
private final Set<Block> validBlocks = Sets.newHashSet();
private final BlockEntitySupplier<? extends T> factory;
public DynamicBlockEntityType(BlockEntitySupplier<? extends T> supplier) {
super(null, Collections.emptySet(), null);
this.factory = supplier;
}
@Override
@Nullable
public T create(BlockPos blockPos, BlockState blockState) {
return factory.create(blockPos, blockState);
}
@Override
public boolean isValid(BlockState blockState) {
return validBlocks.contains(blockState.getBlock());
}
public void registerBlock(Block block) {
validBlocks.add(block);
}
@FunctionalInterface
public interface BlockEntitySupplier<T extends BlockEntity> {
T create(BlockPos blockPos, BlockState blockState);
}
}

View file

@ -0,0 +1,138 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.PickaxeItem;
import net.minecraft.world.level.block.AnvilBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.material.MaterialColor;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.item.v1.FabricItemSettings;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.CustomItemProvider;
import org.betterx.bclib.items.BaseAnvilItem;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public abstract class BaseAnvilBlock extends AnvilBlock implements BlockModelProvider, CustomItemProvider {
public static final IntegerProperty DESTRUCTION = BlockProperties.DESTRUCTION;
public IntegerProperty durability;
public BaseAnvilBlock(MaterialColor color) {
this(FabricBlockSettings.copyOf(Blocks.ANVIL).color(color));
}
public BaseAnvilBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
super.createBlockStateDefinition(builder);
if (getMaxDurability() != 3) {
durability = IntegerProperty.create("durability", 0, getMaxDurability());
} else {
durability = BlockProperties.DEFAULT_ANVIL_DURABILITY;
}
builder.add(DESTRUCTION, durability);
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
return getBlockModel(blockId, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
int destruction = blockState.getValue(DESTRUCTION);
String name = blockId.getPath();
Map<String, String> textures = Maps.newHashMap();
textures.put("%modid%", blockId.getNamespace());
textures.put("%anvil%", name);
textures.put("%top%", name + "_top_" + destruction);
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_ANVIL, textures);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
int destruction = blockState.getValue(DESTRUCTION);
String modId = stateId.getNamespace();
String modelId = "block/" + stateId.getPath() + "_top_" + destruction;
ResourceLocation modelLocation = new ResourceLocation(modId, modelId);
registerBlockModel(stateId, modelLocation, blockState, modelCache);
return ModelsHelper.createFacingModel(modelLocation, blockState.getValue(FACING), false, false);
}
@Override
public BlockItem getCustomItem(ResourceLocation blockID, FabricItemSettings settings) {
return new BaseAnvilItem(this, settings);
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
int destruction = state.getValue(DESTRUCTION);
int durability = state.getValue(getDurabilityProp());
int value = destruction * getMaxDurability() + durability;
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
if (tool != null && tool.getItem() instanceof PickaxeItem) {
ItemStack itemStack = new ItemStack(this);
itemStack.getOrCreateTag().putInt(BaseAnvilItem.DESTRUCTION, value);
return Lists.newArrayList(itemStack);
}
return Collections.emptyList();
}
public IntegerProperty getDurabilityProp() {
return durability;
}
public int getMaxDurability() {
return 3;
}
public BlockState damageAnvilUse(BlockState state, RandomSource random) {
IntegerProperty durability = getDurabilityProp();
int value = state.getValue(durability);
if (value < getMaxDurability() && random.nextInt(8) == 0) {
return state.setValue(durability, value + 1);
}
value = state.getValue(DESTRUCTION);
return value < 2 ? state.setValue(DESTRUCTION, value + 1).setValue(durability, 0) : null;
}
public BlockState damageAnvilFall(BlockState state) {
int destruction = state.getValue(DESTRUCTION);
return destruction < 2 ? state.setValue(DESTRUCTION, destruction + 1) : null;
}
}

View file

@ -0,0 +1,81 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import org.betterx.bclib.util.BlocksHelper;
@SuppressWarnings("deprecation")
public abstract class BaseAttachedBlock extends BaseBlockNotFull {
public static final DirectionProperty FACING = BlockStateProperties.FACING;
public BaseAttachedBlock(Properties settings) {
super(settings);
registerDefaultState(defaultBlockState().setValue(FACING, Direction.UP));
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> stateManager) {
stateManager.add(FACING);
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext ctx) {
BlockState blockState = defaultBlockState();
LevelReader worldView = ctx.getLevel();
BlockPos blockPos = ctx.getClickedPos();
Direction[] directions = ctx.getNearestLookingDirections();
for (Direction direction : directions) {
Direction direction2 = direction.getOpposite();
blockState = blockState.setValue(FACING, direction2);
if (blockState.canSurvive(worldView, blockPos)) {
return blockState;
}
}
return null;
}
@Override
public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
Direction direction = state.getValue(FACING);
BlockPos blockPos = pos.relative(direction.getOpposite());
return canSupportCenter(world, blockPos, direction) || world.getBlockState(blockPos).is(BlockTags.LEAVES);
}
@Override
public BlockState updateShape(BlockState state,
Direction facing,
BlockState neighborState,
LevelAccessor world,
BlockPos pos,
BlockPos neighborPos) {
if (!canSurvive(state, world, pos)) {
return Blocks.AIR.defaultBlockState();
} else {
return state;
}
}
@Override
public BlockState rotate(BlockState state, Rotation rotation) {
return BlocksHelper.rotateHorizontal(state, rotation, FACING);
}
@Override
public BlockState mirror(BlockState state, Mirror mirror) {
return BlocksHelper.mirrorHorizontal(state, mirror, FACING);
}
}

View file

@ -0,0 +1,26 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.PatternsHelper;
import java.util.Optional;
public class BaseBarkBlock extends BaseRotatedPillarBlock {
public BaseBarkBlock(Properties settings) {
super(settings);
}
@Override
protected Optional<String> createBlockPattern(ResourceLocation blockId) {
blockId = Registry.BLOCK.getKey(this);
return PatternsHelper.createJson(BasePatterns.BLOCK_BASE, replacePath(blockId));
}
private ResourceLocation replacePath(ResourceLocation blockId) {
String newPath = blockId.getPath().replace("_bark", "_log_side");
return new ResourceLocation(blockId.getNamespace(), newPath);
}
}

View file

@ -0,0 +1,159 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.stats.Stats;
import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.monster.piglin.PiglinAi;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BarrelBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.phys.BlockHitResult;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.blockentities.BaseBarrelBlockEntity;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.registry.BaseBlockEntities;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseBarrelBlock extends BarrelBlock implements BlockModelProvider {
public BaseBarrelBlock(Block source) {
this(FabricBlockSettings.copyOf(source).noOcclusion());
}
public BaseBarrelBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
public BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
return BaseBlockEntities.BARREL.create(blockPos, blockState);
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
List<ItemStack> drop = super.getDrops(state, builder);
drop.add(new ItemStack(this.asItem()));
return drop;
}
@Override
public InteractionResult use(BlockState state,
Level world,
BlockPos pos,
Player player,
InteractionHand hand,
BlockHitResult hit) {
if (world.isClientSide) {
return InteractionResult.SUCCESS;
} else {
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof BaseBarrelBlockEntity) {
player.openMenu((BaseBarrelBlockEntity) blockEntity);
player.awardStat(Stats.OPEN_BARREL);
PiglinAi.angerNearbyPiglins(player, true);
}
return InteractionResult.CONSUME;
}
}
@Override
public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof BaseBarrelBlockEntity) {
((BaseBarrelBlockEntity) blockEntity).tick();
}
}
@Override
public RenderShape getRenderShape(BlockState state) {
return RenderShape.MODEL;
}
@Override
public void setPlacedBy(Level world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack itemStack) {
if (itemStack.hasCustomHoverName()) {
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof BaseBarrelBlockEntity) {
((BaseBarrelBlockEntity) blockEntity).setCustomName(itemStack.getHoverName());
}
}
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
return getBlockModel(blockId, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
Optional<String> pattern;
if (blockState.getValue(OPEN)) {
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_BARREL_OPEN, blockId);
} else {
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_BOTTOM_TOP, blockId);
}
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
String open = blockState.getValue(OPEN) ? "_open" : "";
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + open);
registerBlockModel(stateId, modelId, blockState, modelCache);
Direction facing = blockState.getValue(FACING);
BlockModelRotation rotation = BlockModelRotation.X0_Y0;
switch (facing) {
case NORTH:
rotation = BlockModelRotation.X90_Y0;
break;
case EAST:
rotation = BlockModelRotation.X90_Y90;
break;
case SOUTH:
rotation = BlockModelRotation.X90_Y180;
break;
case WEST:
rotation = BlockModelRotation.X90_Y270;
break;
case DOWN:
rotation = BlockModelRotation.X180_Y0;
break;
default:
break;
}
return ModelsHelper.createMultiVariant(modelId, rotation.getRotation(), false);
}
}

View file

@ -0,0 +1,76 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.MaterialColor;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.interfaces.BlockModelProvider;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
/**
* Base class for a default Block.
* <p>
* This Block-Type will:
* <ul>
* <li>Drop itself</li>
* <li>Automatically create an Item-Model from the Block-Model</li>
* </ul>
*/
public class BaseBlock extends Block implements BlockModelProvider {
/**
* Creates a new Block with the passed properties
*
* @param settings The properties of the Block.
*/
public BaseBlock(Properties settings) {
super(settings);
}
/**
* {@inheritDoc}
* <p>
* This implementation will drop the Block itself
*/
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
/**
* {@inheritDoc}
* <p>
* This implementation will load the Block-Model and return it as the Item-Model
*/
@Override
public BlockModel getItemModel(ResourceLocation blockId) {
return getBlockModel(blockId, defaultBlockState());
}
/**
* This method is used internally.
* <p>
* It is called from Block-Contructors, to allow the augmentation of the blocks
* preset properties.
* <p>
* For example in {@link BaseLeavesBlock#BaseLeavesBlock(Block, MaterialColor, Consumer)}
*
* @param customizeProperties A {@link Consumer} to call with the preset properties
* @param settings The properties as created by the Block
* @return The reconfigured {@code settings}
*/
static FabricBlockSettings acceptAndReturn(Consumer<FabricBlockSettings> customizeProperties,
FabricBlockSettings settings) {
customizeProperties.accept(settings);
return settings;
}
}

View file

@ -0,0 +1,24 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.block.state.BlockState;
public class BaseBlockNotFull extends BaseBlock {
public BaseBlockNotFull(Properties settings) {
super(settings);
}
public boolean canSuffocate(BlockState state, BlockGetter view, BlockPos pos) {
return false;
}
public boolean isSimpleFullBlock(BlockState state, BlockGetter view, BlockPos pos) {
return false;
}
public boolean allowsSpawning(BlockState state, BlockGetter view, BlockPos pos, EntityType<?> type) {
return false;
}
}

View file

@ -0,0 +1,28 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import java.util.Collections;
import java.util.List;
public class BaseBlockWithEntity extends BaseEntityBlock {
public BaseBlockWithEntity(Properties settings) {
super(settings);
}
@Override
public BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
return null;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
}

View file

@ -0,0 +1,60 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseBookshelfBlock extends BaseBlock {
public BaseBookshelfBlock(Block source) {
this(FabricBlockSettings.copyOf(source));
}
public BaseBookshelfBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
if (tool != null) {
int silk = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.SILK_TOUCH, tool);
if (silk > 0) {
return Collections.singletonList(new ItemStack(this));
}
}
return Collections.singletonList(new ItemStack(Items.BOOK, 3));
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_BOOKSHELF, replacePath(blockId));
return ModelsHelper.fromPattern(pattern);
}
private ResourceLocation replacePath(ResourceLocation blockId) {
String newPath = blockId.getPath().replace("_bookshelf", "");
return new ResourceLocation(blockId.getNamespace(), newPath);
}
}

View file

@ -0,0 +1,108 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.ButtonBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.AttachFace;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public abstract class BaseButtonBlock extends ButtonBlock implements BlockModelProvider {
private final Block parent;
protected BaseButtonBlock(Block parent, Properties properties, boolean sensitive) {
super(sensitive, properties);
this.parent = parent;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.ITEM_BUTTON, parentId);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation resourceLocation, BlockState blockState) {
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
Optional<String> pattern = blockState.getValue(POWERED)
? PatternsHelper.createJson(
BasePatterns.BLOCK_BUTTON_PRESSED,
parentId
)
: PatternsHelper.createJson(BasePatterns.BLOCK_BUTTON, parentId);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
String powered = blockState.getValue(POWERED) ? "_powered" : "";
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + powered);
registerBlockModel(stateId, modelId, blockState, modelCache);
AttachFace face = blockState.getValue(FACE);
boolean isCeiling = face == AttachFace.CEILING;
int x = 0, y = 0;
switch (face) {
case CEILING:
x = 180;
break;
case WALL:
x = 90;
break;
default:
break;
}
switch (blockState.getValue(FACING)) {
case NORTH:
if (isCeiling) {
y = 180;
}
break;
case EAST:
y = isCeiling ? 270 : 90;
break;
case SOUTH:
if (!isCeiling) {
y = 180;
}
break;
case WEST:
y = isCeiling ? 90 : 270;
break;
default:
break;
}
BlockModelRotation rotation = BlockModelRotation.by(x, y);
return ModelsHelper.createMultiVariant(modelId, rotation.getRotation(), face == AttachFace.WALL);
}
}

View file

@ -0,0 +1,75 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.ChainBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.MaterialColor;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseChainBlock extends ChainBlock implements BlockModelProvider, RenderLayerProvider {
public BaseChainBlock(MaterialColor color) {
this(FabricBlockSettings.copyOf(Blocks.CHAIN).color(color));
}
public BaseChainBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
return ModelsHelper.createItemModel(blockId);
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_CHAIN, blockId);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
Direction.Axis axis = blockState.getValue(AXIS);
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath());
registerBlockModel(stateId, modelId, blockState, modelCache);
return ModelsHelper.createRotatedModel(modelId, axis);
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
}

View file

@ -0,0 +1,62 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.registry.BaseBlockEntities;
import java.util.List;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseChestBlock extends ChestBlock implements BlockModelProvider {
private final Block parent;
public BaseChestBlock(Block source) {
super(FabricBlockSettings.copyOf(source).noOcclusion(), () -> BaseBlockEntities.CHEST);
this.parent = source;
}
@Override
public BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
return BaseBlockEntities.CHEST.create(blockPos, blockState);
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
List<ItemStack> drop = super.getDrops(state, builder);
drop.add(new ItemStack(this.asItem()));
return drop;
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.ITEM_CHEST, blockId);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation resourceLocation, BlockState blockState) {
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
return ModelsHelper.createBlockEmpty(parentId);
}
}

View file

@ -0,0 +1,75 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.ComposterBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseComposterBlock extends ComposterBlock implements BlockModelProvider {
public BaseComposterBlock(Block source) {
super(FabricBlockSettings.copyOf(source));
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this.asItem()));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_COMPOSTER, blockId);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath());
registerBlockModel(stateId, modelId, blockState, modelCache);
ModelsHelper.MultiPartBuilder builder = ModelsHelper.MultiPartBuilder.create(stateDefinition);
LEVEL.getPossibleValues().forEach(level -> {
if (level > 0) {
ResourceLocation contentId;
if (level > 7) {
contentId = new ResourceLocation("block/composter_contents_ready");
} else {
contentId = new ResourceLocation("block/composter_contents" + level);
}
builder.part(contentId).setCondition(state -> state.getValue(LEVEL).equals(level)).add();
}
});
builder.part(modelId).add();
return builder.build();
}
}

View file

@ -0,0 +1,68 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.CraftingTableBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseCraftingTableBlock extends CraftingTableBlock implements BlockModelProvider {
public BaseCraftingTableBlock(Block source) {
this(FabricBlockSettings.copyOf(source));
}
public BaseCraftingTableBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this.asItem()));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
String blockName = blockId.getPath();
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_SIDED, new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("%modid%", blockId.getNamespace());
put("%particle%", blockName + "_front");
put("%down%", blockName + "_bottom");
put("%up%", blockName + "_top");
put("%north%", blockName + "_front");
put("%south%", blockName + "_side");
put("%west%", blockName + "_front");
put("%east%", blockName + "_side");
}
});
return ModelsHelper.fromPattern(pattern);
}
}

View file

@ -0,0 +1,124 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.material.Material;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import com.google.common.collect.Lists;
import org.betterx.bclib.util.BlocksHelper;
import org.betterx.bclib.util.MHelper;
import java.util.Collections;
import java.util.List;
public class BaseCropBlock extends BasePlantBlock {
public static final IntegerProperty AGE = IntegerProperty.create("age", 0, 3);
private static final VoxelShape SHAPE = box(2, 0, 2, 14, 14, 14);
private final Block[] terrain;
private final Item drop;
public BaseCropBlock(Item drop, Block... terrain) {
this(
FabricBlockSettings.of(Material.PLANT)
.sound(SoundType.GRASS)
.randomTicks()
.noCollission()
.offsetType(BlockBehaviour.OffsetType.NONE),
drop, terrain
);
}
public BaseCropBlock(BlockBehaviour.Properties properties, Item drop, Block... terrain) {
super(properties.offsetType(BlockBehaviour.OffsetType.NONE));
this.drop = drop;
this.terrain = terrain;
this.registerDefaultState(defaultBlockState().setValue(AGE, 0));
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> stateManager) {
stateManager.add(AGE);
}
@Override
protected boolean isTerrain(BlockState state) {
for (Block block : terrain) {
if (state.is(block)) {
return true;
}
}
return false;
}
@Override
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
if (state.getValue(AGE) < 3) {
return Collections.singletonList(new ItemStack(this));
}
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
if (tool != null && tool.isCorrectToolForDrops(state)) {
int enchantment = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.BLOCK_FORTUNE, tool);
if (enchantment > 0) {
int countSeeds = MHelper.randRange(Mth.clamp(1 + enchantment, 1, 3), 3, MHelper.RANDOM_SOURCE);
int countDrops = MHelper.randRange(Mth.clamp(1 + enchantment, 1, 2), 2, MHelper.RANDOM_SOURCE);
return Lists.newArrayList(new ItemStack(this, countSeeds), new ItemStack(drop, countDrops));
}
}
int countSeeds = MHelper.randRange(1, 3, MHelper.RANDOM_SOURCE);
int countDrops = MHelper.randRange(1, 2, MHelper.RANDOM_SOURCE);
return Lists.newArrayList(new ItemStack(this, countSeeds), new ItemStack(drop, countDrops));
}
@Override
public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) {
int age = state.getValue(AGE);
if (age < 3) {
BlocksHelper.setWithUpdate(level, pos, state.setValue(AGE, age + 1));
}
}
@Override
public boolean isValidBonemealTarget(BlockGetter world, BlockPos pos, BlockState state, boolean isClient) {
return state.getValue(AGE) < 3;
}
@Override
public boolean isBonemealSuccess(Level level, RandomSource random, BlockPos pos, BlockState state) {
return state.getValue(AGE) < 3;
}
@Override
@SuppressWarnings("deprecation")
public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
super.tick(state, world, pos, random);
if (isBonemealSuccess(world, random, pos, state) && random.nextInt(8) == 0) {
performBonemeal(world, random, pos, state);
}
}
@Override
public VoxelShape getShape(BlockState state, BlockGetter view, BlockPos pos, CollisionContext ePos) {
return SHAPE;
}
}

View file

@ -0,0 +1,181 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.util.StringRepresentable;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.DoorHingeSide;
import net.minecraft.world.level.block.state.properties.DoubleBlockHalf;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.api.tag.NamedBlockTags;
import org.betterx.bclib.api.tag.NamedItemTags;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import org.betterx.bclib.interfaces.TagProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseDoorBlock extends DoorBlock implements RenderLayerProvider, BlockModelProvider, TagProvider {
public BaseDoorBlock(Block source) {
this(FabricBlockSettings.copyOf(source).strength(3F, 3F).noOcclusion());
}
public BaseDoorBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
if (state.getValue(HALF) == DoubleBlockHalf.LOWER)
return Collections.singletonList(new ItemStack(this.asItem()));
else return Collections.emptyList();
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation resourceLocation, BlockState blockState) {
DoorType doorType = getDoorType(blockState);
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_DOOR_BOTTOM, resourceLocation);
switch (doorType) {
case TOP_HINGE:
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_DOOR_TOP_HINGE, resourceLocation);
break;
case BOTTOM_HINGE:
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_DOOR_BOTTOM_HINGE, resourceLocation);
break;
case TOP:
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_DOOR_TOP, resourceLocation);
break;
default:
break;
}
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
Direction facing = blockState.getValue(FACING);
DoorType doorType = getDoorType(blockState);
boolean open = blockState.getValue(OPEN);
boolean hinge = doorType.isHinge();
BlockModelRotation rotation = BlockModelRotation.X0_Y0;
switch (facing) {
case EAST:
if (hinge && open) {
rotation = BlockModelRotation.X0_Y90;
} else if (open) {
rotation = BlockModelRotation.X0_Y270;
}
break;
case SOUTH:
if (!hinge && !open || hinge && !open) {
rotation = BlockModelRotation.X0_Y90;
} else if (hinge) {
rotation = BlockModelRotation.X0_Y180;
}
break;
case WEST:
if (!hinge && !open || hinge && !open) {
rotation = BlockModelRotation.X0_Y180;
} else if (hinge) {
rotation = BlockModelRotation.X0_Y270;
} else {
rotation = BlockModelRotation.X0_Y90;
}
break;
case NORTH:
default:
if (!hinge && !open || hinge && !open) {
rotation = BlockModelRotation.X0_Y270;
} else if (!hinge) {
rotation = BlockModelRotation.X0_Y180;
}
break;
}
ResourceLocation modelId = new ResourceLocation(
stateId.getNamespace(),
"block/" + stateId.getPath() + "_" + doorType
);
registerBlockModel(stateId, modelId, blockState, modelCache);
return ModelsHelper.createMultiVariant(modelId, rotation.getRotation(), false);
}
protected DoorType getDoorType(BlockState blockState) {
boolean isHinge = isHinge(blockState.getValue(HINGE), blockState.getValue(OPEN));
switch (blockState.getValue(HALF)) {
case UPPER: {
return isHinge ? DoorType.TOP_HINGE : DoorType.TOP;
}
case LOWER: {
return isHinge ? DoorType.BOTTOM_HINGE : DoorType.BOTTOM;
}
}
return DoorType.BOTTOM;
}
private boolean isHinge(DoorHingeSide hingeSide, boolean open) {
boolean isHinge = hingeSide == DoorHingeSide.RIGHT;
return isHinge && !open || !isHinge && open;
}
@Override
public void addTags(List<TagKey<Block>> blockTags, List<TagKey<Item>> itemTags) {
blockTags.add(NamedBlockTags.DOORS);
itemTags.add(NamedItemTags.DOORS);
}
protected enum DoorType implements StringRepresentable {
BOTTOM_HINGE("bottom_hinge"), TOP_HINGE("top_hinge"), BOTTOM("bottom"), TOP("top");
private final String name;
DoorType(String name) {
this.name = name;
}
public boolean isHinge() {
return this == BOTTOM_HINGE || this == TOP_HINGE;
}
@Override
public String toString() {
return getSerializedName();
}
@Override
public String getSerializedName() {
return name;
}
}
}

View file

@ -0,0 +1,166 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.BonemealableBlock;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.material.Material;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import com.google.common.collect.Lists;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import org.betterx.bclib.items.tool.BaseShearsItem;
import org.betterx.bclib.util.BlocksHelper;
import java.util.List;
public abstract class BaseDoublePlantBlock extends BaseBlockNotFull implements RenderLayerProvider, BonemealableBlock {
private static final VoxelShape SHAPE = box(4, 2, 4, 12, 16, 12);
public static final IntegerProperty ROTATION = BlockProperties.ROTATION;
public static final BooleanProperty TOP = BooleanProperty.create("top");
public BaseDoublePlantBlock() {
this(
FabricBlockSettings.of(Material.PLANT)
.sound(SoundType.GRASS)
.noCollission()
.offsetType(BlockBehaviour.OffsetType.XZ)
);
}
public BaseDoublePlantBlock(int light) {
this(
FabricBlockSettings.of(Material.PLANT)
.sound(SoundType.GRASS)
.lightLevel((state) -> state.getValue(TOP) ? light : 0)
.noCollission()
.offsetType(BlockBehaviour.OffsetType.XZ)
);
}
public BaseDoublePlantBlock(BlockBehaviour.Properties properties) {
super(properties);
this.registerDefaultState(this.stateDefinition.any().setValue(TOP, false));
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> stateManager) {
stateManager.add(TOP, ROTATION);
}
@Override
@SuppressWarnings("deprecation")
public VoxelShape getShape(BlockState state, BlockGetter view, BlockPos pos, CollisionContext ePos) {
Vec3 vec3d = state.getOffset(view, pos);
return SHAPE.move(vec3d.x, vec3d.y, vec3d.z);
}
@Override
@SuppressWarnings("deprecation")
public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
BlockState down = world.getBlockState(pos.below());
BlockState up = world.getBlockState(pos.above());
return state.getValue(TOP) ? down.getBlock() == this : isTerrain(down) && (up.getMaterial().isReplaceable());
}
public boolean canStayAt(BlockState state, LevelReader world, BlockPos pos) {
BlockState down = world.getBlockState(pos.below());
BlockState up = world.getBlockState(pos.above());
return state.getValue(TOP) ? down.getBlock() == this : isTerrain(down) && (up.getBlock() == this);
}
protected abstract boolean isTerrain(BlockState state);
@Override
@SuppressWarnings("deprecation")
public BlockState updateShape(BlockState state,
Direction facing,
BlockState neighborState,
LevelAccessor world,
BlockPos pos,
BlockPos neighborPos) {
if (!canStayAt(state, world, pos)) {
return Blocks.AIR.defaultBlockState();
} else {
return state;
}
}
@Override
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
if (state.getValue(TOP)) {
return Lists.newArrayList();
}
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
//TODO: 1.18.2 Test if shearing still works
if (tool != null && BaseShearsItem.isShear(tool) || EnchantmentHelper.getItemEnchantmentLevel(
Enchantments.SILK_TOUCH,
tool
) > 0) {
return Lists.newArrayList(new ItemStack(this));
} else {
return Lists.newArrayList();
}
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
@Override
public boolean isValidBonemealTarget(BlockGetter world, BlockPos pos, BlockState state, boolean isClient) {
return true;
}
@Override
public boolean isBonemealSuccess(Level level, RandomSource random, BlockPos pos, BlockState state) {
return true;
}
@Override
public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) {
ItemEntity item = new ItemEntity(
level,
pos.getX() + 0.5,
pos.getY() + 0.5,
pos.getZ() + 0.5,
new ItemStack(this)
);
level.addFreshEntity(item);
}
@Override
public void setPlacedBy(Level world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack itemStack) {
int rot = world.random.nextInt(4);
BlockState bs = this.defaultBlockState().setValue(ROTATION, rot);
BlocksHelper.setWithoutUpdate(world, pos, bs);
BlocksHelper.setWithoutUpdate(world, pos.above(), bs.setValue(TOP, true));
}
}

View file

@ -0,0 +1,97 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.FenceBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseFenceBlock extends FenceBlock implements BlockModelProvider {
private final Block parent;
public BaseFenceBlock(Block source) {
super(FabricBlockSettings.copyOf(source).noOcclusion());
this.parent = source;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.ITEM_FENCE, parentId);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
String path = blockId.getPath();
Optional<String> pattern = Optional.empty();
if (path.endsWith("_post")) {
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_FENCE_POST, parentId);
}
if (path.endsWith("_side")) {
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_FENCE_SIDE, parentId);
}
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
ResourceLocation postId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + "_post");
ResourceLocation sideId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + "_side");
registerBlockModel(postId, postId, blockState, modelCache);
registerBlockModel(sideId, sideId, blockState, modelCache);
ModelsHelper.MultiPartBuilder builder = ModelsHelper.MultiPartBuilder.create(stateDefinition);
builder.part(sideId).setCondition(state -> state.getValue(NORTH)).setUVLock(true).add();
builder.part(sideId)
.setCondition(state -> state.getValue(EAST))
.setTransformation(BlockModelRotation.X0_Y90.getRotation())
.setUVLock(true)
.add();
builder.part(sideId)
.setCondition(state -> state.getValue(SOUTH))
.setTransformation(BlockModelRotation.X0_Y180.getRotation())
.setUVLock(true)
.add();
builder.part(sideId)
.setCondition(state -> state.getValue(WEST))
.setTransformation(BlockModelRotation.X0_Y270.getRotation())
.setUVLock(true)
.add();
builder.part(postId).add();
return builder.build();
}
}

View file

@ -0,0 +1,140 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.stats.Stats;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.FurnaceBlock;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.betterx.bclib.blockentities.BaseFurnaceBlockEntity;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import org.betterx.bclib.registry.BaseBlockEntities;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseFurnaceBlock extends FurnaceBlock implements BlockModelProvider, RenderLayerProvider {
public BaseFurnaceBlock(Block source) {
this(FabricBlockSettings.copyOf(source).lightLevel(state -> state.getValue(LIT) ? 13 : 0));
}
public BaseFurnaceBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
public BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
return new BaseFurnaceBlockEntity(blockPos, blockState);
}
@Override
protected void openContainer(Level world, BlockPos pos, Player player) {
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof BaseFurnaceBlockEntity) {
player.openMenu((MenuProvider) blockEntity);
player.awardStat(Stats.INTERACT_WITH_FURNACE);
}
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
String blockName = blockId.getPath();
Map<String, String> textures = Maps.newHashMap();
textures.put("%modid%", blockId.getNamespace());
textures.put("%top%", blockName + "_top");
textures.put("%side%", blockName + "_side");
Optional<String> pattern;
if (blockState.getValue(LIT)) {
textures.put("%front%", blockName + "_front_on");
textures.put("%glow%", blockName + "_glow");
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_FURNACE_LIT, textures);
} else {
textures.put("%front%", blockName + "_front");
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_FURNACE, textures);
}
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
String lit = blockState.getValue(LIT) ? "_lit" : "";
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + lit);
registerBlockModel(stateId, modelId, blockState, modelCache);
return ModelsHelper.createFacingModel(modelId, blockState.getValue(FACING), false, true);
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
List<ItemStack> drop = Lists.newArrayList(new ItemStack(this));
BlockEntity blockEntity = builder.getOptionalParameter(LootContextParams.BLOCK_ENTITY);
if (blockEntity instanceof BaseFurnaceBlockEntity) {
BaseFurnaceBlockEntity entity = (BaseFurnaceBlockEntity) blockEntity;
for (int i = 0; i < entity.getContainerSize(); i++) {
drop.add(entity.getItem(i));
}
}
return drop;
}
@Override
@Nullable
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level,
BlockState blockState,
BlockEntityType<T> blockEntityType) {
return createFurnaceTicker(level, blockEntityType, BaseBlockEntities.FURNACE);
}
@Nullable
protected static <T extends BlockEntity> BlockEntityTicker<T> createFurnaceTicker(Level level,
BlockEntityType<T> blockEntityType,
BlockEntityType<? extends AbstractFurnaceBlockEntity> blockEntityType2) {
return level.isClientSide ? null : createTickerHelper(
blockEntityType,
blockEntityType2,
AbstractFurnaceBlockEntity::serverTick
);
}
}

View file

@ -0,0 +1,85 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.FenceGateBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseGateBlock extends FenceGateBlock implements BlockModelProvider {
private final Block parent;
public BaseGateBlock(Block source) {
super(FabricBlockSettings.copyOf(source).noOcclusion());
this.parent = source;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
boolean inWall = blockState.getValue(IN_WALL);
boolean isOpen = blockState.getValue(OPEN);
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
Optional<String> pattern;
if (inWall) {
pattern = isOpen
? PatternsHelper.createJson(
BasePatterns.BLOCK_GATE_OPEN_WALL,
parentId
)
: PatternsHelper.createJson(BasePatterns.BLOCK_GATE_CLOSED_WALL, parentId);
} else {
pattern = isOpen
? PatternsHelper.createJson(
BasePatterns.BLOCK_GATE_OPEN,
parentId
)
: PatternsHelper.createJson(BasePatterns.BLOCK_GATE_CLOSED, parentId);
}
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
boolean inWall = blockState.getValue(IN_WALL);
boolean isOpen = blockState.getValue(OPEN);
String state = "" + (inWall ? "_wall" : "") + (isOpen ? "_open" : "_closed");
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + state);
registerBlockModel(stateId, modelId, blockState, modelCache);
return ModelsHelper.createFacingModel(modelId, blockState.getValue(FACING), true, false);
}
}

View file

@ -1,8 +1,5 @@
package ru.bclib.blocks;
package org.betterx.bclib.blocks;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.item.ItemStack;
@ -13,9 +10,14 @@ import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import ru.bclib.client.render.BCLRenderLayer;
import ru.bclib.interfaces.RenderLayerProvider;
import ru.bclib.interfaces.tools.AddMineablePickaxe;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import org.betterx.bclib.interfaces.tools.AddMineablePickaxe;
import java.util.Collections;
import java.util.List;
@ -24,6 +26,7 @@ public class BaseGlassBlock extends BaseBlockNotFull implements AddMineablePicka
public BaseGlassBlock(Block block) {
this(block, 0.3f);
}
public BaseGlassBlock(Block block, float resistance) {
super(FabricBlockSettings.copyOf(block)
.resistance(resistance)
@ -44,7 +47,7 @@ public class BaseGlassBlock extends BaseBlockNotFull implements AddMineablePicka
@Environment(EnvType.CLIENT)
public boolean skipRendering(BlockState state, BlockState neighbor, Direction facing) {
return neighbor.getBlock() == this ? true : super.skipRendering(state, neighbor, facing);
return neighbor.getBlock() == this || super.skipRendering(state, neighbor, facing);
}
@Override

View file

@ -0,0 +1,72 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.LadderBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseLadderBlock extends LadderBlock implements RenderLayerProvider, BlockModelProvider {
public BaseLadderBlock(Block block) {
this(FabricBlockSettings.copyOf(block).noOcclusion());
}
public BaseLadderBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
return ModelsHelper.createBlockItem(blockId);
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_LADDER, blockId);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath());
registerBlockModel(stateId, modelId, blockState, modelCache);
return ModelsHelper.createFacingModel(modelId, blockState.getValue(FACING), false, true);
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
}

View file

@ -0,0 +1,118 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.LeavesBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.MaterialColor;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import com.google.common.collect.Lists;
import org.betterx.bclib.api.tag.NamedBlockTags;
import org.betterx.bclib.api.tag.NamedItemTags;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import org.betterx.bclib.interfaces.TagProvider;
import org.betterx.bclib.interfaces.tools.AddMineableHoe;
import org.betterx.bclib.interfaces.tools.AddMineableShears;
import org.betterx.bclib.items.tool.BaseShearsItem;
import org.betterx.bclib.util.MHelper;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
public class BaseLeavesBlock extends LeavesBlock implements BlockModelProvider, RenderLayerProvider, TagProvider, AddMineableShears, AddMineableHoe {
protected final Block sapling;
private static FabricBlockSettings makeLeaves(MaterialColor color) {
return FabricBlockSettings
.copyOf(Blocks.OAK_LEAVES)
.mapColor(color)
//.requiresTool()
.allowsSpawning((state, world, pos, type) -> false)
.suffocates((state, world, pos) -> false)
.blockVision((state, world, pos) -> false);
}
public BaseLeavesBlock(Block sapling, MaterialColor color, Consumer<FabricBlockSettings> customizeProperties) {
super(BaseBlock.acceptAndReturn(customizeProperties, makeLeaves(color)));
this.sapling = sapling;
}
public BaseLeavesBlock(Block sapling,
MaterialColor color,
int light,
Consumer<FabricBlockSettings> customizeProperties) {
super(BaseBlock.acceptAndReturn(customizeProperties, makeLeaves(color).luminance(light)));
this.sapling = sapling;
}
public BaseLeavesBlock(Block sapling, MaterialColor color) {
super(makeLeaves(color));
this.sapling = sapling;
}
public BaseLeavesBlock(Block sapling, MaterialColor color, int light) {
super(makeLeaves(color).lightLevel(light));
this.sapling = sapling;
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return BaseLeavesBlock.getLeaveDrops(this, this.sapling, builder, 16, 16);
}
public static List<ItemStack> getLeaveDrops(ItemLike leaveBlock,
Block sapling,
LootContext.Builder builder,
int fortuneRate,
int dropRate) {
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
if (tool != null) {
if (BaseShearsItem.isShear(tool) || EnchantmentHelper.getItemEnchantmentLevel(
Enchantments.SILK_TOUCH,
tool
) > 0) {
return Collections.singletonList(new ItemStack(leaveBlock));
}
int fortune = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.BLOCK_FORTUNE, tool);
if (MHelper.RANDOM.nextInt(fortuneRate) <= fortune) {
return Lists.newArrayList(new ItemStack(sapling));
}
return Lists.newArrayList();
}
return MHelper.RANDOM.nextInt(dropRate) == 0
? Lists.newArrayList(new ItemStack(sapling))
: Lists.newArrayList();
}
@Override
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
@Override
public void addTags(List<TagKey<Block>> blockTags, List<TagKey<Item>> itemTags) {
blockTags.add(NamedBlockTags.LEAVES);
itemTags.add(NamedItemTags.LEAVES);
}
}

View file

@ -0,0 +1,128 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.IronBarsBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseMetalBarsBlock extends IronBarsBlock implements BlockModelProvider, RenderLayerProvider {
public BaseMetalBarsBlock(Block source) {
this(FabricBlockSettings.copyOf(source).strength(5.0F, 6.0F).noOcclusion());
}
public BaseMetalBarsBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
public Optional<String> getModelString(String block) {
ResourceLocation blockId = Registry.BLOCK.getKey(this);
if (block.contains("item")) {
return PatternsHelper.createJson(BasePatterns.ITEM_BLOCK, blockId);
}
if (block.contains("post")) {
return PatternsHelper.createJson(BasePatterns.BLOCK_BARS_POST, blockId);
} else {
return PatternsHelper.createJson(BasePatterns.BLOCK_BARS_SIDE, blockId);
}
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return ModelsHelper.createBlockItem(resourceLocation);
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
ResourceLocation thisId = Registry.BLOCK.getKey(this);
String path = blockId.getPath();
Optional<String> pattern = Optional.empty();
if (path.endsWith("_post")) {
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_BARS_POST, thisId);
}
if (path.endsWith("_side")) {
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_BARS_SIDE, thisId);
}
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
ResourceLocation postId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + "_post");
ResourceLocation sideId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + "_side");
registerBlockModel(postId, postId, blockState, modelCache);
registerBlockModel(sideId, sideId, blockState, modelCache);
ModelsHelper.MultiPartBuilder builder = ModelsHelper.MultiPartBuilder.create(stateDefinition);
builder.part(postId)
.setCondition(state -> !state.getValue(NORTH) && !state.getValue(EAST) && !state.getValue(SOUTH) && !state
.getValue(WEST))
.add();
builder.part(sideId).setCondition(state -> state.getValue(NORTH)).setUVLock(true).add();
builder.part(sideId)
.setCondition(state -> state.getValue(EAST))
.setTransformation(BlockModelRotation.X0_Y90.getRotation())
.setUVLock(true)
.add();
builder.part(sideId)
.setCondition(state -> state.getValue(SOUTH))
.setTransformation(BlockModelRotation.X0_Y180.getRotation())
.setUVLock(true)
.add();
builder.part(sideId)
.setCondition(state -> state.getValue(WEST))
.setTransformation(BlockModelRotation.X0_Y270.getRotation())
.setUVLock(true)
.add();
return builder.build();
}
@Environment(EnvType.CLIENT)
public boolean skipRendering(BlockState state, BlockState stateFrom, Direction direction) {
if (direction.getAxis().isVertical() && stateFrom.getBlock() == this && !stateFrom.equals(state)) {
return false;
}
return super.skipRendering(state, stateFrom, direction);
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
}

View file

@ -0,0 +1,125 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.util.valueproviders.UniformInt;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TieredItem;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.DropExperienceBlock;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.Material;
import net.minecraft.world.level.material.MaterialColor;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.util.LootUtil;
import org.betterx.bclib.util.MHelper;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
public class BaseOreBlock extends DropExperienceBlock implements BlockModelProvider {
private final Supplier<Item> dropItem;
private final int minCount;
private final int maxCount;
private final int miningLevel;
public BaseOreBlock(Supplier<Item> drop, int minCount, int maxCount, int experience) {
this(drop, minCount, maxCount, experience, 0);
}
public BaseOreBlock(Supplier<Item> drop, int minCount, int maxCount, int experience, int miningLevel) {
this(
FabricBlockSettings
.of(Material.STONE, MaterialColor.SAND)
.requiresTool()
.destroyTime(3F)
.explosionResistance(9F)
.sound(SoundType.STONE),
drop, minCount, maxCount, experience, miningLevel
);
}
public BaseOreBlock(Properties properties, Supplier<Item> drop, int minCount, int maxCount, int experience) {
this(properties, drop, minCount, maxCount, experience, 0);
}
public BaseOreBlock(Properties properties,
Supplier<Item> drop,
int minCount,
int maxCount,
int experience,
int miningLevel) {
super(properties, UniformInt.of(experience > 0 ? 1 : 0, experience));
this.dropItem = drop;
this.minCount = minCount;
this.maxCount = maxCount;
this.miningLevel = miningLevel;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return LootUtil
.getDrops(this, state, builder)
.orElseGet(
() -> BaseOreBlock.getDroppedItems(this,
dropItem.get(),
maxCount,
minCount,
miningLevel,
state,
builder)
);
}
public static List<ItemStack> getDroppedItems(ItemLike block,
Item dropItem,
int maxCount,
int minCount,
int miningLevel,
BlockState state,
LootContext.Builder builder) {
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
if (tool != null && tool.isCorrectToolForDrops(state)) {
boolean canMine = miningLevel == 0;
if (tool.getItem() instanceof TieredItem tired) {
canMine = tired.getTier().getLevel() >= miningLevel;
}
if (canMine) {
if (EnchantmentHelper.getItemEnchantmentLevel(Enchantments.SILK_TOUCH, tool) > 0) {
return Collections.singletonList(new ItemStack(block));
}
int count;
int enchantment = EnchantmentHelper.getItemEnchantmentLevel(Enchantments.BLOCK_FORTUNE, tool);
if (enchantment > 0) {
int min = Mth.clamp(minCount + enchantment, minCount, maxCount);
int max = maxCount + (enchantment / Enchantments.BLOCK_FORTUNE.getMaxLevel());
if (min == max) {
return Collections.singletonList(new ItemStack(dropItem, max));
}
count = MHelper.randRange(min, max, MHelper.RANDOM_SOURCE);
} else {
count = MHelper.randRange(minCount, maxCount, MHelper.RANDOM_SOURCE);
}
return Collections.singletonList(new ItemStack(dropItem, count));
}
}
return Collections.emptyList();
}
@Override
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
}

View file

@ -0,0 +1,101 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import com.google.common.collect.Maps;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BasePathBlock extends BaseBlockNotFull {
private static final VoxelShape SHAPE = box(0, 0, 0, 16, 15, 16);
private Block baseBlock;
public BasePathBlock(Block source) {
super(FabricBlockSettings.copyOf(source).isValidSpawn((state, world, pos, type) -> false));
this.baseBlock = Blocks.DIRT;
if (source instanceof BaseTerrainBlock) {
BaseTerrainBlock terrain = (BaseTerrainBlock) source;
this.baseBlock = terrain.getBaseBlock();
terrain.setPathBlock(this);
}
}
@Override
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
if (tool != null && EnchantmentHelper.getItemEnchantmentLevel(Enchantments.SILK_TOUCH, tool) > 0) {
return Collections.singletonList(new ItemStack(this));
}
return Collections.singletonList(new ItemStack(Blocks.END_STONE));
}
@Override
@SuppressWarnings("deprecation")
public VoxelShape getShape(BlockState state, BlockGetter view, BlockPos pos, CollisionContext ePos) {
return SHAPE;
}
@Override
@SuppressWarnings("deprecation")
public VoxelShape getCollisionShape(BlockState state, BlockGetter view, BlockPos pos, CollisionContext ePos) {
return SHAPE;
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
return getBlockModel(blockId, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
String name = blockId.getPath();
ResourceLocation bottomId = Registry.BLOCK.getKey(baseBlock);
String bottom = bottomId.getNamespace() + ":block/" + bottomId.getPath();
Map<String, String> textures = Maps.newHashMap();
textures.put("%modid%", blockId.getNamespace());
textures.put("%top%", name + "_top");
textures.put("%side%", name.replace("_path", "") + "_side");
textures.put("%bottom%", bottom);
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_PATH, textures);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath());
registerBlockModel(stateId, modelId, blockState, modelCache);
return ModelsHelper.createRandomTopModel(modelId);
}
}

View file

@ -0,0 +1,181 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.BonemealableBlock;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.Material;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import com.google.common.collect.Lists;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import org.betterx.bclib.items.tool.BaseShearsItem;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.jetbrains.annotations.Nullable;
public abstract class BasePlantBlock extends BaseBlockNotFull implements RenderLayerProvider, BonemealableBlock {
private static final VoxelShape SHAPE = box(4, 0, 4, 12, 14, 12);
public BasePlantBlock() {
this(false, p -> p);
}
public BasePlantBlock(int light) {
this(light, p -> p);
}
public BasePlantBlock(int light, Function<Properties, Properties> propMod) {
this(false, light, propMod);
}
public BasePlantBlock(boolean replaceabled) {
this(replaceabled, p -> p);
}
public BasePlantBlock(boolean replaceable, Function<Properties, Properties> propMod) {
this(
propMod.apply(FabricBlockSettings
.of(replaceable ? Material.REPLACEABLE_PLANT : Material.PLANT)
.sound(SoundType.GRASS)
.noCollission()
.offsetType(BlockBehaviour.OffsetType.XZ)
)
);
}
public BasePlantBlock(boolean replaceable, int light) {
this(replaceable, light, p -> p);
}
public BasePlantBlock(boolean replaceable, int light, Function<Properties, Properties> propMod) {
this(
propMod.apply(FabricBlockSettings
.of(replaceable ? Material.REPLACEABLE_PLANT : Material.PLANT)
.luminance(light)
.sound(SoundType.GRASS)
.noCollission()
.offsetType(BlockBehaviour.OffsetType.XZ)
)
);
}
public BasePlantBlock(Properties settings) {
super(settings.offsetType(BlockBehaviour.OffsetType.XZ));
}
protected abstract boolean isTerrain(BlockState state);
@Override
@SuppressWarnings("deprecation")
public VoxelShape getShape(BlockState state, BlockGetter view, BlockPos pos, CollisionContext ePos) {
Vec3 vec3d = state.getOffset(view, pos);
return SHAPE.move(vec3d.x, vec3d.y, vec3d.z);
}
@Override
@SuppressWarnings("deprecation")
public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
BlockState down = world.getBlockState(pos.below());
return isTerrain(down);
}
@Override
@SuppressWarnings("deprecation")
public BlockState updateShape(BlockState state,
Direction facing,
BlockState neighborState,
LevelAccessor world,
BlockPos pos,
BlockPos neighborPos) {
if (!canSurvive(state, world, pos)) {
return Blocks.AIR.defaultBlockState();
} else {
return state;
}
}
@Override
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
//TODO: 1.18.2 Test if shearing still works
if (tool != null && BaseShearsItem.isShear(tool) || EnchantmentHelper.getItemEnchantmentLevel(
Enchantments.SILK_TOUCH,
tool
) > 0) {
return Lists.newArrayList(new ItemStack(this));
} else {
return Lists.newArrayList();
}
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
@Override
public boolean isValidBonemealTarget(BlockGetter world, BlockPos pos, BlockState state, boolean isClient) {
return true;
}
@Override
public boolean isBonemealSuccess(Level level, RandomSource random, BlockPos pos, BlockState state) {
return true;
}
@Override
public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) {
ItemEntity item = new ItemEntity(
level,
pos.getX() + 0.5,
pos.getY() + 0.5,
pos.getZ() + 0.5,
new ItemStack(this)
);
level.addFreshEntity(item);
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return ModelsHelper.createBlockItem(resourceLocation);
}
@Override
@Nullable
@Environment(EnvType.CLIENT)
public BlockModel getBlockModel(ResourceLocation resourceLocation, BlockState blockState) {
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_CROSS, resourceLocation);
return ModelsHelper.fromPattern(pattern);
}
}

View file

@ -0,0 +1,69 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.material.Material;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import java.util.function.Function;
public abstract class BasePlantWithAgeBlock extends BasePlantBlock {
public static final IntegerProperty AGE = BlockProperties.AGE;
public BasePlantWithAgeBlock() {
this(p -> p);
}
public BasePlantWithAgeBlock(Function<Properties, Properties> propMod) {
this(
propMod.apply(FabricBlockSettings.of(Material.PLANT)
.sound(SoundType.GRASS)
.randomTicks()
.noCollission())
);
}
public BasePlantWithAgeBlock(Properties settings) {
super(settings);
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> stateManager) {
stateManager.add(AGE);
}
public abstract void growAdult(WorldGenLevel world, RandomSource random, BlockPos pos);
@Override
public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) {
int age = state.getValue(AGE);
if (age < 3) {
level.setBlockAndUpdate(pos, state.setValue(AGE, age + 1));
} else {
growAdult(level, random, pos);
}
}
@Override
public boolean isBonemealSuccess(Level level, RandomSource random, BlockPos pos, BlockState state) {
return true;
}
@Override
@SuppressWarnings("deprecation")
public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
super.tick(state, world, pos, random);
if (random.nextInt(8) == 0) {
performBonemeal(world, random, pos, state);
}
}
}

View file

@ -0,0 +1,71 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.PressurePlateBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BasePressurePlateBlock extends PressurePlateBlock implements BlockModelProvider {
private final Block parent;
public BasePressurePlateBlock(Sensitivity rule, Block source) {
super(rule, FabricBlockSettings.copyOf(source).noCollission().noOcclusion().strength(0.5F));
this.parent = source;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation resourceLocation, BlockState blockState) {
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
Optional<String> pattern;
if (blockState.getValue(POWERED)) {
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_PLATE_DOWN, parentId);
} else {
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_PLATE_UP, parentId);
}
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
String state = blockState.getValue(POWERED) ? "_down" : "_up";
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + state);
registerBlockModel(stateId, modelId, blockState, modelCache);
return ModelsHelper.createBlockSimple(modelId);
}
}

View file

@ -0,0 +1,67 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.RotatedPillarBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseRotatedPillarBlock extends RotatedPillarBlock implements BlockModelProvider {
public BaseRotatedPillarBlock(Properties settings) {
super(settings);
}
public BaseRotatedPillarBlock(Block block) {
this(FabricBlockSettings.copyOf(block));
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
return getBlockModel(blockId, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
Optional<String> pattern = createBlockPattern(blockId);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath());
registerBlockModel(stateId, modelId, blockState, modelCache);
return ModelsHelper.createRotatedModel(modelId, blockState.getValue(AXIS));
}
protected Optional<String> createBlockPattern(ResourceLocation blockId) {
return PatternsHelper.createBlockPillar(blockId);
}
}

View file

@ -0,0 +1,198 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.network.protocol.game.ClientboundOpenSignEditorPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.block.state.properties.WoodType;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.Fluids;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.item.v1.FabricItemSettings;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.blockentities.BaseSignBlockEntity;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.CustomItemProvider;
import org.betterx.bclib.util.BlocksHelper;
import java.util.Collections;
import java.util.List;
import org.jetbrains.annotations.Nullable;
@SuppressWarnings("deprecation")
public class BaseSignBlock extends SignBlock implements BlockModelProvider, CustomItemProvider {
public static final IntegerProperty ROTATION = BlockStateProperties.ROTATION_16;
public static final BooleanProperty FLOOR = BooleanProperty.create("floor");
private static final VoxelShape[] WALL_SHAPES = new VoxelShape[]{
Block.box(0.0D, 4.5D, 14.0D, 16.0D, 12.5D, 16.0D),
Block.box(0.0D, 4.5D, 0.0D, 2.0D, 12.5D, 16.0D),
Block.box(0.0D, 4.5D, 0.0D, 16.0D, 12.5D, 2.0D),
Block.box(14.0D, 4.5D, 0.0D, 16.0D, 12.5D, 16.0D)
};
private final Block parent;
public BaseSignBlock(Block source) {
super(FabricBlockSettings.copyOf(source).strength(1.0F, 1.0F).noCollission().noOcclusion(), WoodType.OAK);
this.registerDefaultState(this.stateDefinition.any()
.setValue(ROTATION, 0)
.setValue(FLOOR, false)
.setValue(WATERLOGGED, false));
this.parent = source;
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(ROTATION, FLOOR, WATERLOGGED);
}
@Override
public VoxelShape getShape(BlockState state, BlockGetter view, BlockPos pos, CollisionContext ePos) {
return state.getValue(FLOOR) ? SHAPE : WALL_SHAPES[state.getValue(ROTATION) >> 2];
}
@Override
public BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) {
return new BaseSignBlockEntity(blockPos, blockState);
}
@Override
public void setPlacedBy(Level world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack itemStack) {
if (placer instanceof Player) {
BaseSignBlockEntity sign = (BaseSignBlockEntity) world.getBlockEntity(pos);
if (sign != null) {
if (!world.isClientSide) {
sign.setAllowedPlayerEditor(placer.getUUID());
((ServerPlayer) placer).connection.send(new ClientboundOpenSignEditorPacket(pos));
} else {
sign.setEditable(true);
}
}
}
}
@Override
public BlockState updateShape(BlockState state,
Direction facing,
BlockState neighborState,
LevelAccessor world,
BlockPos pos,
BlockPos neighborPos) {
if (state.getValue(WATERLOGGED)) {
world.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(world));
}
if (!canSurvive(state, world, pos)) {
return state.getValue(WATERLOGGED)
? state.getFluidState().createLegacyBlock()
: Blocks.AIR.defaultBlockState();
}
return super.updateShape(state, facing, neighborState, world, pos, neighborPos);
}
@Override
public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
if (!state.getValue(FLOOR)) {
int index = (((state.getValue(ROTATION) >> 2) + 2)) & 3;
return world.getBlockState(pos.relative(BlocksHelper.HORIZONTAL[index])).getMaterial().isSolid();
} else {
return world.getBlockState(pos.below()).getMaterial().isSolid();
}
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext ctx) {
if (ctx.getClickedFace() == Direction.UP) {
FluidState fluidState = ctx.getLevel().getFluidState(ctx.getClickedPos());
return this
.defaultBlockState()
.setValue(FLOOR, true)
.setValue(ROTATION, Mth.floor((180.0 + ctx.getRotation() * 16.0 / 360.0) + 0.5 - 12) & 15)
.setValue(WATERLOGGED, fluidState.getType() == Fluids.WATER);
} else if (ctx.getClickedFace() != Direction.DOWN) {
BlockState blockState = this.defaultBlockState();
FluidState fluidState = ctx.getLevel().getFluidState(ctx.getClickedPos());
LevelReader worldView = ctx.getLevel();
BlockPos blockPos = ctx.getClickedPos();
Direction[] directions = ctx.getNearestLookingDirections();
for (Direction direction : directions) {
if (direction.getAxis().isHorizontal()) {
Direction dir = direction.getOpposite();
int rot = Mth.floor((180.0 + dir.toYRot() * 16.0 / 360.0) + 0.5 + 4) & 15;
blockState = blockState.setValue(ROTATION, rot);
if (blockState.canSurvive(worldView, blockPos)) {
return blockState.setValue(FLOOR, false)
.setValue(WATERLOGGED, fluidState.getType() == Fluids.WATER);
}
}
}
}
return null;
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation resourceLocation, BlockState blockState) {
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
return ModelsHelper.createBlockEmpty(parentId);
}
@Override
public BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(ROTATION, rotation.rotate(state.getValue(ROTATION), 16));
}
@Override
public BlockState mirror(BlockState state, Mirror mirror) {
return state.setValue(ROTATION, mirror.mirror(state.getValue(ROTATION), 16));
}
@Override
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
@Override
public boolean canPlaceLiquid(BlockGetter world, BlockPos pos, BlockState state, Fluid fluid) {
return super.canPlaceLiquid(world, pos, state, fluid);
}
@Override
public boolean placeLiquid(LevelAccessor world, BlockPos pos, BlockState state, FluidState fluidState) {
return super.placeLiquid(world, pos, state, fluidState);
}
@Override
public BlockItem getCustomItem(ResourceLocation blockID, FabricItemSettings settings) {
return new BlockItem(this, settings.stacksTo(16));
}
}

View file

@ -0,0 +1,95 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.SlabBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.SlabType;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.item.v1.FabricItemSettings;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.CustomItemProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseSlabBlock extends SlabBlock implements BlockModelProvider, CustomItemProvider {
private final Block parent;
public final boolean fireproof;
public BaseSlabBlock(Block source) {
this(source, false);
}
public BaseSlabBlock(Block source, boolean fireproof) {
super(FabricBlockSettings.copyOf(source));
this.parent = source;
this.fireproof = fireproof;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
int count = state.getValue(TYPE) == SlabType.DOUBLE ? 2 : 1;
return Collections.singletonList(new ItemStack(this, count));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
Optional<String> pattern;
if (blockState.getValue(TYPE) == SlabType.DOUBLE) {
pattern = PatternsHelper.createBlockSimple(parentId);
} else {
pattern = PatternsHelper.createJson(BasePatterns.BLOCK_SLAB, parentId);
}
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
SlabType type = blockState.getValue(TYPE);
ResourceLocation modelId = new ResourceLocation(
stateId.getNamespace(),
"block/" + stateId.getPath() + "_" + type
);
registerBlockModel(stateId, modelId, blockState, modelCache);
if (type == SlabType.TOP) {
return ModelsHelper.createMultiVariant(modelId, BlockModelRotation.X180_Y0.getRotation(), true);
}
return ModelsHelper.createBlockSimple(modelId);
}
@Override
public BlockItem getCustomItem(ResourceLocation blockID, FabricItemSettings settings) {
if (fireproof) settings = settings.fireproof();
return new BlockItem(this, settings);
}
}

View file

@ -0,0 +1,119 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.StairBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.block.state.properties.StairsShape;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.item.v1.FabricItemSettings;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.CustomItemProvider;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
public class BaseStairsBlock extends StairBlock implements BlockModelProvider, CustomItemProvider {
private final Block parent;
public final boolean fireproof;
public BaseStairsBlock(Block source) {
this(source, false);
}
public BaseStairsBlock(Block source, boolean fireproof) {
super(source.defaultBlockState(), FabricBlockSettings.copyOf(source));
this.parent = source;
this.fireproof = fireproof;
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
ResourceLocation parentId = Registry.BLOCK.getKey(parent);
Optional<String> pattern = PatternsHelper.createJson(switch (blockState.getValue(SHAPE)) {
case STRAIGHT -> BasePatterns.BLOCK_STAIR;
case INNER_LEFT, INNER_RIGHT -> BasePatterns.BLOCK_STAIR_INNER;
case OUTER_LEFT, OUTER_RIGHT -> BasePatterns.BLOCK_STAIR_OUTER;
}, parentId);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
String state;
StairsShape shape = blockState.getValue(SHAPE);
state = switch (shape) {
case INNER_LEFT, INNER_RIGHT -> "_inner";
case OUTER_LEFT, OUTER_RIGHT -> "_outer";
default -> "";
};
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath() + state);
registerBlockModel(stateId, modelId, blockState, modelCache);
boolean isTop = blockState.getValue(HALF) == Half.TOP;
boolean isLeft = shape == StairsShape.INNER_LEFT || shape == StairsShape.OUTER_LEFT;
boolean isRight = shape == StairsShape.INNER_RIGHT || shape == StairsShape.OUTER_RIGHT;
int y = 0;
int x = isTop ? 180 : 0;
switch (blockState.getValue(FACING)) {
case NORTH:
if (isTop && !isRight) y = 270;
else if (!isTop) y = isLeft ? 180 : 270;
break;
case EAST:
if (isTop && isRight) y = 90;
else if (!isTop && isLeft) y = 270;
break;
case SOUTH:
if (isTop) y = isRight ? 180 : 90;
else if (!isLeft) y = 90;
break;
case WEST:
default:
y = (isTop && isRight) ? 270 : (!isTop && isLeft) ? 90 : 180;
break;
}
BlockModelRotation rotation = BlockModelRotation.by(x, y);
return ModelsHelper.createMultiVariant(modelId, rotation.getRotation(), true);
}
@Override
public BlockItem getCustomItem(ResourceLocation blockID, FabricItemSettings settings) {
if (fireproof) settings = settings.fireproof();
return new BlockItem(this, settings);
}
}

View file

@ -0,0 +1,18 @@
package org.betterx.bclib.blocks;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.level.block.Block;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
public class BaseStoneButtonBlock extends BaseButtonBlock {
public BaseStoneButtonBlock(Block source) {
super(source, FabricBlockSettings.copyOf(source).noOcclusion(), false);
}
@Override
protected SoundEvent getSound(boolean clicked) {
return clicked ? SoundEvents.STONE_BUTTON_CLICK_ON : SoundEvents.STONE_BUTTON_CLICK_OFF;
}
}

View file

@ -0,0 +1,54 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.RotatedPillarBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.MaterialColor;
import net.minecraft.world.phys.BlockHitResult;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.api.tag.NamedMineableTags;
import org.betterx.bclib.api.tag.TagAPI;
public class BaseStripableLogBlock extends BaseRotatedPillarBlock {
private final Block striped;
public BaseStripableLogBlock(MaterialColor color, Block striped) {
super(FabricBlockSettings.copyOf(striped).color(color));
this.striped = striped;
}
@Override
@SuppressWarnings("deprecation")
public InteractionResult use(BlockState state,
Level world,
BlockPos pos,
Player player,
InteractionHand hand,
BlockHitResult hit) {
if (TagAPI.isToolWithMineableTag(player.getMainHandItem(), NamedMineableTags.AXE)) {
world.playSound(player, pos, SoundEvents.AXE_STRIP, SoundSource.BLOCKS, 1.0F, 1.0F);
if (!world.isClientSide) {
world.setBlock(pos,
striped.defaultBlockState()
.setValue(RotatedPillarBlock.AXIS, state.getValue(RotatedPillarBlock.AXIS)),
11
);
if (!player.isCreative()) {
player.getMainHandItem().hurt(1, world.random, (ServerPlayer) player);
}
}
return InteractionResult.SUCCESS;
}
return InteractionResult.FAIL;
}
}

View file

@ -0,0 +1,162 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.SnowLayerBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.lighting.LayerLightEngine;
import net.minecraft.world.level.material.MaterialColor;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.phys.BlockHitResult;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import com.google.common.collect.Maps;
import org.betterx.bclib.api.tag.NamedMineableTags;
import org.betterx.bclib.api.tag.TagAPI;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.client.sound.BlockSounds;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
@SuppressWarnings("deprecation")
public class BaseTerrainBlock extends BaseBlock {
private final Block baseBlock;
private Block pathBlock;
public BaseTerrainBlock(Block baseBlock, MaterialColor color) {
super(FabricBlockSettings
.copyOf(baseBlock)
.materialColor(color)
.sound(BlockSounds.TERRAIN_SOUND)
.randomTicks()
);
this.baseBlock = baseBlock;
}
public void setPathBlock(Block roadBlock) {
this.pathBlock = roadBlock;
}
public Block getBaseBlock() {
return baseBlock;
}
@Override
public InteractionResult use(BlockState state,
Level world,
BlockPos pos,
Player player,
InteractionHand hand,
BlockHitResult hit) {
//TODO: 1.18.2 check
if (pathBlock != null && TagAPI.isToolWithMineableTag(player.getMainHandItem(), NamedMineableTags.SHOVEL)) {
//if (pathBlock != null && FabricTagProvider.SHOVELS.contains(player.getMainHandItem().getItem())) {
world.playSound(player, pos, SoundEvents.SHOVEL_FLATTEN, SoundSource.BLOCKS, 1.0F, 1.0F);
if (!world.isClientSide) {
world.setBlockAndUpdate(pos, pathBlock.defaultBlockState());
if (!player.isCreative()) {
player.getMainHandItem().hurt(1, world.random, (ServerPlayer) player);
}
}
return InteractionResult.SUCCESS;
}
return InteractionResult.FAIL;
}
@Override
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
if (tool != null && EnchantmentHelper.getItemEnchantmentLevel(Enchantments.SILK_TOUCH, tool) > 0) {
return Collections.singletonList(new ItemStack(this));
}
return Collections.singletonList(new ItemStack(getBaseBlock()));
}
@Override
public void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
if (random.nextInt(16) == 0 && !canStay(state, world, pos)) {
world.setBlockAndUpdate(pos, getBaseBlock().defaultBlockState());
}
}
public boolean canStay(BlockState state, LevelReader worldView, BlockPos pos) {
BlockPos blockPos = pos.above();
BlockState blockState = worldView.getBlockState(blockPos);
if (blockState.is(Blocks.SNOW) && blockState.getValue(SnowLayerBlock.LAYERS) == 1) {
return true;
} else if (blockState.getFluidState().getAmount() == 8) {
return false;
} else {
int i = LayerLightEngine.getLightBlockInto(
worldView,
state,
pos,
blockState,
blockPos,
Direction.UP,
blockState.getLightBlock(worldView, blockPos)
);
return i < 5;
}
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation blockId) {
return getBlockModel(blockId, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation blockId, BlockState blockState) {
ResourceLocation baseId = Registry.BLOCK.getKey(getBaseBlock());
String modId = blockId.getNamespace();
String path = blockId.getPath();
String bottom = baseId.getNamespace() + ":block/" + baseId.getPath();
Map<String, String> textures = Maps.newHashMap();
textures.put("%top%", modId + ":block/" + path + "_top");
textures.put("%side%", modId + ":block/" + path + "_side");
textures.put("%bottom%", bottom);
Optional<String> pattern = PatternsHelper.createJson(BasePatterns.BLOCK_TOP_SIDE_BOTTOM, textures);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath());
registerBlockModel(stateId, modelId, blockState, modelCache);
return ModelsHelper.createRandomTopModel(modelId);
}
}

View file

@ -0,0 +1,104 @@
package org.betterx.bclib.blocks;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.level.storage.loot.LootContext;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import org.betterx.bclib.client.models.BasePatterns;
import org.betterx.bclib.client.models.ModelsHelper;
import org.betterx.bclib.client.models.PatternsHelper;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.BlockModelProvider;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import java.util.*;
import org.jetbrains.annotations.Nullable;
public class BaseTrapdoorBlock extends TrapDoorBlock implements RenderLayerProvider, BlockModelProvider {
public BaseTrapdoorBlock(Block source) {
this(FabricBlockSettings.copyOf(source).strength(3.0F, 3.0F).noOcclusion());
}
public BaseTrapdoorBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
@SuppressWarnings("deprecation")
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
return Collections.singletonList(new ItemStack(this));
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
@Override
@Environment(EnvType.CLIENT)
public BlockModel getItemModel(ResourceLocation resourceLocation) {
return getBlockModel(resourceLocation, defaultBlockState());
}
@Override
@Environment(EnvType.CLIENT)
public @Nullable BlockModel getBlockModel(ResourceLocation resourceLocation, BlockState blockState) {
String name = resourceLocation.getPath();
Optional<String> pattern = PatternsHelper.createJson(
BasePatterns.BLOCK_TRAPDOOR,
new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("%modid%", resourceLocation.getNamespace());
put("%texture%", name);
put("%side%", name.replace("trapdoor", "door_side"));
}
}
);
return ModelsHelper.fromPattern(pattern);
}
@Override
@Environment(EnvType.CLIENT)
public UnbakedModel getModelVariant(ResourceLocation stateId,
BlockState blockState,
Map<ResourceLocation, UnbakedModel> modelCache) {
ResourceLocation modelId = new ResourceLocation(stateId.getNamespace(), "block/" + stateId.getPath());
registerBlockModel(stateId, modelId, blockState, modelCache);
boolean isTop = blockState.getValue(HALF) == Half.TOP;
boolean isOpen = blockState.getValue(OPEN);
int y = 0;
int x = (isTop && isOpen) ? 270 : isTop ? 180 : isOpen ? 90 : 0;
switch (blockState.getValue(FACING)) {
case EAST:
y = (isTop && isOpen) ? 270 : 90;
break;
case NORTH:
if (isTop && isOpen) y = 180;
break;
case SOUTH:
y = (isTop && isOpen) ? 0 : 180;
break;
case WEST:
y = (isTop && isOpen) ? 90 : 270;
break;
default:
break;
}
BlockModelRotation rotation = BlockModelRotation.by(x, y);
return ModelsHelper.createMultiVariant(modelId, rotation.getRotation(), false);
}
}

View file

@ -0,0 +1,61 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.LiquidBlockContainer;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.Fluids;
import net.minecraft.world.level.material.Material;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
public abstract class BaseUnderwaterWallPlantBlock extends BaseWallPlantBlock implements LiquidBlockContainer {
public BaseUnderwaterWallPlantBlock() {
this(
FabricBlockSettings
.of(Material.WATER_PLANT)
.sound(SoundType.WET_GRASS)
.noCollission()
);
}
public BaseUnderwaterWallPlantBlock(int light) {
this(
FabricBlockSettings
.of(Material.WATER_PLANT)
.luminance(light)
.sound(SoundType.WET_GRASS)
.noCollission()
);
}
public BaseUnderwaterWallPlantBlock(Properties settings) {
super(settings);
}
@Override
public boolean canPlaceLiquid(BlockGetter world, BlockPos pos, BlockState state, Fluid fluid) {
return false;
}
@Override
public boolean placeLiquid(LevelAccessor world, BlockPos pos, BlockState state, FluidState fluidState) {
return false;
}
@Override
@SuppressWarnings("deprecation")
public FluidState getFluidState(BlockState state) {
return Fluids.WATER.getSource(false);
}
@Override
public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
return world.getFluidState(pos).getType() == Fluids.WATER && super.canSurvive(state, world, pos);
}
}

View file

@ -0,0 +1,160 @@
package org.betterx.bclib.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.BonemealableBlock;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.EnumProperty;
import net.minecraft.world.level.material.Material;
import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import com.google.common.collect.Lists;
import org.betterx.bclib.blocks.BlockProperties.TripleShape;
import org.betterx.bclib.client.render.BCLRenderLayer;
import org.betterx.bclib.interfaces.RenderLayerProvider;
import org.betterx.bclib.items.tool.BaseShearsItem;
import org.betterx.bclib.util.BlocksHelper;
import java.util.List;
import java.util.function.Function;
@SuppressWarnings("deprecation")
public class BaseVineBlock extends BaseBlockNotFull implements RenderLayerProvider, BonemealableBlock {
public static final EnumProperty<TripleShape> SHAPE = BlockProperties.TRIPLE_SHAPE;
private static final VoxelShape VOXEL_SHAPE = box(2, 0, 2, 14, 16, 14);
public BaseVineBlock() {
this(0, false);
}
public BaseVineBlock(int light) {
this(light, false);
}
public BaseVineBlock(int light, boolean bottomOnly) {
this(light, bottomOnly, p -> p);
}
public BaseVineBlock(int light, boolean bottomOnly, Function<Properties, Properties> propMod) {
this(
propMod.apply(FabricBlockSettings
.of(Material.PLANT)
.sound(SoundType.GRASS)
.lightLevel((state) -> bottomOnly ? state.getValue(SHAPE) == TripleShape.BOTTOM
? light
: 0 : light)
.noCollission()
.offsetType(BlockBehaviour.OffsetType.XZ))
);
}
public BaseVineBlock(BlockBehaviour.Properties properties) {
super(properties.offsetType(BlockBehaviour.OffsetType.XZ));
this.registerDefaultState(this.stateDefinition.any().setValue(SHAPE, TripleShape.BOTTOM));
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> stateManager) {
stateManager.add(SHAPE);
}
@Override
public VoxelShape getShape(BlockState state, BlockGetter view, BlockPos pos, CollisionContext ePos) {
Vec3 vec3d = state.getOffset(view, pos);
return VOXEL_SHAPE.move(vec3d.x, vec3d.y, vec3d.z);
}
public boolean canGenerate(BlockState state, LevelReader world, BlockPos pos) {
return isSupport(state, world, pos);
}
@Override
public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
return isSupport(state, world, pos);
}
protected boolean isSupport(BlockState state, LevelReader world, BlockPos pos) {
BlockState up = world.getBlockState(pos.above());
return up.is(this) || up.is(BlockTags.LEAVES) || canSupportCenter(world, pos.above(), Direction.DOWN);
}
@Override
public BlockState updateShape(BlockState state,
Direction facing,
BlockState neighborState,
LevelAccessor world,
BlockPos pos,
BlockPos neighborPos) {
if (!canSurvive(state, world, pos)) {
return Blocks.AIR.defaultBlockState();
} else {
if (world.getBlockState(pos.below()).getBlock() != this) return state.setValue(SHAPE, TripleShape.BOTTOM);
else if (world.getBlockState(pos.above()).getBlock() != this) return state.setValue(SHAPE, TripleShape.TOP);
return state.setValue(SHAPE, TripleShape.MIDDLE);
}
}
@Override
public List<ItemStack> getDrops(BlockState state, LootContext.Builder builder) {
ItemStack tool = builder.getParameter(LootContextParams.TOOL);
if (tool != null && BaseShearsItem.isShear(tool) || EnchantmentHelper.getItemEnchantmentLevel(
Enchantments.SILK_TOUCH,
tool
) > 0) {
return Lists.newArrayList(new ItemStack(this));
} else {
return Lists.newArrayList();
}
}
@Override
public BCLRenderLayer getRenderLayer() {
return BCLRenderLayer.CUTOUT;
}
@Override
public boolean isValidBonemealTarget(BlockGetter world, BlockPos pos, BlockState state, boolean isClient) {
while (world.getBlockState(pos).getBlock() == this) {
pos = pos.below();
}
return world.getBlockState(pos).isAir();
}
@Override
public boolean isBonemealSuccess(Level level, RandomSource random, BlockPos pos, BlockState state) {
while (level.getBlockState(pos).getBlock() == this) {
pos = pos.below();
}
return level.isEmptyBlock(pos);
}
@Override
public void performBonemeal(ServerLevel level, RandomSource random, BlockPos pos, BlockState state) {
while (level.getBlockState(pos).getBlock() == this) {
pos = pos.below();
}
level.setBlockAndUpdate(pos, defaultBlockState());
BlocksHelper.setWithoutUpdate(level, pos, defaultBlockState());
}
}

Some files were not shown because too many files have changed in this diff Show more