Commits

Alex Mayfield committed 805d01c

Rename to OdaStats. Fix py2exe build to not require the C runtime which we can't distribute.

Comments (0)

Files changed (3)

+import os, sys, json, time, httplib, datetime, operator, subprocess, StringIO
+
+from PySide.QtCore import *
+from PySide.QtGui import *
+
+from Utils import *
+
+APP_AUTHOR = 'Charlie Gunyon & Alex Mayfield'
+APP_NAME = 'OdaStats'
+IDL_HOST = 'www.intldoomleague.org'
+IDL_PORT = 80
+
+def fix_slashes(s):
+    return s.replace('/', '\\')
+
+def server_from_dict(name, d):
+    return Server(name, d['address'], d['password'])
+
+def get_datetime_filename(extension=None):
+    now = datetime.datetime.now()
+    if extension:
+        return '.'.join((now.strftime('odastats-%Y%m%d_%H%M%S'), extension))
+    else:
+        return now.strftime('odastats-%Y%m%d_%H%M%S')
+
+class InputLog(object):
+
+    def __init__(self, filename):
+        self.fobj = open(filename)
+
+    def get_line(self):
+        return self.fobj.readline().rstrip('\r\n')
+
+    def close(self):
+        self.fobj.close()
+
+class OutputLog(object):
+
+    def __init__(self, filename):
+        self.fobj = open(filename, 'wb')
+        self.fobj.write('{"events": [\n')
+        self.first_event = True
+
+    def write(self, s):
+        if self.first_event:
+            self.first_event = False
+        else:
+            self.fobj.write(',\n')
+        self.fobj.write('    ')
+        self.fobj.write(s)
+        self.fobj.flush()
+
+    def close(self):
+        self.fobj.write('\n]}\n')
+        self.fobj.close()
+
+class Server(object):
+
+    def __init__(self, name, address, password):
+        self.name = name
+        self.password = password
+        if address.startswith('zds://'):
+            self.address = address[6:]
+        else:
+            self.address = address
+
+    def __str__(self):
+        return self.name
+
+    __unicode__ = __str__
+
+    def __repr__(self):
+        return 'Server(%r, %r, %r)' % (self.name, self.address, self.password)
+
+class ServerListModel(QAbstractListModel):
+
+    def __init__(self, servers=None):
+        QAbstractListModel.__init__(self)
+        self.icon_provider = None
+        self.servers = servers or list()
+
+    def rowCount(self, parent=QModelIndex()):
+        return len(self.servers)
+
+    def data(self, index, role=Qt.DisplayRole):
+        if role == Qt.DisplayRole:
+            return self.servers[index.row()].name
+        else:
+            return None
+
+    def headerData(self, index, value, role=Qt.DisplayRole):
+        return QVariant()
+
+    def setData(self, index, value, role=Qt.EditRole):
+        pass # [CG] Can't edit the server list.
+
+    def flags(self, index):
+        return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
+
+class ServerListView(QListView):
+
+    def __init__(self, *args, **kwargs):
+        QListView.__init__(self, *args, **kwargs)
+        self.selectedServer = None
+
+    @property
+    def rowCount(self):
+        model = self.model()
+        if model:
+            return model.rowCount()
+        return 0
+
+    @property
+    def servers(self):
+        return sorted(self.model().servers, key=operator.attrgetter('name'))
+
+    def setModel(self, model):
+        QListView.setModel(self, model)
+        self.selectedRows = set(range(self.rowCount))
+
+    def selectionChanged(self, selected, deselected):
+        QListView.selectionChanged(self, selected, deselected)
+        selected_servers = set([x.row() for x in self.selectedIndexes()])
+        if selected_servers:
+            selected_server_index = list(selected_servers)[0]
+            self.selectedServer = self.servers[selected_server_index]
+        else:
+            self.selectedServer = None
+
+class MainWindow(QMainWindow):
+
+    def __init__(self):
+        QMainWindow.__init__(self)
+        self.setWindowTitle(APP_NAME)
+        self.mainLayout = QHBoxLayout()
+        self.leftLayout = QVBoxLayout()
+        self.rightLayout = QVBoxLayout()
+        self.odamexLayout = QHBoxLayout()
+        self.outputFolderLayout = QHBoxLayout()
+        self.wadFolderLayout = QHBoxLayout()
+        self.demoLayout = QHBoxLayout()
+        self.serverListLayout = QVBoxLayout()
+        self.mainPanel = QWidget()
+        self.leftPanel = QWidget()
+        self.rightPanel = QWidget()
+        self.odamexPanel = QWidget()
+        self.outputFolderPanel = QWidget()
+        self.wadFolderPanel = QWidget()
+        self.demoPanel = QWidget()
+        self.serverPanel = QWidget()
+        self.odamexPathLabel = QLabel('Path to Odamex:')
+        self.odamexPathInput = QLineEdit()
+        self.odamexPathInput.setReadOnly(True)
+        self.odamexBrowseButton = QPushButton('Browse')
+        self.odamexLayout.addWidget(self.odamexPathLabel)
+        self.odamexLayout.addWidget(self.odamexPathInput)
+        self.odamexLayout.addWidget(self.odamexBrowseButton)
+        self.odamexPanel.setLayout(self.odamexLayout)
+        self.outputFolderLabel = QLabel('Output Folder:')
+        self.outputFolderInput = QLineEdit()
+        self.outputFolderInput.setReadOnly(True)
+        self.outputFolderBrowseButton = QPushButton('Browse')
+        self.outputFolderOpenButton = QPushButton('Open')
+        self.outputFolderLayout.addWidget(self.outputFolderLabel)
+        self.outputFolderLayout.addWidget(self.outputFolderInput)
+        self.outputFolderLayout.addWidget(self.outputFolderBrowseButton)
+        self.outputFolderLayout.addWidget(self.outputFolderOpenButton)
+        self.outputFolderPanel.setLayout(self.outputFolderLayout)
+        self.wadFolderLabel = QLabel('WAD Folder:')
+        self.wadFolderInput = QLineEdit()
+        self.wadFolderInput.setReadOnly(True)
+        self.wadFolderBrowseButton = QPushButton('Browse')
+        self.wadFolderOpenButton = QPushButton('Open')
+        self.wadFolderLayout.addWidget(self.wadFolderLabel)
+        self.wadFolderLayout.addWidget(self.wadFolderInput)
+        self.wadFolderLayout.addWidget(self.wadFolderBrowseButton)
+        self.wadFolderLayout.addWidget(self.wadFolderOpenButton)
+        self.wadFolderPanel.setLayout(self.wadFolderLayout)
+        self.demoPathLabel = QLabel('Load a Demo:')
+        self.demoInput = QLineEdit()
+        self.demoInput.setReadOnly(True)
+        self.demoBrowseButton = QPushButton('Browse')
+        self.demoPlayButton = QPushButton('Play')
+        self.demoLayout.addWidget(self.demoPathLabel)
+        self.demoLayout.addWidget(self.demoInput)
+        self.demoLayout.addWidget(self.demoBrowseButton)
+        self.demoLayout.addWidget(self.demoPlayButton)
+        self.demoPanel.setLayout(self.demoLayout)
+        self.serverListModel = ServerListModel()
+        self.serverList = ServerListView()
+        self.serverList.setModel(self.serverListModel)
+        self.serverJoinButton = QPushButton('Join Server')
+        self.serverListLayout.addWidget(self.serverList)
+        self.serverListLayout.addWidget(self.serverJoinButton)
+        self.serverPanel.setLayout(self.serverListLayout)
+        self.leftLayout.addWidget(self.odamexPanel)
+        self.leftLayout.addWidget(self.outputFolderPanel)
+        self.leftLayout.addWidget(self.wadFolderPanel)
+        self.leftLayout.addWidget(self.demoPanel)
+        self.leftPanel.setLayout(self.leftLayout)
+        self.rightLayout.addWidget(self.serverPanel)
+        self.rightPanel.setLayout(self.rightLayout)
+        self.mainLayout.addWidget(self.leftPanel)
+        self.mainLayout.addWidget(self.rightPanel)
+        self.mainPanel.setLayout(self.mainLayout)
+        self.setCentralWidget(self.mainPanel)
+        self.readSettings()
+        self.odamexBrowseButton.clicked.connect(self.setOdamex)
+        self.outputFolderBrowseButton.clicked.connect(self.setOutputFolder)
+        self.outputFolderOpenButton.clicked.connect(self.openOutputFolder)
+        self.wadFolderBrowseButton.clicked.connect(self.setWADFolder)
+        self.wadFolderOpenButton.clicked.connect(self.openWADFolder)
+        self.demoBrowseButton.clicked.connect(self.loadDemo)
+        self.demoPlayButton.clicked.connect(self.playDemo)
+        self.serverJoinButton.clicked.connect(self.connectToServer)
+        try:
+            self.seasonWAD = None
+            self.canConnectToServers = True
+            self.fetchServersAndLoadWAD()
+            self.statusMessage('Ready.')
+        except Exception, e:
+            self.statusMessage(' - '.join((
+                str(e), 'cannot connect to servers.'
+            )))
+            self.canConnectToServers = False
+
+    def statusMessage(self, s):
+        self.statusBar().showMessage(s)
+
+    def fetchServersAndLoadWAD(self):
+        error = False
+        error_code = None
+        conn = httplib.HTTPConnection(IDL_HOST, IDL_PORT)
+        conn.request('GET', '/info/servers', headers={
+            'accept': 'application/json'
+        })
+        response = conn.getresponse()
+        if response.status != 200:
+            raise Exception('Error contacting %s:%d: %d' % (
+                IDL_HOST, IDL_PORT, response.status
+            ))
+        servers = sorted(json.loads(response.read())['servers'].items())
+        self.serverListModel.beginResetModel()
+        self.serverListModel.servers = [
+            server_from_dict(name, d) for name, d in servers
+        ]
+        self.serverListModel.endResetModel()
+        conn.request('GET', '/info/season_wad', headers={
+            'accept': 'application/json'
+        })
+        response = conn.getresponse()
+        if response.status != 200:
+            raise Exception('Error contacting %s:%d: %d' % (
+                IDL_HOST, IDL_PORT, response.status
+            ))
+        server_wad = json.loads(response.read()).get('wad', None)
+        if not server_wad:
+            raise Exception('Bad JSON from idl.org')
+        self.seasonWAD = server_wad
+
+    def closeEvent(self, event):
+        self.writeSettings()
+        event.accept()
+
+    def readSettings(self):
+        settings = QSettings(APP_AUTHOR, APP_NAME)
+        pos = settings.value('pos', QPoint(200, 200))
+        size = settings.value('size', QSize(800, 240))
+        odamex = settings.value('odamex', None)
+        output_folder = settings.value('output_folder', None)
+        wad_folder = settings.value('wad_folder', None)
+        self.resize(QSize(800, 240))
+        self.move(pos)
+        if odamex:
+            self.odamexPathInput.setText(odamex)
+        if output_folder:
+            self.outputFolderInput.setText(output_folder)
+        if wad_folder:
+            self.wadFolderInput.setText(wad_folder)
+
+    def writeSettings(self):
+        settings = QSettings(APP_AUTHOR, APP_NAME)
+        settings.setValue('pos', self.pos())
+        settings.setValue('size', self.size())
+        odamex = self.odamexPathInput.text()
+        output_folder = self.outputFolderInput.text()
+        wad_folder = self.wadFolderInput.text()
+        if odamex:
+            settings.setValue('odamex', odamex)
+        if output_folder:
+            settings.setValue('output_folder', output_folder)
+        if wad_folder:
+            settings.setValue('wad_folder', wad_folder)
+
+    def setOdamex(self):
+        filename, filtr = QFileDialog.getOpenFileName(
+            self,
+            'Set Path to Odamex',
+            '.',
+            'Programs (*.exe);;All files (*.*)'
+        )
+        if filename:
+            filename = fix_slashes(filename)
+            self.statusMessage('Set Odamex executable to "%s".' % (filename))
+            self.odamexPathInput.setText(filename)
+
+    def setWADFolder(self):
+        foldername = QFileDialog.getExistingDirectory(
+            self, 'Select WAD Folder', '.'
+        )
+        if foldername:
+            self.wadFolderInput.setText(foldername)
+            self.statusMessage('Set WAD folder to "%s".' % (foldername))
+
+    def openWADFolder(self):
+        wad_folder = self.wadFolderInput.text()
+        if wad_folder:
+            os.startfile(wad_folder)
+            self.statusMessage('Opening WAD folder "%s".' % (wad_folder))
+        else:
+            self.statusMessage('No WAD folder set.')
+
+    def setOutputFolder(self):
+        foldername = QFileDialog.getExistingDirectory(
+            self, 'Select Output Folder', '.'
+        )
+        if foldername:
+            self.outputFolderInput.setText(foldername)
+            self.statusMessage('Set output folder to "%s".' % (foldername))
+
+    def openOutputFolder(self):
+        output_folder = self.outputFolderInput.text()
+        if output_folder:
+            os.startfile(output_folder)
+            self.statusMessage('Opening output folder "%s".' % (output_folder))
+        else:
+            self.statusMessage('No output folder set.')
+
+    def loadDemo(self):
+        filename, filtr = QFileDialog.getOpenFileName(
+            self,
+            'Open Odamex Demo',
+            '.',
+            'Odamex Demo File (*.odd);;All files (*.*)'
+        )
+        if filename:
+            filename = fix_slashes(filename)
+            self.demoInput.setText(filename)
+            self.statusMessage('Loaded demo "%s".' % (filename))
+
+    def getStats(self, odamex_log_path):
+        from Regexps import get_client_regexps
+        # [CG] We can safely assume the output folder input field is populated
+        #      because this function is only called after the field is checked.
+        event_log_path = os.path.join(
+            self.outputFolderInput.text(),
+            get_datetime_filename(extension='json')
+        )
+        while not os.path.isfile(odamex_log_path):
+            time.sleep(1)
+        input_logfile = InputLog(odamex_log_path)
+        output_logfile = OutputLog(event_log_path)
+        regexps = get_client_regexps()
+        epoch = datetime.datetime(1970, 1, 1)
+        while 1:
+            if self.odamex_pobj.poll() is not None:
+                output_logfile.close()
+                self.statusMessage('Events saved to %s.' % (event_log_path))
+                break
+            line = input_logfile.get_line()
+            if line:
+                e = get_event_from_line(line, regexps)
+                if e and (e.category != 'command' or e.type == 'map_change'):
+                    td = e.dt - epoch
+                    ts = '%s.%s' % (timedelta_in_seconds(td), td.microseconds)
+                    output_logfile.write(json.dumps(dict(
+                        timestamp=ts,
+                        type=e.type,
+                        data=e.data,
+                        category=e.category
+                    )))
+            else:
+                time.sleep(.027)
+
+    def launchOdamex(self, extra_args=None):
+        odamex = self.odamexPathInput.text()
+        if not odamex:
+            self.statusMessage('Odamex path not set.')
+            return
+        output_folder = self.outputFolderInput.text()
+        if not output_folder:
+            self.statusMessage('Output folder not set.')
+            return
+        wad_folder = self.wadFolderInput.text()
+        if not wad_folder:
+            self.statusMessage('WAD folder not set.')
+            return
+        iwad_path = os.path.join(wad_folder, 'doom2.wad')
+        if not os.path.isfile(iwad_path):
+            self.statusMessage('doom2.wad not found in %s.' % (wad_folder))
+            return
+        odamex_log_file = get_datetime_filename(extension='txt')
+        odamex_log_path = os.path.join(output_folder, odamex_log_file)
+        cmd_args = [
+            odamex,
+            '-iwad', iwad_path,
+            '+logfile', odamex_log_path
+        ]
+        if extra_args:
+            cmd_args.extend(extra_args)
+        self.statusMessage('Launching Odamex.')
+        print ' '.join(cmd_args)
+        self.odamex_pobj = subprocess.Popen(cmd_args)
+        self.statusMessage('Gathering events.')
+        self.getStats(odamex_log_path)
+
+    def playDemo(self):
+        demo_path = self.demoInput.text()
+        if not demo_path:
+            self.statusMessage('Demo file not selected.')
+            return
+        wad_folder = self.wadFolderInput.text()
+        if not wad_folder:
+            self.statusMessage('WAD folder not set.')
+            return
+        demo_file = os.path.basename(demo_path).lower()
+        args = ['-waddir', wad_folder, '+netplay', demo_path]
+        self.launchOdamex(args)
+
+    def connectToServer(self):
+        output_folder = self.outputFolderInput.text()
+        if not output_folder:
+            self.statusMessage('Output folder not set.')
+            return
+        server = self.serverList.selectedServer
+        if not server:
+            self.statusMessage('No server selected.')
+            return
+        wad_folder = self.wadFolderInput.text()
+        if not wad_folder:
+            self.statusMessage('WAD folder not set.')
+            return
+        demo_path = os.path.join(output_folder, get_datetime_filename('zdo'))
+        idl_wad_path = os.path.join(wad_folder, self.seasonWAD)
+        self.launchOdamex((
+            '-connect', server.address, server.password,
+            '-file', idl_wad_path,
+            '+netrecord', demo_path,
+        ))
+
+if __name__ == "__main__":
+    app = QApplication(sys.argv)
+    window = MainWindow()
+    window.show()
+    sys.exit(app.exec_())
+
 import py2exe
 
 setup(
-    name='ZDStats',
+    name='OdaStats',
     version='1.0',
-    description='ZDaemon Stats Generator',
-    author='Charles Gunyon',
-    author_email='charles.gunyon@gmail.com',
-    console=[ 'zdstats.py' ],
-    options={ "py2exe": {} }
+    description='Odamex Stats Generator',
+    author='Charles Gunyon, Alex Mayfield',
+    author_email='charles.gunyon@gmail.com, alexmax2742@gmail.com',
+    console=['odastats.py'],
+    options={"py2exe": {"dll_excludes": ["MSVCP90.dll"]}}
 )
 

zdstats.py

-import os, sys, json, time, httplib, datetime, operator, subprocess, StringIO
-
-from PySide.QtCore import *
-from PySide.QtGui import *
-
-from Utils import *
-
-APP_AUTHOR = 'Charlie Gunyon & Alex Mayfield'
-APP_NAME = 'OdaStats'
-IDL_HOST = 'www.intldoomleague.org'
-IDL_PORT = 80
-
-def fix_slashes(s):
-    return s.replace('/', '\\')
-
-def server_from_dict(name, d):
-    return Server(name, d['address'], d['password'])
-
-def get_datetime_filename(extension=None):
-    now = datetime.datetime.now()
-    if extension:
-        return '.'.join((now.strftime('odastats-%Y%m%d_%H%M%S'), extension))
-    else:
-        return now.strftime('odastats-%Y%m%d_%H%M%S')
-
-class InputLog(object):
-
-    def __init__(self, filename):
-        self.fobj = open(filename)
-
-    def get_line(self):
-        return self.fobj.readline().rstrip('\r\n')
-
-    def close(self):
-        self.fobj.close()
-
-class OutputLog(object):
-
-    def __init__(self, filename):
-        self.fobj = open(filename, 'wb')
-        self.fobj.write('{"events": [\n')
-        self.first_event = True
-
-    def write(self, s):
-        if self.first_event:
-            self.first_event = False
-        else:
-            self.fobj.write(',\n')
-        self.fobj.write('    ')
-        self.fobj.write(s)
-        self.fobj.flush()
-
-    def close(self):
-        self.fobj.write('\n]}\n')
-        self.fobj.close()
-
-class Server(object):
-
-    def __init__(self, name, address, password):
-        self.name = name
-        self.password = password
-        if address.startswith('zds://'):
-            self.address = address[6:]
-        else:
-            self.address = address
-
-    def __str__(self):
-        return self.name
-
-    __unicode__ = __str__
-
-    def __repr__(self):
-        return 'Server(%r, %r, %r)' % (self.name, self.address, self.password)
-
-class ServerListModel(QAbstractListModel):
-
-    def __init__(self, servers=None):
-        QAbstractListModel.__init__(self)
-        self.icon_provider = None
-        self.servers = servers or list()
-
-    def rowCount(self, parent=QModelIndex()):
-        return len(self.servers)
-
-    def data(self, index, role=Qt.DisplayRole):
-        if role == Qt.DisplayRole:
-            return self.servers[index.row()].name
-        else:
-            return None
-
-    def headerData(self, index, value, role=Qt.DisplayRole):
-        return QVariant()
-
-    def setData(self, index, value, role=Qt.EditRole):
-        pass # [CG] Can't edit the server list.
-
-    def flags(self, index):
-        return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
-
-class ServerListView(QListView):
-
-    def __init__(self, *args, **kwargs):
-        QListView.__init__(self, *args, **kwargs)
-        self.selectedServer = None
-
-    @property
-    def rowCount(self):
-        model = self.model()
-        if model:
-            return model.rowCount()
-        return 0
-
-    @property
-    def servers(self):
-        return sorted(self.model().servers, key=operator.attrgetter('name'))
-
-    def setModel(self, model):
-        QListView.setModel(self, model)
-        self.selectedRows = set(range(self.rowCount))
-
-    def selectionChanged(self, selected, deselected):
-        QListView.selectionChanged(self, selected, deselected)
-        selected_servers = set([x.row() for x in self.selectedIndexes()])
-        if selected_servers:
-            selected_server_index = list(selected_servers)[0]
-            self.selectedServer = self.servers[selected_server_index]
-        else:
-            self.selectedServer = None
-
-class MainWindow(QMainWindow):
-
-    def __init__(self):
-        QMainWindow.__init__(self)
-        self.setWindowTitle(APP_NAME)
-        self.mainLayout = QHBoxLayout()
-        self.leftLayout = QVBoxLayout()
-        self.rightLayout = QVBoxLayout()
-        self.odamexLayout = QHBoxLayout()
-        self.outputFolderLayout = QHBoxLayout()
-        self.wadFolderLayout = QHBoxLayout()
-        self.demoLayout = QHBoxLayout()
-        self.serverListLayout = QVBoxLayout()
-        self.mainPanel = QWidget()
-        self.leftPanel = QWidget()
-        self.rightPanel = QWidget()
-        self.odamexPanel = QWidget()
-        self.outputFolderPanel = QWidget()
-        self.wadFolderPanel = QWidget()
-        self.demoPanel = QWidget()
-        self.serverPanel = QWidget()
-        self.odamexPathLabel = QLabel('Path to Odamex:')
-        self.odamexPathInput = QLineEdit()
-        self.odamexPathInput.setReadOnly(True)
-        self.odamexBrowseButton = QPushButton('Browse')
-        self.odamexLayout.addWidget(self.odamexPathLabel)
-        self.odamexLayout.addWidget(self.odamexPathInput)
-        self.odamexLayout.addWidget(self.odamexBrowseButton)
-        self.odamexPanel.setLayout(self.odamexLayout)
-        self.outputFolderLabel = QLabel('Output Folder:')
-        self.outputFolderInput = QLineEdit()
-        self.outputFolderInput.setReadOnly(True)
-        self.outputFolderBrowseButton = QPushButton('Browse')
-        self.outputFolderOpenButton = QPushButton('Open')
-        self.outputFolderLayout.addWidget(self.outputFolderLabel)
-        self.outputFolderLayout.addWidget(self.outputFolderInput)
-        self.outputFolderLayout.addWidget(self.outputFolderBrowseButton)
-        self.outputFolderLayout.addWidget(self.outputFolderOpenButton)
-        self.outputFolderPanel.setLayout(self.outputFolderLayout)
-        self.wadFolderLabel = QLabel('WAD Folder:')
-        self.wadFolderInput = QLineEdit()
-        self.wadFolderInput.setReadOnly(True)
-        self.wadFolderBrowseButton = QPushButton('Browse')
-        self.wadFolderOpenButton = QPushButton('Open')
-        self.wadFolderLayout.addWidget(self.wadFolderLabel)
-        self.wadFolderLayout.addWidget(self.wadFolderInput)
-        self.wadFolderLayout.addWidget(self.wadFolderBrowseButton)
-        self.wadFolderLayout.addWidget(self.wadFolderOpenButton)
-        self.wadFolderPanel.setLayout(self.wadFolderLayout)
-        self.demoPathLabel = QLabel('Load a Demo:')
-        self.demoInput = QLineEdit()
-        self.demoInput.setReadOnly(True)
-        self.demoBrowseButton = QPushButton('Browse')
-        self.demoPlayButton = QPushButton('Play')
-        self.demoLayout.addWidget(self.demoPathLabel)
-        self.demoLayout.addWidget(self.demoInput)
-        self.demoLayout.addWidget(self.demoBrowseButton)
-        self.demoLayout.addWidget(self.demoPlayButton)
-        self.demoPanel.setLayout(self.demoLayout)
-        self.serverListModel = ServerListModel()
-        self.serverList = ServerListView()
-        self.serverList.setModel(self.serverListModel)
-        self.serverJoinButton = QPushButton('Join Server')
-        self.serverListLayout.addWidget(self.serverList)
-        self.serverListLayout.addWidget(self.serverJoinButton)
-        self.serverPanel.setLayout(self.serverListLayout)
-        self.leftLayout.addWidget(self.odamexPanel)
-        self.leftLayout.addWidget(self.outputFolderPanel)
-        self.leftLayout.addWidget(self.wadFolderPanel)
-        self.leftLayout.addWidget(self.demoPanel)
-        self.leftPanel.setLayout(self.leftLayout)
-        self.rightLayout.addWidget(self.serverPanel)
-        self.rightPanel.setLayout(self.rightLayout)
-        self.mainLayout.addWidget(self.leftPanel)
-        self.mainLayout.addWidget(self.rightPanel)
-        self.mainPanel.setLayout(self.mainLayout)
-        self.setCentralWidget(self.mainPanel)
-        self.readSettings()
-        self.odamexBrowseButton.clicked.connect(self.setOdamex)
-        self.outputFolderBrowseButton.clicked.connect(self.setOutputFolder)
-        self.outputFolderOpenButton.clicked.connect(self.openOutputFolder)
-        self.wadFolderBrowseButton.clicked.connect(self.setWADFolder)
-        self.wadFolderOpenButton.clicked.connect(self.openWADFolder)
-        self.demoBrowseButton.clicked.connect(self.loadDemo)
-        self.demoPlayButton.clicked.connect(self.playDemo)
-        self.serverJoinButton.clicked.connect(self.connectToServer)
-        try:
-            self.seasonWAD = None
-            self.canConnectToServers = True
-            self.fetchServersAndLoadWAD()
-            self.statusMessage('Ready.')
-        except Exception, e:
-            self.statusMessage(' - '.join((
-                str(e), 'cannot connect to servers.'
-            )))
-            self.canConnectToServers = False
-
-    def statusMessage(self, s):
-        self.statusBar().showMessage(s)
-
-    def fetchServersAndLoadWAD(self):
-        error = False
-        error_code = None
-        conn = httplib.HTTPConnection(IDL_HOST, IDL_PORT)
-        conn.request('GET', '/info/servers', headers={
-            'accept': 'application/json'
-        })
-        response = conn.getresponse()
-        if response.status != 200:
-            raise Exception('Error contacting %s:%d: %d' % (
-                IDL_HOST, IDL_PORT, response.status
-            ))
-        servers = sorted(json.loads(response.read())['servers'].items())
-        self.serverListModel.beginResetModel()
-        self.serverListModel.servers = [
-            server_from_dict(name, d) for name, d in servers
-        ]
-        self.serverListModel.endResetModel()
-        conn.request('GET', '/info/season_wad', headers={
-            'accept': 'application/json'
-        })
-        response = conn.getresponse()
-        if response.status != 200:
-            raise Exception('Error contacting %s:%d: %d' % (
-                IDL_HOST, IDL_PORT, response.status
-            ))
-        server_wad = json.loads(response.read()).get('wad', None)
-        if not server_wad:
-            raise Exception('Bad JSON from idl.org')
-        self.seasonWAD = server_wad
-
-    def closeEvent(self, event):
-        self.writeSettings()
-        event.accept()
-
-    def readSettings(self):
-        settings = QSettings(APP_AUTHOR, APP_NAME)
-        pos = settings.value('pos', QPoint(200, 200))
-        size = settings.value('size', QSize(800, 240))
-        odamex = settings.value('odamex', None)
-        output_folder = settings.value('output_folder', None)
-        wad_folder = settings.value('wad_folder', None)
-        self.resize(QSize(800, 240))
-        self.move(pos)
-        if odamex:
-            self.odamexPathInput.setText(odamex)
-        if output_folder:
-            self.outputFolderInput.setText(output_folder)
-        if wad_folder:
-            self.wadFolderInput.setText(wad_folder)
-
-    def writeSettings(self):
-        settings = QSettings(APP_AUTHOR, APP_NAME)
-        settings.setValue('pos', self.pos())
-        settings.setValue('size', self.size())
-        odamex = self.odamexPathInput.text()
-        output_folder = self.outputFolderInput.text()
-        wad_folder = self.wadFolderInput.text()
-        if odamex:
-            settings.setValue('odamex', odamex)
-        if output_folder:
-            settings.setValue('output_folder', output_folder)
-        if wad_folder:
-            settings.setValue('wad_folder', wad_folder)
-
-    def setOdamex(self):
-        filename, filtr = QFileDialog.getOpenFileName(
-            self,
-            'Set Path to Odamex',
-            '.',
-            'Programs (*.exe);;All files (*.*)'
-        )
-        if filename:
-            filename = fix_slashes(filename)
-            self.statusMessage('Set Odamex executable to "%s".' % (filename))
-            self.odamexPathInput.setText(filename)
-
-    def setWADFolder(self):
-        foldername = QFileDialog.getExistingDirectory(
-            self, 'Select WAD Folder', '.'
-        )
-        if foldername:
-            self.wadFolderInput.setText(foldername)
-            self.statusMessage('Set WAD folder to "%s".' % (foldername))
-
-    def openWADFolder(self):
-        wad_folder = self.wadFolderInput.text()
-        if wad_folder:
-            os.startfile(wad_folder)
-            self.statusMessage('Opening WAD folder "%s".' % (wad_folder))
-        else:
-            self.statusMessage('No WAD folder set.')
-
-    def setOutputFolder(self):
-        foldername = QFileDialog.getExistingDirectory(
-            self, 'Select Output Folder', '.'
-        )
-        if foldername:
-            self.outputFolderInput.setText(foldername)
-            self.statusMessage('Set output folder to "%s".' % (foldername))
-
-    def openOutputFolder(self):
-        output_folder = self.outputFolderInput.text()
-        if output_folder:
-            os.startfile(output_folder)
-            self.statusMessage('Opening output folder "%s".' % (output_folder))
-        else:
-            self.statusMessage('No output folder set.')
-
-    def loadDemo(self):
-        filename, filtr = QFileDialog.getOpenFileName(
-            self,
-            'Open Odamex Demo',
-            '.',
-            'Odamex Demo File (*.odd);;All files (*.*)'
-        )
-        if filename:
-            filename = fix_slashes(filename)
-            self.demoInput.setText(filename)
-            self.statusMessage('Loaded demo "%s".' % (filename))
-
-    def getStats(self, odamex_log_path):
-        from Regexps import get_client_regexps
-        # [CG] We can safely assume the output folder input field is populated
-        #      because this function is only called after the field is checked.
-        event_log_path = os.path.join(
-            self.outputFolderInput.text(),
-            get_datetime_filename(extension='json')
-        )
-        while not os.path.isfile(odamex_log_path):
-            time.sleep(1)
-        input_logfile = InputLog(odamex_log_path)
-        output_logfile = OutputLog(event_log_path)
-        regexps = get_client_regexps()
-        epoch = datetime.datetime(1970, 1, 1)
-        while 1:
-            if self.odamex_pobj.poll() is not None:
-                output_logfile.close()
-                self.statusMessage('Events saved to %s.' % (event_log_path))
-                break
-            line = input_logfile.get_line()
-            if line:
-                e = get_event_from_line(line, regexps)
-                if e and (e.category != 'command' or e.type == 'map_change'):
-                    td = e.dt - epoch
-                    ts = '%s.%s' % (timedelta_in_seconds(td), td.microseconds)
-                    output_logfile.write(json.dumps(dict(
-                        timestamp=ts,
-                        type=e.type,
-                        data=e.data,
-                        category=e.category
-                    )))
-            else:
-                time.sleep(.027)
-
-    def launchOdamex(self, extra_args=None):
-        odamex = self.odamexPathInput.text()
-        if not odamex:
-            self.statusMessage('Odamex path not set.')
-            return
-        output_folder = self.outputFolderInput.text()
-        if not output_folder:
-            self.statusMessage('Output folder not set.')
-            return
-        wad_folder = self.wadFolderInput.text()
-        if not wad_folder:
-            self.statusMessage('WAD folder not set.')
-            return
-        iwad_path = os.path.join(wad_folder, 'doom2.wad')
-        if not os.path.isfile(iwad_path):
-            self.statusMessage('doom2.wad not found in %s.' % (wad_folder))
-            return
-        odamex_log_file = get_datetime_filename(extension='txt')
-        odamex_log_path = os.path.join(output_folder, odamex_log_file)
-        cmd_args = [
-            odamex,
-            '-iwad', iwad_path,
-            '+logfile', odamex_log_path
-        ]
-        if extra_args:
-            cmd_args.extend(extra_args)
-        self.statusMessage('Launching Odamex.')
-        print ' '.join(cmd_args)
-        self.odamex_pobj = subprocess.Popen(cmd_args)
-        self.statusMessage('Gathering events.')
-        self.getStats(odamex_log_path)
-
-    def playDemo(self):
-        demo_path = self.demoInput.text()
-        if not demo_path:
-            self.statusMessage('Demo file not selected.')
-            return
-        wad_folder = self.wadFolderInput.text()
-        if not wad_folder:
-            self.statusMessage('WAD folder not set.')
-            return
-        demo_file = os.path.basename(demo_path).lower()
-        args = ['-waddir', wad_folder, '+netplay', demo_path]
-        self.launchOdamex(args)
-
-    def connectToServer(self):
-        output_folder = self.outputFolderInput.text()
-        if not output_folder:
-            self.statusMessage('Output folder not set.')
-            return
-        server = self.serverList.selectedServer
-        if not server:
-            self.statusMessage('No server selected.')
-            return
-        wad_folder = self.wadFolderInput.text()
-        if not wad_folder:
-            self.statusMessage('WAD folder not set.')
-            return
-        demo_path = os.path.join(output_folder, get_datetime_filename('zdo'))
-        idl_wad_path = os.path.join(wad_folder, self.seasonWAD)
-        self.launchOdamex((
-            '-connect', server.address, server.password,
-            '-file', idl_wad_path,
-            '+netrecord', demo_path,
-        ))
-
-if __name__ == "__main__":
-    app = QApplication(sys.argv)
-    window = MainWindow()
-    window.show()
-    sys.exit(app.exec_())
-