This repository has been archived by the owner on Apr 19, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
parse_resources.py
1295 lines (1003 loc) · 48.3 KB
/
parse_resources.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Copyright 2018 Austin Walker Milt
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# this script parses the dota 2 resource txt file for relevant data (e.g.
# conversion between npc_dota_hero names and in-game names) for both the
# server and client.
# #############################################################################
# GLOBAL IMPORTS ##############################################################
# #############################################################################
import os, re
# #############################################################################
# CONSTANTS ###################################################################
# #############################################################################
HERE = os.path.dirname(os.path.abspath(__file__))
EXTRACT_IMAGES = True
KWD_QUOTE = '"'
KWD_TITLESEP = '_'
KWD_OPEN = '{'
KWD_CLOSE = '}'
KWD_COMMENT = '//'
PRE_DES_ABILITY = 'DOTA_Tooltip_ability_'
PRE_DES_ITEM = 'DOTA_Tooltip_ability_item_'
PRE_VAR = 'dota_ability_variable_'
PRE_HNAME = 'npc_dota_hero_'
PRE_UNAME = 'npc_dota_'
PRE_NPC_ABILITY = ''
PRE_NPC_ITEM = 'item_'
PRE_NPC_UNIT = 'npc_dota_'
PRE_NPC_TOKEN = 'Tokens'
PRE_TTP_LORE = '_Lore'
PRE_TTP_DESC = '_Description'
PRE_TTP_NOTE = '_Note'
PRE_TTP_AGHDESC = '_aghanim_description'
PRE_TTP_AGHMOD = '_scepter'
PRE_TTP_AGHS = '_ultimate_scepter'
RGX_HNAME = re.compile('%s.*(?<!_bio)(?<!_hype)$' % PRE_HNAME)
RGX_UNAME = re.compile('(?!%s)%s.*' % (PRE_HNAME, PRE_UNAME))
RGX_DES_ABILITY = re.compile('(?!%s)%s.*' % (PRE_DES_ITEM, PRE_DES_ABILITY), re.IGNORECASE)
RGX_DES_ITEM = re.compile(PRE_DES_ITEM, re.IGNORECASE)
RGX_VAR = re.compile(PRE_VAR)
RGX_NPC_HNAME = re.compile(r'\s*' + KWD_QUOTE + '%s.*(?<!_bio)(?<!_hype)$' % PRE_HNAME)
RGX_NPC_TOKEN = re.compile(r'\s*' + KWD_QUOTE + PRE_NPC_TOKEN)
RGX_NPC_ABILITY = re.compile(r'\s*' + KWD_QUOTE + PRE_NPC_ABILITY)
RGX_NPC_ITEM = re.compile(r'\s*' + KWD_QUOTE + PRE_NPC_ITEM)
RGX_NPC_UNIT = re.compile(r'\s*' + KWD_QUOTE + PRE_NPC_UNIT)
RGX_TTP_LORE = re.compile(r'.*%s' % PRE_TTP_LORE)
RGX_TTP_DESC = re.compile(r'.*%s' % PRE_TTP_DESC)
RGX_TTP_NOTE = re.compile(r'.*%s\d' % PRE_TTP_NOTE)
RGX_TTP_AGDS = re.compile(r'(?!%s).*%s' % (PRE_TTP_AGHS, PRE_TTP_AGHDESC))
RGX_TTP_AGMD = re.compile(r'.*%s$(?<!%s$)' % (PRE_TTP_AGHMOD, PRE_TTP_AGHS))
RGX_TTP_VARI = re.compile(r'(.*\$.*)')
DEF_DOTA = os.path.abspath(os.path.join(os.environ['ProgramFiles(x86)'], 'Steam', 'SteamApps', 'common', 'dota 2 beta', 'game', 'dota'))
DEF_RESOURCE_FILES = {
'abilities': {
'local': os.path.join(DEF_DOTA, 'scripts', 'npc', 'npc_abilities.txt'),
'url': r'https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/npc_abilities.txt'
},
'heroes': {
'local': os.path.join(DEF_DOTA, 'scripts', 'npc', 'npc_heroes.txt'),
'url': r'https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/npc_heroes.txt'
},
'units': {
'local': os.path.join(DEF_DOTA, 'scripts', 'npc', 'npc_units.txt'),
'url': r'https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/npc_units.txt'
},
'items': {
'local': os.path.join(HERE, 'scripts', 'npc', 'items.txt'), ## has to be extracted from vpk first
'url': r'https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/items.txt'
},
'resource': {
'local': os.path.join(DEF_DOTA, 'resource', 'dota_english.txt'),
'url': r'https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/resource/dota_english.txt'
}
}
DEF_ENCODING = 'utf-16'
VPK_EXE = os.path.abspath(os.path.join(HERE, 'vpk', 'vpk.exe'))
VPK_CALL_LISTFILES = [VPK_EXE, 'l']
VPK_CALL = [VPK_EXE, 'x']
VPK_DEF_VPK = os.path.abspath(os.path.join(DEF_DOTA, r'pak01_dir.vpk'))
VPK_DEF_FIL = ['scripts/npc/items.txt']
VPK_DEF_IMAGES = {
'heroes': [r'resource/flash3/images/heroes'],
'items': [r'resource/flash3/images/items'],
'abilities': [r'resource/flash3/images/spellicons']
}
IMG_DEF_HOST = r'https://storage.googleapis.com/vgv-assets'
IMG_DEF_SUBDIR = {
'heroes': r'images/heroes',
'items': r'images/items',
'abilities': r'images/spellicons'
}
MAN_KEY_ABL = r'Ability\d'
MAN_KEY_ASP = 'AbilitySpecial'
MAN_KEY_MAN = 'AbilityManaCost'
MAN_KEY_COO = 'AbilityCooldown'
MAN_KEY_CST = 'ItemCost'
MAN_ABS_SKP = ('var_type',)
MAN_KWD_VAR = r'%{k}%'
MAN_KWD_PCT = re.compile(r'(%%)(?=([ .!?]|$))')
MAN_KWD_LIN = re.compile('\\\\n')
MAN_RGX = '(.*(?<=%s%s%s))|(.*(?<=%s%s$))'
MAN_FIL_SRV = os.path.abspath(os.path.join(HERE, '..', 'resources', 'keys.pkl'))
MAN_FIL_CLN = os.path.abspath(os.path.join(HERE, '..', '..', '..', 'client', 'deployment', 'chrome', 'page', 'scripts', 'dotapedia.js'))
MAN_V2K = lambda k: k.split(PRE_VAR)[1]
TTP_NPC_BDICT = {
'AbilityBehavior': {
'DOTA_ABILITY_BEHAVIOR_PASSIVE': 'Passive',
'DOTA_ABILITY_BEHAVIOR_UNIT_TARGET': 'Targets Units',
'DOTA_ABILITY_BEHAVIOR_CHANNELLED': 'Channeled',
'DOTA_ABILITY_BEHAVIOR_POINT': 'Point Target',
'DOTA_ABILITY_BEHAVIOR_ROOT_DISABLES': 'Disabled By Root',
'DOTA_ABILITY_BEHAVIOR_AOE': 'AOE',
'DOTA_ABILITY_BEHAVIOR_NO_TARGET': 'No Target',
'DOTA_ABILITY_BEHAVIOR_DONT_RESUME_MOVEMENT': 'Casting Stops Movement',
'DOTA_ABILITY_BEHAVIOR_DONT_RESUME_ATTACK': 'Casting Stops Attack',
'DOTA_ABILITY_BEHAVIOR_DIRECTIONAL': 'Directional Cast',
'DOTA_ABILITY_BEHAVIOR_IMMEDIATE': 'Other (Immediate)',
'DOTA_ABILITY_BEHAVIOR_HIDDEN': 'Other (Hidden)',
'DOTA_ABILITY_BEHAVIOR_NOT_LEARNABLE': 'Not Learnable',
'DOTA_ABILITY_BEHAVIOR_TOGGLE': 'Toggle',
'DOTA_ABILITY_BEHAVIOR_AURA': 'Aura',
'DOTA_ABILITY_BEHAVIOR_IGNORE_BACKSWING': 'Other (Ignore Backswing)',
'DOTA_ABILITY_BEHAVIOR_AUTOCAST': 'Autocast',
'DOTA_ABILITY_BEHAVIOR_ATTACK': 'Attack Modifier',
'DOTA_ABILITY_BEHAVIOR_IGNORE_PSEUDO_QUEUE': 'Usable While Disabled',
'DOTA_ABILITY_BEHAVIOR_NORMAL_WHEN_STOLEN': 'Other (Normal When Stolen)',
'DOTA_ABILITY_BEHAVIOR_OPTIONAL_UNIT_TARGET': 'Usable On Others',
'DOTA_ABILITY_BEHAVIOR_UNRESTRICTED': 'Other (Unrestricted)',
'DOTA_ABILITY_BEHAVIOR_DONT_CANCEL_MOVEMENT': 'Usable While Moving',
'DOTA_ABILITY_BEHAVIOR_DONT_ALERT_TARGET': 'Doesnt Alert Target',
'DOTA_ABILITY_BEHAVIOR_RUNE_TARGET': 'Can Target Runes',
'DOTA_ABILITY_BEHAVIOR_DONT_CANCEL_CHANNEL': 'Doesnt Cancel Channeling',
'DOTA_ABILITY_BEHAVIOR_NOASSIST': 'Other (No Assist)',
'DOTA_ABILITY_BEHAVIOR_IGNORE_CHANNEL': 'Doesnt Cancel Channeling',
'DOTA_ABILITY_TYPE_ULTIMATE': 'Ultimate'
},
'AbilityCastRange': {},
'AbilityCastPoint': {},
'AbilityDamage': {},
'AbilityDuration': {},
'AbilityUnitDamageType': {
'DAMAGE_TYPE_PHYSICAL': 'Physical',
'DAMAGE_TYPE_MAGICAL': 'Magical',
'DAMAGE_TYPE_PURE': 'Pure'
},
'AbilityUnitTargetTeam': {
'DOTA_UNIT_TARGET_TEAM_ENEMY': 'Enemies',
'DOTA_UNIT_TARGET_TEAM_FRIENDLY': 'Allies',
'DOTA_UNIT_TARGET_TEAM_BOTH': 'Allies and Enemies',
'DOTA_UNIT_TARGET_TEAM_CUSTOM': 'Other'
},
'AbilityUnitTargetType': {
u'DOTA_UNIT_TARGET_HERO': 'Hero',
'DOTA_UNIT_TARGET_BASIC': 'Non-Ancient',
'DOTA_UNIT_TARGET_CREEP': 'Creep',
'DOTA_UNIT_TARGET_CUSTOM': 'Other',
'DOTA_UNIT_TARGET_TREE': 'Tree',
'DOTA_UNIT_TARGET_BUILDING': 'Building'
},
'SpellDispellableType': {
'SPELL_DISPELLABLE_YES': 'Any',
'SPELL_DISPELLABLE_NO': 'No',
'SPELL_DISPELLABLE_STRONG': 'Strong Dispels',
'SPELL_DISPELLABLE_YES_STRONG': 'Strong Dispels'
},
'SpellImmunityType': {
'SPELL_IMMUNITY_ENEMIES_NO': 'No',
'SPELL_IMMUNITY_ENEMIES_YES': 'Yes',
}
}
TTP_NPC_LAM = lambda k, v: [TTP_NPC_BDICT[k].get(s.strip(), s.strip()) for s in v.split('|')]
TTP_NPC_BASIC = {
'AbilityBehavior': 'BEHAVIOR:',
'AbilityCastRange': 'CAST RANGE:',
'AbilityCastPoint': 'CAST POINT:',
'AbilityDamage': 'DAMAGE:',
'AbilityDuration': 'DURATION:',
'AbilityUnitDamageType': 'DAMAGE TYPE:',
'AbilityUnitTargetTeam': 'TARGETS:',
'AbilityUnitTargetType': 'TARGET TYPE:',
'SpellDispellableType': 'DISPELLABLE:',
'SpellImmunityType': 'PIERCES SPELL IMMUNITY:'
}
# #############################################################################
# CLASSES #####################################################################
# #############################################################################
class Name(object):
def __init__(self, name=None, key=None, prefix=''):
"""
Name class, basically for storing aliases of of a name, i.e. local
name (e.g. Weaver), npc key (e.g. npc_dota_hero_weaver) and npc short
name (e.g. weaver) using a key-matching prefix.
"""
self.name = name
self.key = key
self._prefix = prefix
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self.name)
def key_to_short(self, key):
"""Get the short name, e.g. npc_dota_hero_weaver -> weaver."""
return key.split(self._prefix)[1]
def short_to_key(self, short):
"""Get the key from the short name, e.g. weaver -> npc_dota_hero_weaver."""
return '%s%s' % (self._prefix, short)
def get_short(self):
"""Get the short name with an assumed prefix, e.g. npc_dota_hero_weaver -> weaver."""
return self.key_to_short(self.key)
class HeroName(Name):
def __init__(self, name=None, key=None):
"""Name for heroes, just defines what the prefix is."""
super(HeroName, self).__init__(name, key, PRE_HNAME)
class ItemName(Name):
def __init__(self, name=None, key=None):
"""Name for items, just defines what the prefix is."""
super(ItemName, self).__init__(name, key, PRE_NPC_ITEM)
class UnitName(Name):
def __init__(self, name=None, key=None):
super(UnitName, self).__init__(name, key, PRE_NPC_UNIT)
class ItemTooltipName(Name):
def __init__(self, name=None, key=None):
"""Name for item tooltips, just defines what the prefix is."""
super(ItemTooltipName, self).__init__(name, key, PRE_DES_ITEM)
class AbilityTooltipName(object):
def __init__(self, name=None, key=None, hero=None):
"""
Name for ability tooltips. Different from other Names in that the short
name relies on knowing the hero name.
"""
self.name = name
self.key = key
self._prefix = PRE_DES_ABILITY
self._hero = hero
def set_hero(self, hero):
"""Sets the hero NPC of self."""
self._hero = hero
def get_short(self):
"""Get the short name with an assumed prefix, where hero matters"""
if self._hero is None: raise ValueError('Cannot get short name without knowing the hero.')
else: return KWD_TITLESEP.join([self._prefix, self._hero.get_short(), self.get_short()])
class AbilityName(AbilityTooltipName):
def __init__(self, name=None, key=None, hero=None):
"""Basically same as AbilityTooltipName, but using a different prefix."""
super(AbilityName, self).__init__(name, key, hero)
self._prefix = PRE_NPC_ABILITY
class NPC(object):
_name_class = Name
def __init__(self, name=None, attributes=None):
"""
General container for storing info from dota resource files (e.g.
dota_english.txt, npc_heroes.txt, etc.
Args:
name (Name): (optional) Name for npc. Default is empty Name().
attributes (dict): (optional) data to store for this npc. Default
is empty dict
"""
if name is None: name = Name()
if attributes is None: attributes = {}
self._name = name
self._data = attributes
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self.get_name())
def get(self, *keys):
"""Get value of data dict at series of *keys."""
if len(keys) == 0: return self._data
d = self._data[keys[0]]
if len(keys) > 1:
for k in keys[1:]: d = d[k]
return d
def has_key(self, key):
"""
Determines if uppermost data level of data dict has the requested
key.
"""
return self._data.has_key(key)
def has_keys(self, *keys):
"""
Determines if dictionary at levels determined by series of *keys has
the requested keys.
"""
if len(keys) == 0: return self.has_key(keys[0])
d = self._data[keys[0]]
if len(keys) > 1:
for k in keys[1:]:
if not d.has_key(k): return False
d = d[k]
return True
def get_name(self):
"""Gets the local name of the NPC."""
return self._name.name
def get_key(self):
"""Gets the full npc key of the NPC."""
return self._name.key
def get_short(self):
"""Gets the short npc key of the NPC."""
return self._name.get_short()
def keys(self):
"""Gets the keys of the uppermost level of the NPC data dict."""
return self._data.keys()
def search_all_keys(self, regex=re.compile(''), keys=[]):
"""
Finds all keys using a regex search (as opposed to match) in the data
subdict gotten by self.get(*keys).
"""
subdict = self.get(*keys)
return [k for k in subdict.keys() if regex.search(k) is not None]
def set(self, value, *keys):
"""
Sets the nested attribute to the value given,
e.g. set('it', ['this', 'is']) would set self._data['this']['is'] = 'it'.
If the nested dictionaries dont exist, they are created.
"""
d = self._data
for key in keys[:-1]:
if key not in d: d[key] = {}
d = d[key]
d[keys[-1]] = value
def set_name(self, name):
"""Sets the local name of the NPC."""
self._name.name = name
def set_key(self, key):
"""Sets the full npc key of the NPC."""
self._name.key = key
@classmethod
def from_name_and_key(cls, name, key, attributes=None):
"""Creates a new NPC with given name, key, and attributes."""
if attributes is None: attributes = {}
return cls(cls._name_class(name, key), attributes)
class HeroNPC(NPC): _name_class = HeroName
class UnitNPC(NPC): _name_class = UnitName
class ItemNPC(NPC): _name_class = ItemName
class AbilityNPC(NPC):
def set_hero(self, hero):
"""Set the hero NPC for this ability."""
self._name.set_hero(hero)
@staticmethod
def from_name_and_key(name, key, hero=None, attributes=None):
"""Creates a new AbilityNPC with given name, key, hero, and attributes."""
if attributes is None: attributes = {}
return AbilityNPC(AbilityName(name, key, hero), attributes)
class NPCSet(object):
_re = re.compile('')
_npc = NPC
def __init__(self, npcs=[]):
"""
General container for storing sets of NPCs. Includes methods for
searching through NPC names, iterating over them, and loading a
set from a file.
"""
self.npcs = {}
self._keys = {}
self._shorts = {}
for npc in npcs: self.add(npc)
def __repr__(self):
return '<%s with %i NPCs>' % (self.__class__.__name__, len(self.npcs))
def __iter__(self):
for k in self.npcs: yield self.npcs[k]
def add(self, npc):
"""Adds a new NPC to the set. Overwrites existing NPCs."""
self.npcs[npc.get_name()] = npc
self._keys[npc.get_key()] = npc
try: self._shorts[npc.get_short()] = npc
except ValueError: pass
def get_by_name(self, name):
"""Get the NPC from the set by its local name."""
return self.npcs[name]
def get_by_key(self, key):
"""Get the NPC from the set by its full npc key."""
return self._keys[key]
def get_by_short(self, short):
"""Get the NPC from the set by its short npc key."""
return self._shorts[short]
def has_key(self, key):
"""Determines if this NPCSet has an NPC with the requested full npc key."""
return self.npcs.has_key(key)
def keys(self):
"""Gets the full npc keys of the NPCs in this set."""
return self.npcs.keys()
def search_all_keys(self, regex=re.compile('')):
"""
Finds keys of all NPCs whose keys match from a regex search (as opposed to match).
"""
return [k for k in self.npcs if regex.search(k) is not None]
@classmethod
def from_txt(cls, txt, encoding=DEF_ENCODING):
"""Creates a new NPCSet (or subclass) from a given file."""
import io
return NPCParser(io.open(txt, 'r', encoding=encoding), cls).parse()
class HeroNPCSet(NPCSet):
_re = RGX_NPC_HNAME
_npc = HeroNPC
class ItemNPCSet(NPCSet):
_re = RGX_NPC_ITEM
_npc = ItemNPC
class UnitNPCSet(NPCSet):
_re = RGX_NPC_UNIT
_npc = UnitNPC
class AbilityNPCSet(NPCSet):
_re = RGX_NPC_ABILITY
_npc = AbilityNPC
class TokenSet(NPCSet):
_re = RGX_NPC_TOKEN
_npc = NPC
class NPCParser:
def __init__(self, filehandle, setclass=NPCSet):
"""
Parser for parsing dota resource files for data on NPCs etc.
Args:
filehandle (file-like object): handle to the open file for reading data
setclass (NPCSet or derivative): class to use for creating new NPCs
from the resource file. Also determines the strings for matching
full npc keys. Default is NPCSet
"""
self.file = filehandle
self._set = setclass
# testing for start/end of a new subdict
@staticmethod
def is_opening(str): return str.strip().startswith(KWD_OPEN)
@staticmethod
def is_closing(str): return str.strip().startswith(KWD_CLOSE)
# testing for comments
@staticmethod
def is_comment(str): return str.strip().startswith(KWD_COMMENT)
# testing for an npc name key to start a new NPC
def is_npc_key(self, str):
if self._set._re.match(str) is None: return False
else: return True
# testing for any key, not necessarily an npc key
def is_key(self, str):
if self.is_comment(str): return False
elif str.count(KWD_QUOTE) == 2: return True
else: return False
# testing a line to see if it contains data
@staticmethod
def is_data_line(line): return len(line.strip().split(KWD_QUOTE)) == 5
# parsing/cleaning data lines in the file
@staticmethod
def parse_data_line(line):
sline = line.split(KWD_QUOTE)
return (sline[1], sline[3])
# parsing key lines
@staticmethod
def parse_key_line(line): return line.strip().split(KWD_QUOTE)[1]
def parse(self):
"""Parses the NPC file and returns a new NPCSet (or of whatever type calls it)."""
# process the file
npcset = self._set()
npcFlag = False
prevKey = ''
npcAttrKeys = []
i = 0
for line in self.file:
i += 1
# arrived at the end of a subdict, so step back a key
if self.is_closing(line):
npcAttrKeys = npcAttrKeys[:-1]
if len(npcAttrKeys) <= 1:
npcFlag = False
try: npcset.add(npc)
except UnboundLocalError:
print 'You may be using the wrong regex to search for keys.'
raise
# inside NPC's attributes section
elif npcFlag:
# arrived at a new subdict, so previous line was a key
if self.is_opening(line): npcAttrKeys.append(prevKey)
# arrived at a data line (key-value pair), add to attributes
elif self.is_data_line(line):
key, value = self.parse_data_line(line)
npc.set(value, *(npcAttrKeys[1:] + [key]))
# arrived at a new key, store for subdicts
elif self.is_key(line): prevKey = self.parse_key_line(line)
# arrived at a new npc's attributes
elif self.is_npc_key(line):
npcKey = self.parse_key_line(line)
prevKey = npcKey
npc = self._set._npc.from_name_and_key(npcKey, npcKey) ## note hero will be undefined for Abilities and wont be able to get_short
npcFlag = True
npcAttrKeys = []
return npcset
class Tooltip:
def __init__(self, **kwargs):
"""
Container for organized tooltip info, for use in main().
Args:
**kwargs: (Optional) keyword arguments which are the descriptors
of the tooltip. Those recognized automatically are:
cooldown (str): cooldown in seconds
cost (str): cost of item
description (str): description text
details (list): list of (descriptor, value) tuples
describing custom details of the npc
icon (str): url of icon image to use in tooltip
lore (str): lore for the NPC
mana (str): mana cost
name (str): name of the npc being described
notes (list): list of strings of notes
scepter (str): special description for scepter upgrade
scepter_mods(list): list of special modifications made to
the NPC upon scepter upgrade
"""
P = {
'name': '', 'description': '', 'details': [], 'cooldown': '',
'mana': '', 'notes': [], 'lore': '', 'scepter': '',
'scepter_mods': [], 'cost': '', 'icon': ''
}
P.update(kwargs)
self._keys = P.keys()
for k in P: self.__dict__[k] = P[k]
def __repr__(self):
return '<%s for %s>:' % (self.__class__.__name__, self.name)
@staticmethod
def _is_lore(string): return (RGX_TTP_LORE.match(string) is not None)
@staticmethod
def _is_desc(string): return (RGX_TTP_DESC.match(string) is not None)
@staticmethod
def _is_aghdesc(string): return (RGX_TTP_AGDS.match(string) is not None)
@staticmethod
def _is_note(string): return (RGX_TTP_NOTE.match(string) is not None)
@staticmethod
def _is_aghmod(string): return (RGX_TTP_AGMD.match(string) is not None)
@staticmethod
def _is_variable(string): return (RGX_TTP_VARI.match(string) is not None)
@staticmethod
def _get_name_key(keys):
if len(keys) == 0: return None
mlen = min([len(k) for k in keys])
return [k for k in keys if len(k) == mlen][0]
@staticmethod
def _descriptor_to_variable(string): return string.rsplit('$',1)[1]
@staticmethod
def from_npc(npc, variables={}):
"""Makes a new Tooltip from an NPC with extra data added in main()."""
# start by getting a list of AbilitySpecial stuff that will be
# substituted for arbitrarily named keys
abilitySpecial = {}
if npc.has_key(MAN_KEY_ASP):
for subdict in npc.get(MAN_KEY_ASP).values(): abilitySpecial.update(subdict)
# now start defining the dictionary items for this tooltip
nameKey = Tooltip._get_name_key(npc.tooltipData.keys())
if nameKey is None: return Tooltip() # for npc's with no tooltip info
name = npc.tooltipData[nameKey]
ttDict = {
'name': name, 'notes': [], 'details': [], 'scepter_mods': [],
'icon': ''
}
# image icon
if npc.icon is not None: ttDict['icon'] = npc.icon
# mana and cooldown (get special formatting in tooltips)
if npc.has_key(MAN_KEY_MAN): ttDict['mana'] = npc.get(MAN_KEY_MAN)
if npc.has_key(MAN_KEY_COO): ttDict['cooldown'] = npc.get(MAN_KEY_COO)
if npc.has_key(MAN_KEY_CST): ttDict['cost'] = npc.get(MAN_KEY_CST)
# other specially treated attributes
for k in npc.tooltipData:
# basic data without special formatting
if Tooltip._is_lore(k): ttDict['lore'] = npc.tooltipData[k]
elif Tooltip._is_desc(k): ttDict['description'] = npc.tooltipData[k]
elif Tooltip._is_note(k): ttDict['notes'].append(npc.tooltipData[k])
elif Tooltip._is_aghdesc(k): ttDict['scepter'] = npc.tooltipData[k]
# scepter mods sometimes (usually?) have special ability
# descriptors that need handling
elif Tooltip._is_aghmod(k):
splitLen = len(k.lower().split(nameKey.lower() + KWD_TITLESEP, 1)[1]) # handles capitalization differences
valueKey = k[-splitLen:]
if valueKey in abilitySpecial:
value = abilitySpecial[valueKey]
descriptor = npc.tooltipData[k]
ttDict['scepter_mods'].append([descriptor, value])
# match a special ability key to the descriptor in a key-value pair
elif k <> nameKey:
split = k.lower().split(nameKey.lower() + KWD_TITLESEP, 1)
if len(split) > 1:
splitLen = len(split[1]) # handles capitalization differences
valueKey = k[-splitLen:]
if valueKey in abilitySpecial:
value = abilitySpecial[valueKey]
descriptor = npc.tooltipData[k]
# replace variables from resource tokens with descriptors
if Tooltip._is_variable(descriptor):
variableKey = Tooltip._descriptor_to_variable(descriptor)
descriptor = '+'
value += ' ' + variables[variableKey]
ttDict['details'].append([descriptor, value])
# other basic "details", e.g. spell immunity, damage type, etc that
# dont have their own tooltips in the resource file
for k in TTP_NPC_BASIC:
if npc.has_key(k):
ttDict['details'].append([TTP_NPC_BASIC[k], TTP_NPC_LAM(k, npc.get(k))])
return Tooltip(**ttDict)
def to_pystr(self):
"""
Returns a string representation of a dictionary containing info for this tooltip.
CAUTION: Highly Experimental
"""
pydict = dict((k, self.__dict__[k]) for k in self._keys)
return str(pydict)
def to_jsstr(self):
"""
Returns a string representation of a dictionary containing info for
this tooltip that could be loaded into a javascript application.
CAUTION: Highly Experimental
"""
import json
return json.dumps(dict((k, self.__dict__[k]) for k in self._keys))
class ImageMap:
KEY = None
REMDIR = ''
LOCDIR = ''
def __init__(self, imageMap):
"""
Container for organized npc images.
Args:
imageMap (dict): mapping from npc short name to image hosting path
(local file or url)
"""
self.map = imageMap
def get(self, key): return self.map[key]
def keys(self): return self.map.keys()
def values(self): return self.map.values()
@staticmethod
def file_to_key(localFile):
return os.path.splitext(os.path.basename(localFile))[0]
@classmethod
def local_to_url(cls, local, host=IMG_DEF_HOST, subdir=None, locdir=None):
# update defaults
if subdir is None: subdir = cls.REMDIR
if locdir is None: locdir = cls.LOCDIR
# skip the parts of the local file path that are not part of the
# remote path and build the remote path from the rest
local = os.path.relpath(local)
locdir = os.path.relpath(locdir)
fileSplit = local.split(os.path.sep)
dirSplit = locdir.split(os.path.sep)
dn = len(dirSplit)
fn = len(fileSplit)
urlParts = host.split('/')
urlParts.extend(subdir.split('/'))
i = 0
while ((i < dn) and (i < fn) and (fileSplit[i] == dirSplit[i])): i += 1
if (i == fn): raise ValueError('Cannot build url for this file with the given parameters.')
urlParts.extend(fileSplit[i:])
# build the url and remove any duplicate slashes
url = '/'.join(urlParts)
url = url[::-1].replace('//','/',url.count('//')-1)[::-1]
return url
@classmethod
def from_local(cls, files, l2u=None, f2k=None):
if l2u is None: l2u = cls.local_to_url
if f2k is None: f2k = cls.file_to_key
return cls(dict((f2k(f), l2u(f)) for f in files))
class HeroImageMap(ImageMap):
KEY = 'heroes'
REMDIR = IMG_DEF_SUBDIR[KEY]
LOCDIR = VPK_DEF_IMAGES[KEY][0]
class ItemImageMap(ImageMap):
KEY = 'items'
REMDIR = IMG_DEF_SUBDIR[KEY]
LOCDIR = VPK_DEF_IMAGES[KEY][0]
class AbilityImageMap(ImageMap):
KEY = 'abilities'
REMDIR = IMG_DEF_SUBDIR[KEY]
LOCDIR = VPK_DEF_IMAGES[KEY][0]
# #############################################################################
# FUNCTIONS ###################################################################
# #############################################################################
def download_resources(urls=None, destination='.'):
"""
Downloads a list of text files at the given urls to the give folder.
Args:
urls (list): (optional) list of urls to download. Default (None)
downloads urls of resource files from dotabuff
desination (list): (optional) directory to save files. By default,
saves to current directory ('.')
Returns:
list: paths to downloaded files
"""
import urllib2, urlparse
if urls is None: urls = [DEF_RESOURCE_FILES[k]['url'] for k in DEF_RESOURCE_FILES]
paths = []
for url in urls:
request = urllib2.Request(url)
connection = urllib2.urlopen(request)
data = connection.read()
basename = os.path.basename(urlparse.urlparse(url).path)
dest = os.path.join(destination, basename)
with open(dest, 'w') as fh: fh.write(data)
paths.append(dest)
return paths
def vpk_extract(vpk=VPK_DEF_VPK, files=VPK_DEF_FIL):
"""
Extracts files from a vpk.
Args:
vpk (str): (optional) path to the vpk file to extract from. Default is
VPK_DEF_VPK
files (list): (optional) list of file paths to extract, where the path
is relative to the root of the vpk. If a file path is determined
to be a directory, the whole directory and its sub-directories will
be extracted. Default is VPK_DEF_FIL
Returns:
list: paths to extracted files, which will have the same directory
structure as the vpk
"""
# imports
import subprocess, os
# get a listing of files in the vpk
call = VPK_CALL_LISTFILES + [r'%s' % vpk]
p = subprocess.Popen(call, stdout=subprocess.PIPE)
out, err = p.communicate()
vpkFiles = [s.strip() for s in out.strip().split()]
# match requested files to those given in the vpk
files = [os.path.relpath(f) for f in files]
toExtract = []
for vpkFile in vpkFiles:
vpkRel = os.path.relpath(vpkFile)
for requestedFile in files:
if vpkRel.startswith(requestedFile):
toExtract.append(vpkFile)
# make temporary directories for vpk.exe to extract to
for f in toExtract:
if not os.path.exists(os.path.dirname(f)):
os.makedirs(os.path.dirname(f))
# extract files
for f in toExtract:
call = VPK_CALL + [r'%s' % vpk] + [r'%s' % f]
subprocess.call(call)
# check that all the files now exist
for f in toExtract:
if not os.path.exists(f):
raise ValueError('File not successfully extracted: %s' % f)
return toExtract
def write_server_data(heroes, abilities, items, units, outfile):
"""
Writes a dict to a Python cPickle for the server to process video and
replay data. For use in main()
"""
import cPickle
data = {'ability_order': {}}
for a in abilities:
if a.has_key('ID'):
data[a.get_key().lower()] = a.get('ID').lower()
for h in heroes:
name = h.get_name().lower()
key = h.get_key().lower()
data[name] = key
data[key] = name
data['ability_order'][name] = h.ablorder
for a in h.abilities.values():
data[a.get_key().lower()] = a.get('ID').lower()
for i in items:
data[i.get_key().lower()] = i.get('ID').lower()
for u in units:
name = u.get_name().lower()
key = u.get_key().lower()
data[name] = key
data[key] = name
data['ability_order'][name] = u.ablorder
for a in u.abilities.values():
data[a.get_key().lower()] = a.get('ID').lower()
with open(outfile, 'wb') as fh: cPickle.dump(data, fh)
return outfile
def write_client_data(heroes, abilities, items, units, outfile):
"""
Writes a javascript file for the client to use when interpreting
data sent from the server.
"""