Skip to main content

Overview

Folia is Paper’s experimental multi-threaded server implementation that splits the world into independent regions, each running on separate threads. BetterModel fully supports Folia with region-aware scheduling and thread-safe operations.
Folia is experimental and still in active development. While BetterModel is tested on Folia, expect potential issues as Folia evolves.

Region-Based Threading

Each region runs on its own thread with independent tick loop

Automatic Scheduling

BetterModel automatically uses region-aware schedulers

Thread-Safe Operations

All model operations are safe across region boundaries

Zero Configuration

Works out-of-the-box with the Paper JAR

What is Folia?

Folia fundamentally changes how Minecraft servers handle concurrency:
1

World Splitting

The world is divided into independent regions (typically chunks or chunk groups).
2

Parallel Execution

Each region runs on a separate thread, allowing true parallel processing.
3

Region Isolation

Entities in different regions cannot directly interact without thread synchronization.
4

Scheduler Changes

Traditional Bukkit scheduler is replaced with region and async schedulers.
Folia is designed for high-player-count servers where regions can be processed independently. It’s not always faster than Paper for small servers.

Detection and Installation

BetterModel automatically detects Folia at runtime:
Folia Detection
public interface BetterModelBukkit extends BetterModelPlatform {
    // Folia detected by RegionizedServer class
    boolean IS_FOLIA = classExists(
        "io.papermc.paper.threadedregions.RegionizedServer"
    );
}

Installation Steps

1

Install Folia

Download Folia from PaperMC:
wget https://api.papermc.io/v2/projects/folia/versions/1.21.11/builds/.../downloads/folia-1.21.11-....jar
2

Use Paper JAR

Install BetterModel using the Paper JAR:
bettermodel-paper-2.2.0.jar
Folia is detected as a Paper fork, so always use the Paper version of BetterModel.
3

Verify detection

Check the console on startup:
[BetterModel] Plugin is loaded. (XXX ms)
[BetterModel] Platform: Folia

Region-Aware Scheduling

The key difference with Folia is the scheduler. BetterModel automatically uses Folia’s region scheduler when detected:
Paper Scheduler (Folia-Compatible)
class PaperScheduler : BukkitModelScheduler {
    
    // Region-specific tasks
    override fun task(location: Location, runnable: Runnable): ModelTask? {
        return Bukkit.getRegionScheduler().run(PLUGIN, location) {
            runnable.run()
        }.wrap()
    }
    
    override fun taskLater(
        location: Location, 
        delay: Long, 
        runnable: Runnable
    ): ModelTask? {
        return Bukkit.getRegionScheduler().runDelayed(
            PLUGIN, location, {
                runnable.run()
            }, delay
        ).wrap()
    }
    
    // Async tasks (thread-pool based)
    override fun asyncTask(runnable: Runnable) {
        return Bukkit.getAsyncScheduler().runNow(PLUGIN) {
            runnable.run()
        }.wrap()
    }
}

Region vs Global Schedulers

Used for tasks that must run in a specific region (entity operations):
Region-Specific Task
BetterModelBukkit platform = BetterModelBukkit.platform();

// Schedule task in entity's region
platform.scheduler().task(entity.getLocation(), () -> {
    // This runs in the region containing the entity
    // Safe to modify entity, spawn displays, etc.
});
Location-based tasks automatically execute in the correct region thread.
Used for tasks that don’t touch world state:
Async Task
platform.scheduler().asyncTask(() -> {
    // This runs in async thread pool
    // Cannot modify world/entities directly
    // Use for I/O, calculations, etc.
});
Folia also has a global region scheduler for tasks that span regions:
Global Task
Bukkit.getGlobalRegionScheduler().run(plugin, (task) -> {
    // Runs once across all regions
});
BetterModel uses this internally for plugin-wide operations.

Thread Safety Considerations

Safe Operations

BetterModel’s API is thread-safe across regions:
Thread-Safe API Usage
// Safe: Getting model renderers (immutable)
ModelRenderer renderer = BetterModel.model("demon_knight").orElse(null);

// Safe: Creating trackers (internally synchronized)
EntityTracker tracker = renderer.create(BukkitAdapter.adapt(entity));

// Safe: Animating (synchronized per tracker)
tracker.animate("attack", AnimationModifier.DEFAULT);

// Safe: Updating display properties
tracker.update(TrackerUpdateAction.tint(0xFF0000));

Region Ownership

Each entity “belongs” to a specific region:
Region Ownership
public void modifyModel(Entity entity) {
    // WRONG: May be called from different region thread
    EntityTracker tracker = getTracker(entity);
    tracker.animate("attack");  // Not safe!
    
    // CORRECT: Schedule in entity's region
    BetterModelBukkit.platform().scheduler().task(
        entity.getLocation(),
        () -> {
            EntityTracker tracker = getTracker(entity);
            tracker.animate("attack");  // Safe!
        }
    );
}
Critical Rule: Always schedule entity operations in the entity’s region using scheduler().task(location, runnable).

Plugin Configuration

The paper-plugin.yml explicitly declares Folia support:
paper-plugin.yml
name: BetterModel
main: kr.toxicity.model.paper.BetterModelPaper
loader: kr.toxicity.model.paper.BetterModelLoader
folia-supported: true  # Declares Folia compatibility
apiVersion: '1.20'
The folia-supported: true flag tells Folia that BetterModel is tested and compatible with regionized threading.

