Skip to content

Repository files navigation

README

Table of contents

Warnings

  • 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.

Introduction

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

Setting up the project

Dependencies

To make the project work, you need

  • jdk-openjdk-21
  • gradle
  • unzip

Setting up in emacs

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 eclipse

Then 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-dependencies

Visual debugging

To enable visual debug,

  1. Create build.local.gradle in the root of the project
  2. Paste code
    loom {
        runs {
            client {
                vmArgs(
                    "-DMC_DEBUG_ENABLED=true"
                )
            }
        }
    }
        
  3. Choose configuration option from here

Example 1

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"
            )
        }
    }
}

Example 2

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"
            )
        }
    }
}

Project structure

Entry point

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();
    ...
}

Mod aspects

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.

Items

Registration

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
	);

Aspect constructor

public Item(Properties settings)

Aspect methods

  • 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 item
  • void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay displayComponent, Consumer<Component> textConsumer, TooltipFlag type) Append description for the item
  • ItemStack finishUsingItem(ItemStack stack, Level world, LivingEntity user) Events that happen after using item

Blocks

Registration

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
    );

Aspect constructor

public Block(Properties settings)

Aspect methods

  • void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) Event that happends when an entity steps on the block in the world
  • void animateTick(BlockState state, Level world, BlockPos blockPos, RandomSource random) Each tick make something happen, usually display particles
  • InteractionResult 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

Make block mineable

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",
		...
	]
}

Adding loot table to the block

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))
    )
);

Entities

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

Registration

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,
			...
    };

Aspect class

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 damage
  • void tick() Describe events that should happen with entity or around it every tick
  • void 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 in ModEntityStatuses.SUMMON_ENTITIES_START
  • SoundEvent getHurtSound(DamageSource source) Get hurt sound of the mob
  • SoundEvent getDeathSound() Get death sound of the mob
  • SoundEvent getAmbientSound() Get ambient sound of the mob

Aspect goals

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));

Entity rendering

Export models, renderers and render states to client directory in entity, which is src/main/java/net/balamah/voiddim/entity/client/.

  1. Create model of an entity in blockbench and put its bbmodel in blockbench directory in the project root.
  2. Export the model to java model and call it EntityNameModel.
  3. 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() {
                 ...
        }
    }
        
  4. 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")
    );
        
  5. Update VoidDimensionRendering arrays. If entity has non humanoid model (biped), add new entity specs to entitySpecs
    protected 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 bipedEntitySpecs

    protected 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 nonLivingEntitySpecs should be updated

    protected 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

  1. Blockbench model
  2. Java model class
  3. ModelLayerLocation
  4. Model registration/baking
  5. Renderer
  6. Render state
  7. Texture/feature layers

Adding animations to the entity

Download blockbench “Animation to Java Converter” plugin. Yes this plugin is deprecated, it is still has to be used.

  1. Export animations by clicking File -> Export -> Export Modded Entity Animations to src/main/java/net/balamah/voiddim/entity/client/
  2. Sanitize code by
    • Specifying package
    • Adding all missing imports
  3. Port animations to new version by using script/port-animations
    scripts/port-animations <path/to/EntityAnimations.java>
        
  4. Create EntityNameRenderState, which is basically a list of entity’s animations
    public class VoidHarbingerRenderState extends LivingEntityRenderState {
         public final AnimationState summonAnimationState = new AnimationState();
         public final AnimationState summonEndAnimationState = new AnimationState();
    }
        
  5. 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);
    	}
        
  6. 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
    +           );
            }
    }
        
  7. 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 TickingGoal

Adding head rotation to the goal

Usually 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

Effects

Registration

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

Aspect constructor

public SoulburnEffect(final MobEffectCategory category, final int color) {
	
}

Aspect methods

  • boolean shouldApplyEffectTickThisTick(int duration, int amplifier) Determine if effect should be applied or not
  • boolean applyEffectTick(ServerLevel world, LivingEntity entity, int amplifier) The main logic of the effect

Custom damage source

  1. Add getter for the damage source
    public static DamageSource corruption(ServerLevel world) {
        return getBasicDamageSource(world, <ModEffects.CORRUPTION_DAMAGE>.identifier());
    }
        
  2. 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"
    }
        
  3. Register damage type in ModEffects
    public static final ResourceKey<DamageType> CORRUPTION_DAMAGE = registerDamageType("corruption");
        

Potions

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);
}

Sources

Sounds

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)

Textures

Corrupted mobs black skin
https://modrinth.com/mod/intothevoid
corrupt_block texture
https://modrinth.com/mod/intothevoid
alpha_steve
https://namemc.com/skin/f2d10ae87c2444ff

Blocks

old_corpse_pile
https://github.com/TroupeMaster/TheDeepVoid
old_corpse
https://github.com/TroupeMaster/TheDeepVoid

Animations

alpha steve walking
OldWalkingAnimation

Models

rana
https://github.com/sakiiiiika/ranamod

Credits

Programmers

  • Balamah

Texture artists

  • Balamah
  • Bluebeerz

Builders

  • Balamah
  • H0peL0ck
  • legendary_pasha
  • bruh_mox

3D modellers

  • Balamah

Lore writers

  • Balamah
  • Oliksa

About

A new dimension, which is hidden under the layer of bedrock

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages