diff --git a/code/game/objects/items/flag.dm b/code/game/objects/items/flag.dm index 2bccf5493f6..f0e2f5b0fc8 100644 --- a/code/game/objects/items/flag.dm +++ b/code/game/objects/items/flag.dm @@ -10,4 +10,9 @@ /obj/item/flag/clown name = "Clown Planet flag" desc = "The banner of His Majesty, King Squiggles the Eighth." - icon_state = "clownflag" \ No newline at end of file + icon_state = "clownflag" + +/obj/item/flag/cargo + name = "Cargonia flag" + desc = "The flag of the independant, sovereign nation of Cargonia." + icon_state = "cargoflag" \ No newline at end of file diff --git a/icons/mob/in-hand/compile.py b/icons/mob/in-hand/compile.py deleted file mode 100644 index 5849530ce33..00000000000 --- a/icons/mob/in-hand/compile.py +++ /dev/null @@ -1,48 +0,0 @@ -''' -Created on Feb 28, 2014 - -@author: Rob -''' -import os, sys, logging - -ToBuild={ -# ' file to build': 'directory to pull from/', - '../items_lefthand.dmi': 'left/', - '../items_righthand.dmi': 'right/' -} - -# Tell Python where to find OpenBYOND. -# Assuming we're in icons/mob/in-hand -sys.path.append('../../../tools/OpenBYOND/src') - -from com.byond.DMI import DMI -from DMITool import compare_all - -def buildDMI(directory, output): - dmi = DMI(output) - logging.info('Creating {0}...'.format(output)) - for root, _, files in os.walk(directory): - for filename in files: - if filename.endswith('.dmi') and not filename.endswith('.new.dmi'): - filepath = os.path.join(root,filename) - logging.info('Adding {0}...'.format(filename,output)) - subdmi = DMI(filepath) - subdmi.loadAll() - if subdmi.icon_height!=32 or subdmi.icon_width!=32: - logging.warn('Skipping {0} - Invalid icon size.'.format(filepath)) - changes = 0 - for state_name in subdmi.states: - if state_name in dmi.states: - logging.warn('Skipping state {0}:{1} - State exists.'.format(filepath,state_name)) - continue - dmi.states[state_name]=subdmi.states[state_name] - changes += 1 - logging.info('Added {0} states.'.format(changes)) - #save - logging.info('Saving {0} states to {1}...'.format(len(dmi.states),output)) - dmi.save(output) -if __name__ == '__main__': - # Cheating, but useful for checking for unsync'd stuff - compare_all('left/','right/','in-hand_sync_report.txt',None,newfile_theirs=False,newfile_mine=False) - for output, input_dir in ToBuild.items(): - buildDMI(input_dir,output) \ No newline at end of file diff --git a/icons/mob/in-hand/in-hand_sync_report.txt b/icons/mob/in-hand/in-hand_sync_report.txt index 4229f734db7..4d6c2c862e6 100644 --- a/icons/mob/in-hand/in-hand_sync_report.txt +++ b/icons/mob/in-hand/in-hand_sync_report.txt @@ -1,18 +1,534 @@ -# DMITool Difference Report: C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\left C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\right ---- C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\left\cable_stuff.dmi -+++ C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\right\cable_stuff.dmi +# DMITool Difference Report: Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\left Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\right +--- Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\left\cable_stuff.dmi ++++ Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\right\cable_stuff.dmi + cuff_cyan + cuff_orange + cuff_pink + cuff_white ---- C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\left\guns.dmi -+++ C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\right\guns.dmi +--- Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\left\guns.dmi ++++ Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\right\guns.dmi + scythe0 ---- C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\left\rpd.dmi -+++ C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\right\rpd.dmi +--- Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\left\paradise_items_lefthand.dmi ++++ Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\right\paradise_items_lefthand.dmi + + DONGS + + altair_locket + + an_tank + + analyzer + + armor + + armor_reflec + + assembly + + atmos_hardsuit + + atmos_helm + + atmos_suit + + atoxinbottle + + autoinjector + + axe0 + + axe1 + + b_mask + + b_shoes + + b_shoesold + + b_suit + + ba_suit + + backpack + + balloon + + balloon-empty + + basketball + + baton + + baton_on + + beachball + + beaker + + bedsheet + + beer + + bgloves + + bible + + bike_horn + + bio_hood + + bio_orange + + bio_suit + + bl_shoes + + bl_shoesold + + bl_suit + + blackskirt + + blade + + blindfold + + blueprints + + bombvest + + brain2 + + brick + + briefcase + + broken_beer + + broom + + browncane + + brutepack + + bucket + + buckler + + bucklerOLD + + buildpipe + + burger + + butch + + butcher_knife + + c20r + + c_tube + + cakehat0 + + cakehat1 + + candy + + cane_nt + + cane_nt_active + + canister + + caphat + + capspacehelmet + + capspacesuit + + captainpack + + card-id + + cardborg + + cardborg_h + + carton + + caution + + ce_hardsuit + + ce_helm + + cell + + centcom + + centhat + + cespace_helmet + + cespace_suit + + chain + + chainmail + + chef + + chefhat + + chickensuit + + chips + + cigoff + + cigon + + cigpacket + + classic_baton + + claymore + + cleaner + + clipboard + + clown + + clown_hat + + clown_shoes + + clownflag + + clownhelmet + + clownpack + + coffee + + coil + + coil-wire + + coil_blue + + coil_red + + cola + + contliquid + + contsolid + + contvapour + + coolant-c + + crossbow + + crossbow-solid + + crowbar + + crowbar_red + + cueball + + cultblade + + culthood + + cultpack + + cultrobes + + cutlass0 + + cutlass1 + + cutters + + cutters_yellow + + daederic + + deagleg + + death_commando_mask + + death_commando_suit + + deathnettle + + det + + det_hat + + det_suit + + dg_suit + + dnainjector + + donut1 + + donut2 + + dpickaxe + + dr_gibb + + dropper + + dualsaber0 + + dualsaber1 + + dualsaberblue1 + + dualsabergreen1 + + dualsaberpurple1 + + dualsaberred1 + + earmuffs + + egg + + egg1 + + egg2 + + egg3 + + egg4 + + egg5 + + eleanorstone + + electronic + + electropack + + emergency + + emergency_double + + emergency_engi + + emp + + energy_drink + + energykill + + energykill0 + + energykill100 + + energykill25 + + energykill50 + + energykill75 + + energystun + + energystun0 + + energystun100 + + energystun25 + + energystun50 + + energystun75 + + eng_hardsuit + + eng_helm + + engi_suit + + engiepack + + engspace_helmet + + engspace_suit + + eshield0 + + eshield1 + + faggot + + fire_extinguisher + + fire_suit + + fireaxe0 + + fireaxe1 + + firefighter + + firstaid + + firstaid-advanced + + firstaid-ointment + + firstaid-toxin + + flamethrower_0 + + flamethrower_1 + + flare + + flashbang + + flashlight + + foamcrossbow + + focus + + fork + + fr_jacket + + g_suit + + galoshes + + gas_alt + + gas_mask + + gift + + giftbag + + glasses + + gold_id + + gpickaxe + + greatcoat + + greenbandana + + gun + + gy_suit + + handcuff + + harpoon + + hatchet + + headset + + healthanalyzer + + helm-command + + helm-orange + + helmet + + hoe + + holidaypriest + + ice_tea_can + + imperium_monk + + implantcase + + ithaqua + + jackboots + + jackhammer + + janitor + + jensencoat + + jensensuit + + jetpack + + jetpack-black + + jetpack-void + + johnny + + johnny_coat + + judge + + katana + + kilt + + kingyellow + + knife + + kobskull + + konaahirano + + koran + + l6closedmag + + l6closednomag + + l6openmag + + l6opennomag + + labcoat + + lamp + + lampgreen + + laser + + latexballon + + launcher + + lawyer_black + + lawyer_blue + + lawyer_red + + lb_suit + + lemon-lime + + lgloves + + lipstick + + lipstickopen + + m_mask + + magboots + + medibottle + + medical_hardsuit + + medical_helm + + medicalpack + + melted + + miniFE + + mining_hardsuit + + mining_helm + + mister + + mop + + mousetrap + + multitool + + muzzle + + nazi1 + + necronomicon + + nettle + + newspaper + + nothing + + ntflag + + nucgun + + nucgun0 + + nucgun100 + + nucgun25 + + nucgun50 + + nucgun75 + + nullrod + + nun + + nursesuit + + o_shoes + + o_shoes1 + + o_suit + + ointment + + overalls + + owl + + owl_mask + + oxygen + + oxygen-c + + oxygen_f + + oxygen_fr + + p_suit + + paintcan + + paper + + pen + + pickaxe + + pie + + pill + + plantbgone + + plasma + + plasma-c + + plasticbag + + plasticx + + pneumatic + + pneumatic-tank + + ppickaxe + + prod + + psyche + + pulse + + pulse0 + + pulse100 + + pulse25 + + pulse50 + + pulse75 + + purple_can + + pwig + + quarrel + + r_feet + + r_hands + + r_head + + r_shoes + + r_suit + + rack_parts + + rad_suit + + radio + + rainbow + + razor + + rcd + + rcdammo + + rejuvbottle + + restock_unit + + rev_headballoon + + revballoon + + rig_helm + + rig_suit + + riot + + riotgun + + ro_suit + + rods + + rolling_pin + + roman_shield + + rubberducky + + s-ninja_hood + + s-ninja_mask + + s-ninja_suit + + s_helmet + + s_mask + + s_suit + + santa + + santahat + + saw3 + + scalpel + + scientology + + scrapbook + + scratch + + screwdriver + + screwdriver_blue + + screwdriver_brown + + screwdriver_cyan + + screwdriver_green + + screwdriver_purple + + screwdriver_yellow + + scythe0 + + sec-case + + sec_hardsuit + + sec_helm + + secsoft + + securitypack + + shard-glass + + sheet-card + + sheet-glass + + sheet-metal + + sheet-rglass + + shotgun + + shovel + + signaler + + silver_id + + sl_suit + + sodawater + + sord + + space-up + + space_helmet_syndicate + + space_mountain_wind + + space_suit_syndicate + + spaceclown_items + + spacemime_items + + spade + + spearglass0 + + spearglass1 + + speargun + + spickaxe + + spoon + + spraycan + + staff + + staffofchange + + staffofhealing + + stamp + + starkist + + stick + + stool + + straight_jacket + + suit-command + + suit-orange + + sunglasses + + swat_gl + + swat_hel + + swat_sh + + swat_suit + + sword0 + + swordblue + + swordchain + + swordgreen + + swordpurple + + swordred + + syndballoon + + syndicate-black + + syndicate-black-blue + + syndicate-black-green + + syndicate-black-red + + syndicate-green + + syndicate-green-dark + + syndicate-helm-black + + syndicate-helm-black-blue + + syndicate-helm-black-green + + syndicate-helm-black-red + + syndicate-helm-green + + syndicate-helm-green-dark + + syndie_hardsuit + + syndie_helm + + syringe_0 + + syringe_1 + + syringe_10 + + syringe_15 + + syringe_5 + + syringe_kit + + syringegun + + table_parts + + taser + + taser0 + + taser100 + + taser25 + + taser50 + + taser75 + + tdgreen + + tdhelm + + tdred + + template + + that + + thirteen_loko + + tile + + tonic + + toolbox_blue + + toolbox_red + + toolbox_syndi + + toolbox_yellow + + toxinbottle + + toysword0 + + toysword1 + + trashbag + + ty_necklace + + unathiknife + + upickaxe + + vhammer + + vhelmet + + vialblue + + vialgreen + + videocam + + violin + + vox-helmet-engineer + + vox-helmet-medical + + vox-helmet-normal + + vox-helmet-science + + vox-helmet-security + + vox-helmet-specops + + vox-pressure-engineer + + vox-pressure-medical + + vox-pressure-normal + + vox-pressure-science + + vox-pressure-security + + vox-pressure-specops + + w_shoes + + w_suit + + waiter + + warden + + wcoat + + welder + + welder1 + + welding + + welding-g + + weldingoff + + wiz_hardsuit + + wiz_helm + + wizhat + + wizrobe + + wizshoe + + wrap_paper + + wrench + + xenos_helm + + xenos_suit + + y_suit + + ygloves + + zippo + + zippoon +--- Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\left\rpd.dmi ++++ Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\right\rpd.dmi + rpd ---- C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\left\swords_axes.dmi -+++ C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\right\swords_axes.dmi +--- Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\left\swords_axes.dmi ++++ Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\right\swords_axes.dmi + axe0 + axe1 - dualsaber0 @@ -22,11 +538,11 @@ - katana - scythe0 - unathiknife ---- C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\left\vox_civvie.dmi -+++ C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\right\vox_civvie.dmi +--- Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\left\vox_civvie.dmi ++++ Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\right\vox_civvie.dmi + vox-pressure-security ---- C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\left\zz_old_items_lefthand.dmi -+++ C:\Users\Rob\Documents\Projects\vgstation13\icons\mob\in-hand\right\zz_old_items_lefthand.dmi +--- Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\left\zz_old_items_lefthand.dmi ++++ Z:\Git\Space Station 13\Baystation12\icons\mob\in-hand\right\zz_old_items_lefthand.dmi + DONGS + Gibtonite ore + altair_locket diff --git a/icons/mob/in-hand/left/flags.dmi b/icons/mob/in-hand/left/flags.dmi new file mode 100644 index 00000000000..227da84987c Binary files /dev/null and b/icons/mob/in-hand/left/flags.dmi differ diff --git a/icons/mob/in-hand/left/paradise_items_lefthand.dmi b/icons/mob/in-hand/left/paradise_items_lefthand.dmi new file mode 100644 index 00000000000..fd773f32b77 Binary files /dev/null and b/icons/mob/in-hand/left/paradise_items_lefthand.dmi differ diff --git a/icons/mob/in-hand/right/flags.dmi b/icons/mob/in-hand/right/flags.dmi new file mode 100644 index 00000000000..7f404a92d4b Binary files /dev/null and b/icons/mob/in-hand/right/flags.dmi differ diff --git a/icons/mob/in-hand/right/paradise_items_righthand.dmi b/icons/mob/in-hand/right/paradise_items_righthand.dmi new file mode 100644 index 00000000000..b02f6804054 Binary files /dev/null and b/icons/mob/in-hand/right/paradise_items_righthand.dmi differ diff --git a/icons/mob/in-hand/ss13_makeinhands.py b/icons/mob/in-hand/ss13_makeinhands.py new file mode 100644 index 00000000000..84d62088588 --- /dev/null +++ b/icons/mob/in-hand/ss13_makeinhands.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +''' +Run within icons/mob/in-hand. + +Usage: + $ cd icons/mob/in-hands + $ python ss13_makeinhands.py + +ss13_makeinhands.py - Generates a large DMI from several smaller DMIs. + Specifically used for making icons/mob/items_(left|right)hand.dmi + +Copyright 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +''' +import os, sys, logging + +ToBuild = { +# ' file to build': 'directory to pull from/', + '../items_lefthand.dmi': 'left/', + '../items_righthand.dmi': 'right/' +} + +# Tell Python where to find BYONDTools. +# Assuming we're in icons/mob/in-hand +sys.path.append('../../../tools/BYONDTools') # For byond +sys.path.append('../../../tools/BYONDTools/scripts') # For dmi + +from byond.DMI import DMI +from dmi import compare_all + +def buildDMI(directory, output): + dmi = DMI(output) + logging.info('Creating {0}...'.format(output)) + for root, _, files in os.walk(directory): + for filename in files: + if filename.endswith('.dmi') and not filename.endswith('.new.dmi'): + filepath = os.path.join(root, filename) + logging.info('Adding {0}...'.format(filename, output)) + subdmi = DMI(filepath) + subdmi.loadAll() + if subdmi.icon_height != 32 or subdmi.icon_width != 32: + logging.warn('Skipping {0} - Invalid icon size.'.format(filepath)) + changes = 0 + for state_name in subdmi.states: + if state_name in dmi.states: + logging.warn('Skipping state {0}:{1} - State exists.'.format(filepath, subdmi.states[state_name].displayName())) + continue + dmi.states[state_name] = subdmi.states[state_name] + changes += 1 + logging.info('Added {0} states.'.format(changes)) + # save + logging.info('Saving {0} states to {1}...'.format(len(dmi.states), output)) + dmi.save(output) + +if __name__ == '__main__': + logging.basicConfig( + format='%(asctime)s [%(levelname)-8s]: %(message)s', + datefmt='%m/%d/%Y %I:%M:%S %p', + level=logging.INFO # , + # filename='logs/main.log', + # filemode='w' + ) + # Cheating, but useful for checking for unsync'd stuff + compare_all('left/', 'right/', 'in-hand_sync_report.txt', None, newfile_theirs=False, newfile_mine=False, check_changed=False) + for output, input_dir in ToBuild.items(): + buildDMI(input_dir, output) diff --git a/icons/mob/items_lefthand.dmi b/icons/mob/items_lefthand.dmi index fd773f32b77..dbf957b8c2d 100644 Binary files a/icons/mob/items_lefthand.dmi and b/icons/mob/items_lefthand.dmi differ diff --git a/icons/mob/items_righthand.dmi b/icons/mob/items_righthand.dmi index b02f6804054..0bad0f69743 100644 Binary files a/icons/mob/items_righthand.dmi and b/icons/mob/items_righthand.dmi differ diff --git a/icons/obj/flag.dmi b/icons/obj/flag.dmi index 33def57be3b..7959f04bc34 100644 Binary files a/icons/obj/flag.dmi and b/icons/obj/flag.dmi differ diff --git a/tools/BYONDTools/.gitignore b/tools/BYONDTools/.gitignore new file mode 100644 index 00000000000..0303a1c6903 --- /dev/null +++ b/tools/BYONDTools/.gitignore @@ -0,0 +1,7 @@ +*.pyc +/doc/_build +*.bak +/.tox +/BYONDTools.egg-info +/build +/dist diff --git a/tools/BYONDTools/.project b/tools/BYONDTools/.project new file mode 100644 index 00000000000..c26ef343673 --- /dev/null +++ b/tools/BYONDTools/.project @@ -0,0 +1,17 @@ + + + ByondTools + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/tools/BYONDTools/LICENSE.md b/tools/BYONDTools/LICENSE.md new file mode 100644 index 00000000000..05c03da6eb1 --- /dev/null +++ b/tools/BYONDTools/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tools/BYONDTools/README.md b/tools/BYONDTools/README.md new file mode 100644 index 00000000000..8875a771921 --- /dev/null +++ b/tools/BYONDTools/README.md @@ -0,0 +1,30 @@ +OpenByond +========= + +A set of tools for BYOND-based games. + +Overview +-------- + +This is a toolkit for developers of BYOND-based games intended to do common, complex +tasks that the engine itself does not currently possess. It also exposes common +interfaces for developers wishing to obtain information from the BYOND object tree, +maps, and DMIs. + +Requirements +------------ + +* Python 2.7+ +* Python Imaging Library (PIL) +* numpy + +Use +--- + +Each script has documentation in its code at the top of each file. + +Support +------- + +No commercial or official support is provided, but you are free to +submit bug reports or harass N3X15 in irc.rizon.net #vgstation. diff --git a/tools/BYONDTools/byond/DMI/State.py b/tools/BYONDTools/byond/DMI/State.py new file mode 100644 index 00000000000..b14388626ed --- /dev/null +++ b/tools/BYONDTools/byond/DMI/State.py @@ -0,0 +1,142 @@ +from byond import directions +from PIL import Image +import logging + +class State: + # So we don't overwrite the static state. + MovementTag = 'M' + def __init__(self, nm): + self.name = nm + self.hotspot = '' + self.frames = 0 + self.dirs = 1 + self.movement = 0 + self.loop = 0 + self.rewind = 0 + self.delay = [] + self.icons = [] + self.positions = [] + + @staticmethod + def MakeKey(name,movement=False): + key = name + tags = '' + if movement: + tags += State.MovementTag + if tags != '': + key += '\t' + tags + return key + + def genManifest(self): + ''' +state = "void" + dirs = 4 + frames = 4 + delay = 2,2,2,2 + ''' + o = '\r\nstate = "{0}"'.format(self.name) + o += self.genManifestLine('hotspot', self.hotspot, '') + o += self.genManifestLine('frames', self.frames, -1) + o += self.genManifestLine('dirs', self.dirs, -1) + o += self.genManifestLine('movement', self.movement, 0) + o += self.genManifestLine('loop', self.loop, 0) + o += self.genManifestLine('rewind', self.rewind, 0) + o += self.genManifestLine('delay', self.delay, []) + + return o + + # Mostly for movement states. + def displayName(self): + tags=[] + if self.movement: + tags += self.MovementTag + if len(tags)>0: + return '{} ({})'.format(self.name,', '.join(tags)) + return self.name + + def genDMIH(self): + o = '\r\nstate "%s" {' % self.name + o += self.genDMIHLine('hotspot', self.hotspot, '') + o += self.genDMIHLine('frames', self.frames, -1) + tdirs = 'ONE' + if self.dirs == 4: + tdirs = 'CARDINAL' + elif self.dirs == 8: + tdirs = 'ALL' + o += self.genDMIHLine('dirs', tdirs, '') + o += self.genDMIHLine('movement', self.movement, 0) + o += self.genDMIHLine('loop', self.loop, 0) + o += self.genDMIHLine('rewind', self.rewind, 0) + o += self.genDMIHLine('delay', self.delay, []) + + o += '\n\timport pngs {' + for vdir in range(self.dirs): + _dir = directions.IMAGE_INDICES[vdir] + o += '\n\t\tdirection "%s" {' % directions.getNameFromDir(_dir) + for f in range(self.frames): + o += '\n\t\t\t"%s"' % self.getFrame(_dir, f) + o += '\n\t\t}' + o += '\n\t}' + o += "\n}" + return o + + def key(self): + return State.MakeKey(self.name, movement=self.movement==1) + + def genDMIHLine(self, name, value, default): + if value != default: + if type(value) is list: + value = ','.join(value) + return '\n\t{0} = {1}'.format(name, value) + return '' + + def genManifestLine(self, name, value, default): + if value != default: + if type(value) is list: + value = ','.join(value) + return '\n\t{0} = {1}'.format(name, value) + return '' + + def ToString(self): + o = '%s: %d frames, ' % (self.name, self.frames) + o += '%d directions' % self.dirs + # o += ' icons: ' + repr(self.icons) + return o + + def numIcons(self): + return self.frames * self.dirs + + def getFrameIndex(self, direction, frame): + _dir = 0 + if self.dirs == 4 or self.dirs == 8: + _dir = directions.IMAGE_INDICES.index(direction) + if self.dirs == 4 and _dir > 3: + _dir = 0 + + frame = _dir + (frame * self.dirs) + if frame > len(self.icons): + logging.warn('Only {} icons in state, args {{dir:{}, frame:{}}}: {}'.format(len(self.icons), direction, frame, self.ToString())) + return frame + + def getFrame(self, direction, frame): + return self.icons[self.getFrameIndex(direction, frame)] + + def setFrame(self, direction, frame, img): + fi = self.getFrameIndex(direction, frame) + if len(self.icons) < fi: + shouldBeSize = 1 + if fi > 7: + logging.error('Unable to set frame: State uninitialized with that many frames.') + return + elif fi > 3: + shouldBeSize = 8 + elif fi > 0: + shouldBeSize = 4 + while len(self.icons) < shouldBeSize: + self.icons.append(Image.new('RGBA', (32, 32))) + logging.warn('{0} now has {1} frames.'.format(self.name, len(self.icons))) + self.icons[fi] = img + + def postProcess(self): + # So old I forgot what everything did. + return diff --git a/tools/BYONDTools/byond/DMI/__init__.py b/tools/BYONDTools/byond/DMI/__init__.py new file mode 100644 index 00000000000..2fd68007e1d --- /dev/null +++ b/tools/BYONDTools/byond/DMI/__init__.py @@ -0,0 +1,379 @@ + +import sys, os, glob, string, traceback, fnmatch, math, shutil, collections + +from PIL import Image, PngImagePlugin +from .State import State +from byond.DMIH import * +import logging + +class DMILoadFlags: + NoImages = 1 + NoPostProcessing = 2 + + +class DMI: + MovementTag = '\t' + def __init__(self, filename): + self.filename = filename + self.version = '' + self.states = collections.OrderedDict() # {} + self.icon_width = 32 + self.icon_height = 32 + self.pixels = None + self.size = () + self.statelist = 'LOLNONE' + self.max_x = -1 + self.max_y = -1 + self.img = None + + def make(self, makefile): + print('>>> Compiling %s -> %s' % (makefile, self.filename)) + h = DMIH() + h.parse(makefile) + for node in h.tokens: + if type(node) is Variable: + if node.name == 'height': + self.icon_height = node.value + elif node.name == 'weight': + self.icon_width = node.value + elif type(node) is directives.State: + self.states[node.state.key()] = node.state + elif type(node) is directives.Import: + if node.ftype == 'dmi': + dmi = DMI(node.filedef) + dmi.extractTo("_tmp/" + os.path.basename(node.filedef)) + for name in dmi.states: + self.states[name] = dmi.states[name] + + def save(self, to, **kwargs): + if len(self.states) == 0: + return # Nope. + # Now build the manifest + manifest = '#BEGIN DMI' + manifest += '\nversion = 4.0' + manifest += '\n\twidth = {0}'.format(self.icon_width) + manifest += '\n\theight = {0}'.format(self.icon_height) + + frames = [] + fdata = [] + + # Sort by name because I'm autistic like that. + ordered = self.states + if kwargs.get('sort', True): + ordered = sorted(self.states.keys()) + + for name in ordered: + if len(self.states[name].icons) > 0: + manifest += self.states[name].genManifest() + numIcons = self.states[name].numIcons() + lenIcons = len(self.states[name].icons) + if numIcons != lenIcons: + logging.warn('numIcons={0}, len(icons)={1} in state {2}!'.format(numIcons, lenIcons, name)) + # frames += self.states[name].icons + # frames.extend(self.states[name].icons) + for i in range(len(self.states[name].icons)): + fdata += ['{}[{}]'.format(self.states[name].name, i)] + frames += [self.states[name].icons[i]] + else: + logging.warn('State {0} has 0 icons.'.format(name)) + manifest += '\n#END DMI' + + # print(manifest) + + # Next bit borrowed from DMIDE. + icons_per_row = math.ceil(math.sqrt(len(frames))) + rows = icons_per_row + + if len(frames) > icons_per_row * rows: + rows += 1 + + sheet = Image.new('RGBA', (int((icons_per_row + 1) * self.icon_width), int(rows * self.icon_height))) + + x = 0 + y = 0 + # for frame in frames: + # print('per_row={0}, rows={1}, size={2}'.format(icons_per_row,rows,sheet.size)) + for f in range(len(frames)): + frame = frames[f] + icon = frame + if isinstance(frame, str): + icon = Image.open(frame, 'r') + box = (x * self.icon_width, y * self.icon_height) + # print('{0} -> ({1},{2}) {3} {4}'.format(f,x,y,box,fdata[f])) + sheet.paste(icon, box, icon) + x += 1 + if x > icons_per_row: + y += 1 + x = 0 + + # More borrowed from DMIDE: + # undocumented class + meta = PngImagePlugin.PngInfo() + + # copy metadata into new object + reserved = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect') + for k, v in sheet.info.items(): + if k in reserved: continue + meta.add_text(k, v, 1) + + # Only need one - Rob + meta.add_text(b'Description', manifest.encode('ascii'), 1) + + # and save + sheet.save(to, 'PNG', pnginfo=meta) + # with open(to+'.txt','w') as f: + # f.write(manifest) + # logging.info('>>> {0} states saved to {1}'.format(len(frames), to)) + + def getDMIH(self): + o = '# DMI Header 1.0 - Generated by DMI.py' + o += self.genDMIHLine('width', self.icon_width, -1) + o += self.genDMIHLine('height', self.icon_height, -1) + + for s in sorted(self.states): + o += self.states[s].genDMIH() + + return o + + def genDMIHLine(self, name, value, default): + if value != default: + if type(value) is list: + value = ','.join(value) + return '\n{0} = {1}'.format(name, value) + return '' + + def extractTo(self, dest, suppress_post_process=False): + flags = 0 + if(suppress_post_process): + flags |= DMILoadFlags.NoPostProcessing + # print('>>> Loading %s...' % self.filename) + self.loadAll(flags) + # print('>>> Extracting %s...' % self.filename) + self.extractAllStates(dest, flags) + + def getFrame(self, state, direction, frame, movement=False): + state = State.MakeKey(state,movement=movement) + if state not in self.states: + return None + return self.states[state].getFrame(direction, frame) + + def setFrame(self, state, direction, frame, img, movement=False): + state = State.MakeKey(state,movement=movement) + if state not in self.states: + self.states[state] = State(state) + return self.states[state].setFrame(direction, frame, img) + + def getHeader(self): + img = Image.open(self.filename) + if(b'Description' not in img.info): + raise Exception("DMI Description is not in the information headers!") + return img.info[b'Description'].decode('ascii') + + def setHeader(self, newHeader, dest): + img = Image.open(self.filename) + + # More borrowed from DMIDE: + # undocumented class + meta = PngImagePlugin.PngInfo() + + # copy metadata into new object + reserved = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect', 'icc_profile') + for k, v in img.info.items(): + if k in reserved: continue + # print(k, v) + meta.add_text(k, v, 1) + + # Only need one - Rob + meta.add_text(b'Description', newHeader.encode('ascii'), 1) + + # and save + img.save(dest + '.tmp', 'PNG', pnginfo=meta) + shutil.move(dest + '.tmp', dest) + + def loadMetadata(self, flags=0): + self.load(flags | DMILoadFlags.NoImages) + + def loadAll(self, flags=0): + self.load(flags) + + def load(self, flags): + self.img = Image.open(self.filename) + + # This is a stupid hack to work around BYOND generating indexed PNGs with unspecified transparency. + # Uncorrected, this will result in PIL(low) trying to read the colors as alpha. + if self.img.mode == 'P': + # If there's no transparency, set it to black. + if 'transparency' not in self.img.info: + logging.warn('({0}): Indexed PNG does not specify transparency! Setting black as transparency. self.img.info = {1}'.format(self.filename, repr(self.img.info))) + self.img.info['transparency'] = 0 + + # Always use RGBA, it causes less problems. + self.img = self.img.convert('RGBA') + + self.size = self.img.size + + # Sanity + if(b'Description' not in self.img.info): + raise Exception("DMI Description is not in the information headers!") + + # Load pixels from image + self.pixels = self.img.load() + + # Load DMI header + desc = self.img.info[b'Description'].decode('ascii') + """ +version = 4.0 + width = 32 + height = 32 +state = "fire" + dirs = 4 + frames = 1 +state = "fire2" + dirs = 1 + frames = 1 +state = "void" + dirs = 4 + frames = 4 + delay = 2,2,2,2 +state = "void2" + dirs = 1 + frames = 4 + delay = 2,2,2,2 + """ + state = None + x = 0 + y = 0 + self.statelist = desc + ii = 0 + for line in desc.split("\n"): + line = line.strip() + if line.startswith("#"): + continue + if '=' in line: + (key, value) = line.split(' = ') + key = key.strip() + value = value.strip().replace('"', '') + if key == 'version': + self.version = value + elif key == 'width': + self.icon_width = int(value) + self.max_x = self.img.size[0] / self.icon_width + elif key == 'height': + self.icon_height = int(value) + self.max_y = self.img.size[1] / self.icon_height + # print(('%s: {sz: %s,h: %d, w: %d, m_x: %d, m_y: %d}'%(self.filename,repr(img.size),self.icon_height,self.icon_width,self.max_x,self.max_y))) + elif key == 'state': + if state != None: + # print(" + %s" % (state.ToString())) + if(self.icon_width == 0 or self.icon_height == 0): + if(len(self.states) > 0): + raise SystemError("Width and height for each cell are not available.") + else: + self.icon_width = self.img.size[0] + self.max_x = 1 + self.icon_height = self.img.size[1] + self.max_y = 1 + elif(self.max_x == -1 or self.max_y == -1): + self.max_x = self.img.size[0] / self.icon_width + self.max_y = self.img.size[1] / self.icon_width + for _ in range(state.numIcons()): + state.positions += [(x, y)] + if (flags & DMILoadFlags.NoImages) == 0: + state.icons += [self.loadIconAt(x, y)] + x += 1 + # print('%s[%d:%d] x=%d, max_x=%d' % (self.filename,ii,i,x,self.max_x)) + if(x >= self.max_x): + x = 0 + y += 1 + self.states[state.key()] = state + # if not suppress_post_process: + # self.states[state.name].postProcess() + ii += 1 + state = State(value) + elif key == 'dirs': + state.dirs = int(value) + elif key == 'frames': + state.frames = int(value) + elif key == 'loop': + state.loop = int(value) + elif key == 'rewind': + state.rewind = int(value) + elif key == 'movement': + state.movement = int(value) + elif key == 'delay': + state.delay = value.split(',') + elif key == 'hotspot': + state.hotspot = value + else: + logging.critical('Unknown key ' + key + ' (value=' + value + ')!') + sys.exit() + + self.states[state.name] = state + for _ in range(state.numIcons()): + self.states[state.name].icons += [self.loadIconAt(x, y)] + x += 1 + if(x >= self.max_x): + x = 0 + y += 1 + + def extractAllStates(self, dest, flags=0): + for _, state in self.states.iteritems(): + # state = State() + for i in xrange(len(state.positions)): + x, y = state.positions[i] + self.extractIconAt(state, dest, x, y, i) + + if (flags & DMILoadFlags.NoPostProcessing) == 0: + self.states[state.name].postProcess() + if dest is not None: + outfolder = os.path.join(dest, os.path.basename(self.filename)) + nfn = self.filename.replace('.dmi', '.dmih') + valid_chars = "-_.()[] %s%s" % (string.ascii_letters, string.digits) + nfn = ''.join(c for c in nfn if c in valid_chars) + nfn = os.path.join(outfolder, nfn) + with open(nfn, 'w') as dmih: + dmih.write(self.getDMIH()) + + def loadIconAt(self, sx, sy): + if(self.icon_width == 0 or self.icon_height == 0): + raise SystemError('Image is {}x{}, an invalid size.'.format(self.icon_height, self.icon_width)) + # print(" X (%d,%d)"%(sx*self.icon_width,sy*self.icon_height)) + icon = Image.new(self.img.mode, (self.icon_width, self.icon_height)) + + newpix = icon.load() + for y in range(self.icon_height): + for x in range(self.icon_width): + _x = x + (sx * self.icon_width) + _y = y + (sy * self.icon_height) + try: + pixel = self.pixels[_x, _y] + if pixel[3] == 0: continue + newpix[x, y] = pixel + except IndexError: + print("!!! Received IndexError in %s <%d,%d> = <%d,%d> + (<%d,%d> * <%d,%d>), max=<%d,%d> halting." % (self.filename, _x, _y, x, y, sx, sy, self.icon_width, self.icon_height, self.max_x, self.max_y)) + print('%s: {sz: %s,h: %d, w: %d, m_x: %d, m_y: %d}' % (self.filename, repr(self.img.size), self.icon_height, self.icon_width, self.max_x, self.max_y)) + print('# of cells: %d' % len(self.states)) + print('Image h/w: %s' % repr(self.size)) + print('--STATES:--') + print(self.statelist) + sys.exit(1) + return icon + + def extractIconAt(self, state, dest, sx, sy, i=0): + icon = self.loadIconAt(sx, sy) + outfolder = os.path.join(dest, os.path.basename(self.filename)) + if not os.path.isdir(outfolder): + os.makedirs(outfolder) + nfn = "{}[{}].png".format(state.name, i) + valid_chars = "-_.()[] %s%s" % (string.ascii_letters, string.digits) + nfn = ''.join(c for c in nfn if c in valid_chars) + nfn = os.path.join(outfolder, nfn) + if os.path.isfile(nfn): + os.remove(nfn) + try: + icon.save(nfn) + except SystemError as e: + print("Received SystemError, halting: %s" % traceback.format_exc(e)) + print('{ih=%d,iw=%d,state=%s,dest=%s,sx=%d,sy=%d,i=%d}' % (self.icon_height, self.icon_width, state.ToString(), dest, sx, sy, i)) + sys.exit(1) + return nfn diff --git a/tools/BYONDTools/byond/DMIH/Variable.py b/tools/BYONDTools/byond/DMIH/Variable.py new file mode 100644 index 00000000000..5ccac96ee58 --- /dev/null +++ b/tools/BYONDTools/byond/DMIH/Variable.py @@ -0,0 +1,9 @@ + + +class Variable(object): + name='' + value=None + + def __init__(self,name,value): + self.name=name + self.value=value \ No newline at end of file diff --git a/tools/BYONDTools/byond/DMIH/__init__.py b/tools/BYONDTools/byond/DMIH/__init__.py new file mode 100644 index 00000000000..d70bcdc353b --- /dev/null +++ b/tools/BYONDTools/byond/DMIH/__init__.py @@ -0,0 +1,135 @@ +''' +Created on Feb 23, 2013 + +@author: Rob +''' + +from . import directives, Variable + +valid_symbol_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" + +class DMIH(object): + """ +# DMI Header 1.0 +# ------------------------ + +height = 32 +width = 32 + +state "AMAZIN' RAISINS" { + dirtype=CARDINAL + frames=5 + + import pngs { + direction NORTH { + "amazinraisin-N-0.png" + "amazinraisin-N-1.png" + "amazinraisin-N-2.png" + "amazinraisin-N-3.png" + "amazinraisin-N-4.png" + } + direction EAST { + "amazinraisin-E-0.png" + "amazinraisin-E-1.png" + "amazinraisin-E-2.png" + "amazinraisin-E-3.png" + "amazinraisin-E-4.png" + } + direction WEST { + "amazinraisin-W-0.png" + "amazinraisin-W-1.png" + "amazinraisin-W-2.png" + "amazinraisin-W-3.png" + "amazinraisin-W-4.png" + } + direction SOUTH { + "amazinraisin-S-0.png" + "amazinraisin-S-1.png" + "amazinraisin-S-2.png" + "amazinraisin-S-3.png" + "amazinraisin-S-4.png" + } + } +} + +# File State orig State new +import dmi "import/ohgodwhy.dmi" "Oh god why" "metroid5" + + """ + + tokens = [] + + ''' + directive arg1 {block} + ''' + directives = { + 'direction': directives.Direction.Direction, + 'import': directives.Import.Import, + 'state': directives.State.State + } + + def parse(self, file): + with open(file) as f: + self.tokens = self.parseBlockContents(f) + + def readSymbol(self, f): + f.seek(f.tell() - 1) # Back up one char + o = '' + while True: + c = f.read(1) + if c in valid_symbol_chars: + o += c + else: + return o + + def readString(self, f, b): + # f.seek(f.tell()-1) # Back up one char + o = '' + while True: + c = f.read(1) + if c == b: + return o + else: + o += c + + def parseBlockContents(self, f): + buf = '' + currentBlock = [] + memory = [] + _in = None + memchanged = False + while True: + b = f.read(1) # .decode('utf-8') + if b in ('"', "'"): + memory += [self.readString(f, b)] + memchanged = True + + if b in valid_symbol_chars: + memory += [self.readSymbol(f)] + f.seek(f.tell() - 1) # Back up one char in case we have to deal with a block. + memchanged = True + + if b == '=': + memory += ['='] + memchanged = True + + if b == '{': + memory += [self.parseBlockContents(f)] + memchanged = True + + if b == '}' or b == '': # End of block or end of file. + return currentBlock + + if memchanged: + token = None + if len(memory) == 3: + if memory[2] == '=': + token = Variable(memory[0], memory[1]) + if len(memory) > 0: + if memory[0] in self.directives: + token = self.directives[memory[0]](memory[0], memory[1:]) + if token: + currentBlock += [token] + memory = [] + memchanged = False + diff --git a/tools/BYONDTools/byond/DMIH/directives/Direction.py b/tools/BYONDTools/byond/DMIH/directives/Direction.py new file mode 100644 index 00000000000..796b254f544 --- /dev/null +++ b/tools/BYONDTools/byond/DMIH/directives/Direction.py @@ -0,0 +1,28 @@ +''' +Created on Feb 23, 2013 + +@author: Rob +''' +from .Directive import Directive +from byond import directions + +class Direction(Directive): + ''' + Tells the compiler which frames will be used for this direction. + ''' + + ''' + Which direction? + ''' + dir=0 + + ''' + List of files. + ''' + frames=[] + def __init__(self,name,frames): + Directive.__init__(self, name, [name,frames]) + _dir = directions.getDirFromName(name) + if _dir: + self.dir=_dir + self.frames=frames \ No newline at end of file diff --git a/tools/BYONDTools/byond/DMIH/directives/Directive.py b/tools/BYONDTools/byond/DMIH/directives/Directive.py new file mode 100644 index 00000000000..ca1db7753e7 --- /dev/null +++ b/tools/BYONDTools/byond/DMIH/directives/Directive.py @@ -0,0 +1,14 @@ +''' +Created on Feb 23, 2013 + +@author: Rob +''' + +class Directive(object): + ''' + Base type for directives. + ''' + name = '' + + def __init__(self,name,args): + self.name=name \ No newline at end of file diff --git a/tools/BYONDTools/byond/DMIH/directives/Import.py b/tools/BYONDTools/byond/DMIH/directives/Import.py new file mode 100644 index 00000000000..1e4e37f74af --- /dev/null +++ b/tools/BYONDTools/byond/DMIH/directives/Import.py @@ -0,0 +1,50 @@ +''' +Created on Feb 23, 2013 + +@author: Rob +''' +from .Directive import Directive + +class Import(Directive): + ''' + Tells the compiler to import a file into the DMI. + ''' + + ''' + pngs or dmi. + ''' + ftype='' + + ''' + The file(s) to import into the DMI. + ''' + filedef=None + + ''' + State(s) to import (DMIs) + ''' + states={} + + def __init__(self,name,params): + Directive.__init__(self,name,params) + self.ftype=params[0] + if self.ftype=='pngs': + self.filedef=params[1] + elif self.ftype=='dmi': + self.filedef=str(params[1]) + if type(params[2]) is list: + for sn in params[2]: + self.addState(sn) + elif type(params[2]) is str: + self.addState(params[2]) + + def addState(self,sn): + oldname=None + newname=None + snp=sn.split('->') + if snp.len == 2: + (oldname, newname)=snp + elif snp.len == 1: + oldname=newname=snp[0] + if oldname and newname: + self.states[oldname]=newname \ No newline at end of file diff --git a/tools/BYONDTools/byond/DMIH/directives/State.py b/tools/BYONDTools/byond/DMIH/directives/State.py new file mode 100644 index 00000000000..134c4786c32 --- /dev/null +++ b/tools/BYONDTools/byond/DMIH/directives/State.py @@ -0,0 +1,51 @@ +''' +Created on Feb 23, 2013 + +@author: Rob +''' +from .Directive import Directive +from .Import import Import +from byond.DMI import State +from .. import Variable + +directly_assignable={ + 'hotspot':tuple, + 'dirs':int, + 'frames':int, + 'movement':int, + 'loop':int, + 'rewind':int, + 'delay':tuple +} + +class State(Directive): + ''' + Tells the compiler to create a state with the specified variables and frames. + ''' + + ''' + The actual state + ''' + state = None + imports = [] + def __init__(self,name,params): + Directive.__init__(self,name,params) + self.state=State() + if params.len != 2: + raise Exception('state directive requires 2 parameters. state "name" { }') + self.state.name=params[0] + for o in params[1]: + if type(o) is Variable and o.name in directly_assignable: + if o.name == 'dirs': + if o.value == 'CARDINAL': + o.value = 4 + elif o.value == 'ALL': + o.value = 8 + else: + o.value = 1 + setattr(self.state,o.name,directly_assignable[o.name](o.value)) + if type(o) is Import: + if o.ftype == 'pngs': + for dirblock in o.filedef: + for i in range(dirblock.frames.len): + self.state.setFrame(dirblock.dir, i, dirblock.frames[i]) \ No newline at end of file diff --git a/tools/BYONDTools/byond/DMIH/directives/__init__.py b/tools/BYONDTools/byond/DMIH/directives/__init__.py new file mode 100644 index 00000000000..b7b57d26429 --- /dev/null +++ b/tools/BYONDTools/byond/DMIH/directives/__init__.py @@ -0,0 +1,4 @@ +from . import Direction, Directive, Import, State +__all__ = [ + Direction, Directive, Import, State +] \ No newline at end of file diff --git a/tools/BYONDTools/byond/__init__.py b/tools/BYONDTools/byond/__init__.py new file mode 100644 index 00000000000..b46c624fe03 --- /dev/null +++ b/tools/BYONDTools/byond/__init__.py @@ -0,0 +1,41 @@ +''' +Created on Sep 21, 2013 + +@author: Rob +''' +import os + +from .map import Map, Tile, MapRenderFlags +from .objtree import ObjectTree + +def GetFilesFromDME(dmefile='baystation12.dme', ext='.dm'): + filesInDME=[] + rootdir = os.path.dirname(dmefile) + with open(dmefile, 'r') as dmeh: + for line in dmeh: + if line.startswith('#include'): + inString = False + # escaped=False + filename = '' + for c in line: + """ + if c == '\\' and not escaped: + escaped = True + continue + if escaped: + if + escaped = False + continue + """ + if c == '"': + inString = not inString + if not inString: + filepath = os.path.join(rootdir, filename) + if filepath.endswith(ext): + filesInDME += [filepath] + filename = '' + continue + else: + if inString: + filename += c + return filesInDME diff --git a/tools/BYONDTools/byond/basetypes.py b/tools/BYONDTools/byond/basetypes.py new file mode 100644 index 00000000000..658a9c4840e --- /dev/null +++ b/tools/BYONDTools/byond/basetypes.py @@ -0,0 +1,454 @@ +''' +Created on Nov 6, 2013 + +@author: Rob +''' +# import logging +AREA_LAYER = 1 +TURF_LAYER = 2 +OBJ_LAYER = 3 +MOB_LAYER = 4 +FLY_LAYER = 5 + +import re +from .utils import eval_expr +REGEX_TABS = re.compile('^(?P\t*)') +class BYONDValue: + """ + Handles numbers and unhandled types like lists. + """ + def __init__(self, string, filename='', line=0, typepath='/', **kwargs): + #: The actual value. + self.value = string + + #: Filename this was found in + self.filename = filename + + #: Line of the originating file. + self.line = line + + #: Typepath of the value. + self.type = typepath + + #: Has this value been inherited? + self.inherited = kwargs.get('inherited', False) + + #: Is this a declaration? (/var) + self.declaration = kwargs.get('declaration', False) + + #: Anything special? (global, const, etc.) + self.special = kwargs.get('special', None) + + #: If a list, what's the size? + self.size = kwargs.get('size', None) + + def copy(self): + '''Make a clone of this without dangling references.''' + return BYONDValue(self.value, self.filename, self.line, self.type, declaration=self.declaration, inherited=self.inherited, special=self.special) + + def __str__(self): + return '{0}'.format(self.value) + + def __repr__(self): + return ''.format(self.value, self.filename, self.line) + + def DumpCode(self, name): + ''' + Try to dump valid BYOND code for this variable. + + .. :param name: The name of this variable. + ''' + decl = [] + if self.declaration: + decl += ['var'] + if self.type != '/' and self.declaration: + decl += self.type.split('/')[1:] + decl += [name] + constructed = '/'.join(decl) + if self.value is not None: + constructed += ' = {0}'.format(str(self)) + return constructed + +class BYONDFileRef(BYONDValue): + """ + Just to format file references differently. + """ + def __init__(self, string, filename='', line=0, **kwargs): + BYONDValue.__init__(self, string, filename, line, '/icon', **kwargs) + + def copy(self): + return BYONDFileRef(self.value, self.filename, self.line, declaration=self.declaration, inherited=self.inherited, special=self.special) + + def __str__(self): + return "'{0}'".format(self.value) + + def __repr__(self): + return ''.format(self.value, self.filename, self.line) + +class BYONDString(BYONDValue): + """ + Correctly formats strings. + """ + def __init__(self, string, filename='', line=0, **kwargs): + BYONDValue.__init__(self, string, filename, line, '/', **kwargs) + + def copy(self): + return BYONDString(self.value, self.filename, self.line, declaration=self.declaration, inherited=self.inherited, special=self.special) + + def __str__(self): + return '"{0}"'.format(self.value) + + def __repr__(self): + return ''.format(self.value, self.filename, self.line) + +class PropertyFlags: + '''Collection of flags that affect :func:`Atom.setProperty` behavior.''' + + #: Property being set should be saved to the map + MAP_SPECIFIED = 1 + + #: Property being set should be handled as a string + STRING = 2 + + #: Property being set should be handled as a file reference + FILEREF = 4 + + #: Property being set should be handled as a value + VALUE = 8 + +class Atom: + ''' + An atom is, in simple terms, what BYOND considers a class. + + :param string path: + The absolute path of this atom. ex: */obj/item/weapon/gun* + :param string filename: + The file this atom originated from. + :param int line: + The line in the aforementioned file. + ''' + + #: Prints all inherited properties, not just the ones that are mapSpecified. + FLAG_INHERITED_PROPERTIES = 1 + + #: writeMap2 prints old_ids instead of the actual IID. + FLAG_USE_OLD_ID = 2 + + def __init__(self, path, filename='', line=0, **kwargs): + global TURF_LAYER, AREA_LAYER, OBJ_LAYER, MOB_LAYER + + #: Absolute path of this atom + self.path = path + + #: Vars of this atom, including inherited vars. + self.properties = {} + + #: List of var names that were specified by the map, if atom was loaded from a :class:`byond.map.Map`. + self.mapSpecified = [] + + #: Child atoms and procs. + self.children = {} + + #: The parent of this atom. + self.parent = None + + #: The file this atom originated from. + self.filename = filename + + #: Line from the originating file. + self.line = line + + #: Instance ID (maps only). Used internally, do NOT change. + self.id = None + + #: Instance ID that was read from the map. + self.old_id = None + + #: Used internally. + self.ob_inherited=False + + #: Loaded from map, but missing in the code. (Maps only) + self.missing=kwargs.get('missing',False) + + #if not self.missing and path == '/area/engine/engineering': + # raise Exception('God damnit') + def copy(self): + ''' + Make a copy of this atom, without dangling references. + + :returns byond.basetypes.Atom + ''' + new_node = Atom(self.path,self.filename,self.line,missing=self.missing) + new_node.properties = self.properties.copy() + new_node.mapSpecified = self.mapSpecified + new_node.id = self.id + new_node.old_id = self.old_id + # new_node.parent = self.parent + return new_node + + def getProperty(self, index, default=None): + ''' + Get the value of the specified property. + + :param string index: + The name of the var we want. + :param mixed default: + Default value, if the var cannot be found. + :returns: + The desired value. + ''' + prop = self.properties.get(index, None) + if prop == None: + return default + elif prop == 'null': + return None + return prop.value + + def setProperty(self, index, value, flags=0): + ''' + Set the value of a property. + + In the event the property cannot be found, a new property is added. + + This function will attempt to convert python types to BYOND types. + Hints can be provided in the form of PropertyFlags given to *flags*. + + :param string index: + The name of the var desired. + :param mixed value: + The new value. + :param int flags: + Changes value assignment behavior. + + +------------------------------------+------------------------------------------------+ + | Flag | Effect | + +====================================+================================================+ + | :attr:`PropertyFlag.MAP_SPECIFIED` | Adds the property to *mapSpecified*, if needed.| + +------------------------------------+------------------------------------------------+ + | :attr:`PropertyFlag.STRING` | Forces conversion of value to a BYONDString. | + +------------------------------------+------------------------------------------------+ + | :attr:`PropertyFlag.FILEREF` | Forces conversion of value to a BYONDFileRef. | + +------------------------------------+------------------------------------------------+ + | :attr:`PropertyFlag.VALUE` | Forces conversion of value to a BYONDValue. | + +------------------------------------+------------------------------------------------+ + + :returns: + The desired value. + ''' + if flags & PropertyFlags.MAP_SPECIFIED: + if index not in self.mapSpecified: + self.mapSpecified += [index] + if flags & PropertyFlags.VALUE: + self.properties[index] = BYONDValue(value) + elif isinstance(value, str) or flags & PropertyFlags.STRING: + if flags & PropertyFlags.STRING: + value = str(value) + self.properties[index] = BYONDString(value) + elif flags & PropertyFlags.FILEREF: + if flags & PropertyFlags.FILEREF: + value = str(value) + self.properties[index] = BYONDFileRef(value) + else: + self.properties[index] = BYONDValue(value) + + def InheritProperties(self): + if self.ob_inherited: return + #debugInheritance=self.path in ('/area','/obj','/mob','/atom/movable','/atom') + if self.parent: + if not self.parent.ob_inherited: + self.parent.InheritProperties() + for key in sorted(self.parent.properties.keys()): + value = self.parent.properties[key].copy() + if key not in self.properties: + self.properties[key] = value + self.properties[key].inherited = True + #if debugInheritance:print(' {0}[{2}] -> {1}'.format(self.parent.path,self.path,key)) + #assert 'name' in self.properties + self.ob_inherited=True + for k in self.children.iterkeys(): + self.children[k].InheritProperties() + + def __ne__(self, atom): + return not self.__eq__(atom) + + def __eq__(self, atom): + if atom == None: + return False + if self.mapSpecified != atom.mapSpecified: + return False + if self.path != atom.path: + return False + return self.properties == atom.properties + + def handle_math(self,expr): + if isinstance(expr,str): + return eval_expr(expr) + return expr + + def __lt__(self, other): + if 'layer' not in self.properties or 'layer' not in other.properties: + return False + myLayer = 0 + otherLayer = 0 + try: + myLayer = self.handle_math(self.getProperty('layer',myLayer)) + except ValueError: + print('Failed to parse {0} as float.'.format(self.properties['layer'].value)) + pass + try: + otherLayer = self.handle_math(other.getProperty('layer',otherLayer)) + except ValueError: + print('Failed to parse {0} as float.'.format(other.properties['layer'].value)) + pass + return myLayer > otherLayer + + def __gt__(self, other): + if 'layer' not in self.properties or 'layer' not in other.properties: + return False + myLayer = 0 + otherLayer = 0 + try: + myLayer = self.handle_math(self.getProperty('layer',myLayer)) + except ValueError: + print('Failed to parse {0} as float.'.format(self.properties['layer'].value)) + pass + try: + otherLayer = self.handle_math(other.getProperty('layer',otherLayer)) + except ValueError: + print('Failed to parse {0} as float.'.format(other.properties['layer'].value)) + pass + return myLayer < otherLayer + + def MapSerialize(self, flags=0): + atomContents = [] + # print(repr(self.mapSpecified)) + if (flags & Atom.FLAG_INHERITED_PROPERTIES): + for key, val in self.properties.items(): + atomContents += ['{0} = {1}'.format(key, val)] + else: + for i in range(len(self.mapSpecified)): + key = self.mapSpecified[i] + if key in self.properties: + val = self.properties[key] + atomContents += ['{0} = {1}'.format(key, val)] + if len(atomContents) > 0: + return self.path + '{' + '; '.join(atomContents) + '}' + else: + return self.path + + def dumpPropInfo(self, name): + o = '{0}: '.format(name) + if name not in self.properties: + return o + 'None' + return o + repr(self.properties[name]) + + def _DumpCode(self): + divider = '//' + ((len(self.path) + 2) * '/') + '\n' + o = divider + o += '// ' + self.path + '\n' + o += divider + o += self.path + '\n' + # o += '\t//{0} properties total\n'.format(len(self.properties)) + for name in sorted(self.properties.keys()): + prop = self.properties[name] + if prop.inherited: continue + _type = prop.type + if not _type.endswith('/'): + _type += '/' + prefix = '' + if prop.declaration: + prefix = 'var' + if not prop.declaration: # and _type == '/': + _type = '' + o += '\t{prefix}{type}{name}'.format(prefix=prefix, type=_type, name=name) + if prop.value is not None: + o += ' = {value}'.format(value=str(prop)) + o += '\n' + + # o += '\n' + # o += '\t//{0} children total\n'.format(len(self.children)) + procs = '' + children = '' + for ck in sorted(self.children.keys()): + co = '\n' + co += self.children[ck]._DumpCode() + if isinstance(self.children[ck], Proc): + procs += co + else: + children += co + o += procs + children + return o + + def DumpCode(self): + return self._DumpCode() + +class Proc(Atom): + def __init__(self, path, arguments, filename='', line=0): + Atom.__init__(self, path, filename, line) + self.name = self.figureOutName(self.path) + self.arguments = arguments + self.code = [] # (indent, line) + self.definition = False + self.origpath = '' + + def figureOutName(self,path): + name = path.split('(')[0] + return name.split('/')[-1] + + def CountTabs(self, line): + m = REGEX_TABS.match(line) + if m is not None: + return len(m.group('tabs')) + return 0 + + def AddCode(self, indentLevel, line): + self.code += [(indentLevel, line)] + + def AddBlankLine(self): + if len(self.code) > 0 and self.code[-1][1] == '': + return + self.code += [(0, '')] + + def MapSerialize(self, flags=0): + return None + + def InheritProperties(self): + return + def getMinimumIndent(self): + # Find minimum indent level + for i in range(len(self.code)): + indent, _ = self.code[i] + if indent == 0: continue + return indent + return 0 + + def _DumpCode(self): + args = self.path[self.path.index('('):] + true_path = self.path[:self.path.index('(')].split('/') + name = true_path[-1] + true_path = true_path[:-1] + if self.definition: + true_path += ['proc'] + true_path += [name + args] + o = '\n' + '/'.join(true_path) + '\n' + min_indent = self.getMinimumIndent() + # Should be 1, so find the difference. + indent_delta = 1 - min_indent + # o += '\t// true_path = {0}\n'.format(repr(true_path)) + # o += '\t// name = {0}\n'.format(name) + # o += '\t// args = {0}\n'.format(args) + # o += '\t// definition = {0}\n'.format(self.definition) + # o += '\t// path = {0}\n'.format(self.path[:self.path.index('(')]) + # o += '\t// origpath = {0}\n'.format(self.origpath) + # o += '\t// min_indent = {0}\n'.format(min_indent) + # o += '\t// indent_delta = {0}\n'.format(indent_delta) + for i in range(len(self.code)): + indent, code = self.code[i] + indent = max(1, indent + indent_delta) + if code == '' and i == len(self.code) - 1: + continue + if code.strip() == '': + o += '\n' + else: + o += (indent * '\t') + code.strip() + '\n' + return o diff --git a/tools/BYONDTools/byond/directions.py b/tools/BYONDTools/byond/directions.py new file mode 100644 index 00000000000..8102b5fdbb4 --- /dev/null +++ b/tools/BYONDTools/byond/directions.py @@ -0,0 +1,47 @@ +''' +Created on Feb 23, 2013 + +@author: Rob +''' +import sys +# NORTH, SOUTH, EAST, and WEST are just #define statements built in DM. +# They represent 1, 2, 4, and 8 + +NORTH = 1 +SOUTH = 2 +EAST = 4 +WEST = 8 + +IMAGE_INDICES=[ + SOUTH, + NORTH, + EAST, + WEST, + (SOUTH|EAST), + (SOUTH|WEST), + (NORTH|EAST), + (NORTH|WEST) + ] + +def getDirFromName(name): + return getattr(sys.modules[__name__],name,None) + +def getNameFromDir(_dir): + if _dir == NORTH: + return 'NORTH' + elif _dir == SOUTH: + return 'SOUTH' + elif _dir == EAST: + return 'EAST' + elif _dir == WEST: + return 'WEST' + elif _dir == (NORTH|WEST): + return 'NORTHWEST' + elif _dir == (NORTH|EAST): + return 'NORTHEAST' + elif _dir == (SOUTH|EAST): + return 'SOUTHEAST' + elif _dir == (SOUTH|WEST): + return 'SOUTHWEST' + else: + return 'UNKNOWN (%d)' %_dir \ No newline at end of file diff --git a/tools/BYONDTools/byond/map.py b/tools/BYONDTools/byond/map.py new file mode 100644 index 00000000000..6d0ad497947 --- /dev/null +++ b/tools/BYONDTools/byond/map.py @@ -0,0 +1,1079 @@ +import os, itertools, sys +from byond.DMI import DMI +from byond.directions import SOUTH, IMAGE_INDICES +from byond.basetypes import Atom, BYONDString, BYONDValue, BYONDFileRef +# from byond.objtree import ObjectTree +from PIL import Image, ImageChops +import logging + +ID_ENCODING_TABLE = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + +# Cache +_icons = {} +_dmis = {} + +def chunker(iterable, chunksize): + """ + Return elements from the iterable in `chunksize`-ed lists. The last returned + chunk may be smaller (if length of collection is not divisible by `chunksize`). + + >>> print list(chunker(xrange(10), 3)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] + """ + i = iter(iterable) + while True: + wrapped_chunk = [list(itertools.islice(i, int(chunksize)))] + if not wrapped_chunk[0]: + break + yield wrapped_chunk.pop() + +# From StackOverflow +def trim(im): + bg = Image.new(im.mode, im.size, im.getpixel((0, 0))) + diff = ImageChops.difference(im, bg) + diff = ImageChops.add(diff, diff, 2.0, -100) + bbox = diff.getbbox() + if bbox: + return im.crop(bbox) + +# Bytes +def tint_image(image, tint_color): + return ImageChops.multiply(image, Image.new('RGBA', image.size, tint_color)) + +def BYOND2RGBA(colorstring, alpha=255): + colorstring = colorstring.strip() + if colorstring[0] == '#': colorstring = colorstring[1:] + if len(colorstring) != 6: + raise ValueError("input #%s is not in #RRGGBB format" % colorstring) + r, g, b = colorstring[:2], colorstring[2:4], colorstring[4:] + r, g, b = [int(n, 16) for n in (r, g, b)] + return (r, g, b, alpha) + +class Tile: + FLAG_USE_OLD_ID = 1 + FLAG_INHERITED_PROPERTIES = 2 + + def __init__(self, _map): + self.origID = '' + self.ID = 0 + self.instances = [] + self.frame = None + self.unselected_frame = None + self.areaSelected = True + self.log = logging.getLogger(__name__ + '.Tile') + self.map = _map + + def ID2String(self, pad=0): + o = '' + _id = self.ID + IET_SIZE = len(ID_ENCODING_TABLE) + while(_id >= len(ID_ENCODING_TABLE)): + i = _id % IET_SIZE + o = ID_ENCODING_TABLE[i] + o + _id -= i + _id /= IET_SIZE + o = ID_ENCODING_TABLE[_id] + o + if pad > len(o): + o = o.rjust(pad, ID_ENCODING_TABLE[0]) + return o + + def GetAtoms(self): + atoms = [] + for instance in self.instances: + atom = self.map.getInstance(instance) + if atom is None: continue + atoms += [atom] + return atoms + + def SortAtoms(self): + return sorted(self.GetAtoms(), reverse=True) + + def GetAtom(self, idx): + return self.map.getInstance(self.instances[idx]) + + def GetInstances(self): + return self.instances + + def __str__(self): + return self.MapSerialize(Tile.FLAG_USE_OLD_ID) + + def MapSerialize(self, flags=0, padding=0): + # "aat" = (/obj/structure/grille,/obj/structure/window/reinforced{dir = 8},/obj/structure/window/reinforced{dir = 1},/obj/structure/window/reinforced,/obj/structure/cable{d1 = 2; d2 = 4; icon_state = "2-4"; tag = ""},/turf/simulated/floor/plating,/area/security/prison) + atoms = [] + atomFlags = 0 + if flags & Tile.FLAG_INHERITED_PROPERTIES: + atomFlags |= Atom.FLAG_INHERITED_PROPERTIES + for i in xrange(len(self.instances)): + iid = self.instances[i] + # print('{0} = {1}'.format(repr(i), repr(iid))) + atom = self.map.getInstance(iid) + if atom.path != '': + atoms += [atom.MapSerialize(atomFlags)] + if not (flags & Tile.FLAG_USE_OLD_ID): + return '"{ID}" = ({atoms})'.format(ID=self.ID2String(padding), atoms=','.join(atoms)) + else: + return '"{ID}" = ({atoms})'.format(ID=self.origID, atoms=','.join(atoms)) + + def MapSerialize2(self, flags=0, padding=0): + ''' + DMM2 serialization method. + ''' + # "aat" = (/obj/structure/grille,/obj/structure/window/reinforced{dir = 8},/obj/structure/window/reinforced{dir = 1},/obj/structure/window/reinforced,/obj/structure/cable{d1 = 2; d2 = 4; icon_state = "2-4"; tag = ""},/turf/simulated/floor/plating,/area/security/prison) + atomFlags = 0 + if flags & Tile.FLAG_INHERITED_PROPERTIES: + atomFlags |= Atom.FLAG_INHERITED_PROPERTIES + instancelist = ','.join([str(i) for i in self.GetInstances()]) + if not (flags & Tile.FLAG_USE_OLD_ID): + return '"{ID}" = ({atoms})'.format(ID=self.ID2String(padding), atoms=instancelist) + else: + return '"{ID}" = ({atoms})'.format(ID=self.origID, atoms=instancelist) + + def __eq__(self, other): + if len(self.instances) != len(other.instances): + return False + else: + return all(self.instances[i] == other.instances[i] for i in xrange(len(self.instances))) + + def RenderToMapTile(self, passnum, basedir, renderflags, **kwargs): + img = Image.new('RGBA', (96, 96)) + self.offset = (32, 32) + foundAPixelOffset = False + render_types = kwargs.get('render_types', ()) + skip_alpha = kwargs.get('skip_alpha', False) + # for atom in sorted(self.GetAtoms(), reverse=True): + for atom in self.SortAtoms(): + if len(render_types) > 0: + found = False + for path in render_types: + if atom.path.startswith(path): + found = True + if not found: + continue + + aid = atom.id + # Ignore /areas. They look like ass. + if atom.path.startswith('/area'): + if not (renderflags & MapRenderFlags.RENDER_AREAS): + continue + + # We're going to turn space black for smaller images. + if atom.path == '/turf/space': + if not (renderflags & MapRenderFlags.RENDER_STARS): + continue + + if 'icon' not in atom.properties: + logging.critical('UNKNOWN ICON IN {0} (atom #{1})'.format(self.origID, aid)) + logging.info(atom.MapSerialize()) + logging.info(atom.MapSerialize(Atom.FLAG_INHERITED_PROPERTIES)) + continue + + dmi_file = atom.properties['icon'].value + + if 'icon_state' not in atom.properties: + # Grab default icon_state ('') if we can't find the one defined. + atom.properties['icon_state'] = BYONDString("") + + state = atom.properties['icon_state'].value + + direction = SOUTH + if 'dir' in atom.properties: + try: + direction = int(atom.properties['dir'].value) + except ValueError: + logging.critical('FAILED TO READ dir = ' + repr(atom.properties['dir'].value)) + continue + + icon_key = '{0}|{1}|{2}'.format(dmi_file, state, direction) + frame = None + pixel_x = 0 + pixel_y = 0 + if icon_key in _icons: + frame, pixel_x, pixel_y = _icons[icon_key] + else: + dmi_path = os.path.join(basedir, dmi_file) + dmi = None + if dmi_path in _dmis: + dmi = _dmis[dmi_path] + else: + try: + dmi = DMI(dmi_path) + dmi.loadAll() + _dmis[dmi_path] = dmi + except Exception as e: + print(str(e)) + for prop in ['icon', 'icon_state', 'dir']: + print('\t{0}'.format(atom.dumpPropInfo(prop))) + pass + if dmi.img is None: + logging.warning('Unable to open {0}!'.format(dmi_path)) + continue + + if dmi.img.mode not in ('RGBA', 'P'): + logging.warn('{} is mode {}!'.format(dmi_file, dmi.img.mode)) + + if direction not in IMAGE_INDICES: + logging.warn('Unrecognized direction {} on atom {} in tile {}!'.format(direction, atom.MapSerialize(), self.origID)) + direction = SOUTH # DreamMaker property editor shows dir = 2. WTF? + + frame = dmi.getFrame(state, direction, 0) + if frame == None: + # Get the error/default state. + frame = dmi.getFrame("", direction, 0) + + if frame == None: + continue + + if frame.mode != 'RGBA': + frame = frame.convert("RGBA") + + pixel_x = 0 + if 'pixel_x' in atom.properties: + pixel_x = int(atom.properties['pixel_x'].value) + + pixel_y = 0 + if 'pixel_y' in atom.properties: + pixel_y = int(atom.properties['pixel_y'].value) + + _icons[icon_key] = (frame, pixel_x, pixel_y) + + # Handle BYOND alpha and coloring + c_frame = frame + alpha = int(atom.getProperty('alpha', 255)) + if skip_alpha: + alpha = 255 + color = atom.getProperty('color', '#FFFFFF') + if alpha != 255 or color != '#FFFFFF': + c_frame = tint_image(frame, BYOND2RGBA(color, alpha)) + img.paste(c_frame, (32 + pixel_x, 32 - pixel_y), c_frame) # Add to the top of the stack. + if pixel_x != 0 or pixel_y != 0: + if passnum == 0: return # Wait for next pass + foundAPixelOffset = True + + if passnum == 1 and not foundAPixelOffset: + return None + if not self.areaSelected: + # Fade out unselected tiles. + bands = list(img.split()) + # Excluding alpha band + for i in range(3): + bands[i] = bands[i].point(lambda x: x * 0.4) + img = Image.merge(img.mode, bands) + + return img + +class MapLayer: + def __init__(self, _map, height=255, width=255): + self.map = _map + self.min = (1, 1) + self.max = (height, width) + self.height = height + self.width = width + self.tiles = [[0 for _ in xrange(self.width)] for _ in xrange(self.height)] + + def SetTileAt(self, x, y, tile): + grow = False + if y >= self.height: + self.height = y + 1 + grow = True + if x >= self.width: + self.width = x + 1 + grow = True + if grow: + self.grow() + try: + self.tiles[y][x] = tile.ID + except IndexError: + logging.critical('Failed to set self.tiles[{}][{}]: IndexError'.format(y, x)) + logging.critical('width: {}'.format(self.width)) + logging.critical('height: {}'.format(self.height)) + logging.critical('real width: {}'.format(len(self.tiles))) + logging.critical('real height: {}'.format(len(self.tiles[0]))) + sys.exit(1) + + def grow(self): + gamt = self.height - len(self.tiles) + if gamt > 0: + logging.debug('y += ' + str(gamt)) + self.tiles += [[0] for _ in xrange(gamt)] + gamt = 0 + for y in range(len(self.tiles)): + gamt = self.width - len(self.tiles[y]) + if gamt > 0: + self.tiles[y] += [0 for _ in xrange(gamt)] + logging.debug('x[{}] += {}'.format(y, gamt)) + + def GetTileAt(self, x, y): + # print(repr(self.tiles)) + return self.map.tileTypes[self.tiles[y][x]] + +class MapRenderFlags: + RENDER_STARS = 1 + RENDER_AREAS = 2 + +class Map: + WRITE_OLD_IDS = 1 + + READ_NO_BASE_COMP = 1 + def __init__(self, tree=None, **kwargs): + self.filename = 'Unknown' + self.readFlags = 0 + self.tileTypes = [] + self.instances = [] + self.zLevels = {} + self.oldID2NewID = {} + self.DMIs = {} + self.width = 0 + self.height = 0 + self.idlen = 0 + self.tree = tree + self.generatedTexAtlas = False + self.selectedAreas = () + self.whitelistTypes = None + self.forgiving_atom_lookups = kwargs.get('forgiving_atom_lookups', False) + self.missing_atoms = set() + + self.log = logging.getLogger(__name__ + '.Map') + + self.duplicates = 0 + self.tileChunk2ID = {} + + self.atomBorders = { + '{':'}', + '"':'"', + '(':')' + } + self.atomCache = {} + nit = self.atomBorders.copy() + for start, stop in self.atomBorders.items(): + if start != stop: + nit[stop] = None + self.atomBorders = nit + + def readMap(self, filename, flags=0): + self.readFlags = flags + if not os.path.isfile(filename): + logging.warn('File ' + filename + " does not exist.") + self.filename = filename + with open(filename, 'r') as f: + print('--- Reading tile types from {0}...'.format(self.filename)) + self.consumeTiles(f) + print('--- Reading tile positions...') + self.consumeTileMap(f) + + + + def writeMap(self, filename, flags=0): + self.filename = filename + tileFlags = 0 + if flags & Map.WRITE_OLD_IDS: + tileFlags |= Tile.FLAG_USE_OLD_ID + padding = len(self.tileTypes[-1].ID2String()) + with open(filename, 'w') as f: + for tile in self.tileTypes: + f.write('{0}\n'.format(tile.MapSerialize(tileFlags, padding))) + for z in self.zLevels.keys(): + f.write('\n(1,1,{0}) = {{"\n'.format(z)) + zlevel = self.zLevels[z] + for y in xrange(zlevel.height): + for x in xrange(zlevel.width): + tile = zlevel.GetTileAt(x, y) + if flags & Map.WRITE_OLD_IDS: + f.write(tile.origID) + else: + f.write(tile.ID2String(padding)) + f.write("\n") + f.write('"}\n') + + def writeMap2(self, filename, flags=0): + self.filename = filename + tileFlags = 0 + atomFlags = 0 + if flags & Map.WRITE_OLD_IDS: + tileFlags |= Tile.FLAG_USE_OLD_ID + atomFlags |= Atom.FLAG_USE_OLD_ID + padding = len(self.tileTypes[-1].ID2String()) + with open(filename, 'w') as f: + f.write('// Atom Instances\n') + for atom in self.instances: + f.write('{0} = {1}\n'.format(atom.id, atom.MapSerialize(atomFlags))) + f.write('// Tiles\n') + for tile in self.tileTypes: + f.write('{0}\n'.format(tile.MapSerialize2(tileFlags, padding))) + f.write('// Layout\n') + for z in self.zLevels.keys(): + f.write('\n(1,1,{0}) = {{"\n'.format(z)) + zlevel = self.zLevels[z] + for y in xrange(zlevel.height): + for x in xrange(zlevel.width): + tile = zlevel.GetTileAt(x, y) + if flags & Map.WRITE_OLD_IDS: + f.write(tile.origID) + else: + f.write(tile.ID2String(padding)) + f.write("\n") + f.write('"}\n') + + + def GetTileAt(self, x, y, z): + if z < len(self.zLevels): + return self.zLevels[z].GetTileAt(x, y) + + def consumeTileMap(self, f): + zLevel = [] + y = 0 + z = 0 + inZLevel = False + width = 0 + height = 0 + while True: + line = f.readline() + if line == '': + return + # (1,1,1) = {" + if line.startswith('('): + coordChunk = line[1:line.index(')')].split(',') + # print(repr(coordChunk)) + z = int(coordChunk[2]) + zLevel = MapLayer(self, 255, 255) + inZLevel = True + y = 0 + width = 0 + height = 0 + continue + if line.strip() == '"}': + inZLevel = False + if height == 0: + height = y + newZ = MapLayer(self, height, width) + self.zLevels[z] = newZ + for ny in range(height): + for nx in range(width): + newZ.SetTileAt(nx, ny, zLevel.GetTileAt(nx, ny)) + self.log.info('Added map layer {0} ({1}x{2})'.format(z, height, width)) + # print(' * Added map layer {0} ({1}x{2})'.format(z, height, width)) + continue + if inZLevel: + if width == 0: + width = len(line) / self.idlen + if width > 255: + logging.warn("Warning: Line is {} blocks long!".format(width)) + x = 0 + for chunk in chunker(line.strip(), self.idlen): + chunk = ''.join(chunk) + tid = self.oldID2NewID[chunk] + zLevel.SetTileAt(x, y, self.tileTypes[tid]) + x += 1 + y += 1 + + def generateTexAtlas(self, basedir, renderflags=0): + if self.generatedTexAtlas: + return + print('--- Generating texture atlas...') + self._icons = {} + self._dmis = {} + self.generatedTexAtlas = True + for tid in xrange(len(self.tileTypes)): + tile = self.tileTypes[tid] + img = Image.new('RGBA', (96, 96)) + tile.offset = (32, 32) + tile.areaSelected = True + tile.render_deferred = False + for atom in sorted(tile.GetAtoms(), reverse=True): + + aid = atom.id + # Ignore /areas. They look like ass. + if atom.path.startswith('/area'): + if not (renderflags & MapRenderFlags.RENDER_AREAS): + continue + + # We're going to turn space black for smaller images. + if atom.path == '/turf/space': + if not (renderflags & MapRenderFlags.RENDER_STARS): + continue + + if 'icon' not in atom.properties: + print('CRITICAL: UNKNOWN ICON IN {0} (atom #{1})'.format(tile.origID, aid)) + print(atom.MapSerialize()) + print(atom.MapSerialize(Atom.FLAG_INHERITED_PROPERTIES)) + continue + + dmi_file = atom.properties['icon'].value + + if 'icon_state' not in atom.properties: + # Grab default icon_state ('') if we can't find the one defined. + atom.properties['icon_state'] = BYONDString("") + + state = atom.properties['icon_state'].value + + direction = SOUTH + if 'dir' in atom.properties: + try: + direction = int(atom.properties['dir'].value) + except ValueError: + print('FAILED TO READ dir = ' + repr(atom.properties['dir'].value)) + continue + + icon_key = '{0}:{1}[{2}]'.format(dmi_file, state, direction) + frame = None + pixel_x = 0 + pixel_y = 0 + if icon_key in self._icons: + frame, pixel_x, pixel_y = self._icons[icon_key] + else: + dmi_path = os.path.join(basedir, dmi_file) + dmi = None + if dmi_path in self._dmis: + dmi = self._dmis[dmi_path] + else: + try: + dmi = self.loadDMI(dmi_path) + self._dmis[dmi_path] = dmi + except Exception as e: + print(str(e)) + for prop in ['icon', 'icon_state', 'dir']: + print('\t{0}'.format(atom.dumpPropInfo(prop))) + pass + + if dmi.img is None: + self.log.warn('Unable to open {0}!'.format(dmi_path)) + continue + + if dmi.img.mode not in ('RGBA', 'P'): + self.log.warn('{} is mode {}!'.format(dmi_file, dmi.img.mode)) + + if direction not in IMAGE_INDICES: + self.log.warn('Unrecognized direction {} on atom {} in tile {}!'.format(direction, atom.MapSerialize(), tile.origID)) + direction = SOUTH # DreamMaker property editor shows dir = 2. WTF? + + frame = dmi.getFrame(state, direction, 0) + if frame == None: + # Get the error/default state. + frame = dmi.getFrame("", direction, 0) + + if frame == None: + continue + + if frame.mode != 'RGBA': + frame = frame.convert("RGBA") + + pixel_x = 0 + if 'pixel_x' in atom.properties: + pixel_x = int(atom.properties['pixel_x'].value) + + pixel_y = 0 + if 'pixel_y' in atom.properties: + pixel_y = int(atom.properties['pixel_y'].value) + + self._icons[icon_key] = (frame, pixel_x, pixel_y) + img.paste(frame, (32 + pixel_x, 32 - pixel_y), frame) # Add to the top of the stack. + if pixel_x != 0 or pixel_y != 0: + tile.render_deferred = True + tile.frame = img + + # Fade out unselected tiles. + bands = list(img.split()) + # Excluding alpha band + for i in range(3): + bands[i] = bands[i].point(lambda x: x * 0.4) + tile.unselected_frame = Image.merge(img.mode, bands) + + self.tileTypes[tid] = tile + + def renderAtom(self, atom, basedir, skip_alpha=False): + if 'icon' not in atom.properties: + logging.critical('UNKNOWN ICON IN ATOM #{0} ({1})'.format(atom.id, atom.path)) + logging.info(atom.MapSerialize()) + logging.info(atom.MapSerialize(Atom.FLAG_INHERITED_PROPERTIES)) + return None + # else: + # logging.info('Icon found for #{}.'.format(atom.id)) + + dmi_file = atom.properties['icon'].value + + # Grab default icon_state ('') if we can't find the one defined. + state = atom.getProperty('icon_state', '') + + direction = SOUTH + if 'dir' in atom.properties: + try: + direction = int(atom.properties['dir'].value) + except ValueError: + logging.critical('FAILED TO READ dir = ' + repr(atom.properties['dir'].value)) + return None + + icon_key = '{0}|{1}|{2}'.format(dmi_file, state, direction) + frame = None + pixel_x = 0 + pixel_y = 0 + if icon_key in _icons: + frame, pixel_x, pixel_y = _icons[icon_key] + else: + dmi_path = os.path.join(basedir, dmi_file) + dmi = None + if dmi_path in _dmis: + dmi = _dmis[dmi_path] + else: + try: + dmi = DMI(dmi_path) + dmi.loadAll() + _dmis[dmi_path] = dmi + except Exception as e: + print(str(e)) + for prop in ['icon', 'icon_state', 'dir']: + print('\t{0}'.format(atom.dumpPropInfo(prop))) + pass + if dmi.img is None: + logging.warning('Unable to open {0}!'.format(dmi_path)) + return None + + if dmi.img.mode not in ('RGBA', 'P'): + logging.warn('{} is mode {}!'.format(dmi_file, dmi.img.mode)) + + if direction not in IMAGE_INDICES: + logging.warn('Unrecognized direction {} on atom {}!'.format(direction, atom.MapSerialize())) + direction = SOUTH # DreamMaker property editor shows dir = 2. WTF? + + frame = dmi.getFrame(state, direction, 0) + if frame == None: + # Get the error/default state. + frame = dmi.getFrame("", direction, 0) + + if frame == None: + return None + + if frame.mode != 'RGBA': + frame = frame.convert("RGBA") + + pixel_x = 0 + if 'pixel_x' in atom.properties: + pixel_x = int(atom.properties['pixel_x'].value) + + pixel_y = 0 + if 'pixel_y' in atom.properties: + pixel_y = int(atom.properties['pixel_y'].value) + + _icons[icon_key] = (frame, pixel_x, pixel_y) + + # Handle BYOND alpha and coloring + c_frame = frame + alpha = int(atom.getProperty('alpha', 255)) + if skip_alpha: + alpha = 255 + color = atom.getProperty('color', '#FFFFFF') + if alpha != 255 or color != '#FFFFFF': + c_frame = tint_image(frame, BYOND2RGBA(color, alpha)) + return c_frame + + def generateImage(self, filename_tpl, basedir='.', renderflags=0, z=None, **kwargs): + ''' + Instead of generating on a tile-by-tile basis, this creates a large canvas and places + each atom on it after sorting layers. This resolves the pixel_(x,y) problem. + ''' + if z is None: + for z in self.zLevels.keys(): + self.generateImage(filename_tpl, basedir, renderflags, z, **kwargs) + return + self.selectedAreas = () + skip_alpha = False + render_types = () + if 'area' in kwargs: + self.selectedAreas = kwargs['area'] + if 'render_types' in kwargs: + render_types = kwargs['render_types'] + if 'skip_alpha' in kwargs: + skip_alpha = kwargs['skip_alpha'] + + print('Checking z-level {0}...'.format(z)) + instancePositions = {} + for y in range(self.zLevels[z].height): + for x in range(self.zLevels[z].width): + t = self.zLevels[z].GetTileAt(x, y) + # print('*** {},{}'.format(x,y)) + if t is None: + continue + if len(self.selectedAreas) > 0: + renderThis = True + for atom in t.GetAtoms(): + if atom.path.startswith('/area'): + if atom.path not in self.selectedAreas: + renderThis = False + if not renderThis: continue + for atom in t.GetAtoms(): + if atom is None: continue + iid = atom.id + if atom.path.startswith('/area'): + if atom.path not in self.selectedAreas: + continue + + # Check for render restrictions + if len(render_types) > 0: + found = False + for path in render_types: + if atom.path.startswith(path): + found = True + if not found: + continue + + # Ignore /areas. They look like ass. + if atom.path.startswith('/area'): + if not (renderflags & MapRenderFlags.RENDER_AREAS): + continue + + # We're going to turn space black for smaller images. + if atom.path == '/turf/space': + if not (renderflags & MapRenderFlags.RENDER_STARS): + continue + + if iid not in instancePositions: + instancePositions[iid] = [] + + # pixel offsets + ''' + pixel_x = int(atom.getProperty('pixel_x', 0)) + pixel_y = int(atom.getProperty('pixel_y', 0)) + t_o_x = int(round(pixel_x / 32)) + t_o_y = int(round(pixel_y / 32)) + pos = (x + t_o_x, y + t_o_y) + ''' + pos = (x, y) + + instancePositions[iid].append(pos) + + if len(instancePositions) == 0: + return + + levelAtoms = [] + for iid in instancePositions: + levelAtoms += [self.getInstance(iid)] + + pic = Image.new('RGBA', ((self.zLevels[z].width + 2) * 32, (self.zLevels[z].height + 2) * 32), "black") + + # Bounding box, used for cropping. + bbox = [99999, 99999, 0, 0] + + # Replace {z} with current z-level. + filename = filename_tpl.replace('{z}', str(z)) + + pastes = 0 + for atom in sorted(levelAtoms, reverse=True): + if atom.id not in instancePositions: + continue + icon = self.renderAtom(atom, basedir, skip_alpha) + if icon is None: + continue + for x, y in instancePositions[atom.id]: + new_bb = self.getBBoxForAtom(x, y, atom, icon) + # print('{0},{1} = {2}'.format(x, y, new_bb)) + # Adjust cropping bounds + if new_bb[0] < bbox[0]: + bbox[0] = new_bb[0] + if new_bb[1] < bbox[1]: + bbox[1] = new_bb[1] + if new_bb[2] > bbox[2]: + bbox[2] = new_bb[2] + if new_bb[3] > bbox[3]: + bbox[3] = new_bb[3] + pic.paste(icon, new_bb, icon) + pastes += 1 + + if len(self.selectedAreas) == 0: + # Autocrop (only works if NOT rendering stars or areas) + pic = trim(pic) + else: + # if nSelAreas == 0: + # continue + pic = pic.crop(bbox) + + if pic is not None: + # Saev + filedir = os.path.dirname(filename) + if not os.path.isdir(filedir): + os.makedirs(filedir) + print(' -> {} ({}x{}) - {} objects'.format(filename, pic.size[0], pic.size[1], pastes)) + pic.save(filename, 'PNG') + + def getBBoxForAtom(self, x, y, atom, icon): + icon_width, icon_height = icon.size + pixel_x = int(atom.getProperty('pixel_x', 0)) + pixel_y = int(atom.getProperty('pixel_y', 0)) + + return self.tilePosToBBox(x, y, pixel_x, pixel_y, icon_height, icon_width) + + def tilePosToBBox(self, tile_x, tile_y, pixel_x, pixel_y, icon_height, icon_width): + # Tile Pos + X = tile_x * 32 + Y = tile_y * 32 + + # pixel offsets + X += pixel_x + Y -= pixel_y + + # BYOND coordinates -> PIL coords. + # BYOND uses LOWER left. + # PIL uses UPPER left + X += 0 + Y += 32 - icon_height + + return ( + X, + Y, + X + icon_width, + Y + icon_height + ) + + def consumeTiles(self, f): + index = 0 + self.duplicates = 0 + lineNumber = 0 + self.tileChunk2ID = {} + while True: + line = f.readline() + lineNumber += 1 + if line.startswith('"'): + t = self.consumeTile(line, lineNumber) + t.ID = index + self.tileTypes += [t] + self.idlen = max(self.idlen, len(t.ID2String())) + self.oldID2NewID[t.origID] = t.ID + index += 1 + # No longer needed, 2fast. + # if((index % 100) == 0): + # print(index) + else: + self.log.info('-- {} tiles loaded, {} duplicates discarded'.format(index, self.duplicates)) + return + + def consumeTile(self, line, lineNumber): + t = Tile(self) + t.origID = self.consumeTileID(line) + tileChunk = line.strip()[line.index('(') + 1:-1] + + if tileChunk in self.tileChunk2ID: + tid = self.tileChunk2ID[tileChunk] + self.log.warn('{} duplicate of {}! Installing redirect...'.format(t.origID, tid)) + self.oldID2NewID[t.origID] = tid + self.duplicates += 1 + return self.tileTypes[tid] + + t.instances = self.consumeTileAtoms(tileChunk, lineNumber) + return t + def getTileTypeID(self, t): + for tile in self.tileTypes: + if tile == t: + return tile.ID + return None + + def consumeTileID(self, line): + e = line.index('"', 1) + return line[1:e] + + def GetInstances(self): + return self.instances + + # So we can read a map without parsing the tree. + def GetAtom(self, path): + if self.tree is not None: + atom = self.tree.GetAtom(path) + if atom is None and self.forgiving_atom_lookups: + self.missing_atoms.add(path) + return Atom(path, self.filename, missing=True) + return atom + return Atom(path) + + def consumeTileAtoms(self, line, lineNumber): + instances = [] + atom_chunks = self.SplitAtoms(line) + + for atom_chunk in atom_chunks: + if atom_chunk in self.atomCache: + instances += [self.atomCache[atom_chunk]] + else: + atom_id = self.consumeAtom(atom_chunk, lineNumber) + self.atomCache[atom_chunk] = atom_id + instances += [atom_id] + + return instances + + def SplitProperties(self, string): + o = [] + buf = [] + inString = False + for chunk in string.split(';'): + if not inString: + if '"' in chunk: + inString = False + pos = 0 + while(True): + pos = chunk.find('"', pos) + if pos == -1: + break + pc = '' + if pos > 0: + pc = chunk[pos - 1] + if pc != '\\': + inString = not inString + pos += 1 + if not inString: + o += [chunk] + else: + buf += [chunk] + else: + o += [chunk] + else: + if '"' in chunk: + o += [';'.join(buf + [chunk])] + inString = False + buf = [] + else: + buf += [chunk] + return o + + def SplitAtoms(self, string): + ignoreLevel = [] + + o = [] + buf = '' + + string = string.rstrip() + line_len = len(string) + for i in xrange(line_len): + c = string[i] + pc = '' + if i > 0: + pc = string[i - 1] + + if c in self.atomBorders and pc != '\\': + end = self.atomBorders[c] + if end == c: # Just toggle. + if len(ignoreLevel) > 0: + if ignoreLevel[-1] == c: + ignoreLevel.pop() + else: + ignoreLevel.append(c) + else: + if end == None: + if len(ignoreLevel) > 0: + if ignoreLevel[-1] == c: + ignoreLevel.pop() + else: + ignoreLevel.append(end) + if c == ',' and len(ignoreLevel) == 0: + o += [buf] + buf = '' + else: + buf += c + + if len(ignoreLevel) > 0: + print(repr(ignoreLevel)) + sys.exit() + return o + [buf] + + def consumeAtom(self, line, lineNumber): + if '{' not in line: + atom = line.strip() + if atom.endswith('/'): + self.log.warn('{file}:{line}: Malformed atom: {data} has ending slash. Stripping slashes from right side.'.format(file=self.filename, line=lineNumber, data=atom)) + atom = atom.rstrip('/') + currentAtom = self.GetAtom(atom) + if currentAtom is not None: + return self.AssignIID(currentAtom) + else: + self.log.critical('{file}:{line}: Failed to consumeAtom({data}): Unable to locate atom.'.format(file=self.filename, line=lineNumber, data=line)) + return None + chunks = line.split('{') + if len(chunks) < 2: + self.log.error('{file}:{line}: Something went wrong in consumeAtom(). line={data}'.format(file=self.filename, line=lineNumber, data=line)) + atom = chunks[0].strip() + if atom.endswith('/'): + self.log.warn('{file}:{line}: Malformed atom: {data} has ending slash. Stripping slashes from right side.'.format(file=self.filename, line=lineNumber, data=atom)) + atom = atom.rstrip('/') + currentAtom = self.GetAtom(atom) + if currentAtom is not None: + currentAtom = currentAtom.copy() + else: + return None + if chunks[1].endswith('}'): + chunks[1] = chunks[1][:-1] + property_chunks = self.SplitProperties(chunks[1]) + mapSupplied = [] + for chunk in property_chunks: + if chunk.endswith('}'): + chunk = chunk[:-1] + pparts = chunk.split('=', 1) + key = pparts[0].strip() + value = pparts[1].strip() + if key == '': + self.log.warn('Ignoring property with blank name. (given {0})'.format(chunk)) + continue + data = self.consumeDataValue(value, lineNumber) + if key not in currentAtom.mapSpecified: + mapSupplied += [key] + currentAtom.properties[key] = data + + currentAtom.mapSpecified = mapSupplied + + # Compare to base + if not (self.readFlags & Map.READ_NO_BASE_COMP): + base_atom = self.GetAtom(currentAtom.path) + assert base_atom != None + # for key in base_atom.properties.keys(): + # val = base_atom.properties[key] + # if key not in currentAtom.properties: + # currentAtom.properties[key] = val + for key in currentAtom.properties.iterkeys(): + val = currentAtom.properties[key].value + if key in base_atom.properties and val == base_atom.properties[key].value: + if key in currentAtom.mapSpecified: + currentAtom.mapSpecified.remove(key) + # print('Removed {0} from atom: Equivalent to base atom!') + + return self.AssignIID(currentAtom) + + def AssignIID(self, atom, replacing=None): + if atom not in self.instances: + if replacing is not None: + atom.id = replacing + self.instances[replacing] = atom + return replacing + else: + atom.id = len(self.instances) + self.instances.append(atom) + return atom.id + else: + r_iid = self.instances.index(atom) + if replacing is not None: + self.ReplaceIIDs(replacing, r_iid) + return r_iid + + def consumeDataValue(self, value, lineNumber): + data = None + if value[0] in ('"', "'"): + quote = value[0] + if quote == '"': + data = BYONDString(value[1:-1], self.filename, lineNumber) + elif quote == "'": + data = BYONDFileRef(value[1:-1], self.filename, lineNumber) + else: + data = BYONDValue(value, self.filename, lineNumber) + return data + + def getInstance(self, iid): + if iid is None: + return None + return self.instances[iid] + + def setInstance(self, iid, atom): + if iid is None: + return None + return self.AssignIID(atom, iid) + + def ReplaceIIDs(self, old_iid, new_iid): + updated = 0 + for tid in xrange(len(self.tileTypes)): + for i in xrange(len(self.tileTypes[tid].instances)): + if old_iid == self.tileTypes[tid].instances[i]: + self.tileTypes[tid].instances[i] = new_iid + updated += 1 + if updated > 0: + self.log.info('Updated {0} tiles (#{1} -> #{2})'.format(updated, old_iid, new_iid)) + diff --git a/tools/BYONDTools/byond/mapfixes/__init__.py b/tools/BYONDTools/byond/mapfixes/__init__.py new file mode 100644 index 00000000000..d2e1db498b7 --- /dev/null +++ b/tools/BYONDTools/byond/mapfixes/__init__.py @@ -0,0 +1,35 @@ +import logging, glob, os, sys +from .base import MapFix, GetDependencies + +def Load(): + print('Loading MapFix Modules...') + for f in glob.glob(os.path.dirname(__file__) + "/*.py"): + modName = 'mapfixes.' + os.path.basename(f)[:-3] + print(' Loading module ' + modName) + mod = __import__(modName) + for attr in dir(mod): + if not attr.startswith('_'): + #print(' {} = {}'.format(attr,getattr(mod, attr))) + globals()[attr] = getattr(mod, attr) + +def GetFixesForNS(namespaces, load_dependencies=True): + selected = [None] + namespaces + depends = GetDependencies() + if load_dependencies: + changed = True + while changed: + changed = False + for cat in selected: + if cat is None: continue # Global namespace is always needed. + print('Checking dependencies for {}...'.format(cat)) + if cat in depends: + for newcat in depends[cat]: + if newcat not in selected: + print('Selected dependency {} (required by {})'.format(newcat,cat)) + selected += [newcat] + changed = True + o = [] + for cat in selected: + for _, val in MapFix.all[cat].items(): + o += [val()] + return o diff --git a/tools/BYONDTools/byond/mapfixes/base.py b/tools/BYONDTools/byond/mapfixes/base.py new file mode 100644 index 00000000000..66bedf75fa6 --- /dev/null +++ b/tools/BYONDTools/byond/mapfixes/base.py @@ -0,0 +1,150 @@ +''' +Global BYOND fixes and matchers + +@author: Rob +''' +import logging +from byond.basetypes import BYONDString, BYONDValue +# from byond.directions import * + +# Decorator +class MapFix(object): + all = {} + def __init__(self, category, _id=None): + self.id = _id + self.category = category + def __call__(self, c): + if self.id is None: + fname_p = c.__name__ + self.id = fname_p + print('Adding MapFix {0}-{1}.'.format(self.category, self.id)) + if self.category not in MapFix.all: + MapFix.all[self.category] = {} + MapFix.all[self.category][self.id] = c + return c + +_dependencies = {} + +def DeclareDependencies(dependee, dependencies): + if dependee not in _dependencies: + _dependencies[dependee] = [] + _dependencies[dependee] += dependencies + print(' Dependencies: {}'.format(dependencies)) + +def GetDependencies(): + return _dependencies + +class Matcher: + def Matches(self, atom): + return False + + def Fix(self, atom): + return atom + + def SetTree(self, tree): + self.tree = tree + +@MapFix(None) +class NukeTags(Matcher): + def __init__(self): + pass + + def Matches(self, atom): + return 'tag' in atom.properties and 'tag' in atom.mapSpecified + + def Fix(self, atom): + atom.mapSpecified.remove('tag') + return atom + + def __str__(self): + return 'Removed tag' + +class RenameProperty(Matcher): + ''' + Generic property renamer. + ''' + + def __init__(self, old, new): + self.old = old + self.new = new + self.removed = False + + def Matches(self, atom): + if self.old in atom.properties and self.old in atom.mapSpecified: + return True + return False + + def Fix(self, atom): + if self.new not in atom.properties: # Defer to the correct one if both exist. + atom.properties[self.new] = atom.properties[self.old] + if self.old in atom.mapSpecified: + if self.new not in atom.mapSpecified: + atom.mapSpecified += [self.new] + else: + self.removed = True + atom.mapSpecified.remove(self.old) + del atom.properties[self.old] + return atom + + def __str__(self): + if self.removed: + return 'Removed {0}'.format(self.old) + else: + return 'Renamed {0} to {1}'.format(self.old, self.new) + +class ChangeType(Matcher): + def __init__(self, old, new, forcetype=False, fuzzy=False): + self.old = old + self.new = new + self.forcetype = forcetype + self.fuzzy = fuzzy + + def Matches(self, atom): + matches = False + if self.fuzzy: + matches = atom.path.startswith(self.old) + if matches: + self.new += atom.path[len(self.old):] + self.old = atom.path + #print('{} -> {}'.format(self.old,self.new)) + else: + matches = self.old == atom.path + if matches: + if atom.missing: + return True + else: + logging.warn('[{}] Found type, but marked not missing: {}'.format(self.__class__.__name__,atom.path)) + logging.warn('{}:{}: Target type found here'.format(atom.filename, atom.line)) + return False + + def Fix(self, atom): + atom.path = self.new + return atom + + def __str__(self): + return 'Change type from {0} to {1}'.format(self.old, self.new) + +@MapFix(None) +class FixStepX(RenameProperty): + def __init__(self): + RenameProperty.__init__(self, 'step_x', 'pixel_x') + +@MapFix(None) +class FixStepY(RenameProperty): + def __init__(self): + RenameProperty.__init__(self, 'step_y', 'pixel_y'), + +@MapFix(None) +class RepairDirections(Matcher): + def __init__(self): + pass + + def Matches(self, atom): + return 'dir' in atom.properties and 'dir' in atom.mapSpecified and isinstance(atom.properties['dir'], BYONDString) + + def Fix(self, atom): + atom.setProperty('dir', int(atom.getProperty('dir'))) + return atom + + def __str__(self): + return 'Repaired direction type' diff --git a/tools/BYONDTools/byond/mapfixes/ss13.py b/tools/BYONDTools/byond/mapfixes/ss13.py new file mode 100644 index 00000000000..ba4c0750835 --- /dev/null +++ b/tools/BYONDTools/byond/mapfixes/ss13.py @@ -0,0 +1,66 @@ + +from .base import Matcher,MapFix +from byond.basetypes import BYONDString, BYONDValue, Atom, PropertyFlags +from byond.directions import * + +@MapFix('ss13') +class StandardizeAPCs(Matcher): + ACT_CLEAR_NAME = 1 + ACT_FIX_OFFSET = 2 + def __init__(self): + self.actions = 0 + self.pixel_x = 0 + self.pixel_y = 0 + + def Matches(self, atom): + self.actions = 0 + if atom.path == '/obj/machinery/power/apc': + # Determine if this APC is pretty close to standard-issue (no weird permissions, etc). + nonstandard_settings = [] + for setting in atom.mapSpecified: + if setting not in ('name', 'pixel_x', 'pixel_y', 'tag', 'dir'): + nonstandard_settings += [setting] + if len(nonstandard_settings) > 0: + print('Non-standard APC #{}: Has strange settings - {}'.format(atom.id,', '.join(nonstandard_settings))) + else: + if 'name' in atom.properties and 'name' in atom.mapSpecified: + self.actions |= self.ACT_CLEAR_NAME + + direction = int(atom.getProperty('dir', 2)) + self.pixel_x = 0 + self.pixel_y = 0 + c_pixel_x = atom.getProperty('pixel_x', 0) + c_pixel_y = atom.getProperty('pixel_y', 0) + if (direction & 3): + self.pixel_x = 0 + if(direction == 1): + self.pixel_y = 24 + else: + self.pixel_y = -24 + else: + self.pixel_y = 0 + if(direction == 4): + self.pixel_x = 24 + else: + self.pixel_x = -24 + if self.pixel_x != c_pixel_x or self.pixel_y != c_pixel_y: + self.actions |= self.ACT_FIX_OFFSET + return self.actions != 0 + + def Fix(self, atom): + if self.actions & self.ACT_CLEAR_NAME: + atom.mapSpecified.remove('name') + if self.actions & self.ACT_FIX_OFFSET: + atom.setProperty('pixel_x', self.pixel_x, PropertyFlags.MAP_SPECIFIED) + atom.setProperty('pixel_y', self.pixel_y, PropertyFlags.MAP_SPECIFIED) + return atom + + def __str__(self): + if self.actions == 0: + return 'APC Standardization' + descr = [] + if self.actions & self.ACT_CLEAR_NAME: + descr += ['Cleared name property'] + if self.actions & self.ACT_FIX_OFFSET: + descr += ['Set pixel offset to {0},{1}'.format(self.pixel_x, self.pixel_y)] + return 'Standardized APC: ' + ', '.join(descr) \ No newline at end of file diff --git a/tools/BYONDTools/byond/mapfixes/ss13_vgstation.py b/tools/BYONDTools/byond/mapfixes/ss13_vgstation.py new file mode 100644 index 00000000000..c7ba8a68a76 --- /dev/null +++ b/tools/BYONDTools/byond/mapfixes/ss13_vgstation.py @@ -0,0 +1,311 @@ +''' +/vg/station-specific fixes. +''' + +from .base import Matcher,MapFix,RenameProperty,DeclareDependencies,ChangeType +from byond.basetypes import BYONDString, BYONDValue, Atom, PropertyFlags +from byond.directions import * + +DeclareDependencies('vgstation',['ss13']) + +@MapFix('vgstation') +class FixNetwork(Matcher): + def __init__(self): + pass + + def Matches(self, atom): + if atom.path.startswith('/obj/machinery/camera') and 'network' in atom.properties: + return isinstance(atom.properties['network'], BYONDString) and not atom.properties['network'].value.startswith('list(') + return False + + def Fix(self, atom): + fix = atom.properties['network'].value + atom.properties['network'] = BYONDValue('list("{0}")'.format(fix)) + return atom + + def __str__(self): + return 'Changed network property to list' + +@MapFix('vgstation') +class NetworkingChangeAtmos(ChangeType): + def __init__(self): + ChangeType.__init__(self,'/obj/machinery/atmospherics','/obj/machinery/networked/atmos', fuzzy = True) + +@MapFix('vgstation') +class NetworkingChangePower(ChangeType): + def __init__(self): + ChangeType.__init__(self,'/obj/machinery/power','/obj/machinery/networked/power', fuzzy = True) + +@MapFix('vgstation') +class NetworkingChangeFiber(ChangeType): + def __init__(self): + ChangeType.__init__(self,'/obj/machinery/fiber','/obj/machinery/networked/fiber', fuzzy = True) + + +class FixIDTags(Matcher): + atomsToFix={} + def __init__(self): + pass + + def Matches(self, atom): + global atomsToFix + if 'id_tag' in atom.properties: + compiled_atom = self.tree.GetAtom(atom.path) + if compiled_atom is None: return False + if 'id_tag' not in compiled_atom.properties: + FixIDTags.atomsToFix[atom.path] = True + return 'id' in atom.properties and 'id' in atom.mapSpecified + # return False + + def Fix(self, atom): + _id = atom.properties['id'] + id_idx = atom.mapSpecified.index('id') + atom.properties['id_tag'] = _id + del atom.properties['id'] + atom.mapSpecified[id_idx] = 'id_tag' + return atom + + def __str__(self): + return 'Renamed id to id_tag' + +@MapFix('vgstation') +class StandardizeManifolds(Matcher): + STATE_TO_TYPE = { + 'manifold-b' :'/obj/machinery/networked/atmos/pipe/manifold/supply/visible', + 'manifold-b-f':'/obj/machinery/networked/atmos/pipe/manifold/supply/hidden', + 'manifold-r' :'/obj/machinery/networked/atmos/pipe/manifold/scrubbers/visible', + 'manifold-r-f':'/obj/machinery/networked/atmos/pipe/manifold/scrubbers/hidden', + 'manifold-c' :'/obj/machinery/networked/atmos/pipe/manifold/cyan/visible', + 'manifold-c-f':'/obj/machinery/networked/atmos/pipe/manifold/cyan/hidden', + 'manifold-y' :'/obj/machinery/networked/atmos/pipe/manifold/yellow/visible', + 'manifold-y-f':'/obj/machinery/networked/atmos/pipe/manifold/yellow/hidden', + 'manifold-g' :'/obj/machinery/networked/atmos/pipe/manifold/filtering/visible', + 'manifold-g-f':'/obj/machinery/networked/atmos/pipe/manifold/filtering/hidden', + 'manifold' :'/obj/machinery/networked/atmos/pipe/manifold/general/visible', + 'manifold-f' :'/obj/machinery/networked/atmos/pipe/manifold/general/hidden', + } + def __init__(self): + return + + def Matches(self, atom): + if atom.path == '/obj/machinery/networked/atmos/pipe/manifold' and 'icon_state' in atom.mapSpecified: + return atom.getProperty('icon_state') in self.STATE_TO_TYPE + return False + + def Fix(self, atom): + icon_state = atom.properties['icon_state'].value + new_atom = Atom(self.STATE_TO_TYPE[icon_state]) + if 'dir' in atom.mapSpecified: + new_atom.setProperty('dir', atom.getProperty('dir'), PropertyFlags.MAP_SPECIFIED) + return new_atom + + def __str__(self): + return 'Standardized pipe manifold' + +@MapFix('vgstation') +class StandardizePiping(Matcher): + TYPE_TRANSLATIONS = { + '/obj/machinery/networked/atmos/pipe/simple': 'simple', + '/obj/machinery/networked/atmos/pipe/manifold': 'manifold', + '/obj/machinery/networked/atmos/pipe/manifold4w': 'manifold4w', + } + COLOR_CODES = { + 'b':'supply', + 'r':'scrubbers', + 'g':'filtering', + 'c':'cyan', + 'y':'yellow', + '': 'general' + } + def __init__(self): + self.before = None + self.after = None + return + + def trans_simple(self, atom): + type_tmpl = '/obj/machinery/networked/atmos/pipe/simple/{color}/{visibility}' + color_code, visible = self.parseIconState(atom.getProperty('icon_state', '')) + return self.getNewType(type_tmpl, color_code, visible) + + def trans_manifold(self, atom): + type_tmpl = '/obj/machinery/networked/atmos/pipe/manifold/{color}/{visibility}' + color_code, visible = self.parseIconState(atom.getProperty('icon_state', '')) + return self.getNewType(type_tmpl, color_code, visible) + + def trans_manifold4w(self, atom): + type_tmpl = '/obj/machinery/networked/atmos/pipe/manifold4w/{color}/{visibility}' + color_code, visible = self.parseIconState(atom.getProperty('icon_state', '')) + return self.getNewType(type_tmpl, color_code, visible) + + def parseIconState(self, state): + parts = state.split('-') + if len(parts) <= 1: + return ('', True) + elif len(parts) == 2: + if parts[1] == 'f': + return ('', True) + return (parts[1], True) + return (parts[1], parts[2] != 'f') + + def getNewType(self, tmpl, color_code, visible, color_wheel=COLOR_CODES): + visibility = 'visible' + if not visible: + visibility = 'hidden' + color = color_wheel[color_code] + return Atom(tmpl.format(color=color, visibility=visibility)) + + def Matches(self, atom): + return atom.path in self.TYPE_TRANSLATIONS + + def Fix(self, atom): + self.before = atom.MapSerialize() + old_dir = None + if 'dir' in atom.mapSpecified: + old_dir = int(atom.getProperty('dir', 2)) + atom = getattr(self, 'trans_{0}'.format(self.TYPE_TRANSLATIONS[atom.path]))(atom) + if old_dir is not None and old_dir != 2: + atom.setProperty('dir', old_dir, PropertyFlags.MAP_SPECIFIED) + self.after = atom.MapSerialize() + return atom + + def __str__(self): + if self.before is not None and self.after is not None: + return 'Standardized pipe: {0} -> {1}'.format(self.before, self.after) + else: + return 'Standardize pipes' + +@MapFix('vgstation') +class StandardizeInsulatedPipes(Matcher): + STATE_TO_TYPE = { + 'intact' :'/obj/machinery/networked/atmos/pipe/simple/insulated/visible', + 'intact-f':'/obj/machinery/networked/atmos/pipe/simple/insulated/hidden' + } + def __init__(self): + return + + def Matches(self, atom): + if atom.path == '/obj/machinery/networked/atmos/pipe/simple/insulated': + return True + if atom.path.startswith('/obj/machinery/networked/atmos/pipe/simple/insulated') and int(atom.getProperty('dir', 0)) in (3, 8, 12): + # print(atom.MapSerialize()) + return True + return False + + def Fix(self, atom): + newtype = atom.path + if atom.path == '/obj/machinery/networked/atmos/pipe/simple/insulated': + icon_state = '' + if 'icon_state' in atom.properties: + icon_state = atom.properties['icon_state'].value + newtype = self.STATE_TO_TYPE.get(icon_state, '/obj/machinery/networked/atmos/pipe/simple/insulated/visible') + new_atom = Atom(newtype) + if 'dir' in atom.mapSpecified: + # Normalize dir + direction = int(atom.getProperty('dir', 2)) + if direction == 3: + direction = 1 + elif direction == 8: # Breaks things, for some reason + direction = 4 + elif direction == 12: + direction = 4 + new_atom.setProperty('dir', direction, PropertyFlags.MAP_SPECIFIED) + return new_atom + + def __str__(self): + return 'Standardized insulated pipe' + +@MapFix('vgstation') +class FixWindows(Matcher): + def __init__(self): + return + + def Matches(self, atom): + if atom.path.startswith('/obj/structure/window/full'): + return False + if atom.path.startswith('/obj/structure/window') and int(atom.getProperty('dir', SOUTH)) in (NORTH | WEST, SOUTH | WEST, NORTH | EAST, SOUTH | EAST): + # print(atom.MapSerialize()) + return True + return False + + def Fix(self, atom): + newtype = atom.path.replace('/obj/structure/window', '/obj/structure/window/full') + atom.path = newtype + atom.properties = {} + atom.mapSpecified = [] + return atom + + def __str__(self): + return 'Standardized full windows' + +@MapFix('vgstation') +class FixVaultFloors(Matcher): + """ + Changes flooring icons to use /vg/'s standardized vault icons. + """ + # state:1 + ICON_STATE_CHANGES = { + 'vault:1' :{'icon_state':'dark-markings', 'dir':2}, + 'vault:2' :{'icon_state':'dark vault stripe', 'dir':2}, + 'vault:4' :{'icon_state':'dark-markings', 'dir':1}, + 'vault:8' :{'icon_state':'dark-markings', 'dir':8}, + 'vault:6' :{'icon_state':'dark vault corner', 'dir':2}, + 'vault:10':{'icon_state':'dark vault corner', 'dir':8}, + 'vault:5' :{'icon_state':'dark vault full', 'dir':2}, + 'vault:9' :{'icon_state':'dark loading', 'dir':4}, + + 'vault-border:1' :{'icon_state':'dark vault stripe', 'dir':2}, + 'vault-border:2' :{'icon_state':'dark vault stripe', 'dir':1}, + 'vault-border:4' :{'icon_state':'dark vault stripe', 'dir':4}, + 'vault-border:8' :{'icon_state':'dark vault stripe', 'dir':8}, + 'vault-border:6' :{'icon_state':'dark vault corner', 'dir':2}, + 'vault-border:10':{'icon_state':'dark vault stripe', 'dir':5}, + 'vault-border:5' :{'icon_state':'dark vault stripe', 'dir':5}, + 'vault-border:9' :{'icon_state':'dark vault stripe', 'dir':6}, + } + def __init__(self): + self.stateKey = '' + self.changesMade = [] + return + + def GetStateKey(self, atom): + icon_state = '' + _dir = '2' + if 'dir' in atom.properties: + _dir = str(atom.getProperty('dir')) + if 'icon_state' in atom.properties: + icon_state = atom.getProperty('icon_state') + return icon_state + ":" + _dir + + def Matches(self, atom): + if atom.path.startswith('/turf/') and 'icon_state' in atom.mapSpecified: + sk = self.GetStateKey(atom) + if sk in self.ICON_STATE_CHANGES: + self.stateKey = sk + return True + return False + + def Fix(self, atom): + self.changesMade = [] + propChanges = self.ICON_STATE_CHANGES[self.stateKey] + if 'tag' in atom.mapSpecified: + atom.mapSpecified.remove('tag') + for key, newval in propChanges.items(): + if key not in atom.mapSpecified: + atom.mapSpecified += [key] + oldval = 'NONE' + if key in atom.properties: + oldval = str(atom.properties[key]) + if isinstance(newval, str): + atom.properties[key] = BYONDString(newval) + elif isinstance(newval, int): + atom.properties[key] = BYONDValue(newval) + self.changesMade += ['{0}: {1} -> {2}'.format(key, oldval, atom.properties[key])] + return atom + + def __str__(self): + return 'Standardized vault flooring (' + ', '.join(self.changesMade) + ')' + +@MapFix('vgstation') +class RenameColorVG(RenameProperty): + def __init__(self): + RenameProperty.__init__(self, "color", "_color") \ No newline at end of file diff --git a/tools/BYONDTools/byond/network/__init__.py b/tools/BYONDTools/byond/network/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/BYONDTools/byond/network/packets/__init__.py b/tools/BYONDTools/byond/network/packets/__init__.py new file mode 100644 index 00000000000..cb661ce9ea4 --- /dev/null +++ b/tools/BYONDTools/byond/network/packets/__init__.py @@ -0,0 +1,91 @@ +""" +BYOND Base Packet Class + +Created with help from tobba. +""" +import logging, struct + +class NetTypes: + BYTE = 0 + SHORT = 1 + LONG = 2 + STRING = 3 + + min_lens = [1, 2, 4, None] + + @staticmethod + def GetMinLength(t): + return NetTypes.min_lens[t] + +PacketTypes = {} + +class Packet: + ID = 0 + Name = '' + def __init__(self): + self.__field_data = {} + self.header = {} + self.min_length = 0 + + self.length = 0 + self.sequence = 0 + + def LinkField(self, datatype, propname, **kwargs): + ''' + Associate a part of a packet to a field in this class + ''' + kwargs['type'] = datatype + kwargs['name'] = propname + self.__field_data[len(self.__field_data)] = kwargs + self.min_length += NetTypes.GetMinLength(datatype) + + def Deserialize(self, msg): + if len(msg) < self.min_length: + logging.error('Received truncated packet {0}: min_length={1}, msg.len={2}'.format(self.Name, self.min_length, len(msg))) + + # TIME FOR ASSUMPTIONS! + pos = 0 + for idx, fieldinfo in self.__field_data.items(): + dtype = fieldinfo['type'] + propname = fieldinfo['name'] + unpacked = None + if dtype == NetTypes.BYTE: + dat = msg[pos:pos + 1] + unpacked = struct.unpack('B', dat) # Unsigned char + pos += 1 + elif dtype == NetTypes.SHORT: + dat = msg[pos:pos + 2] + unpacked = struct.unpack('h', dat) # short (maybe H?) + pos += 2 + elif dtype == NetTypes.LONG: + dat = msg[pos:pos + 4] + unpacked = struct.unpack('l', dat) # short (maybe L?) + pos += 4 + elif dtype == NetTypes.STRING: + dat = msg[pos:] + unpacked = dat.split('\x00', 1) + pos += len(unpacked) + 1 # NUL byte stripped + else: + logging.error('Unable to unpack {0} packet at field {1}: Unknown datatype {2}'.format(self.Name, idx, dtype)) + logging.error('Packet __field_data:'.repr(self.__field_data)) + raise SystemError() + setattr(self, propname, unpacked) + + def Serialize(self): + msg = b'' + for idx, fieldinfo in self.__field_data.items(): + dtype = fieldinfo['type'] + dat = getattr(self, fieldinfo['name']) + if dtype == NetTypes.BYTE: + msg += struct.pack('B', dat) # Unsigned char + elif dtype == NetTypes.SHORT: + msg += struct.pack('h', dat) # short (maybe H?) + elif dtype == NetTypes.LONG: + msg += struct.pack('l', dat) # short (maybe L?) + elif dtype == NetTypes.STRING: + msg += dat + b'\x00' + else: + logging.error('Unable to pack {0} packet at field {1}: Unknown datatype {2}'.format(self.Name, idx, dtype)) + logging.error('Packet __field_data:'.repr(self.__field_data)) + raise SystemError() + return msg diff --git a/tools/BYONDTools/byond/objtree.py b/tools/BYONDTools/byond/objtree.py new file mode 100644 index 00000000000..394c6f1d17a --- /dev/null +++ b/tools/BYONDTools/byond/objtree.py @@ -0,0 +1,709 @@ +''' +Superficially generate an object/property tree. +''' +import re, logging, os + +try: + import cPickle as pickle +except: + import pickle + +from .basetypes import Atom, Proc, BYONDValue, BYONDString, BYONDFileRef +from .utils import md5sum, get_stdlib + +REGEX_TABS = re.compile('^(?P[\t\s]*)') +REGEX_ATOMDEF = re.compile('^(?P\t*)(?P[a-zA-Z0-9_/]+)\\{?\\s*$') +REGEX_ABSOLUTE_PROCDEF = re.compile('^(?P\t*)(?P[a-zA-Z0-9_/]+)/(?P[a-zA-Z0-9_]+)\((?P.*)\)\\{?\s*$') +REGEX_RELATIVE_PROCDEF = re.compile('^(?P\t*)(?P[a-zA-Z0-9_]+)\((?P.*)\)\\{?\\s*$') +REGEX_LINE_COMMENT = re.compile('//.*?$') + +def debug(filename, line, path, message): + print('{0}:{1}: {2} - {3}'.format(filename, line, '/'.join(path), message)) + +class OTRCache: + # : Only used for obliterating outdated data. + VERSION = [28, 4, 2014] + + def __init__(self, filename): + self.filename = filename + self.files = {} + self.atoms = None + self.handle = None + + def StartReading(self): + if self.handle: + self.handle.close() + self.handle = None + if os.path.isfile(self.filename): + self.handle = open(self.filename, 'r') + + def StopReading(self): + if self.handle is not None: + self.handle.close() + + def CheckVersion(self): + # print('READ VERSION') + # Block 1: Version + if pickle.load(self.handle) != self.VERSION: + print('!!! Outdated OTR data, rebuilding.') + return False + return True + + def ReadFiles(self): + # print('READ FILES') + # Block 2: Files + self.files = pickle.load(self.handle) + + def ReadAtoms(self): + # print('READ ATOMS') + return pickle.load(self.handle) + + def CheckFileHash(self, fn, md5): + # print('{0}: {1}'.format(fn,md5)) + if fn not in self.files: + print(' + {0}'.format(fn)) + return False + if self.files[fn] != md5: + print(' * {0}'.format(fn)) + return False + return True + + def PruneFiles(self, file_list): + for fn in self.files.keys(): + if fn not in file_list: + self.files -= [fn] + print(' - {0}'.format(fn)) + + def SetFileMD5(self, fn, md5): + self.files[fn] = md5 + + def GetFiles(self): + return self.files.keys() + + def Save(self, atoms): + with open(self.filename, 'w') as f: + pickle.dump(self.VERSION, f) + pickle.dump(self.files, f) + pickle.dump(atoms, f) + +class ObjectTree: + reserved_words = ('else', 'break', 'return', 'continue', 'spawn') # , 'proc') + stdlib_files = ( + 'dm_std.dm', + 'atom_defaults.dm' + ) + def __init__(self, **options): + #: All atoms, in a list. + self.Atoms = {} + + #: All atoms, in a tree-node structure. + self.Tree = Atom('') + + #: Skip loading from .OTR? + self.skip_otr = False + + self.LoadedStdLib = False + self.cpath = [] + self.popLevels = [] + self.InProc = [] + self.pindent = 0 # Previous Indent + self.ignoreLevel = [] # Block Comments + self.ignoreStartIndent = -1 + self.debugOn = True + self.ignoreDebugOn = False + self.ignoreTokens = { + '/*':'*/', + '{"':'"}' + } + self.defines = {} + self.defineMatchers = {} + self.comments = [] + self.fileLayouts = {} + self.LeavePreprocessorDirectives = options.get('preprocessor_directives', False) + + nit = self.ignoreTokens.copy() + for _, stop in self.ignoreTokens.iteritems(): + nit[stop] = None + self.ignoreTokens = nit + + self.defines['__OBJTREE'] = BYONDValue('1') + + def ProcessMultiString(self, filename, line, ignoreLevels, current_buffer): + return '"{0}"'.format(current_buffer) + + def SplitPath(self, string): + o = [] + buf = [] + inProc = False + for chunk in string.split('/'): + if not inProc: + if '(' in chunk and ')' not in chunk: + inProc = True + buf += [chunk] + else: + o += [chunk] + else: + if ')' in chunk: + o += ['/'.join(buf + [chunk])] + inProc = False + else: + buf += [chunk] + return o + + def ProcessFilesFromDME(self, dmefile='baystation12.dme', ext='.dm', **kwargs): + changed_files = 0 + rootdir = os.path.dirname(dmefile) + projectfile = os.path.join(rootdir, os.path.basename(dmefile).replace('.dme', '.otr')) + + cache = OTRCache(projectfile) + invalid = False + if not self.skip_otr: + if os.path.isfile(projectfile): + print('--- Loading pickled object tree...') + cache.StartReading() + if cache.CheckVersion(): + cache.ReadFiles() + else: + invalid = True + else: + invalid = True + + ToRead = [] + if not self.LoadedStdLib and kwargs.get('load_stdlib', True): + stdlib_dir = get_stdlib() + for filename in self.stdlib_files: + # self.ProcessFile(os.path.join(stdlib_dir, filename)) + ToRead += [os.path.join(stdlib_dir, filename)] + with open(dmefile, 'r') as dmeh: + for line in dmeh: + if line.startswith('#include'): + inString = False + # escaped=False + filename = '' + for c in line: + """ + if c == '\\' and not escaped: + escaped = True + continue + if escaped: + if + escaped = False + continue + """ + if c == '"': + inString = not inString + if not inString: + filepath = os.path.join(rootdir, filename) + if filepath.endswith(ext): + ToRead += [filepath] + filename = '' + continue + else: + if inString: + filename += c + + for filepath in ToRead: + md5 = md5sum(filepath) + if invalid or not cache.CheckFileHash(filepath, md5): + changed_files += 1 + cache.SetFileMD5(filepath, md5) + if invalid or self.skip_otr or changed_files > 0: + if invalid: + print('--- Rebuilding object tree - Parsing DM files...'.format(changed_files)) + else: + print('--- {0} changed files. Parsing DM files...'.format(changed_files)) + for f in ToRead: + self.ProcessFile(f) + print('--- Saving atoms...') + cache.StopReading() + cache.Save(self.Atoms) + self.MakeTree() + else: + print('--- No changes detected, using pickled atoms...') + self.Atoms = cache.ReadAtoms() + cache.StopReading() + self.MakeTree() + + def DetermineContext(self, filename, ln, line, numtabs, atom_prefix=[]): + ''' + Spit out the full path of the atom we're currently in. + + Does NOT update internal positioning. Think peek. + ''' + if numtabs == 0: + return None # Global context + + elif numtabs > self.pindent: + return '/'.join(self.cpath + atom_prefix) + + elif numtabs < self.pindent: + cpath_copy = list(self.cpath) + for _ in range(self.pindent - numtabs + 1): + popsToDo = self.popLevels.pop() + for _ in range(popsToDo): + cpath_copy.pop() + cpath_copy += atom_prefix + return '/'.join(cpath_copy) + + elif numtabs == self.pindent: + cpath_copy = list(self.cpath) + levelsToPop = self.popLevels.pop() + for _ in range(levelsToPop): + cpath_copy.pop() + cpath_copy += atom_prefix + return '/'.join(cpath_copy) + + + def ProcessAtom(self, filename, ln, line, atom, atom_path, numtabs, procArgs=None): + # Reserved words that show up on their own + if atom in ObjectTree.reserved_words: + return + + # Other things to ignore (false positives, comments) + if atom.startswith('var/') or atom.startswith('//'): + return + + # Things part of a string or list. + if numtabs > 0 and atom.strip().startswith('/'): + return + + if self.debugOn: print('{} > {}'.format(numtabs, line.rstrip())) + + if numtabs == 0: + self.cpath = atom_path + if len(self.cpath) == 0: + self.cpath += [''] + elif self.cpath[0] != '': + self.cpath.insert(0, '') + self.popLevels = [len(self.cpath)] + if self.debugOn: debug(filename, ln, self.cpath, '0 - ' + repr(atom_path)) + + elif numtabs > self.pindent: + self.cpath += atom_path + self.popLevels += [len(atom_path)] + if self.debugOn: debug(filename, ln, self.cpath, '>') + + elif numtabs < self.pindent: + if self.debugOn: print('({} - {})={}: {}'.format(self.pindent, numtabs, self.pindent - numtabs, repr(self.cpath))) + for _ in range(self.pindent - numtabs + 1): + popsToDo = self.popLevels.pop() + if self.debugOn: print(' pop {} {}'.format(popsToDo, self.popLevels)) + for i in range(popsToDo): + self.cpath.pop() + if self.debugOn: print(' pop {}/{}: {}'.format(i + 1, popsToDo, repr(self.cpath))) + self.cpath += atom_path + self.popLevels += [len(atom_path)] + if self.debugOn: debug(filename, ln, self.cpath, '<') + + elif numtabs == self.pindent: + levelsToPop = self.popLevels.pop() + for i in range(levelsToPop): + self.cpath.pop() + self.cpath += atom_path + self.popLevels += [len(atom_path)] + if self.debugOn: print('popLevels: ' + repr(self.popLevels)) + if self.debugOn: debug(filename, ln, self.cpath, '==') + + origpath = '/'.join(self.cpath) + # print(npath) + + # definition? + defs = [] + + # Trim off /proc or /var, if needed. + prep_path = list(self.cpath) + + for special in ['proc']: + if special in prep_path: + defs += [special] + prep_path.remove(special) + + npath = '/'.join(prep_path) + + if npath not in self.Atoms: + if procArgs is not None: + assert npath.endswith(')') + # if origpath != npath: + # print(origpath,proc_def) + proc = Proc(npath, procArgs, filename, ln) + proc.origpath = origpath + proc.definition = 'proc' in defs + self.Atoms[npath] = proc + else: + self.Atoms[npath] = Atom(npath, filename, ln) + # if self.debugOn: print('Added ' + npath) + self.pindent = numtabs + return self.Atoms[npath] + + def AddCodeToProc(self, startIndent, code): + if '\n' in code: + for line in code.split('\n'): + self.AddCodeToProc(startIndent, line) + else: + m = REGEX_TABS.match(code) + if m is not None: + numtabs = len(m.group('tabs')) + i = max(1, numtabs - startIndent) + # self.loadingProc.AddCode(i, '/* {0} */ {1}'.format(i,code.strip())) + self.loadingProc.AddCode(i, code.rstrip()) + + def finishComment(self, line, **args): + self.comments += [self.comment] + self.fileLayout += [('COMMENT', len(self.comments) - 1)] + if self.ignoreDebugOn: print('finishComment({}): {}'.format(line, self.comment)) + if self.loadingProc is not None and (args.get('cleansed_line', '') == '' and not self.comment.strip().startswith('//')): + self.AddCodeToProc(self.ignoreStartIndent, self.comment) + self.comment = '' + + def handleOBToken(self, name, context, params): + if context is not None: + context = self.Atoms[context] + name = 'ob_{0}'.format(name.lower()) + getattr(self, name)(context, *params) + + def ProcessFile(self, filename): + self.cpath = [] + self.popLevels = [] + self.pindent = 0 # Previous Indent + self.ignoreLevel = [] + self.debugOn = False + self.ignoreDebugOn = False + self.ignoreStartIndent = -1 + self.loadingProc = None + self.comment = '' + self.fileLayout = [] + self.lineBeforePreprocessing = '' + self.current_filename = filename + with open(filename, 'r') as f: + ln = 0 + ignoreLevel = [] + + for line in f: + ln += 1 + + skipNextChar = False + nl = '' + + line = line.rstrip() + self.lineBeforePreprocessing = line + line_len = len(line) + for i in xrange(line_len): + c = line[i] + nc = '' + if line_len > i + 1: + nc = line[i + 1] + tok = c + nc + # print(tok) + if skipNextChar: + if self.ignoreDebugOn: print('Skipping {}.'.format(repr(tok))) + skipNextChar = False + # self.comment += c + if self.ignoreDebugOn: print('self.comment = {}.'.format(repr(self.comment))) + continue + if tok == '//': + # if self.ignoreDebugOn: debug(filename,ln,self.cpath,'{} ({})'.format(tok,len(ignoreLevel))) + if len(ignoreLevel) == 0: + self.comment = line[i:] + # if self.ignoreDebugOn: print('self.comment = {}.'.format(repr(self.comment))) + # print('Found '+self.comment) + self.finishComment('243', cleaned_line=nl) + break + if tok in self.ignoreTokens: + pc = '' + if i > 0: + pc = line[i - 1] + if tok == '{"' and pc == '"': + self.comment += c + continue + # if self.ignoreDebugOn: print(repr(self.ignoreTokens[tok])) + stop = self.ignoreTokens[tok] + if stop == None: # End comment + if len(ignoreLevel) > 0: + if ignoreLevel[-1] == tok: + skipNextChar = True + self.comment += tok + ignoreLevel.pop() + if len(ignoreLevel) == 0: + self.finishComment('261') + continue + else: + self.comment += c + continue + else: # Start comment + skipNextChar = True + ignoreLevel += [stop] + self.comment = tok + continue + if self.ignoreDebugOn: debug(filename, ln, self.cpath, '{} ({})'.format(tok, len(ignoreLevel))) + if len(ignoreLevel) == 0: + nl += c + else: + self.comment += c + if line != nl: + if self.ignoreDebugOn: print('IN : ' + line) + line = nl + if self.ignoreDebugOn: print('OUT: ' + line) + if self.ignoreDebugOn: print('self.comment = {}.'.format(repr(self.comment))) + + if len(ignoreLevel) > 0: + self.comment += "\n" + continue + + line = REGEX_LINE_COMMENT.sub('', line) + + if line.strip() == '': + if self.loadingProc is not None: + self.loadingProc.AddBlankLine() + continue + + # Preprocessing defines. + if line.strip().startswith("#"): + if line.endswith('\\'): continue + tokenChunks = line.split('#') + tokenChunks = tokenChunks[1].split() + directive = tokenChunks[0] + if directive == 'define': + # #define SOMETHING Value + defineChunks = line.split(None, 3) + if len(defineChunks) == 2: + defineChunks += [1] + elif len(defineChunks) == 3: + defineChunks[2] = self.PreprocessLine(defineChunks[2]) + # print(repr(defineChunks)) + try: + if '.' in defineChunks[2]: + self.defines[defineChunks[1]] = BYONDValue(float(defineChunks[2]), filename, ln) + else: + self.defines[defineChunks[1]] = BYONDValue(int(defineChunks[2]), filename, ln) + except: + self.defines[defineChunks[1]] = BYONDString(defineChunks[2], filename, ln) + self.fileLayout += [('DEFINE', defineChunks[1], defineChunks[2])] + elif directive == 'undef': + undefChunks = line.split(' ', 2) + if undefChunks[1] in self.defines: + del self.defines[undefChunks[1]] + self.fileLayout += [('UNDEF', undefChunks[1])] + + # OpenBYOND tokens. + elif directive.startswith('__OB_'): + numtabs = 0 + m = REGEX_TABS.match(line) + if m is not None: + numtabs = len(m.group('tabs')) + atom = self.DetermineContext(filename, ln, line, numtabs) + # if atom is None: continue + # print('OBTOK {0}'.format(repr(tokenChunks))) + self.handleOBToken(tokenChunks[0].replace('__OB_', ''), atom, tokenChunks[1:]) + # self.fileLayout += [('OBTOK', atom.path)] + continue + else: + chunks = line.split(' ') + self.fileLayout += [('PP_TOKEN', line)] + print('BUG: Unhandled preprocessor directive #{} in {}:{}'.format(directive, filename, ln)) + continue + + # Preprocessing + line = self.PreprocessLine(line) + + m = REGEX_TABS.match(self.lineBeforePreprocessing) + if m is not None: + numtabs = len(m.group('tabs')) + if self.ignoreStartIndent > -1 and self.ignoreStartIndent < numtabs: + if self.loadingProc is not None: + # self.loadingProc.AddCode(numtabs - self.ignoreStartIndent, self.lineBeforePreprocessing.strip()) + self.AddCodeToProc(self.ignoreStartIndent, self.lineBeforePreprocessing) + if self.debugOn: print('TABS: {} ? {} - {}: {}'.format(numtabs, self.ignoreStartIndent, self.loadingProc, line)) + continue + else: + if self.debugOn and self.ignoreStartIndent > -1: print('BREAK ({} -> {}): {}'.format(self.ignoreStartIndent, numtabs, line)) + self.ignoreStartIndent = -1 + self.loadingProc = None + else: + if self.debugOn and self.ignoreStartIndent > -1: print('BREAK ' + line) + self.ignoreStartIndent = -1 + self.loadingProc = None + + if not line.strip().startswith('var/'): + m = REGEX_ATOMDEF.match(line) + if m is not None: + numtabs = len(m.group('tabs')) + atom = m.group('atom') + atom_path = self.SplitPath(atom) + atom = self.ProcessAtom(filename, ln, line, atom, atom_path, numtabs) + if atom is None: continue + self.fileLayout += [('ATOMDEF', atom.path)] + continue + + m = REGEX_ABSOLUTE_PROCDEF.match(line) + if m is not None: + numtabs = len(m.group('tabs')) + atom = '{0}/{1}({2})'.format(m.group('atom'), m.group("proc"), m.group('args')) + atom_path = self.SplitPath(atom) + # print('PROCESSING ABS PROC AT INDENT > ' + str(numtabs) + " " + atom+" -> "+repr(atom_path)) + proc = self.ProcessAtom(filename, ln, line, atom, atom_path, numtabs, m.group('args').split(',')) + if proc is None: continue + self.ignoreStartIndent = numtabs + self.loadingProc = proc + self.fileLayout += [('PROCDEF', proc.path)] + continue + + m = REGEX_RELATIVE_PROCDEF.match(line) + if m is not None: + numtabs = len(m.group('tabs')) + atom = '{}({})'.format(m.group("proc"), m.group('args')) + atom_path = self.SplitPath(atom) + # print('IGNORING RELATIVE PROC AT INDENT > ' + str(numtabs) + " " + line) + proc = self.ProcessAtom(filename, ln, line, atom, atom_path, numtabs, m.group('args').split(',')) + if proc is None: continue + self.ignoreStartIndent = numtabs + self.loadingProc = proc + self.fileLayout += [('PROCDEF', proc.path)] + continue + + path = '/'.join(self.cpath) + # if len(self.cpath) > 0 and 'proc' in self.cpath: + # continue + # if 'proc' in self.cpath: + # continue + if '=' in line or line.strip().startswith('var/'): + if path not in self.Atoms: + self.Atoms[path] = Atom(path) + name, prop = self.consumeVariable(line, filename, ln) + self.Atoms[path].properties[name] = prop + self.fileLayout += [('VAR', path, name)] + self.fileLayouts[filename] = self.fileLayout + + def consumeVariable(self, line, filename, ln): + declaration = False + value = None + size = None + + decl = '' + if self.LeavePreprocessorDirectives: + line = decl = self.lineBeforePreprocessing.strip() + else: + decl = line.strip() + if '[' in line: + line_split, arr_decl = line.split('[', 1) + str_size = arr_decl[:arr_decl.index(']')] + size = -1 + if str_size != '': + try: + size = int(str_size) + except ValueError: + pass + # print(repr({'size':size,'line':line_split})) + line = line_split + + if '=' in line: + decl, value = line.split('=', 1) + decl = decl.strip() + value = value.strip() + else: + decl = line.strip() + value = None + # print(repr({'decl':decl,'value':value})) + + # (var)(/global|const|tmp)(/type/fragment)name + if decl.startswith('var/'): + declaration = True + decl = decl[4:] + pathchunks = decl.split('/') + name = pathchunks[-1] + special = None + typepath = '/' + if declaration: + if pathchunks[0] in ('tmp', 'global', 'const'): + special = pathchunks[0] + pathchunks = pathchunks[1:] + if 'list' not in pathchunks and size is not None: + pathchunks = ['list'] + pathchunks + typepath = '/' + '/'.join(pathchunks[:-1]) + + kwargs = { + 'declaration':declaration, + 'special':special, + 'size':size + } + if typepath != '/': + return (name, BYONDValue(value, filename, ln, typepath, **kwargs)) + elif value and value[0] == '"': + return (name, BYONDString(value[1:-1], filename, ln, **kwargs)) + elif value and value[0] == "'": + return (name, BYONDFileRef(value[1:-1], filename, ln, **kwargs)) + elif value and '.' in value: + try: + return (name, BYONDValue(float(value), filename, ln, typepath, **kwargs)) + except ValueError: + pass + return (name, BYONDValue(value, filename, ln, typepath, **kwargs)) + + def MakeTree(self): + print('Generating Tree...') + self.Tree = Atom('/') + with open('objtree.txt', 'w') as f: + for key in sorted(self.Atoms): + f.write("{0}\n".format(key)) + atom = self.Atoms[key] + cpath = [] + cNode = self.Tree + fullpath = self.SplitPath(atom.path) + truncatedPath = fullpath[1:] + for path_item in truncatedPath: + cpath += [path_item] + cpath_str = '/'.join([''] + cpath) + # if path_item == 'var': + # if path_item not in cNode.properties: + # cNode.properties[fullpath[-1]]='???' + if path_item not in cNode.children: + if cpath_str in self.Atoms: + cNode.children[path_item] = self.Atoms[cpath_str] + else: + if '(' in path_item: + cNode.children[path_item] = Proc('/'.join([''] + cpath), []) + else: + cNode.children[path_item] = Atom('/'.join([''] + cpath),atom.filename,atom.line) + cNode.children[path_item].parent = cNode + parent_type = cNode.children[path_item].getProperty('parent_type') + if parent_type is not None: + print(' - Parent of {0} forced to be {1}'.format(cNode.children[path_item].path, repr(parent_type))) + cNode.children[path_item].parent = self.Atoms[parent_type] + cNode = cNode.children[path_item] + self.Tree.InheritProperties() + print('Processed {0} atoms.'.format(len(self.Atoms))) + # self.Atoms = {} + + def GetAtom(self, path): + if path in self.Atoms: + return self.Atoms[path] + + cpath = [] + cNode = self.Tree + fullpath = path.split('/') + truncatedPath = fullpath[1:] + for path_item in truncatedPath: + cpath += [path_item] + if path_item not in cNode.children: + print('Unable to find {0} (lost at {1})'.format(path, cNode.path)) + print(repr(cNode.children.keys())) + return None + cNode = cNode.children[path_item] + # print('Found {0}!'.format(path)) + self.Atoms[path] = cNode + return cNode + + + def PreprocessLine(self, line): + for key, define in self.defines.items(): + if key in line: + if key not in self.defineMatchers: + self.defineMatchers[key] = re.compile(r'\b' + key + r'\b') + newline = self.defineMatchers[key].sub(str(define.value), line) + if newline != line: + ''' + if filename.endswith('pipes.dm'): + print('OLD: {}'.format(line)) + print('PPD: {}'.format(newline)) + ''' + line = newline + return line diff --git a/tools/BYONDTools/byond/utils.py b/tools/BYONDTools/byond/utils.py new file mode 100644 index 00000000000..8f65a20e970 --- /dev/null +++ b/tools/BYONDTools/byond/utils.py @@ -0,0 +1,48 @@ +import hashlib, ast, os +import operator as op + +def md5sum(filename): + with open(filename, mode='rb') as f: + d = hashlib.md5() + while True: + buf = f.read(4096) # 128 is smaller than the typical filesystem block + if not buf: + break + d.update(buf) + return d.hexdigest().upper() + +# supported operators +operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, + ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor} + +def eval_expr(expr): + """ + >>> eval_expr('2^6') + 4 + >>> eval_expr('2**6') + 64 + >>> eval_expr('1 + 2*3**(4^5) / (6 + -7)') + -5.0 + """ + return eval_(ast.parse(expr).body[0].value) # Module(body=[Expr(value=...)]) + +def eval_(node): + if isinstance(node, ast.Num): # + return node.n + elif isinstance(node, ast.operator): # + return operators[type(node)] + elif isinstance(node, ast.BinOp): # + return eval_(node.op)(eval_(node.left), eval_(node.right)) + else: + raise TypeError(node) + + +_ROOT = os.path.abspath(os.path.dirname(__file__)) + +def get_data(path): + return os.path.join(_ROOT, 'data', path) + +def get_stdlib(path=''): + if path != '': + return os.path.join(get_data('stdlib'), path) + return get_data('stdlib') diff --git a/tools/BYONDTools/doc/Makefile b/tools/BYONDTools/doc/Makefile new file mode 100644 index 00000000000..affb8157b99 --- /dev/null +++ b/tools/BYONDTools/doc/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OpenBYOND.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OpenBYOND.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/OpenBYOND" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OpenBYOND" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/tools/BYONDTools/doc/build_html_docs.bat b/tools/BYONDTools/doc/build_html_docs.bat new file mode 100644 index 00000000000..e963e472f4b --- /dev/null +++ b/tools/BYONDTools/doc/build_html_docs.bat @@ -0,0 +1,3 @@ +rm -rf _build +call make html +pause \ No newline at end of file diff --git a/tools/BYONDTools/doc/byond/basetypes.rst b/tools/BYONDTools/doc/byond/basetypes.rst new file mode 100644 index 00000000000..cca681f2149 --- /dev/null +++ b/tools/BYONDTools/doc/byond/basetypes.rst @@ -0,0 +1,28 @@ +.. module:: com.byond.basetypes + +:mod:`com.byond.basetypes` -- Basic BYOND Types +=============================================== + +Basic Types +----------- + +.. autoclass:: BYONDValue + :members: + +.. autoclass:: BYONDFileRef + :members: + +.. autoclass:: BYONDString + :members: + +Atoms and Procs +--------------- + +.. autoclass:: PropertyFlags + :members: + +.. autoclass:: Atom + :members: + +.. autoclass:: Proc + :members: \ No newline at end of file diff --git a/tools/BYONDTools/doc/conf.py b/tools/BYONDTools/doc/conf.py new file mode 100644 index 00000000000..5852651384b --- /dev/null +++ b/tools/BYONDTools/doc/conf.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +# +# OpenBYOND documentation build configuration file, created by +# sphinx-quickstart on Fri Feb 07 20:03:28 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('../src')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.pngmath', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'BYONDTools' +copyright = u'2013-2014 BYONDTools Team' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'OpenBYONDdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'OpenBYOND.tex', u'OpenBYOND Documentation', + u'OpenBYOND Team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'openbyond', u'OpenBYOND Documentation', + [u'OpenBYOND Team'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'OpenBYOND', u'OpenBYOND Documentation', + u'OpenBYOND Team', 'OpenBYOND', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + +primary_domain = 'py' \ No newline at end of file diff --git a/tools/BYONDTools/doc/index.rst b/tools/BYONDTools/doc/index.rst new file mode 100644 index 00000000000..9504c105da5 --- /dev/null +++ b/tools/BYONDTools/doc/index.rst @@ -0,0 +1,18 @@ +OpenBYOND +========= + +Contents: + +.. toctree:: + :maxdepth: 2 + + byond/basetypes + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/tools/BYONDTools/doc/make.bat b/tools/BYONDTools/doc/make.bat new file mode 100644 index 00000000000..133faa3594c --- /dev/null +++ b/tools/BYONDTools/doc/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\OpenBYOND.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\OpenBYOND.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/tools/BYONDTools/scripts/byondtools-postinstall.py b/tools/BYONDTools/scripts/byondtools-postinstall.py new file mode 100644 index 00000000000..ea57bbc2990 --- /dev/null +++ b/tools/BYONDTools/scripts/byondtools-postinstall.py @@ -0,0 +1,50 @@ +#------------------------------------------------------------------------------ +# Originally cxfreeze-postinstall +# Script run after installation on Windows to fix up the Python location in +# the script as well as create batch files. +#------------------------------------------------------------------------------ + +import distutils.sysconfig +import glob +import os + +vars = distutils.sysconfig.get_config_vars() +prefix = vars["prefix"] +python = os.path.join(prefix, "python.exe") +scriptDir = os.path.join(prefix, "Scripts") + +# Keep in-sync with setup.py. +scripts = [ + 'dmm', + 'dmi', + 'dmindent', + 'dmmrender', + 'dmmfix', + + 'ss13_makeinhands' +] +for fileName in glob.glob(os.path.join(scriptDir, "*.py")): + # skip already created batch files if they exist + name, ext = os.path.splitext(os.path.basename(fileName)) + if name not in scripts: + continue + + print('Running post-install for {}.'.format(name)) + # copy the file with the first line replaced with the correct python + fullName = os.path.join(scriptDir, fileName) + strippedName = os.path.join(scriptDir, name) + lines = open(fullName).readlines() + startidx=1 + if not lines[0].strip().startswith('#!'): + print('WARNING: {} does not have a shebang.'.format(lines[0])) + startidx=0 + outFile = open(fullName, "w") + outFile.write("#!%s\n" % python) + outFile.writelines(lines[startidx:]) + outFile.close() + + # create the batch file + batchFileName = strippedName + ".bat" + command = "%s %s %%*" % (python, fullName) + open(batchFileName, "w").write("@echo off\n\n%s" % command) + diff --git a/tools/BYONDTools/scripts/calculateMaxTechLevels.py b/tools/BYONDTools/scripts/calculateMaxTechLevels.py new file mode 100644 index 00000000000..597925d22af --- /dev/null +++ b/tools/BYONDTools/scripts/calculateMaxTechLevels.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +""" +Usage: + $ python calculateMaxTechLevels.py path/to/your.dme .dm + +calculateMaxTechLevels.py - Get techlevels of all objects and generate reports. + +Copyright 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +import os, sys, re +from byond.objtree import ObjectTree +from byond.basetypes import Atom, Proc + +# Calculated Max Tech Levels. +CMTLs = {} + +# All known atoms with tech origins. +AtomTechOrigins = {} + +def ProcessTechLevels(atom, path=''): + global CMTLs, AtomTechOrigins + if path.endswith(')'): + # print('ignoring '+path) + return + for key, val in atom.properties.iteritems(): + # if 'obj' in path: print('{}: {}'.format(path,key)) + if key == 'origin_tech': + tech_origin = {} + # materials=9;bluespace=10;magnets=3 + text_origin_tech = atom.getProperty('origin_tech', 'null') + if text_origin_tech == 'null' or text_origin_tech == '': + continue + techchunks = text_origin_tech.split(';') + for techchunk in techchunks: + parts = techchunk.split('=') + if len(parts) != 2: + print('Improperly formed origin_tech in {0}: {1}'.format(atom.path, val.value)) + continue + tech = parts[0] + level = int(parts[1]) + tech_origin[tech] = level + if tech not in CMTLs: + CMTLs[tech] = level + if CMTLs[tech] < level: + CMTLs[tech] = level + AtomTechOrigins[path] = tech_origin + break + for key, child in atom.children.iteritems(): + ProcessTechLevels(child, path + '/' + key) + +def prettify(tree, indent=0): + prefix = ' ' * indent + for key in tree.iterkeys(): + atom = tree[key] + print('{}{}/'.format(prefix, key)) + prettify(atom.children, indent + len(key)) + for propkey, value in atom.properties.iteritems(): + print('{}var/{} = {}'.format(' ' * (indent + len(key)), propkey, repr(value))) + +if os.path.isfile(sys.argv[1]): + tree = ObjectTree() + tree.ProcessFilesFromDME(sys.argv[1]) + ProcessTechLevels(tree.Tree) + with open(os.path.join(os.path.dirname(sys.argv[1]), 'tech_origin_list.csv'), 'w') as w: + with open(os.path.join(os.path.dirname(sys.argv[1]), 'max_tech_origins.txt'), 'w') as mto: + tech_columns = [] + mto.write('Calculated Max Tech Levels:\n These tech levels have been determined by parsing ALL origin_tech variables in code included by {0}.\n'.format(sys.argv[1])) + for tech in sorted(CMTLs.keys()): + tech_columns.append(tech) + mto.write('{:>15}: {}\n'.format(tech, CMTLs[tech])) + w.write(','.join(['Atom', 'Name'] + tech_columns) + "\n") + for path in sorted(AtomTechOrigins.keys()): + row = [] + row.append(path) + atom = tree.GetAtom(path) + name = atom.properties.get('name', None) + if name is None: + name = '' + else: + name = name.value + row.append('"' + name.replace('"', '""') + '"') + for tech in tech_columns: + if tech in AtomTechOrigins[path]: + row.append(str(AtomTechOrigins[path][tech])) + else: + row.append('') + w.write(','.join(row) + "\n") + # prettify(tree.Tree.children) diff --git a/tools/BYONDTools/scripts/checkForStringIdiocy.py b/tools/BYONDTools/scripts/checkForStringIdiocy.py new file mode 100644 index 00000000000..6f023fcb589 --- /dev/null +++ b/tools/BYONDTools/scripts/checkForStringIdiocy.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python +import os, sys, re +""" +Usage: + $ python fix_string_idiocy.py path/to/your.dme .dm + +NOTE: NOT PERFECT, CREATES code-fixed DIRECTORY. +*** MERGE THIS MANUALLY OR YOU WILL BREAK SHIT. *** + +fix_string_idiocy.py - Combines multiple string append operations in DreamMaker code + +Copyright 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +REGEX_TO_COMBINE_AS_BLOCK = re.compile('^(?P\t+)(?Pvar/)?(?P[A-Za-z\.]+)\s*(?P\+?)=\s*"(?P.+)"\s*$') +def ProcessFile(filename): + fuckups = [] + with open(filename, 'r') as f: + lastID = '' + declaring = False + lastLevel = 0 + lastWasAlert = False + buffa = '' + tempbuffa = '' + tempfuckup = '' + tempBackup = '' + origIndentLevel = 0 + ln = 0 + for line in f: + ln += 1 + m = REGEX_TO_COMBINE_AS_BLOCK.match(line) + if m is not None: + level = m.group('tabs').count('\t') + ID = m.group('identifier') + content = m.group('content').strip() + indent = '\t' * level + # indentMore = '\t' * (level + 1) + if ID == lastID and level == lastLevel: + if not lastWasAlert: + buffa += '\n' + indent + '// AUTOFIXED BY fix_string_idiocy.py' + buffa += '\n' + indent + '// ' + tempfuckup + buffa += '\n' + tempbuffa + print(tempfuckup) + fuckups.append(tempfuckup) + msg = '{0}:{1}: {2}'.format(filename, ln, line.strip()) + print(msg) + fuckups.append(msg) + buffa += '\n' + # buffa += indentMore + buffa += content + lastWasAlert = True + else: + if lastWasAlert: + buffa += '"}' + buffa += '\n' + ('\t' * origIndentLevel) + '// END AUTOFIX' + buffa += '\n' + lastWasAlert = False + if tempBackup != '': + buffa += tempBackup + tempBackup = line + tempbuffa = indent + origIndentLevel = level + if m.group('declaration') is None: + tempbuffa += '{0} {2}= {{"{1}'.format(ID, content, m.group('operator')) + else: + tempbuffa += 'var/{0} {2}= {{"{1}'.format(ID, content, m.group('operator')) + tempfuckup = '{0}:{1}: {2}'.format(filename, ln, line.strip()) + lastID = ID + lastLevel = level + else: + if line.strip() == '': + tempBackup += line + continue + if lastWasAlert: + buffa += '"}' + buffa += '\n' + indent + '// END AUTOFIX' + buffa += '\n' + lastWasAlert = False + tempBackup = '' + if tempBackup != '': + buffa += tempBackup + tempBackup = '' + lastID = '' + lastLevel = '' + buffa += line + fixpaths = ['code', 'interface', 'RandomZLevels', '_maps'] + fixpath=filename + for fp in fixpaths: + fixpath = fixpath.replace(fp + os.sep, fp + '-fixed' + os.sep) + if len(fuckups) > 0: + if not os.path.isdir(os.path.dirname(fixpath)): + os.makedirs(os.path.dirname(fixpath)) + with open(fixpath, 'w') as fixes: + fixes.write(buffa) + else: + if os.path.isfile(fixpath): + print('RM {0} ({1})'.format(fixpath,os.sep)) + os.remove(fixpath) + # print(' Processed - {0} lines.'.format(ln)) + return fuckups + +def ProcessFilesFromDME(dmefile='baystation12.dme', ext='.dm'): + numFilesTotal = 0 + fileFuckups = {} + rootdir = os.path.dirname(dmefile) + with open(os.path.join(rootdir, 'stringcounts.csv'), 'w') as csv: + with open(dmefile, 'r') as dmeh: + for line in dmeh: + if line.startswith('#include'): + inString = False + # escaped=False + filename = '' + for c in line: + """ + if c == '\\' and not escaped: + escaped = True + continue + if escaped: + if + escaped = False + continue + """ + if c == '"': + inString = not inString + if not inString: + filepath = os.path.join(rootdir, filename) + if filepath.endswith(ext): + # print('Processing {0}...'.format(filepath)) + fileFuckups[filepath] = ProcessFile(filepath) + numFilesTotal += 1 + filename = '' + continue + else: + if inString: + filename += c + +if os.path.isdir(sys.argv[1]): + for root, _, files in os.walk(sys.argv[1]): + for filename in files: + filepath = os.path.join(root, filename) + if filepath.endswith('.dme'): + ProcessFilesFromDME(filepath, sys.argv[2]) + sys.exit(0) +if os.path.isfile(sys.argv[1]): + ProcessFilesFromDME(sys.argv[1], sys.argv[2]) diff --git a/tools/BYONDTools/scripts/dmi.py b/tools/BYONDTools/scripts/dmi.py new file mode 100644 index 00000000000..db8db2abe82 --- /dev/null +++ b/tools/BYONDTools/scripts/dmi.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +""" +DMI SPLITTER-UPPER THING + +Makes merging sprites a hell of a lot easier. +by N3X15 + +Requires PIL +Written for Python 2.7. +""" + +import sys, os, traceback, fnmatch, argparse + +from byond.DMI import DMI + +args = () + +def main(): + opt = argparse.ArgumentParser() # version='0.1') + opt.add_argument('-p', '--suppress-post-processing', dest='suppress_post_process', default=False, action='store_true') + command = opt.add_subparsers(help='The command you wish to execute', dest='MODE') + + _disassemble = command.add_parser('disassemble', help='Disassemble a single DMI file to a destination directory') + _disassemble.add_argument('file', type=str, help='The DMI file to disassemble.', metavar='file.dmi') + _disassemble.add_argument('destination', type=str, help='The directory in which to dump the resulting images.', metavar='dest/') + + _disassemble_all = command.add_parser('disassemble-all', help='Disassemble a directory of DMI files to a destination directory') + _disassemble_all.add_argument('source', type=str, help='The DMI files to disassemble.', metavar='source/') + _disassemble_all.add_argument('destination', type=str, help='The directory in which to dump the resulting images.', metavar='dest/') + + _compile = command.add_parser('compile', help='Compile a .dmi.mak file') + _compile.add_argument('makefile', type=str, help='The .dmi.mak file to compile.', metavar='file.dmi.mak') + _compile.add_argument('destination', type=str, help='The location of the resulting .dmi file.', metavar='file.dmi') + + _compare = command.add_parser('compare', help='Compare two DMI files and note the differences') + _compare.add_argument('theirs', type=str, help='One side of the difference', metavar='theirs.dmi') + _compare.add_argument('mine', type=str, help='The other side.', metavar='mine.dmi') + + _compare_all = command.add_parser('compare-all', help='Compare two DMI file directories and note the differences') + _compare_all.add_argument('theirs', type=str, help='One side of the difference', metavar='theirs/') + _compare_all.add_argument('mine', type=str, help='The other side.', metavar='mine/') + _compare_all.add_argument('report', type=str, help='The file the report is saved to', metavar='report.txt') + + _get_dmi_data = command.add_parser('get-dmi-data', help='Extract DMI header') + _get_dmi_data.add_argument('file', type=str, help='DMI file', metavar='file.dmi') + _get_dmi_data.add_argument('dest', type=str, help='The file where the DMI header will be saved', metavar='dest.txt') + + _set_dmi_data = command.add_parser('set-dmi-data', help='Set DMI header') + _set_dmi_data.add_argument('file', type=str, help='One side of the difference', metavar='file.dmi') + _set_dmi_data.add_argument('metadata', type=str, help='DMI header file', metavar='metadata.txt') + + _set_dmi_data = command.add_parser('clean', help='Clean up temporary files and *.new.dmi files.') + _set_dmi_data.add_argument('basedir', type=str, help='Starting directory', metavar='vgstation/') + + args = opt.parse_args() + #print(args) + if args.MODE == 'compile': + make_dmi(args.makefile, args.destination, args) + if args.MODE == 'compare': + compare(args.theirs, args.mine, args, sys.stdout) + if args.MODE == 'compare-all': + compare_all(args.theirs, args.mine, args.report, args) + elif args.MODE == 'disassemble': + disassemble(args.file, args.destination, args) + elif args.MODE == 'disassemble-all': + disassemble_all(args.source, args.destination, args) + elif args.MODE == 'get-dmi-data': + get_dmi_data(args.file, args.dest, args) + elif args.MODE == 'set-dmi-data': + set_dmi_data(args.file, args.metadata, args) + elif args.MODE == 'cleanup': + cleanup(args.basedir, args) + else: + print('!!! Error, unknown MODE=%r' % args.MODE) + + + +class ModeAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + # print('%s %s %s' % (namespace, values, option_string)) + namespace.MODE = self.dest + namespace.args = values + +def get_dmi_data(path, dest, parser): + if(os.path.isfile(path)): + dmi = DMI(path) + with open(dest, 'w') as f: + f.write(dmi.getHeader()) + +def set_dmi_data(path, headerFile, parser): + if(os.path.isfile(path)): + dmi = DMI(path) + with open(headerFile, 'r') as f: + dmi.setHeader(f.read(), path) + +def make_dmi(path, dest, parser): + if(os.path.isfile(path)): + dmi = None + try: + dmi = DMI(dest) + dmi.make(path) + dmi.save(dest) + except SystemError as e: + print("!!! Received SystemError in %s, halting: %s" % (dmi.filename, traceback.format_exc(e))) + print('# of cells: %d' % len(dmi.states)) + print('Image h/w: %s' % repr(dmi.size)) + sys.exit(1) + except Exception as e: + print("Received error, continuing: %s" % traceback.format_exc()) + +def disassemble(path, to, parser): + print('\tD %s -> %s' % (path, to)) + if(os.path.isfile(path)): + dmi = None + try: + dmi = DMI(path) + dmi.extractTo(to, parser.suppress_post_process) + except SystemError as e: + print("!!! Received SystemError in %s, halting: %s" % (dmi.filename, traceback.format_exc(e))) + print('# of cells: %d' % len(dmi.states)) + print('Image h/w: %s' % repr(dmi.size)) + sys.exit(1) + except Exception as e: + print("Received error, continuing: %s" % traceback.format_exc()) + +def compare(theirsfile, minefile, parser, reportstream, **kwargs): + # print('\tD %s -> %s' % (theirsfile, minefile)) + theirs = [] + theirsDMI = None + mine = [] + mineDMI = None + states = [] + + new2mineFilename = minefile.replace('.dmi', '.new.dmi') + new2theirsFilename = theirsfile.replace('.dmi', '.new.dmi') + + new2mine=None + if os.path.isfile(new2mineFilename): + os.remove(new2mineFilename) + if kwargs.get('newfile_mine',True): + new2mine = DMI(new2mineFilename) + + new2theirs=None + if os.path.isfile(new2theirsFilename): + os.remove(new2theirsFilename) + if kwargs.get('newfile_theirs',False): + new2theirs = DMI(new2theirsFilename) + + + o = '' + if(os.path.isfile(theirsfile)): + try: + theirsDMI = DMI(theirsfile) + theirsDMI.loadAll() + theirs = theirsDMI.states + except SystemError as e: + print("!!! Received SystemError in %s, halting: %s" % (theirs.filename, traceback.format_exc(e))) + print('# of cells: %d' % len(theirs.states)) + print('Image h/w: %s' % repr(theirs.size)) + sys.exit(1) + except Exception as e: + print("Received error, continuing: %s" % traceback.format_exc()) + o += "\n {0}: Received error, continuing: {1}".format(theirsfile, traceback.format_exc()) + for stateName in theirs: + if stateName not in states: + states.append(stateName) + if(os.path.isfile(minefile)): + try: + mineDMI = DMI(minefile) + mineDMI.loadAll() + mine = mineDMI.states + except SystemError as e: + print("!!! Received SystemError in %s, halting: %s" % (mine.filename, traceback.format_exc(e))) + print('# of cells: %d' % len(mine.states)) + print('Image h/w: %s' % repr(mine.size)) + sys.exit(1) + except Exception as e: + print("Received error, continuing: %s" % traceback.format_exc()) + o += "\n {0}: Received error, continuing: {1}".format(minefile, traceback.format_exc()) + for stateName in mine: + if stateName not in states: + states.append(stateName) + for state in sorted(states): + inTheirs = state in theirs + inMine = state in mine + if inTheirs and not inMine: + o += '\n + {1}'.format(minefile, state) + if new2mine is not None: + new2mine.states[state] = theirsDMI.states[state] + elif not inTheirs and inMine: + o += '\n - {1}'.format(theirsfile, state) + if new2theirs is not None: + new2theirs.states[state] = mineDMI.states[state] + elif inTheirs and inMine: + if theirs[state].ToString() != mine[state].ToString(): + o += '\n - {0}: {1}'.format(mine[state].displayName(), mine[state].ToString()) + o += '\n + {0}: {1}'.format(theirs[state].displayName(), theirs[state].ToString()) + elif kwargs.get('check_changed',True): + diff_count=0 + for i in xrange(len(theirs[state].icons)): + theirF = theirs[state].icons[i] + myF = theirs[state].icons[i] + + theirData = list(theirF.getdata()) + myData = list(myF.getdata()) + #diff = [] + + for i in xrange(len(theirData)): + dr = theirData[i][0] - myData[i][0] + dg = theirData[i][1] - myData[i][1] + db = theirData[i][2] - myData[i][2] + #diff[idx] = (abs(dr), abs(dg), abs(db)) + if((dr != 0) or (dg != 0) or (db != 0)): + diff_count += 1 + break + if diff_count > 0: + o += '\n ! {0}: {1} frames differ'.format(theirs[state].displayName(), diff_count) + if new2mine is not None: + new2mine.states[state] = theirsDMI.states[state] + if new2theirs is not None: + new2theirs.states[state] = mineDMI.states[state] + if o != '': + reportstream.write('\n--- {0}'.format(theirsfile)) + reportstream.write('\n+++ {0}'.format(minefile)) + reportstream.write(o) + + if new2mine is not None: + if len(new2mine.states) > 0: + new2mine.save(new2mineFilename) + else: + if os.path.isfile(new2mineFilename): + os.remove(new2mineFilename) + #print('RM {0}'.format(new2mineFilename)) + if new2theirs is not None: + if len(new2theirs.states) > 0: + new2theirs.save(new2theirsFilename) + else: + if os.path.isfile(new2theirsFilename): + os.remove(new2theirsFilename) + #print('RM {0}'.format(new2theirsFilename)) + +def cleanup(subject): + print('Cleaning...') + for root, _, filenames in os.walk(subject): + for filename in fnmatch.filter(filenames, '*.new.dmi'): + path = os.path.join(root, filename) + print('RM {0}'.format(path)) + os.remove(path) + +def disassemble_all(in_dir, out_dir, parser): + print('D_A %s -> %s' % (in_dir, out_dir)) + for root, dirnames, filenames in os.walk(out_dir): + for filename in fnmatch.filter(filenames, '*.new.dmi'): + path = os.path.join(root, filename) + print('RM {0}'.format(path)) + os.remove(path) + for root, dirnames, filenames in os.walk(in_dir): + for filename in fnmatch.filter(filenames, '*.dmi'): + path = os.path.join(root, filename) + to = os.path.join(out_dir, path.replace(in_dir, '').replace(os.path.basename(path), '')) + disassemble(path, to, parser) + + +def compare_all(in_dir, out_dir, report, parser, **kwargs): + with open(report, 'w') as report: + report.write('# DMITool Difference Report: {0} {1}'.format(os.path.abspath(in_dir), os.path.abspath(out_dir))) + for root, dirnames, filenames in os.walk(in_dir): + for filename in fnmatch.filter(filenames, '*.dmi'): + path = os.path.join(root, filename) + to = os.path.join(out_dir, path.replace(in_dir, '').replace(os.path.basename(path), '')) + to = os.path.join(to, filename) + path = os.path.abspath(path) + to = os.path.abspath(to) + compare(path, to, parser, report, **kwargs) + + +if __name__ == '__main__': + main() diff --git a/tools/BYONDTools/scripts/dmindent.py b/tools/BYONDTools/scripts/dmindent.py new file mode 100644 index 00000000000..e4f8979ff0c --- /dev/null +++ b/tools/BYONDTools/scripts/dmindent.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +''' +Created on Jan 5, 2014 + +@author: Rob +''' +import os, sys, argparse +from byond.basetypes import Atom, Proc +from byond import objtree, GetFilesFromDME + +def processFile(tree, origin, destination, args): + atomsWritten=[] + origin = os.path.relpath(origin) + with open(destination, 'w') as f: + print('>>> {0}'.format(destination)) + if args.reorganize: + for ak in sorted(tree.Atoms.keys()): + atom = tree.Atoms[ak] + for a in atomsWritten: + if atom.path.startswith(a): continue + #if atom.filename.replace(os.sep,'/') == sys.argv[2]: + if atom.filename == origin: + f.write(atom.DumpCode()) + atomsWritten+=[atom.path] + else: + for thing in tree.fileLayouts[origin]: + ttype = thing[0] + if ttype == 'ATOMDEF': + f.write(thing[1] + '\n') + elif ttype == 'PROCDEF': + proc = tree.GetAtom(thing[1]) + f.write(proc.DumpCode() + '\n') + elif ttype == 'VAR': + atom = tree.GetAtom(thing[1]) + var = atom.properties[thing[2]] + f.write('\t{}\n'.format(var.DumpCode(thing[2]))) + elif ttype == 'DEFINE': + name = thing[1] + value = thing[2] + f.write('#define {} {}\n'.format(name, value)) + elif ttype == 'UNDEF': + name = thing[1] + f.write('#undef {}\n'.format(name)) + elif ttype == 'COMMENT': + continue + else: + print('wot is ' + ttype + '?') +if __name__ == '__main__': + + opt = argparse.ArgumentParser() + opt.add_argument('project', metavar="project.dme") + opt.add_argument('file', metavar="file.dm", default=None, nargs='?') + opt.add_argument('--reorganize', dest='reorganize', default=False, action='store_true', help="Reorganize the file's contents, instead of keeping current structure.") + opt.add_argument('--output','-o', dest='output', default='', help="Where to put the output (default: .fixed)") + + args = opt.parse_args() + + tree = objtree.ObjectTree(preprocessor_directives=True) + tree.ProcessFilesFromDME(args.project) + + atomsWritten = [] + + if args.file is not None and os.path.isfile(args.file): + output=args.output + if output=='': + output=args.file+'.fixed' + processFile(tree, args.file, output, args) + else: + for filename in GetFilesFromDME(args.project): + fixpath = filename.replace('code' + os.sep, 'code-indented' + os.sep) + fixpath = fixpath.replace('interface' + os.sep, 'interface-indented' + os.sep) + fixpath = fixpath.replace('RandomZLevels' + os.sep, 'RandomZLevels-indented' + os.sep) + fixdir = os.path.dirname(fixpath) + if not os.path.isdir(fixdir): + os.makedirs(fixdir) + fixpath = fixpath.replace('/', os.sep) + processFile(tree, filename, fixpath,args) diff --git a/tools/BYONDTools/scripts/dmm.py b/tools/BYONDTools/scripts/dmm.py new file mode 100644 index 00000000000..62eeec1c9c9 --- /dev/null +++ b/tools/BYONDTools/scripts/dmm.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python +""" +dmm.py - Collection of map tools. + +Copyright 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +import sys, argparse, os +from byond import ObjectTree, Map, MapRenderFlags + +def main(): + opt = argparse.ArgumentParser() # version='0.1') + opt.add_argument('--project', default='baystation12.dme', type=str, help='Project file.', metavar='environment.dme') + + command = opt.add_subparsers(help='The command you wish to execute', dest='MODE') + + _compare = command.add_parser('diff', help='Compare two map files and generate a map patch.') + _compare.add_argument('theirs', type=str, help='One side of the difference', metavar='theirs.dmm') + _compare.add_argument('mine', type=str, help='The other side.', metavar='mine.dmm') + _compare.add_argument('-O', '--output', dest='output', type=str, help='The other side.', metavar='mine.dmm') + + #_compare = command.add_parser('patch', help='Apply a map patch.') + #_compare.add_argument('theirs', type=str, help='One side of the difference', metavar='theirs.dmm') + #_compare.add_argument('mine', type=str, help='The other side.', metavar='mine.dmm') + + _compare = command.add_parser('analyze', help='Generate a report of each atom on a map. WARNING: huge') + _compare.add_argument('map', type=str, help='Map to analyze.', metavar='map.dmm') + + opt.add_argument('map', type=str, help='Map to fix.', metavar='map.dmm') + + args = opt.parse_args() + if args.MODE == 'compare': + compare_dmm(args) + elif args.MODE == 'analyze': + analyze_dmm(args) + #elif args.MODE == 'patch': + # patch_dmm(args) + else: + print('!!! Error, unknown MODE=%r' % args.MODE) + +def compare_dmm(args): + if not os.path.isfile(args.theirs): + print('File {0} does not exist.'.format(args.theirs)) + sys.exit(1) + if not os.path.isfile(args.mine): + print('File {0} does not exist.'.format(args.mine)) + sys.exit(1) + if not os.path.isfile(args.project): + print('DM Environment File {0} does not exist.'.format(args.project)) + sys.exit(1) + + theirs_dmm = Map(forgiving_atom_lookups=True) + theirs_dmm.readMap(args.theirs) + + mine_dmm = Map(forgiving_atom_lookups=True) + mine_dmm.readMap(args.mine) + + if theirs_dmm.width != mine_dmm.width: + print('Width is not equal: {} != {}.'.format(theirs_dmm.width, mine_dmm.width)) + sys.exit(1) + if theirs_dmm.height != mine_dmm.height: + print('Height is not equal: {} != {}.'.format(theirs_dmm.height, mine_dmm.height)) + sys.exit(1) + + ttitle, _ = os.path.splitext(os.path.basename(args.theirs)) + mtitle, _ = os.path.splitext(os.path.basename(args.mine)) + + output = '{} - {}.dmmpatch'.format(ttitle, mtitle) + if args.output: + output = args.output + with open(output, 'w') as f: + stats = { + 'diffs':0, + 'tilediffs':0 + } + print('Comparing maps...') + for z in range(len(theirs_dmm.zLevels)): + for y in range(theirs_dmm.height): + for x in range(theirs_dmm.width): + CHANGES = {} + + tTile = theirs_dmm.GetTileAt(x, y, z) + mTile = mine_dmm.GetTileAt(x, y, z) + + theirs = tTile.GetAtoms() + mine = mTile.GetAtoms() + + for atom in theirs + mine: + key = atom.MapSerialize() + change = None + if atom not in mine: + change = '-' + if atom not in theirs: + change = '+' + if change is not None: + CHANGES[key] = change + + if len(CHANGES) > 0: + f.write('<{},{},{}>\n'.format(x, y, z)) + stats['tilediffs'] += 1 + for key, change in CHANGES.items(): + f.write(' {} {}\n'.format(change, key)) + stats['diffs'] += 1 + print('Compared maps: {} differences in {} tiles.'.format(stats['diffs'],stats['tilediffs'])) + + +def analyze_dmm(args): + tmpl_head = ''' + + + BYONDTools Map Analysis :: {TITLE} + + +

{TITLE}

+ ''' + tmpl_footer = ''' + + + ''' + + presentable_attributes=[ + 'path', + 'id', + 'filename', + 'line' + ] + + def MakePage(**kwargs): + rewt = '.' + depth = kwargs.get('depth', 0) + if depth > 0: + rewt = '/'.join(['..'] * depth) + title = kwargs.get('title', 'LOL NO TITLE') + body = kwargs.get('body', '') + return (tmpl_head + body + tmpl_footer).replace('{TITLE}', title).replace('{ROOT}', rewt) + + if not os.path.isfile(args.project): + print('DM Environment file {0} does not exist.'.format(args.theirs)) + sys.exit(1) + + if not os.path.isfile(args.map): + print('Map {0} does not exist.'.format(args.theirs)) + sys.exit(1) + + tree = ObjectTree() + tree.ProcessFilesFromDME(args.project) + dmm = Map(tree) + dmm.readMap(args.map) + + basedir = os.path.join(os.path.dirname(args.project), 'analysis', os.path.basename(args.map)) + + if not os.path.isdir(basedir): + os.makedirs(os.path.join(basedir,'instances')) + os.makedirs(os.path.join(basedir,'tiles')) + + # Dump instances + instance_info = {} + for atom in dmm.instances: + if atom.path not in instance_info: + instance_info[atom.path] = [] + instance_info[atom.path] += [atom.id] + + with open(os.path.join(basedir, 'instances', str(atom.id) + '.html'), 'w') as f: + body = '

Atom Data:

' + for attr in presentable_attributes: + body += ''.format(attr, getattr(atom, attr, None)) + body += '
NameValue
{0}{1}
' + + body += '

Map-Specified Properties:

' + for attr_name in atom.mapSpecified: + body += ''.format(attr_name, atom.getProperty(attr_name, None)) + body += '
NameValue
{0}{1}
' + + body += '

All Properties:

' + for attr_name in sorted(atom.properties.keys()): + attr = atom.properties[attr_name] + body += ''.format(attr_name, attr.value, attr.filename, attr.line) + body += '
NameValueFile/Line
{0}{1}{2}:{3}
' + f.write(MakePage(title='Instance #{0}'.format(atom.id), depth=1, body=body)) + with open(os.path.join(basedir, 'instances', 'index.html'), 'w') as idx: + body = '
    ' + for atype, instances in instance_info.items(): + body += '
  • {0}
      '.format(atype) + for iid in instances: + body += '
    • #{0}
    • '.format(iid) + body += '
  • ' + body += "
" + idx.write(MakePage(title='Instance Index'.format(atom.id), depth=1, body=body)) + + # Tiles + with open(os.path.join(basedir, 'index.html'), 'w') as f: + body = '' + for tile in dmm.tileTypes: + body += '' + img = tile.RenderToMapTile(0, os.path.dirname(sys.argv[1]), MapRenderFlags.RENDER_STARS) + if img is None: continue + pass_2 = tile.RenderToMapTile(1, os.path.dirname(sys.argv[1]), 0) + if pass_2 is not None: + img.paste(pass_2,(0,0,96,96),pass_2) + img.save(os.path.join(basedir, 'tiles', '{0}.png'.format(tile.ID)), 'PNG') + body += '
IconIDInstances
{0}
    '.format(tile.ID) + for atom in tile.SortAtoms(): + body += '
  • #{0} - {1}
  • '.format(atom.id,atom.path) + body += '
' + f.write(MakePage(title='Tile Index'.format(atom.id), depth=0, body=body)) + +if __name__ == '__main__': + main() diff --git a/tools/BYONDTools/scripts/dmmfix.py b/tools/BYONDTools/scripts/dmmfix.py new file mode 100644 index 00000000000..5d2dfe5fd17 --- /dev/null +++ b/tools/BYONDTools/scripts/dmmfix.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +""" +fixMap.py - Apply various fixes to a map. + +Copyright 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +import sys, argparse +from byond.map import Map +from byond.objtree import ObjectTree +#from byond.basetypes import BYONDString, BYONDValue, Atom, PropertyFlags +#from byond.directions import * + +from byond import mapfixes + + +opt = argparse.ArgumentParser() # version='0.1') +opt.add_argument('-n', '--namespace', dest='namespaces', type=str, nargs='*', default=[], help='MapFix namespace to load (ss13, vgstation).') +opt.add_argument('-N', '--no-deps', dest='no_dependencies', action='store_true', help='Stop loading of namespace dependencies.') +opt.add_argument('-f', '--fix-script', dest='fixscripts', type=str, nargs='*', help='A script that specifies property and type replacements.') + +opt.add_argument('dme', nargs='?', default='baystation12.dme', type=str,help='Project file.', metavar='environment.dme') +opt.add_argument('map', type=str,help='Map to fix.', metavar='map.dmm') + +args = opt.parse_args() + +actions = [] + +mapfixes.Load() +actions += mapfixes.GetFixesForNS(args.namespaces, not args.no_dependencies) + +tree = ObjectTree() +tree.ProcessFilesFromDME(args.dme) + +for fixscript in args.fixscripts: + with open(fixscript, 'r') as repl: + ln = 0 + errors = 0 + for line in repl: + ln += 1 + if line.startswith('#'): + continue + if line.strip() == '': + continue + # PROPERTY: step_x > pixel_x + # TYPE: /obj/item/key > /obj/item/weapon/key/janicart + subject, action = line.split(':') + subject = subject.lower() + if subject == 'property': + old, new = action.split('>') + actions += [mapfixes.base.RenameProperty(old.strip(), new.strip())] + if subject == 'type' or subject == 'type!': + force = subject == 'type!' + old, new = action.split('>') + newtype = new.strip() + if tree.GetAtom(newtype) is None: + print('{0}:{1}: {2}'.format(sys.argv[2], ln, line.strip('\r\n'))) + print(' ERROR: Unable to find replacement type "{0}".'.format(newtype)) + errors += 1 + actions += [mapfixes.base.ChangeType(old.strip(), newtype, force)] + if errors > 0: + print('!!! {0} errors, please fix them.'.format(errors)) + sys.exit(1) +print('Changes to make:') +for action in actions: + print(' * ' + str(action)) +dmm = Map(tree, forgiving_atom_lookups=1) +dmm.readMap(args.map) +dmm.writeMap2(args.map.replace('.dmm', '.dmm2')) +for iid in xrange(len(dmm.instances)): + atom = dmm.getInstance(iid) + changes = [] + for action in actions: + action.SetTree(tree) + if action.Matches(atom): + atom = action.Fix(atom) + changes += [str(action)] + atom.id = iid + + ''' + compiled_atom = tree.GetAtom(atom.path) + if compiled_atom is not None: + for propname in list(atom.properties.keys()): + if propname not in compiled_atom.properties and propname not in ('req_access_txt','req_one_access_txt'): + del atom.properties[propname] + if propname in atom.mapSpecified: + atom.mapSpecified.remove(propname) + changes += ['Dropped property {0} (not found in compiled atom)'.format(propname)] + ''' + dmm.setInstance(iid, atom) + if len(changes) > 0: + print('{0} (#{1}):'.format(atom.path, atom.id)) + for change in changes: + print(' * ' + change) +#for atom, _ in atomsToFix.items(): +# print('Atom {0} needs id_tag.'.format(atom)) +with open(args.map + '.missing', 'w') as f: + for atom in sorted(dmm.missing_atoms): + f.write(atom + "\n") +print('--- Saving...') +dmm.writeMap(args.map + '.fixed', Map.WRITE_OLD_IDS) +dmm.writeMap2(args.map.replace('.dmm', '.dmm2') + '.fixed') diff --git a/tools/BYONDTools/scripts/dmmrender.py b/tools/BYONDTools/scripts/dmmrender.py new file mode 100644 index 00000000000..46d5f07a0de --- /dev/null +++ b/tools/BYONDTools/scripts/dmmrender.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +import os, argparse, logging +from byond.objtree import ObjectTree +from byond.basetypes import Atom +from byond.map import Map, MapRenderFlags +""" +Usage: + $ python dmmrender.py path/to/your/project.dme path/to/your/map.dmm + +generateMap.py - Creates an image of a DMM map. + +Copyright 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +def renderMap(args): + outfile = args.map + '.{z}.png' + if args.area: + kwargs['area'] = args.area + outfile = args.area[0].replace('/', '_') + '.png' + if args.render_types: + print(repr(args.render_types)) + kwargs['render_types'] = args.render_types + if args.outfile: + outfile = args.outfile + if os.path.isdir(outfile): + title, _ = os.path.splitext(os.path.basename(args.map)) + outfile = os.path.join(outfile, '{}.{{z}}.png'.format(title)) + dmm.generateImage(outfile, os.path.dirname(args.project), renderflags, **kwargs) + +logging.basicConfig( + format='%(asctime)s [%(levelname)-8s]: %(message)s', + datefmt='%m/%d/%Y %I:%M:%S %p', + level=logging.INFO # , + # filename='logs/main.log', + # filemode='w' + ) +opt = argparse.ArgumentParser() +opt.add_argument('project', metavar="project.dme") +opt.add_argument('map', metavar="map.dmm") +opt.add_argument('--render-stars', dest='render_stars', default=False, action='store_true', help="Render space. Normally off to prevent ballooning the image size.") +opt.add_argument('--render-areas', dest='render_areas', default=False, action='store_true', help="Render area overlays.") +opt.add_argument('--render-only', dest='render_types', action='append', help="Render ONLY these types. Can be used multiple times to specify more types.") +opt.add_argument('--area', dest='area', type=list, nargs='*', default=None, help="Specify an area to restrict rendering to.") +opt.add_argument('-O', '--out', dest='outfile', type=str, default=None, help="What to name the file ({z} will be replaced with z-level)") +opt.add_argument('--area-list', dest='areas', type=str, default=None, help="A file with area_file.png = /area/path on each line") +args = opt.parse_args() +if os.path.isfile(args.project): + tree = ObjectTree() + tree.ProcessFilesFromDME(args.project) + dmm = Map(tree) + dmm.readMap(args.map) + renderflags = 0 + if args.render_stars: + renderflags |= MapRenderFlags.RENDER_STARS + if args.render_areas: + renderflags |= MapRenderFlags.RENDER_AREAS + kwargs = {} + if args.areas: + with open(args.areas) as f: + for line in f: + if line.startswith("#"): + continue + if '=' not in line: + continue + outfile, area = line.split('=') + args.area = area.strip().split(',') + args.outfile = outfile.strip() + renderMap(args) + else: + renderMap(args) diff --git a/tools/BYONDTools/scripts/genTileQuads.py b/tools/BYONDTools/scripts/genTileQuads.py new file mode 100644 index 00000000000..69c16e00730 --- /dev/null +++ b/tools/BYONDTools/scripts/genTileQuads.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +from byond.DMI import DMI,State +from byond import directions +import os, sys, shutil +import ImageChops +import math, operator +from PIL import Image + +def rmsdiff(im1, im2): + "Calculate the root-mean-square difference between two images" + diff = ImageChops.difference(im1, im2) + h = diff.histogram() + sq = (value * (idx ** 2) for idx, value in enumerate(h)) + sum_of_squares = sum(sq) + rms = math.sqrt(sum_of_squares / float(im1.size[0] * im1.size[1])) + return rms + +def equal(im1, im2): + return ImageChops.difference(im1, im2).getbbox() is None + +quadDefs = [ + # x1,y1,x2,y2 + [0, 0, 15, 15], + [16, 0, 31, 15], + [0, 16, 15, 31], + [16, 16, 31, 31] +] + +extract = [ + 'floor', + # 'redcorner', + 'white', + 'dark', + 'bar', + 'cafeteria', + 'redfull', + 'whiteredfull', + 'bluefull', + 'whitebluefull', + 'greenfull', + 'whitegreenfull', + 'yellowfull', + 'whiteyellowfull', + 'neutralfull', + 'orangefull', + 'purplefull', + 'whitepurplefull', + 'floorgrime', + 'brown', + 'vaultfull' +] + +colors = [ + 'red', + 'whitered', + 'blue', + 'whiteblue', + 'green', + 'whitegreen', + 'yellow', + 'whiteyellow', + 'neutral', + 'orange', + 'purple', + 'whitepurple', + 'grime', + 'brown', + 'vault' +] + +arrangements={ + 'stripe': [ + [False,False,True,True], # S + [True,True,False,False], # N + [False,True,False,True], # E + [True,False,True,False], # W + [False,True,True,True], # SE + [True,False,True,True], # SW + [True,True,False,True], # NE + [True,True,True,False], # NW + ], + 'corner': [ + [False,False,False,True], # S + [True,False,False,False], # N + [False,True,False,False], # E + [False,False,True,False], # W + ], + 'full': [ + [True,True,True,True] + ] +} + +tileDefs={ + 'white':{ + 'base':'white', + 'arrangements':['stripe','corner','full'], + 'colors':['whitered','whiteblue','whitegreen','whitegreen','whiteyellow','whitepurple'] + }, + 'grey':{ + 'base':'floor', + 'arrangements':['stripe','corner','full'], + 'colors':[ + 'red', + 'blue', + 'green', + 'yellow', + 'neutral', + 'orange', + 'purple', + 'brown', + 'white' + ] + }, + 'dark':{ + 'base':'dark', + 'arrangements':['stripe','corner','full'], + 'colors':[ + 'floor', + 'red', + 'blue', + 'green', + 'yellow', + 'neutral', + 'orange', + 'purple', + 'brown', + 'vault' + ] + } +} +knownQuads = [ + {}, {}, {}, {} +] +def isKnownQuad(i, im, icon_state): + for name, kq in knownQuads[i].items(): + if equal(kq, im): + return True + return False +floors = DMI(sys.argv[1]) +floors.loadAll() +basedir = 'floor_quads' +if os.path.isdir('floor_quads'): + shutil.rmtree(basedir) +os.makedirs('floor_quads') +for icon_state in extract: + if icon_state not in floors.states: + print('Can\'t find state {0}!'.format(icon_state)) + continue + for d in range(floors.states[icon_state].dirs): + dirf = 0 + dirn = 'SOUTH' + if floors.states[icon_state].dirs > 1: + dirf = directions.IMAGE_INDICES[d] + dirn = directions.getNameFromDir(dirf) + print('{0} {1} {2}'.format(icon_state, dirn, floors.states[icon_state].dirs)) + img = floors.getFrame(icon_state, dirf, 0) + for i in range(len(quadDefs)): + quad = img.crop(quadDefs[i]) + if isKnownQuad(i, quad, icon_state): + print(' Skipping quad #{}'.format(i + 1)) + continue + color = icon_state + if color.endswith('full'): + color = color[:-4] + qpath = os.path.join(basedir, str(i)) + qfile = os.path.join(qpath, '{0}.png'.format(color)) + if not os.path.isdir(qpath): + os.makedirs(qpath) + quad.save(qfile, 'PNG') + knownQuads[i][color] = quad + +nfloors = DMI('nfloors.dmi') +for tileName,tileDef in tileDefs.items(): + base = floors.getFrame(tileDef['base'],directions.SOUTH,0) + for color in tileDef['colors']: + for arrangement in tileDef['arrangements']: + arrname='' + arrange=[False,False,False,False] + if isinstance(arrangement,str): + arrange=arrangements[arrangement] + arrname=arrangement + elif isinstance(arrangement,list): + arrange=arrangement[1:] + arrname=arrangement[0] + state='{base} {color} {arrangement}'.format(base=tileDef['base'],color=color,arrangement=arrname) + nstate=State(state) + nstate.dirs=len(arrange) + nstate.frames=1 + nstate.icons=[None for _ in range(nstate.dirs)] + #statedebug = DMI(state+'.dmi') + nfloors.states[state]=nstate + for d in range(len(arrange)): + cmap = arrange[d] + dirf = 0 + dirn = 'SOUTH' + if len(arrange) > 1: + dirf = directions.IMAGE_INDICES[d] + dirn = directions.getNameFromDir(dirf) + print(' Generating state {0} ({1})...'.format(repr(state),dirn)) + #print(repr(arrange)) + img=Image.new('RGBA',(32,32)) + img.paste(base) + for i in range(len(cmap)): + bbox=quadDefs[i] + if cmap[i]: + img.paste(knownQuads[i][color],bbox) + nfloors.setFrame(state, dirf, 0, img) + #statedebug.states[state]=nfloors.states[state] + #statedebug.save(state+'.dmi') +nfloors.save('nfloors.dmi') \ No newline at end of file diff --git a/tools/BYONDTools/scripts/makeBigDMI.py b/tools/BYONDTools/scripts/makeBigDMI.py new file mode 100644 index 00000000000..aed318518e3 --- /dev/null +++ b/tools/BYONDTools/scripts/makeBigDMI.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +''' +Created on Feb 28, 2014 to test a bug in DreamMaker. + +Requires arial font. +''' +from byond.DMI import DMI, State +from PIL import Image, ImageDraw, ImageFont + +def makeDMI(): + dmi = DMI('state_limit.dmi') + # + for i in range(513): + # Make a new tile + img = Image.new('RGBA', (32, 32)) + # Set up PIL's drawing stuff + draw = ImageDraw.Draw(img) + # Define a font. + font = ImageFont.truetype('arial.ttf', 10) + # Draw the tile number + draw.text((10, 0), str(i + 1), (0, 0, 0), font=font) + # Make state + state_name='state {0}'.format(i+1) + state=State(state_name) + state.dirs=1 + state.frames=1 + state.icons=[img] + # Add state to DMI + dmi.states[state_name]=state + #save + dmi.save('state_limit.dmi', sort=False) +if __name__ == '__main__': + makeDMI() diff --git a/tools/BYONDTools/scripts/queryObjectTree.py b/tools/BYONDTools/scripts/queryObjectTree.py new file mode 100644 index 00000000000..c00c889f454 --- /dev/null +++ b/tools/BYONDTools/scripts/queryObjectTree.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +''' +Created on Apr 29, 2014 + +@author: Rob +''' +import argparse,os + +from byond.objtree import ObjectTree +from byond.basetypes import Proc + +def dumpSubTypes(atom): + print('{}:{}: {}'.format(atom.filename,atom.line,atom.path)) + for rpath,catom in atom.children.items(): + if not isinstance(catom,Proc): + dumpSubTypes(catom) + +if __name__ == '__main__': + opt = argparse.ArgumentParser() + opt.add_argument('project', metavar="project.dme") + opt.add_argument('--subtypes',type=str,help="List all subtypes of the given type") + args = opt.parse_args() + if os.path.isfile(args.project): + tree = ObjectTree() + tree.ProcessFilesFromDME(args.project) + if args.subtypes: + atom = tree.GetAtom(args.subtypes) + dumpSubTypes(atom) \ No newline at end of file diff --git a/tools/BYONDTools/scripts/scanCode.py b/tools/BYONDTools/scripts/scanCode.py new file mode 100644 index 00000000000..bfa62bd569c --- /dev/null +++ b/tools/BYONDTools/scripts/scanCode.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +import sys, re +from byond.basetypes import Proc +from byond.objtree import ObjectTree + +class CodeScan: + def __init__(self, id): + self.id = id + self.atom = None + self.proc = None + + def SetContext(self, atom, proc): + self.atom = atom + self.proc = proc + + def MatchAtom(self, atom): + return True + + def MatchProc(self, atom, proc): + return True + + def ScanLine(self, line): + return [] + +class RegexScanner(CodeScan): + def __init__(self, id, regex, warning, in_procnames=()): + CodeScan.__init__(self, id) + self.regex = regex + self.warning = warning + self.in_procnames = in_procnames + + def MatchProc(self, atom, proc): + # print(proc.path) + return self.in_procnames == () or proc.name in self.in_procnames + + def ScanLine(self, line): + m = self.regex.search(line) + if m is not None: + return [self.warning] + return [] + +rules = [ + RegexScanner( + id='sleep-in-process', + warning='sleep() should not be used in process().', + regex=re.compile(r'\bsleep\(([0-9]+)\)\b'), + in_procnames=('process') + ), + RegexScanner( + id='spawn-in-process', + warning='spawn() should not be used in process().', + regex=re.compile(r'\bspawn(\(([0-9]*)\))?\b'), + in_procnames=('process') + ), + RegexScanner( + id='del-in-ex_act', + warning='del() should not be used in ex_act(). Consider replacing with qdel.', + regex=re.compile(r'\bdel(\(.*\))?\b'), + in_procnames=('ex_act') + ), +] + +otr = ObjectTree() +otr.ProcessFilesFromDME(sys.argv[1], '.dm') +procs = 0 +alerts = 0 +for path in otr.Atoms: + atom = otr.GetAtom(path) + if isinstance(atom,Proc): continue + proceed = False + skipping = [] + for rule in rules: + if rule.MatchAtom(atom): + proceed = True + else: + skipping += [rule] + if not proceed: continue + for childName in atom.children: + warnings = {} + child = atom.children[childName] + if isinstance(child, Proc): + proceed = False + for rule in rules: + if rule not in skipping and rule.MatchProc(atom, child): + proceed = True + else: + skipping += [rule] + if not proceed: continue + + ''' + if 'process' in path + ' - ' + childName: + print(path + ' - ' + childName) + ''' + # print(path + ' - ' + childName) + procs += 1 + min_indent = child.getMinimumIndent() + # Should be 1, so find the difference. + indent_delta = 1 - min_indent + for i in range(len(child.code)): + line_warnings = [] + indent, code = child.code[i] + indent = max(1, indent + indent_delta) + if code.strip() == '': + o = '\n' + else: + o = (indent * '\t') + code.strip() + '\n' + code = o.strip('\n\r') + for rule in rules: + if rule in skipping: continue + rule.SetContext(atom, child) + line_warnings += rule.ScanLine(code) + if len(line_warnings): + warnings['{0}:{1}: {2}'.format(child.filename, child.line + i, code)] = line_warnings + alerts += len(line_warnings) + if len(warnings) > 0: + print('IN {0}/{1}:'.format(path, childName)) + for context in warnings: + print(' {0}'.format(context)) + for warning in warnings[context]: + print(' WARNING: {0}'.format(warning)) +print('{0} processed procs, {1} alerts raised'.format(procs, alerts)) diff --git a/tools/BYONDTools/scripts/ss13_makeinhands.py b/tools/BYONDTools/scripts/ss13_makeinhands.py new file mode 100644 index 00000000000..84d62088588 --- /dev/null +++ b/tools/BYONDTools/scripts/ss13_makeinhands.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +''' +Run within icons/mob/in-hand. + +Usage: + $ cd icons/mob/in-hands + $ python ss13_makeinhands.py + +ss13_makeinhands.py - Generates a large DMI from several smaller DMIs. + Specifically used for making icons/mob/items_(left|right)hand.dmi + +Copyright 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +''' +import os, sys, logging + +ToBuild = { +# ' file to build': 'directory to pull from/', + '../items_lefthand.dmi': 'left/', + '../items_righthand.dmi': 'right/' +} + +# Tell Python where to find BYONDTools. +# Assuming we're in icons/mob/in-hand +sys.path.append('../../../tools/BYONDTools') # For byond +sys.path.append('../../../tools/BYONDTools/scripts') # For dmi + +from byond.DMI import DMI +from dmi import compare_all + +def buildDMI(directory, output): + dmi = DMI(output) + logging.info('Creating {0}...'.format(output)) + for root, _, files in os.walk(directory): + for filename in files: + if filename.endswith('.dmi') and not filename.endswith('.new.dmi'): + filepath = os.path.join(root, filename) + logging.info('Adding {0}...'.format(filename, output)) + subdmi = DMI(filepath) + subdmi.loadAll() + if subdmi.icon_height != 32 or subdmi.icon_width != 32: + logging.warn('Skipping {0} - Invalid icon size.'.format(filepath)) + changes = 0 + for state_name in subdmi.states: + if state_name in dmi.states: + logging.warn('Skipping state {0}:{1} - State exists.'.format(filepath, subdmi.states[state_name].displayName())) + continue + dmi.states[state_name] = subdmi.states[state_name] + changes += 1 + logging.info('Added {0} states.'.format(changes)) + # save + logging.info('Saving {0} states to {1}...'.format(len(dmi.states), output)) + dmi.save(output) + +if __name__ == '__main__': + logging.basicConfig( + format='%(asctime)s [%(levelname)-8s]: %(message)s', + datefmt='%m/%d/%Y %I:%M:%S %p', + level=logging.INFO # , + # filename='logs/main.log', + # filemode='w' + ) + # Cheating, but useful for checking for unsync'd stuff + compare_all('left/', 'right/', 'in-hand_sync_report.txt', None, newfile_theirs=False, newfile_mine=False, check_changed=False) + for output, input_dir in ToBuild.items(): + buildDMI(input_dir, output) diff --git a/tools/BYONDTools/scripts/stringcounter.py b/tools/BYONDTools/scripts/stringcounter.py new file mode 100644 index 00000000000..d23502d58d0 --- /dev/null +++ b/tools/BYONDTools/scripts/stringcounter.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python +import os, sys +""" +Usage: + $ python countstrings.py path/to/your.dme .dm + +CountStrings.py - Counts strings in DreamMaker code + +Copyright 2013 Rob "N3X15" Nelson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +def CountStringsIn(filename): + with open(filename, 'r') as f: + with open(filename + '.str', 'w') as debug: + numStrings = 0 + inString = False + inMegaString = False + blockCommentLevel = 0 + embeddedLevel = 0 + lastChar = '' + escaped = False + buffer = '' + while(True): + c = f.read(1) + if not c: + if inString: + print('{0}: UNTERMINATED STRING!'.format(filename)) + return numStrings + if not inString: + if c == '/': + if lastChar == '/' and blockCommentLevel == 0: + # debug.write("[LINECOMMENT:{0}]".format(f.tell())) + # Seek to EOL. + while(c not in '\r\n'): + c = f.read(1) + # debug.write("[ENDCOMMENT:{0}]".format(f.tell())) + lastChar = '' + continue + if c == '*': + if lastChar == '/': + # debug.write("[BLOCKCOMMENT:{0}]".format(f.tell())) + blockCommentLevel += 1 + while(blockCommentLevel > 0): + c = f.read(1) + if not c: + return numStrings + if c == '*': + if lastChar == '/': + blockCommentLevel += 1 + elif c == '/': + if lastChar == '*': + blockCommentLevel -= 1 + lastChar = c + # debug.write("[ENDCOMMENT:{0}]".format(f.tell())) + lastChar = '' + continue + elif c == '"': + if lastChar == '{': + # debug.write("[MEGASTRING:{0}]".format(f.tell())) + inString = True + inMegaString = True + continue + else: + inString = True + # debug.write("[NEWSTRING:{0}]".format(f.tell())) + inMegaString = False + continue + elif c == '{': + lastChar = c + continue + else: + lastChar = c + else: + if c == '\\' and not escaped: + escaped = True + continue + if escaped: + escaped = False + # debug.write("[ESCAPE:{0}]".format(repr(c))) + if inString: + buffer += '\\' + c + lastChar = c + continue + if c in ('[', ']'): + if c == '[': + embeddedLevel += 1 + else: + embeddedLevel -= 1 + buffer += c # +"<{0}>".format(str(embeddedLevel)) + lastChar = c + continue + if embeddedLevel > 0: + buffer += c + lastChar = c + continue + if inMegaString: + if c == '}' and lastChar == '"': + # debug.write("[ENDMEGASTRING]") + inString = False + inMegaString = False + escaped = False + numStrings += 1 + debug.write("\n[{0}]={1}".format(numStrings, repr(buffer))) + buffer = '' + continue + else: + if c == '"': + inString = False + # debug.write("[ENDSTRING:{0}]".format(f.tell())) + numStrings += 1 + escaped = False + debug.write("\n[{0}]={1}".format(numStrings, repr(buffer))) + buffer = '' + continue + buffer += c + lastChar = c + return numStrings + +def ProcessFiles(top='.', ext='.dm'): + numStringsTotal = 0 + numStrings = 0 + numFilesTotal = 0 + maxStringsInFile = [0, ''] + for root, _, files in os.walk(top): + for filename in files: + filepath = os.path.join(root, filename) + if filepath.endswith(ext): + numStrings = CountStringsIn(filepath) + numStringsTotal += numStrings + if numStrings > maxStringsInFile[0]: + maxStringsInFile = [numStrings, filepath] + numFilesTotal += 1 + print(','.join([filepath, str(numStrings)])) + print('>>> Total Strings: {0}'.format(numStringsTotal)) + print('>>> Total Files: {0}'.format(numFilesTotal)) + print('>>> Max Strings: {0} in {1}'.format(maxStringsInFile[0], maxStringsInFile[1])) + +def ProcessFilesFromDME(dmefile='baystation12.dme', ext='.dm'): + numStringsTotal = 0 + numStrings = 0 + numFilesTotal = 0 + maxStringsInFile = [0, ''] + rootdir = os.path.dirname(dmefile) + with open(os.path.join(rootdir, 'stringcounts.csv'), 'w') as csv: + with open(dmefile, 'r') as dmeh: + for line in dmeh: + if line.startswith('#include'): + inString = False + # escaped=False + filename = '' + for c in line: + """ + if c == '\\' and not escaped: + escaped = True + continue + if escaped: + if + escaped = False + continue + """ + if c == '"': + inString = not inString + if not inString: + filepath = os.path.join(rootdir, filename) + if filepath.endswith(ext): + numStrings = CountStringsIn(filepath) + numStringsTotal += numStrings + if numStrings > maxStringsInFile[0]: + maxStringsInFile = [numStrings, filepath] + numFilesTotal += 1 + csv.write(','.join([filepath, str(numStrings)]) + "\n") + filename = '' + continue + else: + if inString: + filename += c + print('>>> Total Strings: {0}'.format(numStringsTotal)) + print('>>> Total Files: {0}'.format(numFilesTotal)) + print('>>> Max Strings: {0} in {1}'.format(maxStringsInFile[0], maxStringsInFile[1])) + +if os.path.isdir(sys.argv[1]): + for root, _, files in os.walk(sys.argv[1]): + for filename in files: + filepath = os.path.join(root, filename) + if filepath.endswith('.dme'): + ProcessFilesFromDME(filepath, sys.argv[2]) + sys.exit(0) +if os.path.isfile(sys.argv[1]): + ProcessFilesFromDME(sys.argv[1], sys.argv[2]) +# ProcessFiles(sys.argv[1], sys.argv[2]) + diff --git a/tools/BYONDTools/setup.py b/tools/BYONDTools/setup.py new file mode 100644 index 00000000000..0d2684c388b --- /dev/null +++ b/tools/BYONDTools/setup.py @@ -0,0 +1,48 @@ + +import glob, os, sys +from setuptools import setup +from setuptools.command.install import install as _install + +scripts = [ + 'dmm', + 'dmi', + 'dmindent', + 'dmmrender', + 'dmmfix', + + 'ss13_makeinhands' +] + +def _post_install(dir): + from subprocess import call + print('dir={}'.format(dir)) + call([sys.executable, 'byondtools-postinstall.py'], cwd=dir) + + +class install(_install): + def run(self): + _install.run(self) + # install_lib + self.execute(_post_install, (self.install_scripts,), msg="Running post install task") + +options = {} +if sys.platform == "win32": + scripts.append("byondtools-postinstall") +scripts = ['scripts/{}.py'.format(x) for x in scripts] + +setup(name='BYONDTools', + version='0.1.0', + description='Tools and interfaces for interacting with the BYOND game engine.', + url='http://github.com/N3X15/BYONDTools', + author='N3X15', + author_email='nexisentertainment@gmail.com', + license='MIT', + packages=['byond'], + install_requires=[ + 'Pillow' + ], + tests_require=['unittest-xml-reporting'], + test_suite='tests', + scripts=scripts, + zip_safe=False, + cmdclass={'install': install}) \ No newline at end of file diff --git a/tools/BYONDTools/tests/Atom.py b/tools/BYONDTools/tests/Atom.py new file mode 100644 index 00000000000..a0aced43e68 --- /dev/null +++ b/tools/BYONDTools/tests/Atom.py @@ -0,0 +1,27 @@ +''' +Created on Jan 1, 2014 + +@author: Rob +''' +import unittest + +class AtomTest(unittest.TestCase): + def test_copy_consistency(self): + from byond.basetypes import Atom, BYONDString, BYONDValue + atom = Atom('/datum/test',__file__,0) + atom.properties={ + 'dir': BYONDValue(2), + 'name': BYONDString('test datum') + } + atom.mapSpecified=['dir','name'] + + atom2=atom.copy() + + atom_serialized=atom.MapSerialize() + atom2_serialized=atom2.MapSerialize() + + self.assertEqual(atom_serialized, atom2_serialized) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/tools/BYONDTools/tests/MapParser.py b/tools/BYONDTools/tests/MapParser.py new file mode 100644 index 00000000000..72c6e69bb89 --- /dev/null +++ b/tools/BYONDTools/tests/MapParser.py @@ -0,0 +1,70 @@ +''' +Created on Jan 1, 2014 + +@author: Rob +''' +import unittest + +class MapParserTest(unittest.TestCase): + def setUp(self): + from byond.map import Map, Tile + self.dmm = Map() + + def test_basic_SplitAtoms_operation(self): + testStr = '/obj/effect/landmark{name = "carpspawn"},/obj/structure/lattice,/turf/space,/area' + expectedOutput = ['/obj/effect/landmark{name = "carpspawn"}', '/obj/structure/lattice', '/turf/space', '/area'] + + out = self.dmm.SplitAtoms(testStr) + self.assertListEqual(out, expectedOutput) + + def test_basic_SplitProperties_operation(self): + testStr = 'd1 = 1; d2 = 2; icon_state = "1-2"; tag = ""' + expectedOutput = ['d1 = 1', ' d2 = 2', ' icon_state = "1-2"', ' tag = ""'] + + out = self.dmm.SplitProperties(testStr) + self.assertListEqual(out, expectedOutput) + + def test_basic_consumeTile_operation(self): + from byond.map import Map, Tile + ''' + "aaK" = ( + /obj/structure/cable{ + d1 = 1; + d2 = 2; + icon_state = "1-2"; + tag = "" + }, + /obj/machinery/atmospherics/pipe/simple/supply/hidden{ + dir = 4 + }, + /turf/simulated/floor{ + icon_state = "floorgrime" + }, + /area/security/prison + ) + ''' + testStr = '"aaK" = (/obj/structure/cable{d1 = 1; d2 = 2; icon_state = "1-2"; tag = ""},/obj/machinery/atmospherics/pipe/simple/supply/hidden{dir = 4},/turf/simulated/floor{icon_state = "floorgrime"},/area/security/prison)' + out = self.dmm.consumeTile(testStr, 0) + print('IIDs: {0}'.format(repr(out.instances))) + + self.assertEquals(out.origID, 'aaK', 'origID') + self.assertEquals(len(out.instances), 4, 'instances size') + self.assertEquals(len(out.GetAtom(0).properties), 4, 'instances[0] properties') + self.assertIn('d1', out.GetAtom(0).properties, 'd1 not present in properties') + self.assertListEqual(out.GetAtom(0).mapSpecified,['d1','d2','icon_state','tag']) + + self.assertEquals(len(out.GetAtom(2).properties), 1, 'Failure to parse /turf/simulated/floor{icon_state = "floorgrime"}') + self.assertIn('icon_state', out.GetAtom(2).properties, 'Failure to parse /turf/simulated/floor{icon_state = "floorgrime"}') + + self.assertEqual(out.MapSerialize(Tile.FLAG_USE_OLD_ID), testStr) + + def test_consumeTile_landmark(self): + from byond.map import Map, Tile + testStr='"aah" = (/obj/effect/landmark{name = "carpspawn"},/obj/structure/lattice,/turf/space,/area)' + out = self.dmm.consumeTile(testStr, 0) + self.assertEqual(out.MapSerialize(Tile.FLAG_USE_OLD_ID), testStr) + + +if __name__ == "__main__": + # import sys;sys.argv = ['', 'Test.testName'] + unittest.main() diff --git a/tools/BYONDTools/tests/MapRendering.py b/tools/BYONDTools/tests/MapRendering.py new file mode 100644 index 00000000000..585843d5cdd --- /dev/null +++ b/tools/BYONDTools/tests/MapRendering.py @@ -0,0 +1,35 @@ +''' +Created on Feb 26, 2014 + +@author: Rob +''' +import unittest +#from byond.map import Map +''' +class MapRenderingTest(unittest.TestCase): + def setUp(self): + self.dmm = Map() + + def test_basic_bbox(self): + correct_bbox=( + 3584,4382, + 3616,4414 + ) + # 1126, 440 + # 1126, 470 + + #3584, 4288 + tile_x,tile_y=(112,134) + + pixel_x=0 + pixel_y=-30 + + icon_height=32 + icon_width=32 + + bbox = self.dmm.tilePosToBBox(tile_x, tile_y, pixel_x,pixel_y, icon_height,icon_width) + self.assertTupleEqual(correct_bbox, bbox) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main()''' \ No newline at end of file diff --git a/tools/BYONDTools/tests/ObjectTree.py b/tools/BYONDTools/tests/ObjectTree.py new file mode 100644 index 00000000000..e05fdb15b94 --- /dev/null +++ b/tools/BYONDTools/tests/ObjectTree.py @@ -0,0 +1,74 @@ +''' +Created on Jan 5, 2014 + +@author: Rob +''' +import unittest + +class ObjectTreeTests(unittest.TestCase): + def setUp(self): + from byond.objtree import ObjectTree + self.tree = ObjectTree() + + def test_consumeVariable_basics(self): + test_string = 'var/obj/item/weapon/chainsaw = new' + name, data = self.tree.consumeVariable(test_string, '', 0) + + self.assertEqual(name, 'chainsaw') + self.assertEqual(data.type, '/obj/item/weapon') + self.assertEqual(data.value, 'new') + self.assertEqual(data.declaration, True) + self.assertEqual(data.inherited, False) + self.assertEqual(data.special, None) + + def test_consumeVariable_alternate_array_declaration_01(self): + test_string = 'var/appearance_keylist[0]' + name, data = self.tree.consumeVariable(test_string, '', 0) + + self.assertEqual(name, 'appearance_keylist') + self.assertEqual(data.type, '/list') + self.assertEqual(data.value, None) + self.assertEqual(data.size, 0) + self.assertEqual(data.declaration, True) + self.assertEqual(data.inherited, False) + self.assertEqual(data.special, None) + + def test_consumeVariable_alternate_array_declaration_02(self): + test_string = 'var/medical[] = list()' + name, data = self.tree.consumeVariable(test_string, '', 0) + + self.assertEqual(name, 'medical') + self.assertEqual(data.type, '/list') + self.assertEqual(data.value, None) + self.assertEqual(data.size, -1) + self.assertEqual(data.declaration, True) + self.assertEqual(data.inherited, False) + self.assertEqual(data.special, None) + + def test_consumeVariable_complex_types(self): + test_string = 'var/datum/gas_mixture/air_temporary' + name, data = self.tree.consumeVariable(test_string, '', 0) + + self.assertEqual(name, 'air_temporary') + self.assertEqual(data.type, '/datum/gas_mixture') + self.assertEqual(data.value, None) + self.assertEqual(data.size, None) + self.assertEqual(data.declaration, True) + self.assertEqual(data.inherited, False) + self.assertEqual(data.special, None) + + def test_consumeVariable_file_ref(self): + test_string = 'icon = \'butts.dmi\'' + name, data = self.tree.consumeVariable(test_string, '', 0) + + self.assertEqual(name, 'icon') + self.assertEqual(data.type, '/icon') + self.assertEqual(str(data.value), 'butts.dmi') + self.assertEqual(data.size, None) + self.assertEqual(data.declaration, False) + self.assertEqual(data.inherited, False) + self.assertEqual(data.special, None) + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/tools/BYONDTools/tests/runtests.py b/tools/BYONDTools/tests/runtests.py new file mode 100644 index 00000000000..9fe0c6cc32f --- /dev/null +++ b/tools/BYONDTools/tests/runtests.py @@ -0,0 +1,9 @@ +import unittest + +from Atom import * +from MapParser import * +from MapRendering import * +from ObjectTree import * + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tools/BYONDTools/tox.ini b/tools/BYONDTools/tox.ini new file mode 100644 index 00000000000..fba49901767 --- /dev/null +++ b/tools/BYONDTools/tox.ini @@ -0,0 +1,13 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py27 + +[testenv:py27] +deps = + pytest + Pillow +commands = py.test tests/runtests.py --junit-xml=pyunit-py27.xml