Performance Implications

Benefits on Folia

Parallel Processing

Models in different regions update simultaneously on separate threads

No Main Thread Blocking

Display entity updates don’t bottleneck on single thread

Region Isolation

Performance issues in one region don’t affect others

Async Operations

Resource pack generation and I/O operations remain async

Potential Overhead

Folia adds overhead for:
  • Cross-region entity tracking
  • Scheduler coordination
  • Thread synchronization
For small servers (< 50 players), Paper may perform better than Folia.

Common Patterns

Spawning Models

Folia-Safe Model Spawning
public class ModelSpawner {
    private final BetterModelBukkit platform = BetterModelBukkit.platform();
    
    public void spawnModel(Location location, String modelId) {
        // Schedule in target region
        platform.scheduler().task(location, () -> {
            // Spawn entity
            Entity entity = location.getWorld().spawnEntity(
                location, 
                EntityType.ZOMBIE
            );
            
            // Attach model (safe - same region)
            BetterModel.model(modelId)
                .map(r -> r.create(BukkitAdapter.adapt(entity)))
                .ifPresent(tracker -> {
                    tracker.animate("spawn", AnimationModifier.DEFAULT);
                });
        });
    }
}

Updating Models

Cross-Region Updates
public class ModelUpdater {
    public void updateAllModels(Collection<Entity> entities) {
        // Group entities by region
        Map<Location, List<Entity>> byRegion = entities.stream()
            .collect(Collectors.groupingBy(Entity::getLocation));
        
        // Schedule update in each region
        byRegion.forEach((location, regionEntities) -> {
            platform.scheduler().task(location, () -> {
                regionEntities.forEach(entity -> {
                    getTracker(entity).animate("update");
                });
            });
        });
    }
}

Async Pre-processing

Async + Region Pattern
public void loadAndApplyModel(Entity entity, String modelId) {
    // Heavy computation async
    platform.scheduler().asyncTask(() -> {
        ModelData data = computeModelData(modelId);
        
        // Apply in entity's region
        platform.scheduler().task(entity.getLocation(), () -> {
            applyModelData(entity, data);
        });
    });
}

Debugging Folia Issues

Thread Assertions

Folia will throw exceptions if you violate thread rules:
IllegalStateException: Cannot modify entity from incorrect thread
    at org.bukkit.craftbukkit.entity.CraftEntity.setLocation
Solution: Wrap the operation in scheduler().task(location, runnable).

Checking Current Thread

Debug Thread Context
public void debugThread() {
    Thread thread = Thread.currentThread();
    logger.info("Thread: " + thread.getName());
    
    // Folia region threads are named like "RegionScheduler - x"
    if (thread.getName().startsWith("RegionScheduler")) {
        logger.info("Running in region thread");
    }
}

Monitoring Regions

Check Folia’s region status with:
/folia regions
/folia tps

Limitations

You cannot safely iterate all entities across regions:
// UNSAFE on Folia
for (Entity entity : world.getEntities()) {
    // May be in different region thread!
}
Instead, track entities in your own region-aware data structures.
Event handlers run in the region where the event occurred:
@EventHandler
public void onDamage(EntityDamageEvent event) {
    // This runs in the entity's region
    Entity entity = event.getEntity();
    // Safe to modify entity directly here
}
But you can’t safely access entities in other regions from the event.
Plugin channels work differently on Folia. BetterModel handles this internally, but be aware if you’re implementing custom cross-region communication.

MythicMobs Integration

BetterModel’s MythicMobs integration is Folia-aware:
Folia-Safe Mechanics
AnimationScript.of(BetterModelBukkit.IS_FOLIA) script@ { tracker ->
    // Automatically uses correct scheduler based on platform
    if (IS_FOLIA) {
        // Region-safe execution
    } else {
        // Standard sync execution
    }
}
Mechanics automatically set their thread safety:
abstract class AbstractSkillMechanic : Mechanic {
    init {
        isAsyncSafe = !BetterModelBukkit.IS_FOLIA
    }
}

Best Practices

1

Always schedule region tasks

Never modify entities or world state without scheduling in the correct region:
scheduler.task(entity.getLocation(), () -> {
    // Modifications here
});
2

Use async for I/O

Keep file operations, network calls, and heavy computations async:
scheduler.asyncTask(() -> {
    // I/O or computation
});
3

Minimize cross-region dependencies

Design your systems to work within single regions when possible.
4

Test on Folia

If supporting Folia, test your integration on actual Folia servers. Race conditions may only appear under load.

Migration from Paper

Good news: If you’re using BetterModel’s API correctly, your code should work on Folia without changes!
The scheduler abstraction handles platform differences:
Platform-Agnostic Code
// This works on both Paper and Folia
BetterModelBukkit platform = BetterModelBukkit.platform();

platform.scheduler().task(location, () -> {
    // Your code
});

platform.scheduler().asyncTask(() -> {
    // Async code
});
Behind the scenes:
  • Paper: Uses Bukkit.getScheduler()
  • Folia: Uses Bukkit.getRegionScheduler() and Bukkit.getAsyncScheduler()

Next Steps

Paper Platform

Learn about standard Paper/Bukkit implementation

Trackers

Understand tracker lifecycle and management

Performance Optimization

Optimize BetterModel for high-player-count servers

Examples

See complete API usage examples

Build docs developers (and LLMs) love