Best Practices
This guide covers best practices for developing mods with Amber, including code organization, performance optimization, security considerations, and maintainability tips.
Code Organization
1. Package Structure
Organize your code with a clear package structure:
com.example.mymod/
├── MyMod.java # Main mod class
├── common/ # Common code shared across platforms
│ ├── items/ # Item classes
│ │ ├── MyItems.java # Item registration
│ │ └── CustomItem.java # Custom item implementation
│ ├── blocks/ # Block classes
│ ├── entities/ # Entity classes
│ ├── events/ # Event handlers
│ ├── networking/ # Networking classes
│ ├── config/ # Configuration classes
│ └── utils/ # Utility classes
├── fabric/ # Fabric-specific code
│ └── MyModFabric.java # Fabric entry point
├── forge/ # Forge-specific code
│ └── MyModForge.java # Forge entry point
└── neoforge/ # NeoForge-specific code
└── MyModNeoForge.java # NeoForge entry point2. Class Organization
Keep classes focused and single-purpose:
// Good - Single responsibility
public class ItemRegistry {
private static final DeferredRegister<Item> ITEMS =
DeferredRegister.create("mymod", Registries.ITEM);
public static final RegistrySupplier<Item> MY_ITEM = ITEMS.register("my_item",
() -> new Item(new Item.Properties()));
public static void init() {
ITEMS.register();
}
}
// Bad - Multiple responsibilities
public class MyModStuff {
public static final DeferredRegister<Item> ITEMS = ...;
public static final DeferredRegister<Block> BLOCKS = ...;
// Methods for items, blocks, events, networking, etc.
}3. Constants Management
Use a dedicated class for constants:
public final class ModConstants {
private ModConstants() {} // Prevent instantiation
public static final String MOD_ID = "mymod";
public static final String MOD_NAME = "My Mod";
// GUI constants
public static final int GUI_WIDTH = 176;
public static final int GUI_HEIGHT = 166;
// Energy constants
public static final int MAX_ENERGY = 10000;
public static final int ENERGY_TRANSFER = 100;
// Dimension constants
public static final int OVERWORLD_ID = 0;
public static final int NETHER_ID = 1;
public static final int END_ID = 2;
}Performance Optimization
1. Lazy Initialization
Use lazy initialization for expensive operations:
// Good - Lazy initialization
public class ExpensiveManager {
private static Supplier<ExpensiveManager> instance = Suppliers.memoize(ExpensiveManager::new);
public static ExpensiveManager get() {
return instance.get();
}
private ExpensiveManager() {
// Expensive initialization
}
}
// Bad - Eager initialization
public class ExpensiveManager {
private static final ExpensiveManager INSTANCE = new ExpensiveManager(); // Always created
public static ExpensiveManager get() {
return INSTANCE;
}
}2. Efficient Event Handling
Make event handlers efficient:
// Good - Efficient event handler
BlockEvents.BLOCK_BREAK_AFTER.register((level, player, pos, state, blockEntity) -> {
// Only run expensive operation on specific blocks
if (state.is(ModBlocks.ENERGY_BLOCK.get())) {
updateEnergyNetwork(level, pos);
}
});
// Bad - Inefficient event handler
BlockEvents.BLOCK_BREAK_AFTER.register((level, player, pos, state, blockEntity) -> {
// Always runs expensive operation
updateEnergyNetwork(level, pos); // Runs for every block break!
});3. Caching
Cache expensive computations:
// Good - Cached computation
public class RecipeCache {
private static final Map<Item, List<Recipe>> CACHE = new HashMap<>();
public static List<Recipe> getRecipesForItem(Item item) {
return CACHE.computeIfAbsent(item, i -> {
// Expensive recipe lookup
return findRecipesForItem(i);
});
}
private static List<Recipe> findRecipesForItem(Item item) {
// Implementation
}
}4. Client-Side Optimization
Optimize client-side rendering:
// Good - Efficient rendering
public class CustomRenderer {
private static final Model MODEL = loadModel(); // Load once
public static void render(PoseStack poseStack, MultiBufferSource buffer, int light, int overlay) {
// Reuse model
VertexConsumer vertexConsumer = buffer.getBuffer(RENDER_TYPE);
MODEL.renderToBuffer(poseStack, vertexConsumer, light, overlay);
}
private static Model loadModel() {
// Expensive model loading
}
}Security Considerations
1. Input Validation
Always validate player input:
// Good - Input validation
public static boolean isValidItemStack(ItemStack stack) {
if (stack.isEmpty()) {
return false;
}
if (stack.getCount() <= 0 || stack.getCount() > stack.getMaxStackSize()) {
return false;
}
return true;
}
public static void processItem(ItemStack stack, Player player) {
if (!isValidItemStack(stack)) {
player.sendSystemMessage(Component.literal("Invalid item!"));
return;
}
// Process valid item
}2. Permission Checks
Check permissions before performing operations:
// Good - Permission check
public static void teleportPlayer(Player player, BlockPos pos) {
if (!player.hasPermissions(2)) { // OP level 2
player.sendSystemMessage(Component.literal("You don't have permission to teleport!"));
return;
}
// Teleport player
}3. Server-Side Validation
Validate critical operations on the server:
// Good - Server-side validation
public static void craftItem(ServerPlayer player, ResourceLocation recipeId) {
// Validate recipe exists
Recipe<?> recipe = player.server.getRecipeManager().byKey(recipeId).orElse(null);
if (recipe == null) {
player.connection.disconnect(Component.literal("Invalid recipe!"));
return;
}
// Validate player can craft recipe
if (!canCraftRecipe(player, recipe)) {
player.sendSystemMessage(Component.literal("Cannot craft this recipe!"));
return;
}
// Craft item
}Maintainability Tips
1. Documentation
Document your code with clear comments:
/**
* Calculates the energy transfer rate between two components.
*
* @param fromEnergy The energy of the source component
* @param toEnergy The energy of the target component
* @param maxTransfer The maximum transfer rate
* @return The actual transfer rate
*/
public static int calculateTransferRate(int fromEnergy, int toEnergy, int maxTransfer) {
// Can only transfer if source has energy and target has space
if (fromEnergy <= 0 || toEnergy >= MAX_ENERGY) {
return 0;
}
// Transfer at maximum rate or available amount, whichever is smaller
int availableSpace = MAX_ENERGY - toEnergy;
return Math.min(maxTransfer, Math.min(fromEnergy, availableSpace));
}2. Error Handling
Handle errors gracefully:
// Good - Graceful error handling
public static void loadConfiguration() {
try {
ConfigManager.load();
} catch (Exception e) {
Constants.LOG.error("Failed to load configuration", e);
// Use defaults
ConfigManager.useDefaults();
// Notify user
if (Platform.isClient()) {
Minecraft.getInstance().player.sendSystemMessage(
Component.literal("Failed to load config, using defaults")
);
}
}
}3. Testing
Write tests for your code:
// Example test structure
public class EnergySystemTest {
@Test
public void testEnergyTransfer() {
// Setup
EnergyStorage from = new EnergyStorage(1000);
EnergyStorage to = new EnergyStorage(1000);
// Test
int transferred = EnergyUtils.transferEnergy(from, to, 100);
// Verify
assertEquals(100, transferred);
assertEquals(900, from.getEnergy());
assertEquals(100, to.getEnergy());
}
@Test
public void testEnergyTransferFullTarget() {
// Setup
EnergyStorage from = new EnergyStorage(1000);
EnergyStorage to = new EnergyStorage(950);
// Test
int transferred = EnergyUtils.transferEnergy(from, to, 100);
// Verify
assertEquals(50, transferred); // Only 50 can fit
assertEquals(950, from.getEnergy());
assertEquals(1000, to.getEnergy());
}
}Debugging Tips
1. Logging
Use structured logging:
// Good - Structured logging
public class EnergyNetwork {
private static final Logger LOGGER = LoggerFactory.getLogger("mymod/energynetwork");
public void updateNetwork() {
LOGGER.debug("Updating energy network at {}", networkPos);
try {
// Update logic
LOGGER.info("Updated network with {} nodes, {} energy", nodes.size(), totalEnergy);
} catch (Exception e) {
LOGGER.error("Failed to update energy network", e);
}
}
}2. Debug Commands
Create debug commands for testing:
public class DebugCommands {
public static void registerDebugCommands() {
// Only register in development environment
if (!Platform.isDevelopmentEnvironment()) {
return;
}
Commands.literal("mymoddebug")
.requires(source -> source.hasPermissions(4))
.then(Commands.literal("energy")
.executes(context -> {
ServerPlayer player = context.getSource().getPlayer();
if (player != null) {
EnergyNetwork network = EnergyNetworkManager.getNetwork(player.blockPosition());
player.sendSystemMessage(Component.literal("Network energy: " + network.getEnergy()));
}
return Command.SINGLE_SUCCESS;
}))
.register();
}
}3. Visual Debugging
Use visual debugging aids:
public class VisualDebug {
public static void showEnergyNetwork(Level level, EnergyNetwork network) {
if (!Platform.isDevelopmentEnvironment()) {
return;
}
// Draw particles at network nodes
for (BlockPos pos : network.getNodes()) {
Vec3 particlePos = Vec3.atCenterOf(pos);
level.addParticle(ParticleTypes.END_ROD,
particlePos.x, particlePos.y, particlePos.z,
0, 0.1, 0);
}
}
}Deployment Tips
1. Version Management
Use semantic versioning:
public final class ModVersion {
private ModVersion() {} // Prevent instantiation
public static final String VERSION = "1.2.3";
public static final int MAJOR = 1;
public static final int MINOR = 2;
public static final int PATCH = 3;
public static boolean isCompatible(String version) {
String[] parts = version.split("\\.");
if (parts.length < 2) return false;
int major = Integer.parseInt(parts[0]);
int minor = Integer.parseInt(parts[1]);
// Compatible if same major version and minor version is not greater
return major == MAJOR && minor <= MINOR;
}
}2. Configuration Validation
Validate configuration on load:
public class ConfigValidator {
public static boolean validateConfig(MyModConfig config) {
boolean valid = true;
// Validate energy values
if (config.maxEnergy <= 0) {
Constants.LOG.error("maxEnergy must be positive!");
valid = false;
}
if (config.energyTransfer <= 0 || config.energyTransfer > config.maxEnergy) {
Constants.LOG.error("energyTransfer must be between 0 and maxEnergy!");
valid = false;
}
// Validate item lists
if (config.allowedItems.isEmpty()) {
Constants.LOG.warn("No allowed items configured!");
}
return valid;
}
}3. Migration Support
Support configuration migration:
public class ConfigMigration {
public static MyModConfig migrateConfig(OldConfig oldConfig) {
MyModConfig newConfig = new MyModConfig();
// Migrate common fields
newConfig.enableFeature = oldConfig.enableFeature;
newConfig.maxEnergy = oldConfig.maxEnergy;
// Handle renamed fields
newConfig.energyMultiplier = oldConfig.transferRate; // Renamed
// Handle new fields with defaults
newConfig.enableAdvancedFeatures = true; // New field
return newConfig;
}
}Conclusion
Following these best practices will help you create mods that are:
- Maintainable: Easy to understand and modify
- Performant: Efficient and optimized
- Secure: Robust against common issues
- Reliable: Consistent behavior across platforms
- Future-Proof: Ready for new versions and features
Remember that the most important practice is to keep learning and improving your code as you gain experience with Minecraft modding and Amber.