Compatibility
Minecraft: Java Edition
1.21.8–1.21.11
Platforms
Links
Tags
Creators
Details
Licensed ARR
Published 2 days ago
Updated 3 weeks ago
FakePlayerCount
A Velocity plugin that modifies the online player count displayed in the server list (multiplayer menu).
It adds a fluctuating fake player amount to the real player count, making your server appear more populated.
Features
- Realistic player count faking – The fake count smoothly moves between a configurable minimum and maximum, avoiding sudden jumps.
- Automatic config reloading – Changes to
config.ymlare detected and applied without restarting the server. - Velocity ONLY – Designed exclusively for the Velocity proxy; no Paper/Spigot installation needed.
- Lightweight and efficient – Uses Velocity's scheduler and runs with minimal performance impact.
How it works
The plugin intercepts the ProxyPingEvent (server list ping) and adds a fake number of online players before the response is sent to the client.
The fake number is not static – it slowly drifts within the configured range, simulating natural player fluctuation.
Source code:
import com.google.inject.Inject;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.ServerPing;
import org.slf4j.Logger;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* FakePlayerCount
*
* Fakes the online player count shown in the server list (multiplayer menu)
* by intercepting the ProxyPingEvent and modifying the player count before
* sending it. The proxy itself uses this modified ping response, so it is
* not a pure client-side display illusion but what the proxy actually returns.
*
* The fake number fluctuates realistically between a minimum and maximum
* (config.yml), and config.yml is automatically reloaded whenever it is
* modified.
*/
@Plugin(
id = "fakeplayercount",
name = "FakePlayerCount",
version = "1.0.0",
description = "Fakes the online player count of the proxy by a configurable, fluctuating value",
authors = {"You"}
)
public class FakePlayerCount {
private final ProxyServer server;
private final Logger logger;
private final Path dataDirectory;
// Default values if config.yml cannot be read
private int minFakePlayers = 15;
private int maxFakePlayers = 25;
private int maxPlayers = 100;
// current fake extra count, moves via random walk between min and max
private volatile int currentFakeExtra;
private WatchService watchService;
@Inject
public FakePlayerCount(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) {
this.server = server;
this.logger = logger;
this.dataDirectory = dataDirectory;
}
@Subscribe
public void onProxyInitialize(ProxyInitializeEvent event) {
loadConfig();
setupConfigWatcher();
// watches config.yml for changes and reloads if necessary
server.getScheduler()
.buildTask(this, this::pollConfigWatcher)
.repeat(Duration.ofSeconds(2))
.schedule();
// lets the fake count fluctuate realistically between min and max
server.getScheduler()
.buildTask(this, this::tickFakeCount)
.repeat(Duration.ofSeconds(10))
.schedule();
logger.info("FakePlayerCount loaded! Fake players fluctuate between " + minFakePlayers
+ " and " + maxFakePlayers + " (max-players: " + maxPlayers + ").");
}
// PostOrder.LAST ensures that this listener is guaranteed to run after all
// other plugins, so that our fake count is not overwritten.
@Subscribe(order = PostOrder.LAST)
public void onProxyPing(ProxyPingEvent event) {
int realOnline = server.getPlayerCount();
int fakeOnline = realOnline + currentFakeExtra;
// Max should be at least 1 greater than the displayed online count
// and at the same time not fall below the configured value.
int dynamicMax = fakeOnline + maxPlayers;
ServerPing newPing = event.getPing().asBuilder()
.onlinePlayers(fakeOnline)
.maximumPlayers(dynamicMax)
.build();
event.setPing(newPing);
}
/**
* Moves the current fake count in small, random steps within
* [minFakePlayers, maxFakePlayers], instead of rolling completely new
* on each tick. This looks more organic/"real" than a hard jump.
*/
private void tickFakeCount() {
if (minFakePlayers >= maxFakePlayers) {
currentFakeExtra = minFakePlayers;
return;
}
int range = maxFakePlayers - minFakePlayers;
int maxStep = Math.max(1, range / 4);
int delta = ThreadLocalRandom.current().nextInt(-maxStep, maxStep + 1);
int updated = currentFakeExtra + delta;
updated = Math.min(maxFakePlayers, Math.max(minFakePlayers, updated));
currentFakeExtra = updated;
}
private void setupConfigWatcher() {
try {
watchService = FileSystems.getDefault().newWatchService();
dataDirectory.register(
watchService,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE
);
} catch (IOException e) {
logger.error("Could not set up config.yml watcher, automatic reloading is disabled.", e);
}
}
private void pollConfigWatcher() {
if (watchService == null) {
return;
}
WatchKey key = watchService.poll();
if (key == null) {
return;
}
boolean configChanged = false;
for (WatchEvent<?> watchEvent : key.pollEvents()) {
Object context = watchEvent.context();
if (context instanceof Path && ((Path) context).getFileName().toString().equals("config.yml")) {
configChanged = true;
}
}
key.reset();
if (configChanged) {
logger.info("config.yml was modified, reloading...");
loadConfig();
}
}
private void loadConfig() {
try {
if (!Files.exists(dataDirectory)) {
Files.createDirectories(dataDirectory);
}
Path configFile = dataDirectory.resolve("config.yml");
if (!Files.exists(configFile)) {
String defaultConfig =
"# FakePlayerCount configuration\n" +
"#\n" +
"# min-fake-players / max-fake-players: range in which the\n" +
"# additional (fake) player count fluctuates realistically.\n" +
"min-fake-players: 15\n" +
"max-fake-players: 25\n" +
"\n" +
"# max-players: maximum player count displayed in the server list.\n" +
"max-players: 100\n";
Files.writeString(configFile, defaultConfig);
logger.info("config.yml was created with default values: " + configFile);
}
List<String> lines = Files.readAllLines(configFile);
minFakePlayers = parseIntSetting(lines, "min-fake-players", 15);
maxFakePlayers = parseIntSetting(lines, "max-fake-players", 25);
maxPlayers = parseIntSetting(lines, "max-players", 100);
if (minFakePlayers > maxFakePlayers) {
logger.warn("min-fake-players is larger than max-fake-players, swapping values.");
int tmp = minFakePlayers;
minFakePlayers = maxFakePlayers;
maxFakePlayers = tmp;
}
// adjust current value into the (new) valid range
if (currentFakeExtra < minFakePlayers || currentFakeExtra > maxFakePlayers) {
currentFakeExtra = minFakePlayers + (maxFakePlayers - minFakePlayers) / 2;
}
} catch (IOException e) {
logger.error("Error loading/creating config.yml, using previous/default values.", e);
}
}
private int parseIntSetting(List<String> lines, String key, int defaultValue) {
Pattern pattern = Pattern.compile("^\\s*" + Pattern.quote(key) + "\\s*:\\s*(-?\\d+)\\s*(#.*)?$");
for (String line : lines) {
Matcher matcher = pattern.matcher(line);
if (matcher.matches()) {
return Integer.parseInt(matcher.group(1));
}
}
logger.warn("Could not find '" + key + "' in config.yml, using default: " + defaultValue);
return defaultValue;
}
}


