Home›Java›Mods›FoliaBoard-API
FoliaBoard-API
FoliaBoard
A Folia-native, packet-level scoreboard API for Paper & Folia. It removes the single hardest part of scoreboards on Folia — knowing which thread may touch a player's board and not racing when they move between regions — and gives you a fluent, MiniMessage-first API where every call is safe from any thread.
Code (Java):

FoliaBoard board = FoliaBoard. create ( this ) ;
board. createBoard (player )
     . placeholders ( true )
     . title ( "<gradient:#00c6ff:#0072ff><bold>MY SERVER</bold></gradient>" )
     . blankLine ( )
     . lines ( "<gray>Player: <white>%player%",
            "<gray>Online: <green>%online%",
            "<gray>Ping: <aqua>%ping%ms" )
     . blankLine ( )
     . line ( "<yellow>play.myserver.net" )
     . build ( ) ;
 
─────────────────────────────────────────────
Why it exists
Packet scoreboard libraries are excellent but warn that their board/team objects are not thread-safe. On Folia that warning is the whole game: there is no main thread, players tick on region threads, they migrate between regions, and join/quit fires on region threads.
FoliaBoard's core idea: confine every mutation of a player's board to that player's EntityScheduler — Folia's scheduler that follows the entity across regions and runs its tasks strictly sequentially. That yields thread-safety with zero locks, and it's all hidden. You describe what to show; FoliaBoard handles where and when it's safe to send the packets.
─────────────────────────────────────────────
Requirements
─────────────────────────────────────────────
Installation
Two ways to use it:
1. Depend on the core library:
Code (XML):

    <repositories>
        <repository>
            <id>jitpack.io </id>
            <url>https://jitpack.io </url>
        </repository>
    </repositories>

    <dependency>
        <groupId>com.github.SpirtySprite </groupId>
        <artifactId>ScoreboardAPI-FOLIA </artifactId>
        <version>1.0.3 </version>
    </dependency>
 
2. Shade + relocate it (so two plugins bundling it can't collide):
Code (XML):

<plugin>
  <groupId>org.apache.maven.plugins </groupId>
  <artifactId>maven-shade-plugin </artifactId>
  <version>3.6.0 </version>
  <executions><execution><phase>package </phase></goals>
    <configuration><relocations><relocation>
      <pattern>net.foliaboard </pattern>
      <shadedPattern>com.yourplugin.libs.foliaboard </shadedPattern>
    </relocation></relocations></configuration>
  </execution></executions>
</plugin>
 
3. Mark your plugin Folia-ready — required or it won't load on Folia:
Code (YAML):

name
: YourPlugin
main
: com.yourplugin.YourPlugin
version
: 1.0.0
api-version
: '1.20'
folia-supported
: true
softdepend
: [PlaceholderAPI ]   # optional; enables the placeholder bridge
 
─────────────────────────────────────────────
Quick start
Two styles — pick one.
A. Instance (recommended):
Code (Java):

public final class YourPlugin extends JavaPlugin {
    private FoliaBoard board ;
    @Override public void onEnable ( )   { board = FoliaBoard. create ( this ) ; }
    @Override public void onDisable ( ) { if (board != null ) board. close ( ) ; }
}
 
B. Static handle (if you'd rather not pass the instance around):
Code (Java):

@Override public void onEnable ( )   { ScoreboardAPI. init ( this ) ; }
@Override public void onDisable ( ) { ScoreboardAPI. shutdown ( ) ; }
// anywhere:
ScoreboardAPI. createBoard (player ). title ( "<aqua>Hi" ). line ( "<gray>Welcome!" ). build ( ) ;
 
All examples below use a board (a FoliaBoard); with the static handle just write ScoreboardAPI or ScoreboardAPI.get().
Add import static net.foliaboard.api.text.Text.mini; (or use Text.mini(...)) when you want a Component from a MiniMessage string.
─────────────────────────────────────Sidebars
The right-hand board. There are four ways to drive one — from most convenient to most manual.
1. Fluent builder
Code (Java):

board. createBoard (player )
     . placeholders ( true )               // resolve %built-ins% + PlaceholderAPI on strings
     . refreshEvery ( 4 )                 // ticks between auto-refreshes of dynamic content
     . title ( "<gradient:#00c6ff:#0072ff><bold>MY SERVER</bold></gradient>" )
     . blankLine ( )
     . line ( "<gray>Player: <white>%player%" )
     . lines ( "<gray>Kills: <red>0", "<gray>Deaths: <red>0" )   // several at once
     . blankLine ( )
     . line ( "<yellow>play.myserver.net" )
     . build ( ) ;                         // returns the live Sidebar
 
If any content is a placeholder string, an Animation, or a supplier, the board is dynamic and FoliaBoard auto-refreshes it on the player's own thread — you never write a scheduler. Everything else is painted once.
Titles and lines accept String (MiniMessage), Component, or Animation<Component>. Lines may also carry a number format.
2. Manual control
Full control, still safe from any thread:
Code (Java):

Sidebar sb = board. sidebar (player ) ;         // get or create this player's sidebar
sb. title (mini ( "<gold>Kit Selector" ) ) ;
sb. line ( 0, mini ( "<gray>Coins: <yellow>1500" ) ) ;
sb. line ( 1, mini ( "<gray>Kills" ), NumberFormat. fixed (mini ( "<red>12" ) ) ) ;   // per-line number
sb. visible ( false ) ;                           // hide without discarding
sb. visible ( true ) ;
sb. clearLines ( ) ;
sb. close ( ) ;                                 // remove entirely (auto on quit)
// read-back:
Component title = sb. title ( ) ;
List <Component > lines = sb. lines ( ) ;
int count = sb. lineCount ( ) ;
 
Only genuinely-changed lines produce packets, so frequent updates never flicker.
3. Global provider — one description for everyone
Code (Java):

board. setGlobalSidebar (SidebarProvider. of (
    p -> mini ( "<aqua>MY SERVER" ),
    p -> List. of (mini ( "<gray>Online: <green>" + Bukkit. getOnlinePlayers ( ). size ( ) ) ),
    10 ) ) ;   // refresh every 10 ticks
 
Or implement the interface for visible(player) control. FoliaBoard attaches a sidebar to every player, refreshes it per player on the right thread, and cleans up on quit.
4. Global layout — same shape, per-player content
Code (Java):

board. setGlobalSidebar (Layout. named ( "main", b -> b
    . placeholders ( true )
    . title ( "<aqua>MY SERVER" )
    . line ( "<gray>Rank: <gold>%rank%" ) ) ) ;
 
One driver per board. A global provider/layout yields automatically to any explicit createBoard(...).build() or applyLayout(...) for that player, so they never fight.
Click to expand...
─────────────────────────────────────────────
Layout profiles
A layout is a reusable, named board template you can apply to any player and switch between instantly (lobby ↔ minigame, per-world boards, …). It's just a recorded recipe of builder calls.
Code (Java):

Layout lobby = Layout. named ( "lobby", b -> b
    . placeholders ( true )
    . title (Animations. cycle (Duration. ofMillis ( 400 ), mini ( "<aqua>LOBBY" ), mini ( "<white>LOBBY" ) ) )
    . blankLine ( )
    . line ( "<gray>Rank: <gold>%rank%" )
    . line ( "<gray>Coins: <yellow>%coins%" ) ) ;
Layout minigame = Layout. named ( "minigame", b -> b
    . title ( "<red><bold>SKYWARS</bold>" )
    . line ( "<gray>Kills", NumberFormat. fixed (mini ( "<red>0" ) ) ) ) ;
board. registerLayout (lobby ). registerLayout (minigame ) ;
board. applyLayout (player, "lobby" ) ;           // switch instantly, any time
board. setWorldLayout ( "minigame_world", "minigame" ) ;   // auto-applied on join & world change
board. unregisterLayout ( "minigame" ) ;           // remove a layout
 
─────────────────────────────────────────────
Nametags
Control the text around a player's name (above their head and in the tab list): prefix, suffix, name colour, visibility, collision, and tab-list sort — sent as a scoreboard team, so it doesn't fight Bukkit teams. Updates use team- modify, so they never flicker.
Code (Java):

board. createNametag (player )
     . prefix ( "<gold>[VIP] " )
     . suffix ( " <gray>★" )
     . color (NamedTextColor. YELLOW )
     . tabSort ( 10 )                       // lower sorts higher in the tab list (0–9999)
     . nametagVisibility (Nametag. Visibility. ALWAYS )
     . collision (Nametag. Collision. NEVER )
     . apply ( ) ;
 
Per-viewer nametags
Show a target's name differently to different viewers — classic ally/enemy colouring:
Code (Java):

board. createNametag (player )
     . prefix ( "<gold>[VIP] " )
     . perViewer ( (viewer, target, style ) -> {
          if (areAllies (viewer, target ) )  style. color (NamedTextColor. GREEN ). prefix ( "<green>✦ " ) ;
          else                            style. color (NamedTextColor. RED ). prefix ( "<red>☠ " ) ;
      } )
     . apply ( ) ;
 
The resolver runs per viewer on that viewer's thread; it starts from the global defaults. Call .apply() again whenever relationships change (e.g. on a timer, or on a team-join event).
[HR][/HR]
Below-name & tab-list numbers
Shared objectives that show a number below every player's name, or beside their tab-list entry.
Code (Java):

board. belowName ( ). title (mini ( "<red>❤" ) ). score (player, 20 ) ;   // hearts below the name
board. tabList ( ). score (player, player. getPing ( ) ) ;             // ping in the tab list
board. belowName ( ). remove (player. getName ( ) ) ;                   // remove one entry
board. belowName ( ). hide ( ) ;                                     // hide for everyone…
board. belowName ( ). show ( ) ;                                     // …and bring it back
 
Per-viewer numbers
Code (Java):

board. tabList ( ). scoreFor (viewer, target. getName ( ), value ) ;   // only viewer sees this value
board. tabList ( ). removeFor (viewer, target. getName ( ) ) ;         // revert to the shared value
 
Quit players are cleaned up automatically (no leaks, no phantom scores).
[HR][/HR]
Tab-list header & footer
Code (Java):

board. tabHeaderFooter (player,
    "<gradient:#00c6ff:#fff><bold>MY SERVER</bold></gradient>",
    "<gray>Online: <green>" + Bukkit. getOnlinePlayers ( ). size ( ) ) ;
board. clearTabHeaderFooter (player ) ;
 
Accepts String (MiniMessage) or Component. Sent on the player's region thread.
Tab-list entry styling & sorting
Style how a player appears in the tab list, independently of their above-head nametag, and sort the list — with no scoreboard team, so it doesn't conflict with other team-based plugins.
Code (Java):

board. tabName (player, "<aqua>★ <white>" + player. getName ( ) ) ;   // tab prefix != above-head prefix
board. tabOrder (player, staff ? 100 : 0 ) ;                       // higher sorts higher (Paper 1.21.2+)
board. resetTabName (player ) ;                                     // back to the vanilla name
if ( !board. tabOrderSupported ( ) ) { /* pre-1.21.2: use nametag tabSort instead */ }
 
tabName/ tabOrder are per-target (shown the same to everyone), built on stable Paper API.
Per-viewer tab names
To show a target a different tab name to different viewers, use the packet-level API:
Code (Java):

if (board. perViewerTabSupported ( ) ) {                 // 1.20.6+ with the player-info packet
    board. tabNameFor (viewer, target, "<red>ENEMY " + target. getName ( ) ) ;
    board. resetTabNameFor (viewer, target ) ;           // back to default
}
 
This is a manual send (no automatic lifecycle): re-apply it when you need it, e.g. on the viewer's join or after the server resends player info. It's built on ClientboundPlayerInfoUpdatePacket and fails safe — if the server build doesn't support it, perViewerTabSupported() returns false and the calls no-op rather than erroring.
─────────────────────────────────────────────
Number formats
Control the red score number the client draws on the right of each entry (1.20.3+). Sidebars hide it by default; override per line or on shared objectives.
Code (Java):

NumberFormat. blank ( ) ;                               // hide it (sidebar default)
NumberFormat. fixed (mini ( "<red>✖" ) ) ;                 // replace it with any component
NumberFormat. styled ( Style. style (NamedTextColor. GOLD ) ) ;   // keep the number, restyle it
NumberFormat. defaultFormat ( ) ;                       // the vanilla red number
board. sidebar (player ). line ( 0, mini ( "<gray>Kills" ), NumberFormat. fixed (mini ( "<red>12" ) ) ) ;
 
─────────────────────────────────────────────
Animations
Self-timed — no ticking or registration. Call current() and return it; the frame is derived from the clock.
Code (Java):

Animation <Component > title = Animations. cycle (Duration. ofMillis ( 400 ),
    mini ( "<aqua>HUB" ), mini ( "<white>HUB" ) ) ;
Animation <Component > marquee = Animations. scrollText (Duration. ofMillis ( 150 ),
    "welcome to the server!", 24, TextColor. color (0x8AB4F8 ) ) ;
Animation <Component > pulse = Animations. pulseColor (Duration. ofSeconds ( 2 ),
    "EVENT LIVE", TextColor. color (0xff0000 ), TextColor. color (0xffff00 ) ) ;
Animation <Component > typed = Animations. typewriter (Duration. ofMillis ( 80 ), Component. text ( "Loading…" ) ) ;
// use directly in a builder / layout:
board. createBoard (player ). title (title ). line (marquee ). build ( ) ;
 
cycle, scrollText, pulseColor, and typewriter are code-point safe (won't split emoji). Animations are global wall-clock phase (every player sees the same frame at the same instant).
─────────────────────────────────────────────
Placeholders
A fast engine that replaces %tokens% using, in order: your resolvers → built-ins → PlaceholderAPI (if installed; bridged reflectively, no hard dependency).
Code (Java):

board. placeholders ( ). register ( "rank",  p -> p. isOp ( ) ? "Admin" : "Member" ) ;
board. placeholders ( ). register ( "coins", p -> economy. balance (p ) ) ;
String  text = board. placeholders ( ). apply (player, "Rank: %rank%" ) ;           // -> "Rank: Admin"
Component c   = board. placeholders ( ). component (player, "<gray>Rank: <gold>%rank%" ) ;   // MiniMessage + %papi%
 
Built-ins: %player% / %player_name% / %name%, %displayname%, %world%, %online%, %max_players%, %ping%, %health%, %x% / %y% / %z%.
Injection-safe: in component(...), placeholder values are escaped before parsing, so a value like a display name containing <red> or a PAPI value with <click:...> renders literally and can't inject formatting or click events into your board.
─────────────────────────────────────────────
MiniMessage & text
Text is the one-stop helper.
Code (Java):

Component c   = Text. mini ( "<rainbow>hello</rainbow>" ) ;
Component tag = Text. mini ( "<hover:show_text:'<green>Click!'><click:run_command:/spawn>Spawn</click>" ) ;
String    mm   = Text. toMini (someComponent ) ;   // round-trip back to a string
 
Every String argument across the API goes through MiniMessage, so you rarely need Text directly.
─────────────────────────────────────────────
Events & hooks
Line processor — rewrite every line/title of every board just before it's sent:
Code (Java):

board. addLineProcessor ( (viewer, index, line ) ->
    index == LineProcessor. TITLE ? line
        : line. decoration (TextDecoration. ITALIC, false ) ) ;   // e.g. kill stray italics
 
Bukkit events:
Code (Java):

@EventHandler
public void onCreate (SidebarCreateEvent e ) {
    getLogger ( ). info ( "Board created for " + e. getPlayer ( ). getName ( ) ) ;
}
@EventHandler
public void onLayout (LayoutApplyEvent e ) {           // cancellable + swappable
    if (e. getPlayer ( ). hasPermission ( "vip" ) ) e. setLayout (board. layout ( "vip_lobby" ) ) ;
}
 
Both fire on the player's region thread (synchronous Folia-safe events).
─────────────────────────────────────────────
Async utilities
Folia-safe helpers for your surrounding work (FoliaBoard's own calls are already thread-safe):
Code (Java):

AsyncUtil. async (plugin, ( ) -> {                       // off any game thread
    int coins = db. loadCoins (uuid ) ;
    AsyncUtil. onPlayer (plugin, player, ( ) ->           // hop to the player's region thread
        board. createBoard (player ). line ( "<gold>Coins: " + coins ). build ( ) ) ;
} ) ;
AsyncUtil. asyncLater (plugin, task, Duration. ofSeconds ( 5 ) ) ;
AsyncUtil. global (plugin, ( ) -> { /* global game state */ } ) ;
boolean folia = AsyncUtil. isFolia ( ) ;
 
─────────────────────────────────────────────
Threading model
Internally: each Sidebar keeps desired state (written under a lock from any thread) and sent state (touched only on the region thread inside a debounced flush that diffs and emits minimal packets).
─────────────────────────────────────────────
Performance
FoliaBoard is built to stay cheap even with many players and fast, animated boards:
Practical guidance: pick a refreshEvery(...) that matches your content — 2–4 ticks for smooth animations, 10–20 for mostly-static boards. Static content isn't refreshed at all.
Observability
board.stats() returns a snapshot for profiling TPS impact:
Code (Java):

FoliaBoardStats s = board. stats ( ) ;
// s.totalPackets(), s.providerRefreshes(), s.activeSidebars(), s.activeNametags()
getLogger ( ). info (s. toString ( ) ) ;
 
FoliaBoard also warns (once) when a board exceeds the 15-line client limit or a single line/title is unusually large (likely accidental payload bloat).
─────────────────────────────────────────────
Remembering a player's layout
Optionally persist which layout a player was on, so it's re-applied on their next join with no join-listener glue. Back the store with anything (a map, a config, a database, a storage plugin):
Code (Java):

board. setLayoutStore ( new LayoutStore ( ) {
    public CompletableFuture <Void > remember (UUID player, String layout ) { return db. putAsync (player, layout ) ; }
    public CompletableFuture <String > lastLayout (UUID player ) { return db. getAsync (player ) ; }
} ) ;
 
When set, applyLayout(...) records the layout name, and FoliaBoard re-applies it on join (unless a global or per-world layout already drives that player's board).
─────────────────────────────────────────────
Lifecycle, cleanup & /reload
─────────────────────────────────────────────
API reference
─────────────────────────────────────────────
Version support & the honest caveat
─────────────────────────────────────────────
Building from source
Code (Bash):

mvn clean package
 

Commands

Plugin details

Read from the plugin's own plugin.yml.

Quick facts

Install steps are the general flow for this file type — How to install Minecraft Java mods & modpacks walks through it step by step.

FoliaBoard-API is a free Minecraft Java mod. Compatible with Minecraft 1.20.6, 1.21, 26.1, 26.2. Downloaded 12 times (via Spigot). Download it and open it directly in the game.

Explore more