Type Handlers

Type Handlers

Synapse placeholders can return any type of Object, not just strings. The framework automatically converts returned objects to strings using a powerful Type Handler system.


Built-in Type Handlers

Synapse comes with comprehensive built-in type handlers for common types:

Primitive Types

// Numbers - automatically converted
register("level", context -> player.getLevel()); // Returns int
register("health", context -> player.getHealth()); // Returns double
register("balance", context -> economy.getBalance(player)); // Returns BigDecimal

// Booleans - converted to "true"/"false"
register("is_op", context -> context.user().requirePlayer().isOp());
register("can_fly", context -> player.getAllowFlight());

// Strings - returned as-is
register("name", context -> player.getName());

Usage:

synapse.translate("Level: ${player.level}", player);
// Result: "Level: 15"

synapse.translate("Op: ${player.is_op}", player);
// Result: "Op: false"

Enums

Enums are automatically formatted from SCREAMING_SNAKE_CASE to Title Case:

register("gamemode", context -> {
    Player player = context.user().requirePlayer();
    return player.getGameMode(); // Returns GameMode enum
});

register("difficulty", () -> Bukkit.getWorlds().get(0).getDifficulty());

Usage:

synapse.translate("Mode: ${player.gamemode}", player);
// GameMode.CREATIVE → "Mode: Creative"
// GameMode.SURVIVAL → "Mode: Survival"

Collections

Arrays, Lists, Sets, and other collections are joined with commas:

register("worlds", () -> {
    return Bukkit.getWorlds().stream()
        .map(World::getName)
        .toList(); // Returns List<String>
});

register("permissions", context -> {
    Player player = context.user().requirePlayer();
    return player.getEffectivePermissions().stream()
        .map(PermissionAttachmentInfo::getPermission)
        .limit(5)
        .toArray(String[]::new); // Returns String[]
});

Usage:

synapse.translate("Worlds: ${server.worlds}", player);
// Result: "Worlds: world, world_nether, world_the_end"

synapse.translate("Perms: ${player.permissions}", player);
// Result: "Perms: essentials.spawn, essentials.home, minecraft.command.help"

Maps

Maps are formatted as key=value pairs:

register("stats", context -> {
    Map<String, Integer> stats = new HashMap<>();
    stats.put("kills", getKills(player));
    stats.put("deaths", getDeaths(player));
    return stats; // Returns Map<String, Integer>
});

Usage:

synapse.translate("Stats: ${player.stats}", player);
// Result: "Stats: kills=42, deaths=13"

Date/Time Types

Built-in formatting for Java time types:

register("join_date", context -> {
    return player.getFirstPlayed(); // Returns Instant
});

register("playtime", context -> {
    return Duration.ofSeconds(player.getStatistic(Statistic.PLAY_ONE_MINUTE) / 20);
});

register("last_seen", context -> {
    return LocalDateTime.now(); // Returns LocalDateTime
});

Formatting:

  • Instant: ISO-8601 format (2025-10-20T14:30:25Z)
  • Duration: Human-readable (2h 15m 30s, 45m 20s, 30s)
  • LocalDateTime: yyyy-MM-dd HH:mm:ss format

Wrapper Types

Special handling for common wrapper types:

// Optional - unwraps the value
register("nickname", context -> {
    return Optional.ofNullable(player.getDisplayName());
});

// Supplier - evaluates lazily
register("expensive", () -> {
    return (Supplier<String>) () -> calculateExpensiveValue();
});

// CompletableFuture - returns completed value or null
register("async_data", context -> {
    return CompletableFuture.supplyAsync(() -> fetchFromAPI());
});

Creating Custom Type Handlers

You can register custom type handlers for your own types or to override default behavior.

Simple Handler Registration

public class MyNeuron extends BukkitNeuron {
    
    public MyNeuron(Plugin plugin) {
        super(plugin, Namespace.of("custom"));
        
        // Register handler for custom type
        registerTypeHandler(Location.class, location -> {
            return String.format("%s @ %d, %d, %d",
                location.getWorld().getName(),
                location.getBlockX(),
                location.getBlockY(),
                location.getBlockZ()
            );
        });
        
        // Now you can return Location objects
        register("location", context -> {
            Player player = context.user().requirePlayer();
            return player.getLocation(); // Returns Location
        });
    }
}

Usage:

synapse.translate("You are at: ${player.location}", player);
// Result: "You are at: world @ 125, 64, -230"

Advanced Type Handler Class

For more complex handling, create a dedicated handler class:

public class ItemStackHandler implements TypeHandler<ItemStack> {
    
    @Override
    public Type getType() {
        return ItemStack.class;
    }
    
    @Override
    public String handle(ItemStack item) {
        if (item == null || item.getType() == Material.AIR) {
            return "Empty";
        }
        
        StringBuilder result = new StringBuilder();
        result.append(item.getAmount()).append("x ");
        
        if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) {
            result.append(item.getItemMeta().getDisplayName());
        } else {
            result.append(formatMaterialName(item.getType()));
        }
        
        return result.toString();
    }
    
    private String formatMaterialName(Material material) {
        return Arrays.stream(material.name().split("_"))
            .map(word -> word.charAt(0) + word.substring(1).toLowerCase())
            .collect(Collectors.joining(" "));
    }
}

Registration:

public MyNeuron(Plugin plugin) {
    super(plugin, Namespace.of("items"));
    
    // Register the handler
    registerTypeHandler(new ItemStackHandler());
    
    // Now you can return ItemStack objects
    register("hand", context -> {
        Player player = context.user().requirePlayer();
        return player.getInventory().getItemInMainHand();
    });
    
    register("helmet", context -> {
        Player player = context.user().requirePlayer();
        return player.getInventory().getHelmet();
    });
}

Usage:

synapse.translate("Holding: ${items.hand}", player);
// Result: "Holding: 64x Diamond Sword"

synapse.translate("Wearing: ${items.helmet}", player);
// Result: "Wearing: 1x Netherite Helmet"

Using BaseTypeHandler

For type-safe handlers with automatic type resolution:

public class UUIDHandler extends BaseTypeHandler<UUID> {
    
    @Override
    public String handle(UUID input) {
        // Return short UUID (first 8 characters)
        return input.toString().substring(0, 8);
    }
}

Registration:

registerTypeHandler(new UUIDHandler());

register("uuid", context -> context.user().uniqueId());
register("uuid_full", context -> context.user().uniqueId().toString());

Type Handler Inheritance

The type handler system supports inheritance-based matching:

// Register handler for Number (parent class)
registerTypeHandler(Number.class, number -> {
    return String.format("%.2f", number.doubleValue());
});

// Works for all Number subclasses
register("int_value", () -> 42);           // Integer
register("double_value", () -> 3.14159);   // Double
register("long_value", () -> 999999L);     // Long
register("float_value", () -> 2.5f);       // Float

All will be formatted with 2 decimal places:

"${stat.int_value}"    // "42.00"
"${stat.double_value}" // "3.14"
"${stat.long_value}"   // "999999.00"

Interface Handlers

Handlers also work with interfaces:

// Register handler for any Nameable
registerTypeHandler(Nameable.class, nameable -> {
    return nameable.getCustomName() != null 
        ? nameable.getCustomName() 
        : "Unnamed";
});

register("entity_name", context -> {
    // Returns various Nameable entities
    return getNearestEntity(player);
});

Unregistering Type Handlers

Remove type handlers when needed:

// Unregister a specific type
unregisterTypeHandler(Location.class);

// Revert to default behavior
unregisterTypeHandler(Enum.class);

Best Practices

1. Return Native Types

// ✅ Good - Return native type
register("health", context -> {
    return player.getHealth(); // Returns double
});

// ❌ Avoid - Manual string conversion
register("health", context -> {
    return String.valueOf(player.getHealth());
});

2. Handle Null Values

public class SafeLocationHandler implements TypeHandler<Location> {
    @Override
    public String handle(Location location) {
        if (location == null || location.getWorld() == null) {
            return "Unknown Location";
        }
        return formatLocation(location);
    }
}

3. Keep Handlers Simple

// ✅ Good - Simple, focused handler
registerTypeHandler(Player.class, Player::getName);

// ❌ Avoid - Complex logic in handler
registerTypeHandler(Player.class, player -> {
    // Lots of complex logic...
    // Better to do this in the placeholder itself
});

4. Use Inheritance Wisely

// Register once for parent class
registerTypeHandler(Entity.class, entity -> 
    entity.getType().name().toLowerCase()
);

// Now works for all entity types:
// Player, Zombie, Creeper, ArmorStand, etc.

5. Consider Performance

// ✅ Cached - Handler called once
register("expensive_calc", () -> {
    return calculateComplexValue(); // Returns ComplexObject
}, options -> options.cache(true));

// Handler is called after caching, so:
// 1. calculateComplexValue() runs and returns ComplexObject
// 2. ComplexObject is cached
// 3. When retrieved from cache, handler converts to String

Type Handler Priority

When multiple handlers could match, Synapse uses this priority order:

  1. Exact class match - Handler registered for the specific class
  2. Parent class match - Walking up the inheritance hierarchy
  3. Interface match - Implemented interfaces
  4. Default behavior - Falls back to toString() if no handler found
class Vehicle { }
class Car extends Vehicle { }
class SportsCar extends Car { }

registerTypeHandler(Vehicle.class, v -> "Vehicle");
registerTypeHandler(Car.class, c -> "Car");

register("vehicle", () -> new SportsCar());
// Result: "Car" (closest parent match)

Common Use Cases

Custom Plugin Objects

public class Clan {
    private final String name;
    private final int memberCount;
    // ...
}

registerTypeHandler(Clan.class, clan -> 
    clan.getName() + " (" + clan.getMemberCount() + " members)"
);

register("clan", context -> getPlayerClan(player));

Third-Party Library Types

// WorldGuard regions
registerTypeHandler(ProtectedRegion.class, region ->
    region.getId() + " [" + region.getPriority() + "]"
);

// Vault economy
registerTypeHandler(Economy.class, economy ->
    economy.format(economy.getBalance(player))
);

Complex Data Structures

public class PlayerStats {
    private Map<String, Integer> stats;
    private List<Achievement> achievements;
}

registerTypeHandler(PlayerStats.class, stats -> {
    return String.format("%d stats, %d achievements",
        stats.getStats().size(),
        stats.getAchievements().size()
    );
});

The Type Handler system gives you complete control over how placeholder values are formatted, making it easy to work with any object type in your plugin ecosystem!