From 3b5e2193ae7a56bf75919f869bd5396b2b6e32c1 Mon Sep 17 00:00:00 2001 From: Jesse Litton Date: Mon, 4 Jul 2016 07:07:02 -0500 Subject: [PATCH 01/36] Scale avatar placeholder as necessary --- src/vi/ui/viui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vi/ui/viui.py b/src/vi/ui/viui.py index 9e446be..d7637eb 100644 --- a/src/vi/ui/viui.py +++ b/src/vi/ui/viui.py @@ -951,7 +951,7 @@ class ChatEntryWidget(QtGui.QWidget): def __init__(self, message): QtGui.QWidget.__init__(self) if not self.questionMarkPixmap: - self.questionMarkPixmap = QtGui.QPixmap(resourcePath("vi/ui/res/qmark.png")) + self.questionMarkPixmap = QtGui.QPixmap(resourcePath("vi/ui/res/qmark.png")).scaledToHeight(32) uic.loadUi(resourcePath("vi/ui/ChatEntry.ui"), self) self.avatarLabel.setPixmap(self.questionMarkPixmap) self.message = message From 7e59147b1095c59f5d25618c5a7e430a5b09af60 Mon Sep 17 00:00:00 2001 From: Jesse Litton Date: Tue, 5 Jul 2016 21:02:06 -0500 Subject: [PATCH 02/36] Improve parsing of gate-related messages. --- src/vi/chatparser/chatparser.py | 2 -- src/vi/chatparser/parser_functions.py | 32 ++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/vi/chatparser/chatparser.py b/src/vi/chatparser/chatparser.py index 2b91dba..7bc3b99 100644 --- a/src/vi/chatparser/chatparser.py +++ b/src/vi/chatparser/chatparser.py @@ -131,8 +131,6 @@ def _lineToMessage(self, line, roomname): message.status = states.IGNORE return message - for char in ("*", "?", ",", "!"): - text = text.replace(char, "") while parseShips(rtext): continue while parseUrls(rtext): diff --git a/src/vi/chatparser/parser_functions.py b/src/vi/chatparser/parser_functions.py index 781938e..692e734 100644 --- a/src/vi/chatparser/parser_functions.py +++ b/src/vi/chatparser/parser_functions.py @@ -98,6 +98,9 @@ def formatShipName(text, word): def parseSystems(systems, rtext, foundSystems): + + systemNames = systems.keys() + # words to ignore on the system parser. use UPPER CASE WORDS_TO_IGNORE = ("IN", "IS", "AS") @@ -106,16 +109,31 @@ def formatSystem(text, word, system): text = text.replace(word, newText.format(system, word)) return text - systemNames = systems.keys() - texts = [t for t in rtext.contents if isinstance(t, NavigableString)] - for text in texts: + texts = [t for t in rtext.contents if isinstance(t, NavigableString) and len(t)] + for wtIdx, text in enumerate(texts): worktext = text for char in CHARS_TO_IGNORE: worktext = worktext.replace(char, "") + + # Drop redundant whitespace so as to not throw off word index + worktext = ' '.join(worktext.split()) words = worktext.split(" ") - for word in words: - if len(word.strip()) == 0: - continue + + for idx, word in enumerate(words): + + # Is this about another a system's gate? + if len(words) > idx + 1: + if words[idx+1].upper() == 'GATE': + bailout = True + if len(words) > idx + 2: + if words[idx+2].upper() == 'TO': + # Could be '___ GATE TO somewhere' so check this one. + bailout = False + if bailout: + # '_____ GATE' mentioned in message, which is not what we're + # interested in, so go to checking next word. + continue + upperWord = word.upper() if upperWord != word and upperWord in WORDS_TO_IGNORE: continue if upperWord in systemNames: # - direct hit on name @@ -150,6 +168,8 @@ def formatSystem(text, word, system): formattedText = formatSystem(text, word, system) textReplace(text, formattedText) return True + + return False def parseUrls(rtext): From 39c9c5d07692466a256ccbdaba8c1f7712c0acbc Mon Sep 17 00:00:00 2001 From: Mark Kieling Date: Fri, 8 Jul 2016 13:22:04 -0700 Subject: [PATCH 03/36] Dot reduction - import modules rather than indirect to them --- src/vi/amazon_s3.py | 4 +- src/vi/chatparser/chatparser.py | 10 +- src/vi/evegate.py | 9 +- src/vi/koschecker.py | 6 +- src/vi/threads.py | 11 +- src/vi/ui/systemtray.py | 29 +++--- src/vi/ui/viui.py | 174 ++++++++++++++++---------------- src/vintel.py | 6 +- 8 files changed, 128 insertions(+), 121 deletions(-) diff --git a/src/vi/amazon_s3.py b/src/vi/amazon_s3.py index dfd60b7..560c84d 100644 --- a/src/vi/amazon_s3.py +++ b/src/vi/amazon_s3.py @@ -22,7 +22,7 @@ import logging from PyQt4 import Qt -from PyQt4.QtCore import QThread +from PyQt4.QtCore import QThread, SIGNAL from vi import version from vi.cache.cache import Cache from distutils.version import LooseVersion, StrictVersion @@ -72,7 +72,7 @@ def run(self): # Is there a newer version available? newestVersion = getNewestVersion() if newestVersion and StrictVersion(newestVersion) > StrictVersion(version.VERSION): - self.emit(Qt.SIGNAL("newer_version"), newestVersion) + self.emit(SIGNAL("newer_version"), newestVersion) self.alerted = True except Exception as e: logging.error("Failed NotifyNewVersionThread: %s", e) diff --git a/src/vi/chatparser/chatparser.py b/src/vi/chatparser/chatparser.py index 7bc3b99..dcb88fb 100644 --- a/src/vi/chatparser/chatparser.py +++ b/src/vi/chatparser/chatparser.py @@ -24,9 +24,10 @@ if six.PY2: from io import open -from PyQt4 import QtGui from bs4 import BeautifulSoup from vi import states +from PyQt4.QtGui import QMessageBox + from .parser_functions import parseStatus from .parser_functions import parseUrls, parseShips, parseSystems @@ -70,11 +71,11 @@ def addFile(self, path): content = f.read() except Exception as e: self.ignoredPaths.append(path) - QtGui.QMessageBox.warning(None, "Read a log file failed!", "File: {0} - problem: {1}".format(path, six.text_type(e)), "OK") + QMessageBox.warning(None, "Read a log file failed!", "File: {0} - problem: {1}".format(path, six.text_type(e)), "OK") return None lines = content.split("\n") - if (path not in self.fileData or (roomname in LOCAL_NAMES and "charname" not in self.fileData.get(path, []))): + if path not in self.fileData or (roomname in LOCAL_NAMES and "charname" not in self.fileData.get(path, [])): self.fileData[path] = {} if roomname in LOCAL_NAMES: charname = None @@ -143,8 +144,7 @@ def _lineToMessage(self, line, roomname): # If message says clear and no system? Maybe an answer to a request? if status == states.CLEAR and not systems: maxSearch = 2 # we search only max_search messages in the room - for count, oldMessage in enumerate( - oldMessage for oldMessage in self.knownMessages[-1::-1] if oldMessage.room == roomname): + for count, oldMessage in enumerate(oldMessage for oldMessage in self.knownMessages[-1::-1] if oldMessage.room == roomname): if oldMessage.systems and oldMessage.status == states.REQUEST: for system in oldMessage.systems: systems.add(system) diff --git a/src/vi/evegate.py b/src/vi/evegate.py index 440f8f7..c0a4b16 100644 --- a/src/vi/evegate.py +++ b/src/vi/evegate.py @@ -24,9 +24,10 @@ import requests import logging -from six.moves import urllib from bs4 import BeautifulSoup from vi.cache.cache import Cache +from six.moves.urllib.error import HTTPError +from six.moves.urllib.request import urlopen ERROR = -1 NOT_EXISTS = 0 @@ -157,14 +158,14 @@ def checkPlayername(charname): """ baseUrl = "https://gate.eveonline.com/Profile/" - queryCharname = utils.quote(charname) + queryCharname = requests.utils.quote(charname) url = baseUrl + queryCharname result = -1 try: - urllib.request.urlopen(url) + urlopen(url) result = 1 - except urllib.error.HTTPError as e: + except HTTPError as e: if ("404") in str(e): result = 0 except Exception as e: diff --git a/src/vi/koschecker.py b/src/vi/koschecker.py index 43cabe1..0296ecd 100644 --- a/src/vi/koschecker.py +++ b/src/vi/koschecker.py @@ -17,11 +17,11 @@ # along with this program. If not, see . # ########################################################################### -import json import logging import requests from vi import evegate +from requests.exceptions import RequestException UNKNOWN = "No Result" NOT_KOS = 'Not Kos' @@ -38,7 +38,7 @@ def check(parts): try: kosData = requests.get(CVA_KOS_URL, params = {'c': 'json', 'type': 'multi', 'q': ','.join(names)}).json() - except requests.exceptions.RequestException as e: + except RequestException as e: kosData = None logging.error("Error on pilot KOS check request %s", str(e)) @@ -96,7 +96,7 @@ def check(parts): for corp in corpsToCheck: try: kosData = requests.get(CVA_KOS_URL, params = { 'c': 'json', 'type': 'unit', 'q': corp }).json() - except requests.exceptions.RequestException as e: + except RequestException as e: logging.error("Error on corp KOS check request: %s", str(e)) kosResult = False diff --git a/src/vi/threads.py b/src/vi/threads.py index 95b54c5..f42c9e9 100755 --- a/src/vi/threads.py +++ b/src/vi/threads.py @@ -19,11 +19,10 @@ import time import logging +import six from six.moves import queue -from PyQt4 import QtCore -from PyQt4.QtCore import QThread -from PyQt4.QtCore import SIGNAL +from PyQt4.QtCore import QThread, SIGNAL, QTimer from vi import evegate from vi import koschecker from vi.cache.cache import Cache @@ -166,8 +165,8 @@ def requestStatistics(self): def run(self): - self.refreshTimer = QtCore.QTimer() - self.connect(self.refreshTimer, QtCore.SIGNAL("timeout()"), self.requestStatistics) + self.refreshTimer = QTimer() + self.connect(self.refreshTimer, SIGNAL("timeout()"), self.requestStatistics) while True: # Block waiting for requestStatistics() to enqueue a token self.queue.get() @@ -181,7 +180,7 @@ def run(self): requestData = {"result": "ok", "statistics": statistics} except Exception as e: logging.error("Error in MapStatisticsThread: %s", e) - requestData = {"result": "error", "text": unicode(e)} + requestData = {"result": "error", "text": six.text_type(e)} self.lastStatisticsUpdate = time.time() self.refreshTimer.start(self.pollRate) self.emit(SIGNAL("statistic_data_update"), requestData) diff --git a/src/vi/ui/systemtray.py b/src/vi/ui/systemtray.py index 6179628..5379f2f 100644 --- a/src/vi/ui/systemtray.py +++ b/src/vi/ui/systemtray.py @@ -21,10 +21,13 @@ from six.moves import range from PyQt4 import QtGui, QtCore, Qt +from PyQt4.QtGui import QAction, QActionGroup +from PyQt4.QtGui import QIcon, QSystemTrayIcon from vi.resources import resourcePath from vi import states from vi.soundmanager import SoundManager +from PyQt4.QtCore import SIGNAL class TrayContextMenu(QtGui.QMenu): @@ -40,31 +43,31 @@ def __init__(self, trayIcon): def _buildMenu(self): self.framelessCheck = QtGui.QAction("Frameless Window", self, checkable=True) - self.connect(self.framelessCheck, QtCore.SIGNAL("triggered()"), self.trayIcon.changeFrameless) + self.connect(self.framelessCheck, SIGNAL("triggered()"), self.trayIcon.changeFrameless) self.addAction(self.framelessCheck) self.addSeparator() self.requestCheck = QtGui.QAction("Show status request notifications", self, checkable=True) self.requestCheck.setChecked(True) self.addAction(self.requestCheck) - self.connect(self.requestCheck, QtCore.SIGNAL("triggered()"), self.trayIcon.switchRequest) + self.connect(self.requestCheck, SIGNAL("triggered()"), self.trayIcon.switchRequest) self.alarmCheck = QtGui.QAction("Show alarm notifications", self, checkable=True) self.alarmCheck.setChecked(True) - self.connect(self.alarmCheck, QtCore.SIGNAL("triggered()"), self.trayIcon.switchAlarm) + self.connect(self.alarmCheck, SIGNAL("triggered()"), self.trayIcon.switchAlarm) self.addAction(self.alarmCheck) distanceMenu = self.addMenu("Alarm Distance") - self.distanceGroup = QtGui.QActionGroup(self) + self.distanceGroup = QActionGroup(self) for i in range(0, 6): - action = QtGui.QAction("{0} Jumps".format(i), None, checkable=True) + action = QAction("{0} Jumps".format(i), None, checkable=True) if i == 0: action.setChecked(True) action.alarmDistance = i - self.connect(action, QtCore.SIGNAL("triggered()"), self.changeAlarmDistance) + self.connect(action, SIGNAL("triggered()"), self.changeAlarmDistance) self.distanceGroup.addAction(action) distanceMenu.addAction(action) self.addMenu(distanceMenu) self.addSeparator() - self.quitAction = QtGui.QAction("Quit", self) - self.connect(self.quitAction, Qt.SIGNAL("triggered()"), self.trayIcon.quit) + self.quitAction = QAction("Quit", self) + self.connect(self.quitAction, SIGNAL("triggered()"), self.trayIcon.quit) self.addAction(self.quitAction) def changeAlarmDistance(self): @@ -79,8 +82,8 @@ class TrayIcon(QtGui.QSystemTrayIcon): MIN_WAIT_NOTIFICATION = 15 def __init__(self, app): - self.icon = QtGui.QIcon(resourcePath("vi/ui/res/logo_small.png")) - QtGui.QSystemTrayIcon.__init__(self, self.icon, app) + self.icon = QIcon(resourcePath("vi/ui/res/logo_small.png")) + QSystemTrayIcon.__init__(self, self.icon, app) self.setToolTip("Your Vintel-Information-Service! :)") self.lastNotifications = {} self.setContextMenu(TrayContextMenu(self)) @@ -90,17 +93,17 @@ def __init__(self, app): def changeAlarmDistance(self): distance = self.alarmDistance - self.emit(Qt.SIGNAL("alarm_distance"), distance) + self.emit(SIGNAL("alarm_distance"), distance) def changeFrameless(self): - self.emit(Qt.SIGNAL("change_frameless")) + self.emit(SIGNAL("change_frameless")) @property def distanceGroup(self): return self.contextMenu().distanceGroup def quit(self): - self.emit(Qt.SIGNAL("quit")) + self.emit(SIGNAL("quit")) def switchAlarm(self): newValue = not self.showAlarm diff --git a/src/vi/ui/viui.py b/src/vi/ui/viui.py index d7637eb..a253f1e 100644 --- a/src/vi/ui/viui.py +++ b/src/vi/ui/viui.py @@ -28,18 +28,21 @@ import logging from PyQt4.QtGui import * -from PyQt4 import Qt, QtGui, uic, QtCore -from PyQt4.QtCore import QPoint +from PyQt4 import QtGui, uic, QtCore +from PyQt4.QtCore import QPoint, SIGNAL from PyQt4.QtGui import QImage, QPixmap, QMessageBox from PyQt4.QtWebKit import QWebPage from vi import amazon_s3, evegate -from vi import chatparser, dotlan, filewatcher +from vi import dotlan, filewatcher from vi import states from vi.cache.cache import Cache from vi.resources import resourcePath from vi.soundmanager import SoundManager from vi.threads import AvatarFindThread, KOSCheckerThread, MapStatisticsThread from vi.ui.systemtray import TrayContextMenu +from vi.chatparser import ChatParser +from PyQt4.QtGui import QAction +from PyQt4.QtGui import QMessageBox # Timer intervals MESSAGE_EXPIRY_SECS = 20 * 60 @@ -66,7 +69,7 @@ def __init__(self, pathToLogs, trayIcon, backGroundColor): self.pathToLogs = pathToLogs self.mapTimer = QtCore.QTimer(self) - self.connect(self.mapTimer, QtCore.SIGNAL("timeout()"), self.updateMapView) + self.connect(self.mapTimer, SIGNAL("timeout()"), self.updateMapView) self.clipboardTimer = QtCore.QTimer(self) self.oldClipboardContent = "" self.trayIcon = trayIcon @@ -88,7 +91,7 @@ def __init__(self, pathToLogs, trayIcon, backGroundColor): else: self.knownPlayerNames = set() diagText = "Vintel scans EVE system logs and remembers your characters as they change systems.\n\nSome features (clipboard KOS checking, alarms, etc.) may not work until your character(s) have been registered. Change systems, with each character you want to monitor, while Vintel is running to remedy this." - QtGui.QMessageBox.warning(None, "Known Characters not Found", diagText, "Ok") + QMessageBox.warning(None, "Known Characters not Found", diagText, "Ok") # Set up user's intel rooms roomnames = self.cache.getFromCache("room_names") @@ -106,13 +109,13 @@ def __init__(self, pathToLogs, trayIcon, backGroundColor): self.changeSound() # Set up Transparency menu - fill in opacity values and make connections - self.opacityGroup = QtGui.QActionGroup(self.menu) + self.opacityGroup = QActionGroup(self.menu) for i in (100, 80, 60, 40, 20): - action = QtGui.QAction("Opacity {0}%".format(i), None, checkable=True) + action = QAction("Opacity {0}%".format(i), None, checkable=True) if i == 100: action.setChecked(True) action.opacity = i / 100.0 - self.connect(action, QtCore.SIGNAL("triggered()"), self.changeOpacity) + self.connect(action, SIGNAL("triggered()"), self.changeOpacity) self.opacityGroup.addAction(action) self.menuTransparency.addAction(action) @@ -152,59 +155,59 @@ def recallCachedSettings(self): def wireUpUIConnections(self): # Wire up general UI connections - self.connect(self.clipboard, Qt.SIGNAL("changed(QClipboard::Mode)"), self.clipboardChanged) - self.connect(self.autoScanIntelAction, Qt.SIGNAL("triggered()"), self.changeAutoScanIntel) - self.connect(self.kosClipboardActiveAction, Qt.SIGNAL("triggered()"), self.changeKosCheckClipboard) - self.connect(self.zoomInButton, Qt.SIGNAL("clicked()"), self.zoomMapIn) - self.connect(self.zoomOutButton, Qt.SIGNAL("clicked()"), self.zoomMapOut) - self.connect(self.statisticsButton, Qt.SIGNAL("clicked()"), self.changeStatisticsVisibility) - self.connect(self.jumpbridgesButton, Qt.SIGNAL("clicked()"), self.changeJumpbridgesVisibility) - self.connect(self.chatLargeButton, Qt.SIGNAL("clicked()"), self.chatLarger) - self.connect(self.chatSmallButton, Qt.SIGNAL("clicked()"), self.chatSmaller) - self.connect(self.infoAction, Qt.SIGNAL("triggered()"), self.showInfo) - self.connect(self.showChatAvatarsAction, Qt.SIGNAL("triggered()"), self.changeShowAvatars) - self.connect(self.alwaysOnTopAction, Qt.SIGNAL("triggered()"), self.changeAlwaysOnTop) - self.connect(self.chooseChatRoomsAction, Qt.SIGNAL("triggered()"), self.showChatroomChooser) - self.connect(self.catchRegionAction, Qt.SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.catchRegionAction)) - self.connect(self.providenceRegionAction, Qt.SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.providenceRegionAction)) - self.connect(self.queriousRegionAction, Qt.SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.queriousRegionAction)) - self.connect(self.providenceCatchRegionAction, Qt.SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.providenceCatchRegionAction)) - self.connect(self.providenceCatchCompactRegionAction, Qt.SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.providenceCatchCompactRegionAction)) - self.connect(self.chooseRegionAction, Qt.SIGNAL("triggered()"), self.showRegionChooser) - self.connect(self.showChatAction, Qt.SIGNAL("triggered()"), self.changeChatVisibility) - self.connect(self.soundSetupAction, Qt.SIGNAL("triggered()"), self.showSoundSetup) - self.connect(self.activateSoundAction, Qt.SIGNAL("triggered()"), self.changeSound) - self.connect(self.useSpokenNotificationsAction, Qt.SIGNAL("triggered()"), self.changeUseSpokenNotifications) - self.connect(self.trayIcon, Qt.SIGNAL("alarm_distance"), self.changeAlarmDistance) - self.connect(self.framelessWindowAction, Qt.SIGNAL("triggered()"), self.changeFrameless) - self.connect(self.trayIcon, Qt.SIGNAL("change_frameless"), self.changeFrameless) - self.connect(self.frameButton, Qt.SIGNAL("clicked()"), self.changeFrameless) - self.connect(self.quitAction, Qt.SIGNAL("triggered()"), self.close) - self.connect(self.trayIcon, Qt.SIGNAL("quit"), self.close) - self.connect(self.jumpbridgeDataAction, Qt.SIGNAL("triggered()"), self.showJumbridgeChooser) + self.connect(self.clipboard, SIGNAL("changed(QClipboard::Mode)"), self.clipboardChanged) + self.connect(self.autoScanIntelAction, SIGNAL("triggered()"), self.changeAutoScanIntel) + self.connect(self.kosClipboardActiveAction, SIGNAL("triggered()"), self.changeKosCheckClipboard) + self.connect(self.zoomInButton, SIGNAL("clicked()"), self.zoomMapIn) + self.connect(self.zoomOutButton, SIGNAL("clicked()"), self.zoomMapOut) + self.connect(self.statisticsButton, SIGNAL("clicked()"), self.changeStatisticsVisibility) + self.connect(self.jumpbridgesButton, SIGNAL("clicked()"), self.changeJumpbridgesVisibility) + self.connect(self.chatLargeButton, SIGNAL("clicked()"), self.chatLarger) + self.connect(self.chatSmallButton, SIGNAL("clicked()"), self.chatSmaller) + self.connect(self.infoAction, SIGNAL("triggered()"), self.showInfo) + self.connect(self.showChatAvatarsAction, SIGNAL("triggered()"), self.changeShowAvatars) + self.connect(self.alwaysOnTopAction, SIGNAL("triggered()"), self.changeAlwaysOnTop) + self.connect(self.chooseChatRoomsAction, SIGNAL("triggered()"), self.showChatroomChooser) + self.connect(self.catchRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.catchRegionAction)) + self.connect(self.providenceRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.providenceRegionAction)) + self.connect(self.queriousRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.queriousRegionAction)) + self.connect(self.providenceCatchRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.providenceCatchRegionAction)) + self.connect(self.providenceCatchCompactRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.providenceCatchCompactRegionAction)) + self.connect(self.chooseRegionAction, SIGNAL("triggered()"), self.showRegionChooser) + self.connect(self.showChatAction, SIGNAL("triggered()"), self.changeChatVisibility) + self.connect(self.soundSetupAction, SIGNAL("triggered()"), self.showSoundSetup) + self.connect(self.activateSoundAction, SIGNAL("triggered()"), self.changeSound) + self.connect(self.useSpokenNotificationsAction, SIGNAL("triggered()"), self.changeUseSpokenNotifications) + self.connect(self.trayIcon, SIGNAL("alarm_distance"), self.changeAlarmDistance) + self.connect(self.framelessWindowAction, SIGNAL("triggered()"), self.changeFrameless) + self.connect(self.trayIcon, SIGNAL("change_frameless"), self.changeFrameless) + self.connect(self.frameButton, SIGNAL("clicked()"), self.changeFrameless) + self.connect(self.quitAction, SIGNAL("triggered()"), self.close) + self.connect(self.trayIcon, SIGNAL("quit"), self.close) + self.connect(self.jumpbridgeDataAction, SIGNAL("triggered()"), self.showJumbridgeChooser) self.mapView.page().scrollRequested.connect(self.mapPositionChanged) def setupThreads(self): # Set up threads and their connections self.avatarFindThread = AvatarFindThread() - self.connect(self.avatarFindThread, QtCore.SIGNAL("avatar_update"), self.updateAvatarOnChatEntry) + self.connect(self.avatarFindThread, SIGNAL("avatar_update"), self.updateAvatarOnChatEntry) self.avatarFindThread.start() self.kosRequestThread = KOSCheckerThread() - self.connect(self.kosRequestThread, Qt.SIGNAL("kos_result"), self.showKosResult) + self.connect(self.kosRequestThread, SIGNAL("kos_result"), self.showKosResult) self.kosRequestThread.start() self.filewatcherThread = filewatcher.FileWatcher(self.pathToLogs) - self.connect(self.filewatcherThread, QtCore.SIGNAL("file_change"), self.logFileChanged) + self.connect(self.filewatcherThread, SIGNAL("file_change"), self.logFileChanged) self.filewatcherThread.start() self.versionCheckThread = amazon_s3.NotifyNewVersionThread() - self.versionCheckThread.connect(self.versionCheckThread, Qt.SIGNAL("newer_version"), self.notifyNewerVersion) + self.versionCheckThread.connect(self.versionCheckThread, SIGNAL("newer_version"), self.notifyNewerVersion) self.versionCheckThread.start() self.statisticsThread = MapStatisticsThread() - self.connect(self.statisticsThread, Qt.SIGNAL("statistic_data_update"), self.updateStatisticsOnMap) + self.connect(self.statisticsThread, SIGNAL("statistic_data_update"), self.updateStatisticsOnMap) self.statisticsThread.start() # statisticsThread is blocked until first call of requestStatistics @@ -228,7 +231,7 @@ def setupMap(self, initialize=False): self.dotlan = dotlan.Map(regionName, svg) except dotlan.DotlanException as e: logging.error(e) - QtGui.QMessageBox.critical(None, "Error getting map", six.text_type(e), "Quit") + QMessageBox.critical(None, "Error getting map", six.text_type(e), "Quit") sys.exit(1) if self.dotlan.outdatedCacheError: @@ -236,7 +239,7 @@ def setupMap(self, initialize=False): diagText = "Something went wrong getting map data. Proceeding with older cached data. " \ "Check for a newer version and inform the maintainer.\n\nError: {0} {1}".format(type(e), six.text_type(e)) logging.warn(diagText) - QtGui.QMessageBox.warning(None, "Using map from cache", diagText, "Ok") + QMessageBox.warning(None, "Using map from cache", diagText, "Ok") # Load the jumpbridges @@ -244,7 +247,7 @@ def setupMap(self, initialize=False): self.setJumpbridges(self.cache.getFromCache("jumpbridge_url")) self.systems = self.dotlan.systems logging.critical("Creating chat parser") - self.chatparser = chatparser.ChatParser(self.pathToLogs, self.roomnames, self.systems) + self.chatparser = ChatParser(self.pathToLogs, self.roomnames, self.systems) # Menus - only once if initialize: @@ -258,7 +261,7 @@ def mapContextMenuEvent(event): self.mapView.contextMenu = self.trayIcon.contextMenu() # Clicking links - self.mapView.connect(self.mapView, Qt.SIGNAL("linkClicked(const QUrl&)"), self.mapLinkClicked) + self.mapView.connect(self.mapView, SIGNAL("linkClicked(const QUrl&)"), self.mapLinkClicked) # Also set up our app menus if not regionName: @@ -302,13 +305,13 @@ def startClipboardTimer(self): first initializing the content so we dont kos check from random content """ self.oldClipboardContent = tuple(six.text_type(self.clipboard.text())) - self.connect(self.clipboardTimer, QtCore.SIGNAL("timeout()"), self.clipboardChanged) + self.connect(self.clipboardTimer, SIGNAL("timeout()"), self.clipboardChanged) self.clipboardTimer.start(CLIPBOARD_CHECK_INTERVAL_MSECS) def stopClipboardTimer(self): if self.clipboardTimer: - self.disconnect(self.clipboardTimer, QtCore.SIGNAL("timeout()"), self.clipboardChanged) + self.disconnect(self.clipboardTimer, SIGNAL("timeout()"), self.clipboardChanged) self.clipboardTimer.stop() @@ -408,7 +411,7 @@ def changeSound(self, newValue=None, disable=False): self.activateSoundAction.setEnabled(False) self.soundSetupAction.setEnabled(False) #self.soundButton.setEnabled(False) - QtGui.QMessageBox.warning(None, "Sound disabled", + QMessageBox.warning(None, "Sound disabled", "The lib 'pyglet' which is used to play sounds cannot be found, ""so the soundsystem is disabled.\nIf you want sound, please install the 'pyglet' library. This warning will not be shown again.", "OK") else: @@ -516,9 +519,9 @@ def mapLinkClicked(self, url): systemName = six.text_type(url.path().split("/")[-1]).upper() system = self.systems[str(systemName)] sc = SystemChat(self, SystemChat.SYSTEM, system, self.chatEntries, self.knownPlayerNames) - sc.connect(self, Qt.SIGNAL("chat_message_added"), sc.addChatEntry) - sc.connect(self, Qt.SIGNAL("avatar_loaded"), sc.newAvatarAvailable) - sc.connect(sc, Qt.SIGNAL("location_set"), self.setLocation) + sc.connect(self, SIGNAL("chat_message_added"), sc.addChatEntry) + sc.connect(self, SIGNAL("avatar_loaded"), sc.newAvatarAvailable) + sc.connect(sc, SIGNAL("location_set"), self.setLocation) sc.show() @@ -575,14 +578,14 @@ def mapPositionChanged(self, dx, dy, rectToScroll): def showChatroomChooser(self): chooser = ChatroomsChooser(self) - chooser.connect(chooser, Qt.SIGNAL("rooms_changed"), self.changedRoomnames) + chooser.connect(chooser, SIGNAL("rooms_changed"), self.changedRoomnames) chooser.show() def showJumbridgeChooser(self): url = self.cache.getFromCache("jumpbridge_url") chooser = JumpbridgeChooser(self, url) - chooser.connect(chooser, Qt.SIGNAL("set_jumpbridge_url"), self.setJumpbridges) + chooser.connect(chooser, SIGNAL("set_jumpbridge_url"), self.setJumpbridges) chooser.show() @@ -606,7 +609,7 @@ def setJumpbridges(self, url): self.dotlan.setJumpbridges(data) self.cache.putIntoCache("jumpbridge_url", url, 60 * 60 * 24 * 365 * 8) except Exception as e: - QtGui.QMessageBox.warning(None, "Loading jumpbridges failed!", "Error: {0}".format(six.text_type(e)), "OK") + QMessageBox.warning(None, "Loading jumpbridges failed!", "Error: {0}".format(six.text_type(e)), "OK") def handleRegionMenuItemSelected(self, menuAction=None): @@ -632,7 +635,7 @@ def handleRegionChosen(): self.chooseRegionAction.setChecked(False) chooser = RegionChooser(self) - self.connect(chooser, Qt.SIGNAL("new_region_chosen"), handleRegionChosen) + self.connect(chooser, SIGNAL("new_region_chosen"), handleRegionChosen) chooser.show() @@ -647,8 +650,8 @@ def addMessageToIntelChat(self, message): self.chatListWidget.setItemWidget(listWidgetItem, chatEntryWidget) self.avatarFindThread.addChatEntry(chatEntryWidget) self.chatEntries.append(chatEntryWidget) - self.connect(chatEntryWidget, Qt.SIGNAL("mark_system"), self.markSystemOnMap) - self.emit(Qt.SIGNAL("chat_message_added"), chatEntryWidget) + self.connect(chatEntryWidget, SIGNAL("mark_system"), self.markSystemOnMap) + self.emit(SIGNAL("chat_message_added"), chatEntryWidget) self.pruneMessages() if scrollToBottom: self.chatListWidget.scrollToBottom() @@ -707,7 +710,7 @@ def showInfo(self): uic.loadUi(resourcePath("vi/ui/Info.ui"), infoDialog) infoDialog.versionLabel.setText(u"Version: {0}".format(vi.version.VERSION)) infoDialog.logoLabel.setPixmap(QtGui.QPixmap(resourcePath("vi/ui/res/logo.png"))) - infoDialog.connect(infoDialog.closeButton, Qt.SIGNAL("clicked()"), infoDialog.accept) + infoDialog.connect(infoDialog.closeButton, SIGNAL("clicked()"), infoDialog.accept) infoDialog.show() @@ -715,9 +718,9 @@ def showSoundSetup(self): dialog = QtGui.QDialog(self) uic.loadUi(resourcePath("vi/ui/SoundSetup.ui"), dialog) dialog.volumeSlider.setValue(SoundManager().soundVolume) - dialog.connect(dialog.volumeSlider, Qt.SIGNAL("valueChanged(int)"), SoundManager().setSoundVolume) - dialog.connect(dialog.testSoundButton, Qt.SIGNAL("clicked()"), SoundManager().playSound) - dialog.connect(dialog.closeButton, Qt.SIGNAL("clicked()"), dialog.accept) + dialog.connect(dialog.volumeSlider, SIGNAL("valueChanged(int)"), SoundManager().setSoundVolume) + dialog.connect(dialog.testSoundButton, SIGNAL("clicked()"), SoundManager().playSound) + dialog.connect(dialog.closeButton, SIGNAL("clicked()"), dialog.accept) dialog.show() @@ -737,7 +740,7 @@ def updateAvatarOnChatEntry(self, chatEntry, avatarData): if not updated: self.avatarFindThread.addChatEntry(chatEntry, clearCache=True) else: - self.emit(Qt.SIGNAL("avatar_loaded"), chatEntry.message.user, avatarData) + self.emit(SIGNAL("avatar_loaded"), chatEntry.message.user, avatarData) def updateStatisticsOnMap(self, data): @@ -773,7 +776,7 @@ def logFileChanged(self, path): self.knownPlayerNames.add(message.user) self.setLocation(message.user, message.systems[0]) elif message.status == states.KOS_STATUS_REQUEST: - # Do not accept KOS requests from monitored intel channels + # Do not accept KOS requests from any but monitored intel channels # as we don't want to encourage the use of xxx in those channels. if not message.room in self.roomnames: text = message.message[4:] @@ -786,10 +789,11 @@ def logFileChanged(self, path): self.addMessageToIntelChat(message) # For each system that was mentioned in the message, check for alarm distance to the current system # and alarm if within alarm distance. + systemList = self.dotlan.systems if message.systems: for system in message.systems: systemname = system.name - self.dotlan.systems[systemname].setStatus(message.status) + systemList[systemname].setStatus(message.status) if message.status in (states.REQUEST, states.ALARM) and message.user not in self.knownPlayerNames: alarmDistance = self.alarmDistance if message.status == states.ALARM else 0 for nSystem, data in system.getNeighbours(alarmDistance).items(): @@ -804,9 +808,9 @@ class ChatroomsChooser(QtGui.QDialog): def __init__(self, parent): QtGui.QDialog.__init__(self, parent) uic.loadUi(resourcePath("vi/ui/ChatroomsChooser.ui"), self) - self.connect(self.defaultButton, Qt.SIGNAL("clicked()"), self.setDefaults) - self.connect(self.cancelButton, Qt.SIGNAL("clicked()"), self.accept) - self.connect(self.saveButton, Qt.SIGNAL("clicked()"), self.saveClicked) + self.connect(self.defaultButton, SIGNAL("clicked()"), self.setDefaults) + self.connect(self.cancelButton, SIGNAL("clicked()"), self.accept) + self.connect(self.saveButton, SIGNAL("clicked()"), self.saveClicked) cache = Cache() roomnames = cache.getFromCache("room_names") if not roomnames: @@ -818,7 +822,7 @@ def saveClicked(self): text = six.text_type(self.roomnamesField.toPlainText()) rooms = [six.text_type(name.strip()) for name in text.split(",")] self.accept() - self.emit(Qt.SIGNAL("rooms_changed"), rooms) + self.emit(SIGNAL("rooms_changed"), rooms) def setDefaults(self): @@ -829,8 +833,8 @@ class RegionChooser(QtGui.QDialog): def __init__(self, parent): QtGui.QDialog.__init__(self, parent) uic.loadUi(resourcePath("vi/ui/RegionChooser.ui"), self) - self.connect(self.cancelButton, Qt.SIGNAL("clicked()"), self.accept) - self.connect(self.saveButton, Qt.SIGNAL("clicked()"), self.saveClicked) + self.connect(self.cancelButton, SIGNAL("clicked()"), self.accept) + self.connect(self.saveButton, SIGNAL("clicked()"), self.saveClicked) cache = Cache() regionName = cache.getFromCache("region_name") if not regionName: @@ -866,7 +870,7 @@ def saveClicked(self): if correct: Cache().putIntoCache("region_name", text, 60 * 60 * 24 * 365) self.accept() - self.emit(Qt.SIGNAL("new_region_chosen")) + self.emit(SIGNAL("new_region_chosen")) class SystemChat(QtGui.QDialog): @@ -888,10 +892,10 @@ def __init__(self, parent, chatType, selector, chatEntries, knownPlayerNames): for name in knownPlayerNames: self.playerNamesBox.addItem(name) self.setWindowTitle("Chat for {0}".format(titleName)) - self.connect(self.closeButton, Qt.SIGNAL("clicked()"), self.closeDialog) - self.connect(self.alarmButton, Qt.SIGNAL("clicked()"), self.setSystemAlarm) - self.connect(self.clearButton, Qt.SIGNAL("clicked()"), self.setSystemClear) - self.connect(self.locationButton, Qt.SIGNAL("clicked()"), self.locationSet) + self.connect(self.closeButton, SIGNAL("clicked()"), self.closeDialog) + self.connect(self.alarmButton, SIGNAL("clicked()"), self.setSystemAlarm) + self.connect(self.clearButton, SIGNAL("clicked()"), self.setSystemClear) + self.connect(self.locationButton, SIGNAL("clicked()"), self.locationSet) def _addMessageToChat(self, message, avatarPixmap): @@ -905,7 +909,7 @@ def _addMessageToChat(self, message, avatarPixmap): self.chat.addItem(listWidgetItem) self.chat.setItemWidget(listWidgetItem, entry) self.chatEntries.append(entry) - self.connect(entry, Qt.SIGNAL("mark_system"), self.parent.markSystemOnMap) + self.connect(entry, SIGNAL("mark_system"), self.parent.markSystemOnMap) if scrollToBottom: self.chat.scrollToBottom() @@ -920,7 +924,7 @@ def addChatEntry(self, entry): def locationSet(self): char = six.text_type(self.playerNamesBox.currentText()) - self.emit(Qt.SIGNAL("location_set"), char, self.system.name) + self.emit(SIGNAL("location_set"), char, self.system.name) def newAvatarAvailable(self, charname, avatarData): @@ -956,7 +960,7 @@ def __init__(self, message): self.avatarLabel.setPixmap(self.questionMarkPixmap) self.message = message self.updateText() - self.connect(self.textLabel, QtCore.SIGNAL("linkActivated(QString)"), self.linkClicked) + self.connect(self.textLabel, SIGNAL("linkActivated(QString)"), self.linkClicked) if sys.platform.startswith("win32") or sys.platform.startswith("cygwin"): ChatEntryWidget.TEXT_SIZE = 8 self.changeFontSize(self.TEXT_SIZE) @@ -968,7 +972,7 @@ def linkClicked(self, link): link = six.text_type(link) function, parameter = link.split("/", 1) if function == "mark_system": - self.emit(QtCore.SIGNAL("mark_system"), parameter) + self.emit(SIGNAL("mark_system"), parameter) elif function == "link": webbrowser.open(parameter) @@ -1002,8 +1006,8 @@ class JumpbridgeChooser(QtGui.QDialog): def __init__(self, parent, url): QtGui.QDialog.__init__(self, parent) uic.loadUi(resourcePath("vi/ui/JumpbridgeChooser.ui"), self) - self.connect(self.saveButton, Qt.SIGNAL("clicked()"), self.savePath) - self.connect(self.cancelButton, Qt.SIGNAL("clicked()"), self.accept) + self.connect(self.saveButton, SIGNAL("clicked()"), self.savePath) + self.connect(self.cancelButton, SIGNAL("clicked()"), self.accept) self.urlField.setText(url) # loading format explanation from textfile with open(resourcePath("docs/jumpbridgeformat.txt")) as f: @@ -1015,7 +1019,7 @@ def savePath(self): url = six.text_type(self.urlField.text()) if url != "": requests.get(url).text - self.emit(QtCore.SIGNAL("set_jumpbridge_url"), url) + self.emit(SIGNAL("set_jumpbridge_url"), url) self.accept() except Exception as e: - QtGui.QMessageBox.critical(None, "Finding Jumpbridgedata failed", "Error: {0}".format(six.text_type(e)), "OK") + QMessageBox.critical(None, "Finding Jumpbridgedata failed", "Error: {0}".format(six.text_type(e)), "OK") diff --git a/src/vintel.py b/src/vintel.py index a036fb1..d1f5315 100644 --- a/src/vintel.py +++ b/src/vintel.py @@ -32,7 +32,7 @@ from vi.cache import cache from vi.resources import resourcePath from vi.cache.cache import Cache -from vi import PanningWebView +from PyQt4.QtGui import QApplication, QMessageBox def exceptHook(exceptionType, exceptionValue, tracebackObject): @@ -51,7 +51,7 @@ def exceptHook(exceptionType, exceptionValue, tracebackObject): backGroundColor = "#c6d9ec" -class Application(QtGui.QApplication): +class Application(QApplication): def __init__(self, args): super(Application, self).__init__(args) @@ -75,7 +75,7 @@ def __init__(self, args): chatLogDirectory = os.path.join(documentsPath, "EVE", "logs", "Chatlogs") if not os.path.exists(chatLogDirectory): # None of the paths for logs exist, bailing out - QtGui.QMessageBox.critical(None, "No path to Logs", "No logs found at: " + chatLogDirectory, "Quit") + QMessageBox.critical(None, "No path to Logs", "No logs found at: " + chatLogDirectory, "Quit") sys.exit(1) # Setting local directory for cache and logging From 72efc9af7c42ad273f1aefa8070d9ca106a61e93 Mon Sep 17 00:00:00 2001 From: Jesse Litton Date: Sun, 17 Jul 2016 16:44:45 -0500 Subject: [PATCH 04/36] Fix parsing of messages with periods --- src/vi/chatparser/parser_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vi/chatparser/parser_functions.py b/src/vi/chatparser/parser_functions.py index 692e734..5ad171c 100644 --- a/src/vi/chatparser/parser_functions.py +++ b/src/vi/chatparser/parser_functions.py @@ -42,7 +42,7 @@ from bs4.element import NavigableString from vi import states -CHARS_TO_IGNORE = ("*", "?", ",", "!") +CHARS_TO_IGNORE = ("*", "?", ",", "!", ".") def textReplace(element, newText): From 2873ca9b3411528e0d7356bbc01ddb9d02e0f7d5 Mon Sep 17 00:00:00 2001 From: Mark Kieling Date: Sun, 7 Aug 2016 10:00:45 -0700 Subject: [PATCH 05/36] Users moving to Wine now find their logs under their home directory --- src/vintel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vintel.py b/src/vintel.py index d1f5315..2ebbc95 100644 --- a/src/vintel.py +++ b/src/vintel.py @@ -63,7 +63,9 @@ def __init__(self, args): if not os.path.exists(chatLogDirectory): if sys.platform.startswith("darwin"): - chatLogDirectory = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "Eve Online", + chatLogDirectory = os.path.join(os.path.expanduser("~"), "Documents", "EVE", "logs", "Chatlogs") + if not os.path.exists(chatLogDirectory): + chatLogDirectory = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "Eve Online", "p_drive", "User", "My Documents", "EVE", "logs", "Chatlogs") elif sys.platform.startswith("linux"): chatLogDirectory = os.path.join(os.path.expanduser("~"), "EVE", "logs", "Chatlogs") From 23646a009169c2646e847ed26a40f899a96b7679 Mon Sep 17 00:00:00 2001 From: "igor.gritsay" Date: Mon, 8 Aug 2016 02:35:18 +0300 Subject: [PATCH 06/36] add local room name for russian locale --- src/vi/chatparser/chatparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vi/chatparser/chatparser.py b/src/vi/chatparser/chatparser.py index dcb88fb..87f2010 100644 --- a/src/vi/chatparser/chatparser.py +++ b/src/vi/chatparser/chatparser.py @@ -33,7 +33,7 @@ from .parser_functions import parseUrls, parseShips, parseSystems # Names the local chatlogs could start with (depends on l10n of the client) -LOCAL_NAMES = ("Lokal", "Local") +LOCAL_NAMES = ("Локальный", "Lokal", "Local") class ChatParser(object): From b44e8d89439277817bc03c26745cbffd257d600d Mon Sep 17 00:00:00 2001 From: Mark Kieling Date: Mon, 8 Aug 2016 08:56:39 -0700 Subject: [PATCH 07/36] Build without appending the version to the app name --- src/vintel.spec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vintel.spec b/src/vintel.spec index 0ddb68b..28f40a3 100644 --- a/src/vintel.spec +++ b/src/vintel.spec @@ -1,7 +1,7 @@ # -*- mode: python -*- import sys -app_version = 'vintel-1.2.3' +app_name = 'vintel' a = Analysis(['vintel.py'], pathex=['z:\\mark\\code\\vintel\\src' if sys.platform == 'win32' else '/Users/mark/code/vintel/src'], @@ -40,7 +40,7 @@ exe = EXE(pyz, a.binaries, a.zipfiles, a.datas, - name=os.path.join('dist', app_version + ('.exe' if sys.platform == 'win32' else '')), + name=os.path.join('dist', app_name + ('.exe' if sys.platform == 'win32' else '')), debug=False, strip=None, upx=True, @@ -50,5 +50,5 @@ exe = EXE(pyz, # Build a .app if on OS X if sys.platform == 'darwin': app = BUNDLE(exe, - name=app_version + '.app', + name=app_name + '.app', icon="icon.ico") \ No newline at end of file From e47d350f054eff034f20cab4a958703a63146495 Mon Sep 17 00:00:00 2001 From: Mark Kieling Date: Mon, 8 Aug 2016 08:57:04 -0700 Subject: [PATCH 08/36] Bump to 1.2.4 --- src/vi/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vi/version.py b/src/vi/version.py index f44977b..7859a3d 100644 --- a/src/vi/version.py +++ b/src/vi/version.py @@ -1,3 +1,3 @@ -VERSION = "1.2.3" +VERSION = "1.2.4" SNAPSHOT = False # set to false when releasing From 82759b42b1beabfe5e6ef0e2b92f7ff8a322ae35 Mon Sep 17 00:00:00 2001 From: Mark Kieling Date: Mon, 8 Aug 2016 19:06:51 -0700 Subject: [PATCH 09/36] No ascii in source - no good. --- src/vi/chatparser/chatparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vi/chatparser/chatparser.py b/src/vi/chatparser/chatparser.py index 87f2010..8d7ac21 100644 --- a/src/vi/chatparser/chatparser.py +++ b/src/vi/chatparser/chatparser.py @@ -33,7 +33,7 @@ from .parser_functions import parseUrls, parseShips, parseSystems # Names the local chatlogs could start with (depends on l10n of the client) -LOCAL_NAMES = ("Локальный", "Lokal", "Local") +LOCAL_NAMES = ("Local", "Lokal", six.text_type("\u041B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439")) class ChatParser(object): From c92a9bc390c1892ef49448dd1a10cf1ff2d48f11 Mon Sep 17 00:00:00 2001 From: Mark Kieling Date: Mon, 8 Aug 2016 19:19:59 -0700 Subject: [PATCH 10/36] Spec tuneup --- src/vintel.spec | 110 ++++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/src/vintel.spec b/src/vintel.spec index 28f40a3..6bf561b 100644 --- a/src/vintel.spec +++ b/src/vintel.spec @@ -1,54 +1,56 @@ -# -*- mode: python -*- -import sys - -app_name = 'vintel' - -a = Analysis(['vintel.py'], - pathex=['z:\\mark\\code\\vintel\\src' if sys.platform == 'win32' else '/Users/mark/code/vintel/src'], - hiddenimports=[], - hookspath=None, - runtime_hooks=None, - excludes=None) - -pyz = PYZ(a.pure) - -a.datas += [('vi/ui/MainWindow.ui', 'vi/ui/MainWindow.ui', 'DATA'), - ('vi/ui/SystemChat.ui', 'vi/ui/SystemChat.ui', 'DATA'), - ('vi/ui/ChatEntry.ui', 'vi/ui/ChatEntry.ui', 'DATA'), - ('vi/ui/Info.ui', 'vi/ui/Info.ui', 'DATA'), - ('vi/ui/ChatroomsChooser.ui', 'vi/ui/ChatroomsChooser.ui', 'DATA'), - ('vi/ui/RegionChooser.ui', 'vi/ui/RegionChooser.ui', 'DATA'), - ('vi/ui/SoundSetup.ui', 'vi/ui/SoundSetup.ui', 'DATA'), - ('vi/ui/JumpbridgeChooser.ui', 'vi/ui/JumpbridgeChooser.ui', 'DATA'), - ('vi/ui/res/qmark.png', 'vi/ui/res/qmark.png', 'DATA'), - ('vi/ui/res/logo.png', 'vi/ui/res/logo.png', 'DATA'), - ('vi/ui/res/logo_small.png', 'vi/ui/res/logo_small.png', 'DATA'), - ('vi/ui/res/logo_small_green.png', 'vi/ui/res/logo_small_green.png', 'DATA'), - ('vi/ui/res/178028__zimbot__bosun-whistle-sttos-recreated.wav', - 'vi/ui/res/178028__zimbot__bosun-whistle-sttos-recreated.wav', 'DATA'), - ('vi/ui/res/178031__zimbot__transporterstartbeep0-sttos-recreated.wav', - 'vi/ui/res/178031__zimbot__transporterstartbeep0-sttos-recreated.wav', 'DATA'), - ('vi/ui/res/178032__zimbot__redalert-klaxon-sttos-recreated.wav', - 'vi/ui/res/178032__zimbot__redalert-klaxon-sttos-recreated.wav', 'DATA'), - ('vi/ui/res/mapdata/Providencecatch.svg', - 'vi/ui/res/mapdata/Providencecatch.svg', 'DATA'), - ('docs/jumpbridgeformat.txt', 'docs/jumpbridgeformat.txt', 'DATA'), - ] - -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name=os.path.join('dist', app_name + ('.exe' if sys.platform == 'win32' else '')), - debug=False, - strip=None, - upx=True, - icon="icon.ico", - console=False) - -# Build a .app if on OS X -if sys.platform == 'darwin': - app = BUNDLE(exe, - name=app_name + '.app', - icon="icon.ico") \ No newline at end of file +# -*- mode: python -*- +import sys + +app_name = 'vintel' +block_cipher = None + +a = Analysis(['vintel.py'], + pathex=['z:\\mark\\code\\vintel\\src' if sys.platform == 'win32' else '/Users/mark/code/vintel/src'], + binaries=None, + datas=None, + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +a.datas += [('vi/ui/MainWindow.ui', 'vi/ui/MainWindow.ui', 'DATA'), + ('vi/ui/SystemChat.ui', 'vi/ui/SystemChat.ui', 'DATA'), + ('vi/ui/ChatEntry.ui', 'vi/ui/ChatEntry.ui', 'DATA'), + ('vi/ui/Info.ui', 'vi/ui/Info.ui', 'DATA'), + ('vi/ui/ChatroomsChooser.ui', 'vi/ui/ChatroomsChooser.ui', 'DATA'), + ('vi/ui/RegionChooser.ui', 'vi/ui/RegionChooser.ui', 'DATA'), + ('vi/ui/SoundSetup.ui', 'vi/ui/SoundSetup.ui', 'DATA'), + ('vi/ui/JumpbridgeChooser.ui', 'vi/ui/JumpbridgeChooser.ui', 'DATA'), + ('vi/ui/res/qmark.png', 'vi/ui/res/qmark.png', 'DATA'), + ('vi/ui/res/logo.png', 'vi/ui/res/logo.png', 'DATA'), + ('vi/ui/res/logo_small.png', 'vi/ui/res/logo_small.png', 'DATA'), + ('vi/ui/res/logo_small_green.png', 'vi/ui/res/logo_small_green.png', 'DATA'), + ('vi/ui/res/178028__zimbot__bosun-whistle-sttos-recreated.wav', 'vi/ui/res/178028__zimbot__bosun-whistle-sttos-recreated.wav', 'DATA'), + ('vi/ui/res/178031__zimbot__transporterstartbeep0-sttos-recreated.wav', 'vi/ui/res/178031__zimbot__transporterstartbeep0-sttos-recreated.wav', 'DATA'), + ('vi/ui/res/178032__zimbot__redalert-klaxon-sttos-recreated.wav', 'vi/ui/res/178032__zimbot__redalert-klaxon-sttos-recreated.wav', 'DATA'), + ('vi/ui/res/mapdata/Providencecatch.svg', 'vi/ui/res/mapdata/Providencecatch.svg', 'DATA'), + ('docs/jumpbridgeformat.txt', 'docs/jumpbridgeformat.txt', 'DATA'), + ] + +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + name=os.path.join('dist', app_name + ('.exe' if sys.platform == 'win32' else '')), + debug=False, + strip=False, + icon='icon.ico', + console=False, + cipher=block_cipher) + +# Build a .app if on OS X +if sys.platform == 'darwin': + app = BUNDLE(exe, + name=app_name + '.app', + icon='icon.ico') From a3627dbd8718bf8c7f082040a647f08b2cb12d35 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Tue, 3 Jan 2017 00:11:00 -0500 Subject: [PATCH 11/36] Minor fixes: * make config variable saves to cache more obvious and for a near infintite life * retain secondary info line (either sov. info or region) and display in title chat popup for system * allow view chat popup for systems rendered as squares -- those shown on map but not part of current region * explicit delete from cache on some errors - when have bad svg cached or bad region name saved --- src/vi/cache/cache.py | 21 +++++++++++++++-- src/vi/dotlan.py | 54 ++++++++++++++++++++++++++++++++----------- src/vi/ui/viui.py | 36 +++++++++++++++++------------ src/vintel.py | 5 ++-- 4 files changed, 83 insertions(+), 33 deletions(-) diff --git a/src/vi/cache/cache.py b/src/vi/cache/cache.py index 3d0837b..0a0a8ec 100644 --- a/src/vi/cache/cache.py +++ b/src/vi/cache/cache.py @@ -21,6 +21,7 @@ import threading import time import six +import sys if six.PY2: def to_blob(x): return buffer(str(x)) @@ -98,6 +99,24 @@ def getFromCache(self, key, outdated=False): else: return founds[0][1] + def getConfigValue(self, key): + """ Retrieve a config value from cache that never expires + """ + return self.getFromCache(key, True) + + def saveConfigValue(self, key, value): + """ Save a config value to cache that never expires + """ + return self.putIntoCache(key, value, sys.maxsize) + + def deleteFromCache(self, key): + """ Deleteing from cache + """ + with Cache.SQLITE_WRITE_LOCK: + query = "DELETE FROM cache WHERE key = ?" + self.con.execute(query, (key,)) + self.con.commit() + def putPlayerName(self, name, status): """ Putting a playername into the cache """ @@ -161,5 +180,3 @@ def recallAndApplySettings(self, responder, settingsIdentifier): getattr(obj, setting[1])(setting[2]) except Exception as e: logging.error(e) - - diff --git a/src/vi/dotlan.py b/src/vi/dotlan.py index 182b7bd..9563350 100644 --- a/src/vi/dotlan.py +++ b/src/vi/dotlan.py @@ -90,17 +90,39 @@ def __init__(self, region, svgFile=None): "without the map.\n\nRemember the site for possible " \ "updates: https://github.com/Xanthos-Eve/vintel".format(type(e), six.text_type(e)) raise DotlanException(t) + # Create soup from the svg - self.soup = BeautifulSoup(svg, 'html.parser') - self.systems = self._extractSystemsFromSoup(self.soup) - self.systemsById = {} - for system in self.systems.values(): - self.systemsById[system.systemId] = system - self._prepareSvg(self.soup, self.systems) - self._connectNeighbours() - self._jumpMapsVisible = False - self._statisticsVisible = False - self.marker = self.soup.select("#select_marker")[0] + try: + self.soup = BeautifulSoup(svg, 'html.parser') + self.systems = self._extractSystemsFromSoup(self.soup) + self._cleanSoup(self.soup) + self.systemsById = {} + for system in self.systems.values(): + self.systemsById[system.systemId] = system + self._prepareSvg(self.soup, self.systems) + self._connectNeighbours() + self._jumpMapsVisible = False + self._statisticsVisible = False + self.marker = self.soup.select("#select_marker")[0] + except Exception as e: + # Could get hung up on a bad retreival forever + cache.deleteFromCache("map_" + self.region) + raise + + def _cleanSoup(self, soup): + try: + legend = soup.find('g', id='legend') + copyright = legend.find('text', class_='lc') + copyright.string = '(C) by Wollari & CCP' + for t in legend.select("text"): + if t.text == '= Alliance': + t.string = '= Intel Time' + elif t.text == '= Sov. Lvl' or t.text == 'Z': + t.string = '' + elif t.text == 'YYYYY (Z)': + t.string = 'YYYYY' + except: + pass def _extractSystemsFromSoup(self, soup): systems = {} @@ -118,16 +140,21 @@ def _extractSystemsFromSoup(self, soup): continue for element in symbol.select(".sys"): name = element.select("text")[0].text.strip().upper() + # could be region name or sov. info + secondaryInfo = element.select("text")[1].text.strip().upper() mapCoordinates = {} for keyname in ("x", "y", "width", "height"): mapCoordinates[keyname] = float(uses[symbolId][keyname]) mapCoordinates["center_x"] = (mapCoordinates["x"] + (mapCoordinates["width"] / 2)) mapCoordinates["center_y"] = (mapCoordinates["y"] + (mapCoordinates["height"] / 2)) + # Modify href to just be system name to deal better with out of region systems drawn on map + # leaving slash for now, not sure about effects on provi/catch combined maps + symbol.a['xlink:href'] = "/" + name try: transform = uses[symbolId]["transform"] except KeyError: transform = "translate(0,0)" - systems[name] = System(name, element, self.soup, mapCoordinates, transform, systemId) + systems[name] = System(name, secondaryInfo, element, self.soup, mapCoordinates, transform, systemId) return systems def _prepareSvg(self, soup, systems): @@ -291,9 +318,10 @@ class System(object): UNKNOWN_COLOR = "#FFFFFF" CLEAR_COLOR = "#59FF6C" - def __init__(self, name, svgElement, mapSoup, mapCoordinates, transform, systemId): + def __init__(self, name, secondaryInfo, svgElement, mapSoup, mapCoordinates, transform, systemId): self.status = states.UNKNOWN self.name = name + self.secondaryInfo = secondaryInfo self.svgElement = svgElement self.mapSoup = mapSoup self.origSvgElement = svgElement @@ -485,7 +513,7 @@ def update(self): def convertRegionName(name): """ - Converts a (system)name to the format that dotland uses + Converts a (system)name to the format that dotlan uses """ converted = [] nextUpper = False diff --git a/src/vi/ui/viui.py b/src/vi/ui/viui.py index a253f1e..0cbca6d 100644 --- a/src/vi/ui/viui.py +++ b/src/vi/ui/viui.py @@ -94,12 +94,12 @@ def __init__(self, pathToLogs, trayIcon, backGroundColor): QMessageBox.warning(None, "Known Characters not Found", diagText, "Ok") # Set up user's intel rooms - roomnames = self.cache.getFromCache("room_names") + roomnames = self.cache.getConfigValue("room_names") if roomnames: roomnames = roomnames.split(",") else: roomnames = (u"TheCitadel", u"North Provi Intel", u"North Catch Intel", "North Querious Intel") - self.cache.putIntoCache("room_names", u",".join(roomnames), 60 * 60 * 24 * 365 * 5) + self.cache.saveConfigValue("room_names", u",".join(roomnames)) self.roomnames = roomnames # Disable the sound UI if sound is not available @@ -217,7 +217,7 @@ def setupMap(self, initialize=False): self.filewatcherThread.paused = True logging.info("Finding map file") - regionName = self.cache.getFromCache("region_name") + regionName = self.cache.getConfigValue("region_name") if not regionName: regionName = "Providence" svg = None @@ -233,6 +233,11 @@ def setupMap(self, initialize=False): logging.error(e) QMessageBox.critical(None, "Error getting map", six.text_type(e), "Quit") sys.exit(1) + except Exception as e: + self.cache.deleteFromCache("region_name") + logging.error(e) + QMessageBox.critical(None, "Error setting up map", six.text_type(e), "Quit") + sys.exit(1) if self.dotlan.outdatedCacheError: e = self.dotlan.outdatedCacheError @@ -244,8 +249,9 @@ def setupMap(self, initialize=False): # Load the jumpbridges logging.critical("Load jump bridges") - self.setJumpbridges(self.cache.getFromCache("jumpbridge_url")) + self.setJumpbridges(self.cache.getConfigValue("jumpbridge_url")) self.systems = self.dotlan.systems + logging.critical("Creating chat parser") self.chatparser = ChatParser(self.pathToLogs, self.roomnames, self.systems) @@ -342,7 +348,7 @@ def closeEvent(self, event): (None, "changeUseSpokenNotifications", self.useSpokenNotificationsAction.isChecked()), (None, "changeKosCheckClipboard", self.kosClipboardActiveAction.isChecked()), (None, "changeAutoScanIntel", self.scanIntelForKosRequestsEnabled)) - self.cache.putIntoCache("settings", str(settings), 60 * 60 * 24 * 30) + self.cache.saveConfigValue("settings", str(settings)) # Stop the threads try: @@ -561,7 +567,7 @@ def loadInitialMapPositions(self, newDictionary): def setInitialMapPositionForRegion(self, regionName): try: if not regionName: - regionName = self.cache.getFromCache("region_name") + regionName = self.cache.getConfigValue("region_name") if regionName: xy = self.mapPositionsDict[regionName] self.initialMapPosition = QPoint(xy[0], xy[1]) @@ -570,7 +576,7 @@ def setInitialMapPositionForRegion(self, regionName): def mapPositionChanged(self, dx, dy, rectToScroll): - regionName = self.cache.getFromCache("region_name") + regionName = self.cache.getConfigValue("region_name") if regionName: scrollPosition = self.mapView.page().mainFrame().scrollPosition() self.mapPositionsDict[regionName] = (scrollPosition.x(), scrollPosition.y()) @@ -583,7 +589,7 @@ def showChatroomChooser(self): def showJumbridgeChooser(self): - url = self.cache.getFromCache("jumpbridge_url") + url = self.cache.getConfigValue("jumpbridge_url") chooser = JumpbridgeChooser(self, url) chooser.connect(chooser, SIGNAL("set_jumpbridge_url"), self.setJumpbridges) chooser.show() @@ -607,7 +613,7 @@ def setJumpbridges(self, url): else: data = amazon_s3.getJumpbridgeData(self.dotlan.region.lower()) self.dotlan.setJumpbridges(data) - self.cache.putIntoCache("jumpbridge_url", url, 60 * 60 * 24 * 365 * 8) + self.cache.saveConfigValue("jumpbridge_url", url) except Exception as e: QMessageBox.warning(None, "Loading jumpbridges failed!", "Error: {0}".format(six.text_type(e)), "OK") @@ -623,7 +629,7 @@ def handleRegionMenuItemSelected(self, menuAction=None): menuAction.setChecked(True) regionName = six.text_type(menuAction.property("regionName").toString()) regionName = dotlan.convertRegionName(regionName) - Cache().putIntoCache("region_name", regionName, 60 * 60 * 24 * 365) + Cache().saveConfigValue("region_name", regionName) self.setupMap() @@ -701,7 +707,7 @@ def showKosResult(self, state, text, requestType, hasKos): def changedRoomnames(self, newRoomnames): - self.cache.putIntoCache("room_names", u",".join(newRoomnames), 60 * 60 * 24 * 365 * 5) + self.cache.saveConfigValue("room_names", u",".join(newRoomnames)) self.chatparser.rooms = newRoomnames @@ -812,7 +818,7 @@ def __init__(self, parent): self.connect(self.cancelButton, SIGNAL("clicked()"), self.accept) self.connect(self.saveButton, SIGNAL("clicked()"), self.saveClicked) cache = Cache() - roomnames = cache.getFromCache("room_names") + roomnames = cache.getConfigValue("room_names") if not roomnames: roomnames = u"TheCitadel,North Provi Intel,North Catch Intel,North Querious Intel" self.roomnamesField.setPlainText(roomnames) @@ -836,7 +842,7 @@ def __init__(self, parent): self.connect(self.cancelButton, SIGNAL("clicked()"), self.accept) self.connect(self.saveButton, SIGNAL("clicked()"), self.saveClicked) cache = Cache() - regionName = cache.getFromCache("region_name") + regionName = cache.getConfigValue("region_name") if not regionName: regionName = u"Providence" self.regionNameField.setPlainText(regionName) @@ -868,7 +874,7 @@ def saveClicked(self): logging.error(e) correct = False if correct: - Cache().putIntoCache("region_name", text, 60 * 60 * 24 * 365) + Cache().saveConfigValue("region_name", text) self.accept() self.emit(SIGNAL("new_region_chosen")) @@ -887,7 +893,7 @@ def __init__(self, parent, chatType, selector, chatEntries, knownPlayerNames): self.addChatEntry(entry) titleName = "" if self.chatType == SystemChat.SYSTEM: - titleName = self.selector.name + titleName = "%s [%s]" % (self.selector.name, self.selector.secondaryInfo) self.system = selector for name in knownPlayerNames: self.playerNamesBox.addItem(name) diff --git a/src/vintel.py b/src/vintel.py index 2ebbc95..a72e487 100644 --- a/src/vintel.py +++ b/src/vintel.py @@ -93,10 +93,10 @@ def __init__(self, args): splash = QtGui.QSplashScreen(QtGui.QPixmap(resourcePath("vi/ui/res/logo.png"))) vintelCache = Cache() - logLevel = vintelCache.getFromCache("logging_level") + logLevel = vintelCache.getConfigValue("logging_level") if not logLevel: logLevel = logging.WARN - backGroundColor = vintelCache.getFromCache("background_color") + backGroundColor = vintelCache.getConfigValue("background_color") if backGroundColor: self.setStyleSheet("QWidget { background-color: %s; }" % backGroundColor) @@ -137,4 +137,3 @@ def __init__(self, args): app = Application(sys.argv) sys.exit(app.exec_()) - From c2b8aaca476a18c36c6a7dde6e688b4575126e96 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Tue, 3 Jan 2017 00:12:46 -0500 Subject: [PATCH 12/36] Updates for cygwin & win32 startup - don't try to invoke windows apis from cygwn, on windows fix taskbar icon --- README.md | 17 +++++++++++++++++ src/vintel.py | 17 +++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index db79e05..509b8f5 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,23 @@ pyglet is used to play the sound – If it is not available the sound option wil https://pypi.python.org/pypi/requests - Six for python 3 compatibility https://pypi.python.org/pypi/six +## Note for windows users + +The anaconda python package will come with most of what you need [download](https://www.continuum.io/downloads#windows). +vintel compilation has been tested with 64 bit anaconda2 v4.2.0 for python 2.7 on windows 10. + +You will need to run these two commands to downgrade qt5 to qt4 and install the pyglet package. If you installed anaconda for all +users, you may need to run them in an *admin* anaconda prompt. + +``` +conda install pyqt=4 +pip install pyglet +``` + +## Note for cygwin users + +pyglet doesn't currently work under cygwin. Skip installing it or pip uninstall if it causes problems. + ## Building the Vintel Standalone Package - The standalone is created using pyinstaller. All media files and the .spec-file with the configuration for pyinstaller are included in the source repo. Pyinstaller can be found here: https://github.com/pyinstaller/pyinstaller/wiki. diff --git a/src/vintel.py b/src/vintel.py index a72e487..5b8572a 100644 --- a/src/vintel.py +++ b/src/vintel.py @@ -56,13 +56,20 @@ class Application(QApplication): def __init__(self, args): super(Application, self).__init__(args) + self.setWindowIcon(QtGui.QIcon('icon.ico')) + # windows silliness to set taskbar icon + if sys.platform.startswith("win32"): + import ctypes + myappid = u'eve.vintel.' + version.VERSION + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) + # Set up paths chatLogDirectory = "" if len(sys.argv) > 1: chatLogDirectory = sys.argv[1] if not os.path.exists(chatLogDirectory): - if sys.platform.startswith("darwin"): + if sys.platform.startswith("darwin") or sys.platform.startswith("cygwin"): chatLogDirectory = os.path.join(os.path.expanduser("~"), "Documents", "EVE", "logs", "Chatlogs") if not os.path.exists(chatLogDirectory): chatLogDirectory = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "Eve Online", @@ -71,10 +78,12 @@ def __init__(self, args): chatLogDirectory = os.path.join(os.path.expanduser("~"), "EVE", "logs", "Chatlogs") elif sys.platform.startswith("win32") or sys.platform.startswith("cygwin"): import ctypes.wintypes + from win32com.shell import shellcon buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) - ctypes.windll.shell32.SHGetFolderPathW(0, 5, 0, 0, buf) - documentsPath = buf.value - chatLogDirectory = os.path.join(documentsPath, "EVE", "logs", "Chatlogs") + hResult = ctypes.windll.shell32.SHGetFolderPathW(0, shellcon.CSIDL_PERSONAL, 0, 0, buf) + if hResult == 0: + documentsPath = buf.value + chatLogDirectory = os.path.join(documentsPath, "EVE", "logs", "Chatlogs") if not os.path.exists(chatLogDirectory): # None of the paths for logs exist, bailing out QMessageBox.critical(None, "No path to Logs", "No logs found at: " + chatLogDirectory, "Quit") From 58499f1d2418e84e8dc036ad5c468b9245d8aa5b Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Tue, 3 Jan 2017 00:51:36 -0500 Subject: [PATCH 13/36] Update menus and region selector. --- src/vi/dotlan.py | 14 ++++++--- src/vi/regions.py | 65 +++++++++++++++++++++++++++++++++++++++++ src/vi/ui/MainWindow.ui | 62 +++++++++++++++------------------------ src/vi/ui/viui.py | 29 +++++++++++------- 4 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 src/vi/regions.py diff --git a/src/vi/dotlan.py b/src/vi/dotlan.py index 9563350..0fa356e 100644 --- a/src/vi/dotlan.py +++ b/src/vi/dotlan.py @@ -37,7 +37,6 @@ "88aa00" "FFE4E1", "008080", "00BFFF", "4682B4", "00FF7F", "7FFF00", "ff6600", "CD5C5C", "FFD700", "66CDAA", "AFEEEE", "5F9EA0", "FFDEAD", "696969", "2F4F4F") - class DotlanException(Exception): def __init__(self, *args, **kwargs): Exception.__init__(self, *args, **kwargs) @@ -48,8 +47,6 @@ class Map(object): The map including all information from dotlan """ - DOTLAN_BASIC_URL = u"http://evemaps.dotlan.net/svg/{0}.svg" - @property def svg(self): # Re-render all systems @@ -218,7 +215,7 @@ def _connectNeighbours(self): startSystem.addNeighbour(stopSystem) def _getSvgFromDotlan(self, region): - url = self.DOTLAN_BASIC_URL.format(region) + url = dotlan_url(region) content = requests.get(url).text return content @@ -511,6 +508,15 @@ def update(self): self.secondLine.string = string +def dotlan_url(region): + """ Generate a DOTLAN url from the specified region name (which will be normalized to a DOTLAN region name) + """ + url = u"http://evemaps.dotlan.net/svg/{0}.svg".format(convertRegionName(region)) + jb_id = Cache().getConfigValue("dotlan_jb_id") + if jb_id: + url = url + u"?path=B:{0}".format(jb_id) + return url + def convertRegionName(name): """ Converts a (system)name to the format that dotlan uses diff --git a/src/vi/regions.py b/src/vi/regions.py new file mode 100644 index 0000000..5663e0c --- /dev/null +++ b/src/vi/regions.py @@ -0,0 +1,65 @@ +REGIONS = [ + "Aridia", + "Black Rise", + "Branch", + "Cache", + "Catch", + "Cloud Ring", + "Cobalt Edge", + "Curse", + "Deklein", + "Delve", + "Derelik", + "Detorid", + "Devoid", + "Domain", + "Esoteria", + "Essence", + "Etherium Reach", + "Everyshore", + "Fade", + "Feythabolis", + "Fountain", + "Geminate", + "Genesis", + "Great Wildlands", + "Heimatar", + "Immensea", + "Impass", + "Insmother", + "Kador", + "Khanid", + "Kor-Azor", + "Lonetrek", + "Malpais", + "Metropolis", + "Molden Heath", + "Oasa", + "Omist", + "Outer Passage", + "Outer Ring", + "Paragon Soul", + "Period Basis", + "Perrigen Falls", + "Placid", + "Providence", + "Pure Blind", + "Querious", + "Scalding Pass", + "Sinq Laison", + "Solitude", + "Stain", + "Syndicate", + "Tash-Murkon", + "Tenal", + "Tenerifis", + "The Bleak Lands", + "The Citadel", + "The Forge", + "The Kalevala Expanse", + "The Spire", + "Tribute", + "Vale of the Silent", + "Venal", + "Verge Vendor", + "Wicked Creek" ] diff --git a/src/vi/ui/MainWindow.ui b/src/vi/ui/MainWindow.ui index d3a9ccd..d115a54 100644 --- a/src/vi/ui/MainWindow.ui +++ b/src/vi/ui/MainWindow.ui @@ -37,16 +37,7 @@ - - 0 - - - 0 - - - 0 - - + 0 @@ -72,16 +63,7 @@ - - 0 - - - 0 - - - 0 - - + 0 @@ -255,19 +237,10 @@ All Intel (past 20 minutes) - - 5 - - - 5 - - + 5 - - 5 - - + 5 @@ -346,10 +319,10 @@ - Kos + File - - + + @@ -360,7 +333,6 @@ Chat - @@ -376,15 +348,21 @@ Region + + + Other Region... + + - - + + + @@ -399,10 +377,18 @@ + + + K.O.S. + + + + + @@ -525,7 +511,7 @@ true - Other Region... + Custom Region... diff --git a/src/vi/ui/viui.py b/src/vi/ui/viui.py index 0cbca6d..b62239b 100644 --- a/src/vi/ui/viui.py +++ b/src/vi/ui/viui.py @@ -41,6 +41,7 @@ from vi.threads import AvatarFindThread, KOSCheckerThread, MapStatisticsThread from vi.ui.systemtray import TrayContextMenu from vi.chatparser import ChatParser +from vi.regions import REGIONS from PyQt4.QtGui import QAction from PyQt4.QtGui import QMessageBox @@ -134,6 +135,7 @@ def __init__(self, pathToLogs, trayIcon, backGroundColor): self.wireUpUIConnections() self.recallCachedSettings() self.setupThreads() + self.updateRegionMenu() self.setupMap(True) @@ -152,6 +154,17 @@ def recallCachedSettings(self): # todo: add a button to delete the cache / DB self.trayIcon.showMessage("Settings error", "Something went wrong loading saved state:\n {0}".format(str(e)), 1) + def updateRegionMenu(self): + for region in REGIONS: + menuItem= self.otherRegionSubmenu.addAction(region) + receiver = lambda region=region: self.onRegionSelect(region) + self.connect(menuItem, SIGNAL('triggered()'), receiver) + self.otherRegionSubmenu.addAction(menuItem) + + def onRegionSelect(self, region): + logging.critical("NEW REGION: [%s]" % (region)) + Cache().saveConfigValue("region_name", region) + self.handleRegionChosen() def wireUpUIConnections(self): # Wire up general UI connections @@ -280,8 +293,6 @@ def mapContextMenuEvent(event): self.providenceRegionAction.setChecked(True) elif regionName.startswith("Querious"): self.queriousRegionAction.setChecked(True) - else: - self.chooseRegionAction.setChecked(True) self.jumpbridgesButton.setChecked(False) self.statisticsButton.setChecked(False) @@ -624,7 +635,6 @@ def handleRegionMenuItemSelected(self, menuAction=None): self.queriousRegionAction.setChecked(False) self.providenceCatchRegionAction.setChecked(False) self.providenceCatchCompactRegionAction.setChecked(False) - self.chooseRegionAction.setChecked(False) if menuAction: menuAction.setChecked(True) regionName = six.text_type(menuAction.property("regionName").toString()) @@ -632,16 +642,13 @@ def handleRegionMenuItemSelected(self, menuAction=None): Cache().saveConfigValue("region_name", regionName) self.setupMap() + def handleRegionChosen(self): + self.handleRegionMenuItemSelected(None) + self.setupMap() def showRegionChooser(self): - def handleRegionChosen(): - self.handleRegionMenuItemSelected(None) - self.chooseRegionAction.setChecked(True) - self.setupMap() - - self.chooseRegionAction.setChecked(False) chooser = RegionChooser(self) - self.connect(chooser, SIGNAL("new_region_chosen"), handleRegionChosen) + self.connect(chooser, SIGNAL("new_region_chosen"), self.handleRegionChosen) chooser.show() @@ -854,7 +861,7 @@ def saveClicked(self): self.regionNameField.setPlainText(text) correct = False try: - url = dotlan.Map.DOTLAN_BASIC_URL.format(text) + url = dotlan.dotlan_url(text) content = requests.get(url).text if u"not found" in content: correct = False From 1e8c193b5ccc873e6922d795b845ebebd98238f4 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Mon, 9 Jan 2017 01:37:43 -0500 Subject: [PATCH 14/36] * Dynamically build Region menus based on quick setup preferences. Removed check indicator from region menus as a side effect. * Move jump bridge and chatroom settings into tabbed File > Settings. * Add keyboard shortcuts ctrl + / ctrl - / ctrl 0 for zooming, j to toggle jump bridges. Ctrl mousewheel zooms (except when focus is in map pane, doh) * Add quick setup to paste corp / alliance settings all at once. Make more things configuration settings. * Add flush cache to File menu. --- quick_config_cva | 20 ++ src/vi/cache/cache.py | 10 +- src/vi/dotlan.py | 9 + src/vi/koschecker.py | 8 +- src/vi/ui/ChatroomsChooser.ui | 66 ------ src/vi/ui/JumpbridgeChooser.ui | 106 --------- src/vi/ui/MainWindow.ui | 75 +------ src/vi/ui/SettingsTabs.ui | 389 +++++++++++++++++++++++++++++++++ src/vi/ui/viui.py | 222 ++++++++++++------- 9 files changed, 589 insertions(+), 316 deletions(-) create mode 100644 quick_config_cva delete mode 100644 src/vi/ui/ChatroomsChooser.ui delete mode 100644 src/vi/ui/JumpbridgeChooser.ui create mode 100644 src/vi/ui/SettingsTabs.ui diff --git a/quick_config_cva b/quick_config_cva new file mode 100644 index 0000000..e37c46f --- /dev/null +++ b/quick_config_cva @@ -0,0 +1,20 @@ +{ + "dotlan_jb_id": "", + "jumpbridge_url": "", + "channels": [ + "TheCitadel", + "North Provi Intel", + "North Catch Intel", + "North Querious Intel" + ], + "kos_url": "http://kos.cva-eve.org/api/", + "region_name": "Catch", + "quick_regions": [ + {"label": "Catch"}, + {"label": "Providence"}, + {"label": "Querious"}, + {"label": "---"}, + {"label": "Provi / Catch", "region": "providencecatch"}, + {"label": "Provi / Catch (compact)", "region": "Providence-catch"} + ] +} diff --git a/src/vi/cache/cache.py b/src/vi/cache/cache.py index 0a0a8ec..4426e9a 100644 --- a/src/vi/cache/cache.py +++ b/src/vi/cache/cache.py @@ -76,7 +76,7 @@ def checkVersion(self): updateDatabase(version, self.con) def putIntoCache(self, key, value, maxAge=60 * 60 * 24 * 3): - """ Putting something in the cache maxAge is maximum age in seconds + """ Putting something in the cache maxAge is maximum age in seconds, default is 3 days """ with Cache.SQLITE_WRITE_LOCK: query = "DELETE FROM cache WHERE key = ?" @@ -117,6 +117,14 @@ def deleteFromCache(self, key): self.con.execute(query, (key,)) self.con.commit() + def flush(self): + """ Flush non config items from cache + """ + with Cache.SQLITE_WRITE_LOCK: + query = "DELETE FROM cache WHERE maxAge < ?" + self.con.execute(query, (sys.maxsize,)) + self.con.commit() + def putPlayerName(self, name, status): """ Putting a playername into the cache """ diff --git a/src/vi/dotlan.py b/src/vi/dotlan.py index 0fa356e..266dd5f 100644 --- a/src/vi/dotlan.py +++ b/src/vi/dotlan.py @@ -93,6 +93,7 @@ def __init__(self, region, svgFile=None): self.soup = BeautifulSoup(svg, 'html.parser') self.systems = self._extractSystemsFromSoup(self.soup) self._cleanSoup(self.soup) + self.setJumpbridgesVisibility(False) self.systemsById = {} for system in self.systems.values(): self.systemsById[system.systemId] = system @@ -287,7 +288,15 @@ def changeStatisticsVisibility(self): def changeJumpbridgesVisibility(self): newStatus = False if self._jumpMapsVisible else True + return self.setJumpbridgesVisibility(newStatus) + + def setJumpbridgesVisibility(self, newStatus): value = "visible" if newStatus else "hidden" + try: + for jb in self.soup.select('path.jb'): + jb['visibility'] = value + except: + pass for line in self.soup.select(".jumpbridge"): line["visibility"] = value self._jumpMapsVisible = newStatus diff --git a/src/vi/koschecker.py b/src/vi/koschecker.py index 0296ecd..52ef797 100644 --- a/src/vi/koschecker.py +++ b/src/vi/koschecker.py @@ -36,8 +36,12 @@ def check(parts): namesAsIds = {} names = [name.strip() for name in parts] + kos_url = Cache().getConfigValue("kos_url") + if not kos_url: + kos_url = CVA_KOS_URL + try: - kosData = requests.get(CVA_KOS_URL, params = {'c': 'json', 'type': 'multi', 'q': ','.join(names)}).json() + kosData = requests.get(kos_url, params = {'c': 'json', 'type': 'multi', 'q': ','.join(names)}).json() except RequestException as e: kosData = None logging.error("Error on pilot KOS check request %s", str(e)) @@ -95,7 +99,7 @@ def check(parts): for corp in corpsToCheck: try: - kosData = requests.get(CVA_KOS_URL, params = { 'c': 'json', 'type': 'unit', 'q': corp }).json() + kosData = requests.get(kos_url, params = { 'c': 'json', 'type': 'unit', 'q': corp }).json() except RequestException as e: logging.error("Error on corp KOS check request: %s", str(e)) diff --git a/src/vi/ui/ChatroomsChooser.ui b/src/vi/ui/ChatroomsChooser.ui deleted file mode 100644 index 4e4fb40..0000000 --- a/src/vi/ui/ChatroomsChooser.ui +++ /dev/null @@ -1,66 +0,0 @@ - - - Dialog - - - - 0 - 0 - 556 - 197 - - - - Chatrooms - - - - - - - - Enter the chatrooms to watch into the following field. Separate them by comma. - - - Qt::PlainText - - - true - - - - - - - - - - - - Restore Defaults - - - - - - - Cancel - - - - - - - Save - - - - - - - - - - - - diff --git a/src/vi/ui/JumpbridgeChooser.ui b/src/vi/ui/JumpbridgeChooser.ui deleted file mode 100644 index f6d233c..0000000 --- a/src/vi/ui/JumpbridgeChooser.ui +++ /dev/null @@ -1,106 +0,0 @@ - - - Dialog - - - - 0 - 0 - 696 - 394 - - - - Jumpbridge Data - - - - - - - - - - - Path to the Data - - - - - - For example: http://example.org/jumpbridge_data.txt - - - - - - - - - Please enter the URL to the Jumpbridge data: - - - - - - - - - - - - - - - - - - - - - Format of the jumpbridge data: - - - - - - QPlainTextEdit::NoWrap - - - true - - - - - - - - - - - - - - - - - - Cancel - - - - - - - Save - - - - - - - - - - - diff --git a/src/vi/ui/MainWindow.ui b/src/vi/ui/MainWindow.ui index d115a54..d7296e8 100644 --- a/src/vi/ui/MainWindow.ui +++ b/src/vi/ui/MainWindow.ui @@ -314,15 +314,15 @@ 0 0 936 - 22 + 21 File - - + + @@ -353,12 +353,6 @@ Other Region... - - - - - - @@ -507,9 +501,6 @@ - - true - Custom Region... @@ -519,9 +510,9 @@ Sound Setup... - + - Jumpbridge Data... + Flush Cache @@ -570,59 +561,9 @@ Transparency - - - true - - - Providence - - - providence - - - - - true - - - Catch - - - catch - - - - - true - - - Provi / Catch - - - providencecatch - - - - - true - + - Provi / Catch (compact) - - - Providence-catch - - - - - true - - - Querious - - - querious + Settings @@ -630,7 +571,7 @@ QWebView QWidget -
QtWebKitWidgets/QWebView
+
QtWebKit/QWebView
PanningWebView diff --git a/src/vi/ui/SettingsTabs.ui b/src/vi/ui/SettingsTabs.ui new file mode 100644 index 0000000..7b4be63 --- /dev/null +++ b/src/vi/ui/SettingsTabs.ui @@ -0,0 +1,389 @@ + + + Dialog + + + + 0 + 0 + 640 + 480 + + + + Settings + + + + + 0 + 0 + 640 + 480 + + + + + 0 + 0 + + + + TabWidget + + + 1 + + + + + 0 + 0 + + + + Quick Setup + + + + + 10 + 10 + 611 + 431 + + + + + + + Paste the quick setup text here that was provided by your alliance, corporation, ... + + + Qt::PlainText + + + true + + + + + + + + + + + + Undo + + + + + + + Apply + + + + + + + + + + + Jumpbridges + + + + + 10 + 0 + 611 + 453 + + + + + QLayout::SetMaximumSize + + + + + + + + 0 + 0 + + + + Setup jumpbridges to be displayed on the map. You can setup a url with your own data or include a DOTLAN jumpbridge id. + + + + + + + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + false + + + Please enter the URL to the Jumpbridge data (i.e. http://example.org/jumpbridge_data.txt): + + + false + + + false + + + + + 10 + 24 + 571 + 221 + + + + + + + + + + true + + + + 0 + 0 + + + + + 0 + 100 + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + QPlainTextEdit::WidgetWidth + + + true + + + How to format the data for the jumpbridges: + +Put all the jumpbridges in one single textfile, wher a line in the file represents a jumpbridge. + +The line contains the first system, the connection between the systems and the second system. Between the three parts hast to be a space. + +The connection is a line (minus) in the middle and possible arrows (sign for greater or lower) at the ends. If there is such a sign, an arrow will be drawn on the map. + +Examples: + +System1 <-> System2 +System3 <- System4 +System5 - System6 + +The first line will draw a jumpbridge between System1 and System2 with an arrow on both sides. +The second line will draw a jumpbridge between System3 and System4, but only the side to System3 will have an arrow pointing on the system (could be helpfull for "oneway"-jumpbridges). +The last line draws a line between System5 and System 6 and no arrows at the endpoints. + +Write the systemnames as they are written in the maps. + +Don't forget the space between the System names and the jumpbridge! + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + + 0 + 48 + + + + Use Jumpbridge Info from Dotlan + + + false + + + false + + + + + + QLayout::SetDefaultConstraint + + + 6 + + + + + + 0 + 5 + + + + Dotlan Jumpbridge Id + + + + + + + + 0 + 0 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + Undo + + + + + + + Apply + + + + + + + + + + + Chat Channels + + + + + 10 + 10 + 611 + 431 + + + + + + + Enter the channels to watch below. Separate them by comma. + + + Qt::PlainText + + + true + + + + + + + + + + + + Restore Defaults + + + + + + + Undo + + + + + + + Apply + + + + + + + + + + + + + diff --git a/src/vi/ui/viui.py b/src/vi/ui/viui.py index b62239b..cfa063f 100644 --- a/src/vi/ui/viui.py +++ b/src/vi/ui/viui.py @@ -21,6 +21,8 @@ import sys import time import six +import json +import traceback import requests import webbrowser @@ -95,12 +97,12 @@ def __init__(self, pathToLogs, trayIcon, backGroundColor): QMessageBox.warning(None, "Known Characters not Found", diagText, "Ok") # Set up user's intel rooms - roomnames = self.cache.getConfigValue("room_names") + roomnames = self.cache.getConfigValue("channel_names") if roomnames: roomnames = roomnames.split(",") else: roomnames = (u"TheCitadel", u"North Provi Intel", u"North Catch Intel", "North Querious Intel") - self.cache.saveConfigValue("room_names", u",".join(roomnames)) + self.cache.saveConfigValue("channel_names", u",".join(roomnames)) self.roomnames = roomnames # Disable the sound UI if sound is not available @@ -136,6 +138,7 @@ def __init__(self, pathToLogs, trayIcon, backGroundColor): self.recallCachedSettings() self.setupThreads() self.updateRegionMenu() + self.updateOtherRegionMenu() self.setupMap(True) @@ -145,6 +148,27 @@ def paintEvent(self, event): painter = QPainter(self) self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) + def wheelEvent(self,event): + if event.modifiers() & QtCore.Qt.ControlModifier: + steps = event.delta() // 120 + vector = steps and steps // abs(steps) # 0, 1, or -1 + for step in range(1, abs(steps) + 1): + self.mapView.setZoomFactor(self.mapView.zoomFactor() + vector * 0.1) + + def keyPressEvent(self, event): + key = event.key() + if key == QtCore.Qt.Key_0: + if event.modifiers() & QtCore.Qt.ControlModifier: + self.mapView.setZoomFactor(1.0) + elif key == QtCore.Qt.Key_Plus or key == QtCore.Qt.Key_Equal: + if event.modifiers() & QtCore.Qt.ControlModifier: + self.mapView.setZoomFactor(self.mapView.zoomFactor() + 0.1) + elif key == QtCore.Qt.Key_Minus or key == QtCore.Qt.Key_Underscore: + if event.modifiers() & QtCore.Qt.ControlModifier: + self.mapView.setZoomFactor(self.mapView.zoomFactor() - 0.1) + elif key == QtCore.Qt.Key_J: + self.changeJumpbridgesVisibility() + def recallCachedSettings(self): try: @@ -154,13 +178,38 @@ def recallCachedSettings(self): # todo: add a button to delete the cache / DB self.trayIcon.showMessage("Settings error", "Something went wrong loading saved state:\n {0}".format(str(e)), 1) - def updateRegionMenu(self): + def updateOtherRegionMenu(self): for region in REGIONS: - menuItem= self.otherRegionSubmenu.addAction(region) + menuItem = self.otherRegionSubmenu.addAction(region) receiver = lambda region=region: self.onRegionSelect(region) self.connect(menuItem, SIGNAL('triggered()'), receiver) self.otherRegionSubmenu.addAction(menuItem) + def updateRegionMenu(self): + quick_regions = self.cache.getConfigValue("quick_regions") + if quick_regions: + j = json.loads(quick_regions) + for old_item in self.menuRegion.actions(): + if "Other Region..." == old_item.text(): + orm = self.menuRegion.insertSeparator(old_item) + break + self.menuRegion.removeAction(old_item) + for r in j: + if not 'label' in r: + self.trayIcon.showMessage("Quick regions error", "No label field in :\n {0}".format(str(r)), 1) + label = r['label'] + if label.startswith('-'): + menuItem = self.menuRegion.insertSeparator(orm) + continue + if 'region' in r: + region = r['region'] + else: + region = label + menuItem = self.menuRegion.addAction(label) + receiver = lambda region=region: self.onRegionSelect(region) + self.connect(menuItem, SIGNAL('triggered()'), receiver) + self.menuRegion.insertAction(orm, menuItem) + def onRegionSelect(self, region): logging.critical("NEW REGION: [%s]" % (region)) Cache().saveConfigValue("region_name", region) @@ -180,12 +229,6 @@ def wireUpUIConnections(self): self.connect(self.infoAction, SIGNAL("triggered()"), self.showInfo) self.connect(self.showChatAvatarsAction, SIGNAL("triggered()"), self.changeShowAvatars) self.connect(self.alwaysOnTopAction, SIGNAL("triggered()"), self.changeAlwaysOnTop) - self.connect(self.chooseChatRoomsAction, SIGNAL("triggered()"), self.showChatroomChooser) - self.connect(self.catchRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.catchRegionAction)) - self.connect(self.providenceRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.providenceRegionAction)) - self.connect(self.queriousRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.queriousRegionAction)) - self.connect(self.providenceCatchRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.providenceCatchRegionAction)) - self.connect(self.providenceCatchCompactRegionAction, SIGNAL("triggered()"), lambda : self.handleRegionMenuItemSelected(self.providenceCatchCompactRegionAction)) self.connect(self.chooseRegionAction, SIGNAL("triggered()"), self.showRegionChooser) self.connect(self.showChatAction, SIGNAL("triggered()"), self.changeChatVisibility) self.connect(self.soundSetupAction, SIGNAL("triggered()"), self.showSoundSetup) @@ -197,7 +240,8 @@ def wireUpUIConnections(self): self.connect(self.frameButton, SIGNAL("clicked()"), self.changeFrameless) self.connect(self.quitAction, SIGNAL("triggered()"), self.close) self.connect(self.trayIcon, SIGNAL("quit"), self.close) - self.connect(self.jumpbridgeDataAction, SIGNAL("triggered()"), self.showJumbridgeChooser) + self.connect(self.settingsAction, SIGNAL("triggered()"), self.showSettings) + self.connect(self.flushCacheAction, SIGNAL("triggered()"), self.flushCache) self.mapView.page().scrollRequested.connect(self.mapPositionChanged) @@ -282,17 +326,6 @@ def mapContextMenuEvent(event): # Clicking links self.mapView.connect(self.mapView, SIGNAL("linkClicked(const QUrl&)"), self.mapLinkClicked) - # Also set up our app menus - if not regionName: - self.providenceCatchRegionAction.setChecked(True) - elif regionName.startswith("Providencecatch"): - self.providenceCatchRegionAction.setChecked(True) - elif regionName.startswith("Catch"): - self.catchRegionAction.setChecked(True) - elif regionName.startswith("Providence"): - self.providenceRegionAction.setChecked(True) - elif regionName.startswith("Querious"): - self.queriousRegionAction.setChecked(True) self.jumpbridgesButton.setChecked(False) self.statisticsButton.setChecked(False) @@ -593,24 +626,21 @@ def mapPositionChanged(self, dx, dy, rectToScroll): self.mapPositionsDict[regionName] = (scrollPosition.x(), scrollPosition.y()) - def showChatroomChooser(self): - chooser = ChatroomsChooser(self) + def showSettings(self): + chooser = Settings(self) chooser.connect(chooser, SIGNAL("rooms_changed"), self.changedRoomnames) - chooser.show() - - - def showJumbridgeChooser(self): - url = self.cache.getConfigValue("jumpbridge_url") - chooser = JumpbridgeChooser(self, url) chooser.connect(chooser, SIGNAL("set_jumpbridge_url"), self.setJumpbridges) chooser.show() + def flushCache(self): + self.cache.flush() def setSoundVolume(self, value): SoundManager().setSoundVolume(value) def setJumpbridges(self, url): + if url is None: url = "" try: @@ -630,20 +660,13 @@ def setJumpbridges(self, url): def handleRegionMenuItemSelected(self, menuAction=None): - self.catchRegionAction.setChecked(False) - self.providenceRegionAction.setChecked(False) - self.queriousRegionAction.setChecked(False) - self.providenceCatchRegionAction.setChecked(False) - self.providenceCatchCompactRegionAction.setChecked(False) if menuAction: - menuAction.setChecked(True) regionName = six.text_type(menuAction.property("regionName").toString()) regionName = dotlan.convertRegionName(regionName) Cache().saveConfigValue("region_name", regionName) self.setupMap() def handleRegionChosen(self): - self.handleRegionMenuItemSelected(None) self.setupMap() def showRegionChooser(self): @@ -714,7 +737,7 @@ def showKosResult(self, state, text, requestType, hasKos): def changedRoomnames(self, newRoomnames): - self.cache.saveConfigValue("room_names", u",".join(newRoomnames)) + self.cache.saveConfigValue("channel_names", u",".join(newRoomnames)) self.chatparser.rooms = newRoomnames @@ -817,31 +840,6 @@ def logFileChanged(self, path): self.setMapContent(self.dotlan.svg) -class ChatroomsChooser(QtGui.QDialog): - def __init__(self, parent): - QtGui.QDialog.__init__(self, parent) - uic.loadUi(resourcePath("vi/ui/ChatroomsChooser.ui"), self) - self.connect(self.defaultButton, SIGNAL("clicked()"), self.setDefaults) - self.connect(self.cancelButton, SIGNAL("clicked()"), self.accept) - self.connect(self.saveButton, SIGNAL("clicked()"), self.saveClicked) - cache = Cache() - roomnames = cache.getConfigValue("room_names") - if not roomnames: - roomnames = u"TheCitadel,North Provi Intel,North Catch Intel,North Querious Intel" - self.roomnamesField.setPlainText(roomnames) - - - def saveClicked(self): - text = six.text_type(self.roomnamesField.toPlainText()) - rooms = [six.text_type(name.strip()) for name in text.split(",")] - self.accept() - self.emit(SIGNAL("rooms_changed"), rooms) - - - def setDefaults(self): - self.roomnamesField.setPlainText(u"TheCitadel,North Provi Intel,North Catch Intel,North Querious Intel") - - class RegionChooser(QtGui.QDialog): def __init__(self, parent): QtGui.QDialog.__init__(self, parent) @@ -1015,24 +1013,100 @@ def changeFontSize(self, newSize): self.textLabel.setFont(font) -class JumpbridgeChooser(QtGui.QDialog): - def __init__(self, parent, url): +class Settings(QtGui.QDialog): + def __init__(self, parent): QtGui.QDialog.__init__(self, parent) - uic.loadUi(resourcePath("vi/ui/JumpbridgeChooser.ui"), self) - self.connect(self.saveButton, SIGNAL("clicked()"), self.savePath) - self.connect(self.cancelButton, SIGNAL("clicked()"), self.accept) - self.urlField.setText(url) + uic.loadUi(resourcePath("vi/ui/SettingsTabs.ui"), self) + self.cache = Cache() + self.parent = parent + self.tabs.setCurrentIndex(0) # load displaying first tab, regardless of which page was last open in designer + + # Chatrooms + self.connect(self.chatDefaultButton, SIGNAL("clicked()"), self.setChatToDefaults) + self.connect(self.chatCancelButton, SIGNAL("clicked()"), self.resetChatSettings) + self.connect(self.chatSaveButton, SIGNAL("clicked()"), self.saveChatSettings) + self.resetChatSettings() + + # JBS + self.connect(self.jbSaveButton, SIGNAL("clicked()"), self.saveJbs) + self.connect(self.jbCancelButton, SIGNAL("clicked()"), self.resetJbs) + self.resetJbs() # loading format explanation from textfile - with open(resourcePath("docs/jumpbridgeformat.txt")) as f: - self.formatInfoField.setPlainText(f.read()) + # with open(resourcePath("docs/jumpbridgeformat.txt")) as f: + # self.formatInfoField.setPlainText(f.read()) + # Quick Setup + self.connect(self.quickSettingsSaveButton, SIGNAL("clicked()"), self.saveQuickSettings) + self.connect(self.quickSettingsCancelButton, SIGNAL("clicked()"), self.resetQuickSettings) + self.resetQuickSettings() - def savePath(self): + def resetJbs(self): + self.jbUrlField.setText(self.cache.getConfigValue("jumpbridge_url")) + self.jbIdField.setText(self.cache.getConfigValue("dotlan_jb_id")) + + def saveJbs(self): try: - url = six.text_type(self.urlField.text()) + url = six.text_type(self.jbUrlField.text()) if url != "": requests.get(url).text + self.cache.saveConfigValue("dotlan_jb_id", six.text_type(self.jbIdField.text())) self.emit(SIGNAL("set_jumpbridge_url"), url) - self.accept() except Exception as e: QMessageBox.critical(None, "Finding Jumpbridgedata failed", "Error: {0}".format(six.text_type(e)), "OK") + + def resetChatSettings(self): + roomnames = self.cache.getConfigValue("channel_names") + if not roomnames: + self.setChatToDefaults() + else: + self.roomnamesField.setPlainText(roomnames) + + def saveChatSettings(self): + text = six.text_type(self.roomnamesField.toPlainText()) + rooms = [six.text_type(name.strip()) for name in text.split(",")] + self.emit(SIGNAL("rooms_changed"), rooms) + + def setChatToDefaults(self): + roomnames = self.cache.getConfigValue("default_room_names") + if not roomnames: + self.roomnamesField.setPlainText(u"TheCitadel,North Provi Intel,North Catch Intel,North Querious Intel") + else: + self.roomnamesField.setPlainText(roomnames) + + def resetQuickSettings(self): + self.quickSettingsField.setPlainText("") + + def saveQuickSettings(self): + try: + d = json.loads(six.text_type(self.quickSettingsField.toPlainText())) + if not dict: + QMessageBox.critical(None, "Could not parse input field", "Error: {0}".format(six.text_type(d)), "OK") + return + + if 'channels' in d: + self.cache.saveConfigValue('default_room_names', ",".join(d['channels'])) + self.emit(SIGNAL("rooms_changed"), d['channels']) + + if 'dotlan_jb_id' in d: + self.cache.saveConfigValue("dotlan_jb_id", d['dotlan_jb_id']) + + if 'jumpbridge_url' in d: + self.emit(SIGNAL("set_jumpbridge_url"), d['jumpbridge_url']) + + if 'kos_url' in d: + self.cache.saveConfigValue("kos_url", d['kos_url']) + + if 'region_name' in d: + self.cache.saveConfigValue("region_name", d['region_name']) + self.parent.handleRegionChosen() + + if 'quick_regions' in d: + self.cache.saveConfigValue("quick_regions", json.dumps(d['quick_regions'])) + self.parent.updateRegionMenu() + + self.resetChatSettings() + self.resetJbs() + + except Exception as e: + traceback.print_exc() + QMessageBox.critical(None, "Saving quick settings failed", "Error: {0}".format(six.text_type(e)), "OK") From 5841fd559b3db94eff6e05acd7e72659a06205eb Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sat, 14 Jan 2017 18:16:58 -0500 Subject: [PATCH 15/36] fix broken avatar cache --- src/vi/cache/cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vi/cache/cache.py b/src/vi/cache/cache.py index 4426e9a..68961c7 100644 --- a/src/vi/cache/cache.py +++ b/src/vi/cache/cache.py @@ -165,8 +165,8 @@ def getAvatar(self, name): if len(founds) == 0: return None else: - # dats is buffer, we convert it back to str - data = from_blob(founds[0][0]) + # data is buffer, we convert it back to str + data = from_blob(founds) return data def removeAvatar(self, name): From ed06e6ae32008e0f9f73067ecc48744299e1b316 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 01:31:11 -0500 Subject: [PATCH 16/36] 1. Refactor for reload of history from file on startup and on changing region a. Messages now only store a reference to the system name, it is used to find the system on map in viui.py b. Add list of all systems with regions as systems.py c. Allow mark system / click hyperlink from chat to switch region if system is not in current map. Will prompt first. 2. Add some missing ship types to evegate.py 3. Prep system list for some common abbreviation & misspellings when we create it rather than at search time 4. Set intel lifetime to 60 minutes, update chate history title to reflect live value. Expire "knownMessages" when we prune chat history. TODO: make a config setting. 5. Catching some weird instances of set avatar pixmap where the label object has already been destroyed. I don't see any real issues in the app. 6. Cleanup parser_functions.py some. Better ship matching (start & end of ship name for replace). Changed bad char handling in some places, not sure what value the bad char handler adds - it tends to make test string and original string differ so it's harder to find the replacement. TODO: 1. startup and region switch may be a little slow. may need a spinner. 2. make a config setting for intel time to live --- src/vi/chatparser/chatparser.py | 39 +- src/vi/chatparser/parser_functions.py | 91 +- src/vi/dotlan.py | 34 +- src/vi/evegate.py | 9 +- src/vi/regions.py | 3 +- src/vi/states.py | 2 +- src/vi/systems.py | 5465 +++++++++++++++++++++++++ src/vi/threads.py | 5 +- src/vi/ui/viui.py | 104 +- src/vintel.spec | 4 +- 10 files changed, 5653 insertions(+), 103 deletions(-) create mode 100644 src/vi/systems.py diff --git a/src/vi/chatparser/chatparser.py b/src/vi/chatparser/chatparser.py index 8d7ac21..611b5d8 100644 --- a/src/vi/chatparser/chatparser.py +++ b/src/vi/chatparser/chatparser.py @@ -20,15 +20,15 @@ import datetime import os import time +import logging import six if six.PY2: from io import open from bs4 import BeautifulSoup -from vi import states +from vi import states, evegate from PyQt4.QtGui import QMessageBox - from .parser_functions import parseStatus from .parser_functions import parseUrls, parseShips, parseSystems @@ -40,12 +40,16 @@ class ChatParser(object): """ ChatParser will analyze every new line that was found inside the Chatlogs. """ - def __init__(self, path, rooms, systems): + def HEADER_SIZE(self): return 13 + def REPLAY_SIZE(self): return 500 # retreive up to 500 lines from a logfile when starting up + + def __init__(self, path, rooms, allSystems, messageExpirySecs): """ path = the path with the logs rooms = the rooms to parse""" + self.messageExpirySecs = messageExpirySecs self.path = path # the path with the chatlog self.rooms = rooms # the rooms to watch (excl. local) - self.systems = systems # the known systems as dict name: system + self.allSystems = allSystems # the known systems as dict name: system self.fileData = {} # informations about the files in the directory self.knownMessages = [] # message we allready analyzed self.locations = {} # informations about the location of a char @@ -103,6 +107,9 @@ def _lineToMessage(self, line, roomname): timestamp = datetime.datetime.strptime(timeStr, "%Y.%m.%d %H:%M:%S") except ValueError: return None + if (evegate.currentEveTime() - timestamp).total_seconds() > self.messageExpirySecs: + # logging.debug('ignoring message from ' + timeStr + " || " + str((evegate.currentEveTime() - timestamp).total_seconds()) ) + return None # finding the username of the poster userEnds = line.find(">") username = line[timeEnds + 1:userEnds].strip() @@ -136,7 +143,7 @@ def _lineToMessage(self, line, roomname): continue while parseUrls(rtext): continue - while parseSystems(self.systems, rtext, systems): + while parseSystems(self.allSystems, rtext, systems): continue parsedStatus = parseStatus(rtext) status = parsedStatus if parsedStatus is not None else states.ALARM @@ -154,9 +161,6 @@ def _lineToMessage(self, line, roomname): message.message = six.text_type(rtext) message.status = status self.knownMessages.append(message) - if systems: - for system in systems: - system.messages.append(message) return message def _parseLocal(self, path, line): @@ -205,7 +209,7 @@ def fileModified(self, path): roomname = filename[:-20] if path not in self.fileData: # seems eve created a new file. New Files have 12 lines header - self.fileData[path] = {"lines": 13} + self.fileData[path] = {"lines": self.HEADER_SIZE()} oldLength = self.fileData[path]["lines"] lines = self.addFile(path) if path in self.ignoredPaths: @@ -222,6 +226,22 @@ def fileModified(self, path): messages.append(message) return messages + def rewind(self): + """On region change or startup, rewind files to replay info from logfiles""" + for path in self.fileData: + newsize = self.fileData[path]["lines"] - self.REPLAY_SIZE() + if newsize < self.HEADER_SIZE(): + newsize = self.HEADER_SIZE() + logging.info("Rewinding %d lines from %s" % (self.fileData[path]["lines"]-newsize, path)) + self.fileData[path]["lines"] = newsize + return self.fileData + + def expire(self): + for m in self.knownMessages: + if (evegate.currentEveTime() - m.timestamp).total_seconds() > self.messageExpirySecs: + self.knownMessages.pop(0) + else: + break class Message(object): def __init__(self, room, message, timestamp, user, systems, upperText, plainText="", status=states.ALARM): @@ -235,6 +255,7 @@ def __init__(self, room, message, timestamp, user, systems, upperText, plainText self.plainText = plainText # plain text of the message, as posted # if you add the message to a widget, please add it to widgets self.widgets = [] + self.isOld = False def __key(self): return (self.room, self.plainText, self.timestamp, self.user) diff --git a/src/vi/chatparser/parser_functions.py b/src/vi/chatparser/parser_functions.py index 5ad171c..564ffa5 100644 --- a/src/vi/chatparser/parser_functions.py +++ b/src/vi/chatparser/parser_functions.py @@ -36,13 +36,16 @@ """ import six +import logging +import re import vi.evegate as evegate from bs4 import BeautifulSoup from bs4.element import NavigableString from vi import states +from vi.systems import SYSTEMS -CHARS_TO_IGNORE = ("*", "?", ",", "!", ".") +CHARS_TO_IGNORE_REGEX = '[*?,!.()]' def textReplace(element, newText): @@ -58,16 +61,14 @@ def textReplace(element, newText): def parseStatus(rtext): texts = [t for t in rtext.contents if isinstance(t, NavigableString)] for text in texts: + # upperText = re.sub(CHARS_TO_IGNORE_REGEX, '', text.strip().upper()) # KEEP QUESTION MARK? upperText = text.strip().upper() - originalText = upperText - for char in CHARS_TO_IGNORE: - upperText = upperText.replace(char, "") upperWords = upperText.split() - if (("CLEAR" in upperWords or "CLR" in upperWords) and not originalText.endswith("?")): + if (("CLEAR" in upperWords or "CLR" in upperWords) and not upperText.endswith("?")): return states.CLEAR elif ("STAT" in upperWords or "STATUS" in upperWords): return states.REQUEST - elif ("?" in originalText): + elif ("?" in upperText): return states.REQUEST elif (text.strip().upper() in ("BLUE", "BLUES ONLY", "ONLY BLUE" "STILL BLUE", "ALL BLUES")): return states.CLEAR @@ -75,20 +76,20 @@ def parseStatus(rtext): def parseShips(rtext): def formatShipName(text, word): - newText = u""" {0}""" + newText = u"""{0}""" text = text.replace(word, newText.format(word)) return text texts = [t for t in rtext.contents if isinstance(t, NavigableString)] for text in texts: - upperText = text.upper() + upperText = re.sub(CHARS_TO_IGNORE_REGEX, ' ', text.strip().upper()) for shipName in evegate.SHIPNAMES: if shipName in upperText: hit = True start = upperText.find(shipName) end = start + len(shipName) - if ((start > 0 and upperText[start - 1] not in (" ", "X")) or ( - end < len(upperText) - 1 and upperText[end] not in ("S", " "))): + if ( (start > 0 and re.match('[A-Z0-9]', upperText[start - 1])) + or (end < len(upperText) - 1 and re.match('[A-RT-Z0-9]', upperText[end])) ): hit = False if hit: shipInText = text[start:end] @@ -98,29 +99,30 @@ def formatShipName(text, word): def parseSystems(systems, rtext, foundSystems): - + systemNames = systems.keys() - + # words to ignore on the system parser. use UPPER CASE - WORDS_TO_IGNORE = ("IN", "IS", "AS") + WORDS_TO_IGNORE = ("IN", "IS", "AS", "OR", "NV", "TO", "ME", "HE", "SHE", "YOU", "ARE", + "ON", "HAS", "OF", "IT", "GET", "IF", "THE", "HOT", "OH", "OK", "GJ", "AND", "MY") def formatSystem(text, word, system): newText = u"""{1}""" text = text.replace(word, newText.format(system, word)) return text - texts = [t for t in rtext.contents if isinstance(t, NavigableString) and len(t)] - for wtIdx, text in enumerate(texts): - worktext = text - for char in CHARS_TO_IGNORE: - worktext = worktext.replace(char, "") - + texts = [t for t in rtext.contents if isinstance(t, NavigableString) and len(t)] + for text in texts: + worktext = re.sub(CHARS_TO_IGNORE_REGEX, '', text) + # Drop redundant whitespace so as to not throw off word index worktext = ' '.join(worktext.split()) words = worktext.split(" ") for idx, word in enumerate(words): - + + matchKey = None + # Is this about another a system's gate? if len(words) > idx + 1: if words[idx+1].upper() == 'GATE': @@ -133,42 +135,29 @@ def formatSystem(text, word, system): # '_____ GATE' mentioned in message, which is not what we're # interested in, so go to checking next word. continue - + upperWord = word.upper() if upperWord != word and upperWord in WORDS_TO_IGNORE: continue if upperWord in systemNames: # - direct hit on name - foundSystems.add(systems[upperWord]) # of the system? - formattedText = formatSystem(text, word, upperWord) - textReplace(text, formattedText) - return True - elif 1 < len(upperWord) < 5: # - upperWord < 4 chars. + matchKey = upperWord + elif 1 < len(upperWord) < 5: # - upperWord 2-4 chars. for system in systemNames: # system begins with? if system.startswith(upperWord): - foundSystems.add(systems[system]) - formattedText = formatSystem(text, word, system) - textReplace(text, formattedText) - return True - elif "-" in upperWord and len(upperWord) > 2: # - short with - (minus) - upperWordParts = upperWord.split("-") # (I-I will match I43-IF3) - for system in systemNames: - systemParts = system.split("-") - if (len(upperWordParts) == 2 and len(systemParts) == 2 and len(upperWordParts[0]) > 1 and len( - upperWordParts[1]) > 1 and len(systemParts[0]) > 1 and len(systemParts[1]) > 1 and len( - upperWordParts) == len(systemParts) and upperWordParts[0][0] == systemParts[0][0] and - upperWordParts[1][0] == systemParts[1][0]): - foundSystems.add(systems[system]) - formattedText = formatSystem(text, word, system) - textReplace(text, formattedText) - return True - elif len(upperWord) > 1: # what if F-YH58 is named FY? - for system in systemNames: - clearedSystem = system.replace("-", "") - if clearedSystem.startswith(upperWord): - foundSystems.add(systems[system]) - formattedText = formatSystem(text, word, system) - textReplace(text, formattedText) - return True - + matchKey = system + break + if None == matchKey and not '-' in upperWord: + for system in systemNames: # what if F-YH58 is named FY? + clearedSystem = system.replace("-", "") + if clearedSystem.startswith(upperWord): + matchKey = system + break + + if matchKey: + foundSystems.add(matchKey) + formattedText = formatSystem(text, word, matchKey) + textReplace(text, formattedText) + return True + return False diff --git a/src/vi/dotlan.py b/src/vi/dotlan.py index 266dd5f..a08117d 100644 --- a/src/vi/dotlan.py +++ b/src/vi/dotlan.py @@ -23,6 +23,7 @@ import math import time +import datetime import six import requests import logging @@ -318,11 +319,17 @@ class System(object): A System on the Map """ - ALARM_COLORS = [(60 * 4, "#FF0000", "#FFFFFF"), (60 * 10, "#FF9B0F", "#FFFFFF"), (60 * 15, "#FFFA0F", "#000000"), - (60 * 25, "#FFFDA2", "#000000"), (60 * 60 * 24, "#FFFFFF", "#000000")] - ALARM_COLOR = ALARM_COLORS[0][1] UNKNOWN_COLOR = "#FFFFFF" CLEAR_COLOR = "#59FF6C" + ALARM_COLORS = [ + # Alarmed systems change colors based on how long ago the alarm was received + # maxDiff (seconds), system background color, timer text color + (60 * 4, "#FF0000", "#FFFFFF"), + (60 * 10, "#FF9B0F", "#FFFFFF"), + (60 * 15, "#FFFA0F", "#000000"), + (60 * 25, "#FFFDA2", "#000000"), + (60 * 60 * 24, "#FFFFFF", "#000000") + ] def __init__(self, name, secondaryInfo, svgElement, mapSoup, mapCoordinates, transform, systemId): self.status = states.UNKNOWN @@ -333,11 +340,11 @@ def __init__(self, name, secondaryInfo, svgElement, mapSoup, mapCoordinates, tra self.origSvgElement = svgElement self.rect = svgElement.select("rect")[0] self.secondLine = svgElement.select("text")[1] - self.lastAlarmTime = 0 + self.lastAlarmTime = datetime.datetime.min self.messages = [] self.setStatus(states.UNKNOWN) self.__locatedCharacters = [] - self.backgroundColor = "#FFFFFF" + self.backgroundColor = self.UNKNOWN_COLOR self.mapCoordinates = mapCoordinates self.systemId = systemId self.transform = transform @@ -452,16 +459,21 @@ def removeNeighbour(self, system): if self in system._neighbours: system._neigbours.remove(self) - def setStatus(self, newStatus): + def setStatus(self, newStatus, statusTime = None): + if None == statusTime: + statusTime = evegate.currentEveTime() if newStatus == states.ALARM: - self.lastAlarmTime = time.time() + self.lastAlarmTime = statusTime if "stopwatch" not in self.secondLine["class"]: self.secondLine["class"].append("stopwatch") self.secondLine["alarmtime"] = self.lastAlarmTime self.secondLine["style"] = "fill: #FFFFFF;" - self.setBackgroundColor(self.ALARM_COLOR) + delta = (evegate.currentEveTime() - self.lastAlarmTime).total_seconds() + for maxDiff, alarmColor, secondLineColor in self.ALARM_COLORS: + if delta < maxDiff: + self.setBackgroundColor(alarmColor) elif newStatus == states.CLEAR: - self.lastAlarmTime = time.time() + self.lastAlarmTime = statusTime self.setBackgroundColor(self.CLEAR_COLOR) self.secondLine["alarmtime"] = 0 if "stopwatch" not in self.secondLine["class"]: @@ -491,7 +503,7 @@ def setStatistics(self, statistics): def update(self): # state changed? if (self.status == states.ALARM): - alarmTime = time.time() - self.lastAlarmTime + alarmTime = (evegate.currentEveTime() - self.lastAlarmTime).total_seconds() for maxDiff, alarmColor, secondLineColor in self.ALARM_COLORS: if alarmTime < maxDiff: if self.backgroundColor != alarmColor: @@ -502,7 +514,7 @@ def update(self): self.secondLine["style"] = "fill: {0};".format(secondLineColor) break if self.status in (states.ALARM, states.WAS_ALARMED, states.CLEAR): # timer - diff = math.floor(time.time() - self.lastAlarmTime) + diff = (evegate.currentEveTime() - self.lastAlarmTime).total_seconds() minutes = int(math.floor(diff / 60)) seconds = int(diff - minutes * 60) string = "{m:02d}:{s:02d}".format(m=minutes, s=seconds) diff --git a/src/vi/evegate.py b/src/vi/evegate.py index c0a4b16..442585c 100644 --- a/src/vi/evegate.py +++ b/src/vi/evegate.py @@ -312,7 +312,7 @@ def secondsTillDowntime(): u'CERBERUS', u'CHARON', u'CHEETAH', u'CHIMERA', u'CLAW', u'CLAYMORE', u'COERCER', u'CONDOR', u'CORMORANT', u'COVETOR', u'CRANE', u'CROW', u'CRUCIFIER', u'CRUOR', u'CRUSADER', u'CURSE', u'CYCLONE', u'CYNABAL', u'DAMNATION', u'DAREDEVIL', u'DEIMOS', u'DEVOTER', u'DOMINIX', u'DRAKE', u'DRAMIEL', u'EAGLE', u'EIDOLON', - u'ENIGMA', u'ENYO', u'EOS', u'EREBUS', u'ERIS', u'EXECUTIONER', u'EXEQUROR', u'EXEQUROR NAVY ISSUE', + u'ENDURANCE', u'ENIGMA', u'ENYO', u'EOS', u'EREBUS', u'ERIS', u'EXECUTIONER', u'EXEQUROR', u'EXEQUROR NAVY ISSUE', u'FALCON', u'FEDERATION NAVY COMET', u'FENRIR', u'FEROX', u'FLYCATCHER', u'GALLENTE SHUTTLE', u'GILA', u'GOLD MAGNATE', u'GOLEM', u'GRIFFIN', u'GUARDIAN', u'HARBINGER', u'HARPY', u'HAWK', u'HEL', u'HELIOS', u'HERETIC', u'HERON', u'HOARDER', u'HOUND', u'HUGINN', u'HULK', u'HURRICANE', u'HYENA', u'HYPERION', @@ -325,7 +325,8 @@ def secondsTillDowntime(): u'NOMAD', u'NYX', u'OBELISK', u'OCCATOR', u'OMEN', u'OMEN NAVY ISSUE', u'ONEIROS', u'ONYX', u'OPUX DRAGOON YACHT', u'OPUX LUXURY YACHT', u'ORACLE', u'ORCA', u'OSPREY', u'OSPREY NAVY ISSUE', u'PALADIN', u'PANTHER', u'PHANTASM', u'PHANTOM', u'PHOBOS', u'PHOENIX', u'PILGRIM', u'POLARIS CENTURION', - u'POLARIS INSPECTOR', u'POLARIS LEGATUS', u'PROBE', u'PROCURER', u'PROPHECY', u'PRORATOR', u'PROVIDENCE', + u'POLARIS INSPECTOR', u'POLARIS LEGATUS', u'PORPOISE', u'PROBE', u'PROCURER', u'PROPHECY', u'PRORATOR', + u'PROSPECT', u'PROVIDENCE', u'PROWLER', u'PUNISHER', u'PURIFIER', u'RAGNAROK', u'RAPIER', u'RAPTOR', u'RATTLESNAKE', u'RAVEN', u'RAVEN NAVY ISSUE', u'RAVEN STATE ISSUE', u'REAPER', u'REDEEMER', u'REPUBLIC FLEET FIRETAIL', u'RETRIBUTION', u'RETRIEVER', u'REVELATION', u'RHEA', u'RIFTER', u'ROKH', u'ROOK', u'RORQUAL', u'RUPTURE', @@ -335,7 +336,9 @@ def secondsTillDowntime(): u'TEMPEST TRIBAL ISSUE', u'THANATOS', u'THORAX', u'THRASHER', u'TORMENTOR', u'TRISTAN', u'TYPHOON', u'VAGABOND', u'VARGUR', u'VELATOR', u'VENGEANCE', u'VEXOR', u'VEXOR NAVY ISSUE', u'VIATOR', u'VIGIL', u'VIGILANT', u'VINDICATOR', u'VISITANT', u'VULTURE', u'WIDOW', u'WOLF', u'WORM', u'WRAITH', u'WREATHE', - u'WYVERN', u'ZEALOT', u'CAPSULE',) + u'WYVERN', u'ZEALOT', u'CAPSULE', u'BOMBER', + u'T3D', u'JACKDAW', u'SVIPUL', u'CONFESSOR', u'HECTATE', + u'STORK', u'BIFROST', u'PONTIFEX', u'MAGUS') SHIPNAMES = sorted(SHIPNAMES, key=lambda x: len(x), reverse=True) NPC_CORPS = (u'Republic Justice Department', u'House of Records', u'24th Imperial Crusade', u'Template:NPC corporation', diff --git a/src/vi/regions.py b/src/vi/regions.py index 5663e0c..9c4fd1b 100644 --- a/src/vi/regions.py +++ b/src/vi/regions.py @@ -62,4 +62,5 @@ "Vale of the Silent", "Venal", "Verge Vendor", - "Wicked Creek" ] + "Wicked Creek" +] diff --git a/src/vi/states.py b/src/vi/states.py index 202ea0c..ac3c3c5 100644 --- a/src/vi/states.py +++ b/src/vi/states.py @@ -26,7 +26,7 @@ NOT_CHANGE = 'no change' CLEAR = 'clear' ALARM = 'alarm' -WAS_ALARMED = 'was alarmed' +WAS_ALARMED = 'was alarmed' # UNUSED REQUEST = 'request' LOCATION = 'location' KOS_STATUS_REQUEST = 'kos request' diff --git a/src/vi/systems.py b/src/vi/systems.py new file mode 100644 index 0000000..df6adcc --- /dev/null +++ b/src/vi/systems.py @@ -0,0 +1,5465 @@ +SYSTEMS = { + "Tanoo": "Derelik", + "Lashesih": "Derelik", + "Akpivem": "Derelik", + "Jark": "Derelik", + "Sasta": "Derelik", + "Zaid": "Derelik", + "Yuzier": "Derelik", + "Nirbhi": "Derelik", + "Sooma": "Derelik", + "Chidah": "Derelik", + "Shenela": "Derelik", + "Asabona": "Derelik", + "Onsooh": "Derelik", + "Shamahi": "Derelik", + "Sendaya": "Derelik", + "Nazhgete": "Derelik", + "Futzchag": "Derelik", + "Kazna": "Derelik", + "Podion": "Derelik", + "Lilmad": "Derelik", + "Kuharah": "Derelik", + "Jayneleb": "Derelik", + "Fovihi": "Derelik", + "Kiereend": "Derelik", + "Rashy": "Derelik", + "Ordize": "Derelik", + "Psasa": "Derelik", + "Eshtah": "Derelik", + "Lachailes": "Derelik", + "Kasrasi": "Derelik", + "Mohas": "Derelik", + "Hasiari": "Derelik", + "Radima": "Derelik", + "Alkez": "Derelik", + "Nimambal": "Derelik", + "Yishinoon": "Derelik", + "Uplingur": "Derelik", + "Dooz": "Derelik", + "Bayuka": "Derelik", + "Uzistoon": "Derelik", + "Bairshir": "Derelik", + "Moh": "Derelik", + "Sari": "Derelik", + "Faspera": "Derelik", + "Jaymass": "Derelik", + "Mifrata": "Derelik", + "Majamar": "Derelik", + "Ihal": "Derelik", + "Camal": "Derelik", + "Fera": "Derelik", + "Juddi": "Derelik", + "Maspah": "Derelik", + "Ibaria": "Derelik", + "Shala": "Derelik", + "Zemalu": "Derelik", + "Khankenirdia": "Derelik", + "Nikh": "Derelik", + "Amphar": "Derelik", + "Salashayama": "Derelik", + "Janus": "Derelik", + "Agha": "Derelik", + "Iosantin": "Derelik", + "Orva": "Derelik", + "Zet": "Derelik", + "Akhrad": "Derelik", + "Pirohdim": "Derelik", + "Sharir": "Derelik", + "Usroh": "Derelik", + "Thiarer": "Derelik", + "Gomati": "Derelik", + "Jangar": "Derelik", + "Nakah": "Derelik", + "Irshah": "Derelik", + "Hasateem": "Derelik", + "Assah": "Derelik", + "Tidacha": "Derelik", + "Odlib": "Derelik", + "Jofan": "Derelik", + "Milu": "Derelik", + "Yadi": "Derelik", + "Buftiar": "Derelik", + "Jarizza": "Derelik", + "Ejahi": "Derelik", + "Asghatil": "Derelik", + "Bar": "Derelik", + "Sucha": "Derelik", + "Gelhan": "Derelik", + "Akeva": "Derelik", + "Sosa": "Derelik", + "Ilahed": "Derelik", + "Eshwil": "Derelik", + "Aranir": "Derelik", + "Ishkad": "Derelik", + "Hahyil": "Derelik", + "Asilem": "Derelik", + "Mahnagh": "Derelik", + "Shach": "Derelik", + "Kehrara": "Derelik", + "Arena": "Derelik", + "Timeor": "Derelik", + "Uhtafal": "Derelik", + "Dysa": "Derelik", + "Serad": "Derelik", + "Mahti": "Derelik", + "Abha": "Derelik", + "Shedoo": "Derelik", + "Gamis": "Derelik", + "Nieril": "Derelik", + "Berta": "Derelik", + "Bekirdod": "Derelik", + "Hothomouh": "Derelik", + "Arnola": "Derelik", + "Astabih": "Derelik", + "Ubtes": "Derelik", + "Bimener": "Derelik", + "Kenobanala": "Derelik", + "Khabi": "Derelik", + "Uanzin": "Derelik", + "Itamo": "The Forge", + "Mitsolen": "The Forge", + "Jatate": "The Forge", + "Mahtista": "The Forge", + "Vaankalen": "The Forge", + "Kylmabe": "The Forge", + "Ahtulaima": "The Forge", + "Geras": "The Forge", + "Sirseshin": "The Forge", + "Tuuriainas": "The Forge", + "Unpas": "The Forge", + "Shihuken": "The Forge", + "Nomaa": "The Forge", + "Ansila": "The Forge", + "Hirtamon": "The Forge", + "Hykkota": "The Forge", + "Outuni": "The Forge", + "Ohmahailen": "The Forge", + "Eskunen": "The Forge", + "Ikuchi": "The Forge", + "Urlen": "The Forge", + "Maurasi": "The Forge", + "Kisogo": "The Forge", + "Jita": "The Forge", + "Niyabainen": "The Forge", + "Perimeter": "The Forge", + "New Caldari": "The Forge", + "Saisio": "The Forge", + "Abagawa": "The Forge", + "Jakanerva": "The Forge", + "Gekutami": "The Forge", + "Hurtoken": "The Forge", + "Uoyonen": "The Forge", + "Hampinen": "The Forge", + "Poinen": "The Forge", + "Liekuri": "The Forge", + "Obanen": "The Forge", + "Josameto": "The Forge", + "Otela": "The Forge", + "Olo": "The Forge", + "Ikami": "The Forge", + "Reisen": "The Forge", + "Purjola": "The Forge", + "Maila": "The Forge", + "Akora": "The Forge", + "Messoya": "The Forge", + "Ishisomo": "The Forge", + "Airmia": "The Forge", + "Sakkikainen": "The Forge", + "Friggi": "The Forge", + "Ihakana": "The Forge", + "Vahunomi": "The Forge", + "Otitoh": "The Forge", + "Otomainen": "The Forge", + "Vattuolen": "The Forge", + "Onuse": "The Forge", + "Soshin": "The Forge", + "Keikaken": "The Forge", + "Ukkalen": "The Forge", + "Akkilen": "The Forge", + "Silen": "The Forge", + "Osmon": "The Forge", + "Korsiki": "The Forge", + "Inaya": "The Forge", + "Nuken": "The Forge", + "Uminas": "The Forge", + "Airaken": "The Forge", + "Oijanen": "The Forge", + "Wuos": "The Forge", + "Hentogaira": "The Forge", + "Kiainti": "The Forge", + "Vasala": "The Forge", + "Walvalin": "The Forge", + "Otanuomi": "The Forge", + "Vouskiaho": "The Forge", + "Otsela": "The Forge", + "Tasti": "The Forge", + "Otosela": "The Forge", + "Uemon": "The Forge", + "Paala": "The Forge", + "Fuskunen": "The Forge", + "Akkio": "The Forge", + "Uchoshi": "The Forge", + "Mastakomon": "The Forge", + "Eruka": "The Forge", + "Ohkunen": "The Forge", + "Obe": "The Forge", + "Wirashoda": "The Forge", + "Osaa": "The Forge", + "Sakenta": "The Forge", + "Senda": "The Forge", + "Aokannitoh": "The Forge", + "Uitra": "The Forge", + "LZ-6SU": "Vale of the Silent", + "MC6O-F": "Vale of the Silent", + "U54-1L": "Vale of the Silent", + "B-588R": "Vale of the Silent", + "NCGR-Q": "Vale of the Silent", + "G-LOIT": "Vale of the Silent", + "HE-V4V": "Vale of the Silent", + "N-HSK0": "Vale of the Silent", + "05R-7A": "Vale of the Silent", + "7-UH4Z": "Vale of the Silent", + "5ZO-NZ": "Vale of the Silent", + "FS-RFL": "Vale of the Silent", + "Y0-BVN": "Vale of the Silent", + "X97D-W": "Vale of the Silent", + "0-R5TS": "Vale of the Silent", + "H-UCD1": "Vale of the Silent", + "7-K5EL": "Vale of the Silent", + "H-5GUI": "Vale of the Silent", + "FH-TTC": "Vale of the Silent", + "FMBR-8": "Vale of the Silent", + "3HX-DL": "Vale of the Silent", + "UH-9ZG": "Vale of the Silent", + "NFM-0V": "Vale of the Silent", + "YXIB-I": "Vale of the Silent", + "MY-T2P": "Vale of the Silent", + "FA-DMO": "Vale of the Silent", + "GEKJ-9": "Vale of the Silent", + "Q-R3GP": "Vale of the Silent", + "N-5QPW": "Vale of the Silent", + "XV-8JQ": "Vale of the Silent", + "WBR5-R": "Vale of the Silent", + "4GYV-Q": "Vale of the Silent", + "4-HWWF": "Vale of the Silent", + "YMJG-4": "Vale of the Silent", + "8TPX-N": "Vale of the Silent", + "PM-DWE": "Vale of the Silent", + "K8X-6B": "Vale of the Silent", + "X445-5": "Vale of the Silent", + "KRUN-N": "Vale of the Silent", + "9OO-LH": "Vale of the Silent", + "V-OJEN": "Vale of the Silent", + "EIDI-N": "Vale of the Silent", + "P3EN-E": "Vale of the Silent", + "49-0LI": "Vale of the Silent", + "IPAY-2": "Vale of the Silent", + "DAYP-G": "Vale of the Silent", + "IFJ-EL": "Vale of the Silent", + "47L-J4": "Vale of the Silent", + "Q-L07F": "Vale of the Silent", + "E-D0VZ": "Vale of the Silent", + "6WW-28": "Vale of the Silent", + "A8A-JN": "Vale of the Silent", + "S-NJBB": "Vale of the Silent", + "T-GCGL": "Vale of the Silent", + "0MV-4W": "Vale of the Silent", + "TVN-FM": "Vale of the Silent", + "V-NL3K": "Vale of the Silent", + "AZBR-2": "Vale of the Silent", + "Z-8Q65": "Vale of the Silent", + "0J3L-V": "Vale of the Silent", + "H-NOU5": "Vale of the Silent", + "KX-2UI": "Vale of the Silent", + "MO-FIF": "Vale of the Silent", + "97-M96": "Vale of the Silent", + "MA-XAP": "Vale of the Silent", + "C-J7CR": "Vale of the Silent", + "Q-EHMJ": "Vale of the Silent", + "XSQ-TF": "Vale of the Silent", + "H-1EOH": "Vale of the Silent", + "IR-DYY": "Vale of the Silent", + "C-DHON": "Vale of the Silent", + "F-D49D": "Vale of the Silent", + "MQ-O27": "Vale of the Silent", + "H-EY0P": "Vale of the Silent", + "UNAG-6": "Vale of the Silent", + "E-SCTX": "Vale of the Silent", + "S6QX-N": "Vale of the Silent", + "IT-YAU": "Vale of the Silent", + "1VK-6B": "Vale of the Silent", + "7-PO3P": "Vale of the Silent", + "1W-0KS": "Vale of the Silent", + "669-IX": "Vale of the Silent", + "0R-F2F": "Vale of the Silent", + "R-P7KL": "Vale of the Silent", + "2DWM-2": "Vale of the Silent", + "XF-PWO": "Vale of the Silent", + "1N-FJ8": "Vale of the Silent", + "VI2K-J": "Vale of the Silent", + "ZLZ-1Z": "Vale of the Silent", + "6Y-WRK": "Vale of the Silent", + "RVCZ-C": "Vale of the Silent", + "5T-KM3": "Vale of the Silent", + "LS9B-9": "Vale of the Silent", + "1-GBBP": "Vale of the Silent", + "C-FP70": "Vale of the Silent", + "T-ZWA1": "Vale of the Silent", + "ZA0L-U": "Vale of the Silent", + "G96R-F": "Vale of the Silent", + "Y-ZXIO": "Vale of the Silent", + "B-E3KQ": "Vale of the Silent", + "Y5J-EU": "Vale of the Silent", + "O-LR1H": "Vale of the Silent", + "G5ED-Y": "Vale of the Silent", + "BR-6XP": "Vale of the Silent", + "8-TFDX": "Vale of the Silent", + "UL-4ZW": "Vale of the Silent", + "A-QRQT": "Vale of the Silent", + "WMBZ-U": "Vale of the Silent", + "PX5-LR": "Vale of the Silent", + "A3-RQ3": "Vale of the Silent", + "9-GBPD": "Vale of the Silent", + "LS-JEP": "Vale of the Silent", + "R-RSZZ": "Vale of the Silent", + "MGAM-4": "Vale of the Silent", + "VORM-W": "Vale of the Silent", + "7G-H7D": "Vale of the Silent", + "Q3-BAY": "Vale of the Silent", + "JZV-F4": "Vale of the Silent", + "WF-1LM": "UUA-F4", + "D95-FQ": "UUA-F4", + "ZSPJ-K": "UUA-F4", + "U1F-86": "UUA-F4", + "T-P7A6": "UUA-F4", + "Y-T3JJ": "UUA-F4", + "F3R-IA": "UUA-F4", + "74-YTJ": "UUA-F4", + "8-RS3U": "UUA-F4", + "OVFN-N": "UUA-F4", + "1Q-BBM": "UUA-F4", + "WXNC-N": "UUA-F4", + "G-EA07": "UUA-F4", + "X-L6BO": "UUA-F4", + "D-PHUA": "UUA-F4", + "3-HXHQ": "UUA-F4", + "18A-NB": "UUA-F4", + "3-J5OQ": "UUA-F4", + "GYF-GD": "UUA-F4", + "W-6TS9": "UUA-F4", + "VIG-VR": "UUA-F4", + "KX-P5C": "UUA-F4", + "N-FJBK": "UUA-F4", + "2-4ZT5": "UUA-F4", + "NVN-6F": "UUA-F4", + "09-8TH": "UUA-F4", + "TI0-AX": "UUA-F4", + "7O-POM": "UUA-F4", + "L6Q-SX": "UUA-F4", + "BFJ-TB": "UUA-F4", + "ZZ7-L6": "UUA-F4", + "L-CHVW": "UUA-F4", + "X0LN-U": "UUA-F4", + "RQAE-M": "UUA-F4", + "7CO-SA": "UUA-F4", + "4G-E5A": "UUA-F4", + "A-VWK9": "UUA-F4", + "JQHP-4": "UUA-F4", + "6Q5K-5": "UUA-F4", + "P-MVFP": "UUA-F4", + "J-Z1UW": "UUA-F4", + "W477-P": "UUA-F4", + "NQ1-BL": "UUA-F4", + "K7A-G8": "UUA-F4", + "HP-PMX": "UUA-F4", + "6BN-K9": "UUA-F4", + "WLE-PY": "UUA-F4", + "EH-HXW": "UUA-F4", + "OS-RR3": "UUA-F4", + "V4-GZL": "UUA-F4", + "4C-Z91": "UUA-F4", + "RU-97T": "UUA-F4", + "1S-1V7": "UUA-F4", + "PE1-R1": "UUA-F4", + "Polaris": "UUA-F4", + "JB-007": "UUA-F4", + "USJ2-M": "UUA-F4", + "7M-RAL": "UUA-F4", + "LPBU-U": "UUA-F4", + "RF-342": "UUA-F4", + "J2V-XY": "UUA-F4", + "Z-JBTR": "UUA-F4", + "S-QNXH": "UUA-F4", + "S94-X8": "UUA-F4", + "J-YQEC": "UUA-F4", + "8MX-OR": "UUA-F4", + "97YC-C": "UUA-F4", + "V-AMD5": "UUA-F4", + "U-JC8X": "UUA-F4", + "1HH3-E": "UUA-F4", + "DUIU-Q": "UUA-F4", + "LQH0-H": "UUA-F4", + "FRW3-2": "UUA-F4", + "9MX-1C": "UUA-F4", + "IED-4U": "UUA-F4", + "N-9EOQ": "UUA-F4", + "6F3-TK": "UUA-F4", + "2E0P-2": "UUA-F4", + "U-ITH5": "UUA-F4", + "N-4G5L": "UUA-F4", + "RB-2EA": "UUA-F4", + "ZK5-42": "UUA-F4", + "YRZ-E4": "UUA-F4", + "A3-PAT": "UUA-F4", + "H55-2R": "UUA-F4", + "P6-DBM": "UUA-F4", + "9XI-0X": "UUA-F4", + "Q8T-MC": "UUA-F4", + "Z-YOJ9": "UUA-F4", + "4T4B-L": "UUA-F4", + "F-JB3H": "UUA-F4", + "XBO7-F": "UUA-F4", + "FI-449": "UUA-F4", + "UA7-U4": "UUA-F4", + "VM-QFU": "UUA-F4", + "PU-1Z8": "UUA-F4", + "IEZW-V": "UUA-F4", + "B-DXO9": "UUA-F4", + "1TS-WN": "UUA-F4", + "16-31U": "UUA-F4", + "H472-N": "UUA-F4", + "U8MM-3": "UUA-F4", + "3C-26I": "UUA-F4", + "9K-VDI": "UUA-F4", + "L-SDU7": "UUA-F4", + "4-IPWK": "UUA-F4", + "Q-KCK3": "UUA-F4", + "WU-FHQ": "Detorid", + "V-4DBR": "Detorid", + "B-5UFY": "Detorid", + "SK42-F": "Detorid", + "EU9-J3": "Detorid", + "PQRE-W": "Detorid", + "OEG-K9": "Detorid", + "0-W778": "Detorid", + "DG-8VJ": "Detorid", + "5J4K-9": "Detorid", + "MD-0AW": "Detorid", + "H-FGJO": "Detorid", + "1KAW-T": "Detorid", + "C5-SUU": "Detorid", + "XSUD-1": "Detorid", + "3-LJW3": "Detorid", + "ZLO3-V": "Detorid", + "P7MI-T": "Detorid", + "JFV-ID": "Detorid", + "3-3EZB": "Detorid", + "52CW-6": "Detorid", + "9-OUGJ": "Detorid", + "4NDT-W": "Detorid", + "GR-X26": "Detorid", + "6OU9-U": "Detorid", + "9N-0HF": "Detorid", + "U-OVFR": "Detorid", + "G3D-ZT": "Detorid", + "D-0UI0": "Detorid", + "L8-WNE": "Detorid", + "1-GBVE": "Detorid", + "GC-LTF": "Detorid", + "NB-ALM": "Detorid", + "LT-XI4": "Detorid", + "L-QQ6P": "Detorid", + "5OJ-G2": "Detorid", + "9-02G0": "Detorid", + "XA5-TY": "Detorid", + "M-XUZZ": "Detorid", + "OFVH-Y": "Detorid", + "2-X0PF": "Detorid", + "1-PGSG": "Detorid", + "QLPX-J": "Detorid", + "A-C5TC": "Detorid", + "RZ-PIY": "Detorid", + "FR46-E": "Detorid", + "SLVP-D": "Detorid", + "0-G8NO": "Detorid", + "QRFJ-Q": "Detorid", + "HZFJ-M": "Detorid", + "77S8-E": "Detorid", + "FMH-OV": "Detorid", + "TYB-69": "Detorid", + "EDQG-L": "Detorid", + "7-P1JO": "Detorid", + "T-0JWP": "Detorid", + "J-L9MA": "Detorid", + "DX-TAR": "Detorid", + "A-7XFN": "Detorid", + "O3-4MN": "Detorid", + "U-MFTL": "Detorid", + "8FN-GP": "Detorid", + "FIDY-8": "Detorid", + "X40H-9": "Detorid", + "F2W-C6": "Detorid", + "KZ9T-C": "Detorid", + "XW2H-V": "Detorid", + "F9O-U9": "Detorid", + "S-51XG": "Detorid", + "E-1XVP": "Detorid", + "E-ACV6": "Detorid", + "BOZ1-O": "Detorid", + "QIMO-2": "Detorid", + "Z-2Y2Y": "Detorid", + "Q0J-RH": "Detorid", + "SAI-T9": "Detorid", + "IAS-I5": "Detorid", + "K7S-FF": "Detorid", + "RT-9WL": "Detorid", + "O5Q7-U": "Detorid", + "62O-UE": "Detorid", + "U0W-DR": "Detorid", + "SY-UWN": "Detorid", + "DX-DFJ": "Detorid", + "X-31TE": "Detorid", + "DVWV-3": "Detorid", + "KE-0FB": "Detorid", + "I-9GI1": "Detorid", + "W6P-7U": "Detorid", + "0IF-26": "Detorid", + "H-93YV": "Detorid", + "E51-JE": "Detorid", + "7-A6XV": "Detorid", + "QXE-1N": "Detorid", + "U69-YC": "Detorid", + "L-L7PE": "Detorid", + "MKIG-5": "Wicked Creek", + "YHEN-G": "Wicked Creek", + "E-JCUS": "Wicked Creek", + "W-QN5X": "Wicked Creek", + "LP1M-Q": "Wicked Creek", + "30-YOU": "Wicked Creek", + "384-IN": "Wicked Creek", + "4F89-U": "Wicked Creek", + "G063-U": "Wicked Creek", + "J7-BDX": "Wicked Creek", + "MLQ-O9": "Wicked Creek", + "L-FM3P": "Wicked Creek", + "X-ARMF": "Wicked Creek", + "8-OZU1": "Wicked Creek", + "0TYR-T": "Wicked Creek", + "GM-50Y": "Wicked Creek", + "G9L-LP": "Wicked Creek", + "MWA-5Q": "Wicked Creek", + "H-HHTH": "Wicked Creek", + "JQU-KY": "Wicked Creek", + "UY5A-D": "Wicked Creek", + "C-62I5": "Wicked Creek", + "ZH-GKG": "Wicked Creek", + "GPLB-C": "Wicked Creek", + "GGE-5Q": "Wicked Creek", + "5E-CMA": "Wicked Creek", + "U104-3": "Wicked Creek", + "M3-KAQ": "Wicked Creek", + "6-L4YC": "Wicked Creek", + "UM-SCG": "Wicked Creek", + "F-3FOY": "Wicked Creek", + "OAIG-0": "Wicked Creek", + "UZ-QXW": "Wicked Creek", + "5DE-QS": "Wicked Creek", + "R0-DMM": "Wicked Creek", + "5Q65-4": "Wicked Creek", + "SR-4EK": "Wicked Creek", + "0RI-OV": "Wicked Creek", + "C-LTXS": "Wicked Creek", + "C0O6-K": "Wicked Creek", + "HD-AJ7": "Wicked Creek", + "G9NE-B": "Wicked Creek", + "SJJ-4F": "Wicked Creek", + "F-QQ5N": "Wicked Creek", + "1-7B6D": "Wicked Creek", + "H6-EYX": "Wicked Creek", + "U-HVIX": "Wicked Creek", + "4-EFLU": "Wicked Creek", + "EIH-IU": "Wicked Creek", + "F-EM4Q": "Wicked Creek", + "1L-OEK": "Wicked Creek", + "MN-Q26": "Wicked Creek", + "5H-SM2": "Wicked Creek", + "4-OS2A": "Wicked Creek", + "YI-GV6": "Wicked Creek", + "SO-X5L": "Wicked Creek", + "XQS-GZ": "Wicked Creek", + "Q-GQHN": "Wicked Creek", + "A-4JOO": "Wicked Creek", + "TP7-KE": "Wicked Creek", + "R4N-LD": "Wicked Creek", + "3Q-VZA": "Wicked Creek", + "M-MBRT": "Wicked Creek", + "HPBE-D": "Wicked Creek", + "GRHS-B": "Wicked Creek", + "J-RXYN": "Wicked Creek", + "DUO-51": "Wicked Creek", + "07-SLO": "Wicked Creek", + "Z-A8FS": "Wicked Creek", + "GPD5-0": "Wicked Creek", + "LKZ-CY": "Wicked Creek", + "F5M-CC": "Wicked Creek", + "TZE-UB": "Wicked Creek", + "WRL4-2": "Wicked Creek", + "V7G-RL": "Wicked Creek", + "XEN7-0": "Wicked Creek", + "L-Z9KJ": "Wicked Creek", + "7K-NSE": "Wicked Creek", + "OR-7N5": "Wicked Creek", + "JEQG-7": "Wicked Creek", + "5NQI-E": "Wicked Creek", + "B-WQDP": "Wicked Creek", + "2-2EWC": "Cache", + "E1W-TB": "Cache", + "D-6H64": "Cache", + "8-BIE3": "Cache", + "LMM7-L": "Cache", + "995-3G": "Cache", + "W2T-TR": "Cache", + "Q-UEN6": "Cache", + "BLMX-B": "Cache", + "M-CNUD": "Cache", + "YE1-9S": "Cache", + "IVP-KA": "Cache", + "04EI-U": "Cache", + "B-T6BT": "Cache", + "VK-A5G": "Cache", + "I6-SYN": "Cache", + "O-5TN1": "Cache", + "8-SPNN": "Cache", + "U-QMOA": "Cache", + "4S0-NP": "Cache", + "K-RMI5": "Cache", + "C-6YHJ": "Cache", + "M53-1V": "Cache", + "E5T-CS": "Cache", + "W4C8-Q": "Cache", + "I-2705": "Cache", + "5F-MG1": "Cache", + "P7-45V": "Cache", + "M-MCP8": "Cache", + "JZ-B5Y": "Cache", + "TPG-DD": "Cache", + "NIF-JE": "Cache", + "BTLH-I": "Cache", + "U93O-A": "Cache", + "0LY-W1": "Cache", + "4YO-QK": "Cache", + "LJ-RJK": "Cache", + "8-VC6H": "Cache", + "LQ-01M": "Cache", + "NG-M8K": "Cache", + "RV5-TT": "Cache", + "8OYE-Z": "Cache", + "K85Y-6": "Cache", + "PKN-NJ": "Cache", + "EIN-QG": "Scalding Pass", + "ARG-3R": "Scalding Pass", + "S-E6ES": "Scalding Pass", + "R-3FBU": "Scalding Pass", + "K7-LDX": "Scalding Pass", + "U-IVGH": "Scalding Pass", + "P-N5N9": "Scalding Pass", + "JMH-PT": "Scalding Pass", + "DE-A7P": "Scalding Pass", + "X9V-15": "Scalding Pass", + "K212-A": "Scalding Pass", + "F-5FDA": "Scalding Pass", + "S1-XTL": "Scalding Pass", + "9PX2-F": "Scalding Pass", + "N3-JBX": "Scalding Pass", + "SG-75T": "Scalding Pass", + "GN-PDU": "Scalding Pass", + "AZ3F-N": "Scalding Pass", + "RNM-Y6": "Scalding Pass", + "V-KDY2": "Scalding Pass", + "FYD-TO": "Scalding Pass", + "ER2O-Y": "Scalding Pass", + "J2-PZ6": "Scalding Pass", + "XV-MWG": "Scalding Pass", + "OAQY-M": "Scalding Pass", + "1V-LI2": "Scalding Pass", + "M9-MLR": "Scalding Pass", + "Q-K2T7": "Scalding Pass", + "LBC-AW": "Scalding Pass", + "2-KPW6": "Scalding Pass", + "H5N-V7": "Scalding Pass", + "HQ-Q1Q": "Scalding Pass", + "WHI-61": "Scalding Pass", + "ZFJH-T": "Scalding Pass", + "I-1B7X": "Scalding Pass", + "G15Z-W": "Scalding Pass", + "AH8-Q7": "Scalding Pass", + "SD4A-2": "Scalding Pass", + "U6K-RG": "Scalding Pass", + "V-S9YY": "Scalding Pass", + "F2-NXA": "Scalding Pass", + "NSBE-L": "Scalding Pass", + "8Q-T7B": "Scalding Pass", + "WV0D-1": "Scalding Pass", + "ZNF-OK": "Scalding Pass", + "C8-7AS": "Scalding Pass", + "4E-EZS": "Scalding Pass", + "A-80UA": "Scalding Pass", + "U2-28D": "Scalding Pass", + "LQ-OAI": "Scalding Pass", + "5-MQQ7": "Scalding Pass", + "6-EQYE": "Scalding Pass", + "03-OR2": "Scalding Pass", + "JLO-Z3": "Scalding Pass", + "IAK-JW": "Scalding Pass", + "KZFV-4": "Scalding Pass", + "WO-GC0": "Scalding Pass", + "RYC-19": "Scalding Pass", + "X2-ZA5": "Scalding Pass", + "28Y9-P": "Scalding Pass", + "Q4C-S5": "Scalding Pass", + "B-1UJC": "Scalding Pass", + "Q-NA5H": "Scalding Pass", + "4-CM8I": "Scalding Pass", + "ZDB-HT": "Scalding Pass", + "1QZ-Y9": "Scalding Pass", + "HJ-BCH": "Scalding Pass", + "QPTT-F": "Scalding Pass", + "9M-M0P": "Scalding Pass", + "9BC-EB": "Scalding Pass", + "WFFE-4": "Scalding Pass", + "71-UTX": "Scalding Pass", + "PU-UMM": "Scalding Pass", + "6-KPAB": "Scalding Pass", + "Y5-E1U": "Scalding Pass", + "4-43BW": "Scalding Pass", + "8CN-CH": "Scalding Pass", + "V-F6DQ": "Scalding Pass", + "3S-6VU": "Scalding Pass", + "1-7HVI": "Scalding Pass", + "OX-S7P": "Scalding Pass", + "KDG-TA": "Insmother", + "KD-KPR": "Insmother", + "PT-21C": "Insmother", + "Z182-R": "Insmother", + "EKPB-3": "Insmother", + "5M2-KP": "Insmother", + "TK-DLH": "Insmother", + "C8H5-X": "Insmother", + "O-7LAI": "Insmother", + "7L3-JS": "Insmother", + "WF4C-8": "Insmother", + "TZN-2V": "Insmother", + "8EF-58": "Insmother", + "4DS-OI": "Insmother", + "XQP-9C": "Insmother", + "W-6GBI": "Insmother", + "XKH-6O": "Insmother", + "S0U-MO": "Insmother", + "F39H-1": "Insmother", + "V-QXXK": "Insmother", + "2-Q4YG": "Insmother", + "2JT-3Q": "Insmother", + "I3CR-F": "Insmother", + "7-JT09": "Insmother", + "AGCP-I": "Insmother", + "M4-GJ6": "Insmother", + "5-2PQU": "Insmother", + "SN9-3Z": "Insmother", + "6BJH-3": "Insmother", + "U-UTU9": "Insmother", + "1TG7-W": "Insmother", + "QYD-WK": "Insmother", + "R959-U": "Insmother", + "A-TJ0G": "Insmother", + "88A-RA": "Insmother", + "8G-2FP": "Insmother", + "C-J6MT": "Insmother", + "78-0R6": "Insmother", + "MSG-BZ": "Insmother", + "8-WYQZ": "Insmother", + "4M-QXK": "Insmother", + "X5-0EM": "Insmother", + "G-EURJ": "Insmother", + "SHBF-V": "Insmother", + "RERZ-L": "Insmother", + "0UBC-R": "Insmother", + "3U-48K": "Insmother", + "EFM-C4": "Insmother", + "YPW-M4": "Insmother", + "Q7-FZ8": "Insmother", + "L5-UWT": "Insmother", + "74-VZA": "Insmother", + "I-1QKL": "Insmother", + "GK5Z-T": "Insmother", + "RQN-OO": "Insmother", + "67Y-NR": "Insmother", + "GDHN-K": "Insmother", + "QTME-D": "Insmother", + "A24L-V": "Insmother", + "4CJ-AC": "Insmother", + "EUU-4N": "Insmother", + "Q-3HS5": "Insmother", + "3AE-CP": "Insmother", + "0-VG7A": "Insmother", + "9OLQ-6": "Insmother", + "MOCW-2": "Insmother", + "ZO-4AR": "Insmother", + "MJ-LGH": "Insmother", + "F2A-GX": "Insmother", + "RD-FWY": "Insmother", + "VBPT-T": "Insmother", + "KS-1TS": "Insmother", + "X0-6LH": "Insmother", + "FN0-QS": "Insmother", + "F3-8X2": "Insmother", + "N7-BIY": "Insmother", + "TTP-2B": "Insmother", + "LVL-GZ": "Insmother", + "EJ48-O": "Insmother", + "ROJ-B0": "Insmother", + "DFH-V5": "Insmother", + "B-II34": "Insmother", + "4LB-EL": "Insmother", + "UDE-FX": "Insmother", + "5IH-GL": "Insmother", + "C1G-XC": "Insmother", + "04-EHC": "Insmother", + "3-0FYP": "Insmother", + "N-O53U": "Insmother", + "HZ-O18": "Insmother", + "D-P1EH": "Insmother", + "74L2-U": "Insmother", + "HL-VZX": "Insmother", + "38NZ-1": "Insmother", + "W-MF6J": "Insmother", + "O-9G5Y": "Insmother", + "27-HP0": "Insmother", + "X1-IZ0": "Insmother", + "RZ-TI6": "Insmother", + "FX4L-2": "Insmother", + "1ZF-PJ": "Insmother", + "HFC-AQ": "Insmother", + "0-6VZ5": "Insmother", + "GB-6X5": "Insmother", + "7EX-14": "Insmother", + "N7-KGJ": "Insmother", + "VD-8QY": "Insmother", + "J-ZYSZ": "Insmother", + "5C-RPA": "Insmother", + "CR2-PQ": "Insmother", + "E-OGL4": "Tribute", + "J-GAMP": "Tribute", + "M-OEE8": "Tribute", + "V0DF-2": "Tribute", + "FY0W-N": "Tribute", + "MJI3-8": "Tribute", + "A-DDGY": "Tribute", + "F-RT6Q": "Tribute", + "B-S42H": "Tribute", + "NL6V-7": "Tribute", + "F-749O": "Tribute", + "0-YMBJ": "Tribute", + "UMI-KK": "Tribute", + "GKP-YT": "Tribute", + "AW1-2I": "Tribute", + "15W-GC": "Tribute", + "N-FK87": "Tribute", + "C2X-M5": "Tribute", + "MSHD-4": "Tribute", + "H-W9TY": "Tribute", + "PNDN-V": "Tribute", + "D7-ZAC": "Tribute", + "SH1-6P": "Tribute", + "TRKN-L": "Tribute", + "O-0ERG": "Tribute", + "WH-JCA": "Tribute", + "Q-CAB2": "Tribute", + "PBD-0G": "Tribute", + "L-1HKR": "Tribute", + "9GI-FB": "Tribute", + "3G-LHB": "Tribute", + "DBT-GB": "Tribute", + "U-W3WS": "Tribute", + "DL1C-E": "Tribute", + "YLS8-J": "Tribute", + "2ISU-Y": "Tribute", + "X-CFN6": "Tribute", + "9SL-K9": "Tribute", + "Y-PZHM": "Tribute", + "OY-UZ1": "Tribute", + "S8-NSQ": "Tribute", + "GIH-ZG": "Tribute", + "V7-FB4": "Tribute", + "XD-TOV": "Tribute", + "K-6SNI": "Tribute", + "L-VXTK": "Tribute", + "C8VC-S": "Tribute", + "W-UQA5": "Tribute", + "W6VP-Y": "Tribute", + "IMK-K1": "Tribute", + "NJ4X-S": "Tribute", + "F-G7BO": "Tribute", + "2CG-5V": "Tribute", + "QFF-O6": "Tribute", + "NIH-02": "Great Wildlands", + "JPL-RA": "Great Wildlands", + "NK-7XO": "Great Wildlands", + "E02-IK": "Great Wildlands", + "N-DQ0D": "Great Wildlands", + "M-MD3B": "Great Wildlands", + "FVXK-D": "Great Wildlands", + "6EG7-R": "Great Wildlands", + "56D-TC": "Great Wildlands", + "2X7Z-L": "Great Wildlands", + "8DL-CP": "Great Wildlands", + "UMDQ-6": "Great Wildlands", + "504Z-V": "Great Wildlands", + "F8K-WQ": "Great Wildlands", + "AB-FZE": "Great Wildlands", + "N-6Z8B": "Great Wildlands", + "YUY-LM": "Great Wildlands", + "NE-3GR": "Great Wildlands", + "Y4-GQV": "Great Wildlands", + "7-IDWY": "Great Wildlands", + "AZF-GH": "Great Wildlands", + "UT-UZB": "Great Wildlands", + "M-EKDF": "Great Wildlands", + "CRXA-Y": "Great Wildlands", + "VXO-OM": "Great Wildlands", + "BY5-V8": "Great Wildlands", + "TET3-B": "Great Wildlands", + "VKU-BG": "Great Wildlands", + "WPR-EI": "Great Wildlands", + "0NV-YU": "Great Wildlands", + "V-2GYS": "Great Wildlands", + "168-6H": "Great Wildlands", + "W-RFUO": "Great Wildlands", + "AI-EVH": "Great Wildlands", + "F-MKH3": "Great Wildlands", + "ZM-DNR": "Great Wildlands", + "GF-3FL": "Great Wildlands", + "ZJ-GOU": "Great Wildlands", + "QQ3-YI": "Great Wildlands", + "9-34L5": "Great Wildlands", + "0R-GZQ": "Great Wildlands", + "QM-20X": "Great Wildlands", + "8YC-AN": "Great Wildlands", + "7Q-8Z2": "Great Wildlands", + "SUR-F7": "Great Wildlands", + "OK-6XN": "Great Wildlands", + "Q2FL-T": "Great Wildlands", + "Y7-XFD": "Great Wildlands", + "U3K-4A": "Great Wildlands", + "P1T-LP": "Great Wildlands", + "R-ESG0": "Great Wildlands", + "CI4M-T": "Great Wildlands", + "I-QRJA": "Great Wildlands", + "M-YWAL": "Great Wildlands", + "DE71-9": "Great Wildlands", + "7JF-0Z": "Great Wildlands", + "IX8-JB": "Great Wildlands", + "WTIE-6": "Great Wildlands", + "Y-DSSK": "Great Wildlands", + "F5-CGW": "Great Wildlands", + "H9S-WC": "Great Wildlands", + "B-ROFP": "Great Wildlands", + "1L-AED": "Great Wildlands", + "1C-953": "Great Wildlands", + "SL-YBS": "Great Wildlands", + "UNJ-GX": "Great Wildlands", + "0PI4-E": "Great Wildlands", + "6WT-BE": "Great Wildlands", + "L1S-G1": "Great Wildlands", + "9SNK-O": "Great Wildlands", + "B-VIP9": "Great Wildlands", + "LXTC-S": "Great Wildlands", + "WE3-BX": "Great Wildlands", + "H7O-JZ": "Great Wildlands", + "H-8F5Q": "Great Wildlands", + "O-RXCZ": "Great Wildlands", + "4M-P1I": "Great Wildlands", + "P7UZ-T": "Great Wildlands", + "PUZ-IO": "Great Wildlands", + "HB-1NJ": "Great Wildlands", + "EOE3-N": "Great Wildlands", + "F7A-MR": "Great Wildlands", + "O-8SOC": "Great Wildlands", + "OJOS-T": "Great Wildlands", + "V89M-R": "Great Wildlands", + "66U-1P": "Great Wildlands", + "BRT-OP": "Great Wildlands", + "JUK0-1": "Great Wildlands", + "V-IH6B": "Great Wildlands", + "52V6-B": "Great Wildlands", + "PUC-JZ": "Great Wildlands", + "SB-23C": "Great Wildlands", + "5FCV-A": "Great Wildlands", + "O-OVOQ": "Great Wildlands", + "92-B0X": "Great Wildlands", + "0-3VW8": "Great Wildlands", + "28-QWU": "Great Wildlands", + "UD-AOK": "Great Wildlands", + "M9U-75": "Great Wildlands", + "N-RAEL": "Great Wildlands", + "K-IYNW": "Great Wildlands", + "H-ADOC": "Curse", + "G-G78S": "Curse", + "UW9B-F": "Curse", + "ZZ-ZWC": "Curse", + "OSY-UD": "Curse", + "K-MGJ7": "Curse", + "JWJ-P1": "Curse", + "V-IUEL": "Curse", + "0SHT-A": "Curse", + "D87E-A": "Curse", + "K-B2D3": "Curse", + "PO4F-3": "Curse", + "J7A-UR": "Curse", + "5E-VR8": "Curse", + "V7D-JD": "Curse", + "HLW-HP": "Curse", + "8G-MQV": "Curse", + "RA-NXN": "Curse", + "VOL-MI": "Curse", + "KLMT-W": "Curse", + "XX9-WV": "Curse", + "AAM-1A": "Curse", + "EW-JR5": "Curse", + "YKE4-3": "Curse", + "CL-85V": "Curse", + "K-QWHE": "Curse", + "MDD-79": "Curse", + "RMOC-W": "Curse", + "ES-UWY": "Curse", + "S1DP-Y": "Curse", + "Y-DW5K": "Curse", + "M-N7WD": "Curse", + "QFEW-K": "Curse", + "CVY-UC": "Curse", + "EQX-AE": "Curse", + "G-R4W1": "Curse", + "BPK-XK": "Curse", + "LJ-YSW": "Curse", + "Y-K50G": "Curse", + "K88X-J": "Curse", + "G-0Q86": "Curse", + "CL-1JE": "Curse", + "J4UD-J": "Curse", + "Hemin": "Curse", + "Utopia": "Curse", + "Jorund": "Curse", + "Doril": "Curse", + "Litom": "Curse", + "Farit": "Curse", + "Jamunda": "Curse", + "TD-4XL": "Malpais", + "IBOX-2": "Malpais", + "8AB-Q4": "Malpais", + "VW-PXL": "Malpais", + "JA-G0T": "Malpais", + "IF-KD1": "Malpais", + "7-YHRX": "Malpais", + "Y6-9LF": "Malpais", + "X-PQEX": "Malpais", + "N-H95C": "Malpais", + "NSI-MW": "Malpais", + "N-YLOE": "Malpais", + "NBO-O0": "Malpais", + "F-TQWO": "Malpais", + "0-TRV1": "Malpais", + "13-49W": "Malpais", + "6UT-1K": "Malpais", + "O8W-5O": "Malpais", + "LH-PLU": "Malpais", + "AZA-QE": "Malpais", + "8-2JZA": "Malpais", + "ZT-L3S": "Malpais", + "VVB-QH": "Malpais", + "Z-DDVJ": "Malpais", + "7-2Z93": "Malpais", + "B-VFDD": "Malpais", + "A0M-R8": "Malpais", + "LY-WRW": "Malpais", + "9F-ERQ": "Malpais", + "QCGG-Q": "Malpais", + "1NZV-7": "Malpais", + "NIM-FY": "Malpais", + "DAI-SH": "Malpais", + "V3P-AZ": "Malpais", + "C-KW6X": "Malpais", + "X1W-AL": "Malpais", + "F-WZYG": "Malpais", + "S-R9J2": "Malpais", + "XU-BF8": "Malpais", + "RIU-GC": "Malpais", + "Z0H2-4": "Malpais", + "63-7Q6": "Malpais", + "XCZ5-Y": "Malpais", + "NRD-5Q": "Malpais", + "W5-205": "Malpais", + "T-4H0B": "Malpais", + "Z-EKCY": "Malpais", + "SH-YZY": "Malpais", + "O7-RFZ": "Malpais", + "CLW-SI": "Malpais", + "5-A0PX": "Malpais", + "R-RMDH": "Malpais", + "2XI8-Y": "Malpais", + "5B-YDD": "Malpais", + "W-XY4J": "Malpais", + "PWPY-4": "Malpais", + "QZ1-OH": "Malpais", + "Y-XZA7": "Malpais", + "1-EVAX": "Malpais", + "I8-AJY": "Malpais", + "6-WMKE": "Malpais", + "J-Z8C2": "Malpais", + "XTVZ-E": "Malpais", + "APES-G": "Malpais", + "B2J-5N": "Malpais", + "2Z-HPQ": "Malpais", + "NBW-GD": "Malpais", + "YM-SRU": "Malpais", + "LO5-LN": "Malpais", + "06-70G": "Malpais", + "UYG-YX": "Malpais", + "GL6S-2": "Malpais", + "RUF3-O": "Malpais", + "C-NMG9": "Malpais", + "P3X-TN": "Malpais", + "N6NK-J": "Malpais", + "TP-APY": "Malpais", + "9NI-FW": "Malpais", + "H-EBQG": "Malpais", + "DOA-YU": "Malpais", + "ZOPZ-6": "Malpais", + "863P-X": "Malpais", + "ZO-YJZ": "Malpais", + "6A-FUY": "Malpais", + "HG-YEQ": "Malpais", + "2FL-5W": "Malpais", + "QSCO-D": "Malpais", + "RXTY-4": "Malpais", + "RSE-PT": "Malpais", + "WVJU-4": "Malpais", + "7T-0QS": "Malpais", + "RWML-A": "Malpais", + "V-JCJS": "Malpais", + "8C-VE3": "Malpais", + "S5W-1Z": "Malpais", + "IL-OL1": "Malpais", + "POQP-K": "Malpais", + "FO9-FZ": "Malpais", + "4QY-NT": "Malpais", + "0-N1BJ": "Malpais", + "T-8GWA": "Malpais", + "UW-6MW": "Malpais", + "F9E-KX": "Catch", + "9KOE-A": "Catch", + "U-QVWD": "Catch", + "B-3QPD": "Catch", + "36N-HZ": "Catch", + "SV5-8N": "Catch", + "HY-RWO": "Catch", + "WD-VTV": "Catch", + "HED-GP": "Catch", + "V-3YG7": "Catch", + "QSM-LM": "Catch", + "KDF-GY": "Catch", + "QBQ-RF": "Catch", + "9-8GBA": "Catch", + "6-K738": "Catch", + "ZXIC-7": "Catch", + "2J-WJY": "Catch", + "1P-WGB": "Catch", + "F4R2-Q": "Catch", + "K0CN-3": "Catch", + "WLAR-J": "Catch", + "L7XS-5": "Catch", + "VA6-DR": "Catch", + "S-U2VD": "Catch", + "GE-94X": "Catch", + "GMLH-K": "Catch", + "W9-DID": "Catch", + "KW-I6T": "Catch", + "EX-0LQ": "Catch", + "MB-NKE": "Catch", + "G-7WUF": "Catch", + "6-MM99": "Catch", + "JBY6-F": "Catch", + "FZ-6A5": "Catch", + "RNF-YH": "Catch", + "I-8D0G": "Catch", + "R-K4QY": "Catch", + "JWZ2-V": "Catch", + "OGL8-Q": "Catch", + "GJ0-OJ": "Catch", + "A-803L": "Catch", + "WQH-4K": "Catch", + "J-ODE7": "Catch", + "Q-S7ZD": "Catch", + "6X7-JO": "Catch", + "GE-8JV": "Catch", + "3-OKDA": "Catch", + "3GD6-8": "Catch", + "4M-HGL": "Catch", + "MY-W1V": "Catch", + "AX-DOT": "Catch", + "YHN-3K": "Catch", + "CB4-Q2": "Catch", + "CBL-XP": "Catch", + "WJ-9YO": "Catch", + "UQ-PWD": "Catch", + "N-8BZ6": "Catch", + "A-VILQ": "Catch", + "X3FQ-W": "Catch", + "3-SFWG": "Catch", + "MUXX-4": "Catch", + "E1-4YH": "Catch", + "B-XJX4": "Catch", + "AOK-WQ": "Catch", + "E3-SDZ": "Catch", + "7LHB-Z": "Catch", + "8B-2YA": "Catch", + "SNFV-I": "Catch", + "HP-64T": "Catch", + "V2-VC2": "Catch", + "L-B55M": "Catch", + "CX65-5": "Catch", + "JA-O6J": "Catch", + "ZQ-Z3Y": "Catch", + "G-AOTH": "Catch", + "TA3T-3": "Catch", + "E-YJ8G": "Catch", + "J6QB-P": "Catch", + "KA6D-K": "Catch", + "7MD-S1": "Catch", + "ERVK-P": "Catch", + "UL-7I8": "Catch", + "BR-N97": "Catch", + "IS-R7P": "Catch", + "S25C-K": "Catch", + "K717-8": "Catch", + "NH-1X6": "Catch", + "KH0Z-0": "Catch", + "5-N2EY": "Catch", + "KB-U56": "Catch", + "JGW-OT": "Catch", + "UCG4-B": "Catch", + "BUZ-DB": "Catch", + "QETZ-W": "Catch", + "WFC-MY": "Catch", + "Q-U96U": "Catch", + "X4-WL0": "Catch", + "W-MPTH": "Catch", + "4NBN-9": "Catch", + "EX6-AO": "Catch", + "CZK-ZQ": "Catch", + "CNC-4V": "Catch", + "Y-PNRL": "Catch", + "FAT-6P": "Catch", + "6BPS-T": "Catch", + "25S-6P": "Catch", + "RR-D05": "Catch", + "4-07MU": "Catch", + "Y-W1Q3": "Venal", + "Y6-HPG": "Venal", + "Z-GY5S": "Venal", + "KK-L97": "Venal", + "R-KZK7": "Venal", + "9-R6GU": "Venal", + "N-Q5PW": "Venal", + "P-FSQE": "Venal", + "H-PA29": "Venal", + "1-Y6KI": "Venal", + "YP-J33": "Venal", + "D-8SI1": "Venal", + "9-266Q": "Venal", + "K3JR-J": "Venal", + "CSOA-B": "Venal", + "6W-HRH": "Venal", + "N5Y-4N": "Venal", + "MQFX-Q": "Venal", + "9-8BL8": "Venal", + "N6G-H3": "Venal", + "3A1P-N": "Venal", + "OZ-VAE": "Venal", + "A-AFGR": "Venal", + "92K-H2": "Venal", + "AA-YRK": "Venal", + "BV-1JG": "Venal", + "0-BFTQ": "Venal", + "SS-GED": "Venal", + "AJCJ-1": "Venal", + "6NJ8-V": "Venal", + "Y-4CFK": "Venal", + "HBD-CC": "Venal", + "P-GKF5": "Venal", + "E-7U8U": "Venal", + "0-XIDJ": "Venal", + "SBL5-R": "Venal", + "O-TVTD": "Venal", + "8CIX-S": "Venal", + "D-SKWC": "Venal", + "4RX-EE": "Venal", + "V3X-L8": "Venal", + "N0C-UN": "Venal", + "VG-6CH": "Venal", + "Z0-TJW": "Venal", + "QHJ-FW": "Venal", + "9IPC-E": "Venal", + "EIV-1W": "Venal", + "S-1ZXZ": "Venal", + "N-5476": "Venal", + "PZOZ-K": "Venal", + "W3KK-R": "Venal", + "92D-OI": "Venal", + "EK2-ET": "Venal", + "SE-SHZ": "Venal", + "JURU-T": "Venal", + "MC6-5J": "Venal", + "65V-RH": "Venal", + "4-7IL9": "Venal", + "2PLH-3": "Venal", + "RQ9-OZ": "Venal", + "B-CZXG": "Venal", + "0-O2UT": "Venal", + "Q61Y-F": "Venal", + "PF-QHK": "Venal", + "XW-6TC": "Venal", + "Q-7SUI": "Venal", + "VVD-O6": "Venal", + "6ZJ-SC": "Venal", + "P-VYVL": "Venal", + "HD-JVQ": "Venal", + "H-AJ27": "Venal", + "M2-2V1": "Venal", + "2TH-3F": "Venal", + "E1F-E5": "Venal", + "4S-PVC": "Venal", + "WLF-D3": "Venal", + "LHJ-2G": "Venal", + "SHJO-J": "Venal", + "6UQ-4U": "Venal", + "430-BE": "Venal", + "OJ-CT4": "Venal", + "AZ-UWB": "Venal", + "H-S5BM": "Venal", + "FHB-QA": "Venal", + "Z3U-GI": "Venal", + "B3QP-K": "Venal", + "GVZ-1W": "Venal", + "G9D-XW": "Venal", + "42XJ-N": "Venal", + "L-IE41": "Venal", + "VG-QW1": "Venal", + "2IBE-N": "Venal", + "YJ3-UT": "Venal", + "ZD4-G9": "Venal", + "C2-DDA": "Venal", + "Dantumi": "Lonetrek", + "Antiainen": "Lonetrek", + "Ossa": "Lonetrek", + "Semiki": "Lonetrek", + "Kiskoken": "Lonetrek", + "Aurohunen": "Lonetrek", + "Veisto": "Lonetrek", + "Sobaseki": "Lonetrek", + "Funtanainen": "Lonetrek", + "Isikemi": "Lonetrek", + "Uosusuokko": "Lonetrek", + "Hageken": "Lonetrek", + "Uemisaisen": "Lonetrek", + "Sotrentaira": "Lonetrek", + "Ouranienen": "Lonetrek", + "Erenta": "Lonetrek", + "Kino": "Lonetrek", + "Raussinen": "Lonetrek", + "Iidoken": "Lonetrek", + "Tsuguwa": "Lonetrek", + "Nourvukaiken": "Lonetrek", + "Sarekuwa": "Lonetrek", + "Ekura": "Lonetrek", + "Tunttaras": "Lonetrek", + "Vellaine": "Lonetrek", + "Arvasaras": "Lonetrek", + "Akonoinen": "Lonetrek", + "Vaajaita": "Lonetrek", + "Autaris": "Lonetrek", + "Jan": "Lonetrek", + "Saatuban": "Lonetrek", + "Isikano": "Lonetrek", + "Mara": "Lonetrek", + "Isanamo": "Lonetrek", + "Pakkonen": "Lonetrek", + "Piekura": "Lonetrek", + "Amsen": "Lonetrek", + "Malkalen": "Lonetrek", + "Korama": "Lonetrek", + "Ylandoki": "Lonetrek", + "Aakari": "Lonetrek", + "Isseras": "Lonetrek", + "Aunenen": "Lonetrek", + "Elonaya": "Lonetrek", + "Litiura": "Lonetrek", + "Nonni": "Lonetrek", + "Passari": "Lonetrek", + "Piak": "Lonetrek", + "Airkio": "Lonetrek", + "Kakakela": "Lonetrek", + "Kamokor": "Lonetrek", + "Todaki": "Lonetrek", + "Ruvas": "Lonetrek", + "Umokka": "Lonetrek", + "Kirras": "Lonetrek", + "Autama": "Lonetrek", + "Tsukuras": "Lonetrek", + "Nani": "Lonetrek", + "Ajanen": "Lonetrek", + "Kuoka": "Lonetrek", + "Liukikka": "Lonetrek", + "Rauntaka": "Lonetrek", + "Aikantoh": "Lonetrek", + "Atai": "Lonetrek", + "Daras": "Lonetrek", + "Otalieto": "Lonetrek", + "Iitanmadan": "Lonetrek", + "Jotenen": "Lonetrek", + "Haajinen": "Lonetrek", + "Oipo": "Lonetrek", + "Isinokka": "Lonetrek", + "Yoma": "Lonetrek", + "Ibura": "Lonetrek", + "Torrinos": "Lonetrek", + "Endatoh": "Lonetrek", + "Aivoli": "Lonetrek", + "Uesuro": "Lonetrek", + "Oishami": "Lonetrek", + "Elanoda": "Lonetrek", + "Ohbochi": "Lonetrek", + "Isie": "Lonetrek", + "Tamo": "Lonetrek", + "Nannaras": "Lonetrek", + "Anin": "Lonetrek", + "Karjataimon": "Lonetrek", + "Tartoken": "Lonetrek", + "Saranen": "Lonetrek", + "Vuorrassi": "Lonetrek", + "Oimmo": "Lonetrek", + "Nalvula": "Lonetrek", + "Otsasai": "Lonetrek", + "Taisy": "Lonetrek", + "Hakonen": "Lonetrek", + "Jouvulen": "Lonetrek", + "Akiainavas": "Lonetrek", + "Kappas": "Lonetrek", + "Hitanishio": "Lonetrek", + "Ichinumi": "Lonetrek", + "PZP1-D": "J7HZ-F", + "R1KE-A": "J7HZ-F", + "JGDF-B": "J7HZ-F", + "1SR-HT": "J7HZ-F", + "SQ-2XA": "J7HZ-F", + "Z-FYJR": "J7HZ-F", + "ZA6-9N": "J7HZ-F", + "J1-6CJ": "J7HZ-F", + "7H-Z5R": "J7HZ-F", + "0RZ5-2": "J7HZ-F", + "A9-NB6": "J7HZ-F", + "LG1-TA": "J7HZ-F", + "TNK-BQ": "J7HZ-F", + "E2AX-5": "J7HZ-F", + "HPE-KP": "J7HZ-F", + "THS-MN": "J7HZ-F", + "UBES-K": "J7HZ-F", + "I-R8B0": "J7HZ-F", + "QIW-TQ": "J7HZ-F", + "WLL-QX": "J7HZ-F", + "BJC4-8": "J7HZ-F", + "PQA-9K": "J7HZ-F", + "S5-U0R": "J7HZ-F", + "CW-R71": "J7HZ-F", + "QO-3LC": "J7HZ-F", + "3E-ER7": "J7HZ-F", + "REZ-YZ": "J7HZ-F", + "OU-AIT": "J7HZ-F", + "VYX2-I": "J7HZ-F", + "5-P3CQ": "J7HZ-F", + "M-FDTD": "J7HZ-F", + "54-VNO": "J7HZ-F", + "IAMZ-5": "J7HZ-F", + "HD3-JK": "J7HZ-F", + "PBXG-A": "J7HZ-F", + "9-ERCP": "J7HZ-F", + "KN7M-N": "J7HZ-F", + "Z-D1DW": "J7HZ-F", + "FO-3PJ": "J7HZ-F", + "6-QXE6": "J7HZ-F", + "N-FKXV": "J7HZ-F", + "X7-8IG": "J7HZ-F", + "R-G1SF": "J7HZ-F", + "6-NCE7": "J7HZ-F", + "WDJQ-G": "J7HZ-F", + "JS3-RS": "J7HZ-F", + "JX-T1W": "J7HZ-F", + "CZ-CED": "J7HZ-F", + "BKK4-H": "J7HZ-F", + "Y-4V7U": "J7HZ-F", + "L-TPN0": "J7HZ-F", + "3-XORH": "J7HZ-F", + "G1VU-H": "J7HZ-F", + "W6H6-K": "J7HZ-F", + "6-23NU": "J7HZ-F", + "DVAR-P": "J7HZ-F", + "J-JS0D": "J7HZ-F", + "VR3-PS": "J7HZ-F", + "LH-J8H": "J7HZ-F", + "I9D-0D": "J7HZ-F", + "HGB-C6": "J7HZ-F", + "2L5-FI": "J7HZ-F", + "RS08-B": "J7HZ-F", + "4U-14I": "J7HZ-F", + "H-EDXD": "J7HZ-F", + "8-ULAA": "J7HZ-F", + "KF1-DU": "J7HZ-F", + "W-WQM5": "J7HZ-F", + "G5J-LH": "J7HZ-F", + "H7OL-I": "J7HZ-F", + "TO21-U": "J7HZ-F", + "RN-5K9": "J7HZ-F", + "0M-M64": "J7HZ-F", + "W5-SGC": "J7HZ-F", + "8RV-1L": "J7HZ-F", + "1C-TD6": "J7HZ-F", + "YBYX-1": "J7HZ-F", + "L-WG68": "The Spire", + "E4-E8W": "The Spire", + "HIK-MC": "The Spire", + "B9EA-G": "The Spire", + "E-BFLT": "The Spire", + "GZM-KB": "The Spire", + "5LAJ-8": "The Spire", + "C6C-K9": "The Spire", + "AL-JSG": "The Spire", + "ETO-OT": "The Spire", + "KPI-OW": "The Spire", + "A-J6SN": "The Spire", + "OTJ-4W": "The Spire", + "AG-SYG": "The Spire", + "1I5-0V": "The Spire", + "VX1-HV": "The Spire", + "JNG7-K": "The Spire", + "K-XJJT": "The Spire", + "FO1U-K": "The Spire", + "6U-1RX": "The Spire", + "Y4OK-W": "The Spire", + "P-NI4K": "The Spire", + "T6T-BQ": "The Spire", + "N-PS2Y": "The Spire", + "K-BBYU": "The Spire", + "0J-MQW": "The Spire", + "XT-1E0": "The Spire", + "3ET-G8": "The Spire", + "MOSA-I": "The Spire", + "B6-XE8": "The Spire", + "JLH-FN": "The Spire", + "DFTK-D": "The Spire", + "4HF-4R": "The Spire", + "Y8K-5B": "The Spire", + "L7-BLT": "The Spire", + "8P-LKL": "The Spire", + "Q-UVY6": "The Spire", + "RXA-W1": "The Spire", + "QFU-4S": "The Spire", + "QQGH-G": "The Spire", + "VK6-EZ": "The Spire", + "JVA-FE": "The Spire", + "P65-TA": "The Spire", + "G-VFVB": "The Spire", + "Y4B-BQ": "The Spire", + "EU-WFW": "The Spire", + "K-YL9T": "The Spire", + "GTB-O4": "The Spire", + "6W-6O9": "The Spire", + "H4X-0I": "The Spire", + "C-BHDN": "The Spire", + "R-RE2B": "The Spire", + "4DH-ST": "The Spire", + "OSW-0P": "The Spire", + "GF-GR7": "The Spire", + "DVN6-0": "The Spire", + "Z19-B8": "The Spire", + "HPMN-V": "The Spire", + "XR-ZL7": "The Spire", + "U1-VHY": "The Spire", + "OTJ9-E": "The Spire", + "LH-LY1": "The Spire", + "7-QOYS": "The Spire", + "KS8G-M": "The Spire", + "ZWM-BB": "The Spire", + "S-CUEA": "The Spire", + "L-EUY2": "The Spire", + "JL-ZUQ": "The Spire", + "X-KHRZ": "The Spire", + "WIW-X8": "The Spire", + "QRH-BF": "The Spire", + "M-NP5O": "The Spire", + "2-NF2Z": "A821-A", + "0Z-VHC": "A821-A", + "9-BUSQ": "A821-A", + "LQB-TC": "A821-A", + "II-1B3": "A821-A", + "6-HFD6": "A821-A", + "P3UD-M": "A821-A", + "LCN-0V": "A821-A", + "FX-XMW": "A821-A", + "G-N6MC": "A821-A", + "7-8XK0": "A821-A", + "90G-OA": "A821-A", + "DT-7EO": "A821-A", + "B-Y06L": "A821-A", + "HHQ-8L": "A821-A", + "Z-KPAR": "A821-A", + "8U-RZH": "A821-A", + "2RV-06": "A821-A", + "CLDT-L": "A821-A", + "QU7-EE": "A821-A", + "UC-X28": "A821-A", + "R79-I7": "A821-A", + "E-RPGP": "A821-A", + "ZV-KZO": "A821-A", + "NSE-U1": "A821-A", + "KER-EU": "A821-A", + "69A-54": "A821-A", + "M9-OS2": "A821-A", + "5V-YL6": "A821-A", + "8-UWFS": "A821-A", + "PQWA-L": "A821-A", + "BWO-UU": "A821-A", + "SQVI-U": "A821-A", + "T-YWDD": "A821-A", + "DLY-RG": "A821-A", + "T-C5A0": "A821-A", + "UP-L3Y": "A821-A", + "F-KBNV": "A821-A", + "JL-P9P": "A821-A", + "FR-RCH": "A821-A", + "FNS3-F": "A821-A", + "7BA-TK": "A821-A", + "IAWJ-X": "A821-A", + "50-TJY": "A821-A", + "3-CE1R": "A821-A", + "0IRK-R": "A821-A", + "Tividu": "Tash-Murkon", + "Tendhyes": "Tash-Murkon", + "Goram": "Tash-Murkon", + "Anjedin": "Tash-Murkon", + "Adahum": "Tash-Murkon", + "Ahrosseas": "Tash-Murkon", + "Riramia": "Tash-Murkon", + "Nafomeh": "Tash-Murkon", + "Pimsu": "Tash-Murkon", + "Jarzalad": "Tash-Murkon", + "Matyas": "Tash-Murkon", + "Imeshasa": "Tash-Murkon", + "Ivih": "Tash-Murkon", + "Seil": "Tash-Murkon", + "Mani": "Tash-Murkon", + "Sehmosh": "Tash-Murkon", + "Dabrid": "Tash-Murkon", + "Gyerzen": "Tash-Murkon", + "Hibi": "Tash-Murkon", + "Gemodi": "Tash-Murkon", + "Chamume": "Tash-Murkon", + "Nuzair": "Tash-Murkon", + "Pera": "Tash-Murkon", + "Shousran": "Tash-Murkon", + "Yong": "Tash-Murkon", + "Pimebeka": "Tash-Murkon", + "Baviasi": "Tash-Murkon", + "Tash-Murkon Prime": "Tash-Murkon", + "Emrayur": "Tash-Murkon", + "Shesha": "Tash-Murkon", + "Hilaban": "Tash-Murkon", + "Sacalan": "Tash-Murkon", + "Mimen": "Tash-Murkon", + "Thashkarai": "Tash-Murkon", + "Atoosh": "Tash-Murkon", + "Unkah": "Tash-Murkon", + "Hoona": "Tash-Murkon", + "Teshkat": "Tash-Murkon", + "Keshirou": "Tash-Murkon", + "Nasesharafa": "Tash-Murkon", + "Tirbam": "Tash-Murkon", + "Ordat": "Tash-Murkon", + "Rethan": "Tash-Murkon", + "Lossa": "Tash-Murkon", + "Onazel": "Tash-Murkon", + "Asesamy": "Tash-Murkon", + "Hostni": "Tash-Murkon", + "Mimime": "Tash-Murkon", + "Kibursha": "Tash-Murkon", + "Perdan": "Tash-Murkon", + "Abai": "Tash-Murkon", + "Nehkiah": "Tash-Murkon", + "Iro": "Tash-Murkon", + "Ahkour": "Tash-Murkon", + "Gaknem": "Tash-Murkon", + "Siyi": "Tash-Murkon", + "Remoriu": "Tash-Murkon", + "Yanuel": "Tash-Murkon", + "Nafrivik": "Tash-Murkon", + "Taru": "Tash-Murkon", + "Arkoz": "Tash-Murkon", + "Azhgabid": "Tash-Murkon", + "Jinizu": "Tash-Murkon", + "Phoren": "Tash-Murkon", + "Asezai": "Tash-Murkon", + "Ferira": "Tash-Murkon", + "Yeder": "Tash-Murkon", + "Azerakish": "Tash-Murkon", + "Lari": "Tash-Murkon", + "Yasud": "Tash-Murkon", + "Ghishul": "Tash-Murkon", + "Moutid": "Tash-Murkon", + "Goni": "Tash-Murkon", + "Adar": "Tash-Murkon", + "Paye": "Tash-Murkon", + "Sagain": "Tash-Murkon", + "Modun": "Tash-Murkon", + "Saminer": "Tash-Murkon", + "Marthia": "Tash-Murkon", + "Assiad": "Tash-Murkon", + "Rumida": "Tash-Murkon", + "Nosodnis": "Tash-Murkon", + "Iswa": "Tash-Murkon", + "Rand": "Tash-Murkon", + "Sizamod": "Tash-Murkon", + "Sinid": "Tash-Murkon", + "Alra": "Tash-Murkon", + "Ilas": "Tash-Murkon", + "Zith": "Tash-Murkon", + "Tew": "Tash-Murkon", + "Zehru": "Tash-Murkon", + "Uhodoh": "Tash-Murkon", + "Esa": "Tash-Murkon", + "Hath": "Tash-Murkon", + "Judra": "Tash-Murkon", + "Sharios": "Tash-Murkon", + "Arakor": "Tash-Murkon", + "Ahteer": "Tash-Murkon", + "Kari": "Tash-Murkon", + "Kerepa": "Tash-Murkon", + "Pasha": "Tash-Murkon", + "Safilbab": "Tash-Murkon", + "Seitam": "Tash-Murkon", + "JUE-DX": "Outer Passage", + "HLR-GL": "Outer Passage", + "80G-H5": "Outer Passage", + "2EV-BA": "Outer Passage", + "M1-PX9": "Outer Passage", + "W9-TFD": "Outer Passage", + "QHH-13": "Outer Passage", + "J4AQ-O": "Outer Passage", + "O-O2GN": "Outer Passage", + "I-HRX3": "Outer Passage", + "XUPK-Z": "Outer Passage", + "M4U-EH": "Outer Passage", + "WK2F-Y": "Outer Passage", + "WIO-OL": "Outer Passage", + "1-10QG": "Outer Passage", + "YQM-P1": "Outer Passage", + "6-GRN7": "Outer Passage", + "TFPT-U": "Outer Passage", + "D-JVGJ": "Outer Passage", + "K4UV-G": "Outer Passage", + "Q7E-DU": "Outer Passage", + "9Z-XJN": "Outer Passage", + "ZEZ1-9": "Outer Passage", + "QFRV-2": "Outer Passage", + "HZID-J": "Outer Passage", + "8-AA98": "Outer Passage", + "EZWQ-X": "Outer Passage", + "2ULC-J": "Outer Passage", + "T0DT-T": "Outer Passage", + "QG3-Z0": "Outer Passage", + "RT64-C": "Outer Passage", + "2ID-87": "Outer Passage", + "FVQF-W": "Outer Passage", + "8K-QCZ": "Outer Passage", + "JBUH-H": "Outer Passage", + "XDTW-F": "Outer Passage", + "0-4VQL": "Outer Passage", + "SN-DZ6": "Outer Passage", + "DJ-GBH": "Outer Passage", + "I0N-BM": "Outer Passage", + "QOK-SX": "Outer Passage", + "24I-FE": "Outer Passage", + "4H-YJZ": "Outer Passage", + "2-84WC": "Outer Passage", + "V-SEE6": "Outer Passage", + "U-FQ21": "Outer Passage", + "NHKO-4": "Outer Passage", + "KGCF-5": "Outer Passage", + "Y-UO9U": "Outer Passage", + "XME-SW": "Outer Passage", + "JX-SOA": "Outer Passage", + "VH-9VO": "Outer Passage", + "P-T9VC": "Outer Passage", + "9S-GPT": "Outer Passage", + "UAJ5-K": "Outer Passage", + "XJ-AG7": "Outer Passage", + "2WU-XT": "Outer Passage", + "J7X-VN": "Outer Passage", + "F-WCLC": "Outer Passage", + "G-HE0N": "Outer Passage", + "YC-ANK": "Outer Passage", + "LTT-AP": "Outer Passage", + "8RL-OG": "Outer Passage", + "R3P0-Z": "Outer Passage", + "ZZK-VF": "Outer Passage", + "SN-Q1T": "Outer Passage", + "L1YK-V": "Outer Passage", + "ZJ-5IS": "Outer Passage", + "GA58-7": "Outer Passage", + "J-0KB3": "Outer Passage", + "UC-8XF": "Outer Passage", + "90-A1P": "Outer Passage", + "4AZV-W": "Outer Passage", + "UNV-3J": "Outer Passage", + "7F-2FB": "Outer Passage", + "MC4C-H": "Outer Passage", + "OW-QXW": "Outer Passage", + "3-QNM4": "Outer Passage", + "UEPO-D": "Outer Passage", + "NQ-M6W": "Outer Passage", + "P-8PDJ": "Outer Passage", + "VE-W7O": "Outer Passage", + "CNHV-M": "Outer Passage", + "NEU-UD": "Outer Passage", + "N-I024": "Outer Passage", + "4O-ZRI": "Outer Passage", + "Y-7XVJ": "Outer Passage", + "RQNF-9": "Outer Passage", + "DSS-EZ": "Stain", + "MB4D-4": "Stain", + "LGK-VP": "Stain", + "E-C0SR": "Stain", + "X1E-OQ": "Stain", + "VTGN-U": "Stain", + "0Y1-M7": "Stain", + "Q-Q2S6": "Stain", + "WHG2-7": "Stain", + "9RQ-L8": "Stain", + "32-GI9": "Stain", + "TG-Z23": "Stain", + "IP-MVJ": "Stain", + "4J-ZC9": "Stain", + "7R5-7R": "Stain", + "Y1-UQ2": "Stain", + "HM-UVD": "Stain", + "G-ME2K": "Stain", + "WNS-7J": "Stain", + "57M7-W": "Stain", + "JS-E8E": "Stain", + "FV-SE8": "Stain", + "FZSW-Y": "Stain", + "UF-KKH": "Stain", + "O5Y3-W": "Stain", + "0GN-VO": "Stain", + "9U6-SV": "Stain", + "4GQ-XQ": "Stain", + "R8-5XF": "Stain", + "2IGP-1": "Stain", + "Z2-QQP": "Stain", + "GDEW-0": "Stain", + "PSJ-10": "Stain", + "2-V0KY": "Stain", + "U-WLT9": "Stain", + "ZG8Q-N": "Stain", + "40GX-P": "Stain", + "37S-KO": "Stain", + "4J9-DK": "Stain", + "A-GPTM": "Stain", + "HQ-TDJ": "Stain", + "WBLF-0": "Stain", + "GDO-7H": "Stain", + "NZG-LF": "Stain", + "UJM-RD": "Stain", + "L0AD-B": "Stain", + "8ZO-CK": "Stain", + "WEQT-K": "Stain", + "8O-OSG": "Stain", + "1H-I12": "Stain", + "D9D-GD": "Stain", + "4A-XJ6": "Stain", + "GU-54G": "Stain", + "7-X3RN": "Stain", + "BF-FVB": "Stain", + "9O-ZTS": "Stain", + "8KQR-O": "Stain", + "F9SX-1": "Stain", + "0G-A25": "Stain", + "WJO0-G": "Stain", + "S91-TI": "Stain", + "V1V-6F": "Stain", + "S-DLKC": "Stain", + "42-UOW": "Stain", + "CBGG-0": "Stain", + "A4UG-O": "Stain", + "W-VXL9": "Stain", + "U2-BJ2": "Stain", + "UKYS-5": "Stain", + "RV5-DW": "Stain", + "KP-FQ1": "Stain", + "RLDS-R": "Stain", + "QM-O7J": "Stain", + "0-7XA8": "Stain", + "X5O1-L": "Stain", + "F-TVAP": "Stain", + "6Y-0TW": "Stain", + "TL-T9Z": "Stain", + "E7-WSY": "Stain", + "B-G1LG": "Stain", + "T-8UOF": "Stain", + "DP-2WP": "Stain", + "MMR-LZ": "Stain", + "I-ME3L": "Stain", + "YE17-R": "Stain", + "T7-JNB": "Stain", + "LB0-A1": "Stain", + "S-BWWQ": "Stain", + "Z-R96X": "Stain", + "J-AYLV": "Stain", + "DABV-N": "Stain", + "ZH-KEV": "Stain", + "LC-1ED": "Stain", + "RPS-0K": "Stain", + "VNPF-7": "Stain", + "CJF-1P": "Stain", + "U6-FCE": "Stain", + "L6B-0N": "Stain", + "Z-XMUC": "Stain", + "6QBH-S": "Stain", + "RRWI-5": "Stain", + "Y-4U62": "Stain", + "EAWE-2": "Stain", + "I-3FET": "Stain", + "QCKK-T": "Stain", + "RP-H66": "Stain", + "JU-UYK": "Stain", + "O-FTHE": "Stain", + "W-Q233": "Stain", + "4XW2-D": "Stain", + "J5NU-K": "Stain", + "EOT-XL": "Stain", + "RVRE-Z": "Stain", + "B-2UL0": "Stain", + "L-A9FS": "Stain", + "OOO-FS": "Stain", + "373Z-7": "Stain", + "JVJ2-N": "Stain", + "2B-3M4": "Stain", + "A-XASO": "Stain", + "5J-UEX": "Stain", + "1H4V-O": "Stain", + "LGL-SD": "Stain", + "A-DZA8": "Stain", + "O-CT8N": "Stain", + "Z-6YQC": "Stain", + "F7-ICZ": "Stain", + "XFBE-T": "Stain", + "T-NNJZ": "Stain", + "DK6W-I": "Stain", + "0T-LIB": "Stain", + "NRT4-U": "Stain", + "KQK1-2": "Pure Blind", + "O-BY0Y": "Pure Blind", + "2D-0SO": "Pure Blind", + "UR-E6D": "Pure Blind", + "X47L-Q": "Pure Blind", + "D7T-C0": "Pure Blind", + "KI-TL0": "Pure Blind", + "EL8-4Q": "Pure Blind", + "JC-YX8": "Pure Blind", + "5-9WNU": "Pure Blind", + "XI-VUF": "Pure Blind", + "N-H32Y": "Pure Blind", + "12YA-2": "Pure Blind", + "BDV3-T": "Pure Blind", + "J-CIJV": "Pure Blind", + "X-7OMU": "Pure Blind", + "CXN1-Z": "Pure Blind", + "KLY-C0": "Pure Blind", + "CL6-ZG": "Pure Blind", + "G95-VZ": "Pure Blind", + "ROIR-Y": "Pure Blind", + "EC-P8R": "Pure Blind", + "EWOK-K": "Pure Blind", + "O-N8XZ": "Pure Blind", + "G-M4I8": "Pure Blind", + "MI6O-6": "Pure Blind", + "L-TS8S": "Pure Blind", + "93PI-4": "Pure Blind", + "ION-FG": "Pure Blind", + "C-H9X7": "Pure Blind", + "A8I-C5": "Pure Blind", + "DK-FXK": "Pure Blind", + "M-76XI": "Pure Blind", + "ZJET-E": "Pure Blind", + "U-INPD": "Pure Blind", + "WW-KGD": "Pure Blind", + "XQ-PXU": "Pure Blind", + "M-YCD4": "Pure Blind", + "Q-5211": "Pure Blind", + "R-2R0G": "Pure Blind", + "CR-AQH": "Pure Blind", + "8S-0E1": "Pure Blind", + "5ZXX-K": "Pure Blind", + "JE-D5U": "Pure Blind", + "2-6TGQ": "Pure Blind", + "OE-9UF": "Pure Blind", + "PFU-LH": "Pure Blind", + "R6XN-9": "Pure Blind", + "3V8-LJ": "Pure Blind", + "B8EN-S": "Pure Blind", + "R-LW2I": "Pure Blind", + "DP-1YE": "Pure Blind", + "4-ABS8": "Pure Blind", + "7RM-N0": "Pure Blind", + "S-MDYI": "Pure Blind", + "ZKYV-W": "Pure Blind", + "F-NMX6": "Pure Blind", + "GA-P6C": "Pure Blind", + "FWA-4V": "Pure Blind", + "RZC-16": "Pure Blind", + "RD-G2R": "Pure Blind", + "UC3H-Y": "Pure Blind", + "6GWE-A": "Pure Blind", + "J-OK0C": "Pure Blind", + "KDV-DE": "Pure Blind", + "MT9Q-S": "Pure Blind", + "B-9C24": "Pure Blind", + "P-2TTL": "Pure Blind", + "7X-VKB": "Pure Blind", + "E-Z2ZX": "Pure Blind", + "RORZ-H": "Pure Blind", + "O-A6YN": "Pure Blind", + "MQ-NPY": "Pure Blind", + "D2-HOS": "Pure Blind", + "Y2-6EA": "Pure Blind", + "TFA0-U": "Pure Blind", + "RQH-MY": "Pure Blind", + "HPS5-C": "Pure Blind", + "DT-TCD": "Pure Blind", + "KU5R-W": "Pure Blind", + "H1-J33": "Pure Blind", + "Y-C3EQ": "Pure Blind", + "OGV-AS": "Pure Blind", + "7D-0SQ": "Pure Blind", + "UI-8ZE": "Pure Blind", + "NS2L-4": "Immensea", + "QI-S9W": "Immensea", + "B-S347": "Immensea", + "PPFB-U": "Immensea", + "AF0-V5": "Immensea", + "B-A587": "Immensea", + "Y19P-1": "Immensea", + "B9E-H6": "Immensea", + "SPBS-6": "Immensea", + "JDAS-0": "Immensea", + "A4B-V5": "Immensea", + "LN-56V": "Immensea", + "Y2-QUV": "Immensea", + "O7-7UX": "Immensea", + "Z8-81T": "Immensea", + "XD-JW7": "Immensea", + "DY-P7Q": "Immensea", + "H-RXNZ": "Immensea", + "ZBP-TP": "Immensea", + "XVV-21": "Immensea", + "GXK-7F": "Immensea", + "EA-HSA": "Immensea", + "78TS-Q": "Immensea", + "WYF8-8": "Immensea", + "CJNF-J": "Immensea", + "FYI-49": "Immensea", + "RF6T-8": "Immensea", + "ZJA-6U": "Immensea", + "94FR-S": "Immensea", + "Q-HJ97": "Immensea", + "GM-0K7": "Immensea", + "I-NGI8": "Immensea", + "R-ZUOL": "Immensea", + "E1F-LK": "Immensea", + "Z4-QLD": "Immensea", + "QE-E1D": "Immensea", + "LK1K-5": "Immensea", + "REB-KR": "Immensea", + "Z-H2MA": "Immensea", + "L-5JCJ": "Immensea", + "B-KDOZ": "Immensea", + "4-GB14": "Immensea", + "PH-NFR": "Immensea", + "DW-N2S": "Immensea", + "W-FHWJ": "Immensea", + "X-6WC7": "Immensea", + "D-BAMJ": "Immensea", + "JKWP-U": "Immensea", + "RHE7-W": "Immensea", + "F76-8Q": "Immensea", + "O3Z5-G": "Immensea", + "4DV-1T": "Immensea", + "XS-K1O": "Immensea", + "FN-DSR": "Immensea", + "B-R5RB": "Immensea", + "7-ZT1Y": "Immensea", + "9-XN3F": "Immensea", + "AC-7LZ": "Immensea", + "LBA-SO": "Immensea", + "Y-FZ5N": "Immensea", + "E8-YS9": "Immensea", + "U79-JF": "Immensea", + "B2-UQW": "Immensea", + "U9U-TQ": "Immensea", + "6-I162": "Immensea", + "08-N7Q": "Immensea", + "Y-C4AL": "Immensea", + "CKX-RW": "Immensea", + "8X6T-8": "Immensea", + "W4E-IT": "Immensea", + "OP9L-F": "Immensea", + "J-QA7I": "Immensea", + "2O-EEW": "Immensea", + "Y-N4EF": "Immensea", + "7YSF-E": "Immensea", + "KCDX-7": "Immensea", + "O7-VJ5": "Immensea", + "FRTC-5": "Immensea", + "M-ZJWJ": "Immensea", + "R-ORB7": "Immensea", + "RU-PT9": "Immensea", + "DR-427": "Immensea", + "NI-J0B": "Immensea", + "QN-6J2": "Immensea", + "2G-VDP": "Etherium Reach", + "9F-3CR": "Etherium Reach", + "J7M-3W": "Etherium Reach", + "KRPF-A": "Etherium Reach", + "9P-870": "Etherium Reach", + "QNXJ-M": "Etherium Reach", + "AID-9T": "Etherium Reach", + "PXE-RG": "Etherium Reach", + "5J-62N": "Etherium Reach", + "Z-DRIY": "Etherium Reach", + "8-MXHA": "Etherium Reach", + "LPVL-5": "Etherium Reach", + "D3S-EA": "Etherium Reach", + "KGT3-6": "Etherium Reach", + "4LJ6-Q": "Etherium Reach", + "SAH-AD": "Etherium Reach", + "MF-PGF": "Etherium Reach", + "L-ZJLN": "Etherium Reach", + "G-QTSD": "Etherium Reach", + "3G-LFX": "Etherium Reach", + "NK-VTL": "Etherium Reach", + "D-CR6W": "Etherium Reach", + "BY-7PY": "Etherium Reach", + "GN-TNT": "Etherium Reach", + "QKCU-4": "Etherium Reach", + "0M-24X": "Etherium Reach", + "N06Z-Q": "Etherium Reach", + "YX-0KH": "Etherium Reach", + "KMH-J1": "Etherium Reach", + "CYB-BZ": "Etherium Reach", + "5U-3PW": "Etherium Reach", + "89JS-J": "Etherium Reach", + "C9R-NO": "Etherium Reach", + "FKR-SR": "Etherium Reach", + "1ACJ-6": "Etherium Reach", + "BNX-AS": "Etherium Reach", + "XB-9U2": "Etherium Reach", + "F9-FUV": "Etherium Reach", + "FB-MPY": "Etherium Reach", + "RO-0PZ": "Etherium Reach", + "JTA2-2": "Etherium Reach", + "R-6KYM": "Etherium Reach", + "3H58-R": "Etherium Reach", + "RV-GA8": "Etherium Reach", + "TP-RTO": "Etherium Reach", + "GTY-FW": "Etherium Reach", + "1H5-3W": "Etherium Reach", + "QZV-X3": "Etherium Reach", + "IS-OBW": "Etherium Reach", + "1GH-48": "Etherium Reach", + "IRD-HU": "Etherium Reach", + "B-2VXB": "Etherium Reach", + "FIZU-X": "Etherium Reach", + "JAWX-R": "Etherium Reach", + "Z0G-XG": "Etherium Reach", + "ALC-JM": "Etherium Reach", + "9QS5-C": "Etherium Reach", + "NWX-LI": "Etherium Reach", + "N-SFZK": "Etherium Reach", + "2B-UUQ": "Etherium Reach", + "I64-XB": "Etherium Reach", + "4-QDIX": "Etherium Reach", + "FGJP-J": "Etherium Reach", + "89-JPE": "Etherium Reach", + "D-IZT9": "Etherium Reach", + "WU9-ZR": "Etherium Reach", + "E8-432": "Etherium Reach", + "43-1TL": "Etherium Reach", + "O-LJOO": "Etherium Reach", + "ZS-PNI": "Etherium Reach", + "TZ-74M": "Etherium Reach", + "8KE-YS": "Etherium Reach", + "LXQ2-T": "Etherium Reach", + "HV-EAP": "Etherium Reach", + "3IK-7O": "Etherium Reach", + "O-EUHA": "Etherium Reach", + "MO-I1W": "Etherium Reach", + "ZZ5X-M": "Etherium Reach", + "UAV-1E": "Etherium Reach", + "CL-IRS": "Etherium Reach", + "QBZO-R": "Etherium Reach", + "QHJR-E": "Etherium Reach", + "1PF-BC": "Etherium Reach", + "D-OJEZ": "Etherium Reach", + "C-V6DQ": "Etherium Reach", + "Z-FET0": "Etherium Reach", + "EX-GBT": "Etherium Reach", + "PX-IHN": "Etherium Reach", + "WPV-JN": "Etherium Reach", + "IL-H0A": "Etherium Reach", + "CT8K-0": "Etherium Reach", + "M9-LAN": "Etherium Reach", + "C-4D0W": "Etherium Reach", + "L4X-1V": "Etherium Reach", + "M-V0PQ": "Etherium Reach", + "DYPL-6": "Etherium Reach", + "V-OL61": "Etherium Reach", + "RK-Q51": "Etherium Reach", + "F69O-M": "Etherium Reach", + "T-IDGH": "Etherium Reach", + "Aeddin": "Molden Heath", + "Gulfonodi": "Molden Heath", + "Teonusude": "Molden Heath", + "Gelfiven": "Molden Heath", + "Bosena": "Molden Heath", + "Oddelulf": "Molden Heath", + "Atlar": "Molden Heath", + "Heild": "Molden Heath", + "Hrokkur": "Molden Heath", + "Hrober": "Molden Heath", + "Aedald": "Molden Heath", + "Muttokon": "Molden Heath", + "Audesder": "Molden Heath", + "Illamur": "Molden Heath", + "Horaka": "Molden Heath", + "Eldulf": "Molden Heath", + "Orien": "Molden Heath", + "Varigne": "Molden Heath", + "Meildolf": "Molden Heath", + "Istodard": "Molden Heath", + "Gonheim": "Molden Heath", + "Half": "Molden Heath", + "Sakulda": "Molden Heath", + "Hedaleolfarber": "Molden Heath", + "Altbrard": "Molden Heath", + "Fegomenko": "Molden Heath", + "Osvetur": "Molden Heath", + "Mimiror": "Molden Heath", + "Skarkon": "Molden Heath", + "Ennur": "Molden Heath", + "Unertek": "Molden Heath", + "Klingt": "Molden Heath", + "Weld": "Molden Heath", + "Kattegaud": "Molden Heath", + "Kadlina": "Molden Heath", + "Hegfunden": "Molden Heath", + "Aeditide": "Molden Heath", + "Egbinger": "Molden Heath", + "MR4-MY": "Geminate", + "SR-KBB": "Geminate", + "FDZ4-A": "Geminate", + "2E-ZR5": "Geminate", + "O1-FTD": "Geminate", + "Roua": "Geminate", + "OEY-OR": "Geminate", + "M-MD31": "Geminate", + "WH-2EZ": "Geminate", + "D0-F4W": "Geminate", + "QKTR-L": "Geminate", + "YN3-E3": "Geminate", + "NBPH-N": "Geminate", + "L-HV5C": "Geminate", + "L4X-FH": "Geminate", + "B6-52M": "Geminate", + "V-MZW0": "Geminate", + "BND-16": "Geminate", + "IOO-7O": "Geminate", + "BWF-ZZ": "Geminate", + "4-CUM5": "Geminate", + "8MG-J6": "Geminate", + "RLSI-V": "Geminate", + "39-DGG": "Geminate", + "SV-K8J": "Geminate", + "6RQ9-A": "Geminate", + "K42-IE": "Geminate", + "VSJ-PP": "Geminate", + "3USX-F": "Geminate", + "9-KWXC": "Geminate", + "NQ-9IH": "Geminate", + "KR-V6G": "Geminate", + "AP9-LV": "Geminate", + "0-GZX9": "Geminate", + "2H-TSE": "Geminate", + "4NGK-F": "Geminate", + "O-VWPB": "Geminate", + "LX-ZOJ": "Geminate", + "6L78-1": "Geminate", + "04-LQM": "Geminate", + "4VY-Y1": "Geminate", + "LU-HQS": "Geminate", + "U-L4KS": "Geminate", + "K25-XD": "Geminate", + "6YC-TU": "Geminate", + "Y8R-XZ": "Geminate", + "P-E9GN": "Geminate", + "HJO-84": "Geminate", + "4D9-66": "Geminate", + "L-TOFR": "Geminate", + "Q-TBHW": "Geminate", + "9P4O-F": "Geminate", + "UBX-CC": "Geminate", + "TJM-JJ": "Geminate", + "EOA-ZC": "Geminate", + "G-73MR": "Geminate", + "E-91FV": "Geminate", + "AD-5B8": "Geminate", + "QP0K-B": "Geminate", + "54-MF6": "Geminate", + "D-I9HJ": "Geminate", + "P-6I0B": "Geminate", + "CFYY-J": "Geminate", + "8-KZXQ": "Geminate", + "HKYW-T": "Geminate", + "3SFU-S": "Geminate", + "VJ-NQP": "Geminate", + "U6D-9A": "Geminate", + "Atioth": "Geminate", + "PYY3-5": "Geminate", + "RFGW-V": "Geminate", + "N-HK93": "Geminate", + "LR-2XT": "Geminate", + "TZL-WT": "Geminate", + "4K0N-J": "Geminate", + "B-F1MI": "Geminate", + "W-3BSU": "Geminate", + "BE-UUN": "Geminate", + "O2O-2X": "Geminate", + "JE1-36": "Geminate", + "5F-YRA": "Geminate", + "TDE4-H": "Geminate", + "UER-TH": "Geminate", + "UG-UWZ": "Geminate", + "Hulm": "Heimatar", + "Osoggur": "Heimatar", + "Abudban": "Heimatar", + "Trytedald": "Heimatar", + "Odatrik": "Heimatar", + "Rens": "Heimatar", + "Ameinaka": "Heimatar", + "Alakgur": "Heimatar", + "Dammalin": "Heimatar", + "Bosboger": "Heimatar", + "Olfeim": "Heimatar", + "Lulm": "Heimatar", + "Gulmorogod": "Heimatar", + "Edmalbrurdus": "Heimatar", + "Kronsur": "Heimatar", + "Dumkirinur": "Heimatar", + "Sist": "Heimatar", + "Obrolber": "Heimatar", + "Austraka": "Heimatar", + "Ivar": "Heimatar", + "Meirakulf": "Heimatar", + "Frarn": "Heimatar", + "Illinfrik": "Heimatar", + "Balginia": "Heimatar", + "Gyng": "Heimatar", + "Avesber": "Heimatar", + "Gerek": "Heimatar", + "Tongofur": "Heimatar", + "Gerbold": "Heimatar", + "Rokofur": "Heimatar", + "Ebasgerdur": "Heimatar", + "Ebodold": "Heimatar", + "Amamake": "Heimatar", + "Vard": "Heimatar", + "Siseide": "Heimatar", + "Lantorn": "Heimatar", + "Dal": "Heimatar", + "Auga": "Heimatar", + "Eystur": "Heimatar", + "Pator": "Heimatar", + "Lustrevik": "Heimatar", + "Isendeldik": "Heimatar", + "Ammold": "Heimatar", + "Emolgranlan": "Heimatar", + "Offugen": "Heimatar", + "Roniko": "Heimatar", + "Aralgrund": "Heimatar", + "Eddar": "Heimatar", + "Bogelek": "Heimatar", + "Wiskeber": "Heimatar", + "Eifer": "Heimatar", + "Gusandall": "Heimatar", + "Atgur": "Heimatar", + "Endrulf": "Heimatar", + "Ingunn": "Heimatar", + "Gultratren": "Heimatar", + "Auren": "Heimatar", + "Trer": "Heimatar", + "Egmur": "Heimatar", + "Javrendei": "Heimatar", + "Appen": "Heimatar", + "Klir": "Heimatar", + "Jorus": "Heimatar", + "Onga": "Heimatar", + "Osaumuni": "Heimatar", + "Magiko": "Heimatar", + "Oremmulf": "Heimatar", + "Hurjafren": "Heimatar", + "Vullat": "Heimatar", + "Hrondedir": "Heimatar", + "Sotrenzur": "Heimatar", + "Hrondmund": "Heimatar", + "Bundindus": "Heimatar", + "Otraren": "Heimatar", + "Hedgiviter": "Heimatar", + "Katugumur": "Heimatar", + "Malukker": "Heimatar", + "Hadaugago": "Heimatar", + "Krilmokenur": "Heimatar", + "Todeko": "Heimatar", + "Larkugei": "Heimatar", + "Usteli": "Heimatar", + "Loguttur": "Heimatar", + "1-7KWU": "Impass", + "3-UCBF": "Impass", + "N-CREL": "Impass", + "TM-0P2": "Impass", + "4OIV-X": "Impass", + "Y-JKJ8": "Impass", + "AFJ-NB": "Impass", + "H-64KI": "Impass", + "9I-SRF": "Impass", + "9-IIBL": "Impass", + "5GQ-S9": "Impass", + "YALR-F": "Impass", + "68FT-6": "Impass", + "IV-UNR": "Impass", + "IRE-98": "Impass", + "HOHF-B": "Impass", + "Y-6B0E": "Impass", + "F-3H2P": "Impass", + "DY-40Z": "Impass", + "XWY-YM": "Impass", + "M-9V5D": "Impass", + "O2-39S": "Impass", + "M-VEJZ": "Impass", + "LJK-T0": "Impass", + "E7VE-V": "Impass", + "NUG-OF": "Impass", + "L6BY-P": "Impass", + "U3SQ-X": "Impass", + "01TG-J": "Impass", + "UK-SHL": "Impass", + "A1BK-A": "Impass", + "N-7ECY": "Impass", + "4-MPSJ": "Impass", + "TWJ-AW": "Impass", + "PZMA-E": "Impass", + "442-CS": "Impass", + "Z-N9IP": "Impass", + "9ZFH-Z": "Impass", + "6E-MOW": "Impass", + "GBT4-J": "Impass", + "GZ1-A1": "Impass", + "X-0CKQ": "Impass", + "6B-GKA": "Impass", + "LHGA-W": "Impass", + "4RS-L1": "Impass", + "D-L4H0": "Impass", + "GU-9F4": "Impass", + "FG-1GH": "Impass", + "WFYM-0": "Impass", + "FR-B1H": "Impass", + "DDI-B7": "Impass", + "Pettinck": "Sinq Laison", + "Du Annes": "Sinq Laison", + "Balle": "Sinq Laison", + "Decon": "Sinq Laison", + "Grinacanne": "Sinq Laison", + "Metserel": "Sinq Laison", + "Sharuveil": "Sinq Laison", + "Adreland": "Sinq Laison", + "Erme": "Sinq Laison", + "Aufay": "Sinq Laison", + "Iyen-Oursta": "Sinq Laison", + "Faurent": "Sinq Laison", + "Ambeke": "Sinq Laison", + "Carrou": "Sinq Laison", + "Direrie": "Sinq Laison", + "Ignoitton": "Sinq Laison", + "Ardene": "Sinq Laison", + "Boillair": "Sinq Laison", + "Ney": "Sinq Laison", + "Fasse": "Sinq Laison", + "Ala": "Sinq Laison", + "Gratesier": "Sinq Laison", + "Schoorasana": "Sinq Laison", + "Vylade": "Sinq Laison", + "Auvergne": "Sinq Laison", + "Aunia": "Sinq Laison", + "Agrallarier": "Sinq Laison", + "Dodixie": "Sinq Laison", + "Eglennaert": "Sinq Laison", + "Botane": "Sinq Laison", + "Pulin": "Sinq Laison", + "Foves": "Sinq Laison", + "Alles": "Sinq Laison", + "Misneden": "Sinq Laison", + "Basgerin": "Sinq Laison", + "Chelien": "Sinq Laison", + "Trosquesere": "Sinq Laison", + "Ansone": "Sinq Laison", + "Dunraelare": "Sinq Laison", + "Nausschie": "Sinq Laison", + "Inghenges": "Sinq Laison", + "Estene": "Sinq Laison", + "Gallareue": "Sinq Laison", + "Stayme": "Sinq Laison", + "Parchanier": "Sinq Laison", + "Fluekele": "Sinq Laison", + "Alsottobier": "Sinq Laison", + "Jolia": "Sinq Laison", + "Augnais": "Sinq Laison", + "Deltole": "Sinq Laison", + "Colelie": "Sinq Laison", + "Barmalie": "Sinq Laison", + "Audaerne": "Sinq Laison", + "Dodenvale": "Sinq Laison", + "Olettiers": "Sinq Laison", + "Artisine": "Sinq Laison", + "Chainelant": "Sinq Laison", + "Sileperer": "Sinq Laison", + "Bamiette": "Sinq Laison", + "Crielere": "Sinq Laison", + "Jel": "Sinq Laison", + "Egghelende": "Sinq Laison", + "Odette": "Sinq Laison", + "Ation": "Sinq Laison", + "Stegette": "Sinq Laison", + "Ravarin": "Sinq Laison", + "Aliette": "Sinq Laison", + "Brapelille": "Sinq Laison", + "Bawilan": "Sinq Laison", + "Atier": "Sinq Laison", + "Archee": "Sinq Laison", + "Brybier": "Sinq Laison", + "Adrallezoen": "Sinq Laison", + "Croleur": "Sinq Laison", + "Doussivitte": "Sinq Laison", + "Unel": "Sinq Laison", + "Claysson": "Sinq Laison", + "Auberulle": "Sinq Laison", + "Adiere": "Sinq Laison", + "Stetille": "Sinq Laison", + "Alillere": "Sinq Laison", + "Abenync": "Sinq Laison", + "Pozirblant": "Sinq Laison", + "Bourynes": "Sinq Laison", + "Aurcel": "Sinq Laison", + "Aymaerne": "Sinq Laison", + "Rancer": "Sinq Laison", + "Miroitem": "Sinq Laison", + "Thelan": "Sinq Laison", + "Rorsins": "Sinq Laison", + "Lamadent": "Sinq Laison", + "Otou": "Sinq Laison", + "Assiettes": "Sinq Laison", + "Goinard": "Sinq Laison", + "Raeghoscon": "Sinq Laison", + "Allipes": "Sinq Laison", + "Lermireve": "Sinq Laison", + "Aetree": "Sinq Laison", + "Esmes": "Sinq Laison", + "Vittenyn": "Sinq Laison", + "Mirilene": "Sinq Laison", + "Pucherie": "Sinq Laison", + "Fricoure": "Sinq Laison", + "Caretyn": "Sinq Laison", + "Ainaille": "Sinq Laison", + "Odotte": "Sinq Laison", + "Oirtlair": "Sinq Laison", + "Olelon": "Sinq Laison", + "Trossere": "Sinq Laison", + "Konola": "The Citadel", + "Inoue": "The Citadel", + "Isaziwa": "The Citadel", + "Eitu": "The Citadel", + "Horkkisen": "The Citadel", + "Erila": "The Citadel", + "Ohvosamon": "The Citadel", + "Auviken": "The Citadel", + "Saikanen": "The Citadel", + "Oijamon": "The Citadel", + "Kakki": "The Citadel", + "Jeras": "The Citadel", + "Kausaaja": "The Citadel", + "Oiniken": "The Citadel", + "Kaimon": "The Citadel", + "Ahynada": "The Citadel", + "Aikoro": "The Citadel", + "Alikara": "The Citadel", + "Usi": "The Citadel", + "Ishomilken": "The Citadel", + "Nikkishina": "The Citadel", + "Hasama": "The Citadel", + "Uuna": "The Citadel", + "Manjonakko": "The Citadel", + "Kassigainen": "The Citadel", + "Yashunen": "The Citadel", + "Tennen": "The Citadel", + "Hatakani": "The Citadel", + "Sivala": "The Citadel", + "Iivinen": "The Citadel", + "Kubinen": "The Citadel", + "Uedama": "The Citadel", + "Enderailen": "The Citadel", + "Tunudan": "The Citadel", + "Kulelen": "The Citadel", + "Rairomon": "The Citadel", + "Hogimo": "The Citadel", + "Huttaken": "The Citadel", + "Paara": "The Citadel", + "Annaro": "The Citadel", + "Isutaka": "The Citadel", + "Tasabeshi": "The Citadel", + "Ono": "The Citadel", + "Muvolailen": "The Citadel", + "Halaima": "The Citadel", + "Kamio": "The Citadel", + "Sankkasen": "The Citadel", + "Tintoh": "The Citadel", + "Santola": "The Citadel", + "Ikao": "The Citadel", + "Waira": "The Citadel", + "Inaro": "The Citadel", + "Kaaputenen": "The Citadel", + "Waskisen": "The Citadel", + "Sirppala": "The Citadel", + "Irjunen": "The Citadel", + "Inari": "The Citadel", + "Yria": "The Citadel", + "Oshaima": "The Citadel", + "Hysera": "The Citadel", + "Kaunokka": "The Citadel", + "Venilen": "The Citadel", + "Oisio": "The Citadel", + "Haatomo": "The Citadel", + "Suroken": "The Citadel", + "Kusomonmon": "The Citadel", + "Juunigaishi": "The Citadel", + "Isikesu": "The Citadel", + "Anttiri": "The Citadel", + "Hasmijaala": "The Citadel", + "Nagamanen": "The Citadel", + "Oto": "The Citadel", + "Sujarento": "The Citadel", + "Eranakko": "The Citadel", + "Onatoh": "The Citadel", + "Tannolen": "The Citadel", + "Tama": "The Citadel", + "Uotila": "The Citadel", + "Isenairos": "The Citadel", + "Saila": "The Citadel", + "Aramachi": "The Citadel", + "Oichiya": "The Citadel", + "Motsu": "The Citadel", + "Komo": "The Citadel", + "Urhinichi": "The Citadel", + "Laah": "The Citadel", + "N-JK02": "The Kalevala Expanse", + "JT2I-7": "The Kalevala Expanse", + "XTJ-5Q": "The Kalevala Expanse", + "1-KCSA": "The Kalevala Expanse", + "UJXC-B": "The Kalevala Expanse", + "UDVW-O": "The Kalevala Expanse", + "F48K-D": "The Kalevala Expanse", + "FBH-JN": "The Kalevala Expanse", + "BVRQ-O": "The Kalevala Expanse", + "QX-4HO": "The Kalevala Expanse", + "LS3-HP": "The Kalevala Expanse", + "SH6X-F": "The Kalevala Expanse", + "6V-D0E": "The Kalevala Expanse", + "SG-3HY": "The Kalevala Expanse", + "AU2V-J": "The Kalevala Expanse", + "SY-0AM": "The Kalevala Expanse", + "A-YB15": "The Kalevala Expanse", + "QZX-L9": "The Kalevala Expanse", + "D-6PKO": "The Kalevala Expanse", + "RAI-0E": "The Kalevala Expanse", + "MN9P-A": "The Kalevala Expanse", + "TA9T-P": "The Kalevala Expanse", + "L-TLFU": "The Kalevala Expanse", + "BM-VYZ": "The Kalevala Expanse", + "Q-GICU": "The Kalevala Expanse", + "EPCD-D": "The Kalevala Expanse", + "0S1-GI": "The Kalevala Expanse", + "L-GY1B": "The Kalevala Expanse", + "74-DRC": "The Kalevala Expanse", + "LE-67X": "The Kalevala Expanse", + "B1UE-J": "The Kalevala Expanse", + "O31W-6": "The Kalevala Expanse", + "M3-H2Y": "The Kalevala Expanse", + "G-KCFT": "The Kalevala Expanse", + "WNM-V0": "The Kalevala Expanse", + "6FS-CZ": "The Kalevala Expanse", + "HPV-RJ": "The Kalevala Expanse", + "H7S-5I": "The Kalevala Expanse", + "C3J0-O": "The Kalevala Expanse", + "GSO-SR": "The Kalevala Expanse", + "B3ZU-H": "The Kalevala Expanse", + "G4-QU6": "The Kalevala Expanse", + "V2-GZS": "The Kalevala Expanse", + "HD-HOZ": "The Kalevala Expanse", + "42G-OB": "The Kalevala Expanse", + "LEM-I1": "The Kalevala Expanse", + "1S-SU1": "The Kalevala Expanse", + "ND-GL4": "The Kalevala Expanse", + "9-0QB7": "The Kalevala Expanse", + "M-75WN": "The Kalevala Expanse", + "PNFW-O": "The Kalevala Expanse", + "HVGR-R": "The Kalevala Expanse", + "K76A-3": "The Kalevala Expanse", + "K95-9I": "The Kalevala Expanse", + "R1O-GN": "The Kalevala Expanse", + "GQ-7SP": "The Kalevala Expanse", + "BGMZ-0": "The Kalevala Expanse", + "I2D3-5": "The Kalevala Expanse", + "FZX-PU": "The Kalevala Expanse", + "O9K-FT": "The Kalevala Expanse", + "RQOO-U": "The Kalevala Expanse", + "FB5U-I": "The Kalevala Expanse", + "BZ-BCK": "The Kalevala Expanse", + "5-VFC6": "The Kalevala Expanse", + "O5-YNW": "The Kalevala Expanse", + "86L-9F": "The Kalevala Expanse", + "IUU3-L": "The Kalevala Expanse", + "J-OAH2": "The Kalevala Expanse", + "S-LHPJ": "The Kalevala Expanse", + "4U90-Z": "Deklein", + "T-945F": "Deklein", + "FO8M-2": "Deklein", + "AD-CBT": "Deklein", + "QPO-WI": "Deklein", + "R8S-1K": "Deklein", + "94-H3F": "Deklein", + "CU9-T0": "Deklein", + "XCF-8N": "Deklein", + "FMB-JP": "Deklein", + "0P-F3K": "Deklein", + "K5F-Z2": "Deklein", + "TXME-A": "Deklein", + "YA0-XJ": "Deklein", + "2-KF56": "Deklein", + "VFK-IV": "Deklein", + "2R-CRW": "Deklein", + "CCP-US": "Deklein", + "II-5O9": "Deklein", + "I30-3A": "Deklein", + "2O9G-D": "Deklein", + "NC-N3F": "Deklein", + "JU-OWQ": "Deklein", + "S-DN5M": "Deklein", + "MXX5-9": "Deklein", + "ZZZR-5": "Deklein", + "C7Y-7Z": "Deklein", + "X-Z4DA": "Deklein", + "3OAT-Q": "Deklein", + "N-TFXK": "Deklein", + "33RB-O": "Deklein", + "DKUK-G": "Deklein", + "3QE-9Q": "Deklein", + "E-FIC0": "Deklein", + "ZOYW-O": "Deklein", + "85-B52": "Deklein", + "YZ-UKA": "Deklein", + "RO0-AF": "Deklein", + "5W3-DG": "Deklein", + "LT-DRO": "Deklein", + "7T6P-C": "Deklein", + "8S28-3": "Deklein", + "E3UY-6": "Deklein", + "LEK-N5": "Deklein", + "AGG-NR": "Deklein", + "0V0R-R": "Deklein", + "O-2RNZ": "Deklein", + "OWXT-5": "Deklein", + "3JN9-Q": "Deklein", + "3T7-M8": "Deklein", + "WUZ-WM": "Deklein", + "MZ1E-P": "Deklein", + "43B-O1": "Deklein", + "J1AU-9": "Deklein", + "X3-PBC": "Deklein", + "4N-BUI": "Deklein", + "N2IS-B": "Deklein", + "XCBK-X": "Deklein", + "GY5-26": "Deklein", + "VPLL-N": "Deklein", + "9CK-KZ": "Deklein", + "5S-KXA": "Deklein", + "U-TJ7Y": "Deklein", + "A4L-A2": "Deklein", + "CZDJ-1": "Deklein", + "RG9-7U": "Deklein", + "UJY-HE": "Deklein", + "UEJX-G": "Deklein", + "Tzvi": "Devoid", + "Raa": "Devoid", + "Sifilar": "Devoid", + "Arzad": "Devoid", + "Oyeman": "Devoid", + "Ezzara": "Devoid", + "Odin": "Devoid", + "Esescama": "Devoid", + "Choonka": "Devoid", + "Thasinaz": "Devoid", + "Dihra": "Devoid", + "Dital": "Devoid", + "Eredan": "Devoid", + "Ohide": "Devoid", + "Sasoutikh": "Devoid", + "Gheth": "Devoid", + "Lisudeh": "Devoid", + "Mehatoor": "Devoid", + "Roushzar": "Devoid", + "Labapi": "Devoid", + "Arayar": "Devoid", + "Asghed": "Devoid", + "Tararan": "Devoid", + "Sosan": "Devoid", + "Halmah": "Devoid", + "Rahadalon": "Devoid", + "Soosat": "Devoid", + "Ibash": "Devoid", + "Itsyamil": "Devoid", + "Mendori": "Devoid", + "Ussad": "Devoid", + "Nakatre": "Devoid", + "Laddiaha": "Devoid", + "Hakshma": "Devoid", + "Uadelah": "Devoid", + "Akes": "Devoid", + "Riavayed": "Devoid", + "Hati": "Devoid", + "Naeel": "Devoid", + "Lower Debyl": "Devoid", + "Ehnoum": "Devoid", + "Upper Debyl": "Devoid", + "Shastal": "Devoid", + "Thakala": "Devoid", + "Mili": "Devoid", + "Faktun": "Devoid", + "Halenan": "Devoid", + "Ulerah": "Devoid", + "Uktiad": "Devoid", + "Nidebora": "Devoid", + "Arveyil": "Devoid", + "Palpis": "Devoid", + "Arnatele": "Everyshore", + "Halle": "Everyshore", + "Mormoen": "Everyshore", + "Amattens": "Everyshore", + "Jurlesel": "Everyshore", + "Bereye": "Everyshore", + "Aice": "Everyshore", + "Junsoraert": "Everyshore", + "Harerget": "Everyshore", + "Azer": "Everyshore", + "Cherore": "Everyshore", + "Torvi": "Everyshore", + "Mosson": "Everyshore", + "Mya": "Everyshore", + "Gerper": "Everyshore", + "Marosier": "Everyshore", + "Lirsautton": "Everyshore", + "Blameston": "Everyshore", + "Vaurent": "Everyshore", + "Aclan": "Everyshore", + "Jaschercis": "Everyshore", + "Ardallabier": "Everyshore", + "Athinard": "Everyshore", + "Meves": "Everyshore", + "Ethernity": "Everyshore", + "Mattere": "Everyshore", + "Gicodel": "Everyshore", + "Frarolle": "Everyshore", + "Quier": "Everyshore", + "Atlanins": "Everyshore", + "Leremblompes": "Everyshore", + "Bille": "Everyshore", + "Colcer": "Everyshore", + "Alachene": "Everyshore", + "Uphene": "Everyshore", + "Elarel": "Everyshore", + "Enedore": "Everyshore", + "Angymonne": "Everyshore", + "Averon": "Everyshore", + "Carirgnottin": "Everyshore", + "Laic": "Everyshore", + "Odixie": "Everyshore", + "Antollare": "Everyshore", + "Tolle": "Everyshore", + "Avele": "Everyshore", + "Scuelazyns": "Everyshore", + "Aydoteaux": "Everyshore", + "Muer": "Everyshore", + "Groothese": "Everyshore", + "Olide": "Everyshore", + "Adeel": "Everyshore", + "Mannar": "Everyshore", + "Mormelot": "Everyshore", + "Angatalie": "Everyshore", + "Lamaa": "The Bleak Lands", + "Tuomuta": "The Bleak Lands", + "Otelen": "The Bleak Lands", + "Kuomi": "The Bleak Lands", + "Huola": "The Bleak Lands", + "Kourmonen": "The Bleak Lands", + "Kamela": "The Bleak Lands", + "Sosala": "The Bleak Lands", + "Anka": "The Bleak Lands", + "Iesa": "The Bleak Lands", + "Netsalakka": "The Bleak Lands", + "Sasiekko": "The Bleak Lands", + "Myyhera": "The Bleak Lands", + "Gammel": "The Bleak Lands", + "Uusanen": "The Bleak Lands", + "Erkinen": "The Bleak Lands", + "Saikamon": "The Bleak Lands", + "Jarkkolen": "The Bleak Lands", + "Ronne": "The Bleak Lands", + "Hatori": "The Bleak Lands", + "Junsen": "The Bleak Lands", + "Malpara": "The Bleak Lands", + "Hakodan": "The Bleak Lands", + "Sahtogas": "The Bleak Lands", + "Haras": "The Bleak Lands", + "Oyonata": "The Bleak Lands", + "Kurniainen": "The Bleak Lands", + "Saidusairos": "The Bleak Lands", + "Tannakan": "The Bleak Lands", + "Komaa": "The Bleak Lands", + "Ayeroilen": "The Bleak Lands", + "Imata": "The Bleak Lands", + "Furskeshin": "The Bleak Lands", + "Kurmaru": "The Bleak Lands", + "Satalama": "The Bleak Lands", + "VYJ-DA": "Esoteria", + "HHQ-M1": "Esoteria", + "A-CJGE": "Esoteria", + "G2-INZ": "Esoteria", + "WAC-HW": "Esoteria", + "HT4K-M": "Esoteria", + "RBW-8G": "Esoteria", + "4-OUKF": "Esoteria", + "HAJ-DQ": "Esoteria", + "JAUD-V": "Esoteria", + "DTX8-M": "Esoteria", + "C9N-CC": "Esoteria", + "X-7BIX": "Esoteria", + "5-9UXZ": "Esoteria", + "Q0OH-V": "Esoteria", + "C-VZAK": "Esoteria", + "0-O6XF": "Esoteria", + "D-FVI7": "Esoteria", + "VL7-60": "Esoteria", + "NH-R5B": "Esoteria", + "FN-GFQ": "Esoteria", + "XKZ8-H": "Esoteria", + "WX-6UX": "Esoteria", + "BZ-0GW": "Esoteria", + "16P-PX": "Esoteria", + "CR-0E5": "Esoteria", + "Z-Y9C3": "Esoteria", + "A1-AUH": "Esoteria", + "F-UVBV": "Esoteria", + "R-FM0G": "Esoteria", + "TEIZ-C": "Esoteria", + "VUAC-Y": "Esoteria", + "V-XANH": "Esoteria", + "450I-W": "Esoteria", + "OIOM-Y": "Esoteria", + "G-YZUX": "Esoteria", + "CZ6U-1": "Esoteria", + "D-PNP9": "Esoteria", + "E1UU-3": "Esoteria", + "P-3XVV": "Esoteria", + "BY-MSY": "Esoteria", + "6EK-BV": "Esoteria", + "IR-FDV": "Esoteria", + "NIZJ-0": "Esoteria", + "J-RVGD": "Esoteria", + "V1ZC-S": "Esoteria", + "H-T40Z": "Esoteria", + "6-TYRX": "Esoteria", + "Q1-R7K": "Esoteria", + "111-F1": "Esoteria", + "JD-TYH": "Esoteria", + "02V-BK": "Esoteria", + "A5MT-B": "Esoteria", + "R-ARKN": "Esoteria", + "SN9S-N": "Esoteria", + "MS2-V8": "Esoteria", + "Z-MO29": "Esoteria", + "G-JC9R": "Esoteria", + "DIBH-Q": "Esoteria", + "DNEP-Y": "Esoteria", + "YAP-TN": "Esoteria", + "PE-H02": "Esoteria", + "H-YHYM": "Esoteria", + "G-4H4C": "Esoteria", + "HHE5-L": "Esoteria", + "P9F-ZG": "Esoteria", + "QFGB-E": "Esoteria", + "7P-J38": "Esoteria", + "WT-2J9": "Esoteria", + "PK-PHZ": "Esoteria", + "L-M6JK": "Esoteria", + "C-PEWN": "Esoteria", + "DL-CDY": "Esoteria", + "29YH-V": "Esoteria", + "LG-RO2": "Esoteria", + "X-HISR": "Esoteria", + "QS-530": "Esoteria", + "VR-YRV": "Esoteria", + "IPX-H5": "Esoteria", + "KSM-1T": "Esoteria", + "YRV-MZ": "Esoteria", + "6SB-BN": "Esoteria", + "B1D-KU": "Esoteria", + "QFIU-K": "Esoteria", + "2R-KLH": "Esoteria", + "QB-AE6": "Oasa", + "G-W1ND": "Oasa", + "MZLW-9": "Oasa", + "ND-X7X": "Oasa", + "DGDT-3": "Oasa", + "2-WNTD": "Oasa", + "83-YGI": "Oasa", + "KH-EWC": "Oasa", + "3VL6-I": "Oasa", + "F-816R": "Oasa", + "DS3-6A": "Oasa", + "V0-H4L": "Oasa", + "T-HMWP": "Oasa", + "DYS-CG": "Oasa", + "MTGF-2": "Oasa", + "0-QP56": "Oasa", + "GTQ-C9": "Oasa", + "M-NWLB": "Oasa", + "ORB4-J": "Oasa", + "GGMF-J": "Oasa", + "IG-4OF": "Oasa", + "LQQH-J": "Oasa", + "W5-VBR": "Oasa", + "J-D5U7": "Oasa", + "Y-770C": "Oasa", + "X-Z4JW": "Oasa", + "R8WV-7": "Oasa", + "6U-MFQ": "Oasa", + "1EO-OE": "Oasa", + "YQTK-R": "Oasa", + "FZCR-3": "Oasa", + "5-9L3H": "Oasa", + "1-HDQ4": "Oasa", + "WVMS-X": "Oasa", + "7-UVMT": "Oasa", + "R-ZESX": "Oasa", + "IO-R2S": "Oasa", + "HF-K3O": "Oasa", + "QE2-FS": "Oasa", + "Q-ITV5": "Oasa", + "5JEZ-I": "Oasa", + "XEF6-Z": "Oasa", + "SON-TW": "Oasa", + "V-X0KM": "Oasa", + "U9SE-N": "Oasa", + "XXZ-3W": "Oasa", + "RF-X7V": "Oasa", + "BQ0-UU": "Oasa", + "3-JG3X": "Oasa", + "GK3-RX": "Oasa", + "1P-QWR": "Oasa", + "FJ-GUR": "Oasa", + "UGR-J2": "Oasa", + "QZ-DIZ": "Oasa", + "Y-0HVF": "Oasa", + "21M1-B": "Oasa", + "KED-2O": "Oasa", + "U-RELP": "Oasa", + "IAMJ-Q": "Oasa", + "E6Q-LE": "Oasa", + "HO4E-Q": "Oasa", + "QY2Y-N": "Oasa", + "X-9ZZR": "Oasa", + "RO-AIQ": "Oasa", + "VZEG-B": "Oasa", + "P-ZWKH": "Oasa", + "9G5J-1": "Oasa", + "B-ETDW": "Oasa", + "0PU2-R": "Oasa", + "XM-RMD": "Oasa", + "91-KD8": "Oasa", + "OZ-DS5": "Oasa", + "LA2-KV": "Oasa", + "WW-OVQ": "Oasa", + "S7WI-F": "Oasa", + "1-BK1Q": "Oasa", + "X-CYNC": "Oasa", + "RJBC-I": "Oasa", + "H-MHWF": "Oasa", + "PND-SI": "Oasa", + "XKM-DE": "Oasa", + "JXQJ-B": "Oasa", + "Y-BIPM": "Oasa", + "QYT-X8": "Oasa", + "5-IH57": "Oasa", + "MHC-R3": "Syndicate", + "F67E-Q": "Syndicate", + "6E-578": "Syndicate", + "Poitot": "Syndicate", + "ZVN5-H": "Syndicate", + "ATY-2U": "Syndicate", + "X-BV98": "Syndicate", + "2X-PQG": "Syndicate", + "FD-MLJ": "Syndicate", + "PF-346": "Syndicate", + "X-M2LR": "Syndicate", + "K5-JRD": "Syndicate", + "6-CZ49": "Syndicate", + "EZA-FM": "Syndicate", + "8-JYPM": "Syndicate", + "PVH8-0": "Syndicate", + "M2-CF1": "Syndicate", + "JH-M2W": "Syndicate", + "PC9-AY": "Syndicate", + "T22-QI": "Syndicate", + "X-PYH5": "Syndicate", + "ZN0-SR": "Syndicate", + "5-DSFH": "Syndicate", + "AK-QBU": "Syndicate", + "QWF-6P": "Syndicate", + "AAS-8R": "Syndicate", + "V4-L0X": "Syndicate", + "PFP-GU": "Syndicate", + "0EK-NJ": "Syndicate", + "1-NKVT": "Syndicate", + "UM-Q7F": "Syndicate", + "T-LIWS": "Syndicate", + "KTHT-O": "Syndicate", + "97X-CH": "Syndicate", + "5-T0PZ": "Syndicate", + "6R-PWU": "Syndicate", + "2Q-I6Q": "Syndicate", + "A-ZLHX": "Syndicate", + "UTKS-5": "Syndicate", + "Y9G-KS": "Syndicate", + "I-YGGI": "Syndicate", + "VV-VCR": "Syndicate", + "5-75MB": "Syndicate", + "IIRH-G": "Syndicate", + "35-RK9": "Syndicate", + "XS-XAY": "Syndicate", + "DP34-U": "Syndicate", + "617I-I": "Syndicate", + "6-U2M8": "Syndicate", + "I0AB-R": "Syndicate", + "MXYS-8": "Syndicate", + "A-3ES3": "Syndicate", + "8V-SJJ": "Syndicate", + "5-FGQI": "Syndicate", + "3KNK-A": "Syndicate", + "TXW-EI": "Syndicate", + "3MOG-V": "Syndicate", + "NG-C6Y": "Syndicate", + "XYY-IA": "Syndicate", + "BMNV-P": "Syndicate", + "BY-S36": "Syndicate", + "31-MLU": "Syndicate", + "0LTQ-C": "Syndicate", + "A9D-R0": "Syndicate", + "2P-4LS": "Syndicate", + "RF-GGF": "Syndicate", + "LSC4-P": "Syndicate", + "A-SJ8X": "Syndicate", + "10UZ-P": "Syndicate", + "EN-VOD": "Syndicate", + "9GYL-O": "Syndicate", + "VLGD-R": "Syndicate", + "S-GKKR": "Syndicate", + "9U-TTJ": "Syndicate", + "Y-W6GF": "Syndicate", + "KFR-ZE": "Syndicate", + "KLYN-8": "Syndicate", + "D85-VD": "Syndicate", + "5-VKCN": "Syndicate", + "U0V6-T": "Syndicate", + "5KS-AB": "Syndicate", + "0T-AMZ": "Syndicate", + "57-YRU": "Syndicate", + "4L-E5P": "Syndicate", + "UFXF-C": "Syndicate", + "RLL-9R": "Syndicate", + "51-5XG": "Syndicate", + "EF-F36": "Syndicate", + "3-IN0V": "Syndicate", + "Z-QENW": "Syndicate", + "D-B7YK": "Syndicate", + "DUV-5Y": "Syndicate", + "GRNJ-3": "Syndicate", + "VSIG-K": "Syndicate", + "RSS-KA": "Syndicate", + "CIS-7X": "Syndicate", + "DCHR-L": "Syndicate", + "EU0I-T": "Syndicate", + "4-JWWQ": "Syndicate", + "G-6SXJ": "Syndicate", + "S-U8A4": "Syndicate", + "ZV-72W": "Syndicate", + "2G38-I": "Syndicate", + "CY-ZLP": "Syndicate", + "U4-Q2V": "Syndicate", + "98Q-8O": "Syndicate", + "Bei": "Metropolis", + "Uttindar": "Metropolis", + "Hagilur": "Metropolis", + "Anher": "Metropolis", + "Ragnarg": "Metropolis", + "Hek": "Metropolis", + "Hror": "Metropolis", + "Amo": "Metropolis", + "Resbroko": "Metropolis", + "Hadozeko": "Metropolis", + "Ardar": "Metropolis", + "Auner": "Metropolis", + "Evati": "Metropolis", + "Ofstold": "Metropolis", + "Todifrauan": "Metropolis", + "Helgatild": "Metropolis", + "Arnstur": "Metropolis", + "Lasleinur": "Metropolis", + "Arnher": "Metropolis", + "Brin": "Metropolis", + "Nakugard": "Metropolis", + "Traun": "Metropolis", + "Uriok": "Metropolis", + "Barkrik": "Metropolis", + "Inder": "Metropolis", + "Tvink": "Metropolis", + "Lanngisi": "Metropolis", + "Hjoramold": "Metropolis", + "Dudreda": "Metropolis", + "Hakisalki": "Metropolis", + "Arwa": "Metropolis", + "Krirald": "Metropolis", + "Arifsdald": "Metropolis", + "Ansen": "Metropolis", + "Floseswin": "Metropolis", + "Uisper": "Metropolis", + "Aset": "Metropolis", + "Eytjangard": "Metropolis", + "Turnur": "Metropolis", + "Isbrabata": "Metropolis", + "Vimeini": "Metropolis", + "Avenod": "Metropolis", + "Frerstorn": "Metropolis", + "Ontorn": "Metropolis", + "Sirekur": "Metropolis", + "Gebuladi": "Metropolis", + "Ebolfer": "Metropolis", + "Eszur": "Metropolis", + "Hofjaldgund": "Metropolis", + "Klogori": "Metropolis", + "Orfrold": "Metropolis", + "Egmar": "Metropolis", + "Taff": "Metropolis", + "Ualkin": "Metropolis", + "Gukarla": "Metropolis", + "Arlulf": "Metropolis", + "Brundakur": "Metropolis", + "Stirht": "Metropolis", + "Illuin": "Metropolis", + "Nedegulf": "Metropolis", + "Aldilur": "Metropolis", + "Alf": "Metropolis", + "Eust": "Metropolis", + "Flost": "Metropolis", + "Todrir": "Metropolis", + "Asgeir": "Metropolis", + "Evuldgenzo": "Metropolis", + "Ongund": "Metropolis", + "Jondik": "Metropolis", + "Olbra": "Metropolis", + "Altrinur": "Metropolis", + "Vilur": "Metropolis", + "Reset": "Metropolis", + "Eygfe": "Metropolis", + "Eiluvodi": "Metropolis", + "Freatlidur": "Metropolis", + "Roleinn": "Metropolis", + "Maturat": "Metropolis", + "Bongveber": "Metropolis", + "Anbald": "Metropolis", + "Vorsk": "Metropolis", + "Hjortur": "Metropolis", + "Egbonbet": "Metropolis", + "Totkubad": "Metropolis", + "Meimungen": "Metropolis", + "Agtver": "Metropolis", + "Datulen": "Metropolis", + "Situner": "Metropolis", + "Tamekamur": "Metropolis", + "Evettullur": "Metropolis", + "Leurtmar": "Metropolis", + "Ryddinjorn": "Metropolis", + "Arlek": "Metropolis", + "Elgoi": "Metropolis", + "Eram": "Metropolis", + "Yrmori": "Metropolis", + "Aldagolf": "Metropolis", + "Aldrat": "Metropolis", + "Urnhard": "Metropolis", + "Hardbako": "Metropolis", + "Erstur": "Metropolis", + "Fredagod": "Metropolis", + "Libold": "Metropolis", + "Wirdalen": "Metropolis", + "Nein": "Metropolis", + "Enden": "Metropolis", + "Erstet": "Metropolis", + "Anstard": "Metropolis", + "Osvestmunnur": "Metropolis", + "Hilfhurmur": "Metropolis", + "Geffur": "Metropolis", + "Oppold": "Metropolis", + "Tratokard": "Metropolis", + "Lumegen": "Metropolis", + "Gedugaud": "Metropolis", + "Polstodur": "Metropolis", + "Hebisa": "Metropolis", + "Tollus": "Metropolis", + "Ogoten": "Metropolis", + "Earled": "Metropolis", + "Aderkan": "Metropolis", + "Ansher": "Metropolis", + "Earwik": "Metropolis", + "Finanar": "Metropolis", + "Moselgi": "Metropolis", + "Mateber": "Metropolis", + "Iluin": "Metropolis", + "Ofage": "Metropolis", + "Josekorn": "Metropolis", + "Nifflung": "Metropolis", + "Hakeri": "Metropolis", + "Oraekja": "Metropolis", + "Dantbeinn": "Metropolis", + "Irgrus": "Metropolis", + "Orduin": "Metropolis", + "Engosi": "Metropolis", + "Atonder": "Metropolis", + "Hotrardik": "Metropolis", + "Ridoner": "Metropolis", + "Klaevik": "Metropolis", + "Lirerim": "Metropolis", + "Offikatlin": "Metropolis", + "Diromitur": "Metropolis", + "Eldjaerin": "Metropolis", + "Erlendur": "Metropolis", + "Aldik": "Metropolis", + "Tabbetzur": "Metropolis", + "Eurgrana": "Metropolis", + "Frulegur": "Metropolis", + "Hroduko": "Metropolis", + "Hodrold": "Metropolis", + "Odebeinn": "Metropolis", + "Konora": "Metropolis", + "Erindur": "Metropolis", + "Abrat": "Metropolis", + "Orgron": "Metropolis", + "Embod": "Metropolis", + "Erego": "Metropolis", + "Fildar": "Metropolis", + "Amarr": "Domain", + "Boranai": "Domain", + "Hedion": "Domain", + "Mabnen": "Domain", + "Toshabia": "Domain", + "Irnin": "Domain", + "Kehour": "Domain", + "Martha": "Domain", + "Simbeloud": "Domain", + "Ebidan": "Domain", + "Akhragan": "Domain", + "Mikhir": "Domain", + "Bashakru": "Domain", + "Sukirah": "Domain", + "Shuria": "Domain", + "Narai": "Domain", + "Ziona": "Domain", + "Gaha": "Domain", + "Armala": "Domain", + "Murema": "Domain", + "Cailanar": "Domain", + "Ilonarav": "Domain", + "Uchat": "Domain", + "Joppaya": "Domain", + "Pelkia": "Domain", + "Raren": "Domain", + "Mazitah": "Domain", + "Hiramu": "Domain", + "Sakhti": "Domain", + "Aldali": "Domain", + "Hutian": "Domain", + "Noli": "Domain", + "Nomash": "Domain", + "Aghesi": "Domain", + "Fabin": "Domain", + "Airshaz": "Domain", + "Patzcha": "Domain", + "Charra": "Domain", + "Harva": "Domain", + "Thebeka": "Domain", + "Rasile": "Domain", + "Nererut": "Domain", + "Sitanan": "Domain", + "Vashkah": "Domain", + "Ardishapur Prime": "Domain", + "Gid": "Domain", + "Dakba": "Domain", + "Nifshed": "Domain", + "Shumam": "Domain", + "Milal": "Domain", + "Sobenah": "Domain", + "Bourar": "Domain", + "Rammi": "Domain", + "Arodan": "Domain", + "Rimbah": "Domain", + "Mamenkhanar": "Domain", + "Seiradih": "Domain", + "Arera": "Domain", + "Hizhara": "Domain", + "Neziel": "Domain", + "Ahala": "Domain", + "Knophtikoo": "Domain", + "Ruchy": "Domain", + "Hai": "Domain", + "Sadye": "Domain", + "Bika": "Domain", + "Arshat": "Domain", + "Jerma": "Domain", + "Miyeli": "Domain", + "Reyi": "Domain", + "Moussou": "Domain", + "Nadohman": "Domain", + "Sahdil": "Domain", + "Esteban": "Domain", + "Luromooh": "Domain", + "Nalu": "Domain", + "Jarshitsan": "Domain", + "Hadonoo": "Domain", + "Azizora": "Domain", + "Ahmak": "Domain", + "Shabura": "Domain", + "Adia": "Domain", + "Ebo": "Domain", + "Avair": "Domain", + "Rayl": "Domain", + "Asoutar": "Domain", + "Porsharrah": "Domain", + "Tastela": "Domain", + "Clarelam": "Domain", + "Isamm": "Domain", + "Ebtesham": "Domain", + "Artoun": "Domain", + "Safizon": "Domain", + "Zatsyaki": "Domain", + "Eba": "Domain", + "Bhizheba": "Domain", + "Fahruni": "Domain", + "Sahda": "Domain", + "Naguton": "Domain", + "Ealur": "Domain", + "Shajarleg": "Domain", + "Basan": "Domain", + "Akila": "Domain", + "Amod": "Domain", + "Unefsih": "Domain", + "Mista": "Domain", + "Valmu": "Domain", + "Sibot": "Domain", + "Andabiar": "Domain", + "Kheram": "Domain", + "Arbaz": "Domain", + "Penirgman": "Domain", + "Chaven": "Domain", + "Khopa": "Domain", + "Ashab": "Domain", + "Orkashu": "Domain", + "Youl": "Domain", + "Ekid": "Domain", + "Raravoss": "Domain", + "Nakri": "Domain", + "Zaimeth": "Domain", + "Sharhelund": "Domain", + "Mai": "Domain", + "Sharji": "Domain", + "Kudi": "Domain", + "Bahromab": "Domain", + "Madirmilire": "Domain", + "Niarja": "Domain", + "Fabum": "Domain", + "Saana": "Domain", + "Teshi": "Domain", + "Sayartchen": "Domain", + "Gosalav": "Domain", + "Sorzielang": "Domain", + "Somouh": "Domain", + "Abaim": "Domain", + "Ides": "Domain", + "Yeeramoun": "Domain", + "Anila": "Domain", + "Pedel": "Domain", + "Etav": "Domain", + "Saheri": "Domain", + "Lahnina": "Domain", + "Mahrokht": "Domain", + "Alkabsi": "Domain", + "Sarum Prime": "Domain", + "Hama": "Domain", + "Irnal": "Domain", + "Bagodan": "Domain", + "Murzi": "Domain", + "Chesoh": "Domain", + "Herila": "Domain", + "Chemilip": "Domain", + "Raravath": "Domain", + "Hisoufad": "Domain", + "Jesoyeh": "Domain", + "Hahda": "Domain", + "Namaili": "Domain", + "Afivad": "Domain", + "Uzigh": "Domain", + "Erzoh": "Domain", + "Merz": "Domain", + "Miakie": "Domain", + "Sirkahri": "Domain", + "Faswiba": "Domain", + "Hayumtom": "Domain", + "Zanka": "Domain", + "Galeh": "Domain", + "Yuhelia": "Domain", + "Maiah": "Domain", + "Hamse": "Domain", + "Barira": "Domain", + "Lashkai": "Domain", + "Zhilshinou": "Domain", + "Jaswelu": "Domain", + "Ana": "Domain", + "Warouh": "Domain", + "Jambu": "Domain", + "Bittanshal": "Domain", + "Arton": "Domain", + "Sieh": "Domain", + "Madimal": "Domain", + "Mamet": "Domain", + "Hoshoun": "Domain", + "Biphi": "Domain", + "Ziriert": "Domain", + "Misaba": "Domain", + "Rephirib": "Domain", + "Deepari": "Domain", + "Fora": "Domain", + "Hanan": "Domain", + "Horir": "Domain", + "Conomette": "Solitude", + "Aimoguier": "Solitude", + "Yveve": "Solitude", + "Meunvon": "Solitude", + "Cadelanne": "Solitude", + "Elore": "Solitude", + "Anckee": "Solitude", + "Vevelonel": "Solitude", + "Pertnineere": "Solitude", + "Boystin": "Solitude", + "Lour": "Solitude", + "Maire": "Solitude", + "Oerse": "Solitude", + "Octanneve": "Solitude", + "Larryn": "Solitude", + "Niballe": "Solitude", + "Postouvin": "Solitude", + "Odinesyn": "Solitude", + "Weraroix": "Solitude", + "Sarline": "Solitude", + "Aeter": "Solitude", + "Gererique": "Solitude", + "Harner": "Solitude", + "Yvaeroure": "Solitude", + "Vecodie": "Solitude", + "Arasare": "Solitude", + "Yvelet": "Solitude", + "Lazer": "Solitude", + "Stoure": "Solitude", + "Heluene": "Solitude", + "Arittant": "Solitude", + "Oruse": "Solitude", + "Hare": "Solitude", + "Ogaria": "Solitude", + "Faurulle": "Solitude", + "Agaullores": "Solitude", + "Babirmoult": "Solitude", + "Ratillose": "Solitude", + "Ondree": "Solitude", + "Pochelympe": "Solitude", + "Eggheron": "Solitude", + "Toustain": "Solitude", + "Straloin": "Solitude", + "H1-ESN": "Tenal", + "3DR-CR": "Tenal", + "RLTG-3": "Tenal", + "S-EVIQ": "Tenal", + "EOY-BG": "Tenal", + "PNS7-J": "Tenal", + "IG-ZAM": "Tenal", + "0-UVHJ": "Tenal", + "NCG-PW": "Tenal", + "1QH-0K": "Tenal", + "ZH3-BS": "Tenal", + "ZJ-QOO": "Tenal", + "ZXA-V6": "Tenal", + "I1-BE8": "Tenal", + "W8O-19": "Tenal", + "U1TX-A": "Tenal", + "1BWK-S": "Tenal", + "KMV-CQ": "Tenal", + "RKE-CP": "Tenal", + "NV-3KA": "Tenal", + "S-1LIO": "Tenal", + "S-KSWL": "Tenal", + "5-O8B1": "Tenal", + "R-YWID": "Tenal", + "30-D5G": "Tenal", + "HB-FSO": "Tenal", + "J1-KJP": "Tenal", + "KW-1MV": "Tenal", + "G06-8Y": "Tenal", + "U-O2DA": "Tenal", + "WV-0R2": "Tenal", + "SZ6-TA": "Tenal", + "6-AOLS": "Tenal", + "IKTD-P": "Tenal", + "33CE-7": "Tenal", + "L-P3XM": "Tenal", + "DCJ-ZT": "Tenal", + "O36A-P": "Tenal", + "Z-LO6I": "Tenal", + "0M-103": "Tenal", + "6OYQ-Z": "Tenal", + "HE5T-A": "Tenal", + "A-1IJ9": "Tenal", + "Y-YHZQ": "Tenal", + "Z-SR1I": "Tenal", + "GW7P-8": "Tenal", + "SF-XJS": "Tenal", + "A1RR-M": "Tenal", + "AR-5SY": "Tenal", + "OE-4HB": "Tenal", + "ZK-YQ3": "Tenal", + "MZPH-W": "Tenal", + "W0X-MG": "Tenal", + "JI-1UQ": "Tenal", + "EN-GTB": "Tenal", + "U5-XW7": "Tenal", + "JSI-LL": "Tenal", + "M-UC0S": "Tenal", + "V7-MID": "Tenal", + "SY0W-2": "Tenal", + "2-3Q2G": "Tenal", + "Q1U-IU": "Tenal", + "C-XNUA": "Tenal", + "7D-PAT": "Tenal", + "V-LDEJ": "Tenal", + "T-K10W": "Tenal", + "P-UCRP": "Tenal", + "3-QYVE": "Tenal", + "C8-CHY": "Fade", + "E-9ORY": "Fade", + "CR-IFM": "Fade", + "HHK-VL": "Fade", + "P-33KR": "Fade", + "DO6H-Q": "Fade", + "DW-T2I": "Fade", + "O-CNPR": "Fade", + "L-SCBU": "Fade", + "VRH-H7": "Fade", + "O1Y-ED": "Fade", + "K4YZ-Y": "Fade", + "X36Y-G": "Fade", + "L-C3O7": "Fade", + "YKSC-A": "Fade", + "FIO1-8": "Fade", + "C-OK0R": "Fade", + "0-ARFO": "Fade", + "E9KD-N": "Fade", + "8W-OSE": "Fade", + "WQY-IQ": "Fade", + "C4C-Z4": "Fade", + "GME-PQ": "Fade", + "MPPA-A": "Fade", + "X5-UME": "Fade", + "I-UUI5": "Fade", + "8QMO-E": "Fade", + "G-5EN2": "Providence", + "9-F0B2": "Providence", + "YWS0-Z": "Providence", + "4B-NQN": "Providence", + "9UY4-H": "Providence", + "49GC-R": "Providence", + "D-GTMI": "Providence", + "FSW-3C": "Providence", + "FX-7EM": "Providence", + "MH9C-S": "Providence", + "G7AQ-7": "Providence", + "QBL-BV": "Providence", + "T-RPFU": "Providence", + "I7S-1S": "Providence", + "U-HYMT": "Providence", + "FC-3YI": "Providence", + "QR-K85": "Providence", + "5IO8-U": "Providence", + "DP-JD4": "Providence", + "OXIY-V": "Providence", + "H6-CX8": "Providence", + "D61A-G": "Providence", + "Shintaht": "Providence", + "Y-MPWL": "Providence", + "D-6WS1": "Providence", + "SI-I89": "Providence", + "KBP7-G": "Providence", + "B-WPLZ": "Providence", + "XHQ-7V": "Providence", + "E-YCML": "Providence", + "TU-O0T": "Providence", + "Y9-MDG": "Providence", + "PI5-39": "Providence", + "GN7-XY": "Providence", + "F-DTOO": "Providence", + "5KG-PY": "Providence", + "QO-SRI": "Providence", + "INQ-WR": "Providence", + "S9X-AX": "Providence", + "TU-RI6": "Providence", + "08Z-JJ": "Providence", + "X-4WZD": "Providence", + "6-OQJV": "Providence", + "AY-YCU": "Providence", + "ZT-LPU": "Providence", + "3GXF-U": "Providence", + "VKI-T7": "Providence", + "8P9-BM": "Providence", + "F-YH5B": "Providence", + "H-GKI6": "Providence", + "YQB-22": "Providence", + "2-TEGJ": "Providence", + "MVCJ-E": "Providence", + "AY-24I": "Providence", + "BK4-YC": "Providence", + "K1I1-J": "Providence", + "LF-2KP": "Providence", + "JEIV-E": "Providence", + "O-Y5JQ": "Providence", + "DNR-7M": "Providence", + "N-RMSH": "Providence", + "K1Y-5H": "Providence", + "IWZ3-C": "Providence", + "1-1I53": "Providence", + "N8XA-L": "Providence", + "18-GZM": "Providence", + "R3-K7K": "Providence", + "X-R3NM": "Providence", + "8B-VLX": "Providence", + "G-B22J": "Providence", + "X6AB-Y": "Providence", + "2V-CS5": "Providence", + "H9-J8N": "Providence", + "HP-6Z6": "Providence", + "GA9P-0": "Providence", + "7YWV-S": "Providence", + "TXJ-II": "Providence", + "C1-HAB": "Providence", + "3KB-J0": "Providence", + "0B-HLZ": "Providence", + "Z-RFE3": "Providence", + "I-MGAB": "Providence", + "18XA-C": "Providence", + "3D-CQU": "Providence", + "Agoze": "Placid", + "Intaki": "Placid", + "Brarel": "Placid", + "Vey": "Placid", + "Annancale": "Placid", + "Ostingele": "Placid", + "Harroule": "Placid", + "Stacmon": "Placid", + "Covryn": "Placid", + "Iges": "Placid", + "Dastryns": "Placid", + "Slays": "Placid", + "Uphallant": "Placid", + "Alperaute": "Placid", + "Aunsou": "Placid", + "Cumemare": "Placid", + "Reynire": "Placid", + "Pain": "Placid", + "Gare": "Placid", + "Pelille": "Placid", + "Dour": "Placid", + "Grispire": "Placid", + "Brellystier": "Placid", + "Vivanier": "Placid", + "Algasienan": "Placid", + "Osmallanais": "Placid", + "Ivorider": "Placid", + "Mollin": "Placid", + "Iffrue": "Placid", + "Vilinnon": "Placid", + "Ommaerrer": "Placid", + "Aulbres": "Placid", + "Barleguet": "Placid", + "Vestouve": "Placid", + "Ausmaert": "Placid", + "Espigoure": "Placid", + "Kenninck": "Placid", + "Archavoinet": "Placid", + "Eugales": "Placid", + "Frarie": "Placid", + "Aubenall": "Placid", + "Moclinamaud": "Placid", + "Renarelle": "Placid", + "Orvolle": "Placid", + "Osmeden": "Placid", + "Adacyne": "Placid", + "Oulley": "Placid", + "Chardalane": "Placid", + "Maut": "Placid", + "Vlillirier": "Placid", + "Aldranette": "Placid", + "Oicx": "Placid", + "Evaulon": "Placid", + "Anchauttes": "Placid", + "Alsavoinon": "Placid", + "Esesier": "Placid", + "Avaux": "Placid", + "Gallusiene": "Placid", + "Ruerrotta": "Placid", + "Hedoubel": "Placid", + "Amoen": "Placid", + "Amasiree": "Placid", + "Aubonnie": "Placid", + "Alparena": "Placid", + "Reschard": "Placid", + "Arderonne": "Placid", + "Mercomesier": "Placid", + "Alamel": "Placid", + "Mantenault": "Placid", + "Athounon": "Placid", + "Odamia": "Placid", + "Gousoviba": "Khanid", + "Neyi": "Khanid", + "Kihtaled": "Khanid", + "Ipref": "Khanid", + "Agil": "Khanid", + "Khanid Prime": "Khanid", + "Jachanu": "Khanid", + "Sazre": "Khanid", + "Bukah": "Khanid", + "Ervekam": "Khanid", + "Mashtarmem": "Khanid", + "Sehsasez": "Khanid", + "Osis": "Khanid", + "Geztic": "Khanid", + "Yezara": "Khanid", + "Kahah": "Khanid", + "Saloti": "Khanid", + "Hishai": "Khanid", + "Molea": "Khanid", + "Gidali": "Khanid", + "Palas": "Khanid", + "Safshela": "Khanid", + "Reteka": "Khanid", + "Moniyyuku": "Khanid", + "Lansez": "Khanid", + "Keberz": "Khanid", + "Nourbal": "Khanid", + "Arzanni": "Khanid", + "Ashmarir": "Khanid", + "Kaira": "Khanid", + "Badivefi": "Khanid", + "Talidal": "Khanid", + "Ashi": "Khanid", + "Tzashrah": "Khanid", + "Efa": "Khanid", + "Moro": "Khanid", + "Sabusi": "Khanid", + "Ainsan": "Khanid", + "Claini": "Khanid", + "Gehi": "Khanid", + "Seshala": "Khanid", + "Vezila": "Khanid", + "Ham": "Khanid", + "Upt": "Khanid", + "Hemouner": "Khanid", + "Afnakat": "Khanid", + "Col": "Khanid", + "Chamemi": "Khanid", + "Firbha": "Khanid", + "Tegheon": "Khanid", + "Bashyam": "Khanid", + "Parses": "Khanid", + "Balanaz": "Khanid", + "Edani": "Khanid", + "Danera": "Khanid", + "Bomana": "Khanid", + "Rahabeda": "Khanid", + "Aurejet": "Khanid", + "Rilera": "Khanid", + "Amafi": "Khanid", + "Hakana": "Khanid", + "Ashkoo": "Khanid", + "Baratar": "Khanid", + "Arzieh": "Khanid", + "Nahrneder": "Khanid", + "Nandeza": "Khanid", + "Dimoohan": "Khanid", + "Chitiamem": "Khanid", + "Kuhri": "Khanid", + "Zahefeus": "Khanid", + "Zephan": "Khanid", + "Neda": "Khanid", + "Goudiyah": "Khanid", + "Sassecho": "Khanid", + "Timudan": "Khanid", + "Ibani": "Khanid", + "Cabeki": "Khanid", + "Irmalin": "Khanid", + "Nakis": "Khanid", + "Hezere": "Khanid", + "Fanathor": "Khanid", + "Zirsem": "Khanid", + "Pout": "Khanid", + "Rafeme": "Khanid", + "A2-V27": "Querious", + "T8H-66": "Querious", + "A3-LOG": "Querious", + "7V-KHW": "Querious", + "O3L-95": "Querious", + "0-WT2D": "Querious", + "7GCD-P": "Querious", + "G-3BOG": "Querious", + "K7D-II": "Querious", + "L-6BE1": "Querious", + "1M4-FK": "Querious", + "V-LEKM": "Querious", + "9ES-SI": "Querious", + "UQY-IK": "Querious", + "60M-TG": "Querious", + "0TKF-6": "Querious", + "TV8-HS": "Querious", + "VT-G2P": "Querious", + "YOP-0T": "Querious", + "9-HM04": "Querious", + "MKD-O8": "Querious", + "GOP-GE": "Querious", + "SKR-SP": "Querious", + "V-3U8T": "Querious", + "T8T-RA": "Querious", + "A-BO4V": "Querious", + "W-IX39": "Querious", + "K-B8DK": "Querious", + "L-6W1J": "Querious", + "P4-3TJ": "Querious", + "K-Z0V4": "Querious", + "LNVW-K": "Querious", + "8B-SAJ": "Querious", + "Q2-N6W": "Querious", + "C-9RRR": "Querious", + "A-5F4A": "Querious", + "P-ZMZV": "Querious", + "9CG6-H": "Querious", + "NDII-Q": "Querious", + "UYU-VV": "Querious", + "K-L690": "Querious", + "W6V-VM": "Querious", + "OGY-6D": "Querious", + "8-SNUD": "Querious", + "H-4R6Z": "Querious", + "IGE-NE": "Querious", + "UVHO-F": "Querious", + "Z-XX2J": "Querious", + "YW-SYT": "Querious", + "Z-UZZN": "Querious", + "DS-LO3": "Querious", + "BX2-ZX": "Querious", + "RF-CN3": "Querious", + "C-7SBM": "Querious", + "ZAU-JW": "Querious", + "YF-6L1": "Querious", + "K-YI1L": "Querious", + "KEJY-U": "Querious", + "3BK-O7": "Querious", + "8-GE2P": "Querious", + "QXQ-I6": "Querious", + "L3-I3K": "Querious", + "3-JCJT": "Querious", + "W-IIYI": "Querious", + "AO-N1P": "Querious", + "4-GJT1": "Querious", + "5V-BJI": "Querious", + "49-U6U": "Querious", + "M1BZ-2": "Querious", + "N-M1A3": "Querious", + "8QT-H4": "Querious", + "F2OY-X": "Querious", + "4-2UXV": "Querious", + "RKM-GE": "Querious", + "DG-L7S": "Querious", + "K4-RFZ": "Querious", + "L-FVHR": "Querious", + "3-FKCZ": "Querious", + "ED-L9T": "Querious", + "LS-V29": "Querious", + "9SBB-9": "Querious", + "I1Y-IU": "Querious", + "U-HYZN": "Querious", + "8-YNBE": "Querious", + "YQX-7U": "Querious", + "QY1E-N": "Querious", + "E-VKJV": "Querious", + "BX-VEX": "Querious", + "B-7DFU": "Querious", + "ZXJ-71": "Querious", + "F-NXLQ": "Querious", + "ES-Q0W": "Querious", + "H74-B0": "Querious", + "NU4-2G": "Querious", + "3D5K-R": "Querious", + "1-3HWZ": "Cloud Ring", + "XT-R36": "Cloud Ring", + "5-MLDT": "Cloud Ring", + "B-DBYQ": "Cloud Ring", + "QXW-PV": "Cloud Ring", + "DY-F70": "Cloud Ring", + "FD53-H": "Cloud Ring", + "O-ZXUV": "Cloud Ring", + "77-KDQ": "Cloud Ring", + "F7C-H0": "Cloud Ring", + "TN-T7T": "Cloud Ring", + "1-NW2G": "Cloud Ring", + "O-IVNH": "Cloud Ring", + "O-0HW8": "Cloud Ring", + "YI-8ZM": "Cloud Ring", + "OU-X3P": "Cloud Ring", + "6-4V20": "Cloud Ring", + "Q-UA3C": "Cloud Ring", + "W-4NUU": "Cloud Ring", + "8R-RTB": "Cloud Ring", + "6Z9-0M": "Cloud Ring", + "FQ9W-C": "Cloud Ring", + "9-4RP2": "Cloud Ring", + "O-BDXB": "Cloud Ring", + "G8AD-C": "Cloud Ring", + "XZH-4X": "Cloud Ring", + "Z-Y7R7": "Cloud Ring", + "MJYW-3": "Cloud Ring", + "PPG-XC": "Cloud Ring", + "QA1-BT": "Cloud Ring", + "5S-KNL": "Cloud Ring", + "00TY-J": "Cloud Ring", + "XG-D1L": "Cloud Ring", + "6RCQ-V": "Cloud Ring", + "28O-JY": "Cloud Ring", + "CX7-70": "Cloud Ring", + "6ON-RW": "Cloud Ring", + "U65-CN": "Cloud Ring", + "X-M9ON": "Cloud Ring", + "P5-KCC": "Cloud Ring", + "Hiroudeh": "Kador", + "Dresi": "Kador", + "Aphend": "Kador", + "Romi": "Kador", + "Zororzih": "Kador", + "Aharalel": "Kador", + "Gensela": "Kador", + "Ghesis": "Kador", + "Gamdis": "Kador", + "Joamma": "Kador", + "Gonan": "Kador", + "Joramok": "Kador", + "Neburab": "Kador", + "Aband": "Kador", + "Uanim": "Kador", + "Murini": "Kador", + "Askonak": "Kador", + "Nordar": "Kador", + "Kador Prime": "Kador", + "Khafis": "Kador", + "Dantan": "Kador", + "Turba": "Kador", + "Sonama": "Kador", + "Halibai": "Kador", + "Suner": "Kador", + "Inis-Ilix": "Kador", + "Kothe": "Kador", + "Ansasos": "Kador", + "Dehrokh": "Kador", + "Bordan": "Kador", + "Zimmem": "Kador", + "Chaneya": "Kador", + "Oberen": "Kador", + "Finid": "Kador", + "Yarebap": "Kador", + "Mandoo": "Kador", + "Miah": "Kador", + "Peyiri": "Kador", + "Kamda": "Kador", + "Rayeret": "Kador", + "Bushemal": "Kador", + "Ardhis": "Kador", + "Gasavak": "Kador", + "Iaokit": "Kador", + "Menri": "Kador", + "Chanoun": "Kador", + "Garisas": "Kador", + "Aphi": "Kador", + "Jakri": "Kador", + "Nidupad": "Kador", + "Zimse": "Kador", + "Koona": "Kador", + "Munory": "Kador", + "Hostakoh": "Kador", + "Yooh": "Kador", + "Jeshideh": "Kador", + "Hilmar": "Kador", + "Kasi": "Kador", + "Shura": "Kador", + "Mod": "Kador", + "Omam": "Kador", + "Bersyrim": "Kador", + "Sechmaren": "Kador", + "Zinoo": "Kador", + "Hiremir": "Kador", + "Hikansog": "Kador", + "Syrikos": "Kador", + "Yebouz": "Kador", + "Hapala": "Kador", + "Salah": "Kador", + "Akhmoh": "Kador", + "Jennim": "Kador", + "Elmed": "Kador", + "Shaggoth": "Kador", + "Ustnia": "Kador", + "Kooreng": "Kador", + "Minin": "Kador", + "Yehnifi": "Kador", + "Shemah": "Kador", + "Asrios": "Kador", + "Ithar": "Kador", + "Telang": "Kador", + "Lazara": "Kador", + "Zorrabed": "Kador", + "Akhwa": "Kador", + "FV-YEA": "Cobalt Edge", + "J-A5QD": "Cobalt Edge", + "BI0Y-X": "Cobalt Edge", + "SK7-G6": "Cobalt Edge", + "4-PCHD": "Cobalt Edge", + "5-3722": "Cobalt Edge", + "GQLB-V": "Cobalt Edge", + "5E-EZC": "Cobalt Edge", + "9KE-IT": "Cobalt Edge", + "P-NRD3": "Cobalt Edge", + "Y-RAW3": "Cobalt Edge", + "S-W8CF": "Cobalt Edge", + "X-41DA": "Cobalt Edge", + "YVSL-2": "Cobalt Edge", + "5E6I-W": "Cobalt Edge", + "KIG9-K": "Cobalt Edge", + "I-CMZA": "Cobalt Edge", + "H23-B5": "Cobalt Edge", + "A-0IIQ": "Cobalt Edge", + "CBY8-J": "Cobalt Edge", + "E-BYOS": "Cobalt Edge", + "ETXT-F": "Cobalt Edge", + "MK-YNM": "Cobalt Edge", + "2-9Z6V": "Cobalt Edge", + "5HN-D6": "Cobalt Edge", + "E-B957": "Cobalt Edge", + "P-H5IY": "Cobalt Edge", + "4A-6NI": "Cobalt Edge", + "1M7-RK": "Cobalt Edge", + "87-1PM": "Cobalt Edge", + "C2-1B5": "Cobalt Edge", + "JE-VLG": "Cobalt Edge", + "5ED-4E": "Cobalt Edge", + "B-U299": "Cobalt Edge", + "DN58-U": "Cobalt Edge", + "VAF1-P": "Cobalt Edge", + "FV1-RQ": "Cobalt Edge", + "QT-EBC": "Cobalt Edge", + "O-F4SN": "Cobalt Edge", + "CUT-0V": "Cobalt Edge", + "9-WEMC": "Cobalt Edge", + "U6R-F9": "Cobalt Edge", + "L-Z9NB": "Cobalt Edge", + "EJ-5X2": "Cobalt Edge", + "HXK-J6": "Cobalt Edge", + "4LNE-M": "Cobalt Edge", + "DK0-N8": "Cobalt Edge", + "E0DR-G": "Cobalt Edge", + "KI2-S3": "Cobalt Edge", + "CHP-76": "Cobalt Edge", + "T-67F8": "Cobalt Edge", + "58Z-IH": "Cobalt Edge", + "M-VACR": "Cobalt Edge", + "0B-VOJ": "Cobalt Edge", + "J-QOKQ": "Cobalt Edge", + "4GSZ-1": "Cobalt Edge", + "E-EFAM": "Cobalt Edge", + "SBEN-Q": "Cobalt Edge", + "9-7SRQ": "Cobalt Edge", + "VEQ-3V": "Cobalt Edge", + "4T-VDE": "Cobalt Edge", + "D9Z-VY": "Cobalt Edge", + "MO-YDG": "Cobalt Edge", + "42SU-L": "Cobalt Edge", + "RGU1-T": "Cobalt Edge", + "1GT-MA": "Cobalt Edge", + "VY-866": "Cobalt Edge", + "HB-5L3": "Cobalt Edge", + "Q-VTWJ": "Cobalt Edge", + "Van": "Aridia", + "Shakasi": "Aridia", + "Zayi": "Aridia", + "Shirshocin": "Aridia", + "Maalna": "Aridia", + "Maseera": "Aridia", + "Yehaba": "Aridia", + "Kenahehab": "Aridia", + "Gens": "Aridia", + "Kamih": "Aridia", + "Hier": "Aridia", + "Jasson": "Aridia", + "Sadana": "Aridia", + "Isid": "Aridia", + "Onanam": "Aridia", + "Udianoor": "Aridia", + "Vehan": "Aridia", + "Marmeha": "Aridia", + "Haimeh": "Aridia", + "Avada": "Aridia", + "Chibi": "Aridia", + "Mishi": "Aridia", + "Bazadod": "Aridia", + "Pahineh": "Aridia", + "Fihrneh": "Aridia", + "Parouz": "Aridia", + "Edilkam": "Aridia", + "Hakatiz": "Aridia", + "Khnar": "Aridia", + "Ertoo": "Aridia", + "Yiratal": "Aridia", + "Balas": "Aridia", + "Pemsah": "Aridia", + "Feshur": "Aridia", + "Hoseen": "Aridia", + "Yekh": "Aridia", + "Gesh": "Aridia", + "Nema": "Aridia", + "Shenda": "Aridia", + "Rashagh": "Aridia", + "Sazilid": "Aridia", + "Afrah": "Aridia", + "Sota": "Aridia", + "Soliara": "Aridia", + "Nielez": "Aridia", + "Tukanas": "Aridia", + "Fageras": "Aridia", + "Ajna": "Aridia", + "Sheri": "Aridia", + "Ahraghen": "Aridia", + "Nalnifan": "Aridia", + "Jerhesh": "Aridia", + "Getrenjesa": "Aridia", + "Shafrak": "Aridia", + "Defsunun": "Aridia", + "Zazamye": "Aridia", + "Yahyerer": "Aridia", + "Esubara": "Aridia", + "Ghekon": "Aridia", + "Vaini": "Aridia", + "Zaveral": "Aridia", + "Anohel": "Aridia", + "Soza": "Aridia", + "Pserz": "Aridia", + "Illi": "Aridia", + "Keba": "Aridia", + "Bapraya": "Aridia", + "Efu": "Aridia", + "Tisot": "Aridia", + "Sakht": "Aridia", + "Naga": "Aridia", + "Anath": "Aridia", + "Omigiav": "Aridia", + "Fobiner": "Aridia", + "Huna": "Aridia", + "Esaeel": "Aridia", + "Karan": "Aridia", + "Nouta": "Aridia", + "Ned": "Aridia", + "Hophib": "Aridia", + "UQ9-3C": "Branch", + "DCI7-7": "Branch", + "J7YR-1": "Branch", + "PKG4-7": "Branch", + "EWN-2U": "Branch", + "VL3I-M": "Branch", + "KMC-WI": "Branch", + "4-48K1": "Branch", + "NTV0-1": "Branch", + "C-HCGU": "Branch", + "XW-2XP": "Branch", + "Q-FEEJ": "Branch", + "0P9Z-I": "Branch", + "AH-B84": "Branch", + "JTAU-5": "Branch", + "HB7R-F": "Branch", + "O-JPKH": "Branch", + "F-9F6Q": "Branch", + "B-GC1T": "Branch", + "V8W-QS": "Branch", + "JRZ-B9": "Branch", + "X4UV-Z": "Branch", + "S-B7IT": "Branch", + "BKG-Q2": "Branch", + "OJ-A8M": "Branch", + "CX-1XF": "Branch", + "3-TD6L": "Branch", + "Q-NJZ4": "Branch", + "NLPB-0": "Branch", + "R4O-I6": "Branch", + "KL3O-J": "Branch", + "Z-K495": "Branch", + "XM-4L0": "Branch", + "QCWA-Z": "Branch", + "52G-NZ": "Branch", + "5LJ-MD": "Branch", + "B8O-KJ": "Branch", + "6-O5GY": "Branch", + "KV-8SN": "Branch", + "UB-UQZ": "Branch", + "YG-82V": "Branch", + "8-4GQM": "Branch", + "T-Q2DD": "Branch", + "LRWD-B": "Branch", + "QXQ-BA": "Branch", + "X7R-JW": "Branch", + "M-HU4V": "Branch", + "CS-ZGD": "Branch", + "3-N3OO": "Branch", + "A-G1FM": "Branch", + "4-BE0M": "Branch", + "I-7RIS": "Branch", + "P7Z-R3": "Branch", + "ZIU-EP": "Branch", + "LXWN-W": "Branch", + "C-LP3N": "Branch", + "9F-7PZ": "Branch", + "1G-MJE": "Branch", + "WO-AIJ": "Branch", + "MA-VDX": "Branch", + "RO90-H": "Branch", + "BWI1-9": "Branch", + "C-LBQS": "Branch", + "J52-BH": "Branch", + "5-P1Y2": "Branch", + "KMQ4-V": "Branch", + "KJ-QWL": "Branch", + "SVB-RE": "Branch", + "C-4ZOS": "Branch", + "K-8SQS": "Branch", + "C-VGYO": "Branch", + "O94U-A": "Branch", + "XW-JHT": "Branch", + "NEH-CS": "Branch", + "4DTQ-K": "Branch", + "J9-5MQ": "Branch", + "D4R-H7": "Branch", + "313I-B": "Branch", + "EQI2-2": "Branch", + "Q-4DEC": "Branch", + "3F-JZF": "Branch", + "5-0WB9": "Branch", + "W-4FA9": "Branch", + "1IX-C0": "Branch", + "2B7A-3": "Branch", + "PUWL-4": "Branch", + "Y-1918": "Branch", + "9-B1DS": "Branch", + "ME-4IU": "Branch", + "BU-IU4": "Branch", + "I-7JR4": "Branch", + "CH9L-K": "Branch", + "QYZM-W": "Branch", + "3KNA-N": "Branch", + "UD-VZW": "Feythabolis", + "3-YX2D": "Feythabolis", + "V-TN6Q": "Feythabolis", + "CFLF-P": "Feythabolis", + "QBH5-F": "Feythabolis", + "9-ZFCG": "Feythabolis", + "J-TPTA": "Feythabolis", + "PMV-G6": "Feythabolis", + "5-IZGE": "Feythabolis", + "OXC-UL": "Feythabolis", + "F-8Y13": "Feythabolis", + "4AZ-J8": "Feythabolis", + "X6-J6R": "Feythabolis", + "BGN1-O": "Feythabolis", + "DUU1-K": "Feythabolis", + "3L-Y9M": "Feythabolis", + "BLC-X0": "Feythabolis", + "K-X5AX": "Feythabolis", + "BJD4-E": "Feythabolis", + "TSG-NO": "Feythabolis", + "O9V-R7": "Feythabolis", + "Z-PNIA": "Feythabolis", + "OCU4-R": "Feythabolis", + "BG-W90": "Feythabolis", + "Y-YGMW": "Feythabolis", + "75C-WN": "Feythabolis", + "I5Q2-S": "Feythabolis", + "PO-3QW": "Feythabolis", + "5XR-KZ": "Feythabolis", + "VF-FN6": "Feythabolis", + "C-0ND2": "Feythabolis", + "JI-LGM": "Feythabolis", + "U-BXU9": "Feythabolis", + "ZXOG-O": "Feythabolis", + "NW2S-A": "Feythabolis", + "U-JJEW": "Feythabolis", + "NX5W-U": "Feythabolis", + "U1-C18": "Feythabolis", + "6O-XIO": "Feythabolis", + "H65-HE": "Feythabolis", + "BJ-ZFD": "Feythabolis", + "5ELE-A": "Feythabolis", + "H-P4LB": "Feythabolis", + "2UK4-N": "Feythabolis", + "QK-CDG": "Feythabolis", + "M-CMLV": "Feythabolis", + "AZN-D2": "Feythabolis", + "E-PR0S": "Feythabolis", + "TR07-S": "Feythabolis", + "VNGJ-U": "Feythabolis", + "2-F3OE": "Feythabolis", + "5-LCI7": "Feythabolis", + "Y2-I3W": "Feythabolis", + "VVO-R6": "Feythabolis", + "CL-J9W": "Feythabolis", + "YHP2-D": "Feythabolis", + "J94-MU": "Feythabolis", + "M2GJ-X": "Feythabolis", + "JO-32L": "Feythabolis", + "UB5Z-3": "Feythabolis", + "MSKR-1": "Feythabolis", + "GPUS-A": "Feythabolis", + "3-BADZ": "Feythabolis", + "23M-PX": "Feythabolis", + "UTDH-N": "Feythabolis", + "ZS-2LT": "Feythabolis", + "DB1R-4": "Feythabolis", + "P8-BKO": "Feythabolis", + "RIT-A7": "Feythabolis", + "R4K-8L": "Feythabolis", + "GHZ-SJ": "Feythabolis", + "K-J50B": "Feythabolis", + "NLO-3Z": "Feythabolis", + "5P-AIP": "Feythabolis", + "M-PGT0": "Feythabolis", + "NPD9-A": "Feythabolis", + "D6SK-L": "Feythabolis", + "HYPL-V": "Feythabolis", + "I9-ZQZ": "Feythabolis", + "0OYZ-G": "Feythabolis", + "SWBV-2": "Feythabolis", + "R97-CI": "Feythabolis", + "6-ELQP": "Feythabolis", + "OBK-K8": "Feythabolis", + "KJ-V0P": "Feythabolis", + "ZID-LE": "Feythabolis", + "K-9UG4": "Feythabolis", + "D4-2XN": "Feythabolis", + "2-RSC7": "Feythabolis", + "C0T-77": "Outer Ring", + "RL-KT0": "Outer Ring", + "UO9-YG": "Outer Ring", + "ZQP-QV": "Outer Ring", + "P-NUWP": "Outer Ring", + "ZJQH-S": "Outer Ring", + "E9G-MT": "Outer Ring", + "TQ-RR8": "Outer Ring", + "1L-BHT": "Outer Ring", + "D5IW-F": "Outer Ring", + "F-XWIN": "Outer Ring", + "4C-B7X": "Outer Ring", + "LGUZ-1": "Outer Ring", + "BF-SDP": "Outer Ring", + "F5FO-U": "Outer Ring", + "5WAE-M": "Outer Ring", + "0-WVQS": "Outer Ring", + "0-9UHT": "Outer Ring", + "M-NKZM": "Outer Ring", + "H-M1BY": "Outer Ring", + "J1H-R4": "Outer Ring", + "J9SH-A": "Outer Ring", + "JKJ-VJ": "Outer Ring", + "RTX0-S": "Outer Ring", + "33FN-P": "Outer Ring", + "NM-OEA": "Outer Ring", + "MT-2VJ": "Outer Ring", + "3HQC-6": "Outer Ring", + "OX-RGN": "Outer Ring", + "R-OCBA": "Outer Ring", + "GA-2V7": "Outer Ring", + "DB-6W4": "Outer Ring", + "7-692B": "Outer Ring", + "L3-XYO": "Outer Ring", + "AN-G54": "Outer Ring", + "ZXI-K2": "Outer Ring", + "T-Z6J2": "Outer Ring", + "CT7-5V": "Outer Ring", + "2JJ-0E": "Outer Ring", + "B0C-LD": "Outer Ring", + "NP6-38": "Outer Ring", + "G-YT55": "Outer Ring", + "IZ-AOB": "Outer Ring", + "G5-EN3": "Outer Ring", + "W-Z3HW": "Outer Ring", + "W2F-ZH": "Outer Ring", + "BMU-V1": "Outer Ring", + "ZXC8-1": "Outer Ring", + "LBV-Q1": "Outer Ring", + "Z-40CG": "Outer Ring", + "O-RIDF": "Outer Ring", + "A-5M31": "Outer Ring", + "BOE7-P": "Outer Ring", + "E-GCX0": "Outer Ring", + "VBFC-8": "Outer Ring", + "YVA-F0": "Outer Ring", + "0D-CHA": "Outer Ring", + "A2V6-6": "Outer Ring", + "VJ0-81": "Outer Ring", + "XF-TQL": "Fountain", + "4-EP12": "Fountain", + "YZS5-4": "Fountain", + "3WE-KY": "Fountain", + "IR-WT1": "Fountain", + "9-VO0Q": "Fountain", + "A8-XBW": "Fountain", + "PNQY-Y": "Fountain", + "RP2-OQ": "Fountain", + "YVBE-E": "Fountain", + "BYXF-Q": "Fountain", + "AC2E-3": "Fountain", + "C-C99Z": "Fountain", + "CL-BWB": "Fountain", + "R3W-XU": "Fountain", + "E-BWUU": "Fountain", + "Y-1W01": "Fountain", + "9R4-EJ": "Fountain", + "SPLE-Y": "Fountain", + "Q-XEB3": "Fountain", + "K8L-X7": "Fountain", + "5-D82P": "Fountain", + "8ESL-G": "Fountain", + "JGOW-Y": "Fountain", + "APM-6K": "Fountain", + "RE-C26": "Fountain", + "AL8-V4": "Fountain", + "KCT-0A": "Fountain", + "N2-OQG": "Fountain", + "OW-TPO": "Fountain", + "9O-ORX": "Fountain", + "IGE-RI": "Fountain", + "Z9PP-H": "Fountain", + "7-8S5X": "Fountain", + "EI-O0O": "Fountain", + "7X-02R": "Fountain", + "D2AH-Z": "Fountain", + "J5A-IX": "Fountain", + "B17O-R": "Fountain", + "6F-H3W": "Fountain", + "H-NPXW": "Fountain", + "L-1SW8": "Fountain", + "U-SOH2": "Fountain", + "DBRN-Z": "Fountain", + "00GD-D": "Fountain", + "C1XD-X": "Fountain", + "G95F-H": "Fountain", + "B32-14": "Fountain", + "C-N4OD": "Fountain", + "CHA2-Q": "Fountain", + "UAYL-F": "Fountain", + "ESC-RI": "Fountain", + "671-ST": "Fountain", + "A-HZYL": "Fountain", + "H-S80W": "Fountain", + "Z30S-A": "Fountain", + "6VDT-H": "Fountain", + "NDH-NV": "Fountain", + "QV28-G": "Fountain", + "15U-JY": "Fountain", + "NY6-FH": "Fountain", + "XJP-Y7": "Fountain", + "AV-VB6": "Fountain", + "HMF-9D": "Fountain", + "7BX-6F": "Fountain", + "YZ-LQL": "Fountain", + "MN5N-X": "Fountain", + "A-1CON": "Fountain", + "75FA-Z": "Fountain", + "WY-9LL": "Fountain", + "D-Q04X": "Fountain", + "Serpentis Prime": "Fountain", + "P5-EFH": "Fountain", + "L-A5XP": "Fountain", + "D4KU-5": "Fountain", + "YRNJ-8": "Fountain", + "3ZTV-V": "Fountain", + "9D6O-M": "Fountain", + "LIWW-P": "Fountain", + "G-UTHL": "Fountain", + "38IA-E": "Fountain", + "M-KXEH": "Fountain", + "TU-Y2A": "Fountain", + "7BIX-A": "Fountain", + "I-CUVX": "Fountain", + "J-RQMF": "Fountain", + "TEG-SD": "Fountain", + "14YI-D": "Fountain", + "87XQ-0": "Fountain", + "LJ-TZW": "Fountain", + "KVN-36": "Fountain", + "57-KJB": "Fountain", + "V6-NY1": "Fountain", + "OL3-78": "Fountain", + "9DQW-W": "Fountain", + "PXF-RF": "Fountain", + "R-BGSU": "Fountain", + "O-PNSN": "Fountain", + "1-5GBW": "Fountain", + "C-FER9": "Fountain", + "F2-2C3": "Fountain", + "F-88PJ": "Fountain", + "ATQ-QS": "Fountain", + "XUW-3X": "Fountain", + "006-L3": "Fountain", + "PB-0C1": "Fountain", + "ZUE-NS": "Fountain", + "L7-APB": "Fountain", + "ZTS-4D": "Fountain", + "4HS-CR": "Fountain", + "WMH-SO": "Fountain", + "LBGI-2": "Fountain", + "G1CA-Y": "Fountain", + "Y-2ANO": "Fountain", + "Z-YN5Y": "Fountain", + "JI-K5H": "Paragon Soul", + "33-JRO": "Paragon Soul", + "ARBX-9": "Paragon Soul", + "5-CSE3": "Paragon Soul", + "O-MCZR": "Paragon Soul", + "9T-APQ": "Paragon Soul", + "4Y-OBL": "Paragon Soul", + "0-MX34": "Paragon Soul", + "5AQ-5H": "Paragon Soul", + "T-ZFID": "Paragon Soul", + "0ZN7-G": "Paragon Soul", + "H8-ZTO": "Paragon Soul", + "YV-FDG": "Paragon Soul", + "LUL-WX": "Paragon Soul", + "8Q-UYU": "Paragon Soul", + "3PPT-9": "Paragon Soul", + "S-KU8B": "Paragon Soul", + "JK-GLL": "Paragon Soul", + "UAAU-C": "Paragon Soul", + "HHJD-5": "Paragon Soul", + "ZWV-GD": "Paragon Soul", + "1DDR-X": "Paragon Soul", + "LG-WA9": "Paragon Soul", + "AA-GWF": "Paragon Soul", + "O4T-Z5": "Paragon Soul", + "O-97ZG": "Paragon Soul", + "2I-520": "Paragon Soul", + "GQ2S-8": "Paragon Soul", + "0SUF-3": "Paragon Soul", + "G-M4GK": "Paragon Soul", + "G1D0-G": "Paragon Soul", + "KU3-BB": "Paragon Soul", + "O1Q-P1": "Paragon Soul", + "LD-2VL": "Paragon Soul", + "ZBY-0I": "Paragon Soul", + "MP5-KR": "Paragon Soul", + "O-N589": "Paragon Soul", + "ZDYA-G": "Paragon Soul", + "LX5K-W": "Paragon Soul", + "UHKL-N": "Delve", + "Z3V-1W": "Delve", + "A-ELE2": "Delve", + "KFIE-Z": "Delve", + "1DH-SX": "Delve", + "PR-8CA": "Delve", + "NOL-M9": "Delve", + "O-IOAI": "Delve", + "QX-LIJ": "Delve", + "HM-XR2": "Delve", + "4K-TRB": "Delve", + "AJI-MA": "Delve", + "FWST-8": "Delve", + "YZ9-F6": "Delve", + "0N-3RO": "Delve", + "G-TT5V": "Delve", + "319-3D": "Delve", + "I3Q-II": "Delve", + "RF-K9W": "Delve", + "E3OI-U": "Delve", + "IP6V-X": "Delve", + "R5-MM8": "Delve", + "1B-VKF": "Delve", + "T-J6HT": "Delve", + "D-W7F0": "Delve", + "JP4-AA": "Delve", + "FM-JK5": "Delve", + "PDE-U3": "Delve", + "23G-XC": "Delve", + "T5ZI-S": "Delve", + "4X0-8B": "Delve", + "Q-HESZ": "Delve", + "1-SMEB": "Delve", + "M5-CGW": "Delve", + "6Q-R50": "Delve", + "ZA9-PY": "Delve", + "RCI-VL": "Delve", + "MJXW-P": "Delve", + "QC-YX6": "Delve", + "T-M0FA": "Delve", + "4O-239": "Delve", + "LUA5-L": "Delve", + "T-IPZB": "Delve", + "Q-JQSG": "Delve", + "D-3GIQ": "Delve", + "K-6K16": "Delve", + "QY6-RK": "Delve", + "W-KQPI": "Delve", + "PUIG-F": "Delve", + "J-LPX7": "Delve", + "0-HDC8": "Delve", + "F-TE1T": "Delve", + "SVM-3K": "Delve", + "1DQ1-A": "Delve", + "8WA-Z6": "Delve", + "5BTK-M": "Delve", + "N-8YET": "Delve", + "Y-OMTZ": "Delve", + "3-DMQT": "Delve", + "MO-GZ5": "Delve", + "39P-1J": "Delve", + "HZAQ-W": "Delve", + "7G-QIG": "Delve", + "NIDJ-K": "Delve", + "PS-94K": "Delve", + "8RQJ-2": "Delve", + "KEE-N6": "Delve", + "M2-XFE": "Delve", + "5-CQDA": "Delve", + "I-E3TG": "Delve", + "S-6HHN": "Delve", + "ZXB-VC": "Delve", + "GY6A-L": "Delve", + "UEXO-Z": "Delve", + "9O-8W1": "Delve", + "8F-TK3": "Delve", + "PF-KUQ": "Delve", + "N8D9-Z": "Delve", + "F-9PXR": "Delve", + "Y5C-YD": "Delve", + "31X-RE": "Delve", + "Q-02UL": "Delve", + "7UTB-F": "Delve", + "5-6QW7": "Delve", + "7-K6UE": "Delve", + "C6Y-ZF": "Delve", + "6Z-CKS": "Delve", + "G-M5L3": "Delve", + "KBAK-I": "Delve", + "M-SRKS": "Delve", + "9GNS-2": "Delve", + "YAW-7M": "Delve", + "C3N-3S": "Delve", + "CX8-6K": "Delve", + "LWX-93": "Delve", + "1-2J4P": "Delve", + "M0O-JG": "Delve", + "WB-AYY": "Tenerifis", + "BW-WJ2": "Tenerifis", + "S4-9DN": "Tenerifis", + "DT-PXH": "Tenerifis", + "UALX-3": "Tenerifis", + "3L3N-X": "Tenerifis", + "Y-ORBJ": "Tenerifis", + "6-IAFR": "Tenerifis", + "4-P4FE": "Tenerifis", + "RH0-EG": "Tenerifis", + "D-9UEV": "Tenerifis", + "H-HWQR": "Tenerifis", + "QRBN-M": "Tenerifis", + "78R-PI": "Tenerifis", + "ZD1-Z2": "Tenerifis", + "C-FD0D": "Tenerifis", + "S-9RCJ": "Tenerifis", + "ZMV9-A": "Tenerifis", + "FE-6YQ": "Tenerifis", + "W-16DY": "Tenerifis", + "M-4KDB": "Tenerifis", + "C3-0YD": "Tenerifis", + "PDF-3Z": "Tenerifis", + "9-MJVQ": "Tenerifis", + "L2GN-K": "Tenerifis", + "4-IT9G": "Tenerifis", + "PEK-8Z": "Tenerifis", + "2PG-KN": "Tenerifis", + "ABE-M2": "Tenerifis", + "IL-YTR": "Tenerifis", + "KW-OAM": "Tenerifis", + "U2U5-A": "Tenerifis", + "EQWO-Y": "Tenerifis", + "JK-Q77": "Tenerifis", + "QI9-42": "Tenerifis", + "YF-P4X": "Tenerifis", + "JI1-SY": "Tenerifis", + "X-1QGA": "Tenerifis", + "CCE-0J": "Tenerifis", + "T2-V8F": "Tenerifis", + "0VK-43": "Tenerifis", + "TY2X-C": "Tenerifis", + "Q0G-L8": "Tenerifis", + "Q5KZ-W": "Tenerifis", + "WE-KK2": "Tenerifis", + "B8HU-Z": "Tenerifis", + "16AM-3": "Tenerifis", + "A-REKV": "Tenerifis", + "BB-EKF": "Tenerifis", + "DZ6-I5": "Tenerifis", + "R-XDKM": "Tenerifis", + "G1-0UI": "Tenerifis", + "QCDG-H": "Tenerifis", + "XUDX-A": "Tenerifis", + "QLU-P0": "Tenerifis", + "OQTY-Z": "Tenerifis", + "Y-EQ0C": "Tenerifis", + "7M4C-F": "Tenerifis", + "MS1-KJ": "Tenerifis", + "8-BEW8": "Tenerifis", + "NZW-ZO": "Tenerifis", + "WSK-1A": "Tenerifis", + "5-NZNW": "Tenerifis", + "NR8S-Y": "Tenerifis", + "F-ZBO0": "Tenerifis", + "3Q1T-O": "Tenerifis", + "8-4KME": "Tenerifis", + "T6GY-Y": "Tenerifis", + "R1-IMO": "Tenerifis", + "7KIK-H": "Tenerifis", + "B-6STA": "Tenerifis", + "0P-U0Q": "Tenerifis", + "XGH-SH": "Tenerifis", + "G-D0N3": "Tenerifis", + "T-AKQZ": "Tenerifis", + "46DP-O": "Tenerifis", + "9-980U": "Tenerifis", + "EMIG-F": "Tenerifis", + "M-RPN3": "Tenerifis", + "ZO-P5K": "Tenerifis", + "JV1V-O": "Tenerifis", + "9MWZ-B": "Omist", + "LS-QLX": "Omist", + "S-XZHU": "Omist", + "CO-7BI": "Omist", + "ZJG-7D": "Omist", + "C-WPWH": "Omist", + "VULA-I": "Omist", + "R2TJ-1": "Omist", + "G-B3PR": "Omist", + "73-JQO": "Omist", + "XPUM-L": "Omist", + "KR8-27": "Omist", + "LQ-AHE": "Omist", + "LOI-L1": "Omist", + "Y-MSJN": "Omist", + "MJ-X5V": "Omist", + "3FKU-H": "Omist", + "M9-FIB": "Omist", + "D2EZ-X": "Omist", + "DJK-67": "Omist", + "AXDX-F": "Omist", + "J-4FNO": "Omist", + "PEM-LC": "Omist", + "X-EHHD": "Omist", + "6T3I-L": "Omist", + "QSF-EJ": "Omist", + "L-AS00": "Omist", + "NZPK-G": "Omist", + "K-1OY3": "Omist", + "MMUF-8": "Omist", + "99-0GS": "Omist", + "X-3AUU": "Omist", + "H90-C9": "Omist", + "0DD-MH": "Omist", + "RI-JB1": "Omist", + "NQH-MR": "Omist", + "1I6F-9": "Omist", + "Z-7OK1": "Omist", + "UEP0-A": "Omist", + "66-PMM": "Omist", + "OKEO-X": "Omist", + "7-8EOE": "Omist", + "7L9-ZC": "Omist", + "L-YMYU": "Period Basis", + "35-JWD": "Period Basis", + "F-M1FU": "Period Basis", + "0-NTIS": "Period Basis", + "VR-YIQ": "Period Basis", + "XZ-SKZ": "Period Basis", + "I6M-9U": "Period Basis", + "MG0-RD": "Period Basis", + "TPAR-G": "Period Basis", + "VYO-68": "Period Basis", + "TCAG-3": "Period Basis", + "UR-E46": "Period Basis", + "CW9-1Y": "Period Basis", + "1-NJLK": "Period Basis", + "Y-CWQY": "Period Basis", + "8KR9-5": "Period Basis", + "VQE-CN": "Period Basis", + "L5D-ZL": "Period Basis", + "G-C8QO": "Period Basis", + "EIMJ-M": "Period Basis", + "0A-KZ0": "Period Basis", + "E-DOF2": "Period Basis", + "48I1-X": "Period Basis", + "0OTX-J": "Period Basis", + "3OP-3E": "Period Basis", + "JZL-VB": "Period Basis", + "RJ3H-0": "Period Basis", + "08S-39": "Period Basis", + "ZU-MS3": "Period Basis", + "HIX4-H": "Period Basis", + "GR-J8B": "Period Basis", + "OY0-2T": "Period Basis", + "E2-RDQ": "Period Basis", + "TN25-J": "Period Basis", + "PA-VE3": "Period Basis", + "G-Q5JU": "Period Basis", + "RYQC-I": "Period Basis", + "1E-W5I": "Period Basis", + "Z-M5A1": "Period Basis", + "MVUO-F": "Period Basis", + "Luminaire": "Essence", + "Mies": "Essence", + "Oursulaert": "Essence", + "Renyn": "Essence", + "Duripant": "Essence", + "Algogille": "Essence", + "Caslemon": "Essence", + "Jolevier": "Essence", + "Mesybier": "Essence", + "Charmerout": "Essence", + "Yvangier": "Essence", + "Pemene": "Essence", + "Heydieles": "Essence", + "Fliet": "Essence", + "Actee": "Essence", + "Indregulle": "Essence", + "Amane": "Essence", + "Abune": "Essence", + "Deven": "Essence", + "Estaunitte": "Essence", + "Deninard": "Essence", + "Hulmate": "Essence", + "Annages": "Essence", + "Onne": "Essence", + "Vitrauze": "Essence", + "Palmon": "Essence", + "Villore": "Essence", + "Arant": "Essence", + "Allamotte": "Essence", + "Obalyu": "Essence", + "Vifrevaert": "Essence", + "Parts": "Essence", + "Ladistier": "Essence", + "Old Man Star": "Essence", + "Arnon": "Essence", + "Laurvier": "Essence", + "Adirain": "Essence", + "Attyn": "Essence", + "Ignebaener": "Essence", + "Aere": "Essence", + "Lisbaetanne": "Essence", + "Aeschee": "Essence", + "Allebin": "Essence", + "Atlulle": "Essence", + "Droselory": "Essence", + "Haine": "Essence", + "Perckhevin": "Essence", + "Isenan": "Essence", + "Synchelle": "Essence", + "Wysalan": "Essence", + "Yona": "Essence", + "Noghere": "Essence", + "Aporulie": "Essence", + "Seyllin": "Essence", + "Adrel": "Essence", + "Ane": "Essence", + "Clorteler": "Essence", + "Atlangeins": "Essence", + "Derririntel": "Essence", + "Cat": "Essence", + "Ommare": "Essence", + "Andole": "Essence", + "Vale": "Essence", + "Couster": "Essence", + "Hecarrin": "Essence", + "Henebene": "Essence", + "Mesokel": "Essence", + "Fensi": "Kor-Azor", + "Nebian": "Kor-Azor", + "Khabara": "Kor-Azor", + "Jeni": "Kor-Azor", + "Bridi": "Kor-Azor", + "Ami": "Kor-Azor", + "Amdonen": "Kor-Azor", + "Mora": "Kor-Azor", + "Kor-Azor Prime": "Kor-Azor", + "Leva": "Kor-Azor", + "Nishah": "Kor-Azor", + "Masanuh": "Kor-Azor", + "Sehmy": "Kor-Azor", + "Nakregde": "Kor-Azor", + "Danyana": "Kor-Azor", + "Nahyeen": "Kor-Azor", + "Jinkah": "Kor-Azor", + "Nibainkier": "Kor-Azor", + "Polfaly": "Kor-Azor", + "Andrub": "Kor-Azor", + "Kulu": "Kor-Azor", + "Choga": "Kor-Azor", + "Soumi": "Kor-Azor", + "Imih": "Kor-Azor", + "Nare": "Kor-Azor", + "Zinkon": "Kor-Azor", + "Kizama": "Kor-Azor", + "Shaha": "Kor-Azor", + "Neesher": "Kor-Azor", + "Misha": "Kor-Azor", + "Ordion": "Kor-Azor", + "Perbhe": "Kor-Azor", + "Abath": "Kor-Azor", + "Schmaeel": "Kor-Azor", + "Mafra": "Kor-Azor", + "Arzi": "Kor-Azor", + "Kerying": "Kor-Azor", + "Zorenyen": "Kor-Azor", + "Oguser": "Kor-Azor", + "Nahol": "Kor-Azor", + "Tadadan": "Kor-Azor", + "Tralasa": "Kor-Azor", + "Gademam": "Kor-Azor", + "Pananan": "Kor-Azor", + "Daran": "Kor-Azor", + "Latari": "Kor-Azor", + "Shokal": "Kor-Azor", + "Atarli": "Kor-Azor", + "Keproh": "Kor-Azor", + "Zatamaka": "Kor-Azor", + "Rannoze": "Kor-Azor", + "Piri": "Kor-Azor", + "Enal": "Kor-Azor", + "Jedandan": "Kor-Azor", + "Miroona": "Kor-Azor", + "Ranni": "Kor-Azor", + "Arza": "Kor-Azor", + "Liparer": "Kor-Azor", + "Annad": "Kor-Azor", + "Chaktaren": "Kor-Azor", + "Conoban": "Kor-Azor", + "B-B0ME": "Perrigen Falls", + "TDP-T3": "Perrigen Falls", + "H-HGGJ": "Perrigen Falls", + "OJT-J3": "Perrigen Falls", + "A9-F18": "Perrigen Falls", + "DE-IHK": "Perrigen Falls", + "AY9X-Q": "Perrigen Falls", + "XU7-CH": "Perrigen Falls", + "2V-ZHM": "Perrigen Falls", + "V-3K7C": "Perrigen Falls", + "AK-L0Z": "Perrigen Falls", + "R-AG7W": "Perrigen Falls", + "E-WMT7": "Perrigen Falls", + "FLK-LJ": "Perrigen Falls", + "0FG-KS": "Perrigen Falls", + "F-5WYK": "Perrigen Falls", + "EF-QZK": "Perrigen Falls", + "RZ3O-K": "Perrigen Falls", + "LW-YEW": "Perrigen Falls", + "HB-KSF": "Perrigen Falls", + "EH2I-P": "Perrigen Falls", + "OP7-BP": "Perrigen Falls", + "5ZU-VG": "Perrigen Falls", + "6-1T6Z": "Perrigen Falls", + "R-AYGT": "Perrigen Falls", + "G-GRSZ": "Perrigen Falls", + "6-8QLA": "Perrigen Falls", + "5T-A3D": "Perrigen Falls", + "H-FOYG": "Perrigen Falls", + "1A8-6G": "Perrigen Falls", + "PE-SAM": "Perrigen Falls", + "RY-2FX": "Perrigen Falls", + "K-3PQW": "Perrigen Falls", + "4-M1TY": "Perrigen Falls", + "C6CG-W": "Perrigen Falls", + "H-29TM": "Perrigen Falls", + "KOI8-Z": "Perrigen Falls", + "D-QJR9": "Perrigen Falls", + "U4-V3J": "Perrigen Falls", + "B9N2-2": "Perrigen Falls", + "6Q4-X6": "Perrigen Falls", + "BEG-RL": "Perrigen Falls", + "972C-1": "Perrigen Falls", + "U-W436": "Perrigen Falls", + "Z-ENUD": "Perrigen Falls", + "MJ-5F9": "Perrigen Falls", + "M5NO-B": "Perrigen Falls", + "JZ-UQC": "Perrigen Falls", + "JPEZ-R": "Perrigen Falls", + "9WVY-F": "Perrigen Falls", + "7M4-4C": "Perrigen Falls", + "2-YO2K": "Perrigen Falls", + "M-SG47": "Perrigen Falls", + "SR-10Z": "Perrigen Falls", + "W-KXEX": "Perrigen Falls", + "TAL1-3": "Perrigen Falls", + "QHY-RU": "Perrigen Falls", + "7AH-SF": "Perrigen Falls", + "7MMJ-3": "Perrigen Falls", + "PVF-N9": "Perrigen Falls", + "9-EXU9": "Perrigen Falls", + "4-1ECP": "Perrigen Falls", + "UYOC-1": "Perrigen Falls", + "5-U12M": "Perrigen Falls", + "5V-Q1R": "Perrigen Falls", + "M4-KX5": "Perrigen Falls", + "4F9Y-3": "Perrigen Falls", + "MS-RXH": "Perrigen Falls", + "U-3FKL": "Perrigen Falls", + "0XN-SK": "Perrigen Falls", + "J9A-BH": "Perrigen Falls", + "4F6-VZ": "Perrigen Falls", + "B-7LYC": "Perrigen Falls", + "JM0A-4": "Perrigen Falls", + "PT-2KR": "Perrigen Falls", + "L-POLO": "Perrigen Falls", + "8B-A4E": "Perrigen Falls", + "49V-E4": "Perrigen Falls", + "3LL-O0": "Perrigen Falls", + "A1F-22": "Perrigen Falls", + "9-ZA4Z": "Perrigen Falls", + "IU-E9T": "Perrigen Falls", + "NGM-OK": "Perrigen Falls", + "O-QKSM": "Perrigen Falls", + "QKQ3-L": "Perrigen Falls", + "VWES-Y": "Perrigen Falls", + "SY-OLX": "Perrigen Falls", + "XY-ZCI": "Perrigen Falls", + "7JRA-G": "Perrigen Falls", + "W-CSFY": "Perrigen Falls", + "PFV-ZH": "Perrigen Falls", + "L5Y4-M": "Perrigen Falls", + "9IZ-HU": "Perrigen Falls", + "OBV-YC": "Perrigen Falls", + "2AUL-X": "Perrigen Falls", + "F-HQWV": "Perrigen Falls", + "F-A3TR": "Perrigen Falls", + "PA-ALN": "Perrigen Falls", + "01B-88": "Perrigen Falls", + "F18-AY": "Perrigen Falls", + "RZ8A-P": "Perrigen Falls", + "MTO2-2": "Perrigen Falls", + "C3I-D5": "Perrigen Falls", + "0-U2M4": "Perrigen Falls", + "Shera": "Genesis", + "Lor": "Genesis", + "Cleyd": "Genesis", + "Vecamia": "Genesis", + "Ahbazon": "Genesis", + "Atreen": "Genesis", + "Pakhshi": "Genesis", + "Tar": "Genesis", + "Tekaima": "Genesis", + "Manarq": "Genesis", + "Emsar": "Genesis", + "Ourapheh": "Genesis", + "Yulai": "Genesis", + "Tarta": "Genesis", + "Kemerk": "Genesis", + "Nardiarang": "Genesis", + "Ziasad": "Genesis", + "Sibe": "Genesis", + "Makhwasan": "Genesis", + "Zarer": "Genesis", + "Toon": "Genesis", + "Hesarid": "Genesis", + "Ashokon": "Genesis", + "Avyuh": "Genesis", + "Apanake": "Genesis", + "Sheroo": "Genesis", + "Sosh": "Genesis", + "Sigga": "Genesis", + "Keseya": "Genesis", + "Zoohen": "Genesis", + "Serren": "Genesis", + "Hadji": "Genesis", + "Assez": "Genesis", + "Alal": "Genesis", + "Dom-Aphis": "Genesis", + "Iderion": "Genesis", + "Chamja": "Genesis", + "Diaderi": "Genesis", + "Manatirid": "Genesis", + "Pashanai": "Genesis", + "Pamah": "Genesis", + "Leran": "Genesis", + "Beke": "Genesis", + "Malma": "Genesis", + "Noranim": "Genesis", + "Chej": "Genesis", + "Menai": "Genesis", + "Aring": "Genesis", + "Gayar": "Genesis", + "Petidu": "Genesis", + "Naka": "Genesis", + "Madomi": "Genesis", + "Gergish": "Genesis", + "Tahli": "Genesis", + "Imya": "Genesis", + "Kobam": "Genesis", + "Hirizan": "Genesis", + "Anyed": "Genesis", + "Habu": "Genesis", + "Asanot": "Genesis", + "Anzalaisio": "Genesis", + "Chiga": "Genesis", + "Abhan": "Genesis", + "Saphthar": "Genesis", + "Itrin": "Genesis", + "Bantish": "Genesis", + "Korridi": "Genesis", + "Lela": "Genesis", + "Keri": "Genesis", + "Antem": "Genesis", + "Djimame": "Genesis", + "Mozzidit": "Genesis", + "Angur": "Genesis", + "Hangond": "Genesis", + "Access": "Genesis", + "Bherdasopt": "Genesis", + "Gonditsa": "Genesis", + "Simela": "Genesis", + "Shalne": "Genesis", + "Shapisin": "Genesis", + "Olin": "Genesis", + "Galnafsad": "Genesis", + "Otakod": "Genesis", + "Azedi": "Genesis", + "Sharza": "Genesis", + "Pirna": "Genesis", + "Seshi": "Genesis", + "Anara": "Genesis", + "Partod": "Genesis", + "Exit": "Genesis", + "Gateway": "Genesis", + "Central Point": "Genesis", + "Promised Land": "Genesis", + "Dead End": "Genesis", + "New Eden": "Genesis", + "Canard": "Genesis", + "Girani-Fa": "Genesis", + "Nasreri": "Genesis", + "Heorah": "Genesis", + "Ebasez": "Genesis", + "Agal": "Genesis", + "Doza": "Genesis", + "Bania": "Genesis", + "Murethand": "Verge Vendor", + "Melmaniel": "Verge Vendor", + "Ouelletta": "Verge Vendor", + "Costolle": "Verge Vendor", + "Muetralle": "Verge Vendor", + "Loes": "Verge Vendor", + "Tourier": "Verge Vendor", + "Alenia": "Verge Vendor", + "Merolles": "Verge Vendor", + "Alentene": "Verge Vendor", + "Cistuvaert": "Verge Vendor", + "Vaere": "Verge Vendor", + "Aidart": "Verge Vendor", + "Jufvitte": "Verge Vendor", + "Ansalle": "Verge Vendor", + "Scheenins": "Verge Vendor", + "Amygnon": "Verge Vendor", + "Gisleres": "Verge Vendor", + "Ellmay": "Verge Vendor", + "Theruesse": "Verge Vendor", + "Eletta": "Verge Vendor", + "Luse": "Verge Vendor", + "Ekuenbiron": "Verge Vendor", + "Vay": "Verge Vendor", + "Raneilles": "Verge Vendor", + "Hevrice": "Verge Vendor", + "Jovainnon": "Verge Vendor", + "Scolluzer": "Verge Vendor", + "Sortet": "Verge Vendor", + "Claulenne": "Verge Vendor", + "Masalle": "Verge Vendor", + "Annelle": "Verge Vendor", + "Chesiette": "Verge Vendor", + "Reblier": "Verge Vendor", + "Amoderia": "Verge Vendor", + "Arraron": "Verge Vendor", + "Chantrousse": "Verge Vendor", + "Osmomonne": "Verge Vendor", + "Stou": "Verge Vendor", + "Tierijev": "Verge Vendor", + "Adallier": "Verge Vendor", + "Channace": "Verge Vendor", + "Clacille": "Verge Vendor", + "Clellinon": "Verge Vendor", + "Hykanima": "Black Rise", + "Okagaiken": "Black Rise", + "Kehjari": "Black Rise", + "Villasen": "Black Rise", + "Sarenemi": "Black Rise", + "Ashitsu": "Black Rise", + "Korasen": "Black Rise", + "Ienakkamon": "Black Rise", + "Kinakka": "Black Rise", + "Raihbaka": "Black Rise", + "Innia": "Black Rise", + "Iralaja": "Black Rise", + "Martoh": "Black Rise", + "Eha": "Black Rise", + "Pavanakka": "Black Rise", + "Uchomida": "Black Rise", + "Samanuni": "Black Rise", + "Astoh": "Black Rise", + "Onnamon": "Black Rise", + "Rohamaa": "Black Rise", + "Uuhulanen": "Black Rise", + "Tsuruma": "Black Rise", + "Ahtila": "Black Rise", + "Ichoriya": "Black Rise", + "Okkamon": "Black Rise", + "Vaaralen": "Black Rise", + "Asakai": "Black Rise", + "Prism": "Black Rise", + "Mushikegi": "Black Rise", + "Teskanen": "Black Rise", + "Elunala": "Black Rise", + "Ikoskio": "Black Rise", + "Hikkoken": "Black Rise", + "Enaluri": "Black Rise", + "Aivonen": "Black Rise", + "Hallanen": "Black Rise", + "Akidagi": "Black Rise", + "Immuri": "Black Rise", + "Nennamaila": "Black Rise", + "Hirri": "Black Rise", + "Kedama": "Black Rise", + "Oinasiken": "Black Rise", + "Notoras": "Black Rise", + "Rakapas": "Black Rise", + "Teimo": "Black Rise", + "Iwisoda": "Black Rise", + "Nisuwa": "Black Rise", + "Pynekastoh": "Black Rise", + "Reitsato": "Black Rise" +} + +import logging + +def buildUpperKeyedAliases(): + """Return a dictionary of systems keyed on their upper case name. + Each entry is a dictionary with name and region fields in standardized DOTLAN case + """ + systems = dict() + for s in SYSTEMS: + value = { 'name': s, 'region': SYSTEMS[s] } + upper = s.upper() + systems[upper] = value + + if '-' in upper: + # - short with - (minus), I-I will match I43-IF3 + parts = upper.split("-") + if len(parts) != 2: + logging.critical("Unexpected number of dashes in system name [%s]", s) + systems[parts[0][0]+':'+parts[1][0]] = value + # also add systems removing dashes + nodash = upper.replace("-", "") + systems[nodash] = value + + if '0' in upper: + ohs = upper.replace("0","O") + systems[ohs] = value + + if 'O' in upper: + zeros = upper.replace("O","0") + systems[zeros] = value + + return systems diff --git a/src/vi/threads.py b/src/vi/threads.py index f42c9e9..596814d 100755 --- a/src/vi/threads.py +++ b/src/vi/threads.py @@ -38,9 +38,9 @@ def __init__(self): self.active = True - def addChatEntry(self, chatEntry, clearCache=False): + def addChatEntry(self, chatEntry, clearAvatarCacheForUser=False): try: - if clearCache: + if clearAvatarCacheForUser: cache = Cache() cache.removeAvatar(chatEntry.message.user) @@ -78,6 +78,7 @@ def run(self): lastCall = time.time() if avatar: cache.putAvatar(charname, avatar) + logging.debug("AvatarFindThread storing avatar for %s" % charname) if avatar: logging.debug("AvatarFindThread emit avatar_update for %s" % charname) self.emit(SIGNAL("avatar_update"), chatEntry, avatar) diff --git a/src/vi/ui/viui.py b/src/vi/ui/viui.py index cfa063f..1aa39c7 100644 --- a/src/vi/ui/viui.py +++ b/src/vi/ui/viui.py @@ -36,7 +36,7 @@ from PyQt4.QtWebKit import QWebPage from vi import amazon_s3, evegate from vi import dotlan, filewatcher -from vi import states +from vi import states, systems from vi.cache.cache import Cache from vi.resources import resourcePath from vi.soundmanager import SoundManager @@ -48,7 +48,7 @@ from PyQt4.QtGui import QMessageBox # Timer intervals -MESSAGE_EXPIRY_SECS = 20 * 60 +MESSAGE_EXPIRY_SECS = 60 * 60 * 1 MAP_UPDATE_INTERVAL_MSECS = 4 * 1000 CLIPBOARD_CHECK_INTERVAL_MSECS = 4 * 1000 @@ -86,6 +86,10 @@ def __init__(self, pathToLogs, trayIcon, backGroundColor): self.scanIntelForKosRequestsEnabled = True self.initialMapPosition = None self.mapPositionsDict = {} + self.chatparser = None + self.systemsWithRegions = systems.buildUpperKeyedAliases() + + self.chatbox.setTitle("All Intel (past {0} minues)".format(str(MESSAGE_EXPIRY_SECS/60))) # Load user's toon names self.knownPlayerNames = self.cache.getFromCache("known_player_names") @@ -140,6 +144,7 @@ def __init__(self, pathToLogs, trayIcon, backGroundColor): self.updateRegionMenu() self.updateOtherRegionMenu() self.setupMap(True) + self.replayLogs() def paintEvent(self, event): @@ -211,7 +216,7 @@ def updateRegionMenu(self): self.menuRegion.insertAction(orm, menuItem) def onRegionSelect(self, region): - logging.critical("NEW REGION: [%s]" % (region)) + logging.info("NEW REGION: [%s]", region) Cache().saveConfigValue("region_name", region) self.handleRegionChosen() @@ -307,10 +312,18 @@ def setupMap(self, initialize=False): # Load the jumpbridges logging.critical("Load jump bridges") self.setJumpbridges(self.cache.getConfigValue("jumpbridge_url")) + self.systems = self.dotlan.systems logging.critical("Creating chat parser") - self.chatparser = ChatParser(self.pathToLogs, self.roomnames, self.systems) + oldParser = self.chatparser + self.chatparser = ChatParser(self.pathToLogs, self.roomnames, self.systemsWithRegions, MESSAGE_EXPIRY_SECS) + if oldParser: + scrollPosition = self.chatListWidget.verticalScrollBar().value() + self.chatListWidget.clear() + self.processLogMessages(oldParser.knownMessages) + self.chatListWidget.verticalScrollBar().setSliderPosition(scrollPosition) + self.chatparser.knownMessages = oldParser.knownMessages # Menus - only once if initialize: @@ -334,6 +347,7 @@ def mapContextMenuEvent(event): self.updateMapView() self.setInitialMapPositionForRegion(regionName) self.mapTimer.start(MAP_UPDATE_INTERVAL_MSECS) + # Allow the file watcher to run now that all else is set up self.filewatcherThread.paused = False logging.critical("Map setup complete") @@ -576,8 +590,24 @@ def mapLinkClicked(self, url): def markSystemOnMap(self, systemname): - self.systems[six.text_type(systemname)].mark() - self.updateMapView() + n = six.text_type(systemname) + if n in self.systems: + self.systems[n].mark() + self.updateMapView() + elif n.upper() in self.systemsWithRegions: + logging.warn('System [%s] is in another region [%s]', + self.systemsWithRegions[n]['name'], self.systemsWithRegions[n]['region']) + ans = QMessageBox.question(self, + u"System not on current map", + u"{0} is in {1}. Would you like to view the {1} map?".format( + six.text_type(self.systemsWithRegions[n]['name']), + six.text_type(self.systemsWithRegions[n]['region'])), + QMessageBox.Ok, QMessageBox.Cancel) + if QMessageBox.Ok == ans: + self.onRegionSelect(self.systemsWithRegions[n]['region']) + self.markSystemOnMap(systemname) + else: + logging.warn('System [%s] is unknown.', n) def setLocation(self, char, newSystem): @@ -585,7 +615,7 @@ def setLocation(self, char, newSystem): system.removeLocatedCharacter(char) if not newSystem == "?" and newSystem in self.systems: self.systems[newSystem].addLocatedCharacter(char) - self.setMapContent(self.dotlan.svg) + self.updateMapView() def setMapContent(self, content): @@ -674,6 +704,16 @@ def showRegionChooser(self): self.connect(chooser, SIGNAL("new_region_chosen"), self.handleRegionChosen) chooser.show() + def replayLogs(self): + """On startup, replay info from logfiles""" + messages = [] + for path in self.chatparser.rewind(): + messages.extend(self.chatparser.fileModified(path)) + messages.sort(key=lambda x: x.timestamp) + # we use these parsed messages to replay events on region switch, reset them to a time ordered list + self.chatparser.knownMessages = messages + self.processLogMessages(messages) + logging.critical("Done with replay") def addMessageToIntelChat(self, message): scrollToBottom = False @@ -694,6 +734,7 @@ def addMessageToIntelChat(self, message): def pruneMessages(self): + self.chatparser.expire() try: now = time.mktime(evegate.currentEveTime().timetuple()) for row in range(self.chatListWidget.count()): @@ -774,7 +815,7 @@ def systemTrayActivated(self, reason): def updateAvatarOnChatEntry(self, chatEntry, avatarData): updated = chatEntry.updateAvatar(avatarData) if not updated: - self.avatarFindThread.addChatEntry(chatEntry, clearCache=True) + self.avatarFindThread.addChatEntry(chatEntry) # , clearCache=True) else: self.emit(SIGNAL("avatar_loaded"), chatEntry.message.user, avatarData) @@ -806,7 +847,14 @@ def zoomMapOut(self): def logFileChanged(self, path): messages = self.chatparser.fileModified(path) + self.processLogMessages(messages) + + def processLogMessages(self, messages): for message in messages: + + # This function is a resource pig, give others a chance to run while we process messages + time.sleep(0) + # If players location has changed if message.status == states.LOCATION: self.knownPlayerNames.add(message.user) @@ -825,11 +873,13 @@ def logFileChanged(self, path): self.addMessageToIntelChat(message) # For each system that was mentioned in the message, check for alarm distance to the current system # and alarm if within alarm distance. - systemList = self.dotlan.systems if message.systems: - for system in message.systems: - systemname = system.name - systemList[systemname].setStatus(message.status) + for systemname in message.systems: + if not systemname in self.systems: + logging.debug("No dotlan match for system [%s], maybe it's not shown right now:", systemname) + continue + system = self.systems[systemname] + system.setStatus(message.status, message.timestamp) if message.status in (states.REQUEST, states.ALARM) and message.user not in self.knownPlayerNames: alarmDistance = self.alarmDistance if message.status == states.ALARM else 0 for nSystem, data in system.getNeighbours(alarmDistance).items(): @@ -837,8 +887,10 @@ def logFileChanged(self, path): chars = nSystem.getLocatedCharacters() if len(chars) > 0 and message.user not in chars: self.trayIcon.showNotification(message, system.name, ", ".join(chars), distance) - self.setMapContent(self.dotlan.svg) + system.messages.append(message) + # call once after all messages are processed + self.updateMapView() class RegionChooser(QtGui.QDialog): def __init__(self, parent): @@ -893,13 +945,16 @@ def __init__(self, parent, chatType, selector, chatEntries, knownPlayerNames): self.parent = parent self.chatType = 0 self.selector = selector - self.chatEntries = [] - for entry in chatEntries: - self.addChatEntry(entry) titleName = "" + self.chatEntries = [] if self.chatType == SystemChat.SYSTEM: - titleName = "%s [%s]" % (self.selector.name, self.selector.secondaryInfo) self.system = selector + systemDisplayName = self.system.name + if systemDisplayName in parent.systemsWithRegions: + systemDisplayName = parent.systemsWithRegions[systemDisplayName]['name'] + titleName = "%s [%s]" % (systemDisplayName, self.selector.secondaryInfo) + for entry in chatEntries: + self.addChatEntry(entry) for name in knownPlayerNames: self.playerNamesBox.addItem(name) self.setWindowTitle("Chat for {0}".format(titleName)) @@ -928,10 +983,12 @@ def _addMessageToChat(self, message, avatarPixmap): def addChatEntry(self, entry): if self.chatType == SystemChat.SYSTEM: message = entry.message - avatarPixmap = entry.avatarLabel.pixmap() - if self.selector in message.systems: - self._addMessageToChat(message, avatarPixmap) - + try: + avatarPixmap = entry.avatarLabel.pixmap() + if self.system.name in message.systems: + self._addMessageToChat(message, avatarPixmap) + except: + pass def locationSet(self): char = six.text_type(self.playerNamesBox.currentText()) @@ -1003,7 +1060,10 @@ def updateAvatar(self, avatarData): if pixmap.isNull(): return False scaledAvatar = pixmap.scaled(32, 32) - self.avatarLabel.setPixmap(scaledAvatar) + try: + self.avatarLabel.setPixmap(scaledAvatar) + except: + pass return True diff --git a/src/vintel.spec b/src/vintel.spec index 6bf561b..62fd559 100644 --- a/src/vintel.spec +++ b/src/vintel.spec @@ -19,11 +19,9 @@ a = Analysis(['vintel.py'], pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) a.datas += [('vi/ui/MainWindow.ui', 'vi/ui/MainWindow.ui', 'DATA'), - ('vi/ui/SystemChat.ui', 'vi/ui/SystemChat.ui', 'DATA'), + ('vi/ui/SettingsTabs.ui', 'vi/ui/SettingsTabs.ui', 'DATA'), ('vi/ui/ChatEntry.ui', 'vi/ui/ChatEntry.ui', 'DATA'), ('vi/ui/Info.ui', 'vi/ui/Info.ui', 'DATA'), - ('vi/ui/ChatroomsChooser.ui', 'vi/ui/ChatroomsChooser.ui', 'DATA'), - ('vi/ui/RegionChooser.ui', 'vi/ui/RegionChooser.ui', 'DATA'), ('vi/ui/SoundSetup.ui', 'vi/ui/SoundSetup.ui', 'DATA'), ('vi/ui/JumpbridgeChooser.ui', 'vi/ui/JumpbridgeChooser.ui', 'DATA'), ('vi/ui/res/qmark.png', 'vi/ui/res/qmark.png', 'DATA'), From 6ab34e80ee54b44fcb060fa091a606286564ddd0 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 01:39:05 -0500 Subject: [PATCH 17/36] more ships & less character ignoring --- src/vi/chatparser/parser_functions.py | 3 ++- src/vi/evegate.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vi/chatparser/parser_functions.py b/src/vi/chatparser/parser_functions.py index 564ffa5..ad4f030 100644 --- a/src/vi/chatparser/parser_functions.py +++ b/src/vi/chatparser/parser_functions.py @@ -82,7 +82,8 @@ def formatShipName(text, word): texts = [t for t in rtext.contents if isinstance(t, NavigableString)] for text in texts: - upperText = re.sub(CHARS_TO_IGNORE_REGEX, ' ', text.strip().upper()) + # upperText = re.sub(CHARS_TO_IGNORE_REGEX, ' ', text.strip().upper()) + upperText = text.upper() for shipName in evegate.SHIPNAMES: if shipName in upperText: hit = True diff --git a/src/vi/evegate.py b/src/vi/evegate.py index 442585c..7ceef99 100644 --- a/src/vi/evegate.py +++ b/src/vi/evegate.py @@ -336,8 +336,10 @@ def secondsTillDowntime(): u'TEMPEST TRIBAL ISSUE', u'THANATOS', u'THORAX', u'THRASHER', u'TORMENTOR', u'TRISTAN', u'TYPHOON', u'VAGABOND', u'VARGUR', u'VELATOR', u'VENGEANCE', u'VEXOR', u'VEXOR NAVY ISSUE', u'VIATOR', u'VIGIL', u'VIGILANT', u'VINDICATOR', u'VISITANT', u'VULTURE', u'WIDOW', u'WOLF', u'WORM', u'WRAITH', u'WREATHE', - u'WYVERN', u'ZEALOT', u'CAPSULE', u'BOMBER', + u'WYVERN', u'ZEALOT', u'CAPSULE', + u'BOMBER', u'LOGI', u'T3D', u'JACKDAW', u'SVIPUL', u'CONFESSOR', u'HECTATE', + u'T3C', u'TENGU', u'LOKI', u'LEGION', u'PROTEUS', u'STORK', u'BIFROST', u'PONTIFEX', u'MAGUS') SHIPNAMES = sorted(SHIPNAMES, key=lambda x: len(x), reverse=True) From dc432021aedc0b6ef27a3bb04e83f15dc24ed1bf Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 10:29:39 -0500 Subject: [PATCH 18/36] rename for extension --- quick_config_cva => quick_config_cva.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename quick_config_cva => quick_config_cva.json (100%) diff --git a/quick_config_cva b/quick_config_cva.json similarity index 100% rename from quick_config_cva rename to quick_config_cva.json From f10b25aa4a2c911e41228c0a19cd125901821278 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 11:21:04 -0500 Subject: [PATCH 19/36] Document the quick config file. --- SETUP.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 SETUP.md diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..7ae2f81 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,53 @@ +# Vintel Setup + +This document describes the settings available in the Vintel application. + +## Quick Config + +The first pane of the settings allows you to paste a quick configuration JSON +blob that may be provided by your alliance. + +1. *dotlan_jb_id* The id of a jumpbridge list from DOTLAN. Include just the id, + i.e. from http://evemaps.dotlan.net/bridges/*XXXXxxxx* use the XXXXxxxx +2. *jumpbridge_url* A url to a file containing your bridge list. Each line + in the file should contain two systems separated by a `<->`. i.e. + `HED-GP <-> 36N-HZ` +3. *channels* A JSON list of the channels to be monitored. +4. *kos_url* A URL to your alliances KOS server. +5. *region_name* The default region to load on launch. Later, the application will + remember the last region viewed and load that when you restart. +6. *quick_regions* The regions listed here are automatically added to the + application's *Region* menu. Any name starting with a dash will be interpreted + as a separator and will draw a horizontal line between entries. Each entry + should have a *label* field which is how it will appear in the menu. Each + may also have a *region* field which can be used to override the region name + used when looking up this system on DOTLAN if the label is not an exact match + (spaces will be automatically converted to underscores). + + The region should match a DOTLAN url - i.e. http://evemaps.dotlan.net/map/*Catch*. + The region can also be one of the special or combined maps listed at the + bottom of the DOTLAN universe page. + + +``` +{ + "dotlan_jb_id": "", + "jumpbridge_url": "", + "channels": [ + "TheCitadel", + "North Provi Intel", + "North Catch Intel", + "North Querious Intel" + ], + "kos_url": "http://kos.cva-eve.org/api/", + "region_name": "Catch", + "quick_regions": [ + {"label": "Catch"}, + {"label": "Providence"}, + {"label": "Querious"}, + {"label": "---"}, + {"label": "Provi / Catch", "region": "providencecatch"}, + {"label": "Provi / Catch (compact)", "region": "Providence-catch"} + ] +} +``` From 7d5822fbd3a7184d609a711e793647ba31735054 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 11:25:11 -0500 Subject: [PATCH 20/36] urls were not meant to be clickable --- SETUP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SETUP.md b/SETUP.md index 7ae2f81..382e464 100644 --- a/SETUP.md +++ b/SETUP.md @@ -8,7 +8,7 @@ The first pane of the settings allows you to paste a quick configuration JSON blob that may be provided by your alliance. 1. *dotlan_jb_id* The id of a jumpbridge list from DOTLAN. Include just the id, - i.e. from http://evemaps.dotlan.net/bridges/*XXXXxxxx* use the XXXXxxxx + i.e. from http://evemaps.dotlan.net/bridges/*XXXXxxxx* use the XXXXxxxx 2. *jumpbridge_url* A url to a file containing your bridge list. Each line in the file should contain two systems separated by a `<->`. i.e. `HED-GP <-> 36N-HZ` @@ -24,7 +24,7 @@ blob that may be provided by your alliance. used when looking up this system on DOTLAN if the label is not an exact match (spaces will be automatically converted to underscores). - The region should match a DOTLAN url - i.e. http://evemaps.dotlan.net/map/*Catch*. + The region should match a DOTLAN url - i.e. http://evemaps.dotlan.net/map/*Catch*. The region can also be one of the special or combined maps listed at the bottom of the DOTLAN universe page. From 9a63420012df11fea43dde01499b5b7e86072bdf Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 11:27:12 -0500 Subject: [PATCH 21/36] be bolder --- SETUP.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/SETUP.md b/SETUP.md index 382e464..2b828f8 100644 --- a/SETUP.md +++ b/SETUP.md @@ -7,25 +7,25 @@ This document describes the settings available in the Vintel application. The first pane of the settings allows you to paste a quick configuration JSON blob that may be provided by your alliance. -1. *dotlan_jb_id* The id of a jumpbridge list from DOTLAN. Include just the id, - i.e. from http://evemaps.dotlan.net/bridges/*XXXXxxxx* use the XXXXxxxx -2. *jumpbridge_url* A url to a file containing your bridge list. Each line +1. **dotlan_jb_id** The id of a jumpbridge list from DOTLAN. Include just the id, + i.e. from http://evemaps.dotlan.net/bridges/**XXXXxxxx** use the XXXXxxxx +2. **jumpbridge_url** A url to a file containing your bridge list. Each line in the file should contain two systems separated by a `<->`. i.e. `HED-GP <-> 36N-HZ` -3. *channels* A JSON list of the channels to be monitored. -4. *kos_url* A URL to your alliances KOS server. -5. *region_name* The default region to load on launch. Later, the application will +3. **channels** A JSON list of the channels to be monitored. +4. **kos_url** A URL to your alliances KOS server. +5. **region_name** The default region to load on launch. Later, the application will remember the last region viewed and load that when you restart. -6. *quick_regions* The regions listed here are automatically added to the +6. **quick_regions** The regions listed here are automatically added to the application's *Region* menu. Any name starting with a dash will be interpreted as a separator and will draw a horizontal line between entries. Each entry - should have a *label* field which is how it will appear in the menu. Each - may also have a *region* field which can be used to override the region name + should have a **label** field which is how it will appear in the menu. Each + may also have a **region** field which can be used to override the region name used when looking up this system on DOTLAN if the label is not an exact match (spaces will be automatically converted to underscores). - The region should match a DOTLAN url - i.e. http://evemaps.dotlan.net/map/*Catch*. - The region can also be one of the special or combined maps listed at the + The **region** should match a DOTLAN url - i.e. http://evemaps.dotlan.net/map/**Catch**. + The **region** can also be one of the special or combined maps listed at the bottom of the DOTLAN universe page. From 0b7760613d1e28a352300c12702dde0130d02250 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 11:28:55 -0500 Subject: [PATCH 22/36] Update SETUP.md --- SETUP.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SETUP.md b/SETUP.md index 2b828f8..ddd0694 100644 --- a/SETUP.md +++ b/SETUP.md @@ -26,8 +26,10 @@ blob that may be provided by your alliance. The **region** should match a DOTLAN url - i.e. http://evemaps.dotlan.net/map/**Catch**. The **region** can also be one of the special or combined maps listed at the - bottom of the DOTLAN universe page. + bottom of the DOTLAN universe page. Vintel also recognizes the custom region + providencecatch. +This is what the Vintel defaults would look like as a quick configuration entry: ``` { From 5b6e3af3032235d6e806f1feb893f8e223fc40cc Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 11:45:19 -0500 Subject: [PATCH 23/36] Add info about in app menus. --- SETUP.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/SETUP.md b/SETUP.md index ddd0694..5dfd59b 100644 --- a/SETUP.md +++ b/SETUP.md @@ -2,7 +2,23 @@ This document describes the settings available in the Vintel application. -## Quick Config +## Settings Dialog + +The settings dialog has three panes: *Quick Setup*, *Jumpbridges*, and *Chat Channels*. + +The *Jumpbridges* pane allows you to specify a source for jumpbridges to be +rendered on the maps. You may specify either a DOTLAN jumpbridge list id or an URL to +a file containing your own jumpbridge list. More details on the format of the file +can be seen in the application pane or in *Quick Setup* below. + +The *Chat Channels* pane allows you to specify EVE chat channels to monitor for +intelligence reports. You must keep this chat channel open in a tab in game +and have *Log Chat to File* selected in EVE Settings > Chat. Vintel monitors and +processes these log files on your local disk. + +The *Quick Setup* + +## Quick Setup The first pane of the settings allows you to paste a quick configuration JSON blob that may be provided by your alliance. @@ -53,3 +69,61 @@ This is what the Vintel defaults would look like as a quick configuration entry: ] } ``` + +## Other Menu Settings + +### File > Clear Cache + +If you notice any weirdness with maps or avatars, you may try flushing the Vintel +cache. Avatars and maps will be redownloaded. + +### Chat > Show Chat + +Hide the chat history on the right side of the screen + +### Chat > Show Chat Avatars + +Do not show character portraits in the chat history. Unfortunately selecting this +doesn't yet make the chat window more compact. + +### Sound > Activate Sound + +This should be checked to enable sound in Vintel. + +## Sound > Sound Setup ... + +Test the sound configuration. + +## Sound > Spoken Notifications + +Doesn't work on Windows 10, what does it do? + +## Region + +Select a region from the quick region shortcuts configured in *Quick Settings* + +## Region > Other Region + +Select from a list of all known EVE regions. + +## Region > Custom Region ... + +Displays a freeform text box to enter a standard or custom region by name. + +## K.O.S. + +Activate some of the Kill On Sight processing and warnings. + +## Window > Always On Top + +Keep this window above all other windows. + +## Window > Frameless Main Window + +Remove OS decorations from the window: titlebar, menubar, ... Once activated a *Restore Frame* button will +be available in the upper left corner of the screen. Click that to restore the window decorations. + +## Window > Transparency + +Set the window opacity. + From 2218ea6054b64935c7c5e654190682f721900297 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 11:45:54 -0500 Subject: [PATCH 24/36] Update SETUP.md --- SETUP.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SETUP.md b/SETUP.md index 5dfd59b..ff9b475 100644 --- a/SETUP.md +++ b/SETUP.md @@ -94,36 +94,36 @@ This should be checked to enable sound in Vintel. Test the sound configuration. -## Sound > Spoken Notifications +### Sound > Spoken Notifications Doesn't work on Windows 10, what does it do? -## Region +### Region Select a region from the quick region shortcuts configured in *Quick Settings* -## Region > Other Region +### Region > Other Region Select from a list of all known EVE regions. -## Region > Custom Region ... +### Region > Custom Region ... Displays a freeform text box to enter a standard or custom region by name. -## K.O.S. +### K.O.S. Activate some of the Kill On Sight processing and warnings. -## Window > Always On Top +### Window > Always On Top Keep this window above all other windows. -## Window > Frameless Main Window +### Window > Frameless Main Window Remove OS decorations from the window: titlebar, menubar, ... Once activated a *Restore Frame* button will be available in the upper left corner of the screen. Click that to restore the window decorations. -## Window > Transparency +### Window > Transparency Set the window opacity. From a970b9973f7fb0812b774a452e37109f69ea987c Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 11:46:14 -0500 Subject: [PATCH 25/36] Update SETUP.md --- SETUP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SETUP.md b/SETUP.md index ff9b475..7106b9f 100644 --- a/SETUP.md +++ b/SETUP.md @@ -90,7 +90,7 @@ doesn't yet make the chat window more compact. This should be checked to enable sound in Vintel. -## Sound > Sound Setup ... +### Sound > Sound Setup ... Test the sound configuration. From 3c85adb474411cd105e2831c42db9a97489b501e Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 11:46:40 -0500 Subject: [PATCH 26/36] Update SETUP.md --- SETUP.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/SETUP.md b/SETUP.md index 7106b9f..d798e21 100644 --- a/SETUP.md +++ b/SETUP.md @@ -16,8 +16,6 @@ intelligence reports. You must keep this chat channel open in a tab in game and have *Log Chat to File* selected in EVE Settings > Chat. Vintel monitors and processes these log files on your local disk. -The *Quick Setup* - ## Quick Setup The first pane of the settings allows you to paste a quick configuration JSON From 4c8e1f413b0b71b914c72005c76c8bee96e480e0 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 21:39:14 -0500 Subject: [PATCH 27/36] Merge remote-tracking branch 'origin/qt4x-dev' into qt5x-dev. Interrupted stash mid-operation, hope this captured all files :( # Conflicts: # src/vi/amazon_s3.py # src/vi/cache/cache.py # src/vi/chatparser/chatparser.py # src/vi/chatparser/parser_functions.py # src/vi/evegate.py # src/vi/koschecker.py # src/vi/threads.py # src/vi/ui/MainWindow.ui # src/vi/ui/systemtray.py # src/vi/ui/viui.py # src/vi/version.py # src/vintel.py # src/vintel.spec --- README.md | 5 +- TODO.md | 19 + src/vi/PanningWebView.py | 98 +++-- src/vi/amazon_s3.py | 10 +- src/vi/cache/cache.py | 14 - src/vi/chatparser/chatparser.py | 5 +- src/vi/evegate.py | 2 +- src/vi/filewatcher.py | 10 +- src/vi/koschecker.py | 10 +- src/vi/soundmanager.py | 181 ++------- src/vi/threads.py | 30 +- src/vi/ui/SoundSetup.ui | 13 + src/vi/ui/res/mapdata/Providencecatch.svg | 2 +- src/vi/ui/systemtray.py | 41 +- src/vi/ui/viui.py | 463 ++++++++++++++-------- src/vi/version.py | 4 +- src/vintel.py | 18 +- 17 files changed, 499 insertions(+), 426 deletions(-) create mode 100644 TODO.md diff --git a/README.md b/README.md index 509b8f5..3e0d0cc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Visual intel chat analysis, planning and notification application for [EVE Online](http://www.eveonline.com). Gathers status through in-game intelligence channels on all known hostiles and presents all the data on a [dotlan](http://evemaps.dotlan.net/map/Cache#npc24) generated regional map. The map is annotated in real-time as players report intel in monitored chat channels. -Vintel is written with Python 2.7, using PyQt4 for the application presentation layer, BeautifulSoup4 for SVG parsing, and Pyglet for audio playback. +Vintel is written with Python 2.7, using PyQt5 for the application presentation layer, BeautifulSoup4 for SVG parsing, and Pyglet for audio playback. ### News _The current release version of Vintel [can be found here](https://github.com/Xanthos-Eve/vintel/releases). Both Mac and Windows distributions are now available for download with this release._ @@ -79,11 +79,10 @@ https://pypi.python.org/pypi/requests The anaconda python package will come with most of what you need [download](https://www.continuum.io/downloads#windows). vintel compilation has been tested with 64 bit anaconda2 v4.2.0 for python 2.7 on windows 10. -You will need to run these two commands to downgrade qt5 to qt4 and install the pyglet package. If you installed anaconda for all +You will need to run this command to install the pyglet package. If you installed anaconda for all users, you may need to run them in an *admin* anaconda prompt. ``` -conda install pyqt=4 pip install pyglet ``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..a49ad8c --- /dev/null +++ b/TODO.md @@ -0,0 +1,19 @@ +ROLLED THIS BACK FROM MainWindowUi, seems QWebEngineWidgets is not available in most PyQt5 builds + +``` + + + + QWebView + QWebEngineView + QWidget +
QtWebKit/QWebView
+
PyQt5/QtWebEngineWidgets/QWebEngineView
+
+ + PanningWebView + QWebView + QWebEngineView +
vi/PanningWebView
+
+``` diff --git a/src/vi/PanningWebView.py b/src/vi/PanningWebView.py index cba19e0..6fbd4b1 100644 --- a/src/vi/PanningWebView.py +++ b/src/vi/PanningWebView.py @@ -1,12 +1,20 @@ -from PyQt4.QtWebKit import QWebView -from PyQt4.QtGui import * -from PyQt4 import QtCore -from PyQt4.QtCore import QPoint -from PyQt4.QtCore import QString -from PyQt4.QtCore import QEvent +from PyQt5.QtCore import Qt, QPoint, QRect, QEvent, pyqtSignal, QUrl +from PyQt5.QtGui import QMouseEvent +from PyQt5.QtWidgets import QApplication +from vi.ui.viui import MainWindow -class PanningWebView(QWebView): +if MainWindow.oldStyleWebKit: + from PyQt5.QtWebKitWidgets import QWebPage + from PyQt5.QtWebKitWidgets import QWebView +else: + from PyQt5.QtWebEngineWidgets import QWebEnginePage + from PyQt5.QtWebEngineWidgets import QWebEngineView + + +class PanningWebView(QWebView if MainWindow.oldStyleWebKit else QWebEngineView): + + mapLinkClicked = pyqtSignal(QUrl) def __init__(self, parent=None): super(PanningWebView, self).__init__() @@ -14,36 +22,43 @@ def __init__(self, parent=None): self.scrolling = False self.ignored = [] self.position = None - self.offset = 0 + self.offset = QPoint(0, 0) self.handIsClosed = False self.clickedInScrollBar = False + self.widget = None + self.setPage(VintelSvgPage(parent=self)) + + + def setPage(self, newPage): + super(PanningWebView, self).setPage(newPage) + if MainWindow.oldStyleWebKit: + self.widget = self.page().mainFrame() + else: + self.widget = self.page() def mousePressEvent(self, mouseEvent): pos = mouseEvent.pos() - if self.pointInScroller(pos, QtCore.Qt.Vertical) or self.pointInScroller(pos, QtCore.Qt.Horizontal): + if mouseEvent.buttons() == Qt.LeftButton and (self.pointInScroller(pos, Qt.Vertical) or self.pointInScroller(pos, Qt.Horizontal)): self.clickedInScrollBar = True else: if self.ignored.count(mouseEvent): self.ignored.remove(mouseEvent) - return QWebView.mousePressEvent(self, mouseEvent) + return super(PanningWebView, self).mousePressEvent(mouseEvent) - if not self.pressed and not self.scrolling and mouseEvent.modifiers() == QtCore.Qt.NoModifier: - if mouseEvent.buttons() == QtCore.Qt.LeftButton: + if not self.pressed and not self.scrolling and mouseEvent.modifiers() == Qt.NoModifier: + if mouseEvent.buttons() == Qt.LeftButton: self.pressed = True self.scrolling = False self.handIsClosed = False - QApplication.setOverrideCursor(QtCore.Qt.OpenHandCursor) + QApplication.setOverrideCursor(Qt.OpenHandCursor) self.position = mouseEvent.pos() - frame = self.page().mainFrame() - xTuple = frame.evaluateJavaScript("window.scrollX").toInt() - yTuple = frame.evaluateJavaScript("window.scrollY").toInt() - self.offset = QPoint(xTuple[0], yTuple[0]) + self.offset = self.widget.scrollPosition() return - return QWebView.mousePressEvent(self, mouseEvent) + return super(PanningWebView, self).mousePressEvent(mouseEvent) def mouseReleaseEvent(self, mouseEvent): @@ -52,7 +67,7 @@ def mouseReleaseEvent(self, mouseEvent): else: if self.ignored.count(mouseEvent): self.ignored.remove(mouseEvent) - return QWebView.mousePressEvent(self, mouseEvent) + return super(PanningWebView, self).mousePressEvent(mouseEvent) if self.scrolling: self.pressed = False @@ -67,14 +82,17 @@ def mouseReleaseEvent(self, mouseEvent): self.handIsClosed = False QApplication.restoreOverrideCursor() - event1 = QMouseEvent(QEvent.MouseButtonPress, self.position, QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier) + event1 = QMouseEvent(QEvent.MouseButtonPress, self.position, Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) event2 = QMouseEvent(mouseEvent) + self.ignored.append(event1) self.ignored.append(event2) + QApplication.postEvent(self, event1) QApplication.postEvent(self, event2) return - return QWebView.mouseReleaseEvent(self, mouseEvent) + + return super(PanningWebView, self).mouseReleaseEvent(mouseEvent) def mouseMoveEvent(self, mouseEvent): @@ -82,24 +100,46 @@ def mouseMoveEvent(self, mouseEvent): if self.scrolling: if not self.handIsClosed: QApplication.restoreOverrideCursor() - QApplication.setOverrideCursor(QtCore.Qt.ClosedHandCursor) + QApplication.setOverrideCursor(Qt.ClosedHandCursor) self.handIsClosed = True delta = mouseEvent.pos() - self.position p = self.offset - delta - frame = self.page().mainFrame() - frame.evaluateJavaScript(QString("window.scrollTo(%1, %2);").arg(p.x()).arg(p.y())); + if MainWindow.oldStyleWebKit: + self.widget.setScrollPosition(p) + else: + self.widget.runJavaScript('window.scrollTo({}, {});'.format(p.x(), p.y())) return if self.pressed: self.pressed = False self.scrolling = True return - return QWebView.mouseMoveEvent(self, mouseEvent) + + return super(PanningWebView, self).mouseMoveEvent(mouseEvent) def pointInScroller(self, position, orientation): - rect = self.page().mainFrame().scrollBarGeometry(orientation) - leftTop = self.mapToGlobal(QtCore.QPoint(rect.left(), rect.top())) - rightBottom = self.mapToGlobal(QtCore.QPoint(rect.right(), rect.bottom())) - globalRect = QtCore.QRect(leftTop.x(), leftTop.y(), rightBottom.x(), rightBottom.y()) + rect = self.widget.scrollBarGeometry(orientation) + topLeft = self.mapToGlobal(rect.topLeft()) + bottomRight = self.mapToGlobal(rect.bottomRight()) + globalRect = QRect(topLeft, bottomRight) return globalRect.contains(self.mapToGlobal(position)) + + +class VintelSvgPage(QWebPage if MainWindow.oldStyleWebKit else QWebEnginePage): + + def __init__(self, parent=None): + if MainWindow.oldStyleWebKit: + QWebPage.__init__(self, parent) + else: + QWebEnginePage.__init__(self, parent) + + + def acceptNavigationRequest(self, url, type, isMainFrame): + if MainWindow.oldStyleWebKit: + return super(PanningWebView, self).acceptNavigationRequest(url, type, isMainFrame) + else: + if type == QWebEnginePage.NavigationTypeLinkClicked: + self.view().mapLinkClicked.emit(url) + return False + return True diff --git a/src/vi/amazon_s3.py b/src/vi/amazon_s3.py index 560c84d..c881762 100644 --- a/src/vi/amazon_s3.py +++ b/src/vi/amazon_s3.py @@ -21,11 +21,10 @@ import requests import logging -from PyQt4 import Qt -from PyQt4.QtCore import QThread, SIGNAL +from PyQt5.QtCore import pyqtSignal, QThread +from distutils.version import StrictVersion from vi import version from vi.cache.cache import Cache -from distutils.version import LooseVersion, StrictVersion def getJumpbridgeData(region): @@ -62,6 +61,9 @@ def getNewestVersion(): class NotifyNewVersionThread(QThread): + + newVersion = pyqtSignal(str) + def __init__(self): QThread.__init__(self) self.alerted = False @@ -72,7 +74,7 @@ def run(self): # Is there a newer version available? newestVersion = getNewestVersion() if newestVersion and StrictVersion(newestVersion) > StrictVersion(version.VERSION): - self.emit(SIGNAL("newer_version"), newestVersion) + self.newVersion.emit(newestVersion) self.alerted = True except Exception as e: logging.error("Failed NotifyNewVersionThread: %s", e) diff --git a/src/vi/cache/cache.py b/src/vi/cache/cache.py index 68961c7..a34ba17 100644 --- a/src/vi/cache/cache.py +++ b/src/vi/cache/cache.py @@ -32,8 +32,6 @@ def to_blob(x): return x def from_blob(x): return x - -import logging from vi.cache.dbstructure import updateDatabase @@ -176,15 +174,3 @@ def removeAvatar(self, name): query = "DELETE FROM avatars WHERE charname = ?" self.con.execute(query, (name,)) self.con.commit() - - def recallAndApplySettings(self, responder, settingsIdentifier): - settings = self.getFromCache(settingsIdentifier) - if settings: - settings = eval(settings) - for setting in settings: - obj = responder if not setting[0] else getattr(responder, setting[0]) - # logging.debug("{0} | {1} | {2}".format(str(obj), setting[1], setting[2])) - try: - getattr(obj, setting[1])(setting[2]) - except Exception as e: - logging.error(e) diff --git a/src/vi/chatparser/chatparser.py b/src/vi/chatparser/chatparser.py index 611b5d8..ed2b95c 100644 --- a/src/vi/chatparser/chatparser.py +++ b/src/vi/chatparser/chatparser.py @@ -22,12 +22,13 @@ import time import logging import six + +from PyQt5.QtWidgets import QMessageBox if six.PY2: from io import open - from bs4 import BeautifulSoup from vi import states, evegate -from PyQt4.QtGui import QMessageBox + from .parser_functions import parseStatus from .parser_functions import parseUrls, parseShips, parseSystems diff --git a/src/vi/evegate.py b/src/vi/evegate.py index 7ceef99..2efd7b0 100644 --- a/src/vi/evegate.py +++ b/src/vi/evegate.py @@ -25,9 +25,9 @@ import logging from bs4 import BeautifulSoup -from vi.cache.cache import Cache from six.moves.urllib.error import HTTPError from six.moves.urllib.request import urlopen +from vi.cache.cache import Cache ERROR = -1 NOT_EXISTS = 0 diff --git a/src/vi/filewatcher.py b/src/vi/filewatcher.py index 75032a1..849b1e1 100644 --- a/src/vi/filewatcher.py +++ b/src/vi/filewatcher.py @@ -20,10 +20,9 @@ import os import stat import time -import logging -from PyQt4 import QtCore -from PyQt4.QtCore import SIGNAL +from PyQt5 import QtCore +from PyQt5.QtCore import pyqtSignal """ There is a problem with the QFIleWatcher on Windows and the log @@ -40,6 +39,9 @@ DEFAULT_MAX_AGE = 60 * 60 * 24 class FileWatcher(QtCore.QThread): + + fileChanged = pyqtSignal(str) + def __init__(self, path, maxAge=DEFAULT_MAX_AGE): QtCore.QThread.__init__(self) self.path = path @@ -69,7 +71,7 @@ def run(self): if not stat.S_ISREG(pathStat.st_mode): continue if modified < pathStat.st_size: - self.emit(SIGNAL("file_change"), path) + self.fileChanged.emit(path) self.files[path] = pathStat.st_size diff --git a/src/vi/koschecker.py b/src/vi/koschecker.py index 52ef797..b34e7a3 100644 --- a/src/vi/koschecker.py +++ b/src/vi/koschecker.py @@ -20,8 +20,8 @@ import logging import requests -from vi import evegate from requests.exceptions import RequestException +from vi import evegate UNKNOWN = "No Result" NOT_KOS = 'Not Kos' @@ -36,12 +36,8 @@ def check(parts): namesAsIds = {} names = [name.strip() for name in parts] - kos_url = Cache().getConfigValue("kos_url") - if not kos_url: - kos_url = CVA_KOS_URL - try: - kosData = requests.get(kos_url, params = {'c': 'json', 'type': 'multi', 'q': ','.join(names)}).json() + kosData = requests.get(CVA_KOS_URL, params = {'c': 'json', 'type': 'multi', 'q': ','.join(names)}).json() except RequestException as e: kosData = None logging.error("Error on pilot KOS check request %s", str(e)) @@ -99,7 +95,7 @@ def check(parts): for corp in corpsToCheck: try: - kosData = requests.get(kos_url, params = { 'c': 'json', 'type': 'unit', 'q': corp }).json() + kosData = requests.get(CVA_KOS_URL, params = { 'c': 'json', 'type': 'unit', 'q': corp }).json() except RequestException as e: logging.error("Error on corp KOS check request: %s", str(e)) diff --git a/src/vi/soundmanager.py b/src/vi/soundmanager.py index ff8bbfd..3b35a50 100644 --- a/src/vi/soundmanager.py +++ b/src/vi/soundmanager.py @@ -18,31 +18,23 @@ ########################################################################### import os -import subprocess import sys -import re -import requests -import time import six - -from collections import namedtuple -from PyQt4.QtCore import QThread -from .resources import resourcePath -from six.moves import queue - import logging + +from PyQt5.QtCore import QThread, QUrl +from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent +from six.moves.queue import Queue +from vi.resources import resourcePath from vi.singleton import Singleton -global gPygletAvailable +global festivalAvailable try: - import pyglet - from pyglet import media - - gPygletAvailable = True -except ImportError: - gPygletAvailable = False - + import festival + festivalAvailable = True +except: + festivalAvailable = False class SoundManager(six.with_metaclass(Singleton)): SOUNDS = {"alarm": "178032__zimbot__redalert-klaxon-sttos-recreated.wav", @@ -65,10 +57,10 @@ def __init__(self): self._soundThread.start() def platformSupportsAudio(self): - return self.platformSupportsSpeech() or gPygletAvailable + return True def platformSupportsSpeech(self): - if self._soundThread.isDarwin: + if self._soundThread.isDarwin or festivalAvailable: return True return False @@ -92,6 +84,9 @@ def playSound(self, name="alarm", message="", abbreviatedMessage=""): audioFile = resourcePath("vi/ui/res/{0}".format(self.SOUNDS[name])) self._soundThread.queue.put((audioFile, message, abbreviatedMessage)) + def say(self, message='This is a test!'): + self._soundThread.speak(message) + def quit(self): if self.soundAvailable: self._soundThread.quit() @@ -102,21 +97,14 @@ def quit(self): class SoundThread(QThread): queue = None - useGoogleTTS = False - useVoiceRss = False - VOICE_RSS_API_KEY = '896a7f61ec5e478cba856a78babab79c' - GOOGLE_TTS_API_KEY = '' isDarwin = sys.platform.startswith("darwin") volume = 25 def __init__(self): QThread.__init__(self) - self.queue = queue.Queue() - if gPygletAvailable: - self.player = media.Player() - else: - self.player = None + self.queue = Queue() + self.player = QMediaPlayer() self.active = True @@ -133,7 +121,8 @@ def run(self): if abbreviatedMessage != "": message = abbreviatedMessage if not self.speak(message): - self.playAudioFile(audioFile, False) + if audioFile: + self.playAudioFile(audioFile, False) logging.error("SoundThread: sorry, speech not yet implemented on this platform") elif audioFile is not None: self.playAudioFile(audioFile, False) @@ -148,141 +137,31 @@ def quit(self): def speak(self, message): - if self.useGoogleTTS: - self.audioExtractToMp3(inputText=message) # experimental - elif self.useVoiceRss: - self.playTTS(message) # experimental - elif self.isDarwin: + if self.isDarwin: self.darwinSpeak(message) + elif festivalAvailable: + festival.sayText(message) else: return False return True - def handleIdleTasks(self): - self.speakRandomChuckNorrisJoke() - - - # Audio subsytem access + # + # Audio subsytem access + # def playAudioFile(self, filename, stream=False): try: - volume = float(self.volume) / 100.0 - if self.player: - src = media.load(filename, streaming=stream) - self.player.queue(src) - self.player.volume = volume - self.player.play() - elif self.isDarwin: - subprocess.call(["afplay -v {0} {1}".format(volume, filename)], shell=True) + content = QMediaContent(QUrl.fromLocalFile(filename)) + self.player.setMedia(content) + self.player.setVolume(float(self.volume)) + self.player.play() except Exception as e: logging.error("SoundThread.playAudioFile exception: %s", e) + def darwinSpeak(self, message): try: os.system("say [[volm {0}]] '{1}'".format(float(self.volume) / 100.0, message)) except Exception as e: logging.error("SoundThread.darwinSpeak exception: %s", e) - - # - # Experimental text-to-speech stuff below - # - - # VoiceRss - - def playTTS(self, inputText=''): - try: - mp3url = 'http://api.voicerss.org/?c=WAV&key={self.VOICE_RSS_API_KEY}&src={inputText}&hl=en-us'.format( - **locals()) - self.playAudioFile(requests.get(mp3url, stream=True).raw) - time.sleep(.5) - except requests.exceptions.RequestException as e: - logging.error('playTTS error: %s', str(e)) - - # google_tts - - def audioExtractToMp3(self, inputText='', args=None): - # This accepts : - # a dict, - # an audio_args named tuple - # or arg parse object - audioArgs = namedtuple('audio_args', ['language', 'output']) - if args is None: - args = audioArgs(language='en', output=open('output.mp3', 'w')) - if type(args) is dict: - args = audioArgs(language=args.get('language', 'en'), output=open(args.get('output', 'output.mp3'), 'w')) - # Process inputText into chunks - # Google TTS only accepts up to (and including) 100 characters long texts. - # Split the text in segments of maximum 100 characters long. - combinedText = self.splitText(inputText) - - # Download chunks and write them to the output file - for idx, val in enumerate(combinedText): - mp3url = "http://translate.google.com/translate_tts?tl=%s&q=%s&total=%s&idx=%s&ie=UTF-8&client=t&key=%s" % ( - args.language, requests.utils.quote(val), len(combinedText), idx, self.GOOGLE_TTS_API_KEY) - headers = {"Host": "translate.google.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1)"} - sys.stdout.write('.') - sys.stdout.flush() - if len(val) > 0: - try: - args.timeout.write(requests.get(mp3url, headers=headers).content) - time.sleep(.5) - except requests.exceptions.RequestException as e: - logging.error('audioExtractToMp3 error: %s', e) - args.output.close() - return args.output.name - - def splitText(self, inputText, maxLength=100): - """ - Try to split between sentences to avoid interruptions mid-sentence. - Failing that, split between words. - See splitText_rec - """ - - def splitTextRecursive(inputText, regexps, maxLength=maxLength): - """ - Split a string into substrings which are at most maxLength. - Tries to make each substring as big as possible without exceeding - maxLength. - Will use the first regexp in regexps to split the input into - substrings. - If it it impossible to make all the segments less or equal than - maxLength with a regexp then the next regexp in regexps will be used - to split those into subsegments. - If there are still substrings who are too big after all regexps have - been used then the substrings, those will be split at maxLength. - - Args: - inputText: The text to split. - regexps: A list of regexps. - If you want the separator to be included in the substrings you - can add parenthesis around the regular expression to create a - group. Eg.: '[ab]' -> '([ab])' - - Returns: - a list of strings of maximum maxLength length. - """ - if (len(inputText) <= maxLength): - return [inputText] - - # Mistakenly passed a string instead of a list - if isinstance(regexps, basestring): - regexps = [regexps] - regexp = regexps.pop(0) if regexps else '(.{%d})' % maxLength - - textList = re.split(regexp, inputText) - combinedText = [] - # First segment could be >max_length - combinedText.extend(splitTextRecursive(textList.pop(0), regexps, maxLength)) - for val in textList: - current = combinedText.pop() - concat = current + val - if (len(concat) <= maxLength): - combinedText.append(concat) - else: - combinedText.append(current) - # val could be > maxLength - combinedText.extend(splitTextRecursive(val, regexps, maxLength)) - return combinedText - - return splitTextRecursive(inputText.replace('\n', ''), ['([\,|\.|;]+)', '( )']) diff --git a/src/vi/threads.py b/src/vi/threads.py index 596814d..e0873ae 100755 --- a/src/vi/threads.py +++ b/src/vi/threads.py @@ -21,8 +21,9 @@ import logging import six -from six.moves import queue -from PyQt4.QtCore import QThread, SIGNAL, QTimer +#from six.moves import queue +from six.moves.queue import Queue +from PyQt5.QtCore import pyqtSignal, QThread, QTimer from vi import evegate from vi import koschecker from vi.cache.cache import Cache @@ -32,15 +33,17 @@ class AvatarFindThread(QThread): + avatarUpdate = pyqtSignal(object, object) + def __init__(self): QThread.__init__(self) - self.queue = queue.Queue() + self.queue = Queue() self.active = True - def addChatEntry(self, chatEntry, clearAvatarCacheForUser=False): + def addChatEntry(self, chatEntry, clearCache=False): try: - if clearAvatarCacheForUser: + if clearCache: cache = Cache() cache.removeAvatar(chatEntry.message.user) @@ -78,10 +81,9 @@ def run(self): lastCall = time.time() if avatar: cache.putAvatar(charname, avatar) - logging.debug("AvatarFindThread storing avatar for %s" % charname) if avatar: logging.debug("AvatarFindThread emit avatar_update for %s" % charname) - self.emit(SIGNAL("avatar_update"), chatEntry, avatar) + self.avatarUpdate.emit(chatEntry, avatar) except Exception as e: logging.error("Error in AvatarFindThread : %s", e) @@ -94,9 +96,11 @@ def quit(self): class KOSCheckerThread(QThread): + showKos = pyqtSignal(str, str, str, bool) + def __init__(self): QThread.__init__(self) - self.queue = queue.Queue() + self.queue = Queue() self.recentRequestNamesAndTimes = {} self.active = True @@ -142,7 +146,7 @@ def run(self): logging.info("KOSCheckerThread emitting kos_result for: state = {0}, text = {1}, requestType = {2}, hasKos = {3}".format( "ok", text, requestType, hasKos)) - self.emit(SIGNAL("kos_result"), "ok", text, requestType, hasKos) + self.showKos.emit("ok", text, requestType, hasKos) def quit(self): self.active = False @@ -152,9 +156,11 @@ def quit(self): class MapStatisticsThread(QThread): + updateMap = pyqtSignal(dict) + def __init__(self): QThread.__init__(self) - self.queue = queue.Queue(maxsize=1) + self.queue = Queue(maxsize=1) self.lastStatisticsUpdate = time.time() self.pollRate = STATISTICS_UPDATE_INTERVAL_MSECS self.refreshTimer = None @@ -167,7 +173,7 @@ def requestStatistics(self): def run(self): self.refreshTimer = QTimer() - self.connect(self.refreshTimer, SIGNAL("timeout()"), self.requestStatistics) + self.refreshTimer.timeout.connect(self.requestStatistics) while True: # Block waiting for requestStatistics() to enqueue a token self.queue.get() @@ -184,7 +190,7 @@ def run(self): requestData = {"result": "error", "text": six.text_type(e)} self.lastStatisticsUpdate = time.time() self.refreshTimer.start(self.pollRate) - self.emit(SIGNAL("statistic_data_update"), requestData) + self.updateMap.emit(requestData) logging.debug("MapStatisticsThread emitted statistic_data_update") diff --git a/src/vi/ui/SoundSetup.ui b/src/vi/ui/SoundSetup.ui index eb9add3..6dd58bb 100644 --- a/src/vi/ui/SoundSetup.ui +++ b/src/vi/ui/SoundSetup.ui @@ -47,6 +47,19 @@ Close + + + + 10 + 70 + 103 + 26 + + + + Test Voice + + diff --git a/src/vi/ui/res/mapdata/Providencecatch.svg b/src/vi/ui/res/mapdata/Providencecatch.svg index 933799b..537f29b 100644 --- a/src/vi/ui/res/mapdata/Providencecatch.svg +++ b/src/vi/ui/res/mapdata/Providencecatch.svg @@ -134,7 +134,7 @@ y="757.5" class="lc" id="text17" - style="font-size:11px;font-family:Arial, Helvetica, sans-serif;text-anchor:middle;fill:#000000">© by Wollari & CCP + style="font-size:11px;font-family:Arial, Helvetica, sans-serif;text-anchor:middle;fill:#000000"> by Wollari & CCP ") - message = chatparser.chatparser.Message("Vintel KOS-Check", text, evegate.currentEveTime(), "VINTEL", - [], states.NOT_CHANGE, text.upper(), text) + message = Message("Vintel KOS-Check", text, evegate.currentEveTime(), "VINTEL", [], states.NOT_CHANGE, text.upper(), text) self.addMessageToIntelChat(message) elif state == "error": self.trayIcon.showMessage("KOS Failure", text, 3) @@ -783,26 +890,27 @@ def changedRoomnames(self, newRoomnames): def showInfo(self): - infoDialog = QtGui.QDialog(self) + infoDialog = QDialog(self) uic.loadUi(resourcePath("vi/ui/Info.ui"), infoDialog) infoDialog.versionLabel.setText(u"Version: {0}".format(vi.version.VERSION)) infoDialog.logoLabel.setPixmap(QtGui.QPixmap(resourcePath("vi/ui/res/logo.png"))) - infoDialog.connect(infoDialog.closeButton, SIGNAL("clicked()"), infoDialog.accept) + infoDialog.closeButton.clicked.connect(infoDialog.accept) infoDialog.show() def showSoundSetup(self): - dialog = QtGui.QDialog(self) + dialog = QDialog(self) uic.loadUi(resourcePath("vi/ui/SoundSetup.ui"), dialog) dialog.volumeSlider.setValue(SoundManager().soundVolume) - dialog.connect(dialog.volumeSlider, SIGNAL("valueChanged(int)"), SoundManager().setSoundVolume) - dialog.connect(dialog.testSoundButton, SIGNAL("clicked()"), SoundManager().playSound) - dialog.connect(dialog.closeButton, SIGNAL("clicked()"), dialog.accept) + dialog.volumeSlider.valueChanged.connect(SoundManager().setSoundVolume) + dialog.testSoundButton.clicked.connect(lambda: SoundManager().playSound()) + dialog.testVoiceButton.clicked.connect(lambda: SoundManager().say('Test... 1, 2, 3.')) + dialog.closeButton.clicked.connect(dialog.accept) dialog.show() def systemTrayActivated(self, reason): - if reason == QtGui.QSystemTrayIcon.Trigger: + if reason == QSystemTrayIcon.Trigger: if self.isMinimized(): self.showNormal() self.activateWindow() @@ -817,7 +925,7 @@ def updateAvatarOnChatEntry(self, chatEntry, avatarData): if not updated: self.avatarFindThread.addChatEntry(chatEntry) # , clearCache=True) else: - self.emit(SIGNAL("avatar_loaded"), chatEntry.message.user, avatarData) + self.avatarLoaded.emit(chatEntry.message.user, avatarData) def updateStatisticsOnMap(self, data): @@ -873,6 +981,7 @@ def processLogMessages(self, messages): self.addMessageToIntelChat(message) # For each system that was mentioned in the message, check for alarm distance to the current system # and alarm if within alarm distance. + systemList = self.dotlan.systems if message.systems: for systemname in message.systems: if not systemname in self.systems: @@ -892,12 +1001,15 @@ def processLogMessages(self, messages): # call once after all messages are processed self.updateMapView() -class RegionChooser(QtGui.QDialog): +class RegionChooser(QDialog): + + newRegionChosen = pyqtSignal() + def __init__(self, parent): - QtGui.QDialog.__init__(self, parent) + QDialog.__init__(self, parent) uic.loadUi(resourcePath("vi/ui/RegionChooser.ui"), self) - self.connect(self.cancelButton, SIGNAL("clicked()"), self.accept) - self.connect(self.saveButton, SIGNAL("clicked()"), self.saveClicked) + self.cancelButton.clicked.connect(self.accept) + self.saveButton.clicked.connect(self.saveClicked) cache = Cache() regionName = cache.getConfigValue("region_name") if not regionName: @@ -933,14 +1045,16 @@ def saveClicked(self): if correct: Cache().saveConfigValue("region_name", text) self.accept() - self.emit(SIGNAL("new_region_chosen")) + self.newRegionChosen.emit() + +class SystemChat(QDialog): -class SystemChat(QtGui.QDialog): + setLocationSignal = pyqtSignal(str, str) SYSTEM = 0 def __init__(self, parent, chatType, selector, chatEntries, knownPlayerNames): - QtGui.QDialog.__init__(self, parent) + QDialog.__init__(self, parent) uic.loadUi(resourcePath("vi/ui/SystemChat.ui"), self) self.parent = parent self.chatType = 0 @@ -958,10 +1072,10 @@ def __init__(self, parent, chatType, selector, chatEntries, knownPlayerNames): for name in knownPlayerNames: self.playerNamesBox.addItem(name) self.setWindowTitle("Chat for {0}".format(titleName)) - self.connect(self.closeButton, SIGNAL("clicked()"), self.closeDialog) - self.connect(self.alarmButton, SIGNAL("clicked()"), self.setSystemAlarm) - self.connect(self.clearButton, SIGNAL("clicked()"), self.setSystemClear) - self.connect(self.locationButton, SIGNAL("clicked()"), self.locationSet) + self.closeButton.clicked.connect(self.closeDialog) + self.alarmButton.clicked.connect(self.setSystemAlarm) + self.clearButton.clicked.connect(self.setSystemClear) + self.locationButton.clicked.connect(self.locationSet) def _addMessageToChat(self, message, avatarPixmap): @@ -970,12 +1084,12 @@ def _addMessageToChat(self, message, avatarPixmap): scrollToBottom = True entry = ChatEntryWidget(message) entry.avatarLabel.setPixmap(avatarPixmap) - listWidgetItem = QtGui.QListWidgetItem(self.chat) + listWidgetItem = QtWidgets.QListWidgetItem(self.chat) listWidgetItem.setSizeHint(entry.sizeHint()) self.chat.addItem(listWidgetItem) self.chat.setItemWidget(listWidgetItem, entry) self.chatEntries.append(entry) - self.connect(entry, SIGNAL("mark_system"), self.parent.markSystemOnMap) + entry.markSystem.connect(self.parent.markSystemOnMap) if scrollToBottom: self.chat.scrollToBottom() @@ -992,7 +1106,7 @@ def addChatEntry(self, entry): def locationSet(self): char = six.text_type(self.playerNamesBox.currentText()) - self.emit(SIGNAL("location_set"), char, self.system.name) + self.setLocationSignal.emit(char, self.system.name) def newAvatarAvailable(self, charname, avatarData): @@ -1015,20 +1129,22 @@ def closeDialog(self): self.accept() -class ChatEntryWidget(QtGui.QWidget): +class ChatEntryWidget(QWidget): + + markSystem = pyqtSignal(object) TEXT_SIZE = 11 SHOW_AVATAR = True questionMarkPixmap = None def __init__(self, message): - QtGui.QWidget.__init__(self) + QWidget.__init__(self) if not self.questionMarkPixmap: self.questionMarkPixmap = QtGui.QPixmap(resourcePath("vi/ui/res/qmark.png")).scaledToHeight(32) uic.loadUi(resourcePath("vi/ui/ChatEntry.ui"), self) self.avatarLabel.setPixmap(self.questionMarkPixmap) self.message = message self.updateText() - self.connect(self.textLabel, SIGNAL("linkActivated(QString)"), self.linkClicked) + self.textLabel.linkActivated.connect(self.linkClicked) if sys.platform.startswith("win32") or sys.platform.startswith("cygwin"): ChatEntryWidget.TEXT_SIZE = 8 self.changeFontSize(self.TEXT_SIZE) @@ -1040,7 +1156,7 @@ def linkClicked(self, link): link = six.text_type(link) function, parameter = link.split("/", 1) if function == "mark_system": - self.emit(SIGNAL("mark_system"), parameter) + self.markSystem.emit(parameter) elif function == "link": webbrowser.open(parameter) @@ -1073,7 +1189,7 @@ def changeFontSize(self, newSize): self.textLabel.setFont(font) -class Settings(QtGui.QDialog): +class Settings(QDialog): def __init__(self, parent): QtGui.QDialog.__init__(self, parent) uic.loadUi(resourcePath("vi/ui/SettingsTabs.ui"), self) @@ -1082,22 +1198,23 @@ def __init__(self, parent): self.tabs.setCurrentIndex(0) # load displaying first tab, regardless of which page was last open in designer # Chatrooms - self.connect(self.chatDefaultButton, SIGNAL("clicked()"), self.setChatToDefaults) - self.connect(self.chatCancelButton, SIGNAL("clicked()"), self.resetChatSettings) - self.connect(self.chatSaveButton, SIGNAL("clicked()"), self.saveChatSettings) + self.chatDefaultButton.clicked.connect(self.setChatToDefaults) + self.chatCancelButton.clicked.connect(self.resetChatSettings) + self.chatSaveButton.clicked.connect(self.saveChatSettings) self.resetChatSettings() # JBS - self.connect(self.jbSaveButton, SIGNAL("clicked()"), self.saveJbs) - self.connect(self.jbCancelButton, SIGNAL("clicked()"), self.resetJbs) + self.jbSaveButton.clicked.connect(self.saveJbs) + self.jbCancelButton.clicked.connect(self.resetJbs) self.resetJbs() + # loading format explanation from textfile # with open(resourcePath("docs/jumpbridgeformat.txt")) as f: # self.formatInfoField.setPlainText(f.read()) # Quick Setup - self.connect(self.quickSettingsSaveButton, SIGNAL("clicked()"), self.saveQuickSettings) - self.connect(self.quickSettingsCancelButton, SIGNAL("clicked()"), self.resetQuickSettings) + self.quickSettingsSaveButton.clicked.connect(self.saveQuickSettings) + self.quickSettingsCancelButton.clicked.connect(self.resetQuickSettings) self.resetQuickSettings() def resetJbs(self): @@ -1110,7 +1227,7 @@ def saveJbs(self): if url != "": requests.get(url).text self.cache.saveConfigValue("dotlan_jb_id", six.text_type(self.jbIdField.text())) - self.emit(SIGNAL("set_jumpbridge_url"), url) + self.setJumpbridgeUrl.emit(url) except Exception as e: QMessageBox.critical(None, "Finding Jumpbridgedata failed", "Error: {0}".format(six.text_type(e)), "OK") @@ -1124,7 +1241,7 @@ def resetChatSettings(self): def saveChatSettings(self): text = six.text_type(self.roomnamesField.toPlainText()) rooms = [six.text_type(name.strip()) for name in text.split(",")] - self.emit(SIGNAL("rooms_changed"), rooms) + self.roomsChanged.emit(rooms) def setChatToDefaults(self): roomnames = self.cache.getConfigValue("default_room_names") @@ -1145,13 +1262,13 @@ def saveQuickSettings(self): if 'channels' in d: self.cache.saveConfigValue('default_room_names', ",".join(d['channels'])) - self.emit(SIGNAL("rooms_changed"), d['channels']) + self.roomsChanged.emit(d['channels']) if 'dotlan_jb_id' in d: self.cache.saveConfigValue("dotlan_jb_id", d['dotlan_jb_id']) if 'jumpbridge_url' in d: - self.emit(SIGNAL("set_jumpbridge_url"), d['jumpbridge_url']) + self.setJumpbridgeUrl.emit(d['jumpbridge_url']) if 'kos_url' in d: self.cache.saveConfigValue("kos_url", d['kos_url']) diff --git a/src/vi/version.py b/src/vi/version.py index 7859a3d..94c70c1 100644 --- a/src/vi/version.py +++ b/src/vi/version.py @@ -1,3 +1,3 @@ -VERSION = "1.2.4" -SNAPSHOT = False # set to false when releasing +VERSION = "2.0.0" +SNAPSHOT = True # set to false when releasing diff --git a/src/vintel.py b/src/vintel.py index 5b8572a..514861a 100644 --- a/src/vintel.py +++ b/src/vintel.py @@ -26,13 +26,14 @@ from logging.handlers import RotatingFileHandler from logging import StreamHandler -from PyQt4 import QtGui +from PyQt5 import QtGui, QtWidgets +from PyQt5.QtCore import QT_VERSION_STR +from PyQt5.QtWidgets import QApplication, QMessageBox from vi import version from vi.ui import viui, systemtray from vi.cache import cache from vi.resources import resourcePath from vi.cache.cache import Cache -from PyQt4.QtGui import QApplication, QMessageBox def exceptHook(exceptionType, exceptionValue, tracebackObject): @@ -76,7 +77,10 @@ def __init__(self, args): "p_drive", "User", "My Documents", "EVE", "logs", "Chatlogs") elif sys.platform.startswith("linux"): chatLogDirectory = os.path.join(os.path.expanduser("~"), "EVE", "logs", "Chatlogs") - elif sys.platform.startswith("win32") or sys.platform.startswith("cygwin"): + if not os.path.exists(chatLogDirectory): + # Default path created by EveLauncher: https://forums.eveonline.com/default.aspx?g=posts&t=482663 + chatLogDirectory = os.path.join(os.path.expanduser("~"), "Documents","EVE", "logs", "Chatlogs") + elif sys.platform.startswith("win32"): import ctypes.wintypes from win32com.shell import shellcon buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) @@ -99,7 +103,7 @@ def __init__(self, args): if not os.path.exists(vintelLogDirectory): os.mkdir(vintelLogDirectory) - splash = QtGui.QSplashScreen(QtGui.QPixmap(resourcePath("vi/ui/res/logo.png"))) + splash = QtWidgets.QSplashScreen(QtGui.QPixmap(resourcePath("vi/ui/res/logo.png"))) vintelCache = Cache() logLevel = vintelCache.getConfigValue("logging_level") @@ -129,15 +133,19 @@ def __init__(self, args): logging.critical("") logging.critical("------------------- Vintel %s starting up -------------------", version.VERSION) logging.critical("") + logging.critical("QT version %s", QT_VERSION_STR) logging.debug("Looking for chat logs at: %s", chatLogDirectory) logging.debug("Cache maintained here: %s", cache.Cache.PATH_TO_CACHE) logging.debug("Writing logs to: %s", vintelLogDirectory) + self.setOrganizationName("Vintel Development Team") + self.setOrganizationDomain("https://github.com/Xanthos-Eve/vintel") + self.setApplicationName("Vintel") + trayIcon = systemtray.TrayIcon(self) trayIcon.show() self.mainWindow = viui.MainWindow(chatLogDirectory, trayIcon, backGroundColor) self.mainWindow.show() - self.mainWindow.raise_() splash.finish(self.mainWindow) From ffddc708db9bb7b13188b2bccca329743948b4d4 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 23:18:31 -0500 Subject: [PATCH 28/36] more merging, still unstable with Anaconda 4.3.4 and PyQT5.6.0. --- src/vi/ui/viui.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/vi/ui/viui.py b/src/vi/ui/viui.py index 8232b9a..11b1a79 100644 --- a/src/vi/ui/viui.py +++ b/src/vi/ui/viui.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import pyqtSignal, QSettings, QPoint, QByteArray from PyQt5.QtWidgets import QMessageBox, QAction, QActionGroup, QStyleOption, QStyle, QSystemTrayIcon, QDialog, QWidget from PyQt5.QtGui import QImage, QPixmap, QPainter -from vi import amazon_s3, evegate, dotlan, filewatcher, states, version +from vi import amazon_s3, evegate, dotlan, filewatcher, states, systems, version from vi.cache.cache import Cache from vi.resources import resourcePath from vi.soundmanager import SoundManager @@ -42,7 +42,7 @@ from vi.regions import REGIONS from vi.chatparser.chatparser import ChatParser, Message -OLD_STYLE_WEBKIT = "OLD_STYLE_WEBKIT" in os.environ +OLD_STYLE_WEBKIT = True # "OLD_STYLE_WEBKIT" in os.environ #TODO - SET SOMETHING? if OLD_STYLE_WEBKIT: from PyQt5.QtWebKitWidgets import QWebPage @@ -57,8 +57,6 @@ class MainWindow(QtWidgets.QMainWindow): chatMessageAdded = pyqtSignal(object) avatarLoaded = pyqtSignal(str, object) - setJumpbridgeUrl = pyqtSignal(str) - roomsChanged = pyqtSignal(object) oldStyleWebKit = OLD_STYLE_WEBKIT def __init__(self, pathToLogs, trayIcon, backGroundColor): @@ -209,13 +207,14 @@ def updateRegionMenu(self): else: region = label menuItem = self.menuRegion.addAction(label) - receiver = lambda region=region: self.onRegionSelect(region) - self.menuItem.triggered.connect(receiver) + #receiver = lambda region=region: self.onRegionSelect(region) + logging.critical('Assigning lambda ' + region) + menuItem.triggered.connect(lambda region=region: self.onRegionSelect(region)) self.menuRegion.insertAction(orm, menuItem) def onRegionSelect(self, region): - logging.info("NEW REGION: [%s]", region) + logging.critical("NEW REGION: [%s]", region) Cache().saveConfigValue("region_name", region) self.handleRegionChosen() @@ -809,7 +808,7 @@ def handleRegionChosen(self): def showRegionChooser(self): chooser = RegionChooser(self) - chooser.newRegionChosen.connect(handleRegionChosen) + chooser.newRegionChosen.connect(self.handleRegionChosen) chooser.show() def replayLogs(self): @@ -1190,8 +1189,12 @@ def changeFontSize(self, newSize): class Settings(QDialog): + + setJumpbridgeUrl = pyqtSignal(str) + roomsChanged = pyqtSignal(object) + def __init__(self, parent): - QtGui.QDialog.__init__(self, parent) + QDialog.__init__(self, parent) uic.loadUi(resourcePath("vi/ui/SettingsTabs.ui"), self) self.cache = Cache() self.parent = parent From 1eb151e2ed27fbf72a637e5cb51a740902f592a8 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 15 Jan 2017 23:40:29 -0500 Subject: [PATCH 29/36] Restore some stuff trashed in the merge. --- src/vi/ui/MainWindow.ui | 6 +++--- src/vi/ui/viui.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vi/ui/MainWindow.ui b/src/vi/ui/MainWindow.ui index d7296e8..39f1da9 100644 --- a/src/vi/ui/MainWindow.ui +++ b/src/vi/ui/MainWindow.ui @@ -569,13 +569,13 @@ - QWebView + QWebEngineView QWidget -
QtWebKit/QWebView
+
QtWebEngineWidgets/QWebEngineView
PanningWebView - QWebView + QWebEngineView
vi/PanningWebView
diff --git a/src/vi/ui/viui.py b/src/vi/ui/viui.py index 11b1a79..005300e 100644 --- a/src/vi/ui/viui.py +++ b/src/vi/ui/viui.py @@ -42,7 +42,7 @@ from vi.regions import REGIONS from vi.chatparser.chatparser import ChatParser, Message -OLD_STYLE_WEBKIT = True # "OLD_STYLE_WEBKIT" in os.environ #TODO - SET SOMETHING? +OLD_STYLE_WEBKIT = "OLD_STYLE_WEBKIT" in os.environ if OLD_STYLE_WEBKIT: from PyQt5.QtWebKitWidgets import QWebPage From bd13d9fe5422d03a5035433c687c513944c633b6 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Mon, 16 Jan 2017 00:38:07 -0500 Subject: [PATCH 30/36] Fix region menus, can't use lambdas??? --- src/vi/ui/viui.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/vi/ui/viui.py b/src/vi/ui/viui.py index 005300e..0b5e8a8 100644 --- a/src/vi/ui/viui.py +++ b/src/vi/ui/viui.py @@ -18,6 +18,7 @@ ########################################################################### import datetime +import functools import os import sys import time @@ -30,7 +31,7 @@ import vi from PyQt5 import QtWidgets, QtGui, uic, QtCore -from PyQt5.QtCore import pyqtSignal, QSettings, QPoint, QByteArray +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSettings, QPoint, QByteArray from PyQt5.QtWidgets import QMessageBox, QAction, QActionGroup, QStyleOption, QStyle, QSystemTrayIcon, QDialog, QWidget from PyQt5.QtGui import QImage, QPixmap, QPainter from vi import amazon_s3, evegate, dotlan, filewatcher, states, systems, version @@ -158,7 +159,7 @@ def paintEvent(self, event): def wheelEvent(self,event): if event.modifiers() & QtCore.Qt.ControlModifier: - steps = event.delta() // 120 + steps = event.angleDelta().y() // 120 vector = steps and steps // abs(steps) # 0, 1, or -1 for step in range(1, abs(steps) + 1): self.mapView.setZoomFactor(self.mapView.zoomFactor() + vector * 0.1) @@ -181,8 +182,7 @@ def keyPressEvent(self, event): def updateOtherRegionMenu(self): for region in REGIONS: menuItem = self.otherRegionSubmenu.addAction(region) - receiver = lambda region=region: self.onRegionSelect(region) - menuItem.triggered.connect(receiver) + menuItem.triggered.connect(functools.partial(self.onRegionSelect, region)) self.otherRegionSubmenu.addAction(menuItem) @@ -207,12 +207,11 @@ def updateRegionMenu(self): else: region = label menuItem = self.menuRegion.addAction(label) - #receiver = lambda region=region: self.onRegionSelect(region) - logging.critical('Assigning lambda ' + region) - menuItem.triggered.connect(lambda region=region: self.onRegionSelect(region)) + menuItem.triggered.connect(functools.partial(self.onRegionSelect, region)) self.menuRegion.insertAction(orm, menuItem) + @pyqtSlot(str) def onRegionSelect(self, region): logging.critical("NEW REGION: [%s]", region) Cache().saveConfigValue("region_name", region) From 90e353cbabb0e410a5f4c2bb0a80a87c3172b6ef Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Wed, 18 Jan 2017 00:56:04 -0500 Subject: [PATCH 31/36] Fix infinite loop. Remove redundant system check. --- src/vi/chatparser/parser_functions.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/vi/chatparser/parser_functions.py b/src/vi/chatparser/parser_functions.py index ad4f030..49de95e 100644 --- a/src/vi/chatparser/parser_functions.py +++ b/src/vi/chatparser/parser_functions.py @@ -45,7 +45,9 @@ from vi import states from vi.systems import SYSTEMS -CHARS_TO_IGNORE_REGEX = '[*?,!.()]' +# Do not ignore <>/" which keep html from word matching on replacement +# Do not ignore ? which triggers status change to request +CHARS_TO_IGNORE_REGEX = '[*,!.()]' def textReplace(element, newText): @@ -61,15 +63,15 @@ def textReplace(element, newText): def parseStatus(rtext): texts = [t for t in rtext.contents if isinstance(t, NavigableString)] for text in texts: - # upperText = re.sub(CHARS_TO_IGNORE_REGEX, '', text.strip().upper()) # KEEP QUESTION MARK? + upperText = re.sub(CHARS_TO_IGNORE_REGEX, ' ', text.strip().upper()) # KEEP QUESTION MARK? upperText = text.strip().upper() upperWords = upperText.split() - if (("CLEAR" in upperWords or "CLR" in upperWords) and not upperText.endswith("?")): + if ("?" in upperText): + return states.REQUEST + elif ("CLEAR" in upperWords or "CLR" in upperWords): return states.CLEAR elif ("STAT" in upperWords or "STATUS" in upperWords): return states.REQUEST - elif ("?" in upperText): - return states.REQUEST elif (text.strip().upper() in ("BLUE", "BLUES ONLY", "ONLY BLUE" "STILL BLUE", "ALL BLUES")): return states.CLEAR @@ -82,7 +84,7 @@ def formatShipName(text, word): texts = [t for t in rtext.contents if isinstance(t, NavigableString)] for text in texts: - # upperText = re.sub(CHARS_TO_IGNORE_REGEX, ' ', text.strip().upper()) + upperText = re.sub(CHARS_TO_IGNORE_REGEX, ' ', text.upper()) upperText = text.upper() for shipName in evegate.SHIPNAMES: if shipName in upperText: @@ -90,9 +92,11 @@ def formatShipName(text, word): start = upperText.find(shipName) end = start + len(shipName) if ( (start > 0 and re.match('[A-Z0-9]', upperText[start - 1])) - or (end < len(upperText) - 1 and re.match('[A-RT-Z0-9]', upperText[end])) ): + or (end < len(upperText) and re.match('[A-RT-Z0-9]', upperText[end])) ): hit = False if hit: + if (end < len(upperText) and 'S' == upperText[end]): + end += 1 shipInText = text[start:end] formatted = formatShipName(text, shipInText) textReplace(text, formatted) @@ -114,7 +118,7 @@ def formatSystem(text, word, system): texts = [t for t in rtext.contents if isinstance(t, NavigableString) and len(t)] for text in texts: - worktext = re.sub(CHARS_TO_IGNORE_REGEX, '', text) + worktext = re.sub(CHARS_TO_IGNORE_REGEX, ' ', text) # Drop redundant whitespace so as to not throw off word index worktext = ' '.join(worktext.split()) @@ -146,13 +150,6 @@ def formatSystem(text, word, system): if system.startswith(upperWord): matchKey = system break - if None == matchKey and not '-' in upperWord: - for system in systemNames: # what if F-YH58 is named FY? - clearedSystem = system.replace("-", "") - if clearedSystem.startswith(upperWord): - matchKey = system - break - if matchKey: foundSystems.add(matchKey) formattedText = formatSystem(text, word, matchKey) From a7b40a3a7b3256fa8cb89945fb420d3b13166ee1 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Wed, 18 Jan 2017 02:37:45 -0500 Subject: [PATCH 32/36] Be more careful with word boundaries in system/ship replacement. Use normalized system name in URL. --- src/vi/chatparser/parser_functions.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vi/chatparser/parser_functions.py b/src/vi/chatparser/parser_functions.py index 49de95e..be70f46 100644 --- a/src/vi/chatparser/parser_functions.py +++ b/src/vi/chatparser/parser_functions.py @@ -49,6 +49,8 @@ # Do not ignore ? which triggers status change to request CHARS_TO_IGNORE_REGEX = '[*,!.()]' +REPLACE_WORD_REGEX = r'(^|(?<=[^0-9a-zA-Z_-])){0}((?=[^0-9a-zA-Z_])|$)' + def textReplace(element, newText): newText = "" + newText + "" @@ -79,7 +81,8 @@ def parseStatus(rtext): def parseShips(rtext): def formatShipName(text, word): newText = u"""{0}""" - text = text.replace(word, newText.format(word)) + # Only do replacements at word boundaries + text = re.sub(REPLACE_WORD_REGEX.format(word), newText.format(word), text) return text texts = [t for t in rtext.contents if isinstance(t, NavigableString)] @@ -109,11 +112,13 @@ def parseSystems(systems, rtext, foundSystems): # words to ignore on the system parser. use UPPER CASE WORDS_TO_IGNORE = ("IN", "IS", "AS", "OR", "NV", "TO", "ME", "HE", "SHE", "YOU", "ARE", - "ON", "HAS", "OF", "IT", "GET", "IF", "THE", "HOT", "OH", "OK", "GJ", "AND", "MY") + "ON", "HAS", "OF", "IT", "GET", "IF", "THE", "HOT", "OH", "OK", "GJ", "AND", "MY", + "SAY", "ANY", "NO", "FOR", "OUT", "WH", "MAN", "PART", "AT", "AN" ) def formatSystem(text, word, system): newText = u"""{1}""" - text = text.replace(word, newText.format(system, word)) + # Only do replacements at word boundaries: "no cyno onboard" would replace both "no"s in the first pass and then find a "cy" + text = re.sub(REPLACE_WORD_REGEX.format(word), newText.format(system, word), text) return text texts = [t for t in rtext.contents if isinstance(t, NavigableString) and len(t)] @@ -144,11 +149,11 @@ def formatSystem(text, word, system): upperWord = word.upper() if upperWord != word and upperWord in WORDS_TO_IGNORE: continue if upperWord in systemNames: # - direct hit on name - matchKey = upperWord + matchKey = systems[upperWord]['name'] elif 1 < len(upperWord) < 5: # - upperWord 2-4 chars. for system in systemNames: # system begins with? if system.startswith(upperWord): - matchKey = system + matchKey = systems[system]['name'] break if matchKey: foundSystems.add(matchKey) From c0fecc4e25c5896e33816200ae189f96acc94a4e Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sat, 21 Jan 2017 01:16:58 -0500 Subject: [PATCH 33/36] Fix error: 01/21 12:56:22| -- Unhandled Exception -- 01/21 12:56:22| File "...\PanningWebView.py", line 140, in acceptNavigationRequest return super(PanningWebView, self).acceptNavigationRequest(url, type, isMainFrame) 01/21 12:56:22| : super(type, obj): obj must be an instance or subtype of type --- src/vi/PanningWebView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vi/PanningWebView.py b/src/vi/PanningWebView.py index 6fbd4b1..cddfb5c 100644 --- a/src/vi/PanningWebView.py +++ b/src/vi/PanningWebView.py @@ -137,7 +137,7 @@ def __init__(self, parent=None): def acceptNavigationRequest(self, url, type, isMainFrame): if MainWindow.oldStyleWebKit: - return super(PanningWebView, self).acceptNavigationRequest(url, type, isMainFrame) + return super(VintelSvgPage, self).acceptNavigationRequest(url, type, isMainFrame) else: if type == QWebEnginePage.NavigationTypeLinkClicked: self.view().mapLinkClicked.emit(url) From 5afd9636211e048bca99825f0636bbbc27cd3a15 Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sat, 21 Jan 2017 17:25:00 -0500 Subject: [PATCH 34/36] Fix - highlight ship name starting with dash stuck in infinite loop. --- src/vi/chatparser/parser_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vi/chatparser/parser_functions.py b/src/vi/chatparser/parser_functions.py index be70f46..d9d38f9 100644 --- a/src/vi/chatparser/parser_functions.py +++ b/src/vi/chatparser/parser_functions.py @@ -49,7 +49,7 @@ # Do not ignore ? which triggers status change to request CHARS_TO_IGNORE_REGEX = '[*,!.()]' -REPLACE_WORD_REGEX = r'(^|(?<=[^0-9a-zA-Z_-])){0}((?=[^0-9a-zA-Z_])|$)' +REPLACE_WORD_REGEX = r'(^|(?<=[^0-9a-zA-Z])){0}((?=[^0-9a-zA-Z_])|$)' def textReplace(element, newText): From 3a92e683e04cbf80efe8e2f3d7a2731494d21d9b Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Sun, 22 Jan 2017 21:37:28 -0500 Subject: [PATCH 35/36] System was rendering white if alarms came in too fast. --- src/vi/dotlan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vi/dotlan.py b/src/vi/dotlan.py index a08117d..1d41038 100644 --- a/src/vi/dotlan.py +++ b/src/vi/dotlan.py @@ -472,6 +472,7 @@ def setStatus(self, newStatus, statusTime = None): for maxDiff, alarmColor, secondLineColor in self.ALARM_COLORS: if delta < maxDiff: self.setBackgroundColor(alarmColor) + break elif newStatus == states.CLEAR: self.lastAlarmTime = statusTime self.setBackgroundColor(self.CLEAR_COLOR) From 3f36041da3d2f6f1fc96bd24460b0831fc2cf93a Mon Sep 17 00:00:00 2001 From: IslayTzash Date: Fri, 27 Jan 2017 20:33:11 -0500 Subject: [PATCH 36/36] fix icon cache in python3 --- src/vi/cache/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vi/cache/cache.py b/src/vi/cache/cache.py index d40ac39..658d740 100644 --- a/src/vi/cache/cache.py +++ b/src/vi/cache/cache.py @@ -31,7 +31,7 @@ def from_blob(x): def to_blob(x): return x def from_blob(x): - return x + return x[0][0] from vi.cache.dbstructure import updateDatabase