Building DedinMod: A Comprehensive Minecraft Fabric Mod

After diving into Fabric modding, I wanted to create something substantial—not just adding a few blocks or items, but building a comprehensive mod that demonstrates the full capabilities of the Fabric framework. The result is DedinMod, a feature-rich Minecraft mod for version 1.21.11 that adds over 30 custom blocks, numerous items, custom entities, status effects, and complex game systems.

Project Scope and Architecture

DedinMod is built on a solid technical foundation:

Architectural Patterns

Centralized Initialization

Rather than scattering registration code throughout the mod, I use a centralized initialization pattern with dedicated "Init" classes:

// Main entry point
public class Dedinmod implements ModInitializer {
    @Override
    public void onInitialize() {
        BlockInit.load();       // Register all blocks
        ItemInit.load();        // Register all items
        EntityInit.load();      // Register all entities
        EffectInit.load();      // Register status effects
        // ... more systems
    }
}

Registry Pattern with Utilities

All content registration uses a custom RegisterUtil helper that wraps vanilla registry operations for cleaner, more maintainable code:

public class BlockInit {
    public static final Block TELEPORTER_BLOCK = 
        RegisterUtil.registerBlock("teleporter_block",
            new TeleporterBlock(AbstractBlock.Settings.create()
                .strength(4.0f)
                .requiresTool()));
    
    public static final Block PHASE_BLOCK = 
        RegisterUtil.registerBlock("phase_block",
            new PhaseBlock(AbstractBlock.Settings.create()
                .nonOpaque()
                .noCollision()));
}

Split Source Sets

The project uses separate source directories for client and server code:

This separation prevents accidentally referencing client-side classes on the server, which would cause crashes.

Key Features Implementation

1. Teleportation Network System

One of the most complex features is the teleportation network. It required:

public class TeleporterBlockEntity extends BlockEntity {
    private String teleporterName = "";
    private String destinationName = "";
    
    public void teleportPlayer(ServerPlayerEntity player) {
        if (destinationName.isEmpty()) return;
        
        BlockPos destination = findDestination(destinationName);
        if (destination != null) {
            player.teleport(
                destination.getX() + 0.5,
                destination.getY() + 1,
                destination.getZ() + 0.5
            );
            // Play sound and particle effects
        }
    }
}

2. Custom Status Effects

The mod includes 9+ unique status effects that modify player behavior:

public class AntiGravityEffect extends StatusEffect {
    @Override
    public boolean applyUpdateEffect(LivingEntity entity, int amplifier) {
        if (!entity.getWorld().isClient) {
            // Apply upward velocity
            Vec3d velocity = entity.getVelocity();
            entity.setVelocity(velocity.x, 0.15, velocity.z);
            entity.velocityModified = true;
        }
        return true;
    }
}

3. Custom Entities and NPCs

The mod adds several custom entities including the "Kasper" NPC character:

public class KasperEntity extends PassiveEntity {
    @Override
    protected void initGoals() {
        this.goalSelector.add(0, new SwimGoal(this));
        this.goalSelector.add(1, new WanderAroundFarGoal(this, 1.0));
        this.goalSelector.add(2, new LookAtEntityGoal(this, 
            PlayerEntity.class, 8.0f));
    }
    
    @Override
    public ActionResult interactMob(PlayerEntity player, Hand hand) {
        if (!this.getWorld().isClient) {
            // Custom interaction logic
            player.sendMessage(Text.literal("Hello, traveler!"));
        }
        return ActionResult.SUCCESS;
    }
}

4. Data Generation API

One of Fabric's most powerful features is the data generation API. Instead of manually writing JSON files for recipes, loot tables, and models, I generate them programmatically:

public class DedinmodRecipeProvider extends FabricRecipeProvider {
    @Override
    public void generate(RecipeExporter exporter) {
        // Shaped recipe for teleporter block
        ShapedRecipeJsonBuilder.create(RecipeCategory.MISC, 
            BlockInit.TELEPORTER_BLOCK)
            .pattern("OEO")
            .pattern("EPE")
            .pattern("OEO")
            .input('O', Items.OBSIDIAN)
            .input('E', Items.ENDER_PEARL)
            .input('P', Items.ENDER_EYE)
            .criterion(hasItem(Items.ENDER_PEARL), 
                conditionsFromItem(Items.ENDER_PEARL))
            .offerTo(exporter);
    }
}

This approach has several benefits:

Technical Challenges and Solutions

Challenge 1: Client-Server Synchronization

Minecraft's architecture separates client and server logic, even in single-player. Synchronizing custom data (like teleporter names) required implementing custom network payloads:

public record TeleportPayload(String destination) 
    implements CustomPayload {
    
    public static final Id ID = 
        new Id<>(new Identifier("dedinmod", "teleport"));
    
    @Override
    public Id getId() {
        return ID;
    }
}

Challenge 2: Mixin Integration

Sometimes you need to modify vanilla Minecraft behavior. Mixins allow bytecode injection without modifying Minecraft's source:

@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
    @Inject(method = "travel", at = @At("HEAD"))
    private void modifyTravel(Vec3d movementInput, 
        CallbackInfo ci) {
        LivingEntity self = (LivingEntity)(Object)this;
        
        // Custom movement modification logic
        if (self.hasStatusEffect(EffectInit.ANTI_GRAVITY)) {
            // Override normal gravity behavior
        }
    }
}

Challenge 3: Resource Loading and Textures

Properly organizing and loading textures, models, and blockstates is crucial. The mod follows Minecraft's resource conventions:

Development Workflow

Gradle Tasks

The development cycle uses several Gradle tasks:

# Run client for testing
./gradlew runClient

# Generate data files (recipes, models, etc.)
./gradlew runDatagen

# Build final mod JAR
./gradlew build

# Clean build artifacts
./gradlew clean

Testing Methodology

  1. Write feature code
  2. Run data generation to create JSON files
  3. Launch test client with runClient
  4. Test in-game functionality
  5. Iterate on issues

Custom Blocks Showcase

The mod includes diverse custom blocks with unique mechanics:

Performance Considerations

When adding content to Minecraft, performance is crucial:

Lessons Learned

1. Plan Your Registry Names Early

Registry names are permanent once players start using your mod. Changing them breaks existing worlds. Use descriptive, future-proof names from the start.

2. Use Data Generation

Writing JSON files by hand is error-prone and tedious. The data generation API saves countless hours and prevents mistakes.

3. Test on Dedicated Servers

Single-player testing isn't enough. Always test on a dedicated server to catch client-server synchronization bugs.

4. Document Your Mixins

Mixins are powerful but can be fragile when Minecraft updates. Document why each mixin exists and what it modifies.

What's Next for DedinMod?

Future planned features include:

Resources for Aspiring Modders

Conclusion

Building DedinMod has been an incredible learning experience. Fabric's modular architecture and powerful APIs make it possible to create complex, feature-rich mods without fighting the framework. Whether you're adding simple blocks or building entire game systems, Fabric provides the tools you need.

The source code organization, data generation workflow, and architectural patterns demonstrated in DedinMod can serve as a template for your own ambitious modding projects. Start small, iterate often, and don't be afraid to dive deep into Minecraft's source code to understand how it all works.

Happy modding!