All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.landuo.l4d2.utils.A2sUtil Maven / Gradle / Ivy

package io.github.landuo.l4d2.utils;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.landuo.l4d2.entity.A2sPlayers;
import io.github.landuo.l4d2.entity.Player;
import io.github.landuo.l4d2.entity.SourceServerInfo;
import io.github.landuo.l4d2.exception.ServiceException;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author accidia
 * @see Server Queries
 */
public class A2sUtil {
    private static final String A2S_INFO = "FF FF FF FF 54 53 6F 75 72 63 65 20 45 6E 67 69 6E 65 20 51 75 65 72 79 00 FF FF FF FF";
    private static final String A2S_PLAYER = "FF FF FF FF 55 FF FF FF FF";
    private static final String A2S_RULE = "FF FF FF FF 56 FF FF FF FF";
    private static final String A2S_PING = "FF FF FF FF 69";

    public static Integer soTimeOut = 2000;

    /**
     * 计算服务器延迟
     *
     * @param ipPort ip:port
     * @return 服务器延迟, 结果为-1时表示连接不上服务器
     */
    public static Long getPing(String ipPort) {
        long current = System.currentTimeMillis();
        String[] split = ipPort.split(":");
        String[] payload = A2S_PING.split(" ");
        ByteBuffer buffer = ByteBuffer.allocate(payload.length);
        for (String s : payload) {
            buffer.put((byte) Integer.parseInt(s, 16));
        }
        DatagramSocket ds;
        try {
            ds = new DatagramSocket();
            ds.setSoTimeout(soTimeOut);
            InetSocketAddress inetSocketAddress = new InetSocketAddress(split[0], Integer.parseInt(split[1]));
            ds.connect(inetSocketAddress);
            // 请求challenge number
            DatagramPacket packet = new DatagramPacket(buffer.array(), 0, buffer.array().length, inetSocketAddress);
            ds.send(packet);
            byte[] challengeResponse = new byte[100];
            packet = new DatagramPacket(challengeResponse, challengeResponse.length);
            ds.receive(packet);
            ds.close();
        } catch (IOException e) {
            return -1L;
        }
        return (System.currentTimeMillis() - current);
    }

    /**
     * Returns the server rules, or configuration variables in name/value pairs.
     *
     * @param ipPort ip:port
     * @return server configuration variables
     */
    public static Map getA2sRules(String ipPort) {
        List list;
        String[] split = ipPort.split(":");
        try {
            list = send(split[0], Integer.valueOf(split[1]), A2S_RULE);
        } catch (IOException e) {
            throw new ServiceException(e.getMessage(), e);
        }
        byte[] data = new byte[list.stream().mapToInt(e -> e.length).sum()];
        int tempIndex = 0;
        // 组装成一个大数组
        for (int i = 0; i < list.size(); i++) {
            final int thisIndex = i;
            Optional optional = list.stream().filter(e -> e[9] == thisIndex).findFirst();
            if (!optional.isPresent()) {
                continue;
            }
            byte[] response = optional.get();
            int j = (thisIndex == 0) ? 19 : 12;
            System.arraycopy(response, j, data, tempIndex, response.length - j);
            tempIndex += response.length - j;
        }
        Map result = new LinkedHashMap<>();
        byte[] temp = new byte[100];
        String key = null;
        tempIndex = 0;
        for (byte datum : data) {
            if (datum == 0) {
                if ("".equals(key)) {
                    break;
                }
                // key为null时说明已经存放完一对键值对
                if (key == null) {
                    key = new String(temp, StandardCharsets.UTF_8).trim();
                } else {
                    result.put(key, new String(temp, StandardCharsets.UTF_8).trim());
                    key = null;
                }
                temp = new byte[100];
                // 重置缓存数组计数
                tempIndex = 0;
                continue;
            }
            temp[tempIndex++] = datum;
        }
        return result;
    }

