Advertisement
Guest User

Untitled

a guest
Aug 24th, 2019
77
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.99 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. import sys
  5. import base64
  6. import json
  7. import urllib.request
  8. import io
  9. import csv
  10. import time
  11. import operator
  12. import os
  13. import subprocess
  14.  
  15. # server example:
  16. # {"battlEye":true,"firstPersonOnly":false,"shard":"private","timeAcceleration":4,"time":"2019-08-24T10:17:00","mods":[{"name":"Summer_Chernarus","steamWorkshopId":1644467354},{"name":"BuildAnywhere","steamWorkshopId":1574054508},{"name":"SIX-DayZ-Auto-Run","steamWorkshopId":1781132597}],"sponsor":false,"profile":false,"nameOverride":false,"endpoint":{"ip":"74.89.233.235","port":27900},"name":"Dystopia |Event/Dev Server|Discord.gg/kUfqxpX ","map":"chernarusplus","players":0,"maxPlayers":60,"environment":"w","password":true,"version":"1.04.152166","vac":true,"gamePort":2402}
  17.  
  18. #=== Configuration ===
  19. cfg_dzsaServerListUrl = "https://dayzsalauncher.com/api/v1/launcher/servers/dayz"
  20. cfg_dzsaServerUrl = "https://dayzsalauncher.com/api/v1/query/{ip}/{steamQueryPort}" # eg. 194.26.183.65/27016
  21. # Alternative servers source: Absolute path to dzsa launcher's dayz-servers.json file
  22. cfg_dzsaServersFile = "C:/Users/lukas_000/Documents/dzsalauncher/dayz-servers.json"
  23. # Relative path to servers file (cached results):
  24. cfg_serversFile = "servers.json"
  25. cfg_modsFile = "mods.json"
  26. cfg_cacheExpireSeconds = 60 * 20 # Fetch freshly if local data is outdated (20 min)
  27. # Relative path to the log file:
  28. cfg_logFile = "log.txt"
  29. # Relative path to the filtered and ranked serverlist file:
  30. cfg_rankedServersFile = "ranked-servers.txt"
  31. cfg_pingMaxRetries = 1
  32. #--- Reduce spam in output
  33. cfg_serverOutputHiddenKeys = ["sponsor", "profile", "nameOverride", "environment"]
  34. cgf_serverOutputSimpleModlist = True
  35. cfg_printServersLimit = 20
  36. #--- Filters ---
  37. cfg_filter_minMaxPlayers = 60
  38. cfg_filter_minPlayers = 1
  39. cfg_filter_serverName = "" # contains, ignore case
  40. cfg_filter_match = {"battlEye": True, "vac": True, "password": False, "map": "chernarusplus"}
  41. # each entry is a list of aliases for that mod (eg. list of mods that are equally valid to fulfill that entry)
  42. cfg_filter_requiredMods = [["DisableBaseDestruction"],["PartyMe"],["Base Furniture Mods"],["VanillaPlusPlusMap"],["Unlimited Stamina","UnlimitedStamina"],["Code Lock"],["Trader"]]
  43. # Blacklisted mods
  44. cfg_filter_blacklistedMods = ["C4BaseRaid", "Base-Raiding Breachingcharge", "Breachingcharge", "BaseRaidToolsV2", "BaseRaidTools"]
  45. # mod name : sorting weight (negative also possible) TODO unused currently
  46. cfg_filter_optionalMods = {"NoVehicleDamage":1, "DayZ-Auto-Run":1, "SIX-DayZ-Auto-Run":1, "SQUAD MSF-C":1, "Banking":1, "OP_BaseItems":1, "BaseBuildingPlus":1, "BaseBuildingLogs":1, "Server_Information_Panel":1, "MoreGuns":1, "BulletStacksPlusPlus":1, "MasssManyItemOverhaul":1, "Fast Access - Code Lock":1, "BuildAnywhere":1, "GoreZ":1}
  47. cfg_filter_max_ping = 1000 # in ms TODO unused currently
  48. #=== End of configuration ===
  49.  
  50. # Mirror program output to log file:
  51. class Logger(object):
  52. def __init__(self):
  53. self.terminal = sys.stdout
  54. self.log = open(cfg_logFile, "w", encoding="utf-8")
  55.  
  56. def write(self, message):
  57. self.terminal.write(message)
  58. self.log.write(message)
  59. def flush(self):
  60. self.terminal.flush();
  61. self.log.flush()
  62.  
  63. # Replace stdout to support unicode strings
  64. sys.stdout = open(1, 'w', encoding='utf-8', closefd=False); # fd 1 is stdout
  65. sys.stdout = Logger() # mirror to log file
  66.  
  67. def openUrl(method, url, data = None):
  68. if data:
  69. dataEnc = data.encode()
  70. else:
  71. dataEnc = data
  72. request = urllib.request.Request(url, dataEnc)
  73.  
  74. request.add_header("Content-Type", "application/json")
  75. request.add_header("Accept", "application/json")
  76. request.add_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0") # dummy
  77.  
  78. request.get_method = lambda: method
  79.  
  80. try:
  81. response = urllib.request.urlopen(request)
  82. respData = response.read().decode("utf-8")
  83. except urllib.error.HTTPError as error:
  84. error_details = error.read();
  85. error_message = "HTTP ERROR: %s %s" % (error.code, error.reason)
  86. print("%s" % error_message, flush=True)
  87.  
  88. sys.exit(error_message)
  89.  
  90. if respData:
  91. result = json.load(io.StringIO(respData))
  92. else:
  93. result = []
  94. return result
  95.  
  96. def fetchDZSAServers():
  97. print("Fetching serverlist from '" + cfg_dzsaServerListUrl + "' ...", flush=True)
  98. start = time.perf_counter()
  99. response = openUrl("GET", cfg_dzsaServerListUrl)
  100. servers = response["result"]
  101. duration = time.perf_counter() - start
  102. print("Fetched " + str(len(servers)) + " servers in %.2f seconds." % duration, flush=True)
  103. return servers
  104.  
  105. def loadJsonFile(filePath):
  106. start = time.perf_counter()
  107. with open(filePath, encoding="utf-8") as jsonFile:
  108. jsonData = json.load(jsonFile);
  109. duration = time.perf_counter() - start
  110. return jsonData, duration
  111.  
  112. def writeJsonFile(filePath, jsonData, pretty=False):
  113. start = time.perf_counter()
  114. with open(filePath, "w", encoding="utf-8") as jsonFile:
  115. if pretty:
  116. json.dump(jsonData, jsonFile, sort_keys=True, ensure_ascii=False, indent=4)
  117. else:
  118. json.dump(jsonData, jsonFile, sort_keys=True, ensure_ascii=False)
  119. duration = time.perf_counter() - start
  120. return duration
  121.  
  122. # Loads from dzsa launcher's file
  123. def loadDZSAServers():
  124. print("Loading serverlist from file '" + cfg_dzsaServersFile + "' ...", flush=True)
  125. servers, duration = loadJsonFile(cfg_dzsaServersFile)
  126. print("Loaded " + str(len(servers)) + " servers in %.2f seconds." % duration, flush=True)
  127. return servers
  128.  
  129. def loadServers():
  130. print("Loading serverlist from file '" + cfg_serversFile + "' ...", flush=True)
  131. servers, duration = loadJsonFile(cfg_serversFile)
  132. print("Loaded " + str(len(servers)) + " servers in %.2f seconds." % duration, flush=True)
  133. return servers
  134.  
  135. def writeServers(servers):
  136. print("Writing serverlist to file '" + cfg_serversFile + "' ...", flush=True)
  137. duration = writeJsonFile(cfg_serversFile, servers, pretty=True)
  138. print("Serverlist written to file in %.2f seconds." % duration, flush=True)
  139.  
  140. def loadOrFetchServers():
  141. # Check local cache:
  142. if os.path.isfile(cfg_serversFile):
  143. localServerlistAge = time.time() - os.path.getmtime(cfg_serversFile)
  144. print("Age of local serverlist: %.2f seconds, Expiration: %.2f seconds" % (localServerlistAge, cfg_cacheExpireSeconds), flush=True)
  145. if abs(localServerlistAge) < cfg_cacheExpireSeconds:
  146. return loadServers(), False # still valid
  147. else:
  148. print(" Local serverlist is expired.", flush=True)
  149.  
  150. # Else: Fetch from DZSA API and save for later re-use
  151. servers = fetchDZSAServers()
  152. writeServers(servers)
  153. return servers, True # freshly fetched
  154.  
  155. def writeRankedServers(rankedServers):
  156. print("Writing ranked serverlist to file '" + cfg_rankedServersFile + "' ...", flush=True)
  157. duration = writeJsonFile(cfg_rankedServersFile, rankedServers, pretty=True)
  158. print("Ranked serverlist written to file in %.2f seconds." % duration, flush=True)
  159.  
  160. def loadMods():
  161. print("Loading modlist from file '" + cfg_modsFile + "' ...", flush=True)
  162. mods, duration = loadJsonFile(cfg_modsFile)
  163. print("Loaded " + str(len(mods)) + " mods in %.2f seconds." % duration, flush=True)
  164. return mods
  165.  
  166. def writeMods(mods):
  167. print("Writing mods to file '" + cfg_modsFile + "' ...", flush=True)
  168. duration = writeJsonFile(cfg_modsFile, mods, pretty=True)
  169. print("Mods written to file in %.2f seconds." % duration, flush=True)
  170.  
  171. def parseMods(servers):
  172. print("Parsing mods ...", flush=True)
  173. start = time.perf_counter()
  174. # Each key (mod name) is mapped to a list of workshop ids (usually of size 1)
  175. mods = {} # each
  176. for server in servers:
  177. serverMods = server["mods"]
  178. for serverMod in serverMods:
  179. modName = serverMod["name"]
  180. workshopId = serverMod["steamWorkshopId"]
  181. if modName not in mods:
  182. mods[modName] = [workshopId]
  183. else:
  184. workshopIds = mods[modName]
  185. if workshopId not in workshopIds:
  186. workshopIds.append(workshopId)
  187. print("Found different workshop ids for mod: " + modName)
  188. duration = time.perf_counter() - start
  189. print("Parsed " + str(len(mods)) + " mods in %.2f seconds." % duration, flush=True)
  190. return mods
  191.  
  192. def loadOrParseMods(servers, forceParse=False):
  193. # Freshly parse mods if forced (eg. after the servers got refreshed) or if the local modlist is missing
  194. if forceParse or not os.path.isfile(cfg_modsFile):
  195. # Parse and save mod list:
  196. mods = parseMods(servers)
  197. writeMods(mods)
  198. return mods, True # freshly parsed
  199. else:
  200. return loadMods(), False # Loaded from cache
  201.  
  202. # server example:
  203. # {"battlEye":true,"firstPersonOnly":false,"shard":"private","timeAcceleration":4,"time":"2019-08-24T10:17:00","mods":[{"name":"Summer_Chernarus","steamWorkshopId":1644467354},{"name":"BuildAnywhere","steamWorkshopId":1574054508},{"name":"SIX-DayZ-Auto-Run","steamWorkshopId":1781132597}],"sponsor":false,"profile":false,"nameOverride":false,"endpoint":{"ip":"74.89.233.235","port":27900},"name":"Dystopia |Event/Dev Server|Discord.gg/kUfqxpX ","map":"chernarusplus","players":0,"maxPlayers":60,"environment":"w","password":true,"version":"1.04.152166","vac":true,"gamePort":2402}
  204. def serverFilter(server):
  205. # Min max players:
  206. maxPlayers = server["maxPlayers"]
  207. if maxPlayers < cfg_filter_minMaxPlayers: return False
  208. # Min players:
  209. players = server["players"]
  210. if players < cfg_filter_minPlayers: return False
  211. # Server name filter:
  212. serverName = server["name"]
  213. if cfg_filter_serverName not in serverName.lower(): return False
  214. # Data matching:
  215. for key, value in cfg_filter_match.items():
  216. if not (server[key] == value): return False
  217. # Required mods:
  218. mods = [mod["name"] for mod in server["mods"]]
  219. for requiredModAliases in cfg_filter_requiredMods:
  220. found = False
  221. for requiredMod in requiredModAliases:
  222. if requiredMod in mods:
  223. found = True
  224. break
  225. if not found: return False # Missing required mod
  226. # Blacklisted mods:
  227. for blacklistedMods in cfg_filter_blacklistedMods:
  228. if blacklistedMods in mods: return False
  229.  
  230. return True # Passed all filters
  231.  
  232.  
  233. def filterServers(servers):
  234. print("Filtering " + str(len(servers)) + " servers ...", flush=True)
  235. filteredServers = list(filter(serverFilter, servers))
  236. print("Found " + str(len(filteredServers)) + " matching servers.", flush=True)
  237. return filteredServers
  238.  
  239. def serverRank(server):
  240. serverValue = 0
  241. mods = [mod["name"] for mod in server["mods"]]
  242. for optionalMod, modValue in cfg_filter_optionalMods.items():
  243. if optionalMod in mods:
  244. serverValue+=modValue
  245. return serverValue
  246.  
  247. def rankServers(filteredServers):
  248. print("Ranking servers ...", flush=True)
  249. rankedServers = [{"server": server, "ranking-value": serverRank(server)} for server in filteredServers]
  250. rankedServers.sort(key=lambda x: x["ranking-value"], reverse=True)
  251. print("Servers ranked.", flush=True)
  252. return rankedServers
  253.  
  254. # Note: This is very slow! Limit number of calls
  255. # Latency parsed from ping command in ms, or 9999 if unreachable
  256. # TODO inaccurate? Result latency is usually slightly better (~30ms) than what is displayed inside the launcher
  257. def ping(host):
  258. return _ping(host)
  259.  
  260. def _ping(host, attempt=1):
  261. # TODO ugly
  262. output = subprocess.Popen(["ping", "-n", "1", host], stdout=subprocess.PIPE).communicate()[0].decode("utf-8", "ignore") # ignore decoding errors
  263. try:
  264. return int(output.split("ms")[0].split("=")[-1])
  265. except Exception as e:
  266. if attempt < cfg_pingMaxRetries:
  267. # retry:
  268. return _ping(host, attempt=attempt+1)
  269. else:
  270. return 9999 # eg. unreachable
  271.  
  272. def formatServer(server):
  273. formattedServer = server.copy() # shallow copy
  274. # Replace mod entries with simple list of mods names:
  275. if cgf_serverOutputSimpleModlist:
  276. formattedServer["mods"] = [mod["name"] for mod in server["mods"]]
  277. # Removed hidden details:
  278. for hiddenKey in cfg_serverOutputHiddenKeys:
  279. del formattedServer[hiddenKey]
  280. return formattedServer
  281.  
  282. def toJsonString(jsonObject, pretty=True):
  283. if pretty:
  284. return json.dumps(jsonObject, sort_keys=True, ensure_ascii=False, indent=4)
  285. else:
  286. return json.dumps(jsonObject, sort_keys=True, ensure_ascii=False)
  287.  
  288. def printRankedServers(rankedServers, number=cfg_printServersLimit):
  289. for i in range(number):
  290. if i >= len(rankedServers): break
  291. rankedServer = rankedServers[i]
  292. server = rankedServer["server"]
  293. rankingValue = rankedServer["ranking-value"]
  294. serverPing = ping(server["endpoint"]["ip"])
  295. print(str(i + 1) + ") ranking-value=" + str(rankingValue) + " , ping: " + str(serverPing) + " ms")
  296. print(toJsonString(formatServer(server), pretty=True), flush=True)
  297. if len(rankedServers) > number:
  298. print("... (there are more servers fulfilling the filter rules)", flush=True)
  299.  
  300. def main():
  301. servers, serversFetched = loadOrFetchServers()
  302. mods, modsParsed = loadOrParseMods(servers, forceParse=serversFetched)
  303. # TODO print info about the configured filters
  304. filteredServers = filterServers(servers)
  305. rankedServers = rankServers(filteredServers)
  306. #writeRankedServers(rankedServers)
  307. printRankedServers(rankedServers, number=cfg_printServersLimit)
  308. #latency = pingLatency("195.201.174.52")
  309. #print(str(latency))
  310. print("Done.", flush=True)
  311.  
  312. if __name__ == '__main__':
  313. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement