forked from HENDRIX-ZT2/APE2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathAPE2.py
1866 lines (1684 loc) · 74.5 KB
/
APE2.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
from tkinter import BooleanVar,StringVar,IntVar,Tk,ttk,Menu,filedialog,Checkbutton,Text,messagebox
import random
import xml.etree.ElementTree as ET
import zipfile
import os
import sys
import re
import time
import requests
import webbrowser
from urllib import parse
from bs4 import BeautifulSoup
#custom file processing
import WIKI
import BFB
import BFMAT
import NIF
import ExtraUI
import sentences
try:
approot = os.path.dirname(os.path.abspath(__file__))
except NameError: # We are the main py2exe script, not a module
import sys
approot = os.path.dirname(os.path.abspath(sys.argv[0]))
os.environ['REQUESTS_CA_BUNDLE'] = os.path.join(approot, "cacert.pem")
def clean_directory(top, target=""):
try:
for rootdir, dirs, files in os.walk(top, topdown=False):
for name in files:
path = os.path.join(rootdir, name)
if target in path: os.remove(path)
for name in dirs:
path = os.path.join(rootdir, name)
if target in path: os.rmdir(path)
except:
messagebox.showinfo("Error","Could not clear "+top)
def create_dir(dir):
"""
Create folder if it does not exist
"""
if not os.path.exists( dir ):
try:
os.makedirs( dir )
except OSError as exc: # Guard against race condition
if exc.errno != errno.EEXIST:
raise
return dir
class Application:
def print_s(self, *msg):
try:
print(*msg)
except:
print("Could not print this!")
def load_translations(self):
self.translations={}
self.languages_2_codes={}
for file in os.listdir(self.dir_translations):
if file.endswith(".txt"):
try:
code, lang = file[:-4].split("=")
self.languages_2_codes[lang]=code
self.translations[code]={}
for line in self.read_encoded(os.path.join(self.dir_translations,file)).split("\r\n"):
line = line.strip()
if line:
k,v = line.split("=")[0:2]
self.translations[code][k] = v
except:
messagebox.showinfo("Error","Could not load translation " + os.path.basename(file))
def read_encoded(self, file, encodings=("utf-8", 'iso-8859-1', "cp1252" ) ):
"""Open a file and decode to UTF8"""
f = open(file, 'rb')
data = f.read()
f.close()
for encoding in encodings:
try:
return data.decode(encoding)
except UnicodeDecodeError:
messagebox.showinfo("Error","Illegal characters in encoding. Trying ANSI to UTF-8 conversion... Hit the original coder with a stick!")
def write_utf8(self, file, data):
"""Save UTF8 to file"""
f = open(file, 'wb')
f.write(data.encode('utf-8'))
f.close()
def debug_xml_file(self, file, err):
"""de-bugs an xml styled file with duplicate attributes in a line that otherwise crashes the parser. Also fixes BFM files with two root tags"""
lineno, column = err.position
self.print_s(err.msg)
self.update_message("Debugging " + os.path.basename(file))
data = self.read_encoded(file)
lines = data.split("\n")
for line in lines:
i = lines.index(line)
#not well formed
if i+1 == lineno:
if err.code == 4:
if "&" in line:
self.print_s("Not well-formed - removing &")
line = line.replace("&","and")
else:
self.print_s("Not well-formed - adding space")
line = line[:column]+" "+line[column:]
#double attr
if err.code == 8:
deletelist=[]
attribs=re.findall('[a-z,A-Z,_]*="',line)
for attrib in attribs:
if attribs.count(attrib)>1 and attrib not in deletelist:
deletelist.append(attrib)
for attr in deletelist:
self.print_s("Duplicate attribute - removing")
line=re.sub(attr+'[a-zA-Z0-9.\s-]*?" ', "", line, count=1)
#junk after xml
lines[i] = line.replace("</BFM>","")
data = '\n'.join(lines)
if file.endswith((".bfm",".BFM")):
self.print_s("Junk after XML - fixing")
data +="\r\n</BFM>"
else:
if err.code == 9:
self.print_s("Junk after XML - fixing")
data=data[:-1]
self.write_utf8(file, data)
def parse_xml(self, filepath, debug = 10):
"""Parse an XML, try to debug and return the tree"""
#http://stackoverflow.com/questions/27779375/get-better-parse-error-message-from-elementtree
try:
return ET.parse(filepath)
except ET.ParseError as err:
self.debug_xml_file(filepath, err)
#9 (junk) is not really supported
if err.code in (4,8,9):
if debug > 0: return self.parse_xml(filepath, debug = debug -1)
else:
messagebox.showinfo("Error","Could not debug "+filepath+". Must be debugged manually in a text editor!\n"+err.msg)
def indent(self, e, level=0):
i = "\n" + level*" "
if len(e):
if not e.text or not e.text.strip(): e.text = i + " "
if not e.tail or not e.tail.strip(): e.tail = i
for e in e: self.indent(e, level+1)
if not e.tail or not e.tail.strip(): e.tail = i
else:
if level and (not e.tail or not e.tail.strip()): e.tail = i
def get_binder(self, root_obj, tag, attr):
for binder in root_obj.getiterator(tag):
if binder.attrib['binderName']==attr:
return binder
def is_real(self, file):
"""Test if an XML is something like a species that we need"""
#Giant Sable antelope Items folder...
file = file.lower()
#dummies
if ".xml" in file:
if "eggs" in file: return False
if "items" in file: return False
if "puzzles" in file: return False
#because lang files also contain "entityname"
if "strings" in file: return False
if "entries" in file: return False
#fine decisions for animals
if "animals" in file and "_" in file: return False
if "idae" in file: return False
if "formes" in file: return False
if "oidea" in file: return False
if os.path.basename(file) in self.badlist: return False
if "entities" in file: return True
def is_z2f(self, name):
return name.lower().endswith((".z2f",".zip"))
def read_z2f_lib(self):
"""Read all Z2F files in the ZT2 folder and store their contents in a dict so we find the latest version for updated files"""
self.progbar.start()
#this dict remembers all files and where an updated version was found so it can be fetched to construct the animal
self.file_to_z2f = {}
#lowercase for lookup
self.lower_to_content = {}
if not os.path.isdir(self.dir_zt2):
messagebox.showinfo("Error","Could not find 'Zoo Tycoon 2' program folder!")
self.dir_zt2 = filedialog.askdirectory(initialdir=self.dir_zt2, parent=self.parent, title="Locate 'Zoo Tycoon 2' program folder" )
self.save_list(os.path.join(os.path.dirname(os.getcwd()),self.dir_config,"dirs_zt2.txt"), (self.dir_zt2, self.dir_downloads))
if not os.path.isdir(self.dir_downloads):
messagebox.showinfo("Error","Could not find ZT2's 'downloads' folder!")
self.dir_downloads = filedialog.askdirectory(initialdir=self.dir_downloads, parent=self.parent, title="Locate ZT2 'downloads' folder" )
self.save_list(os.path.join(os.path.dirname(os.getcwd()),self.dir_config,"dirs_zt2.txt"), (self.dir_zt2, self.dir_downloads))
z2f_files = [os.path.join(self.dir_zt2, z2f_name) for z2f_name in os.listdir(self.dir_zt2) if self.is_z2f(z2f_name)]
z2f_dls = [os.path.join(root, z2f_name) for root, dirs, files in os.walk(self.dir_downloads) for z2f_name in files if self.is_z2f(z2f_name)]
files = z2f_files + z2f_dls
self.z2f_to_path = {}
#a dict of lists
self.versions = {}
#sort everything by filename instead of path
for filepath in sorted(files, key=lambda file: os.path.basename(file)):
self.update_message('Reading '+os.path.basename(filepath)[0:50])
try:
z2f_file = zipfile.ZipFile(filepath)
#store for the zt2 issue finder
self.z2f_to_path[z2f_file] = os.path.basename(filepath)
contents = z2f_file.namelist()
# now we store all contents in a dict and remember where they were from to be able to rebuild them later, also it only stores the latest versions
for r_path in contents:
l_path = os.path.normpath(r_path.lower())
self.file_to_z2f[r_path] = z2f_file
self.lower_to_content[l_path] = r_path
#lookup by standardized name
if l_path not in self.versions:
self.versions[l_path] = []
#append the path where this was found
self.versions[l_path].append(os.path.basename(filepath))
except zipfile.BadZipFile:
messagebox.showinfo("Error",os.path.basename(filepath)[0:50]+' is a not a ZIP file! Maybe a RAR file?')
self.progbar.stop()
self.fill_entity_tree()
self.fill_zoopedia()
def zip_z2f(self, defaultname):
"""Packs the temp dir into a Z2F of given name"""
z2f_path = filedialog.asksaveasfilename(filetypes = [('Z2F', '.z2f')], defaultextension=".z2f", initialfile=defaultname, initialdir=self.dir_zt2, parent=self.parent, title="Save Z2F File" )
if z2f_path:
self.update_message("Creating Z2F file...")
z2f = zipfile.ZipFile(z2f_path, 'w')
for root, dirs, files in os.walk(os.getcwd()):
for file in files:
z2f.write(os.path.join(root, file), os.path.join(root, file).replace(os.getcwd(),""), zipfile.ZIP_DEFLATED)
z2f.close()
self.update_message("Created "+os.path.basename(z2f_path))
def unzip_z2f(self, assorted_files):
"""Unzips a list of files, case insensitive, from the Z2F files that have been opened on startup"""
self.update_message("Unzipping Files")
for lower in assorted_files:
file = self.lower_to_content[os.path.normpath(lower.lower())]
try: self.file_to_z2f[file].extract(file)
except: self.update_message("ERROR: Could not unzip_z2f "+file)
def find_file(self, filename, exts):
"""Finds the best file from a list of extensions, also ignoring case, optionally stripping the extensions. First try to find temp versions, then get from Z2F if not found
Always returns a normpath"""
#first check locally
norm = os.path.normpath(filename)
self.print_s(filename)
lowername = norm.lower()
#strip extensions
for ext in exts:
lowername = lowername.replace(ext,"")
for ext in exts:
newlowername = lowername+ext
#first check if it exists already exactly like that
if os.path.isfile(norm):
return norm
#then walk to see if the file exists, but it does not have the right / missing dir
for root, dirs, files in os.walk(os.getcwd()):
rootrel = os.path.relpath(root)
for file in files:
jo = os.path.join(rootrel, file)
if newlowername in jo.lower():
return os.path.relpath(jo)
#then look in the z2f
#directly
if newlowername in self.lower_to_content:
self.unzip_z2f([self.lower_to_content[newlowername],])
return os.path.normpath(self.lower_to_content[newlowername])
#and also for wrong / missing paths
for lowerc in self.lower_to_content:
if newlowername in lowerc:
self.unzip_z2f([self.lower_to_content[lowerc],])
return os.path.normpath(self.lower_to_content[lowerc])
def add_to_list(self, e, l, exts=("",)):
try:
f = self.find_file(e, exts)
if f:
#note: lower to be able to check for dupes effectively
if f.lower() not in [item.lower() for item in l]:
l.append(f)
except:
messagebox.showinfo("Warning",e+" does not exist! It might have been used in a BETA version but is likely irrelevant now.")
def is_this_entity(self, entity, codename):
if codename.lower()+"." in entity.lower() or codename.lower()+"_" in entity.lower():
return True
return False
def gather_files(self, codename, newname = "", main_only=False, eggs_only=False, dependencies=False):
"""Gathers files from XML files. Unzips and replaces if a new codingname is given exists, otherwise just return the files from temp"""
self.print_s("Selected",codename, newname)
#in rare cases, we want to use more than one replacer -> foliage -> see below
replacer = re.compile(re.escape(codename), re.IGNORECASE)
replacers = [replacer,]
self.update_message("Gathering files...")
ai_files = []
bf_files = []
bfm_files = []
bfmat_files = []
dds_files = []
model_files = []
xml_files = []
dep_files = []
#AI & XML files
if newname:
ai_files = [self.lower_to_content[entity] for entity in self.lower_to_content if self.is_this_entity(entity, codename) and "tasks" in entity]
xml_files = [self.lower_to_content[entity] for entity in self.lower_to_content if self.is_this_entity(entity, codename) and ".xml" in entity]
self.unzip_z2f(ai_files + xml_files)
else:
#better solution for walk in temp dir?
for root, dirs, files in os.walk(os.getcwd()):
for file in files:
if self.is_this_entity(file, codename):
if file.endswith((".tsk", ".beh", ".trk")):
ai_files.append(os.path.relpath(os.path.join(root, file)))
if file.endswith((".xml")):
xml_files.append(os.path.relpath(os.path.join(root, file)))
#as mentioned above, this must only occur for foliage
if any("foliage" in file for file in xml_files):
#only escape the first split so we don't replace biomes
#this is dangerous for things like
#showplatform, showplatform_mm
#for x in codename.split("_"):
self.update_message("Foliage name replace mode!!!")
replacers.append(re.compile(re.escape(codename.split("_")[0]), re.IGNORECASE))
if main_only:
main_xmls = []
for xml in xml_files:
if self.is_real(xml) and not self.is_bad(xml):
main_xmls.append(xml)
#double check, maybe integrate into the normal check
#pack loading calls with "" codename
#thus if we have a codename, we usually want to load only that very one main XML so make sure we don't load shit
if codename:
if len(main_xmls) > 1:
alt = [xml for xml in main_xmls if "\\"+codename.lower()+".xml" in xml]
return alt
return main_xmls
if eggs_only:
egg_xmls = []
for xml in xml_files:
if "eggs" in xml:
egg_xmls.append(xml)
return egg_xmls
#only replace ai files here because we parse the xml files below
if newname:
for ai_file in ai_files:
self.rename_replace_file(ai_file, replacers, newname)
#parse XML files
for xml in xml_files:
#might not be parsable so we just skip it here
#perhaps even remove the file?
xml_tree = self.parse_xml(xml)
if xml_tree:
BFTypedBinder = xml_tree.getroot()
for UIToggleButton in BFTypedBinder.getiterator("UIToggleButton"):
default = UIToggleButton.find("./UIAspect/default")
if default is not None:
self.add_to_list(default.attrib['image'], dds_files)
for BFNamedBinder in BFTypedBinder.getiterator("BFNamedBinder"):
#find the bfm
BFActorComponent = BFNamedBinder.find("./instance/BFPhysObj/BFActorComponent")
if BFActorComponent is not None:
if "actorfile" in BFActorComponent.attrib:
self.add_to_list(BFActorComponent.attrib['actorfile'], bfm_files)
#find static models
for type in ("BFSimpleLODComponent", "BFRSceneGraphComponent", "BFSceneGraphComponent"):
component = BFNamedBinder.find("./instance/BFPhysObj/"+type)
if component is not None:
self.add_to_list(component.attrib['modelfile'], model_files, (".bfb", ".nif"))
#find skins
BFSharedRandomTextureInfo = BFNamedBinder.find("./shared/BFSharedRandomTextureInfo")
if BFSharedRandomTextureInfo is not None:
for replacementSet in BFSharedRandomTextureInfo:
for group in replacementSet:
for item in group:
#mat = item.attrib['material']
self.add_to_list(item.attrib['image'], dds_files)
if BFNamedBinder.attrib['binderName'] == 'texController':
stateList = BFNamedBinder.find("./instance/BFAITextureController/stateList")
for state in stateList:
textureData = state.find("./textureData")
for binder in textureData:
self.add_to_list(binder.attrib['image'], dds_files)
for ZTPuzzlePiece in BFTypedBinder.getiterator("ZTPuzzlePiece"):
self.add_to_list(codename+"/"+ZTPuzzlePiece.attrib['texture'], dds_files)
self.rename_replace_file(xml, replacers, newname)
#xmls done, inspect BFMs now
for bfm_file in bfm_files:
bfm_tree = self.parse_xml(bfm_file)
BFM = bfm_tree.getroot()
self.add_to_list(BFM.attrib["modelname"], model_files, (".bfb", ".nif"))
if newname:
BFM.attrib["modelname"] = replacer.sub(newname, BFM.attrib["modelname"])
Graph = BFM.find("./Graph")
Graph.attrib["name"] = replacer.sub(newname, Graph.attrib["name"])
self.indent(BFM)
bfm_tree.write(bfm_file)
self.rename_replace_file(bfm_file, replacers, newname)
#for the dependency checker
for animation in BFM:
try:
dep_files.append(animation.attrib["anim"])
except:
pass
if dependencies:
return dep_files
#BFMs done, inspect models now
for model in model_files:
if model.endswith(".bfb"):
for matname in BFB.process(model, codename, newname):
self.add_to_list(matname+".bfmat", bfmat_files)
if model.endswith(".nif"):
for ddsname in NIF.process(model, codename, newname):
self.add_to_list(ddsname, dds_files)
self.rename_replace_file(model, replacers, newname)
#models done, inspect BFMATs now
for bfmat in bfmat_files:
for texture in BFMAT.process(bfmat, codename, newname):
self.add_to_list(texture, dds_files)
self.rename_replace_file(bfmat, replacers, newname)
#BFMATs done, rename DDSs now
for dds in dds_files:
self.rename_replace_file(dds, replacers, newname)
return ai_files, bf_files, bfm_files, bfmat_files, dds_files, model_files, xml_files
def rename_replace_file(self, file, replacers, new):
""" Rename a file, if a new name is given. Replace its content if possible too, ie. in XML type files where length does not matter"""
try:
os.chmod(file, 0o664)
if new:
if file.endswith((".beh",".tsk",".xml",".bfmat",)):
for replacer in replacers:
self.write_utf8(file, replacer.sub(new, self.read_encoded(file)))
self.pretty_print(file)
#the first one may already change the name, so beware
for replacer in replacers:
try:
os.renames(file, replacer.sub(new, file))
file = replacer.sub(new, file)
#self.print_s("renamed",file,replacers[0].sub(new, file))
except:
self.print_s("failed",file)
pass
except:
#for example, happens when the same icon is called by two gift xmls
self.update_message("File "+file+" has already been renamed and replaced.")
def load_z2f(self):
"""Unzips a Z2F file into the temp dir and loads the entities into the project"""
start = time.clock()
z2f_list = filedialog.askopenfilenames(filetypes = [('Z2F', '.z2f')], defaultextension=".z2f", initialdir=self.dir_zt2, parent=self.parent, title="Load Z2F File" )
if z2f_list:
for z2f in z2f_list:
self.update_message("Unzipping "+os.path.basename(z2f))
z = zipfile.ZipFile(z2f,"r")
z.extractall("")
z.close()
#find new entities
for main_xml in self.gather_files("", main_only=True):
codename = os.path.basename(main_xml)[:-4]
if codename not in self.project_entities:
self.project_entities.append(codename)
self.update_ui("")
self.update_message("Done in {0:.2f} seconds".format(time.clock()-start))
def save_and_test(self):
"""If something is in the project, save it and run ZT.exe"""
if self.save_z2f(): os.startfile(os.path.join(self.dir_zt2,"zt.exe"))
def save_z2f(self):
"""If something is in the project, save it"""
if self.project_entities:
name = "z"
for codename in self.project_entities:
name += "_"+codename
if len(name) > 50:
name = name[0:49]
try:
self.zip_z2f(name)
except PermissionError:
messagebox.showinfo("Error","You do not have the required permission to save here, or you are trying to overwrite a file that is already opened!")
else:
self.update_message("Nothing to save!")
def find_dependencies(self):
"""If something is in the project, see if it needs other anims"""
z2f_dependencies = []
missing_anims = []
if self.project_entities:
report = ""
for codename in self.project_entities:
for r_path in self.gather_files(codename, dependencies=True):
l_path = os.path.normpath(r_path.lower())
try:
for z2f_name in self.versions[l_path]:
if z2f_name not in z2f_dependencies:
z2f_dependencies.append(z2f_name)
except:
missing_anims.append(os.path.basename(r_path))
report += "\n\n"+codename+" requires animations from:\n"+"\n".join(z2f_dependencies)
if missing_anims:
report+="\nThe following animations are missing from your installation:\n"+"\n".join(missing_anims)
messagebox.showinfo("Dependencies",report.strip())
else:
self.update_message("Load or clone something first, then try again!")
def find_interferences(self):
"""Load a z2f file and see which parts of it are overwritten by which ensuing files."""
start = time.clock()
z2f_list = filedialog.askopenfilenames(filetypes = [('Z2F', '.z2f')], defaultextension=".z2f", initialdir=self.dir_zt2, parent=self.parent, title="Find Interferences with this Z2F File" )
if z2f_list:
for z2f_path in z2f_list:
z2f_name = os.path.basename(z2f_path)
self.update_message("Reading "+z2f_name)
z = zipfile.ZipFile(z2f_path,"r")
contents = z.namelist()
z.close()
interferences = []
for r_path in contents:
l_path = os.path.normpath(r_path.lower())
overwrites = self.versions[l_path]
#see if it exists in other files as well
if len(overwrites) > 1:
ind = overwrites.index(z2f_name)
#start at the given index of this z2f file and see what comes after
for overwriter in overwrites[ind+1:]:
if overwriter not in interferences:
interferences.append(overwriter)
#print(overwrites)
report = "Contents of "+z2f_name+" are overwritten by:\n"+"\n".join(interferences)
messagebox.showinfo("Interferences",report)
self.update_message("Done in {0:.2f} seconds".format(time.clock()-start))
def create_hack(self):
"""Dynamic hack creation"""
if self.abort_erase(): return
self.update_message("Gathering Files")
assorted_files = [entity for entity in self.file_to_z2f if os.path.dirname(entity) in self.entity_types and self.is_real(entity)]
self.unzip_z2f(assorted_files)
self.update_message("Creating Hack")
# options_BFAIEntityDataShared = {"s_Product":"AD" }
# options_ZTPlacementData = {}
# options_footself.print_s = {}
options_BFAIEntityDataShared = {"f_RequiredInitialSpace":"1",
"f_RequiredAdditionalSpace":"1",
"f_RequiredInitialTankSpace":"1",
"f_RequiredAdditionalTankSpace":"1",
"f_RequiredTankDepth":"1"}
options_ZTPlacementData = {"waterPlacement":"true",
"tankPlacement":"true",
"landPlacement":"true"}
options_footself.print_s = {"width":"1",
"height":"1"}
#results=[]
for rootdir, dirs, files in os.walk(os.getcwd()):
for file in files:
if "frontgate" not in file:
xml_path=os.path.join(rootdir, file)
xml_tree = self.parse_xml(xml_path)
if xml_tree is not None:
BFTypedBinder = xml_tree.getroot()
BFAIEntityDataShared = BFTypedBinder.find("./shared/BFAIEntityDataShared")
if BFAIEntityDataShared is not None:
#for item in BFAIEntityDataShared.keys():
# if item not in results: results.append(item)
for option in options_BFAIEntityDataShared:
#only overwrite
if option in BFAIEntityDataShared.attrib: BFAIEntityDataShared.attrib[option]=options_BFAIEntityDataShared[option]
#BFAIEntityDataShared.attrib[option]=options_BFAIEntityDataShared[option]
ZTPlacementData = BFTypedBinder.find("./shared/ZTPlacementData")
if ZTPlacementData is not None:
for option in options_ZTPlacementData: ZTPlacementData.attrib[option]=options_ZTPlacementData[option]
cfootself.print_s = ZTPlacementData.find("./cfootself.print_s")
if cfootself.print_s is not None:
for option in options_footself.print_s: cfootself.print_s.attrib[option]=options_footself.print_s[option]
dfootself.print_s = ZTPlacementData.find("./dfootself.print_s")
if dfootself.print_s is not None:
for option in options_footself.print_s: dfootself.print_s.attrib[option]=options_footself.print_s[option]
BFGBiomeData = BFTypedBinder.find("./shared/BFGBiomeData")
if BFGBiomeData is not None:
max = "10"
if "locationSensitivity" in BFGBiomeData.attrib: max = BFGBiomeData.attrib["locationSensitivity"]
for biome in BFGBiomeData: biome.attrib["sensitivity"]=max
self.indent(BFTypedBinder)
xml_tree.write(xml_path)
#results.sort()
#self.save_list("test.txt", results)
self.zip_z2f("zBiomeSpacePlacementHack.z2f")
clean_directory(os.getcwd())
def find_bugs(self):
"""Dynamic bug finder. Only reports bugs from space in location atm."""
if self.abort_erase(): return
start = time.clock()
self.update_message("Gathering Files")
assorted_files = [entity for entity in self.file_to_z2f if os.path.dirname(entity) in self.entity_types and self.is_real(entity)]
self.unzip_z2f(assorted_files)
self.update_message("Looking for Bugs")
for rootdir, dirs, files in os.walk(os.getcwd()):
for file in files:
if "frontgate" not in file:
xml_path=os.path.join(rootdir, file)
xml_tree = self.parse_xml(xml_path)
if xml_tree is not None:
BFTypedBinder = xml_tree.getroot()
BFGBiomeData = BFTypedBinder.find("./shared/BFGBiomeData")
if BFGBiomeData is not None:
loc = BFGBiomeData.attrib["location"]
if " " in loc:
#change this?
realfile = self.find_file(file)
z2f = self.file_to_z2f[realfile]
z2fname = self.z2f_to_path[z2f]
messagebox.showinfo("Found Bug","Space in "+loc+" location in "+file+" in "+z2fname)
clean_directory(os.getcwd())
self.update_message("Done in {0:.2f} seconds".format(time.clock()-start))
def build_bfms(self):
nodes=[]
bffolder = filedialog.askdirectory(initialdir=os.getcwd(),mustexist=True,parent=self.parent,title="Select a folder containing BFM/NIF and BF files")
if bffolder:
bflist = [bf[:-3] for bf in os.listdir(bffolder) if bf.endswith(".bf")]
niflist = [model[:-4] for model in os.listdir(bffolder) if (model.endswith(".nif") or model.endswith(".bfb"))]
if niflist and bflist:
for nif in niflist:
name=nif.split("_")[0]
BFM = ET.Element('BFM')
BFM.attrib["modelname"] = "entities/units/animals/"+name+"/"+nif+".nif"
shortnames = []
for anim in bflist:
split = anim.split("_")
if split[-2] not in nodes:
nodes.append(split[-2])
short_anim = "_".join(anim.split("_")[-2:])
shortnames.append(short_anim)
animation=ET.SubElement(BFM, "animation")
animation.attrib["anim"] = "entities/units/animals/"+name+"/"+anim+".bf"
animation.attrib["animName"] = short_anim
animation.attrib["animSpeed"] = "1.0"
animation.attrib["explicitUseOnly"] = "false"
animation.attrib["debug"] = "false"
animation.attrib["resolveUnitCollisions"] = "true"
animation.attrib["load"] = "true"
Graph=ET.SubElement(BFM, "Graph")
Graph.attrib["name"] = nif
Graph.attrib["version"] = "1"
for nodename in nodes:
node = ET.SubElement(Graph, "node")
node.attrib["name"] = nodename
table = ET.SubElement(node, "table")
for anim in shortnames:
if anim.startswith(nodename+"_"):
if nodename+"_2" in anim:
edgename = anim.replace(nodename+"_2","")
edge=ET.SubElement(node, "edge")
edge.attrib["name"] = edgename
etable=ET.SubElement(edge, "table")
ET.SubElement(etable, anim)
else:
ET.SubElement(table, anim)
bfmtree=ET.ElementTree()
bfmtree._setroot(BFM)
self.indent(BFM)
bfm_path = bffolder+"/"+nif+".bfm"
try:
bfmtree.write(bfm_path)
except:
os.chmod(bfm_path, 0o664)
bfmtree.write(bfm_path)
printstr=""
for nif in niflist:
printstr+=nif+".bfm\n"
messagebox.showinfo("Created BFMs", printstr)
return
messagebox.showinfo("Error","Select a folder containing at least one BFB or NIF model and BF animations.")
def get_virtual_nodes(self, file):
"""Dynamic bug finder. Only reports bugs from space in location atm."""
virtual_nodes = []
#xml_path=os.path.join(rootdir, file)
xml_tree = self.parse_xml(file)
if xml_tree is not None:
BFTypedBinder = xml_tree.getroot()
for BFNamedBinder in BFTypedBinder.getiterator("BFNamedBinder"):
#find the bfm
BFAIEntityDataShared = BFTypedBinder.find("./shared/BFAIEntityDataShared")
if BFAIEntityDataShared is not None:
for att in BFAIEntityDataShared.attrib:
if att.startswith("p_"): virtual_nodes.append(att)
for BFNamedBinder in BFTypedBinder.getiterator("BFNamedBinder"):
#find the bfm
virtualNodes = BFNamedBinder.find("./shared/BFSharedPhysVars/virtualNodes")
if virtualNodes is not None:
for node in virtualNodes:
virtual_nodes.append(node.tag)
return virtual_nodes
def debug_entity(self):
"""Debugger for everything in the current project"""
codename = self.var_current_codename.get()
if codename:
ai_files, bf_files, bfm_files, bfmat_files, dds_files, model_files, xml_files = self.gather_files(codename)
self.update_message("Looking for Bugs")
self.behsets_e = []
self.behsets_n = []
self.anims_e = []
self.anims_n = []
#bones in lowercase
self.bones_e = []
self.bones_n = []
self.macros_e = []
for file in model_files:
if file.endswith(".bfb"):
for bone in BFB.get_bones(file):
if bone not in self.bones_e:
self.bones_e.append(bone)
for file in xml_files:
xml_tree = self.parse_xml(file)
if xml_tree is not None:
BFTypedBinder = xml_tree.getroot()
for BFNamedBinder in BFTypedBinder.getiterator("BFNamedBinder"):
BFAIEntityDataShared = BFTypedBinder.find("./shared/BFAIEntityDataShared")
if BFAIEntityDataShared is not None:
for att in BFAIEntityDataShared.attrib:
if att.startswith("p_"):
self.bones_e.append(att.lower())
for BFNamedBinder in BFTypedBinder.getiterator("BFNamedBinder"):
MACROS = BFNamedBinder.find("./shared/BFTextTagMacrosComponent/MACROS")
if MACROS is not None:
for macro in MACROS:
te = macro.attrib["text"]
s=te.split("'")
for i in range(0,len(s)):
if not i%2==0:
self.macros_e.append(s[i])
#self.print_s(re.search("'(.*)'", macro.attrib["text"]).group(1))
virtualNodes = BFNamedBinder.find("./shared/BFSharedPhysVars/virtualNodes")
if virtualNodes is not None:
for node in virtualNodes:
self.bones_e.append(node.tag.lower())
#self.print_s(ai_files, bfm_files)
for file in bfm_files:
BFM = self.parse_xml(file).getroot()
for animation in BFM.findall("animation"):
animName = animation.attrib["animName"]
#test only if using 1 list for all
#would be better to test every single bfm seperately
if animName not in self.anims_e:
self.anims_e.append(animName)
for file in ai_files:
if file.endswith(".beh"):
BehaviorSets = self.parse_xml(file).getroot()
self.debug_ai(BehaviorSets)
for behavior in BehaviorSets:
self.behsets_e.append(behavior.tag)
for file in ai_files:
if file.endswith(".tsk"):
BFAITaskTemplateList = self.parse_xml(file).getroot()
self.debug_ai(BFAITaskTemplateList)
self.anims_m = [anim for anim in self.anims_n if anim not in self.anims_e]
self.macros_m = [anim for anim in self.macros_e if anim not in self.anims_e]
self.bones_m = [bone for bone in self.bones_n if bone.lower() not in self.bones_e]
self.behsets_m = [behset for behset in self.behsets_n if behset not in self.behsets_e]
messagebox.showinfo("Debugging Info","These nodes are used in the BEH and TSK but not defined in the BFB or XML virtual nodes: "+str(self.bones_m)+"\n\nThese animations are used in the XML macros but not defined in the BFM: "+str(self.macros_m)+"\n\nThese animations are used in the BEH and TSK but not defined in the BFM: "+str(self.anims_m)+"\n\n These BEH sets are used in the BEH and TSK but not defined in the BEH: "+str(self.behsets_m))
self.update_message("Done")
def debug_ai(self, el):
"""Parse along an element and log its content"""
for child in el:
self.debug_ai(child)
if el.tag == "randomAnims":
self.anims_n.append(child.tag)
for att in el.attrib:
if att in ("anim", "targetAnim"):
self.anims_n.append(el.attrib[att])
if att in ("behSet", "detachBehSet", "targetBehSet", "subjectBehSet"):
self.behsets_n.append(el.attrib[att])
if att in ("subjectNode", "targetNode"):
self.bones_n.append(el.attrib[att])
def abort_erase(self):
if self.project_entities:
if not messagebox.askokcancel("Warning","Continuing will erase your current project! Do you want to continue?"): return True
#clean the gui and project files and clean temp, just to be sure
for entity in self.project_entities: self.delete_entity()
clean_directory(os.getcwd())
return False
def document(self):
"""ZT2 documentation generator"""
if self.abort_erase(): return
self.update_message("Gathering Files")
assorted_files = [entity for entity in self.file_to_z2f if "tasks" in entity.lower()]
self.unzip_z2f(assorted_files)
self.update_message("Looking for Bugs")
self.log_children={}
self.log_attribs={}
self.log_parameters={}
for rootdir, dirs, files in os.walk(os.getcwd()):
for file in files:
xml_path=os.path.join(rootdir, file)
xml_tree = self.parse_xml(xml_path)
if xml_tree is not None:
BFTypedBinder = xml_tree.getroot()
self.scan(BFTypedBinder)
if True:
#cut = 100
children_cut = 99999
attribs_cut = 99999
parameters_cut = 5
for e in self.log_children:
if len(self.log_children[e]) > children_cut:
self.log_children[e] = self.log_children[e][0:children_cut]#+["..."]
for e in self.log_attribs:
if len(self.log_attribs[e]) > attribs_cut:
self.log_attribs[e] = self.log_attribs[e][0:attribs_cut]#+["..."]
for e in self.log_parameters:
if len(self.log_parameters[e]) > parameters_cut:
self.log_parameters[e] = self.log_parameters[e][0:parameters_cut]#+["..."]
clean_directory(os.getcwd())
self.str=""
self.export("behaviors")
self.write_utf8("behaviors.txt", self.str)
self.str=""
self.export("BFAITaskTemplateList")
self.write_utf8("BFAITaskTemplateList.txt", self.str)
self.str=""
self.update_message("Done")
def export2(self, el, lv=0):
try:
self.str += "->"*lv + el
try:
for att in self.log_attribs[el]:
self.str += " "+att+"=("+", ".join(self.log_parameters[att])+"),"
except:
pass
self.str += "\n"
for child in self.log_children[el]:
self.export(child, lv=lv+1)
except: pass
def export(self, el, lv=0):
#try:
ignored_children = ("animTable", "behaviorTable","randomAnims", "randomSets", "textkeys", "avoidEntityTypes", "TrickLearning", "Subjects_AND", "Targets_AND", "emoteSets")
self.str += "\n\n---\n\n## "+el
self.str += "\n#### Attributes:"
if el in self.log_attribs:
for att in sorted(self.log_attribs[el]):
pstring = ", ".join(sorted(self.log_parameters[att]))+"),"
if self.log_parameters[att][0].lower() in ("true", "false"):
ty = "bool"
elif "." in self.log_parameters[att][0].lower():
ty = "float"
else:
try:
i = int(self.log_parameters[att][0].replace("GE","").replace("LE","").replace("E",""))
ty = "int"
except:
ty = "string"
self.str += "\n- __"+att+"__ (_"+ty+"_) - ("+pstring
else:
self.str += "\n- None"
self.str += "\n#### Children:"
if el in self.log_children and el not in ignored_children:
for child in sorted(self.log_children[el]):
self.str += "\n- ["+child+"](#"+child.lower()+")"
else:
self.str += "\n- None"
#and log its children
if el in self.log_children and el not in ignored_children:
for child in sorted(self.log_children[el]):
self.export(child, lv=lv+1)
#except: pass
def scan(self, el):
"""Parse along an element and log its content"""
for child in el:
if el.tag not in ("Subjects", "Targets", "Objects", ):
#why does it include the parent here
if child.tag not in ("behaviors", "subjects", el.tag):
if el.tag not in self.log_children:
self.log_children[el.tag] = []
if child.tag not in self.log_children[el.tag]:
self.log_children[el.tag].append(child.tag)
self.scan(child)
for att in el.attrib:
if el.tag not in self.log_attribs:
self.log_attribs[el.tag] = []
if att not in self.log_attribs[el.tag]:
self.log_attribs[el.tag].append(att)
if att not in self.log_parameters:
self.log_parameters[att] = []
if el.attrib[att] not in self.log_parameters[att]:
self.log_parameters[att].append(el.attrib[att])
def open_temp_dir(self):
"""Opens the temp directory in Windows Explorer"""
os.startfile(os.getcwd())
def exit(self):
app_root.quit()
def create_menubar(self):
#menu
menubar = Menu(app_root)
#Start menu
filemenu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Start", menu=filemenu)
filemenu.add_command(label="Load Z2F", command=self.load_z2f)
filemenu.add_command(label="Save Z2F", command=self.save_z2f)
filemenu.add_command(label="Save Z2F and Test", command=self.save_and_test)
filemenu.add_command(label="Open Temp Dir", command=self.open_temp_dir)
filemenu.add_command(label="Exit", command=self.exit)
#help menu
filemenu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Extras", menu=filemenu)
filemenu.add_command(label="(Re)Build BFMs", command=self.build_bfms)
filemenu.add_command(label="Create Biome+Space+Placement Hack", command=self.create_hack)
filemenu.add_command(label="Debug Current Entity", command=self.debug_entity)
filemenu.add_command(label="Document ZT2 API", command=self.document)
filemenu.add_command(label="Find Bugs in ZT2", command=self.find_bugs)
filemenu.add_command(label="Find Dependencies", command=self.find_dependencies)
filemenu.add_command(label="Find Interferences", command=self.find_interferences)
filemenu.add_command(label="Rebuild Entity Filter", command=self.rebuild_badlist)
#help menu
filemenu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Help", menu=filemenu)
filemenu.add_command(label="About", command=self.about)
filemenu.add_command(label="Online Tutorial", command=self.online_tutorial)
filemenu.add_command(label="Online Support", command=self.online_support)
return menubar
def about(self):
messagebox.showinfo("APE2","This program allows you to copy any entity (object, animal, etc.) you can find in your ZT2 installation. You can also change properties and create zoopedias for your projects.\nProgrammed by HENDRIX of AuroraDesigns.")
def online_tutorial(self):
webbrowser.open("http://thezt2roundtable.com/topic/11479528/1", new=2)