    /**
     * 查询服务器信息
     *
     * @param ipPort ip:port
     * @return 新source server返回的服务器信息
     */
    public static SourceServerInfo getA2sInfo(String ipPort) {
        List list;
        String[] split = ipPort.split(":");
        try {
            list = send(split[0], Integer.valueOf(split[1]), A2S_INFO);
        } catch (IOException e) {
            throw new ServiceException(e.getMessage(), e);
        }
        Map result = new HashMap<>(16);
        byte[] response = list.get(0);
        if (response[4] == (byte) 0x49) {
            getA2sInfoResponseWithNewApi(result, response);
        }
        result.put("times", getPing(ipPort));
        ObjectMapper mapper = new ObjectMapper();
        return mapper.convertValue(result, SourceServerInfo.class);
    }

    /**
     * 获取服务器玩家信息
     *
     * @param ipPort ip:port
     * @return 服务器当前玩家信息
     */
    public static A2sPlayers getPlayers(String ipPort) {
        String[] arr = ipPort.split(":");
        List list = null;
        try {
            list = send(arr[0], Integer.valueOf(arr[1]), A2S_PLAYER);
        } catch (IOException e) {
            throw new ServiceException(e.getMessage(), e);
        }
        A2sPlayers a2sPlayers = new A2sPlayers();
        byte[] response = list.get(0);
        if (response[4] == (byte) 0x44) {
            int i = 5;
            int total = response[i++];
            a2sPlayers.setTotal(total);
            List players = new LinkedList<>();
            for (int j = 0; j < total; j++) {
                Player player = new Player();
                player.setIndex((int) response[i++]);
                byte[] tmp = new byte[100];
                int k = 0;
                while (response[i] != 0) {
                    tmp[k++] = response[i++];
                }
                i++;
                player.setName(new String(tmp, StandardCharsets.UTF_8).trim());

                ByteBuffer buffer = ByteBuffer.allocate(4);
                buffer.order(ByteOrder.LITTLE_ENDIAN);
                for (int z = 0; z < 4; z++) {
                    buffer.put(response[i++]);
                }
                player.setScore((long) buffer.getInt(0));
                buffer.clear();
                for (int z = 0; z < 4; z++) {
                    buffer.put(z, response[i++]);
                }
                player.setDuration(convertFloatToDurationTime(buffer.getFloat(0)));
                players.add(player);
            }
            players.sort(Comparator.comparing(Player::getScore).reversed());
            a2sPlayers.setPlayers(players);
            return a2sPlayers;
        }
        return null;
    }

    /**
     * float类型数据转化成 0h0m0s 格式
     */
    private static String convertFloatToDurationTime(Float duration) {
        int intValue = duration.intValue();
        if (intValue < 60) {
            return intValue + "s";
        }
        if (intValue <= 3600) {
            return (intValue / 60) + "m" + (intValue % 60) + "s";
        }
        int h = intValue / 3600;
        int m = intValue - (h * 3600);
        return h + "h" + (m / 60) + "m" + (m % 60) + "s";
    }

    /**
     * 组装服务器信息结果
     *
     * @param result   返回的结果
     * @param response 接收到的Source Server数据
     */
    private static void getA2sInfoResponseWithNewApi(Map result, byte[] response) {
        int i = 6;
        // 取前四个内容转ascii
        i = putStrToResult(response, i, result, "name");
        i = putStrToResult(response, i, result, "map");
        i = putStrToResult(response, i, result, "folder");
        i = putStrToResult(response, i, result, "game");

        // Steam Application ID of game.
        i = putNumberToResult(2, response, i, result, "id");
        // Number of players on the server.
        result.put("players", String.valueOf(response[i++]));
        // Maximum number of players the server reports it can hold.
        result.put("maxPlayers", String.valueOf(response[i++]));
        //Number of bots on the server.
        result.put("bots", String.valueOf(response[i++]));
        //Indicates the type of server:
        result.put("serverType", String.valueOf((char) response[i++]));
        //Indicates the operating system of the server:
        char serverType = (char) response[i++];
        result.put("environment", "l".equals(String.valueOf(serverType)) ? "Linux" : ("w".equals(String.valueOf(serverType)) ? "Windows" : "Mac"));
        //Indicates whether the server requires a password:
        result.put("visibility", "0".equals(String.valueOf(response[i++])) ? "public" : "private");
        //Indicates whether the server requires a password:
        result.put("vac", "0".equals(String.valueOf(response[i++])) ? "unsecured" : "secured");
        // Version of the game installed on the server.
        i = putStrToResult(response, i, result, "version");

        // 结束标识位
        byte EDF = response[i++];
        if ((EDF & (byte) 0x80) != 0) {
            i = putNumberToResult(2, response, i, result, "port");
        }
        if ((EDF & (byte) 0x10) != 0) {
            i = putNumberToResult(8, response, i, result, "steamID");
        }
        if ((EDF & (byte) 0x40) != 0) {
            i = putNumberToResult(2, response, i, result, "sourceTVPort");
            i = putStrToResult(response, i, result, "sourceTVName");
        }
        if ((EDF & (byte) 0x20) != 0) {
            i = putStrToResult(response, i, result, "keywords");
        }
        if ((EDF & (byte) 0x01) != 0) {
            i = putNumberToResult(8, response, i, result, "gameID");
        }
    }

