[PHP] 纯文本查看 复制代码createWorldDirectories();
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($this->socket === false) {
die("创建套接字失败: " . socket_strerror(socket_last_error()));
}
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
if (!socket_bind($this->socket, $host, $port)) {
die("绑定套接字失败: " . socket_strerror(socket_last_error()));
}
if (!socket_listen($this->socket)) {
die("监听套接字失败: " . socket_strerror(socket_last_error()));
}
socket_set_nonblock($this->socket);
$this->loadWorld();
echo "Minecraft 1.8 服务器已启动在 $host:$port\n";
echo "世界大小: {$this->worldSize}x{$this->worldSize} 方块\n";
echo "世界版本: v{$this->worldVersion} (数据清理优化版)\n";
echo "输入 'stop' 关闭服务器\n";
echo "输入 'list' 显示在线玩家\n";
echo "输入 'say ' 广播消息\n";
echo "输入 'save' 手动保存世界\n";
echo "输入 'gc' 强制垃圾回收\n";
echo "输入 'worldinfo' 显示世界信息\n";
echo "输入 'repair' 修复世界数据\n";
echo "输入 'cleanup' 清理无效数据\n";
}
private function createWorldDirectories() {
if (!is_dir($this->worldDir)) {
mkdir($this->worldDir, 0777, true);
echo "创建世界目录: {$this->worldDir}\n";
}
if (!is_dir($this->playerDataDir)) {
mkdir($this->playerDataDir, 0777, true);
echo "创建玩家数据目录: {$this->playerDataDir}\n";
}
}
/**
* 关键修复:改进世界加载逻辑,增加数据清理
*/
private function loadWorld() {
// 首先尝试加载主世界文件
if (file_exists($this->worldFile)) {
echo "正在从 JSON 加载世界...";
$worldData = file_get_contents($this->worldFile);
if ($worldData !== false) {
$data = json_decode($worldData, true);
if ($this->validateWorldData($data)) {
$this->world = $data['blocks'];
$this->worldSize = $data['worldSize'] ?? 48;
$this->entities = $data['entities'] ?? [];
$this->worldVersion = $data['version'] ?? 2;
$this->worldFullyLoaded = true;
// 加载后立即进行数据清理
$this->deepCleanWorldData();
$blockCount = count($this->world);
$nonAirBlocks = 0;
foreach ($this->world as $blockId) {
if ($blockId != self::BLOCK_AIR) $nonAirBlocks++;
}
echo "完成 (已加载 $blockCount 个方块, $nonAirBlocks 个非空气方块)\n";
echo "世界版本: {$this->worldVersion}, 大小: {$this->worldSize}x{$this->worldSize}\n";
// 验证世界完整性
$this->validateWorldIntegrity();
// 创建备份
$this->createWorldBackup();
// 修复:确保世界数据完全覆盖默认地形
$this->ensureWorldConsistency();
return;
} else {
echo "世界文件格式无效,尝试加载备份...\n";
}
} else {
echo "无法读取世界文件,尝试加载备份...\n";
}
}
// 如果主文件失败,尝试加载备份
if (file_exists($this->worldBackupFile)) {
echo "正在从备份加载世界...";
$backupData = file_get_contents($this->worldBackupFile);
if ($backupData !== false) {
$data = json_decode($backupData, true);
if ($this->validateWorldData($data)) {
$this->world = $data['blocks'];
$this->worldSize = $data['worldSize'] ?? 48;
$this->entities = $data['entities'] ?? [];
$this->worldVersion = $data['version'] ?? 2;
$this->worldFullyLoaded = true;
// 加载后立即进行数据清理
$this->deepCleanWorldData();
$blockCount = count($this->world);
$nonAirBlocks = 0;
foreach ($this->world as $blockId) {
if ($blockId != self::BLOCK_AIR) $nonAirBlocks++;
}
echo "完成 (从备份加载了 $blockCount 个方块, $nonAirBlocks 个非空气方块)\n";
echo "世界版本: {$this->worldVersion}, 大小: {$this->worldSize}x{$this->worldSize}\n";
// 验证世界完整性
$this->validateWorldIntegrity();
// 修复:确保世界数据完全覆盖默认地形
$this->ensureWorldConsistency();
// 恢复备份到主文件
if (copy($this->worldBackupFile, $this->worldFile)) {
echo "已从备份恢复世界文件\n";
}
return;
}
}
}
// 如果都失败,生成新世界
echo "世界文件不存在或损坏,生成新世界...\n";
$this->generateFlatWorld();
$this->saveWorld();
$this->worldFullyLoaded = true;
}
/**
* 深度清理世界数据 - 新增方法
* 移除所有无效的方块数据
*/
private function deepCleanWorldData() {
echo "执行深度数据清理...";
$cleanedCount = 0;
$invalidCoords = 0;
$invalidBlocks = 0;
$duplicateCoords = 0;
$cleanedWorld = [];
$processedCoords = [];
foreach ($this->world as $key => $blockId) {
// 检查坐标格式
$coords = explode(',', $key);
if (count($coords) !== 3) {
$invalidCoords++;
continue;
}
// 验证坐标值
$x = (int)$coords[0];
$y = (int)$coords[1];
$z = (int)$coords[2];
// 检查坐标是否有效
if ($x = $this->worldSize ||
$z = $this->worldSize ||
$y = 128) {
$invalidCoords++;
continue;
}
// 检查方块ID是否有效
if (!in_array($blockId, self::VALID_BLOCK_IDS)) {
$invalidBlocks++;
continue;
}
// 检查重复坐标
$coordKey = "$x,$y,$z";
if (isset($processedCoords[$coordKey])) {
$duplicateCoords++;
continue;
}
$processedCoords[$coordKey] = true;
$cleanedWorld[$coordKey] = $blockId;
}
$this->world = $cleanedWorld;
$cleanedCount = $invalidCoords + $invalidBlocks + $duplicateCoords;
echo "完成 (清理了 $cleanedCount 个无效数据: $invalidCoords 个无效坐标, $invalidBlocks 个无效方块, $duplicateCoords 个重复坐标)\n";
if ($cleanedCount > 0) {
// 如果有清理操作,立即保存清理后的世界
$this->saveWorld();
}
}
/**
* 确保世界数据一致性
*/
private function ensureWorldConsistency() {
echo "确保世界数据一致性...";
$fixedBlocks = 0;
$missingBlocks = 0;
// 检查世界边界内的所有位置
for ($x = 0; $x worldSize; $x++) {
for ($z = 0; $z worldSize; $z++) {
for ($y = 0; $y world[$key])) {
$this->world[$key] = self::BLOCK_AIR;
$fixedBlocks++;
$missingBlocks++;
}
}
}
}
echo "完成 (修复了 $fixedBlocks 个方块, 发现 $missingBlocks 个缺失方块)\n";
}
/**
* 验证世界数据的完整性和正确性
*/
private function validateWorldData($data) {
if ($data === null || !is_array($data)) {
echo "错误: 世界数据不是有效的JSON数组\n";
return false;
}
// 检查必需字段
$requiredFields = ['version', 'worldSize', 'blocks'];
foreach ($requiredFields as $field) {
if (!isset($data[$field])) {
echo "错误: 世界数据缺少必需字段 '$field'\n";
return false;
}
}
// 验证版本兼容性
$version = $data['version'] ?? 1;
if ($version $this->worldVersion + 1) {
echo "错误: 不兼容的世界版本 $version (当前支持版本: {$this->worldVersion})\n";
return false;
}
// 验证世界大小
$worldSize = $data['worldSize'] ?? 0;
if (!is_int($worldSize) || $worldSize 1000) {
echo "错误: 无效的世界大小: $worldSize\n";
return false;
}
// 验证方块数据
$blocks = $data['blocks'] ?? null;
if (!is_array($blocks)) {
echo "错误: 方块数据不是数组\n";
return false;
}
// 验证方块键值格式
$invalidEntries = 0;
foreach ($blocks as $key => $blockId) {
if (!is_string($key)) {
$invalidEntries++;
continue;
}
if (!is_int($blockId)) {
$invalidEntries++;
continue;
}
// 验证坐标格式
$coords = explode(',', $key);
if (count($coords) !== 3) {
$invalidEntries++;
continue;
}
foreach ($coords as $coord) {
if (!is_numeric($coord)) {
$invalidEntries++;
continue 2;
}
}
// 验证方块ID范围
if ($blockId 255) {
$invalidEntries++;
continue;
}
}
if ($invalidEntries > 0) {
echo "警告: 发现 $invalidEntries 个无效方块条目\n";
}
return true;
}
/**
* 验证世界完整性
*/
private function validateWorldIntegrity() {
$totalExpected = $this->worldSize * $this->worldSize * 128;
$actual = count($this->world);
echo "世界完整性检查: 实际保存 $actual 个方块, 理论上限 $totalExpected 个方块\n";
if ($actual > $totalExpected) {
echo "警告: 世界数据可能损坏,方块数量超过理论上限\n";
// 尝试修复:移除超出范围的方块
$this->removeOutOfBoundsBlocks();
}
// 检查是否有无效的方块ID
$invalidBlockIds = 0;
foreach ($this->world as $blockId) {
if (!in_array($blockId, self::VALID_BLOCK_IDS)) {
$invalidBlockIds++;
}
}
if ($invalidBlockIds > 0) {
echo "警告: 发现 $invalidBlockIds 个无效方块ID\n";
}
}
/**
* 移除超出世界边界的方块
*/
private function removeOutOfBoundsBlocks() {
$removed = 0;
foreach ($this->world as $key => $value) {
$coords = explode(',', $key);
if (count($coords) === 3) {
$x = (int)$coords[0];
$y = (int)$coords[1];
$z = (int)$coords[2];
if ($x = $this->worldSize ||
$z = $this->worldSize ||
$y = 128) {
unset($this->world[$key]);
$removed++;
}
} else {
// 坐标格式错误,移除
unset($this->world[$key]);
$removed++;
}
}
if ($removed > 0) {
echo "已移除 $removed 个超出边界的方块\n";
}
}
private function createWorldBackup() {
if (file_exists($this->worldFile)) {
copy($this->worldFile, $this->worldBackupFile);
}
}
/**
* 关键修复:改进世界保存,增加数据验证
*/
private function saveWorld() {
// 先创建备份
if (file_exists($this->worldFile)) {
$this->createWorldBackup();
}
// 修复:在保存前清理无效方块
$this->cleanWorldData();
$worldData = [
'version' => $this->worldVersion,
'worldSize' => $this->worldSize,
'blocks' => $this->world,
'entities' => $this->entities,
'timestamp' => time(),
'description' => 'PHP Minecraft Server 1.8 World - Data Cleaned JSON Format',
'checksum' => $this->calculateWorldChecksum(),
'totalBlocks' => count($this->world),
'validated' => true
];
$jsonData = json_encode($worldData, JSON_PRETTY_PRINT);
// 验证JSON编码是否成功
if ($jsonData === false) {
echo "错误: JSON编码失败: " . json_last_error_msg() . "\n";
return false;
}
$result = file_put_contents($this->worldFile, $jsonData, LOCK_EX);
if ($result !== false) {
$blockCount = count($this->world);
$nonAirBlocks = 0;
foreach ($this->world as $blockId) {
if ($blockId != self::BLOCK_AIR) $nonAirBlocks++;
}
// 验证保存的文件
if ($this->verifySavedWorld()) {
echo "世界已保存到 '{$this->worldFile}' ($blockCount 个方块, $nonAirBlocks 个非空气方块)\n";
return true;
} else {
echo "警告: 世界文件保存后验证失败,可能已损坏\n";
return false;
}
} else {
echo "保存世界失败! 请检查目录权限。\n";
return false;
}
}
/**
* 清理世界数据,移除无效的方块
*/
private function cleanWorldData() {
echo "清理世界数据...";
$cleaned = 0;
$newWorld = [];
foreach ($this->world as $key => $blockId) {
$coords = explode(',', $key);
if (count($coords) === 3) {
$x = (int)$coords[0];
$y = (int)$coords[1];
$z = (int)$coords[2];
// 只保存世界边界内的有效方块
if ($x >= 0 && $x worldSize &&
$z >= 0 && $z worldSize &&
$y >= 0 && $y world = $newWorld;
if ($cleaned > 0) {
echo "清理了 $cleaned 个无效方块\n";
} else {
echo "无需清理\n";
}
}
/**
* 计算世界数据的校验和
*/
private function calculateWorldChecksum() {
$data = [
'blocks' => $this->world,
'worldSize' => $this->worldSize,
'version' => $this->worldVersion
];
return md5(serialize($data));
}
/**
* 验证保存的世界文件
*/
private function verifySavedWorld() {
if (!file_exists($this->worldFile)) {
return false;
}
$content = file_get_contents($this->worldFile);
if ($content === false) {
return false;
}
$data = json_decode($content, true);
return $this->validateWorldData($data);
}
private function savePlayerData($username, $data) {
$filename = $this->playerDataDir . '/' . $username . '.json';
// 验证玩家数据
if (!$this->validatePlayerData($data)) {
echo "玩家数据验证失败: $username\n";
return false;
}
$playerData = json_encode($data, JSON_PRETTY_PRINT);
// 创建备份
$backupFile = $filename . '.backup';
if (file_exists($filename)) {
copy($filename, $backupFile);
}
$result = file_put_contents($filename, $playerData, LOCK_EX);
if ($result === false) {
echo "保存玩家数据失败: $username\n";
return false;
}
return true;
}
private function loadPlayerData($username) {
$filename = $this->playerDataDir . '/' . $username . '.json';
$backupFile = $filename . '.backup';
// 首先尝试主文件
if (file_exists($filename)) {
$data = file_get_contents($filename);
if ($data !== false) {
$playerData = json_decode($data, true);
if ($playerData !== null && $this->validatePlayerData($playerData)) {
return $playerData;
}
}
}
// 如果主文件失败,尝试备份
if (file_exists($backupFile)) {
$data = file_get_contents($backupFile);
if ($data !== false) {
$playerData = json_decode($data, true);
if ($playerData !== null && $this->validatePlayerData($playerData)) {
echo "从备份加载玩家数据: $username\n";
// 恢复备份
copy($backupFile, $filename);
return $playerData;
}
}
}
return null;
}
/**
* 验证玩家数据
*/
private function validatePlayerData($data) {
if (!is_array($data)) {
return false;
}
$required = ['x', 'y', 'z'];
foreach ($required as $field) {
if (!isset($data[$field]) || !is_numeric($data[$field])) {
return false;
}
}
// 验证坐标范围
$x = $data['x'];
$y = $data['y'];
$z = $data['z'];
$margin = 10;
if ($x = $this->worldSize + $margin ||
$z = $this->worldSize + $margin ||
$y 256) {
return false;
}
return true;
}
/**
* 关键修复:重写地形生成 - 生成超平坦地形
*/
private function generateFlatWorld() {
$this->world = [];
echo "生成超平坦世界中...\n";
// 生成超平坦世界 - 保存所有方块以确保完整性
for ($x = 0; $x worldSize; $x++) {
for ($z = 0; $z worldSize; $z++) {
for ($y = 0; $y = 1 && $y = 4 && $y setBlock($x, $y, $z, $blockId);
}
}
// 显示进度
if ($x % 16 === 0) {
$progress = round(($x / $this->worldSize) * 100);
echo "进度: $progress%\n";
}
}
// 生成一些装饰
$this->generateDecorations();
$totalBlocks = count($this->world);
$nonAirBlocks = 0;
foreach ($this->world as $blockId) {
if ($blockId != self::BLOCK_AIR) $nonAirBlocks++;
}
echo "世界生成完成 (已生成 $totalBlocks 个方块, $nonAirBlocks 个非空气方块)\n";
}
/**
* 设置方块 - 关键修复:总是保存所有方块,但验证数据有效性
*/
private function setBlock($x, $y, $z, $blockId) {
// 验证坐标有效性
if ($x = $this->worldSize ||
$z = $this->worldSize ||
$y = 128) {
return; // 忽略超出边界的方块
}
// 验证方块ID有效性
if (!in_array($blockId, self::VALID_BLOCK_IDS)) {
echo "警告: 尝试设置无效方块ID: $blockId 在 ($x, $y, $z)\n";
return;
}
$key = "$x,$y,$z";
$this->world[$key] = $blockId;
}
/**
* 获取方块 - 关键修复:只使用保存的世界数据
*/
private function getBlock($x, $y, $z) {
// 世界边界检查
if ($x = $this->worldSize ||
$z = $this->worldSize ||
$y = 128) {
return self::BLOCK_AIR; // 边界外为空气
}
$key = "$x,$y,$z";
// 关键修复:如果在保存的数据中找到,返回方块ID
// 如果没有找到,返回空气(而不是默认地形)
return isset($this->world[$key]) ? $this->world[$key] : self::BLOCK_AIR;
}
/**
* 生成一些装饰物
*/
private function generateDecorations() {
$centerX = $this->worldSize / 2;
$centerZ = $this->worldSize / 2;
// 在中心区域生成一些树
$treePositions = [
[$centerX + 5, $centerZ + 5],
[$centerX - 5, $centerZ - 5],
[$centerX + 8, $centerZ - 3],
[$centerX - 7, $centerZ + 6],
[$centerX + 2, $centerZ - 8]
];
foreach ($treePositions as $pos) {
$treeX = $pos[0];
$treeZ = $pos[1];
// 确保树的位置在世界范围内
if ($treeX >= 2 && $treeX worldSize - 2 &&
$treeZ >= 2 && $treeZ worldSize - 2) {
$this->generateTree($treeX, $treeZ);
}
}
// 生成一个小房子在出生点
$this->generateSpawnHouse($centerX, $centerZ);
}
private function generateTree($x, $z) {
$groundY = 6; // 草方块高度
// 生成树干(橡木)
$trunkHeight = 5;
for ($y = $groundY + 1; $y setBlock($x, $y, $z, self::BLOCK_WOOD);
}
// 生成树叶
$leavesStartY = $groundY + $trunkHeight - 1;
$leavesEndY = $groundY + $trunkHeight + 2;
for ($ly = $leavesStartY; $ly = 0 && $leafX worldSize &&
$leafZ >= 0 && $leafZ worldSize &&
$ly >= 0 && $ly getBlock($leafX, $ly, $leafZ);
if ($currentBlock == self::BLOCK_AIR) {
$this->setBlock($leafX, $ly, $leafZ, self::BLOCK_LEAVES);
}
}
}
}
}
}
private function generateSpawnHouse($centerX, $centerZ) {
$houseSize = 5;
$startX = $centerX - floor($houseSize / 2);
$startZ = $centerZ - floor($houseSize / 2);
$wallHeight = 4;
$groundY = 6;
echo "在出生点生成房屋...\n";
// 地基
for ($x = $startX; $x setBlock($x, $groundY, $z, self::BLOCK_WOOD_PLANK);
}
}
// 墙壁
for ($y = $groundY + 1; $y setBlock($x, $y, $startZ, self::BLOCK_WOOD);
$this->setBlock($x, $y, $startZ + $houseSize - 1, self::BLOCK_WOOD);
}
for ($z = $startZ; $z setBlock($startX, $y, $z, self::BLOCK_WOOD);
$this->setBlock($startX + $houseSize - 1, $y, $z, self::BLOCK_WOOD);
}
}
// 门(正面中间)
$doorX = $startX + floor($houseSize / 2);
$this->setBlock($doorX, $groundY + 1, $startZ, self::BLOCK_AIR);
$this->setBlock($doorX, $groundY + 2, $startZ, self::BLOCK_AIR);
// 窗户
$window1X = $startX + 1;
$window1Z = $startZ + floor($houseSize / 2);
$this->setBlock($window1X, $groundY + 2, $window1Z, self::BLOCK_GLASS);
$window2X = $startX + $houseSize - 2;
$this->setBlock($window2X, $groundY + 2, $window1Z, self::BLOCK_GLASS);
// 屋顶
$roofY = $groundY + $wallHeight + 1;
for ($x = $startX; $x setBlock($x, $roofY, $z, self::BLOCK_WOOD);
}
}
}
private function getSpawnHeight($x, $z) {
// 从最高处开始向下寻找第一个固体方块
for ($y = 127; $y >= 0; $y--) {
$blockId = $this->getBlock($x, $y, $z);
if ($blockId != self::BLOCK_AIR) {
return $y + 1;
}
}
return 7; // 默认高度
}
public function run() {
$stdin = fopen('php://stdin', 'r');
stream_set_blocking($stdin, false);
$lastAutoSave = time();
$lastMemoryCheck = 0;
while ($this->running) {
$currentTime = microtime(true);
$this->loopCount++;
$input = fgets($stdin);
if ($input) {
$this->handleConsoleCommand(trim($input));
}
if ($currentTime - $this->lastKeepAlive > 20) {
$this->sendKeepAliveToAll();
$this->lastKeepAlive = $currentTime;
}
if (time() - $lastAutoSave > 120) {
if ($this->saveWorld()) {
echo "世界已自动保存\n";
} else {
echo "世界自动保存失败!\n";
}
$lastAutoSave = time();
}
if ($this->loopCount % $this->memoryCheckInterval === 0) {
$this->checkMemoryUsage();
$lastMemoryCheck = $currentTime;
}
$client = @socket_accept($this->socket);
if ($client !== false) {
socket_set_nonblock($client);
$clientId = $this->nextClientId++;
$this->clients[$clientId] = [
'socket' => $client,
'state' => 'handshaking',
'buffer' => '',
'compression' => false,
'username' => '',
'entityId' => $clientId,
'lastKeepAlive' => $currentTime,
'connected' => true
];
echo "新客户端连接: $clientId\n";
}
$clientIds = array_keys($this->clients);
foreach ($clientIds as $clientId) {
if (!isset($this->clients[$clientId])) {
continue;
}
$clientData = &$this->clients[$clientId];
if (!$clientData['connected']) {
$this->cleanupClient($clientId);
continue;
}
if ($currentTime - $clientData['lastKeepAlive'] > 60) {
echo "客户端 $clientId 超时\n";
$this->safeDisconnectClient($clientId, "超时");
continue;
}
$data = @socket_read($clientData['socket'], 4096);
if ($data === false) {
$error = socket_last_error($clientData['socket']);
if ($error !== SOCKET_EWOULDBLOCK) {
$this->safeDisconnectClient($clientId, "连接错误: " . socket_strerror($error));
}
continue;
}
if ($data === '') {
$this->safeDisconnectClient($clientId, "连接已关闭");
continue;
}
$clientData['buffer'] .= $data;
$clientData['lastKeepAlive'] = $currentTime;
if (strlen($clientData['buffer']) > 65536) {
echo "客户端 $clientId 缓冲区过大,断开连接\n";
$this->safeDisconnectClient($clientId, "缓冲区溢出");
continue;
}
$this->processClientData($clientId, $clientData);
}
$this->cleanupDisconnectedClients();
usleep(10000);
}
fclose($stdin);
}
private function checkMemoryUsage() {
$memoryUsage = memory_get_usage(true);
$memoryUsageMB = round($memoryUsage / 1024 / 1024, 2);
$memoryPeak = memory_get_peak_usage(true);
$memoryPeakMB = round($memoryPeak / 1024 / 1024, 2);
echo "内存使用: {$memoryUsageMB}MB, 峰值: {$memoryPeakMB}MB, 客户端: " . count($this->clients) . "\n";
if ($memoryUsage > 100 * 1024 * 1024) {
echo "内存使用较高,执行垃圾回收...\n";
gc_collect_cycles();
$afterGC = round(memory_get_usage(true) / 1024 / 1024, 2);
echo "垃圾回收后: {$afterGC}MB\n";
}
}
private function handleConsoleCommand($command) {
if ($command === 'stop') {
$this->shutdown();
} elseif ($command === 'list') {
$players = array_filter($this->clients, function($client) {
return $client['state'] === 'play' && !empty($client['username']) && $client['connected'];
});
$playerNames = array_map(function($client) {
return $client['username'];
}, $players);
if (empty($playerNames)) {
echo "没有玩家在线\n";
} else {
echo "在线玩家 (" . count($playerNames) . "): " . implode(', ', $playerNames) . "\n";
}
} elseif (strpos($command, 'say ') === 0) {
$message = substr($command, 4);
if (!empty($message)) {
$this->broadcastMessage("§6[服务器] §f" . $message);
echo "广播: $message\n";
}
} elseif ($command === 'save') {
if ($this->saveWorld()) {
echo "世界已手动保存\n";
} else {
echo "世界手动保存失败!\n";
}
} elseif ($command === 'gc') {
$before = memory_get_usage(true);
gc_collect_cycles();
$after = memory_get_usage(true);
$freed = round(($before - $after) / 1024 / 1024, 2);
echo "垃圾回收完成,释放 {$freed}MB 内存\n";
} elseif ($command === 'worldinfo') {
$totalBlocks = count($this->world);
$nonAirBlocks = 0;
foreach ($this->world as $blockId) {
if ($blockId != self::BLOCK_AIR) $nonAirBlocks++;
}
// 检查无效数据
$invalidData = 0;
foreach ($this->world as $key => $blockId) {
$coords = explode(',', $key);
if (count($coords) !== 3) {
$invalidData++;
continue;
}
if (!in_array($blockId, self::VALID_BLOCK_IDS)) {
$invalidData++;
}
}
echo "世界信息:\n";
echo "大小: {$this->worldSize}x{$this->worldSize}\n";
echo "总方块: $totalBlocks\n";
echo "非空气方块: $nonAirBlocks\n";
echo "无效数据: $invalidData\n";
echo "文件: {$this->worldFile}\n";
echo "版本: {$this->worldVersion}\n";
echo "完全加载: " . ($this->worldFullyLoaded ? "是" : "否") . "\n";
} elseif ($command === 'repair') {
echo "尝试修复世界数据...\n";
if ($this->repairWorldData()) {
echo "世界数据修复完成\n";
} else {
echo "世界数据修复失败\n";
}
} elseif ($command === 'cleanup') {
echo "执行数据清理...\n";
$before = count($this->world);
$this->deepCleanWorldData();
$after = count($this->world);
$cleaned = $before - $after;
echo "数据清理完成,移除了 $cleaned 个无效方块\n";
// 保存清理后的世界
if ($this->saveWorld()) {
echo "清理后的世界已保存\n";
}
} elseif (!empty($command)) {
echo "未知命令: $command\n";
echo "可用命令: stop, list, say , save, gc, worldinfo, repair, cleanup\n";
}
}
private function repairWorldData() {
echo "尝试修复世界数据...\n";
$backupExists = file_exists($this->worldBackupFile);
$mainExists = file_exists($this->worldFile);
if (!$mainExists && !$backupExists) {
echo "没有可修复的世界文件\n";
return false;
}
if ($mainExists) {
$data = file_get_contents($this->worldFile);
if ($data !== false) {
$worldData = json_decode($data, true);
if ($worldData !== null) {
$repaired = false;
if (!isset($worldData['version'])) {
$worldData['version'] = $this->worldVersion;
$repaired = true;
}
if (!isset($worldData['worldSize'])) {
$worldData['worldSize'] = $this->worldSize;
$repaired = true;
}
if (!isset($worldData['blocks']) || !is_array($worldData['blocks'])) {
$worldData['blocks'] = [];
$repaired = true;
}
if ($repaired) {
$jsonData = json_encode($worldData, JSON_PRETTY_PRINT);
if (file_put_contents($this->worldFile, $jsonData, LOCK_EX) !== false) {
echo "已修复世界文件格式\n";
$this->loadWorld();
return true;
}
} else {
echo "世界文件格式正确,无需修复\n";
return true;
}
}
}
}
if ($backupExists) {
if (copy($this->worldBackupFile, $this->worldFile)) {
echo "已从备份恢复世界文件\n";
$this->loadWorld();
return true;
}
}
return false;
}
private function sendKeepAliveToAll() {
$currentTime = time();
foreach ($this->clients as $clientId => &$clientData) {
if ($clientData['state'] === 'play' && $clientData['connected']) {
$this->sendPacket($clientId, 0x00, $this->writeVarInt($currentTime));
}
}
}
private function processClientData($clientId, &$clientData) {
while (strlen($clientData['buffer']) > 0 && $clientData['connected']) {
$originalBuffer = $clientData['buffer'];
$packetLength = $this->readVarInt($clientData['buffer']);
if ($packetLength === null) {
return;
}
if (strlen($clientData['buffer']) readVarInt($packetData);
if ($dataLength === null) {
continue;
}
if ($dataLength > 0) {
$decompressed = @zlib_decode($packetData);
if ($decompressed === false) {
echo "解压客户端 $clientId 的数据包失败\n";
continue;
}
$packetData = $decompressed;
}
}
$packetId = $this->readVarInt($packetData);
if ($packetId === null) {
continue;
}
$this->handlePacket($clientId, $clientData, $packetId, $packetData);
}
}
private function handlePacket($clientId, &$clientData, $packetId, $data) {
if (!$clientData['connected']) {
return;
}
switch ($clientData['state']) {
case 'handshaking':
$this->handleHandshaking($clientId, $clientData, $packetId, $data);
break;
case 'status':
$this->handleStatus($clientId, $clientData, $packetId, $data);
break;
case 'login':
$this->handleLogin($clientId, $clientData, $packetId, $data);
break;
case 'play':
$this->handlePlay($clientId, $clientData, $packetId, $data);
break;
}
}
private function handleHandshaking($clientId, &$clientData, $packetId, $data) {
if ($packetId === 0x00) {
$protocolVersion = $this->readVarInt($data);
$serverAddress = $this->readString($data);
$serverPort = $this->readUnsignedShort($data);
$nextState = $this->readVarInt($data);
if ($protocolVersion === null || $serverAddress === null || $serverPort === null || $nextState === null) {
$this->safeDisconnectClient($clientId, "无效的握手数据");
return;
}
$clientData['state'] = ($nextState === 1) ? 'status' : 'login';
echo "客户端 $clientId 握手: 协议版本=$protocolVersion, 下一状态=$nextState\n";
}
}
private function handleStatus($clientId, &$clientData, $packetId, $data) {
if ($packetId === 0x00) {
$response = json_encode([
"version" => [
"name" => "1.8.x",
"protocol" => 47
],
"players" => [
"max" => 20,
"online" => count(array_filter($this->clients, function($client) {
return $client['state'] === 'play' && $client['connected'];
})),
"sample" => []
],
"description" => [
"text" => "PHP Minecraft 服务器 1.8"
],
"favicon" => ""
]);
$this->sendPacket($clientId, 0x00, $this->writeString($response));
} elseif ($packetId === 0x01) {
$payload = $this->readLong($data);
if ($payload !== null) {
$this->sendPacket($clientId, 0x01, $this->writeLong($payload));
}
}
}
private function handleLogin($clientId, &$clientData, $packetId, $data) {
if ($packetId === 0x00) {
$username = $this->readString($data);
if ($username === null) {
$this->safeDisconnectClient($clientId, "无效的登录数据");
return;
}
echo "客户端 $clientId 登录为: $username\n";
$this->sendPacket($clientId, 0x03, $this->writeVarInt($this->compression_threshold));
$clientData['compression'] = true;
$uuid = $this->generateUUID($username);
$loginSuccess = $this->writeString($uuid) . $this->writeString($username);
$this->sendPacket($clientId, 0x02, $loginSuccess);
$clientData['state'] = 'play';
$clientData['username'] = $username;
$this->initializePlayer($clientId, $clientData);
}
}
private function generateUUID($username) {
$data = md5("OfflinePlayer:" . $username, true);
$data[6] = chr(ord($data[6]) & 0x0f | 0x30);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
$uuid = bin2hex($data);
return substr($uuid, 0, 8) . '-' .
substr($uuid, 8, 4) . '-' .
substr($uuid, 12, 4) . '-' .
substr($uuid, 16, 4) . '-' .
substr($uuid, 20, 12);
}
private function initializePlayer($clientId, &$clientData) {
$spawnX = $this->worldSize / 2 + 0.5;
$spawnZ = $this->worldSize / 2 + 0.5;
$spawnY = $this->getSpawnHeight(floor($spawnX), floor($spawnZ)) + 1.0;
$playerData = $this->loadPlayerData($clientData['username']);
if ($playerData !== null) {
$spawnX = $playerData['x'] ?? $spawnX;
$spawnY = $playerData['y'] ?? $spawnY;
$spawnZ = $playerData['z'] ?? $spawnZ;
echo "已加载玩家 {$clientData['username']} 的位置数据\n";
}
$this->playerPositions[$clientId] = [
'x' => $spawnX,
'y' => $spawnY,
'z' => $spawnZ,
'yaw' => 0.0,
'pitch' => 0.0,
'onGround' => true
];
$joinGame = $this->writeInt($clientData['entityId']) .
chr(0x08) .
chr(0) .
chr(0) .
chr(20) .
$this->writeString("default") .
chr(0);
$this->sendPacket($clientId, 0x01, $joinGame);
$spawnPos = $this->writePosition(floor($spawnX), floor($spawnY) - 1, floor($spawnZ));
$this->sendPacket($clientId, 0x05, $spawnPos);
$abilities = chr(0x0F) .
$this->writeFloat(0.05) .
$this->writeFloat(0.1);
$this->sendPacket($clientId, 0x39, $abilities);
$posLook = $this->writeDouble($spawnX) .
$this->writeDouble($spawnY) .
$this->writeDouble($spawnZ) .
$this->writeFloat(0.0) .
$this->writeFloat(0.0) .
chr(0x00);
$this->sendPacket($clientId, 0x08, $posLook);
$this->sendInitialChunks($clientId);
echo "玩家 {$clientData['username']} 加入了游戏 (创造模式)\n";
$this->broadcastMessage("§e{$clientData['username']} 加入了游戏");
$this->sendMessage($clientId, "§a欢迎来到 PHP Minecraft 服务器!");
$this->sendMessage($clientId, "§6你是创造模式,可以飞行和放置方块");
$this->sendMessage($clientId, "§6输入 §a/help §6查看可用命令");
$this->sendMessage($clientId, "§6世界已加载: {$this->worldSize}x{$this->worldSize}, " . count($this->world) . " 个方块");
if (!$this->worldFullyLoaded) {
$this->sendMessage($clientId, "§c警告: 世界可能没有完全加载,如果发现问题请重新加入");
}
}
private function sendInitialChunks($clientId) {
$centerChunkX = floor($this->worldSize / 2) >> 4;
$centerChunkZ = floor($this->worldSize / 2) >> 4;
for ($dx = -1; $dx sendChunk($clientId, $chunkX, $chunkZ);
}
}
}
private function sendChunk($clientId, $chunkX, $chunkZ) {
$chunkData = $this->buildChunkData($chunkX, $chunkZ);
if ($chunkData !== null) {
$this->sendPacket($clientId, 0x21, $chunkData);
}
}
private function buildChunkData($chunkX, $chunkZ) {
$groundUpContinuous = true;
$primaryBitMask = 0x1F;
$chunkSections = '';
for ($sectionY = 0; $sectionY buildChunkSection($chunkX, $sectionY, $chunkZ);
$chunkSections .= $sectionData;
}
$biomeData = str_repeat(chr(1), 256);
$data = $chunkSections . $biomeData;
$chunkPacket = $this->writeInt($chunkX) .
$this->writeInt($chunkZ) .
chr($groundUpContinuous ? 1 : 0) .
$this->writeUnsignedShort($primaryBitMask) .
$this->writeVarInt(strlen($data)) .
$data;
return $chunkPacket;
}
private function buildChunkSection($chunkX, $sectionY, $chunkZ) {
$blocksPerSection = 16 * 16 * 16;
$baseY = $sectionY * 16;
$blockData = '';
$blockLight = str_repeat("\xFF", 2048);
$skyLight = str_repeat("\xFF", 2048);
for ($y = 0; $y getBlock($worldX, $worldY, $worldZ);
$metadata = 0;
$value = ($blockId getBlock($worldX, $worldY, $worldZ) != self::BLOCK_AIR) {
$nonAirBlocks++;
}
}
}
}
$sectionData = $this->writeUnsignedShort($nonAirBlocks) .
$blockData .
$blockLight .
$skyLight;
return $sectionData;
}
private function handlePlay($clientId, &$clientData, $packetId, $data) {
if (!$clientData['connected']) {
return;
}
switch ($packetId) {
case 0x00:
$keepAliveId = $this->readVarInt($data);
if ($keepAliveId !== null) {
$clientData['lastKeepAlive'] = microtime(true);
$this->sendPacket($clientId, 0x00, $this->writeVarInt($keepAliveId));
}
break;
case 0x01:
$message = $this->readString($data);
if ($message !== null && !empty(trim($message))) {
$trimmedMessage = trim($message);
echo "来自 {$clientData['username']} 的聊天: $trimmedMessage\n";
if (strpos($trimmedMessage, '/') === 0) {
$this->handlePlayerCommand($clientId, $clientData, $trimmedMessage);
} else {
$this->broadcastMessage("§7 §f$trimmedMessage");
}
}
break;
case 0x04:
$x = $this->readDouble($data);
$feetY = $this->readDouble($data);
$z = $this->readDouble($data);
$onGround = $this->readBool($data);
if ($x !== null && $feetY !== null && $z !== null && $onGround !== null) {
$this->updatePlayerPosition($clientId, $clientData, $x, $feetY, $z, $onGround);
}
break;
case 0x05:
$yaw = $this->readFloat($data);
$pitch = $this->readFloat($data);
$onGround = $this->readBool($data);
if ($yaw !== null && $pitch !== null && $onGround !== null) {
$this->updatePlayerOrientation($clientId, $yaw, $pitch, $onGround);
}
break;
case 0x06:
$x = $this->readDouble($data);
$feetY = $this->readDouble($data);
$z = $this->readDouble($data);
$yaw = $this->readFloat($data);
$pitch = $this->readFloat($data);
$onGround = $this->readBool($data);
if ($x !== null && $feetY !== null && $z !== null &&
$yaw !== null && $pitch !== null && $onGround !== null) {
$this->updatePlayerPosition($clientId, $clientData, $x, $feetY, $z, $onGround);
$this->updatePlayerOrientation($clientId, $yaw, $pitch, $onGround);
}
break;
case 0x07:
$status = $this->readByte($data);
$location = $this->readPosition($data);
$face = $this->readByte($data);
if ($status !== null && $location !== null && $face !== null) {
$this->handlePlayerDigging($clientId, $clientData, $status, $location, $face);
}
break;
case 0x08:
$location = $this->readPosition($data);
$face = $this->readByte($data);
$heldItem = $this->readSlot($data);
$cursorX = $this->readByte($data);
$cursorY = $this->readByte($data);
$cursorZ = $this->readByte($data);
if ($location !== null && $face !== null) {
$this->handlePlayerBlockPlacement($clientId, $clientData, $location, $face, $heldItem);
}
break;
case 0x0B:
break;
case 0x0C:
$entityId = $this->readVarInt($data);
$actionId = $this->readVarInt($data);
$actionParam = $this->readVarInt($data);
if ($actionId === 0 || $actionId === 1) {
$actionName = $actionId === 0 ? "开始潜行" : "停止潜行";
echo "玩家 {$clientData['username']} $actionName\n";
} elseif ($actionId === 3 || $actionId === 4) {
$actionName = $actionId === 3 ? "开始冲刺" : "停止冲刺";
echo "玩家 {$clientData['username']} $actionName\n";
}
break;
default:
break;
}
}
private function handlePlayerDigging($clientId, $clientData, $status, $location, $face) {
list($x, $y, $z) = $location;
if ($status === 0) {
$blockId = $this->getBlock($x, $y, $z);
if ($blockId != self::BLOCK_AIR) {
echo "玩家 {$clientData['username']} 开始挖掘方块在 ($x, $y, $z) - 方块ID: $blockId\n";
$this->setBlock($x, $y, $z, self::BLOCK_AIR);
$this->broadcastBlockChange($x, $y, $z, self::BLOCK_AIR);
$this->sendMessage($clientId, "§a方块已被破坏");
if ($this->saveWorld()) {
$this->sendMessage($clientId, "§a世界已自动保存");
} else {
$this->sendMessage($clientId, "§c世界保存失败,更改可能丢失!");
}
}
}
}
private function handlePlayerBlockPlacement($clientId, $clientData, $location, $face, $heldItem) {
list($x, $y, $z) = $location;
switch ($face) {
case 0: $y--; break;
case 1: $y++; break;
case 2: $z--; break;
case 3: $z++; break;
case 4: $x--; break;
case 5: $x++; break;
}
if ($x >= 0 && $x worldSize &&
$z >= 0 && $z worldSize &&
$y >= 0 && $y getBlock($x, $y, $z) == self::BLOCK_AIR) {
$this->setBlock($x, $y, $z, self::BLOCK_STONE);
$this->broadcastBlockChange($x, $y, $z, self::BLOCK_STONE);
echo "玩家 {$clientData['username']} 放置方块在 ($x, $y, $z)\n";
$this->sendMessage($clientId, "§a方块已放置");
if ($this->saveWorld()) {
$this->sendMessage($clientId, "§a世界已自动保存");
} else {
$this->sendMessage($clientId, "§c世界保存失败,更改可能丢失!");
}
}
}
private function broadcastBlockChange($x, $y, $z, $blockId) {
$blockChangePacket = $this->writePosition($x, $y, $z) .
$this->writeVarInt($blockId);
foreach ($this->clients as $id => $client) {
if ($client['state'] === 'play' && $client['connected']) {
$this->sendPacket($id, 0x23, $blockChangePacket);
}
}
}
private function updatePlayerPosition($clientId, $clientData, $x, $y, $z, $onGround) {
$margin = 5;
if ($x = $this->worldSize + $margin ||
$z = $this->worldSize + $margin ||
$y 256) {
$spawnX = $this->worldSize / 2 + 0.5;
$spawnZ = $this->worldSize / 2 + 0.5;
$spawnY = $this->getSpawnHeight(floor($spawnX), floor($spawnZ)) + 1.0;
$posLook = $this->writeDouble($spawnX) .
$this->writeDouble($spawnY) .
$this->writeDouble($spawnZ) .
$this->writeFloat($this->playerPositions[$clientId]['yaw']) .
$this->writeFloat($this->playerPositions[$clientId]['pitch']) .
chr(0x00);
$this->sendPacket($clientId, 0x08, $posLook);
$this->playerPositions[$clientId]['x'] = $spawnX;
$this->playerPositions[$clientId]['y'] = $spawnY;
$this->playerPositions[$clientId]['z'] = $spawnZ;
$this->sendMessage($clientId, "§c您不能超出世界边界");
return;
}
$this->playerPositions[$clientId]['x'] = $x;
$this->playerPositions[$clientId]['y'] = $y;
$this->playerPositions[$clientId]['z'] = $z;
$this->playerPositions[$clientId]['onGround'] = $onGround;
if (!empty($clientData['username']) && time() % 30 === 0) {
$playerData = [
'x' => $x,
'y' => $y,
'z' => $z,
'yaw' => $this->playerPositions[$clientId]['yaw'],
'pitch' => $this->playerPositions[$clientId]['pitch'],
'lastSave' => time()
];
$this->savePlayerData($clientData['username'], $playerData);
}
}
private function updatePlayerOrientation($clientId, $yaw, $pitch, $onGround) {
$this->playerPositions[$clientId]['yaw'] = $yaw;
$this->playerPositions[$clientId]['pitch'] = $pitch;
$this->playerPositions[$clientId]['onGround'] = $onGround;
}
private function handlePlayerCommand($clientId, $clientData, $command) {
$parts = explode(' ', $command);
$cmd = strtolower($parts[0]);
switch ($cmd) {
case '/help':
$this->sendMessage($clientId, "§6可用命令:");
$this->sendMessage($clientId, "§a/help §7- 显示此帮助");
$this->sendMessage($clientId, "§a/list §7- 列出在线玩家");
$this->sendMessage($clientId, "§a/spawn §7- 传送至出生点");
$this->sendMessage($clientId, "§a/gamemode §7- 切换游戏模式");
$this->sendMessage($clientId, "§a/time set §7- 设置时间");
$this->sendMessage($clientId, "§a/save §7- 手动保存世界");
$this->sendMessage($clientId, "§a/worldinfo §7- 显示世界信息");
$this->sendMessage($clientId, "§a/repair §7- 尝试修复世界数据");
$this->sendMessage($clientId, "§a/cleanup §7- 清理无效数据");
break;
case '/list':
$players = array_filter($this->clients, function($client) {
return $client['state'] === 'play' && !empty($client['username']) && $client['connected'];
});
$playerNames = array_map(function($client) {
return $client['username'];
}, $players);
$this->sendMessage($clientId, "§6在线玩家 (§a" . count($playerNames) . "§6): §e" . implode('§7, §e', $playerNames));
break;
case '/spawn':
$spawnX = $this->worldSize / 2 + 0.5;
$spawnZ = $this->worldSize / 2 + 0.5;
$spawnY = $this->getSpawnHeight(floor($spawnX), floor($spawnZ)) + 1.0;
$posLook = $this->writeDouble($spawnX) .
$this->writeDouble($spawnY) .
$this->writeDouble($spawnZ) .
$this->writeFloat($this->playerPositions[$clientId]['yaw']) .
$this->writeFloat($this->playerPositions[$clientId]['pitch']) .
chr(0x00);
$this->sendPacket($clientId, 0x08, $posLook);
$this->playerPositions[$clientId]['x'] = $spawnX;
$this->playerPositions[$clientId]['y'] = $spawnY;
$this->playerPositions[$clientId]['z'] = $spawnZ;
$this->sendMessage($clientId, "§a已传送至出生点");
break;
case '/gamemode':
if (count($parts) sendMessage($clientId, "§c用法: /gamemode ");
break;
}
$mode = intval($parts[1]);
if ($mode === 0 || $mode === 1) {
$gameStateChange = chr(3) . $this->writeFloat($mode);
$this->sendPacket($clientId, 0x2B, $gameStateChange);
$modeName = $mode === 0 ? "生存" : "创造";
$this->sendMessage($clientId, "§a游戏模式已更改为 $modeName 模式");
} else {
$this->sendMessage($clientId, "§c无效的游戏模式。使用 0(生存) 或 1(创造)");
}
break;
case '/time':
if (count($parts) sendMessage($clientId, "§c用法: /time set ");
break;
}
$time = strtolower($parts[2]);
if ($time === 'day') {
$worldAge = 0;
$timeOfDay = 0;
} elseif ($time === 'night') {
$worldAge = 0;
$timeOfDay = 13000;
} else {
$this->sendMessage($clientId, "§c无效的时间。使用 'day' 或 'night'");
break;
}
$timeUpdate = $this->writeLong($worldAge) . $this->writeLong($timeOfDay);
$this->broadcastPacket(0x03, $timeUpdate);
$this->sendMessage($clientId, "§a时间已设置为 $time");
break;
case '/save':
if ($this->saveWorld()) {
$this->sendMessage($clientId, "§a世界保存成功!");
$this->broadcastMessage("§6[服务器] §a世界已由 {$clientData['username']} 手动保存");
} else {
$this->sendMessage($clientId, "§c世界保存失败!");
}
break;
case '/worldinfo':
$totalBlocks = count($this->world);
$nonAirBlocks = 0;
foreach ($this->world as $blockId) {
if ($blockId != self::BLOCK_AIR) $nonAirBlocks++;
}
$invalidData = 0;
foreach ($this->world as $key => $blockId) {
$coords = explode(',', $key);
if (count($coords) !== 3) {
$invalidData++;
continue;
}
if (!in_array($blockId, self::VALID_BLOCK_IDS)) {
$invalidData++;
}
}
$worldSize = $this->worldSize;
$this->sendMessage($clientId, "§6世界信息:");
$this->sendMessage($clientId, "§a大小: §e{$worldSize}x{$worldSize} 方块");
$this->sendMessage($clientId, "§a总方块: §e$totalBlocks 个");
$this->sendMessage($clientId, "§a非空气方块: §e$nonAirBlocks 个");
$this->sendMessage($clientId, "§a无效数据: §e$invalidData 个");
$this->sendMessage($clientId, "§a文件: §e{$this->worldFile}");
$this->sendMessage($clientId, "§a版本: §e{$this->worldVersion}");
$this->sendMessage($clientId, "§a完全加载: §e" . ($this->worldFullyLoaded ? "是" : "否"));
break;
case '/repair':
$this->sendMessage($clientId, "§a正在尝试修复世界数据...");
if ($this->repairWorldData()) {
$this->sendMessage($clientId, "§a世界数据修复完成");
} else {
$this->sendMessage($clientId, "§c世界数据修复失败");
}
break;
case '/cleanup':
$this->sendMessage($clientId, "§a正在执行数据清理...");
$before = count($this->world);
$this->deepCleanWorldData();
$after = count($this->world);
$cleaned = $before - $after;
$this->sendMessage($clientId, "§a数据清理完成,移除了 $cleaned 个无效方块");
if ($this->saveWorld()) {
$this->sendMessage($clientId, "§a清理后的世界已保存");
}
break;
default:
$this->sendMessage($clientId, "§c未知命令。输入 §a/help §c获取帮助。");
break;
}
}
private function broadcastPacket($packetId, $data) {
foreach ($this->clients as $id => $client) {
if ($client['state'] === 'play' && $client['connected']) {
$this->sendPacket($id, $packetId, $data);
}
}
}
private function sendMessage($clientId, $message) {
$chatPacket = $this->writeString(json_encode([
"text" => $message
])) . chr(0);
$this->sendPacket($clientId, 0x02, $chatPacket);
}
private function broadcastMessage($message) {
$chatPacket = $this->writeString(json_encode([
"text" => $message
])) . chr(0);
foreach ($this->clients as $id => $client) {
if ($client['state'] === 'play' && $client['connected']) {
$this->sendPacket($id, 0x02, $chatPacket);
}
}
}
private function sendPacket($clientId, $packetId, $data) {
if (!isset($this->clients[$clientId]) || !$this->clients[$clientId]['connected']) {
return false;
}
$clientData = $this->clients[$clientId];
$packet = $this->writeVarInt($packetId) . $data;
if ($clientData['compression']) {
if (strlen($packet) >= $this->compression_threshold) {
$compressed = @zlib_encode($packet, ZLIB_ENCODING_DEFLATE, 1);
if ($compressed === false) {
echo "压缩客户端 $clientId 的数据包失败\n";
return false;
}
$packet = $this->writeVarInt(strlen($packet)) . $compressed;
} else {
$packet = $this->writeVarInt(0) . $packet;
}
}
$fullPacket = $this->writeVarInt(strlen($packet)) . $packet;
$result = @socket_write($clientData['socket'], $fullPacket);
if ($result === false) {
$error = socket_last_error($clientData['socket']);
$errorMsg = socket_strerror($error);
if ($error === 32 || $error === 104) {
$this->safeDisconnectClient($clientId, "连接已断开 ($errorMsg)");
return false;
}
echo "发送客户端 $clientId 错误 [$error]: $errorMsg\n";
return false;
}
return true;
}
private function safeDisconnectClient($clientId, $reason = "") {
if (!isset($this->clients[$clientId])) {
return;
}
$this->clients[$clientId]['connected'] = false;
$this->clientTimeouts[$clientId] = microtime(true);
$username = $this->clients[$clientId]['username'];
if (!empty($username)) {
echo "玩家 $username 断开连接" . ($reason ? " ($reason)" : "") . "\n";
$this->broadcastMessage("§e{$username} 离开了游戏");
if (isset($this->playerPositions[$clientId])) {
$pos = $this->playerPositions[$clientId];
$playerData = [
'x' => $pos['x'],
'y' => $pos['y'],
'z' => $pos['z'],
'yaw' => $pos['yaw'],
'pitch' => $pos['pitch'],
'lastSave' => time()
];
$this->savePlayerData($username, $playerData);
}
} else {
echo "客户端 $clientId 断开连接" . ($reason ? " ($reason)" : "") . "\n";
}
}
private function cleanupClient($clientId) {
if (isset($this->clients[$clientId])) {
@socket_close($this->clients[$clientId]['socket']);
unset($this->clients[$clientId]);
}
if (isset($this->playerPositions[$clientId])) {
unset($this->playerPositions[$clientId]);
}
if (isset($this->clientTimeouts[$clientId])) {
unset($this->clientTimeouts[$clientId]);
}
if (count($this->clients) % 5 === 0) {
gc_collect_cycles();
}
}
private function cleanupDisconnectedClients() {
$currentTime = microtime(true);
$toRemove = [];
foreach ($this->clientTimeouts as $clientId => $timeoutTime) {
if ($currentTime - $timeoutTime > 1.0) {
$toRemove[] = $clientId;
}
}
foreach ($toRemove as $clientId) {
$this->cleanupClient($clientId);
}
}
public function shutdown() {
echo "正在关闭服务器...\n";
$this->running = false;
$this->broadcastMessage("§c服务器正在关闭");
if ($this->saveWorld()) {
echo "世界保存成功\n";
} else {
echo "世界保存失败!\n";
}
foreach ($this->clients as $clientId => $clientData) {
if (!empty($clientData['username']) && isset($this->playerPositions[$clientId])) {
$pos = $this->playerPositions[$clientId];
$playerData = [
'x' => $pos['x'],
'y' => $pos['y'],
'z' => $pos['z'],
'yaw' => $pos['yaw'],
'pitch' => $pos['pitch'],
'lastSave' => time()
];
$this->savePlayerData($clientData['username'], $playerData);
}
}
foreach ($this->clients as $clientId => $clientData) {
if ($clientData['connected']) {
$disconnectMsg = $this->writeString(json_encode([
"text" => "服务器已关闭"
]));
$this->sendPacket($clientId, 0x40, $disconnectMsg);
}
}
usleep(500000);
foreach (array_keys($this->clients) as $clientId) {
$this->cleanupClient($clientId);
}
socket_close($this->socket);
echo "服务器已停止。\n";
exit(0);
}
private function readVarInt(&$data) {
$value = 0;
$shift = 0;
do {
if (strlen($data) === 0) return null;
$byte = ord($data[0]);
$data = substr($data, 1);
$value |= ($byte & 0x7F) 35) return null;
} while ($byte & 0x80);
return $value;
}
private function writeVarInt($value) {
$bytes = '';
do {
$byte = $value & 0x7F;
$value >>= 7;
if ($value !== 0) $byte |= 0x80;
$bytes .= chr($byte);
} while ($value !== 0);
return $bytes;
}
private function readString(&$data) {
$length = $this->readVarInt($data);
if ($length === null || strlen($data) writeVarInt(strlen($string)) . $string;
}
private function readUnsignedShort(&$data) {
if (strlen($data) = 0x80000000) $value -= 0x100000000;
$data = substr($data, 4);
return $value;
}
private function writeInt($value) {
return pack('N', $value);
}
private function readLong(&$data) {
if (strlen($data) > 32) & 0xFFFFFFFF;
$low = $value & 0xFFFFFFFF;
return pack('NN', $high, $low);
}
private function readDouble(&$data) {
if (strlen($data) writeLong((($x & 0x3FFFFFF) readLong($data);
if ($val === null) return null;
$x = $val >> 38;
$y = ($val >> 26) & 0xFFF;
$z = $val > 38;
if ($x >= 0x2000000) $x -= 0x4000000;
if ($y >= 0x800) $y -= 0x1000;
if ($z >= 0x2000000) $z -= 0x4000000;
return [$x, $y, $z];
}
private function readSlot(&$data) {
$itemId = $this->readShort($data);
if ($itemId === null) return null;
if ($itemId !== -1) {
$this->readByte($data);
$this->readShort($data);
$nbtLength = $this->readShort($data);
if ($nbtLength > 0) {
$this->readBytes($data, $nbtLength);
}
}
return $itemId;
}
private function readShort(&$data) {
if (strlen($data) = 0x8000) $value -= 0x10000;
$data = substr($data, 2);
return $value;
}
private function readBytes(&$data, $length) {
if (strlen($data) run();
} catch (Exception $e) {
echo "服务器错误: " . $e->getMessage() . "\n";
exit(1);
}
?>

