Skip to content

Instantly share code, notes, and snippets.

@mataha
Last active May 16, 2021 16:57
Show Gist options
  • Save mataha/10f65976a7b2bdf5a4ecc13d8b9d38be to your computer and use it in GitHub Desktop.
Save mataha/10f65976a7b2bdf5a4ecc13d8b9d38be to your computer and use it in GitHub Desktop.
GunZ server population scraper
#!/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