    /**
     * 发送查询命令并接收结果
     */
    private static List send(String ip, Integer port, String request) throws IOException {
        InetSocketAddress inetSocketAddress = new InetSocketAddress(ip, port);
        DatagramSocket ds = new DatagramSocket();
        ds.setSoTimeout(soTimeOut);
        ds.connect(inetSocketAddress);
        String[] payload = request.split(" ");
        ByteBuffer buffer = ByteBuffer.allocate(payload.length);
        for (String s : payload) {
            buffer.put((byte) Integer.parseInt(s, 16));
        }
        // 请求challenge number
        DatagramPacket packet = new DatagramPacket(buffer.array(), 0, buffer.array().length, inetSocketAddress);
        ds.send(packet);
        byte[] challengeResponse = new byte[9];
        packet = new DatagramPacket(challengeResponse, challengeResponse.length);
        ds.receive(packet);

        for (int i = 0; i < 4; i++) {
            buffer.put(payload.length - 4 + i, challengeResponse[i + 5]);
        }
        packet = new DatagramPacket(buffer.array(), 0, buffer.limit(), inetSocketAddress);
        ds.send(packet);
        List list = new LinkedList<>();
        byte[] response = new byte[4096];
        try {
            while (true) {
                packet = new DatagramPacket(response, response.length);
                ds.receive(packet);
                byte[] data = new byte[packet.getLength()];
                System.arraycopy(response, 0, data, 0, packet.getLength());
                list.add(data);
                if (response[0] == (byte) 0xFF) {
                    return list;
                }
            }
        } catch (SocketTimeoutException ignored) {
            list.sort(Comparator.comparing(e -> e[9]));
        }
        ds.close();
        return list;
    }

    /**
     * 组装字符串类型结果
     */
    private static int putStrToResult(byte[] response, int i, Map result, String key) {
        int j = 0;
        byte[] bytes = new byte[100];
        for (; i < response.length; i++) {
            if (response[i] == 0) {
                result.put(key, new String(bytes).trim());
                break;
            }
            bytes[j++] = response[i];
        }
        return ++i;
    }

    /**
     * 组装数字类型结果
     */
    private static int putNumberToResult(Integer capacity, byte[] response, Integer i, Map result, String key) {
        ByteBuffer buffer = ByteBuffer.allocate(capacity);
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        for (int j = 0; j < capacity; j++) {
            buffer.put(response[i++]);
        }
        // 根据容量选择需要的数字类型
        String number = null;
        switch (capacity) {
            case 1:
                number = String.valueOf(buffer.get(0));
                break;
            case 2:
                number = String.valueOf(buffer.getShort(0));
                break;
            case 4:
                number = String.valueOf(buffer.getInt(0));
                break;
            //long long type
            case 8:
                number = String.valueOf(buffer.getLong(0));
                break;
            default:
                break;
        }
        result.put(key, number);
        return i;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy