diff --git a/app.py b/app.py new file mode 100644 index 0000000..eddaa31 --- /dev/null +++ b/app.py @@ -0,0 +1,17 @@ + + +""" + Main application functions. + + * backing up logs. + - from directory to directory + - compression as option + + * log-sessions: + - contains one session of log + - has a source (directory, file) + - determines user + - parses logs + + +""" \ No newline at end of file diff --git a/backup.py b/backup.py new file mode 100644 index 0000000..ea3c1ca --- /dev/null +++ b/backup.py @@ -0,0 +1,69 @@ +""" + Backup Directories, Handle Files... +""" + +import os, logging, zipfile + +def make_zipfile(output_filename, source_dir): + relroot = os.path.abspath(os.path.join(source_dir, os.pardir)) + with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zip: + for root, dirs, files in os.walk(source_dir): + # add directory (needed for empty dirs) + zip.write(root, os.path.relpath(root, relroot)) + for file in files: + filename = os.path.join(root, file) + if os.path.isfile(filename): # regular files only + arcname = os.path.join(os.path.relpath(root, relroot), file) + zip.write(filename, arcname) + +def backup_log_directory(log_directory, backup_directory, compress=True, + ommit_level=2, verbose=False): + # @todo: raw copy + # ommit_level 0: overwrite. + # ommit_level 1: write if selected compression method not backuped yet + # ommit_level 2: write only if neither method contains directory. + nothing_found = True + # get all directory names in log_directory. + # zip them into backup_directory + for directory in os.listdir(log_directory): + full_dir = os.path.join(log_directory, directory) + nothing_found = False + if os.path.isdir(full_dir): + if os.path.exists(os.path.join(full_dir, 'combat.log'))\ + and os.path.exists(os.path.join(full_dir, 'game.log'))\ + and os.path.exists(os.path.join(full_dir, 'chat.log'))\ + and os.path.exists(os.path.join(full_dir, 'game.net.log')): + output_filename = '%s.zip' % directory + if os.path.exists(os.path.join(backup_directory, output_filename))\ + and ((ommit_level >= 1 and compress) or (ommit_level==2 and not compress)): + logging.warning('Log %s exists as zip backup, ommited.' % output_filename) + elif os.path.exists(os.path.join(backup_directory, directory))\ + and ((ommit_level == 2 and compress) or (ommit_level>=1 and not compress)): + logging.warning('Log %s exists as directory backup, ommited.' % directory) + else: + # do the backup + if compress: + make_zipfile(os.path.join(backup_directory, output_filename), + full_dir) + logging.info('Backed up %s' % directory) + if verbose: + print "Backed up %s" % directory + else: + if verbose: + print "Directory Raw Backup not implemented yet." + raise NotImplementedError + else: + if verbose: + print "%s is not a directory." % full_dir + if verbose and nothing_found: + print "Nothing to backup found in %s" % log_directory + + +if __name__ == '__main__': + print "Performing Log Backup (Dev)" + log_source = os.path.join(os.path.expanduser('~'), + 'Documents', 'My Games', 'StarConflict', 'logs') + log_dest = os.path.join(os.path.expanduser('~'), + 'Documents', 'My Games', 'sc') + backup_log_directory(log_source, log_dest, verbose=True, compress=True) + diff --git a/brainstorm.py b/brainstorm.py index c1cf942..d79ed1a 100644 --- a/brainstorm.py +++ b/brainstorm.py @@ -1,80 +1,81 @@ -""" - Brainstorm File for Star Conflict Log Parsing - - Needed - - find steam/scon folder on windows - - find steam/scon folder on mac - - find steam/scon folder on linux - - what about steamless installs? - - Elaborate - - which GUI to use? wx? PyQt4? PySide? - - take over the database stuff from weltenfall.starconflict? - - Investigate - - language based log files? -""" -#from win32com.shell import shell, shellcon -import os, sys, logging -from logs.logresolver import LogFileResolver as LogFile -from logs import combat - -# for windows its kinda this: -settings = {'logfiles': os.path.join(os.path.expanduser('~'), - 'Documents', - 'My Games', - 'StarConflict', - 'logs' - )} - -def find_log_files(logpath): - ''' returns a list of 4-tuples representing - (combat.log, game.log, chat.log, game.net.log) - for each directory in the logpath - ''' - ret = [] - for directory in os.listdir(logpath): - full_dir = os.path.join(logpath, directory) - if os.path.isdir(full_dir): - if os.path.exists(os.path.join(full_dir, 'combat.log'))\ - and os.path.exists(os.path.join(full_dir, 'game.log'))\ - and os.path.exists(os.path.join(full_dir, 'chat.log'))\ - and os.path.exists(os.path.join(full_dir, 'game.net.log')): - ret.append(( - os.path.join(full_dir, 'combat.log'), - os.path.join(full_dir, 'game.log'), - os.path.join(full_dir, 'chat.log'), - os.path.join(full_dir, 'game.net.log') - )) - return ret - -def parse_games(logfiles): - _logfiles = [] - for logpack in logfiles: - combatlog, gamelog, chatlog, gamenetlog = logpack - _logfiles.append(LogFile(combatlog)) - #_logfiles.append(LogFile(gamelog)) - #_logfiles.append(LogFile(chatlog)) - #_logfiles.append(LogFile(gamenetlog)) - return _logfiles - -if __name__ == '__main__': - logfiles = find_log_files(settings['logfiles']) - logfiles = parse_games(logfiles) - #f = open('output.txt', 'w') - rex = {} - for logf in logfiles: - logf.parse() - for l in logf.lines: - if isinstance(l, dict): - #print l - pass - else: - if not l.unpack(): - rex[l.__class__.__name__] = rex.get(l.__class__.__name__, 0) + 1 - if not isinstance(l, combat.UserEvent): - print l.values['log'] - #f.write(l.values['log'] + '\n') - #f.close() - #print type(l) +""" + Brainstorm File for Star Conflict Log Parsing + + Needed + - find steam/scon folder on windows + - find steam/scon folder on mac + - find steam/scon folder on linux + - what about steamless installs? + + Elaborate + - which GUI to use? wx? PyQt4? PySide? + - take over the database stuff from weltenfall.starconflict? + + Investigate + - language based log files? +""" +#from win32com.shell import shell, shellcon +import os, sys, logging +from logs.logresolver import LogFileResolver as LogFile +from logs import combat + +# for windows its kinda this: +settings = {'logfiles': os.path.join(os.path.expanduser('~'), + 'Documents', + 'My Games', + 'StarConflict', + 'logs' + )} + +def find_log_files(logpath): + ''' returns a list of 4-tuples representing + (combat.log, game.log, chat.log, game.net.log) + for each directory in the logpath + ''' + ret = [] + for directory in os.listdir(logpath): + full_dir = os.path.join(logpath, directory) + if os.path.isdir(full_dir): + if os.path.exists(os.path.join(full_dir, 'combat.log'))\ + and os.path.exists(os.path.join(full_dir, 'game.log'))\ + and os.path.exists(os.path.join(full_dir, 'chat.log'))\ + and os.path.exists(os.path.join(full_dir, 'game.net.log')): + ret.append(( + os.path.join(full_dir, 'combat.log'), + os.path.join(full_dir, 'game.log'), + os.path.join(full_dir, 'chat.log'), + os.path.join(full_dir, 'game.net.log') + )) + return ret + +def parse_games(logfiles): + _logfiles = [] + for logpack in logfiles: + combatlog, gamelog, chatlog, gamenetlog = logpack + _logfiles.append(LogFile(combatlog)) + #_logfiles.append(LogFile(gamelog)) + #_logfiles.append(LogFile(chatlog)) + #_logfiles.append(LogFile(gamenetlog)) + return _logfiles + +if __name__ == '__main__': + logfiles = find_log_files(settings['logfiles']) + logfiles = parse_games(logfiles) + #f = open('output.txt', 'w') + rex = {} + for logf in logfiles: + logf.read() + logf.parse() + for l in logf.lines: + if isinstance(l, dict): + #print l + pass + else: + if not l.unpack(): + rex[l.__class__.__name__] = rex.get(l.__class__.__name__, 0) + 1 + if not isinstance(l, combat.UserEvent): + print l.values['log'] + #f.write(l.values['log'] + '\n') + #f.close() + #print type(l) print rex \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..1f3341d --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,4 @@ +""" + Handle SCon's config file. + +""" \ No newline at end of file diff --git a/config/display_config.py b/config/display_config.py new file mode 100644 index 0000000..619ef9b --- /dev/null +++ b/config/display_config.py @@ -0,0 +1,52 @@ +""" + Simple brainstorm to display a config file. +""" +import os, logging +logging.basicConfig(level=logging.INFO) +# import ET: +try: + ET = None + import lxml.etree as ET + logging.info('Using LXML.') +except ImportError: + try: + import cElementTree as ET + logging.info('Using cElementTree') + except ImportError: + try: + import elementtree.ElementTree as ET + logging.info('Using ElementTree') + except ImportError: + import xml.etree.ElementTree as ET # python 2.5 + logging.info('Using xml.ElementTree') +finally: + if not ET: + raise NotImplementedError, "XML Parser not found in your Python." +################################################################################################## + + +CONFIG_FILE = os.path.join(os.path.expanduser('~'), + 'Documents', + 'My Games', + 'StarConflict', + 'user_config.xml') + +def read_config(config_file): + tree = ET.parse(config_file) + # doc = tree.getroot() + return tree + +if __name__ == '__main__': + # Read the config + tree = read_config(CONFIG_FILE) + doc = tree.getroot() + if doc.tag == 'UserConfig' \ + and len(doc) == 1\ + and doc[0].tag == 'CVars'\ + and doc[0].attrib['version'] == '4': + print "Found valid config file." + cvars = doc[0] + for child in cvars: + print '%s = %s' % (child.tag, child.attrib['val']) + else: + print "Not found valid config file." \ No newline at end of file diff --git a/game/__init__.py b/game/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/game/battle.py b/game/battle.py new file mode 100644 index 0000000..33891d5 --- /dev/null +++ b/game/battle.py @@ -0,0 +1,17 @@ +""" + Represents a battle instance. + + todo: finding battles. factory for missions, skirmishes? +""" + +class Battle(object): + __slots__ = ['players', + 'teams', + 'time_start', + 'time_end',] + def __init__(self, parent=None): + # parent is a log-session usually + pass + + + \ No newline at end of file diff --git a/game/mission.py b/game/mission.py new file mode 100644 index 0000000..e69de29 diff --git a/game/skirmish.py b/game/skirmish.py new file mode 100644 index 0000000..e69de29 diff --git a/gui/treeview.py b/gui/treeview.py new file mode 100644 index 0000000..e42a9c2 --- /dev/null +++ b/gui/treeview.py @@ -0,0 +1,444 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +## FROM: +# https://github.com/cloudformdesign/cloudtb/blob/master/extra/PyQt/treeview.py +# ****** The Cloud Toolbox v0.1.2****** +# This is the cloud toolbox -- a single module used in several packages +# found at +# For more information see +# +# This module may be a part of a python package, and may be out of date. +# This behavior is intentional, do NOT update it. +# +# You are encouraged to use this pacakge, or any code snippets in it, in +# your own projects. Hopefully they will be helpful to you! +# +# This project is Licenced under The MIT License (MIT) +# +# Copyright (c) 2013 Garrett Berg cloudformdesign.com +# An updated version of this file can be found at: +# +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +# http://opensource.org/licenses/MIT + +# import pdb +import os + +from PyQt4 import QtCore, QtGui +import sys +#import icons_rc + + +# from cloudtb import dbe + +class Node(object): + '''A general node stucture to be used in treeview +the attrib_dict can store any information your overall treeview +needs it to store. +''' + def __init__(self, name, parent=None, icon = None, + attrib_dict = None): + + self._name = name + self._attrib = attrib_dict + self._children = [] + self._parent = parent + self.icon = icon + + if parent is not None: + parent.addChild(self) + + def addChild(self, child): + self._children.append(child) + + def insertChild(self, position, child): + + if position < 0 or position > len(self._children): + return False + + self._children.insert(position, child) + child._parent = self + return True + + def removeChild(self, position): + + if position < 0 or position > len(self._children): + return False + child = self._children.pop(position) + child._parent = None + + return True + + def name(self): + return self._name + + def setName(self, name): + self._name = name + + def child(self, row): + return self._children[row] + + def childCount(self): + return len(self._children) + + def parent(self): + return self._parent + + def row(self): + if self._parent is not None: + return self._parent._children.index(self) + + + def log(self, tabLevel=-1): + + output = "" + tabLevel += 1 + + for i in range(tabLevel): + output += " " + + output += "|-" + self._name + "\n" + + for child in self._children: + output += child.log(tabLevel) + + tabLevel -= 1 +# output += "\n" + + return output + + def __repr__(self): + return self.log() + +class TreeViewModel(QtCore.QAbstractItemModel): + """INPUTS: Node, QObject""" + def __init__(self, root, parent=None, header_title = None): + super(TreeViewModel, self).__init__(parent) + self.is_editable = False + self.is_selectable = True + self.is_enabled = True + self.set_flags() + self._rootNode = root + self.header_title = header_title + + # The following are created functions called in "data" -- which is a + # Qt defined funciton. This way of doing things is FAR more pythonic + # and allows classes to inherit this one and not have to rewrite the + # entire data method + # all of them recieve an index and a node + + def role_display(self, index, node): + if index.column() == 0: + return node.name() + + def role_edit(self, index, node): + return self.role_display(index, node) + + def role_tool_tip(self, index, node): + return + + def role_check_state(self, index, node): + return + + def role_decoration(self, index, node): + if index.column() == 0: + icon = node.icon + if icon == None: + return False + else: + return icon + + def role_flags(self, index, node): + '''While not technically a "role" it behaves in much the same way. +This method is called by the "flags" method for all indexes with +a node''' + return self.BASE_FLAGS + + def set_flags(self, is_editable = None, is_selectable = None, + is_enabled = None): + ''' Sets new flags to the BASE_FLAGS variable''' + if is_editable != None: + self.is_editable = is_editable + if is_selectable != None: + self.is_selectable = is_selectable + if is_enabled != None: + self.is_enabled = is_enabled + self.BASE_FLAGS = QtCore.Qt.ItemFlags( + (QtCore.Qt.ItemIsEnabled * bool(self.is_enabled)) + | (QtCore.Qt.ItemIsSelectable * bool(self.is_selectable)) + | (QtCore.Qt.ItemIsEditable * bool(self.is_editable)) + ) + + """INPUTS: QModelIndex""" + """OUTPUT: int""" + def rowCount(self, parent): + if not parent.isValid(): + parentNode = self._rootNode + else: + parentNode = parent.internalPointer() + + return parentNode.childCount() + + """INPUTS: QModelIndex""" + """OUTPUT: int""" + def columnCount(self, parent): + return 1 + + """INPUTS: QModelIndex, int""" + """OUTPUT: QVariant, strings are cast to QString which is a QVariant""" + def data(self, index, role): + '''index is an object that contains a pointer to the item inside +internPointer(). Note that this was set during the insertRows +method call, so you don't need to track them! +''' + if not index.isValid(): + return None + + node = index.internalPointer() + + if role == QtCore.Qt.EditRole: + return self.role_edit(index, node) + + if role == QtCore.Qt.ToolTipRole: + return self.role_tool_tip(index, node) + + if role == QtCore.Qt.CheckStateRole: + return self.role_check_state(index, node) + + if role == QtCore.Qt.DisplayRole: + return self.role_display(index, node) + + if role == QtCore.Qt.DecorationRole: + return self.role_decoration(index, node) + + """INPUTS: QModelIndex, QVariant, int (flag)""" + def setData(self, index, value, role=QtCore.Qt.EditRole): + if index.isValid(): + if role == QtCore.Qt.EditRole: + node = index.internalPointer() + node.setName(value) + return True + return False + + """INPUTS: int, Qt::Orientation, int""" + """OUTPUT: QVariant, strings are cast to QString which is a QVariant""" + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole: + if self.header_title != None: + return self.header_title + if section == 0: + return "Scenegraph" + else: + return "Typeinfo" + + """INPUTS: QModelIndex""" + """OUTPUT: int (flag)""" +# def flags(self, index): +# return (QtCore.Qt.ItemIsEnabled | +# QtCore.Qt.ItemIsSelectable #| +## QtCore.Qt.ItemIsEditable +# ) + def flags(self, index): + if not index.isValid(): + return self.BASE_FLAGS + + node = index.internalPointer() + return self.role_flags(index, node) + + """INPUTS: QModelIndex""" + """OUTPUT: QModelIndex""" + """Should return the parent of the node with the given QModelIndex""" + def parent(self, index): + node = self.getNode(index) + parentNode = node.parent() + + if parentNode == self._rootNode: + return QtCore.QModelIndex() + return self.createIndex(parentNode.row(), 0, parentNode) + + """INPUTS: int, int, QModelIndex""" + """OUTPUT: QModelIndex""" + """Should return a QModelIndex that corresponds to the given row, +column and parent node""" + def index(self, row, column, parent): + # This is how Qt creates the nested (tree) list. It knows how many + # rows it has because of insertRows, and it uses index and + # createIndex to build the tree. +# print 'Index called', row, column + parentNode = self.getNode(parent) + childItem = parentNode.child(row) + if childItem: + return self.createIndex(row, column, childItem) + else: + return QtCore.QModelIndex() + + """CUSTOM""" + """INPUTS: QModelIndex""" + def getNode(self, index): + if index.isValid(): + node = index.internalPointer() + if node: + return node + + return self._rootNode + + + """INPUTS: int, List of Nodes, QModelIndex""" + def insertRows(self, position, rows, parent=QtCore.QModelIndex()): + parentNode = self.getNode(parent) + + self.beginInsertRows(parent, position, position + len(rows) - 1) + + for i, row in enumerate(rows): +# childCount = parentNode.childCount() + childNode = row + success = parentNode.insertChild(position + i, childNode) + + self.endInsertRows() + + return success + + """INPUTS: int, int, QModelIndex""" + def removeRows(self, position, rows, parent=QtCore.QModelIndex()): + if rows == 0: + return + + parentNode = self.getNode(parent) + self.beginRemoveRows(parent, position, position + rows - 1) + + for row in range(rows): + success = parentNode.removeChild(position) + # TODO: break if not success? + + self.endRemoveRows() + + return success + + def clear_rows(self): + return self.removeRows(0, self._rootNode.childCount()) + +# TODO: doesn't work. Not sure how to get icons +ICON_FOLDER = QtGui.QIcon.fromTheme('folder') + +def _node_compare(a, b): + return b.isdir - a.isdir + +def get_file_folder_node(fdata, parent): + '''return the node structure of the data. +[[(dir_name, path), +[dir_name, path), +[(file, path), +(file, path)]] +] +] +''' + # TODO: set icons correctly + + nodes = [] + for fobj in fdata: + path = fobj[0] + name = os.path.split(path)[1] + + if len(fobj) == 1: + fileobj = Node(name, parent = parent, icon = None) + fileobj.full_path = path + fileobj.isdir = False + nodes.append(fileobj) + continue + folderobj = Node(name, parent = parent, icon = ICON_FOLDER, + ) + folderobj.full_path = path + folderobj.isdir = True + + get_file_folder_node(fobj[1], parent = folderobj) + nodes.append(folderobj) + nodes.sort(cmp = _node_compare) + return nodes +import itertools + +def _get_filelist_nodes(iter_file_list, dir_path = ''): + '''Takes a sorted file list iterator and returns the files in a +format that can be converted''' + files = [] + dir_path = os.path.join(dir_path, '') # Put into directory syntax + len_dp = len(dir_path) + while True: + try: + fpath = next(iter_file_list) + except StopIteration: + break + if dir_path != fpath[:len_dp]: + iter_file_list = itertools.chain((fpath,), iter_file_list) + break + + if os.path.isdir(fpath): + iter_file_list, new_files = _get_filelist_nodes(iter_file_list, + dir_path = fpath) + files.append((fpath, new_files)) + else: + files.append((fpath,)) + return iter_file_list, files + +def get_filelist_nodes(file_list, parent = None): + file_list = sorted(file_list) + file_tuples = _get_filelist_nodes(iter(file_list))[1] + return get_file_folder_node(file_tuples, parent) + +def dev_show_file_list(file_objects): + '''For developemnet''' + + app = QtGui.QApplication(sys.argv) + + rootNode = Node("Rootdir") + model = TreeViewModel(rootNode) + + treeView = QtGui.QTreeView() + treeView.show() + + treeView.setModel(model) + model.insertRows(0, file_objects, QtCore.QModelIndex()) + sys.exit(app.exec_()) + + +if __name__ == '__main__': + from pprint import pprint + + files = '''/home/user/Projects/Learning/LearningQt/LearningQt.pro.user +/home/user/Projects/Learning/LearningQt/LearningQt.pro +/home/user/Projects/Learning/LearningQt/qmlapplicationviewer/qmlapplicationviewer.h +/home/user/Projects/Learning/LearningQt/qmlapplicationviewer/qmlapplicationviewer.cpp +/home/user/Projects/Learning/LearningQt/qmlapplicationviewer/qmlapplicationviewer.pri +/home/user/Projects/Learning/LearningQt/qmlapplicationviewer +/home/user/Projects/Learning/LearningQt/LearningQt64.png +/home/user/Projects/Learning/LearningQt/LearningQt_harmattan.desktop +/home/user/Projects/Learning/LearningQt/LearningQt.svg +/home/user/Projects/Learning/LearningQt/main.cpp +/home/user/Projects/Learning/LearningQt/LearningQt.desktop +/home/user/Projects/Learning/LearningQt/qml/LearningQt/main.qml +/home/user/Projects/Learning/LearningQt/qml/LearningQt +/home/user/Projects/Learning/LearningQt/qml +/home/user/Projects/Learning/LearningQt/LearningQt80.png''' + nodes = get_filelist_nodes(files.split('\n')) + for n in nodes: + print n + dev_show_file_list(nodes) + \ No newline at end of file diff --git a/gui/viewer.py b/gui/viewer.py index 0980948..49ded97 100644 --- a/gui/viewer.py +++ b/gui/viewer.py @@ -7,6 +7,19 @@ import sys from PyQt4 import QtCore, QtGui, QtWebKit +from treeview import TreeViewModel, Node + +class MenuTree(QtGui.QTreeView): + def __init__(self, *args, **kwargs): + QtGui.QTreeView.__init__(self, *args, **kwargs) + self.test_tree() + + def test_tree(self): + self.rootNode = Node("Rootdir") + model = TreeViewModel(self.rootNode) + self.setModel(model) + self.rootNode.addChild(Node('Hey')) + class Browser(QtGui.QMainWindow): @@ -41,9 +54,14 @@ class Browser(QtGui.QMainWindow): self.horizontalLayout.addWidget(self.bt_ahead) self.horizontalLayout.addWidget(self.tb_url) self.gridLayout.addLayout(self.horizontalLayout) - + + self.horizontalMainLayout = QtGui.QHBoxLayout() + self.gridLayout.addLayout(self.horizontalMainLayout) + # + self.menu = MenuTree() self.html = QtWebKit.QWebView() - self.gridLayout.addWidget(self.html) + self.horizontalMainLayout.addWidget(self.menu) + self.horizontalMainLayout.addWidget(self.html) self.mainLayout.addWidget(self.frame) self.setCentralWidget(self.centralwidget) diff --git a/logs/base.py b/logs/base.py new file mode 100644 index 0000000..a4ae7c0 --- /dev/null +++ b/logs/base.py @@ -0,0 +1,9 @@ + + +class Log(object): + matcher = None + + @classmethod + def is_handler(cls, log): + return False + \ No newline at end of file diff --git a/logs/combat.py b/logs/combat.py index ede4acd..8673003 100644 --- a/logs/combat.py +++ b/logs/combat.py @@ -1,162 +1,157 @@ -""" - todo: - - English implementation first. - - parsing combat.log - - Prosa. - All logs start with something like - 23:53:29.137 | LOGDATA - - LOGDATA can be quite different depending on the logfile. - - other forms encountered: - 23:54:00.600 WARNING| - - combat logs: - 01:04:38.805 CMBT | - - - The typical log entry -""" -import re - -class Log(object): - matcher = None - - @classmethod - def is_handler(cls, log): - return False - -class CombatLog(Log): - @classmethod - def _log_handler(cls, log): - if log.get('log', '').strip().startswith(cls.__name__): - return True - return False - - @classmethod - def is_handler(cls, log): - if log.get('logtype', None) == 'CMBT': - return cls._log_handler(log) - return False - - def __init__(self, values=None): - self.values = values - - def unpack(self): - # unpacks the data from the values. - if hasattr(self, 'matcher') and self.matcher: - matchers = self.matcher - if not isinstance(matchers, list): - matchers = [matchers,] - for matcher in matchers: - m = matcher.match(self.values.get('log', '')) - if m: - self.values.update(m.groupdict()) - return True - -# @todo: where does this come from? -class Action(CombatLog): - pass - -class Gameplay(CombatLog): - matcher = [ - # usual: team(reason). explained reason. - re.compile(r"^Gameplay\sfinished\.\sWinner\steam\:\s+(?P\d+)\((?P\w+)\)\.\sFinish\sreason\:\s'(?P[^']+)'\.\sActual\sgame\stime\s+(?P\d+|\d+\.\d+)\ssec"), - # team, unexplained reason (unknown, Timeout) - re.compile(r"^Gameplay\sfinished\.\sWinner\steam\:\s+(?P\d+).\sFinish\sreason\:\s'(?P[^']+)'\.\sActual\sgame\stime\s+(?P\d+|\d+\.\d+)\ssec"), - ] - -class Apply(CombatLog): # Apply Aura. - matcher = re.compile(r"^Apply\saura\s'(?P\w+)'\sid\s(?P\d+)\stype\s(?P\w+)\sto\s'(?P[^\']+)'") - -class Damage(CombatLog): - matcher = re.compile(r"^Damage\s+(?P[^\s]+)\s\->\s+(?P[^\s]+)\s+(?P(?:\d+|\d+\.\d+))(?:\s(?P[^\s]+)\s|\s{2,2})(?P(?:\w|\|)+)") - -class Spawn(CombatLog): - matcher = re.compile(r"^Spawn\sSpaceShip\sfor\splayer(?P\d+)\s\((?P[^,]+),\s+(?P#\w+)\)\.\s+'(?P\w+)'") - -class Spell(CombatLog): - matcher = re.compile(r"^Spell\s'(?P\w+)'\sby\s+(?P.*)(?:\((?P\w+)\)|)\stargets\((?P\d+)\)\:(?:$|\s(?P.+))") - -class Reward(CombatLog): - matcher = re.compile(r"^Reward\s+(?P[^\s]+)(?:\s(?P\w+)\s+|\s+)(?P\d+)\s(?P.*)\s+for\s(?P.*)") - -class Participant(CombatLog): - matcher = re.compile(r"^\s+Participant\s+(?P[^\s]+)(?:\s{2}(?P\w+)|\s{30,})\s+(?:totalDamage\s(?P(?:\d+|\d+\.\d+));\smostDamageWith\s'(?P[^']+)';(?P.*)|<(?P\w+)>)") - -class Rocket(CombatLog): - matcher = re.compile(r"^Rocket\s(?Plaunch|detonation)\.\sowner\s'(?P[^']+)'(?:,\s(?:def\s'(?P\w+)'|target\s'(?P[^']+)'|reason\s'(?P\w+)'|directHit\s'(?P[^']+)'))+") - -class Heal(CombatLog): - matcher = [ - # heal by module - re.compile(r"^Heal\s+(?P[^\s]+)\s\->\s+(?P[^\s]+)\s+(?P(?:\d+|\d+\.\d+))\s(?P[^\s]+)"), - # direct heal by source or n/a (global buff) - re.compile(r"^Heal\s+(?:n/a|(?P\w+))\s+\->\s+(?P[^\s]+)\s+(?P(?:\d+|\d+\.\d+))"), - ] - -class Killed(CombatLog): - matcher = [ - re.compile(r"^Killed\s(?P[^\s]+)\s+(?P\w+);\s+killer\s(?P[^\s]+)\s*"), - re.compile(r"^Killed\s(?P[^\(]+)\((?P\w+)\);\s+killer\s(?P[^\s]+)\s*"), - re.compile(r"^Killed\s(?P[^\;]+);\s+killer\s(?P[^\s]+)\s+.*"), - ] - -class Captured(CombatLog): - matcher = re.compile(r"^Captured\s'(?P[^']+)'\(team\s(?P\d+)\)\.(?:\sAttackers\:(?P.*)|.*)") - -class AddStack(CombatLog): - matcher = re.compile(r"^AddStack\saura\s'(?P\w+)'\sid\s(?P\d+)\stype\s(?P\w+)\.\snew\sstacks\scount\s(?P\d+)") - -class Cancel(CombatLog): - matcher = re.compile(r"^Cancel\saura\s'(?P\w+)'\sid\s(?P\d+)\stype\s(?P\w+)\sfrom\s'(?P[^']+)'") - -class Scores(CombatLog): - matcher = re.compile(r"^Scores\s+-\sTeam1\((?P(?:\d+|\d+\.\d+))\)\sTeam2\((?P(?:\d+|\d+\.\d+))\)") - -# Special classes -class GameEvent(CombatLog): - matcher = [ - # game session identifier. - re.compile(r"^Connect\sto\sgame\ssession\s+(?P\d+)"), - # start gameplay identifier. - re.compile(r"^Start\sgameplay\s'(?P\w+)'\smap\s+'(?P\w+)',\slocal\sclient\steam\s(?P\d+)"), - # pve mission identifier. - re.compile(r"^Start\sPVE\smission\s'(?P\w+)'\smap\s+'(?P\w+)'"), - ] - - @classmethod - def _log_handler(cls, log): - if log.get('log', '').strip().startswith('======='): - return True - return False - - def unpack(self): - # unpacks the data from the values. - if hasattr(self, 'matcher') and self.matcher: - matchers = self.matcher - if not isinstance(matchers, list): - matchers = [matchers,] - for matcher in matchers: - m = matcher.match(self.values.get('log', '').strip('=').strip()) - if m: - self.values.update(m.groupdict()) - return True - -class UserEvent(CombatLog): - """ special class for combat logs that might be associated with the playing player """ - @classmethod - def _log_handler(cls, log): - if log.get('log', '').strip(): - return True - return False - -# Action? -COMBAT_LOGS = [ Apply, Damage, Spawn, Spell, Reward, Participant, Rocket, Heal, - Gameplay, #? - Scores, - Killed, Captured, AddStack, Cancel, - GameEvent, UserEvent - ] - +""" + todo: + - English implementation first. + - parsing combat.log + + Prosa. + All logs start with something like + 23:53:29.137 | LOGDATA + + LOGDATA can be quite different depending on the logfile. + + other forms encountered: + 23:54:00.600 WARNING| + + combat logs: + 01:04:38.805 CMBT | + + + The typical log entry +""" +import re +from base import Log + +class CombatLog(Log): + @classmethod + def _log_handler(cls, log): + if log.get('log', '').strip().startswith(cls.__name__): + return True + return False + + @classmethod + def is_handler(cls, log): + if log.get('logtype', None) == 'CMBT': + return cls._log_handler(log) + return False + + def __init__(self, values=None): + self.values = values + + def unpack(self): + # unpacks the data from the values. + if hasattr(self, 'matcher') and self.matcher: + matchers = self.matcher + if not isinstance(matchers, list): + matchers = [matchers,] + for matcher in matchers: + m = matcher.match(self.values.get('log', '')) + if m: + self.values.update(m.groupdict()) + return True + +# @todo: where does this come from? +class Action(CombatLog): + pass + +class Gameplay(CombatLog): + matcher = [ + # usual: team(reason). explained reason. + re.compile(r"^Gameplay\sfinished\.\sWinner\steam\:\s+(?P\d+)\((?P\w+)\)\.\sFinish\sreason\:\s'(?P[^']+)'\.\sActual\sgame\stime\s+(?P\d+|\d+\.\d+)\ssec"), + # team, unexplained reason (unknown, Timeout) + re.compile(r"^Gameplay\sfinished\.\sWinner\steam\:\s+(?P\d+).\sFinish\sreason\:\s'(?P[^']+)'\.\sActual\sgame\stime\s+(?P\d+|\d+\.\d+)\ssec"), + ] + +class Apply(CombatLog): # Apply Aura. + matcher = re.compile(r"^Apply\saura\s'(?P\w+)'\sid\s(?P\d+)\stype\s(?P\w+)\sto\s'(?P[^\']+)'") + +class Damage(CombatLog): + matcher = re.compile(r"^Damage\s+(?P[^\s]+)\s\->\s+(?P[^\s]+)\s+(?P(?:\d+|\d+\.\d+))(?:\s(?P[^\s]+)\s|\s{2,2})(?P(?:\w|\|)+)") + +class Spawn(CombatLog): + matcher = re.compile(r"^Spawn\sSpaceShip\sfor\splayer(?P\d+)\s\((?P[^,]+),\s+(?P#\w+)\)\.\s+'(?P\w+)'") + +class Spell(CombatLog): + matcher = re.compile(r"^Spell\s'(?P\w+)'\sby\s+(?P.*)(?:\((?P\w+)\)|)\stargets\((?P\d+)\)\:(?:$|\s(?P.+))") + +class Reward(CombatLog): + matcher = re.compile(r"^Reward\s+(?P[^\s]+)(?:\s(?P\w+)\s+|\s+)(?P\d+)\s(?P.*)\s+for\s(?P.*)") + +class Participant(CombatLog): + matcher = re.compile(r"^\s+Participant\s+(?P[^\s]+)(?:\s{2}(?P\w+)|\s{30,})\s+(?:totalDamage\s(?P(?:\d+|\d+\.\d+));\smostDamageWith\s'(?P[^']+)';(?P.*)|<(?P\w+)>)") + +class Rocket(CombatLog): + matcher = re.compile(r"^Rocket\s(?Plaunch|detonation)\.\sowner\s'(?P[^']+)'(?:,\s(?:def\s'(?P\w+)'|target\s'(?P[^']+)'|reason\s'(?P\w+)'|directHit\s'(?P[^']+)'))+") + +class Heal(CombatLog): + matcher = [ + # heal by module + re.compile(r"^Heal\s+(?P[^\s]+)\s\->\s+(?P[^\s]+)\s+(?P(?:\d+|\d+\.\d+))\s(?P[^\s]+)"), + # direct heal by source or n/a (global buff) + re.compile(r"^Heal\s+(?:n/a|(?P\w+))\s+\->\s+(?P[^\s]+)\s+(?P(?:\d+|\d+\.\d+))"), + ] + +class Killed(CombatLog): + matcher = [ + re.compile(r"^Killed\s(?P[^\s]+)\s+(?P\w+);\s+killer\s(?P[^\s]+)\s*"), + re.compile(r"^Killed\s(?P[^\(]+)\((?P\w+)\);\s+killer\s(?P[^\s]+)\s*"), + re.compile(r"^Killed\s(?P[^\;]+);\s+killer\s(?P[^\s]+)\s+.*"), + ] + +class Captured(CombatLog): + matcher = re.compile(r"^Captured\s'(?P[^']+)'\(team\s(?P\d+)\)\.(?:\sAttackers\:(?P.*)|.*)") + +class AddStack(CombatLog): + matcher = re.compile(r"^AddStack\saura\s'(?P\w+)'\sid\s(?P\d+)\stype\s(?P\w+)\.\snew\sstacks\scount\s(?P\d+)") + +class Cancel(CombatLog): + matcher = re.compile(r"^Cancel\saura\s'(?P\w+)'\sid\s(?P\d+)\stype\s(?P\w+)\sfrom\s'(?P[^']+)'") + +class Scores(CombatLog): + matcher = re.compile(r"^Scores\s+-\sTeam1\((?P(?:\d+|\d+\.\d+))\)\sTeam2\((?P(?:\d+|\d+\.\d+))\)") + +# Special classes +class GameEvent(CombatLog): + matcher = [ + # game session identifier. + re.compile(r"^Connect\sto\sgame\ssession\s+(?P\d+)"), + # start gameplay identifier. + re.compile(r"^Start\sgameplay\s'(?P\w+)'\smap\s+'(?P\w+)',\slocal\sclient\steam\s(?P\d+)"), + # pve mission identifier. + re.compile(r"^Start\sPVE\smission\s'(?P\w+)'\smap\s+'(?P\w+)'"), + ] + + @classmethod + def _log_handler(cls, log): + if log.get('log', '').strip().startswith('======='): + return True + return False + + def unpack(self): + # unpacks the data from the values. + # small override to remove trailing "="s in the matching. + if hasattr(self, 'matcher') and self.matcher: + matchers = self.matcher + if not isinstance(matchers, list): + matchers = [matchers,] + for matcher in matchers: + m = matcher.match(self.values.get('log', '').strip('=').strip()) + if m: + self.values.update(m.groupdict()) + return True + +class UserEvent(CombatLog): + """ special class for combat logs that might be associated with the playing player """ + @classmethod + def _log_handler(cls, log): + if log.get('log', '').strip(): + return True + return False + +# Action? +COMBAT_LOGS = [ Apply, Damage, Spawn, Spell, Reward, Participant, Rocket, Heal, + Gameplay, #? + Scores, + Killed, Captured, AddStack, Cancel, + GameEvent, UserEvent + ] + diff --git a/logs/game.py b/logs/game.py new file mode 100644 index 0000000..0e86b97 --- /dev/null +++ b/logs/game.py @@ -0,0 +1,4 @@ + + + +GAME_LOGS = [] \ No newline at end of file diff --git a/logs/logfile.py b/logs/logfile.py index 6211b70..18e8305 100644 --- a/logs/logfile.py +++ b/logs/logfile.py @@ -1,78 +1,82 @@ -# -""" - Author: Gabor Guzmics, 2013-2014 - - LogFile is an object capable to load SCon Logfiles and parse their ingredients - It can be extended by overriding resolve to understand Logentries further. - Each Logfile represents a physical file parsed, however theoretically, you can also parse arbitrary - data by setting the LogFile._data yourself. -""" -import re -RE_SCLOG = r'^(?P\d{2,2})\:(?P\d{2,2})\:(?P\d{2,2})\.(?P\d{3,3})\s(?P\s*[^\|\s]+\s*|\s+)\|\s(?P.*)' -R_SCLOG = re.compile(RE_SCLOG) - -class LogFile(object): - def __init__(self, fname=None, - folder=None): - self.fname = fname - self.folder = folder # only for custom tagging. - self.lines = [] - self._data = None - if self.fname is not None: - self.open(self.fname) - - def open(self, fname): - f = open(fname, 'r') - self._data = f.read() - f.close() - - def parse(self): - # parse _data if we still have no lines. - if self._data: - data_lines = self._data.replace('\r', '\n').replace('\n\n', '\n').split('\n') - lines = [] - for line in data_lines: - if not line: - continue - elif not isinstance(line, basestring): - lines.append(line) - continue - elif line.startswith('---'): - continue - else: - # get the timecode & logtype - m = R_SCLOG.match(line) - if m: - g = m.groupdict() - if 'logtype' in g.keys(): - g['logtype'] = g['logtype'].strip() - lines.append(g) - else: - lines.append(line) - self.lines = lines - # try to identify (resolve) lines. - if self.lines: - lines = [] - for line in self.lines: - l = line - if isinstance(line, basestring): - # Unknown Log? - pass - elif isinstance(line, dict): - # Unresolved Log. - l = self.resolve(line) - elif line is None: - # dafuq? - pass - else: - # might be an object? - pass - lines.append(l) - - self.lines = lines - - def resolve(self, line): - # line is a dict. - # try to find a class that is responsible for this log. - return line - +# +""" + Author: Gabor Guzmics, 2013-2014 + + LogFile is an object capable to load SCon Logfiles and parse their ingredients + It can be extended by overriding resolve to understand Logentries further. + Each Logfile represents a physical file parsed, however theoretically, you can also parse arbitrary + data by setting the LogFile._data yourself. +""" +import re +RE_SCLOG = r'^(?P\d{2,2})\:(?P\d{2,2})\:(?P\d{2,2})\.(?P\d{3,3})\s(?P\s*[^\|\s]+\s*|\s+)\|\s(?P.*)' +R_SCLOG = re.compile(RE_SCLOG) + +class LogFile(object): + def __init__(self, fname=None, + folder=None): + self.fname = fname + self.folder = folder # only for custom tagging. + self.lines = [] + self._data = None + + def read(self, fname=None): + fname = fname or self.fname + try: + f = open(fname, 'r') + self._data = f.read() + finally: + f.close() + + def set_data(self, data): + self._data = data + + def parse(self): + # parse _data if we still have no lines. + if self._data: + data_lines = self._data.replace('\r', '\n').replace('\n\n', '\n').split('\n') + lines = [] + for line in data_lines: + if not line: + continue + elif not isinstance(line, basestring): + lines.append(line) + continue + elif line.startswith('---'): + continue + else: + # get the timecode & logtype + m = R_SCLOG.match(line) + if m: + g = m.groupdict() + if 'logtype' in g.keys(): + g['logtype'] = g['logtype'].strip() + lines.append(g) + else: + lines.append(line) + self.lines = lines + # try to identify (resolve) lines. + if self.lines: + lines = [] + for line in self.lines: + l = line + if isinstance(line, basestring): + # Unknown Log? + pass + elif isinstance(line, dict): + # Unresolved Log. + l = self.resolve(line) + elif line is None: + # dafuq? + pass + else: + # might be an object? + pass + lines.append(l) + + self.lines = lines + + def resolve(self, line): + # line is a dict. + # try to find a class that is responsible for this log. + return line + diff --git a/logs/logfiles.py b/logs/logfiles.py new file mode 100644 index 0000000..32574f5 --- /dev/null +++ b/logs/logfiles.py @@ -0,0 +1,38 @@ +""" + Resolves Logs. +""" + +from logfile import LogFile +from combat import COMBAT_LOGS +from game import GAME_LOGS + +class LogFileResolver(LogFile): + ''' dynamic logfile resolver ''' + resolution_classes = COMBAT_LOGS + + def __init__(self, *args, **kwargs): + super(LogFileResolver, self).__init__(*args, **kwargs) + self.resolution_classes = self.resolution_classes or [] + + def resolve(self, line): + for klass in self.resolution_classes: + if klass.is_handler(line): + return klass(line) + return line + +class CombatLogFile(LogFile): + ''' Combat Log ''' + def resolve(self, line): + for klass in COMBAT_LOGS: + if klass.is_handler(line): + return klass(line) + return line + +class GameLogFile(LogFile): + ''' Game Log ''' + def resolve(self, line): + for klass in GAME_LOGS: + if klass.is_handler(line): + return klass(line) + return line + diff --git a/logs/logresolver.py b/logs/logresolver.py deleted file mode 100644 index e20a537..0000000 --- a/logs/logresolver.py +++ /dev/null @@ -1,19 +0,0 @@ -""" - Resolves Logs. -""" - -from logfile import LogFile -from combat import COMBAT_LOGS - -class LogFileResolver(LogFile): - resolution_classes = COMBAT_LOGS - - def __init__(self, *args, **kwargs): - super(LogFileResolver, self).__init__(*args, **kwargs) - self.resolution_classes = self.resolution_classes or [] - - def resolve(self, line): - for klass in self.resolution_classes: - if klass.is_handler(line): - return klass(line) - return line diff --git a/logs/session.py b/logs/session.py new file mode 100644 index 0000000..706985a --- /dev/null +++ b/logs/session.py @@ -0,0 +1,70 @@ +""" + Logging Session. +""" +import zipfile, logging, os +from logfiles import CombatLogFile, GameLogFile + +class LogSession(object): + """ + The Log-Session is supposed to save one directory of logs. + It can parse its logs, and build up its internal structure into Battle Instances etc. + """ + + def __init__(self, directory): + ''' if directory is a file, it will be handled as a compressed folder ''' + self.battles = [] + self.user = None + + # various logfiles used. + self.combat_log = None + self.game_log = None + self.chat_log = None + # self.net_log = None + + self.directory = directory + self._zip_source = False + + def parse_files(self): + ''' parses the logfiles ''' + # check if directory is a file + self._zip_source = os.path.isfile(self.directory) or False + if self._zip_source: + self._unzip_logs() + else: + self.combat_log = CombatLogFile(os.path.join(self.directory, 'combat.log')) + self.combat_log.read() + self.game_log = GameLogFile(os.path.join(self.directory, 'game.log')) + self.game_log.read() + # parse all files + self.combat_log.parse() + self.game_log.parse() + + def determine_owner(self): + ''' determines the user in the parsed gamelog ''' + pass + + def parse_battles(self): + ''' parses the battles ''' + pass + + def _unzip_logs(self): + z = zipfile.ZipFile(self.directory, "r") + for filename in z.namelist(): + fn = os.path.split(filename)[1] or '' + fn = fn.lower() + if fn: + if fn == 'combat.log': + self.combat_log = CombatLogFile(fn) + self.combat_log.set_data(z.read(filename)) + elif fn == 'game.log': + self.game_log = GameLogFile(fn) + self.game_log.set_data(z.read(filename)) + + + +if __name__ == '__main__': + l_raw = LogSession('D:\\Users\\g4b\\Documents\\My Games\\sc\\2014.05.17 15.50.28') + l_zip = LogSession('D:\\Users\\g4b\\Documents\\My Games\\sc\\2014.05.20 23.49.19.zip') + + l_zip.parse_files() + print l_zip.combat_log.lines \ No newline at end of file