Last active
May 16, 2021 16:57
-
-
Save mataha/10f65976a7b2bdf5a4ecc13d8b9d38be to your computer and use it in GitHub Desktop.
GunZ server population scraper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# To the extent possible under law, mataha has waived all | |
# copyright and related or neighboring rights to this file, | |
# as it is written in the following disclaimers: | |
# - https://unlicense.org/ | |
# - https://creativecommons.org/publicdomain/zero/1.0/ | |
import argparse | |
import pathlib | |
import socket | |
import struct | |
import sys | |
from typing import Any, Final, NoReturn, Optional, Tuple, Union | |
# https://github.com/python/typeshed/blob/master/stdlib/socket.pyi#L607 | |
_Address = Union[Tuple[Any, ...], str] | |
_Arguments = Optional[list[str]] | |
PAYLOAD: Final[bytes] = bytes.fromhex("64 00 0b 00 73 00 05 00 41 9c 00") | |
BUFFER_SIZE = 1024 | |
DATA_OFFSET = 0x20 | |
class SingleLineArgumentParser(argparse.ArgumentParser): | |
def convert_arg_line_to_args(self, arg_line: str) -> list[str]: | |
return arg_line.split() | |
def players_online(server: _Address, /, offset: int = 0x00) -> Tuple[int, int]: | |
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client: | |
client.settimeout(1.0) | |
client.sendto(PAYLOAD, server) | |
data, _ = client.recvfrom(BUFFER_SIZE) | |
layout = "<HH" | |
size = struct.calcsize(layout) | |
offset += DATA_OFFSET | |
maximum, current = struct.unpack(layout, data[offset:offset + size]) | |
return maximum, current | |
def report_status(host: str, port: int, /, offset: int = 0x00) -> None: | |
server = (host, port) | |
try: | |
maximum, current = players_online(server, offset) | |
print(f"Players online: {current}/{maximum}") | |
except socket.timeout: | |
print("Request timed out.") | |
def error(exception: Exception) -> NoReturn: | |
program = pathlib.Path(sys.argv[0]).name | |
error = exception.__str__() | |
if error: | |
message = f"{error!s}" | |
else: | |
message = type(exception).__name__ | |
sys.exit(f"{program}: error: {message}") | |
def main(argv: _Arguments = None) -> None: | |
if argv is None: | |
argv = sys.argv[1:] | |
parser = SingleLineArgumentParser( | |
epilog="example: %(prog)s 127.0.0.1 8901", | |
fromfile_prefix_chars='@' | |
) | |
parser.add_argument("host", type=str, help="GunZ server hostname") | |
parser.add_argument("port", type=int, help="GunZ server port") | |
group = parser.add_mutually_exclusive_group() | |
group.add_argument( | |
"--igunz", action="store_true", help="International GunZ data format" | |
) | |
group.add_argument( | |
"--fgunz", action="store_true", help="Freestyle GunZ data format" | |
) | |
args = parser.parse_args(argv) | |
try: | |
report_status(args.host, args.port, 0x08 if args.fgunz else 0x00) | |
except Exception as exception: | |
error(exception) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment