├── .gitignore ├── .vscode └── launch.json ├── README.md ├── builder.py ├── builder_parts ├── abilities.py ├── chat_wheel.py ├── emoticons.py ├── facets.py ├── heroes.py ├── items.py ├── loadingscreens.py ├── patches.py ├── responses.py ├── talents.py └── voices.py ├── builderdata ├── criteria_matchkeys.json ├── criteria_pretty.json ├── hero_aliases.json ├── hero_colors.json ├── hero_names.json ├── item_aliases.json ├── loadingscreen_heroes.json ├── old_patch_dates.json └── voice_actors.json ├── criteria_sentancing.py ├── generate_json.py ├── requirements.txt ├── utils.py ├── valve2json.py └── vccd_reader.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Other ignores 86 | obj 87 | bin 88 | .vs 89 | *.suo 90 | 91 | dota-vpk/ 92 | notes/ 93 | jsoncache/ 94 | log/ 95 | scripts/ 96 | config.json 97 | 98 | temp.py 99 | 100 | VpkExtractor/packages 101 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Dotabase Build", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "builder.py", 12 | "console": "integratedTerminal" 13 | }, 14 | { 15 | "name": "Dotabase (chat_wheel)", 16 | "type": "python", 17 | "request": "launch", 18 | "program": "builder.py", 19 | "console": "integratedTerminal", 20 | "args": [ "chat_wheel" ] 21 | }, 22 | 23 | { 24 | "name": "Dotabase (emoticons)", 25 | "type": "python", 26 | "request": "launch", 27 | "program": "builder.py", 28 | "console": "integratedTerminal", 29 | "args": [ "emoticons" ] 30 | }, 31 | 32 | { 33 | "name": "Dotabase (items)", 34 | "type": "python", 35 | "request": "launch", 36 | "program": "builder.py", 37 | "console": "integratedTerminal", 38 | "args": [ "items" ] 39 | }, 40 | 41 | { 42 | "name": "Dotabase (facets)", 43 | "type": "python", 44 | "request": "launch", 45 | "program": "builder.py", 46 | "console": "integratedTerminal", 47 | "args": [ "facets" ] 48 | }, 49 | 50 | { 51 | "name": "Dotabase (abilities)", 52 | "type": "python", 53 | "request": "launch", 54 | "program": "builder.py", 55 | "console": "integratedTerminal", 56 | "args": [ "abilities" ] 57 | }, 58 | 59 | { 60 | "name": "Dotabase (heroes)", 61 | "type": "python", 62 | "request": "launch", 63 | "program": "builder.py", 64 | "console": "integratedTerminal", 65 | "args": [ "heroes" ] 66 | }, 67 | 68 | { 69 | "name": "Dotabase (talents)", 70 | "type": "python", 71 | "request": "launch", 72 | "program": "builder.py", 73 | "console": "integratedTerminal", 74 | "args": [ "talents" ] 75 | }, 76 | 77 | { 78 | "name": "Dotabase (voices)", 79 | "type": "python", 80 | "request": "launch", 81 | "program": "builder.py", 82 | "console": "integratedTerminal", 83 | "args": [ "voices" ] 84 | }, 85 | 86 | { 87 | "name": "Dotabase (responses)", 88 | "type": "python", 89 | "request": "launch", 90 | "program": "builder.py", 91 | "console": "integratedTerminal", 92 | "args": [ "responses" ] 93 | }, 94 | 95 | { 96 | "name": "Dotabase (loadingscreens)", 97 | "type": "python", 98 | "request": "launch", 99 | "program": "builder.py", 100 | "console": "integratedTerminal", 101 | "args": [ "loadingscreens" ] 102 | }, 103 | 104 | { 105 | "name": "Dotabase (patches)", 106 | "type": "python", 107 | "request": "launch", 108 | "program": "builder.py", 109 | "console": "integratedTerminal", 110 | "args": [ "patches" ] 111 | }, 112 | { 113 | "name": "Run Current File", 114 | "type": "python", 115 | "request": "launch", 116 | "program": "${file}", 117 | "console": "integratedTerminal" 118 | } 119 | ] 120 | } 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dotabase Builder 2 | A collection of scripts and programs to extract dota's game files and build an sqlite database. See the output of this builder at my [Dotabase repository](https://github.com/mdiller/dotabase "Dotabase"). 3 | 4 | ## VPK Extraction 5 | The main library/tool that this builder leans on is [ValveResourceFormat](https://github.com/SteamDatabase/ValveResourceFormat "Valve Source 2 file decompiler/compiler"). This lovely project is what allows me to extract the data from dota's vpk files, and decompile some of the obscure file formats like vsnd_c into more friendly ones like mp3. I'm using the Decompiler from this project in a call that looks about like this: 6 | ``` 7 | ./Decompiler.exe -i "" --vpk_cache -d -e "txt,dat,vsndevts_c,vxml_c,vjs_c,vcss_c,png,cfg,res,vsnd_c,vtex_c" -o "" --threads 16 8 | ``` 9 | 10 | ## Dota 2 Wiki Scraper 11 | ~~As a focus of dotabase is the extraction of the Hero Responses data, I wanted to extract the subtitles/captions for each response. Unfortunatly, these are stored in .dat files, and as of the time of this project creation, I have not found a reliable way to decompile these back into their original .txt format. Instead, I have decided to scrape this information from the [Dota 2 Wiki](http://dota2.gamepedia.com/Dota_2_Wiki "Dota 2 Wiki - Gamepedia"). A bit of a hackish method, but it works.~~ 12 | 13 | The voice lines texts are now taken directly from the subtitles.dat files from the vpk. See vccd_reader.py for more info on how that works. 14 | 15 | ## valve2json.py 16 | Although the [ValveResourceFormat](https://github.com/SteamDatabase/ValveResourceFormat "Valve Source 2 file decompiler/compiler") decompiler does a good job of decompiling the game files into readable text files, they are still not in a format that is easily readable by programs. To that end, I convert all of the files containing information I need into .json files. I do this by doing a bunch of regex substitutions. -------------------------------------------------------------------------------- /builder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.6 2 | 3 | from sqlalchemy import desc 4 | from dotabase import * 5 | from utils import * 6 | import importlib.metadata 7 | import os 8 | import sys 9 | 10 | single_part = None 11 | if len(sys.argv) > 1: 12 | single_part = sys.argv[1] 13 | 14 | if __name__ == "__main__" and config.overwrite_db and os.path.isfile(dotabase_db) and (single_part is None): 15 | print("overwriting db") 16 | os.remove(dotabase_db) 17 | session = dotabase_session() 18 | 19 | 20 | from generate_json import generate_json 21 | 22 | # Import all parts now that we have the things they need 23 | from builder_parts import ( 24 | chat_wheel, 25 | emoticons, 26 | items, 27 | facets, 28 | abilities, 29 | heroes, 30 | talents, 31 | responses, 32 | voices, 33 | loadingscreens, 34 | patches) 35 | 36 | parts_dict = { 37 | "chat_wheel": chat_wheel, 38 | "emoticons": emoticons, 39 | "items": items, 40 | "facets": facets, 41 | "abilities": abilities, 42 | "heroes": heroes, 43 | "talents": talents, 44 | "voices": voices, 45 | "responses": responses, 46 | "loadingscreens": loadingscreens, 47 | "patches": patches 48 | } 49 | 50 | def update_pkg_version(): 51 | pkgversion = importlib.metadata.version("dotabase") 52 | filename = os.path.join(dotabase_dir, "../VERSION") 53 | 54 | with open(filename, "r") as f: 55 | fileversion = f.read() 56 | 57 | if fileversion.strip() == pkgversion.strip(): 58 | version = list(map(lambda i: int(i), pkgversion.split("."))) 59 | version[-1] += 1 60 | version = ".".join(map(lambda s: str(s), version)) 61 | with open(filename, "w+") as f: 62 | f.write(version) 63 | print(f"version updated to: {version}") 64 | 65 | def dump_sql(): 66 | print("dumping sql...") 67 | os.system(f"cd \"{dotabase_dir}\" && sqlite3 dotabase.db \".dump\" > dotabase.db.sql") 68 | 69 | # updates the dotabase readme info 70 | def update_readme(): 71 | print("updating readme info...") 72 | dota_version_path = os.path.join(dotabase_dir, "../DOTA_VERSION") 73 | data = read_json(dota_version_path) 74 | 75 | patch = session.query(Patch).order_by(desc(Patch.timestamp)).first() 76 | if patch is None: 77 | printerr("No patches found!") 78 | else: 79 | if data["message"] != patch.number: 80 | print(f"dota version updated to: {patch.number}") 81 | data["message"] = patch.number 82 | else: 83 | print(f"keeping patch at: {data['message']}") 84 | 85 | write_json(dota_version_path, data) 86 | 87 | def build_dotabase(): 88 | if single_part: 89 | if single_part not in parts_dict: 90 | print("invalid builder part. valid parts are:") 91 | for key in parts_dict: 92 | print(key) 93 | return None 94 | parts_dict[single_part].load() 95 | else: 96 | chat_wheel.load() 97 | emoticons.load() 98 | items.load() 99 | facets.load() 100 | abilities.load() 101 | heroes.load() 102 | talents.load() 103 | voices.load() 104 | responses.load() 105 | loadingscreens.load() 106 | patches.load() 107 | generate_json() 108 | dump_sql() 109 | update_readme() 110 | update_pkg_version() 111 | print("done!") 112 | 113 | 114 | if __name__ == "__main__": 115 | try: 116 | build_dotabase() 117 | except KeyboardInterrupt: 118 | print("\ndone (canceled)") 119 | -------------------------------------------------------------------------------- /builder_parts/abilities.py: -------------------------------------------------------------------------------- 1 | ''''' 2 | PROMPT: 3 | 4 | [- Used So Far: 0.0154¢ | 217 tokens -] 5 | ''''' 6 | from builder import session 7 | from dotabase import * 8 | from utils import * 9 | from valve2json import DotaFiles, DotaPaths, ValveFile, valve_readfile 10 | import re 11 | 12 | def build_replacements_dict_facetabilitystrings(facet: Facet, ability: Ability): 13 | specials = json.loads(facet.ability_special, object_pairs_hook=OrderedDict) 14 | result = build_replacements_dict(ability) 15 | if not ability.name.startswith("special_bonus_"): 16 | for attrib in specials: 17 | key = attrib["key"] 18 | value = attrib["value"] 19 | if key in result: 20 | result[key] = value 21 | else: 22 | modified_key = re.sub("^bonus_", "", key) 23 | if modified_key in result: 24 | result[modified_key] = value 25 | else: 26 | result[key] = value 27 | return result 28 | 29 | def build_replacements_dict_facet(facet: Facet): 30 | specials = json.loads(facet.ability_special, object_pairs_hook=OrderedDict) 31 | result = {} 32 | for attrib in specials: 33 | if attrib["key"] not in result: 34 | result[attrib["key"]] = attrib["value"] 35 | return result 36 | 37 | def build_replacements_dict(ability: Ability, scepter=False, shard=False): 38 | specials = json.loads(ability.ability_special, object_pairs_hook=OrderedDict) 39 | result = { 40 | "abilityduration": ability.duration, 41 | "abilitychanneltime": ability.channel_time, 42 | "abilitycastpoint": ability.cast_point, 43 | "abilitycastrange": ability.cast_range, 44 | "abilitychargerestoretime": ability.cooldown, 45 | "charge_restore_time": ability.cooldown, 46 | "abilitycooldown": ability.cooldown, 47 | "max_charges": ability.charges, 48 | "AbilityCharges": ability.charges, 49 | "abilitymanacost": ability.mana_cost 50 | } 51 | for attrib in specials: 52 | is_scepter_upgrade = attrib.get("scepter_upgrade") == "1" and not ability.scepter_grants 53 | is_shard_upgrade = attrib.get("shard_upgrade") == "1" and not ability.shard_grants 54 | if (is_scepter_upgrade and not scepter) or (is_shard_upgrade and not shard): 55 | if (attrib["key"] in result): 56 | continue # skip this if we we don't want to *override* stuff with shard/scepter stuff 57 | if (attrib["key"] not in result) or is_scepter_upgrade or is_shard_upgrade: 58 | value = attrib.get("value") 59 | if shard and attrib.get("shard_value"): 60 | value = attrib.get("shard_value") 61 | if scepter and attrib.get("scepter_value"): 62 | value = attrib.get("scepter_value") 63 | if value and value != "": 64 | result[attrib["key"]] = value 65 | if shard and attrib.get("shard_bonus"): 66 | result[f"bonus_{attrib['key']}"] = attrib.get("shard_bonus") 67 | result[f"shard_{attrib['key']}"] = attrib.get("shard_value") 68 | if scepter and attrib.get("scepter_bonus"): 69 | result[f"bonus_{attrib['key']}"] = attrib.get("scepter_bonus") 70 | result[f"scepter_{attrib['key']}"] = attrib.get("scepter_value") 71 | return result 72 | 73 | def load(): 74 | session.query(Ability).delete() 75 | print("Abilities") 76 | 77 | added_ids = [] 78 | 79 | print("- loading abilities from ability scripts") 80 | # load all of the ability scripts data information 81 | ability_id_map = DotaFiles.npc_ids.read()["DOTAAbilityIDs"]["UnitAbilities"]["Locked"] 82 | main_data = DotaFiles.npc_abilities.read()["DOTAAbilities"] 83 | ability_base = main_data["ability_base"] 84 | 85 | # FIX ID_MAP CUZ APPARENTLY THESE ONES ARE BROKEN FOR SOME REASON 86 | id_remap = { 87 | "special_bonus_unique_lina_dragon_slave_crits": "special_bonus_unique_lina_crit_debuff", 88 | # "special_bonus_unique_timbersaw_reactive_armor_regen_per_stack1": "special_bonus_unique_timbersaw_reactive_armor_regen_per_stack" 89 | } 90 | for bad_name in id_remap: 91 | good_name = id_remap[bad_name] 92 | ability_id_map[good_name] = ability_id_map[bad_name] 93 | del ability_id_map[bad_name] 94 | 95 | # TODO: THEY SEEM TO HAVE JUST REMOVED TALENTS FROM main_data, SO WE GOTTA JUST ADD ABILITIES BY ID FROM ability_id_map, AND ACCEPT THAT THE DATA MIGHT NOT EXIST 96 | # this method called by loop below it 97 | def add_ability(abilityname, data_source): 98 | if(abilityname == "Version" or 99 | abilityname == "ability_deward" or 100 | abilityname == "dota_base_ability"): 101 | return 102 | 103 | ability_data = data_source.get(abilityname) 104 | ability = Ability() 105 | 106 | def get_val(key, default_base=False): 107 | if key in ability_data: 108 | val = ability_data[key] 109 | if ' ' in val and all(x == val.split(' ')[0] for x in val.split(' ')): 110 | return val.split(' ')[0] 111 | return val 112 | elif "AbilityValues" in ability_data and key in ability_data["AbilityValues"]: 113 | val = ability_data["AbilityValues"][key] 114 | if not isinstance(val, str): 115 | if "value" in val: 116 | val = val["value"] 117 | elif default_base: 118 | return ability_base[key] 119 | else: 120 | return None 121 | if ' ' in val and all(x == val.split(' ')[0] for x in val.split(' ')): 122 | return val.split(' ')[0] 123 | return val 124 | elif default_base: 125 | return ability_base[key] 126 | else: 127 | return None 128 | 129 | # TEMP? CODE TO IGNORE ABILITIES THAT DONT HAVE IDS 130 | # if abilityname not in ability_id_map and get_val("BaseClass") == "special_bonus_base": 131 | # printerr(f"Missing ID for {abilityname}") 132 | # return 133 | if abilityname in id_remap or "special_bonus_unique_timbersaw_reactive_armor_regen_per_stack1" == abilityname: 134 | return # these are not real abilities 135 | 136 | def get_ability_id(name): 137 | if name in ability_id_map: 138 | return ability_id_map[name] 139 | name = name.replace("1", "") 140 | return ability_id_map[name] 141 | 142 | ability.name = abilityname 143 | ability.id = get_ability_id(ability.name) 144 | ability.json_data = "{}" 145 | ability.ability_special = "[]" 146 | 147 | if ability_data is not None: 148 | ability.type = get_val('AbilityType', default_base=True) 149 | ability.behavior = get_val('AbilityBehavior', default_base=True) 150 | ability.cast_range = clean_values(get_val('AbilityCastRange')) 151 | ability.cast_point = clean_values(get_val('AbilityCastPoint')) 152 | ability.channel_time = clean_values(get_val('AbilityChannelTime')) 153 | ability.charges = clean_values(get_val('AbilityCharges')) 154 | if ability.charges: 155 | ability.cooldown = clean_values(get_val('AbilityChargeRestoreTime')) 156 | else: 157 | ability.cooldown = clean_values(get_val('AbilityCooldown')) 158 | ability.duration = clean_values(get_val('AbilityDuration')) 159 | ability.damage = clean_values(get_val('AbilityDamage')) 160 | ability.health_cost = clean_values(get_val('AbilityHealthCost')) 161 | ability.mana_cost = clean_values(get_val('AbilityManaCost')) 162 | ability.ability_special = json.dumps(get_ability_special(ability_data, ability.name), indent=4) 163 | ability.scepter_grants = get_val("IsGrantedByScepter") == "1" 164 | ability.shard_grants = get_val("IsGrantedByShard") == "1" 165 | ability.scepter_upgrades = get_val("HasScepterUpgrade") == "1" 166 | ability.shard_upgrades = get_val("HasShardUpgrade") == "1" 167 | ability.innate = get_val("Innate") == "1" 168 | 169 | def get_enum_val(key, prefix): 170 | value = get_val(key) 171 | if value: 172 | return re.sub(prefix, "", value).lower().replace(" ", "") 173 | else: 174 | return value 175 | 176 | ability.behavior = get_enum_val('AbilityBehavior', "DOTA_ABILITY_BEHAVIOR_") 177 | ability.damage_type = get_enum_val('AbilityUnitDamageType', "DAMAGE_TYPE_") 178 | ability.spell_immunity = get_enum_val('SpellImmunityType', "SPELL_IMMUNITY_(ENEMIES|ALLIES)_") 179 | ability.target_team = get_enum_val('AbilityUnitTargetTeam', "DOTA_UNIT_TARGET_TEAM_") 180 | ability.dispellable = get_enum_val('SpellDispellableType', "SPELL_DISPELLABLE_") 181 | 182 | ability.json_data = json.dumps(ability_data, indent=4) 183 | 184 | 185 | if ability.id in added_ids: 186 | printerr(f"duplicate id on: {ability.name}") 187 | return 188 | added_ids.append(ability.id) 189 | 190 | session.add(ability) 191 | 192 | 193 | for root, dirs, files in os.walk(config.vpk_path + DotaPaths.npc_hero_scripts): 194 | for file in files: 195 | hero_data = valve_readfile(DotaPaths.npc_hero_scripts + file, "kv")["DOTAAbilities"] 196 | for key in hero_data: 197 | add_ability(key, hero_data) 198 | 199 | for key in ability_id_map: 200 | add_ability(key, main_data) 201 | 202 | 203 | print("- loading ability localization files") 204 | # Load additional information from the ability localization files 205 | english_data = DotaFiles.abilities_english.read()["lang"]["Tokens"] 206 | english_data = CaseInsensitiveDict(english_data) 207 | lang_data = [] 208 | for lang, file in DotaFiles.lang_abilities: 209 | data = file.read()["lang"]["Tokens"] 210 | data = CaseInsensitiveDict(data) 211 | lang_data.append((lang, data)) 212 | 213 | print("- intermediate ability linking") 214 | # intermedate re-linking and setting of ability metadata 215 | for ability in session.query(Ability): 216 | ability_data = json.loads(ability.json_data, object_pairs_hook=OrderedDict) 217 | abilityvalues = ability_data.get("AbilityValues") 218 | if abilityvalues: 219 | for key, valdict in abilityvalues.items(): 220 | if not isinstance(valdict, str): 221 | for subkey in valdict: 222 | if subkey.startswith("special_bonus"): 223 | # this is a talent value we need to link 224 | value = valdict[subkey] 225 | if isinstance(value, OrderedDict): 226 | if "value" in value: 227 | value = value["value"] 228 | else: 229 | value = "0" 230 | value = re.sub(r"(\+|-)", "", value) # clean it up so we dont have duplicate things (the header contains these) 231 | 232 | # special_bonus_facet_drow_ranger_sidestep 233 | facet = session.query(Facet).filter_by(name=subkey).first() 234 | facet_prefix = "special_bonus_facet_" 235 | if subkey.startswith(facet_prefix): 236 | facet_name = subkey.replace(facet_prefix, "") 237 | talent = session.query(Facet).filter_by(name=facet_name).first() 238 | else: 239 | talent = session.query(Ability).filter_by(name=subkey).first() 240 | if talent is None: 241 | if subkey not in [ "special_bonus_scepter", "special_bonus_shard" ]: 242 | # EXPLANATION: When parsing ability_special/AbilityValues, can't find the right talent/facet to link this upgrade to 243 | printerr(f"Can't find special_bonus when attempting to link '{ability.name}' '{key}' ('{subkey}')") 244 | break 245 | talent_ability_special = json.loads(talent.ability_special, object_pairs_hook=OrderedDict) 246 | talent_ability_special.append({ 247 | "key": f"bonus_{key}", 248 | "value": value 249 | }) 250 | talent_ability_special = ability_special_add_header(talent_ability_special, english_data, ability.name) 251 | talent.ability_special = json.dumps(talent_ability_special, indent=4) 252 | 253 | progress = ProgressBar(session.query(Ability).count(), title="- loading data from ability localization files") 254 | for ability in session.query(Ability): 255 | progress.tick() 256 | ability_tooltip = "DOTA_Tooltip_ability_" + ability.name 257 | 258 | # do ability_special with just english 259 | ability_special_value_fixes = { 260 | "abilityduration": ability.duration 261 | } 262 | ability_special = json.loads(ability.ability_special, object_pairs_hook=OrderedDict) 263 | ability_special = ability_special_add_talent(ability_special, session.query(Ability), ability.name) 264 | ability_special = ability_special_add_header(ability_special, english_data, ability.name) 265 | for key in ability_special_value_fixes: 266 | for special in ability_special: 267 | if special["key"] == key and special["value"] == "": 268 | special["value"] = ability_special_value_fixes[key] 269 | ability.ability_special = json.dumps(ability_special, indent=4) 270 | 271 | # construct replacement dicts 272 | replacements_dict = build_replacements_dict(ability) 273 | replacements_dict_scepter = build_replacements_dict(ability, scepter=True) 274 | replacements_dict_shard = build_replacements_dict(ability, shard=True) 275 | 276 | # language-specific stuff 277 | for lang, data in lang_data: 278 | info = {} 279 | info["localized_name"] = data.get(ability_tooltip, ability.name) 280 | info["description"] = data.get(ability_tooltip + "_Description", "") 281 | info["lore"] = data.get(ability_tooltip + "_Lore", "") 282 | 283 | if ability.scepter_upgrades: 284 | info["scepter_description"] = data.get(ability_tooltip + "_scepter_description", "") 285 | else: 286 | info["scepter_description"] = "" 287 | if ability.shard_upgrades: 288 | info["shard_description"] = data.get(ability_tooltip + "_shard_description", "") 289 | else: 290 | info["shard_description"] = "" 291 | 292 | notes = [] 293 | for i in range(8): 294 | key = f"{ability_tooltip}_Note{i}" 295 | if key in data: 296 | notes.append(data[key]) 297 | info["note"] = "" if len(notes) == 0 else "\n".join(notes) 298 | 299 | is_probably_talent = ability.name.startswith("special_bonus") 300 | 301 | report_errors = lang == "english" 302 | 303 | info["localized_name"] = clean_description(info["localized_name"], replacements_dict, value_bolding=False, report_errors=report_errors and not is_probably_talent) 304 | info["description"] = clean_description(info["description"], replacements_dict, report_errors=report_errors and not is_probably_talent) 305 | info["note"] = clean_description(info["note"], replacements_dict, report_errors=report_errors) 306 | info["scepter_description"] = clean_description(info["scepter_description"], replacements_dict_scepter, report_errors=report_errors) 307 | info["shard_description"] = clean_description(info["shard_description"], replacements_dict_shard, report_errors=report_errors) 308 | 309 | if lang == "english": 310 | for key in info: 311 | setattr(ability, key, info[key]) 312 | else: 313 | for key in info: 314 | addLocaleString(session, lang, ability, key, info[key]) 315 | 316 | if ability.scepter_grants and ability.scepter_description == "": 317 | ability.scepter_description = f"Adds new ability: {ability.localized_name}." 318 | 319 | if ability.shard_grants and ability.shard_description == "": 320 | ability.shard_description = f"Adds new ability: {ability.localized_name}." 321 | 322 | # special case for skywrath who has an innate shard 323 | if ability.id == 5584: 324 | ability.shard_description = data.get("DOTA_Tooltip_ability_skywrath_mage_shard_description", "") 325 | 326 | print("- adding ability icon files") 327 | # Add img files to ability 328 | for ability in session.query(Ability): 329 | iconpath = DotaPaths.ability_icon_images + ability.name + "_png.png" 330 | if os.path.isfile(config.vpk_path + iconpath): 331 | ability.icon = iconpath 332 | elif ability.innate: 333 | ability.icon = "/panorama/images/hud/facets/innate_icon_large_png.png" 334 | else: 335 | ability.icon = DotaPaths.ability_icon_images + "attribute_bonus_png.png" 336 | 337 | session.query(FacetAbilityString).delete() 338 | # LOCALIZE FACET STRINGS 339 | lang_data = [] 340 | facet_ability_string_id_current = 1 341 | 342 | for lang, file in DotaFiles.lang_abilities: 343 | data = file.read()["lang"]["Tokens"] 344 | data = CaseInsensitiveDict(data) 345 | lang_data.append((lang, data)) 346 | 347 | lang_data.sort(key=lambda x: x[0] != 'english') 348 | 349 | facet_related_keys = [] 350 | for lang, data in lang_data: 351 | for key in data: 352 | key = key.lower() 353 | if "facet" in key and key not in facet_related_keys: 354 | facet_related_keys.append(key) 355 | 356 | facets = session.query(Facet).all() 357 | progress = ProgressBar(len(facets), title="- localize strings for facets") 358 | for facet in facets: 359 | progress.tick() 360 | replacements_dict = build_replacements_dict_facet(facet) 361 | abilitystrings_pattern = f"DOTA_Tooltip_ability_(.*)_Facet_{facet.name}" 362 | abilitystrings_keys = [key.lower() for key in facet_related_keys if re.match(abilitystrings_pattern, key, re.I)] 363 | abilitystrings_map = {} 364 | 365 | for lang, data in lang_data: 366 | localized_name = data.get(f"DOTA_Tooltip_facet_{facet.name}", "") 367 | description = data.get(f"DOTA_Tooltip_facet_{facet.name}_Description", "") 368 | 369 | if localized_name == "": 370 | localized_name = data.get(f"DOTA_Tooltip_ability_{facet.name}", "") 371 | if description == "": 372 | description = data.get(f"DOTA_Tooltip_ability_{facet.name}_Description", "") 373 | 374 | description = clean_description(description, replacements_dict, report_errors=(lang == "english")) 375 | 376 | if lang == "english": 377 | facet.localized_name = localized_name 378 | facet.description = description 379 | else: 380 | addLocaleString(session, lang, facet, "localized_name", localized_name) 381 | addLocaleString(session, lang, facet, "description", description) 382 | 383 | # DO ABILITYSTRINGS STUFF 384 | for key in abilitystrings_keys: 385 | if key in data: 386 | if lang == "english": 387 | ability_name = re.match(abilitystrings_pattern, key, re.I).group(1) 388 | ability = session.query(Ability).filter_by(name=ability_name).first() 389 | 390 | if ability is not None: 391 | bold_values = True 392 | if ability.name.startswith("special_bonus_"): 393 | bold_values = False # don't bold values if this is a talent 394 | newstring = FacetAbilityString() 395 | newstring.id = facet_ability_string_id_current 396 | newstring.facet_id = facet.id 397 | newstring.ability_id = ability.id 398 | 399 | replacements_dict = build_replacements_dict_facetabilitystrings(facet, ability) 400 | newstring.description = clean_description(data[key], replacements_dict, value_bolding=bold_values, report_errors=True) 401 | 402 | session.add(newstring) 403 | abilitystrings_map[key] = (newstring, replacements_dict) 404 | facet_ability_string_id_current += 1 405 | else: 406 | if key in abilitystrings_map: 407 | thestring, replacements_dict = abilitystrings_map[key] 408 | description = clean_description(data[key], replacements_dict, value_bolding=False, report_errors=False) 409 | addLocaleString(session, lang, thestring, "description", description) 410 | 411 | 412 | session.commit() -------------------------------------------------------------------------------- /builder_parts/chat_wheel.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | from valve2json import valve_readfile, DotaFiles 5 | import os 6 | 7 | 8 | def load(): 9 | session.query(ChatWheelMessage).delete() 10 | print("Chat Wheels") 11 | 12 | print("- loading chat_wheel vsndevts infos") 13 | # load sounds info from vsndevts file 14 | vsndevts_data = CaseInsensitiveDict(DotaFiles.game_sounds_vsndevts.read()) 15 | extra_vsndevts_dirs = [ "teamfandom", "team_fandom", "stickers" ] 16 | for subdir in extra_vsndevts_dirs: 17 | fulldirpath = os.path.join(config.vpk_path, f"soundevents/{subdir}") 18 | for file in os.listdir(fulldirpath): 19 | if file.endswith(".vsndevts"): 20 | filepath = f"/soundevents/{subdir}/{file}" 21 | more_vsndevts_data = valve_readfile(filepath, "vsndevts") 22 | vsndevts_data.update(more_vsndevts_data) 23 | 24 | print("- loading chat_wheel info from scripts") 25 | # load all of the item scripts data information 26 | scripts_data = DotaFiles.chat_wheel.read()["chat_wheel"] 27 | scripts_data_categories = DotaFiles.chat_wheel_categories.read()["chat_wheel"] 28 | chatwheel_scripts_subdir = "scripts/chat_wheels" 29 | scripts_messages = CaseInsensitiveDict(scripts_data["messages"]) 30 | scripts_categories = CaseInsensitiveDict(scripts_data_categories["categories"]) 31 | for file in os.listdir(os.path.join(config.vpk_path, chatwheel_scripts_subdir)): 32 | filepath = f"/{chatwheel_scripts_subdir}/{file}" 33 | more_chatwheel_data = valve_readfile(filepath, "kv", encoding="utf-8")["chat_wheel"] 34 | scripts_messages.update(more_chatwheel_data.get("messages", {})) 35 | scripts_categories.update(more_chatwheel_data.get("categories", {})) 36 | 37 | existing_ids = set() 38 | print("- process all chat_wheel data") 39 | data = DotaFiles.chat_wheel.read()["chat_wheel"] 40 | for key in scripts_messages: 41 | msg_data = scripts_messages[key] 42 | 43 | message_id = int(msg_data["message_id"]) 44 | if message_id in existing_ids: 45 | printerr(f"duplicate message_id {message_id} found, skipping") 46 | continue 47 | existing_ids.add(message_id) 48 | 49 | message = ChatWheelMessage() 50 | message.id = message_id 51 | message.name = key 52 | message.label = msg_data.get("label") 53 | message.message = msg_data.get("message") 54 | message.sound = msg_data.get("sound") 55 | message.image = msg_data.get("image") 56 | message.source = msg_data.get("source") 57 | message.all_chat = msg_data.get("all_chat") == "1" 58 | if message.sound: 59 | if message.sound not in vsndevts_data: 60 | printerr(f"Couldn't find vsndevts entry for {message.sound}, skipping") 61 | continue 62 | if "vsnd_files" not in vsndevts_data[message.sound]: 63 | printerr(f"no associated vsnd files found for {message.sound}, skipping") 64 | continue 65 | 66 | soundfile = vsndevts_data[message.sound]["vsnd_files"] 67 | if isinstance(soundfile, list): 68 | soundfile = soundfile[0] 69 | message.sound = "/" + soundfile 70 | 71 | if not os.path.exists(config.vpk_path + message.sound): 72 | message.sound = message.sound.replace("vsnd", "wav") 73 | if not os.path.exists(config.vpk_path + message.sound): 74 | message.sound = message.sound.replace("wav", "mp3") 75 | 76 | if not os.path.exists(config.vpk_path + message.sound): 77 | printerr(f"Missing chatweel id {message.id} file: {message.sound}") 78 | if message.image: 79 | message.image = f"/panorama/images/{message.image}" 80 | 81 | session.add(message) 82 | 83 | for category in scripts_categories: 84 | for msg in scripts_categories[category]["messages"]: 85 | for message in session.query(ChatWheelMessage).filter_by(name=msg): 86 | if message.category is not None: 87 | raise ValueError(f"More than one category for chatwheel: {message.name}") 88 | message.category = category 89 | 90 | print("- loading chat wheel data from dota_english") 91 | # Load localization info from dota_english.txt and teamfandom_english.txt 92 | data = DotaFiles.dota_english.read()["lang"]["Tokens"] 93 | data.update(DotaFiles.teamfandom_english.read()["lang"]["Tokens"]) 94 | 95 | for message in session.query(ChatWheelMessage): 96 | if message.label is None or message.message is None: 97 | continue 98 | if message.label.startswith("#") and message.label[1:] in data: 99 | message.label = data[message.label[1:]] 100 | if message.message.startswith("#") and message.message[1:] in data: 101 | message.message = data[message.message[1:]] 102 | if message.id in [ 71, 72 ]: 103 | message.message = message.message.replace("%s1", "A hero") 104 | 105 | session.commit() -------------------------------------------------------------------------------- /builder_parts/emoticons.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | from PIL import Image 5 | from valve2json import valve_readfile, DotaFiles, DotaPaths 6 | import os 7 | 8 | def load(): 9 | session.query(Emoticon).delete() 10 | print("Emoticons") 11 | 12 | print("- loading emoticons from scripts") 13 | data = DotaFiles.emoticons.read()["emoticons"] 14 | emoticons_scripts_subdir = "scripts/emoticons" 15 | for file in os.listdir(os.path.join(config.vpk_path, emoticons_scripts_subdir)): 16 | filepath = f"/{emoticons_scripts_subdir}/{file}" 17 | more_emoticon_data = valve_readfile(filepath, "kv", encoding="UTF-8")["emoticons"] 18 | data.update(more_emoticon_data) 19 | 20 | print("- process emoticon data") 21 | for emoticonid in data: 22 | if len(data[emoticonid]['aliases']) == 0 or data[emoticonid]['aliases']['0'] == "proteam": 23 | continue # These are team emoticons, all with the same key. skip em 24 | emoticon = Emoticon() 25 | emoticon.id = int(emoticonid) 26 | emoticon.name = data[emoticonid]['aliases']['0'] 27 | emoticon.ms_per_frame = data[emoticonid]['ms_per_frame'] 28 | emoticon.url = DotaPaths.emoticon_images + data[emoticonid]['image_name'].replace(".png", "_png.png") 29 | try: 30 | img = Image.open(config.vpk_path + emoticon.url) 31 | emoticon.frames = int(img.size[0] / img.size[1]) 32 | except: 33 | # Error loading this image, so dont add it to the database 34 | printerr(f"Couldn't find emoticon {emoticon.name} at {emoticon.url}") 35 | continue 36 | 37 | session.add(emoticon) 38 | 39 | session.commit() -------------------------------------------------------------------------------- /builder_parts/facets.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | from valve2json import DotaFiles, DotaPaths 5 | 6 | 7 | def load(): 8 | session.query(Facet).delete() 9 | print("Facets") 10 | 11 | current_id = 1 12 | 13 | # load all of the item scripts data information 14 | data = DotaFiles.npc_heroes.read()["DOTAHeroes"] 15 | progress = ProgressBar(len(data), title="- loading from hero scripts") 16 | for heroname in data: 17 | progress.tick() 18 | if(heroname == "Version" or 19 | heroname == "npc_dota_hero_target_dummy" or 20 | heroname == "npc_dota_hero_base"): 21 | continue 22 | 23 | hero_data = data[heroname] 24 | 25 | if "Facets" not in hero_data: 26 | continue 27 | 28 | slot = 0 29 | for facetname in hero_data["Facets"]: 30 | facet_data = hero_data["Facets"][facetname] 31 | 32 | if facet_data.get("Deprecated") == "true": 33 | slot += 1 # skip this but it matters for the slot id 34 | continue 35 | 36 | facet = Facet() 37 | 38 | facet.id = current_id 39 | facet.name = facetname 40 | # facet.hero_id = "" 41 | facet.icon_name = facet_data["Icon"].lower() 42 | facet.icon = DotaPaths.facet_icon_images + facet.icon_name + "_png.png" 43 | facet.color = facet_data["Color"] 44 | facet.gradient_id = int(facet_data.get("GradientID", 0)) 45 | facet.slot = slot 46 | 47 | facet.ability_special = "[]" 48 | facet.json_data = json.dumps(facet_data, indent=4) 49 | 50 | session.add(facet) 51 | current_id += 1 52 | slot += 1 53 | 54 | # abilities_english 55 | # DOTA_Tooltip_Facet_drow_ranger_sidestep (name) 56 | # DOTA_Tooltip_ability_drow_ranger_multishot_Facet_drow_ranger_sidestep (description/bullet on a related ability) 57 | # DOTA_Tooltip_Facet_drow_ranger_vantage_point_Description (description) 58 | # DOTA_Tooltip_ability_vengefulspirit_soul_strike_bat_tooltip (BASE ATTACK TIME: xx) 59 | # "DOTA_Tooltip_ability_abaddon_the_quickening_Note0": "Every time a unit dies within %radius% range of Abaddon, reduce all of his Cooldowns by %cooldown_reduction_creeps% seconds if they are a creep or %cooldown_reduction_heroes% seconds if they are a hero.", 60 | # "DOTA_Tooltip_facet_alchemist_seed_money_Description": "Alchemist starts with {s:bonus_starting_gold_bonus} more gold.", 61 | 62 | # string localization for facets done in abilities.py cuz we need our ability_special stuff filled in 63 | 64 | session.commit() 65 | -------------------------------------------------------------------------------- /builder_parts/heroes.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | from valve2json import DotaFiles, DotaPaths 5 | import re 6 | 7 | attribute_dict = { 8 | "DOTA_ATTRIBUTE_STRENGTH": "strength", 9 | "DOTA_ATTRIBUTE_AGILITY": "agility", 10 | "DOTA_ATTRIBUTE_INTELLECT": "intelligence", 11 | "DOTA_ATTRIBUTE_ALL": "universal" 12 | } 13 | 14 | def load(): 15 | session.query(Hero).delete() 16 | print("Heroes") 17 | 18 | # load all of the hero scripts data information 19 | data = DotaFiles.npc_heroes.read()["DOTAHeroes"] 20 | progress = ProgressBar(len(data), title="- loading from hero scripts") 21 | for heroname in data: 22 | progress.tick() 23 | if(heroname == "Version" or 24 | heroname == "npc_dota_hero_target_dummy" or 25 | heroname == "npc_dota_hero_base"): 26 | continue 27 | 28 | hero = Hero() 29 | hero_data = data[heroname] 30 | 31 | def get_val(key): 32 | if key in hero_data: 33 | return hero_data[key] 34 | else: 35 | return data["npc_dota_hero_base"].get(key) 36 | 37 | hero.full_name = heroname 38 | hero.media_name = hero_data['VoiceFile'][37:-9] 39 | hero.name = heroname.replace("npc_dota_hero_", "") 40 | hero.id = get_val('HeroID') 41 | hero.team = get_val('Team') 42 | hero.base_health_regen = get_val('StatusHealthRegen') 43 | hero.base_mana_regen = get_val('StatusManaRegen') 44 | hero.base_movement = get_val('MovementSpeed') 45 | hero.base_attack_speed = get_val('BaseAttackSpeed') 46 | hero.turn_rate = get_val('MovementTurnRate') 47 | hero.base_armor = get_val('ArmorPhysical') 48 | hero.magic_resistance = get_val('MagicalResistance') 49 | hero.attack_range = get_val('AttackRange') 50 | hero.attack_projectile_speed = get_val('ProjectileSpeed') 51 | hero.attack_damage_min = get_val('AttackDamageMin') 52 | hero.attack_damage_max = get_val('AttackDamageMax') 53 | hero.attack_rate = get_val('AttackRate') 54 | hero.attack_point = get_val('AttackAnimationPoint') 55 | hero.attr_primary = attribute_dict[get_val('AttributePrimary')] 56 | hero.attr_strength_base = get_val('AttributeBaseStrength') 57 | hero.attr_strength_gain = get_val('AttributeStrengthGain') 58 | hero.attr_intelligence_base = get_val('AttributeBaseIntelligence') 59 | hero.attr_intelligence_gain = get_val('AttributeIntelligenceGain') 60 | hero.attr_agility_base = get_val('AttributeBaseAgility') 61 | hero.attr_agility_gain = get_val('AttributeAgilityGain') 62 | hero.vision_day = get_val('VisionDaytimeRange') 63 | hero.vision_night = get_val('VisionNighttimeRange') 64 | hero.is_melee = get_val('AttackCapabilities') == "DOTA_UNIT_CAP_MELEE_ATTACK" 65 | hero.material = get_val('GibType') 66 | hero.legs = get_val('Legs') 67 | hero.roles = hero_data.get('Role', '').replace(',', '|') 68 | hero.role_levels = hero_data.get('Rolelevels', '').replace(',', '|') 69 | glow_color = hero_data.get('HeroGlowColor', None) 70 | hero.color = "#ffffff" # should have a default color 71 | if glow_color: 72 | hero.color = "#{0:02x}{1:02x}{2:02x}".format(*map(int, glow_color.split(' '))) 73 | 74 | hero.json_data = json.dumps(hero_data, indent=4) 75 | 76 | talents = [] 77 | 78 | # Link facets 79 | for facetname in hero_data.get("Facets", []): 80 | facet = session.query(Facet).filter_by(name=facetname).first() 81 | if facet is None: 82 | continue 83 | facet.hero_id = hero.id 84 | facet_data = hero_data["Facets"][facetname] 85 | for ability in facet_data.get("Abilities", []): 86 | abilityinfo = facet_data["Abilities"][ability] 87 | abilityname = abilityinfo["AbilityName"] 88 | ability = session.query(Ability).filter_by(name=abilityname).first() 89 | if ability.facet_id != None: 90 | printerr(f"Ability {ability.name} has an existing facet_id {ability.facet_id}, being overridden by {facet.id}") 91 | ability.facet_id = facet.id 92 | ability.hero_id = hero.id 93 | if "AbilityIndex" in abilityinfo: 94 | ability.slot = int(abilityinfo["AbilityIndex"]) + 1 95 | 96 | # Link abilities 97 | for slot in range(1, 30): 98 | if "Ability" + str(slot) in hero_data: 99 | abilityname = hero_data["Ability" + str(slot)] 100 | if abilityname == "generic_hidden": 101 | continue 102 | ability = session.query(Ability).filter_by(name=abilityname).first() 103 | if ability: 104 | if not ability.name.startswith("special_bonus"): 105 | ability.hero_id = hero.id 106 | ability.slot = slot 107 | 108 | session.add(hero) 109 | 110 | 111 | print("- loading hero names from abilities_lang files") 112 | # Load hero names 113 | lang_data = DotaFiles.lang_abilities 114 | for hero in session.query(Hero): 115 | for lang, data in lang_data: 116 | data = data.read()["lang"]["Tokens"] 117 | hero_full_name = hero.full_name + ":n" 118 | 119 | localized_name = data.get(hero_full_name, "") 120 | localized_name = re.sub(r"#\|[fm]\|#", "", localized_name) 121 | if lang == "english": 122 | hero.localized_name = localized_name 123 | else: 124 | addLocaleString(session, lang, hero, "localized_name", localized_name) 125 | 126 | print("- loading hero hype from dota_lang files") 127 | # load hero hype 128 | lang_data = DotaFiles.lang_dota 129 | for hero in session.query(Hero): 130 | for lang, data in lang_data: 131 | data = data.read()["lang"]["Tokens"] 132 | 133 | hype = clean_description(data.get(hero.full_name + "_hype")) 134 | if lang == "english": 135 | hero.hype = hype 136 | else: 137 | addLocaleString(session, lang, hero, "hype", hype) 138 | 139 | 140 | print("- loading bio from hero lore files") 141 | # Load bio from hero lore file 142 | lang_data = DotaFiles.lang_hero_lore 143 | for hero in session.query(Hero): 144 | for lang, data in lang_data: 145 | data = data.read()["lang"]["Tokens"] 146 | bio = clean_description(data.get(hero.full_name + "_bio")) 147 | if lang == "english": 148 | hero.bio = bio 149 | else: 150 | addLocaleString(session, lang, hero, "bio", bio) 151 | 152 | 153 | print("- adding hero image files") 154 | # Add img files to hero 155 | for hero in session.query(Hero): 156 | file_ending = hero.full_name + "_png.png" 157 | hero.icon = DotaPaths.hero_icon_images + file_ending 158 | hero.image = DotaPaths.hero_side_images + file_ending 159 | hero.portrait = DotaPaths.hero_selection_images + file_ending 160 | 161 | print("- adding hero real names") 162 | data = read_json("builderdata/hero_names.json") 163 | for hero in session.query(Hero): 164 | hero.real_name = data.get(hero.name, "") 165 | 166 | print("- adding hero aliases") 167 | data = read_json("builderdata/hero_aliases.json") 168 | for hero in session.query(Hero): 169 | aliases = [] 170 | aliases.append(hero.name.replace("_", " ")) 171 | text = re.sub(r'[^a-z^\s]', r'', hero.localized_name.replace("_", " ").lower()) 172 | if text not in aliases: 173 | aliases.append(text) 174 | if hero.real_name != "": 175 | aliases.append(re.sub(r'[^a-z^\s]', r'', hero.real_name.lower())) 176 | aliases.extend(data.get(hero.name, [])) 177 | hero.aliases = "|".join(aliases) 178 | 179 | print("- adding hero colors") 180 | data = read_json("builderdata/hero_colors.json") 181 | for hero_name in data: 182 | hero = session.query(Hero).filter_by(name=hero_name).first() 183 | hero.color = data[hero_name] 184 | 185 | 186 | 187 | session.commit() 188 | -------------------------------------------------------------------------------- /builder_parts/items.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | from valve2json import DotaFiles, DotaPaths 5 | 6 | 7 | def build_replacements_dict(item: Item): 8 | specials = json.loads(item.ability_special, object_pairs_hook=OrderedDict) 9 | result = { 10 | "abilityhealthcost": item.health_cost, 11 | "abilitychargerestoretime": item.cooldown, 12 | "abilitycooldown": item.cooldown, 13 | "abilityduration": item.duration, 14 | "abilitycastrange": item.cast_range, 15 | "customval_team_tomes_used": "0", 16 | "abilitychanneltime": json.loads(item.json_data).get("AbilityChannelTime", "") 17 | } 18 | for attrib in specials: 19 | if attrib["key"] not in result: 20 | result[attrib["key"]] = attrib["value"] 21 | return result 22 | 23 | def load(): 24 | session.query(Item).delete() 25 | print("Items") 26 | 27 | added_ids = [] 28 | 29 | item_name_fixes = { 30 | "item_trident1": "item_trident" 31 | } 32 | print("- loading items from item scripts") 33 | 34 | # load all of the item scripts data information 35 | item_id_map = DotaFiles.npc_ids.read()["DOTAAbilityIDs"]["ItemAbilities"]["Locked"] 36 | data = DotaFiles.items.read()["DOTAAbilities"] 37 | for itemname in data: 38 | if itemname == "Version": 39 | continue 40 | item_data = data[itemname] 41 | if item_data.get('IsObsolete') == "1": 42 | continue # ignore obsolete items 43 | item = Item() 44 | 45 | item.name = item_name_fixes.get(itemname, itemname) 46 | item.id = item_id_map[item.name] 47 | item.cost = item_data.get('ItemCost') 48 | item.aliases = "|".join(item_data.get("ItemAliases", "").split(";")) # moved this cuz they moved it to the localizations 49 | item.quality = item_data.get("ItemQuality") 50 | item.health_cost = clean_values(item_data.get('AbilityHealthCost')) 51 | item.mana_cost = clean_values(item_data.get('AbilityManaCost')) 52 | item.cooldown = clean_values(item_data.get('AbilityCooldown')) 53 | if item_data.get('AbilityChargeRestoreTime'): 54 | item.cooldown = clean_values(item_data.get('AbilityChargeRestoreTime')) 55 | item.cast_range = clean_values(item_data.get('AbilityCastRange')) 56 | item.duration = clean_values(item_data.get('AbilityDuration')) 57 | item.base_level = item_data.get("ItemBaseLevel") 58 | item.secret_shop = item_data.get("SecretShop") == "1" 59 | item.shop_tags = "|".join(item_data.get("ItemShopTags", "").split(";")) 60 | item.ability_special = json.dumps(get_ability_special(item_data, item.name), indent=4) 61 | item.is_neutral_enhancement = item_data.get("ItemIsNeutralPassiveDrop") == "1" 62 | 63 | item.json_data = json.dumps(item_data, indent=4) 64 | 65 | if item.id in added_ids: 66 | print(f"duplicate id on: {itemname}") 67 | continue 68 | added_ids.append(item.id) 69 | 70 | session.add(item) 71 | 72 | 73 | print("- adding item aliases") 74 | data = read_json("builderdata/item_aliases.json") 75 | for item in session.query(Item): 76 | aliases = item.aliases.split("|") 77 | aliases.extend(data.get(item.name, [])) 78 | item.aliases = "|".join(aliases) 79 | 80 | # Load additional information from the dota_english.txt file 81 | english_data = DotaFiles.abilities_english.read()["lang"]["Tokens"] 82 | lang_data = DotaFiles.lang_abilities 83 | progress = ProgressBar(session.query(Item).count(), title="- loading item data from lang files") 84 | for item in session.query(Item): 85 | progress.tick() 86 | item_tooltip = "DOTA_Tooltip_Ability_" + item.name 87 | item_tooltip2 = "DOTA_Tooltip_ability_" + item.name 88 | 89 | ability_special = json.loads(item.ability_special, object_pairs_hook=OrderedDict) 90 | ability_special = ability_special_add_header(ability_special, english_data, item.name) 91 | item.ability_special = json.dumps(ability_special, indent=4) 92 | replacements_dict = build_replacements_dict(item) 93 | 94 | new_aliases = english_data.get(f"DOTA_SearchAlias_Ability_{item.name}", "") 95 | new_aliases = new_aliases.split(";") 96 | aliases = item.aliases.split("|") 97 | aliases.extend(new_aliases) 98 | aliases = list(filter(lambda a: a != "", aliases)) 99 | item.aliases = "|".join(aliases) 100 | 101 | 102 | for lang, data in lang_data: 103 | data = CaseInsensitiveDict(data.read()["lang"]["Tokens"], remove_colons=True) 104 | info = {} 105 | info["localized_name"] = data.get(item_tooltip, item.name) 106 | info["description"] = data.get(item_tooltip + "_Description", data.get(item_tooltip2 + "_Description", "")) 107 | info["lore"] = data.get(item_tooltip + "_Lore", data.get(item_tooltip2 + "_Lore", "")) 108 | 109 | report_errors = lang == "english" 110 | 111 | info["description"] = clean_description(info["description"], replacements_dict, base_level=item.base_level, report_errors=report_errors) 112 | info["lore"] = clean_description(info["lore"]) 113 | 114 | if lang == "english": 115 | for key in info: 116 | setattr(item, key, info[key]) 117 | else: 118 | for key in info: 119 | addLocaleString(session, lang, item, key, info[key]) 120 | 121 | print("- adding neutral item data") 122 | data = DotaFiles.neutral_items.read()["neutral_items"]["neutral_tiers"] 123 | item_tier_map = {} 124 | for tier in data: 125 | for name in data[tier]["items"]: 126 | if name not in item_tier_map: 127 | item_tier_map[name] = tier 128 | for name in data[tier]["enhancements"]: 129 | if name not in item_tier_map: 130 | item_tier_map[name] = tier 131 | for item in session.query(Item): 132 | if item.name in item_tier_map: 133 | item.neutral_tier = item_tier_map[item.name] 134 | 135 | print("- linking recipes") 136 | for recipe in session.query(Item): 137 | json_data = json.loads(recipe.json_data) 138 | if json_data.get("ItemRecipe", "0") != "0": 139 | components = list(json_data.get("ItemRequirements", {"01": None}).values())[0] 140 | if components is None: 141 | continue 142 | components = components.replace(";", " ").replace("*", "").strip().split(" ") 143 | if recipe.cost != 0: 144 | components.append(recipe.name) 145 | crafted_item_name = json_data.get("ItemResult") 146 | crafted_item = session.query(Item).filter_by(name=crafted_item_name).first() 147 | if not crafted_item: 148 | raise ValueError(f"Can't find crafted item {crafted_item_name}") 149 | crafted_item.recipe = "|".join(components) 150 | if recipe.neutral_tier is not None: # stuff like trident 151 | crafted_item.neutral_tier = recipe.neutral_tier 152 | 153 | if recipe.cost == 0 and not json_data.get("ItemIsNeutralDrop"): 154 | session.delete(recipe) 155 | 156 | print("- adding item icon files") 157 | # Add img files to item 158 | for item in session.query(Item): 159 | iconpath = (DotaPaths.item_images + item.name.replace("item_", "") + "_png.png").lower() 160 | if os.path.isfile(config.vpk_path + iconpath): 161 | item.icon = iconpath 162 | else: 163 | if "recipe" in item.name: 164 | item.icon = DotaPaths.item_images + "recipe.png" 165 | else: 166 | printerr(f"icon file not found for {item.name}") 167 | 168 | session.commit() 169 | -------------------------------------------------------------------------------- /builder_parts/loadingscreens.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | from valve2json import DotaFiles, ItemsGame 5 | from PIL import Image 6 | import datetime 7 | import os 8 | import colorgram 9 | import colorsys 10 | 11 | def rgb_to_hsv(rgb): 12 | rgb = tuple(map(lambda v: v / 255.0, rgb)) 13 | hsv = colorsys.rgb_to_hsv(*rgb) 14 | return tuple(map(lambda v: int(v * 255), hsv)) 15 | 16 | def load(): 17 | session.query(LoadingScreen).delete() 18 | print("loadingscreens") 19 | 20 | items_game = ItemsGame() 21 | 22 | custom_paths = { 23 | "Default Loading Screen": "/panorama/images/loadingscreens/default/startup_background_logo_png.png" 24 | } 25 | 26 | # this will be used later for assigning category 27 | couriers = list(map(lambda i: i.get("name"), items_game.by_prefab["courier"])) 28 | 29 | print("- loading loadingscreens from items_game") 30 | # load all of the item scripts data information 31 | for data in items_game.by_prefab["loading_screen"]: 32 | loadingscreen = LoadingScreen() 33 | loadingscreen.id = int(data["id"]) 34 | loadingscreen.name = data.get("name") 35 | date_array = list(map(int, data.get("creation_date").split("-"))) 36 | loadingscreen.creation_date = datetime.date(date_array[0], date_array[1], date_array[2]) 37 | loadingscreen.category = "other" 38 | 39 | 40 | if loadingscreen.name in custom_paths: 41 | loadingscreen.image = custom_paths[loadingscreen.name] 42 | else: 43 | ass_mod = items_game.get_asset_modifier(data, "loading_screen") 44 | if ass_mod: 45 | image_path = ass_mod.asset 46 | if ".vtex" in image_path: 47 | image_path = image_path.replace(".vtex", ".png") 48 | elif ".png" in image_path: 49 | image_path = image_path.replace(".png", "_png.png") 50 | else: 51 | image_path += "_tga.png" 52 | loadingscreen.image = f"/panorama/images/{image_path}" 53 | 54 | loadingscreen.thumbnail = os.path.dirname(loadingscreen.image) + "/thumbnail.png" 55 | 56 | if not os.path.exists(config.vpk_path + loadingscreen.image): 57 | printerr(f"Couldn't find loadingscreen at {loadingscreen.image}, skipping") 58 | continue # skip this loadingscreen because it doesn't exist 59 | 60 | session.add(loadingscreen) 61 | 62 | progress = ProgressBar(session.query(LoadingScreen).count(), title="- making thumbnails and retrieving colors") 63 | for loadingscreen in session.query(LoadingScreen): 64 | progress.tick() 65 | 66 | if not os.path.exists(config.vpk_path + loadingscreen.thumbnail): 67 | image = Image.open(config.vpk_path + loadingscreen.image) 68 | image.thumbnail((128, 64), Image.LANCZOS) 69 | image.save(config.vpk_path + loadingscreen.thumbnail, format="PNG") 70 | 71 | colors = colorgram.extract(config.vpk_path + loadingscreen.thumbnail, 5) 72 | 73 | loadingscreen.color = "#{0:02x}{1:02x}{2:02x}".format(*colors[0].rgb) 74 | hsv = rgb_to_hsv(colors[0].rgb) 75 | loadingscreen.hue = hsv[0] 76 | loadingscreen.saturation = hsv[1] 77 | loadingscreen.value = hsv[2] 78 | 79 | ## Categories: 80 | # hero_set 81 | # hud_skin 82 | # tournament 83 | # courier 84 | # other 85 | 86 | item_type_to_category = { 87 | "#DOTA_WearableType_Hud_Skin_Bundle": "hud_skin", 88 | "#DOTA_WearableType_Tournament_Bundle": "tournament" 89 | } 90 | 91 | print("- associating item packs") 92 | for data in items_game.by_prefab["bundle"]: 93 | for name in data.get("bundle", []): 94 | for loadingscreen in session.query(LoadingScreen).filter_by(name=name): 95 | heroes = data.get("used_by_heroes", {}) 96 | for hero_name in heroes: 97 | hero = session.query(Hero).filter_by(full_name=hero_name).first() 98 | if hero: 99 | loadingscreen.hero_ids = str(hero.id) 100 | loadingscreen.category = "hero_set" 101 | if loadingscreen.category == "hero_set": 102 | continue 103 | category = item_type_to_category.get(data.get("item_type_name")) 104 | if category: 105 | loadingscreen.category = category 106 | continue 107 | if any(x in couriers for x in data.get("bundle", [])): 108 | loadingscreen.category = "courier" 109 | continue 110 | 111 | 112 | print("- linking heroes") 113 | data = read_json("builderdata/loadingscreen_heroes.json") 114 | for screen in session.query(LoadingScreen): 115 | if screen.name in data: 116 | heroes = [] 117 | if screen.hero_ids: 118 | heroes.append(screen.hero_ids) 119 | for heroname in data[screen.name]: 120 | hero = session.query(Hero).filter_by(name=heroname).first() 121 | heroes.append(str(hero.id)) 122 | screen.hero_ids = "|".join(heroes) 123 | 124 | session.commit() -------------------------------------------------------------------------------- /builder_parts/patches.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | import requests 5 | from datetime import datetime, time 6 | 7 | 8 | def load(): 9 | session.query(Patch).delete() 10 | print("Patches") 11 | 12 | print("- loading older patches from wiki data") 13 | # old patches and their dates scraped from dota 2 wiki 14 | data = read_json("builderdata/old_patch_dates.json") 15 | for patch_number in data: 16 | datestring = data[patch_number] 17 | patch = Patch() 18 | 19 | patch.number = patch_number 20 | if datestring is not None: 21 | date = datetime.strptime(datestring, "%Y-%m-%d") 22 | timeofday = time(hour=21) # use ~2pm PST as an estimate 23 | date = datetime.combine(date, timeofday) 24 | patch.timestamp = date 25 | patch.wiki_url = f"https://dota2.gamepedia.com/Version_{patch.number}" 26 | 27 | session.add(patch) 28 | 29 | print("- loading patches from api") 30 | # load all of the item scripts data information 31 | data = requests.get("https://www.dota2.com/datafeed/patchnoteslist?language=english").json() 32 | for patch_data in data["patches"]: 33 | patch = Patch() 34 | 35 | patch.number = patch_data["patch_number"] 36 | patch.timestamp = datetime.fromtimestamp(patch_data["patch_timestamp"]) 37 | patch.wiki_url = f"https://dota2.gamepedia.com/Version_{patch.number}" 38 | patch.dota_url = f"https://www.dota2.com/patches/{patch.number}" 39 | if "patch_website" in patch_data: 40 | patch.custom_url = f"https://www.dota2.com/{patch_data['patch_website']}" 41 | 42 | session.add(patch) 43 | 44 | session.commit() -------------------------------------------------------------------------------- /builder_parts/responses.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | from valve2json import valve_readfile, DotaFiles, DotaPaths 5 | from vccd_reader import ClosedCaptionFile 6 | import criteria_sentancing 7 | import re 8 | import os 9 | 10 | file_types = [ "mp3", "wav", "aac" ] 11 | 12 | 13 | def load(): 14 | session.query(Response).delete() 15 | session.query(Criterion).delete() 16 | print("Responses") 17 | 18 | progress = ProgressBar(session.query(Voice).count(), title="- loading from vsnd files:") 19 | for voice in session.query(Voice): 20 | progress.tick() 21 | 22 | if not voice.media_name: 23 | continue 24 | 25 | vsndevts_path = f"/soundevents/voscripts/game_sounds_vo_{voice.media_name}.vsndevts" 26 | vsndevts_data = valve_readfile(vsndevts_path, "vsndevts") 27 | captionsFilename = f"{config.vpk_path}/resource/subtitles/subtitles_{voice.media_name}_english.dat" 28 | captionsFilename2 = f"{config.vpk_path}/resource/subtitles/subtitles_{voice.media_name}_english_staging.dat" 29 | if os.path.exists(captionsFilename): 30 | captionsFile = ClosedCaptionFile(captionsFilename) 31 | elif os.path.exists(captionsFilename2): 32 | captionsFile = ClosedCaptionFile(captionsFilename2) 33 | else: 34 | printerr(f"missing {captionsFilename}") 35 | captionsFile = None 36 | 37 | for key in vsndevts_data: 38 | data = vsndevts_data[key] 39 | if data is None: 40 | continue 41 | vsnd_file = data["vsnd_files"] 42 | if not isinstance(vsnd_file, str): 43 | vsnd_file = vsnd_file[0] # this is an array, get the first one 44 | filename = "/" + vsnd_file.replace("vsnd", "mp3") 45 | 46 | response = Response() 47 | response.fullname = key 48 | response.name = os.path.basename(filename).replace(".mp3", "") 49 | 50 | for ext in file_types: 51 | newname = filename.replace(".mp3", f".{ext}") 52 | if os.path.exists(config.vpk_path + newname): 53 | filename = newname 54 | break 55 | 56 | if not os.path.exists(config.vpk_path + filename): 57 | printerr(f"Missing file: {filename}") 58 | 59 | response.mp3 = filename 60 | response.voice_id = voice.id 61 | response.hero_id = voice.hero_id 62 | response.criteria = "" 63 | 64 | if captionsFile: 65 | text = captionsFile.lookup(response.fullname) 66 | if text: 67 | response.text = text 68 | response.text_simple = text.replace("...", " ") 69 | response.text_simple = " " + re.sub(r'[^a-z^0-9^A-Z^\s]', r'', response.text_simple).lower() + " " 70 | response.text_simple = re.sub(r'\s+', r' ', response.text_simple) 71 | else: 72 | response.text = "" 73 | 74 | session.add(response) 75 | 76 | print("- loading criteria") 77 | rules = {} 78 | groups = {} 79 | criteria = {} 80 | # Load response_rules 81 | for root, dirs, files in os.walk(config.vpk_path + DotaPaths.response_rules): 82 | for file in files: 83 | data = valve_readfile(DotaPaths.response_rules + file, "rules") 84 | for key in data: 85 | if key.startswith("rule_"): 86 | rules[key[5:]] = data[key] 87 | elif key.startswith("response_"): 88 | groups[key[9:]] = data[key] 89 | elif key.startswith("criterion_"): 90 | criteria[key[10:]] = data[key] 91 | 92 | for key in criteria: 93 | criterion = Criterion() 94 | criterion.name = key 95 | vals = criteria[key].split(" ") 96 | criterion.matchkey = vals[0] 97 | criterion.matchvalue = vals[1] 98 | if "weight" in vals: 99 | criterion.weight = float(vals[vals.index("weight") + 1]) 100 | else: 101 | criterion.weight = 1.0 102 | criterion.required = "required" in vals 103 | session.add(criterion) 104 | 105 | voice_linker = {} 106 | 107 | 108 | custom_voice_criteria = { # because valve did customresponse:arcana for 2 things 109 | "Tempest Helm of the Thundergod": "IsZeusEconArcana" 110 | } 111 | # fix up voice.criteria 112 | for voice in session.query(Voice): 113 | if voice.criteria: 114 | if voice.name in custom_voice_criteria: 115 | voice.criteria = custom_voice_criteria[voice.name] 116 | continue 117 | crits = [] 118 | for crit in voice.criteria.split("|"): 119 | key, value = crit.split(":") 120 | realcrit = session.query(Criterion).filter_by(matchkey=key).filter_by(matchvalue=value).first() 121 | if realcrit: 122 | crits.append(realcrit.name) 123 | voice.criteria = "|".join(crits) 124 | pattern = f"(^|\|| ){voice.criteria}($|\|| )" 125 | voice_linker[pattern] = voice 126 | 127 | progress = ProgressBar(len(rules) + session.query(Response).count(), title="- linking rules:") 128 | pre_responses = {} 129 | for key in rules: 130 | progress.tick() 131 | response_criteria = rules[key]['criteria'].rstrip() 132 | for fullname in groups[rules[key]['response']]: 133 | if fullname not in pre_responses: 134 | pre_responses[fullname] = response_criteria 135 | else: 136 | pre_responses[fullname] += "|" + response_criteria 137 | 138 | for response in session.query(Response): 139 | progress.tick() 140 | if response.fullname in pre_responses: 141 | response.criteria = pre_responses[response.fullname] 142 | for pattern, voice in voice_linker.items(): 143 | if re.search(pattern, response.criteria): 144 | response.voice_id = voice.id 145 | 146 | print("- adding hero chatwheel criteria") 147 | chatwheel_criteria = [] 148 | chatwheel_data = DotaFiles.chat_wheel_heroes.read()["chat_wheel"] 149 | for hero in chatwheel_data["hero_messages"]: 150 | for chatwheel_id in chatwheel_data["hero_messages"][hero]: 151 | chatwheel_message = chatwheel_data["hero_messages"][hero][chatwheel_id] 152 | badge_tier = chatwheel_message['unlock_hero_badge_tier'] 153 | if badge_tier not in chatwheel_criteria: 154 | chatwheel_criteria.append(badge_tier) 155 | new_criteria = f"HeroChatWheel {badge_tier}" 156 | for response in session.query(Response).filter_by(fullname=chatwheel_message["sound"]): 157 | if response.criteria != "": 158 | new_criteria = "|" + new_criteria 159 | response.criteria += new_criteria 160 | for crit in chatwheel_criteria: 161 | criterion = Criterion() 162 | criterion.name = crit 163 | criterion.matchkey = "badge_tier" 164 | criterion.matchvalue = crit.replace("Tier", "").lower() 165 | criterion.weight = 1.0 166 | criterion.required = True 167 | session.add(criterion) 168 | criterion = Criterion() 169 | criterion.name = "HeroChatWheel" 170 | criterion.matchkey = "Concept" 171 | criterion.matchvalue = "DOTA_CHATWHEEL_THINGIE" 172 | criterion.weight = 1.0 173 | criterion.required = True 174 | session.add(criterion) 175 | 176 | 177 | print("- generating pretty criteria") 178 | criteria_sentancing.load_pretty_criteria(session) 179 | 180 | session.commit() -------------------------------------------------------------------------------- /builder_parts/talents.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | import re 5 | 6 | # Talent slots appear like this on the tree: 7 | 8 | # 7 6 9 | # 5 4 10 | # 3 2 11 | # 1 0 12 | 13 | def load(): 14 | session.query(Talent).delete() 15 | print("Talents") 16 | 17 | link_fixes = { 18 | "axe_counter_culling_blade": "axe_culling_blade", 19 | "troll_warlord_whirling_axes": "troll_warlord_whirling_axes_ranged troll_warlord_whirling_axes_melee", 20 | "invoker_sunstrike": "invoker_sun_strike", 21 | "morphling_adaptive_strike": "morphling_adaptive_strike_agi morphling_adaptive_strike_str" 22 | } 23 | 24 | # load all talents from heroes 25 | talent_names = [] 26 | progress = ProgressBar(session.query(Hero).count(), title="- loading talents from heroes") 27 | for hero in session.query(Hero): 28 | # Link abilities and add talents 29 | progress.tick() 30 | talent_slot = 0 31 | hero_data = json.loads(hero.json_data) 32 | for slot in range(1, 30): 33 | if "Ability" + str(slot) in hero_data: 34 | ability = session.query(Ability).filter_by(name=hero_data["Ability" + str(slot)]).first() 35 | if ability: 36 | if ability.name.startswith("special_bonus"): 37 | # create a new talent 38 | talent = Talent() 39 | talent.hero_id = hero.id 40 | talent.ability_id = ability.id 41 | talent.slot = talent_slot 42 | talent_names.append(ability.name) 43 | # link talents 44 | ability_data = json.loads(ability.json_data) 45 | ability_specials = ability_data.get("AbilitySpecial", {}).values() 46 | for special in ability_specials: 47 | link = special.get("ad_linked_abilities") 48 | if not link: 49 | link = ability_data.get("ad_linked_abilities") 50 | if link: 51 | if link in [ "special_bonus_inherent" ]: 52 | continue # doesn't link to a different ability 53 | if link in link_fixes: 54 | link = link_fixes[link] 55 | link = link.replace(" ", "|") 56 | talent.linked_abilities = link 57 | 58 | for pattern in ATTRIBUTE_TEMPLATE_PATTERNS: 59 | if re.search(pattern, ability.localized_name): 60 | printerr(f"Missing attribute in {hero.localized_name}'s talent: '{ability.localized_name}'") 61 | if re.search(pattern, ability.description): 62 | printerr(f"Missing attribute in {hero.localized_name}'s talent: '{ability.description}'") 63 | 64 | session.add(talent) 65 | talent_slot += 1 66 | if talent_slot != 8: 67 | raise ValueError("{} only has {} talents?".format(hero.localized_name, talent_slot)) 68 | 69 | talent_names = "|".join(talent_names) 70 | print("- scanning abilities to link them to talents where necessary") 71 | for ability in session.query(Ability): 72 | if "AbilityValues" in ability.json_data and re.search(talent_names, ability.json_data): 73 | ability_data = json.loads(ability.json_data) 74 | for value in ability_data["AbilityValues"].values(): 75 | if not isinstance(value, str): 76 | for key in value: 77 | if re.match(f"^({talent_names})$", key): 78 | talent_ability = session.query(Ability).filter_by(name=key).first() 79 | talent = session.query(Talent).filter_by(ability_id=talent_ability.id).first() 80 | if talent.linked_abilities is None: 81 | talent.linked_abilities = ability.name 82 | elif ability.name not in talent.linked_abilities: 83 | talent.linked_abilities += f"|{ability.name}" 84 | 85 | 86 | 87 | 88 | # load ability draft gold talents 89 | print("- loading ability draft talents") 90 | for ability in session.query(Ability): 91 | gold_talent_match = re.match(r"ad_special_bonus_gold_(lvl\d+_.)", ability.name) 92 | if gold_talent_match: 93 | talent_slot = [ 94 | "lvl10_l", # note that "l" apparently means right, and "r" apparently means left. 95 | "lvl10_r", 96 | "lvl15_l", 97 | "lvl15_r", 98 | "lvl20_l", 99 | "lvl20_r", 100 | "lvl25_l", 101 | "lvl25_r" 102 | ].index(gold_talent_match.group(1)) 103 | talent = Talent() 104 | talent.ability_id = ability.id 105 | talent.slot = talent_slot 106 | session.add(talent) 107 | 108 | session.commit() -------------------------------------------------------------------------------- /builder_parts/voices.py: -------------------------------------------------------------------------------- 1 | from builder import session 2 | from dotabase import * 3 | from utils import * 4 | from valve2json import ItemsGame 5 | 6 | def name_to_url(name): 7 | conversions = { 8 | ' ': '_', 9 | '\'': '%27', 10 | '.': '%2E', 11 | '&': '%26' 12 | } 13 | for key in conversions: 14 | name = name.replace(key, conversions[key]) 15 | return name 16 | 17 | def vsndevts_to_media_name(text): 18 | text = text.replace("soundevents/voscripts/game_sounds_vo_", "") 19 | text = text.replace(".vsndevts", "") 20 | return text 21 | 22 | def load(): 23 | session.query(Voice).delete() 24 | print("Voices") 25 | 26 | print("- loading from heroes") 27 | for hero in session.query(Hero): 28 | voice = Voice() 29 | 30 | voice.id = hero.id 31 | voice.name = hero.localized_name 32 | voice.icon = hero.icon 33 | voice.image = hero.portrait 34 | voice.url = name_to_url(hero.localized_name) + "/Responses" 35 | voice.criteria = None 36 | 37 | voice.media_name = vsndevts_to_media_name(json.loads(hero.json_data).get("VoiceFile")) 38 | voice.hero_id = hero.id 39 | 40 | session.add(voice) 41 | 42 | print("- loading cosmetics file (takes a bit)") 43 | items_game = ItemsGame() 44 | 45 | custom_urls = { 46 | "Announcer: Tuskar": "Announcer:_Tusk", 47 | "Default Announcer": "Announcer_responses", 48 | "Default Mega-Kill Announcer": "Announcer_responses", 49 | "Announcer: Bristleback": "Bristleback_Announcer_Pack", 50 | "Mega-Kills: Bristleback": "Bristleback_Announcer_Pack" 51 | } 52 | custom_media_name = { 53 | "Default Announcer": "announcer", 54 | "Default Mega-Kill Announcer": "announcer_killing_spree" 55 | } 56 | 57 | print("- loading from announcers") 58 | for announcer in items_game.by_prefab["announcer"]: 59 | voice = Voice() 60 | 61 | # the first announcer has id = 586, so this will not interfere with hero ids 62 | voice.id = int(announcer["id"]) 63 | voice.name = announcer["name"] 64 | voice.icon = "/panorama/images/icon_announcer_psd.png" 65 | voice.image = f"/panorama/images/{announcer['image_inventory']}_png.png" 66 | voice.criteria = None 67 | 68 | if voice.name in custom_urls: 69 | voice.url = custom_urls[voice.name] 70 | else: 71 | voice.url = name_to_url(announcer["name"]) 72 | 73 | if voice.name in custom_media_name: 74 | voice.media_name = custom_media_name[voice.name] 75 | else: 76 | ass_mod = items_game.get_asset_modifier(announcer, "announcer") 77 | if ass_mod: 78 | voice.media_name = ass_mod.asset.replace("npc_dota_hero_", "") 79 | 80 | session.add(voice) 81 | 82 | added_names = [] 83 | print("- loading from hero cosmetics") 84 | for item in items_game.by_prefab["wearable"]: 85 | criteria = [] 86 | for ass_mod in items_game.get_asset_modifiers(item, "response_criteria"): 87 | criteria.append(ass_mod.asset) 88 | if len(criteria) == 0: 89 | continue 90 | criteria = "|".join(map(lambda c: f"customresponse:{c}", criteria)) 91 | icon = None 92 | for ass_mod in items_game.get_asset_modifiers(item, "icon_replacement_hero_minimap"): 93 | icon = f"/panorama/images/heroes/icons/{ass_mod.modifier}_png.png" 94 | skip = False 95 | for pack in items_game.by_prefab["bundle"]: 96 | if item["name"] not in pack["bundle"]: 97 | continue 98 | for item_name in pack["bundle"]: 99 | if item_name in added_names: 100 | voice = session.query(Voice).filter_by(name=item_name).first() 101 | voice.criteria += f"|{criteria}" 102 | skip = True 103 | if not icon: 104 | related_item = items_game.item_name_dict[item_name] 105 | for ass_mod in items_game.get_asset_modifiers(related_item, "icon_replacement_hero_minimap"): 106 | icon = f"/panorama/images/heroes/icons/{ass_mod.modifier}_png.png" 107 | if icon and "npc_dota_hero_rubick_alt1" in icon: # this one is wrong in the data, so heres an ugly fix 108 | icon = f"/panorama/images/heroes/icons/npc_dota_hero_rubick_alt_png.png" 109 | break 110 | if skip: 111 | continue 112 | 113 | voice = Voice() 114 | voice.id = int(item["id"]) 115 | voice.name = item["name"] 116 | voice.image = f"/panorama/images/{item['image_inventory']}_png.png" 117 | voice.icon = icon 118 | voice.criteria = criteria 119 | voice.media_name = None 120 | 121 | for hero_name in item.get("used_by_heroes", {}): 122 | hero = session.query(Hero).filter_by(full_name=hero_name).first() 123 | if hero: 124 | voice.hero_id = hero.id 125 | if not voice.icon: 126 | voice.icon = hero.icon 127 | 128 | added_names.append(voice.name) 129 | session.add(voice) 130 | 131 | 132 | print("- associating announcer packs") 133 | for pack in items_game.by_prefab["bundle"]: 134 | if pack.get("name") == "Assembly of Announcers Pack": 135 | continue 136 | for name in pack.get("bundle", []): 137 | for voice in session.query(Voice).filter_by(name=name): 138 | voice.url = name_to_url(pack["name"]) 139 | 140 | 141 | data = read_json("builderdata/voice_actors.json") 142 | print("- adding voice actors") 143 | for voice in session.query(Voice): 144 | if str(voice.id) in data: 145 | voice.voice_actor = data[str(voice.id)] 146 | 147 | 148 | 149 | session.commit() -------------------------------------------------------------------------------- /builderdata/criteria_matchkeys.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "drop_type", 4 | "convert": { 5 | "common": "a common", 6 | "rare": "a rare", 7 | "ultra_rare": "an ultra rare" 8 | } 9 | }, 10 | { 11 | "key": "arrowhithero", 12 | "template": "Arrow hits {}", 13 | "convert": { 14 | "yes": "a hero", 15 | "no": "a creep", 16 | "roshan": "roshan", 17 | "neutral_creep": "a neutral creep" 18 | } 19 | }, 20 | { 21 | "key": "team_attacked", 22 | "convert": { 23 | "good": "Radiant", 24 | "bad": "Dire" 25 | } 26 | }, 27 | { 28 | "key": "lane", 29 | "convert": { 30 | "mid": "middle", 31 | "bot": "bottom", 32 | "top": "top", 33 | "base": "base" 34 | } 35 | }, 36 | { 37 | "key": "nagtime", 38 | "convert": { 39 | ">=7": "", 40 | ">5,<7": "medium naggy", 41 | "<3": "very naggy" 42 | }, 43 | "new_key": "nag" 44 | }, 45 | { 46 | "key": "expensive_item", 47 | "convert": { 48 | "1": "expensive" 49 | }, 50 | "new_key": "price" 51 | }, 52 | { 53 | "key": "multiplekillcount", 54 | "template": "{} heroes", 55 | "convert": { 56 | "2": "two", 57 | "3": "three", 58 | "4": "four", 59 | ">4": "more than four" 60 | }, 61 | "new_key": "hero" 62 | }, 63 | { 64 | "key": "special_spawn", 65 | "template": "Spawning in the loadout" 66 | }, 67 | { 68 | "key": "deny", 69 | "convert": { 70 | "0": "destroyed", 71 | "1": "denied" 72 | } 73 | }, 74 | { 75 | "key": "streak", 76 | "template": "{} heroes", 77 | "convert": { 78 | "2": "two", 79 | "3": "three", 80 | "4": "four", 81 | "5": "five", 82 | ">9": "more than 9", 83 | ">84,<90": "84 to 90", 84 | "default": "{}" 85 | }, 86 | "new_key": "hero" 87 | }, 88 | { 89 | "key": "badge_tier" 90 | }, 91 | { 92 | "key": "victim_team", 93 | "convert": { 94 | "radiant": "Dire", 95 | "dire": "Radiant" 96 | }, 97 | "new_key": "victor_team" 98 | }, 99 | { 100 | "key": "dire_heroes_alive", 101 | "template": "Dire", 102 | "new_key": "wiped_team" 103 | }, 104 | { 105 | "key": "radiant_heroes_alive", 106 | "template": "Radiant", 107 | "new_key": "wiped_team" 108 | }, 109 | { 110 | "key": "victim_in_fountain", 111 | "template": "in their fountain" 112 | }, 113 | { 114 | "key": "player_team", 115 | "template": "" 116 | } 117 | ] -------------------------------------------------------------------------------- /builderdata/criteria_pretty.json: -------------------------------------------------------------------------------- 1 | { 2 | "InTheBag": "In-the-bag", 3 | "Immortality": "Grabbing aegis", 4 | "Victory": "Winning a game", 5 | "Taunt": "Taunting", 6 | "Thanks": "Thanking", 7 | "Defeat": "Losing a game", 8 | "Kill": "Killing {hero|a hero|%}{ability|| with %}", 9 | "Powerup": "Activating {rune|a rune|%}", 10 | "Respawn": "Respawning", 11 | "CastOrder": "Ordered to cast {ability|an ability|%}", 12 | "CastExecute": "Casting {ability|an ability|%}{hero|| on %}", 13 | "Death": "Dying", 14 | "Deny": "Denying", 15 | "Bottling": "Bottling {rune|a rune|%}", 16 | "Move": "Moving", 17 | "ItemDrop": "Recieving {drop_type|a|%} cosmetic item", 18 | "Custom": "", 19 | "Missing": "Calling missing for {lane|a|%} lane", 20 | "Spawn": "Spawning", 21 | "Learn": "Leveling up {ability|an ability|%}", 22 | "LastHit": "Last hitting", 23 | "Purchase": "Buying {item|an {price||%} item|%}", 24 | "LevelUpAbility": "Leveling up {ability|an ability|%}", 25 | "Attack": "Attacking", 26 | "Emote": "Laughing", 27 | "IsEmoteLaugh": "", 28 | "Cooldown": "Item/Ability on cooldown", 29 | "Select": "Rare", 30 | "NoMana": "Not enough mana", 31 | "LevelUp": "Leveling up", 32 | "Pain": "Taking damage", 33 | "Followup_Generic": "Followup", 34 | "Followup_Negative": "Followup (Negative)", 35 | "Followup_Negative_Ongoing": "Followup (Ongoing Negative)", 36 | "Followup_Positive": "Followup (Positive)", 37 | 38 | "AllyNear": "{hero|An ally|%} is nearby", 39 | "IsGolem": "while in golem form", 40 | "IsTerrorDemonForm": "while in demon form", 41 | "IsInDragonForm": "while in dragon form", 42 | "IsBearForm": "while in bear form", 43 | "IsInWolfForm": "while in wolf form", 44 | "IsFirstBlood": "(First Blood)", 45 | "IsReincarnating": "and respawning from aegis", 46 | "IsGameStart": "As the game starts", 47 | 48 | "IsAnnounceHeroPick": "A player picked {hero|a hero|%}", 49 | "IsRoshanKilledGood": "Roshan killed by The Radiant", 50 | "IsRoshanKilledBad": "Roshan killed by The Dire", 51 | "IsTowerAttacked": "{team_attacked|Your|%'s}{lane|| %} tower is being attacked", 52 | "IsBarracksAttacked": "{team_attacked|Your|%'s}{lane|| %} barracks are being attacked", 53 | "IsFortAttacked": "{team_attacked|Your|%'s} ancient is being attacked", 54 | "IsShrineAttacked": "{team_attacked|Your|%'s}{lane|| %} shrine is being attacked", 55 | "IsTowerKilled": "A friendly{lane|| %} tower was {deny|destroyed|%}", 56 | "IsEnemyTowerKilled": "An enemy{lane| |'s %} tower was {deny|destroyed|%}", 57 | "IsGoodTowerKilled": "{lane|A Radiant|Radiant's %} tower was {deny|destroyed|%}", 58 | "IsBadTowerKilled": "{lane|A Dire|Dire's %} tower was {deny|destroyed|%}", 59 | "IsBarracksKilled": "A friendly{lane|| %} barracks was {deny|destroyed|%}", 60 | "IsEnemyBarracksKilled": "An enemy's{lane|| %} barracks was {deny|destroyed|%}", 61 | "IsGoodBarracksKilled": "{lane|A Radiant|Radiant's %} barracks was {deny|destroyed|%}", 62 | "IsBadBarracksKilled": "{lane|A Dire|Dire's %} barracks was {deny|destroyed|%}", 63 | "IsShrineKilled": "A friendly{lane|| %} shrine was {deny|destroyed|%}", 64 | "IsEnemyShrineKilled": "An enemy{lane| |'s %} shrine was {deny|destroyed|%}", 65 | "IsGoodShrineKilled": "{lane|A Radiant|Radiant's %} shrine was {deny|destroyed|%}", 66 | "IsBadShrineKilled": "{lane|A Dire|Dire's %} shrine was {deny|destroyed|%}", 67 | "IsKillMessage": "{hero||Killing spree of %}{victor_team|| for %}", 68 | "HeroDeath": "{wiped_team|{hero|A hero|%} was killed|% team was wiped}{victor_team|| by %}", 69 | 70 | "HeroChatWheel": "A{badge_tier|| %} hero chat wheel was used" 71 | } -------------------------------------------------------------------------------- /builderdata/hero_aliases.json: -------------------------------------------------------------------------------- 1 | { 2 | "antimage": [ "am", "magina" ], 3 | "bloodseeker": [ "bs" ], 4 | "crystal_maiden": [ "cm" ], 5 | "drow_ranger": [ "drow", "sylvanas" ], 6 | "earthshaker": [ "es", "shaker" ], 7 | "juggernaut": [ "jugg" ], 8 | "mirana": [ "potm" ], 9 | "morphling": [ "morph" ], 10 | "nevermore": [ "sf", "shadow friend" ], 11 | "phantom_lancer": [ "pl" ], 12 | "sand_king": [ "sk" ], 13 | "storm_spirit": [ "ss", "storm" ], 14 | "tiny": [ "tony" ], 15 | "vengefulspirit": [ "vs", "venge" ], 16 | "windrunner": [ "wr" ], 17 | "shadow_shaman": [ "ss" ], 18 | "tidehunter": [ "tide", "watermelon" ], 19 | "witch_doctor": [ "wd" ], 20 | "lich": [ "kelthuzad" ], 21 | "tinker": [ "tink" ], 22 | "necrolyte": [ "necro" ], 23 | "beastmaster": [ "bm", "rexar" ], 24 | "queenofpain": [ "qop" ], 25 | "venomancer": [ "veno" ], 26 | "faceless_void": [ "void", "fv" ], 27 | "skeleton_king": [ "wk" ], 28 | "death_prophet": [ "dp" ], 29 | "phantom_assassin": [ "pa" ], 30 | "templar_assassin": [ "ta" ], 31 | "dragon_knight": [ "dk" ], 32 | "rattletrap": [ "clock" ], 33 | "leshrac": [ "lesh" ], 34 | "furion": [ "np" ], 35 | "life_stealer": [ "ls" ], 36 | "omniknight": [ "omni" ], 37 | "enchantress": [ "ench", "bambi" ], 38 | "night_stalker": [ "ns" ], 39 | "broodmother": [ "brood", "spider" ], 40 | "bounty_hunter": [ "bh", "bounty" ], 41 | "jakiro": [ "jak" ], 42 | "batrider": [ "bat" ], 43 | "ancient_apparition": [ "aa" ], 44 | "ursa": [ "bear" ], 45 | "spirit_breaker": [ "sb", "bara" ], 46 | "gyrocopter": [ "gyro" ], 47 | "alchemist": [ "alch" ], 48 | "obsidian_destroyer": [ "od" ], 49 | "brewmaster": [ "brew" ], 50 | "shadow_demon": [ "sd" ], 51 | "lone_druid": [ "ld", "bear" ], 52 | "chaos_knight": [ "ck" ], 53 | "treant": [ "tree" ], 54 | "ogre_magi": [ "ogre" ], 55 | "undying": [ "zombie" ], 56 | "disruptor": [ "thrall" ], 57 | "nyx_assassin": [ "nyx" ], 58 | "naga_siren": [ "naga" ], 59 | "keeper_of_the_light": [ "kotl" ], 60 | "troll_warlord": [ "troll" ], 61 | "shredder": [ "timber" ], 62 | "bristleback": [ "bristle", "bb" ], 63 | "skywrath_mage": [ "sky" ], 64 | "abaddon": [ "arthas" ], 65 | "elder_titan": [ "et" ], 66 | "legion_commander": [ "lc" ], 67 | "ember_spirit": [ "es", "ember" ], 68 | "earth_spirit": [ "es" ], 69 | "abyssal_underlord": [ "pit lord" ], 70 | "terrorblade": [ "tb", "illidan" ], 71 | "phoenix": [ "icarus" ], 72 | "winter_wyvern": [ "ww" ], 73 | "arc_warden": [ "aw" ], 74 | "monkey_king": [ "mk" ], 75 | "dark_willow": [ "sylph", "dw" ], 76 | "grimstroke": [ "gs" ], 77 | "medusa": [ "dusa" ], 78 | "dawnbreaker": [ "db" ] 79 | } -------------------------------------------------------------------------------- /builderdata/hero_colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "bane": "#412f88", 3 | "drow_ranger": "#3e5595", 4 | "mirana": "#4a9cd2", 5 | "morphling": "#17adf1", 6 | "phantom_lancer": "#94cde6", 7 | "puck": "#4aacda", 8 | "pudge": "#b7b662", 9 | "razor": "#006ba4", 10 | "tiny": "#7e92b0", 11 | "vengefulspirit": "#7d85e6", 12 | "slardar": "#635e85", 13 | "witch_doctor": "#4f388f", 14 | "riki": "#6b82d7", 15 | "venomancer": "#93a705", 16 | "faceless_void": "#ac95db", 17 | "skeleton_king": "#5cbb75", 18 | "death_prophet": "#2cb799", 19 | "phantom_assassin": "#68b0ad", 20 | "pugna": "#a7cc00", 21 | "templar_assassin": "#c250a3", 22 | "viper": "#30903b", 23 | "luna": "#8286b8", 24 | "dragon_knight": "#952d07", 25 | "rattletrap": "#ad917c", 26 | "furion": "#0e8e6d", 27 | "life_stealer": "#ac824f", 28 | "dark_seer": "#a546cf", 29 | "clinkz": "#be6b22", 30 | "enchantress": "#559135", 31 | "broodmother": "#47425e", 32 | "weaver": "#02c9c8", 33 | "jakiro": "#6a5e77", 34 | "batrider": "#cf5300", 35 | "chen": "#ebeac8", 36 | "spectre": "#491e8e", 37 | "ancient_apparition": "#4385de", 38 | "doom_bringer": "#ad2e14", 39 | "ursa": "#8980aa", 40 | "spirit_breaker": "#72c2c2", 41 | "gyrocopter": "#6dcfc1", 42 | "alchemist": "#b89228", 43 | "invoker": "#b50d57", 44 | "silencer": "#58208b", 45 | "obsidian_destroyer": "#2cdaa5", 46 | "lycan": "#553b23", 47 | "brewmaster": "#b07c2b", 48 | "shadow_demon": "#511f66", 49 | "lone_druid": "#d7cc0f", 50 | "chaos_knight": "#991d04", 51 | "meepo": "#6f8ebe", 52 | "treant": "#4e9c15", 53 | "ogre_magi": "#3967b3", 54 | "undying": "#5cad76", 55 | "rubick": "#1ab816", 56 | "disruptor": "#38bcc5", 57 | "nyx_assassin": "#c49a53", 58 | "naga_siren": "#cf8940", 59 | "keeper_of_the_light": "#e6bb14", 60 | "wisp": "#88c8da", 61 | "visage": "#5aa8c1", 62 | "slark": "#5db8dd", 63 | "medusa": "#98cc9c", 64 | "troll_warlord": "#b53d2c", 65 | "centaur": "#a24b42", 66 | "magnataur": "#7a4327", 67 | "shredder": "#93938a", 68 | "bristleback": "#c4871f", 69 | "tusk": "#b0beb7", 70 | "skywrath_mage": "#ebad4f", 71 | "abaddon": "#3d38b3", 72 | "elder_titan": "#dab522", 73 | "legion_commander": "#d7b932", 74 | "techies": "#cf9745", 75 | "ember_spirit": "#da2507", 76 | "earth_spirit": "#a5b800", 77 | "abyssal_underlord": "#966e83", 78 | "terrorblade": "#4446a7", 79 | "phoenix": "#ebcc1e", 80 | "oracle": "#41b2bd", 81 | "winter_wyvern": "#4bbac7", 82 | "arc_warden": "#51a1f0", 83 | "monkey_king": "#bb5e63", 84 | "dark_willow": "#a21845", 85 | "grimstroke": "#9d0700", 86 | "mars": "#fd4700", 87 | "snapfire": "#f38047", 88 | "void_spirit": "#bf00ff" 89 | } -------------------------------------------------------------------------------- /builderdata/hero_names.json: -------------------------------------------------------------------------------- 1 | { 2 | "axe": "Mogul Khan", 3 | "bane": "Atropos", 4 | "bloodseeker": "Strygwyr", 5 | "crystal_maiden": "Rylai", 6 | "drow_ranger": "Traxex", 7 | "earthshaker": "Raigor Stonehoof", 8 | "juggernaut": "Yurnero", 9 | "nevermore": "Nevermore", 10 | "phantom_lancer": "Azwraith", 11 | "sand_king": "Crixalis", 12 | "storm_spirit": "Raijin Thunderkeg", 13 | "vengefulspirit": "Shendelzare", 14 | "windrunner": "Lyralei", 15 | "shadow_shaman": "Rhasta", 16 | "witch_doctor": "Zharvakko", 17 | "lich": "Ethreain", 18 | "tinker": "Boush", 19 | "sniper": "Kardel Sharpeye", 20 | "necrolyte": "Rotund'jere", 21 | "warlock": "Demnok Lannik", 22 | "beastmaster": "Karroch", 23 | "queenofpain": "Akasha", 24 | "venomancer": "Lesale", 25 | "faceless_void": "Darkterror", 26 | "skeleton_king": "Ostarion", 27 | "death_prophet": "Krobelus", 28 | "phantom_assassin": "Mortred", 29 | "templar_assassin": "Lanaya", 30 | "dragon_knight": "Davion", 31 | "rattletrap": "Rattletrap", 32 | "life_stealer": "N'aix", 33 | "dark_seer": "Ish'Kafel", 34 | "omniknight": "Purist Thunderwrath", 35 | "enchantress": "Aiushtha", 36 | "night_stalker": "Balanar", 37 | "broodmother": "Black Arachnia", 38 | "bounty_hunter": "Gondar", 39 | "weaver": "Skitskurr", 40 | "spectre": "Mercurial", 41 | "ancient_apparition": "Kaldr", 42 | "doom_bringer": "Lucifer", 43 | "ursa": "Ulfsaar", 44 | "spirit_breaker": "Barathrum", 45 | "gyrocopter": "Aurel", 46 | "alchemist": "Razzil Darkbrew", 47 | "invoker": "Carl", 48 | "silencer": "Nortrom", 49 | "obsidian_destroyer": "Harbinger", 50 | "lycan": "Banehallow", 51 | "brewmaster": "Mangix", 52 | "lone_druid": "Sylla", 53 | "treant": "Rooftrellen", 54 | "ogre_magi": "Aggron Stonebreak", 55 | "naga_siren": "Silithice", 56 | "keeper_of_the_light": "Ezalor", 57 | "troll_warlord": "Jah'rakal", 58 | "centaur": "Bradwarden", 59 | "shredder": "Rizzrack", 60 | "bristleback": "Rigwarl", 61 | "tusk": "Ymir", 62 | "skywrath_mage": "Dragonus", 63 | "legion_commander": "Tresdin", 64 | "techies": "Squee, Spleen, and Spoon", 65 | "ember_spirit": "Xin", 66 | "earth_spirit": "Kaolin", 67 | "abyssal_underlord": "Vrogros", 68 | "oracle": "Nerif", 69 | "winter_wyvern": "Auroth", 70 | "arc_warden": "Zet", 71 | "monkey_king": "Sun Wukong", 72 | "dark_willow": "Mireska Sunbreeze", 73 | "pangolier": "Donte Panlin", 74 | "snapfire": "Beatrix", 75 | "void_spirit": "Inai", 76 | "dawnbreaker": "Valora" 77 | } -------------------------------------------------------------------------------- /builderdata/item_aliases.json: -------------------------------------------------------------------------------- 1 | { 2 | "item_enchanted_mango": [ "mango" ] 3 | } -------------------------------------------------------------------------------- /builderdata/loadingscreen_heroes.json: -------------------------------------------------------------------------------- 1 | { 2 | "Ancient Rhythms Loading Screen": [ "crystal_maiden", "winter_wyvern", "nevermore", "lina" ], 3 | "Arms of Rising Fury Loading Screen": [ "sven" ], 4 | "Ascendant Bounty Hunter Loading Screen": [ "bounty_hunter" ], 5 | "Ascendant Brewmaster Loading Screen": [ "brewmaster" ], 6 | "Ascendant Crystal Maiden Loading Screen": [ "crystal_maiden" ], 7 | "Ascendant Faceless Void Loading Screen": [ "faceless_void" ], 8 | "Ascendant Lone Druid Loading Screen": [ "lone_druid" ], 9 | "Ascendant Nyx Loading Screen": [ "nyx_assassin" ], 10 | "Ascendant Phantom Lancer Loading Screen": [ "phantom_lancer" ], 11 | "Ascendant Razor Loading Screen": [ "razor" ], 12 | "Ascendant Riki Loading Screen": [ "riki" ], 13 | "Ascendant Skywrath Mage Loading Screen": [ "skywrath_mage" ], 14 | "Ascendant Timbersaw Loading Screen": [ "shredder" ], 15 | "Ascendant Tusk Loading Screen": [ "tusk" ], 16 | "Ascendant Ursa Loading Screen": [ "ursa" ], 17 | "Ascendant Vengeful Spirit Loading Screen": [ "vengefulspirit" ], 18 | "Beneath the War Moon": [ "templar_assassin", "rubick", "enigma", "batrider", "alchemist", "chaos_knight", "crystal_maiden", "puck", "furion" ], 19 | "Blade and Bow": [ "windrunner", "juggernaut" ], 20 | "Blueheart Maiden Loading Screen": [ "crystal_maiden" ], 21 | "Bonds of Madness Loading Screen": [ "life_stealer" ], 22 | "Burning Trio": [ "jakiro", "huskar", "batrider" ], 23 | "Caucus of Heroes": [ "enchantress", "tidehunter", "chen", "rubick", "naga_siren", "nevermore", "invoker", "sand_king", "queenofpain", "tinker" ], 24 | "Chemical Rage": [ "alchemist" ], 25 | "Clash of Heroes": [ "nevermore", "tidehunter", "razor", "viper", "faceless_void", "tiny", "juggernaut", "sven", "lina", "crystal_maiden" ], 26 | "Cloak of the Fallen Loading Screen": [ "clinkz" ], 27 | "Crack Shot Loading Screen": [ "sniper" ], 28 | "D2CL Season 5 Loading Screen": [ "necrolyte", "tidehunter" ], 29 | "Devilish Conjurer Loading Screen": [ "witch_doctor" ], 30 | "Envisioning Invoker Loading Screen": [ "invoker" ], 31 | "Envisioning Juggernaut Loading Screen": [ "juggernaut" ], 32 | "Envisioning Legion Commander Loading Screen": [ "legion_commander" ], 33 | "Envisioning Meepo Loading Screen": [ "meepo" ], 34 | "Envisioning Ogre Magi Loading Screen": [ "ogre_magi" ], 35 | "Envisioning Outworld Devourer Loading Screen": [ "obsidian_destroyer" ], 36 | "Envisioning Phantom Assassin Loading Screen": [ "phantom_assassin" ], 37 | "Envisioning Phantom Lancer Loading Screen": [ "phantom_lancer" ], 38 | "Envisioning Queen of Pain Loading Screen": [ "queenofpain" ], 39 | "Envisioning Tidehunter Loading Screen": [ "tidehunter" ], 40 | "Envisioning Treant Protector Loading Screen": [ "treant" ], 41 | "Envisioning Weaver Loading Screen": [ "weaver" ], 42 | "Envisioning Winter Wyvern Loading Screen": [ "winter_wyvern" ], 43 | "Envisioning Witch Doctor Loading Screen": [ "witch_doctor" ], 44 | "Envisioning Wraith King Loading Screen": [ "skeleton_king" ], 45 | "Fall 2016 Battle Pass Loading Screen II": [ "slark" ], 46 | "Fall 2016 Battle Pass Loading Screen III": [ "antimage", "dragon_knight" ], 47 | "Fall of the Year Beast": [ "invoker", "mirana", "phoenix", "windrunner", "juggernaut" ], 48 | "Familiar Foe": [ "visage" ], 49 | "Fiend Summoner Loading Screen": [ "warlock" ], 50 | "Fiery Slayer Loading Screen": [ "lina" ], 51 | "Flying Arrow Loading Screen": [ "windrunner" ], 52 | "Galloping Light": [ "keeper_of_the_light" ], 53 | "Good Day Sir": [ "axe" ], 54 | "Guardians of Nature": [ "enchantress", "treant" ], 55 | "Heavenly Light Loading Screen": [ "omniknight" ], 56 | "Hidden Mysteries Loading Screen": [ "templar_assassin" ], 57 | "Invokation": [ "invoker" ], 58 | "Let's Race Loading Screen": [ "juggernaut", "earthshaker", "lone_druid", "ursa", "crystal_maiden", "tusk", "kunkka", "tidehunter" ], 59 | "Majesty of the Forbidden Sands Loadingscreen": [ "sand_king" ], 60 | "Mid Lane": [ "pudge", "death_prophet", "witch_doctor", "drow_ranger", "jakiro", "tiny" ], 61 | "Monument": [ "wisp", "furion", "chaos_knight", "crystal_maiden", "puck" ], 62 | "Mystic Coils Loading Screen": [ "puck" ], 63 | "One's Legion": [ "phantom_lancer" ], 64 | "Path of the Blossom Loading Screen": [ "ember_spirit", "earth_spirit", "storm_spirit" ], 65 | "Phantasm": [ "phantom_assassin" ], 66 | "Prisoner's Pounce": [ "slark" ], 67 | "Pure Skill": [ "ogre_magi" ], 68 | "Quake and Fissure": [ "earthshaker" ], 69 | "Reap the Whirlwind": [ "windrunner" ], 70 | "Reef's Edge Loading Screen": [ "slardar", "naga_siren", "slark" ], 71 | "Scions of the Sky Loading Screen": [ "skywrath_mage", "vengefulspirit" ], 72 | "Shambling Trickster Loading Screen": [ "witch_doctor" ], 73 | "Soul Devourer Loading Screen": [ "nevermore" ], 74 | "Stormcrafter's Assault": [ "axe", "disruptor" ], 75 | "Sweet Toxin Loading Screen": [ "venomancer" ], 76 | "Teacher of the Flame Loading Screen": [ "ember_spirit" ], 77 | "Tethered Spirits": [ "wisp" ], 78 | "The Harbinger Comes": [ "obsidian_destroyer" ], 79 | "The International 2016 Loading Screen": [ "ancient_apparition", "naga_siren", "earthshaker", "storm_spirit", "gyrocopter" ], 80 | "The Siren's Song": [ "naga_siren" ], 81 | "The Wailing Inferno Loading Screen": [ "warlock" ], 82 | "Three Heroes Loading Screen": [ "axe", "juggernaut", "crystal_maiden" ], 83 | "Top Lane": [ "tidehunter", "life_stealer", "dragon_knight" ], 84 | "Traxex the Drow Ranger": [ "drow_ranger" ], 85 | "Wild Tamer Loading Screen": [ "beastmaster" ], 86 | "Winter 2016 Loading Screen I": [ "furion" ], 87 | "Winter 2016 Loading Screen II": [ "obsidian_destroyer" ], 88 | "Winter 2016 Loading Screen III": [ "naga_siren", "enigma", "kunkka", "dazzle" ], 89 | "Wolf Pack": [ "lycan" ] 90 | } -------------------------------------------------------------------------------- /builderdata/old_patch_dates.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.60": "2004-02-03", 3 | "0.90": "2004-02-02", 4 | "0.95": "2004-02-03", 5 | "0.96a": null, 6 | "0.96b": null, 7 | "0.96c": null, 8 | "0.98": null, 9 | "0.999": null, 10 | "2.00": "2004-02-20", 11 | "2.60": "2004-03-01", 12 | "3.00": "2004-03-23", 13 | "3.00d": null, 14 | "3.2a": "2004-03-24", 15 | "5.10": "2004-04-30", 16 | "5.20": null, 17 | "5.31": "2004-05-04", 18 | "5.33": null, 19 | "5.35": "2004-05-06", 20 | "5.36": "2004-05-06", 21 | "5.40": "2004-05-06", 22 | "5.50": null, 23 | "5.51": null, 24 | "5.52": "2004-06-02", 25 | "5.53": "2004-06-06", 26 | "5.54": "2004-06-07", 27 | "5.55": null, 28 | "5.58": "2004-07-19", 29 | "5.59": "2004-07-22", 30 | "5.60": null, 31 | "5.60b": null, 32 | "5.61": null, 33 | "5.62": "2004-09-18", 34 | "5.63": "2004-09-19", 35 | "5.64": "2004-09-19", 36 | "5.65": "2004-09-23", 37 | "5.66": null, 38 | "5.68": null, 39 | "5.71": null, 40 | "5.72": null, 41 | "5.74": "2004-10-11", 42 | "5.75": "2004-10-15", 43 | "5.76": "2004-10-22", 44 | "5.77": null, 45 | "5.78": null, 46 | "5.79": null, 47 | "5.80": null, 48 | "5.82": null, 49 | "5.83": null, 50 | "5.84": null, 51 | "5.84b": null, 52 | "6.00": "2005-03-01", 53 | "6.01": null, 54 | "6.02": null, 55 | "6.03": null, 56 | "6.03b": null, 57 | "6.04": null, 58 | "6.04b": null, 59 | "6.05": null, 60 | "6.06": null, 61 | "6.07": null, 62 | "6.08": null, 63 | "6.09": null, 64 | "6.09b": null, 65 | "6.10": "2005-07-09", 66 | "6.11": null, 67 | "6.12": null, 68 | "6.12b": null, 69 | "6.13": null, 70 | "6.14": null, 71 | "6.15": null, 72 | "6.16": null, 73 | "6.16b": null, 74 | "6.17": null, 75 | "6.18": "2005-10-01", 76 | "6.18b": "2005-09-30", 77 | "6.19": "2006-10-01", 78 | "6.19b": null, 79 | "6.20": "2005-11-04", 80 | "6.21": null, 81 | "6.22": null, 82 | "6.23": null, 83 | "6.24": null, 84 | "6.25": null, 85 | "6.26": null, 86 | "6.27": null, 87 | "6.27b": null, 88 | "6.28": "2006-03-15", 89 | "6.29": null, 90 | "6.29b": null, 91 | "6.30": "2006-04-12", 92 | "6.30b": null, 93 | "6.31": null, 94 | "6.32": null, 95 | "6.32b": null, 96 | "6.33": null, 97 | "6.33b": null, 98 | "6.34": null, 99 | "6.35": null, 100 | "6.35b": null, 101 | "6.36": "2006-08-24", 102 | "6.36b": null, 103 | "6.37": null, 104 | "6.38": "2006-10-27", 105 | "6.38b": null, 106 | "6.39": "2007-01-02", 107 | "6.39b": null, 108 | "6.40": null, 109 | "6.41": null, 110 | "6.42": null, 111 | "6.43": null, 112 | "6.43b": null, 113 | "6.44": "2007-06-03", 114 | "6.44b": null, 115 | "6.45": null, 116 | "6.46": null, 117 | "6.46b": null, 118 | "6.47": "2007-08-24", 119 | "6.48": null, 120 | "6.48b": "2007-09-06", 121 | "6.49": "2007-10-26", 122 | "6.49b": null, 123 | "6.49c": null, 124 | "6.50": null, 125 | "6.50b": null, 126 | "6.51": null, 127 | "6.52": null, 128 | "6.52b": null, 129 | "6.52c": null, 130 | "6.52d": null, 131 | "6.52e": null, 132 | "6.53": "2008-07-10", 133 | "6.54": null, 134 | "6.54b": null, 135 | "6.55": "2008-10-07", 136 | "6.55b": null, 137 | "6.56": null, 138 | "6.57": null, 139 | "6.57b": null, 140 | "6.58": "2009-01-11", 141 | "6.58b": null, 142 | "6.59": null, 143 | "6.59b": null, 144 | "6.59c": null, 145 | "6.59d": null, 146 | "6.60": "2009-06-09", 147 | "6.60b": "2009-06-13", 148 | "6.61": "2009-07-07", 149 | "6.61b": "2009-07-07", 150 | "6.61c": "2009-08-05", 151 | "6.62": "2009-08-25", 152 | "6.62b": "2009-09-02", 153 | "6.63": "2009-09-16", 154 | "6.63b": "2009-09-16", 155 | "6.64": "2009-10-13", 156 | "6.65": "2009-12-23", 157 | "6.66": "2010-01-13", 158 | "6.66b": "2010-01-26", 159 | "6.67": "2010-03-24", 160 | "6.67b": "2010-03-24", 161 | "6.67c": "2010-03-30", 162 | "6.68": "2010-07-26", 163 | "6.68b": "2010-07-27", 164 | "6.68c": "2010-07-27", 165 | "6.69": "2010-10-11", 166 | "6.69b": "2010-10-16", 167 | "6.69c": "2010-11-05", 168 | "6.70": "2011-01-18", 169 | "6.70b": "2010-12-28", 170 | "6.70c": "2010-12-29", 171 | "6.71": "2011-01-23", 172 | "6.71b": "2011-01-23", 173 | "6.72": "2011-04-27", 174 | "6.72b": "2011-05-04", 175 | "6.72c": "2011-06-02", 176 | "6.72d": "2011-07-22", 177 | "6.72e": "2011-07-18", 178 | "6.72f": "2011-07-22", 179 | "6.73": "2012-01-12", 180 | "6.73b": "2011-12-27", 181 | "6.73c": "2012-01-10", 182 | "6.74": "2012-03-15", 183 | "6.74b": "2012-03-20", 184 | "6.74c": "2012-03-21", 185 | "6.75": "2012-10-04", 186 | "6.75b": "2012-10-04", 187 | "6.76": "2012-10-25", 188 | "6.76b": "2012-10-25", 189 | "6.76c": "2012-10-29", 190 | "6.77": "2012-12-19", 191 | "6.77b": "2013-01-11", 192 | "6.77c": "2013-03-21", 193 | "6.78": "2013-06-04", 194 | "6.78b": "2013-03-06", 195 | "6.78c": "2013-07-03", 196 | "6.79": "2013-10-21", 197 | "6.79b": "2013-11-27", 198 | "6.79c": "2013-12-12", 199 | "6.79d": "2014-01-07", 200 | "6.79e": "2014-01-12", 201 | "6.80": "2014-01-29", 202 | "6.80b": "2014-03-02", 203 | "6.80c": "2014-03-03", 204 | "6.81": "2014-04-29", 205 | "6.81b": "2014-06-02", 206 | "6.81c": "2014-08-03", 207 | "6.81d": "2014-08-09", 208 | "6.82": "2014-09-25", 209 | "6.82b": "2014-09-28", 210 | "6.82c": "2014-10-15", 211 | "6.83": "2014-12-17", 212 | "6.83b": "2015-01-13", 213 | "6.83c": "2015-02-12", 214 | "6.83d": "2015-06-09", 215 | "6.84": "2015-04-30", 216 | "6.84b": "2015-05-09", 217 | "6.84c": "2015-05-18", 218 | "6.85": "2015-09-24", 219 | "6.85b": "2015-11-01", 220 | "6.86": "2015-12-16", 221 | "6.86b": "2015-12-20", 222 | "6.86c": "2015-12-29", 223 | "6.86d": "2016-01-20", 224 | "6.86e": "2016-02-05", 225 | "6.86f": "2016-02-21", 226 | "6.87": "2016-04-25", 227 | "6.87b": "2016-04-29", 228 | "6.87c": "2016-05-07", 229 | "6.87d": "2016-05-22", 230 | "6.88": "2016-06-12", 231 | "6.88b": "2016-07-12", 232 | "6.88c": "2016-08-19", 233 | "6.88d": "2016-09-02", 234 | "6.88e": "2016-10-02", 235 | "6.88f": "2016-10-17", 236 | "7.00": "2016-12-12", 237 | "7.01": "2016-12-20", 238 | "7.02": "2017-02-08", 239 | "7.03": "2017-03-15", 240 | "7.04": "2017-03-23", 241 | "7.05": "2017-04-09", 242 | "7.06": "2017-05-15", 243 | "7.06b": "2017-05-21", 244 | "7.06c": "2017-05-29", 245 | "7.06d": "2017-06-11", 246 | "7.06e": "2017-07-02", 247 | "7.06f": "2017-08-20", 248 | "7.07": "2017-10-31", 249 | "7.07b": "2017-11-05", 250 | "7.07c": "2017-11-17", 251 | "7.07d": "2017-12-19" 252 | } -------------------------------------------------------------------------------- /builderdata/voice_actors.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": "Sam A. Mowry", 3 | "2": "Jon St. John", 4 | "3": "David Scully", 5 | "4": "Jon St. John", 6 | "5": "Gin Hammond", 7 | "6": "Gin Hammond", 8 | "7": "John Patrick Lowrie", 9 | "8": "David Scully", 10 | "9": "Gin Hammond", 11 | "10": "David Scully", 12 | "11": "John Patrick Lowrie", 13 | "12": "Barry Dennen", 14 | "13": "Jen Taylor", 15 | "14": "John Patrick Lowrie", 16 | "15": "Eric Newsome", 17 | "16": "David Scully", 18 | "17": "John Patrick Lowrie", 19 | "18": "David Scully", 20 | "19": "Eric Newsome", 21 | "20": "Gin Hammond", 22 | "21": "Jen Taylor", 23 | "22": "Eric Newsome", 24 | "23": "Jon St. John", 25 | "25": "Jen Taylor", 26 | "26": "Tom Chantler", 27 | "27": "Gary Schwartz", 28 | "28": "Sam A. Mowry", 29 | "29": "Eric Newsome", 30 | "30": "Tom Chantler", 31 | "31": "Gary Schwartz", 32 | "32": "Tom Chantler", 33 | "33": "Jon St. John", 34 | "34": "Harry S. Robins", 35 | "35": "Gary Schwartz", 36 | "36": "Sam A. Mowry", 37 | "37": "Sam A. Mowry", 38 | "38": "Sam A. Mowry", 39 | "39": "Linda K. Morris", 40 | "40": "Dave Fennoy", 41 | "41": "Dave Fennoy", 42 | "42": "Dave Fennoy", 43 | "43": "Ellen McLain", 44 | "44": "Gin Hammond", 45 | "45": "Gary Schwartz", 46 | "46": "Linda K. Morris", 47 | "47": "Tony Todd", 48 | "48": "Linda K. Morris", 49 | "49": "Tony Todd", 50 | "50": "David Scully", 51 | "51": "Sam A. Mowry", 52 | "52": "Eric Newsome", 53 | "53": "Eric Newsome", 54 | "54": "Bruce Miles", 55 | "55": "John Patrick Lowrie", 56 | "56": "Michael Gregory", 57 | "57": "Bruce Miles", 58 | "58": "Gin Hammond", 59 | "59": "Dave Fennoy", 60 | "60": "Tony Todd", 61 | "61": "Ellen McLain", 62 | "62": "Michael Gregory", 63 | "63": "David Scully", 64 | "64": "Dave Fennoy", 65 | "65": "Dave Fennoy", 66 | "66": "Eric Newsome", 67 | "67": "Gin Hammond", 68 | "68": "John Patrick Lowrie", 69 | "69": "John Patrick Lowrie", 70 | "70": "Fred Tatasciore", 71 | "71": "Fred Tatasciore", 72 | "72": "Nolan North", 73 | "73": "Bruce Miles", 74 | "74": "Dennis Bateman", 75 | "75": "Michael Gregory", 76 | "76": "Tom Chantler", 77 | "77": "Nolan North", 78 | "78": "Nolan North", 79 | "79": "Nolan North", 80 | "80": "Nolan North", 81 | "81": "Barry Dennen", 82 | "82": "Nolan North", 83 | "83": "Fred Tatasciore", 84 | "84": "Nolan North", 85 | "85": "Fred Tatasciore", 86 | "86": "Barry Dennen", 87 | "87": "Fred Tatasciore", 88 | "88": "Dempsey Pappion", 89 | "89": "Linda K. Morris", 90 | "90": "Nolan North", 91 | "92": "Dempsey Pappion", 92 | "93": "Tom Chantler", 93 | "94": "Jen Taylor", 94 | "95": "Nolan North", 95 | "96": "Tom Chantler", 96 | "97": "Dempsey Pappion", 97 | "98": "TJ Ramini", 98 | "99": "TJ Ramini", 99 | "100": "Tom Chantler", 100 | "101": "TJ Ramini", 101 | "102": "TJ Ramini", 102 | "103": "Jim French", 103 | "104": "Merle Dandridge", 104 | "105": "Dee Bradley Baker", 105 | "106": "Dave Fennoy", 106 | "107": "Nolan North", 107 | "108": "TJ Ramini", 108 | "109": "David Sobolov", 109 | "111": "Mike Shapiro", 110 | "112": "Merle Dandridge", 111 | "113": "James Kirkland", 112 | "114": "Bill Millsap", 113 | "119": "Ashly Burch", 114 | "120": "Phil LaMarr", 115 | "121": "Steven Blum", 116 | "126": "Rene Mujica", 117 | "128": "Laraine Newman", 118 | "129": "Trevor Devall" 119 | } -------------------------------------------------------------------------------- /criteria_sentancing.py: -------------------------------------------------------------------------------- 1 | from valve2json import valve_readfile, read_json 2 | from dotabase import * 3 | from sqlalchemy import func 4 | import re 5 | 6 | #removes duplicates from a list 7 | def remove_dupes(in_list): 8 | result = [] 9 | for item in in_list: 10 | if item not in result: 11 | result.append(item) 12 | return result 13 | 14 | def pretty_time(time): 15 | if "," in time: 16 | return "between {} and {}".format(pretty_time(time.split(",")[0][1:]), pretty_time(time.split(",")[1][1:])) 17 | if time.startswith("<"): 18 | return "less than {}".format(pretty_time(time[1:])) 19 | if time.startswith(">"): 20 | return "more than {}".format(pretty_time(time[1:])) 21 | seconds = int(time) 22 | if seconds < 60: 23 | return "{} seconds".format(seconds) 24 | elif seconds == 60: 25 | return "1 minute" 26 | elif seconds < 3600: 27 | return "{} minutes".format(seconds // 60) 28 | elif seconds == 3600: 29 | return "1 hour" 30 | else: 31 | return "{} hour and {} minutes".format(seconds // 3600, (seconds % 3600) // 60) 32 | 33 | 34 | # pretty criteria which alters the first criteria in the list 35 | def build_dictionaries(session): 36 | global pretty_dict 37 | global crit_type_dict 38 | pretty_dict = read_json("builderdata/criteria_pretty.json") 39 | crit_type_dict = {} 40 | 41 | replace_dict = {} 42 | replace_type_dict = {} 43 | for hero in session.query(Hero): 44 | replace_dict[hero.full_name] = hero.localized_name 45 | replace_type_dict[hero.full_name] = "hero" 46 | 47 | for ability in session.query(Ability): 48 | replace_dict[ability.name] = ability.localized_name 49 | replace_type_dict[ability.name] = "ability" 50 | 51 | for item in session.query(Item): 52 | replace_dict[item.name] = item.localized_name 53 | replace_type_dict[item.name] = "item" 54 | 55 | replace_dict.update({ 56 | "DOTA_RUNE_ARCANE": "an arcane rune", 57 | "DOTA_RUNE_DOUBLEDAMAGE": "a double damage rune", 58 | "DOTA_RUNE_REGENERATION": "a regeneration rune", 59 | "DOTA_RUNE_BOUNTY": "a bounty rune", 60 | "DOTA_RUNE_HASTE": "a haste rune", 61 | "DOTA_RUNE_ILLUSION": "an illusion rune", 62 | "DOTA_RUNE_INVISIBILITY": "an invisibility rune" 63 | }) 64 | for key in replace_dict: 65 | if key.startswith("DOTA_RUNE"): 66 | replace_type_dict[key] = "rune" 67 | 68 | for match in replace_dict: 69 | for crit in session.query(Criterion).filter(func.lower(Criterion.matchvalue) == func.lower(match)): 70 | pretty_dict[crit.name] = replace_dict[match] 71 | crit_type_dict[crit.name] = replace_type_dict.get(match) 72 | if crit.matchkey == "stolenspell": 73 | pretty_dict[crit.name] = "and stealing " + replace_dict[match] 74 | crit_type_dict[crit.name] = "stolenspell" 75 | 76 | ignore_matchkeys = [ "classname", "announcer_voice", "taunt_type", "spectator", "player_team", "special_spawn", "customresponse" ] 77 | for key in ignore_matchkeys: 78 | for crit in session.query(Criterion).filter_by(matchkey=key): 79 | pretty_dict[crit.name] = "" 80 | crit_type_dict[crit.name] = None 81 | 82 | for crit in session.query(Criterion).filter(Criterion.name.like("Chance_%")): 83 | pretty_dict[crit.name] = f"{crit.name[7:]} chance" 84 | crit_type_dict[crit.name] = "chance" 85 | 86 | for crit in session.query(Criterion).filter(Criterion.name.like("IsAnnouncerLine_announcer_%")): 87 | pretty_dict[crit.name] = "" 88 | crit_type_dict[crit.name] = None 89 | 90 | for crit in session.query(Criterion).filter_by(matchkey="gametime"): 91 | pretty_dict[crit.name] = f"at {pretty_time(crit.matchvalue)} in" 92 | crit_type_dict[crit.name] = "gametime" 93 | 94 | pretty_matchkeys = read_json("builderdata/criteria_matchkeys.json") 95 | for matchkey in pretty_matchkeys: 96 | for crit in session.query(Criterion).filter_by(matchkey=matchkey["key"]): 97 | template = matchkey.get("template", "{}") 98 | value = crit.matchvalue 99 | if "convert" in matchkey: 100 | if crit.matchvalue.lower() in matchkey["convert"]: 101 | value = matchkey["convert"][crit.matchvalue.lower()] 102 | elif "default" in matchkey["convert"]: 103 | value = matchkey["convert"]["default"].format(value) 104 | else: 105 | print(f"Missing key '{crit.matchvalue}' for matchkey '{crit.matchkey}'") 106 | pretty_dict[crit.name] = template.format(value) 107 | crit_type_dict[crit.name] = matchkey.get("new_key", matchkey["key"]) 108 | 109 | 110 | pretty_dict = {k.lower():v for k, v in pretty_dict.items()} 111 | crit_type_dict = {k.lower():v for k, v in crit_type_dict.items()} 112 | 113 | def replace_template(template, crit_list): 114 | pattern = re.compile(r"\{([^\{\}|]*?)\|([^\{\}|]*?)\|([^\{\}|]*?)\}") 115 | match = pattern.search(template) 116 | 117 | while match: 118 | replacement = match.group(2) 119 | for i in range(len(crit_list)): 120 | if crit_type_dict.get(crit_list[i].lower()) == match.group(1) and pretty_dict[crit_list[i].lower()] != "": 121 | replacement = match.group(3).replace("%", pretty_dict[crit_list[i].lower()]) 122 | crit_list.pop(i) 123 | break 124 | 125 | template = re.sub(pattern, replacement, template, count=1) 126 | match = pattern.search(template) 127 | 128 | return template 129 | 130 | def pretty_response_crit(crits): 131 | def is_significant(crit): 132 | if (crit == "Custom" or crit.startswith("Followup") or (crit_type_dict.get(crit.lower()) in [ "gametime", "chance", "player_team", "victor_team" ])): 133 | return False 134 | if pretty_dict.get(crit.lower()) == "": 135 | return False 136 | return True 137 | 138 | crits = crits.split(" ") 139 | 140 | result = None 141 | for i in range(len(crits)): 142 | if is_significant(crits[i]): 143 | result = crits.pop(i) 144 | break 145 | if not result: 146 | result = crits.pop(0) 147 | 148 | result = pretty_dict.get(result.lower(), result) 149 | 150 | result = replace_template(result, crits) 151 | ending = replace_template("{gametime|| %}{nag|| (%)}{chance|| (%)}", crits) 152 | 153 | for crit in crits: 154 | temp = pretty_dict.get(crit.lower(), crit) 155 | if temp != "": 156 | result += " " + temp 157 | 158 | result += ending 159 | return result.strip() 160 | 161 | def load_pretty_criteria(session): 162 | build_dictionaries(session) 163 | 164 | for criterion in session.query(Criterion): 165 | criterion.pretty = replace_template(pretty_dict.get(criterion.name.lower(), criterion.name), []) 166 | for response in session.query(Response): 167 | if response.criteria == "" or response.criteria is None: 168 | response.pretty_criteria = "Unused" 169 | else: 170 | crits = [pretty_response_crit(c) for c in response.criteria.split("|")] 171 | while "" in crits: 172 | crits.remove("") 173 | if len(crits) == 0: 174 | crits.append("Unused") 175 | crits = remove_dupes(crits) 176 | response.pretty_criteria = "|".join(crits) 177 | 178 | -------------------------------------------------------------------------------- /generate_json.py: -------------------------------------------------------------------------------- 1 | # Generates json files from the database.db 2 | # these files serve 2 purposes: 3 | # 1. gives the viewer an idea of what is in the database 4 | # 2. provides a way to look at what changes between each update 5 | from __main__ import session 6 | from dotabase import * 7 | from collections import OrderedDict 8 | import os 9 | import json 10 | import shutil 11 | import re 12 | 13 | json_path = os.path.join(dotabase_dir, "../json/") 14 | 15 | def write_json(filename, data): 16 | text = json.dumps(data, indent="\t") 17 | with open(filename, "w+") as f: 18 | f.write(text) # Do it like this so it doesnt break mid-file 19 | 20 | 21 | # dumps an sqlalchemy table to json 22 | def dump_table(table, query=None): 23 | full_data = [] 24 | if query is None: 25 | query = session.query(table) 26 | for item in query: 27 | data = OrderedDict() 28 | for col in table.__table__.columns: 29 | value = getattr(item, col.name) 30 | if col.name in [ "json_data", "ability_special" ]: 31 | data[col.name] = json.loads(value, object_pairs_hook=OrderedDict) 32 | elif value is None or value == "": 33 | continue 34 | elif isinstance(value, int) or isinstance(value, bool) or isinstance(value, float): 35 | data[col.name] = value 36 | else: 37 | data[col.name] = str(value) 38 | full_data.append(data) 39 | return full_data 40 | 41 | def dump_heroes(filename): 42 | data = dump_table(Hero) 43 | write_json(filename, data) 44 | 45 | def dump_abilities(filename): 46 | data = dump_table(Ability) 47 | write_json(filename, data) 48 | 49 | def dump_items(filename): 50 | data = dump_table(Item) 51 | write_json(filename, data) 52 | 53 | def dump_emoticons(filename): 54 | data = dump_table(Emoticon) 55 | write_json(filename, data) 56 | 57 | def dump_chatwheel(filename): 58 | data = dump_table(ChatWheelMessage) 59 | write_json(filename, data) 60 | 61 | def dump_criteria(filename): 62 | data = dump_table(Criterion) 63 | write_json(filename, data) 64 | 65 | def dump_voices(filename): 66 | data = dump_table(Voice) 67 | write_json(filename, data) 68 | 69 | def dump_loadingscreens(filename): 70 | data = dump_table(LoadingScreen) 71 | write_json(filename, data) 72 | 73 | def dump_patches(filename): 74 | data = dump_table(Patch) 75 | write_json(filename, data) 76 | 77 | def dump_talents(filename): 78 | data = dump_table(Talent) 79 | write_json(filename, data) 80 | 81 | def dump_responses(directory): 82 | os.makedirs(directory) 83 | for voice in session.query(Voice): 84 | data = dump_table(Response, voice.responses) 85 | filename = voice.name.lower().replace(" ", "_") 86 | filename = re.sub(r"[^a-z_]", "", filename) 87 | filename = os.path.join(directory, f"{filename}.json") 88 | write_json(filename, data) 89 | 90 | 91 | def generate_json(): 92 | print("generating json files...") 93 | if os.path.exists(json_path): 94 | shutil.rmtree(json_path) 95 | os.makedirs(json_path) 96 | dump_heroes(json_path + "heroes.json") 97 | dump_items(json_path + "items.json") 98 | dump_abilities(json_path + "abilities.json") 99 | dump_emoticons(json_path + "emoticons.json") 100 | dump_chatwheel(json_path + "chatwheel.json") 101 | dump_criteria(json_path + "criteria.json") 102 | dump_voices(json_path + "voices.json") 103 | dump_loadingscreens(json_path + "loadingscreens.json") 104 | dump_patches(json_path + "patches.json") 105 | dump_talents(json_path + "talents.json") 106 | dump_responses(json_path + "responses") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | olefile==0.44 2 | Pillow>=6.2.0 3 | SQLAlchemy>=1.4.40 4 | beautifulsoup4>=4.5.3 5 | colorgram.py>=1.1.0 6 | colorama>=0.3.9 7 | requests>=2.25.1 -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import sys, os, json, re 2 | import decimal 3 | import colorama # support ansi colors on windows 4 | import datetime 5 | import re 6 | from collections import OrderedDict 7 | from dotabase import LocaleString 8 | colorama.init() 9 | 10 | def clean_values(values: str, join_string=" ", percent=False): 11 | if isinstance(values, OrderedDict) and "value" in values: 12 | values = values["value"] 13 | if values is None or values == "": 14 | return values 15 | values = values.strip().split(" ") 16 | for i in range(len(values)): 17 | values[i] = re.sub(r"\.0+$", "", values[i]) 18 | values[i] = re.sub(r"^=", "", values[i]) 19 | if percent and values[i][-1] != "%": 20 | if not re.match(r"^x[0-9]+$", values[i]): 21 | values[i] += "%" 22 | if all(x == values[0] for x in values): 23 | return values[0] 24 | return join_string.join(values) 25 | 26 | def bold_values(values, separator, base_level): 27 | if values is None: 28 | printerr("bad values passed to bold_values()") 29 | return ""; 30 | values = values.split(separator) 31 | if base_level and base_level <= len(values): 32 | values[base_level - 1] = f"**{values[base_level - 1]}**" 33 | else: 34 | values = map(lambda v: f"**{v}**", values) 35 | return separator.join(values) 36 | 37 | 38 | 39 | # adds/subtracts the modifier from the base value. Assumes 0 if no base value given 40 | def do_simple_math(base_value, modifier): 41 | if base_value is None: 42 | base_value = "0" 43 | if " " in base_value or " " in modifier: 44 | # this is a multi-part value like "10 20 30 40" so just do these individually 45 | values = base_value.split(" ") 46 | modifiers = modifier.split(" ") 47 | valcount = max(len(values), len(modifiers)) 48 | 49 | if len(values) == 1: 50 | values = [base_value] * valcount 51 | if len(modifiers) == 1: 52 | modifiers = [modifier] * valcount 53 | 54 | results = [] 55 | for i in range(valcount): 56 | results.append(do_simple_math(values[i], modifiers[i])) 57 | return " ".join(results) 58 | 59 | if base_value == "FIELD_INTEGER": 60 | base_value_decimal = 0 # god dammit valve y u do this. supposed to be a number n you give me "FIELD_INTEGER"???? 61 | else: 62 | base_value_decimal = decimal.Decimal(base_value) 63 | 64 | operation = lambda a, b: a + b 65 | 66 | if "%" in modifier: 67 | modifier = modifier.replace("%", "") 68 | modifier_decimal = decimal.Decimal(modifier) 69 | modifier_decimal = modifier_decimal / 100 70 | elif "=" in modifier: # TODO: this for lifestealer infest aghs, sets it to the new value. too tired to implement right now 71 | modifier = modifier.replace("=", "") 72 | modifier_decimal = decimal.Decimal(modifier) 73 | elif "x" in modifier: 74 | modifier = modifier.replace("x", "") 75 | operation = lambda a, b: a * b 76 | modifier_decimal = decimal.Decimal(modifier) 77 | else: 78 | modifier_decimal = decimal.Decimal(modifier) 79 | 80 | value = operation(base_value_decimal, modifier_decimal) 81 | value = str(value) 82 | value = re.sub(r"\.0+$", "", value) 83 | return value 84 | 85 | ability_special_talent_keys = { 86 | "LinkedSpecialBonus": "talent_name", 87 | "LinkedSpecialBonusField": "talent_value_key", 88 | "LinkedSpecialBonusOperation": "talent_operation", 89 | "RequiresScepter": "scepter_upgrade", 90 | "RequiresShard": "shard_upgrade", 91 | "RequiresFacet": "facet_upgrade" 92 | } 93 | def get_ability_special_AbilityValues(ability_values, name): 94 | result = [] 95 | for avkey, value in ability_values.items(): 96 | new_item = OrderedDict() 97 | new_item["key"] = avkey 98 | if isinstance(value, str): 99 | new_item["value"] = value 100 | else: 101 | # value is a dictionary 102 | valuekeys = [ "values", "value" ] 103 | for key in valuekeys: 104 | if key in value: 105 | new_item["value"] = value[key] 106 | break 107 | if not "value" in new_item: 108 | prefix = "special_bonus_facet_" 109 | for key in value: 110 | if key.startswith(prefix): 111 | new_item["value"] = value[key] 112 | break 113 | for subkey in value: 114 | if subkey in ability_special_talent_keys: 115 | new_item[ability_special_talent_keys[subkey]] = value[subkey] 116 | 117 | if "special_bonus_shard" in value: 118 | new_item["shard_value"] = do_simple_math(value.get("value"), value["special_bonus_shard"]) 119 | new_item["shard_bonus"] = re.sub(r"[^\d]", "", value["special_bonus_shard"]) 120 | if "special_bonus_scepter" in value: 121 | new_item["scepter_value"] = do_simple_math(value.get("value"), value["special_bonus_scepter"]) 122 | new_item["scepter_bonus"] = re.sub(r"[^\d]", "", value["special_bonus_scepter"]) 123 | 124 | result.append(new_item) 125 | return result 126 | 127 | def get_ability_special_AbilitySpecial(ability_special, name): 128 | result = [] 129 | for index_key in ability_special: 130 | if isinstance(ability_special[index_key], str): 131 | obj = { "value": ability_special[index_key] } 132 | else: 133 | obj = ability_special[index_key].copy() 134 | 135 | new_item = OrderedDict() 136 | 137 | # remove unneeded stuff (mostly ablility draft? linking) 138 | bad_keys = [ "CalculateSpellDamageTooltip", "levelkey", "ad_linked_ability", "ad_linked_abilities", "linked_ad_abilities" ] 139 | for key in bad_keys: 140 | if key in obj: 141 | del obj[key] 142 | 143 | # useful keys we can add to the abilityspecial 144 | good_keys = { 145 | "DamageTypeTooltip": "damagetype" 146 | } 147 | for key in good_keys: 148 | if key in obj: 149 | new_item[good_keys[key]] = obj[key] 150 | del obj[key] 151 | 152 | for key in ability_special_talent_keys: 153 | if key in obj: 154 | new_item[ability_special_talent_keys[key]] = obj[key] 155 | del obj[key] 156 | 157 | items = list(obj.items()) 158 | if len(items) != 2: # catch this for future bad_keys 159 | bad_keys = list(map(lambda i: i[0], items)) 160 | if "var_type" in bad_keys: 161 | bad_keys.remove("var_type") 162 | printerr(f"Theres a bad key in the AbilitySpecial of {name}: one of {bad_keys}") 163 | 164 | if items[0][0] == "var_type": 165 | del items[0] 166 | 167 | if len(items) == 0: 168 | printerr(f"Empty AbilitySpecial entry in {name}") 169 | continue 170 | 171 | new_item["key"] = items[0][0] 172 | new_item["value"] = clean_values(items[0][1]) 173 | result.append(new_item) 174 | 175 | return result 176 | 177 | 178 | def get_ability_special(json_data, name): 179 | if "AbilitySpecial" in json_data: 180 | return get_ability_special_AbilitySpecial(json_data.get("AbilitySpecial"), name) 181 | elif "AbilityValues" in json_data: 182 | return get_ability_special_AbilityValues(json_data.get("AbilityValues"), name) 183 | else: 184 | return [] 185 | 186 | # adds talent info 187 | def ability_special_add_talent(ability_special, ability_query, ability_name): 188 | for attribute in ability_special: 189 | talent = attribute.get("talent_name") 190 | if talent: 191 | talent = ability_query.filter_by(name=talent).first() 192 | value_key = attribute.get("talent_value_key", "value") 193 | talent_operation = attribute.get("talent_operation", "SPECIAL_BONUS_ADD") # SUBTRACT, MULTIPLY 194 | 195 | if talent is None: 196 | printerr(f"The wrong MISSING talent ({attribute.get('talent_name')}) is linked to the ability special for {ability_name}: {attribute.get('key')}") 197 | return ability_special 198 | 199 | talent_ability_special = json.loads(talent.ability_special, object_pairs_hook=OrderedDict) 200 | 201 | talent_attribute = next((a for a in talent_ability_special if a["key"] == value_key), None) 202 | 203 | if talent_attribute is None: 204 | printerr(f"The wrong talent ({talent.name}: {talent.localized_name}) is linked to the ability special for {ability_name}: {attribute.get('key')}") 205 | return ability_special 206 | 207 | def do_op(value1, value2): 208 | return { 209 | "SPECIAL_BONUS_ADD": value1 + value2, 210 | "SPECIAL_BONUS_SUBTRACT": value1 - value2, 211 | "SPECIAL_BONUS_MULTIPLY": value1 * value2, 212 | "SPECIAL_BONUS_PERCENTAGE_ADD": value1 * (1 + (value2 / 100)) 213 | }[talent_operation] 214 | 215 | values = attribute["value"].split(" ") 216 | talent_value = float(re.sub(r"[a-z]", "", talent_attribute["value"])) 217 | for i in range(len(values)): 218 | if values[i] == "": 219 | values[i] = "0" 220 | values[i] = str(do_op(float(values[i]), talent_value)) 221 | attribute["talent_value"] = clean_values(" ".join(values)) 222 | return ability_special 223 | 224 | def ability_special_add_header(ability_special, strings, name): 225 | for attribute in ability_special: 226 | key = re.sub("^bonus_", "", attribute['key']) 227 | keys = [] 228 | for a in ["ability", "Ability"]: 229 | for b in [key, attribute['key']]: 230 | keys.append(f"DOTA_Tooltip_{a}_{name}_{b}") 231 | 232 | header = None 233 | for k in keys: 234 | if header is None: 235 | header = strings.get(k) 236 | if header is None: 237 | continue 238 | match = re.match(r"(%)?([\+\-]\$)?(.*)", header) 239 | header = match.group(3) 240 | 241 | if "value" in attribute: 242 | attribute["value"] = clean_values(attribute["value"], percent=match.group(1)) 243 | if "talent_value" in attribute: 244 | attribute["talent_value"] = clean_values(attribute["talent_value"], percent=match.group(1)) 245 | 246 | if match.group(2): 247 | attribute["header"] = match.group(2)[0] 248 | attribute["footer"] = strings[f"dota_ability_variable_{header}"] 249 | attribute["footer"] = re.sub(r"<[^>]*>", "", attribute["footer"]) 250 | else: 251 | # check if we look like "-Something" (without colon) or w/ a plus 252 | header = re.sub(r"<[^>]*>", "", header) 253 | match = re.match(r"([\+\-])([^:]*)", header) 254 | if match: 255 | attribute["header"] = match.group(1) 256 | attribute["footer"] = match.group(2) 257 | else: 258 | attribute["header"] = header 259 | return ability_special 260 | 261 | ATTRIBUTE_TEMPLATE_PATTERNS = [ 262 | r'%([^%}\s/]*)%', 263 | r'\{s:([^}\s]*)\}' 264 | ] 265 | 266 | # Cleans up the descriptions of items and abilities 267 | def clean_description(text, replacements_dict=None, base_level=None, value_bolding=True, report_errors=True): 268 | if text is None or text == "": 269 | return text 270 | text = re.sub(r' ', r'', text) 271 | text = re.sub(r'

