- Sodium mod is recommended if you set render distance more than 20 chunks. The terrain is very cpu heavy, or maybe it is a skill issue, idk.
- Do not edit directly blockbench models, copy them in a different directory, and edit them in the copied directory. In each blockbench model set texture path to the texture path in the project
- Do not edit the original Blockbench models directly. Instead, create a copy in a separate working directory before making changes. This prevents accidental loss or corruption of the source models.
- In every Blockbench model, make sure the texture path points to the correct texture location inside the project resources directory.
This mod adds a whole new void-based world and new tool material that is stronger than netherite.
Package for my classes is net.balamah.voiddim
To make the project work, you need
- jdk-openjdk-21
- gradle
- unzip
If you want to make your project work using emacs immediately,
evaluate all code blocks using M-x org-babel-execute-subtree
To make project working, you need to build it first using gradle
./gradlew clean build genSources eclipseThen load project configuration by pressing C-c p f l or execute code block
(project-config-file-find-load)If lsp throws errors, run
./gradlew eclipse --refresh-dependenciesTo enable visual debug,
- Create
build.local.gradlein the root of the project - Paste code
loom { runs { client { vmArgs( "-DMC_DEBUG_ENABLED=true" ) } } } - Choose configuration option from here
I want to debug visually path finding of the mobs and enable debug hotkeys. To
do that, i need to add "-DMC_DEBUG_PATHFINDING=true -DMC_DEBUG_HOTKEYS=true" with
"-DMC_DEBUG_ENABLED=true"
loom {
runs {
client {
vmArgs(
"-DMC_DEBUG_ENABLED=true",
"-DMC_DEBUG_PATHFINDING=true",
"-DMC_DEBUG_HOTKEYS=true"
)
}
}
}I want to have debug hotkeys with dev commands and see goals of the entities
loom {
runs {
client {
vmArgs(
"-DMC_DEBUG_ENABLED=true",
"-DMC_DEBUG_HOTKEYS=true",
"-DMC_DEBUG_DEV_COMMANDS=true",
"-DMC_DEBUG_GOAL_SELECTOR=true"
)
}
server {
vmArgs(
"-DMC_DEBUG_ENABLED=true",
"-DMC_DEBUG_GOAL_SELECTOR=true"
)
}
}
}Class VoidDimension initializes the mod by invoking the registration
methods of each mod aspect from its onInitialize() method. Each aspect
exposes a method named registerModAspectName() that serves as the entry
point for initializing that aspect.
Each aspect has elements, which are registered through static fields. This design allows to reference elements directly while keeping readability of the code.
The entry point has the following structure:
public void onInitialize() {
LOGGER.info("Registering mod components");
ModAspectName.registerModAspectName();
...
}Each mod aspect is located in its separated directory aspectName. The
directory always contains main aspect class for registration — ModAspectName
and usually directory called custom for unique functionality.
The aspect elements are registered using register(String, <Settings>, <Settings>)
methods alike. First the method creates instance of the aspect with
settings, then the aspect is registered in the aspect registry and
finally the instance of the aspect is returned. The key to the aspect
in registry is Identifier.fromNamespaceAndPath(VoidDimension.MOD_ID, aspect_name),
whose string output is void-dimension:aspect_name.
If you are confused in the aspects, take a short break.
Main registration methods are
- Item register(String name, Function<Item.Properties, Item> itemFactory, Supplier<Item.Properties> settingsFactory)
- Item registerBlockItem(Block block, BiFunction<Block, Item.Properties, Item> factory, Supplier<Item.Properties> settingsFactory)
- Item registerArmor(String name, ArmorMaterial material, ArmorType type, boolean isVoid)
- Item registerSpawnEgg(String name, EntityType<? extends Mob> mob)
- Item registerFoodItem(String name, FoodProperties foodComponent)
- Item registerFoodItem(String name, FoodProperties foodComponent, Consumable consumableComponent)
Example registration
public static final Item VOID_SHARD = register(
"void_shard", Item::new, ModItems::getVoidItemSettings
);To register custom item, change Function<Item.Properties, Item> settings from
Item::new to settings -> new PrayerItem(settings, 60, 900, 3). Example
registration
public static final Item ORTHODOX_CROSS =
register(
"orthodox_cross",
settings -> new PrayerItem(settings, 60, 900, 3),
ModItems::prayerItemSettings
);public Item(Properties settings)InteractionResult use(Level world, Player user, InteractionHand hand)The method is activated on using the item, which is done by pressing right click.ItemUseAnimation getUseAnimation(ItemStack stack)Return animation for using itemvoid appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay displayComponent, Consumer<Component> textConsumer, TooltipFlag type)Append description for the itemItemStack finishUsingItem(ItemStack stack, Level world, LivingEntity user)Events that happen after using item
The main registration method of ModBlocks is
protected static Block register(
String name, Function<BlockBehaviour.Properties, Block> blockFactory,
BlockBehaviour.Properties settings, boolean shouldRegisterItem,
boolean isItemUnstackable
)Example registration is. It is a default block with sound of netherite block, which is registered as an item and stackable
public static final Block VOID_SHARD_BLOCK =
register(
"void_shard_block",
Block::new,
BlockBehaviour.Properties.of().requiresCorrectToolForDrops().strength(50.0F, 1200.0F)
.sound(SoundType.NETHERITE_BLOCK),
true, false
);Registering block with custom functionality is similar to registering an item with unique functionality. Usually the second method is changed
public static final Block CORRUPT_BLOCK =
register(
"corrupt_block",
CorruptBlock::new,
BlockBehaviour.Properties.of().sound(SoundType.SCULK)
.requiresCorrectToolForDrops().strength(2.6F, 4.7F),
true, false
);public Block(Properties settings)void stepOn(Level world, BlockPos pos, BlockState state, Entity entity)Event that happends when an entity steps on the block in the worldvoid animateTick(BlockState state, Level world, BlockPos blockPos, RandomSource random)Each tick make something happen, usually display particlesInteractionResult useItemOn(ItemStack stack, BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit)Make block interactable with an item stack with current block state in world on pos
The block is not mineable at first, therefore you need to to make it mineable by specifying tool and its material.
For example, i want to make corrupted void shard ore mineable with a
diamond shovel. I need to specify that it needs diamond tool, which is
shovel.
src/main/resources/data/minecraft/tags/block/needs_diamond_tool.json
{
"replace": false,
"values": [
"void-dimension:corrupt_void_shard_ore",
...
]
}
{
"replace": false,
"values": [
"void-dimension:corrupt_void_shard_ore",
...
]
}
Now we made the block mineable, but it does not drop anything yet, for
Minecraft doesn’t know which item to drop from the block. Therefore we
need to specify it in VoidDimensionBlockLootTableProvider by using
either add(final Block block, final LootTable.Builder builder) or
dropSelf(final Block block). In our case ModBlocks.CORRUPT_VOID_SHARD_ORE
should drop ModItems.VOID_SHARD
this.add(
ModBlocks.CORRUPTD_VOID_SHARD_ORE,
LootTable.lootTable()
.withPool(
McGenHelper.getPool(McGenHelper.constantNumber(1))
.add(McGenHelper.getItemEntry(ModItems.VOID_SHARD, 1, 3))
)
);This aspect is the hardest to update and code, but completely neccessary. To create an entity, you should specify its attributes, goals, size, model, renderer and animations
EntityType<T> register(String name, Class<T> entityClass, EntityType.Builder<T> entityBuilder)Example registration of void harbinger, which is 3 blocks tall and 1 block wide. You have to specify entity class for the entity registration.
public static final EntityType<VoidHarbingerEntity> VOID_HARBINGER =
register(
"void_harbinger",
VoidHarbingerEntity.class,
EntityType.Builder.of(VoidHarbingerEntity::new, MobCategory.CREATURE)
.sized(0.6F, 3.0F)
);Then its attributes have to be registered, otherwise the entity will not be spawned
public static void registerEntityAttributes() {
FabricDefaultAttributeRegistry.register(ModEntities.VOID_HARBINGER, VoidHarbingerEntity.createAttributes());
}If entity should spawn in the dimension naturally, add it in the
spawnRestrictedEntities array
@SuppressWarnings("unchecked")
protected static final EntityType<? extends Mob>[] spawnRestrictedEntities =
new EntityType[] {
...
ModEntities.VOID_HARBINGER,
...
};All the work with an entity usually happens in these methods
AttributeSupplier.Builder createAttributes()Return attribute instance for the entity, from follow range to attack damagevoid tick()Describe events that should happen with entity or around it everytickvoid registerGoals()Register goals for the entity. The lower priority number, the higher priority will be of the goal, 0 is the highest.Also entities can have brain system, but i don’t know how to use it yet
void handleEntityEvent(byte status)In my mod the method is usually used for playing animations, whose ids are stored inModEntityStatuses.SUMMON_ENTITIES_STARTSoundEvent getHurtSound(DamageSource source)Get hurt sound of the mobSoundEvent getDeathSound()Get death sound of the mobSoundEvent getAmbientSound()Get ambient sound of the mob
In this example ShatterGroundGoal will be shown. Class definition will
force entity class to have several methods
public class ShatterGroundGoal<T extends BossEntity & ShatterGroundUser>
extends SlowMovementGoal<T>Each goal has a constructor, most important method is entity, which is usually put in the generic, but you can add attributes as much as you would like, just make all of them used somehow
public ShatterGroundGoal(T entity) {
super(entity);
}Each goal should start() if canUse() is true, the main process happends
every tick in tick() and also has events after finishing goal in stop().
Boolean canContinueToUse() specifies if the goal can be continued further.
To add a goal, update registerGoals() method by adding
this.goalSelector.addGoal(6, new ShatterGroundGoal<>(this));Export models, renderers and render states to client directory in
entity, which is src/main/java/net/balamah/voiddim/entity/client/.
- Create model of an entity in blockbench and put its bbmodel in
blockbenchdirectory in the project root. - Export the model to
javamodel and call itEntityNameModel. - Add model layer location to the model, the structure should be similar to
public class VoidHarbingerModel extends EntityModel<VoidHarbingerRenderState> { public static final ModelLayerLocation VOID_HARBINGER = new ModelLayerLocation( Identifier.fromNamespaceAndPath(VoidDimension.MOD_ID, "void_harbinger"), "main" ); private final ModelPart root; ... public VoidHarbingerModel(ModelPart root) { super(root); this.root = root.getChild("root"); } public static LayerDefinition getTexturedModelData() { ... } }
- Create renderer, which has to return render state and the texture
location, which depends of render state
public class VoidHarbingerRenderer extends MobRenderer<VoidHarbingerEntity, VoidHarbingerRenderState, VoidHarbingerModel> { public VoidHarbingerRenderer(EntityRendererProvider.Context context) { super(context, new VoidHarbingerModel(context.bakeLayer(VoidHarbingerModel.VOID_HARBINGER)), 0.75f); this.addLayer( new GlowFeatureRenderer<>(this, "textures/entity/glow/void_harbinger.png") ); } @Override public VoidHarbingerRenderState createRenderState() { return new VoidHarbingerRenderState(); } @Override public Identifier getTextureLocation(VoidHarbingerRenderState state) { return Identifier.fromNamespaceAndPath(VoidDimension.MOD_ID, "textures/entity/void_harbinger.png"); } }
You can also add glow to the mob by moving parts of the entity texture to glow texture and add to the constructor
this.addLayer( new GlowFeatureRenderer<>(this, "textures/entity/glow/void_harbinger.png") );
- Update
VoidDimensionRenderingarrays. If entity has non humanoid model (biped), add new entity specs toentitySpecsprotected List<EntitySpecs<? extends LivingEntity>> entitySpecs = List.of(... new EntitySpecs<>(ModEntities.VOID_HARBINGER, VoidHarbingerModel.VOID_HARBINGER, VoidHarbingerModel.getTexturedModelData(), VoidHarbingerRenderer::new), ... );
If entity model extends humanoid entity model (biped model), update
bipedEntitySpecsprotected List<BipedEntitySpecs<? extends Entity>> bipedEntitySpecs = List.of(new BipedEntitySpecs<>(ModEntities.HOLLOWED_ALPHA_STEVE, HollowedAlphaSteveModel.HOLLOWED_ALPHA_STEVE, HollowedAlphaSteveModel.getTexturedModelData(), context -> new HollowedAlphaSteveRenderer<>( context, new HollowedAlphaSteveModel<>( context.bakeLayer(HollowedAlphaSteveModel.HOLLOWED_ALPHA_STEVE) ), 0.5f )), ... );
If entity is non living, then
nonLivingEntitySpecsshould be updatedprotected List<NonLivingEntitySpecs<? extends Entity>> nonLivingEntitySpecs = List.of(new NonLivingEntitySpecs<>(ModEntities.VOID_SPHERE, VoidSphereModel.VOID_SPHERE, VoidSphereModel.createBodyLayer(), VoidSphereRenderer::new), ... );
Don’t be afraid if you got confused in my confusing text, here are steps how to implement entity rendering
- Blockbench model
- Java model class
- ModelLayerLocation
- Model registration/baking
- Renderer
- Render state
- Texture/feature layers
Download blockbench “Animation to Java Converter” plugin. Yes this plugin is deprecated, it is still has to be used.
- Export animations by clicking
File -> Export -> Export Modded Entity Animationstosrc/main/java/net/balamah/voiddim/entity/client/ - Sanitize code by
- Specifying package
- Adding all missing imports
- Port animations to new version by using
script/port-animationsscripts/port-animations <path/to/EntityAnimations.java>
- Create
EntityNameRenderState, which is basically a list of entity’s animationspublic class VoidHarbingerRenderState extends LivingEntityRenderState { public final AnimationState summonAnimationState = new AnimationState(); public final AnimationState summonEndAnimationState = new AnimationState(); }
- Connect animations in renderer by extracting render state in
extractRenderState(Entity, RenderState, float f)@Override public void extractRenderState( VoidHarbingerEntity entity, VoidHarbingerRenderState renderState, float f ) { super.extractRenderState(entity, renderState, f); renderState.summonAnimationState.copyFrom(entity.summonAnimationState); renderState.summonEndAnimationState.copyFrom(entity.summonEndAnimationState); }
- Connect animations to the model
public class VoidHarbingerModel extends EntityModel<VoidHarbingerRenderState> { public static final ModelLayerLocation VOID_HARBINGER = new ModelLayerLocation( Identifier.fromNamespaceAndPath(VoidDimension.MOD_ID, "void_harbinger"), "main" ); private final ModelPart root; ... + private final KeyframeAnimation summonAnimation; + private final KeyframeAnimation summonEndAnimation; public VoidHarbingerModel(ModelPart root) { super(root); this.root = root.getChild("root"); ... + this.summonAnimation = VoidHarbingerAnimations.SUMMON.bake(root); + this.summonEndAnimation = VoidHarbingerAnimations.SUMMON_END.bake(root); } public static LayerDefinition getTexturedModelData() { ... } @Override public void setupAnim(VoidHarbingerRenderState state) { super.setupAnim(state); ... + this.summonAnimation.apply( + state.summonAnimationState, state.ageInTicks + ); + this.summonEndAnimation.apply( + state.summonEndAnimationState, state.ageInTicks + ); } } - Make entity use the animations
public class VoidHarbingerEntity extends BossEntity implements TeleportUser { + public AnimationState summonAnimationState = new AnimationState(); + public AnimationState summonEndAnimationState = new AnimationState(); ... + @Override + public void handleEntityEvent(byte status) { + switch (status) { + case ModEntityStatuses.SUMMON_ENTITIES_START: + this.summonAnimationState.start(this.tickCount); + break; + case ModEntityStatuses.SUMMON_ENTITIES_FINISH: + this.summonAnimationState.stop(); + this.summonEndAnimationState.start(this.tickCount); + break; + default: super.handleEntityEvent(status); + break; + } + } ... }
To play entity animations, use Level (World) class static method
world.broadcastEntityEvent(final Entity entity, final byte event), or
in ticking goals (which extends TickingGoal) — sendEntityStatus(byte status)
world.broadcastEntityEvent(this, ModEntityStatuses.SUICIDE);
this.sendEntityStatus(ModEntityStatuses.SUICIDE); // goal which extends TickingGoalUsually try using this method
protected void setHeadAngles(float headYaw, float headPitch) {
float yawRad = headYaw * ((float)Math.PI / 180F);
float pitchRad = headPitch * ((float)Math.PI / 180F);
pitchRad = Mth.clamp(
pitchRad,
-25F * ((float)Math.PI / 180F),
45F * ((float)Math.PI / 180F)
);
// if your head is rotated sideways in Blockbench, apply pitch to roll instead:
this.head.yRot = yawRad; // left/right
this.head.zRot = pitchRad * -1; // up/down (compensating for sideways model)
this.head.xRot = 0.0F; // reset old pitch
}And call it in setupAnim(RenderState)
@Override
public void setupAnim(VoidHarbingerRenderState state) {
super.setupAnim(state);
this.setHeadAngles(state.yRot, state.xRot);
}If it doesn’t work, use ai or something by giving prompt
<Pasted entity model class code>
Make entity head rotating according to the target
Main registration method is
protected static Holder<MobEffect> register(String name, MobEffect entry) {
return Registry.registerForHolder(
BuiltInRegistries.MOB_EFFECT,
Identifier.fromNamespaceAndPath(VoidDimension.MOD_ID, name),
entry
);
}And here is example registration of an effect
public static final Holder<MobEffect> SOUL_BURN = register("soul_burn", new SoulburnEffect());The effects must reference their own classes
public SoulburnEffect(final MobEffectCategory category, final int color) {
}boolean shouldApplyEffectTickThisTick(int duration, int amplifier)Determine if effect should be applied or notboolean applyEffectTick(ServerLevel world, LivingEntity entity, int amplifier)The main logic of the effect
- Add getter for the damage source
public static DamageSource corruption(ServerLevel world) { return getBasicDamageSource(world, <ModEffects.CORRUPTION_DAMAGE>.identifier()); }
- Add new damage type in
src/main/resources/data/void-dimension/damage_type/{ "exhaustion": 0.0, "message_id": "<effect-message-id>", "scaling": "when_caused_by_living_non_player" } - Register damage type in
ModEffectspublic static final ResourceKey<DamageType> CORRUPTION_DAMAGE = registerDamageType("corruption");
This aspect has the least steps to accomplish. First register the
potion in ModPotions by binding the effect
public static final Potion VOID_SALVATION_POTION =
register("void_salvation", ModEffects.VOID_SALVATION, 1200, 0);Then build recipe for it registerModPotions() by specifying base,
material and the potion output
public static void registerModPotions() {
buildRecipe(Potions.<BASE>, Item, POTION);
}- void_harbinger.shoot_prepare
- https://pixabay.com/sound-effects/search/dark%20magic/
- void_harbinger.shoot
- https://pixabay.com/sound-effects/search/dark%20magic/
- void_harbinger.teleport
- blade of darkness (desaparece-vamp1.wav)
- shattered_sentinel_master.death
- https://depositphotos.com/sound-effect/rumbling-impact-ancient-stony-stride-784492644.html
- staring_entity.death
- blade of darkness (muerte-Ldm.wav)
- special_attack.lightning
- https://pixabay.com/sound-effects/search/lightning/
- special_attacks.counter_attack
- https://pixabay.com/sound-effects/search/power%20attack/
- entity303.death
- nazgul screams from The lord of the rings
- corrupted_entity.death
- blade of darkness (muerte-Lch2.wav)
- Corrupted mobs black skin
- https://modrinth.com/mod/intothevoid
- corrupt_block texture
- https://modrinth.com/mod/intothevoid
- alpha_steve
- https://namemc.com/skin/f2d10ae87c2444ff
- old_corpse_pile
- https://github.com/TroupeMaster/TheDeepVoid
- old_corpse
- https://github.com/TroupeMaster/TheDeepVoid
- alpha steve walking
- OldWalkingAnimation
- Balamah
- Balamah
- Bluebeerz
- Balamah
- H0peL0ck
- legendary_pasha
- bruh_mox
- Balamah
- Balamah
- Oliksa