#!/usr/bin/env python3 #=============================================================================== # Copyright (C) 2010-2011 Anton Vorobyov # # This file is part of eos. # # eos is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # eos is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with eos. If not, see . #=============================================================================== """ This script is used to compare two different database versions. It shows removed/changed/new items with list of changed effects, changed attributes and effects which were renamed """ import argparse import os.path import re import sqlite3 script_dir = os.path.dirname(__file__) default_old = os.path.join(script_dir, "..", "eve.db") def main(old, new, groups=True, effects=True, attributes=True, renames=True): # Open both databases and get their cursors old_db = sqlite3.connect(os.path.expanduser(old)) old_cursor = old_db.cursor() new_db = sqlite3.connect(os.path.expanduser(new)) new_cursor = new_db.cursor() # Force some of the items to make them published FORCEPUB_TYPES = ("Ibis", "Impairor", "Velator", "Reaper", "Amarr Tactical Destroyer Propulsion Mode", "Amarr Tactical Destroyer Sharpshooter Mode", "Amarr Tactical Destroyer Defense Mode") OVERRIDES_TYPEPUB = 'UPDATE invtypes SET published = 1 WHERE typeName = ?' for typename in FORCEPUB_TYPES: old_cursor.execute(OVERRIDES_TYPEPUB, (typename,)) new_cursor.execute(OVERRIDES_TYPEPUB, (typename,)) # Initialization of few things used by both changed/renamed effects list script_dir = os.path.dirname(__file__) effectspath = os.path.join(script_dir, "..", "eos", "effects.py") implemented = set() with open(effectspath) as f: for line in f: for m in re.finditer(r'class Effect(?P\d+)\(BaseEffect\):', line): effectid = int(m.group('eid')) implemented.add(effectid) # Effects' names are used w/o any special symbols by eos stripspec = "[^A-Za-z0-9]" # Method to get data if effect is implemented in eos or not def geteffst(effectid): return effectid in implemented def findrenames(ren_dict, query, strip=False): old_namedata = {} new_namedata = {} for cursor, dictionary in ((old_cursor, old_namedata), (new_cursor, new_namedata)): cursor.execute(query) for row in cursor: id = row[0] name = row[1] if strip is True: name = re.sub(stripspec, "", name) dictionary[id] = name for id in set(old_namedata.keys()).intersection(list(new_namedata.keys())): oldname = old_namedata[id] if old_namedata[id] is not None else 'None' newname = new_namedata[id] if new_namedata[id] is not None else 'None' if oldname != newname: ren_dict[id] = (oldname, newname) return def printrenames(ren_dict, title): if len(ren_dict) > 0: print('\nRenamed ' + title + ':') for id in sorted(ren_dict): couple = ren_dict[id] print((" \"{0}\": \"{1}\",".format(couple[0], couple[1]))) groupcats = {} def getgroupcat(grp): """Get group category from the new db""" if grp in groupcats: cat = groupcats[grp] else: query = 'SELECT categoryID FROM invgroups WHERE groupID = ?' new_cursor.execute(query, (grp,)) cat = 0 for row in new_cursor: cat = row[0] groupcats[grp] = cat return cat itemnames = {} def getitemname(item): """Get item name from the new db""" if item in itemnames: name = itemnames[item] else: query = 'SELECT typeName FROM invtypes WHERE typeID = ?' new_cursor.execute(query, (item,)) name = "" for row in new_cursor: name = row[0] if not name: old_cursor.execute(query, (item,)) for row in old_cursor: name = row[0] itemnames[item] = name return name groupnames = {} def getgroupname(grp): """Get group name from the new db""" if grp in groupnames: name = groupnames[grp] else: query = 'SELECT name FROM invgroups WHERE groupID = ?' new_cursor.execute(query, (grp,)) name = "" for row in new_cursor: name = row[0] if not name: old_cursor.execute(query, (grp,)) for row in old_cursor: name = row[0] groupnames[grp] = name return name effectnames = {} def geteffectname(effect): """Get effect name from the new db""" if effect in effectnames: name = effectnames[effect] else: query = 'SELECT effectName FROM dgmeffects WHERE effectID = ?' new_cursor.execute(query, (effect,)) name = "" for row in new_cursor: name = row[0] if not name: old_cursor.execute(query, (effect,)) for row in old_cursor: name = row[0] effectnames[effect] = name return name attrnames = {} def getattrname(attr): """Get attribute name from the new db""" if attr in attrnames: name = attrnames[attr] else: query = 'SELECT attributeName FROM dgmattribs WHERE attributeID = ?' new_cursor.execute(query, (attr,)) name = "" for row in new_cursor: name = row[0] if not name: old_cursor.execute(query, (attr,)) for row in old_cursor: name = row[0] attrnames[attr] = name return name # State table S = {"unchanged": 0, "removed": 1, "changed": 2, "added": 3 } if effects or attributes or groups: # Format: # Key: item id # Value: [groupID, set(effects), {attribute id : value}] old_itmdata = {} new_itmdata = {} for cursor, dictionary in ((old_cursor, old_itmdata), (new_cursor, new_itmdata)): # Compose list of items we're interested in, filtered by category query = 'SELECT it.typeID, it.groupID FROM invtypes AS it INNER JOIN invgroups AS ig ON it.groupID = ig.groupID INNER JOIN invcategories AS ic ON ig.categoryID = ic.categoryID WHERE it.published = 1 AND ic.name IN ("Ship", "Module", "Charge", "Skill", "Drone", "Implant", "Subsystem", "Structure", "Structure Module", "Fighter")' cursor.execute(query) for row in cursor: itemid = row[0] groupID = row[1] # Initialize container for the data for each item with empty stuff besides groupID dictionary[itemid] = [groupID, set(), {}] # Add items filtered by group query = 'SELECT it.typeID, it.groupID FROM invtypes AS it INNER JOIN invgroups AS ig ON it.groupID = ig.groupID WHERE it.published = 1 AND ig.name IN ("Effect Beacon", "Ship Modifiers", "Mutaplasmids", "MassiveEnvironments", "Abyssal Hazards", "Non-Interactable Object")' cursor.execute(query) for row in cursor: itemid = row[0] groupID = row[1] dictionary[itemid] = [groupID, set(), {}] if effects: # Pull all eff query = 'SELECT it.typeID, de.effectID FROM invtypes AS it INNER JOIN dgmtypeeffects AS dte ON dte.typeID = it.typeID INNER JOIN dgmeffects AS de ON de.effectID = dte.effectID WHERE it.published = 1' cursor.execute(query) for row in cursor: itemid = row[0] effectID = row[1] # Process only items we need if itemid in dictionary: # Add effect to the set effectSet = dictionary[itemid][1] effectSet.add(effectID) if attributes: # Add attribute data for other attributes query = 'SELECT dta.typeID, dta.attributeID, dta.value FROM dgmtypeattribs AS dta' cursor.execute(query) for row in cursor: itemid = row[0] if itemid in dictionary: attrid = row[1] attrval = row[2] attrdict = dictionary[itemid][2] attrdict[attrid] = attrval # Get set of IDs from both dictionaries items_old = set(old_itmdata.keys()) items_new = set(new_itmdata.keys()) # Format: # Key: item state # Value: {item id: ((group state, old group, new group), {effect state: set(effects)}, {attribute state: {attributeID: (old value, new value)}})} global_itmdata = {} # Initialize it for state in S: global_itmdata[S[state]] = {} # Fill all the data for removed items for item in items_old.difference(items_new): # Set item state to removed state = S["removed"] # Set only old group for item oldgroup = old_itmdata[item][0] groupdata = (S["unchanged"], oldgroup, None) # Set old set of effects and mark all as unchanged effectsdata = {S["unchanged"]: set()} if effects: oldeffects = old_itmdata[item][1] effectsdata[S["unchanged"]].update(oldeffects) # Set old set of attributes and mark all as unchanged attrdata = {S["unchanged"]: {}} if attributes: oldattrs = old_itmdata[item][2] for attr in oldattrs: # NULL will mean there's no such attribute in db attrdata[S["unchanged"]][attr] = (oldattrs[attr], "NULL") # Fill global dictionary with data we've got global_itmdata[state][item] = (groupdata, effectsdata, attrdata) # Now, for added items for item in items_new.difference(items_old): # Set item state to added state = S["added"] # Set only new group for item newgroup = new_itmdata[item][0] groupdata = (S["unchanged"], None, newgroup) # Set new set of effects and mark all as unchanged effectsdata = {S["unchanged"]: set()} if effects: neweffects = new_itmdata[item][1] effectsdata[S["unchanged"]].update(neweffects) # Set new set of attributes and mark all as unchanged attrdata = {S["unchanged"]: {}} if attributes: newattrs = new_itmdata[item][2] for attr in newattrs: # NULL will mean there's no such attribute in db attrdata[S["unchanged"]][attr] = ("NULL", newattrs[attr]) # Fill global dictionary with data we've got global_itmdata[state][item] = (groupdata, effectsdata, attrdata) # Now, check all the items which exist in both databases for item in items_old.intersection(items_new): # Set group data for an item oldgroup = old_itmdata[item][0] newgroup = new_itmdata[item][0] # If we're not asked to compare groups, mark them as unchanged anyway groupdata = (S["changed"] if oldgroup != newgroup and groups else S["unchanged"], oldgroup, newgroup) # Fill effects data into appropriate groups effectsdata = {} for state in S: # We do not have changed effects whatsoever if state != "changed": effectsdata[S[state]] = set() if effects: oldeffects = old_itmdata[item][1] neweffects = new_itmdata[item][1] effectsdata[S["unchanged"]].update(oldeffects.intersection(neweffects)) effectsdata[S["removed"]].update(oldeffects.difference(neweffects)) effectsdata[S["added"]].update(neweffects.difference(oldeffects)) # Go through all attributes, filling global data dictionary attrdata = {} for state in S: attrdata[S[state]] = {} if attributes: oldattrs = old_itmdata[item][2] newattrs = new_itmdata[item][2] for attr in set(oldattrs.keys()).union(list(newattrs.keys())): # NULL will mean there's no such attribute in db oldattr = oldattrs.get(attr, "NULL") newattr = newattrs.get(attr, "NULL") attrstate = S["unchanged"] if oldattr == "NULL" and newattr != "NULL": attrstate = S["added"] elif oldattr != "NULL" and newattr == "NULL": attrstate = S["removed"] elif oldattr != newattr: attrstate = S["changed"] attrdata[attrstate][attr] = (oldattr, newattr) # Consider item as unchanged by default and set it to change when we see any changes in sub-items state = S["unchanged"] if state == S["unchanged"] and groupdata[0] != S["unchanged"]: state = S["changed"] if state == S["unchanged"] and (len(effectsdata[S["removed"]]) > 0 or len(effectsdata[S["added"]]) > 0): state = S["changed"] if state == S["unchanged"] and (len(attrdata[S["removed"]]) > 0 or len(attrdata[S["changed"]]) > 0 or len(attrdata[S["added"]]) > 0): state = S["changed"] # Fill global dictionary with data we've got global_itmdata[state][item] = (groupdata, effectsdata, attrdata) # As eos uses names as unique IDs in lot of places, we have to keep track of name changes if renames: ren_effects = {} query = 'SELECT effectID, effectName FROM dgmeffects' findrenames(ren_effects, query, strip = True) ren_attributes = {} query = 'SELECT attributeID, attributeName FROM dgmattribs' findrenames(ren_attributes, query) ren_categories = {} query = 'SELECT categoryID, name FROM invcategories' findrenames(ren_categories, query) ren_groups = {} query = 'SELECT groupID, name FROM invgroups' findrenames(ren_groups, query) ren_marketgroups = {} query = 'SELECT marketGroupID, marketGroupName FROM invmarketgroups' findrenames(ren_marketgroups, query) ren_items = {} query = 'SELECT typeID, typeName FROM invtypes' findrenames(ren_items, query) try: # Get db metadata old_meta = {} new_meta = {} query = 'SELECT field_name, field_value FROM metadata WHERE field_name LIKE "client_build"' old_cursor.execute(query) for row in old_cursor: old_meta[row[0]] = row[1] new_cursor.execute(query) for row in new_cursor: new_meta[row[0]] = row[1] except (KeyboardInterrupt, SystemExit): raise except: pass # Print jobs print(("Comparing databases:\n{0} -> {1}\n".format(old_meta.get("client_build"), new_meta.get("client_build")))) if renames: title = 'effects' printrenames(ren_effects, title) title = 'attributes' printrenames(ren_attributes, title) title = 'categories' printrenames(ren_categories, title) title = 'groups' printrenames(ren_groups, title) title = 'market groups' printrenames(ren_marketgroups, title) title = 'items' printrenames(ren_items, title) print print if effects or attributes or groups: # Print legend only when there're any interesting changes if len(global_itmdata[S["removed"]]) > 0 or len(global_itmdata[S["changed"]]) > 0 or len(global_itmdata[S["added"]]) > 0: genleg = "[+] - new item\n[-] - removed item\n[*] - changed item\n" grpleg = "(x => y) - group changes\n" if groups else "" attreffleg = " [+] - effect or attribute has been added to item\n [-] - effect or attribute has been removed from item\n" if attributes or effects else "" effleg = " [y] - effect is implemented\n [n] - effect is not implemented\n" if effects else "" print(("{0}{1}{2}{3}\nItems:".format(genleg, grpleg, attreffleg, effleg))) # Make sure our states are sorted stateorder = sorted(global_itmdata) TG = {S["unchanged"]: "+", S["changed"]: "*", S["removed"]: "-", S["added"]: "+"} # Cycle through states for itmstate in stateorder: # Skip unchanged items if itmstate == S["unchanged"]: continue items = global_itmdata[itmstate] # Sort by name first itemorder = sorted(items, key=lambda item: getitemname(item)) # Then by group id itemorder = sorted(itemorder, key=lambda item: items[item][0][2] or items[item][0][1]) # Then by category id itemorder = sorted(itemorder, key=lambda item: getgroupcat(items[item][0][2] or items[item][0][1])) for item in itemorder: groupdata = items[item][0] groupstr = " ({0} => {1})".format(getgroupname(groupdata[1]), getgroupname(groupdata[2])) if groupdata[0] == S["changed"] else "" print(("\n[{0}] {1}{2}".format(TG[itmstate], getitemname(item).encode('utf-8'), groupstr))) effdata = items[item][1] for effstate in stateorder: # Skip unchanged effect sets, but always include them for added or removed ships # Also, always skip empty data if (effstate == S["unchanged"] and itmstate not in (S["removed"], S["added"])) or effstate not in effdata: continue effects = effdata[effstate] efforder = sorted(effects, key=lambda eff: geteffectname(eff)) for eff in efforder: # Take tag from item if item was added or removed tag = TG[effstate] if itmstate not in (S["removed"], S["added"]) else TG[itmstate] print((" [{0}|{1}] {2}".format(tag, "y" if geteffst(eff) else "n", geteffectname(eff)))) attrdata = items[item][2] for attrstate in stateorder: # Skip unchanged and empty attribute sets, also skip attributes display for added and removed items if (attrstate == S["unchanged"] and itmstate != S["added"]) or itmstate in (S["removed"], ) or attrstate not in attrdata: continue attrs = attrdata[attrstate] attrorder = sorted(attrs, key=lambda attr: getattrname(attr)) for attr in attrorder: valline = "" if attrs[attr][0] == "NULL" or itmstate == S["added"]: valline = "{0}".format(attrs[attr][1] or 0) elif attrs[attr][1] == "NULL": valline = "{0}".format(attrs[attr][0] or 0) else: valline = "{0} => {1}".format(attrs[attr][0] or 0, attrs[attr][1] or 0) print((" [{0}] {1}: {2}".format(TG[attrstate], getattrname(attr), valline))) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Compare two databases generated from eve dump to find eos-related differences") parser.add_argument("-o", "--old", type=str, help="path to old cache data dump, defaults to current pyfa eve.db", default=default_old) parser.add_argument("-n", "--new", type=str, required=True, help="path to new cache data dump") parser.add_argument("-g", "--nogroups", action="store_false", default=True, dest="groups", help="don't show changed groups") parser.add_argument("-e", "--noeffects", action="store_false", default=True, dest="effects", help="don't show list of changed effects") parser.add_argument("-a", "--noattributes", action="store_false", default=True, dest="attributes", help="don't show list of changed attributes") parser.add_argument("-r", "--norenames", action="store_false", default=True, dest="renames", help="don't show list of renamed data") args = parser.parse_args() main(args.old, args.new, args.groups, args.effects, args.attributes, args.renames)