([^<]+)

', r'\n# \1\n', text) 272 | text = re.sub(r'<(br|BR) ?/?>', r'\n', text) 273 | text = re.sub(r"([^<]+)", r"\*\1\*", text) 274 | text = re.sub(r'(.*)', r'**\1**', text) 275 | text = re.sub(r'(.*)', r'\1', text) 276 | text = re.sub(r" color='[^']+'>", r'>', text) 277 | text = re.sub(r'([^<]+)', r'**\1**', text) 278 | 279 | if replacements_dict: 280 | def replace_attrib(match): 281 | key = match.group(1) 282 | if key == "": 283 | return "%" 284 | else: 285 | new_value = None 286 | if key in replacements_dict: 287 | new_value = replacements_dict[key] 288 | elif key.lower() in replacements_dict: 289 | new_value = replacements_dict[key.lower()] 290 | 291 | if new_value is not None: 292 | new_value = clean_values(new_value, "/") 293 | if value_bolding: 294 | new_value = bold_values(new_value, "/", base_level) 295 | return new_value 296 | 297 | if report_errors: 298 | printerr(f"Missing attrib '{key}' FROM {text}") 299 | return f"%{key}%" 300 | 301 | for pattern in ATTRIBUTE_TEMPLATE_PATTERNS: 302 | text = re.sub(pattern, replace_attrib, text) 303 | 304 | # include the percent in bold if the value is in bold 305 | text = re.sub(r'\*\*%', '%**', text) 306 | # replace double percents that are redundant now 307 | text = re.sub(r'%%', '%', text) 308 | # do what we think the ": {s" stuff means 309 | text = re.sub(r"^: [x\+\-]?", "", text) 310 | 311 | if text.startswith("\n"): 312 | text = text[1:] 313 | 314 | return text 315 | 316 | class ansicolors: 317 | CLEAR = '\033[0m' 318 | RED = '\033[31m' 319 | GREEN = '\033[32m' 320 | YELLOW = '\033[33m' 321 | BLUE = '\033[34m' 322 | 323 | def printerr(error_text): 324 | global CURRENT_PROGRESS_BAR 325 | if CURRENT_PROGRESS_BAR is not None: 326 | CURRENT_PROGRESS_BAR.errors.append(error_text) 327 | return 328 | print(f"{ansicolors.RED} {error_text}{ansicolors.CLEAR}") 329 | 330 | 331 | def write_json(filename, data): 332 | text = json.dumps(data, indent="\t") 333 | with open(filename, "w+") as f: 334 | f.write(text) # Do it like this so it doesnt break mid-file 335 | 336 | def read_json(filename): 337 | with open(filename) as f: 338 | return json.load(f, object_pairs_hook=OrderedDict) 339 | 340 | # this class pulled from https://stackoverflow.com/questions/2082152/case-insensitive-dictionary 341 | class CaseInsensitiveDict(dict): 342 | @classmethod 343 | def _k(cls, key): 344 | return key.lower() if isinstance(key, str) else key 345 | def __init__(self, *args, **kwargs): 346 | super(CaseInsensitiveDict, self).__init__(*args, **kwargs) 347 | remove_colons = kwargs.get("remove_colons", False) 348 | self._convert_keys(remove_colons) 349 | def __getitem__(self, key): 350 | return super(CaseInsensitiveDict, self).__getitem__(self.__class__._k(key)) 351 | def __setitem__(self, key, value): 352 | super(CaseInsensitiveDict, self).__setitem__(self.__class__._k(key), value) 353 | def __delitem__(self, key): 354 | return super(CaseInsensitiveDict, self).__delitem__(self.__class__._k(key)) 355 | def __contains__(self, key): 356 | return super(CaseInsensitiveDict, self).__contains__(self.__class__._k(key)) 357 | def has_key(self, key): 358 | return super(CaseInsensitiveDict, self).has_key(self.__class__._k(key)) 359 | def pop(self, key, *args, **kwargs): 360 | return super(CaseInsensitiveDict, self).pop(self.__class__._k(key), *args, **kwargs) 361 | def get(self, key, *args, **kwargs): 362 | return super(CaseInsensitiveDict, self).get(self.__class__._k(key), *args, **kwargs) 363 | def setdefault(self, key, *args, **kwargs): 364 | return super(CaseInsensitiveDict, self).setdefault(self.__class__._k(key), *args, **kwargs) 365 | def update(self, E={}, **F): 366 | super(CaseInsensitiveDict, self).update(self.__class__(E)) 367 | super(CaseInsensitiveDict, self).update(self.__class__(**F)) 368 | def _convert_keys(self, remove_colons=False): 369 | for k in list(self.keys()): 370 | v = super(CaseInsensitiveDict, self).pop(k) 371 | if remove_colons: 372 | k = re.sub(r":.+$", "", k) 373 | self.__setitem__(k, v) 374 | 375 | 376 | CURRENT_PROGRESS_BAR = None 377 | 378 | class ProgressBar: 379 | def __init__(self, total, title="Percent:"): 380 | global CURRENT_PROGRESS_BAR 381 | self.total = total 382 | self.current = 0 383 | self.max_chunks = 10 384 | self.title = title 385 | self.errors = [] 386 | self.render() 387 | CURRENT_PROGRESS_BAR = self 388 | 389 | def tick(self): 390 | oldpercent = int(self.percent * 100) 391 | self.current += 1 392 | if oldpercent != int(self.percent * 100): 393 | self.render() 394 | 395 | @property 396 | def percent(self): 397 | if self.total == 0: 398 | return 0 399 | return self.current / self.total 400 | 401 | def render(self): 402 | global CURRENT_PROGRESS_BAR 403 | chunks = '█' * int(round(self.percent * self.max_chunks)) 404 | spaces = ' ' * (self.max_chunks - len(chunks)) 405 | sys.stdout.write(f"\r{self.title} |{chunks + spaces}| {self.percent:.0%}".encode("utf8").decode(sys.stdout.encoding)) 406 | if self.current == self.total: 407 | sys.stdout.write("\n") 408 | sys.stdout.flush() 409 | if self.current == self.total: 410 | sys.stderr.flush() 411 | CURRENT_PROGRESS_BAR = None 412 | for error in self.errors: 413 | printerr(error) 414 | 415 | 416 | class Config: 417 | def __init__(self): 418 | self.path = "config.json" 419 | self.defaults = OrderedDict([ ("vpk_path", None), ("overwrite_db", True), ("overwrite_json", False) ]) 420 | if not os.path.exists(self.path): 421 | self.json_data = self.defaults 422 | self.save_settings() 423 | self.bad_config_file() 424 | else: 425 | self.json_data = read_json(self.path) 426 | if self.vpk_path is None: 427 | self.bad_config_file() 428 | 429 | def bad_config_file(self): 430 | print("You gotta fill out the config.json file") 431 | print("vpk_path example: C:/foo/dota-vpk") 432 | sys.exit() 433 | 434 | 435 | def save_settings(self): 436 | write_json(self.path, self.json_data) 437 | 438 | @property 439 | def vpk_path(self): 440 | return self.json_data["vpk_path"] 441 | 442 | @property 443 | def overwrite_db(self): 444 | return self.json_data["overwrite_db"] 445 | 446 | @property 447 | def overwrite_json(self): 448 | return self.json_data["overwrite_json"] 449 | config = Config() 450 | 451 | 452 | class SimpleTimer(): 453 | def __init__(self, message=None): 454 | self.message = message 455 | self.start = datetime.datetime.now() 456 | self.end = None 457 | 458 | def __enter__(self): 459 | self.start = datetime.datetime.now() 460 | return self 461 | 462 | def __exit__(self, type, value, traceback): 463 | self.stop() 464 | if self.message: 465 | print(self.message + f": {self.miliseconds} ms") 466 | 467 | def stop(self): 468 | self.end = datetime.datetime.now() 469 | 470 | @property 471 | def seconds(self): 472 | if self.end is None: 473 | self.stop() 474 | return int((self.end - self.start).total_seconds()) 475 | 476 | @property 477 | def miliseconds(self): 478 | if self.end is None: 479 | self.stop() 480 | return int((self.end - self.start).total_seconds() * 1000.0) 481 | 482 | def __str__(self): 483 | s = self.seconds % 60 484 | m = self.seconds // 60 485 | text = f"{s} second{'s' if s != 1 else ''}" 486 | if m > 0: 487 | text = f"{m} minute{'s' if m != 1 else ''} and " + text 488 | return text 489 | 490 | def __repr__(self): 491 | return self.__str__() 492 | 493 | # adds a locale string to the 494 | def addLocaleString(session, lang, target, column, value): 495 | if value == "" or value == None or value == getattr(target, column): 496 | return None 497 | string = LocaleString() 498 | string.lang = lang 499 | string.target = target 500 | string.column = column 501 | string.value = value 502 | session.add(string) -------------------------------------------------------------------------------- /valve2json.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import path 2 | import json 3 | import re 4 | import string 5 | import os 6 | import os.path 7 | import collections 8 | from utils import * 9 | import typing 10 | 11 | # Converts valve's obsure and unusable text formats to json 12 | # can do the following formats: 13 | # KV3 (KeyValue) 14 | # response_rules script format 15 | 16 | json_cache_dir = "jsoncache" 17 | if not os.path.exists(json_cache_dir): 18 | os.makedirs(json_cache_dir) 19 | 20 | def dict_handle_duplicates(ordered_pairs): 21 | d = collections.OrderedDict() 22 | for k, v in ordered_pairs: 23 | original_k = k 24 | i = 1 25 | while k in d: 26 | k = f"{original_k}{i}" 27 | i += 1 28 | d[k] = v 29 | return d 30 | 31 | class CustomJsonParsingException(Exception): 32 | def __init__(self, message): 33 | self.message = message 34 | 35 | def tryloadjson(text, strict=True, parser=None): 36 | try: 37 | return json.loads(text, strict=strict, object_pairs_hook=dict_handle_duplicates) 38 | except json.JSONDecodeError as e: 39 | filename = "jsoncache/errored.json" 40 | with open(filename, "w+", encoding="utf-16") as f: 41 | f.write(text) 42 | print(f"bad converted file saved to: {filename}") 43 | 44 | lines = text.split("\n") 45 | start = e.lineno - 4 46 | end = e.lineno + 4 47 | if start < 0: 48 | start = 0 49 | if end > len(lines): 50 | end = len(lines) 51 | if parser: 52 | print(f"Parser: {parser}()") 53 | raise CustomJsonParsingException("Error parsing this JSON text:\n" + "\n".join(lines[start:end]) + "\n") 54 | 55 | # Redefine with error printing 56 | def read_json(filename): 57 | with open(filename, 'r') as f: 58 | text = f.read() 59 | return tryloadjson(text) 60 | 61 | def uncommentkvfile(text): 62 | in_value = False 63 | in_comment = False 64 | result = "" 65 | 66 | for i in range(len(text)): 67 | if in_comment: 68 | if text[i] == "\n": 69 | in_comment = False 70 | result += text[i] 71 | continue 72 | 73 | if text[i] == '"': 74 | in_value = not in_value 75 | result += text[i] 76 | continue 77 | 78 | if (not in_value) and text[i] == "/": 79 | in_comment = True 80 | continue 81 | 82 | result += text[i] 83 | 84 | return result 85 | 86 | # converts an old file to the new format 87 | def vsndevts_from_old(text): 88 | file_data = kvfile2json(text) 89 | for key in file_data: 90 | old_data = file_data[key]["operator_stacks"]["update_stack"]["reference_operator"] 91 | data = OrderedDict() 92 | data["type"] = old_data["reference_stack"] 93 | for var in old_data["operator_variables"]: 94 | value = old_data["operator_variables"][var]["value"] 95 | if isinstance(value, str): 96 | data[var] = value 97 | else: 98 | value_list = [] 99 | for i in value: 100 | value_list.append(value[i]) 101 | data[var] = value_list 102 | file_data[key] = data 103 | return file_data 104 | 105 | 106 | def vsndevts2json(text): 107 | # If this isnt there, its a kv1 file 108 | # if not (re.match(r'^\{\s*[^\s"]+\s=\s+\{', text) or ("', "", text) 114 | # To convert Valve's KeyValue format to Json 115 | text = re.sub(r'"\n{', r'": {', text) 116 | text = re.sub(r'(\n\s*)([^\s]+) =', r'\1"\2":', text) 117 | text = re.sub(r'(null|]|"|}|\d)(\n[\s]+")', r'\1,\2', text) 118 | text = re.sub(r'("|\d),(\n\s*)(]|})', r'\1\2\3', text) 119 | 120 | text = re.sub(r'{\s*{([^{}]+)}\s*}', r'{\1}', text) 121 | if not re.match(r"^\s*\{", text): 122 | text = "{ " + text + " }" 123 | # To re-include non-functional quotes 124 | text = re.sub(r'TEMP_QUOTE_TOKEN', '\\"', text) 125 | 126 | # undo places where we quoted already-quoted stuff: 127 | text = re.sub(r'(\n\s+")"([^"]+)"(":\s*\n)', r'\1\2\3', text) 128 | 129 | return tryloadjson(text, strict=False, parser="vsndevts2json") 130 | 131 | # Regex strings for vk2json from: 132 | # http://dev.dota2.com/showthread.php?t=87191 133 | # Loads a kv file as json 134 | def kv_nocommentfile2json(text): 135 | return kvfile2json(text, False) 136 | 137 | # Regex strings for vk2json from: 138 | # http://dev.dota2.com/showthread.php?t=87191 139 | # Loads a kv file as json 140 | def kvfile2json(text, remove_comments=True): 141 | # To temporarily hide non-functional quotes 142 | text = re.sub(r'\\"', 'TEMP_QUOTE_TOKEN', text) 143 | # remove the null hex char at the end of some files 144 | text = re.sub(r'\x00$', '', text) 145 | # fix places where people forgot a closing quote 146 | text = re.sub(r'(\n\s+"[^"]*"\s*"[^"\n]*)(?=\n\s+"[^\s\n])', r'\1"', text) 147 | 148 | # get rid of troublesome comments 149 | if remove_comments: 150 | text = uncommentkvfile(text) 151 | 152 | # To convert Valve's KeyValue format to Json 153 | text = re.sub('', '', text) # remove zero width no-break space 154 | 155 | text = re.sub(r'(\n\s+)(value)', r'\1"\2"', text) # fix a thing where some valve employees forgot to put double quotes around the thing 156 | text = re.sub(r'(?:\n\s*)([a-z_]+)\s*\n\s*{', r'"\1": {', text) 157 | text = re.sub(r'"([^"]*)"(\s*){', r'"\1": {', text) 158 | text = re.sub(r'(\n\s+"[^"]*"\s*)([0-9\-]+)(?=\n\s+["}])', r'\1"\2"', text) # fix places where a number doesnt have quotes around it 159 | text = re.sub(r'"([^"]*)"\s*"([^"]*)"', r'"\1": "\2",', text) 160 | text = re.sub(r',(\s*[}\]])', r'\1', text) 161 | text = re.sub(r'([}\]])(\s*)("[^"]*":\s*)?([{\[])', r'\1,\2\3\4', text) 162 | text = re.sub(r'}(\s*"[^"]*":)', r'},\1', text) 163 | if not re.match(r"^\s*\{", text): 164 | text = "{ " + text + " }" 165 | 166 | # cut-off things after closing quotes 167 | text = re.sub(r'(\n\s+"[^"]*":\s*"[^"\n]*",?\s*)[^,"{}]+\n', r'\1\n', text) 168 | 169 | # ignore dangling quotes 170 | text = re.sub(r'\n\s*"\s*\n', r'\n', text) 171 | 172 | # uncomment single quotes 173 | text = re.sub(r"\\\\'", "'", text) 174 | text = re.sub(r"\\'", "'", text) 175 | 176 | # custom fixes because Valve does dum things (this is for when this is just randomly on a line) 177 | # text = re.sub("and turn rate reduced by %dMODIFIER_PROPERTY_TURN_RATE_PERCENTAGE%%%.", "", text) 178 | 179 | # To re-include non-functional quotes 180 | text = re.sub(r'TEMP_QUOTE_TOKEN', '\\"', text) 181 | 182 | return tryloadjson(text, strict=False, parser="kvfile2json") 183 | 184 | # Loads a response_rules file as json 185 | def rulesfile2json(text): 186 | text = "\n" + text + "\n" 187 | text = re.sub(r'\n//[^\n]*', r'\n', text) 188 | text = re.sub(r'\n#[^\n]*', r'\n', text) 189 | 190 | text = re.sub(r'scene "(.*)".*\n', r'"\1",\n', text) 191 | text = re.sub(r'"scenes.*/(.*)\.vcd"', r'"\1"', text) 192 | text = re.sub(r'Response ([^\s]*)\n{([^}]*)}', r'"response_\1": [\2],', text) 193 | text = re.sub(r'Rule ([^\s]*)\n{([^}]*)}', r'"rule_\1": {\2},', text) 194 | text = re.sub(r'criteria (.*)\n', r'"criteria": "\1",\n', text) 195 | text = re.sub(r'response (.*)\n', r'"response": "\1",\n', text) 196 | text = re.sub(r'criterion\s*"(.*)"\s*"(.*)"\s*"(.*)\n?"(.*)\n', r'"criterion_\1": "\2 \3\4",\n', text) 197 | text = "{" + text + "}" 198 | text = re.sub(r',(\s*)}', r'\1}', text) 199 | text = re.sub(r',(\s*)]', r'\1]', text) 200 | 201 | return tryloadjson(text, parser="rulesfile2json") 202 | 203 | class AssetModifier(): 204 | def __init__(self, data): 205 | self.data = data 206 | self.type = data.get("type") 207 | self.asset = data.get("asset") 208 | self.modifier = data.get("modifier") 209 | 210 | # a class that allows easier access to the items_game file 211 | class ItemsGame(): 212 | def __init__(self): 213 | self.data = DotaFiles.items_game.read()["items_game"] 214 | self.items = self.data["items"] 215 | self.item_name_dict = {} 216 | self.by_prefab = {} 217 | for key, item in self.items.items(): 218 | item["id"] = key 219 | self.item_name_dict[item.get("name")] = item 220 | prefab = item.get("prefab") 221 | if prefab not in self.by_prefab: 222 | self.by_prefab[prefab] = [] 223 | self.by_prefab[prefab].append(item) 224 | 225 | def get_asset_modifiers(self, item, asset_type): 226 | result = [] 227 | for key, data in item.get("visuals", {}).items(): 228 | if not "asset_modifier" in key: 229 | continue 230 | elif data.get("type") == asset_type: 231 | result.append(AssetModifier(data)) 232 | return result 233 | 234 | def get_asset_modifier(self, item, asset_type): 235 | for key, data in item.get("visuals", {}).items(): 236 | if not "asset_modifier" in key: 237 | continue 238 | elif data.get("type") == asset_type: 239 | return AssetModifier(data) 240 | return None 241 | 242 | 243 | # Reads from converted json file unless overwrite parameter is specified 244 | def valve_readfile(filepath, fileformat, encoding=None, overwrite=False) -> dict: 245 | sourcedir = config.vpk_path 246 | json_file = os.path.splitext(json_cache_dir + filepath)[0]+'.json' 247 | vpk_file = sourcedir + filepath 248 | 249 | try: 250 | if (not (overwrite or config.overwrite_json)) and os.path.isfile(json_file) and (os.path.getmtime(json_file) > os.path.getmtime(vpk_file)): 251 | with open(json_file, 'r') as f: 252 | text = f.read() 253 | return tryloadjson(text) 254 | 255 | if(fileformat in file_formats): 256 | with open(vpk_file, 'r', encoding=encoding) as f: 257 | text = f.read() 258 | data = file_formats[fileformat](text) 259 | else: 260 | raise ValueError("invalid fileformat argument: " + fileformat) 261 | except CustomJsonParsingException as e: 262 | message = f"Errored loading: {vpk_file}\n" + e.message 263 | print(message) 264 | exit(1) 265 | 266 | os.makedirs(os.path.dirname(json_file), exist_ok=True) 267 | write_json(json_file, data) 268 | return data 269 | 270 | class ValveFile(): 271 | path: str 272 | format: str 273 | encoding: str 274 | def __init__(self, path, format="kv", encoding=None): 275 | self.path = path 276 | self.format = format 277 | self.encoding = encoding 278 | self.read_data = None 279 | 280 | def read(self) -> dict: 281 | if self.read_data: 282 | return self.read_data 283 | else: 284 | self.read_data = valve_readfile(self.path, self.format, self.encoding) 285 | return self.read_data 286 | 287 | # creates a list of tuples of a given type of lang files 288 | def createLangFiles(dir, pattern) -> typing.List[typing.Tuple[str, ValveFile]]: 289 | fulldir = config.vpk_path + dir 290 | files = os.listdir(fulldir) 291 | files = [f for f in files if os.path.isfile(os.path.join(fulldir, f)) and re.search(pattern, f)] 292 | results = [] 293 | for file in files: 294 | match = re.search(pattern, file) 295 | lang = match.group(1) 296 | new_tuple = ( 297 | lang, 298 | ValveFile(dir + file, encoding="UTF-8") 299 | ) 300 | if lang == "english": 301 | results.insert(0, new_tuple) 302 | else: 303 | results.append(new_tuple) 304 | 305 | return results 306 | 307 | file_formats = { 308 | "kv": kvfile2json, 309 | "rules": rulesfile2json, 310 | "kv_nocomment": kv_nocommentfile2json, 311 | "vsndevts": vsndevts2json 312 | } 313 | 314 | class DotaFiles(): 315 | npc_ids = ValveFile("/scripts/npc/npc_ability_ids.txt") 316 | npc_abilities = ValveFile("/scripts/npc/npc_abilities.txt") 317 | npc_heroes = ValveFile("/scripts/npc/npc_heroes.txt") 318 | items = ValveFile("/scripts/npc/items.txt") 319 | neutral_items = ValveFile("/scripts/npc/neutral_items.txt") 320 | emoticons = ValveFile("/scripts/emoticons.txt", encoding="UTF-16") 321 | chat_wheel = ValveFile("/scripts/chat_wheel.txt", encoding="utf-8") 322 | chat_wheel_categories = ValveFile("/scripts/chat_wheel_categories.txt", encoding="utf-8") 323 | chat_wheel_heroes = ValveFile("/scripts/chat_wheel_heroes.txt", encoding="utf-8") 324 | game_sounds_vsndevts = ValveFile("/soundevents/game_sounds.vsndevts", "vsndevts") 325 | dota_english = ValveFile("/resource/localization/dota_english.txt", encoding="UTF-8") 326 | items_game = ValveFile("/scripts/items/items_game.txt", "kv_nocomment", encoding="UTF-8") 327 | hero_lore_english = ValveFile("/resource/localization/hero_lore_english.txt", encoding="utf-8") 328 | abilities_english = ValveFile("/resource/localization/abilities_english.txt", encoding="UTF-8") 329 | teamfandom_english = ValveFile("/resource/localization/teamfandom_english.txt", encoding="utf-8") 330 | 331 | lang_abilities = createLangFiles("/resource/localization/", r"abilities_(.*)\.txt") 332 | lang_hero_lore = createLangFiles("/resource/localization/", r"hero_lore_(.*)\.txt") 333 | lang_dota = createLangFiles("/resource/localization/", r"dota_(.*)\.txt") 334 | 335 | class DotaPaths(): 336 | response_mp3s = "/sounds/vo/" 337 | item_images = "/panorama/images/items/" 338 | ability_icon_images = "/panorama/images/spellicons/" 339 | hero_side_images = "/panorama/images/heroes/" 340 | hero_icon_images = "/panorama/images/heroes/icons/" 341 | facet_icon_images = "/panorama/images/hud/facets/icons/" 342 | hero_selection_images = "/panorama/images/heroes/selection/" 343 | emoticon_images = "/panorama/images/emoticons/" 344 | response_rules = "/scripts/talker/" 345 | npc_hero_scripts = "/scripts/npc/heroes/" 346 | 347 | 348 | -------------------------------------------------------------------------------- /vccd_reader.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | # based on / reverse engineered from https://github.com/ValveSoftware/source-sdk-2013/blob/master/sp/src/public/captioncompiler.h 4 | 5 | CrcTable = [ 6 | 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 7 | 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 8 | 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 9 | 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 10 | 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 11 | 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 12 | 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 13 | 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 14 | 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 15 | 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 16 | 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 17 | 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 18 | 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 19 | 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 20 | 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 21 | 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 22 | 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 23 | 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 24 | 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 25 | 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 26 | 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 27 | 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 28 | 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 29 | 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 30 | 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 31 | 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 32 | 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 33 | 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 34 | 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 35 | 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 36 | 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 37 | 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 38 | 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 39 | 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 40 | 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 41 | 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 42 | 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 43 | 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 44 | 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 45 | 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 46 | 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 47 | 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 48 | 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 49 | 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 50 | 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 51 | 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 52 | 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 53 | 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 54 | 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 55 | 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 56 | 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 57 | 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 58 | 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 59 | 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 60 | 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 61 | 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 62 | 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 63 | 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 64 | 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 65 | 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 66 | 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 67 | 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 68 | 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 69 | 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d 70 | ] 71 | 72 | def crcHash(buffer): 73 | crc = 0xFFFFFFFF 74 | 75 | for i in range(len(buffer)): 76 | crc = (crc >> 8) ^ CrcTable[ord(buffer[i]) ^ (crc & 0xFF)] 77 | 78 | crc = ~crc 79 | if crc < 0: 80 | crc += 1 << 32 81 | return crc 82 | 83 | 84 | # print(crcHash("abaddon_abad_ally_01")) 85 | 86 | class ClosedCaption(): 87 | def __init__(self, f, version): 88 | self.hash = struct.unpack(" 1: 91 | self.extra = f"0x{f.read(4).hex()}" 92 | self.blocknum = struct.unpack("