📄 Source: clientid.php
<?php
/**
* ClientID mod integration.
*
* Wraps the /clientid viewmods and /clientid viewpacks commands and parses
* their two-line RCON responses into arrays.
*
* /clientid viewmods <player>
* → "ezyalex700_'s mod list:"
* → "appleskin, architectury, balm, ..."
*
* /clientid viewpacks <player>
* → "ezyalex700_'s pack list:"
* → "Default, Fabric Mod \"AppleSkin\", Fabric Mod \"Balm\", ..."
*/
require_once __DIR__ . '/rcon.php';
/**
* Run an RCON command and return the raw response text.
*/
function clientid_rcon(string $serverDir, array $cfg, string $command): string {
$rcon = rcon_from_properties($serverDir, $cfg['rcon']);
if (empty($rcon['password'])) {
throw new RuntimeException('RCON not configured in server.properties');
}
$r = new Rcon($rcon['host'], (int)$rcon['port'], $rcon['password'], (int)$rcon['timeout']);
try {
return $r->command($command);
} finally {
$r->close();
}
}
/**
* Parse the "viewmods" response. Returns a lowercase list of mod IDs.
*/
function clientid_parse_mods(string $raw): array {
// The response is two lines. First line ends with "'s mod list:".
// Second line is the comma-separated list (may be empty).
$lines = preg_split('/\r?\n/', trim($raw));
$list = [];
$foundHeader = false;
foreach ($lines as $line) {
if (stripos($line, "mod list") !== false) {
$foundHeader = true;
continue;
}
if ($foundHeader) {
foreach (explode(',', $line) as $piece) {
$piece = trim($piece);
if ($piece !== '') $list[] = strtolower($piece);
}
}
}
// Deduplicate, keep order
return array_values(array_unique($list));
}
/**
* Parse the "viewpacks" response. Returns the list of pack names as-is.
* Fabric mod resources come through as: Fabric Mod "Name"
*/
function clientid_parse_packs(string $raw): array {
$lines = preg_split('/\r?\n/', trim($raw));
$list = [];
$foundHeader = false;
foreach ($lines as $line) {
if (stripos($line, "pack list") !== false) {
$foundHeader = true;
continue;
}
if ($foundHeader) {
// Split on ", " but respect quotes inside Fabric Mod "Name"
// Simple approach: split on commas that are followed by "Fabric Mod" or "Default" or a bare word
$parts = preg_split('/,\s*(?=Fabric Mod |Default|[^,]*$)/', $line);
foreach ($parts as $piece) {
$piece = trim($piece, " \t\n\r\0\x0B");
if ($piece !== '') $list[] = $piece;
}
}
}
return array_values(array_unique($list));
}
/**
* Filter a mod list against a baseline (case-insensitive).
*/
function clientid_filter_mods(array $playerMods, array $baseline): array {
$base = array_map('strtolower', $baseline);
$baseSet = array_flip($base);
$extra = [];
foreach ($playerMods as $m) {
$m = strtolower($m);
if (!isset($baseSet[$m])) $extra[] = $m;
}
sort($extra);
return $extra;
}
/**
* Filter a pack list against a baseline (case-insensitive).
*/
function clientid_filter_packs(array $playerPacks, array $baseline): array {
$baseSet = [];
foreach ($baseline as $b) $baseSet[strtolower(trim($b))] = true;
$extra = [];
foreach ($playerPacks as $p) {
$p = trim($p);
if (!isset($baseSet[strtolower($p)])) $extra[] = $p;
}
sort($extra);
return $extra;
}
/**
* ClientID reports every Fabric mod's bundled resources as a "pack".
* Those aren't player-installed packs — they're noise. Strip them out.
*/
function clientid_strip_mod_packs(array $packs): array {
$out = [];
foreach ($packs as $p) {
// Skip "Fabric Mod \"...\"" entries
if (preg_match('/^Fabric Mod\s+"/i', $p)) continue;
// Keep "Default" out too — every vanilla client has it
if (strcasecmp(trim($p), 'Default') === 0) continue;
$out[] = $p;
}
return $out;
}
← Back