From 81e85d3aec30e5626c1b18a73602a79e11100450 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:31:26 +0000 Subject: [PATCH 1/6] Initial plan From 3dfcc61f1c9c153f28d892561ea41c25687a3bbd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:47:11 +0000 Subject: [PATCH 2/6] Convert Python 2 code to Python 3 compatible Co-authored-by: jncraton <103612+jncraton@users.noreply.github.com> --- makefile | 6 +- tools/Libs/AIBIN.py | 4272 +++++++++++++++++++------------------- tools/Libs/DAT.py | 2244 ++++++++++---------- tools/Libs/IScriptBIN.py | 2378 ++++++++++----------- tools/Libs/PAL.py | 2 +- tools/Libs/PCX.py | 266 +-- tools/Libs/TBL.py | 6 +- tools/Libs/setutils.py | 1518 +++++++------- tools/Libs/trace.py | 2 +- tools/Libs/utils.py | 2666 ++++++++++++------------ tools/PyAI.pyw | 376 ++-- tools/eud_gen_trigs.py | 2 +- 12 files changed, 6885 insertions(+), 6853 deletions(-) diff --git a/makefile b/makefile index 8628ab5..bef32a5 100644 --- a/makefile +++ b/makefile @@ -44,15 +44,15 @@ build/patch_rt.mpq: build/aiscript.bin maps: build/aiscript.bin @echo Creating Maps @truncate -s 64050 build/aiscript.bin - @python2 tools/eud_write_bin.py + @python3 tools/eud_write_bin.py triggers: build/aiscript.bin @echo Creating triggers - @python2 tools/eud_gen_trigs.py + @python3 tools/eud_gen_trigs.py build/aiscript.bin: build/combined.pyai @echo Creating script binaries - @python2 tools/PyAI.pyw --compile --hidewarns ../$< ../$@ ../build/bwscript.bin + @python3 tools/PyAI.pyw --compile --hidewarns ../$< ../$@ ../build/bwscript.bin @echo aiscript.bin size: @wc -c < $@ diff --git a/tools/Libs/AIBIN.py b/tools/Libs/AIBIN.py index 911706a..a71d336 100644 --- a/tools/Libs/AIBIN.py +++ b/tools/Libs/AIBIN.py @@ -1,2136 +1,2136 @@ -from utils import * -import TBL, DAT - -import struct, re, os, sys -from math import log, floor -from zlib import compress, decompress - -default_ais = {'Protoss':'PMCu','BWProtoss':'PMCx','Terran':'TMCu','BWTerran':'TMCx','Zerg':'ZMCu','BWZerg':'ZMCx'} - -types = [ - 'byte', - 'word', - 'unit', - 'building', - 'military', - 'gg_military', - 'ag_military', - 'ga_military', - 'aa_military', - 'upgrade', - 'technology', - 'string' -] - -def convflags(num): - if isstr(num): - b = list(num) - b.reverse() - return sum([int(x)*(2**n) for n,x in enumerate(b)]) - b = [str(num/(2**n)%2) for n in range(3)] - b.reverse() - return ''.join(b) - -class AIBIN: - labels = [ - 'Goto', - 'DoesntCommandGoto', - 'Wait', - 'StartTownManagement', - 'StartAreaTownManagement', - 'RunCodeForExpansion', - 'Build', - 'ResearchUpgrade', - 'ResearchTechnology', - 'WaitUntilConstructionFinished', - 'WaitUntilConstructionStarted', - 'ClearAttackData', - 'AddToAttackParty', - 'PrepareAttackParty', - 'AttackWithAttackParty', - 'WaitForSecureUnknown', - 'CaptureExpandUnknown', - 'BuildBunkersUnknown', - 'WaitForBunkersUnknown', - 'BuildGroundToGroundDefenceUnit', - 'BuildAirToGroundDefenceUnit', - 'BuildGroundToAirDefenceUnit', - 'BuildAirToAirDefenceUnit', - 'UseForGroundToGroundDefence', - 'UseForAirToGroundDefence', - 'UseForGroundToAirDefence', - 'UseForAirToAirDefence', - 'ClearGroundToGroundDefence', - 'ClearAirToGroundDefence', - 'ClearGroundToAirDefence', - 'ClearAirToAirDefence', - 'AllUnitsSuicideMission', - 'MakeSelectedPlayerEnemy', - 'MakeSelectedPlayerAlly', - 'DefaultMinUnknown', - 'DefaultBuildOffUnknown', - 'StopCodeSection', - 'SwitchComputerToRescuable', - 'MoveDarkTemplarToLocation', - 'Debug', - 'CauseFatalError', - 'EnterBunker', - 'ValueThisAreaHigher', - 'DontManageOrBuildTransports', - 'UseTransportsUpToMax', - 'BuildNukes', - 'MaxForceUnknown', - 'ClearPreviousCombatData', - 'ChanceGoto', - 'AfterTimeGoto', - 'BuildSupplyOnlyWhenNeeded', - 'BuildSupplyBeforeNeeded', - 'BuildTurretsUnknown', - 'WaitForTurretsUnknown', - 'DefaultBuildUnknown', - 'HarassFactorUnknown', - 'StartCampaignAI', - 'NearestRaceGoto', - 'EnemyInRangeGoto', - 'MoveWorkersToExpansion', - 'EnemyReachableByGroundGoto', - 'GuardTown', - 'WaitUntilCommandAmount', - 'SendUnitsToGuardResources', - 'CallSubroutine', - 'ReturnFromSubroutine', - 'EvalHarrassUnkown', - 'PlaceTowers', - 'AttackedGoto', - 'BuildIfNeeded', - 'DoMorphUnknown', - 'WaitForUpgradesUnknown', - 'RunSimultaneously', - 'PredifinedConditionalGoto', - 'ScoutWithUnknown', - 'MaxAmountOfUnit', - 'Train', - 'TargetExpansionUnknown', - 'WaitUntilCommands', - 'SetAttacksUnknown', - 'SetGeneralCommandTarget', - 'MakeUnitsPatrol', - 'GiveResourcesWhenLow', - 'PrepDownUnknown', - 'ComputerHasResourcesGoto', - 'EnterNearestTransport', - 'ExitTransport', - 'EnableSharedVision', - 'DisableSharedVision', - 'NukeSelectedLocation', - 'HarassSelectedLocation', - 'ImplodeUnknown', - 'GuardAllUnknown', - 'EnemyCommandsGoto', - 'EnemyHasResourcesGoto', - 'IfDifUnknown', - 'EasyAttackUnknown', - 'KillCurrentThread', - 'AllowOtherThreadsToKillCurrent', - 'WaitForAttackGroupToAttack', - 'BigAttackPrepare', - 'JunkyardDog', - 'FakeNukeUnknown', - 'DisruptionWebSelectedLocation', - 'RecallSelectedLocation', - 'SetRandomSeed', - 'IfOwnedUnknown', - 'CreateNuke', - 'CreateUnitAtCoordinates', - 'LaunchNukeAtCoordinates', - 'AskForHelpWhenInTrouble', - 'WatchAlliesUnknown', - 'TryTownpointUnknown', - 'IfTowns', - ] - short_labels = [ - 'goto', #0x00 - 0 - 'notowns_jump', #0x01 - 1 - 'wait', #0x02 - 2 - 'start_town', #0x03 - 3 - 'start_areatown', #0x04 - 4 - 'expand', #0x05 - 5 - 'build', #0x06 - 6 - 'upgrade', #0x07 - 7 - 'tech', #0x08 - 8 - 'wait_build', #0x09 - 9 - 'wait_buildstart', #0x0A - 10 - 'attack_clear', #0x0B - 11 - 'attack_add', #0x0C - 12 - 'attack_prepare', #0x0D - 13 - 'attack_do', #0x0E - 14 - 'wait_secure', #0x0F - 15 - 'capt_expand', #0x10 - 16 - 'build_bunkers', #0x11 - 17 - 'wait_bunkers', #0x12 - 18 - 'defensebuild_gg', #0x13 - 19 - 'defensebuild_ag', #0x14 - 20 - 'defensebuild_ga', #0x15 - 21 - 'defensebuild_aa', #0x16 - 22 - 'defenseuse_gg', #0x17 - 23 - 'defenseuse_ag', #0x18 - 24 - 'defenseuse_ga', #0x19 - 25 - 'defenseuse_aa', #0x1A - 26 - 'defenseclear_gg', #0x1B - 27 - 'defenseclear_ag', #0x1C - 28 - 'defenseclear_ga', #0x1D - 29 - 'defenseclear_aa', #0x1E - 30 - 'send_suicide', #0x1F - 31 - 'player_enemy', #0x20 - 32 - 'player_ally', #0x21 - 33 - 'default_min', #0x22 - 34 - 'defaultbuild_off', #0x23 - 35 - 'stop', #0x24 - 36 - 'switch_rescue', #0x25 - 37 - 'move_dt', #0x26 - 38 - 'debug', #0x27 - 39 - 'fatal_error', #0x28 - 40 - 'enter_bunker', #0x29 - 41 - 'value_area', #0x2A - 42 - 'transports_off', #0x2B - 43 - 'check_transports', #0x2C - 44 - 'nuke_rate', #0x2D - 45 - 'max_force', #0x2E - 46 - 'clear_combatdata', #0x2F - 47 - 'random_jump', #0x30 - 48 - 'time_jump', #0x31 - 49 - 'farms_notiming', #0x32 - 50 - 'farms_timing', #0x33 - 51 - 'build_turrets', #0x34 - 52 - 'wait_turrets', #0x35 - 53 - 'default_build', #0x36 - 54 - 'harass_factor', #0x37 - 55 - 'start_campaign', #0x38 - 56 - 'race_jump', #0x39 - 57 - 'region_size', #0x3A - 58 - 'get_oldpeons', #0x3B - 59 - 'groundmap_jump', #0x3C - 60 - 'place_guard', #0x3D - 61 - 'wait_force', #0x3E - 62 - 'guard_resources', #0x3F - 63 - 'call', #0x40 - 64 - 'return', #0x41 - 65 - 'eval_harass', #0x42 - 66 - 'creep', #0x43 - 67 - 'panic', #0x44 - 68 - 'player_need', #0x45 - 69 - 'do_morph', #0x46 - 70 - 'wait_upgrades', #0x47 - - 'multirun', #0x48 - - 'rush', #0x49 - - 'scout_with', #0x4A - - 'define_max', #0x4B - - 'train', #0x4C - - 'target_expansion', #0x4D - - 'wait_train', #0x4E - - 'set_attacks', #0x4F - - 'set_gencmd', #0x50 - - 'make_patrol', #0x51 - - 'give_money', #0x52 - - 'prep_down', #0x53 - - 'resources_jump', #0x54 - - 'enter_transport', #0x55 - - 'exit_transport', #0x56 - - 'sharedvision_on', #0x57 - - 'sharedvision_off', #0x58 - - 'nuke_location', #0x59 - - 'harass_location', #0x5A - - 'implode', #0x5B - - 'guard_all', #0x5C - - 'enemyowns_jump', #0x5D - - 'enemyresources_jump',#0x5E - - 'if_dif', #0x5F - - 'easy_attack', #0x60 - - 'kill_thread', #0x61 - - 'killable', #0x62 - - 'wait_finishattack', #0x63 - - 'quick_attack', #0x64 - - 'junkyard_dog', #0x65 - - 'fake_nuke', #0x66 - - 'disruption_web', #0x67 - - 'recall_location', #0x68 - - 'set_randomseed', #0x69 - - 'if_owned', #0x6A - - 'create_nuke', #0x6B - - 'create_unit', #0x6C - - 'nuke_pos', #0x6D - - 'help_iftrouble', #0x6E - - 'allies_watch', #0x6F - - 'try_townpoint', #0x70 - - 'if_towns', #0x71 - - ] - - separate = [ - 'wait', - 'goto', - 'debug', - 'race_jump', - 'nearestracegoto', - 'return', - 'returnfromsubroutine', - 'stop', - 'stopcodesection', - ] - - def __init__(self, bwscript=None, units=None, upgrades=None, techs=None, stat_txt=None): - if bwscript == None: - bwscript = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'scripts', 'bwscript.bin') - if units == None: - units = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'units.dat') - if upgrades == None: - upgrades = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'upgrades.dat') - if techs == None: - techs = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'techdata.dat') - if stat_txt == None: - stat_txt = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'stat_txt.tbl') - self.ais = odict() - self.aisizes = {} - self.externaljumps = [[{},{}],[{},{}]] - self.varinfo = odict() - self.aiinfo = {} - self.warnings = None - self.bwscript = None - self.warnings = [] - self.nobw = bwscript == None - if bwscript != False: - self.bwscript = BWBIN() - if bwscript: - self.warnings = self.bwscript.load_file(bwscript) - self.externaljumps = self.bwscript.externaljumps - if isinstance(units, DAT.UnitsDAT): - self.unitsdat = units - else: - self.unitsdat = DAT.UnitsDAT() - self.unitsdat.load_file(units) - if isinstance(upgrades, DAT.UpgradesDAT): - self.upgradesdat = upgrades - else: - self.upgradesdat = DAT.UpgradesDAT() - self.upgradesdat.load_file(upgrades) - if isinstance(techs, DAT.TechDAT): - self.techdat = techs - else: - self.techdat = DAT.TechDAT() - self.techdat.load_file(techs) - if isinstance(stat_txt, TBL.TBL): - self.tbl = stat_txt - else: - self.tbl = TBL.TBL() - self.tbl.load_file(stat_txt) - self.parameters = [ - [self.ai_address], - [self.ai_unit,self.ai_address], - [self.ai_word], - None, - None, - [self.ai_byte, self.ai_address], - [self.ai_byte, self.ai_building, self.ai_byte], - [self.ai_byte, self.ai_upgrade, self.ai_byte], - [self.ai_technology,self.ai_byte], - [self.ai_byte, self.ai_building], - [self.ai_byte, self.ai_unit], - None, - [self.ai_byte, self.ai_military], - None, - None, - None, - None, - None, - None, - [self.ai_byte, self.ai_ggmilitary], - [self.ai_byte, self.ai_agmilitary], - [self.ai_byte, self.ai_gamilitary], - [self.ai_byte, self.ai_aamilitary], - [self.ai_byte, self.ai_ggmilitary], - [self.ai_byte, self.ai_agmilitary], - [self.ai_byte, self.ai_gamilitary], - [self.ai_byte, self.ai_aamilitary], - None, - None, - None, - None, - [self.ai_byte], - None, - None, - [self.ai_byte], - None, - None, - None, - None, - [self.ai_address,self.ai_string], - None, - None, - None, - None, - None, - [self.ai_byte], - [self.ai_word], - None, - [self.ai_byte, self.ai_address], - [self.ai_byte, self.ai_address], - None, - None, - None, - None, - None, - None, #Possibly one parameter - None, - [self.ai_address, self.ai_address, self.ai_address], - [self.ai_byte, self.ai_address], - [self.ai_byte], - [self.ai_address], - [self.ai_unit, self.ai_byte], - [self.ai_byte, self.ai_military], - [self.ai_military], - [self.ai_address], - None, - None, #Possibly one parameter - [self.ai_byte], - [self.ai_address], - [self.ai_byte,self.ai_building], - [self.ai_byte,self.ai_unit], - None, - [self.ai_address], - [self.ai_byte, self.ai_address], - [self.ai_military], - [self.ai_byte, self.ai_unit], - [self.ai_byte, self.ai_military], - None, - [self.ai_byte, self.ai_military], - [self.ai_byte], - None, - None, - None, - None, #Possibly three parameters - [self.ai_word, self.ai_word, self.ai_address], - None, - None, - [self.ai_byte], - [self.ai_byte], - None, - None, - None, - None, - [self.ai_unit, self.ai_address], - [self.ai_word, self.ai_word, self.ai_address], - None, #Possibly three parameters - None, #Possibly two parameters - None, - None, - None, - None, - None, - None, - None, - None, - [self.ai_word,self.ai_word], - [self.ai_unit,self.ai_address], - None, - [self.ai_unit, self.ai_word, self.ai_word], - [self.ai_word, self.ai_word], - None, - [self.ai_byte, self.ai_address], - [self.ai_byte, self.ai_address], - None, - ] - self.builds = [] - for c in [6,19,20,21,22,69]: - self.builds.append(self.labels[c]) - self.builds.append(self.short_labels[c]) - self.starts = [] - for c in [3,4,56]: - self.starts.append(self.labels[c]) - self.starts.append(self.short_labels[c]) - self.types = { - 'byte':[self.ai_byte,self.ai_word], - 'word':[self.ai_word], - 'unit':[self.ai_unit], - 'building':[self.ai_building,self.ai_unit], - 'military':[self.ai_military,self.ai_unit],#,self.ai_ggmilitary,self.ai_agmilitary,self.ai_gamilitary,self.ai_aamilitary - 'gg_military':[self.ai_ggmilitary,self.ai_gamilitary,self.ai_military,self.ai_unit], - 'ag_military':[self.ai_agmilitary,self.ai_aamilitary,self.ai_military,self.ai_unit], - 'ga_military':[self.ai_gamilitary,self.ai_ggmilitary,self.ai_military,self.ai_unit], - 'aa_military':[self.ai_aamilitary,self.ai_agmilitary,self.ai_military,self.ai_unit], - 'upgrade':[self.ai_upgrade], - 'technology':[self.ai_technology], - 'string':[self.ai_string] - } - self.typescanbe = { - 'byte':[self.ai_byte], - 'word':[self.ai_word,self.ai_byte], - 'unit':[self.ai_unit,self.ai_building,self.ai_military,self.ai_ggmilitary,self.ai_agmilitary,self.ai_gamilitary,self.ai_aamilitary], - 'building':[self.ai_building], - 'military':[self.ai_military,self.ai_ggmilitary,self.ai_agmilitary,self.ai_gamilitary,self.ai_aamilitary], - 'gg_military':[self.ai_ggmilitary,self.ai_gamilitary,self.ai_military], - 'ag_military':[self.ai_agmilitary,self.ai_aamilitary,self.ai_military], - 'ga_military':[self.ai_gamilitary,self.ai_ggmilitary,self.ai_military], - 'aa_military':[self.ai_aamilitary,self.ai_agmilitary,self.ai_military], - 'upgrade':[self.ai_upgrade], - 'technology':[self.ai_technology], - 'string':[self.ai_string] - } - self.script_endings = [0,36,39,57,65,97] #goto, stop, debug, racejump, return, kill_thread - - def load_file(self, file, addstrings=False): - try: - if isstr(file): - f = open(file,'rb') - data = f.read() - f.close() - else: - data = file.read() - except: - raise PyMSError('Load',"Could not load aiscript.bin '%s'" % file) - try: - offset = struct.unpack(' len(self.tbl.strings): - if addstrings: - if string-len(self.tbl.strings) > 1: - self.tbl.strings.extend(['Generated by PyAI\x00'] * (string-len(self.tbl.strings)-1)) - self.tbl.strings.append('Custom name generated by PyAI for AI with ID: %s\x00' % id) - warnings.append(PyMSWarning('Load',"The AI with ID '%s' had a custom string (#%s) not present in the stat_txt.tbl, so PyAI generated one." % (id,string), level=1)) - else: - raise PyMSError('Load',"String id '%s' is not present in the stat_txt.tbl" % string) - aioffsets.append([id,loc,string-1,flags]) - offset += 4 - offsets.sort() - try: - externaljumps = [[{},{}],[dict(self.bwscript.externaljumps[1][0]),dict(self.bwscript.externaljumps[1][1])]] - except: - externaljumps = [[{},{}],[{},{}]] - totaloffsets = {} - findtotaloffsets = {} - checknones = [] - for id,loc,string,flags in aioffsets: - ais[id] = [loc,string,flags,[],[]] - if loc: - curdata = data[loc:offsets[offsets.index(loc)+1]] - curoffset = 0 - cmdoffsets = [] - findoffset = {} - while curoffset < len(curdata): - if curoffset+loc in findtotaloffsets: - for fo in findtotaloffsets[curoffset+loc]: - ais[fo[0]][4][fo[1]] = [id,len(cmdoffsets)] - fo[2][fo[3]] = [id,len(cmdoffsets)] - ais[id][4].append(len(cmdoffsets)) - if not id in externaljumps[0][0]: - externaljumps[0][0][id] = {} - if not len(cmdoffsets) in externaljumps[0][0][id]: - externaljumps[0][0][id][len(cmdoffsets)] = [] - externaljumps[0][0][id][len(cmdoffsets)].append(fo[0]) - if not fo[0] in externaljumps[0][1]: - externaljumps[0][1][fo[0]] = [] - if not id in externaljumps[0][1][fo[0]]: - externaljumps[0][1][fo[0]].append(id) - del findtotaloffsets[curoffset+loc] - if curoffset in findoffset: - for fo in findoffset[curoffset]: - ais[id][4][fo[0]] = len(cmdoffsets) - fo[1][fo[2]] = len(cmdoffsets) - del findoffset[curoffset] - totaloffsets[curoffset+loc] = [id,len(cmdoffsets)] - cmdoffsets.append(curoffset) - cmd,curoffset = ord(curdata[curoffset]),curoffset + 1 - #print id,loc,curoffset,self.short_labels[cmd] - if not cmd and curoffset == len(curdata): - break - if cmd >= len(self.labels): - raise PyMSError('Load','Invalid command, could possibly be a corrrupt aiscript.bin') - ai = [cmd] - if self.parameters[cmd]: - for p in self.parameters[cmd]: - d = p(curdata[curoffset:]) - if p == self.ai_address: - if d[1] < loc: - if d[1] not in totaloffsets: - raise PyMSError('Load','Incorrect jump location, it could possibly be a corrupt aiscript.bin') - tos = totaloffsets[d[1]] - ais[id][4].append(tos) - ai.append(tos) - # print tos - # print externaljumps - if not tos[0] in externaljumps[0][0]: - externaljumps[0][0][tos[0]] = {} - if not tos[1] in externaljumps[0][0][tos[0]]: - externaljumps[0][0][tos[0]][tos[1]] = [] - externaljumps[0][0][tos[0]][tos[1]].append(id) - if not id in externaljumps[0][1]: - externaljumps[0][1][id] = [] - if not tos[0] in externaljumps[0][1][id]: - externaljumps[0][1][id].append(tos[0]) - elif d[1] >= offsets[offsets.index(loc)+1]: - if not d[1] in findtotaloffsets: - findtotaloffsets[d[1]] = [] - findtotaloffsets[d[1]].append([id,len(ais[id][4]),ai,len(ai)]) - ais[id][4].append(None) - ai.append(None) - elif d[1] - loc in cmdoffsets: - pos = cmdoffsets.index(d[1] - loc) - ais[id][4].append(pos) - ai.append(pos) - else: - if not d[1] - loc in findoffset: - findoffset[d[1] - loc] = [] - findoffset[d[1] - loc].append([len(ais[id][4]),ai,len(ai)]) - ais[id][4].append(None) - ai.append(None) - else: - ai.append(d[1]) - curoffset += d[0] - ais[id][3].append(ai) - aisizes[id] = curoffset - if None in ais[id][4]: - checknones.append(id) - for c in checknones: - if None in ais[id][4]: - raise PyMSError('Load','Incorrect jump location, could possibly be a corrupt aiscript.bin') -#tName<0>v[description]<0>E -#AIID[description]<0> - #label<0>[description]<0> - #<0> - #LN - #<0> -#<0> - if offset < len(data): - def getstr(dat,o): - i = dat[o:].index('\x00') - return [dat[o:o+i],o+i+1] - try: - info = decompress(data[offset:]) - offset = 0 - t = ord(info[offset])-1 - while t > -1: - offset += 1 - name,offset = getstr(info,offset) - o,val = self.types[types[t]][0](info[offset:]) - offset += o - desc,offset = getstr(info,offset) - varinfo[name] = [t,val[1],desc] - t = ord(info[offset])-1 - offset += 1 - while info[offset] != '\x00': - id = info[offset:offset+4] - offset += 4 - desc,offset = getstr(info,offset) - aiinfo[id] = [desc,odict(),[]] - while info[offset] != '\x00': - label,offset = getstr(info,offset) - desc,offset = getstr(info,offset) - aiinfo[id][1][label] = desc - offset += 1 - p = int(floor(log(len(aiinfo[id][1]),256))) - s,n = '<' + ['B','H','L','L'][p],[1,2,4,4][p] - while info[offset:offset+n].replace('\x00',''): - aiinfo[id][2].append(aiinfo[id][1].getkey(struct.unpack(s,info[offset:offset+n])[0]-1)) - offset += n - offset += 1 - except: - aiinfo = {} - warnings.append(PyMSWarning('Load','Unsupported extra script information section in aiscript.bin, could possibly be corrupt. Continuing without extra script information')) - self.ais = ais - self.aisizes = aisizes - self.externaljumps = externaljumps - self.varinfo = varinfo - self.aiinfo = aiinfo - def pprint(obj, depth=0, max=2): - depth += 1 - string = '' - if isinstance(obj, dict) and max: - if obj: - string += '{\\\n' - for key in obj: - string += '%s%s:' % ('\t'*depth, repr(key)) - string += pprint(obj[key], depth, max-1) - string += '%s},\\\n' % ('\t'*(depth-1)) - else: - string += '{},\\\n' - elif isinstance(obj, list) and max: - if obj: - string += '[\\\n' - for item in obj: - string += ('%s' % ('\t'*depth)) - string += pprint(item, depth, max-1) - string += '%s],\\\n' % ('\t'*(depth-1)) - else: - string += '[],\\\n' - else: - string += '%s,\\\n' % (repr(obj),) - if depth == 1: - return string[:-3] - return string - print pprint(self.externaljumps) - return warnings - except PyMSError: - raise - except: - raise PyMSError('Load',"Unsupported aiscript.bin '%s', could possibly be invalid or corrupt" % file,exception=sys.exc_info()) - - #Stages: - # 0 - Load file - # 1 - Decompile - # 2 - Compile - # 3 - Interpret - def ai_byte(self, data, stage=0): - """byte - A number in the range 0 to 255""" - if not stage: - v = ord(data[0]) - elif stage == 1: - v = str(data) - elif stage == 2: - v = chr(data) - else: - try: - v = int(data) - if v < 0 or v > 255: - raise - except: - raise PyMSError('Parameter',"Invalid byte value '%s', it must be a number in the range 0 to 255" % data) - return [1,v] - - def ai_word(self, data, stage=0): - """word - A number in the range 0 to 65535""" - if not stage: - v = struct.unpack(' 65535: - raise - except: - raise PyMSError('Parameter',"Invalid word value '%s', it must be a number in the range 0 to 65535" % data) - return [2,v] - - def ai_address(self, data, stage=0): - """block - The block label name (Can not be used as a variable type!)""" - if stage == 1: - return [2,data] - return self.ai_word(data, stage) - - def ai_unit(self, data, stage=0): - """unit - A unit ID from 0 to 227, or a full unit name from stat_txt.tbl""" - if not stage: - v = ord(data[0]) - elif stage == 1: - s = self.tbl.strings[data].split('\x00') - if s[1] != '*': - v = TBL.decompile_string('\x00'.join(s[:2]), '\x0A\x28\x29\x2C') - else: - v = TBL.decompile_string(s[0], '\x0A\x28\x29\x2C') - elif stage == 2: - v = chr(data) + '\x00' - else: - try: - v = int(data) - if -1 > v or v > DAT.UnitsDAT.count: - raise - except: - for i,name in enumerate(self.tbl.strings[:DAT.UnitsDAT.count]): - n = name.split('\x00')[:2] - if TBL.compile_string(data) == n[0] or (n[1] != '*' and TBL.compile_string(data) == '\x00'.join(n)): - v = i - break - else: - raise PyMSError('Parameter',"Unit '%s' was not found" % data) - return [2,v] - - def ai_building(self, data, stage=0): - """building - Same as unit type, but only units that are Buildings, Resource Miners, and Overlords""" - v = self.ai_unit(data, stage) - if stage == 3: - flags = self.unitsdat.get_value(v[1],'SpecialAbilityFlags') - if not flags & 8 and not flags & 1 and not self.unitsdat.get_value(v[1],'SupplyProvided'):#v != 42 and - raise PyMSWarning('Parameter','Unit is not a building or worker', extra=v, level=1) - return v - - def ai_military(self, data, stage=0): - """military - Same as unit type, but only military units (not Buildings)""" - v = self.ai_unit(data, stage) - if stage == 3: - flags = self.unitsdat.get_value(v[1],'SpecialAbilityFlags') - if flags & 1: - raise PyMSWarning('Parameter','Unit is a building', extra=v, level=1) - return v - - def ai_ggmilitary(self, data, stage=0): - """gg_military - Same as Military type, but only for defending against an enemy Ground unit attacking your Ground unit""" - v = self.ai_military(data, stage) - return v - - def ai_agmilitary(self, data, stage=0): - """ag_military - Same as Military type, but only for defending against an enemy Air unit attacking your Ground unit""" - v = self.ai_military(data, stage) - return v - - def ai_gamilitary(self, data, stage=0): - """ga_military - Same as Military type, but only for defending against an enemy Ground unit attacking your Air unit""" - v = self.ai_military(data, stage) - return v - - def ai_aamilitary(self, data, stage=0): - """aa_military - Same as Military type, but only for defending against an enemy Air unit attacking your Air unit""" - v = self.ai_military(data, stage) - return v - - def ai_upgrade(self, data, stage=0): - """upgrade - An upgrade ID from 0 to 60, or a full upgrade name from stat_txt.tbl""" - if not stage: - v = ord(data[0]) - elif stage == 1: - v = TBL.decompile_string(self.tbl.strings[self.upgradesdat.get_value(data,'Label') - 1].split('\x00',1)[0].strip(), '\x0A\x28\x29\x2C') - elif stage == 2: - v = chr(data) + '\x00' - else: - try: - v = int(data) - if -1 > v or v > DAT.UpgradesDAT.count: - raise - except: - for i in range(len(self.upgradesdat.entries)): - if TBL.compile_string(data) == self.tbl.strings[self.upgradesdat.get_value(i,'Label') - 1].split('\x00', 1)[0].strip(): - v = i - break - else: - raise PyMSError('Parameter',"Upgrade '%s' was not found" % data) - return [2,v] - - def ai_technology(self, data, stage=0): - """technology - An technology ID from 0 to 43, or a full technology name from stat_txt.tbl""" - if not stage: - v = ord(data[0]) - elif stage == 1: - v = TBL.decompile_string(self.tbl.strings[self.techdat.get_value(data,'Label') - 1].split('\x00',1)[0].strip(), '\x0A\x28\x29\x2C') - elif stage == 2: - v = chr(data) + '\x00' - else: - try: - v = int(data) - if -1 > v or v > DAT.TechDAT.count: - raise - except: - for i in range(len(self.techdat.entries)): - if TBL.compile_string(data) == self.tbl.strings[self.techdat.get_value(i,'Label') - 1].split('\x00', 1)[0].strip(): - v = i - break - else: - raise PyMSError('Parameter',"Technology '%s' was not found" % data) - return [2,v] - - def ai_string(self, data, stage=0): - """string - A string of any characters (except for nulls: <0>) in TBL string formatting (use <40> for an open parenthesis '(', <41> for a close parenthesis ')', and <44> for a comma ',')""" - if not stage: - e = data.index('\x00') - return [e+1,TBL.decompile_string(data[:e], '\x0A\x28\x29\x2C')] - elif stage == 1: - return [len(data)+1,data] - elif stage == 2: - s = TBL.compile_string(data) - if '\x00' in s: - raise PyMSError('Parameter',"String '%s' contains a null (<0>)" % data) - return [len(s) + 1,s + '\x00'] - return [len(data),data] - - def interpret(self, files, defs=None, extra=False): - if not isinstance(files, list): - files = [files] - alldata = [] - for file in files: - try: - if isstr(file): - f = open(file,'r') - alldata.append(f.readlines()) - f.close() - else: - alldata.append(file.readlines()) - file.close() - except: - raise PyMSError('Interpreting',"Could not load file '%s'" % file) - ais = odict() - aisizes = dict(self.aisizes) - bwsizes = dict(self.bwscript.aisizes) - aisize = 0 - externaljumps = [[{},{}],[{},{}]] - bwais = odict() - varinfo = odict(self.varinfo) - aiinfo = dict(self.aiinfo) - bwinfo = dict(self.bwscript.aiinfo) - curinfo = None - ai = [] - cmdn = 0 - jumps = {} - curlabel = [] - warnings = [] - variables = {} - notused = False - town = False - totaljumps = {} - findtotaljumps = odict() - findgoto = odict() - unused = {} - scriptids = [[],[]] - nextinfo = None - multiline = False - lastmulti = [None,None] - loaded = [] - def load_defs(defname): - try: - deffile = os.path.join(os.path.dirname(files[0]),defname) - except: - deffile = os.path.abspath(defname) - if deffile in loaded: - return - try: - d = open(deffile,'r') - ddata = d.readlines() - d.close() - except: - raise PyMSError('External Definition',"External definition file '%s' was not found" % defname, warnings=warnings) - for n,l in enumerate(ddata): - if len(l) > 1: - line = l.strip().split('#',1)[0] - if line: - match = re.match('\\A(\\S+)\\s+(.+)\\s+=\\s+(.+?)(?:\\s*\\{(.+)\\})?\\Z', line) - if match: - t,name,dat,vinfo = match.groups() - if re.match('[\x00,(){}]',name): - raise PyMSError('External Definition',"The variable name '%s' in external definition file '%s' is invalid, it must not contain a null, or the characters: , ( ) { }" % (name,defname),n,line, warnings=warnings) - t = t.lower() - if t == 'label' or t not in self.types: - raise PyMSError('External Definition',"Invalid variable type '%s' for variable '%s' in external definition file '%s', consult the reference" % (t, name, defname),n,line, warnings=warnings) - if name.lower() in variables: - raise PyMSError('External Definition',"The variable name '%s' in external definition file '%s' is already in use" % (name, defname),n,line, warnings=warnings) - if vinfo: - warnings.append(PyMSWarning('External Definition',"External definition files do not support Information Comments, information is discarded",n,line)) - try: - v = self.types[t][0](dat,3)[1] - except PyMSWarning, w: - w.line = n + 1 - w.code = line - warnings.append(w) - v = w.extra[1] - except PyMSError, e: - e.line = n + 1 - e.code = line - e.warnings = warnings - raise e - except: - raise PyMSError('External Definition',"Invalid value '%s' for variable '%s' of type '%s' in external definition file '%s'" % (dat, name, t, defname),n,line, warnings=warnings) - variables[name.lower()] = [self.types[t],dat] - varinfo[name] = [types.index(t),v,None] - else: - raise PyMSError('External Definition','Invalid syntax, unknown line format',n,l, warnings=warnings) - loaded.append(deffile) - if defs: - if isstr(defs): - defs = defs.split(',') - - for deffile in defs: - load_defs(deffile) - for data in alldata: - for n,l in enumerate(data): - if len(l) > 1: - line = l.split('#',1)[0].strip() - if line: - if multiline: - if line.strip() == '}': - if len(nextinfo) == 3: - if not nextinfo[0][nextinfo[1]][1][nextinfo[2]].replace('\n',''): - raise PyMSError('Interpreting','The Information Comment has no text',n,line, warnings=warnings) - nextinfo[0][nextinfo[1]][1][nextinfo[2]] = nextinfo[0][nextinfo[1]][1][nextinfo[2]][:-1] - else: - if not nextinfo[0][nextinfo[1]][0].replace('\n',''): - raise PyMSError('Interpreting','The Information Comment has no text',n,line, warnings=warnings) - nextinfo[0][nextinfo[1]][0] = nextinfo[0][nextinfo[1]][0][:-1] - nextinfo = None - multiline = False - elif len(nextinfo) == 3: - nextinfo[0][nextinfo[1]][1][nextinfo[2]] += line + '\n' - else: - nextinfo[0][nextinfo[1]][0] += line + '\n' - else: - match = re.match('\\Aextdef\\s*(.+)\\Z',line) - if match: - load_defs(match.group(1)) - continue - match = re.match('\\A(\\S+)\\s+(\\S+)\\s+=\\s+(.+?)(?:\\s*\\{(.+)\\})?\\Z', line) - if match: - t,name,dat,vinfo = match.groups() - if re.match('[\x00,(){}]',name): - raise PyMSError('Interpreting',"The variable name '%s' is invalid, it must not contain a null, or the characters: , ( ) { }",n,line, warnings=warnings) - t = t.lower() - if t == 'label' or t not in self.types: - raise PyMSError('Interpreting',"Invalid variable type '%s', consult the reference" % t,n,line, warnings=warnings) - if name.lower() in variables: - raise PyMSError('Interpreting',"The variable name '%s' is already in use" % name,n,line, warnings=warnings) - try: - self.types[t][0](dat,3) - except PyMSWarning, w: - w.line = n - w.code = line - warnings.append(w) - except PyMSError, e: - e.line = n + 1 - e.code = line - e.warnings = warnings - raise e - except: - raise PyMSError('Interpreting',"Invalid variable value '%s' for type '%s'" % (dat, t),n,line, warnings=warnings) - variables[name.lower()] = [self.types[t],dat] - varinfo[name] = [types.index(t),self.types[t][0](dat,3)[1],''] - if vinfo: - if '\x00' in vinfo: - raise PyMSError('Interpreting','Information Comments can not contain null bytes',n,line, warnings=warnings) - varinfo[name][2] = vinfo - nextinfo = None - else: - nextinfo = [2,varinfo[name]] - continue - if re.match('\\A[^(]+\\([^)]+\\):\\s*(?:\\{.+\\})?\\Z', line): - newai = re.match('\\A(.+)\\(\s*(.+)\s*,\s*(.+)\s*,\s*(\w+)\s*\\):\\s*(?:\\{(.+)\\})?\\Z', line) - if not newai: - raise PyMSError('Interpreting','Invalid syntax, expected a new script header',n,line, warnings=warnings) - id = newai.group(1) - if id in default_ais: - id = default_ais[id] - elif len(id) != 4: - raise PyMSError('Interpreting',"Invalid AI ID '%s' (must be 4 characeters long, or one of the keywords: Protoss, BWProtoss, Terran, BWTerran, Zerg, BWZerg)" % id,n,line, warnings=warnings) - elif re.match('[,\x00:()]', id): - raise PyMSError('Interpreting',"Invalid AI ID '%s', it can not contain a null byte, or the characters: , ( ) :" % id,n,line, warnings=warnings) - try: - string = int(newai.group(2)) - except: - raise PyMSError('Interpreting','Invalid stat_txt.tbl entry (must be an integer)',n,line, warnings=warnings) - if string < 0 or string >= len(self.tbl.strings): - raise PyMSError('Interpreting','Invalid stat_txt.tbl entry (must be between 0 and %s)' % (len(self.tbl.strings)-1),n,line, warnings=warnings) - if not re.match('[01]{3}', newai.group(3)): - raise PyMSError('Interpreting',"Invalid AI flags '%s' (must be three 0's and/or 1's, see readme for more info)" % newai.group(3),n,line, warnings=warnings) - if not newai.group(4) in ['bwscript','aiscript']: - raise PyMSError('Interpreting',"Invalid script file '%s', it must be either 'aiscript' or 'bwscript'" % newai.group(4),n,line, warnings=warnings) - if ai: - if not ai[4]: - raise PyMSError('Interpreting',"AI with ID '%s' has no commands" % ai[0], warnings=warnings) - if None in ai[5]: - dat = blocknames[ai[5].index(None)] - if type(dat) == str: - raise PyMSError('Interpreting',"There is no block with name '%s' in AI with ID '%s'" % (dat,ai[0]), warnings=warnings) - if ai[0] in findtotaljumps and findtotaljumps[ai[0]]: - n = findtotaljumps[ai[0]].keys()[0] - raise PyMSError('Interpreting',"There is no block with name '%s' in AI with ID '%s'" % (n,ai[0]), warnings=warnings) - if ai[4][-1][0] not in self.script_endings: - warnings.append(PyMSWarning('Interpreting', "The AI with ID '%s' does not end with a stop or definite loop. To ensure your script doesn't run into the next script, it must end with one of: goto(), stop(), debug(), time_jump(), or race_jump()" % ai[0], level=1)) - if ai[0] in findgoto: - for l,f in findgoto[ai[0]].iteritems(): - if f[0]: - del findgoto[ai[0]][l] - if not findgoto[ai[0]]: - del findgoto[ai[0]] - if ai[1]: - ais[ai[0]] = ai[1:] - aisizes[ai[0]] = aisize - else: - ais[ai[0]] = ai[1:4] + [[],[]] - bwais[ai[0]] = [1] + ai[4:] - bwsizes[ai[0]] = aisize - for l in curinfo[ai[0]][2]: - if not l in curinfo[ai[0]][1]: - curinfo[ai[0]][1][l] = '' - notused = False - if newai.group(4) == 'aiscript': - curinfo = aiinfo - scriptids[0].append(id) - loc = 1 - else: - curinfo = bwinfo - scriptids[1].append(id) - loc = 0 - ai = [id,loc,string,convflags(newai.group(3)),[],[]] - aisize = 0 - curinfo[id] = ['',odict(),[]] - if newai.group(5): - if '\x00' in newai.group(5): - raise PyMSError('Interpreting','Information Comments can not contain null bytes',n,line, warnings=warnings) - aiinfo[id][0] = newai.group(5) - nextinfo = None - else: - nextinfo = [curinfo,id] - totaljumps[id] = {} - curlabel = [] - blocknames = [] - if not id in findgoto: - findgoto[id] = odict() - jumps = {} - town = False - if ai[0] in ais: - raise PyMSError('Interpreting',"Duplicate AI ID '%s'" % ai[0],n,line, warnings=warnings) - cmdn = 0 - continue - if ai: - match = re.match('\\A(.+)\\(\\s*(.+)?\\s*\\)\\Z', line) - if match: - cmd = match.group(1).lower() - if cmd in self.labels: - ai[4].append([self.labels.index(cmd)]) - elif cmd in self.short_labels: - ai[4].append([self.short_labels.index(cmd)]) - else: - raise PyMSError('Interpreting',"Invalid command name '%s'" % cmd,n,line, warnings=warnings) - if cmd in self.builds and not town: - warnings.append(PyMSWarning('Interpreting',"You may not have initiated a town in the script '%s' with one of the start_* commands before building units" % ai[0],n,line,level=1)) - elif cmd in self.starts: - town = True - if notused: - warnings.append(PyMSWarning('Interpreting',"This command and everything up to the next script or block will never be run",*notused)) - notused = False - dat = [] - if match.group(2): - dat = re.split('\\s*,\\s*', match.group(2)) - params = self.parameters[ai[4][-1][0]] - if params and len(dat) != len(params): - raise PyMSError('Interpreting','Incorrect amount of parameters (got %s, needed %s)' % (len(dat), len(params)),n,line, warnings=warnings) - if not params and dat: - raise PyMSError('Interpreting','Command requires no parameters, but got %s' % len(dat),n,line, warnings=warnings) - aisize += 1 - if params and dat: - for d,p in zip(dat,params): - if p == self.ai_address: - aisize += 2 - match = re.match('\\A(.+):(.+)\\Z', d) - if match: - cid,label = match.group(1),match.group(2) - if cid in default_ais: - cid = default_ais[cid] - elif len(cid) != 4: - raise PyMSError('Interpreting',"Invalid AI ID '%s' (must be 4 characeters long, or one of the keywords: Protoss, BWProtoss, Terran, BWTerran, Zerg, BWZerg)" % cid,n,line, warnings=warnings) - elif re.match('[\x00:(),]',cid): - raise PyMSError('Interpreting',"Invalid AI ID '%s', it can not contain a null byte, or the characters: , ( ) :" % cid,n,line, warnings=warnings) - if re.match('[\x00:(),]',label): - raise PyMSError('Interpreting',"Invalid label name '%s', it can not contain a null byte, or the characters: , ( ) :" % label,n,line, warnings=warnings) - if cid in totaljumps: - if curlabel: - if label in curlabel: - warnings.append(PyMSWarning('Interpreting',"All loops require at least 1 wait statement. Block '%s' seems to not have one" % label)) - curlabel = [] - if id in scriptids[0]: - a = 'aiscript' - else: - a = 'bwscript' - if cid in scriptids[0]: - b = 'aiscript' - else: - b = 'bwscript' - if a != b: - raise PyMSError('Interpreting',"You can't jump to an external script in '%s.bin' from a script in '%s.bin'" % (b,a),n,line, warnings=warnings) - if not label in totaljumps[cid]: - raise PyMSError('Interpreting',"The AI with ID '%s' has no label '%s'" % (cid,label), warnings=warnings) - tjs = totaljumps[cid][label] - ai[4][-1].append(tjs) - ai[5].append(tjs) - bw = id in scriptids[0] - if not tjs[0] in externaljumps[bw][0]: - externaljumps[bw][0][tjs[0]] = {} - if not tjs[1] in externaljumps[bw][0][tjs[0]]: - externaljumps[bw][0][tjs[0]][tjs[1]] = [] - externaljumps[bw][0][tjs[0]][tjs[1]].append(id) - if not id in externaljumps[bw][1]: - externaljumps[bw][1][id] = [] - if not tjs[0] in externaljumps[bw][1][id]: - externaljumps[bw][1][id].append(tjs[0]) - if not notused and (cid,label) in unused: - unused[(cid,label)] = False - else: - if not cid in findtotaljumps: - findtotaljumps[cid] = odict() - if not label in findtotaljumps[cid]: - findtotaljumps[cid][label] = [] - findtotaljumps[cid][label].append([ai[4][-1],len(ai[4][-1]),ai[5],len(ai[5]),ai[0]]) - ai[4][-1].append(None) - ai[5].append(None) - blocknames.append([cid,label]) - if not cid in findgoto: - findgoto[cid] = odict() - findgoto[cid][label] = [True,len(ai[5])-1] - else: - if curlabel: - if d in curlabel: - warnings.append(PyMSWarning('Interpreting',"All loops require at least 1 wait statement. Block '%s' seems to not have one" % d)) - curlabel = [] - if type(jumps.get(d)) == int: - ai[4][-1].append(jumps[d]) - ai[5].append(jumps[d]) - else: - if not d in jumps: - jumps[d] = [] - jumps[d].append([ai[4][-1],len(ai[4][-1]),ai[5],len(ai[5])]) - ai[4][-1].append(None) - ai[5].append(None) - blocknames.append(d) - findgoto[id][d] = [True,len(ai[5])-1] - if not notused and (id,d) in unused: - unused[(id,d)] = False - curinfo[id][2].append(d) - else: - try: - var = None - da = d - if d.lower() in variables: - for pt in self.typescanbe[p.__doc__.split(' ',1)[0]]: - if pt in variables[d.lower()][0]: - da = variables[d.lower()][1] - break - else: - raise PyMSError('Variable',"Incorrect type on varaible '%s'. Excpected '%s' but got '%s'" % (d.lower(), p.__doc__.split(' ',1)[0], variables[d.lower()][0][0].__doc__.split(' ',1)[0]),n,line, warnings=warnings) - var = PyMSWarning('Variable',"The variable '%s' of type '%s' was set to '%s'" % (d, variables[d.lower()][0][0].__doc__.split(' ',1)[0], variables[d.lower()][1])) - cs = p(da,3) - ai[4][-1].append(cs[1]) - aisize += cs[0] - except PyMSWarning, w: - ai[4][-1].append(w.extra[1]) - aisize += w.extra[0] - w.line = n + 1 - w.code = line - warnings.append(w) - if var: - var.warning += ' when the above warning happened' - warnings.append(var) - except PyMSError, e: - e.line = n + 1 - e.code = line - e.warnings = warnings - if var: - var.warning += ' when the above error happened' - e.warnings.append(var) - raise e - except: - raise PyMSError('Parameter',"Invalid parameter data '%s', looking for type '%s'" % (d,p.__doc__.split(' ',1)[0]),n,line, warnings=warnings) - if cmd != 'wait' and cmd in self.separate: - notused = (n,line) - if cmd.lower() in self.separate: - curlabel = [] - cmdn += 1 - nextinfo = None - continue - match = re.match('\\A--\s*(.+)\s*--\\s*(?:\\{(.+)\\})?\\Z', line) - if match: - notused = False - label = match.group(1) - if re.match('[\x00:(),]',label): - raise PyMSError('Interpreting',"Invalid label name '%s', it can not contain a null byte, or the characters: , ( ) :" % label,n,line, warnings=warnings) - if type(jumps.get(label)) == int: - raise PyMSError('Interpreting',"There is already a block with the name '%s'" % label,n,line, warnings=warnings) - curlabel.append(label) - if ai[0] in findtotaljumps and label in findtotaljumps[ai[0]]: - if ai[0] in scriptids[0]: - a = 'aiscript' - else: - a = 'bwscript' - for fj in findtotaljumps[ai[0]][label]: - if fj[4] in scriptids[0]: - b = 'aiscript' - else: - b = 'bwscript' - if a != b: - raise PyMSError('Interpreting',"You can't jump to an external script in '%s.bin' from a script in '%s.bin'" % (a,b),n,line, warnings=warnings) - for a,y,j,x,i in findtotaljumps[id][label]: - a[y] = [id,cmdn] - j[x] = [id,cmdn] - bw = not ai[0] in scriptids[0] - if not id in externaljumps[bw][0]: - externaljumps[bw][0][id] = {} - if not cmdn in externaljumps[bw][0][id]: - externaljumps[bw][0][id][cmdn] = [] - externaljumps[bw][0][id][cmdn].append(i) - if not i in externaljumps[bw][1]: - externaljumps[bw][1][i] = [] - if not id in externaljumps[bw][1][i]: - externaljumps[bw][1][i].append(id) - del findtotaljumps[id][label] - if not findtotaljumps[id]: - del findtotaljumps[id] - if label in jumps: - for a,y,j,x in jumps[label]: - a[y] = cmdn - j[x] = cmdn - jumps[label] = cmdn - else: - jumps[label] = cmdn - if not label in findgoto[id]: - findgoto[id][label] = [False,len(ai[5])] - if not (id,label) in unused: - unused[(id,label)] = True - ai[5].append(cmdn) - totaljumps[id][label] = [id,cmdn] - curinfo[id][1][label] = '' - blocknames.append(label) - if match.group(2): - if '\x00' in match.group(2): - raise PyMSError('Interpreting','Information Comments can not contain null bytes',n,line, warnings=warnings) - curinfo[id][1][label] = match.group(2) - nextinfo = None - else: - nextinfo = [curinfo,id,label] - continue - if line.startswith('{'): - if not nextinfo: - raise PyMSError('Interpreting','An Information Comment must be afer a variable, a script header, or a block label',n,line, warnings=warnings) - match = re.match('\\A\\{(?:(.+)\\})?\\Z', line) - if match.group(1): - if len(nextinfo) == 3: - nextinfo[0][curinfo[1]][1][curinfo[2]] = match.group(1) - else: - nextinfo[0][curinfo[1]][0] = match.group(1) - nextinfo = None - else: - multiline = True - lastmulti = [n,line] - continue - raise PyMSError('Interpreting','Invalid syntax, unknown line format',n,line, warnings=warnings) - if multiline: - raise PyMSError('Interpreting',"There is an unclosed Information Comment in the AI with ID '%s'" % ai[0],lastmulti[0],lastmulti[1], warnings=warnings) - if ai: - if not ai[4]: - raise PyMSError('Interpreting',"AI with ID '%s' has no commands" % ai[0], warnings=warnings) - if None in ai[5]: - dat = blocknames[ai[5].index(None)] - if type(dat) == str: - raise PyMSError('Interpreting',"There is no block with name '%s' in AI with ID '%s'" % (dat,ai[0]), warnings=warnings) - if ai[0] in findtotaljumps and findtotaljumps[ai[0]]: - n = findtotaljumps[ai[0]].keys()[0] - raise PyMSError('Interpreting',"There is no block with name '%s' in AI with ID '%s'" % (n,ai[0]), warnings=warnings) - if ai[4][-1][0] not in self.script_endings: - warnings.append(PyMSWarning('Interpreting', "The AI with ID '%s' does not end with a stop or definite loop. To ensure your script doesn't run into the next script, it must end with one of: goto(), stop(), debug(), time_jump(), or race_jump()" % ai[0], level=1)) - if ai[0] in findgoto: - for l,f in findgoto[ai[0]].iteritems(): - if f[0]: - del findgoto[ai[0]][l] - if not findgoto[ai[0]]: - del findgoto[ai[0]] - if ai[1]: - ais[ai[0]] = ai[1:] - aisizes[ai[0]] = aisize - else: - ais[ai[0]] = ai[1:4] + [[],[]] - bwais[ai[0]] = [1] + ai[4:] - bwsizes[ai[0]] = aisize - for l in curinfo[ai[0]][2]: - if not l in curinfo[ai[0]][1]: - curinfo[ai[0]][1][l] = '' - s = 2+sum(aisizes.values()) - if s > 65535: - raise PyMSError('Interpreting',"There is not enough room in your aiscript.bin to compile these changes. The current file is %sB out of the max 65535B, these changes would make the file %sB." % (2+sum(self.aisizes.values()),s)) - s = 2+sum(bwsizes.values()) - if s > 65535: - raise PyMSError('Interpreting',"There is not enough room in your bwscript.bin to compile these changes. The current file is %sB out of the max 65535B, these changes would make the file %sB." % (2+sum(self.bwsizes.values()),s)) - if findtotaljumps: - i = findtotaljumps.peek() - l = i[1].peek() - raise PyMSError('Interpreting',"The external jump '%s:%s' in AI script '%s' jumps to an AI script that was not found while interpreting (you must include the scripts for all external jumps)" % (i[0],l[0],l[1][0][4]), warnings=warnings) - if findgoto: - remove = [{},{}] - for i in findgoto.iteritems(): - if not i[0] in remove: - remove[i[0] not in aiinfo][i[0]] = [] - for l,f in i[1].iteritems(): - if not f[0]: - warnings.append(PyMSWarning('Interpeting',"The label '%s' in AI script '%s' is unused, label is discarded" % (l,i[0]))) - remove[i[0] not in aiinfo][i[0]].append(f[1]) - if i[0] in aiinfo: - aiinfo[i[0]][1].remove(l) - else: - bwinfo[i[0]][1].remove(l) - for b,r in enumerate(remove): - for i in r.iterkeys(): - if r[i]: - r[i].sort() - n = 0 - for x in r[i]: - if b: - del bwais[i][1][x-n] - else: - del ais[i][4][x-n] - n += 1 - for id,u in unused.iteritems(): - if u and (not id[0] in findgoto or not id[1] in findgoto[id[0]]): - warnings.append(PyMSWarning('Interpeting',"The label '%s' in AI script '%s' is only referenced by commands that cannot be reached and is therefore unused" % (id[1],id[0]))) - if self.ais: - for id,dat in ais.iteritems(): - self.ais[id] = dat - else: - self.ais = ais - self.aisizes = aisizes - for a,b in ((0,0),(0,1),(1,0),(1,1)): - if self.externaljumps[a][b]: - for id,dat in externaljumps[a][b].iteritems(): - self.externaljumps[a][b][id] = dat - else: - self.externaljumps[a][b] = externaljumps[a][b] - if self.bwscript.ais: - for id,dat in bwais.iteritems(): - self.bwscript.ais[id] = dat - else: - self.bwscript.ais = bwais - self.bwsizes = bwsizes - if extra: - self.varinfo = varinfo - self.aiinfo = aiinfo - self.bwscript.aiinfo = bwinfo - return warnings - - def reference(self, file): - file.write('#----------------------------------------------------\n# Parameter Types:\n') - done = [] - for p in self.parameters: - if p: - for t in p: - if t: - n = t.__doc__.split(' ',1)[0] - if not n in done: - file.write('# %s\n' % t.__doc__) - done.append(n) - file.write('#\n# Commands:\n') - for c,ps in zip(self.short_labels,self.parameters): - if c: - file.write('# %s(' % c) - if ps: - comma = False - for p in ps: - if comma: - file.write(', ') - else: - comma = True - file.write(p.__doc__.split(' ',1)[0]) - file.write(')\n') - # file.write('#\n# Descriptive Commands:\n') - # for c,ps in zip(self.labels,self.parameters): - # if c: - # file.write('# %s(' % c) - # if ps: - # comma = False - # for p in ps: - # if comma: - # file.write(', ') - # else: - # comma = True - # file.write(p.__doc__.split(' ',1)[0]) - # file.write(')\n') - file.write('#----------------------------------------------------\n\n') - - def decompile(self, file, defs=None, ref=False, shortlabel=True, scripts=None): - if isstr(file): - try: - f = open(file, 'w') - except: - raise PyMSError('Decompile',"Could not load file '%s'" % file, warnings=warnings) - else: - f = file - if scripts == None: - scripts = self.ais.keys() - if shortlabel: - labels = self.short_labels - else: - labels = self.labels - if ref: - self.reference(f) - warnings = [] - extjumps = {} - if defs: - variables = {} - if isstr(defs): - defs = defs.split(',') - loaded = [] - for dname in defs: - try: - deffile = os.path.join(os.path.dirname(file),dname) - except: - deffile = os.path.abspath(dname) - if dname in loaded: - continue - try: - d = open(deffile,'r') - ddata = d.readlines() - d.close() - except: - raise PyMSError('External Definition',"External definition file '%s' was not found" % dname, warnings=warnings) - for n,l in enumerate(ddata): - if len(l) > 1: - line = l.strip().split('#',1)[0] - if line: - match = re.match('\\A(\\S+)\\s+(.+)\\s+=\\s+(.+?)(?:\\s*\\{(.+)\\})?\\Z', line) - if match: - t,name,dat,vinfo = match.groups() - if re.match('[\x00,(){}]',name): - raise PyMSError('External Definition',"The variable name '%s' in external definition file '%s' is invalid, it must not contain a null, or the characters: , ( ) { }" % (name,dname),n,line, warnings=warnings) - t = t.lower() - if t == 'label' or t not in self.types: - raise PyMSError('External Definition',"Invalid variable type '%s' for variable '%s' in external definition file '%s', consult the reference" % (t, name, dname),n,line, warnings=warnings) - if name.lower() in variables: - raise PyMSError('External Definition',"The variable name '%s' in external definition file '%s' is already in use" % (name, dname),n,line, warnings=warnings) - if vinfo: - warnings.append(PyMSWarning('External Definition',"External definition files do not support Information Comments, information is discarded",n,line)) - try: - v = self.types[t][0](dat,3)[1] - except PyMSWarning, w: - w.line = n - w.code = line - warnings.append(w) - v = w.extra[1] - except PyMSError, e: - e.line = n + 1 - e.code = line - e.warnings = warnings - raise e - except: - raise PyMSError('External Definition',"Invalid value '%s' for variable '%s' of type '%s' in external definition file '%s'" % (dat, name, t, dname),n,line, warnings=warnings) - variables[name.lower()] = [self.types[t],dat] - self.varinfo[name] = [types.index(t),v,None] - else: - raise PyMSError('External Definition','Invalid syntax, unknown line format',n,l, warnings=warnings) - f.write('extdef %s\n' % dname) - loaded.append(dname) - f.write('\n') - values = {} - for name,dat in self.varinfo.iteritems(): - vtype = types[dat[0]] - if dat[2] != None: - f.write('%s %s = %s' % (vtype,name,dat[1])) - if dat[2] and not '\n' in dat[2]: - f.write(' {%s}' % dat[2]) - t = self.types[vtype][0](dat[1],1)[1] - if t != str(dat[1]): - f.write(' # Translated Value: %s' % t) - f.write('\n') - if dat[2] and '\n' in dat[2]: - f.write('{\n%s\n}\n' % dat[2]) - if not vtype in values: - values[vtype] = {} - values[vtype][dat[1]] = name - f.write('\n') - for id in scripts: - if id not in self.ais: - raise PyMSError('Decompile',"There is no AI Script with ID '%s'" % id, warnings=warnings) - loc,string,flags,ai,jumps = self.ais[id] - if loc: - cmdn = 0 - if id in extjumps: - j = len(extjumps[id]) - jump = dict(extjumps[id]) - else: - j = 0 - jump = {} - f.write('# stat_txt.tbl entry %s: %s\n%s(%s, %s, aiscript):' % (string,TBL.decompile_string(self.tbl.strings[string]),id,string,convflags(flags))) - labelnames = None - if id in self.aiinfo: - labelnames = self.aiinfo[id][1].copy() - namenum = 0 - gotonum = 0 - i = self.aiinfo[id][0] - if '\n' in i: - f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) - elif i: - f.write(' {%s}' % i) - f.write('\n') - for cmd in ai: - if cmdn in jump: - if cmdn: - f.write('\n') - if id in self.externaljumps[0][0] and cmdn in self.externaljumps[0][0][id]: - s = '' - if len(self.externaljumps[0][0][id][cmdn]) > 1: - s = 's' - f.write('\t\t# Referenced externally by script%s: %s\n' % (s, ', '.join(self.externaljumps[0][0][id][cmdn]))) - f.write('\t\t--%s--' % jump[cmdn]) - if labelnames and jump[cmdn] in labelnames: - i = labelnames.get(jump[cmdn],'') - if '\n' in i: - f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) - elif i: - f.write(' {%s}' % i) - f.write('\n') - elif cmdn in jumps: - if labelnames: - jump[cmdn] = labelnames.getkey(namenum) - else: - jump[cmdn] = '%s %04d' % (id,j) - if cmdn: - f.write('\n') - if id in self.externaljumps[0][0] and cmdn in self.externaljumps[0][0][id]: - s = '' - if len(self.externaljumps[0][0][id][cmdn]) > 1: - s = 's' - f.write('\t\t# Referenced externally by script%s: %s\n' % (s, ', '.join(self.externaljumps[0][0][id][cmdn]))) - f.write('\t\t--%s--' % jump[cmdn]) - if labelnames and jump[cmdn] in labelnames: - i = labelnames.get(jump[cmdn],'') - if '\n' in i: - f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) - elif i: - f.write(' {%s}' % i) - f.write('\n') - j += 1 - f.write('\t%s(' % labels[cmd[0]]) - if len(cmd) > 1: - comma = False - for p,t in zip(cmd[1:],self.parameters[cmd[0]]): - if comma: - f.write(', ') - else: - comma = True - temp = t(p,1) - if t == self.ai_address: - if type(temp[1]) == list: - if temp[1][0] in extjumps and temp[1][1] in extjumps[temp[1][0]]: - f.write('%s:%s' % (temp[1][0],extjumps[temp[1][0]][temp[1][1]])) - else: - if not temp[1][0] in extjumps: - extjumps[temp[1][0]] = {} - if labelnames: - t = self.aiinfo[id][2][gotonum] - n = t.split(':',1) - f.write(t) - extjumps[n[0]][temp[1][1]] = n[1] - else: - n = '%s %04d' % (temp[1][0],len(extjumps[temp[1][0]])) - f.write('%s:%s' % (temp[1][0],n)) - extjumps[temp[1][0]][temp[1][1]] = n - elif temp[1] in jump: - f.write(jump[temp[1]]) - else: - if labelnames: - jump[temp[1]] = self.aiinfo[id][2][gotonum] - else: - jump[temp[1]] = '%s %04d' % (id,j) - f.write(jump[temp[1]]) - j += 1 - if labelnames: - gotonum += 1 - else: - for vt in self.typescanbe[t.__doc__.split(' ',1)[0]]: - vtype = vt.__doc__.split(' ',1)[0] - if vtype in values and p in values[vtype]: - f.write(values[vtype][p]) - break - else: - f.write(temp[1]) - f.write(')\n') - if labels[cmd[0]].lower() in self.separate: - f.write('\n') - cmdn += 1 - f.write('\n') - else: - f.write('# stat_txt.tbl entry %s: %s\n%s(%s, %s, bwscript):' % (string,TBL.decompile_string(self.tbl.strings[string]),id,string,convflags(flags))) - labelnames = None - w = self.bwscript.decompile(f, (self.externaljumps,extjumps), values, shortlabel, [id]) - if w: - warnings.extend(w) - f.close() - return warnings - - def compile(self, file, bwscript=None, extra=False): - if isstr(file): - try: - f = open(file, 'wb') - except: - raise PyMSError('Compile',"Could not load file '%s'" % file) - else: - f = file - warnings = [] - ais = '' - table = '' - offset = 4 - totaloffsets = {} - for id in self.ais.keys(): - loc,string,flags,ai,jumps = self.ais[id] - if loc: - cmdn = 0 - totaloffsets[id] = {} - for cmd in ai: - if cmdn in jumps: - totaloffsets[id][cmdn] = offset - if self.parameters[cmd[0]]: - for p,t in zip(cmd[1:],self.parameters[cmd[0]]): - try: - if t == self.ai_address: - offset += t(0,2)[0] - else: - offset += t(p,2)[0] - except PyMSWarning, e: - if not warnings: - warnings.append(e) - offset += e.extra[0] - cmdn += 1 - offset += 1 - offset = 4 - for id in self.ais.keys(): - loc,string,flags,ai,jumps = self.ais[id] - if loc: - table += struct.pack('<4s3L', id, offset, string+1, flags) - for cmd in ai: - ais += chr(cmd[0]) - if self.parameters[cmd[0]]: - for p,t in zip(cmd[1:],self.parameters[cmd[0]]): - try: - if t == self.ai_address: - if type(p) == list: - d = t(totaloffsets[p[0]][p[1]],2) - else: - d = t(totaloffsets[id][p],2) - else: - d = t(p,2) - except PyMSWarning, e: - if not warnings: - warnings.append(e) - offset += e.extra[0] - ais += e.extra[1] - else: - ais += d[1] - offset += d[0] - offset += 1 - else: - table += struct.pack('<4s3L', id, 0, string+1, flags) - f.write('%s%s%s\x00\x00\x00\x00' % (struct.pack(' len(self.labels): - raise PyMSError('Load','Invalid command, could possibly be a corrrupt bwscript.bin') - ai = [cmd] - if self.parameters[cmd]: - for p in self.parameters[cmd]: - d = p(curdata[curoffset:]) - if p == self.ai_address: - if d[1] < loc: - if d[1] not in totaloffsets: - raise PyMSError('Load','Incorrect jump location, could possibly be a corrupt bwscript.bin') - tos = totaloffsets[d[1]] - ais[id][2].append(tos) - ai.append(tos) - if not tos[0] in externaljumps[1][0]: - externaljumps[1][0][tos[0]] = {} - if not len(cmdoffsets) in externaljumps[1][0][tos[0]]: - externaljumps[1][0][tos[0]][tos[1]] = [] - externaljumps[1][0][tos[0]][tos[1]].append(id) - if not id in externaljumps[1][1]: - externaljumps[1][1][id] = [] - if not tos[0] in externaljumps[1][1][id]: - externaljumps[1][1][id].append(tos[0]) - elif d[1] >= offsets[offsets.index(loc)+1]: - if not d[1] in findtotaloffsets: - findtotaloffsets[d[1]] = [] - findtotaloffsets[d[1]].append([id,len(ais[id][2]),ai,len(ai)]) - ais[id][2].append(None) - ai.append(None) - elif d[1] - loc in cmdoffsets: - pos = cmdoffsets.index(d[1] - loc) - ais[id][2].append(pos) - ai.append(pos) - else: - if not d[1] - loc in findoffset: - findoffset[d[1] - loc] = [] - findoffset[d[1] - loc].append([len(ais[id][2]),ai,len(ai)]) - ais[id][2].append(None) - ai.append(None) - else: - ai.append(d[1]) - curoffset += d[0] - ais[id][1].append(ai) - aisizes[id] = curoffset - if None in ais[id][2]: - checknones.append(id) - for c in checknones: - if None in ais[id][2]: - raise PyMSError('Load','Incorrect jump location, could possibly be a corrupt bwscript.bin') - if offset < len(data): - def getstr(dat,o): - i = dat[o:].index('\x00') - return [dat[o:o+i],o+i+1] - try: - info = decompress(data[offset:]) - offset = 0 - while info[offset] != '\x00': - id = info[offset:offset+4] - offset += 4 - desc,offset = getstr(info,offset) - aiinfo[id] = [desc,odict(),[]] - while info[offset] != '\x00': - label,offset = getstr(info,offset) - desc,offset = getstr(info,offset) - aiinfo[id][1][label] = desc - offset += 1 - p = int(floor(log(len(aiinfo),256))) - s,n = '<' + ['B','H','L','L'][p],[1,2,4,4][p] - while info[offset] != '\x00': - aiinfo[id][2].append(aiinfo[id][1].getkey(struct.unpack(s,info[offset:offset+n])[0]-1)) - offset += n - offset += 1 - except: - warnings.append(PyMSWarning('Load','Unsupported extra script information section in bwscript.bin, could possibly be corrupt. Continuing without extra script information')) - self.ais = ais - self.aisizes = aisizes - self.externaljumps = externaljumps - self.aiinfo = aiinfo - return warnings - except PyMSError: - raise - except: - raise PyMSError('Load',"Unsupported bwscript.bin '%s', could possibly be invalid or corrupt" % file,exception=sys.exc_info()) - - def decompile(self, filename, extjumps, values, shortlabel=True, scripts=None): - close = True - if isstr(filename): - try: - f = open(filename, 'w') - except: - raise PyMSError('Decompile',"Could not load file '%s'" % filename) - else: - f = filename - if scripts == None: - scripts = self.ais.keys() - if shortlabel: - labels = self.short_labels - else: - labels = self.labels - externaljumps = extjumps[0] - extjumps = extjumps[1] - warnings = [] - for id in scripts: - if not id in self.ais: - raise PyMSError('Decompile',"There is no AI Script with ID '%s'" % id) - loc,ai,jumps = self.ais[id] - cmdn = 0 - if id in extjumps: - j = len(extjumps[id]) - jump = dict(extjumps[id]) - else: - j = 0 - jump = {} - labelnames = None - if id in self.aiinfo: - labelnames = self.aiinfo[id][1].copy() - namenum = 0 - gotonum = 0 - i = self.aiinfo[id][0] - if '\n' in i: - f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) - elif i: - f.write(' {%s}' % i) - f.write('\n') - for cmd in ai: - if cmdn in jump: - if cmdn: - f.write('\n') - if id in self.externaljumps[1][0] and cmdn in self.externaljumps[1][0][id]: - s = '' - if len(self.externaljumps[1][0][id][cmdn]) > 1: - s = 's' - f.write('\t\t# Referenced externally by script%s: %s\n' % (s, ', '.join(self.externaljumps[1][0][id][cmdn]))) - f.write('\t\t--%s--' % jump[cmdn]) - if labelnames: - i = labelnames.get(jump[cmdn],'') - if '\n' in i: - f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) - elif i: - f.write(' {%s}' % i) - namenum += 1 - f.write('\n') - elif cmdn in jumps: - if labelnames: - jump[cmdn] = labelnames.getkey(namenum) - namenum += 1 - else: - jump[cmdn] = '%s %04d' % (id,j) - if cmdn: - f.write('\n') - if id in self.externaljumps[1][0] and cmdn in self.externaljumps[1][0][id]: - s = '' - if len(self.externaljumps[1][0][id][cmdn]) > 1: - s = 's' - f.write('\t\t# Referenced externally by script%s: %s\n' % (s, ', '.join(self.externaljumps[1][0][id][cmdn]))) - f.write('\t\t--%s--' % jump[cmdn]) - if labelnames and jump[cmdn] in labelnames: - i = labelnames.get(jump[cmdn],'') - if '\n' in i: - f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) - elif i: - f.write(' {%s}' % i) - f.write('\n') - j += 1 - f.write('\t%s(' % labels[cmd[0]]) - if len(cmd) > 1: - comma = False - for p,t in zip(cmd[1:],self.parameters[cmd[0]]): - if comma: - f.write(', ') - else: - comma = True - temp = t(p,1) - if t == self.ai_address: - if type(temp[1]) == list: - if temp[1][0] in extjumps and temp[1][1] in extjumps[temp[1][0]]: - f.write('%s:%s' % (temp[1][0],extjumps[temp[1][0]][temp[1][1]])) - else: - if not temp[1][0] in extjumps: - extjumps[temp[1][0]] = {} - if labelnames: - t = self.aiinfo[id][2][gotonum] - n = t.split(':',1) - f.write(t) - extjumps[n[0]][temp[1][1]] = n[1] - else: - n = '%s %04d' % (temp[1][0],len(extjumps[temp[1][0]])) - f.write('%s:%s' % (temp[1][0],n)) - extjumps[temp[1][0]][temp[1][1]] = n - elif temp[1] in jump: - f.write(jump[temp[1]]) - else: - if labelnames: - jump[temp[1]] = self.aiinfo[id][2][gotonum] - else: - jump[temp[1]] = '%s %04d' % (id,j) - f.write(jump[temp[1]]) - j += 1 - if labelnames: - gotonum += 1 - else: - for vt in self.typescanbe[t.__doc__.split(' ',1)[0]]: - vtype = vt.__doc__.split(' ',1)[0] - if vtype in values and p in values[vtype]: - f.write(values[vtype][p]) - break - else: - f.write(temp[1]) - f.write(')\n') - cmdn += 1 - f.write('\n') - if close: - f.close() - return warnings - - def compile(self, file, extra=False): - if isstr(file): - try: - f = open(file, 'wb') - except: - raise PyMSError('Compile',"Could not load file '%s'" % file) - else: - f = file - ais = '' - table = '' - offset = 4 - totaloffsets = {} - for id in self.ais.keys(): - loc,ai,jumps = self.ais[id] - if loc: - cmdn = 0 - totaloffsets[id] = {} - for cmd in ai: - totaloffsets[id][cmdn] = offset - if self.parameters[cmd[0]]: - for p,t in zip(cmd[1:],self.parameters[cmd[0]]): - try: - if t == self.ai_address: - offset += t(0,1)[0] - else: - offset += t(p,1)[0] - except PyMSWarning, e: - if not warnings: - warnings.append(e) - cmdn += 1 - offset += 1 - offset = 4 - for id in self.ais.keys(): - loc,ai,jumps = self.ais[id] - table += struct.pack('<4sL', id, offset) - for cmd in ai: - ais += chr(cmd[0]) - if self.parameters[cmd[0]]: - for p,t in zip(cmd[1:],self.parameters[cmd[0]]): - try: - if t == self.ai_address: - if type(p) == list: - d = t(totaloffsets[p[0]][p[1]],2) - else: - d = t(totaloffsets[id][p],2) - else: - d = t(p,2) - ais += d[1] - offset += d[0] - except PyMSWarning, e: - if not warnings: - warnings.append(e) - offset += 1 - f.write('%s%s%s\x00\x00\x00\x00' % (struct.pack(' len(self.tbl.strings): + if addstrings: + if string-len(self.tbl.strings) > 1: + self.tbl.strings.extend(['Generated by PyAI\x00'] * (string-len(self.tbl.strings)-1)) + self.tbl.strings.append('Custom name generated by PyAI for AI with ID: %s\x00' % id) + warnings.append(PyMSWarning('Load',"The AI with ID '%s' had a custom string (#%s) not present in the stat_txt.tbl, so PyAI generated one." % (id,string), level=1)) + else: + raise PyMSError('Load',"String id '%s' is not present in the stat_txt.tbl" % string) + aioffsets.append([id,loc,string-1,flags]) + offset += 4 + offsets.sort() + try: + externaljumps = [[{},{}],[dict(self.bwscript.externaljumps[1][0]),dict(self.bwscript.externaljumps[1][1])]] + except: + externaljumps = [[{},{}],[{},{}]] + totaloffsets = {} + findtotaloffsets = {} + checknones = [] + for id,loc,string,flags in aioffsets: + ais[id] = [loc,string,flags,[],[]] + if loc: + curdata = data[loc:offsets[offsets.index(loc)+1]] + curoffset = 0 + cmdoffsets = [] + findoffset = {} + while curoffset < len(curdata): + if curoffset+loc in findtotaloffsets: + for fo in findtotaloffsets[curoffset+loc]: + ais[fo[0]][4][fo[1]] = [id,len(cmdoffsets)] + fo[2][fo[3]] = [id,len(cmdoffsets)] + ais[id][4].append(len(cmdoffsets)) + if not id in externaljumps[0][0]: + externaljumps[0][0][id] = {} + if not len(cmdoffsets) in externaljumps[0][0][id]: + externaljumps[0][0][id][len(cmdoffsets)] = [] + externaljumps[0][0][id][len(cmdoffsets)].append(fo[0]) + if not fo[0] in externaljumps[0][1]: + externaljumps[0][1][fo[0]] = [] + if not id in externaljumps[0][1][fo[0]]: + externaljumps[0][1][fo[0]].append(id) + del findtotaloffsets[curoffset+loc] + if curoffset in findoffset: + for fo in findoffset[curoffset]: + ais[id][4][fo[0]] = len(cmdoffsets) + fo[1][fo[2]] = len(cmdoffsets) + del findoffset[curoffset] + totaloffsets[curoffset+loc] = [id,len(cmdoffsets)] + cmdoffsets.append(curoffset) + cmd,curoffset = ord(curdata[curoffset]),curoffset + 1 + #print(id),loc,curoffset,self.short_labels[cmd] + if not cmd and curoffset == len(curdata): + break + if cmd >= len(self.labels): + raise PyMSError('Load','Invalid command, could possibly be a corrrupt aiscript.bin') + ai = [cmd] + if self.parameters[cmd]: + for p in self.parameters[cmd]: + d = p(curdata[curoffset:]) + if p == self.ai_address: + if d[1] < loc: + if d[1] not in totaloffsets: + raise PyMSError('Load','Incorrect jump location, it could possibly be a corrupt aiscript.bin') + tos = totaloffsets[d[1]] + ais[id][4].append(tos) + ai.append(tos) + # print(tos) + # print(externaljumps) + if not tos[0] in externaljumps[0][0]: + externaljumps[0][0][tos[0]] = {} + if not tos[1] in externaljumps[0][0][tos[0]]: + externaljumps[0][0][tos[0]][tos[1]] = [] + externaljumps[0][0][tos[0]][tos[1]].append(id) + if not id in externaljumps[0][1]: + externaljumps[0][1][id] = [] + if not tos[0] in externaljumps[0][1][id]: + externaljumps[0][1][id].append(tos[0]) + elif d[1] >= offsets[offsets.index(loc)+1]: + if not d[1] in findtotaloffsets: + findtotaloffsets[d[1]] = [] + findtotaloffsets[d[1]].append([id,len(ais[id][4]),ai,len(ai)]) + ais[id][4].append(None) + ai.append(None) + elif d[1] - loc in cmdoffsets: + pos = cmdoffsets.index(d[1] - loc) + ais[id][4].append(pos) + ai.append(pos) + else: + if not d[1] - loc in findoffset: + findoffset[d[1] - loc] = [] + findoffset[d[1] - loc].append([len(ais[id][4]),ai,len(ai)]) + ais[id][4].append(None) + ai.append(None) + else: + ai.append(d[1]) + curoffset += d[0] + ais[id][3].append(ai) + aisizes[id] = curoffset + if None in ais[id][4]: + checknones.append(id) + for c in checknones: + if None in ais[id][4]: + raise PyMSError('Load','Incorrect jump location, could possibly be a corrupt aiscript.bin') +#tName<0>v[description]<0>E +#AIID[description]<0> + #label<0>[description]<0> + #<0> + #LN + #<0> +#<0> + if offset < len(data): + def getstr(dat,o): + i = dat[o:].index('\x00') + return [dat[o:o+i],o+i+1] + try: + info = decompress(data[offset:]) + offset = 0 + t = ord(info[offset])-1 + while t > -1: + offset += 1 + name,offset = getstr(info,offset) + o,val = self.types[types[t]][0](info[offset:]) + offset += o + desc,offset = getstr(info,offset) + varinfo[name] = [t,val[1],desc] + t = ord(info[offset])-1 + offset += 1 + while info[offset] != '\x00': + id = info[offset:offset+4] + offset += 4 + desc,offset = getstr(info,offset) + aiinfo[id] = [desc,odict(),[]] + while info[offset] != '\x00': + label,offset = getstr(info,offset) + desc,offset = getstr(info,offset) + aiinfo[id][1][label] = desc + offset += 1 + p = int(floor(log(len(aiinfo[id][1]),256))) + s,n = '<' + ['B','H','L','L'][p],[1,2,4,4][p] + while info[offset:offset+n].replace('\x00',''): + aiinfo[id][2].append(aiinfo[id][1].getkey(struct.unpack(s,info[offset:offset+n])[0]-1)) + offset += n + offset += 1 + except: + aiinfo = {} + warnings.append(PyMSWarning('Load','Unsupported extra script information section in aiscript.bin, could possibly be corrupt. Continuing without extra script information')) + self.ais = ais + self.aisizes = aisizes + self.externaljumps = externaljumps + self.varinfo = varinfo + self.aiinfo = aiinfo + def pprint(obj, depth=0, max=2): + depth += 1 + string = '' + if isinstance(obj, dict) and max: + if obj: + string += '{\\\n' + for key in obj: + string += '%s%s:' % ('\t'*depth, repr(key)) + string += pprint(obj[key], depth, max-1) + string += '%s},\\\n' % ('\t'*(depth-1)) + else: + string += '{},\\\n' + elif isinstance(obj, list) and max: + if obj: + string += '[\\\n' + for item in obj: + string += ('%s' % ('\t'*depth)) + string += pprint(item, depth, max-1) + string += '%s],\\\n' % ('\t'*(depth-1)) + else: + string += '[],\\\n' + else: + string += '%s,\\\n' % (repr(obj),) + if depth == 1: + return string[:-3] + return string + print(pprint)(self.externaljumps) + return warnings + except PyMSError: + raise + except: + raise PyMSError('Load',"Unsupported aiscript.bin '%s', could possibly be invalid or corrupt" % file,exception=sys.exc_info()) + + #Stages: + # 0 - Load file + # 1 - Decompile + # 2 - Compile + # 3 - Interpret + def ai_byte(self, data, stage=0): + """byte - A number in the range 0 to 255""" + if not stage: + v = ord(data[0]) + elif stage == 1: + v = str(data) + elif stage == 2: + v = chr(data) + else: + try: + v = int(data) + if v < 0 or v > 255: + raise + except: + raise PyMSError('Parameter',"Invalid byte value '%s', it must be a number in the range 0 to 255" % data) + return [1,v] + + def ai_word(self, data, stage=0): + """word - A number in the range 0 to 65535""" + if not stage: + v = struct.unpack(' 65535: + raise + except: + raise PyMSError('Parameter',"Invalid word value '%s', it must be a number in the range 0 to 65535" % data) + return [2,v] + + def ai_address(self, data, stage=0): + """block - The block label name (Can not be used as a variable type!)""" + if stage == 1: + return [2,data] + return self.ai_word(data, stage) + + def ai_unit(self, data, stage=0): + """unit - A unit ID from 0 to 227, or a full unit name from stat_txt.tbl""" + if not stage: + v = ord(data[0]) + elif stage == 1: + s = self.tbl.strings[data].split('\x00') + if s[1] != '*': + v = TBL.decompile_string('\x00'.join(s[:2]), '\x0A\x28\x29\x2C') + else: + v = TBL.decompile_string(s[0], '\x0A\x28\x29\x2C') + elif stage == 2: + v = chr(data) + '\x00' + else: + try: + v = int(data) + if -1 > v or v > DAT.UnitsDAT.count: + raise + except: + for i,name in enumerate(self.tbl.strings[:DAT.UnitsDAT.count]): + n = name.split('\x00')[:2] + if TBL.compile_string(data) == n[0] or (n[1] != '*' and TBL.compile_string(data) == '\x00'.join(n)): + v = i + break + else: + raise PyMSError('Parameter',"Unit '%s' was not found" % data) + return [2,v] + + def ai_building(self, data, stage=0): + """building - Same as unit type, but only units that are Buildings, Resource Miners, and Overlords""" + v = self.ai_unit(data, stage) + if stage == 3: + flags = self.unitsdat.get_value(v[1],'SpecialAbilityFlags') + if not flags & 8 and not flags & 1 and not self.unitsdat.get_value(v[1],'SupplyProvided'):#v != 42 and + raise PyMSWarning('Parameter','Unit is not a building or worker', extra=v, level=1) + return v + + def ai_military(self, data, stage=0): + """military - Same as unit type, but only military units (not Buildings)""" + v = self.ai_unit(data, stage) + if stage == 3: + flags = self.unitsdat.get_value(v[1],'SpecialAbilityFlags') + if flags & 1: + raise PyMSWarning('Parameter','Unit is a building', extra=v, level=1) + return v + + def ai_ggmilitary(self, data, stage=0): + """gg_military - Same as Military type, but only for defending against an enemy Ground unit attacking your Ground unit""" + v = self.ai_military(data, stage) + return v + + def ai_agmilitary(self, data, stage=0): + """ag_military - Same as Military type, but only for defending against an enemy Air unit attacking your Ground unit""" + v = self.ai_military(data, stage) + return v + + def ai_gamilitary(self, data, stage=0): + """ga_military - Same as Military type, but only for defending against an enemy Ground unit attacking your Air unit""" + v = self.ai_military(data, stage) + return v + + def ai_aamilitary(self, data, stage=0): + """aa_military - Same as Military type, but only for defending against an enemy Air unit attacking your Air unit""" + v = self.ai_military(data, stage) + return v + + def ai_upgrade(self, data, stage=0): + """upgrade - An upgrade ID from 0 to 60, or a full upgrade name from stat_txt.tbl""" + if not stage: + v = ord(data[0]) + elif stage == 1: + v = TBL.decompile_string(self.tbl.strings[self.upgradesdat.get_value(data,'Label') - 1].split('\x00',1)[0].strip(), '\x0A\x28\x29\x2C') + elif stage == 2: + v = chr(data) + '\x00' + else: + try: + v = int(data) + if -1 > v or v > DAT.UpgradesDAT.count: + raise + except: + for i in range(len(self.upgradesdat.entries)): + if TBL.compile_string(data) == self.tbl.strings[self.upgradesdat.get_value(i,'Label') - 1].split('\x00', 1)[0].strip(): + v = i + break + else: + raise PyMSError('Parameter',"Upgrade '%s' was not found" % data) + return [2,v] + + def ai_technology(self, data, stage=0): + """technology - An technology ID from 0 to 43, or a full technology name from stat_txt.tbl""" + if not stage: + v = ord(data[0]) + elif stage == 1: + v = TBL.decompile_string(self.tbl.strings[self.techdat.get_value(data,'Label') - 1].split('\x00',1)[0].strip(), '\x0A\x28\x29\x2C') + elif stage == 2: + v = chr(data) + '\x00' + else: + try: + v = int(data) + if -1 > v or v > DAT.TechDAT.count: + raise + except: + for i in range(len(self.techdat.entries)): + if TBL.compile_string(data) == self.tbl.strings[self.techdat.get_value(i,'Label') - 1].split('\x00', 1)[0].strip(): + v = i + break + else: + raise PyMSError('Parameter',"Technology '%s' was not found" % data) + return [2,v] + + def ai_string(self, data, stage=0): + """string - A string of any characters (except for nulls: <0>) in TBL string formatting (use <40> for an open parenthesis '(', <41> for a close parenthesis ')', and <44> for a comma ',')""" + if not stage: + e = data.index('\x00') + return [e+1,TBL.decompile_string(data[:e], '\x0A\x28\x29\x2C')] + elif stage == 1: + return [len(data)+1,data] + elif stage == 2: + s = TBL.compile_string(data) + if '\x00' in s: + raise PyMSError('Parameter',"String '%s' contains a null (<0>)" % data) + return [len(s) + 1,s + '\x00'] + return [len(data),data] + + def interpret(self, files, defs=None, extra=False): + if not isinstance(files, list): + files = [files] + alldata = [] + for file in files: + try: + if isstr(file): + f = open(file,'r') + alldata.append(f.readlines()) + f.close() + else: + alldata.append(file.readlines()) + file.close() + except: + raise PyMSError('Interpreting',"Could not load file '%s'" % file) + ais = odict() + aisizes = dict(self.aisizes) + bwsizes = dict(self.bwscript.aisizes) + aisize = 0 + externaljumps = [[{},{}],[{},{}]] + bwais = odict() + varinfo = odict(self.varinfo) + aiinfo = dict(self.aiinfo) + bwinfo = dict(self.bwscript.aiinfo) + curinfo = None + ai = [] + cmdn = 0 + jumps = {} + curlabel = [] + warnings = [] + variables = {} + notused = False + town = False + totaljumps = {} + findtotaljumps = odict() + findgoto = odict() + unused = {} + scriptids = [[],[]] + nextinfo = None + multiline = False + lastmulti = [None,None] + loaded = [] + def load_defs(defname): + try: + deffile = os.path.join(os.path.dirname(files[0]),defname) + except: + deffile = os.path.abspath(defname) + if deffile in loaded: + return + try: + d = open(deffile,'r') + ddata = d.readlines() + d.close() + except: + raise PyMSError('External Definition',"External definition file '%s' was not found" % defname, warnings=warnings) + for n,l in enumerate(ddata): + if len(l) > 1: + line = l.strip().split('#',1)[0] + if line: + match = re.match('\\A(\\S+)\\s+(.+)\\s+=\\s+(.+?)(?:\\s*\\{(.+)\\})?\\Z', line) + if match: + t,name,dat,vinfo = match.groups() + if re.match('[\x00,(){}]',name): + raise PyMSError('External Definition',"The variable name '%s' in external definition file '%s' is invalid, it must not contain a null, or the characters: , ( ) { }" % (name,defname),n,line, warnings=warnings) + t = t.lower() + if t == 'label' or t not in self.types: + raise PyMSError('External Definition',"Invalid variable type '%s' for variable '%s' in external definition file '%s', consult the reference" % (t, name, defname),n,line, warnings=warnings) + if name.lower() in variables: + raise PyMSError('External Definition',"The variable name '%s' in external definition file '%s' is already in use" % (name, defname),n,line, warnings=warnings) + if vinfo: + warnings.append(PyMSWarning('External Definition',"External definition files do not support Information Comments, information is discarded",n,line)) + try: + v = self.types[t][0](dat,3)[1] + except PyMSWarning as w: + w.line = n + 1 + w.code = line + warnings.append(w) + v = w.extra[1] + except PyMSError as e: + e.line = n + 1 + e.code = line + e.warnings = warnings + raise e + except: + raise PyMSError('External Definition',"Invalid value '%s' for variable '%s' of type '%s' in external definition file '%s'" % (dat, name, t, defname),n,line, warnings=warnings) + variables[name.lower()] = [self.types[t],dat] + varinfo[name] = [types.index(t),v,None] + else: + raise PyMSError('External Definition','Invalid syntax, unknown line format',n,l, warnings=warnings) + loaded.append(deffile) + if defs: + if isstr(defs): + defs = defs.split(',') + + for deffile in defs: + load_defs(deffile) + for data in alldata: + for n,l in enumerate(data): + if len(l) > 1: + line = l.split('#',1)[0].strip() + if line: + if multiline: + if line.strip() == '}': + if len(nextinfo) == 3: + if not nextinfo[0][nextinfo[1]][1][nextinfo[2]].replace('\n',''): + raise PyMSError('Interpreting','The Information Comment has no text',n,line, warnings=warnings) + nextinfo[0][nextinfo[1]][1][nextinfo[2]] = nextinfo[0][nextinfo[1]][1][nextinfo[2]][:-1] + else: + if not nextinfo[0][nextinfo[1]][0].replace('\n',''): + raise PyMSError('Interpreting','The Information Comment has no text',n,line, warnings=warnings) + nextinfo[0][nextinfo[1]][0] = nextinfo[0][nextinfo[1]][0][:-1] + nextinfo = None + multiline = False + elif len(nextinfo) == 3: + nextinfo[0][nextinfo[1]][1][nextinfo[2]] += line + '\n' + else: + nextinfo[0][nextinfo[1]][0] += line + '\n' + else: + match = re.match('\\Aextdef\\s*(.+)\\Z',line) + if match: + load_defs(match.group(1)) + continue + match = re.match('\\A(\\S+)\\s+(\\S+)\\s+=\\s+(.+?)(?:\\s*\\{(.+)\\})?\\Z', line) + if match: + t,name,dat,vinfo = match.groups() + if re.match('[\x00,(){}]',name): + raise PyMSError('Interpreting',"The variable name '%s' is invalid, it must not contain a null, or the characters: , ( ) { }",n,line, warnings=warnings) + t = t.lower() + if t == 'label' or t not in self.types: + raise PyMSError('Interpreting',"Invalid variable type '%s', consult the reference" % t,n,line, warnings=warnings) + if name.lower() in variables: + raise PyMSError('Interpreting',"The variable name '%s' is already in use" % name,n,line, warnings=warnings) + try: + self.types[t][0](dat,3) + except PyMSWarning as w: + w.line = n + w.code = line + warnings.append(w) + except PyMSError as e: + e.line = n + 1 + e.code = line + e.warnings = warnings + raise e + except: + raise PyMSError('Interpreting',"Invalid variable value '%s' for type '%s'" % (dat, t),n,line, warnings=warnings) + variables[name.lower()] = [self.types[t],dat] + varinfo[name] = [types.index(t),self.types[t][0](dat,3)[1],''] + if vinfo: + if '\x00' in vinfo: + raise PyMSError('Interpreting','Information Comments can not contain null bytes',n,line, warnings=warnings) + varinfo[name][2] = vinfo + nextinfo = None + else: + nextinfo = [2,varinfo[name]] + continue + if re.match(r'\\A[^(]+\\([^)]+\\):\\s*(?:\\{.+\\})?\\Z', line): + newai = re.match(r'\\A(.+)\\(\s*(.+)\s*,\s*(.+)\s*,\s*(\w+)\s*\\):\\s*(?:\\{(.+)\\})?\\Z', line) + if not newai: + raise PyMSError('Interpreting','Invalid syntax, expected a new script header',n,line, warnings=warnings) + id = newai.group(1) + if id in default_ais: + id = default_ais[id] + elif len(id) != 4: + raise PyMSError('Interpreting',"Invalid AI ID '%s' (must be 4 characeters long, or one of the keywords: Protoss, BWProtoss, Terran, BWTerran, Zerg, BWZerg)" % id,n,line, warnings=warnings) + elif re.match('[,\x00:()]', id): + raise PyMSError('Interpreting',"Invalid AI ID '%s', it can not contain a null byte, or the characters: , ( ) :" % id,n,line, warnings=warnings) + try: + string = int(newai.group(2)) + except: + raise PyMSError('Interpreting','Invalid stat_txt.tbl entry (must be an integer)',n,line, warnings=warnings) + if string < 0 or string >= len(self.tbl.strings): + raise PyMSError('Interpreting','Invalid stat_txt.tbl entry (must be between 0 and %s)' % (len(self.tbl.strings)-1),n,line, warnings=warnings) + if not re.match('[01]{3}', newai.group(3)): + raise PyMSError('Interpreting',"Invalid AI flags '%s' (must be three 0's and/or 1's, see readme for more info)" % newai.group(3),n,line, warnings=warnings) + if not newai.group(4) in ['bwscript','aiscript']: + raise PyMSError('Interpreting',"Invalid script file '%s', it must be either 'aiscript' or 'bwscript'" % newai.group(4),n,line, warnings=warnings) + if ai: + if not ai[4]: + raise PyMSError('Interpreting',"AI with ID '%s' has no commands" % ai[0], warnings=warnings) + if None in ai[5]: + dat = blocknames[ai[5].index(None)] + if type(dat) == str: + raise PyMSError('Interpreting',"There is no block with name '%s' in AI with ID '%s'" % (dat,ai[0]), warnings=warnings) + if ai[0] in findtotaljumps and findtotaljumps[ai[0]]: + n = findtotaljumps[ai[0]].keys()[0] + raise PyMSError('Interpreting',"There is no block with name '%s' in AI with ID '%s'" % (n,ai[0]), warnings=warnings) + if ai[4][-1][0] not in self.script_endings: + warnings.append(PyMSWarning('Interpreting', "The AI with ID '%s' does not end with a stop or definite loop. To ensure your script doesn't run into the next script, it must end with one of: goto(), stop(), debug(), time_jump(), or race_jump()" % ai[0], level=1)) + if ai[0] in findgoto: + for l,f in findgoto[ai[0]].items(): + if f[0]: + del findgoto[ai[0]][l] + if not findgoto[ai[0]]: + del findgoto[ai[0]] + if ai[1]: + ais[ai[0]] = ai[1:] + aisizes[ai[0]] = aisize + else: + ais[ai[0]] = ai[1:4] + [[],[]] + bwais[ai[0]] = [1] + ai[4:] + bwsizes[ai[0]] = aisize + for l in curinfo[ai[0]][2]: + if not l in curinfo[ai[0]][1]: + curinfo[ai[0]][1][l] = '' + notused = False + if newai.group(4) == 'aiscript': + curinfo = aiinfo + scriptids[0].append(id) + loc = 1 + else: + curinfo = bwinfo + scriptids[1].append(id) + loc = 0 + ai = [id,loc,string,convflags(newai.group(3)),[],[]] + aisize = 0 + curinfo[id] = ['',odict(),[]] + if newai.group(5): + if '\x00' in newai.group(5): + raise PyMSError('Interpreting','Information Comments can not contain null bytes',n,line, warnings=warnings) + aiinfo[id][0] = newai.group(5) + nextinfo = None + else: + nextinfo = [curinfo,id] + totaljumps[id] = {} + curlabel = [] + blocknames = [] + if not id in findgoto: + findgoto[id] = odict() + jumps = {} + town = False + if ai[0] in ais: + raise PyMSError('Interpreting',"Duplicate AI ID '%s'" % ai[0],n,line, warnings=warnings) + cmdn = 0 + continue + if ai: + match = re.match('\\A(.+)\\(\\s*(.+)?\\s*\\)\\Z', line) + if match: + cmd = match.group(1).lower() + if cmd in self.labels: + ai[4].append([self.labels.index(cmd)]) + elif cmd in self.short_labels: + ai[4].append([self.short_labels.index(cmd)]) + else: + raise PyMSError('Interpreting',"Invalid command name '%s'" % cmd,n,line, warnings=warnings) + if cmd in self.builds and not town: + warnings.append(PyMSWarning('Interpreting',"You may not have initiated a town in the script '%s' with one of the start_* commands before building units" % ai[0],n,line,level=1)) + elif cmd in self.starts: + town = True + if notused: + warnings.append(PyMSWarning('Interpreting',"This command and everything up to the next script or block will never be run",*notused)) + notused = False + dat = [] + if match.group(2): + dat = re.split('\\s*,\\s*', match.group(2)) + params = self.parameters[ai[4][-1][0]] + if params and len(dat) != len(params): + raise PyMSError('Interpreting','Incorrect amount of parameters (got %s, needed %s)' % (len(dat), len(params)),n,line, warnings=warnings) + if not params and dat: + raise PyMSError('Interpreting','Command requires no parameters, but got %s' % len(dat),n,line, warnings=warnings) + aisize += 1 + if params and dat: + for d,p in zip(dat,params): + if p == self.ai_address: + aisize += 2 + match = re.match('\\A(.+):(.+)\\Z', d) + if match: + cid,label = match.group(1),match.group(2) + if cid in default_ais: + cid = default_ais[cid] + elif len(cid) != 4: + raise PyMSError('Interpreting',"Invalid AI ID '%s' (must be 4 characeters long, or one of the keywords: Protoss, BWProtoss, Terran, BWTerran, Zerg, BWZerg)" % cid,n,line, warnings=warnings) + elif re.match('[\x00:(),]',cid): + raise PyMSError('Interpreting',"Invalid AI ID '%s', it can not contain a null byte, or the characters: , ( ) :" % cid,n,line, warnings=warnings) + if re.match('[\x00:(),]',label): + raise PyMSError('Interpreting',"Invalid label name '%s', it can not contain a null byte, or the characters: , ( ) :" % label,n,line, warnings=warnings) + if cid in totaljumps: + if curlabel: + if label in curlabel: + warnings.append(PyMSWarning('Interpreting',"All loops require at least 1 wait statement. Block '%s' seems to not have one" % label)) + curlabel = [] + if id in scriptids[0]: + a = 'aiscript' + else: + a = 'bwscript' + if cid in scriptids[0]: + b = 'aiscript' + else: + b = 'bwscript' + if a != b: + raise PyMSError('Interpreting',"You can't jump to an external script in '%s.bin' from a script in '%s.bin'" % (b,a),n,line, warnings=warnings) + if not label in totaljumps[cid]: + raise PyMSError('Interpreting',"The AI with ID '%s' has no label '%s'" % (cid,label), warnings=warnings) + tjs = totaljumps[cid][label] + ai[4][-1].append(tjs) + ai[5].append(tjs) + bw = id in scriptids[0] + if not tjs[0] in externaljumps[bw][0]: + externaljumps[bw][0][tjs[0]] = {} + if not tjs[1] in externaljumps[bw][0][tjs[0]]: + externaljumps[bw][0][tjs[0]][tjs[1]] = [] + externaljumps[bw][0][tjs[0]][tjs[1]].append(id) + if not id in externaljumps[bw][1]: + externaljumps[bw][1][id] = [] + if not tjs[0] in externaljumps[bw][1][id]: + externaljumps[bw][1][id].append(tjs[0]) + if not notused and (cid,label) in unused: + unused[(cid,label)] = False + else: + if not cid in findtotaljumps: + findtotaljumps[cid] = odict() + if not label in findtotaljumps[cid]: + findtotaljumps[cid][label] = [] + findtotaljumps[cid][label].append([ai[4][-1],len(ai[4][-1]),ai[5],len(ai[5]),ai[0]]) + ai[4][-1].append(None) + ai[5].append(None) + blocknames.append([cid,label]) + if not cid in findgoto: + findgoto[cid] = odict() + findgoto[cid][label] = [True,len(ai[5])-1] + else: + if curlabel: + if d in curlabel: + warnings.append(PyMSWarning('Interpreting',"All loops require at least 1 wait statement. Block '%s' seems to not have one" % d)) + curlabel = [] + if type(jumps.get(d)) == int: + ai[4][-1].append(jumps[d]) + ai[5].append(jumps[d]) + else: + if not d in jumps: + jumps[d] = [] + jumps[d].append([ai[4][-1],len(ai[4][-1]),ai[5],len(ai[5])]) + ai[4][-1].append(None) + ai[5].append(None) + blocknames.append(d) + findgoto[id][d] = [True,len(ai[5])-1] + if not notused and (id,d) in unused: + unused[(id,d)] = False + curinfo[id][2].append(d) + else: + try: + var = None + da = d + if d.lower() in variables: + for pt in self.typescanbe[p.__doc__.split(' ',1)[0]]: + if pt in variables[d.lower()][0]: + da = variables[d.lower()][1] + break + else: + raise PyMSError('Variable',"Incorrect type on varaible '%s'. Excpected '%s' but got '%s'" % (d.lower(), p.__doc__.split(' ',1)[0], variables[d.lower()][0][0].__doc__.split(' ',1)[0]),n,line, warnings=warnings) + var = PyMSWarning('Variable',"The variable '%s' of type '%s' was set to '%s'" % (d, variables[d.lower()][0][0].__doc__.split(' ',1)[0], variables[d.lower()][1])) + cs = p(da,3) + ai[4][-1].append(cs[1]) + aisize += cs[0] + except PyMSWarning as w: + ai[4][-1].append(w.extra[1]) + aisize += w.extra[0] + w.line = n + 1 + w.code = line + warnings.append(w) + if var: + var.warning += ' when the above warning happened' + warnings.append(var) + except PyMSError as e: + e.line = n + 1 + e.code = line + e.warnings = warnings + if var: + var.warning += ' when the above error happened' + e.warnings.append(var) + raise e + except: + raise PyMSError('Parameter',"Invalid parameter data '%s', looking for type '%s'" % (d,p.__doc__.split(' ',1)[0]),n,line, warnings=warnings) + if cmd != 'wait' and cmd in self.separate: + notused = (n,line) + if cmd.lower() in self.separate: + curlabel = [] + cmdn += 1 + nextinfo = None + continue + match = re.match(r'\\A--\s*(.+)\s*--\\s*(?:\\{(.+)\\})?\\Z', line) + if match: + notused = False + label = match.group(1) + if re.match('[\x00:(),]',label): + raise PyMSError('Interpreting',"Invalid label name '%s', it can not contain a null byte, or the characters: , ( ) :" % label,n,line, warnings=warnings) + if type(jumps.get(label)) == int: + raise PyMSError('Interpreting',"There is already a block with the name '%s'" % label,n,line, warnings=warnings) + curlabel.append(label) + if ai[0] in findtotaljumps and label in findtotaljumps[ai[0]]: + if ai[0] in scriptids[0]: + a = 'aiscript' + else: + a = 'bwscript' + for fj in findtotaljumps[ai[0]][label]: + if fj[4] in scriptids[0]: + b = 'aiscript' + else: + b = 'bwscript' + if a != b: + raise PyMSError('Interpreting',"You can't jump to an external script in '%s.bin' from a script in '%s.bin'" % (a,b),n,line, warnings=warnings) + for a,y,j,x,i in findtotaljumps[id][label]: + a[y] = [id,cmdn] + j[x] = [id,cmdn] + bw = not ai[0] in scriptids[0] + if not id in externaljumps[bw][0]: + externaljumps[bw][0][id] = {} + if not cmdn in externaljumps[bw][0][id]: + externaljumps[bw][0][id][cmdn] = [] + externaljumps[bw][0][id][cmdn].append(i) + if not i in externaljumps[bw][1]: + externaljumps[bw][1][i] = [] + if not id in externaljumps[bw][1][i]: + externaljumps[bw][1][i].append(id) + del findtotaljumps[id][label] + if not findtotaljumps[id]: + del findtotaljumps[id] + if label in jumps: + for a,y,j,x in jumps[label]: + a[y] = cmdn + j[x] = cmdn + jumps[label] = cmdn + else: + jumps[label] = cmdn + if not label in findgoto[id]: + findgoto[id][label] = [False,len(ai[5])] + if not (id,label) in unused: + unused[(id,label)] = True + ai[5].append(cmdn) + totaljumps[id][label] = [id,cmdn] + curinfo[id][1][label] = '' + blocknames.append(label) + if match.group(2): + if '\x00' in match.group(2): + raise PyMSError('Interpreting','Information Comments can not contain null bytes',n,line, warnings=warnings) + curinfo[id][1][label] = match.group(2) + nextinfo = None + else: + nextinfo = [curinfo,id,label] + continue + if line.startswith('{'): + if not nextinfo: + raise PyMSError('Interpreting','An Information Comment must be afer a variable, a script header, or a block label',n,line, warnings=warnings) + match = re.match('\\A\\{(?:(.+)\\})?\\Z', line) + if match.group(1): + if len(nextinfo) == 3: + nextinfo[0][curinfo[1]][1][curinfo[2]] = match.group(1) + else: + nextinfo[0][curinfo[1]][0] = match.group(1) + nextinfo = None + else: + multiline = True + lastmulti = [n,line] + continue + raise PyMSError('Interpreting','Invalid syntax, unknown line format',n,line, warnings=warnings) + if multiline: + raise PyMSError('Interpreting',"There is an unclosed Information Comment in the AI with ID '%s'" % ai[0],lastmulti[0],lastmulti[1], warnings=warnings) + if ai: + if not ai[4]: + raise PyMSError('Interpreting',"AI with ID '%s' has no commands" % ai[0], warnings=warnings) + if None in ai[5]: + dat = blocknames[ai[5].index(None)] + if type(dat) == str: + raise PyMSError('Interpreting',"There is no block with name '%s' in AI with ID '%s'" % (dat,ai[0]), warnings=warnings) + if ai[0] in findtotaljumps and findtotaljumps[ai[0]]: + n = findtotaljumps[ai[0]].keys()[0] + raise PyMSError('Interpreting',"There is no block with name '%s' in AI with ID '%s'" % (n,ai[0]), warnings=warnings) + if ai[4][-1][0] not in self.script_endings: + warnings.append(PyMSWarning('Interpreting', "The AI with ID '%s' does not end with a stop or definite loop. To ensure your script doesn't run into the next script, it must end with one of: goto(), stop(), debug(), time_jump(), or race_jump()" % ai[0], level=1)) + if ai[0] in findgoto: + for l,f in findgoto[ai[0]].items(): + if f[0]: + del findgoto[ai[0]][l] + if not findgoto[ai[0]]: + del findgoto[ai[0]] + if ai[1]: + ais[ai[0]] = ai[1:] + aisizes[ai[0]] = aisize + else: + ais[ai[0]] = ai[1:4] + [[],[]] + bwais[ai[0]] = [1] + ai[4:] + bwsizes[ai[0]] = aisize + for l in curinfo[ai[0]][2]: + if not l in curinfo[ai[0]][1]: + curinfo[ai[0]][1][l] = '' + s = 2+sum(aisizes.values()) + if s > 65535: + raise PyMSError('Interpreting',"There is not enough room in your aiscript.bin to compile these changes. The current file is %sB out of the max 65535B, these changes would make the file %sB." % (2+sum(self.aisizes.values()),s)) + s = 2+sum(bwsizes.values()) + if s > 65535: + raise PyMSError('Interpreting',"There is not enough room in your bwscript.bin to compile these changes. The current file is %sB out of the max 65535B, these changes would make the file %sB." % (2+sum(self.bwsizes.values()),s)) + if findtotaljumps: + i = findtotaljumps.peek() + l = i[1].peek() + raise PyMSError('Interpreting',"The external jump '%s:%s' in AI script '%s' jumps to an AI script that was not found while interpreting (you must include the scripts for all external jumps)" % (i[0],l[0],l[1][0][4]), warnings=warnings) + if findgoto: + remove = [{},{}] + for i in findgoto.items(): + if not i[0] in remove: + remove[i[0] not in aiinfo][i[0]] = [] + for l,f in i[1].items(): + if not f[0]: + warnings.append(PyMSWarning('Interpeting',"The label '%s' in AI script '%s' is unused, label is discarded" % (l,i[0]))) + remove[i[0] not in aiinfo][i[0]].append(f[1]) + if i[0] in aiinfo: + aiinfo[i[0]][1].remove(l) + else: + bwinfo[i[0]][1].remove(l) + for b,r in enumerate(remove): + for i in r.iterkeys(): + if r[i]: + r[i].sort() + n = 0 + for x in r[i]: + if b: + del bwais[i][1][x-n] + else: + del ais[i][4][x-n] + n += 1 + for id,u in unused.items(): + if u and (not id[0] in findgoto or not id[1] in findgoto[id[0]]): + warnings.append(PyMSWarning('Interpeting',"The label '%s' in AI script '%s' is only referenced by commands that cannot be reached and is therefore unused" % (id[1],id[0]))) + if self.ais: + for id,dat in ais.items(): + self.ais[id] = dat + else: + self.ais = ais + self.aisizes = aisizes + for a,b in ((0,0),(0,1),(1,0),(1,1)): + if self.externaljumps[a][b]: + for id,dat in externaljumps[a][b].items(): + self.externaljumps[a][b][id] = dat + else: + self.externaljumps[a][b] = externaljumps[a][b] + if self.bwscript.ais: + for id,dat in bwais.items(): + self.bwscript.ais[id] = dat + else: + self.bwscript.ais = bwais + self.bwsizes = bwsizes + if extra: + self.varinfo = varinfo + self.aiinfo = aiinfo + self.bwscript.aiinfo = bwinfo + return warnings + + def reference(self, file): + file.write('#----------------------------------------------------\n# Parameter Types:\n') + done = [] + for p in self.parameters: + if p: + for t in p: + if t: + n = t.__doc__.split(' ',1)[0] + if not n in done: + file.write('# %s\n' % t.__doc__) + done.append(n) + file.write('#\n# Commands:\n') + for c,ps in zip(self.short_labels,self.parameters): + if c: + file.write('# %s(' % c) + if ps: + comma = False + for p in ps: + if comma: + file.write(', ') + else: + comma = True + file.write(p.__doc__.split(' ',1)[0]) + file.write(')\n') + # file.write('#\n# Descriptive Commands:\n') + # for c,ps in zip(self.labels,self.parameters): + # if c: + # file.write('# %s(' % c) + # if ps: + # comma = False + # for p in ps: + # if comma: + # file.write(', ') + # else: + # comma = True + # file.write(p.__doc__.split(' ',1)[0]) + # file.write(')\n') + file.write('#----------------------------------------------------\n\n') + + def decompile(self, file, defs=None, ref=False, shortlabel=True, scripts=None): + if isstr(file): + try: + f = open(file, 'w') + except: + raise PyMSError('Decompile',"Could not load file '%s'" % file, warnings=warnings) + else: + f = file + if scripts == None: + scripts = self.ais.keys() + if shortlabel: + labels = self.short_labels + else: + labels = self.labels + if ref: + self.reference(f) + warnings = [] + extjumps = {} + if defs: + variables = {} + if isstr(defs): + defs = defs.split(',') + loaded = [] + for dname in defs: + try: + deffile = os.path.join(os.path.dirname(file),dname) + except: + deffile = os.path.abspath(dname) + if dname in loaded: + continue + try: + d = open(deffile,'r') + ddata = d.readlines() + d.close() + except: + raise PyMSError('External Definition',"External definition file '%s' was not found" % dname, warnings=warnings) + for n,l in enumerate(ddata): + if len(l) > 1: + line = l.strip().split('#',1)[0] + if line: + match = re.match('\\A(\\S+)\\s+(.+)\\s+=\\s+(.+?)(?:\\s*\\{(.+)\\})?\\Z', line) + if match: + t,name,dat,vinfo = match.groups() + if re.match('[\x00,(){}]',name): + raise PyMSError('External Definition',"The variable name '%s' in external definition file '%s' is invalid, it must not contain a null, or the characters: , ( ) { }" % (name,dname),n,line, warnings=warnings) + t = t.lower() + if t == 'label' or t not in self.types: + raise PyMSError('External Definition',"Invalid variable type '%s' for variable '%s' in external definition file '%s', consult the reference" % (t, name, dname),n,line, warnings=warnings) + if name.lower() in variables: + raise PyMSError('External Definition',"The variable name '%s' in external definition file '%s' is already in use" % (name, dname),n,line, warnings=warnings) + if vinfo: + warnings.append(PyMSWarning('External Definition',"External definition files do not support Information Comments, information is discarded",n,line)) + try: + v = self.types[t][0](dat,3)[1] + except PyMSWarning as w: + w.line = n + w.code = line + warnings.append(w) + v = w.extra[1] + except PyMSError as e: + e.line = n + 1 + e.code = line + e.warnings = warnings + raise e + except: + raise PyMSError('External Definition',"Invalid value '%s' for variable '%s' of type '%s' in external definition file '%s'" % (dat, name, t, dname),n,line, warnings=warnings) + variables[name.lower()] = [self.types[t],dat] + self.varinfo[name] = [types.index(t),v,None] + else: + raise PyMSError('External Definition','Invalid syntax, unknown line format',n,l, warnings=warnings) + f.write('extdef %s\n' % dname) + loaded.append(dname) + f.write('\n') + values = {} + for name,dat in self.varinfo.items(): + vtype = types[dat[0]] + if dat[2] != None: + f.write('%s %s = %s' % (vtype,name,dat[1])) + if dat[2] and not '\n' in dat[2]: + f.write(' {%s}' % dat[2]) + t = self.types[vtype][0](dat[1],1)[1] + if t != str(dat[1]): + f.write(' # Translated Value: %s' % t) + f.write('\n') + if dat[2] and '\n' in dat[2]: + f.write('{\n%s\n}\n' % dat[2]) + if not vtype in values: + values[vtype] = {} + values[vtype][dat[1]] = name + f.write('\n') + for id in scripts: + if id not in self.ais: + raise PyMSError('Decompile',"There is no AI Script with ID '%s'" % id, warnings=warnings) + loc,string,flags,ai,jumps = self.ais[id] + if loc: + cmdn = 0 + if id in extjumps: + j = len(extjumps[id]) + jump = dict(extjumps[id]) + else: + j = 0 + jump = {} + f.write('# stat_txt.tbl entry %s: %s\n%s(%s, %s, aiscript):' % (string,TBL.decompile_string(self.tbl.strings[string]),id,string,convflags(flags))) + labelnames = None + if id in self.aiinfo: + labelnames = self.aiinfo[id][1].copy() + namenum = 0 + gotonum = 0 + i = self.aiinfo[id][0] + if '\n' in i: + f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) + elif i: + f.write(' {%s}' % i) + f.write('\n') + for cmd in ai: + if cmdn in jump: + if cmdn: + f.write('\n') + if id in self.externaljumps[0][0] and cmdn in self.externaljumps[0][0][id]: + s = '' + if len(self.externaljumps[0][0][id][cmdn]) > 1: + s = 's' + f.write('\t\t# Referenced externally by script%s: %s\n' % (s, ', '.join(self.externaljumps[0][0][id][cmdn]))) + f.write('\t\t--%s--' % jump[cmdn]) + if labelnames and jump[cmdn] in labelnames: + i = labelnames.get(jump[cmdn],'') + if '\n' in i: + f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) + elif i: + f.write(' {%s}' % i) + f.write('\n') + elif cmdn in jumps: + if labelnames: + jump[cmdn] = labelnames.getkey(namenum) + else: + jump[cmdn] = '%s %04d' % (id,j) + if cmdn: + f.write('\n') + if id in self.externaljumps[0][0] and cmdn in self.externaljumps[0][0][id]: + s = '' + if len(self.externaljumps[0][0][id][cmdn]) > 1: + s = 's' + f.write('\t\t# Referenced externally by script%s: %s\n' % (s, ', '.join(self.externaljumps[0][0][id][cmdn]))) + f.write('\t\t--%s--' % jump[cmdn]) + if labelnames and jump[cmdn] in labelnames: + i = labelnames.get(jump[cmdn],'') + if '\n' in i: + f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) + elif i: + f.write(' {%s}' % i) + f.write('\n') + j += 1 + f.write('\t%s(' % labels[cmd[0]]) + if len(cmd) > 1: + comma = False + for p,t in zip(cmd[1:],self.parameters[cmd[0]]): + if comma: + f.write(', ') + else: + comma = True + temp = t(p,1) + if t == self.ai_address: + if type(temp[1]) == list: + if temp[1][0] in extjumps and temp[1][1] in extjumps[temp[1][0]]: + f.write('%s:%s' % (temp[1][0],extjumps[temp[1][0]][temp[1][1]])) + else: + if not temp[1][0] in extjumps: + extjumps[temp[1][0]] = {} + if labelnames: + t = self.aiinfo[id][2][gotonum] + n = t.split(':',1) + f.write(t) + extjumps[n[0]][temp[1][1]] = n[1] + else: + n = '%s %04d' % (temp[1][0],len(extjumps[temp[1][0]])) + f.write('%s:%s' % (temp[1][0],n)) + extjumps[temp[1][0]][temp[1][1]] = n + elif temp[1] in jump: + f.write(jump[temp[1]]) + else: + if labelnames: + jump[temp[1]] = self.aiinfo[id][2][gotonum] + else: + jump[temp[1]] = '%s %04d' % (id,j) + f.write(jump[temp[1]]) + j += 1 + if labelnames: + gotonum += 1 + else: + for vt in self.typescanbe[t.__doc__.split(' ',1)[0]]: + vtype = vt.__doc__.split(' ',1)[0] + if vtype in values and p in values[vtype]: + f.write(values[vtype][p]) + break + else: + f.write(temp[1]) + f.write(')\n') + if labels[cmd[0]].lower() in self.separate: + f.write('\n') + cmdn += 1 + f.write('\n') + else: + f.write('# stat_txt.tbl entry %s: %s\n%s(%s, %s, bwscript):' % (string,TBL.decompile_string(self.tbl.strings[string]),id,string,convflags(flags))) + labelnames = None + w = self.bwscript.decompile(f, (self.externaljumps,extjumps), values, shortlabel, [id]) + if w: + warnings.extend(w) + f.close() + return warnings + + def compile(self, file, bwscript=None, extra=False): + if isstr(file): + try: + f = open(file, 'wb') + except: + raise PyMSError('Compile',"Could not load file '%s'" % file) + else: + f = file + warnings = [] + ais = '' + table = '' + offset = 4 + totaloffsets = {} + for id in self.ais.keys(): + loc,string,flags,ai,jumps = self.ais[id] + if loc: + cmdn = 0 + totaloffsets[id] = {} + for cmd in ai: + if cmdn in jumps: + totaloffsets[id][cmdn] = offset + if self.parameters[cmd[0]]: + for p,t in zip(cmd[1:],self.parameters[cmd[0]]): + try: + if t == self.ai_address: + offset += t(0,2)[0] + else: + offset += t(p,2)[0] + except PyMSWarning as e: + if not warnings: + warnings.append(e) + offset += e.extra[0] + cmdn += 1 + offset += 1 + offset = 4 + for id in self.ais.keys(): + loc,string,flags,ai,jumps = self.ais[id] + if loc: + table += struct.pack('<4s3L', id, offset, string+1, flags) + for cmd in ai: + ais += chr(cmd[0]) + if self.parameters[cmd[0]]: + for p,t in zip(cmd[1:],self.parameters[cmd[0]]): + try: + if t == self.ai_address: + if type(p) == list: + d = t(totaloffsets[p[0]][p[1]],2) + else: + d = t(totaloffsets[id][p],2) + else: + d = t(p,2) + except PyMSWarning as e: + if not warnings: + warnings.append(e) + offset += e.extra[0] + ais += e.extra[1] + else: + ais += d[1] + offset += d[0] + offset += 1 + else: + table += struct.pack('<4s3L', id, 0, string+1, flags) + f.write('%s%s%s\x00\x00\x00\x00' % (struct.pack(' len(self.labels): + raise PyMSError('Load','Invalid command, could possibly be a corrrupt bwscript.bin') + ai = [cmd] + if self.parameters[cmd]: + for p in self.parameters[cmd]: + d = p(curdata[curoffset:]) + if p == self.ai_address: + if d[1] < loc: + if d[1] not in totaloffsets: + raise PyMSError('Load','Incorrect jump location, could possibly be a corrupt bwscript.bin') + tos = totaloffsets[d[1]] + ais[id][2].append(tos) + ai.append(tos) + if not tos[0] in externaljumps[1][0]: + externaljumps[1][0][tos[0]] = {} + if not len(cmdoffsets) in externaljumps[1][0][tos[0]]: + externaljumps[1][0][tos[0]][tos[1]] = [] + externaljumps[1][0][tos[0]][tos[1]].append(id) + if not id in externaljumps[1][1]: + externaljumps[1][1][id] = [] + if not tos[0] in externaljumps[1][1][id]: + externaljumps[1][1][id].append(tos[0]) + elif d[1] >= offsets[offsets.index(loc)+1]: + if not d[1] in findtotaloffsets: + findtotaloffsets[d[1]] = [] + findtotaloffsets[d[1]].append([id,len(ais[id][2]),ai,len(ai)]) + ais[id][2].append(None) + ai.append(None) + elif d[1] - loc in cmdoffsets: + pos = cmdoffsets.index(d[1] - loc) + ais[id][2].append(pos) + ai.append(pos) + else: + if not d[1] - loc in findoffset: + findoffset[d[1] - loc] = [] + findoffset[d[1] - loc].append([len(ais[id][2]),ai,len(ai)]) + ais[id][2].append(None) + ai.append(None) + else: + ai.append(d[1]) + curoffset += d[0] + ais[id][1].append(ai) + aisizes[id] = curoffset + if None in ais[id][2]: + checknones.append(id) + for c in checknones: + if None in ais[id][2]: + raise PyMSError('Load','Incorrect jump location, could possibly be a corrupt bwscript.bin') + if offset < len(data): + def getstr(dat,o): + i = dat[o:].index('\x00') + return [dat[o:o+i],o+i+1] + try: + info = decompress(data[offset:]) + offset = 0 + while info[offset] != '\x00': + id = info[offset:offset+4] + offset += 4 + desc,offset = getstr(info,offset) + aiinfo[id] = [desc,odict(),[]] + while info[offset] != '\x00': + label,offset = getstr(info,offset) + desc,offset = getstr(info,offset) + aiinfo[id][1][label] = desc + offset += 1 + p = int(floor(log(len(aiinfo),256))) + s,n = '<' + ['B','H','L','L'][p],[1,2,4,4][p] + while info[offset] != '\x00': + aiinfo[id][2].append(aiinfo[id][1].getkey(struct.unpack(s,info[offset:offset+n])[0]-1)) + offset += n + offset += 1 + except: + warnings.append(PyMSWarning('Load','Unsupported extra script information section in bwscript.bin, could possibly be corrupt. Continuing without extra script information')) + self.ais = ais + self.aisizes = aisizes + self.externaljumps = externaljumps + self.aiinfo = aiinfo + return warnings + except PyMSError: + raise + except: + raise PyMSError('Load',"Unsupported bwscript.bin '%s', could possibly be invalid or corrupt" % file,exception=sys.exc_info()) + + def decompile(self, filename, extjumps, values, shortlabel=True, scripts=None): + close = True + if isstr(filename): + try: + f = open(filename, 'w') + except: + raise PyMSError('Decompile',"Could not load file '%s'" % filename) + else: + f = filename + if scripts == None: + scripts = self.ais.keys() + if shortlabel: + labels = self.short_labels + else: + labels = self.labels + externaljumps = extjumps[0] + extjumps = extjumps[1] + warnings = [] + for id in scripts: + if not id in self.ais: + raise PyMSError('Decompile',"There is no AI Script with ID '%s'" % id) + loc,ai,jumps = self.ais[id] + cmdn = 0 + if id in extjumps: + j = len(extjumps[id]) + jump = dict(extjumps[id]) + else: + j = 0 + jump = {} + labelnames = None + if id in self.aiinfo: + labelnames = self.aiinfo[id][1].copy() + namenum = 0 + gotonum = 0 + i = self.aiinfo[id][0] + if '\n' in i: + f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) + elif i: + f.write(' {%s}' % i) + f.write('\n') + for cmd in ai: + if cmdn in jump: + if cmdn: + f.write('\n') + if id in self.externaljumps[1][0] and cmdn in self.externaljumps[1][0][id]: + s = '' + if len(self.externaljumps[1][0][id][cmdn]) > 1: + s = 's' + f.write('\t\t# Referenced externally by script%s: %s\n' % (s, ', '.join(self.externaljumps[1][0][id][cmdn]))) + f.write('\t\t--%s--' % jump[cmdn]) + if labelnames: + i = labelnames.get(jump[cmdn],'') + if '\n' in i: + f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) + elif i: + f.write(' {%s}' % i) + namenum += 1 + f.write('\n') + elif cmdn in jumps: + if labelnames: + jump[cmdn] = labelnames.getkey(namenum) + namenum += 1 + else: + jump[cmdn] = '%s %04d' % (id,j) + if cmdn: + f.write('\n') + if id in self.externaljumps[1][0] and cmdn in self.externaljumps[1][0][id]: + s = '' + if len(self.externaljumps[1][0][id][cmdn]) > 1: + s = 's' + f.write('\t\t# Referenced externally by script%s: %s\n' % (s, ', '.join(self.externaljumps[1][0][id][cmdn]))) + f.write('\t\t--%s--' % jump[cmdn]) + if labelnames and jump[cmdn] in labelnames: + i = labelnames.get(jump[cmdn],'') + if '\n' in i: + f.write('\n\t{\n\t%s\n\t}' % i.replace('\n','\n\t')) + elif i: + f.write(' {%s}' % i) + f.write('\n') + j += 1 + f.write('\t%s(' % labels[cmd[0]]) + if len(cmd) > 1: + comma = False + for p,t in zip(cmd[1:],self.parameters[cmd[0]]): + if comma: + f.write(', ') + else: + comma = True + temp = t(p,1) + if t == self.ai_address: + if type(temp[1]) == list: + if temp[1][0] in extjumps and temp[1][1] in extjumps[temp[1][0]]: + f.write('%s:%s' % (temp[1][0],extjumps[temp[1][0]][temp[1][1]])) + else: + if not temp[1][0] in extjumps: + extjumps[temp[1][0]] = {} + if labelnames: + t = self.aiinfo[id][2][gotonum] + n = t.split(':',1) + f.write(t) + extjumps[n[0]][temp[1][1]] = n[1] + else: + n = '%s %04d' % (temp[1][0],len(extjumps[temp[1][0]])) + f.write('%s:%s' % (temp[1][0],n)) + extjumps[temp[1][0]][temp[1][1]] = n + elif temp[1] in jump: + f.write(jump[temp[1]]) + else: + if labelnames: + jump[temp[1]] = self.aiinfo[id][2][gotonum] + else: + jump[temp[1]] = '%s %04d' % (id,j) + f.write(jump[temp[1]]) + j += 1 + if labelnames: + gotonum += 1 + else: + for vt in self.typescanbe[t.__doc__.split(' ',1)[0]]: + vtype = vt.__doc__.split(' ',1)[0] + if vtype in values and p in values[vtype]: + f.write(values[vtype][p]) + break + else: + f.write(temp[1]) + f.write(')\n') + cmdn += 1 + f.write('\n') + if close: + f.close() + return warnings + + def compile(self, file, extra=False): + if isstr(file): + try: + f = open(file, 'wb') + except: + raise PyMSError('Compile',"Could not load file '%s'" % file) + else: + f = file + ais = '' + table = '' + offset = 4 + totaloffsets = {} + for id in self.ais.keys(): + loc,ai,jumps = self.ais[id] + if loc: + cmdn = 0 + totaloffsets[id] = {} + for cmd in ai: + totaloffsets[id][cmdn] = offset + if self.parameters[cmd[0]]: + for p,t in zip(cmd[1:],self.parameters[cmd[0]]): + try: + if t == self.ai_address: + offset += t(0,1)[0] + else: + offset += t(p,1)[0] + except PyMSWarning as e: + if not warnings: + warnings.append(e) + cmdn += 1 + offset += 1 + offset = 4 + for id in self.ais.keys(): + loc,ai,jumps = self.ais[id] + table += struct.pack('<4sL', id, offset) + for cmd in ai: + ais += chr(cmd[0]) + if self.parameters[cmd[0]]: + for p,t in zip(cmd[1:],self.parameters[cmd[0]]): + try: + if t == self.ai_address: + if type(p) == list: + d = t(totaloffsets[p[0]][p[1]],2) + else: + d = t(totaloffsets[id][p],2) + else: + d = t(p,2) + ais += d[1] + offset += d[0] + except PyMSWarning as e: + if not warnings: + warnings.append(e) + offset += 1 + f.write('%s%s%s\x00\x00\x00\x00' % (struct.pack('>8 - - def load_file(self, file): - if isstr(file): - try: - f = open(file,'rb') - data = f.read() - f.close() - except: - raise PyMSError('Load',"Could not load %s file '%s'" % (self.datname, file)) - else: - data = file.read() - if len(data) != self.filesize: - raise PyMSError('Load',"'%s' is an invalid %s file (the file size must be %s, but got a file with size %s)" % (file,self.datname,self.filesize,len(data))) - try: - entries = [] - offset = 0 - array = 0 - arraylen = 0 - for x,format in enumerate(self.format): - bytes = format[1] - if len(format) == 3: - arraylen = array = format[2] - vals = self.count - values = [] - if format[0]: - vals = format[0][1] - format[0][0] - values.extend([0]*format[0][0]) - for y in range(vals): - o = offset+y*arraylen*bytes+(arraylen-array)*bytes - values.append(struct.unpack('<%s' % ['', 'B','H','','L'][bytes], data[o:o+bytes])[0]) - if not array: - offset += bytes - if format[0]: - values.extend([0]*(self.count - format[0][1])) - if array: - array -= 1 - if not array: - offset += arraylen * bytes * vals - arraylen = 0 - # elif format[0]: - # vals = format[0][1] - format[0][0] - # values = [0]*format[0][0] + list(struct.unpack('<%s%s' % (vals, ['', 'B','H','','L'][bytes]), data[offset:offset+bytes*vals])) + [0]*(self.count - format[0][1]) - # offset += bytes * vals - # else: - # values = struct.unpack('<%s%s' % (self.count, ['','B','H','','L'][bytes]), data[offset:offset+bytes*self.count]) - # offset += bytes * self.count - for id,value in enumerate(values): - if x == 0: - entries.append([value]) - elif self.labels[x] in self.special: - entries[id].append(self.special[self.labels[x]](value)) - # elif self.datname == 'units.dat' and x == 8: #HitPoints - # entries[id].append(value>>8) - # elif self.datname == 'weapons.dat' and x in [4,5]: - # entries[id].append(value>>4) - else: - entries[id].append(value) - self.entries = entries - except: - raise PyMSError('Load','Unsupported %s file, could possibly be corrupt' % self.datname) - - def dat_value(self, file, value): - if value == 65535: - return '%s.dat entry: None' - return '%s.dat entry: %s' % (file, DATA_CACHE[file + '.txt'][value]) - - def info_value(self, file, value): - if file == 'ElevationLevels': - s = 'Unit elevation level: ' - elif file == 'UnitSize': - s = 'Unit size: ' - elif file == 'Rightclick': - s = 'Right-click order action: ' - return s + DATA_CACHE[file + '.txt'][value] - - def time_value(self, _, value): - return 'Length in seconds: %s' % (value / 24) - - def stattxt_value(self, type, value): - if type == 'Sublabel': - value += 1301 - return '%s in stat_txt.tbl, item %s: %s' % (type, value, TBL.decompile_string(self.tbl.strings[value])) - - def interpret(self, file): - if len(self.entries) != self.count: - raise PyMSError('Interpreting',"No default %s file was loaded as a base" % self.datname) - try: - f = open(file,'r') - data = f.readlines() - f.close() - except: - raise PyMSError('Interpreting',"Could not load file '%s'" % file) - entries = {} - entrydata = False - curentry = -1 - for n,l in enumerate(data): - if len(l) > 1: - ln = l.strip().split('#',1)[0] - line = re.split('\s+', ln) - if entrydata: - if line[0] == self.header + 'ID': - raise PyMSError('Interpreting',"Unexpected line, expected a value",n,ln) - if line[0] not in self.labels: - raise PyMSError('Interpreting',"'%s' is an invalid value label" % line[0],n,ln) - label = self.labels.index(line[0]) - value = None - if line[0] in self.flaglens: - if len(line[1]) != self.flaglens[line[0]]: - raise PyMSError('Interpreting',"Incorrect amount of flags (expected %s, got %s)" % (self.flaglens[line[0]],len(line[1])),n,ln) - if re.match('[^01]',line[1]): - raise PyMSError('Interpreting',"'%s' is an invalid set of flags" % line[1],n,ln) - value = sum(int(x)*(2**n) for n,x in enumerate(reversed(line[1]))) - if value == None: - try: - value = int(line[1]) - if value < 0 or value > 256 ** self.format[label][1]: - raise - except: - raise PyMSError('Interpreting',"'%s' is an invalid value (must be in range 0 to %s)" % (line[1],256**self.format[label][1]),n,ln) - if self.datname == 'units.dat' and line[0] == 'SightRange' and value > 11: - raise PyMSError('Interpreting',"SightRange can not be more then 11",n,ln) - entries[curentry][label] = value - if not None in entries[curentry]: - entrydata = False - else: - if line[0] != self.header + 'ID' or not line[1].endswith(':'): - raise PyMSError('Interpreting',"Unexpected line, expected a new %sID header" % self.header,n,ln) - try: - id = int(line[1][:-1]) - if id < 0 or id >= self.count: - raise - except: - raise PyMSError('Interpreting',"Invalid %sID value (must be in the range 0 to %s)" % (self.header,self.count),n,ln) - entries[id] = [None]*len(self.format) - for n,format in enumerate(self.format): - if format[0] and not id in range(*format[0]): - entries[id][n] = 0 - curentry = id - entrydata = True - if None in entries[curentry]: - raise PyMSError('Interpreting',"Entry '%s' is missing a value for %s" % (curentry,self.labels[entries.curentry.index(None)]),n,ln) - for id,entry in entries.iteritems(): - self.entries[id] = entry - return entries.keys() - - def decompile(self, file, ref=False, ids=None): - try: - f = open(file, 'w') - except: - raise PyMSError('Interpreting',"Could not load file '%s'" % file) - if ref: - f.write('#----------------------------------------------------') - for file,name in DATA_REFERENCE.iteritems(): - f.write('\n# %s:' % name) - for n,value in enumerate(DATA_CACHE[file]): - pad = ' ' * (3 - len(str(n))) - f.write('\n# %s%s = %s' % (pad,n,value)) - f.write('\n#') - f.write('----------------------------------------------------\n\n') - if ids == None: - ids = range(self.count) - for id in ids: - f.write('%sID %s:%s# %s name: %s\n' % (self.header, id, ' ' * (12 + self.longlabel - len(self.header) - len(str(id))), self.header, DATA_CACHE[self.idfile][id])) - for format,data,label,value in zip(self.format,self.data,self.labels,self.entries[id]): - if not format[0] or id in range(*format[0]): - if label in self.flaglens: - v = ''.join(reversed([str(value/(2**n)%2) for n in range(self.flaglens[label])])) - else: - v = value - f.write(' %s%s%s' % (label, ' ' * (self.longlabel + 1 - len(label)), v)) - if data[0][0]: - f.write('%s# %s' % (' ' * (11 - len(str(v))), data[0][0](data[0][1], v))) - f.write('\n') - f.write('\n') - f.close() - - def compile(self, file): - if len(self.entries) != self.count: - raise - if isstr(file): - try: - f = open(file, 'wb') - except: - raise PyMSError('Compile',"Could not load file '%s'" % file) - else: - f = file - data = [] - for id,entry in enumerate(self.entries): - if len(entry) < len(self.format): - raise - array = 0 - d,fd = 0,0 - for x,value in enumerate(entry): - if array: - d += 1 - fd += 1 - format = self.format[x-fd] - if x-d == len(data): - data.append([]) - if not array: - if len(format) == 3: - array = format[2] - 1 - else: - array -= 1 - if array == 0: - fd = 0 - if format[0] and not id in range(*format[0]): - continue - data[x-d].append(value) - l = list(self.labels) - d = 0 - for n,values in enumerate(data): - format,label = self.format[n+d],self.labels[n+d] - l.remove(label) - if len(format) == 3: - d += format[2]-1 - if label in self.special: - for value in values: - f.write(self.special[label](value,True)) - else: - f.write(struct.pack('<%s%s' % (len(values), ['', 'B','H','','L'][format[1]]), *values)) - f.close() - -class WeaponsDAT(UnitsDAT): - format = [ - [[],2], - [[],4], - [[],1], - [[],2], - [[],4], - [[],4], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],2], - [[],2], - [[],2], - [[],2], - [[],2], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],2], - [[],2] - ] - labels = [ - 'Label', - 'Graphics', - 'Unused', - 'TargetFlags', - 'MinimumRange', - 'MaximumRange', - 'DamageUpgrade', - 'WeaponType', - 'WeaponBehavior', - 'RemoveAfter', - 'ExplosionType', - 'InnerSplashRange', - 'MediumSplashRange', - 'OuterSplashRange', - 'DamageAmount', - 'DamageBonus', - 'WeaponCooldown', - 'DamageFactor', - 'AttackAngle', - 'LaunchSpin', - 'ForwardOffset', - 'UpwardOffset', - 'TargetErrorMessage', - 'Icon' - ] - flaglens = {'TargetFlags':9} - longlabel = 18 - datname = 'weapons.dat' - header = 'Weapon' - idfile = 'Weapons.txt' - filesize = 5460 - count = 130 - - def __init__(self, stat_txt=None): - UnitsDAT.__init__(self, stat_txt) - self.data = [ - [[self.stattxt_value,'Weapon Label'],''], - [[self.dat_value,'Flingy'],''], - [[self.info_value,'Techdata'],''], - [[None,''],'Air,Ground,Mechanical,Organic,non-Building,non-Robotic,Terrain,Organic or Mechanical,Own'], - [[None,''],''], - [[None,''],''], - [[self.dat_value,'Upgrades'],''], - [[self.info_value,'DamTypes'],''], - [[self.info_value,'Behaviours'],''], - [[None,''],''], - [[self.info_value,'Explosions'],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[self.stattxt_value,'Targetting Error Message'],''], - [[self.info_value,'Icons'],''] - ] - self.special = { - 'MinimumRange':self.range, - 'MaximumRange':self.range, - } - - def info_value(self, file, value): - if file == 'DamTypes': - s = 'Damage Type: ' - elif file == 'Behaviours': - s = 'Projectile Behaviour: ' - elif file == 'Explosions': - s = 'Explosion Type: ' - elif file == 'Icons': - s = 'Weapon Icon: ' - elif file == 'Techdata': - s = 'Has no effect. Assumed value: ' - return s + DATA_CACHE[file + '.txt'][value] - - def range(self, value, save=False): - if save: - return struct.pack('>4 - -class FlingyDAT(UnitsDAT): - format = [ - [[],2], - [[],4], - [[],2], - [[],4], - [[],1], - [[],1], - [[],1] - ] - labels = [ - 'Sprite', - 'Speed', - 'Acceleration', - 'HaltDistance', - 'TurnRadius', - 'Unused', - 'MovementControl' - ] - longlabel = 15 - datname = 'flingy.dat' - header = 'Flingy' - idfile = 'Flingy.txt' - filesize = 3135 - count = 209 - - def __init__(self, stat_txt=None): - UnitsDAT.__init__(self, stat_txt) - self.data = [ - [[self.dat_value, 'Sprites'],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[self.info_value, 'FlingyControl'],''] - ] - self.special = {} - - def info_value(self, file, value): - return 'Flingy Control: ' + DATA_CACHE[file + '.txt'][value] - -class SpritesDAT(UnitsDAT): - format = [ - [[],2], - [[130,517],1], - [[],1], - [[],1], - [[130,517],1], - [[130,517],1] - ] - labels = [ - 'ImageFile', - 'HealthBar', - 'Unknown', - 'IsVisible', - 'SelectionCircleImage', - 'SelectionCircleOffset' - ] - longlabel = 21 - datname = 'sprites.dat' - header = 'Sprite' - idfile = 'Sprites.txt' - filesize = 3229 - count = 517 - - def __init__(self, stat_txt=None): - UnitsDAT.__init__(self, stat_txt) - self.data = [ - [[self.dat_value,'Images'],''], - [[self.hpbar_value,''],''], - [[None,''],''], - [[None,''],''], - [[self.info_value, 'SelCircleSize'],''], - [[None,''],''] - ] - self.special = {} - - def hpbar_value(self, _, value): - return 'Health Bar Boxes: %s' % (int((value -1) / 3)) - - def info_value(self, file, value): - return 'Selection Circle Size: ' + DATA_CACHE[file + '.txt'][value] - -class ImagesDAT(UnitsDAT): - format = [ - [[],4], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],4], - [[],4], - [[],4], - [[],4], - [[],4], - [[],4], - [[],4] - ] - labels = [ - 'GRPFile', - 'GfxTurns', - 'Clickable', - 'UseFullIscript', - 'DrawIfCloaked', - 'DrawFunction', - 'Remapping', - 'IscriptID', - 'ShieldOverlay', - 'AttackOverlay', - 'DamageOverlay', - 'SpecialOverlay', - 'LandingDustOverlay', - 'LiftOffDustOverlay' - ] - longlabel = 18 - datname = 'images.dat' - header = 'Image' - idfile = 'Images.txt' - filesize = 37962 - count = 999 - - def __init__(self, grps=None): - if grps == None: - grps = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'images.tbl') - UnitsDAT.__init__(self, grps) - self.data = [ - [[self.stattxt_value,'GRP File Path: '],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[self.info_value,'DrawList'],''], - [[self.info_value,'Remapping'],''], - [[self.info_value,'IscriptIDList'],''], - [[self.stattxt_value,'Shield Overlay File Path'],''], - [[self.stattxt_value,'Attack Overlay File Path'],''], - [[self.stattxt_value,'Damage Overlay File Path'],''], - [[self.stattxt_value,'Special Overlay File Path'],''], - [[self.stattxt_value,'Landing Dust Overlay File Path'],''], - [[self.stattxt_value,'Lift-Off Dust Overlay File Path'],''] - ] - self.special = {} - - def stattxt_value(self, type, value): - if value == 0: - return '%s: None' % type - return '%s in images.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) - - def info_value(self, file, value): - if file == 'DrawList': - s = 'Drawing Function: ' - elif file == 'Remapping': - s = 'Additional Remapping Palette: ' - elif file == 'IscriptIDList': - s = 'IScript Name: ' - return s + DATA_CACHE[file + '.txt'][value] - -class UpgradesDAT(UnitsDAT): - format = [ - [[],2], - [[],2], - [[],2], - [[],2], - [[],2], - [[],2], - [[],2], - [[],2], - [[],2], - [[],1], - [[],1], - [[],1] - ] - labels = [ - 'MineralCostBase', - 'MineralCostFactor', - 'VespeneCostBase', - 'VespeneCostFactor', - 'ResearchTimeBase', - 'ResearchTimeFactor', - 'Unknown', - 'Icon', - 'Label', - 'Race', - 'MaxRepeats', - 'BroodwarOnly' - ] - longlabel = 18 - datname = 'upgrades.dat' - header = 'Upgrade' - idfile = 'Upgrades.txt' - filesize = 1281 - count = 61 - - def __init__(self, stat_txt=None): - UnitsDAT.__init__(self, stat_txt) - self.data = [ - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[self.time_value,''],''], - [[self.time_value,''],''], - [[None,''],''], - [[self.info_value,'Icons'],''], - [[self.stattxt_value,'Upgrade Label'],''], - [[self.info_value,'Races'],''], - [[None,''],''], - [[None,''],''] - ] - self.special = {} - - def stattxt_value(self, type, value): - if value == 0: - return '%s: None' % type - return '%s in stat_txt.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) - - def info_value(self, file, value): - if file == 'Icons': - s = 'Upgrade Icon: ' - elif file == 'Races': - s = 'Used by Race: ' - return s + DATA_CACHE[file + '.txt'][value] - -class TechDAT(UnitsDAT): - format = [ - [[],2], - [[],2], - [[],2], - [[],2], - [[],4], - [[],2], - [[],2], - [[],1], - [[],1], - [[],1] - ] - labels = [ - 'MineralCost', - 'VespeneCost', - 'ResearchTime', - 'EnergyRequired', - 'Unknown', - 'Icon', - 'Label', - 'Race', - 'Unused', - 'BroodwarOnly' - ] - longlabel = 14 - datname = 'techdata.dat' - header = 'Tech' - idfile = 'Techdata.txt' - filesize = 836 - count = 44 - - def __init__(self, stat_txt=None): - UnitsDAT.__init__(self, stat_txt) - self.data = [ - [[None,''],''], - [[None,''],''], - [[self.time_value,''],''], - [[None,''],''], - [[None,''],''], - [[self.info_value,'Icons'],''], - [[self.stattxt_value,'Technology Label'],''], - [[self.info_value,'Races'],''], - [[None,''],''], - [[None,''],''] - ] - self.special = {} - - def stattxt_value(self, type, value): - if value == 0: - return '%s: None' % type - return '%s in stat_txt.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) - - def info_value(self, file, value): - if file == 'Icons': - s = 'Upgrade Icon: ' - elif file == 'Races': - s = 'Used by Race: ' - return s + DATA_CACHE[file + '.txt'][value] - -class SoundsDAT(UnitsDAT): - format = [ - [[],4], - [[],1], - [[],1], - [[],2], - [[],1] - ] - labels = [ - 'SoundFile', - 'Unknown1', - 'Unknown2', - 'Unknown3', - 'Unknown4' - ] - longlabel = 9 - datname = 'sfxdata.dat' - header = 'Sound' - idfile = 'Sfxdata.txt' - filesize = 10296 - count = 1144 - - def __init__(self, sfx=None): - if sfx == None: - sfx = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'sfxdata.tbl') - UnitsDAT.__init__(self, sfx) - self.data = [ - [[self.stattxt_value,'Sound File Path'],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''] - ] - self.special = {} - - def stattxt_value(self, type, value): - if value == 0: - return '%s: None' % type - return '%s in sfxdata.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) - -class PortraitDAT(UnitsDAT): - format = [ - [[],4], - [[],1], - [[],1] - ] - labels = [ - 'PortraitFile', - 'SMKChange', - 'Unknown' - ] - longlabel = 12 - datname = 'portdata.dat' - header = 'Portrait' - idfile = 'Portdata.txt' - filesize = 1320 - count = 220 - - def __init__(self, ports=None): - if ports == None: - ports = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'portdata.tbl') - UnitsDAT.__init__(self, ports) - self.data = [ - [[self.stattxt_value,'Portrait File Path'],''], - [[None,''],''], - [[None,''],''] - ] - self.special = {} - - def stattxt_value(self, type, value): - if value == 0: - return '%s: None' % type - return '%s in portdata.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) - -class CampaignDAT(UnitsDAT): - format = [ - [[],4] - ] - labels = [ - 'MapFile' - ] - longlabel = 7 - datname = 'mapdata.dat' - header = 'Map' - idfile = 'Mapdata.txt' - filesize = 260 - count = 65 - - def __init__(self, maps=None): - if maps == None: - maps = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'mapdata.tbl') - UnitsDAT.__init__(self, maps) - self.data = [ - [[self.stattxt_value,'Map File Path'],''] - ] - self.special = {} - - def stattxt_value(self, type, value): - if value == 65: - return '%s in mapdata.tbl, item %s: None' % (type, value) - return '%s in mapdata.tbl, item %s: %s' % (type, value, TBL.decompile_string(self.tbl.strings[value])) - -class OrdersDAT(UnitsDAT): - format = [ - [[],2], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],1], - [[],2], - [[],2], - [[],1] - ] - labels = [ - 'Label', - 'UseWeaponTargeting', - 'Unknown1', - 'MainOrSecondary', - 'Unknown3', - 'Unknown4', - 'Interruptable', - 'Unknown5', - 'Queueable', - 'Unknown6', - 'Unknown7', - 'Unknown8', - 'Unknown9', - 'Targeting', - 'Energy', - 'Animation', - 'Highlight', - 'Unknown10', - 'ObscuredOrder' - ] - longlabel = 18 - datname = 'orders.dat' - header = 'Order' - idfile = 'Orders.txt' - filesize = 4158 - count = 189 - - def __init__(self, stat_txt=None): - UnitsDAT.__init__(self, stat_txt) - self.data = [ - [[self.stattxt_value,'Order Label'],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[None,''],''], - [[self.dat_value,'Weapons'],''], - [[self.info_value,'Techdata'],''], - [[self.info_value,'Animations'],''], - [[self.info_value,'Icons'],''], - [[None,''],''], - [[self.dat_value,'Orders'],''] - ] - self.special = {} - - def info_value(self, file, value): - if file == 'Techdata': - s = 'Energy Cost Technology: ' - elif file == 'Animations': - s = 'Unit IScript Animation: ' - elif file == 'Icons': - s = 'Highlight Icon: ' - if value == 65535: - return s + 'None' - return s + DATA_CACHE[file + '.txt'][value] - -# for DAT in [SpritesDAT]:#UnitsDAT,WeaponsDAT,FlingyDAT,SpritesDAT,ImagesDAT,UpgradesDAT,TechDAT,SoundsDAT,PortraitDAT,CampaignDAT,OrdersDAT]: - # d = DAT() - # print d.datname - # d.load_file('Default\\' + d.datname) - # d.compile('Test\\' + d.datname) -#d = ImagesDAT() -#d.load_file('Default\images.dat') -#d.decompile('blah.txt',[0,1]) -#d.interpret('blah.txt') -#d.decompile('test.txt',[0,1]) -#d.compile('test.dat') - -# class DAT(UnitsDAT): - # format = [ - - # ] - # labels = [ - - # ] - # longlabel = 0 - # datname = '.dat' - # header = '' - # idfile = '.txt' - # filesize = 0 - # count = 0 - - # def __init__(self, stat_txt='Default\stat_txt.tbl'): - # UnitsDAT.__init__(self, stat_txt) - # self.data = [ - - # ] - - # def info_value(self, file, value): - # if file == 'DamTypes': - # s = 'Damage Type: ' - # elif file == 'Behaviours': - # s = 'Projectile Behaviour: ' - # elif file == 'Explosions': - # s = 'Explosion Type: ' - # elif file == 'Icons': - # s = 'Weapon Icon: ' - # elif file == 'Techdata': - # s = 'Has no effect. Assumed value: ' +from .utils import * +from . import TBL + +import struct, os, re + +DATA_REFERENCE = { + 'SelCircleSize.txt':'Selection Circle Sizes', + 'Rightclick.txt':'Right Click Actions', + 'Flingy.txt':'Flingy Entries', + 'Behaviours.txt':'Behaviours', + 'DamTypes.txt':'Damage Types', + 'Mapdata.txt':'Campaign Names', + 'Units.txt':'Default Units', + 'Remapping.txt':'Remapping', + 'DrawList.txt':'Draw Types', + 'FlingyControl.txt':'Flingy Controlers', + 'Sprites.txt':'Default Sprites', + 'Animations.txt':'IScript Animations', + 'Orders.txt':'Default Orders', + 'IscriptIDList.txt':"IScript ID's", + 'Portdata.txt':'Default Campaign', + 'Weapons.txt':'Default Weapons', + 'UnitSize.txt':'Unit Sizes', + 'Techdata.txt':'Default Technologies', + 'ElevationLevels.txt':'Elevation Levels', + 'Images.txt':'Default Images', + 'Upgrades.txt':'Default Upgrades', + 'Explosions.txt':'Explosion Types', + 'Races.txt':'Races', + 'Icons.txt':'Icons', + 'Sfxdata.txt':'Sound Effects', + 'ShieldSize.txt':'Shield Sizes' +} + +DATA_CACHE = {} +for d in DATA_REFERENCE.keys(): + f = open(os.path.join(BASE_DIR, 'Libs', 'Data', d),'r') + DATA_CACHE[d] = [l.rstrip() for l in f.readlines()] + f.close() + +class UnitsDAT: + format = [ + [[],1], + [[],2], + [[],2], + #[Inclusive,Exclusive] + [[106,202],2], + [[],4], + [[],1], + [[],1], + [[],2], + [[],4], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],4], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[0,106],2], + [[],2], + [[],2], + [[0,106],2], + [[0,106],2], + [[0,106],2], + [[0,106],2], + [[],2, 2], + [[],2], + [[106,202],2, 2], + [[106,202],2], + [[],2, 4], + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],2], + [[],2], + [[],2], + [[],1], + [[],2] + ] + labels = [ + 'Graphics', + 'Subunit1', + 'Subunit2', + 'Infestation', + 'ConstructionAnimation', + 'UnitDirection', + 'ShieldEnable', + 'ShieldAmount', + 'HitPoints', + 'ElevationLevel', + 'Unknown', + 'Sublabel', + 'CompAIIdle', + 'HumanAIIdle', + 'ReturntoIdle', + 'AttackUnit', + 'AttackMove', + 'GroundWeapon', + 'MaxGroundHits', + 'AirWeapon', + 'MaxAirHits', + 'AIInternal', + 'SpecialAbilityFlags', + 'TargetAcquisitionRange', + 'SightRange', + 'ArmorUpgrade', + 'UnitSize', + 'Armor', + 'RightClickAction', + 'ReadySound', + 'WhatSoundStart', + 'WhatSoundEnd', + 'PissSoundStart', + 'PissSoundEnd', + 'YesSoundStart', + 'YesSoundEnd', + 'StarEditPlacementBoxWidth', + 'StarEditPlacementBoxHeight', + 'AddonHorizontal', + 'AddonVertical', + 'UnitSizeLeft', + 'UnitSizeUp', + 'UnitSizeRight', + 'UnitSizeDown', + 'Portrait', + 'MineralCost', + 'VespeneCost', + 'BuildTime', + 'Unknown1', + 'StarEditGroupFlags', + 'SupplyProvided', + 'SupplyRequired', + 'SpaceRequired', + 'SpaceProvided', + 'BuildScore', + 'DestroyScore', + 'UnitMapString', + 'BroodwarUnitFlag', + 'StarEditAvailabilityFlags' + ] + flaglens = { + 'SpecialAbilityFlags':32, + 'StarEditGroupFlags':8, + 'StarEditAvailabilityFlags':16 + } + longlabel = 26 + datname = 'units.dat' + header = 'Unit' + idfile = 'Units.txt' + filesize = 19876 + count = 228 + def __init__(self, stat_txt=None): + if stat_txt == None: + stat_txt = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'stat_txt.tbl') + if isstr(stat_txt): + self.tbl = TBL.TBL() + self.tbl.load_file(stat_txt) + else: + self.tbl = stat_txt + self.entries = [[0] * len(self.labels) for _ in range(self.count)] + if self.datname == 'units.dat': + self.data = [ + [[self.dat_value,'Flingy'],''], + [[self.dat_value,'Units'],''], + [[self.dat_value,'Units'],''], + [[self.dat_value,'Units'],''], + [[self.dat_value,'Images'],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[self.info_value,'ElevationLevels'],''], + [[None,''],'Old Movement?: 0x1,0x2,0x4,0x8,0x10,0x20,0x40,0x80 (Mine-safe)'], + [[self.stattxt_value,'Sublabel'],''], + [[self.dat_value,'Orders'],''], + [[self.dat_value,'Orders'],''], + [[self.dat_value,'Orders'],''], + [[self.dat_value,'Orders'],''], + [[self.dat_value,'Orders'],''], + [[self.dat_value,'Weapons'],''], + [[None,''],''], + [[self.dat_value,'Weapons'],''], + [[None,''],''], + [[None,''],''], + [[None,''],'Building,Addon,Flyer,Worker,Subunit,"Flying Building",Hero,"Regenerates HP","Animated Idle(?)",Cloakable,"Two Units in 1 Egg","Single Entity","Resource Depot","Resource Container","Robotic Unit",Detector,"Organic Unit","Requires Creep",Unused(?),"Requires Psi",Burrowable,Spellcaster,"Permanent Cloak","Pickup Item(?)","Ignore Supply Check","Use Medium Overlays","Use Large Overlays","Battle Reactions","Full Auto-Attack",Invincible,"Mechanical Unit","Produces Units(?)"'], + [[None,''],''], + [[None,''],'StarCraft crashes with values above 11'], + [[self.dat_value,'Upgrades'],''], + [[self.info_value,'UnitSize'],''], + [[None,''],''], + [[self.info_value,'Rightclick'],''], + [[self.dat_value,'Sfxdata'],''], + [[self.dat_value,'Sfxdata'],''], + [[self.dat_value,'Sfxdata'],''], + [[self.dat_value,'Sfxdata'],''], + [[self.dat_value,'Sfxdata'],''], + [[self.dat_value,'Sfxdata'],''], + [[self.dat_value,'Sfxdata'],''], + [[None,''],''], + [[None,''],''], + [[None,''],'X Position'], + [[None,''],'Y Position'], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[self.dat_value,'Portdata'],''], + [[None,''],''], + [[None,''],''], + [[self.time_value,''],''], + [[None,''],''], + [[None,''],'Zerg,Terran,Protoss,Men,Building,Factory,Independent,Neutral'], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],'Non-Neutral,"Unit Listing&&Palette","Mission Briefing","Player Settings","All Races","Set Doodad State","Non-Location Triggers","Unit&&Hero Settings","Location Triggers","BroodWar Only","Unused (0x400)","Unused (0x800)","Unused (0x1000)","Unused (0x2000)","Unused (0x4000)","Unused (0x8000)"'] + ] + self.special = { + 'HitPoints':self.hitpoints + } + + def get_value(self, entry, label): + try: + return self.entries[entry][self.labels.index(label)] + except: + return None + + def set_value(self, entry, label, value): + try: + self.entries[entry][self.labels.index(label)] = value + except: + return False + return True + + def hitpoints(self, value, save=False): + if save: + hp = struct.pack('>8 + + def load_file(self, file): + if isstr(file): + try: + f = open(file,'rb') + data = f.read() + f.close() + except: + raise PyMSError('Load',"Could not load %s file '%s'" % (self.datname, file)) + else: + data = file.read() + if len(data) != self.filesize: + raise PyMSError('Load',"'%s' is an invalid %s file (the file size must be %s, but got a file with size %s)" % (file,self.datname,self.filesize,len(data))) + try: + entries = [] + offset = 0 + array = 0 + arraylen = 0 + for x,format in enumerate(self.format): + bytes = format[1] + if len(format) == 3: + arraylen = array = format[2] + vals = self.count + values = [] + if format[0]: + vals = format[0][1] - format[0][0] + values.extend([0]*format[0][0]) + for y in range(vals): + o = offset+y*arraylen*bytes+(arraylen-array)*bytes + values.append(struct.unpack('<%s' % ['', 'B','H','','L'][bytes], data[o:o+bytes])[0]) + if not array: + offset += bytes + if format[0]: + values.extend([0]*(self.count - format[0][1])) + if array: + array -= 1 + if not array: + offset += arraylen * bytes * vals + arraylen = 0 + # elif format[0]: + # vals = format[0][1] - format[0][0] + # values = [0]*format[0][0] + list(struct.unpack('<%s%s' % (vals, ['', 'B','H','','L'][bytes]), data[offset:offset+bytes*vals])) + [0]*(self.count - format[0][1]) + # offset += bytes * vals + # else: + # values = struct.unpack('<%s%s' % (self.count, ['','B','H','','L'][bytes]), data[offset:offset+bytes*self.count]) + # offset += bytes * self.count + for id,value in enumerate(values): + if x == 0: + entries.append([value]) + elif self.labels[x] in self.special: + entries[id].append(self.special[self.labels[x]](value)) + # elif self.datname == 'units.dat' and x == 8: #HitPoints + # entries[id].append(value>>8) + # elif self.datname == 'weapons.dat' and x in [4,5]: + # entries[id].append(value>>4) + else: + entries[id].append(value) + self.entries = entries + except: + raise PyMSError('Load','Unsupported %s file, could possibly be corrupt' % self.datname) + + def dat_value(self, file, value): + if value == 65535: + return '%s.dat entry: None' + return '%s.dat entry: %s' % (file, DATA_CACHE[file + '.txt'][value]) + + def info_value(self, file, value): + if file == 'ElevationLevels': + s = 'Unit elevation level: ' + elif file == 'UnitSize': + s = 'Unit size: ' + elif file == 'Rightclick': + s = 'Right-click order action: ' + return s + DATA_CACHE[file + '.txt'][value] + + def time_value(self, _, value): + return 'Length in seconds: %s' % (value / 24) + + def stattxt_value(self, type, value): + if type == 'Sublabel': + value += 1301 + return '%s in stat_txt.tbl, item %s: %s' % (type, value, TBL.decompile_string(self.tbl.strings[value])) + + def interpret(self, file): + if len(self.entries) != self.count: + raise PyMSError('Interpreting',"No default %s file was loaded as a base" % self.datname) + try: + f = open(file,'r') + data = f.readlines() + f.close() + except: + raise PyMSError('Interpreting',"Could not load file '%s'" % file) + entries = {} + entrydata = False + curentry = -1 + for n,l in enumerate(data): + if len(l) > 1: + ln = l.strip().split('#',1)[0] + line = re.split(r'\s+', ln) + if entrydata: + if line[0] == self.header + 'ID': + raise PyMSError('Interpreting',"Unexpected line, expected a value",n,ln) + if line[0] not in self.labels: + raise PyMSError('Interpreting',"'%s' is an invalid value label" % line[0],n,ln) + label = self.labels.index(line[0]) + value = None + if line[0] in self.flaglens: + if len(line[1]) != self.flaglens[line[0]]: + raise PyMSError('Interpreting',"Incorrect amount of flags (expected %s, got %s)" % (self.flaglens[line[0]],len(line[1])),n,ln) + if re.match('[^01]',line[1]): + raise PyMSError('Interpreting',"'%s' is an invalid set of flags" % line[1],n,ln) + value = sum(int(x)*(2**n) for n,x in enumerate(reversed(line[1]))) + if value == None: + try: + value = int(line[1]) + if value < 0 or value > 256 ** self.format[label][1]: + raise + except: + raise PyMSError('Interpreting',"'%s' is an invalid value (must be in range 0 to %s)" % (line[1],256**self.format[label][1]),n,ln) + if self.datname == 'units.dat' and line[0] == 'SightRange' and value > 11: + raise PyMSError('Interpreting',"SightRange can not be more then 11",n,ln) + entries[curentry][label] = value + if not None in entries[curentry]: + entrydata = False + else: + if line[0] != self.header + 'ID' or not line[1].endswith(':'): + raise PyMSError('Interpreting',"Unexpected line, expected a new %sID header" % self.header,n,ln) + try: + id = int(line[1][:-1]) + if id < 0 or id >= self.count: + raise + except: + raise PyMSError('Interpreting',"Invalid %sID value (must be in the range 0 to %s)" % (self.header,self.count),n,ln) + entries[id] = [None]*len(self.format) + for n,format in enumerate(self.format): + if format[0] and not id in range(*format[0]): + entries[id][n] = 0 + curentry = id + entrydata = True + if None in entries[curentry]: + raise PyMSError('Interpreting',"Entry '%s' is missing a value for %s" % (curentry,self.labels[entries.curentry.index(None)]),n,ln) + for id,entry in entries.items(): + self.entries[id] = entry + return entries.keys() + + def decompile(self, file, ref=False, ids=None): + try: + f = open(file, 'w') + except: + raise PyMSError('Interpreting',"Could not load file '%s'" % file) + if ref: + f.write('#----------------------------------------------------') + for file,name in DATA_REFERENCE.items(): + f.write('\n# %s:' % name) + for n,value in enumerate(DATA_CACHE[file]): + pad = ' ' * (3 - len(str(n))) + f.write('\n# %s%s = %s' % (pad,n,value)) + f.write('\n#') + f.write('----------------------------------------------------\n\n') + if ids == None: + ids = range(self.count) + for id in ids: + f.write('%sID %s:%s# %s name: %s\n' % (self.header, id, ' ' * (12 + self.longlabel - len(self.header) - len(str(id))), self.header, DATA_CACHE[self.idfile][id])) + for format,data,label,value in zip(self.format,self.data,self.labels,self.entries[id]): + if not format[0] or id in range(*format[0]): + if label in self.flaglens: + v = ''.join(reversed([str(value/(2**n)%2) for n in range(self.flaglens[label])])) + else: + v = value + f.write(' %s%s%s' % (label, ' ' * (self.longlabel + 1 - len(label)), v)) + if data[0][0]: + f.write('%s# %s' % (' ' * (11 - len(str(v))), data[0][0](data[0][1], v))) + f.write('\n') + f.write('\n') + f.close() + + def compile(self, file): + if len(self.entries) != self.count: + raise + if isstr(file): + try: + f = open(file, 'wb') + except: + raise PyMSError('Compile',"Could not load file '%s'" % file) + else: + f = file + data = [] + for id,entry in enumerate(self.entries): + if len(entry) < len(self.format): + raise + array = 0 + d,fd = 0,0 + for x,value in enumerate(entry): + if array: + d += 1 + fd += 1 + format = self.format[x-fd] + if x-d == len(data): + data.append([]) + if not array: + if len(format) == 3: + array = format[2] - 1 + else: + array -= 1 + if array == 0: + fd = 0 + if format[0] and not id in range(*format[0]): + continue + data[x-d].append(value) + l = list(self.labels) + d = 0 + for n,values in enumerate(data): + format,label = self.format[n+d],self.labels[n+d] + l.remove(label) + if len(format) == 3: + d += format[2]-1 + if label in self.special: + for value in values: + f.write(self.special[label](value,True)) + else: + f.write(struct.pack('<%s%s' % (len(values), ['', 'B','H','','L'][format[1]]), *values)) + f.close() + +class WeaponsDAT(UnitsDAT): + format = [ + [[],2], + [[],4], + [[],1], + [[],2], + [[],4], + [[],4], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],2], + [[],2] + ] + labels = [ + 'Label', + 'Graphics', + 'Unused', + 'TargetFlags', + 'MinimumRange', + 'MaximumRange', + 'DamageUpgrade', + 'WeaponType', + 'WeaponBehavior', + 'RemoveAfter', + 'ExplosionType', + 'InnerSplashRange', + 'MediumSplashRange', + 'OuterSplashRange', + 'DamageAmount', + 'DamageBonus', + 'WeaponCooldown', + 'DamageFactor', + 'AttackAngle', + 'LaunchSpin', + 'ForwardOffset', + 'UpwardOffset', + 'TargetErrorMessage', + 'Icon' + ] + flaglens = {'TargetFlags':9} + longlabel = 18 + datname = 'weapons.dat' + header = 'Weapon' + idfile = 'Weapons.txt' + filesize = 5460 + count = 130 + + def __init__(self, stat_txt=None): + UnitsDAT.__init__(self, stat_txt) + self.data = [ + [[self.stattxt_value,'Weapon Label'],''], + [[self.dat_value,'Flingy'],''], + [[self.info_value,'Techdata'],''], + [[None,''],'Air,Ground,Mechanical,Organic,non-Building,non-Robotic,Terrain,Organic or Mechanical,Own'], + [[None,''],''], + [[None,''],''], + [[self.dat_value,'Upgrades'],''], + [[self.info_value,'DamTypes'],''], + [[self.info_value,'Behaviours'],''], + [[None,''],''], + [[self.info_value,'Explosions'],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[self.stattxt_value,'Targetting Error Message'],''], + [[self.info_value,'Icons'],''] + ] + self.special = { + 'MinimumRange':self.range, + 'MaximumRange':self.range, + } + + def info_value(self, file, value): + if file == 'DamTypes': + s = 'Damage Type: ' + elif file == 'Behaviours': + s = 'Projectile Behaviour: ' + elif file == 'Explosions': + s = 'Explosion Type: ' + elif file == 'Icons': + s = 'Weapon Icon: ' + elif file == 'Techdata': + s = 'Has no effect. Assumed value: ' + return s + DATA_CACHE[file + '.txt'][value] + + def range(self, value, save=False): + if save: + return struct.pack('>4 + +class FlingyDAT(UnitsDAT): + format = [ + [[],2], + [[],4], + [[],2], + [[],4], + [[],1], + [[],1], + [[],1] + ] + labels = [ + 'Sprite', + 'Speed', + 'Acceleration', + 'HaltDistance', + 'TurnRadius', + 'Unused', + 'MovementControl' + ] + longlabel = 15 + datname = 'flingy.dat' + header = 'Flingy' + idfile = 'Flingy.txt' + filesize = 3135 + count = 209 + + def __init__(self, stat_txt=None): + UnitsDAT.__init__(self, stat_txt) + self.data = [ + [[self.dat_value, 'Sprites'],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[self.info_value, 'FlingyControl'],''] + ] + self.special = {} + + def info_value(self, file, value): + return 'Flingy Control: ' + DATA_CACHE[file + '.txt'][value] + +class SpritesDAT(UnitsDAT): + format = [ + [[],2], + [[130,517],1], + [[],1], + [[],1], + [[130,517],1], + [[130,517],1] + ] + labels = [ + 'ImageFile', + 'HealthBar', + 'Unknown', + 'IsVisible', + 'SelectionCircleImage', + 'SelectionCircleOffset' + ] + longlabel = 21 + datname = 'sprites.dat' + header = 'Sprite' + idfile = 'Sprites.txt' + filesize = 3229 + count = 517 + + def __init__(self, stat_txt=None): + UnitsDAT.__init__(self, stat_txt) + self.data = [ + [[self.dat_value,'Images'],''], + [[self.hpbar_value,''],''], + [[None,''],''], + [[None,''],''], + [[self.info_value, 'SelCircleSize'],''], + [[None,''],''] + ] + self.special = {} + + def hpbar_value(self, _, value): + return 'Health Bar Boxes: %s' % (int((value -1) / 3)) + + def info_value(self, file, value): + return 'Selection Circle Size: ' + DATA_CACHE[file + '.txt'][value] + +class ImagesDAT(UnitsDAT): + format = [ + [[],4], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],4], + [[],4], + [[],4], + [[],4], + [[],4], + [[],4], + [[],4] + ] + labels = [ + 'GRPFile', + 'GfxTurns', + 'Clickable', + 'UseFullIscript', + 'DrawIfCloaked', + 'DrawFunction', + 'Remapping', + 'IscriptID', + 'ShieldOverlay', + 'AttackOverlay', + 'DamageOverlay', + 'SpecialOverlay', + 'LandingDustOverlay', + 'LiftOffDustOverlay' + ] + longlabel = 18 + datname = 'images.dat' + header = 'Image' + idfile = 'Images.txt' + filesize = 37962 + count = 999 + + def __init__(self, grps=None): + if grps == None: + grps = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'images.tbl') + UnitsDAT.__init__(self, grps) + self.data = [ + [[self.stattxt_value,'GRP File Path: '],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[self.info_value,'DrawList'],''], + [[self.info_value,'Remapping'],''], + [[self.info_value,'IscriptIDList'],''], + [[self.stattxt_value,'Shield Overlay File Path'],''], + [[self.stattxt_value,'Attack Overlay File Path'],''], + [[self.stattxt_value,'Damage Overlay File Path'],''], + [[self.stattxt_value,'Special Overlay File Path'],''], + [[self.stattxt_value,'Landing Dust Overlay File Path'],''], + [[self.stattxt_value,'Lift-Off Dust Overlay File Path'],''] + ] + self.special = {} + + def stattxt_value(self, type, value): + if value == 0: + return '%s: None' % type + return '%s in images.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) + + def info_value(self, file, value): + if file == 'DrawList': + s = 'Drawing Function: ' + elif file == 'Remapping': + s = 'Additional Remapping Palette: ' + elif file == 'IscriptIDList': + s = 'IScript Name: ' + return s + DATA_CACHE[file + '.txt'][value] + +class UpgradesDAT(UnitsDAT): + format = [ + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],2], + [[],1], + [[],1], + [[],1] + ] + labels = [ + 'MineralCostBase', + 'MineralCostFactor', + 'VespeneCostBase', + 'VespeneCostFactor', + 'ResearchTimeBase', + 'ResearchTimeFactor', + 'Unknown', + 'Icon', + 'Label', + 'Race', + 'MaxRepeats', + 'BroodwarOnly' + ] + longlabel = 18 + datname = 'upgrades.dat' + header = 'Upgrade' + idfile = 'Upgrades.txt' + filesize = 1281 + count = 61 + + def __init__(self, stat_txt=None): + UnitsDAT.__init__(self, stat_txt) + self.data = [ + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[self.time_value,''],''], + [[self.time_value,''],''], + [[None,''],''], + [[self.info_value,'Icons'],''], + [[self.stattxt_value,'Upgrade Label'],''], + [[self.info_value,'Races'],''], + [[None,''],''], + [[None,''],''] + ] + self.special = {} + + def stattxt_value(self, type, value): + if value == 0: + return '%s: None' % type + return '%s in stat_txt.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) + + def info_value(self, file, value): + if file == 'Icons': + s = 'Upgrade Icon: ' + elif file == 'Races': + s = 'Used by Race: ' + return s + DATA_CACHE[file + '.txt'][value] + +class TechDAT(UnitsDAT): + format = [ + [[],2], + [[],2], + [[],2], + [[],2], + [[],4], + [[],2], + [[],2], + [[],1], + [[],1], + [[],1] + ] + labels = [ + 'MineralCost', + 'VespeneCost', + 'ResearchTime', + 'EnergyRequired', + 'Unknown', + 'Icon', + 'Label', + 'Race', + 'Unused', + 'BroodwarOnly' + ] + longlabel = 14 + datname = 'techdata.dat' + header = 'Tech' + idfile = 'Techdata.txt' + filesize = 836 + count = 44 + + def __init__(self, stat_txt=None): + UnitsDAT.__init__(self, stat_txt) + self.data = [ + [[None,''],''], + [[None,''],''], + [[self.time_value,''],''], + [[None,''],''], + [[None,''],''], + [[self.info_value,'Icons'],''], + [[self.stattxt_value,'Technology Label'],''], + [[self.info_value,'Races'],''], + [[None,''],''], + [[None,''],''] + ] + self.special = {} + + def stattxt_value(self, type, value): + if value == 0: + return '%s: None' % type + return '%s in stat_txt.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) + + def info_value(self, file, value): + if file == 'Icons': + s = 'Upgrade Icon: ' + elif file == 'Races': + s = 'Used by Race: ' + return s + DATA_CACHE[file + '.txt'][value] + +class SoundsDAT(UnitsDAT): + format = [ + [[],4], + [[],1], + [[],1], + [[],2], + [[],1] + ] + labels = [ + 'SoundFile', + 'Unknown1', + 'Unknown2', + 'Unknown3', + 'Unknown4' + ] + longlabel = 9 + datname = 'sfxdata.dat' + header = 'Sound' + idfile = 'Sfxdata.txt' + filesize = 10296 + count = 1144 + + def __init__(self, sfx=None): + if sfx == None: + sfx = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'sfxdata.tbl') + UnitsDAT.__init__(self, sfx) + self.data = [ + [[self.stattxt_value,'Sound File Path'],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''] + ] + self.special = {} + + def stattxt_value(self, type, value): + if value == 0: + return '%s: None' % type + return '%s in sfxdata.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) + +class PortraitDAT(UnitsDAT): + format = [ + [[],4], + [[],1], + [[],1] + ] + labels = [ + 'PortraitFile', + 'SMKChange', + 'Unknown' + ] + longlabel = 12 + datname = 'portdata.dat' + header = 'Portrait' + idfile = 'Portdata.txt' + filesize = 1320 + count = 220 + + def __init__(self, ports=None): + if ports == None: + ports = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'portdata.tbl') + UnitsDAT.__init__(self, ports) + self.data = [ + [[self.stattxt_value,'Portrait File Path'],''], + [[None,''],''], + [[None,''],''] + ] + self.special = {} + + def stattxt_value(self, type, value): + if value == 0: + return '%s: None' % type + return '%s in portdata.tbl, item %s: %s' % (type, value - 1, TBL.decompile_string(self.tbl.strings[value - 1])) + +class CampaignDAT(UnitsDAT): + format = [ + [[],4] + ] + labels = [ + 'MapFile' + ] + longlabel = 7 + datname = 'mapdata.dat' + header = 'Map' + idfile = 'Mapdata.txt' + filesize = 260 + count = 65 + + def __init__(self, maps=None): + if maps == None: + maps = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'mapdata.tbl') + UnitsDAT.__init__(self, maps) + self.data = [ + [[self.stattxt_value,'Map File Path'],''] + ] + self.special = {} + + def stattxt_value(self, type, value): + if value == 65: + return '%s in mapdata.tbl, item %s: None' % (type, value) + return '%s in mapdata.tbl, item %s: %s' % (type, value, TBL.decompile_string(self.tbl.strings[value])) + +class OrdersDAT(UnitsDAT): + format = [ + [[],2], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],1], + [[],2], + [[],2], + [[],1] + ] + labels = [ + 'Label', + 'UseWeaponTargeting', + 'Unknown1', + 'MainOrSecondary', + 'Unknown3', + 'Unknown4', + 'Interruptable', + 'Unknown5', + 'Queueable', + 'Unknown6', + 'Unknown7', + 'Unknown8', + 'Unknown9', + 'Targeting', + 'Energy', + 'Animation', + 'Highlight', + 'Unknown10', + 'ObscuredOrder' + ] + longlabel = 18 + datname = 'orders.dat' + header = 'Order' + idfile = 'Orders.txt' + filesize = 4158 + count = 189 + + def __init__(self, stat_txt=None): + UnitsDAT.__init__(self, stat_txt) + self.data = [ + [[self.stattxt_value,'Order Label'],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[None,''],''], + [[self.dat_value,'Weapons'],''], + [[self.info_value,'Techdata'],''], + [[self.info_value,'Animations'],''], + [[self.info_value,'Icons'],''], + [[None,''],''], + [[self.dat_value,'Orders'],''] + ] + self.special = {} + + def info_value(self, file, value): + if file == 'Techdata': + s = 'Energy Cost Technology: ' + elif file == 'Animations': + s = 'Unit IScript Animation: ' + elif file == 'Icons': + s = 'Highlight Icon: ' + if value == 65535: + return s + 'None' + return s + DATA_CACHE[file + '.txt'][value] + +# for DAT in [SpritesDAT]:#UnitsDAT,WeaponsDAT,FlingyDAT,SpritesDAT,ImagesDAT,UpgradesDAT,TechDAT,SoundsDAT,PortraitDAT,CampaignDAT,OrdersDAT]: + # d = DAT() + # print(d).datname + # d.load_file('Default\\' + d.datname) + # d.compile('Test\\' + d.datname) +#d = ImagesDAT() +#d.load_file('Default\images.dat') +#d.decompile('blah.txt',[0,1]) +#d.interpret('blah.txt') +#d.decompile('test.txt',[0,1]) +#d.compile('test.dat') + +# class DAT(UnitsDAT): + # format = [ + + # ] + # labels = [ + + # ] + # longlabel = 0 + # datname = '.dat' + # header = '' + # idfile = '.txt' + # filesize = 0 + # count = 0 + + # def __init__(self, stat_txt='Default\stat_txt.tbl'): + # UnitsDAT.__init__(self, stat_txt) + # self.data = [ + + # ] + + # def info_value(self, file, value): + # if file == 'DamTypes': + # s = 'Damage Type: ' + # elif file == 'Behaviours': + # s = 'Projectile Behaviour: ' + # elif file == 'Explosions': + # s = 'Explosion Type: ' + # elif file == 'Icons': + # s = 'Weapon Icon: ' + # elif file == 'Techdata': + # s = 'Has no effect. Assumed value: ' # return s + DATA_CACHE[file + '.txt'][value] \ No newline at end of file diff --git a/tools/Libs/IScriptBIN.py b/tools/Libs/IScriptBIN.py index 104c464..66799e9 100644 --- a/tools/Libs/IScriptBIN.py +++ b/tools/Libs/IScriptBIN.py @@ -1,1189 +1,1189 @@ -from utils import * -import TBL,DAT - -import struct, re, os -try: - from cPickle import * -except ImportError: - from pickle import * -from zlib import compress, decompress - -# import sys -# sys.stdout = open('stdieo.txt','w') - -TYPE_HELP = { - 'Frame':'The index of a frame in a GRP, in decimal or hexadecimal (number in the range 0 to 65535. framesets are increments of 17, so 17 or 0x11, 34 or 0x22, 51 or 0x33, etc.)', - 'Byte':'A number in the range 0 to 255', - 'SByte':'A number in the range -128 to 127', - 'Label':'A label name of a block in the script', - 'ImageID':'The ID of an images.dat entry', - 'SpriteID':'The ID of a sprites.dat entry', - 'FlingyID':'The ID of a flingy.dat entry', - 'OverlayID':'The ID of an overlay', - 'FlipState':'The flip state to set on the current image overlay', - 'SoundID':'The ID of a sfxdata.dat entry', - 'Sounds':'How many sounds to pick from', - 'SignalID':'A signal order ID', - 'Weapon':'Either 1 for ground attack, or not 1 for air attack', - 'WeaponID':'The ID of a weapons.dat entry', - 'Speed':'The speed to set on the flingy.dat entry of the current flingy', - 'GasOverlay':'The ID of a gas overlay', - 'Short':'A number in the range 0 to 65535', -} -HEADER_HELP = [ - 'Initial animation', - 'Death animation', - 'Initial ground attack animation', - 'Initial air attack animation', - 'Unknown/unused animation', - 'Repeated ground attack animation', - 'Repeated air attack animation', - 'Spell casting animation', - 'Animation for returning to an idle state after a ground attack', - 'Animation for returning to an idle state after an air attack', - 'Unknown/unused animation', - 'Walking/moving animation', - 'Animation for returning to an idle state after walking/moving', - 'Some sort of category of special animations, in some cases an in-transit animation, sometimes used for special orders, sometimes having to do with the animation when something finishes morphing, or the first stage of a construction animation', - 'Some sort of category of special animations, in some cases a burrowed animation, sometimes used for special orders, sometimes having to do with the animation when canceling a morph, or the second stage of a construction animation', - 'An animation for one part of the building process', - 'Final animation before finishing being built', - 'Landing animation', - 'Lifting off animation', - 'Animation for when researching an upgrade/technology or training/building units and some other animations for some sort of work being done', - 'Animation for returning to an idle state after IsWorking', - 'Warping in animation', - 'Unknown/unused animation', - 'Previously called InitTurret, this is actually an alternate initial animation for StarEdit a.k.a. the Campaign Editor', - 'Animation for becoming disabled, either through the "Set Doodad State" trigger action or by not being in the psi field of any pylons', - 'Burrowing animation', - 'Unburrowing animation', - 'Animation for becoming enabled, either through the "Set Doodad State" trigger action or by being in the psi field of a pylon', - 'The iscript id of this animation set; it is referenced by images.dat, each set has a unique id', - 'This is the type of set; there are 28 different types, each with a different number of animations', -] -CMD_HELP = [ - 'Display Frame(1), adjusted for direction.', - 'Display Frame(1) dependant on tileset.', - 'Set the horizontal offset of the current image overlay to Byte(1).', - 'Set the vertical position of an image overlay to Byte(1).', - 'Set the horizontal and vertical position of the current image overlay to Byte(1) and Byte(2) respectively.', - 'Pauses script execution for a Byte(1) number of ticks.', - 'Pauses script execution for a random number of ticks between Byte(1) and Byte(2).', - 'Unconditionally jumps to code block Label(1).', - 'Display ImageID(1) as an active image overlay at an animation level higher than the current image overlay at offset position (Byte(1),Byte(2)).', - 'Display ImageID(1) as an active image overlay at an animation level lower than the current image overlay at offset position (Byte(1),Byte(2)).', - 'Display ImageID(1) as an active image overlay at an animation level higher than the current image overlay at the relative origin offset position.', - 'Only for powerups, this is hypothesised to replace the image overlay that was first created by the current image overlay.', #WTF? - 'Unknown.', - 'Displays an active image overlay at an animation level higher than the current image overlay, using a LO* file to determine the offset position.', #WTF? - 'Displays an active image overlay at an animation level lower than the current image overlay, using a LO* file to determine the offset position.', #WTF? - 'Spawns SpriteID(1) one animation level above the current image overlay at offset position (Byte(1),Byte(2)).', - 'Spawns SpriteID(1) at the highest animation level at offset position (Byte(1),Byte(2)).', - 'spawns SpriteID(1) at the lowest animation level at offset position (Byte(1),Byte(2)).', - 'Create FlingyID(1) with restrictions; supposedly crashes in most cases.', - 'Spawns SpriteID(1) one animation level below the current image overlay at offset position (Byte(1),Byte(2)). The new sprite inherits the direction of the current sprite. Requires LO* file for unknown reason.', - 'Spawns SpriteID(1) one animation level below the current image overlay at offset position (Byte(1),Byte(2)). The new sprite inherits the direction of the current sprite.', - 'Spawns SpriteID(1) one animation level above the current image overlay, using a specified LO* file for the offset position information. The new sprite inherits the direction of the current sprite.', #WTF? - 'Destroys the current active image overlay, also removing the current sprite if the image overlay is the last in one in the current sprite.', - 'Sets the flip state of the current image overlay to FlipState(1).', - 'Plays SoundID(1).', - "Plays a random sound from a list containing Sounds(1) number of SoundID(1)'s.", - 'Plays a random sound between SoundID(1) and SoundID(2) inclusively.', - 'Causes the damage of a weapon flingy to be applied according to its weapons.dat entry.', - "Applies damage to target without creating a flingy and plays a random sound from a list containing Sounds(1) number of SoundID(1)'s..", - 'Causes the current image overlay to display the same frame as the parent image overlay.', - 'Randomly jump to Label(1) with a chance of Byte(1) out of 255.', - 'Turns the flingy counterclockwise by Byte(1) direction units.', - 'Turns the flingy clockwise by Byte(1) direction units.', - 'Turns the flingy clockwise by one direction unit.', - 'Turns the flingy by Byte(1) direction units in a random direction, with a heavy bias towards turning clockwise.', - 'in specific situations, performs a natural rotation to the direction Byte(1).', - "Allows the current unit's order to proceed if it has paused for an animation to be completed.", #WTF? - 'Attack with either the ground or air weapon depending on Weapon(1).', - 'Attack with either the ground or air weapon depending on target.', - "Identifies when a spell should be cast in a spellcasting animation. The spell is determined by the unit's current order.", - 'Makes the unit use WeaponID(1) on its target.', - 'Sets the unit to move forward Byte(1) pixels at the end of the current tick.', - "Signals to StarCraft that after this point, when the unit's cooldown time is over, the repeat attack animation can be called.", - 'Plays Frame(1), often used in engine glow animations.', - 'Plays the frame set Frame(1), often used in engine glow animations.', - 'Hypothesised to hide the current image overlay until the next animation.', - 'Holds the processing of player orders until a nobrkcodeend is encountered.', - 'Allows the processing of player orders after a nobrkcodestart instruction.', - 'Conceptually, this causes the script to stop until the next animation is called.', - 'Creates the weapon flingy at a distance of Byte(1) in front of the unit.', - 'Sets the current image overlay state to hidden.', - 'Sets the current image overlay state to visible.', - 'Sets the current direction of the flingy to Byte(1).', - 'Calls the code block Label(1).', - 'Returns from call.', - 'Sets the flingy.dat speed of the current flingy to Short(1).', - 'Creates gas image overlay GasOverlay(1) at offsets specified by LO* files.', - 'Jumps to code block Label(1) if the current unit is a powerup and it is currently picked up.', - 'Jumps to code block Label(1) depending on the distance to the target.', #WTF? - 'Jumps to code block Label(1) depending on the current angle of the target.', #WTF? - 'Only for units. Jump to code block Label(1) if the current sprite is facing a particular direction.', #WTF? - 'Displays an active image overlay at the shadow animation level at a offset position (Byte(1),Byte(2)). The image overlay that will be displayed is the one that is after the current image overlay in images.dat.', - 'Unknown.', - 'Jumps to code block Label(1) when the current unit is a building that is lifted off.', - "Hypothesised to display Frame(1) from the current image overlay clipped to the outline of the parent image overlay.", - "Most likely used with orders that continually repeat, like the Medic's healing and the Valkyrie's afterburners (which no longer exist), to clear the sigorder flag to stop the order.", #WTF? - 'Spawns SpriteID(1) one animation level above the current image overlay at offset position (Byte(1),Byte(2)), but only if the current sprite is over ground-passable terrain.', - 'Unknown.', - 'Applies damage like domissiledmg when on ground-unit-passable terrain.', -] - -#stages: 0 = size, 1 = decompile, 2 = interpret -def type_frame(stage, bin, data=None): - """Frame""" - if data == None: - return 2 - if stage == 1: - if data % 17: - return (str(data),'Frame set %s, direction %s' % (data/17,data%17)) - return ('0x' + hex(data)[2:].zfill(2),'Frame set %s' % (data/17)) - try: - if data.startswith('0x'): - v = int(data[2:], 16) - else: - v = int(data) - if 0 > v or v > 65535: - raise - if bin and bin.grpframes and v > bin.grpframes: - raise PyMSWarning('Parameter',"'%s' is an invalid frame for one or more of the GRP's specified in the header, and may cause a crash (max frame value is %s, or frameset 0x%02x)" % (data, bin.grpframes, (bin.grpframes - 17) / 17 * 17),extra=v) - if bin and bin.grpframes and v > bin.grpframes - 17: - raise PyMSWarning('Parameter',"'%s' is an invalid frameset for one or more of the GRP's specified in the header, and may cause a crash (max frame value is %s, or frameset 0x%02x)" % (data, bin.grpframes , (bin.grpframes - 17) / 17 * 17),extra=v) - except PyMSWarning: - raise - except: - raise PyMSError('Parameter',"Invalid Frame value '%s', it must be a number in the range 0 to 65535 in decimal or hexidecimal (framesets are increments of 17: 17 or 0x11, 34 or 0x22, 51 or 0x33, etc.)" % data) - return v - -def type_bframe(stage, bin, data=None): - """BFrame""" - if data == None: - return 1 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 255: - raise - except: - raise PyMSError('Parameter',"Invalid BFrame value '%s', it must be a number in the range 0 to 256" % data) - return v - -def type_byte(stage, bin, data=None): - """Byte""" - if data == None: - return 1 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 255: - raise - except: - raise PyMSError('Parameter',"Invalid Byte value '%s', it must be a number in the range 0 to 255" % data) - return v - -def type_sbyte(stage, bin, data=None): - """SByte""" - if data == None: - return 1 - if stage == 1: - if data > 127: - data = -(256 - data) - return (str(data),'') - try: - v = int(data) - if -128 > v or v > 127: - raise - if v < 0: - v = 256 + v - except: - raise PyMSError('Parameter',"Invalid SByte value '%s', it must be a number in the range -128 to 127" % data) - return v - -def type_label(stage, bin): - """Label""" - return 2 - -def type_imageid(stage, bin, data=None): - """ImageID""" - if data == None: - return 2 - if stage == 1: - return (str(data),'%s (%s)' % (DAT.DATA_CACHE['Images.txt'][data], TBL.decompile_string(bin.imagestbl.strings[bin.imagesdat.get_value(data,'GRPFile')-1][:-1]))) - try: - v = int(data) - if 0 > v or v > DAT.ImagesDAT.count: - raise - except: - raise PyMSError('Parameter',"Invalid ImageID value '%s', it must be a number in the range 0 to %s" % (data,DAT.ImagesDAT.count)) - return v - -def type_spriteid(stage, bin, data=None): - """SpriteID""" - if data == None: - return 2 - if stage == 1: - return (str(data),'%s (%s)' % (DAT.DATA_CACHE['Sprites.txt'][data], TBL.decompile_string(bin.imagestbl.strings[bin.imagesdat.get_value(bin.spritesdat.get_value(data,'ImageFile'),'GRPFile')-1][:-1]))) - try: - v = int(data) - if 0 > v or v > DAT.SpritesDAT.count: - raise - except: - raise PyMSError('Parameter',"Invalid SpriteID value '%s', it must be a number in the range 0 to %s" % (data,DAT.SpritesDAT.count)) - return v - -def type_flingy(stage, bin, data=None): - """FlingyID""" - if data == None: - return 2 - if stage == 1: - return (str(data),'%s (%s)' % (DAT.DATA_CACHE['Flingy.txt'][data], TBL.decompile_string(bin.imagestbl.strings[bin.imagesdat.get_value(bin.spritesdat.get_value(bin.flingydat.get_value(data,'Sprite'),'ImageFile'),'GRPFile')-1][:-1]))) - try: - v = int(data) - if 0 > v or v > DAT.FlingyDAT.count: - raise - except: - raise PyMSError('Parameter',"Invalid FlingyID value '%s', it must be a number in the range 0 to %s" % (data,DAT.FlingyDAT.count)) - return v - -def type_overlayid(stage, bin, data=None): - """OverlayID""" - if data == None: - return 1 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 255: - raise - except: - raise PyMSError('Parameter',"Invalid OverlayID value '%s', it must be a number in the range 0 to 255" % data) - return v # Restrictions? - # /"Overlay 1" renamed to "Attack" - # /"Overlay 2" renamed to "HP Damage" - # /"Overlay 3" renamed to "Special" - # /"Overlay 4" renamed to "Landing Dust" - # /"Overlay 5" renamed to "Lift-off Dust" - -def type_flipstate(stage, bin, data=None): - """FlipState""" - if data == None: - return 1 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 255: - raise - except: - raise PyMSError('Parameter',"Invalid FlipState value '%s', it must be a number in the range 0 to 255" % data) - return v - -def type_soundid(stage, bin, data=None): - """SoundID""" - if data == None: - return 2 - if stage == 1: - return (str(data), TBL.decompile_string(bin.sfxdatatbl.strings[bin.soundsdat.get_value(data,'SoundFile')-1][:-1])) - try: - v = int(data) - if 0 > v or v > DAT.SoundsDAT.count: - raise - except: - raise PyMSError('Parameter',"Invalid SoundID value '%s', it must be a number in the range 0 to %s" % (data,DAT.SoundsDAT.count)) - return v - -def type_sounds(stage, bin, data=None): - """Sounds""" - if data == None: - return -1 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 255: - raise - except: - raise PyMSError('Parameter',"Invalid Sounds value '%s', it must be a number in the range 0 to 255" % data) - return v - -def type_signalid(stage, bin, data=None): - """SignalID""" - if data == None: - return 1 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 255: - raise - except: - raise PyMSError('Parameter',"Invalid SignalID value '%s', it must be a number in the range 0 to 255" % data) - return v - -def type_weapon(stage, bin, data=None): - """Weapon""" - if data == None: - return 1 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 255: - raise - except: - raise PyMSError('Parameter',"Invalid Weapon value '%s', it must be 1 for ground attack or not 1 for air attack." % data) - return v - -def type_weaponid(stage, bin, data=None): - """WeaponID""" - if data == None: - return 1 - if stage == 1: - return (str(data),'%s (%s)' % (DAT.DATA_CACHE['Weapons.txt'][data], TBL.decompile_string(bin.tbl.strings[bin.weaponsdat.get_value(data,'Label')-1][:-1]))) - try: - v = int(data) - if 0 > v or v > DAT.WeaponsDAT.count: - raise - except: - raise PyMSError('Parameter',"Invalid WeaponID value '%s', it must be a number in the range 0 to %s" % (data,DAT.WeaponsDAT.count)) - return v - -def type_speed(stage, bin, data=None): - """Speed""" - if data == None: - return 2 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 65535: - raise - except: - raise PyMSError('Parameter',"Invalid Speed value '%s', it must be a number in the range 0 to 65535" % data) - return v - -def type_gasoverlay(stage, bin, data=None): - """GasOverlay""" - if data == None: - return 1 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 255: - raise - except: - raise PyMSError('Parameter',"Invalid GasOverlay value '%s', it must be a number in the range 0 to 255" % data) # restrictions? - return v - -def type_short(stage, bin, data=None): - """Short""" - if data == None: - return 2 - if stage == 1: - return (str(data),'') - try: - v = int(data) - if 0 > v or v > 65535: - raise - except: - raise PyMSError('Parameter',"Invalid Short value '%s', it must be a number in the range 0 to 65535" % data) - return v - -ENTRY_TYPES = {0:2,1:2,2:4,12:14,13:14,14:16,15:16,20:22,21:22,23:24,24:26,26:28,27:28,28:28,29:28} - -HEADER = [ - ('Init',), - ('Death',), - ('GndAttkInit',), - ('AirAttkInit',), - ('Unused1',), - ('GndAttkRpt',), - ('AirAttkRpt',), - ('CastSpell',), - ('GndAttkToIdle',), - ('AirAttkToIdle',), - ('Unused2',), - ('Walking',), - ('WalkingToIdle',), - ('SpecialState1',), - ('SpecialState2',), - ('AlmostBuilt',), - ('Built',), - ('Landing',), - ('LiftOff',), - ('IsWorking',), - ('WorkingToIdle',), - ('WarpIn',), - ('Unused3',), - ('StarEditInit',), - ('Disable',), - ('Burrow',), - ('UnBurrow',), - ('Enable',), -] - -OPCODES = [ - [('playfram',), [type_frame]], #0 - [('playframtile',), [type_frame]], - [('sethorpos',), [type_sbyte]], - [('setvertpos',), [type_sbyte]], - [('setpos',), [type_sbyte,type_sbyte]], - [('wait',), [type_byte]], - [('waitrand',), [type_byte,type_byte]], - [('goto',), [type_label]], #7 - [('imgol',), [type_imageid,type_sbyte,type_sbyte]], - [('imgul',), [type_imageid,type_sbyte,type_sbyte]], - [('imgolorig',), [type_imageid]], - [('switchul',), [type_imageid]], - [('__0c',), []], - [('imgoluselo',), [type_imageid,type_sbyte,type_sbyte]], - [('imguluselo',), [type_imageid,type_sbyte,type_sbyte]], - [('sprol',), [type_spriteid,type_sbyte,type_sbyte]], - [('highsprol',), [type_spriteid,type_sbyte,type_sbyte]], - [('lowsprul',), [type_spriteid,type_sbyte,type_sbyte]], - [('uflunstable',), [type_flingy]], - [('spruluselo',), [type_spriteid,type_sbyte,type_sbyte]], - [('sprul',), [type_spriteid,type_sbyte,type_sbyte]], - [('sproluselo',), [type_spriteid,type_overlayid]], - [('end',), []], #22 - [('setflipstate',), [type_flipstate]], - [('playsnd',), [type_soundid]], - [('playsndrand',), [type_sounds,type_soundid]], - [('playsndbtwn',), [type_soundid,type_soundid]], - [('domissiledmg',), []], - [('attackmelee',), [type_sounds,type_soundid]], - [('followmaingraphic',), []], - [('randcondjmp',), [type_byte,type_label]], #30 - [('turnccwise',), [type_byte]], - [('turncwise',), [type_byte]], - [('turnlcwise',), []], - [('turnrand',), [type_byte]], - [('setspawnframe',), [type_byte]], - [('sigorder',), [type_signalid]], - [('attackwith',), [type_weapon]], - [('attack',), []], - [('castspell',), []], - [('useweapon',), [type_weaponid]], - [('move',), [type_byte]], - [('gotorepeatattk',), []], - [('engframe',), [type_bframe]], - [('engset',), [type_frame]], - [('__2d',), []], - [('nobrkcodestart',), []], - [('nobrkcodeend',), []], - [('ignorerest',), []], - [('attkshiftproj',), [type_byte]], - [('tmprmgraphicstart',), []], - [('tmprmgraphicend',), []], - [('setfldirect',), [type_byte]], - [('call',), [type_label]], #53 - [('return',), []], #54 - [('setflspeed',), [type_speed]], - [('creategasoverlays',), [type_gasoverlay]], - [('pwrupcondjmp',), [type_label]], #57 - [('trgtrangecondjmp',), [type_short,type_label]], # - [('trgtarccondjmp',), [type_short,type_short,type_label]], # - [('curdirectcondjmp',), [type_short,type_short,type_label]], # - [('imgulnextid',), [type_sbyte,type_sbyte]], - [('__3e',), []], - [('liftoffcondjmp',), [type_label]], - [('warpoverlay',), [type_frame]], - [('orderdone',), [type_signalid]], - [('grdsprol',), [type_spriteid,type_sbyte,type_sbyte]], - [('__43',), []], - [('dogrddamage',), []], -] - -REV_HEADER = {'IsId':-2,'Type':-1} -for h,n in enumerate(HEADER): - for name in n: - REV_HEADER[name] = h - -REV_OPCODES = {} -for o,c in enumerate(OPCODES): - for name in c[0]: - REV_OPCODES[name] = o - -class IScriptBIN: - def __init__(self, weaponsdat=None, flingydat=None, imagesdat=None, spritesdat=None, soundsdat=None, stat_txt=None, imagestbl=None, sfxdatatbl=None): - if weaponsdat == None: - weaponsdat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'weapons.dat') - if flingydat == None: - flingydat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'flingy.dat') - if imagesdat == None: - imagesdat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'images.dat') - if spritesdat == None: - spritesdat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'sprites.dat') - if soundsdat == None: - soundsdat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'sfxdata.dat') - if stat_txt == None: - stat_txt = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'stat_txt.tbl') - if imagestbl == None: - imagestbl = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'images.tbl') - if sfxdatatbl == None: - sfxdatatbl = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'sfxdata.tbl') - self.headers = {} - self.offsets = {} - self.code = odict() - self.extrainfo = {} - if isinstance(stat_txt, TBL.TBL): - self.tbl = stat_txt - else: - self.tbl = TBL.TBL() - self.tbl.load_file(stat_txt) - if isinstance(weaponsdat, DAT.WeaponsDAT): - self.weaponsdat = weaponsdat - else: - self.weaponsdat = DAT.WeaponsDAT(self.tbl) - self.weaponsdat.load_file(weaponsdat) - if isinstance(flingydat, DAT.FlingyDAT): - self.flingydat = flingydat - else: - self.flingydat = DAT.FlingyDAT(self.tbl) - self.flingydat.load_file(flingydat) - if isinstance(imagesdat, DAT.ImagesDAT): - self.imagesdat = imagesdat - else: - self.imagesdat = DAT.ImagesDAT(self.tbl) - self.imagesdat.load_file(imagesdat) - if isinstance(spritesdat, DAT.SpritesDAT): - self.spritesdat = spritesdat - else: - self.spritesdat = DAT.SpritesDAT() - self.spritesdat.load_file(spritesdat) - if isinstance(soundsdat, DAT.SoundsDAT): - self.soundsdat = soundsdat - else: - self.soundsdat = DAT.SoundsDAT() - self.soundsdat.load_file(soundsdat) - if isinstance(imagestbl, TBL.TBL): - self.imagestbl = imagestbl - else: - self.imagestbl = TBL.TBL() - self.imagestbl.load_file(imagestbl) - if isinstance(sfxdatatbl, TBL.TBL): - self.sfxdatatbl = sfxdatatbl - else: - self.sfxdatatbl = TBL.TBL() - self.sfxdatatbl.load_file(sfxdatatbl) - - def load_file(self, file): - try: - if isstr(file): - f = open(file,'rb') - data = f.read() - f.close() - else: - data = file.read() - except: - raise PyMSError('Load',"Could not load iscript.bin '%s'" % file) - try: - headers = {} - offsets = {} - code = {} - extrainfo = {} - entry_tblstart = struct.unpack('= len(OPCODES): - raise PyMSError('Load','Invalid command, could possibly be a corrrupt iscript.bin') - cmd = [c] - params = OPCODES[c][1] - if params: - firstparam = params[0](0, self) - if firstparam > 0: - for param in params: - p = param(0, self) - cmd.append(struct.unpack(['','B',' 1: - m = re.match('\\A\\s*##GRP:\\s+(.+?)\\s*\\Z', l, re.I) - if checkframes and m: - if not state in [0,4]: - warnings.append(PyMSWarning('Interpreting','The special "##GRP:" extension must be used inside a scripts header. This is being treated simply as a comment.'),n,line) - else: - if self.grpframes: - self.grpframes = min(self.grpframes,checkframes(m.group(1))) - else: - self.grpframes = checkframes(m.group(1)) - continue - m = re.match('\\A\\s*##Name:\\s+(.+?)\\s*\\Z', l, re.I) - if m: - if state != 3: - warnings.append(PyMSWarning('Interpreting','The special "##Name:" extension must be used inside a scripts header after the IsId header descriptor. This is being treated simply as a comment.'),n,line) - elif ':' in m.group(1): - warnings.append(PyMSWarning('Interpreting','Names defined with the special "##Name:" extension can not contain a colon (":"). This is being treated simply as a comment.'),n,line) - else: - extrainfo[id] = m.group(1) - continue - line = l.strip().split('#',1)[0] - # print line - if line: - if re.match('\\A\\.headerstart\\s*\\Z',line): - if not state in [0,4]: - raise PyMSError('Interpreting','Unexpected ".headerstart"',n,line, warnings=warnings) - state = 1 - header = [] - self.grpframes = None - elif re.match('\\A\\.headerend\\s*\\Z',line): - if state != 3: - raise PyMSError('Interpreting','Unexpected ".headerend"',n,line, warnings=warnings) - if len(header[3]) != ENTRY_TYPES[header[1]]: - raise PyMSError('Interpreting','Unexpected ".headerend", expected a "%s" header descriptor' % HEADER[len(header[3])][0],n,line, warnings=warnings) - state = 4 - headers[header[0]] = header[1:] - elif state == 1: - m = re.match('\\AIsId\\s+(.+)\\s*\\Z',line) - if not m: - raise PyMSError('Interpreting', 'Expected "IsId" header descriptor',n,line, warnings=warnings) - try: - id = int(m.group(1)) - if 0 > id or id > 65535: - raise - except: - raise PyMSError('Interpreting', 'Invalid IScript ID, must be a number in te range 0 to 65535',n,line, warnings=warnings) - header.append(id) - state = 2 - elif state == 2: - m = re.match('\\AType\\s+(.+)\\s*\\Z',line) - if not m: - raise PyMSError('Interpreting', 'Expected "Type" header descriptor',n,line, warnings=warnings) - try: - type = int(m.group(1)) - if not type in ENTRY_TYPES: - raise - except: - raise PyMSError('Interpreting', 'Invalid Type value, must be one of the numbers: %s' % ', '.join([str(n) for n in ENTRY_TYPES.keys()])) - header.extend([type,0,[]]) - state = 3 - elif state == 3: - if len(header[3]) == ENTRY_TYPES[header[1]]: - raise PyMSError('Interpreting', 'Expected ".headerend"',n,line, warnings=warnings) - m = re.match('\\A(\\S+)\\s+(\\S+)\\s*\\Z',line) - if not m.group(1) in HEADER[len(header[3])]: - raise PyMSError('Interpreting', 'Expected "%s" header descriptor' % HEADER[len(header[3])][0],n,line, warnings=warnings) - label = m.group(2) - if label in labels: - header[3].append(labels[label]) - else: - if label != '[NONE]': - if not label in findlabels: - findlabels[label] = [(header,len(header[3]))] - else: - findlabels[label].append((header,len(header[3]))) - header[3].append(None) - elif state == 4: - m = re.match('\\A(\\S+):\\s*\\Z', line) - if m: - label = m.group(1) - if label in labels: - raise PyMSError('Interpreting', 'Duplicate label name "%s"' % label,n,line, warnings=warnings) - labels[label] = offset - if label in findlabels: - #print label - for d in findlabels[label]: - #print d - if isinstance(d, tuple): - d[0][3][d[1]] = offset - if not offset in offsets: - offsets[offset] = [[d[0][0],d[1]]] - elif not [d[0][0],d[1]] in offsets[offset]: - offsets[offset].append([d[0][0],d[1]]) - else: - d[0][d[1]] = offset - if not offset in offsets: - offsets[offset] = [d[2]] - elif not d[2] in offsets[offset]: - offsets[offset].append(d[2]) - del findlabels[label] - elif flowthrough < 1: - unused.append(label) - else: - m = re.match('\\A\\t*(\\S+)(?:\\s+(.+?))?\\s*\\Z', line) - if m: - opcode,dat = m.group(1),[] - if m.group(2): - dat = re.split('\\s+',m.group(2)) - if not opcode in REV_OPCODES: - raise PyMSError('Interpreting', 'Unknown opcode "%s"' % opcode,n,line, warnings=warnings) - c = REV_OPCODES[opcode] - if c in [7,22,54]: - flowthrough = -1 - cmd = [c] - code[offset] = cmd - params = OPCODES[c][1] - if params: - if params[0](0,self) < 0: - if not dat: - raise PyMSError('Interpreting','Incorrect amount of parameters (need at least 1)',n,line, warnings=warnings) - offset += interpret_params(cmd,params[0],dat[0]) - if len(dat)-1 != cmd[-1]: - raise PyMSError('Interpreting','Incorrect amount of parameters (got %s, needed %s)' % (len(dat)-1, cmd[-1]),n,line, warnings=warnings) - for v in dat[1:]: - offset += interpret_params(cmd,params[1],v) - else: - if params and len(dat) != len(params): - raise PyMSError('Interpreting','Incorrect amount of parameters (got %s, needed %s)' % (len(dat), len(params)),n,line, warnings=warnings) - if not params and dat: - raise PyMSError('Interpreting','Command requires no parameters, but got %s' % len(dat),n,line, warnings=warnings) - if params and dat: - for d,p in zip(dat,params): - if p == type_label: - if not d in labels: - if not d in findlabels: - findlabels[d] = [[cmd,len(cmd),id]] - else: - findlabels[d].append([cmd,len(cmd),id]) - cmd.append(None) - else: - cmd.append(labels[d]) - if not labels[d] in offsets: - offsets[labels[d]] = [id] - elif not id in offsets[labels[d]]: - offsets[labels[d]].append(id) - if d in unused: - unused.remove(d) - offset += p(0, self) - continue - offset += interpret_params(cmd,p,d) - offset += 1 - else: - raise PyMSError('Interpreting','Invalid syntax, unknown line format',n,line, warnings=warnings) - flowthrough += 1 - if state == 1: - raise PyMSError('Interpreting', 'Unexpected end of file, expected "IsId" header descriptor',n,line, warnings=warnings) - if state == 2: - raise PyMSError('Interpreting', 'Unexpected end of file, expected "Type" header descriptor',n,line, warnings=warnings) - if state == 3: - raise PyMSError('Interpreting', 'Unexpected end of file, expected "%s" header descriptor' % HEADER[len(header[3])][0],n,line, warnings=warnings) - if findlabels: - raise PyMSError('Interpreting', "The label '%s' was not found" % findlabels.getkey(0),n,line, warnings=warnings) - if unused: - for l in unused: - warnings.append(PyMSWarning('Interpeting', "The label '%s' is unused, label is discarded" % l)) - r = odict(code, sorted(code.keys())) - self.remove_code(labels[l], code=r, offsets=offsets) - code = r.dict - # print 'Headers: ' + pprint(headers) - # print 'Offsets: ' + pprint(offsets) - # print 'Code : ' + pprint(code) - # print 'Labels : ' + pprint(labels) - # print 'FLabels: ' + pprint(findlabels) - for id in headers.keys(): - if id in self.headers: - for o in self.headers[id][2]: - if o != None and o in self.offsets: - self.remove_code(o,id) - self.headers[id] = headers[id] - for o,i in offsets.iteritems(): - self.offsets[o] = i - c = dict(self.code.dict) - for o,cmd in code.iteritems(): - c[o] = cmd - k = c.keys() - k.sort() - self.code = odict(c,k) - self.extrainfo.update(extrainfo) - return warnings - - def decompile(self, file, reference=False, ids=None): - if isstr(file): - try: - f = open(file, 'w') - except: - raise PyMSError('Decompile',"Could not load file '%s'" % file, warnings=warnings) - else: - f = file - if ids == None: - ids = self.header.keys() - longheader = max([len(h[0]) for h in HEADER] + [13]) + 1 - longopcode = max([len(o[0][0]) for o in OPCODES] + [13]) + 1 - warnings = [] - labels = {} - completed = [] - def setlabel(o,local,entry): - entry = re.sub('[\\/\\(\\)-]','_',entry.replace(' ','').replace("'",'')) - f = [] - for i in self.offsets[o]: - if isinstance(i,list) and i[0] == id: - f.append(i[1]) - if f: - f.sort() - labels[o] = entry + HEADER[f[0]][0] - return 0 - labels[o] = entry + 'Local' + str(local).zfill(2) - return 1 - def decompile_offset(o,code,local,id): - if id in self.extrainfo: - entry = self.extrainfo[id].replace(' ','_') - elif id < len(DAT.DATA_CACHE['IscriptIDList.txt']): - entry = DAT.DATA_CACHE['IscriptIDList.txt'][id] - else: - entry = 'Unnamed Custom Entry' - if not o in completed: - completed.append(o) - if not o in labels: - labels[o] = re.sub('[\\/\\(\\)-]','_',entry.replace(' ','').replace("'",'')) + 'Local%s' % local - local += 1 - code += labels[o] + ':\n' - curcmd = self.code.index(o) - # print '\t%s' % o - donext = [] - while True: - # print curcmd - co = self.code.getkey(curcmd) - # print co - if co in self.offsets and not co in labels: - local += setlabel(co,local,entry) - completed.append(co) - code += '%s:\n' % labels[co] - cmd = self.code.getitem(curcmd) - c = OPCODES[cmd[0]][0][0] - if cmd[0] == 7: - if not cmd[1] in labels: - local += setlabel(cmd[1],local,entry) - code += ' %s%s %s\n\n' % (c,' ' * (longopcode-len(c)),labels[cmd[1]]) - code,local,curcmd = decompile_offset(cmd[1],code,local,id) - break - elif cmd[0] in [22,54]: #end,return - code += ' %s\n\n' % c - break - elif cmd[0] in [30,53,57,58,59,60,63]: #randcondjump,call,pwrupcondjmp,trgtrangecondjmp,trgtarccondjmp,curdirectcondjmp,liftoffcondjump - if cmd[-1] not in labels: - local += setlabel(cmd[-1],local,entry) - if not cmd[-1] in donext: - donext.append(cmd[-1]) - extra = [] - code += ' %s%s ' % (c,' ' * (longopcode-len(c))) - for v,t in zip(cmd[1:-1],OPCODES[cmd[0]][1][:-1]): - p,e = t(1,self,v) - if e: - extra.append(e) - code += p + ' ' - code += labels[cmd[-1]] - if extra: - code = code[:-1] + '\t# ' + ' | '.join(extra) - code += '\n' - curcmd += 1 - else: - extra = [] - code += ' ' + c - if OPCODES[cmd[0]][1]: - code += ' ' * (longopcode-len(c)) + '\t' - d,o = cmd[1:],OPCODES[cmd[0]][1] - if OPCODES[cmd[0]][1][0](0,self) == -1: - n = OPCODES[cmd[0]][1][0](1,self,d[0])[0] - code += n + ' ' - o = [OPCODES[cmd[0]][1][1]] * int(n) - del d[0] - for v,t in zip(d,o): - p,e = t(1,self,v) - if e: - extra.append(e) - code += p + ' ' - if extra: - code = code[:-1] + '\t# ' + ' | '.join(extra) - code += '\n' - curcmd += 1 - if donext: - for d in donext: - code,local,curcmd = decompile_offset(d,code,local,id) - return (code,local,curcmd) - return (code,local,-1) - usedby = {} - for i in range(DAT.ImagesDAT.count): - id = self.imagesdat.get_value(i, 'IscriptID') - if id in ids: - if not id in usedby: - usedby[id] = '# This header is used by images.dat entries:\n' - usedby[id] += '# %s %s (%s)\n' % (str(i).zfill(3), DAT.DATA_CACHE['Images.txt'][i], TBL.decompile_string(self.imagestbl.strings[self.imagesdat.get_value(i,'GRPFile')-1][:-1])) - invalid = [] - unknownid = 0 - for id in ids: - code = '' - local = 0 - if not id in self.headers: - invalid.append(id) - continue - type, offset, header = self.headers[id] - u = '' - if id in usedby: - u = usedby[id] - f.write('# ----------------------------------------------------------------------------- #\n%s.headerstart\nIsId %s\nType %s\n' % (u, id, type)) - for o,l in zip(header,HEADER[:ENTRY_TYPES[type]]): - if o == None: - label = '[NONE]' - elif o in labels: - label = labels[o] - else: - if id in self.extrainfo: - entry = self.extrainfo[id].replace(' ','_') - elif id < len(DAT.DATA_CACHE['IscriptIDList.txt']): - entry = DAT.DATA_CACHE['IscriptIDList.txt'][id] - else: - entry = 'Unnamed Custom Entry' - local += setlabel(o,local,entry) - label = labels[o]#'%s%s' % (DAT.DATA_CACHE['IscriptIDList.txt'][id],l[0]) - f.write('%s%s %s\n' % (l[0],' ' * (longheader-len(l[0])),label)) - if o != None: - code,local,curcmd = decompile_offset(o,code,local,id) - if id in self.extrainfo: - f.write('##Name: %s\n' % self.extrainfo[id]) - f.write('.headerend\n# ----------------------------------------------------------------------------- #\n\n' + code) - f.close() - - def compile(self, file): - try: - f = open(file, 'wb') - except: - raise PyMSError('Compile',"Could not load file '%s'" % file) - code = '' - offsets = {} - offset = 1372 - for o,cmd in self.code.iteritems(): - offsets[o] = offset - #print sum([1] + [p(0,self) for p in OPCODES[cmd[0]][1]]) - offset += 1 - ps = OPCODES[cmd[0]][1] - if ps: - p1 = ps[0](0,self) - if p1 < 0: - offset += sum([-p1,ps[1](0,self)*cmd[1]]) - else: - offset += sum([p(0,self) for p in ps]) - for o,cmd in self.code.iteritems(): - ps = OPCODES[cmd[0]][1] - if ps: - p1 = ps[0](0,self) - if p1 < 0: - code += struct.pack(' v or v > 65535: + raise + if bin and bin.grpframes and v > bin.grpframes: + raise PyMSWarning('Parameter',"'%s' is an invalid frame for one or more of the GRP's specified in the header, and may cause a crash (max frame value is %s, or frameset 0x%02x)" % (data, bin.grpframes, (bin.grpframes - 17) / 17 * 17),extra=v) + if bin and bin.grpframes and v > bin.grpframes - 17: + raise PyMSWarning('Parameter',"'%s' is an invalid frameset for one or more of the GRP's specified in the header, and may cause a crash (max frame value is %s, or frameset 0x%02x)" % (data, bin.grpframes , (bin.grpframes - 17) / 17 * 17),extra=v) + except PyMSWarning: + raise + except: + raise PyMSError('Parameter',"Invalid Frame value '%s', it must be a number in the range 0 to 65535 in decimal or hexidecimal (framesets are increments of 17: 17 or 0x11, 34 or 0x22, 51 or 0x33, etc.)" % data) + return v + +def type_bframe(stage, bin, data=None): + """BFrame""" + if data == None: + return 1 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 255: + raise + except: + raise PyMSError('Parameter',"Invalid BFrame value '%s', it must be a number in the range 0 to 256" % data) + return v + +def type_byte(stage, bin, data=None): + """Byte""" + if data == None: + return 1 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 255: + raise + except: + raise PyMSError('Parameter',"Invalid Byte value '%s', it must be a number in the range 0 to 255" % data) + return v + +def type_sbyte(stage, bin, data=None): + """SByte""" + if data == None: + return 1 + if stage == 1: + if data > 127: + data = -(256 - data) + return (str(data),'') + try: + v = int(data) + if -128 > v or v > 127: + raise + if v < 0: + v = 256 + v + except: + raise PyMSError('Parameter',"Invalid SByte value '%s', it must be a number in the range -128 to 127" % data) + return v + +def type_label(stage, bin): + """Label""" + return 2 + +def type_imageid(stage, bin, data=None): + """ImageID""" + if data == None: + return 2 + if stage == 1: + return (str(data),'%s (%s)' % (DAT.DATA_CACHE['Images.txt'][data], TBL.decompile_string(bin.imagestbl.strings[bin.imagesdat.get_value(data,'GRPFile')-1][:-1]))) + try: + v = int(data) + if 0 > v or v > DAT.ImagesDAT.count: + raise + except: + raise PyMSError('Parameter',"Invalid ImageID value '%s', it must be a number in the range 0 to %s" % (data,DAT.ImagesDAT.count)) + return v + +def type_spriteid(stage, bin, data=None): + """SpriteID""" + if data == None: + return 2 + if stage == 1: + return (str(data),'%s (%s)' % (DAT.DATA_CACHE['Sprites.txt'][data], TBL.decompile_string(bin.imagestbl.strings[bin.imagesdat.get_value(bin.spritesdat.get_value(data,'ImageFile'),'GRPFile')-1][:-1]))) + try: + v = int(data) + if 0 > v or v > DAT.SpritesDAT.count: + raise + except: + raise PyMSError('Parameter',"Invalid SpriteID value '%s', it must be a number in the range 0 to %s" % (data,DAT.SpritesDAT.count)) + return v + +def type_flingy(stage, bin, data=None): + """FlingyID""" + if data == None: + return 2 + if stage == 1: + return (str(data),'%s (%s)' % (DAT.DATA_CACHE['Flingy.txt'][data], TBL.decompile_string(bin.imagestbl.strings[bin.imagesdat.get_value(bin.spritesdat.get_value(bin.flingydat.get_value(data,'Sprite'),'ImageFile'),'GRPFile')-1][:-1]))) + try: + v = int(data) + if 0 > v or v > DAT.FlingyDAT.count: + raise + except: + raise PyMSError('Parameter',"Invalid FlingyID value '%s', it must be a number in the range 0 to %s" % (data,DAT.FlingyDAT.count)) + return v + +def type_overlayid(stage, bin, data=None): + """OverlayID""" + if data == None: + return 1 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 255: + raise + except: + raise PyMSError('Parameter',"Invalid OverlayID value '%s', it must be a number in the range 0 to 255" % data) + return v # Restrictions? + # /"Overlay 1" renamed to "Attack" + # /"Overlay 2" renamed to "HP Damage" + # /"Overlay 3" renamed to "Special" + # /"Overlay 4" renamed to "Landing Dust" + # /"Overlay 5" renamed to "Lift-off Dust" + +def type_flipstate(stage, bin, data=None): + """FlipState""" + if data == None: + return 1 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 255: + raise + except: + raise PyMSError('Parameter',"Invalid FlipState value '%s', it must be a number in the range 0 to 255" % data) + return v + +def type_soundid(stage, bin, data=None): + """SoundID""" + if data == None: + return 2 + if stage == 1: + return (str(data), TBL.decompile_string(bin.sfxdatatbl.strings[bin.soundsdat.get_value(data,'SoundFile')-1][:-1])) + try: + v = int(data) + if 0 > v or v > DAT.SoundsDAT.count: + raise + except: + raise PyMSError('Parameter',"Invalid SoundID value '%s', it must be a number in the range 0 to %s" % (data,DAT.SoundsDAT.count)) + return v + +def type_sounds(stage, bin, data=None): + """Sounds""" + if data == None: + return -1 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 255: + raise + except: + raise PyMSError('Parameter',"Invalid Sounds value '%s', it must be a number in the range 0 to 255" % data) + return v + +def type_signalid(stage, bin, data=None): + """SignalID""" + if data == None: + return 1 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 255: + raise + except: + raise PyMSError('Parameter',"Invalid SignalID value '%s', it must be a number in the range 0 to 255" % data) + return v + +def type_weapon(stage, bin, data=None): + """Weapon""" + if data == None: + return 1 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 255: + raise + except: + raise PyMSError('Parameter',"Invalid Weapon value '%s', it must be 1 for ground attack or not 1 for air attack." % data) + return v + +def type_weaponid(stage, bin, data=None): + """WeaponID""" + if data == None: + return 1 + if stage == 1: + return (str(data),'%s (%s)' % (DAT.DATA_CACHE['Weapons.txt'][data], TBL.decompile_string(bin.tbl.strings[bin.weaponsdat.get_value(data,'Label')-1][:-1]))) + try: + v = int(data) + if 0 > v or v > DAT.WeaponsDAT.count: + raise + except: + raise PyMSError('Parameter',"Invalid WeaponID value '%s', it must be a number in the range 0 to %s" % (data,DAT.WeaponsDAT.count)) + return v + +def type_speed(stage, bin, data=None): + """Speed""" + if data == None: + return 2 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 65535: + raise + except: + raise PyMSError('Parameter',"Invalid Speed value '%s', it must be a number in the range 0 to 65535" % data) + return v + +def type_gasoverlay(stage, bin, data=None): + """GasOverlay""" + if data == None: + return 1 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 255: + raise + except: + raise PyMSError('Parameter',"Invalid GasOverlay value '%s', it must be a number in the range 0 to 255" % data) # restrictions? + return v + +def type_short(stage, bin, data=None): + """Short""" + if data == None: + return 2 + if stage == 1: + return (str(data),'') + try: + v = int(data) + if 0 > v or v > 65535: + raise + except: + raise PyMSError('Parameter',"Invalid Short value '%s', it must be a number in the range 0 to 65535" % data) + return v + +ENTRY_TYPES = {0:2,1:2,2:4,12:14,13:14,14:16,15:16,20:22,21:22,23:24,24:26,26:28,27:28,28:28,29:28} + +HEADER = [ + ('Init',), + ('Death',), + ('GndAttkInit',), + ('AirAttkInit',), + ('Unused1',), + ('GndAttkRpt',), + ('AirAttkRpt',), + ('CastSpell',), + ('GndAttkToIdle',), + ('AirAttkToIdle',), + ('Unused2',), + ('Walking',), + ('WalkingToIdle',), + ('SpecialState1',), + ('SpecialState2',), + ('AlmostBuilt',), + ('Built',), + ('Landing',), + ('LiftOff',), + ('IsWorking',), + ('WorkingToIdle',), + ('WarpIn',), + ('Unused3',), + ('StarEditInit',), + ('Disable',), + ('Burrow',), + ('UnBurrow',), + ('Enable',), +] + +OPCODES = [ + [('playfram',), [type_frame]], #0 + [('playframtile',), [type_frame]], + [('sethorpos',), [type_sbyte]], + [('setvertpos',), [type_sbyte]], + [('setpos',), [type_sbyte,type_sbyte]], + [('wait',), [type_byte]], + [('waitrand',), [type_byte,type_byte]], + [('goto',), [type_label]], #7 + [('imgol',), [type_imageid,type_sbyte,type_sbyte]], + [('imgul',), [type_imageid,type_sbyte,type_sbyte]], + [('imgolorig',), [type_imageid]], + [('switchul',), [type_imageid]], + [('__0c',), []], + [('imgoluselo',), [type_imageid,type_sbyte,type_sbyte]], + [('imguluselo',), [type_imageid,type_sbyte,type_sbyte]], + [('sprol',), [type_spriteid,type_sbyte,type_sbyte]], + [('highsprol',), [type_spriteid,type_sbyte,type_sbyte]], + [('lowsprul',), [type_spriteid,type_sbyte,type_sbyte]], + [('uflunstable',), [type_flingy]], + [('spruluselo',), [type_spriteid,type_sbyte,type_sbyte]], + [('sprul',), [type_spriteid,type_sbyte,type_sbyte]], + [('sproluselo',), [type_spriteid,type_overlayid]], + [('end',), []], #22 + [('setflipstate',), [type_flipstate]], + [('playsnd',), [type_soundid]], + [('playsndrand',), [type_sounds,type_soundid]], + [('playsndbtwn',), [type_soundid,type_soundid]], + [('domissiledmg',), []], + [('attackmelee',), [type_sounds,type_soundid]], + [('followmaingraphic',), []], + [('randcondjmp',), [type_byte,type_label]], #30 + [('turnccwise',), [type_byte]], + [('turncwise',), [type_byte]], + [('turnlcwise',), []], + [('turnrand',), [type_byte]], + [('setspawnframe',), [type_byte]], + [('sigorder',), [type_signalid]], + [('attackwith',), [type_weapon]], + [('attack',), []], + [('castspell',), []], + [('useweapon',), [type_weaponid]], + [('move',), [type_byte]], + [('gotorepeatattk',), []], + [('engframe',), [type_bframe]], + [('engset',), [type_frame]], + [('__2d',), []], + [('nobrkcodestart',), []], + [('nobrkcodeend',), []], + [('ignorerest',), []], + [('attkshiftproj',), [type_byte]], + [('tmprmgraphicstart',), []], + [('tmprmgraphicend',), []], + [('setfldirect',), [type_byte]], + [('call',), [type_label]], #53 + [('return',), []], #54 + [('setflspeed',), [type_speed]], + [('creategasoverlays',), [type_gasoverlay]], + [('pwrupcondjmp',), [type_label]], #57 + [('trgtrangecondjmp',), [type_short,type_label]], # + [('trgtarccondjmp',), [type_short,type_short,type_label]], # + [('curdirectcondjmp',), [type_short,type_short,type_label]], # + [('imgulnextid',), [type_sbyte,type_sbyte]], + [('__3e',), []], + [('liftoffcondjmp',), [type_label]], + [('warpoverlay',), [type_frame]], + [('orderdone',), [type_signalid]], + [('grdsprol',), [type_spriteid,type_sbyte,type_sbyte]], + [('__43',), []], + [('dogrddamage',), []], +] + +REV_HEADER = {'IsId':-2,'Type':-1} +for h,n in enumerate(HEADER): + for name in n: + REV_HEADER[name] = h + +REV_OPCODES = {} +for o,c in enumerate(OPCODES): + for name in c[0]: + REV_OPCODES[name] = o + +class IScriptBIN: + def __init__(self, weaponsdat=None, flingydat=None, imagesdat=None, spritesdat=None, soundsdat=None, stat_txt=None, imagestbl=None, sfxdatatbl=None): + if weaponsdat == None: + weaponsdat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'weapons.dat') + if flingydat == None: + flingydat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'flingy.dat') + if imagesdat == None: + imagesdat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'images.dat') + if spritesdat == None: + spritesdat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'sprites.dat') + if soundsdat == None: + soundsdat = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'arr', 'sfxdata.dat') + if stat_txt == None: + stat_txt = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'stat_txt.tbl') + if imagestbl == None: + imagestbl = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'images.tbl') + if sfxdatatbl == None: + sfxdatatbl = os.path.join(BASE_DIR, 'Libs', 'MPQ', 'rez', 'sfxdata.tbl') + self.headers = {} + self.offsets = {} + self.code = odict() + self.extrainfo = {} + if isinstance(stat_txt, TBL.TBL): + self.tbl = stat_txt + else: + self.tbl = TBL.TBL() + self.tbl.load_file(stat_txt) + if isinstance(weaponsdat, DAT.WeaponsDAT): + self.weaponsdat = weaponsdat + else: + self.weaponsdat = DAT.WeaponsDAT(self.tbl) + self.weaponsdat.load_file(weaponsdat) + if isinstance(flingydat, DAT.FlingyDAT): + self.flingydat = flingydat + else: + self.flingydat = DAT.FlingyDAT(self.tbl) + self.flingydat.load_file(flingydat) + if isinstance(imagesdat, DAT.ImagesDAT): + self.imagesdat = imagesdat + else: + self.imagesdat = DAT.ImagesDAT(self.tbl) + self.imagesdat.load_file(imagesdat) + if isinstance(spritesdat, DAT.SpritesDAT): + self.spritesdat = spritesdat + else: + self.spritesdat = DAT.SpritesDAT() + self.spritesdat.load_file(spritesdat) + if isinstance(soundsdat, DAT.SoundsDAT): + self.soundsdat = soundsdat + else: + self.soundsdat = DAT.SoundsDAT() + self.soundsdat.load_file(soundsdat) + if isinstance(imagestbl, TBL.TBL): + self.imagestbl = imagestbl + else: + self.imagestbl = TBL.TBL() + self.imagestbl.load_file(imagestbl) + if isinstance(sfxdatatbl, TBL.TBL): + self.sfxdatatbl = sfxdatatbl + else: + self.sfxdatatbl = TBL.TBL() + self.sfxdatatbl.load_file(sfxdatatbl) + + def load_file(self, file): + try: + if isstr(file): + f = open(file,'rb') + data = f.read() + f.close() + else: + data = file.read() + except: + raise PyMSError('Load',"Could not load iscript.bin '%s'" % file) + try: + headers = {} + offsets = {} + code = {} + extrainfo = {} + entry_tblstart = struct.unpack('= len(OPCODES): + raise PyMSError('Load','Invalid command, could possibly be a corrrupt iscript.bin') + cmd = [c] + params = OPCODES[c][1] + if params: + firstparam = params[0](0, self) + if firstparam > 0: + for param in params: + p = param(0, self) + cmd.append(struct.unpack(['','B',' 1: + m = re.match('\\A\\s*##GRP:\\s+(.+?)\\s*\\Z', l, re.I) + if checkframes and m: + if not state in [0,4]: + warnings.append(PyMSWarning('Interpreting','The special "##GRP:" extension must be used inside a scripts header. This is being treated simply as a comment.'),n,line) + else: + if self.grpframes: + self.grpframes = min(self.grpframes,checkframes(m.group(1))) + else: + self.grpframes = checkframes(m.group(1)) + continue + m = re.match('\\A\\s*##Name:\\s+(.+?)\\s*\\Z', l, re.I) + if m: + if state != 3: + warnings.append(PyMSWarning('Interpreting','The special "##Name:" extension must be used inside a scripts header after the IsId header descriptor. This is being treated simply as a comment.'),n,line) + elif ':' in m.group(1): + warnings.append(PyMSWarning('Interpreting','Names defined with the special "##Name:" extension can not contain a colon (":"). This is being treated simply as a comment.'),n,line) + else: + extrainfo[id] = m.group(1) + continue + line = l.strip().split('#',1)[0] + # print(line) + if line: + if re.match('\\A\\.headerstart\\s*\\Z',line): + if not state in [0,4]: + raise PyMSError('Interpreting','Unexpected ".headerstart"',n,line, warnings=warnings) + state = 1 + header = [] + self.grpframes = None + elif re.match('\\A\\.headerend\\s*\\Z',line): + if state != 3: + raise PyMSError('Interpreting','Unexpected ".headerend"',n,line, warnings=warnings) + if len(header[3]) != ENTRY_TYPES[header[1]]: + raise PyMSError('Interpreting','Unexpected ".headerend", expected a "%s" header descriptor' % HEADER[len(header[3])][0],n,line, warnings=warnings) + state = 4 + headers[header[0]] = header[1:] + elif state == 1: + m = re.match('\\AIsId\\s+(.+)\\s*\\Z',line) + if not m: + raise PyMSError('Interpreting', 'Expected "IsId" header descriptor',n,line, warnings=warnings) + try: + id = int(m.group(1)) + if 0 > id or id > 65535: + raise + except: + raise PyMSError('Interpreting', 'Invalid IScript ID, must be a number in te range 0 to 65535',n,line, warnings=warnings) + header.append(id) + state = 2 + elif state == 2: + m = re.match('\\AType\\s+(.+)\\s*\\Z',line) + if not m: + raise PyMSError('Interpreting', 'Expected "Type" header descriptor',n,line, warnings=warnings) + try: + type = int(m.group(1)) + if not type in ENTRY_TYPES: + raise + except: + raise PyMSError('Interpreting', 'Invalid Type value, must be one of the numbers: %s' % ', '.join([str(n) for n in ENTRY_TYPES.keys()])) + header.extend([type,0,[]]) + state = 3 + elif state == 3: + if len(header[3]) == ENTRY_TYPES[header[1]]: + raise PyMSError('Interpreting', 'Expected ".headerend"',n,line, warnings=warnings) + m = re.match('\\A(\\S+)\\s+(\\S+)\\s*\\Z',line) + if not m.group(1) in HEADER[len(header[3])]: + raise PyMSError('Interpreting', 'Expected "%s" header descriptor' % HEADER[len(header[3])][0],n,line, warnings=warnings) + label = m.group(2) + if label in labels: + header[3].append(labels[label]) + else: + if label != '[NONE]': + if not label in findlabels: + findlabels[label] = [(header,len(header[3]))] + else: + findlabels[label].append((header,len(header[3]))) + header[3].append(None) + elif state == 4: + m = re.match('\\A(\\S+):\\s*\\Z', line) + if m: + label = m.group(1) + if label in labels: + raise PyMSError('Interpreting', 'Duplicate label name "%s"' % label,n,line, warnings=warnings) + labels[label] = offset + if label in findlabels: + #print(label) + for d in findlabels[label]: + #print(d) + if isinstance(d, tuple): + d[0][3][d[1]] = offset + if not offset in offsets: + offsets[offset] = [[d[0][0],d[1]]] + elif not [d[0][0],d[1]] in offsets[offset]: + offsets[offset].append([d[0][0],d[1]]) + else: + d[0][d[1]] = offset + if not offset in offsets: + offsets[offset] = [d[2]] + elif not d[2] in offsets[offset]: + offsets[offset].append(d[2]) + del findlabels[label] + elif flowthrough < 1: + unused.append(label) + else: + m = re.match('\\A\\t*(\\S+)(?:\\s+(.+?))?\\s*\\Z', line) + if m: + opcode,dat = m.group(1),[] + if m.group(2): + dat = re.split('\\s+',m.group(2)) + if not opcode in REV_OPCODES: + raise PyMSError('Interpreting', 'Unknown opcode "%s"' % opcode,n,line, warnings=warnings) + c = REV_OPCODES[opcode] + if c in [7,22,54]: + flowthrough = -1 + cmd = [c] + code[offset] = cmd + params = OPCODES[c][1] + if params: + if params[0](0,self) < 0: + if not dat: + raise PyMSError('Interpreting','Incorrect amount of parameters (need at least 1)',n,line, warnings=warnings) + offset += interpret_params(cmd,params[0],dat[0]) + if len(dat)-1 != cmd[-1]: + raise PyMSError('Interpreting','Incorrect amount of parameters (got %s, needed %s)' % (len(dat)-1, cmd[-1]),n,line, warnings=warnings) + for v in dat[1:]: + offset += interpret_params(cmd,params[1],v) + else: + if params and len(dat) != len(params): + raise PyMSError('Interpreting','Incorrect amount of parameters (got %s, needed %s)' % (len(dat), len(params)),n,line, warnings=warnings) + if not params and dat: + raise PyMSError('Interpreting','Command requires no parameters, but got %s' % len(dat),n,line, warnings=warnings) + if params and dat: + for d,p in zip(dat,params): + if p == type_label: + if not d in labels: + if not d in findlabels: + findlabels[d] = [[cmd,len(cmd),id]] + else: + findlabels[d].append([cmd,len(cmd),id]) + cmd.append(None) + else: + cmd.append(labels[d]) + if not labels[d] in offsets: + offsets[labels[d]] = [id] + elif not id in offsets[labels[d]]: + offsets[labels[d]].append(id) + if d in unused: + unused.remove(d) + offset += p(0, self) + continue + offset += interpret_params(cmd,p,d) + offset += 1 + else: + raise PyMSError('Interpreting','Invalid syntax, unknown line format',n,line, warnings=warnings) + flowthrough += 1 + if state == 1: + raise PyMSError('Interpreting', 'Unexpected end of file, expected "IsId" header descriptor',n,line, warnings=warnings) + if state == 2: + raise PyMSError('Interpreting', 'Unexpected end of file, expected "Type" header descriptor',n,line, warnings=warnings) + if state == 3: + raise PyMSError('Interpreting', 'Unexpected end of file, expected "%s" header descriptor' % HEADER[len(header[3])][0],n,line, warnings=warnings) + if findlabels: + raise PyMSError('Interpreting', "The label '%s' was not found" % findlabels.getkey(0),n,line, warnings=warnings) + if unused: + for l in unused: + warnings.append(PyMSWarning('Interpeting', "The label '%s' is unused, label is discarded" % l)) + r = odict(code, sorted(code.keys())) + self.remove_code(labels[l], code=r, offsets=offsets) + code = r.dict + # print('Headers: ') + pprint(headers) + # print('Offsets: ') + pprint(offsets) + # print('Code : ') + pprint(code) + # print('Labels : ') + pprint(labels) + # print('FLabels: ') + pprint(findlabels) + for id in headers.keys(): + if id in self.headers: + for o in self.headers[id][2]: + if o != None and o in self.offsets: + self.remove_code(o,id) + self.headers[id] = headers[id] + for o,i in offsets.items(): + self.offsets[o] = i + c = dict(self.code.dict) + for o,cmd in code.items(): + c[o] = cmd + k = c.keys() + k.sort() + self.code = odict(c,k) + self.extrainfo.update(extrainfo) + return warnings + + def decompile(self, file, reference=False, ids=None): + if isstr(file): + try: + f = open(file, 'w') + except: + raise PyMSError('Decompile',"Could not load file '%s'" % file, warnings=warnings) + else: + f = file + if ids == None: + ids = self.header.keys() + longheader = max([len(h[0]) for h in HEADER] + [13]) + 1 + longopcode = max([len(o[0][0]) for o in OPCODES] + [13]) + 1 + warnings = [] + labels = {} + completed = [] + def setlabel(o,local,entry): + entry = re.sub('[\\/\\(\\)-]','_',entry.replace(' ','').replace("'",'')) + f = [] + for i in self.offsets[o]: + if isinstance(i,list) and i[0] == id: + f.append(i[1]) + if f: + f.sort() + labels[o] = entry + HEADER[f[0]][0] + return 0 + labels[o] = entry + 'Local' + str(local).zfill(2) + return 1 + def decompile_offset(o,code,local,id): + if id in self.extrainfo: + entry = self.extrainfo[id].replace(' ','_') + elif id < len(DAT.DATA_CACHE['IscriptIDList.txt']): + entry = DAT.DATA_CACHE['IscriptIDList.txt'][id] + else: + entry = 'Unnamed Custom Entry' + if not o in completed: + completed.append(o) + if not o in labels: + labels[o] = re.sub('[\\/\\(\\)-]','_',entry.replace(' ','').replace("'",'')) + 'Local%s' % local + local += 1 + code += labels[o] + ':\n' + curcmd = self.code.index(o) + # print('\t%s') % o + donext = [] + while True: + # print(curcmd) + co = self.code.getkey(curcmd) + # print(co) + if co in self.offsets and not co in labels: + local += setlabel(co,local,entry) + completed.append(co) + code += '%s:\n' % labels[co] + cmd = self.code.getitem(curcmd) + c = OPCODES[cmd[0]][0][0] + if cmd[0] == 7: + if not cmd[1] in labels: + local += setlabel(cmd[1],local,entry) + code += ' %s%s %s\n\n' % (c,' ' * (longopcode-len(c)),labels[cmd[1]]) + code,local,curcmd = decompile_offset(cmd[1],code,local,id) + break + elif cmd[0] in [22,54]: #end,return + code += ' %s\n\n' % c + break + elif cmd[0] in [30,53,57,58,59,60,63]: #randcondjump,call,pwrupcondjmp,trgtrangecondjmp,trgtarccondjmp,curdirectcondjmp,liftoffcondjump + if cmd[-1] not in labels: + local += setlabel(cmd[-1],local,entry) + if not cmd[-1] in donext: + donext.append(cmd[-1]) + extra = [] + code += ' %s%s ' % (c,' ' * (longopcode-len(c))) + for v,t in zip(cmd[1:-1],OPCODES[cmd[0]][1][:-1]): + p,e = t(1,self,v) + if e: + extra.append(e) + code += p + ' ' + code += labels[cmd[-1]] + if extra: + code = code[:-1] + '\t# ' + ' | '.join(extra) + code += '\n' + curcmd += 1 + else: + extra = [] + code += ' ' + c + if OPCODES[cmd[0]][1]: + code += ' ' * (longopcode-len(c)) + '\t' + d,o = cmd[1:],OPCODES[cmd[0]][1] + if OPCODES[cmd[0]][1][0](0,self) == -1: + n = OPCODES[cmd[0]][1][0](1,self,d[0])[0] + code += n + ' ' + o = [OPCODES[cmd[0]][1][1]] * int(n) + del d[0] + for v,t in zip(d,o): + p,e = t(1,self,v) + if e: + extra.append(e) + code += p + ' ' + if extra: + code = code[:-1] + '\t# ' + ' | '.join(extra) + code += '\n' + curcmd += 1 + if donext: + for d in donext: + code,local,curcmd = decompile_offset(d,code,local,id) + return (code,local,curcmd) + return (code,local,-1) + usedby = {} + for i in range(DAT.ImagesDAT.count): + id = self.imagesdat.get_value(i, 'IscriptID') + if id in ids: + if not id in usedby: + usedby[id] = '# This header is used by images.dat entries:\n' + usedby[id] += '# %s %s (%s)\n' % (str(i).zfill(3), DAT.DATA_CACHE['Images.txt'][i], TBL.decompile_string(self.imagestbl.strings[self.imagesdat.get_value(i,'GRPFile')-1][:-1])) + invalid = [] + unknownid = 0 + for id in ids: + code = '' + local = 0 + if not id in self.headers: + invalid.append(id) + continue + type, offset, header = self.headers[id] + u = '' + if id in usedby: + u = usedby[id] + f.write('# ----------------------------------------------------------------------------- #\n%s.headerstart\nIsId %s\nType %s\n' % (u, id, type)) + for o,l in zip(header,HEADER[:ENTRY_TYPES[type]]): + if o == None: + label = '[NONE]' + elif o in labels: + label = labels[o] + else: + if id in self.extrainfo: + entry = self.extrainfo[id].replace(' ','_') + elif id < len(DAT.DATA_CACHE['IscriptIDList.txt']): + entry = DAT.DATA_CACHE['IscriptIDList.txt'][id] + else: + entry = 'Unnamed Custom Entry' + local += setlabel(o,local,entry) + label = labels[o]#'%s%s' % (DAT.DATA_CACHE['IscriptIDList.txt'][id],l[0]) + f.write('%s%s %s\n' % (l[0],' ' * (longheader-len(l[0])),label)) + if o != None: + code,local,curcmd = decompile_offset(o,code,local,id) + if id in self.extrainfo: + f.write('##Name: %s\n' % self.extrainfo[id]) + f.write('.headerend\n# ----------------------------------------------------------------------------- #\n\n' + code) + f.close() + + def compile(self, file): + try: + f = open(file, 'wb') + except: + raise PyMSError('Compile',"Could not load file '%s'" % file) + code = '' + offsets = {} + offset = 1372 + for o,cmd in self.code.items(): + offsets[o] = offset + #print(sum)([1] + [p(0,self) for p in OPCODES[cmd[0]][1]]) + offset += 1 + ps = OPCODES[cmd[0]][1] + if ps: + p1 = ps[0](0,self) + if p1 < 0: + offset += sum([-p1,ps[1](0,self)*cmd[1]]) + else: + offset += sum([p(0,self) for p in ps]) + for o,cmd in self.code.items(): + ps = OPCODES[cmd[0]][1] + if ps: + p1 = ps[0](0,self) + if p1 < 0: + code += struct.pack(' 256 or ymax > 256 or planes != 1): - raise PyMSError('Load', "Unsupported special palette (PCX) file '%s'" % file) - palette = [] - for x in range(0,768,3): - if x == 765: - c = list(struct.unpack('3B', data[-768+x:])) - else: - c = list(struct.unpack('3B', data[-768+x:-765+x])) - palette.append(c) - image = [[]] - x = 128 - while x < len(data) - 769: - c = ord(data[x]) - x += 1 - if c & 192 == 192: - image[-1].extend([ord(data[x])] * (63 & c)) - x += 1 - else: - image[-1].append(c) - if len(image[-1]) > xmax: - image.append(image[-1][xmax:]) - image[-2] = image[-2][:xmax] - elif len(image[-1]) == xmax and len(image) < ymax: - image.append([]) - # print """\ -# --- %s --------------- -# x min/max : %s %s -# y min/max : %s %s -# h/v dpi : %s %s -# planes : %s -# bytes/plane : %s -# palinfo : %s -# h/v screen size: %s %s -# palette : %s""" % (file,xmin,xmax,ymin,ymax,hdpi,vdpi,planes,bytesperline,palinfo,hscreensize,vscreensize,palette) - for y in image: - if len(y) < xmax: - y.extend([0]*(xmax-len(y))) - self.width,self.height,self.palette,self.image = xmax,ymax,palette,image - except PyMSError: - raise - except: - raise PyMSError('Load',"Unsupported PCX file '%s', could possibly be corrupt" % file) - - def load_data(self, image, palette=None): - self.height = len(image) - self.width = len(image[0]) - if palette: - self.palette = list(palette) - self.image = [list(y) for y in image] - - def save_file(self, file): - try: - f = open(file,'wb') - except: - raise PyMSError('Save',"Could not save PCX to file '%s'" % file) - f.write('\x0A\x05\x01\x08' + struct.pack('<6H49xB4H54x', 0, 0, self.width-1, self.height-1, 72, 72, 1, int(math.ceil(self.width / 2.0) * 2), 0, 0, 0)) - for y in self.image: - last = y[0] - repeat = 1 - for index in y[1:]: - if index == last: - if repeat == 63: - f.write('\xFF%c' % index) - repeat = 1 - else: - repeat += 1 - else: - if repeat > 1: - f.write('%c%c' % (repeat | 0xC0, last)) - elif last >= 192: - f.write('\xC1%c' % last) - else: - f.write(chr(last)) - last = index - repeat = 1 - if repeat > 1: - f.write('%c%c' % (repeat | 0xC0, last)) - elif last >= 192: - f.write('\xC1%c' % last) - else: - f.write(chr(last)) - f.write('\x0C' + ''.join(struct.pack('3B',*c) for c in self.palette)) - f.close() - -# import sys -# sys.stdout = open('stdeo.txt','w') -# import BMP -# p = PCX() -#p.load_file(r'C:\Documents and Settings\Administrator\Desktop\SCScrnShot_021909_220704.pcx') -# p.load_file(r'C:\Documents and Settings\Administrator\Desktop\test.pcx') -# b = BMP.BMP() -# b.load_data(p.image, p.palette) -# b.save_file(r'C:\Documents and Settings\Administrator\Desktop\test.bmp') -# p = PCX() -# b = BMP.BMP() -# for f in ['ticon','bfire','gfire','ofire']: - # p.load_file(f + '.pcx') - # # for l in p.image: - # # print len(l) - # # print '-----' - # b.load_data(p.image, p.palette) - # b.save_file(f + '.bmp') +from .utils import * + +import struct, math + +# This class is designed for StarCraft PCX's, there is no guarantee it works with other PCX files +class PCX: + def __init__(self, palette=[]): + self.width = 0 + self.height = 0 + self.palette = palette + self.image = [] + + def load_file(self, file, pal=False): + try: + if isstr(file): + f = open(file,'rb') + data = f.read() + f.close() + else: + data = file.read() + except: + raise PyMSError('Load',"Could not load the PCX '%s'" % file) + if data[:4] != '\x0A\x05\x01\x08': + raise PyMSError('Load',"'%s' is not a PCX file (no PCX header)" % file) + try: + xmin,ymin,xmax,ymax,hdpi,vdpi = struct.unpack('<6H',data[4:16]) + planes,bytesperline,palinfo,hscreensize,vscreensize = struct.unpack(' 256 or ymax > 256 or planes != 1): + raise PyMSError('Load', "Unsupported special palette (PCX) file '%s'" % file) + palette = [] + for x in range(0,768,3): + if x == 765: + c = list(struct.unpack('3B', data[-768+x:])) + else: + c = list(struct.unpack('3B', data[-768+x:-765+x])) + palette.append(c) + image = [[]] + x = 128 + while x < len(data) - 769: + c = ord(data[x]) + x += 1 + if c & 192 == 192: + image[-1].extend([ord(data[x])] * (63 & c)) + x += 1 + else: + image[-1].append(c) + if len(image[-1]) > xmax: + image.append(image[-1][xmax:]) + image[-2] = image[-2][:xmax] + elif len(image[-1]) == xmax and len(image) < ymax: + image.append([]) + # print("")"\ +# --- %s --------------- +# x min/max : %s %s +# y min/max : %s %s +# h/v dpi : %s %s +# planes : %s +# bytes/plane : %s +# palinfo : %s +# h/v screen size: %s %s +# palette : %s""" % (file,xmin,xmax,ymin,ymax,hdpi,vdpi,planes,bytesperline,palinfo,hscreensize,vscreensize,palette) + for y in image: + if len(y) < xmax: + y.extend([0]*(xmax-len(y))) + self.width,self.height,self.palette,self.image = xmax,ymax,palette,image + except PyMSError: + raise + except: + raise PyMSError('Load',"Unsupported PCX file '%s', could possibly be corrupt" % file) + + def load_data(self, image, palette=None): + self.height = len(image) + self.width = len(image[0]) + if palette: + self.palette = list(palette) + self.image = [list(y) for y in image] + + def save_file(self, file): + try: + f = open(file,'wb') + except: + raise PyMSError('Save',"Could not save PCX to file '%s'" % file) + f.write('\x0A\x05\x01\x08' + struct.pack('<6H49xB4H54x', 0, 0, self.width-1, self.height-1, 72, 72, 1, int(math.ceil(self.width / 2.0) * 2), 0, 0, 0)) + for y in self.image: + last = y[0] + repeat = 1 + for index in y[1:]: + if index == last: + if repeat == 63: + f.write('\xFF%c' % index) + repeat = 1 + else: + repeat += 1 + else: + if repeat > 1: + f.write('%c%c' % (repeat | 0xC0, last)) + elif last >= 192: + f.write('\xC1%c' % last) + else: + f.write(chr(last)) + last = index + repeat = 1 + if repeat > 1: + f.write('%c%c' % (repeat | 0xC0, last)) + elif last >= 192: + f.write('\xC1%c' % last) + else: + f.write(chr(last)) + f.write('\x0C' + ''.join(struct.pack('3B',*c) for c in self.palette)) + f.close() + +# import sys +# sys.stdout = open('stdeo.txt','w') +# import BMP +# p = PCX() +#p.load_file(r'C:\Documents and Settings\Administrator\Desktop\SCScrnShot_021909_220704.pcx') +# p.load_file(r'C:\Documents and Settings\Administrator\Desktop\test.pcx') +# b = BMP.BMP() +# b.load_data(p.image, p.palette) +# b.save_file(r'C:\Documents and Settings\Administrator\Desktop\test.bmp') +# p = PCX() +# b = BMP.BMP() +# for f in ['ticon','bfire','gfire','ofire']: + # p.load_file(f + '.pcx') + # # for l in p.image: + # # print(len)(l) + # # print('-----') + # b.load_data(p.image, p.palette) + # b.save_file(f + '.bmp') diff --git a/tools/Libs/TBL.py b/tools/Libs/TBL.py index 2a4dad2..944e7b5 100644 --- a/tools/Libs/TBL.py +++ b/tools/Libs/TBL.py @@ -1,4 +1,4 @@ -from utils import * +from .utils import * import struct, re @@ -87,7 +87,7 @@ def special_chr(o): if -1 > c or 255 < c: return o.group(0) return chr(c) - return re.sub('<(\d+)>', special_chr, string) + return re.sub(r'<(\\d+)>', special_chr, string) def decompile_string(string, exclude='', include=''): def special_chr(o): @@ -117,7 +117,7 @@ def load_file(self, file): findlen = {} for x in offsets: findlen[x] = 1 - findlen = findlen.keys() + [len(data)] + findlen = list(findlen.keys()) + [len(data)] findlen.sort() strings = [] for o in offsets: diff --git a/tools/Libs/setutils.py b/tools/Libs/setutils.py index 20f5c80..cff1059 100644 --- a/tools/Libs/setutils.py +++ b/tools/Libs/setutils.py @@ -1,758 +1,762 @@ -from utils import * -from Libs import PCX,PAL,TBL,AIBIN,DAT,IScriptBIN - -from Tkinter import * -from tkMessageBox import * - -import re, os - -def check_update(p): - settings = loadsettings('PyMS',{'remindme':1}) - if settings['remindme'] == 1 or settings['remindme'] != PyMS_LONG_VERSION: - try: - d = urllib.urlopen('http://www.broodwarai.com/PyMS/update.txt').read() - except: - return - if len(d) == 3: - d = tuple(ord(l) for l in d) - if PyMS_VERSION != d and d[0] > 0 and d[0] < 4: - def callback(): - UpdateDialog(p,'v%s.%s.%s' % d,settings) - p.after(1, callback) - -def loadsize(window, settings, setting, full=False): - set = settings[setting] - f = set.endswith('^') - if f: - set = set[:-1] - window.geometry(set) - window.update_idletasks() - cur = window.winfo_geometry() - if set != cur: - def parsegeom(g): - s = g.split('+',1)[0].split('x') - return (int(s[0]),int(s[1])) - sets = parsegeom(set) - curs = parsegeom(cur) - window.geometry('%sx%s+%s' % (sets[0] + (sets[0] - curs[0]),sets[1] + (sets[1] - curs[1]),set.split('+',1)[1])) - if f and full: - try: - window.wm_state('zoomed') - except: - pass - -def savesize(window, settings, setting='window'): - z = ['','^'][window.wm_state() == 'zoomed'] - if z: - window.wm_state('normal') - settings[setting] = window.winfo_geometry() + z - -def loadsettings(program, default={}): - settings = default - try: - settings.update(eval(file(os.path.join(BASE_DIR,'Settings','%s.txt' % program), 'r').read(),{})) - except IOError, e: - if e.args[0] != 2: - raise - return settings - -def pprint(obj, depth=0, max=2): - depth += 1 - string = '' - if isinstance(obj, dict) and max: - if obj: - string += '{\\\n' - for key in obj: - string += '%s%s:' % ('\t'*depth, repr(key)) - string += pprint(obj[key], depth, max-1) - string += '%s},\\\n' % ('\t'*(depth-1)) - else: - string += '{},\\\n' - elif isinstance(obj, list) and max: - if obj: - string += '[\\\n' - for item in obj: - string += ('%s' % ('\t'*depth)) - string += pprint(item, depth, max-1) - string += '%s],\\\n' % ('\t'*(depth-1)) - else: - string += '[],\\\n' - else: - string += '%s,\\\n' % (repr(obj),) - if depth == 1: - return string[:-3] - return string - -class UpdateDialog(PyMSDialog): - def __init__(self, parent, v, settings=[]): - self.version = v - self.settings = settings - PyMSDialog.__init__(self, parent, 'New Version Found') - - def widgetize(self): - self.resizable(False, False) - Label(self, justify=LEFT, anchor=W, text="Your version of PyMS (%s) is older then the current version (%s).\nIt is recommended that you update as soon as possible." % (PyMS_LONG_VERSION,self.version)).pack(pady=5,padx=5) - f = Frame(self) - self.remind = IntVar() - self.remind.set(self.settings.get('remindme',1) == 1 or self.settings.get('remindme') != PyMS_LONG_VERSION) - Checkbutton(f, text='Remind me later', variable=self.remind).pack(side=LEFT, padx=5) - Hotlink(f, 'Homepage', self.homepage).pack(side=RIGHT, padx=5) - f.pack(fill=X, expand=1) - ok = Button(self, text='Ok', width=10, command=self.ok) - ok.pack(pady=5) - return ok - - def homepage(self, e=None): - webbrowser.open('http://www.broodwarai.com/index.php?page=pyms') - - def ok(self): - self.settings['remindme'] = [PyMS_LONG_VERSION,1][self.remind.get()] - try: - f = file(os.path.join(BASE_DIR,'Settings','PyMS.txt'),'w') - f.write(pprint(self.settings)) - f.close() - except: - pass - PyMSDialog.ok(self) - -class BadFile: - def __init__(self, file): - self.file = file - - def __str__(self): - return self.file - - def __nonzero__(self): - return False - -class MPQHandler: - def __init__(self, mpqs=[], listfiles=None): - self.mpqs = list(mpqs) - if listfiles == None: - self.listfiles = [os.path.join(BASE_DIR,'Libs','Data','Listfile.txt')] - else: - self.listfiles = listfiles - self.handles = {} - self.open = False - MpqInitialize() - - def clear(self): - if self.open: - self.close_mpqs() - self.mpqs = [] - - def add_defaults(self): - if SC_DIR: - for f in ['StarDat','BrooDat','Patch_rt']: - p = os.path.join(SC_DIR, '%s%smpq' % (f,os.extsep)) - if os.path.exists(p) and not p in self.mpqs: - h = SFileOpenArchive(p) - if not SFInvalidHandle(h): - SFileCloseArchive(h) - self.mpqs.append(p) - - def set_mpqs(self, mpqs): - if self.open: - raise PyMSError('MPQ','Cannot set mpqs when the current mpqs are open.') - self.mpqs = list(mpqs) - - def open_mpqs(self): - missing = [[],[]] - if not FOLDER: - handles = {} - self.open = True - for p,m in enumerate(self.mpqs): - if not os.path.exists(m): - missing[0].append(m) - continue - handles[m] = SFileOpenArchive(m, p) - if SFInvalidHandle(handles[m]): - missing[1].append(m) - elif self.open == True: - self.open = handles[m] - self.handles = handles - return missing - - def missing(self, missing): - t = '' - if missing[0]: - t = 'Could not find:\n\t' + '\n\t'.join(missing[0]) - if missing[1]: - t += 'Error loading:\n\t' + '\n\t'.join(missing[1]) - return t - - def close_mpqs(self): - self.open = False - for h in self.handles.values(): - if not SFInvalidHandle(h): - SFileCloseArchive(h) - - # folder(True)=Get only from folder,folder(None)=Get from either, MPQ first, folder second,folder(False)=Get only from MPQ - def get_file(self, path, folder=None): - mpq = path.startswith('MPQ:') - if mpq: - op = path - path = path[4:].split('\\') - if not FOLDER and not folder and mpq: - if self.open == False: - self.open_mpqs() - close = True - else: - close = False - if self.open and self.open != True: - f = SFileOpenFileEx(self.open, '\\'.join(path), SFILE_SEARCH_ALL_OPEN) - print f - if not SFInvalidHandle(f): - r = SFileReadFile(f) - SFileCloseFile(f) - print r - p = SFile(r[0], '\\'.join(path)) - return p - if close: - self.close_mpqs() - if folder != False: - if mpq: - p = os.path.join(BASE_DIR, 'Libs', 'MPQ', *path) - if os.path.exists(p): - return open(p, 'rb') - elif os.path.exists(path): - return open(path, 'rb') - if mpq: - return BadFile(op) - return BadFile(path) - - def has_file(self, path, folder=None): - mpq = path.startswith('MPQ:') - if mpq: - path = path[4:].split('\\') - if not FOLDER and not folder and mpq: - if self.open == False: - self.open_mpqs() - close = True - else: - close = False - if self.open and self.open != True: - f = SFileOpenFileEx(self.open, '\\'.join(path), SFILE_SEARCH_ALL_OPEN) - if not SFInvalidHandle(f): - SFileCloseFile(f) - return True - if close: - self.close_mpqs() - if folder != False: - if mpq: - return os.path.exists(os.path.join(BASE_DIR, 'Libs', 'MPQ', *path)) - else: - return os.path.exists(path) - return False - - # Type: 0 = structs, 1 = dict - def list_files(self, type=0, handles=None): - if type == 1: - files = {} - else: - files = [] - if self.mpqs: - if handles == None: - handles = self.handles.values() - elif isinstance(handles, int): - handles = [handles] - if self.open == False: - self.open_mpqs() - close = True - else: - close = False - for h in handles: - for e in SFileListFiles(h, '\r\n'.join(self.listfiles)): - if e.fileExists: - if type == 1: - if not e.fileName in self.files: - self.files[e.fileName] = {} - self.files[e.locale] = e - else: - files.append(e) - if close: - self.close_mpqs() - return files - -class MpqSelect(PyMSDialog): - def __init__(self, parent, mpqhandler, type, search, settings): - self.mpqhandler = mpqhandler - self.search = StringVar() - self.search.set(search) - self.search.trace('w', self.updatesearch) - self.settings = settings - self.regex = IntVar() - self.regex.set(0) - self.files = [] - self.file = None - self.resettimer = None - self.searchtimer = None - PyMSDialog.__init__(self, parent, 'Open a ' + type) - - def widgetize(self): - listframe = Frame(self, bd=2, relief=SUNKEN) - scrollbar = Scrollbar(listframe) - self.listbox = Listbox(listframe, width=35, height=1, bd=0, yscrollcommand=scrollbar.set, exportselection=0, activestyle=DOTBOX) - bind = [ - ('', self.scroll), - ('', lambda a,i=0: self.move(a,i)), - ('', lambda a,i=END: self.move(a,i)), - ('', lambda e,i=0: self.movestring(e,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda e,i=1: self.movestring(e,i)), - ('', lambda a,i=1: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=-10: self.move(a,i)), - ('', lambda a,i=10: self.move(a,i)), - ] - for b in bind: - listframe.bind(*b) - scrollbar.config(command=self.listbox.yview) - scrollbar.pack(side=RIGHT, fill=Y) - self.listbox.pack(side=LEFT, fill=BOTH, expand=1) - listframe.pack(fill=BOTH, padx=1, pady=1, expand=1) - listframe.focus_set() - s = Frame(self) - self.textdrop = TextDropDown(s, self.search, self.settings.get('mpqselecthistory',[])[::-1]) - self.textdrop.entry.c = self.textdrop.entry['bg'] - self.textdrop.pack(side=LEFT, fill=X, padx=1, pady=2) - self.open = Button(s, text='Open', width=10, command=self.ok) - self.open.pack(side=RIGHT, padx=1, pady=3) - s.pack(fill=X) - s = Frame(self) - Radiobutton(s, text='Wildcard', variable=self.regex, value=0, command=self.updatelist).pack(side=LEFT, padx=1, pady=2) - Radiobutton(s, text='Regex', variable=self.regex, value=1, command=self.updatelist).pack(side=LEFT, padx=1, pady=2) - Button(s, text='Cancel', width=10, command=self.cancel).pack(side=RIGHT, padx=1, pady=3) - s.pack(fill=X) - - self.listfiles() - self.updatelist() - - if 'mpqselectwindow' in self.settings: - loadsize(self, self.settings, 'mpqselectwindow', True) - - return self.open - - def scroll(self, e): - if e.delta > 0: - self.listbox.yview('scroll', -2, 'units') - else: - self.listbox.yview('scroll', 2, 'units') - - def move(self, e, a): - if a == END: - a = self.listbox.size()-2 - elif a not in [0,END]: - a = max(min(self.listbox.size()-1, int(self.listbox.curselection()[0]) + a),0) - self.listbox.select_clear(0,END) - self.listbox.select_set(a) - self.listbox.see(a) - - def listfiles(self): - filelists = os.path.join(BASE_DIR,'Libs','Data','Listfile.txt') - self.files = [] - self.mpqhandler.open_mpqs() - for h in self.mpqhandler.handles.values(): - for e in SFileListFiles(h, filelists): - if e.fileName and not e.fileName in self.files: - self.files.append(e.fileName) - self.mpqhandler.close_mpqs() - m = os.path.join(BASE_DIR,'Libs','MPQ','') - for p in os.walk(m): - folder = p[0].replace(m,'') - for f in p[2]: - a = '%s\\%s' % (folder,f) - if not a in self.files: - self.files.append(a) - self.files.sort() - - def updatelist(self): - if self.searchtimer: - self.after_cancel(self.searchtimer) - self.searchtimer = None - self.listbox.delete(0,END) - s = self.search.get() - if not self.regex.get(): - s = '^' + re.escape(s).replace('\\?','.').replace('\\*','.+?') + '$' - try: - r = re.compile(s) - except: - self.resettimer = self.after(1000, self.updatecolor) - self.textdrop.entry['bg'] = '#FFB4B4' - else: - for f in filter(lambda p: r.match(p), self.files): - self.listbox.insert(END,f) - if self.listbox.size(): - self.listbox.select_set(0) - self.open['state'] = NORMAL - else: - self.open['state'] = DISABLED - - def updatecolor(self): - if self.resettimer: - self.after_cancel(self.resettimer) - self.resettimer = None - self.textdrop.entry['bg'] = self.textdrop.entry.c - - def updatesearch(self, *_): - if self.searchtimer: - self.after_cancel(self.searchtimer) - self.searchtimer = self.after(200, self.updatelist) - - def cancel(self): - savesize(self, self.settings, 'mpqselectwindow') - PyMSDialog.ok(self) - - def ok(self): - savesize(self, self.settings, 'mpqselectwindow') - f = self.listbox.get(self.listbox.curselection()[0]) - self.file = 'MPQ:' + f - if not 'mpqselecthistory' in self.settings: - self.settings['mpqselecthistory'] = [] - if f in self.settings['mpqselecthistory']: - self.settings['mpqselecthistory'].remove(f) - self.settings['mpqselecthistory'].append(f) - if len(self.settings['mpqselecthistory']) > 10: - del self.settings['mpqselecthistory'][0] - PyMSDialog.ok(self) - -class MPQSettings(Frame): - def __init__(self, parent, mpqs, settings, setdlg=None): - if setdlg == None: - self.setdlg = parent.parent - else: - self.setdlg = setdlg - self.mpqs = list(mpqs) - self.settings = settings - Frame.__init__(self, parent) - Label(self, text='MPQ Settings:', font=('Courier', -12, 'bold'), anchor=W).pack(fill=X) - Label(self, text="Files will be read from the highest priority MPQ that contains them.\nThe higher an MPQ is on the list the higher its priority.", anchor=W, justify=LEFT).pack(fill=X) - self.listframe = Frame(self, bd=2, relief=SUNKEN) - scrollbar = Scrollbar(self.listframe) - self.listbox = Listbox(self.listframe, width=35, height=1, bd=0, yscrollcommand=scrollbar.set, exportselection=0, activestyle=DOTBOX) - bind = [ - ('', self.scroll), - ('', lambda a,i=0: self.move(a,i)), - ('', lambda a,i=END: self.move(a,i)), - ('', lambda e,i=0: self.movestring(e,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda e,i=1: self.movestring(e,i)), - ('', lambda a,i=1: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=-10: self.move(a,i)), - ('', lambda a,i=10: self.move(a,i)), - ] - for b in bind: - self.listframe.bind(*b) - scrollbar.config(command=self.listbox.yview) - scrollbar.pack(side=RIGHT, fill=Y) - self.listbox.pack(side=LEFT, fill=BOTH, expand=1) - self.listframe.pack(fill=BOTH, padx=1, pady=1, expand=1) - for mpq in self.mpqs: - self.listbox.insert(0,mpq) - if self.listbox.size(): - self.listbox.select_set(0) - - buttons = [ - ('add', self.add, 'Add MPQ (Insert)', NORMAL, 'Insert', LEFT), - ('remove', self.remove, 'Remove MPQ (Delete)', DISABLED, 'Delete', LEFT), - ('opendefault', self.adddefault, "Add default StarCraft MPQ's (Shift+Insert)", NORMAL, 'Shift+Insert', LEFT), - ('up', lambda e=None,i=0: self.movempq(e,i), 'Move MPQ Up (Shift+Up)', DISABLED, 'Shift+Up', RIGHT), - ('down', lambda e=None,i=1: self.movempq(e,i), 'Move MPQ Down (Shift+Down)', DISABLED, 'Shift+Down', RIGHT), - ] - self.buttons = {} - toolbar = Frame(self) - for btn in buttons: - if isinstance(btn, tuple): - image = PhotoImage(file=os.path.join(BASE_DIR,'Images','%s.gif' % btn[0])) - button = Button(toolbar, image=image, width=20, height=20, command=btn[1], state=btn[3]) - button.image = image - button.tooltip = Tooltip(button, btn[2]) - button.pack(side=btn[5], padx=[0,10][btn[0] == 'opendefault']) - self.buttons[btn[0]] = button - a = btn[4] - if a: - if not a.startswith('F'): - self.bind('<%s%s>' % (a[:-1].replace('Ctrl','Control').replace('+','-'), a[-1].lower()), btn[1]) - else: - self.bind('<%s>' % a, btn[1]) - else: - Frame(toolbar, width=btn).pack(side=LEFT) - toolbar.pack(fill=X, padx=51, pady=1) - - self.action_states() - - def activate(self): - self.listframe.focus_set() - - def action_states(self): - select = [NORMAL,DISABLED][not self.listbox.curselection()] - for btn in ['remove','up','down']: - self.buttons[btn]['state'] = select - - def scroll(self, e): - if e.delta > 0: - self.listbox.yview('scroll', -2, 'units') - else: - self.listbox.yview('scroll', 2, 'units') - - def move(self, e, a): - if a == END: - a = self.listbox.size()-2 - elif a not in [0,END]: - a = max(min(self.listbox.size()-1, int(self.listbox.curselection()[0]) + a),0) - self.listbox.select_clear(0,END) - self.listbox.select_set(a) - self.listbox.see(a) - - def movempq(self, key=None, dir=0): - if key and self.buttons[['up','down'][dir]]['state'] != NORMAL: - return - i = int(self.listbox.curselection()[0]) - if i == [0,self.listbox.size()-1][dir]: - return - s = self.listbox.get(i) - n = i + [-1,1][dir] - self.mpqs[i] = self.mpqs[n] - self.mpqs[n] = s - self.listbox.delete(i) - self.listbox.insert(n, s) - self.listbox.select_clear(0, END) - self.listbox.select_set(n) - self.listbox.see(n) - self.setdlg.edited = True - - def select_files(self): - path = self.settings.get('lastpath', BASE_DIR) - file = tkFileDialog.askopenfilename(parent=self, title="Add MPQ's", defaultextension='.mpq', filetypes=[('MPQ Files','*.mpq'),('All Files','*')], initialdir=path, multiple=True) - if file: - self.settings['lastpath'] = os.path.dirname(file[0]) - return file - - def add(self, key=None, add=None): - if add == None: - n,s = 0,0 - add = self.select_files() - else: - n,s = END,self.listbox.size() - if add: - error = [] - for i in add: - if not i in self.mpqs: - h = SFileOpenArchive(i) - if h not in [None,-1]: - SFileCloseFile(h) - self.mpqs.insert([0,-1][n == END],i) - self.listbox.insert(n,i) - self.listbox.select_clear(0,END) - self.listbox.select_set(s) - self.listbox.see(s) - self.action_states() - self.setdlg.edited = True - - def remove(self, key=None): - if key and self.buttons['remove']['state'] != NORMAL: - return - i = int(self.listbox.curselection()[0]) - del self.mpqs[i] - self.listbox.delete(i) - if self.listbox.size(): - i = min(i,self.listbox.size()-1) - self.listbox.select_set(i) - self.listbox.see(i) - self.action_states() - self.setdlg.edited = True - - def adddefault(self, key=None): - if SC_DIR: - a = [] - for f in ['StarDat','BrooDat','Patch_rt']: - p = os.path.join(SC_DIR, '%s%smpq' % (f,os.extsep)) - if os.path.exists(p) and not p in self.mpqs: - a.append(p) - if a: - self.add(add=a) - -class SettingsPanel(Frame): - types = { - 'AIBIN':(AIBIN.AIBIN,'aiscript.bin','bin',[('AI Scripts','*.bin'),('All Files','*')]), - } - - def __init__(self, parent, entries, settings, mpqhandler, setdlg=None): - if setdlg == None: - self.setdlg = parent.parent - else: - self.setdlg = setdlg - self.settings = settings - self.mpqhandler = mpqhandler - self.find = PhotoImage(file=os.path.join(BASE_DIR,'Images','find.gif')) - self.variables = {} - inmpq = False - Frame.__init__(self, parent) - for _ in entries: - if len(_) == 5: - f,e,v,t,c = _ - else: - f,e,v,t = _ - c = None - self.variables[f] = (IntVar(),StringVar(),[]) - v = settings[v] - m = v.startswith('MPQ:') - self.variables[f][0].set(m) - if m: - self.variables[f][1].set(v[4:]) - else: - self.variables[f][1].set(v) - self.variables[f][1].trace('w', self.edited) - datframe = Frame(self) - if isstr(e): - Label(datframe, text=f, font=('Courier', -12, 'bold'), anchor=W).pack(fill=X, expand=1) - Label(datframe, text=e, anchor=W).pack(fill=X, expand=1) - elif e: - Label(datframe, text=f, font=('Courier', -12, 'bold'), anchor=W).pack(fill=X, expand=1) - else: - Label(datframe, text=f, anchor=W).pack(fill=X, expand=1) - entryframe = Frame(datframe) - e = Entry(entryframe, textvariable=self.variables[f][1], state=DISABLED) - b = Button(entryframe, image=self.find, width=20, height=20, command=lambda f=f,t=self.types[t],e=e,c=c: self.setting(f,t,e,c)) - self.variables[f][2].extend([e,b]) - if not t == 'Palette': - inmpq = True - y = Checkbutton(entryframe, text='', variable=self.variables[f][0]) - self.variables[f][2].append(y) - y.pack(side=LEFT) - e.pack(side=LEFT, fill=X, expand=1) - e.xview(END) - b.pack(side=LEFT, padx=1) - entryframe.pack(fill=X, expand=1) - datframe.pack(side=TOP, fill=X) - if inmpq: - Label(self, text='Check the checkbox beside an entry to use a file in the MPQs').pack(fill=X) - - def edited(self, *_): - if hasattr(self.setdlg, 'edited'): - self.setdlg.edited = True - - def select_file(self, t, e, f): - path = self.settings.get('lastpath', BASE_DIR) - file = tkFileDialog.askopenfilename(parent=self, title="Open a " + t, defaultextension='.' + e, filetypes=f, initialdir=path) - if file: - self.settings['lastpath'] = os.path.dirname(file) - return file - - def setting(self, f, t, e, cb): - file = '' - if self.variables[f][0].get(): - m = MpqSelect(self.setdlg, self.mpqhandler, t[1], '*.' + t[2], self.settings) - if m.file: - self.mpqhandler.open_mpqs() - if t[1] == 'FNT': - file = (self.mpqhandler.get_file(m.file, False),self.mpqhandler.get_file(m.file, True)) - else: - file = self.mpqhandler.get_file(m.file) - self.mpqhandler.close_mpqs() - else: - file = self.select_file(t[1],t[2],t[3]) - if file: - c = t[0]() - if t[1] == 'FNT': - try: - c.load_file(file[0]) - self.variables[f][1].set(file[0].file) - except PyMSError: - try: - c.load_file(file[1]) - self.variables[f][1].set(file[0].file) - except PyMSError, err: - ErrorDialog(self.setdlg, err) - return - else: - try: - c.load_file(file) - except PyMSError, e: - ErrorDialog(self.setdlg, e) - return - self.variables[f][1].set(file) - e.xview(END) - if cb: - cb(c) - else: - self.setdlg.edited = True - - def save(self, d, m): - for s in d[1]: - self.setdlg.parent.settings[s[2]] = ['','MPQ:'][self.variables[s[0]][0].get()] + self.variables[s[0]][1].get().replace(m,'MPQ:',1) - -class SettingsDialog(PyMSDialog): - def __init__(self, parent, data, min_size, err=None, mpqs=True): - self.min_size = min_size - self.data = data - self.pages = [] - self.err = err - self.mpqs = mpqs - self.edited = False - PyMSDialog.__init__(self, parent, 'Settings') - - def widgetize(self): - self.minsize(*self.min_size) - if self.data: - self.tabs = Notebook(self) - if self.mpqs: - self.mpqsettings = MPQSettings(self.tabs, self.parent.mpqhandler.mpqs, self.parent.settings) - self.tabs.add_tab(self.mpqsettings, 'MPQ Settings') - for d in self.data: - if isinstance(d[1],list): - self.pages.append(SettingsPanel(self.tabs, d[1], self.parent.settings, self.parent.mpqhandler)) - else: - self.pages.append(d[1](self.tabs)) - self.tabs.add_tab(self.pages[-1], d[0]) - self.tabs.pack(fill=BOTH, expand=1, padx=5, pady=5) - else: - self.mpqsettings = MPQSettings(self, self.parent.mpqhandler.mpqs, self.parent.settings) - self.mpqsettings.pack(fill=BOTH, expand=1, padx=5, pady=5) - btns = Frame(self) - ok = Button(btns, text='Ok', width=10, command=self.ok) - ok.pack(side=LEFT, padx=3, pady=3) - Button(btns, text='Cancel', width=10, command=self.cancel).pack(side=LEFT, padx=3, pady=3) - btns.pack() - if 'settingswindow' in self.parent.settings: - loadsize(self, self.parent.settings, 'settingswindow', True) - if self.err: - self.after(1, self.showerr) - return ok - - def showerr(self): - ErrorDialog(self, self.err) - - def cancel(self): - if self.err and askyesno(parent=self, title='Exit?', message="One or more files required for this program can not be found and must be chosen. Canceling will close the program, do you wish to continue?"): - self.parent.after(1, self.parent.exit) - PyMSDialog.ok(self) - elif not self.edited or askyesno(parent=self, title='Cancel?', message="Are you sure you want to cancel?\nAll unsaved changes will be lost."): - self.parent.settings['settingswindow'] = self.winfo_geometry() + ['','^'][self.wm_state() == 'zoomed'] - PyMSDialog.ok(self) - - def ok(self): - if self.edited: - if self.mpqs: - t = self.parent.mpqhandler.mpqs - self.parent.mpqhandler.set_mpqs(self.mpqsettings.mpqs) - o = dict(self.parent.settings) - m = os.path.join(BASE_DIR,'Libs','MPQ','') - for p,d in zip(self.pages,self.data): - p.save(d,m) - try: - e = self.parent.open_files() - except AttributeError: - pass - else: - if e: - if self.mpqs: - self.parent.mpqhandler.set_mpqs(t) - self.parent.settings = b - ErrorDialog(self, e) - return - savesize(self, self.parent.settings, 'settingswindow') +from .utils import * +from Libs import PCX,PAL,TBL,AIBIN,DAT,IScriptBIN + +try: + from Tkinter import * + from tkMessageBox import * +except ImportError: + from tkinter import * + from tkinter.messagebox import * + +import re, os + +def check_update(p): + settings = loadsettings('PyMS',{'remindme':1}) + if settings['remindme'] == 1 or settings['remindme'] != PyMS_LONG_VERSION: + try: + d = urllib.urlopen('http://www.broodwarai.com/PyMS/update.txt').read() + except: + return + if len(d) == 3: + d = tuple(ord(l) for l in d) + if PyMS_VERSION != d and d[0] > 0 and d[0] < 4: + def callback(): + UpdateDialog(p,'v%s.%s.%s' % d,settings) + p.after(1, callback) + +def loadsize(window, settings, setting, full=False): + set = settings[setting] + f = set.endswith('^') + if f: + set = set[:-1] + window.geometry(set) + window.update_idletasks() + cur = window.winfo_geometry() + if set != cur: + def parsegeom(g): + s = g.split('+',1)[0].split('x') + return (int(s[0]),int(s[1])) + sets = parsegeom(set) + curs = parsegeom(cur) + window.geometry('%sx%s+%s' % (sets[0] + (sets[0] - curs[0]),sets[1] + (sets[1] - curs[1]),set.split('+',1)[1])) + if f and full: + try: + window.wm_state('zoomed') + except: + pass + +def savesize(window, settings, setting='window'): + z = ['','^'][window.wm_state() == 'zoomed'] + if z: + window.wm_state('normal') + settings[setting] = window.winfo_geometry() + z + +def loadsettings(program, default={}): + settings = default + try: + settings.update(eval(file(os.path.join(BASE_DIR,'Settings','%s.txt' % program), 'r').read(),{})) + except IOError as e: + if e.args[0] != 2: + raise + return settings + +def pprint(obj, depth=0, max=2): + depth += 1 + string = '' + if isinstance(obj, dict) and max: + if obj: + string += '{\\\n' + for key in obj: + string += '%s%s:' % ('\t'*depth, repr(key)) + string += pprint(obj[key], depth, max-1) + string += '%s},\\\n' % ('\t'*(depth-1)) + else: + string += '{},\\\n' + elif isinstance(obj, list) and max: + if obj: + string += '[\\\n' + for item in obj: + string += ('%s' % ('\t'*depth)) + string += pprint(item, depth, max-1) + string += '%s],\\\n' % ('\t'*(depth-1)) + else: + string += '[],\\\n' + else: + string += '%s,\\\n' % (repr(obj),) + if depth == 1: + return string[:-3] + return string + +class UpdateDialog(PyMSDialog): + def __init__(self, parent, v, settings=[]): + self.version = v + self.settings = settings + PyMSDialog.__init__(self, parent, 'New Version Found') + + def widgetize(self): + self.resizable(False, False) + Label(self, justify=LEFT, anchor=W, text="Your version of PyMS (%s) is older then the current version (%s).\nIt is recommended that you update as soon as possible." % (PyMS_LONG_VERSION,self.version)).pack(pady=5,padx=5) + f = Frame(self) + self.remind = IntVar() + self.remind.set(self.settings.get('remindme',1) == 1 or self.settings.get('remindme') != PyMS_LONG_VERSION) + Checkbutton(f, text='Remind me later', variable=self.remind).pack(side=LEFT, padx=5) + Hotlink(f, 'Homepage', self.homepage).pack(side=RIGHT, padx=5) + f.pack(fill=X, expand=1) + ok = Button(self, text='Ok', width=10, command=self.ok) + ok.pack(pady=5) + return ok + + def homepage(self, e=None): + webbrowser.open('http://www.broodwarai.com/index.php?page=pyms') + + def ok(self): + self.settings['remindme'] = [PyMS_LONG_VERSION,1][self.remind.get()] + try: + f = file(os.path.join(BASE_DIR,'Settings','PyMS.txt'),'w') + f.write(pprint(self.settings)) + f.close() + except: + pass + PyMSDialog.ok(self) + +class BadFile: + def __init__(self, file): + self.file = file + + def __str__(self): + return self.file + + def __nonzero__(self): + return False + +class MPQHandler: + def __init__(self, mpqs=[], listfiles=None): + self.mpqs = list(mpqs) + if listfiles == None: + self.listfiles = [os.path.join(BASE_DIR,'Libs','Data','Listfile.txt')] + else: + self.listfiles = listfiles + self.handles = {} + self.open = False + MpqInitialize() + + def clear(self): + if self.open: + self.close_mpqs() + self.mpqs = [] + + def add_defaults(self): + if SC_DIR: + for f in ['StarDat','BrooDat','Patch_rt']: + p = os.path.join(SC_DIR, '%s%smpq' % (f,os.extsep)) + if os.path.exists(p) and not p in self.mpqs: + h = SFileOpenArchive(p) + if not SFInvalidHandle(h): + SFileCloseArchive(h) + self.mpqs.append(p) + + def set_mpqs(self, mpqs): + if self.open: + raise PyMSError('MPQ','Cannot set mpqs when the current mpqs are open.') + self.mpqs = list(mpqs) + + def open_mpqs(self): + missing = [[],[]] + if not FOLDER: + handles = {} + self.open = True + for p,m in enumerate(self.mpqs): + if not os.path.exists(m): + missing[0].append(m) + continue + handles[m] = SFileOpenArchive(m, p) + if SFInvalidHandle(handles[m]): + missing[1].append(m) + elif self.open == True: + self.open = handles[m] + self.handles = handles + return missing + + def missing(self, missing): + t = '' + if missing[0]: + t = 'Could not find:\n\t' + '\n\t'.join(missing[0]) + if missing[1]: + t += 'Error loading:\n\t' + '\n\t'.join(missing[1]) + return t + + def close_mpqs(self): + self.open = False + for h in self.handles.values(): + if not SFInvalidHandle(h): + SFileCloseArchive(h) + + # folder(True)=Get only from folder,folder(None)=Get from either, MPQ first, folder second,folder(False)=Get only from MPQ + def get_file(self, path, folder=None): + mpq = path.startswith('MPQ:') + if mpq: + op = path + path = path[4:].split('\\') + if not FOLDER and not folder and mpq: + if self.open == False: + self.open_mpqs() + close = True + else: + close = False + if self.open and self.open != True: + f = SFileOpenFileEx(self.open, '\\'.join(path), SFILE_SEARCH_ALL_OPEN) + print(f) + if not SFInvalidHandle(f): + r = SFileReadFile(f) + SFileCloseFile(f) + print(r) + p = SFile(r[0], '\\'.join(path)) + return p + if close: + self.close_mpqs() + if folder != False: + if mpq: + p = os.path.join(BASE_DIR, 'Libs', 'MPQ', *path) + if os.path.exists(p): + return open(p, 'rb') + elif os.path.exists(path): + return open(path, 'rb') + if mpq: + return BadFile(op) + return BadFile(path) + + def has_file(self, path, folder=None): + mpq = path.startswith('MPQ:') + if mpq: + path = path[4:].split('\\') + if not FOLDER and not folder and mpq: + if self.open == False: + self.open_mpqs() + close = True + else: + close = False + if self.open and self.open != True: + f = SFileOpenFileEx(self.open, '\\'.join(path), SFILE_SEARCH_ALL_OPEN) + if not SFInvalidHandle(f): + SFileCloseFile(f) + return True + if close: + self.close_mpqs() + if folder != False: + if mpq: + return os.path.exists(os.path.join(BASE_DIR, 'Libs', 'MPQ', *path)) + else: + return os.path.exists(path) + return False + + # Type: 0 = structs, 1 = dict + def list_files(self, type=0, handles=None): + if type == 1: + files = {} + else: + files = [] + if self.mpqs: + if handles == None: + handles = self.handles.values() + elif isinstance(handles, int): + handles = [handles] + if self.open == False: + self.open_mpqs() + close = True + else: + close = False + for h in handles: + for e in SFileListFiles(h, '\r\n'.join(self.listfiles)): + if e.fileExists: + if type == 1: + if not e.fileName in self.files: + self.files[e.fileName] = {} + self.files[e.locale] = e + else: + files.append(e) + if close: + self.close_mpqs() + return files + +class MpqSelect(PyMSDialog): + def __init__(self, parent, mpqhandler, type, search, settings): + self.mpqhandler = mpqhandler + self.search = StringVar() + self.search.set(search) + self.search.trace('w', self.updatesearch) + self.settings = settings + self.regex = IntVar() + self.regex.set(0) + self.files = [] + self.file = None + self.resettimer = None + self.searchtimer = None + PyMSDialog.__init__(self, parent, 'Open a ' + type) + + def widgetize(self): + listframe = Frame(self, bd=2, relief=SUNKEN) + scrollbar = Scrollbar(listframe) + self.listbox = Listbox(listframe, width=35, height=1, bd=0, yscrollcommand=scrollbar.set, exportselection=0, activestyle=DOTBOX) + bind = [ + ('', self.scroll), + ('', lambda a,i=0: self.move(a,i)), + ('', lambda a,i=END: self.move(a,i)), + ('', lambda e,i=0: self.movestring(e,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda e,i=1: self.movestring(e,i)), + ('', lambda a,i=1: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=-10: self.move(a,i)), + ('', lambda a,i=10: self.move(a,i)), + ] + for b in bind: + listframe.bind(*b) + scrollbar.config(command=self.listbox.yview) + scrollbar.pack(side=RIGHT, fill=Y) + self.listbox.pack(side=LEFT, fill=BOTH, expand=1) + listframe.pack(fill=BOTH, padx=1, pady=1, expand=1) + listframe.focus_set() + s = Frame(self) + self.textdrop = TextDropDown(s, self.search, self.settings.get('mpqselecthistory',[])[::-1]) + self.textdrop.entry.c = self.textdrop.entry['bg'] + self.textdrop.pack(side=LEFT, fill=X, padx=1, pady=2) + self.open = Button(s, text='Open', width=10, command=self.ok) + self.open.pack(side=RIGHT, padx=1, pady=3) + s.pack(fill=X) + s = Frame(self) + Radiobutton(s, text='Wildcard', variable=self.regex, value=0, command=self.updatelist).pack(side=LEFT, padx=1, pady=2) + Radiobutton(s, text='Regex', variable=self.regex, value=1, command=self.updatelist).pack(side=LEFT, padx=1, pady=2) + Button(s, text='Cancel', width=10, command=self.cancel).pack(side=RIGHT, padx=1, pady=3) + s.pack(fill=X) + + self.listfiles() + self.updatelist() + + if 'mpqselectwindow' in self.settings: + loadsize(self, self.settings, 'mpqselectwindow', True) + + return self.open + + def scroll(self, e): + if e.delta > 0: + self.listbox.yview('scroll', -2, 'units') + else: + self.listbox.yview('scroll', 2, 'units') + + def move(self, e, a): + if a == END: + a = self.listbox.size()-2 + elif a not in [0,END]: + a = max(min(self.listbox.size()-1, int(self.listbox.curselection()[0]) + a),0) + self.listbox.select_clear(0,END) + self.listbox.select_set(a) + self.listbox.see(a) + + def listfiles(self): + filelists = os.path.join(BASE_DIR,'Libs','Data','Listfile.txt') + self.files = [] + self.mpqhandler.open_mpqs() + for h in self.mpqhandler.handles.values(): + for e in SFileListFiles(h, filelists): + if e.fileName and not e.fileName in self.files: + self.files.append(e.fileName) + self.mpqhandler.close_mpqs() + m = os.path.join(BASE_DIR,'Libs','MPQ','') + for p in os.walk(m): + folder = p[0].replace(m,'') + for f in p[2]: + a = '%s\\%s' % (folder,f) + if not a in self.files: + self.files.append(a) + self.files.sort() + + def updatelist(self): + if self.searchtimer: + self.after_cancel(self.searchtimer) + self.searchtimer = None + self.listbox.delete(0,END) + s = self.search.get() + if not self.regex.get(): + s = '^' + re.escape(s).replace('\\?','.').replace('\\*','.+?') + '$' + try: + r = re.compile(s) + except: + self.resettimer = self.after(1000, self.updatecolor) + self.textdrop.entry['bg'] = '#FFB4B4' + else: + for f in filter(lambda p: r.match(p), self.files): + self.listbox.insert(END,f) + if self.listbox.size(): + self.listbox.select_set(0) + self.open['state'] = NORMAL + else: + self.open['state'] = DISABLED + + def updatecolor(self): + if self.resettimer: + self.after_cancel(self.resettimer) + self.resettimer = None + self.textdrop.entry['bg'] = self.textdrop.entry.c + + def updatesearch(self, *_): + if self.searchtimer: + self.after_cancel(self.searchtimer) + self.searchtimer = self.after(200, self.updatelist) + + def cancel(self): + savesize(self, self.settings, 'mpqselectwindow') + PyMSDialog.ok(self) + + def ok(self): + savesize(self, self.settings, 'mpqselectwindow') + f = self.listbox.get(self.listbox.curselection()[0]) + self.file = 'MPQ:' + f + if not 'mpqselecthistory' in self.settings: + self.settings['mpqselecthistory'] = [] + if f in self.settings['mpqselecthistory']: + self.settings['mpqselecthistory'].remove(f) + self.settings['mpqselecthistory'].append(f) + if len(self.settings['mpqselecthistory']) > 10: + del self.settings['mpqselecthistory'][0] + PyMSDialog.ok(self) + +class MPQSettings(Frame): + def __init__(self, parent, mpqs, settings, setdlg=None): + if setdlg == None: + self.setdlg = parent.parent + else: + self.setdlg = setdlg + self.mpqs = list(mpqs) + self.settings = settings + Frame.__init__(self, parent) + Label(self, text='MPQ Settings:', font=('Courier', -12, 'bold'), anchor=W).pack(fill=X) + Label(self, text="Files will be read from the highest priority MPQ that contains them.\nThe higher an MPQ is on the list the higher its priority.", anchor=W, justify=LEFT).pack(fill=X) + self.listframe = Frame(self, bd=2, relief=SUNKEN) + scrollbar = Scrollbar(self.listframe) + self.listbox = Listbox(self.listframe, width=35, height=1, bd=0, yscrollcommand=scrollbar.set, exportselection=0, activestyle=DOTBOX) + bind = [ + ('', self.scroll), + ('', lambda a,i=0: self.move(a,i)), + ('', lambda a,i=END: self.move(a,i)), + ('', lambda e,i=0: self.movestring(e,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda e,i=1: self.movestring(e,i)), + ('', lambda a,i=1: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=-10: self.move(a,i)), + ('', lambda a,i=10: self.move(a,i)), + ] + for b in bind: + self.listframe.bind(*b) + scrollbar.config(command=self.listbox.yview) + scrollbar.pack(side=RIGHT, fill=Y) + self.listbox.pack(side=LEFT, fill=BOTH, expand=1) + self.listframe.pack(fill=BOTH, padx=1, pady=1, expand=1) + for mpq in self.mpqs: + self.listbox.insert(0,mpq) + if self.listbox.size(): + self.listbox.select_set(0) + + buttons = [ + ('add', self.add, 'Add MPQ (Insert)', NORMAL, 'Insert', LEFT), + ('remove', self.remove, 'Remove MPQ (Delete)', DISABLED, 'Delete', LEFT), + ('opendefault', self.adddefault, "Add default StarCraft MPQ's (Shift+Insert)", NORMAL, 'Shift+Insert', LEFT), + ('up', lambda e=None,i=0: self.movempq(e,i), 'Move MPQ Up (Shift+Up)', DISABLED, 'Shift+Up', RIGHT), + ('down', lambda e=None,i=1: self.movempq(e,i), 'Move MPQ Down (Shift+Down)', DISABLED, 'Shift+Down', RIGHT), + ] + self.buttons = {} + toolbar = Frame(self) + for btn in buttons: + if isinstance(btn, tuple): + image = PhotoImage(file=os.path.join(BASE_DIR,'Images','%s.gif' % btn[0])) + button = Button(toolbar, image=image, width=20, height=20, command=btn[1], state=btn[3]) + button.image = image + button.tooltip = Tooltip(button, btn[2]) + button.pack(side=btn[5], padx=[0,10][btn[0] == 'opendefault']) + self.buttons[btn[0]] = button + a = btn[4] + if a: + if not a.startswith('F'): + self.bind('<%s%s>' % (a[:-1].replace('Ctrl','Control').replace('+','-'), a[-1].lower()), btn[1]) + else: + self.bind('<%s>' % a, btn[1]) + else: + Frame(toolbar, width=btn).pack(side=LEFT) + toolbar.pack(fill=X, padx=51, pady=1) + + self.action_states() + + def activate(self): + self.listframe.focus_set() + + def action_states(self): + select = [NORMAL,DISABLED][not self.listbox.curselection()] + for btn in ['remove','up','down']: + self.buttons[btn]['state'] = select + + def scroll(self, e): + if e.delta > 0: + self.listbox.yview('scroll', -2, 'units') + else: + self.listbox.yview('scroll', 2, 'units') + + def move(self, e, a): + if a == END: + a = self.listbox.size()-2 + elif a not in [0,END]: + a = max(min(self.listbox.size()-1, int(self.listbox.curselection()[0]) + a),0) + self.listbox.select_clear(0,END) + self.listbox.select_set(a) + self.listbox.see(a) + + def movempq(self, key=None, dir=0): + if key and self.buttons[['up','down'][dir]]['state'] != NORMAL: + return + i = int(self.listbox.curselection()[0]) + if i == [0,self.listbox.size()-1][dir]: + return + s = self.listbox.get(i) + n = i + [-1,1][dir] + self.mpqs[i] = self.mpqs[n] + self.mpqs[n] = s + self.listbox.delete(i) + self.listbox.insert(n, s) + self.listbox.select_clear(0, END) + self.listbox.select_set(n) + self.listbox.see(n) + self.setdlg.edited = True + + def select_files(self): + path = self.settings.get('lastpath', BASE_DIR) + file = tkFileDialog.askopenfilename(parent=self, title="Add MPQ's", defaultextension='.mpq', filetypes=[('MPQ Files','*.mpq'),('All Files','*')], initialdir=path, multiple=True) + if file: + self.settings['lastpath'] = os.path.dirname(file[0]) + return file + + def add(self, key=None, add=None): + if add == None: + n,s = 0,0 + add = self.select_files() + else: + n,s = END,self.listbox.size() + if add: + error = [] + for i in add: + if not i in self.mpqs: + h = SFileOpenArchive(i) + if h not in [None,-1]: + SFileCloseFile(h) + self.mpqs.insert([0,-1][n == END],i) + self.listbox.insert(n,i) + self.listbox.select_clear(0,END) + self.listbox.select_set(s) + self.listbox.see(s) + self.action_states() + self.setdlg.edited = True + + def remove(self, key=None): + if key and self.buttons['remove']['state'] != NORMAL: + return + i = int(self.listbox.curselection()[0]) + del self.mpqs[i] + self.listbox.delete(i) + if self.listbox.size(): + i = min(i,self.listbox.size()-1) + self.listbox.select_set(i) + self.listbox.see(i) + self.action_states() + self.setdlg.edited = True + + def adddefault(self, key=None): + if SC_DIR: + a = [] + for f in ['StarDat','BrooDat','Patch_rt']: + p = os.path.join(SC_DIR, '%s%smpq' % (f,os.extsep)) + if os.path.exists(p) and not p in self.mpqs: + a.append(p) + if a: + self.add(add=a) + +class SettingsPanel(Frame): + types = { + 'AIBIN':(AIBIN.AIBIN,'aiscript.bin','bin',[('AI Scripts','*.bin'),('All Files','*')]), + } + + def __init__(self, parent, entries, settings, mpqhandler, setdlg=None): + if setdlg == None: + self.setdlg = parent.parent + else: + self.setdlg = setdlg + self.settings = settings + self.mpqhandler = mpqhandler + self.find = PhotoImage(file=os.path.join(BASE_DIR,'Images','find.gif')) + self.variables = {} + inmpq = False + Frame.__init__(self, parent) + for _ in entries: + if len(_) == 5: + f,e,v,t,c = _ + else: + f,e,v,t = _ + c = None + self.variables[f] = (IntVar(),StringVar(),[]) + v = settings[v] + m = v.startswith('MPQ:') + self.variables[f][0].set(m) + if m: + self.variables[f][1].set(v[4:]) + else: + self.variables[f][1].set(v) + self.variables[f][1].trace('w', self.edited) + datframe = Frame(self) + if isstr(e): + Label(datframe, text=f, font=('Courier', -12, 'bold'), anchor=W).pack(fill=X, expand=1) + Label(datframe, text=e, anchor=W).pack(fill=X, expand=1) + elif e: + Label(datframe, text=f, font=('Courier', -12, 'bold'), anchor=W).pack(fill=X, expand=1) + else: + Label(datframe, text=f, anchor=W).pack(fill=X, expand=1) + entryframe = Frame(datframe) + e = Entry(entryframe, textvariable=self.variables[f][1], state=DISABLED) + b = Button(entryframe, image=self.find, width=20, height=20, command=lambda f=f,t=self.types[t],e=e,c=c: self.setting(f,t,e,c)) + self.variables[f][2].extend([e,b]) + if not t == 'Palette': + inmpq = True + y = Checkbutton(entryframe, text='', variable=self.variables[f][0]) + self.variables[f][2].append(y) + y.pack(side=LEFT) + e.pack(side=LEFT, fill=X, expand=1) + e.xview(END) + b.pack(side=LEFT, padx=1) + entryframe.pack(fill=X, expand=1) + datframe.pack(side=TOP, fill=X) + if inmpq: + Label(self, text='Check the checkbox beside an entry to use a file in the MPQs').pack(fill=X) + + def edited(self, *_): + if hasattr(self.setdlg, 'edited'): + self.setdlg.edited = True + + def select_file(self, t, e, f): + path = self.settings.get('lastpath', BASE_DIR) + file = tkFileDialog.askopenfilename(parent=self, title="Open a " + t, defaultextension='.' + e, filetypes=f, initialdir=path) + if file: + self.settings['lastpath'] = os.path.dirname(file) + return file + + def setting(self, f, t, e, cb): + file = '' + if self.variables[f][0].get(): + m = MpqSelect(self.setdlg, self.mpqhandler, t[1], '*.' + t[2], self.settings) + if m.file: + self.mpqhandler.open_mpqs() + if t[1] == 'FNT': + file = (self.mpqhandler.get_file(m.file, False),self.mpqhandler.get_file(m.file, True)) + else: + file = self.mpqhandler.get_file(m.file) + self.mpqhandler.close_mpqs() + else: + file = self.select_file(t[1],t[2],t[3]) + if file: + c = t[0]() + if t[1] == 'FNT': + try: + c.load_file(file[0]) + self.variables[f][1].set(file[0].file) + except PyMSError: + try: + c.load_file(file[1]) + self.variables[f][1].set(file[0].file) + except PyMSError as err: + ErrorDialog(self.setdlg, err) + return + else: + try: + c.load_file(file) + except PyMSError as e: + ErrorDialog(self.setdlg, e) + return + self.variables[f][1].set(file) + e.xview(END) + if cb: + cb(c) + else: + self.setdlg.edited = True + + def save(self, d, m): + for s in d[1]: + self.setdlg.parent.settings[s[2]] = ['','MPQ:'][self.variables[s[0]][0].get()] + self.variables[s[0]][1].get().replace(m,'MPQ:',1) + +class SettingsDialog(PyMSDialog): + def __init__(self, parent, data, min_size, err=None, mpqs=True): + self.min_size = min_size + self.data = data + self.pages = [] + self.err = err + self.mpqs = mpqs + self.edited = False + PyMSDialog.__init__(self, parent, 'Settings') + + def widgetize(self): + self.minsize(*self.min_size) + if self.data: + self.tabs = Notebook(self) + if self.mpqs: + self.mpqsettings = MPQSettings(self.tabs, self.parent.mpqhandler.mpqs, self.parent.settings) + self.tabs.add_tab(self.mpqsettings, 'MPQ Settings') + for d in self.data: + if isinstance(d[1],list): + self.pages.append(SettingsPanel(self.tabs, d[1], self.parent.settings, self.parent.mpqhandler)) + else: + self.pages.append(d[1](self.tabs)) + self.tabs.add_tab(self.pages[-1], d[0]) + self.tabs.pack(fill=BOTH, expand=1, padx=5, pady=5) + else: + self.mpqsettings = MPQSettings(self, self.parent.mpqhandler.mpqs, self.parent.settings) + self.mpqsettings.pack(fill=BOTH, expand=1, padx=5, pady=5) + btns = Frame(self) + ok = Button(btns, text='Ok', width=10, command=self.ok) + ok.pack(side=LEFT, padx=3, pady=3) + Button(btns, text='Cancel', width=10, command=self.cancel).pack(side=LEFT, padx=3, pady=3) + btns.pack() + if 'settingswindow' in self.parent.settings: + loadsize(self, self.parent.settings, 'settingswindow', True) + if self.err: + self.after(1, self.showerr) + return ok + + def showerr(self): + ErrorDialog(self, self.err) + + def cancel(self): + if self.err and askyesno(parent=self, title='Exit?', message="One or more files required for this program can not be found and must be chosen. Canceling will close the program, do you wish to continue?"): + self.parent.after(1, self.parent.exit) + PyMSDialog.ok(self) + elif not self.edited or askyesno(parent=self, title='Cancel?', message="Are you sure you want to cancel?\nAll unsaved changes will be lost."): + self.parent.settings['settingswindow'] = self.winfo_geometry() + ['','^'][self.wm_state() == 'zoomed'] + PyMSDialog.ok(self) + + def ok(self): + if self.edited: + if self.mpqs: + t = self.parent.mpqhandler.mpqs + self.parent.mpqhandler.set_mpqs(self.mpqsettings.mpqs) + o = dict(self.parent.settings) + m = os.path.join(BASE_DIR,'Libs','MPQ','') + for p,d in zip(self.pages,self.data): + p.save(d,m) + try: + e = self.parent.open_files() + except AttributeError: + pass + else: + if e: + if self.mpqs: + self.parent.mpqhandler.set_mpqs(t) + self.parent.settings = b + ErrorDialog(self, e) + return + savesize(self, self.parent.settings, 'settingswindow') PyMSDialog.ok(self) \ No newline at end of file diff --git a/tools/Libs/trace.py b/tools/Libs/trace.py index d46adbf..67d1200 100644 --- a/tools/Libs/trace.py +++ b/tools/Libs/trace.py @@ -1,4 +1,4 @@ -from utils import * +from .utils import * import sys,os diff --git a/tools/Libs/utils.py b/tools/Libs/utils.py index c4df484..162e280 100644 --- a/tools/Libs/utils.py +++ b/tools/Libs/utils.py @@ -1,1323 +1,1343 @@ -from Tkinter import * -from tkMessageBox import askquestion,OK -import tkFileDialog -from textwrap import wrap -import os,re,webbrowser,sys,traceback,urllib -win_reg = True -try: - from _winreg import * -except: - win_reg = False - -PyMS_VERSION = (1,2,1) -PyMS_LONG_VERSION = 'v%s.%s.%s' % PyMS_VERSION -if hasattr(sys, 'frozen'): - BASE_DIR = os.path.dirname(unicode(sys.executable, sys.getfilesystemencoding())) -else: - BASE_DIR = os.path.dirname(os.path.dirname(unicode(__file__, sys.getfilesystemencoding()))) -if os.path.exists(BASE_DIR): - os.chdir(BASE_DIR) -SC_DIR = '' -if win_reg: - try: - h = OpenKey(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Blizzard Entertainment\\Starcraft') - SC_DIR = QueryValueEx(h, 'InstallPath')[0] - except: - pass - if not os.path.exists(SC_DIR): - SC_DIR = '' -couriernew = ('Courier', -12, 'normal') -couriersmall = ('Courier', -8, 'normal') -ARROW = None - -def isstr(s): - return isinstance(s,str) or isinstance(s,unicode) - -def register_registry(prog,type,filetype,progpath,icon): - if not win_reg: - raise PyMSError('Registry', 'You can currently only set as the default program on Windows machines.') - def delkey(key,sub_key): - try: - h = OpenKey(key,sub_key) - except WindowsError, e: - if e.errno == 2: - return - raise - except: - raise - try: - while True: - n = EnumKey(h,0) - delkey(h,n) - except EnvironmentError: - pass - h.Close() - DeleteKey(key,sub_key) - - key = '%s:%s' % (prog,filetype) - try: - delkey(HKEY_CLASSES_ROOT, '.' + filetype) - delkey(HKEY_CLASSES_ROOT, key) - SetValue(HKEY_CLASSES_ROOT, '.' + filetype, REG_SZ, key) - SetValue(HKEY_CLASSES_ROOT, key, REG_SZ, 'StarCraft %s *.%s file (%s)' % (type,filetype,prog)) - SetValue(HKEY_CLASSES_ROOT, key + '\\DefaultIcon', REG_SZ, icon) - SetValue(HKEY_CLASSES_ROOT, key + '\\Shell', REG_SZ, 'open') - SetValue(HKEY_CLASSES_ROOT, key + '\\Shell\\open\\command', REG_SZ, '"%s" "%s" --gui "%%1"' % (sys.executable.replace('python.exe','pythonw.exe'),progpath)) - except: - raise PyMSError('Registry', 'Could not complete file association.',exception=sys.exc_info()) - askquestion(title='Success!', message='The file association was set.', type=OK) - -def flags(value, length): - if isstr(value): - if len(value) != length or value.replace('0','').replace('1',''): - raise - return sum(int(x)*(2**n) for n,x in enumerate(reversed(value))) - return ''.join(reversed([str(value/(2**n)%2) for n in range(length)])) - -def ccopy(lst): - r = [] - for item in lst: - if isinstance(item, list): - r.append(ccopy(item)) - else: - r.append(item) - return r - -def fit(label, text, width=80, end=False, indent=0): - r = label - if not indent: - s = len(r) - else: - s = indent - indent = False - for l in wrap(text, width - s): - if indent: - r += ' ' * s - else: - indent = True - r += l - if len(l) != width - s or end: - r += '\n' - return r - -def removedir(path): - if os.path.exists(path): - for r,ds,fs in os.walk(path, topdown=False): - for f in fs: - os.remove(os.path.join(r, f)) - for d in ds: - p = os.path.join(r, d) - removedir(p) - os.rmdir(p) - os.rmdir(path) - -class DependencyError(Tk): - def __init__(self, prog, msg, hotlink=None): - #Window - Tk.__init__(self) - self.resizable(False,False) - self.title('Dependency Error') - try: - self.icon = os.path.join(BASE_DIR,'Images','%s.ico' % prog) - self.wm_iconbitmap(self.icon) - except: - self.icon = '@%s' % os.path.join(BASE_DIR, 'Images','%s.xbm' % prog) - self.wm_iconbitmap(self.icon) - Label(self, text=msg, anchor=W, justify=LEFT).pack(side=TOP,pady=2,padx=2) - if hotlink: - f = Frame(self) - Hotlink(f, *hotlink).pack(side=RIGHT, padx=10, pady=2) - f.pack(side=TOP,fill=X) - Button(self, text='Ok', width=10, command=self.destroy).pack(side=TOP, pady=2) - self.update_idletasks() - w,h = self.winfo_width(),self.winfo_height() - self.geometry('%ix%i+%i+%i' % (w,h,(self.winfo_screenwidth() - w)/2,(self.winfo_screenheight() - h)/2)) - -class PyMSError(Exception): - def __init__(self, type, error, line=None, code=None, warnings=[], exception=None): - self.type = type - self.error = error - self.line = line - if self.line != None: - self.line += 1 - self.code = code - self.warnings = warnings - self.exception = exception - - def repr(self): - r = '%s Error: %s' % (self.type, self.error) - if self.line: - r += '\n Line %s: %s' % (self.line, self.code) - return r - - def __repr__(self): - r = fit('%s Error: ' % self.type, self.error) - if self.line: - r += fit(' Line %s: ' % self.line, self.code) - if self.warnings: - for w in self.warnings: - r += repr(w) - return r[:-1] - -class PyMSWarning(Exception): - def __init__(self, type, warning, line=None, code=None, extra=None, level=0): - self.type = type - self.warning = warning - self.line = line - if self.line != None: - self.line += 1 - self.code = code - self.extra = extra - self.level = level - - def repr(self): - r = fit('%s Warning: ' % self.type, self.warning, end=True) - if self.line: - r += fit(' Line %s: ' % self.line, self.code, end=True) - return r - - def __repr__(self): - r = fit('%s Warning: ' % self.type, self.warning) - if self.line: - r += fit(' Line %s: ' % self.line, self.code) - return r[:-1] - -class PyMSWarnList(Exception): - def __init__(self, warnings): - self.warnings = warnings - - def __repr__(self): - r = '' - for w in self.warnings: - r += repr(w) - return r[:-1] - -class PyMSDialog(Toplevel): - def __init__(self, parent, title, center=True, grabwait=True, hidden=False): - Toplevel.__init__(self, parent) - self.title(title) - self.icon = parent.icon - self.wm_iconbitmap(parent.icon) - self.protocol('WM_DELETE_WINDOW', self.cancel) - #self.transient(parent) - self.parent = parent - focus = self.widgetize() - if not focus: - focus = self - focus.focus_set() - self.update_idletasks() - size = self.winfo_geometry().split('+',1)[0].split('x') - if center: - self.geometry('+%d+%d' % (self.winfo_screenwidth()/2-int(size[0])/2,self.winfo_screenheight()/2-int(size[1])/2)) - if grabwait: - self.grab_set() - self.wait_window(self) - - def widgetize(self): - pass - - def ok(self): - self.withdraw() - self.update_idletasks() - self.parent.focus_set() - self.destroy() - - def cancel(self): - self.ok() - -class InternalErrorDialog(PyMSDialog): - def __init__(self, parent, prog, handler=None, txt=None): - self.prog = prog - self.handler = handler - self.txt = txt - PyMSDialog.__init__(self, parent, 'PyMS Internal Error!', grabwait=not not txt) - - def widgetize(self): - self.bind('', self.selectall) - Label(self, text='The PyMS program "%s" has encountered an unknown internal error.\nThe program will attempt to continue, but may cause problems or crash once you press Ok.\nPlease contact poiuy_qwert and send him this traceback with any relivant information.' % self.prog, justify=LEFT).pack(side=TOP, padx=2, pady=2, fill=X) - r = Frame(self) - Hotlink(r, 'Contact', self.contact).pack(side=RIGHT, padx=10, pady=2) - r.pack(fill=X) - frame = Frame(self, bd=2, relief=SUNKEN) - hscroll = Scrollbar(frame, orient=HORIZONTAL) - vscroll = Scrollbar(frame) - self.text = Text(frame, bd=0, highlightthickness=0, width=70, height=10, xscrollcommand=hscroll.set, yscrollcommand=vscroll.set, wrap=NONE, exportselection=0, state=DISABLED) - if self.txt: - self.text['state'] = NORMAL - self.text.insert(END, self.txt) - self.text['state'] = DISABLED - self.text.grid(sticky=NSEW) - hscroll.config(command=self.text.xview) - hscroll.grid(sticky=EW) - vscroll.config(command=self.text.yview) - vscroll.grid(sticky=NS, row=0, column=1) - frame.grid_rowconfigure(0, weight=1) - frame.grid_columnconfigure(0, weight=1) - frame.pack(fill=BOTH, pady=2, padx=2, expand=1) - buttonbar = Frame(self) - ok = Button(buttonbar, text='Ok', width=10, command=self.ok) - ok.pack(side=LEFT, padx=3) - buttonbar.pack(side=BOTTOM, pady=10) - return ok - - def selectall(self, key=None): - self.text.focus_set() - self.text.tag_add(SEL, 1.0, END) - - def ok(self): - if self.handler: - self.handler.window = None - PyMSDialog.ok(self) - - def contact(self, e=None): - webbrowser.open(os.path.join(os.path.dirname(BASE_DIR), 'Docs', 'intro.html')) - -class ErrorDialog(PyMSDialog): - def __init__(self, parent, error): - self.error = error - PyMSDialog.__init__(self, parent, '%s Error!' % error.type) - - def widgetize(self): - self.resizable(False, False) - Label(self, justify=LEFT, anchor=W, text=self.error.repr(), wraplen=640).pack(pady=10, padx=5) - frame = Frame(self) - ok = Button(frame, text='Ok', width=10, command=self.ok) - ok.pack(side=LEFT, padx=3) - w = len(self.error.warnings) - p = 's' - if w == 1: - p = '' - Button(frame, text='%s Warning%s' % (w, p), width=10, command=self.viewwarnings, state=[NORMAL,DISABLED][not self.error.warnings]).pack(side=LEFT, padx=3) - Button(frame, text='Copy', width=10, command=self.copy).pack(side=LEFT, padx=6) - if self.error.exception: - Button(frame, text='Internal Error', width=10, command=self.internal).pack(side=LEFT, padx=6) - frame.pack(pady=10) - return ok - - def copy(self): - self.clipboard_clear() - self.clipboard_append(self.error.repr()) - - def viewwarnings(self): - WarningDialog(self, self.error.warnings) - - def internal(self): - InternalErrorDialog(self, sys.stderr.prog, txt=''.join(traceback.format_exception(*self.error.exception))) - -class WarningDialog(PyMSDialog): - def __init__(self, parent, warnings, cont=False): - self.warnings = warnings - self.cont = cont - PyMSDialog.__init__(self, parent, 'Warning!') - - def widgetize(self): - self.bind('', self.selectall) - self.resizable(False, False) - frame = Frame(self, bd=2, relief=SUNKEN) - hscroll = Scrollbar(frame, orient=HORIZONTAL) - vscroll = Scrollbar(frame) - self.warntext = Text(frame, bd=0, highlightthickness=0, width=60, height=10, xscrollcommand=hscroll.set, yscrollcommand=vscroll.set, wrap=NONE, exportselection=0) - self.warntext.tag_config('highlevel', foreground='#960000') - self.warntext.grid() - hscroll.config(command=self.warntext.xview) - hscroll.grid(sticky=EW) - vscroll.config(command=self.warntext.yview) - vscroll.grid(sticky=NS, row=0, column=1) - for warning in self.warnings: - if warning.level: - self.warntext.insert(END, warning.repr(), 'highlevel') - else: - self.warntext.insert(END, warning.repr()) - self.warntext['state'] = DISABLED - frame.pack(side=TOP, pady=2, padx=2) - buttonbar = Frame(self) - ok = Button(buttonbar, text='Ok', width=10, command=self.ok) - ok.pack(side=LEFT, padx=3) - if self.cont: - Button(buttonbar, text='Cancel', width=10, command=self.cancel).pack(side=LEFT) - buttonbar.pack(pady=10) - return ok - - def selectall(self, key=None): - self.warntext.focus_set() - self.warntext.tag_add(SEL, 1.0, END) - - def ok(self): - self.cont = True - PyMSDialog.ok(self) - - def cancel(self): - self.cont = False - PyMSDialog.ok(self) - -class AboutDialog(PyMSDialog): - def __init__(self, parent, program, version, thanks=[]): - self.program = program - self.version = version - self.thanks = thanks - self.thanks.extend([ - ('ShadowFlare','For SFmpq, some file specs, and all her tools!'), - ('BroodWarAI.com','Support and hosting of course!'), - ('Blizzard','For creating StarCraft and BroodWar...'), - ]) - PyMSDialog.__init__(self, parent, 'About %s' % program) - - def widgetize(self): - self.resizable(False, False) - name = Label(self, text='%s %s' % (self.program, self.version), font=('Courier', 18, 'bold')) - name.pack() - frame = Frame(self) - Label(frame, text='Author:').grid(stick=E) - Label(frame, text='Homepage:').grid(stick=E) - Hotlink(frame, 'poiuy_qwert (p.q.poiuy_qwert@gmail.com)', self.author).grid(row=0, column=1, stick=W) - Hotlink(frame, 'http://www.broodwarai.com/index.php?page=pyms', self.homepage).grid(row=1, column=1, stick=W) - frame.pack(padx=1, pady=2) - if self.thanks: - Label(self, text='Special Thanks To:', font=('Courier', 10, 'bold')).pack(pady=2) - thanks = Frame(self) - font = ('Courier', 8, 'bold') - row = 0 - for who,why in self.thanks: - if who == 'BroodWarAI.com': - Hotlink(thanks, who, self.broodwarai, [('Courier', 8, 'bold'),('Courier', 8, 'bold underline')]).grid(sticky=E) - else: - Label(thanks, text=who, font=font).grid(stick=E) - Label(thanks, text=why).grid(row=row, column=1, stick=W) - row += 1 - thanks.pack(pady=1) - ok = Button(self, text='Ok', width=10, command=self.ok) - ok.pack(pady=5) - return ok - - def author(self, e=None): - webbrowser.open('mailto:p.q.poiuy.qwert@hotmail.com') - - def homepage(self, e=None): - webbrowser.open('http://www.broodwarai.com/index.php?page=pyms') - - def broodwarai(self, e=None): - webbrowser.open('http://www.broodwarai.com') - -class Hotlink(Label): - def __init__(self, parent, text, callback=None, fonts=[('Courier', 8, 'normal'),('Courier', 8, 'underline')]): - self.fonts = fonts - Label.__init__(self, parent, text=text, foreground='#0000FF', cursor='hand2', font=fonts[0]) - self.bind('', self.enter) - self.bind('', self.leave) - if callback: - self.bind('', callback) - - def enter(self, e): - self['font'] = self.fonts[1] - - def leave(self, e): - self['font'] = self.fonts[0] - -class Notebook(Frame): - def __init__(self, parent, relief=RAISED, switchcallback=None): - self.parent = parent - self.active = None - self.tab = IntVar() - self.notebook = Frame(parent) - self.tabs = Frame(self.notebook) - self.tabs.pack(fill=X) - self.pages = {} - Frame.__init__(self, self.notebook, borderwidth=2, relief=relief) - Frame.pack(self, fill=BOTH, expand=1) - - def pack(self, **kw): - self.notebook.pack(kw) - - def add_tab(self, fr, title): - b = Radiobutton(self.tabs, text=title, indicatoron=0, variable=self.tab, value=len(self.pages), command=lambda: self.display(title)) - b.pack(side=LEFT) - self.pages[title] = [fr,len(self.pages)] - if not self.active: - self.display(title) - return b - - def display(self, title): - if self.active: - if hasattr(self.active, 'deactivate'): - self.active.deactivate() - self.active.forget() - self.tab.set(self.pages[title][1]) - self.active = self.pages[title][0] - self.active.pack(fill=BOTH, expand=1, padx=6, pady=6) - if hasattr(self.active, 'activate'): - self.active.activate() - -class NotebookTab(Frame): - def __init__(self, parent): - self.parent = parent - Frame.__init__(self, parent) - - def activate(self): - pass - - def deactivate(self): - pass - -class DropDown(Frame): - def __init__(self, parent, variable, entries, display=None, width=1, blank=None, state=NORMAL): - self.variable = variable - self.variable.set = self.set - self.display = display - self.blank = blank - if display and isinstance(display, Variable): - display.callback = self.set - self.size = min(10,len(entries)) - Frame.__init__(self, parent, borderwidth=2, relief=SUNKEN) - self.listbox = Listbox(self, selectmode=SINGLE, font=couriernew, width=width, height=1, borderwidth=0, exportselection=1, activestyle=DOTBOX) - self.listbox.bind('', self.choose) - bind = [ - ('', self.scroll), - ('', lambda a,i=0: self.move(a,i)), - ('', lambda a,i=END: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=1: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=-10: self.move(a,i)), - ('', lambda a,i=10: self.move(a,i)), - ] - for b in bind: - self.bind(*b) - self.setentries(entries) - self.listbox.pack(side=LEFT, fill=X, expand=1) - self.listbox['state'] = state - global ARROW - if not ARROW: - ARROW = PhotoImage(file=os.path.join(BASE_DIR, 'Images', 'arrow.gif')) - self.button = Button(self, image=ARROW, command=self.choose, state=state) - self.button.image = ARROW - self.button.pack(side=LEFT, fill=Y) - - def __setitem__(self, item, value): - if item == 'state': - self.listbox['state'] = value - self.button['state'] = value - else: - Frame.__setitem__(self, item, value) - - def setentries(self, entries): - self.entries = list(entries) - self.listbox.delete(0,END) - for entry in entries: - self.listbox.insert(END, entry) - if self.blank != None: - self.listbox.insert(END, '') - self.listbox.see(self.variable.get()) - - def set(self, num): - self.change(num) - Variable.set(self.variable, num) - self.disp(num) - - def change(self, num): - if num >= self.listbox.size(): - num = self.listbox.size()-1 - self.listbox.select_clear(0,END) - #self.listbox.select_set(num) - self.listbox.see(num) - - def scroll(self, e): - if self.listbox['state'] == NORMAL: - if e.delta > 0: - self.move(None, -1) - elif self.blank != None or self.variable.get() < self.listbox.size()-2: - self.move(None, 1) - - def move(self, e, a): - if self.listbox['state'] == NORMAL: - if a == END and self.blank != None: - a = self.listbox.size()-2 - elif a not in [0,END]: - a = max(min([self.listbox.size()-2,self.listbox.size()-1][self.blank == None],self.variable.get() + a),0) - self.set(a) - self.listbox.select_set(a) - - def choose(self, e=None): - if self.listbox['state'] == NORMAL: - i = self.variable.get() - c = DropDownChooser(self, self.entries, i) - self.set(c.result) - self.listbox.select_set(c.result) - - def disp(self, n): - if self.display: - if n == self.listbox.size()-1 and self.blank != None: - n = self.blank - if isinstance(self.display, Variable): - self.display.set(n) - else: - self.display(n) - -class TextDropDown(Frame): - def __init__(self, parent, variable, history=[], width=None, state=NORMAL): - self.variable = variable - self.set = self.variable.set - self.history = history - Frame.__init__(self, parent, borderwidth=2, relief=SUNKEN) - self.entry = Entry(self, textvariable=self.variable, width=width, bd=0) - self.entry.pack(side=LEFT, fill=X, expand=1) - self.entry['state'] = state - global ARROW - if not ARROW: - ARROW = PhotoImage(file=os.path.join(BASE_DIR, 'Images', 'arrow.gif')) - self.button = Button(self, image=ARROW, command=self.choose, state=state) - self.button.pack(side=LEFT, fill=Y) - - def focus_set(self): - self.entry.focus_set() - - def __setitem__(self, item, value): - if item == 'state': - self.entry['state'] = value - self.button['state'] = value - else: - self.entry[item] = value - - def __getitem__(self, item): - return self.entry[item] - - def choose(self, e=None): - if self.entry['state'] == NORMAL and self.history: - i = -1 - if self.variable.get() in self.history: - i = self.history.index(self.variable.get()) - c = DropDownChooser(self, self.history, i) - if c.result > -1: - self.variable.set(self.history[c.result]) - -class DropDownChooser(Toplevel): - def __init__(self, parent, list, select): - self.focus = 0 - self.parent = parent - self.result = select - Toplevel.__init__(self, parent, relief=SOLID, borderwidth=1) - self.protocol('WM_LOSE_FOCUS', self.select) - self.wm_overrideredirect(1) - scrollbar = Scrollbar(self) - self.listbox = Listbox(self, selectmode=SINGLE, height=min(10,len(list)), borderwidth=0, font=couriernew, highlightthickness=0, yscrollcommand=scrollbar.set, activestyle=DOTBOX) - for e in list: - self.listbox.insert(END,e) - if self.result > -1: - self.listbox.select_set(self.result) - self.listbox.see(self.result) - self.listbox.bind('', self.select) - bind = [ - ('', lambda e,i=1: self.enter(e,i)), - ('', lambda e,i=0: self.enter(e,i)), - ('', self.focusout), - ('', self.select), - ('', self.close), - ('', self.scroll), - ('', lambda a,i=0: self.move(a,i)), - ('', lambda a,i=END: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=1: self.move(a,i)), - ('', lambda a,i=-1: self.move(a,i)), - ('', lambda a,i=-10: self.move(a,i)), - ('', lambda a,i=10: self.move(a,i)), - ] - for b in bind: - self.bind(*b) - scrollbar.config(command=self.listbox.yview) - if len(list) > 10: - scrollbar.pack(side=RIGHT, fill=Y) - self.listbox.pack(side=LEFT, fill=BOTH, expand=1) - self.focus_set() - self.update_idletasks() - size = self.parent.winfo_geometry().split('+',1)[0].split('x') - if self.parent.winfo_rooty() + self.parent.winfo_reqheight() + self.winfo_reqheight() > self.winfo_screenheight(): - self.geometry('%sx%s+%d+%d' % (size[0],self.winfo_reqheight(),self.parent.winfo_rootx(), self.parent.winfo_rooty() - self.winfo_reqheight())) - else: - self.geometry('%sx%s+%d+%d' % (size[0],self.winfo_reqheight(),self.parent.winfo_rootx(), self.parent.winfo_rooty() + self.parent.winfo_reqheight())) - self.grab_set() - self.update_idletasks() - self.wait_window(self) - - def enter(self, e, f): - self.focus = f - - def focusout(self, e): - if not self.focus: - self.select() - - def move(self, e, a): - if not a in [0,END]: - a = max(min(self.listbox.size()-1,int(self.listbox.curselection()[0]) + a),0) - self.listbox.select_clear(0,END) - self.listbox.select_set(a) - self.listbox.see(a) - - def scroll(self, e): - if e.delta > 0: - self.listbox.yview('scroll', -2, 'units') - else: - self.listbox.yview('scroll', 2, 'units') - - def home(self, e): - self.listbox.yview('moveto', 0.0) - - def end(self, e): - self.listbox.yview('moveto', 1.0) - - def up(self, e): - self.listbox.yview('scroll', -1, 'units') - - def down(self, e): - self.listbox.yview('scroll', 1, 'units') - - def pageup(self, e): - self.listbox.yview('scroll', -1, 'pages') - - def pagedown(self, e): - self.listbox.yview('scroll', 1, 'pages') - - def select(self, e=None): - s = self.listbox.curselection() - if s: - self.result = int(s[0]) - self.close() - - def close(self, e=None): - self.withdraw() - self.update_idletasks() - self.destroy() - self.parent.focus_set() - -class CodeText(Frame): - autoindent = re.compile('^([ \\t]*)') - selregex = re.compile('\\bsel\\b') - - def __init__(self, parent, ecallback=None, icallback=None, scallback=None, acallback=None, state=NORMAL): - self.dispatch_output = False - self.edited = False - self.taboverride = False - # Edit Callback - # INSERT Callback - # Selection Callback - # Auto-complete Callback - self.ecallback = ecallback - self.icallback = icallback - self.scallback = scallback - self.acallback = acallback - - Frame.__init__(self, parent, bd=2, relief=SUNKEN) - frame = Frame(self) - font = ('Courier New', -12, 'normal') - self.lines = Text(frame, height=1, font=font, bd=0, bg='#E4E4E4', fg='#808080', width=8, cursor='') - self.lines.pack(side=LEFT, fill=Y) - hscroll = Scrollbar(self, orient=HORIZONTAL) - self.vscroll = Scrollbar(self) - self.text = Text(frame, height=1, font=font, bd=0, undo=1, maxundo=100, wrap=NONE, xscrollcommand=hscroll.set, yscrollcommand=self.yscroll, exportselection=0) - self.text.configure(tabs=self.tk.call("font", "measure", self.text["font"], "-displayof", frame, ' ')) - self.text.pack(side=LEFT, fill=BOTH, expand=1) - self.text.bind('', lambda e: self.after(1, self.selectall)) - self.text.bind('', lambda e,i=True: self.indent(e, i)) - self.text.bind('', self.popup) - frame.grid(sticky=NSEW) - hscroll.config(command=self.text.xview) - hscroll.grid(sticky=EW) - self.vscroll.config(command=self.yview) - self.vscroll.grid(sticky=NS, row=0, column=1) - self.grid_rowconfigure(0,weight=1) - self.grid_columnconfigure(0,weight=1) - - textmenu = [ - ('Undo', self.undo, 0), # 0 - None, - ('Cut', lambda: self.copy(True), 2), # 2 - ('Copy', self.copy, 0), # 3 - ('Paste', self.paste, 0), # 4 - ('Delete', lambda: self.text.delete('Selection.first', 'Selection.last'), 0), # 5 - None, - ('Select All', lambda: self.after(1, self.selectall), 7), # 7 - ] - self.textmenu = Menu(self, tearoff=0) - for m in textmenu: - if m: - l,c,u = m - self.textmenu.add_command(label=l, command=c, underline=u) - else: - self.textmenu.add_separator() - - self.lines.insert('1.0', ' 1') - self.lines.bind('', self.selectline) - self.text.mark_set('return', '1.0') - self.text.orig = self.text._w + '_orig' - self.tk.call('rename', self.text._w, self.text.orig) - self.tk.createcommand(self.text._w, self.dispatch) - - self['state'] = state - - self.tag_configure = self.text.tag_configure - self.tag_add = self.text.tag_add - self.tag_remove = self.text.tag_remove - self.tag_raise = self.text.tag_raise - self.tag_nextrange = self.text.tag_nextrange - self.tag_prevrange = self.text.tag_prevrange - self.tag_ranges = self.text.tag_ranges - self.tag_names = self.text.tag_names - self.tag_bind = self.text.tag_bind - self.tag_delete = self.text.tag_delete - self.mark_set = self.text.mark_set - self.index = self.text.index - self.get = self.text.get - self.see = self.text.see - self.compare = self.text.compare - self.edited = False - self.afterid = None - self.dodelete = None - self.deleteid = None - self.tags = {} - # None - Nothing, True - Continue coloring, False - Stop coloring - self.coloring = None - self.dnd = False - - self.setup() - - def popup(self, e): - if self.text['state'] == NORMAL: - s,i,r = self.text.index('@1,1'),self.text.index(INSERT),self.text.tag_ranges('Selection') - try: - self.text.edit_undo() - except: - self.textmenu.entryconfig(0, state=DISABLED) - else: - self.text.edit_redo() - self.textmenu.entryconfig(0, state=NORMAL) - if r: - self.tag_add('Selection', *r) - self.text.mark_set(INSERT,i) - self.text.yview_pickplace(s) - s = s.split('.')[0] - if s in '1 2 3 4 5 6 7 8': - self.text.yview_scroll(s, 'lines') - if not self.text.tag_ranges('Selection'): - sel = DISABLED - else: - sel = NORMAL - for i in [2,3,5]: - self.textmenu.entryconfig(i, state=sel) - try: - c = not self.selection_get(selection='CLIPBOARD') - except: - c = 1 - self.textmenu.entryconfig(4, state=[NORMAL,DISABLED][c]) - self.textmenu.post(e.x_root, e.y_root) - - def undo(self): - self.text.edit_undo() - - def copy(self, cut=False): - self.clipboard_clear() - self.clipboard_append(self.text.get('Selection.first','Selection.last')) - if cut: - r = self.text.tag_ranges('Selection') - self.text.delete('Selection.first','Selection.last') - self.update_lines() - self.update_range('%s linestart' % r[0], '%s lineend' % r[1]) - - def paste(self): - try: - text = self.selection_get(selection='CLIPBOARD') - except: - pass - else: - if self.text.tag_ranges('Selection'): - self.text.mark_set(INSERT, 'Selection.first') - self.text.delete('Selection.first','Selection.last') - i = self.text.index(INSERT) - try: - self.tk.call(self.text.orig, 'insert', INSERT, text) - except: - pass - self.update_lines() - self.update_range(i, i + "+%dc" % len(text)) - - def focus_set(self): - self.text.focus_set() - - def __setitem__(self, item, value): - if item == 'state': - self.lines['state'] = value - self.text['state'] = value - else: - Frame.__setitem__(self, item, value) - - def selectall(self, e=None): - self.text.tag_remove('Selection', '1.0', END) - self.text.tag_add('Selection', '1.0', END) - self.text.mark_set(INSERT, '1.0') - - def indent(self, e=None, dedent=False): - item = self.text.tag_ranges('Selection') - if item and not self.taboverride: - if self.deleteid: - self.after_cancel(self.deleteid) - self.dodelete = None - self.deleteid = None - head,tail = self.index('%s linestart' % item[0]),self.index('%s linestart' % item[1]) - while self.text.compare(head, '!=', END) and self.text.compare(head, '<=', tail): - if dedent and self.text.get(head) in ' \t': - self.tk.call(self.text.orig, 'delete', head) - elif not dedent: - self.tk.call(self.text.orig, 'insert', head, '\t') - head = self.index('%s +1line' % head) - self.update_range(self.index('%s linestart' % item[0]), self.index('%s lineend' % item[1])) - return True - elif not item and self.taboverride: - self.taboverride = False - - def yview(self, *args): - self.lines.yview(*args) - self.text.yview(*args) - - def yscroll(self, *args): - self.vscroll.set(*args) - self.lines.yview(MOVETO, args[0]) - - def selectline(self, e=None): - self.text.tag_remove('Selection', '1.0', END) - head = self.lines.index('current linestart') - tail = self.index('%s lineend+1c' % head) - self.text.tag_add('Selection', head, tail) - self.text.mark_set(INSERT, tail) - self.text.focus_set() - - def setedit(self): - self.edited = True - - def insert(self, index, text, tags=None): - if text == '\t': - #c,d = self.acallback != None,self.acallback() - #if (self.acallback != None and self.acallback()) or self.indent(): - if (self.acallback != None and self.acallback()) or self.indent(): - self.setedit() - return - elif self.taboverride and text in self.taboverride and self.tag_ranges('Selection'): - self.tag_remove('Selection', '1.0', END) - self.taboverride = False - self.setedit() - if self.deleteid: - try: - self.tk.call(self.text.orig, 'delete', self.dodelete[0], self.dodelete[1]) - except: - pass - self.after_cancel(self.deleteid) - self.dodelete = None - self.deleteid = None - if text == '\n': - i = self.index('%s linestart' % index) - while i != '1.0' and not self.get(i, '%s lineend' % i).split('#',1)[0]: - i = self.index('%s -1lines' % i) - m = self.autoindent.match(self.get(i, '%s lineend' % i)) - if m: - text += m.group(1) - i = self.text.index(index) - self.tk.call(self.text.orig, 'insert', i, text, tags) - self.update_lines() - self.update_range(i, i + "+%dc" % len(text)) - - def delete(self, start, end=None): - #print start,end - self.setedit() - try: - self.tk.call(self.text.orig, 'delete', start, end) - except: - pass - else: - self.update_lines() - self.update_range(start) - - def update_lines(self): - lines = self.lines.get('1.0', END).count('\n') - dif = self.text.get('1.0', END).count('\n') - lines - if dif > 0: - self.lines.insert(END, '\n' + '\n'.join(['%s%s' % (' ' * (7-len(str(n))), n) for n in range(lines+1,lines+1+dif)])) - elif dif: - self.lines.delete('%s%slines' % (END,dif),END) - - def update_range(self, start='1.0', end=END): - self.tag_add("Update", start, end) - if self.coloring: - self.coloring = False - if not self.afterid: - self.afterid = self.after(1, self.docolor) - - def update_insert(self): - if self.icallback != None: - self.icallback() - - def update_selection(self): - if self.scallback != None: - self.scallback() - - def dispatch(self, cmd, *args): - a = [] - if args: - for n in args: - if isstr(n): - a.append(self.selregex.sub('Selection', n)) - a = tuple(a) - # if self.dispatch_output: - # sys.stderr.write('%s %s' % (cmd, a)) - if cmd == 'insert': - self.after(1, self.update_insert) - self.after(1, self.update_selection) - return self.insert(*a) - elif cmd == 'delete': - self.dodelete = a - self.deleteid = self.after(1, lambda n=a: self.delete(*n)) - self.after(1, self.update_insert) - self.after(1, self.update_selection) - return - elif cmd == 'edit' and a[0] != 'separator': - self.after(1, self.update_lines) - self.after(1, self.update_range) - self.after(1, self.update_insert) - self.after(1, self.update_selection) - elif cmd == 'mark' and a[0:2] == ('set', INSERT): - self.after(1, self.update_insert) - elif cmd == 'tag' and a[1] == 'Selection' and a[0] in ['add','remove']: - if self.dnd: - return '' - self.after(1, self.update_selection) - try: - return self.tk.call((self.text.orig, cmd) + a) - except TclError: - return "" - - def setup(self, tags=None): - r = self.tag_ranges('Selection') - if self.tags: - for tag in self.tags.keys(): - self.tag_delete(tag) - if tags: - self.tags = tags - else: - self.setupparser() - self.tags['Update'] = {'foreground':None,'background':None,'font':None} - if not 'Selection' in self.tags: - self.tags['Selection'] = {'foreground':None,'background':'#C0C0C0','font':None} - for tag, cnf in self.tags.items(): - if cnf: - self.tag_configure(tag, **cnf) - self.tag_raise('Selection') - if r: - self.tag_add('Selection', *r) - self.text.tag_bind('Selection', '', self.selclick) - self.text.tag_bind('Selection', '', self.selrelease) - self.text.focus_set() - self.update_range() - - def docolor(self): - self.afterid = None - if self.coloring: - return - self.coloring = True - self.colorize() - self.coloring = None - if self.tag_nextrange('Update', '1.0'): - self.after_id = self.after(1, self.docolor) - - def setupparser(self): - # Overload to setup your own parser - pass - - def colorize(self): - # Overload to do parsing - pass - - def readlines(self): - return self.text.get('1.0',END).split('\n') - - def selclick(self, e): - self.dnd = True - self.text.bind('', self.selmotion) - - def selmotion(self, e): - self.text.mark_set(INSERT, '@%s,%s' % (e.x,e.y)) - - def selrelease(self, e): - self.dnd = False - self.text.unbind('') - sel = self.tag_nextrange('Selection', '1.0') - text = self.text.get(*sel) - self.delete(*sel) - self.text.insert(INSERT, text) - -class Tooltip: - def __init__(self, widget, text='', font=None, delay=750, press=False, mouse=False): - self.widget = widget - self.setupbinds(press) - self.text = text - self.font = font - self.delay = delay - self.mouse = mouse - self.id = None - self.tip = None - self.pos = None - - def setupbinds(self, press): - self.widget.bind('', self.enter, '+') - self.widget.bind('', self.leave, '+') - self.widget.bind('', self.motion, '+') - self.widget.bind('', self.leave, '+') - if press: - self.widget.bind('', self.leave) - - def enter(self, e=None): - self.unschedule() - self.id = self.widget.after(self.delay, self.showtip) - - def leave(self, e=None): - self.unschedule() - self.hidetip() - - def motion(self, e=None): - if self.id: - self.widget.after_cancel(self.id) - self.id = self.widget.after(self.delay, self.showtip) - - def unschedule(self): - if self.id: - self.widget.after_cancel(self.id) - self.id = None - - def showtip(self): - if self.tip: - return - self.tip = Toplevel(self.widget, relief=SOLID, borderwidth=1) - self.tip.wm_overrideredirect(1) - frame = Frame(self.tip, background='#FFFFC8', borderwidth=0) - Label(frame, text=self.text, justify=LEFT, font=self.font, background='#FFFFC8', relief=FLAT).pack(padx=1, pady=1) - frame.pack() - pos = list(self.widget.winfo_pointerxy()) - self.tip.wm_geometry('+%d+%d' % (pos[0],pos[1]+22)) - self.tip.update_idletasks() - move = False - if not self.mouse: - move = True - pos = [self.widget.winfo_rootx() + self.widget.winfo_reqwidth(), self.widget.winfo_rooty() + self.widget.winfo_reqheight()] - if pos[0] + self.tip.winfo_reqwidth() > self.tip.winfo_screenwidth(): - move = True - pos[0] = self.tip.winfo_screenwidth() - self.tip.winfo_reqwidth() - if pos[1] + self.tip.winfo_reqheight() + 22 > self.tip.winfo_screenheight(): - move = True - pos[1] -= self.tip.winfo_reqheight() + 44 - if move: - self.tip.wm_geometry('+%d+%d' % (pos[0],pos[1]+22)) - - def hidetip(self): - if self.tip: - self.tip.destroy() - self.tip = None - -class IntegerVar(StringVar): - def __init__(self, val='0', range=[None,None], exclude=[], callback=None): - StringVar.__init__(self) - self.check = True - self.defaultval = val - self.lastvalid = val - self.set(val) - self.range = range - self.exclude = exclude - self.callback = callback - self.trace('w', self.editvalue) - - def editvalue(self, *_): - #print self.check - if self.check: - #print s,self.range - if self.get(True): - try: - s = self.get() - if self.range[0] != None and self.range[0] >= 0 and self.get(True).startswith('-'): - #print '1' - raise - s = self.get() - if s in self.exclude: - #print '2' - raise - except: - #raise - s = self.lastvalid - else: - if self.range[0] != None and s < self.range[0]: - #print '3' - s = self.range[0] - elif self.range[1] != None and s > self.range[1]: - #print '4' - s = self.range[1] - #print s - self.set(s) - if self.callback: - self.callback(s) - elif self.range[0] != None: - s = self.range[0] - else: - s = self.defaultval - self.lastvalid = s - else: - self.lastvalid = self.get(True) - self.check = True - - def get(self, s=False): - if s: - return StringVar.get(self) - return int(StringVar.get(self) or 0) - - def setrange(self, range): - self.range = list(range) - self.set(max(range[0],min(range[1],self.get()))) - -class FloatVar(IntegerVar): - def __init__(self, val='0', range=[None,None], exclude=[], callback=None, precision=None): - self.precision = precision - IntegerVar.__init__(self, val, range, exclude, callback) - - def editvalue(self, *_): - if self.check: - s = self.get(True) - if s: - try: - if self.range[0] != None and self.range[0] >= 0 and self.get(True).startswith('-'): - raise - isfloat = self.get(True) - s = self.get() - if s in self.exclude: - raise - s = str(s) - if self.precision and not s.endswith('.0') and len(s)-s.index('.')-1 > self.precision: - raise - if not isfloat.endswith('.') and not isfloat.endswith('.0') and s.endswith('.0'): - s = s[:-2] - s = int(s) - else: - s = float(s) - except: - s = self.lastvalid - else: - if self.range[0] != None and s < self.range[0]: - s = self.range[0] - elif self.range[1] != None and s > self.range[1]: - s = self.range[1] - self.set(s) - if self.callback: - self.callback(s) - elif self.range[0] != None: - s = self.range[0] - else: - s = self.defaultval - self.lastvalid = s - else: - self.lastvalid = self.get(True) - self.check = True - - def get(self, s=False): - if s: - return StringVar.get(self) - return float(StringVar.get(self)) - -class SStringVar(StringVar): - def __init__(self, val='', length=0, callback=None): - StringVar.__init__(self) - self.check = True - self.length = length - self.lastvalid = val - self.set(val) - self.callback = callback - self.trace('w', self.editvalue) - - def editvalue(self, *_): - if self.check: - s = self.get() - if self.length and len(s) > self.length: - self.set(self.lastvalid) - else: - self.lastvalid = s - if self.callback: - self.callback(s) - else: - self.lastvalid = self.get() - self.check = True - -class odict: - def __init__(self, d=None, k=None): - self.keynames = [] - self.dict = {} - if d: - if k: - self.keynames = list(k) - self.dict = dict(d) - else: - self.keynames = list(d.keynames) - self.dict = dict(d.dict) - - def __delitem__(self, key): - del self.dict[key] - self.keynames.remove(key) - - def __setitem__(self, key, item): - self.dict[key] = item - if key not in self.keynames: - self.keynames.append(key) - - def __getitem__(self, key): - return self.dict[key] - - def __contains__(self, key): - if key in self.keynames: - return True - return False - - def __len__(self): - return len(self.keynames) - - def iteritems(self): - iter = [] - for k in self.keynames: - iter.append((k,self.dict[k])) - return iter - - def iterkeys(self): - return list(self.keynames) - - def peek(self): - return (self.keynames[0],self.dict[self.keynames[0]]) - - def keys(self): - return list(self.keynames) - - def index(self, key): - return self.keynames.index(key) - - def get(self, key, default=None): - if not key in self.keynames: - return default - return self.dict[key] - - def getkey(self, n): - print 'len',len(self.keynames),type(self.keynames),n - return self.keynames[n] - - def getitem(self, n): - return self.dict[self.keynames[n]] - - def remove(self, n): - self.keynames.remove(n) - del self.dict[n] - - def __repr__(self): - return '%s@%s' % (self.keynames,self.dict) - - def copy(self): - return odict(self) - - def sort(self): - self.keynames.sort() +try: + from Tkinter import * + from tkMessageBox import askquestion, OK + import tkFileDialog +except ImportError: + from tkinter import * + from tkinter.messagebox import askquestion, OK + import tkinter.filedialog as tkFileDialog + +from textwrap import wrap +import os, re, webbrowser, sys, traceback +try: + import urllib +except ImportError: + import urllib.request as urllib + +win_reg = True +try: + from _winreg import * +except ImportError: + try: + from winreg import * + except ImportError: + win_reg = False + +PyMS_VERSION = (1,2,1) +PyMS_LONG_VERSION = 'v%s.%s.%s' % PyMS_VERSION +if hasattr(sys, 'frozen'): + try: + BASE_DIR = os.path.dirname(str(sys.executable)) + except: + BASE_DIR = os.path.dirname(sys.executable) +else: + try: + BASE_DIR = os.path.dirname(os.path.dirname(str(__file__))) + except: + BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +if os.path.exists(BASE_DIR): + os.chdir(BASE_DIR) +SC_DIR = '' +if win_reg: + try: + h = OpenKey(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Blizzard Entertainment\\Starcraft') + SC_DIR = QueryValueEx(h, 'InstallPath')[0] + except: + pass + if not os.path.exists(SC_DIR): + SC_DIR = '' +couriernew = ('Courier', -12, 'normal') +couriersmall = ('Courier', -8, 'normal') +ARROW = None + +def isstr(s): + return isinstance(s,str) or isinstance(s,unicode) + +def register_registry(prog,type,filetype,progpath,icon): + if not win_reg: + raise PyMSError('Registry', 'You can currently only set as the default program on Windows machines.') + def delkey(key,sub_key): + try: + h = OpenKey(key,sub_key) + except WindowsError as e: + if e.errno == 2: + return + raise + except: + raise + try: + while True: + n = EnumKey(h,0) + delkey(h,n) + except EnvironmentError: + pass + h.Close() + DeleteKey(key,sub_key) + + key = '%s:%s' % (prog,filetype) + try: + delkey(HKEY_CLASSES_ROOT, '.' + filetype) + delkey(HKEY_CLASSES_ROOT, key) + SetValue(HKEY_CLASSES_ROOT, '.' + filetype, REG_SZ, key) + SetValue(HKEY_CLASSES_ROOT, key, REG_SZ, 'StarCraft %s *.%s file (%s)' % (type,filetype,prog)) + SetValue(HKEY_CLASSES_ROOT, key + '\\DefaultIcon', REG_SZ, icon) + SetValue(HKEY_CLASSES_ROOT, key + '\\Shell', REG_SZ, 'open') + SetValue(HKEY_CLASSES_ROOT, key + '\\Shell\\open\\command', REG_SZ, '"%s" "%s" --gui "%%1"' % (sys.executable.replace('python.exe','pythonw.exe'),progpath)) + except: + raise PyMSError('Registry', 'Could not complete file association.',exception=sys.exc_info()) + askquestion(title='Success!', message='The file association was set.', type=OK) + +def flags(value, length): + if isstr(value): + if len(value) != length or value.replace('0','').replace('1',''): + raise + return sum(int(x)*(2**n) for n,x in enumerate(reversed(value))) + return ''.join(reversed([str(value/(2**n)%2) for n in range(length)])) + +def ccopy(lst): + r = [] + for item in lst: + if isinstance(item, list): + r.append(ccopy(item)) + else: + r.append(item) + return r + +def fit(label, text, width=80, end=False, indent=0): + r = label + if not indent: + s = len(r) + else: + s = indent + indent = False + for l in wrap(text, width - s): + if indent: + r += ' ' * s + else: + indent = True + r += l + if len(l) != width - s or end: + r += '\n' + return r + +def removedir(path): + if os.path.exists(path): + for r,ds,fs in os.walk(path, topdown=False): + for f in fs: + os.remove(os.path.join(r, f)) + for d in ds: + p = os.path.join(r, d) + removedir(p) + os.rmdir(p) + os.rmdir(path) + +class DependencyError(Tk): + def __init__(self, prog, msg, hotlink=None): + #Window + Tk.__init__(self) + self.resizable(False,False) + self.title('Dependency Error') + try: + self.icon = os.path.join(BASE_DIR,'Images','%s.ico' % prog) + self.wm_iconbitmap(self.icon) + except: + self.icon = '@%s' % os.path.join(BASE_DIR, 'Images','%s.xbm' % prog) + self.wm_iconbitmap(self.icon) + Label(self, text=msg, anchor=W, justify=LEFT).pack(side=TOP,pady=2,padx=2) + if hotlink: + f = Frame(self) + Hotlink(f, *hotlink).pack(side=RIGHT, padx=10, pady=2) + f.pack(side=TOP,fill=X) + Button(self, text='Ok', width=10, command=self.destroy).pack(side=TOP, pady=2) + self.update_idletasks() + w,h = self.winfo_width(),self.winfo_height() + self.geometry('%ix%i+%i+%i' % (w,h,(self.winfo_screenwidth() - w)/2,(self.winfo_screenheight() - h)/2)) + +class PyMSError(Exception): + def __init__(self, type, error, line=None, code=None, warnings=[], exception=None): + self.type = type + self.error = error + self.line = line + if self.line != None: + self.line += 1 + self.code = code + self.warnings = warnings + self.exception = exception + + def repr(self): + r = '%s Error: %s' % (self.type, self.error) + if self.line: + r += '\n Line %s: %s' % (self.line, self.code) + return r + + def __repr__(self): + r = fit('%s Error: ' % self.type, self.error) + if self.line: + r += fit(' Line %s: ' % self.line, self.code) + if self.warnings: + for w in self.warnings: + r += repr(w) + return r[:-1] + +class PyMSWarning(Exception): + def __init__(self, type, warning, line=None, code=None, extra=None, level=0): + self.type = type + self.warning = warning + self.line = line + if self.line != None: + self.line += 1 + self.code = code + self.extra = extra + self.level = level + + def repr(self): + r = fit('%s Warning: ' % self.type, self.warning, end=True) + if self.line: + r += fit(' Line %s: ' % self.line, self.code, end=True) + return r + + def __repr__(self): + r = fit('%s Warning: ' % self.type, self.warning) + if self.line: + r += fit(' Line %s: ' % self.line, self.code) + return r[:-1] + +class PyMSWarnList(Exception): + def __init__(self, warnings): + self.warnings = warnings + + def __repr__(self): + r = '' + for w in self.warnings: + r += repr(w) + return r[:-1] + +class PyMSDialog(Toplevel): + def __init__(self, parent, title, center=True, grabwait=True, hidden=False): + Toplevel.__init__(self, parent) + self.title(title) + self.icon = parent.icon + self.wm_iconbitmap(parent.icon) + self.protocol('WM_DELETE_WINDOW', self.cancel) + #self.transient(parent) + self.parent = parent + focus = self.widgetize() + if not focus: + focus = self + focus.focus_set() + self.update_idletasks() + size = self.winfo_geometry().split('+',1)[0].split('x') + if center: + self.geometry('+%d+%d' % (self.winfo_screenwidth()/2-int(size[0])/2,self.winfo_screenheight()/2-int(size[1])/2)) + if grabwait: + self.grab_set() + self.wait_window(self) + + def widgetize(self): + pass + + def ok(self): + self.withdraw() + self.update_idletasks() + self.parent.focus_set() + self.destroy() + + def cancel(self): + self.ok() + +class InternalErrorDialog(PyMSDialog): + def __init__(self, parent, prog, handler=None, txt=None): + self.prog = prog + self.handler = handler + self.txt = txt + PyMSDialog.__init__(self, parent, 'PyMS Internal Error!', grabwait=not not txt) + + def widgetize(self): + self.bind('', self.selectall) + Label(self, text='The PyMS program "%s" has encountered an unknown internal error.\nThe program will attempt to continue, but may cause problems or crash once you press Ok.\nPlease contact poiuy_qwert and send him this traceback with any relivant information.' % self.prog, justify=LEFT).pack(side=TOP, padx=2, pady=2, fill=X) + r = Frame(self) + Hotlink(r, 'Contact', self.contact).pack(side=RIGHT, padx=10, pady=2) + r.pack(fill=X) + frame = Frame(self, bd=2, relief=SUNKEN) + hscroll = Scrollbar(frame, orient=HORIZONTAL) + vscroll = Scrollbar(frame) + self.text = Text(frame, bd=0, highlightthickness=0, width=70, height=10, xscrollcommand=hscroll.set, yscrollcommand=vscroll.set, wrap=NONE, exportselection=0, state=DISABLED) + if self.txt: + self.text['state'] = NORMAL + self.text.insert(END, self.txt) + self.text['state'] = DISABLED + self.text.grid(sticky=NSEW) + hscroll.config(command=self.text.xview) + hscroll.grid(sticky=EW) + vscroll.config(command=self.text.yview) + vscroll.grid(sticky=NS, row=0, column=1) + frame.grid_rowconfigure(0, weight=1) + frame.grid_columnconfigure(0, weight=1) + frame.pack(fill=BOTH, pady=2, padx=2, expand=1) + buttonbar = Frame(self) + ok = Button(buttonbar, text='Ok', width=10, command=self.ok) + ok.pack(side=LEFT, padx=3) + buttonbar.pack(side=BOTTOM, pady=10) + return ok + + def selectall(self, key=None): + self.text.focus_set() + self.text.tag_add(SEL, 1.0, END) + + def ok(self): + if self.handler: + self.handler.window = None + PyMSDialog.ok(self) + + def contact(self, e=None): + webbrowser.open(os.path.join(os.path.dirname(BASE_DIR), 'Docs', 'intro.html')) + +class ErrorDialog(PyMSDialog): + def __init__(self, parent, error): + self.error = error + PyMSDialog.__init__(self, parent, '%s Error!' % error.type) + + def widgetize(self): + self.resizable(False, False) + Label(self, justify=LEFT, anchor=W, text=self.error.repr(), wraplen=640).pack(pady=10, padx=5) + frame = Frame(self) + ok = Button(frame, text='Ok', width=10, command=self.ok) + ok.pack(side=LEFT, padx=3) + w = len(self.error.warnings) + p = 's' + if w == 1: + p = '' + Button(frame, text='%s Warning%s' % (w, p), width=10, command=self.viewwarnings, state=[NORMAL,DISABLED][not self.error.warnings]).pack(side=LEFT, padx=3) + Button(frame, text='Copy', width=10, command=self.copy).pack(side=LEFT, padx=6) + if self.error.exception: + Button(frame, text='Internal Error', width=10, command=self.internal).pack(side=LEFT, padx=6) + frame.pack(pady=10) + return ok + + def copy(self): + self.clipboard_clear() + self.clipboard_append(self.error.repr()) + + def viewwarnings(self): + WarningDialog(self, self.error.warnings) + + def internal(self): + InternalErrorDialog(self, sys.stderr.prog, txt=''.join(traceback.format_exception(*self.error.exception))) + +class WarningDialog(PyMSDialog): + def __init__(self, parent, warnings, cont=False): + self.warnings = warnings + self.cont = cont + PyMSDialog.__init__(self, parent, 'Warning!') + + def widgetize(self): + self.bind('', self.selectall) + self.resizable(False, False) + frame = Frame(self, bd=2, relief=SUNKEN) + hscroll = Scrollbar(frame, orient=HORIZONTAL) + vscroll = Scrollbar(frame) + self.warntext = Text(frame, bd=0, highlightthickness=0, width=60, height=10, xscrollcommand=hscroll.set, yscrollcommand=vscroll.set, wrap=NONE, exportselection=0) + self.warntext.tag_config('highlevel', foreground='#960000') + self.warntext.grid() + hscroll.config(command=self.warntext.xview) + hscroll.grid(sticky=EW) + vscroll.config(command=self.warntext.yview) + vscroll.grid(sticky=NS, row=0, column=1) + for warning in self.warnings: + if warning.level: + self.warntext.insert(END, warning.repr(), 'highlevel') + else: + self.warntext.insert(END, warning.repr()) + self.warntext['state'] = DISABLED + frame.pack(side=TOP, pady=2, padx=2) + buttonbar = Frame(self) + ok = Button(buttonbar, text='Ok', width=10, command=self.ok) + ok.pack(side=LEFT, padx=3) + if self.cont: + Button(buttonbar, text='Cancel', width=10, command=self.cancel).pack(side=LEFT) + buttonbar.pack(pady=10) + return ok + + def selectall(self, key=None): + self.warntext.focus_set() + self.warntext.tag_add(SEL, 1.0, END) + + def ok(self): + self.cont = True + PyMSDialog.ok(self) + + def cancel(self): + self.cont = False + PyMSDialog.ok(self) + +class AboutDialog(PyMSDialog): + def __init__(self, parent, program, version, thanks=[]): + self.program = program + self.version = version + self.thanks = thanks + self.thanks.extend([ + ('ShadowFlare','For SFmpq, some file specs, and all her tools!'), + ('BroodWarAI.com','Support and hosting of course!'), + ('Blizzard','For creating StarCraft and BroodWar...'), + ]) + PyMSDialog.__init__(self, parent, 'About %s' % program) + + def widgetize(self): + self.resizable(False, False) + name = Label(self, text='%s %s' % (self.program, self.version), font=('Courier', 18, 'bold')) + name.pack() + frame = Frame(self) + Label(frame, text='Author:').grid(stick=E) + Label(frame, text='Homepage:').grid(stick=E) + Hotlink(frame, 'poiuy_qwert (p.q.poiuy_qwert@gmail.com)', self.author).grid(row=0, column=1, stick=W) + Hotlink(frame, 'http://www.broodwarai.com/index.php?page=pyms', self.homepage).grid(row=1, column=1, stick=W) + frame.pack(padx=1, pady=2) + if self.thanks: + Label(self, text='Special Thanks To:', font=('Courier', 10, 'bold')).pack(pady=2) + thanks = Frame(self) + font = ('Courier', 8, 'bold') + row = 0 + for who,why in self.thanks: + if who == 'BroodWarAI.com': + Hotlink(thanks, who, self.broodwarai, [('Courier', 8, 'bold'),('Courier', 8, 'bold underline')]).grid(sticky=E) + else: + Label(thanks, text=who, font=font).grid(stick=E) + Label(thanks, text=why).grid(row=row, column=1, stick=W) + row += 1 + thanks.pack(pady=1) + ok = Button(self, text='Ok', width=10, command=self.ok) + ok.pack(pady=5) + return ok + + def author(self, e=None): + webbrowser.open('mailto:p.q.poiuy.qwert@hotmail.com') + + def homepage(self, e=None): + webbrowser.open('http://www.broodwarai.com/index.php?page=pyms') + + def broodwarai(self, e=None): + webbrowser.open('http://www.broodwarai.com') + +class Hotlink(Label): + def __init__(self, parent, text, callback=None, fonts=[('Courier', 8, 'normal'),('Courier', 8, 'underline')]): + self.fonts = fonts + Label.__init__(self, parent, text=text, foreground='#0000FF', cursor='hand2', font=fonts[0]) + self.bind('', self.enter) + self.bind('', self.leave) + if callback: + self.bind('', callback) + + def enter(self, e): + self['font'] = self.fonts[1] + + def leave(self, e): + self['font'] = self.fonts[0] + +class Notebook(Frame): + def __init__(self, parent, relief=RAISED, switchcallback=None): + self.parent = parent + self.active = None + self.tab = IntVar() + self.notebook = Frame(parent) + self.tabs = Frame(self.notebook) + self.tabs.pack(fill=X) + self.pages = {} + Frame.__init__(self, self.notebook, borderwidth=2, relief=relief) + Frame.pack(self, fill=BOTH, expand=1) + + def pack(self, **kw): + self.notebook.pack(kw) + + def add_tab(self, fr, title): + b = Radiobutton(self.tabs, text=title, indicatoron=0, variable=self.tab, value=len(self.pages), command=lambda: self.display(title)) + b.pack(side=LEFT) + self.pages[title] = [fr,len(self.pages)] + if not self.active: + self.display(title) + return b + + def display(self, title): + if self.active: + if hasattr(self.active, 'deactivate'): + self.active.deactivate() + self.active.forget() + self.tab.set(self.pages[title][1]) + self.active = self.pages[title][0] + self.active.pack(fill=BOTH, expand=1, padx=6, pady=6) + if hasattr(self.active, 'activate'): + self.active.activate() + +class NotebookTab(Frame): + def __init__(self, parent): + self.parent = parent + Frame.__init__(self, parent) + + def activate(self): + pass + + def deactivate(self): + pass + +class DropDown(Frame): + def __init__(self, parent, variable, entries, display=None, width=1, blank=None, state=NORMAL): + self.variable = variable + self.variable.set = self.set + self.display = display + self.blank = blank + if display and isinstance(display, Variable): + display.callback = self.set + self.size = min(10,len(entries)) + Frame.__init__(self, parent, borderwidth=2, relief=SUNKEN) + self.listbox = Listbox(self, selectmode=SINGLE, font=couriernew, width=width, height=1, borderwidth=0, exportselection=1, activestyle=DOTBOX) + self.listbox.bind('', self.choose) + bind = [ + ('', self.scroll), + ('', lambda a,i=0: self.move(a,i)), + ('', lambda a,i=END: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=1: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=-10: self.move(a,i)), + ('', lambda a,i=10: self.move(a,i)), + ] + for b in bind: + self.bind(*b) + self.setentries(entries) + self.listbox.pack(side=LEFT, fill=X, expand=1) + self.listbox['state'] = state + global ARROW + if not ARROW: + ARROW = PhotoImage(file=os.path.join(BASE_DIR, 'Images', 'arrow.gif')) + self.button = Button(self, image=ARROW, command=self.choose, state=state) + self.button.image = ARROW + self.button.pack(side=LEFT, fill=Y) + + def __setitem__(self, item, value): + if item == 'state': + self.listbox['state'] = value + self.button['state'] = value + else: + Frame.__setitem__(self, item, value) + + def setentries(self, entries): + self.entries = list(entries) + self.listbox.delete(0,END) + for entry in entries: + self.listbox.insert(END, entry) + if self.blank != None: + self.listbox.insert(END, '') + self.listbox.see(self.variable.get()) + + def set(self, num): + self.change(num) + Variable.set(self.variable, num) + self.disp(num) + + def change(self, num): + if num >= self.listbox.size(): + num = self.listbox.size()-1 + self.listbox.select_clear(0,END) + #self.listbox.select_set(num) + self.listbox.see(num) + + def scroll(self, e): + if self.listbox['state'] == NORMAL: + if e.delta > 0: + self.move(None, -1) + elif self.blank != None or self.variable.get() < self.listbox.size()-2: + self.move(None, 1) + + def move(self, e, a): + if self.listbox['state'] == NORMAL: + if a == END and self.blank != None: + a = self.listbox.size()-2 + elif a not in [0,END]: + a = max(min([self.listbox.size()-2,self.listbox.size()-1][self.blank == None],self.variable.get() + a),0) + self.set(a) + self.listbox.select_set(a) + + def choose(self, e=None): + if self.listbox['state'] == NORMAL: + i = self.variable.get() + c = DropDownChooser(self, self.entries, i) + self.set(c.result) + self.listbox.select_set(c.result) + + def disp(self, n): + if self.display: + if n == self.listbox.size()-1 and self.blank != None: + n = self.blank + if isinstance(self.display, Variable): + self.display.set(n) + else: + self.display(n) + +class TextDropDown(Frame): + def __init__(self, parent, variable, history=[], width=None, state=NORMAL): + self.variable = variable + self.set = self.variable.set + self.history = history + Frame.__init__(self, parent, borderwidth=2, relief=SUNKEN) + self.entry = Entry(self, textvariable=self.variable, width=width, bd=0) + self.entry.pack(side=LEFT, fill=X, expand=1) + self.entry['state'] = state + global ARROW + if not ARROW: + ARROW = PhotoImage(file=os.path.join(BASE_DIR, 'Images', 'arrow.gif')) + self.button = Button(self, image=ARROW, command=self.choose, state=state) + self.button.pack(side=LEFT, fill=Y) + + def focus_set(self): + self.entry.focus_set() + + def __setitem__(self, item, value): + if item == 'state': + self.entry['state'] = value + self.button['state'] = value + else: + self.entry[item] = value + + def __getitem__(self, item): + return self.entry[item] + + def choose(self, e=None): + if self.entry['state'] == NORMAL and self.history: + i = -1 + if self.variable.get() in self.history: + i = self.history.index(self.variable.get()) + c = DropDownChooser(self, self.history, i) + if c.result > -1: + self.variable.set(self.history[c.result]) + +class DropDownChooser(Toplevel): + def __init__(self, parent, list, select): + self.focus = 0 + self.parent = parent + self.result = select + Toplevel.__init__(self, parent, relief=SOLID, borderwidth=1) + self.protocol('WM_LOSE_FOCUS', self.select) + self.wm_overrideredirect(1) + scrollbar = Scrollbar(self) + self.listbox = Listbox(self, selectmode=SINGLE, height=min(10,len(list)), borderwidth=0, font=couriernew, highlightthickness=0, yscrollcommand=scrollbar.set, activestyle=DOTBOX) + for e in list: + self.listbox.insert(END,e) + if self.result > -1: + self.listbox.select_set(self.result) + self.listbox.see(self.result) + self.listbox.bind('', self.select) + bind = [ + ('', lambda e,i=1: self.enter(e,i)), + ('', lambda e,i=0: self.enter(e,i)), + ('', self.focusout), + ('', self.select), + ('', self.close), + ('', self.scroll), + ('', lambda a,i=0: self.move(a,i)), + ('', lambda a,i=END: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=1: self.move(a,i)), + ('', lambda a,i=-1: self.move(a,i)), + ('', lambda a,i=-10: self.move(a,i)), + ('', lambda a,i=10: self.move(a,i)), + ] + for b in bind: + self.bind(*b) + scrollbar.config(command=self.listbox.yview) + if len(list) > 10: + scrollbar.pack(side=RIGHT, fill=Y) + self.listbox.pack(side=LEFT, fill=BOTH, expand=1) + self.focus_set() + self.update_idletasks() + size = self.parent.winfo_geometry().split('+',1)[0].split('x') + if self.parent.winfo_rooty() + self.parent.winfo_reqheight() + self.winfo_reqheight() > self.winfo_screenheight(): + self.geometry('%sx%s+%d+%d' % (size[0],self.winfo_reqheight(),self.parent.winfo_rootx(), self.parent.winfo_rooty() - self.winfo_reqheight())) + else: + self.geometry('%sx%s+%d+%d' % (size[0],self.winfo_reqheight(),self.parent.winfo_rootx(), self.parent.winfo_rooty() + self.parent.winfo_reqheight())) + self.grab_set() + self.update_idletasks() + self.wait_window(self) + + def enter(self, e, f): + self.focus = f + + def focusout(self, e): + if not self.focus: + self.select() + + def move(self, e, a): + if not a in [0,END]: + a = max(min(self.listbox.size()-1,int(self.listbox.curselection()[0]) + a),0) + self.listbox.select_clear(0,END) + self.listbox.select_set(a) + self.listbox.see(a) + + def scroll(self, e): + if e.delta > 0: + self.listbox.yview('scroll', -2, 'units') + else: + self.listbox.yview('scroll', 2, 'units') + + def home(self, e): + self.listbox.yview('moveto', 0.0) + + def end(self, e): + self.listbox.yview('moveto', 1.0) + + def up(self, e): + self.listbox.yview('scroll', -1, 'units') + + def down(self, e): + self.listbox.yview('scroll', 1, 'units') + + def pageup(self, e): + self.listbox.yview('scroll', -1, 'pages') + + def pagedown(self, e): + self.listbox.yview('scroll', 1, 'pages') + + def select(self, e=None): + s = self.listbox.curselection() + if s: + self.result = int(s[0]) + self.close() + + def close(self, e=None): + self.withdraw() + self.update_idletasks() + self.destroy() + self.parent.focus_set() + +class CodeText(Frame): + autoindent = re.compile('^([ \\t]*)') + selregex = re.compile('\\bsel\\b') + + def __init__(self, parent, ecallback=None, icallback=None, scallback=None, acallback=None, state=NORMAL): + self.dispatch_output = False + self.edited = False + self.taboverride = False + # Edit Callback + # INSERT Callback + # Selection Callback + # Auto-complete Callback + self.ecallback = ecallback + self.icallback = icallback + self.scallback = scallback + self.acallback = acallback + + Frame.__init__(self, parent, bd=2, relief=SUNKEN) + frame = Frame(self) + font = ('Courier New', -12, 'normal') + self.lines = Text(frame, height=1, font=font, bd=0, bg='#E4E4E4', fg='#808080', width=8, cursor='') + self.lines.pack(side=LEFT, fill=Y) + hscroll = Scrollbar(self, orient=HORIZONTAL) + self.vscroll = Scrollbar(self) + self.text = Text(frame, height=1, font=font, bd=0, undo=1, maxundo=100, wrap=NONE, xscrollcommand=hscroll.set, yscrollcommand=self.yscroll, exportselection=0) + self.text.configure(tabs=self.tk.call("font", "measure", self.text["font"], "-displayof", frame, ' ')) + self.text.pack(side=LEFT, fill=BOTH, expand=1) + self.text.bind('', lambda e: self.after(1, self.selectall)) + self.text.bind('', lambda e,i=True: self.indent(e, i)) + self.text.bind('', self.popup) + frame.grid(sticky=NSEW) + hscroll.config(command=self.text.xview) + hscroll.grid(sticky=EW) + self.vscroll.config(command=self.yview) + self.vscroll.grid(sticky=NS, row=0, column=1) + self.grid_rowconfigure(0,weight=1) + self.grid_columnconfigure(0,weight=1) + + textmenu = [ + ('Undo', self.undo, 0), # 0 + None, + ('Cut', lambda: self.copy(True), 2), # 2 + ('Copy', self.copy, 0), # 3 + ('Paste', self.paste, 0), # 4 + ('Delete', lambda: self.text.delete('Selection.first', 'Selection.last'), 0), # 5 + None, + ('Select All', lambda: self.after(1, self.selectall), 7), # 7 + ] + self.textmenu = Menu(self, tearoff=0) + for m in textmenu: + if m: + l,c,u = m + self.textmenu.add_command(label=l, command=c, underline=u) + else: + self.textmenu.add_separator() + + self.lines.insert('1.0', ' 1') + self.lines.bind('', self.selectline) + self.text.mark_set('return', '1.0') + self.text.orig = self.text._w + '_orig' + self.tk.call('rename', self.text._w, self.text.orig) + self.tk.createcommand(self.text._w, self.dispatch) + + self['state'] = state + + self.tag_configure = self.text.tag_configure + self.tag_add = self.text.tag_add + self.tag_remove = self.text.tag_remove + self.tag_raise = self.text.tag_raise + self.tag_nextrange = self.text.tag_nextrange + self.tag_prevrange = self.text.tag_prevrange + self.tag_ranges = self.text.tag_ranges + self.tag_names = self.text.tag_names + self.tag_bind = self.text.tag_bind + self.tag_delete = self.text.tag_delete + self.mark_set = self.text.mark_set + self.index = self.text.index + self.get = self.text.get + self.see = self.text.see + self.compare = self.text.compare + self.edited = False + self.afterid = None + self.dodelete = None + self.deleteid = None + self.tags = {} + # None - Nothing, True - Continue coloring, False - Stop coloring + self.coloring = None + self.dnd = False + + self.setup() + + def popup(self, e): + if self.text['state'] == NORMAL: + s,i,r = self.text.index('@1,1'),self.text.index(INSERT),self.text.tag_ranges('Selection') + try: + self.text.edit_undo() + except: + self.textmenu.entryconfig(0, state=DISABLED) + else: + self.text.edit_redo() + self.textmenu.entryconfig(0, state=NORMAL) + if r: + self.tag_add('Selection', *r) + self.text.mark_set(INSERT,i) + self.text.yview_pickplace(s) + s = s.split('.')[0] + if s in '1 2 3 4 5 6 7 8': + self.text.yview_scroll(s, 'lines') + if not self.text.tag_ranges('Selection'): + sel = DISABLED + else: + sel = NORMAL + for i in [2,3,5]: + self.textmenu.entryconfig(i, state=sel) + try: + c = not self.selection_get(selection='CLIPBOARD') + except: + c = 1 + self.textmenu.entryconfig(4, state=[NORMAL,DISABLED][c]) + self.textmenu.post(e.x_root, e.y_root) + + def undo(self): + self.text.edit_undo() + + def copy(self, cut=False): + self.clipboard_clear() + self.clipboard_append(self.text.get('Selection.first','Selection.last')) + if cut: + r = self.text.tag_ranges('Selection') + self.text.delete('Selection.first','Selection.last') + self.update_lines() + self.update_range('%s linestart' % r[0], '%s lineend' % r[1]) + + def paste(self): + try: + text = self.selection_get(selection='CLIPBOARD') + except: + pass + else: + if self.text.tag_ranges('Selection'): + self.text.mark_set(INSERT, 'Selection.first') + self.text.delete('Selection.first','Selection.last') + i = self.text.index(INSERT) + try: + self.tk.call(self.text.orig, 'insert', INSERT, text) + except: + pass + self.update_lines() + self.update_range(i, i + "+%dc" % len(text)) + + def focus_set(self): + self.text.focus_set() + + def __setitem__(self, item, value): + if item == 'state': + self.lines['state'] = value + self.text['state'] = value + else: + Frame.__setitem__(self, item, value) + + def selectall(self, e=None): + self.text.tag_remove('Selection', '1.0', END) + self.text.tag_add('Selection', '1.0', END) + self.text.mark_set(INSERT, '1.0') + + def indent(self, e=None, dedent=False): + item = self.text.tag_ranges('Selection') + if item and not self.taboverride: + if self.deleteid: + self.after_cancel(self.deleteid) + self.dodelete = None + self.deleteid = None + head,tail = self.index('%s linestart' % item[0]),self.index('%s linestart' % item[1]) + while self.text.compare(head, '!=', END) and self.text.compare(head, '<=', tail): + if dedent and self.text.get(head) in ' \t': + self.tk.call(self.text.orig, 'delete', head) + elif not dedent: + self.tk.call(self.text.orig, 'insert', head, '\t') + head = self.index('%s +1line' % head) + self.update_range(self.index('%s linestart' % item[0]), self.index('%s lineend' % item[1])) + return True + elif not item and self.taboverride: + self.taboverride = False + + def yview(self, *args): + self.lines.yview(*args) + self.text.yview(*args) + + def yscroll(self, *args): + self.vscroll.set(*args) + self.lines.yview(MOVETO, args[0]) + + def selectline(self, e=None): + self.text.tag_remove('Selection', '1.0', END) + head = self.lines.index('current linestart') + tail = self.index('%s lineend+1c' % head) + self.text.tag_add('Selection', head, tail) + self.text.mark_set(INSERT, tail) + self.text.focus_set() + + def setedit(self): + self.edited = True + + def insert(self, index, text, tags=None): + if text == '\t': + #c,d = self.acallback != None,self.acallback() + #if (self.acallback != None and self.acallback()) or self.indent(): + if (self.acallback != None and self.acallback()) or self.indent(): + self.setedit() + return + elif self.taboverride and text in self.taboverride and self.tag_ranges('Selection'): + self.tag_remove('Selection', '1.0', END) + self.taboverride = False + self.setedit() + if self.deleteid: + try: + self.tk.call(self.text.orig, 'delete', self.dodelete[0], self.dodelete[1]) + except: + pass + self.after_cancel(self.deleteid) + self.dodelete = None + self.deleteid = None + if text == '\n': + i = self.index('%s linestart' % index) + while i != '1.0' and not self.get(i, '%s lineend' % i).split('#',1)[0]: + i = self.index('%s -1lines' % i) + m = self.autoindent.match(self.get(i, '%s lineend' % i)) + if m: + text += m.group(1) + i = self.text.index(index) + self.tk.call(self.text.orig, 'insert', i, text, tags) + self.update_lines() + self.update_range(i, i + "+%dc" % len(text)) + + def delete(self, start, end=None): + #print(start),end + self.setedit() + try: + self.tk.call(self.text.orig, 'delete', start, end) + except: + pass + else: + self.update_lines() + self.update_range(start) + + def update_lines(self): + lines = self.lines.get('1.0', END).count('\n') + dif = self.text.get('1.0', END).count('\n') - lines + if dif > 0: + self.lines.insert(END, '\n' + '\n'.join(['%s%s' % (' ' * (7-len(str(n))), n) for n in range(lines+1,lines+1+dif)])) + elif dif: + self.lines.delete('%s%slines' % (END,dif),END) + + def update_range(self, start='1.0', end=END): + self.tag_add("Update", start, end) + if self.coloring: + self.coloring = False + if not self.afterid: + self.afterid = self.after(1, self.docolor) + + def update_insert(self): + if self.icallback != None: + self.icallback() + + def update_selection(self): + if self.scallback != None: + self.scallback() + + def dispatch(self, cmd, *args): + a = [] + if args: + for n in args: + if isstr(n): + a.append(self.selregex.sub('Selection', n)) + a = tuple(a) + # if self.dispatch_output: + # sys.stderr.write('%s %s' % (cmd, a)) + if cmd == 'insert': + self.after(1, self.update_insert) + self.after(1, self.update_selection) + return self.insert(*a) + elif cmd == 'delete': + self.dodelete = a + self.deleteid = self.after(1, lambda n=a: self.delete(*n)) + self.after(1, self.update_insert) + self.after(1, self.update_selection) + return + elif cmd == 'edit' and a[0] != 'separator': + self.after(1, self.update_lines) + self.after(1, self.update_range) + self.after(1, self.update_insert) + self.after(1, self.update_selection) + elif cmd == 'mark' and a[0:2] == ('set', INSERT): + self.after(1, self.update_insert) + elif cmd == 'tag' and a[1] == 'Selection' and a[0] in ['add','remove']: + if self.dnd: + return '' + self.after(1, self.update_selection) + try: + return self.tk.call((self.text.orig, cmd) + a) + except TclError: + return "" + + def setup(self, tags=None): + r = self.tag_ranges('Selection') + if self.tags: + for tag in self.tags.keys(): + self.tag_delete(tag) + if tags: + self.tags = tags + else: + self.setupparser() + self.tags['Update'] = {'foreground':None,'background':None,'font':None} + if not 'Selection' in self.tags: + self.tags['Selection'] = {'foreground':None,'background':'#C0C0C0','font':None} + for tag, cnf in self.tags.items(): + if cnf: + self.tag_configure(tag, **cnf) + self.tag_raise('Selection') + if r: + self.tag_add('Selection', *r) + self.text.tag_bind('Selection', '', self.selclick) + self.text.tag_bind('Selection', '', self.selrelease) + self.text.focus_set() + self.update_range() + + def docolor(self): + self.afterid = None + if self.coloring: + return + self.coloring = True + self.colorize() + self.coloring = None + if self.tag_nextrange('Update', '1.0'): + self.after_id = self.after(1, self.docolor) + + def setupparser(self): + # Overload to setup your own parser + pass + + def colorize(self): + # Overload to do parsing + pass + + def readlines(self): + return self.text.get('1.0',END).split('\n') + + def selclick(self, e): + self.dnd = True + self.text.bind('', self.selmotion) + + def selmotion(self, e): + self.text.mark_set(INSERT, '@%s,%s' % (e.x,e.y)) + + def selrelease(self, e): + self.dnd = False + self.text.unbind('') + sel = self.tag_nextrange('Selection', '1.0') + text = self.text.get(*sel) + self.delete(*sel) + self.text.insert(INSERT, text) + +class Tooltip: + def __init__(self, widget, text='', font=None, delay=750, press=False, mouse=False): + self.widget = widget + self.setupbinds(press) + self.text = text + self.font = font + self.delay = delay + self.mouse = mouse + self.id = None + self.tip = None + self.pos = None + + def setupbinds(self, press): + self.widget.bind('', self.enter, '+') + self.widget.bind('', self.leave, '+') + self.widget.bind('', self.motion, '+') + self.widget.bind('', self.leave, '+') + if press: + self.widget.bind('', self.leave) + + def enter(self, e=None): + self.unschedule() + self.id = self.widget.after(self.delay, self.showtip) + + def leave(self, e=None): + self.unschedule() + self.hidetip() + + def motion(self, e=None): + if self.id: + self.widget.after_cancel(self.id) + self.id = self.widget.after(self.delay, self.showtip) + + def unschedule(self): + if self.id: + self.widget.after_cancel(self.id) + self.id = None + + def showtip(self): + if self.tip: + return + self.tip = Toplevel(self.widget, relief=SOLID, borderwidth=1) + self.tip.wm_overrideredirect(1) + frame = Frame(self.tip, background='#FFFFC8', borderwidth=0) + Label(frame, text=self.text, justify=LEFT, font=self.font, background='#FFFFC8', relief=FLAT).pack(padx=1, pady=1) + frame.pack() + pos = list(self.widget.winfo_pointerxy()) + self.tip.wm_geometry('+%d+%d' % (pos[0],pos[1]+22)) + self.tip.update_idletasks() + move = False + if not self.mouse: + move = True + pos = [self.widget.winfo_rootx() + self.widget.winfo_reqwidth(), self.widget.winfo_rooty() + self.widget.winfo_reqheight()] + if pos[0] + self.tip.winfo_reqwidth() > self.tip.winfo_screenwidth(): + move = True + pos[0] = self.tip.winfo_screenwidth() - self.tip.winfo_reqwidth() + if pos[1] + self.tip.winfo_reqheight() + 22 > self.tip.winfo_screenheight(): + move = True + pos[1] -= self.tip.winfo_reqheight() + 44 + if move: + self.tip.wm_geometry('+%d+%d' % (pos[0],pos[1]+22)) + + def hidetip(self): + if self.tip: + self.tip.destroy() + self.tip = None + +class IntegerVar(StringVar): + def __init__(self, val='0', range=[None,None], exclude=[], callback=None): + StringVar.__init__(self) + self.check = True + self.defaultval = val + self.lastvalid = val + self.set(val) + self.range = range + self.exclude = exclude + self.callback = callback + self.trace('w', self.editvalue) + + def editvalue(self, *_): + #print(self).check + if self.check: + #print(s),self.range + if self.get(True): + try: + s = self.get() + if self.range[0] != None and self.range[0] >= 0 and self.get(True).startswith('-'): + #print('1') + raise + s = self.get() + if s in self.exclude: + #print('2') + raise + except: + #raise + s = self.lastvalid + else: + if self.range[0] != None and s < self.range[0]: + #print('3') + s = self.range[0] + elif self.range[1] != None and s > self.range[1]: + #print('4') + s = self.range[1] + #print(s) + self.set(s) + if self.callback: + self.callback(s) + elif self.range[0] != None: + s = self.range[0] + else: + s = self.defaultval + self.lastvalid = s + else: + self.lastvalid = self.get(True) + self.check = True + + def get(self, s=False): + if s: + return StringVar.get(self) + return int(StringVar.get(self) or 0) + + def setrange(self, range): + self.range = list(range) + self.set(max(range[0],min(range[1],self.get()))) + +class FloatVar(IntegerVar): + def __init__(self, val='0', range=[None,None], exclude=[], callback=None, precision=None): + self.precision = precision + IntegerVar.__init__(self, val, range, exclude, callback) + + def editvalue(self, *_): + if self.check: + s = self.get(True) + if s: + try: + if self.range[0] != None and self.range[0] >= 0 and self.get(True).startswith('-'): + raise + isfloat = self.get(True) + s = self.get() + if s in self.exclude: + raise + s = str(s) + if self.precision and not s.endswith('.0') and len(s)-s.index('.')-1 > self.precision: + raise + if not isfloat.endswith('.') and not isfloat.endswith('.0') and s.endswith('.0'): + s = s[:-2] + s = int(s) + else: + s = float(s) + except: + s = self.lastvalid + else: + if self.range[0] != None and s < self.range[0]: + s = self.range[0] + elif self.range[1] != None and s > self.range[1]: + s = self.range[1] + self.set(s) + if self.callback: + self.callback(s) + elif self.range[0] != None: + s = self.range[0] + else: + s = self.defaultval + self.lastvalid = s + else: + self.lastvalid = self.get(True) + self.check = True + + def get(self, s=False): + if s: + return StringVar.get(self) + return float(StringVar.get(self)) + +class SStringVar(StringVar): + def __init__(self, val='', length=0, callback=None): + StringVar.__init__(self) + self.check = True + self.length = length + self.lastvalid = val + self.set(val) + self.callback = callback + self.trace('w', self.editvalue) + + def editvalue(self, *_): + if self.check: + s = self.get() + if self.length and len(s) > self.length: + self.set(self.lastvalid) + else: + self.lastvalid = s + if self.callback: + self.callback(s) + else: + self.lastvalid = self.get() + self.check = True + +class odict: + def __init__(self, d=None, k=None): + self.keynames = [] + self.dict = {} + if d: + if k: + self.keynames = list(k) + self.dict = dict(d) + else: + self.keynames = list(d.keynames) + self.dict = dict(d.dict) + + def __delitem__(self, key): + del self.dict[key] + self.keynames.remove(key) + + def __setitem__(self, key, item): + self.dict[key] = item + if key not in self.keynames: + self.keynames.append(key) + + def __getitem__(self, key): + return self.dict[key] + + def __contains__(self, key): + if key in self.keynames: + return True + return False + + def __len__(self): + return len(self.keynames) + + def iteritems(self): + iter = [] + for k in self.keynames: + iter.append((k,self.dict[k])) + return iter + + def iterkeys(self): + return list(self.keynames) + + def peek(self): + return (self.keynames[0],self.dict[self.keynames[0]]) + + def keys(self): + return list(self.keynames) + + def index(self, key): + return self.keynames.index(key) + + def get(self, key, default=None): + if not key in self.keynames: + return default + return self.dict[key] + + def getkey(self, n): + print('len'),len(self.keynames),type(self.keynames),n + return self.keynames[n] + + def getitem(self, n): + return self.dict[self.keynames[n]] + + def remove(self, n): + self.keynames.remove(n) + del self.dict[n] + + def __repr__(self): + return '%s@%s' % (self.keynames,self.dict) + + def copy(self): + return odict(self) + + def sort(self): + self.keynames.sort() diff --git a/tools/PyAI.pyw b/tools/PyAI.pyw index fb119f8..1bcb246 100644 --- a/tools/PyAI.pyw +++ b/tools/PyAI.pyw @@ -3,22 +3,30 @@ from Libs.setutils import * from Libs.trace import setup_trace from Libs import AIBIN, TBL, DAT -from Tkinter import * -from tkMessageBox import * -import tkFileDialog,tkColorChooser +try: + from Tkinter import * + from tkMessageBox import * + import tkFileDialog, tkColorChooser + from thread import start_new_thread +except ImportError: + # Python 3 + from tkinter import * + from tkinter.messagebox import * + import tkinter.filedialog as tkFileDialog + import tkinter.colorchooser as tkColorChooser + from _thread import start_new_thread -from thread import start_new_thread from shutil import copy import optparse, os, re, webbrowser, sys VERSION = (2,4) -LONG_VERSION = 'v%s.%s' % VERSION +LONG_VERSION = 'v%s.%s)' % VERSION IMG_CACHE = {} def get_img(n): if n in IMG_CACHE: return IMG_CACHE[n] - IMG_CACHE[n] = PhotoImage(file=os.path.join(BASE_DIR, 'Images','%s.gif' % n)) + IMG_CACHE[n] = PhotoImage(file=os.path.join(BASE_DIR, 'Images','%s.gif)' % n)) return IMG_CACHE[n] types = [ @@ -296,7 +304,7 @@ class FindReplaceDialog(PyMSDialog): m = r.search(self.parent.text.get(INSERT, END)) if m: self.parent.text.tag_remove('Selection', '1.0', END) - s,e = '%s +%sc' % (INSERT, m.start(0)),'%s +%sc' % (INSERT,m.end(0)) + s,e = '%s +%sc)' % (INSERT, m.start(0)),'%s +%sc)' % (INSERT,m.end(0)) self.parent.text.tag_add('Selection', s, e) self.parent.text.mark_set(INSERT, e) self.parent.text.see(s) @@ -312,15 +320,15 @@ class FindReplaceDialog(PyMSDialog): i = self.parent.text.index(INSERT) if i == e: return - if i == self.parent.text.index('%s %s' % (INSERT, rlse)): - i = self.parent.text.index('%s %s1lines %s' % (INSERT, s, lse)) + if i == self.parent.text.index('%s %s)' % (INSERT, rlse)): + i = self.parent.text.index('%s %s1lines %s)' % (INSERT, s, lse)) n = -1 while not u or i != e: if u: - m = r.search(self.parent.text.get(i, '%s %s' % (i, rlse))) + m = r.search(self.parent.text.get(i, '%s %s)' % (i, rlse))) else: m = None - a = r.finditer(self.parent.text.get('%s %s' % (i, rlse), i)) + a = r.finditer(self.parent.text.get('%s %s)' % (i, rlse), i)) c = 0 for x,f in enumerate(a): if x == n or n == -1: @@ -330,22 +338,22 @@ class FindReplaceDialog(PyMSDialog): if m: self.parent.text.tag_remove('Selection', '1.0', END) if u: - s,e = '%s +%sc' % (i,m.start(0)),'%s +%sc' % (i,m.end(0)) + s,e = '%s +%sc)' % (i,m.start(0)),'%s +%sc)' % (i,m.end(0)) self.parent.text.mark_set(INSERT, e) else: - s,e = '%s linestart +%sc' % (i,m.start(0)),'%s linestart +%sc' % (i,m.end(0)) + s,e = '%s linestart +%sc)' % (i,m.start(0)),'%s linestart +%sc)' % (i,m.end(0)) self.parent.text.mark_set(INSERT, s) self.parent.text.tag_add('Selection', s, e) self.parent.text.see(s) self.check(3) break - if (not u and n == -1 and self.parent.text.index('%s lineend' % i) == e) or i == e: + if (not u and n == -1 and self.parent.text.index('%s lineend)' % i) == e) or i == e: p = self if key and key.keycode == 13: p = self.parent askquestion(parent=p, title='Find', message="Can't find text.", type=OK) break - i = self.parent.text.index('%s %s1lines %s' % (i, s, lse)) + i = self.parent.text.index('%s %s1lines %s)' % (i, s, lse)) else: p = self if key and key.keycode == 13: @@ -364,7 +372,7 @@ class FindReplaceDialog(PyMSDialog): self.resettimer = self.after(1000, self.updatecolor) self.findentry['bg'] = '#FFB4B4' return - askquestion(parent=self, title='Count', message='%s matches found.' % len(r.findall(self.parent.text.get('1.0', END))), type=OK) + askquestion(parent=self, title='Count', message='%s matches found.)' % len(r.findall(self.parent.text.get('1.0', END))), type=OK) def replaceall(self): f = self.find.get() @@ -383,7 +391,7 @@ class FindReplaceDialog(PyMSDialog): self.parent.text.delete('1.0', END) self.parent.text.insert('1.0', text[0].rstrip('\n')) self.parent.text.update_range('1.0') - askquestion(parent=self, title='Replace Complete', message='%s matches replaced.' % text[1], type=OK) + askquestion(parent=self, title='Replace Complete', message='%s matches replaced.)' % text[1], type=OK) def updatecolor(self): if self.resettimer: @@ -503,7 +511,7 @@ class CodeColors(PyMSDialog): if [self.fg,self.bg][i].get(): v = [self.fgcanvas,self.bgcanvas][i] g = ['foreground','background'][i] - c = tkColorChooser.askcolor(parent=self, initialcolor=v['background'], title='Select %s color' % g) + c = tkColorChooser.askcolor(parent=self, initialcolor=v['background'], title='Select %s color)' % g) if c[1]: v['background'] = c[1] self.tags[self.info.getkey(int(self.listbox.curselection()[0])).replace(' ','')][g] = c[1] @@ -553,31 +561,31 @@ class AICodeText(CodeText): def commentrange(self, e=None): item = self.tag_ranges('Selection') if item: - head,tail = self.index('%s linestart' % item[0]),self.index('%s linestart' % item[1]) + head,tail = self.index('%s linestart)' % item[0]),self.index('%s linestart)' % item[1]) while self.text.compare(head, '<=', tail): - m = re.match('(\s*)(#?)(.*)', self.get(head, '%s lineend' % head)) + m = re.match(r'(\s*)(#?)(.*)', self.get(head, '%s lineend)' % head)) if m.group(2): - self.tk.call(self.text.orig, 'delete', '%s +%sc' % (head, len(m.group(1)))) + self.tk.call(self.text.orig, 'delete', '%s +%sc)' % (head, len(m.group(1)))) elif m.group(3): self.tk.call(self.text.orig, 'insert', head, '#') - head = self.index('%s +1line' % head) - self.update_range(self.index('%s linestart' % item[0]), self.index('%s lineend' % item[1])) + head = self.index('%s +1line)' % head) + self.update_range(self.index('%s linestart)' % item[0]), self.index('%s lineend)' % item[1])) def setupparser(self): - infocomment = '(?P\\{[^\\n]+\\})' - multiinfocomment = '^[ \\t]*(?P\\{[ \\t]*(?:\\n[^}]*)?\\}?)$' - comment = '(?P#[^\\n]*$)' - header = '^(?P[^\n\x00,():]{4})(?=\\([^#]+,[^#]+,[^#]+\\):.+$)' - header_string = '\\b(?P\\d+)(?=,[^#]+,[^#]+\\):.+$)' - header_flags = '\\b(?P[01]{3})(?=,[^#]+\\):.+$)' - block = '^[ \\t]*(?P--[^\x00:(),\\n]+--)(?=.+$)' - cmds = '\\b(?P%s)\\b' % '|'.join(AIBIN.AIBIN.short_labels) - num = '\\b(?P\\d+)\\b' - tbl = '(?P<0*(?:25[0-5]|2[0-4]\d|1?\d?\d)?>)' - operators = '(?P[():,=])' - kw = '\\b(?Pextdef|aiscript|bwscript)\\b' - types = '\\b(?P%s)\\b' % '|'.join(AIBIN.types) - self.basic = re.compile('|'.join((infocomment, multiinfocomment, comment, header, header_string, header_flags, block, cmds, num, tbl, operators, kw, types, '(?P\\n)')), re.S | re.M) + infocomment = r'(?P\{[^\n]+\})' + multiinfocomment = r'^[ \t]*(?P\{[ \t]*(?:\n[^}]*)?\}?)$' + comment = r'(?P#[^\n]*$)' + header = r'^(?P[^\n\x00,():]{4})(?=\([^#]+,[^#]+,[^#]+\):.+$)' + header_string = r'\b(?P\d+)(?=,[^#]+,[^#]+\):.+$)' + header_flags = r'\b(?P[01]{3})(?=,[^#]+\):.+$)' + block = r'^[ \t]*(?P--[^\x00:(),\n]+--)(?=.+$)' + cmds = r'\b(?P%s)\b)' % '|'.join(AIBIN.AIBIN.short_labels) + num = r'\b(?P\d+)\b' + tbl = r'(?P<0*(?:25[0-5]|2[0-4]\d|1?\d?\d)?>)' + operators = r'(?P[():,=])' + kw = r'\b(?Pextdef|aiscript|bwscript)\b' + types = r'\b(?P%s)\b)' % '|'.join(AIBIN.types) + self.basic = re.compile('|'.join((infocomment, multiinfocomment, comment, header, header_string, header_flags, block, cmds, num, tbl, operators, kw, types, r'(?P\n)')), re.S | re.M) self.tooptips = [CommandCodeTooltip(self.text,self.ai),TypeCodeTooltip(self.text,self.ai),StringCodeTooltip(self.text,self.ai),FlagCodeTooltip(self.text,self.ai)] self.tags = dict(self.highlights) @@ -600,7 +608,7 @@ class AICodeText(CodeText): ok = False while not ok: mark = next - next = self.index(mark + '+%d lines linestart' % lines_to_get) + next = self.index(mark + '+%d lines linestart)' % lines_to_get) lines_to_get = min(lines_to_get * 2, 100) ok = 'Newline' in self.tag_names(next + '-1c') line = self.get(mark, next) @@ -615,7 +623,7 @@ class AICodeText(CodeText): for key, value in m.groupdict().items(): if value != None: a, b = m.span(key) - self.tag_add(key, head + '+%dc' % a, head + '+%dc' % b) + self.tag_add(key, head + '+%dc)' % a, head + '+%dc)' % b) m = self.basic.search(chars, m.end()) if 'Newline' in self.tag_names(next + '-1c'): head = next @@ -649,7 +657,7 @@ class CodeTooltip(Tooltip): t = '' if self.tag: pos = list(self.widget.winfo_pointerxy()) - head,tail = self.widget.tag_prevrange(self.tag,self.widget.index('@%s,%s+1c' % (pos[0] - self.widget.winfo_rootx(),pos[1] - self.widget.winfo_rooty()))) + head,tail = self.widget.tag_prevrange(self.tag,self.widget.index('@%s,%s+1c)' % (pos[0] - self.widget.winfo_rootx(),pos[1] - self.widget.winfo_rooty()))) t = self.widget.get(head,tail) try: t = self.gettext(t) @@ -659,7 +667,7 @@ class CodeTooltip(Tooltip): Label(frame, text=t, justify=LEFT, font=self.font, background='#FFFFC8', relief=FLAT).pack(padx=1, pady=1) frame.pack() pos = list(self.widget.winfo_pointerxy()) - self.tip.wm_geometry('+%d+%d' % (pos[0],pos[1]+22)) + self.tip.wm_geometry('+%d+%d)' % (pos[0],pos[1]+22)) self.tip.update_idletasks() move = False if pos[0] + self.tip.winfo_reqwidth() > self.tip.winfo_screenwidth(): @@ -669,7 +677,7 @@ class CodeTooltip(Tooltip): move = True pos[1] -= self.tip.winfo_reqheight() + 44 if move: - self.tip.wm_geometry('+%d+%d' % (pos[0],pos[1]+22)) + self.tip.wm_geometry('+%d+%d)' % (pos[0],pos[1]+22)) except: if self.tip: try: @@ -687,9 +695,9 @@ class CommandCodeTooltip(CodeTooltip): tag = 'Commands' def gettext(self, cmd): - for help,info in CMD_HELP.iteritems(): + for help,info in CMD_HELP.items(): if cmd in info: - text = '%s Command:\n %s(' % (help, cmd) + text = '%s Command:\n %s()' % (help, cmd) break params = self.ai.parameters[self.ai.short_labels.index(cmd)] pinfo = '' @@ -700,7 +708,7 @@ class CommandCodeTooltip(CodeTooltip): t = p.__doc__.split(' ',1)[0] text += t + ', ' if not t in done: - pinfo += fit(' %s: ' % t, TYPE_HELP[t], end=True, indent=4) + pinfo += fit(' %s: )' % t, TYPE_HELP[t], end=True, indent=4) done.append(t) text = text[:-2] text += ')' @@ -710,7 +718,7 @@ class TypeCodeTooltip(CodeTooltip): tag = 'Types' def gettext(self, type): - return '%s:\n%s' % (type, fit(' ', TYPE_HELP[type], end=True)[:-1]) + return '%s:\n%s)' % (type, fit(' ', TYPE_HELP[type], end=True)[:-1]) class StringCodeTooltip(CodeTooltip): tag = 'HeaderString' @@ -719,16 +727,16 @@ class StringCodeTooltip(CodeTooltip): stringid = int(stringid) m = len(self.ai.tbl.strings) if stringid > m: - text = 'Invalid String ID (Range is 0 to %s)' % (m-1) + text = 'Invalid String ID (Range is 0 to %s))' % (m-1) else: - text = 'String %s:\n %s' % (stringid, TBL.decompile_string(self.ai.tbl.strings[stringid])) + text = 'String %s:\n %s)' % (stringid, TBL.decompile_string(self.ai.tbl.strings[stringid])) return text class FlagCodeTooltip(CodeTooltip): tag = 'HeaderFlags' def gettext(self, flags): - text = 'AI Script Flags:\n %s:\n ' % flags + text = 'AI Script Flags:\n %s:\n )' % flags if flags == '000': text += 'None' else: @@ -852,8 +860,8 @@ class CodeEditDialog(PyMSDialog): def docomplete(s, e, v, t): self.text.delete(s, e) self.text.insert(s, v) - ss = '%s+%sc' % (s,len(t)) - se = '%s+%sc' % (s,len(v)) + ss = '%s+%sc)' % (s,len(t)) + se = '%s+%sc)' % (s,len(v)) self.text.tag_remove('Selection', '1.0', END) self.text.tag_add('Selection', ss, se) if self.complete[0] == None: @@ -864,13 +872,13 @@ class CodeEditDialog(PyMSDialog): if self.complete[0] != None: t,f,s,e = self.complete else: - s,e = self.text.index('%s -1c wordstart' % INSERT),self.text.index('%s -1c wordend' % INSERT) + s,e = self.text.index('%s -1c wordstart)' % INSERT),self.text.index('%s -1c wordend)' % INSERT) t,f = self.text.get(s,e),0 if t and t[0].lower() in 'abcdefghijklmnopqrstuvwxyz': ac = list(self.autocomptext) m = re.match('\\A\\s*[a-z\\{]+\\Z',t) if not m: - for _,c in CMD_HELP.iteritems(): + for _,c in CMD_HELP.items(): ac.extend(c.keys()) for ns in self.parent.tbl.strings[:228]: cs = ns.split('\x00') @@ -905,14 +913,14 @@ class CodeEditDialog(PyMSDialog): id = self.text.get(*item) if id != aiid: block = id + ':' - block += self.text.get('%s +2c' % head,'%s -2c' % tail) + block += self.text.get('%s +2c)' % head,'%s -2c)' % tail) if not block in ac: ac.append(block) head = tail ac.sort() if not m: x = [] - for _,c in CMD_HELP.iteritems(): + for _,c in CMD_HELP.items(): x.extend(c.keys()) x.sort() ac = x + ac @@ -943,7 +951,7 @@ class CodeEditDialog(PyMSDialog): item = self.text.tag_ranges('Selection') if item: i[2] = len(self.text.get(*item)) - self.scriptstatus.set('Line: %s Column: %s Selected: %s' % tuple(i)) + self.scriptstatus.set('Line: %s Column: %s Selected: %s)' % tuple(i)) def edited(self): if not self.completing: @@ -951,7 +959,7 @@ class CodeEditDialog(PyMSDialog): self.complete = [None, 0] self.editstatus['state'] = NORMAL if self.file: - self.title('AI Script Editor [*%s*]' % self.file) + self.title('AI Script Editor [*%s*])' % self.file) def cancel(self): if self.text.edited: @@ -979,18 +987,18 @@ class CodeEditDialog(PyMSDialog): warnings = i.interpret(self, self.parent.extdefs) for id in i.ais.keys(): if id in self.parent.ai.externaljumps[0]: - for o,l in self.parent.ai.externaljumps[0].iteritems(): + for o,l in self.parent.ai.externaljumps[0].items(): for cid in l: if not cid in i.ais: - raise PyMSError('Interpreting',"You can't edit scripts (%s) that are referenced externally with out editing the scripts with the external references (%s) at the same time." % (id,cid)) - except PyMSError, e: + raise PyMSError('Interpreting',"You can't edit scripts (%s) that are referenced externally with out editing the scripts with the external references (%s) at the same time.)" % (id,cid)) + except PyMSError as e: if e.line != None: - self.text.see('%s.0' % e.line) - self.text.tag_add('Error', '%s.0' % e.line, '%s.end' % e.line) + self.text.see('%s.0)' % e.line) + self.text.tag_add('Error', '%s.0)' % e.line, '%s.end)' % e.line) if e.warnings: for w in e.warnings: if w.line != None: - self.text.tag_add('Warning', '%s.0' % w.line, '%s.end' % w.line) + self.text.tag_add('Warning', '%s.0)' % w.line, '%s.end)' % w.line) ErrorDialog(self, e) return if warnings: @@ -998,9 +1006,9 @@ class CodeEditDialog(PyMSDialog): for w in warnings: if w.line != None: if not c: - self.text.see('%s.0' % w.line) + self.text.see('%s.0)' % w.line) c = True - self.text.tag_add('Warning', '%s.0' % w.line, '%s.end' % w.line) + self.text.tag_add('Warning', '%s.0)' % w.line, '%s.end)' % w.line) WarningDialog(self, warnings, True) else: askquestion(parent=self, title='Test Completed', message='The code compiles with no errors or warnings.', type=OK) @@ -1012,7 +1020,7 @@ class CodeEditDialog(PyMSDialog): f = open(self.file, 'w') f.write(self.text.get('1.0', END)) f.close() - self.title('AI Script Editor [%s]' % self.file) + self.title('AI Script Editor [%s])' % self.file) def exportas(self, e=None): file = self.parent.select_file('Export Code', False, '.txt', [('Text Files','*.txt'),('All Files','*')], self) @@ -1030,7 +1038,7 @@ class CodeEditDialog(PyMSDialog): self.text.insert('1.0', f.read()) f.close() except: - ErrorDialog(self, PyMSError('Import',"Could not import file '%s'" % iimport)) + ErrorDialog(self, PyMSError('Import',"Could not import file '%s')" % iimport)) def find(self, e=None): if not self.findwindow: @@ -1058,7 +1066,7 @@ class CodeEditDialog(PyMSDialog): else: beforeheader += line.replace(';','#',1) + '\n' elif line.lstrip().startswith(':'): - data += ' --%s--\n' % line.split('#',1)[0].strip()[1:] + data += ' --%s--\n)' % line.split('#',1)[0].strip()[1:] elif line.lstrip().startswith('script_name ') and headerinfo[3] == None: headerinfo[3] = line.lstrip()[12:] if re.match('bw|brood ?war',headerinfo[3],re.I): @@ -1076,7 +1084,7 @@ class CodeEditDialog(PyMSDialog): elif line.strip(): d = line.lstrip().split(';',1)[0].strip().split(' ') if d[0] in AIBIN.AIBIN.short_labels: - data += ' %s(%s)' % (d[0], ', '.join(d[1:])) + data += ' %s(%s))' % (d[0], ', '.join(d[1:])) if ';' in line: data += ' # ' + line.split('#',1)[1] else: @@ -1120,9 +1128,9 @@ class CodeEditDialog(PyMSDialog): #'allies_watch':('',), #'try_townpoint':('',), } - header = re.compile('\\A([^(]{4})\\([^)]+\\):\\s*(?:\\{.+\\})?(?:\\s*#.*)?\\Z') - label = re.compile('\\A\\s*--\\s*(.+)\\s*--(?:\\s*\\{(.+)\\})?(?:\\s*#.*)?\\Z') - jump = re.compile('\\A(\\s*)(%s)\((.+)\)(\\s*#.*)?\\Z' % '|'.join(debug.keys())) + header = re.compile(r'\A([^(]{4})\([^)]+\):\s*(?:\{.+\})?(?:\s*#.*)?\Z') + label = re.compile(r'\A\s*--\s*(.+)\s*--(?:\s*\{(.+)\})?(?:\s*#.*)?\Z') + jump = re.compile(r'\A(\s*)(%s)\((.+)\)(\s*#.*)?\Z)' % '|'.join(debug.keys())) script,block = '','' for n,line in enumerate(self.text.text.get('1.0',END).split('\n')): m = header.match(line) @@ -1140,22 +1148,22 @@ class CodeEditDialog(PyMSDialog): if m and m.group(2) in debug: inblock = '' if block: - inblock = ' block "%s"' % block + inblock = ' block "%s")' % block rep = { - 'debug1':'== Debug %s ==' % d, - 'debug2':'== Debug %s ==' % (d+1), - 'debug3':'== Debug %s ==' % (d+2), - 's':'[Line: %s | Inside script "%s"%s]' % (n, script, inblock), + 'debug1':'== Debug %s ==)' % d, + 'debug2':'== Debug %s ==)' % (d+1), + 'debug3':'== Debug %s ==)' % (d+2), + 's':'[Line: %s | Inside script "%s"%s])' % (n, script, inblock), 'c':m.group(4) or '', } params = self.parent.ai.parameters[self.parent.ai.short_labels.index(m.group(2))] if params: - p = re.match('\\A%s\\Z' % ','.join(['\\s*(.+)\\s*'] * len(params)), m.group(3)) + p = re.match('\\A%s\\Z)' % ','.join(['\\s*(.+)\\s*'] * len(params)), m.group(3)) if not p: data += line + '\n' continue for g,param in enumerate(p.groups()): - rep['param%s' % (g+1)] = param + rep['param%s)' % (g+1)] = param data += m.group(1) + (debug[m.group(2)][0] % rep).replace('\n','\n' + m.group(1)) + '\n' d += debug[m.group(2)][1] continue @@ -1168,7 +1176,7 @@ class CodeEditDialog(PyMSDialog): def load(self): try: warnings = self.parent.ai.decompile(self, self.parent.extdefs, self.parent.reference.get(), 1, self.ids) - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) return if warnings: @@ -1343,7 +1351,7 @@ class FindDialog(PyMSDialog): if not regex.endswith('\\Z'): regex = regex + '.*' else: - regex = '.*%s.*' % re.escape(t.get()) + regex = '.*%s.*)' % re.escape(t.get()) try: m.append(re.compile(regex, [re.I,0][self.casesens.get()])) except: @@ -1356,7 +1364,7 @@ class FindDialog(PyMSDialog): for n,s in enumerate(self.tbl.strings): l = TBL.decompile_string(s) if m[0].match(str(n)) and m[1].match(l): - self.listbox.insert(END, '%s%s %s' % (' ' * (pad - len(str(n))), n, l)) + self.listbox.insert(END, '%s%s %s)' % (' ' * (pad - len(str(n))), n, l)) else: m = [] for t,e in [(self.id, self.identry), (self.flags, self.flagsentry), (self.stringid, self.stringidentry), (self.string, self.stringentry), (self.extra, self.extraentry), (None, self.extraentry)]: @@ -1374,7 +1382,7 @@ class FindDialog(PyMSDialog): regex = t.get() else: regex = self.extraentry.get(1.0,END) - regex = '.*%s.*' % re.escape(regex) + regex = '.*%s.*)' % re.escape(regex) try: m.append(re.compile(regex, [re.I,0][self.casesens.get()])) except: @@ -1383,15 +1391,15 @@ class FindDialog(PyMSDialog): self.reset = e self.resettimer = self.after(1000, self.updatecolor) return - for id,ai in self.ai.ais.iteritems(): + for id,ai in self.ai.ais.items(): flags = AIBIN.convflags(ai[2]) string = TBL.decompile_string(self.tbl.strings[ai[1]]) if m[0].match(id) and (not self.bw.get() or min(ai[0],1) != self.bw.get()-1) and m[1].match(flags) and m[2].match(str(ai[1])) and m[3].match(string): - self.listbox.insert(END,'%s %s %s %s' % (id, ['BW',' '][min(ai[0],1)], flags, string)) + self.listbox.insert(END,'%s %s %s %s)' % (id, ['BW',' '][min(ai[0],1)], flags, string)) def select(self): if self.findstr: - index = re.split('\s+', self.listbox.get(self.listbox.curselection()[0]).strip())[0] + index = re.split(r'\s+', self.listbox.get(self.listbox.curselection()[0]).strip())[0] self.parent.listbox.select_clear(0,END) self.parent.listbox.select_set(index) self.parent.listbox.see(index) @@ -1614,7 +1622,7 @@ class ContinueImportDialog(PyMSDialog): PyMSDialog.__init__(self, parent, 'Continue Importing?') def widgetize(self): - Label(self, text="The AI Script with ID '%s' already exists, overwrite it?" % self.id).pack(pady=10) + Label(self, text="The AI Script with ID '%s' already exists, overwrite it?)" % self.id).pack(pady=10) frame = Frame(self) yes = Button(frame, text='Yes', width=10, command=self.yes) yes.pack(side=LEFT, padx=3) @@ -1751,7 +1759,7 @@ class EditScriptDialog(PyMSDialog): def ok(self): id = self.id.get() if self.initialid != id and id in self.parent.ai.ais: - replace = askquestion(parent=self, title='Replace Script?', message="The script with ID '%s' already exists, replace it?" % id, default=YES, type=YESNOCANCEL) + replace = askquestion(parent=self, title='Replace Script?', message="The script with ID '%s' already exists, replace it?)" % id, default=YES, type=YESNOCANCEL) if replace == 'yes': if not self.ai.ais[id][0]: del self.ai.bwscript.ais[id] @@ -1829,7 +1837,7 @@ class StringEditor(PyMSDialog): self.strings = parent.strings self.resort = parent.resort self.select_file = parent.select_file - PyMSDialog.__init__(self, parent, '%s (%s)' % (title, parent.stattxt())) + PyMSDialog.__init__(self, parent, '%s (%s))' % (title, parent.stattxt())) def widgetize(self): self.bind('', self.open) @@ -1981,14 +1989,14 @@ class StringEditor(PyMSDialog): size = len(self.parent.tbl.strings) pad = len(str(size)) for n,s in enumerate(self.parent.tbl.strings): - self.listbox.insert(END, '%s%s %s' % (' ' * (pad - len(str(n))), n, TBL.decompile_string(s))) + self.listbox.insert(END, '%s%s %s)' % (' ' * (pad - len(str(n))), n, TBL.decompile_string(s))) self.listbox.select_set(sel) self.listbox.see(sel) - self.status.set('Strings: %s' % size) + self.status.set('Strings: %s)' % size) def open(self, file=None): if self.parent.edittbl(): - save = askquestion(parent=self, title='Save Changes?', message="Save changes to '%s'?" % self.parent.stattxt(), default=YES, type=YESNOCANCEL) + save = askquestion(parent=self, title='Save Changes?', message="Save changes to '%s'?)" % self.parent.stattxt(), default=YES, type=YESNOCANCEL) if save != 'no': if save == 'cancel': return @@ -2002,12 +2010,12 @@ class StringEditor(PyMSDialog): tbl = TBL.TBL() try: tbl.load_file(file) - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) return max = len(tbl.strings) ids = {} - for s,i in self.parent.strings.iteritems(): + for s,i in self.parent.strings.items(): if s >= max: ids[s] = i if ids: @@ -2016,7 +2024,7 @@ class StringEditor(PyMSDialog): self.parent.ai.tbl = tbl self.parent.tbl = tbl self.parent.stattxt(file) - self.title('String Editor (%s)' % file) + self.title('String Editor (%s))' % file) self.update() self.parent.edittbl(False) @@ -2031,7 +2039,7 @@ class StringEditor(PyMSDialog): try: self.tbl.compile(file) self.parent.stattxt(file) - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) return self.tbledited = False @@ -2064,14 +2072,14 @@ class StringEditor(PyMSDialog): string = int(self.listbox.curselection()[0]) if self.parent.ai: ids = {} - for s,i in self.parent.strings.iteritems(): + for s,i in self.parent.strings.items(): if s > string: ids[s] = i if ids: plural = 0 i = '' e = 0 - for s,x in ids.iteritems(): + for s,x in ids.items(): if e < 6: i += ' ' comma = False @@ -2085,13 +2093,13 @@ class StringEditor(PyMSDialog): if e < 6: i += n if e < 6: - i += ': %s\n' % s + i += ': %s\n)' % s e += 1 if e > 5: - i += 'And %s other scripts. ' % (e-5) + i += 'And %s other scripts. )' % (e-5) if plural == 2: plural = 1 - if not askquestion(parent=self, title='Remove String?', message="Deleting string '%s' will effect the AI Script%s:\n%sContinue removing string anyway?" % (string, 's' * plural, i), default=YES): + if not askquestion(parent=self, title='Remove String?', message="Deleting string '%s' will effect the AI Script%s:\n%sContinue removing string anyway?)" % (string, 's' * plural, i), default=YES): return end = self.listbox.size()-1 if end in self.parent.strings: @@ -2198,17 +2206,17 @@ class ListboxTooltip(Tooltip): flags += d if flags: flags += '\n' - text = "Script ID : %s\nIn bwscript.bin : %s\n%sString ID : %s\n" % (id, ['No','Yes'][item[1]], flags, item[3]) + text = "Script ID : %s\nIn bwscript.bin : %s\n%sString ID : %s\n)" % (id, ['No','Yes'][item[1]], flags, item[3]) ai = self.widget.master.master.ai text += fit('String : ', TBL.decompile_string(ai.tbl.strings[ai.ais[id][1]]), end=True) if id in ai.aiinfo and ai.aiinfo[id][0]: - text += 'Extra Information : %s' % ai.aiinfo[id][0].replace('\n','\n ') + text += 'Extra Information : %s)' % ai.aiinfo[id][0].replace('\n','\n ') else: text = text[:-1] frame = Frame(self.tip, background='#FFFFC8', relief=SOLID, borderwidth=1) Label(frame, text=text, justify=LEFT, font=self.font, background='#FFFFC8', relief=FLAT).pack(padx=1, pady=1) frame.pack() - self.tip.wm_geometry('+%d+%d' % (pos[0],pos[1]+22)) + self.tip.wm_geometry('+%d+%d)' % (pos[0],pos[1]+22)) self.tip.update_idletasks() move = False if pos[0] + self.tip.winfo_reqwidth() > self.tip.winfo_screenwidth(): @@ -2218,7 +2226,7 @@ class ListboxTooltip(Tooltip): move = True pos[1] -= self.tip.winfo_reqheight() + 44 if move: - self.tip.wm_geometry('+%d+%d' % (pos[0],pos[1]+22)) + self.tip.wm_geometry('+%d+%d)' % (pos[0],pos[1]+22)) class PyAI(Tk): def __init__(self, guifile=None): @@ -2241,7 +2249,7 @@ class PyAI(Tk): self.icon = os.path.join(BASE_DIR, 'Images','PyAI.ico') self.wm_iconbitmap(self.icon) except: - self.icon = '@%s' % os.path.join(BASE_DIR, 'Images','PyAI.xbm') + self.icon = '@%s)' % os.path.join(BASE_DIR, 'Images','PyAI.xbm') self.wm_iconbitmap(self.icon) self.protocol('WM_DELETE_WINDOW', self.exit) setup_trace(self, 'PyAI') @@ -2335,7 +2343,7 @@ class PyAI(Tk): self.menus = {} menubar = Menu(self) self.config(menu=menubar) - for name,menu in menus.iteritems(): + for name,menu in menus.items(): self.menus[name] = Menu(menubar, tearoff=0) for n,m in enumerate(menu): if m: @@ -2350,9 +2358,9 @@ class PyAI(Tk): self.menus[name].add_command(label=l, command=c, state=s, accelerator=a, underline=u) if a: if not a.startswith('F'): - self.bind('<%s%s>' % (a[:-1].replace('Ctrl','Control').replace('+','-'), a[-1].lower()), c) + self.bind('<%s%s>)' % (a[:-1].replace('Ctrl','Control').replace('+','-'), a[-1].lower()), c) else: - self.bind('<%s>' % a, c) + self.bind('<%s>)' % a, c) else: self.menus[name].add_separator() menubar.add_cascade(label=name, menu=self.menus[name], underline=0) @@ -2525,7 +2533,7 @@ class PyAI(Tk): tbl.load_file(file) self.stat_txt = file self.tbl = tbl - except PyMSError, e: + except PyMSError as e: err = e else: self.unitsdatdat = unitsdat @@ -2543,11 +2551,11 @@ class PyAI(Tk): global LONG_VERSION if not text: text = self.titletext - Tk.title(self,'PyAI %s (%s)' % (LONG_VERSION, text)) + Tk.title(self,'PyAI %s (%s))' % (LONG_VERSION, text)) self.titletext = text def get_entry(self, index): - match = re.match('(....)\s{5}(\s\s|BW)\s{5}([01]{3})\s{5}(.+)', self.listbox.get(index)) + match = re.match(r'(....)\s{5}(\s\s|BW)\s{5}([01]{3})\s{5}(.+)', self.listbox.get(index)) id = match.group(1) return (id, match.group(2) == 'BW', match.group(3), self.ai.ais[id][1], match.group(4)) @@ -2559,7 +2567,7 @@ class PyAI(Tk): aiinfo = '' if id in self.ai.aiinfo: aiinfo = self.ai.aiinfo[id][0] - return '%s %s %s %s%s%s' % (id, [' ','BW'][bw], flags, string, ' ' * (55-len(string)), aiinfo) + return '%s %s %s %s%s%s)' % (id, [' ','BW'][bw], flags, string, ' ' * (55-len(string)), aiinfo) def set_entry(self, index, id, bw, flags, string): if index != END: @@ -2611,7 +2619,7 @@ class PyAI(Tk): def unsaved(self): if self.tbledited: - save = askquestion(parent=self, title='Save Changes?', message="Save changes to '%s'?" % self.stat_txt, default=YES, type=YESNOCANCEL) + save = askquestion(parent=self, title='Save Changes?', message="Save changes to '%s'?)" % self.stat_txt, default=YES, type=YESNOCANCEL) if save != 'no': if save == 'cancel': return True @@ -2624,7 +2632,7 @@ class PyAI(Tk): bwscript = self.bwscript if not bwscript: bwscript = 'bwscript.bin' - save = askquestion(parent=self, title='Save Changes?', message="Save changes to '%s' and '%s'?" % (aiscript, bwscript), default=YES, type=YESNOCANCEL) + save = askquestion(parent=self, title='Save Changes?', message="Save changes to '%s' and '%s'?)" % (aiscript, bwscript), default=YES, type=YESNOCANCEL) if save != 'no': if save == 'cancel': return True @@ -2701,12 +2709,12 @@ class PyAI(Tk): ai = AIBIN.AIBIN(bwscript, self.unitsdat, self.upgradesdat, self.techdat, self.tbl) warnings.extend(ai.warnings) warnings.extend(ai.load_file(aiscript, True)) - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) return self.ai = ai self.strings = {} - for id,ai in self.ai.ais.iteritems(): + for id,ai in self.ai.ais.items(): if not ai[1] in self.strings: self.strings[ai[1]] = [] self.strings[ai[1]].append(id) @@ -2718,13 +2726,13 @@ class PyAI(Tk): self.redos = [] if not bwscript: bwscript = 'bwscript.bin' - self.title('%s, %s' % (aiscript,bwscript)) + self.title('%s, %s)' % (aiscript,bwscript)) self.status.set('Load Successful!') self.resort() self.action_states() - s = 'aiscript.bin: %s (%s B) ' % (len(self.ai.ais),sum(self.ai.aisizes.values())) + s = 'aiscript.bin: %s (%s B) )' % (len(self.ai.ais),sum(self.ai.aisizes.values())) if self.ai.bwscript: - s += ' bwscript.bin: %s (%s B)' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) + s += ' bwscript.bin: %s (%s B))' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) self.scriptstatus.set(s) if warnings: WarningDialog(self, warnings) @@ -2738,12 +2746,12 @@ class PyAI(Tk): return h = SFileOpenArchive(file) if SFInvalidHandle(h): - ErrorDialog(self, PyMSError('Open','Could not open MPQ "%s"' % file)) + ErrorDialog(self, PyMSError('Open','Could not open MPQ "%s")' % file)) return ai = SFile(file='scripts\\aiscript.bin') bw = SFile(file='scripts\\bwscirpt.bin') for t in ['ai','bw']: - f = SFileOpenFileEx(h, 'scripts\\%sscript.bin' % t) + f = SFileOpenFileEx(h, 'scripts\\%sscript.bin)' % t) if f in [None,-1]: if t == 'ai': SFileCloseArchive(h) @@ -2776,7 +2784,7 @@ class PyAI(Tk): self.stat_txt = file try: self.tbl.compile(file, extra=self.extrainfo.get()) - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) return self.tbledited = False @@ -2786,7 +2794,7 @@ class PyAI(Tk): if bw != None: self.bwscript = bw self.status.set('Save Successful!') - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) def saveas(self, key=None): @@ -2800,12 +2808,12 @@ class PyAI(Tk): bwscript = self.select_file('Save bwscript.bin As (Cancel to save aiscript.bin only)', False) if self.save(ai=aiscript, bw=bwscript): self.tbledited = False - self.title('%s, %s' % (self.aiscript,self.bwscript)) + self.title('%s, %s)' % (self.aiscript,self.bwscript)) def savempq(self, key=None): file = self.select_file('Save MPQ to...', False, '.mpq', [('MPQ Files','*.mpq'),('Self-executing MPQ','*.exe'),('All Files','*')], self) if file: - if file.endswith('%sexe' % os.extsep): + if file.endswith('%sexe)' % os.extsep): if os.path.exists(file): h = MpqOpenArchiveForUpdate(file, MOAU_OPEN_ALWAYS | MOAU_MAINTAIN_LISTFILE) else: @@ -2817,23 +2825,23 @@ class PyAI(Tk): else: h = MpqOpenArchiveForUpdate(file, MOAU_OPEN_ALWAYS | MOAU_MAINTAIN_LISTFILE) if h == -1: - ErrorDialog(self, PyMSError('Saving','Could not open %sMPQ "%s".' % (['','SE'][file.endswith('%sexe' % os.extsep)],file))) + ErrorDialog(self, PyMSError('Saving','Could not open %sMPQ "%s".)' % (['','SE'][file.endswith('%sexe)' % os.extsep)],file))) return ai = SFile() bw = SFile() try: self.ai.compile(ai, bw, self.extrainfo.get()) - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) undone = [] for f,s in [('ai',ai),('bw',bw)]: try: - MpqAddFileFromBuffer(h, s.text, 'scripts\\%sscript.bin' % f, MAFA_COMPRESS | MAFA_REPLACE_EXISTING) + MpqAddFileFromBuffer(h, s.text, 'scripts\\%sscript.bin)' % f, MAFA_COMPRESS | MAFA_REPLACE_EXISTING) except: - undone.append('scripts\\%sscript.bin' % f) + undone.append('scripts\\%sscript.bin)' % f) MpqCloseUpdatedArchive(h) if undone: - askquestion(parent=self, title='Save problems', message='%s could not be saved to the MPQ.' % ' and '.join(undone), type=OK) + askquestion(parent=self, title='Save problems', message='%s could not be saved to the MPQ.)' % ' and '.join(undone), type=OK) def close(self, key=None): if key and self.buttons['close']['state'] != NORMAL: @@ -2856,11 +2864,11 @@ class PyAI(Tk): def register(self, e=None): try: register_registry('PyAI','AI','bin',os.path.join(BASE_DIR, 'PyAI.pyw'),os.path.join(BASE_DIR,'Images','PyAI.ico')) - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) def help(self, e=None): - webbrowser.open('file:///%s' % os.path.join(BASE_DIR, 'Docs', 'PyAI.html')) + webbrowser.open('file:///%s)' % os.path.join(BASE_DIR, 'Docs', 'PyAI.html')) def about(self): thanks = [ @@ -2899,7 +2907,7 @@ class PyAI(Tk): except: pass self.listbox.delete(0, END) - for id,ai in self.ai.ais.iteritems(): + for id,ai in self.ai.ais.items(): self.set_entry(END, id, not ai[0], AIBIN.convflags(ai[2]), ai[1]) if sel and id in sel: self.listbox.select_set(END) @@ -2938,8 +2946,8 @@ class PyAI(Tk): pass self.listbox.delete(0, END) ais = [] - for id,ai in self.ai.ais.iteritems(): - ais.append('%s %s' % (ai[0], id)) + for id,ai in self.ai.ais.items(): + ais.append('%s %s)' % (ai[0], id)) ais.sort() for a in ais: id = a.split(' ',1)[1] @@ -2961,8 +2969,8 @@ class PyAI(Tk): pass self.listbox.delete(0, END) ais = [] - for id,ai in self.ai.ais.iteritems(): - ais.append('%s %s' % (AIBIN.convflags(ai[2]), id)) + for id,ai in self.ai.ais.items(): + ais.append('%s %s)' % (AIBIN.convflags(ai[2]), id)) ais.sort() ais.reverse() for a in ais: @@ -2985,8 +2993,8 @@ class PyAI(Tk): pass self.listbox.delete(0, END) ais = [] - for id,ai in self.ai.ais.iteritems(): - ais.append('%s\x00%s' % (TBL.decompile_string(self.ai.tbl.strings[ai[1]]), id)) + for id,ai in self.ai.ais.items(): + ais.append('%s\x00%s)' % (TBL.decompile_string(self.ai.tbl.strings[ai[1]]), id)) ais.sort() for a in ais: id = a.split('\x00')[-1] @@ -3034,9 +3042,9 @@ class PyAI(Tk): self.listbox.select_set(start, END) self.listbox.see(start) self.action_states() - s = 'aiscript.bin: %s (%s B) ' % (len(self.ai.ais),sum(self.ai.aisizes.values())) + s = 'aiscript.bin: %s (%s B) )' % (len(self.ai.ais),sum(self.ai.aisizes.values())) if self.ai.bwscript: - s += ' bwscript.bin: %s (%s B)' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) + s += ' bwscript.bin: %s (%s B))' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) self.scriptstatus.set(s) elif undo[0] == 'add': id = undo[1][0] @@ -3049,9 +3057,9 @@ class PyAI(Tk): del self.strings[undo[1][1][1]] self.resort() self.action_states() - s = 'aiscript.bin: %s (%s B) ' % (len(self.ai.ais),sum(self.ai.aisizes.values())) + s = 'aiscript.bin: %s (%s B) )' % (len(self.ai.ais),sum(self.ai.aisizes.values())) if self.ai.bwscript: - s += ' bwscript.bin: %s (%s B)' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) + s += ' bwscript.bin: %s (%s B))' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) self.scriptstatus.set(s) elif undo[0] == 'edit': oldid,id,oldflags,flags,oldstrid,strid,oldaiinfo,aiinfo = undo[1] @@ -3108,9 +3116,9 @@ class PyAI(Tk): del self.strings[ai[1]] self.resort() self.action_states() - s = 'aiscript.bin: %s (%s B) ' % (len(self.ai.ais),sum(self.ai.aisizes.values())) + s = 'aiscript.bin: %s (%s B) )' % (len(self.ai.ais),sum(self.ai.aisizes.values())) if self.ai.bwscript: - s += ' bwscript.bin: %s (%s B)' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) + s += ' bwscript.bin: %s (%s B))' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) self.scriptstatus.set(s) elif redo[0] == 'add': id = redo[1][0] @@ -3125,9 +3133,9 @@ class PyAI(Tk): self.strings[ai[1]].append(id) self.resort() self.action_states() - s = 'aiscript.bin: %s (%s B) ' % (len(self.ai.ais),sum(self.ai.aisizes.values())) + s = 'aiscript.bin: %s (%s B) )' % (len(self.ai.ais),sum(self.ai.aisizes.values())) if self.ai.bwscript: - s += ' bwscript.bin: %s (%s B)' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) + s += ' bwscript.bin: %s (%s B))' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) self.scriptstatus.set(s) elif redo[0] == 'edit': id,oldid,flags,oldflags,strid,oldstrid,aiinfo,oldaiinfo = redo[1] @@ -3185,9 +3193,9 @@ class PyAI(Tk): self.edited = True self.editstatus['state'] = NORMAL self.add_undo('add', [id, ai, e.aiinfo]) - s = 'aiscript.bin: %s (%s B) ' % (len(self.ai.ais),sum(self.ai.aisizes.values())) + s = 'aiscript.bin: %s (%s B) )' % (len(self.ai.ais),sum(self.ai.aisizes.values())) if self.ai.bwscript: - s += ' bwscript.bin: %s (%s B)' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) + s += ' bwscript.bin: %s (%s B))' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) self.scriptstatus.set(s) def remove(self, key=None): @@ -3200,7 +3208,7 @@ class PyAI(Tk): external = [] e = self.get_entry(index) if e[0] in self.ai.externaljumps[e[1]][0]: - for d in self.ai.externaljumps[e[1]][0][e[0]].iteritems(): + for d in self.ai.externaljumps[e[1]][0][e[0]].items(): for id in d[1]: if not id in external: external.append(id) @@ -3210,8 +3218,8 @@ class PyAI(Tk): ids.append(index) if cantremove: more = len(cantremove) != len(indexs) - t = '\n'.join(['\t%s referenced by: %s' % (id,', '.join(refs)) for id,refs in cantremove.iteritems()]) - cont = askquestion(parent=self, title='Removing', message="These scripts can not be removed because they are referenced by other scripts:\n%s%s" % (t,['','\n\nContinue removing the other scripts?'][more]), default=[None,YES][more], type=[OK,YESNOCANCEL][more]) + t = '\n'.join(['\t%s referenced by: %s)' % (id,', '.join(refs)) for id,refs in cantremove.items()]) + cont = askquestion(parent=self, title='Removing', message="These scripts can not be removed because they are referenced by other scripts:\n%s%s)" % (t,['','\n\nContinue removing the other scripts?'][more]), default=[None,YES][more], type=[OK,YESNOCANCEL][more]) undo = [] n = 0 for index in ids: @@ -3246,9 +3254,9 @@ class PyAI(Tk): self.edited = True self.editstatus['state'] = NORMAL self.add_undo('remove', undo) - s = 'aiscript.bin: %s (%s B) ' % (len(self.ai.ais),sum(self.ai.aisizes.values())) + s = 'aiscript.bin: %s (%s B) )' % (len(self.ai.ais),sum(self.ai.aisizes.values())) if self.ai.bwscript: - s += ' bwscript.bin: %s (%s B)' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) + s += ' bwscript.bin: %s (%s B))' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) self.scriptstatus.set(s) def find(self, key=None): @@ -3279,13 +3287,13 @@ class PyAI(Tk): ids.extend(external) try: warnings = self.ai.decompile(export, self.extdefs, self.reference.get(), 1, ids) - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) return if warnings: WarningDialog(self, warnings) if external: - askquestion(parent=self, title='External References', message='One or more of the scripts you are exporting references an external block, so the scripts that are referenced have been exported as well:\n %s' % '\n '.join(external), type=OK) + askquestion(parent=self, title='External References', message='One or more of the scripts you are exporting references an external block, so the scripts that are referenced have been exported as well:\n %s)' % '\n '.join(external), type=OK) def iimport(self, key=None, iimport=None, c=True, parent=None, extra=None): if key and self.buttons['import']['state'] != NORMAL: @@ -3306,8 +3314,8 @@ class PyAI(Tk): for o,l in self.ai.externaljumps[0]: for cid in l: if not cid in i.ais: - raise PyMSError('Interpreting',"You can't edit scripts (%s) that are referenced externally with out editing the scripts with the external references (%s) at the same time." % (id,cid)) - except PyMSError, e: + raise PyMSError('Interpreting',"You can't edit scripts (%s) that are referenced externally with out editing the scripts with the external references (%s) at the same time.)" % (id,cid)) + except PyMSError as e: ErrorDialog(parent, e) return -1 cont = c @@ -3315,7 +3323,7 @@ class PyAI(Tk): w = WarningDialog(parent, warnings, True) cont = w.cont if cont: - for id,ai in i.ais.iteritems(): + for id,ai in i.ais.items(): if id in self.ai.ais and (cont == True or cont != 2): x = ContinueImportDialog(parent, id) cont = x.cont @@ -3344,9 +3352,9 @@ class PyAI(Tk): if id not in self.strings[ai[1]]: self.strings[ai[1]].append(id) self.resort() - s = 'aiscript.bin: %s (%s B) ' % (len(self.ai.ais),sum(self.ai.aisizes.values())) + s = 'aiscript.bin: %s (%s B) )' % (len(self.ai.ais),sum(self.ai.aisizes.values())) if self.ai.bwscript: - s += ' bwscript.bin: %s (%s B)' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) + s += ' bwscript.bin: %s (%s B))' % (len(self.ai.bwscript.ais),sum(self.ai.bwscript.aisizes.values())) self.scriptstatus.set(s) self.action_states() self.edited = True @@ -3445,7 +3453,7 @@ class PyAI(Tk): try: files = open(file,'r').readlines() except: - showerror('Invalid File',"Could not open '%s'." % file) + showerror('Invalid File',"Could not open '%s'.)" % file) sets = [ TBL.TBL(), DAT.UnitsDAT(), @@ -3455,7 +3463,7 @@ class PyAI(Tk): for n,s in enumerate(sets): try: s.load_file(files[n] % {'path':BASE_DIR}) - except PyMSError, e: + except PyMSError as e: ErrorDialog(self, e) return self.tbl = sets[0] @@ -3473,8 +3481,8 @@ class PyAI(Tk): try: set = open(file,'w') except: - showerror('Invalid File',"Could not save to '%s'." % file) - set.write(('%s\n%s\n%s\n%s' % (self.stat_txt, self.settings['self.unitsdat'], self.settings['self.upgradesdat'], self.settings['self.techdat'])).replace(BASE_DIR, '%(path)s')) + showerror('Invalid File',"Could not save to '%s'.)" % file) + set.write(('%s\n%s\n%s\n%s)' % (self.stat_txt, self.settings['self.unitsdat'], self.settings['self.upgradesdat'], self.settings['self.techdat'])).replace(BASE_DIR, '%(path)s')) set.close() def main(): @@ -3483,7 +3491,7 @@ def main(): gui = PyAI() gui.mainloop() else: - p = optparse.OptionParser(usage='usage: PyAI [options] [out|aiscriptout bwscriptout]', version='PyAI %s' % LONG_VERSION) + p = optparse.OptionParser(usage='usage: PyAI [options] [out|aiscriptout bwscriptout]', version='PyAI %s)' % LONG_VERSION) p.add_option('-d', '--decompile', action='store_true', dest='convert', help="Decompile AI's from aiscript.bin and/or bwscript.bin [default]", default=True) p.add_option('-c', '--compile', action='store_false', dest='convert', help="Compile AI's to an aiscript.bin and/or bwscript.bin") p.add_option('-e', '--extrainfo', action='store_true', help="Save extra info from your script (variables, label names, and information comments) [default: Off]", default=False) @@ -3514,11 +3522,11 @@ def main(): if opt.convert: if len(args) < 2: p.error('Invalid amount of arguments, missing bwscript.bin') - args.append('%s%s%s' % (os.path.join(path,os.extsep.join(os.path.basename(args[0]).split(os.extsep)[:-1])), os.extsep, 'txt')) + args.append('%s%s%s)' % (os.path.join(path,os.extsep.join(os.path.basename(args[0]).split(os.extsep)[:-1])), os.extsep, 'txt')) else: if len(args) < 2: - args.append('%s%s%s' % (os.path.join(path,os.extsep.join(os.path.basename(args[0]).split(os.extsep)[:-1])), os.extsep, 'bin')) - args.append('%s%s%s' % (os.path.join(path,'bw' + os.extsep.join(os.path.basename(args[0]).split(os.extsep)[:-1])), os.extsep, 'bin')) + args.append('%s%s%s)' % (os.path.join(path,os.extsep.join(os.path.basename(args[0]).split(os.extsep)[:-1])), os.extsep, 'bin')) + args.append('%s%s%s)' % (os.path.join(path,'bw' + os.extsep.join(os.path.basename(args[0]).split(os.extsep)[:-1])), os.extsep, 'bin')) warnings = [] try: if opt.convert: @@ -3526,46 +3534,46 @@ def main(): ids = [] for i in opt.scripts.split(','): if len(i) != 4: - print 'Invalid ID: %s' % ids[-1] + print('Invalid ID: %s' % ids[-1]) return ids.append(i) else: ids = None - print "Loading bwscript.bin '%s', units.dat '%s', upgrades.dat '%s', techdata.dat '%s', and stat_txt.tbl '%s'" % (args[1],opt.units,opt.upgrades,opt.techdata,opt.stattxt) + print("Loading bwscript.bin '%s', units.dat '%s', upgrades.dat '%s', techdata.dat '%s', and stat_txt.tbl '%s'" % (args[1],opt.units,opt.upgrades,opt.techdata,opt.stattxt)) bin = AIBIN.AIBIN(args[1],opt.units,opt.upgrades,opt.techdata,opt.stattxt) warnings.extend(bin.warnings) - print " - Loading finished successfully\nReading BINs '%s' and '%s'..." % (args[0],args[1]) + print(" - Loading finished successfully\nReading BINs '%s' and '%s'..." % (args[0],args[1])) warnings.extend(bin.load_file(args[0])) - print " - BINs read successfully\nWriting AI Scripts to '%s'..." % args[2] + print(" - BINs read successfully\nWriting AI Scripts to '%s'..." % args[2]) warnings.extend(bin.decompile(args[2],opt.deffile,opt.reference,opt.longlabels,ids)) - print " - '%s' written succesfully" % args[2] + print(" - '%s' written succesfully" % args[2]) else: if opt.bwscript: - print "Loading base bwscript.bin '%s', units.dat '%s', upgrades.dat '%s', techdata.dat '%s', and stat_txt.tbl '%s'" % (os.path.abspath(opt.bwscript),opt.units,opt.upgrades,opt.techdata,opt.stattxt) + print("Loading base bwscript.bin '%s', units.dat '%s', upgrades.dat '%s', techdata.dat '%s', and stat_txt.tbl '%s'" % (os.path.abspath(opt.bwscript),opt.units,opt.upgrades,opt.techdata,opt.stattxt)) bin = AIBIN.AIBIN(os.path.abspath(opt.bwscript),opt.units,opt.upgrades,opt.techdata,opt.stattxt) else: bin = AIBIN.AIBIN('',opt.units,opt.upgrades,opt.techdata,opt.stattxt) if opt.aiscript: - print "Loading base aiscript.bin '%s'..." % os.path.abspath(opt.aiscript) + print("Loading base aiscript.bin '%s'..." % os.path.abspath(opt.aiscript)) bin.load_file(os.path.abspath(opt.aiscript)) - print "Interpreting file '%s'..." % args[0] + print("Interpreting file '%s'..." % args[0]) warnings.extend(bin.interpret(args[0],opt.deffile)) - print "Compiling to '%s' and '%s'..." % (args[1], args[2]) + print("Compiling to '%s' and '%s'..." % (args[1], args[2])) bin.compile(args[1], args[2], opt.extrainfo) if(opt.mpq): - print "Saving to " + opt.mpq + print("Saving to " + opt.mpq) h = MpqOpenArchiveForUpdate(opt.mpq, MOAU_OPEN_ALWAYS | MOAU_MAINTAIN_LISTFILE) MpqAddFileToArchive(h, args[1], 'scripts\\aiscript.bin', MAFA_COMPRESS | MAFA_REPLACE_EXISTING) MpqAddFileToArchive(h, args[2], 'scripts\\bwscript.bin', MAFA_COMPRESS | MAFA_REPLACE_EXISTING) MpqCloseUpdatedArchive(h) if not opt.hidewarns: for warning in warnings: - print repr(warning) - except PyMSError, e: + print(repr(warning)) + except PyMSError as e: if warnings and not opt.hidewarns: for warning in warnings: - print repr(warning) - print repr(e) + print(repr(warning)) + print(repr(e)) if __name__ == '__main__': main() \ No newline at end of file diff --git a/tools/eud_gen_trigs.py b/tools/eud_gen_trigs.py index d8132a7..97627ba 100644 --- a/tools/eud_gen_trigs.py +++ b/tools/eud_gen_trigs.py @@ -18,7 +18,7 @@ while(next): # Null pad - next = (next + '\x00\x00\x00\x00')[0:4] + next = (next + b'\x00\x00\x00\x00')[0:4] value = struct.unpack('i', next)[0] From f45ed98c84a53afa7003f2c89ad8fbc9b18c48c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:01:00 +0000 Subject: [PATCH 3/6] Complete Python 3 conversion - build now works Co-authored-by: jncraton <103612+jncraton@users.noreply.github.com> --- tools/Libs/AIBIN.py | 47 +++++++++++++++++++++++---------------------- tools/Libs/TBL.py | 3 +++ tools/Libs/utils.py | 4 ++++ 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/tools/Libs/AIBIN.py b/tools/Libs/AIBIN.py index a71d336..a25fcc6 100644 --- a/tools/Libs/AIBIN.py +++ b/tools/Libs/AIBIN.py @@ -741,7 +741,7 @@ def ai_unit(self, data, stage=0): if not stage: v = ord(data[0]) elif stage == 1: - s = self.tbl.strings[data].split('\x00') + s = self.tbl.strings[data].split(b'\x00') if s[1] != '*': v = TBL.decompile_string('\x00'.join(s[:2]), '\x0A\x28\x29\x2C') else: @@ -755,8 +755,9 @@ def ai_unit(self, data, stage=0): raise except: for i,name in enumerate(self.tbl.strings[:DAT.UnitsDAT.count]): - n = name.split('\x00')[:2] - if TBL.compile_string(data) == n[0] or (n[1] != '*' and TBL.compile_string(data) == '\x00'.join(n)): + n = name.split(b'\x00')[:2] if isinstance(name, bytes) else name.split('\x00')[:2] + compiled_data = TBL.compile_string(data).encode('latin-1') if isinstance(name, bytes) else TBL.compile_string(data) + if compiled_data == n[0] or (n[1] != b'*' if isinstance(name, bytes) else n[1] != '*') and (compiled_data == (b'\x00' if isinstance(name, bytes) else '\x00').join(n)): v = i break else: @@ -806,7 +807,7 @@ def ai_upgrade(self, data, stage=0): if not stage: v = ord(data[0]) elif stage == 1: - v = TBL.decompile_string(self.tbl.strings[self.upgradesdat.get_value(data,'Label') - 1].split('\x00',1)[0].strip(), '\x0A\x28\x29\x2C') + v = TBL.decompile_string(self.tbl.strings[self.upgradesdat.get_value(data,'Label') - 1].split(b'\x00', 1)[0].strip(), '\x0A\x28\x29\x2C') elif stage == 2: v = chr(data) + '\x00' else: @@ -816,7 +817,7 @@ def ai_upgrade(self, data, stage=0): raise except: for i in range(len(self.upgradesdat.entries)): - if TBL.compile_string(data) == self.tbl.strings[self.upgradesdat.get_value(i,'Label') - 1].split('\x00', 1)[0].strip(): + if TBL.compile_string(data).encode("latin-1") == self.tbl.strings[self.upgradesdat.get_value(i,'Label') - 1].split(b'\x00', 1)[0].strip(): v = i break else: @@ -828,7 +829,7 @@ def ai_technology(self, data, stage=0): if not stage: v = ord(data[0]) elif stage == 1: - v = TBL.decompile_string(self.tbl.strings[self.techdat.get_value(data,'Label') - 1].split('\x00',1)[0].strip(), '\x0A\x28\x29\x2C') + v = TBL.decompile_string(self.tbl.strings[self.techdat.get_value(data,'Label') - 1].split(b'\x00', 1)[0].strip(), '\x0A\x28\x29\x2C') elif stage == 2: v = chr(data) + '\x00' else: @@ -838,7 +839,7 @@ def ai_technology(self, data, stage=0): raise except: for i in range(len(self.techdat.entries)): - if TBL.compile_string(data) == self.tbl.strings[self.techdat.get_value(i,'Label') - 1].split('\x00', 1)[0].strip(): + if TBL.compile_string(data).encode("latin-1") == self.tbl.strings[self.techdat.get_value(i,'Label') - 1].split(b'\x00', 1)[0].strip(): v = i break else: @@ -918,7 +919,7 @@ def load_defs(defname): if len(l) > 1: line = l.strip().split('#',1)[0] if line: - match = re.match('\\A(\\S+)\\s+(.+)\\s+=\\s+(.+?)(?:\\s*\\{(.+)\\})?\\Z', line) + match = re.match(r'\A(\\S+)\s+(.+)\s+=\s+(.+?)(?:\s*\{(.+)\})?\Z', line) if match: t,name,dat,vinfo = match.groups() if re.match('[\x00,(){}]',name): @@ -977,11 +978,11 @@ def load_defs(defname): else: nextinfo[0][nextinfo[1]][0] += line + '\n' else: - match = re.match('\\Aextdef\\s*(.+)\\Z',line) + match = re.match(r'\Aextdef\s*(.+)\Z',line) if match: load_defs(match.group(1)) continue - match = re.match('\\A(\\S+)\\s+(\\S+)\\s+=\\s+(.+?)(?:\\s*\\{(.+)\\})?\\Z', line) + match = re.match(r'\A(\\S+)\s+(\\S+)\s+=\s+(.+?)(?:\s*\{(.+)\})?\Z', line) if match: t,name,dat,vinfo = match.groups() if re.match('[\x00,(){}]',name): @@ -1014,8 +1015,8 @@ def load_defs(defname): else: nextinfo = [2,varinfo[name]] continue - if re.match(r'\\A[^(]+\\([^)]+\\):\\s*(?:\\{.+\\})?\\Z', line): - newai = re.match(r'\\A(.+)\\(\s*(.+)\s*,\s*(.+)\s*,\s*(\w+)\s*\\):\\s*(?:\\{(.+)\\})?\\Z', line) + if re.match(r'\A[^(]+\([^)]+\):\s*(?:\{.+\})?\Z', line): + newai = re.match(r'\A(.+)\(\s*(.+)\s*,\s*(.+)\s*,\s*(\w+)\s*\):\s*(?:\{(.+)\})?\Z', line) if not newai: raise PyMSError('Interpreting','Invalid syntax, expected a new script header',n,line, warnings=warnings) id = newai.group(1) @@ -1094,7 +1095,7 @@ def load_defs(defname): cmdn = 0 continue if ai: - match = re.match('\\A(.+)\\(\\s*(.+)?\\s*\\)\\Z', line) + match = re.match(r'\A(.+)\(\s*(.+)?\s*\)\Z', line) if match: cmd = match.group(1).lower() if cmd in self.labels: @@ -1112,7 +1113,7 @@ def load_defs(defname): notused = False dat = [] if match.group(2): - dat = re.split('\\s*,\\s*', match.group(2)) + dat = re.split(r'\s*,\s*', match.group(2)) params = self.parameters[ai[4][-1][0]] if params and len(dat) != len(params): raise PyMSError('Interpreting','Incorrect amount of parameters (got %s, needed %s)' % (len(dat), len(params)),n,line, warnings=warnings) @@ -1123,7 +1124,7 @@ def load_defs(defname): for d,p in zip(dat,params): if p == self.ai_address: aisize += 2 - match = re.match('\\A(.+):(.+)\\Z', d) + match = re.match(r'\A(.+):(.+)\Z', d) if match: cid,label = match.group(1),match.group(2) if cid in default_ais: @@ -1238,7 +1239,7 @@ def load_defs(defname): cmdn += 1 nextinfo = None continue - match = re.match(r'\\A--\s*(.+)\s*--\\s*(?:\\{(.+)\\})?\\Z', line) + match = re.match(r'\A--\s*(.+)\s*--\s*(?:\{(.+)\})?\Z', line) if match: notused = False label = match.group(1) @@ -1301,7 +1302,7 @@ def load_defs(defname): if line.startswith('{'): if not nextinfo: raise PyMSError('Interpreting','An Information Comment must be afer a variable, a script header, or a block label',n,line, warnings=warnings) - match = re.match('\\A\\{(?:(.+)\\})?\\Z', line) + match = re.match(r'\A\{(?:(.+)\})?\Z', line) if match.group(1): if len(nextinfo) == 3: nextinfo[0][curinfo[1]][1][curinfo[2]] = match.group(1) @@ -1367,7 +1368,7 @@ def load_defs(defname): else: bwinfo[i[0]][1].remove(l) for b,r in enumerate(remove): - for i in r.iterkeys(): + for i in r.keys(): if r[i]: r[i].sort() n = 0 @@ -1483,7 +1484,7 @@ def decompile(self, file, defs=None, ref=False, shortlabel=True, scripts=None): if len(l) > 1: line = l.strip().split('#',1)[0] if line: - match = re.match('\\A(\\S+)\\s+(.+)\\s+=\\s+(.+?)(?:\\s*\\{(.+)\\})?\\Z', line) + match = re.match(r'\A(\\S+)\s+(.+)\s+=\s+(.+?)(?:\s*\{(.+)\})?\Z', line) if match: t,name,dat,vinfo = match.groups() if re.match('[\x00,(){}]',name): @@ -1662,8 +1663,8 @@ def compile(self, file, bwscript=None, extra=False): else: f = file warnings = [] - ais = '' - table = '' + ais = b'' + table = b'' offset = 4 totaloffsets = {} for id in self.ais.keys(): @@ -1691,9 +1692,9 @@ def compile(self, file, bwscript=None, extra=False): for id in self.ais.keys(): loc,string,flags,ai,jumps = self.ais[id] if loc: - table += struct.pack('<4s3L', id, offset, string+1, flags) + table += struct.pack('<4s3L', id.encode('latin-1') if isinstance(id, str) else id, offset, string+1, flags) for cmd in ai: - ais += chr(cmd[0]) + ais += bytes([cmd[0]]) if self.parameters[cmd[0]]: for p,t in zip(cmd[1:],self.parameters[cmd[0]]): try: diff --git a/tools/Libs/TBL.py b/tools/Libs/TBL.py index 944e7b5..dbbb9c3 100644 --- a/tools/Libs/TBL.py +++ b/tools/Libs/TBL.py @@ -90,6 +90,9 @@ def special_chr(o): return re.sub(r'<(\\d+)>', special_chr, string) def decompile_string(string, exclude='', include=''): + # Handle bytes in Python 3 + if isinstance(string, bytes): + string = string.decode('latin-1') def special_chr(o): return '<%s>' % ord(o.group(0)) decompile = DEF_DECOMPILE + include diff --git a/tools/Libs/utils.py b/tools/Libs/utils.py index 162e280..dcdae7e 100644 --- a/tools/Libs/utils.py +++ b/tools/Libs/utils.py @@ -1304,6 +1304,10 @@ def iteritems(self): for k in self.keynames: iter.append((k,self.dict[k])) return iter + + def items(self): + """Python 3 compatibility""" + return self.iteritems() def iterkeys(self): return list(self.keynames) From 3af46613694337ccf68976b479841bdb7b3b50ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:02:09 +0000 Subject: [PATCH 4/6] Update GitHub Actions to use Python 3 Co-authored-by: jncraton <103612+jncraton@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a71c196..8867fce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,8 +15,8 @@ jobs: with: node-version: 14 - - name: Install python-tk - run: sudo apt-get install --fix-missing python-tk + - name: Install python3-tk + run: sudo apt-get install --fix-missing python3-tk - name: Run make run: make From 81b4d824cf2d4ae801eaf56e4ee568adb52ba911 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:13:33 +0000 Subject: [PATCH 5/6] Fix remaining bytes/str issues and update to Ubuntu 22.04 - Fix ai_byte, ai_unit, ai_upgrade, ai_technology, ai_string to return bytes in stage 2 - Fix AIBIN and BWBIN compile to use bytes for binary data - Update GitHub Actions to use ubuntu-22.04 - Add verification step to show checksums in CI - Add BUILD_VERIFICATION.md with checksums for verification Co-authored-by: jncraton <103612+jncraton@users.noreply.github.com> --- .github/workflows/build.yml | 8 ++++++- BUILD_VERIFICATION.md | 44 +++++++++++++++++++++++++++++++++++++ tools/Libs/AIBIN.py | 24 ++++++++++---------- 3 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 BUILD_VERIFICATION.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8867fce..589acd4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout repository @@ -20,3 +20,9 @@ jobs: - name: Run make run: make + + - name: Verify build output + run: | + echo "Build completed successfully" + ls -lh build/aiscript.bin build/patch_rt.mpq + sha256sum build/aiscript.bin build/patch_rt.mpq diff --git a/BUILD_VERIFICATION.md b/BUILD_VERIFICATION.md new file mode 100644 index 0000000..51342f6 --- /dev/null +++ b/BUILD_VERIFICATION.md @@ -0,0 +1,44 @@ +# Build Verification + +This document explains how to verify that the Python 3 build produces identical output to the Python 2 build. + +## Verification Steps + +To verify that builds are reproducible and produce identical output: + +1. Build the project: + ```bash + make clean + make + ``` + +2. Generate checksums of the output files: + ```bash + sha256sum build/aiscript.bin build/patch_rt.mpq + ``` + +3. Compare the checksums with the reference values below. + +## Reference Checksums (Python 3 Build) + +These checksums were generated from a clean build using Python 3.12.3 on Ubuntu 24.04: + +``` +cf162d5eaf73b1f333ddf7317a44296c05658aa5ee0a6a3196238785bd054fb1 build/aiscript.bin +4524930427332fe3dad1c2ad893cd446b824928d08add671cdedcbd60e849604 build/patch_rt.mpq +``` + +### File Sizes + +- `aiscript.bin`: 38,398 bytes (truncated to 64,050 bytes in the final MPQ) +- `patch_rt.mpq`: ~999 KB + +## Notes + +- The build is deterministic for a given set of source files and build environment +- Minor variations in checksums may occur due to differences in: + - Node.js version (JavaScript build step) + - System architecture or libraries + - Timestamp-related data in MPQ files + +If you observe significant differences in file sizes or the build fails, please open an issue with details about your build environment. diff --git a/tools/Libs/AIBIN.py b/tools/Libs/AIBIN.py index a25fcc6..2653b4c 100644 --- a/tools/Libs/AIBIN.py +++ b/tools/Libs/AIBIN.py @@ -703,7 +703,7 @@ def ai_byte(self, data, stage=0): elif stage == 1: v = str(data) elif stage == 2: - v = chr(data) + v = bytes([data]) else: try: v = int(data) @@ -747,7 +747,7 @@ def ai_unit(self, data, stage=0): else: v = TBL.decompile_string(s[0], '\x0A\x28\x29\x2C') elif stage == 2: - v = chr(data) + '\x00' + v = bytes([data]) + b'\x00' else: try: v = int(data) @@ -809,7 +809,7 @@ def ai_upgrade(self, data, stage=0): elif stage == 1: v = TBL.decompile_string(self.tbl.strings[self.upgradesdat.get_value(data,'Label') - 1].split(b'\x00', 1)[0].strip(), '\x0A\x28\x29\x2C') elif stage == 2: - v = chr(data) + '\x00' + v = bytes([data]) + b'\x00' else: try: v = int(data) @@ -831,7 +831,7 @@ def ai_technology(self, data, stage=0): elif stage == 1: v = TBL.decompile_string(self.tbl.strings[self.techdat.get_value(data,'Label') - 1].split(b'\x00', 1)[0].strip(), '\x0A\x28\x29\x2C') elif stage == 2: - v = chr(data) + '\x00' + v = bytes([data]) + b'\x00' else: try: v = int(data) @@ -857,7 +857,7 @@ def ai_string(self, data, stage=0): s = TBL.compile_string(data) if '\x00' in s: raise PyMSError('Parameter',"String '%s' contains a null (<0>)" % data) - return [len(s) + 1,s + '\x00'] + return [len(s) + 1,s.encode('latin-1') + b'\x00'] return [len(data),data] def interpret(self, files, defs=None, extra=False): @@ -1715,8 +1715,8 @@ def compile(self, file, bwscript=None, extra=False): offset += d[0] offset += 1 else: - table += struct.pack('<4s3L', id, 0, string+1, flags) - f.write('%s%s%s\x00\x00\x00\x00' % (struct.pack(' Date: Tue, 28 Oct 2025 22:29:18 +0000 Subject: [PATCH 6/6] Remove BUILD_VERIFICATION.md Co-authored-by: jncraton <103612+jncraton@users.noreply.github.com> --- BUILD_VERIFICATION.md | 44 ------------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 BUILD_VERIFICATION.md diff --git a/BUILD_VERIFICATION.md b/BUILD_VERIFICATION.md deleted file mode 100644 index 51342f6..0000000 --- a/BUILD_VERIFICATION.md +++ /dev/null @@ -1,44 +0,0 @@ -# Build Verification - -This document explains how to verify that the Python 3 build produces identical output to the Python 2 build. - -## Verification Steps - -To verify that builds are reproducible and produce identical output: - -1. Build the project: - ```bash - make clean - make - ``` - -2. Generate checksums of the output files: - ```bash - sha256sum build/aiscript.bin build/patch_rt.mpq - ``` - -3. Compare the checksums with the reference values below. - -## Reference Checksums (Python 3 Build) - -These checksums were generated from a clean build using Python 3.12.3 on Ubuntu 24.04: - -``` -cf162d5eaf73b1f333ddf7317a44296c05658aa5ee0a6a3196238785bd054fb1 build/aiscript.bin -4524930427332fe3dad1c2ad893cd446b824928d08add671cdedcbd60e849604 build/patch_rt.mpq -``` - -### File Sizes - -- `aiscript.bin`: 38,398 bytes (truncated to 64,050 bytes in the final MPQ) -- `patch_rt.mpq`: ~999 KB - -## Notes - -- The build is deterministic for a given set of source files and build environment -- Minor variations in checksums may occur due to differences in: - - Node.js version (JavaScript build step) - - System architecture or libraries - - Timestamp-related data in MPQ files - -If you observe significant differences in file sizes or the build fails, please open an issue with details about your build environment.