├── .gitignore ├── Examples ├── aimbot.owpy ├── arithmetic.owpy ├── arrays.owpy ├── attributes.owpy ├── basic.owpy ├── bezier.owpy ├── booleans.owpy ├── chase.owpy ├── comments.owpy ├── conditionals.owpy ├── conditions.owpy ├── const.owpy ├── contains.owpy ├── error_parameter.owpy ├── for.owpy ├── func_args.owpy ├── functions.owpy ├── imports.owpy ├── lib │ ├── child.owpy │ └── lib2 │ │ └── child2.owpy ├── nested.owpy ├── optional_args.owpy ├── strings.owpy ├── time.owpy ├── trigonometry.owpy ├── variables.owpy ├── vectors.owpy └── while.owpy ├── LICENSE ├── OWScript.py ├── OWScript ├── AST.py ├── Errors.py ├── Importer.py ├── Lexer.py ├── Parser.py ├── Tokens.py ├── Transpiler.py ├── Workshop.json ├── Workshop.py └── __init__.py ├── README.md ├── Syntax ├── Comments.tmPreferences └── OWScript.sublime-syntax └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ 3 | Test/ 4 | temp.out 5 | node_modules/ -------------------------------------------------------------------------------- /Examples/aimbot.owpy: -------------------------------------------------------------------------------- 1 | Rule "Start Aimbot" 2 | Event 3 | On Each Player 4 | All 5 | All 6 | Conditions 7 | Is Button Held(Event Player, Primary Fire) 8 | Event Player.hero != Hero(Widowmaker) 9 | Event Player.hero != Hero(Soldier: 76) 10 | Is Alive(Player Closest To Reticle(Event Player, Opposite Team Of(Event Player.team))) 11 | Players In Slot(1, All Teams) 12 | Actions 13 | Start Facing 14 | Event Player 15 | Direction Towards 16 | Event Player 17 | Player Closest To Reticle 18 | Event Player 19 | Opposite Team Of(Event Player.team) + <0, 0.240, 0> 20 | 10000 21 | To World 22 | Direction and Turn Rate 23 | 24 | Rule "Stop Aimbot" 25 | Event 26 | On Each Player 27 | All 28 | All 29 | Conditions 30 | not Event Player.LMB 31 | Event Player.hero != Hero(Widowmaker) 32 | Players In Slot(1, All Teams) 33 | Actions 34 | Stop Facing(Event Player) -------------------------------------------------------------------------------- /Examples/arithmetic.owpy: -------------------------------------------------------------------------------- 1 | Rule "Arithmetic Operations" 2 | Actions 3 | z = Count Of(Allowed Heroes(Event Player)) 4 | y = 1 + 1 5 | y += 2 - 1 6 | y *= 3 / 3 7 | y = 2 ^ 2 8 | y = (3 + 1) % 4 9 | pvar num_heroes = 3 10 | pvar formula = -------------------------------------------------------------------------------- /Examples/arrays.owpy: -------------------------------------------------------------------------------- 1 | Rule "Advanced Arrays" 2 | Event 3 | On Global 4 | Actions 5 | array = [] 6 | array2 = [1, 2, 3] 7 | array[0] = 2 8 | array[1] = array2[1] + array2[2] 9 | array.append(<1, 2, 3>) 10 | array.append(<6, 6, 6>) 11 | array3 = [1, "Rank B", "Rank A"] 12 | for elem in array3: 13 | Msg(Everyone, elem) -------------------------------------------------------------------------------- /Examples/attributes.owpy: -------------------------------------------------------------------------------- 1 | Rule "Attributes" 2 | Event 3 | On Each Player 4 | All 5 | All 6 | 7 | Conditions 8 | Position Of(Event Player).x < 10 9 | Event Player.moving 10 | Event Player.jumping -------------------------------------------------------------------------------- /Examples/basic.owpy: -------------------------------------------------------------------------------- 1 | Rule "My First Rule" 2 | Event 3 | On Global 4 | All 5 | All 6 | 7 | Conditions 8 | All True 9 | Array: All Players 10 | Team: Team 2 11 | Condition: Has Spawned 12 | Element: Current Array Element 13 | == True 14 | 15 | Actions 16 | pvar h@Event Player = 0 17 | h = 1 18 | b = h 19 | c = pvar h -------------------------------------------------------------------------------- /Examples/bezier.owpy: -------------------------------------------------------------------------------- 1 | Rule "Bezier Curve" 2 | Event 3 | On Global 4 | Actions 5 | a = 0 6 | b = 1 7 | c = floor(b) * 3 8 | Chase Global Variable At Rate 9 | Variable: a 10 | Destination: a[c] * ((1 - b % 1)^3) + (((a[1 + c] * 3) * 3) * b % 1) * ((1 - b % 1)^2) + ((a[2 + c * 3] * 3) * (b % 1)^2 * (1 - b % 1) + a[3 + c * 3]) * (b % 1) * 3 11 | Rate: 0.2 12 | Reevaluation: Destination And Rate -------------------------------------------------------------------------------- /Examples/booleans.owpy: -------------------------------------------------------------------------------- 1 | Rule "Boolean Operators" 2 | Event 3 | On Global 4 | 5 | Actions 6 | n = 1 and 2 or 3 and not 4 -------------------------------------------------------------------------------- /Examples/chase.owpy: -------------------------------------------------------------------------------- 1 | Rule "Chase Test" 2 | Actions 3 | pvar chase_var = 100 4 | Chase Global Variable At Rate 5 | Variable: chase_var 6 | Destination: 0 7 | Rate: -1 8 | Reevaluation: Destination And Rate 9 | Set Move Speed(Event Player, chase_var) -------------------------------------------------------------------------------- /Examples/comments.owpy: -------------------------------------------------------------------------------- 1 | Rule "Ton commentaire n'est pas necessaire" 2 | Event 3 | On Global 4 | Actions 5 | /* We should ignore 6 | all dis text */ 7 | // x = 1 8 | Msg(Event Player, /* even inline comments 9 | should work */ "Hello") 10 | var = 1 // Yes, it is indeed 11 | /* Comments are the best! */ -------------------------------------------------------------------------------- /Examples/conditionals.owpy: -------------------------------------------------------------------------------- 1 | Rule "If and Else" 2 | Event 3 | On Global 4 | Actions 5 | z = 1 6 | h = 2 7 | if z == 1: 8 | z = 3 9 | y = 4 10 | elif h > 1: 11 | h = 2 12 | y = 3 13 | y = 4 14 | elif h < 1: 15 | z = 1 16 | else: 17 | z = 'yes' -------------------------------------------------------------------------------- /Examples/conditions.owpy: -------------------------------------------------------------------------------- 1 | Rule "Multiple Conditions" 2 | Event 3 | On Global 4 | Conditions 5 | Distance Between 6 | Event Player 7 | <10, 20, 30> 8 | <= 1.5 9 | 123 < 456 -------------------------------------------------------------------------------- /Examples/const.owpy: -------------------------------------------------------------------------------- 1 | Rule "Constants for cleaner code!" 2 | Event 3 | On Each Player 4 | Actions 5 | const speed = 100 6 | Set Move Speed(Event Player, speed) 7 | // speed = 150 // const is immutable, so this causes an error -------------------------------------------------------------------------------- /Examples/contains.owpy: -------------------------------------------------------------------------------- 1 | Rule "x in array?" 2 | Event 3 | On Global 4 | Actions 5 | a = Event Player in Everyone 6 | b = 1 not in [1, 2, 3] -------------------------------------------------------------------------------- /Examples/error_parameter.owpy: -------------------------------------------------------------------------------- 1 | Rule "Wrong Parameter!" 2 | Event 3 | On Global 4 | Actions 5 | Set Facing(Event Player, Event Player.eyepos, "No") -------------------------------------------------------------------------------- /Examples/for.owpy: -------------------------------------------------------------------------------- 1 | Rule "For Loop" 2 | Event 3 | On Global 4 | Actions 5 | angles = [15, 30, 45, 60, 75, 90] 6 | angles2 = range(15, 91, 15) 7 | for ang in angles: 8 | Set Facing 9 | Event Player 10 | Direction From Angles 11 | horizontal: ang 12 | vertical: 45 13 | To World 14 | Msg(Everyone, "HEllo") 15 | for i in range(3): 16 | Msg(Everyone, i) 17 | for player in Everyone: 18 | Msg(player, "Hello") -------------------------------------------------------------------------------- /Examples/func_args.owpy: -------------------------------------------------------------------------------- 1 | // Functions can be used as rule factories 2 | %create_portal(origin, destination, radius) 3 | Rule "Create Portal" 4 | Event 5 | On Each Player 6 | All 7 | All 8 | Actions 9 | Create Effect 10 | Visible: Event Player 11 | Type: Good Aura 12 | Color: Yellow 13 | Position: origin 14 | Radius: radius 15 | Reeval: Visible To, Position, and Radius 16 | Rule "Teleportation" 17 | Event 18 | On Each Player 19 | All 20 | All 21 | Conditions 22 | Event Player in Players In Radius 23 | Center: origin 24 | Radius: radius 25 | Team: All 26 | LOS: Surfaces 27 | == True 28 | Actions 29 | Teleport 30 | Event Player 31 | destination 32 | 33 | create_portal(<0, 1, 2>, <3, 4, 5>, 10) -------------------------------------------------------------------------------- /Examples/functions.owpy: -------------------------------------------------------------------------------- 1 | %event_func 2 | Event 3 | On Each Player 4 | All 5 | All 6 | %add_rule(a, b, name_) 7 | Rule "test" 8 | event_func() 9 | Actions 10 | c = a + b 11 | Rule "Function Demo" 12 | event_func() 13 | add_rule(1, 5, "Add Two") -------------------------------------------------------------------------------- /Examples/imports.owpy: -------------------------------------------------------------------------------- 1 | #import "lib/child" 2 | 3 | Rule "Debug Import" 4 | Actions 5 | debug_func("Hello!") -------------------------------------------------------------------------------- /Examples/lib/child.owpy: -------------------------------------------------------------------------------- 1 | #import 'lib2/child2.owpy' 2 | 3 | %debug_func(text) 4 | Msg(Everyone, text) 5 | nested(Lucio) -------------------------------------------------------------------------------- /Examples/lib/lib2/child2.owpy: -------------------------------------------------------------------------------- 1 | %nested(hero_) 2 | Set Hero(Event Player, hero_) -------------------------------------------------------------------------------- /Examples/nested.owpy: -------------------------------------------------------------------------------- 1 | Rule "Nested Blocks" 2 | Event 3 | On Global 4 | Actions 5 | if a == 1: 6 | b = 2 7 | for i in range(4): 8 | Msg(Everyone, i) 9 | elif a == 2: 10 | b = 3 -------------------------------------------------------------------------------- /Examples/optional_args.owpy: -------------------------------------------------------------------------------- 1 | %CreateEffect(pos, type?=Ring, color?=White) 2 | Create Effect 3 | Visible_To: Everyone 4 | Type: type 5 | Color: color 6 | Position: pos 7 | Radius: 1.5 8 | Reevaluation: Visible To 9 | 10 | Rule "Optional/Default Arguments" 11 | Event 12 | On Global 13 | Actions 14 | CreateEffect(<0,0,0>) -------------------------------------------------------------------------------- /Examples/strings.owpy: -------------------------------------------------------------------------------- 1 | Rule "String Builder" 2 | Event 3 | On Each Player 4 | All 5 | All 6 | Actions 7 | pvar money = 100 8 | test = "Hello" 9 | Msg 10 | Everyone 11 | `Money: {}!` 12 | pvar money 13 | `#use ultimate ability, up` 14 | `{}`(pvar money) 15 | 16 | /*%RuleFactory(name_) 17 | Rule "My Custom " name_ 18 | Event 19 | On Global*/ 20 | 21 | //RuleFactory("Portal") -------------------------------------------------------------------------------- /Examples/time.owpy: -------------------------------------------------------------------------------- 1 | Rule "Time Shorthands" 2 | Actions 3 | Wait(16ms) 4 | Wait /* It takes a parameter, so indent if you please! */ 5 | 0.35min -------------------------------------------------------------------------------- /Examples/trigonometry.owpy: -------------------------------------------------------------------------------- 1 | Rule "Trigonometry In Vectors" 2 | Actions 3 | pvar vec1 = -------------------------------------------------------------------------------- /Examples/variables.owpy: -------------------------------------------------------------------------------- 1 | Rule "Variables" 2 | Event 3 | On Global 4 | 5 | Actions 6 | pvar h@Event Player = 1 -------------------------------------------------------------------------------- /Examples/vectors.owpy: -------------------------------------------------------------------------------- 1 | Rule "Variables & Vectors" 2 | Actions 3 | first_Vector = Vector(1, 2, 3) 4 | gvar vec2 = Vector 5 | X: 1 6 | 2 7 | this_is_a_comment_lol_gottem: 3 8 | pVar CoolV3ctor = <1, 2, 3> 9 | // Vectors can span multiple lines -------------------------------------------------------------------------------- /Examples/while.owpy: -------------------------------------------------------------------------------- 1 | Rule "While Loop" 2 | Event 3 | On Global 4 | Actions 5 | n = 0 6 | while n < 5: 7 | n += 1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Adam Papenhausen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OWScript.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import re 4 | import sys 5 | import time 6 | from OWScript import Errors 7 | from OWScript.Errors import Logger 8 | from OWScript.Lexer import Lexer 9 | from OWScript.Parser import Parser 10 | from OWScript.Transpiler import Transpiler 11 | 12 | def transpile(text, path, args): 13 | """Transpiles an OWScript code into Overwatch Workshop rules.""" 14 | start = time.time() 15 | Errors.TEXT = text 16 | lexer = Lexer(text=text + '\n') 17 | tokens = lexer.lex() 18 | if args.tokens: 19 | if args.save: 20 | with open(args.save, 'w', errors='ignore') as f: 21 | f.write(lexer.print_tokens()) 22 | else: 23 | lexer.print_tokens() 24 | parser = Parser(tokens=tokens) 25 | tree = parser.script() 26 | if args.tree: 27 | print(tree.string()) 28 | logger = Logger(log_level=args.debug) 29 | transpiler = Transpiler(tree=tree, path=path, logger=logger, credit=args.no_credit) 30 | code = transpiler.run() 31 | if args.min: 32 | code = re.sub(r'[\s\n]*', '', code) 33 | if not args.save: 34 | if sys.stdout.encoding.strip() != 'utf-8': 35 | sys.stderr.write( 36 | f'[WARNING] Python encoding output set to {sys.stdout.encoding} (not utf-8), ' 37 | 'unicode characters on the output will be interpreted as ascii. ' 38 | 'Consider using `set PYTHONIOENCODING=utf_8` and running the command again.' 39 | ) 40 | sys.stdout.write(code) 41 | else: 42 | try: 43 | with open(args.save, 'wb') as f: 44 | code = code.encode('utf-8') 45 | f.write(code) 46 | except FileNotFoundError: 47 | raise Errors.FileNotFoundError('Output directory not found.') 48 | sys.exit(Errors.ExitCode.OutputNotFound) 49 | if args.copy: 50 | import pyperclip 51 | pyperclip.copy(code) 52 | sys.stdout.write('\nCode copied to clipboard.') 53 | end = time.time() 54 | if args.time: 55 | print('\nTime Elapsed: {}s'.format(round(end - start, 2))) 56 | 57 | if __name__ == '__main__': 58 | parser = argparse.ArgumentParser(description='Generate Overwatch Workshop code from OWScript') 59 | parser.add_argument('input', nargs='*', type=str, help='Standard input to process') 60 | parser.add_argument('-m', '--min', action='store_true', help='Minifies the output by removing whitespace') 61 | parser.add_argument('-s', '--save', help='Save the output to a file instead of printing it') 62 | parser.add_argument('-c', '--copy', action='store_true', help='Copies output to clipboard automatically') 63 | parser.add_argument('-t', '--time', action='store_true', help='Debug: outputs the time elapsed to generate the output') 64 | parser.add_argument('-d', '--debug', type=int, default=Logger.WARN, help='The severity level of the logger (1=Info, 2=Warning, 3=Debug)') 65 | parser.add_argument('--no-credit', action='store_true', help='Author credit rule is not generated in the output') 66 | parser.add_argument('--tokens', action='store_true', help='Debug: shows the tokens created by the lexer') 67 | parser.add_argument('--tree', action='store_true', help='Debug: visualizes the AST generated by the parser') 68 | args = parser.parse_args() 69 | if args.input: 70 | file_input = args.input[0] 71 | path = os.path.abspath(file_input) 72 | try: 73 | with open(path, 'rb') as f: 74 | text = f.read().decode('utf-8') 75 | except FileNotFoundError: 76 | raise Errors.FileNotFoundError('Input file not found.') 77 | sys.exit(Errors.ExitCode.InputNotFound) 78 | else: 79 | text = sys.stdin.read() 80 | path = os.getcwd() 81 | try: 82 | transpile(text, path=path, args=args) 83 | except Errors.OWSError as ex: 84 | sys.stderr.write('Error: {}'.format(ex)) 85 | sys.exit(Errors.ExitCode.CompileError) -------------------------------------------------------------------------------- /OWScript/AST.py: -------------------------------------------------------------------------------- 1 | class AST: 2 | children = [] 3 | def __init__(self): 4 | self.children = [] 5 | 6 | @property 7 | def format_children(self): 8 | return ', '.join(map(repr, self.children)) 9 | 10 | @property 11 | def x(self): 12 | return 'X Component Of({})' 13 | 14 | @property 15 | def y(self): 16 | return 'Y Component Of({})' 17 | 18 | @property 19 | def z(self): 20 | return 'Z Component Of({})' 21 | 22 | @property 23 | def moving(self): 24 | return 'Compare(Speed Of({}), >, 0)' 25 | 26 | @property 27 | def facing(self): 28 | return 'Facing Direction Of({})' 29 | 30 | @property 31 | def pos(self): 32 | return 'Position Of({})' 33 | 34 | @property 35 | def eyepos(self): 36 | return 'Eye Position({})' 37 | 38 | @property 39 | def hero(self): 40 | return 'Hero Of({})' 41 | 42 | @property 43 | def team(self): 44 | return 'Team Of({})' 45 | 46 | @property 47 | def jumping(self): 48 | return 'Is Button Held({}, Jump)' 49 | 50 | @property 51 | def crouching(self): 52 | return 'Is Button Held({}, Crouch)' 53 | 54 | @property 55 | def interacting(self): 56 | return 'Is Button Held({}, Interact)' 57 | 58 | @property 59 | def lmb(self): 60 | return 'Is Button Held({}, Primary Fire)' 61 | 62 | @property 63 | def rmb(self): 64 | return 'Is Button Held({}, Secondary Fire)' 65 | 66 | def string(self, indent=0): 67 | string = '' 68 | if not self.__class__ == Block: 69 | string += ' ' * indent + '{}'.format(self.__class__.__name__) + '\n' 70 | else: 71 | indent -= 3 72 | for child in self.children: 73 | string += child.string(indent=indent + 3) 74 | return string 75 | 76 | def __repr__(self): 77 | if not self.children: 78 | return self.__class__.__name__ 79 | return '{}({})'.format(self.__class__.__name__, self.format_children) 80 | 81 | class Raw(AST): 82 | def __init__(self, code): 83 | self.code = code 84 | 85 | def __repr__(self): 86 | return ''.format(len(self.code)) 87 | 88 | class Import(AST): 89 | def __init__(self, path): 90 | self.path = path 91 | 92 | def __repr__(self): 93 | return '#import {}'.format(self.path) 94 | 95 | # Workshop Types 96 | class WorkshopType(AST): 97 | @classmethod 98 | def get_values(cls): 99 | return cls._values + [x().get_values() for x in cls._extends] 100 | 101 | def __repr__(self): 102 | try: 103 | return self.__name__ 104 | except AttributeError: 105 | return self.__class__.__name__ 106 | 107 | class Transformation(WorkshopType): 108 | _values = ['ROTATION', 'ROTATION AND TRANSLATION'] 109 | _extends = [] 110 | 111 | class InvisibleTo(WorkshopType): 112 | _values = ['ALL', 'ENEMIES', 'NONE'] 113 | _extends = [] 114 | 115 | class Color(WorkshopType): 116 | _values = ['BLUE', 'GREEN', 'PURPLE', 'RED', 'TEAM 1', 'TEAM 2', 'WHITE', 'YELLOW'] 117 | _extends = [] 118 | 119 | class Button(WorkshopType): 120 | _values = ['ABILITY 1', 'ABILITY 2', 'CROUCH', 'INTERACT', 'JUMP', 'PRIMARY FIRE', 'SECONDARY FIRE', 'ULTIMATE'] 121 | _extends = [] 122 | 123 | class Operation(WorkshopType): 124 | _values = ['ADD', 'APPEND TO ARRAY', 'DIVIDE', 'MAX', 'MIN', 'MODULO', 'MULTIPLY', 'RAISE TO POWER', 'REMOVE FROM ARRAY BY INDEX', 'REMOVE FROM ARRAY BY VALUE', 'SUBTRACT'] 125 | _extends = [] 126 | 127 | class Event(WorkshopType): 128 | _values = ['ONGOING - EACH PLAYER', 'ONGOING - GLOBAL', 'PLAYER DEALT DAMAGE', 'PLAYER DEALT FINAL BLOW', 'PLAYER DIED', 'PLAYER EARNED ELIMINATION', 'PLAYER TOOK DAMAGE', 'PLAYER DEALT HEALING', 'PLAYER RECEIVED HEALING', 'PLAYER JOINED MATCH', 'PLAYER LEFT MATCH'] 129 | _extends = [] 130 | 131 | class StringConstant(WorkshopType): 132 | _values = ['', '----------', '-> {0}', '!', '!!', '!!!', '#{0}', '({0})', '*', '...', '?', '??', '???', '{0} - {1}', '{0} - {1} - {2}', '{0} ->', '{0} -> {1}', '{0} != {1}', '{0} * {1}', '{0} / {1}', '{0} : {1} : {2}', '{0} {1}', '{0} {1} {2}', '{0} + {1}', '{0} <-', '{0} <- {1}', '{0} <->', '{0} <-> {1}', '{0} < {1}', '{0} <= {1}', '{0} = {1}', '{0} == {1}', '{0} > {1}', '{0} >= {1}', '{0} AND {1}', '{0} M', '{0} M/S', '{0} SEC', '{0} VS {1}', '{0}!', '{0}!!', '{0}!!!', '{0}%', '{0}, {1}', '{0}, {1}, AND {2}', '{0}:', '{0}: {1}', '{0}: {1} AND {2}', '{0}:{1}', '{0}?', '{0}??', '{0}???', '<- {0}', '<-> {0}', 'ABILITIES', 'ABILITY', 'ABILITY 1', 'ABILITY 2', 'ALERT', 'ALIVE', 'ALLIES', 'ALLY', 'ATTACK', 'ATTACKED', 'ATTACKING', 'ATTEMPT', 'ATTEMPTS', 'AVERAGE', 'AVOID', 'AVOIDED', 'AVOIDING', 'BACKWARD', 'BAD', 'BAN', 'BANNED', 'BANNING', 'BEST', 'BETTER', 'BOSS', 'BOSSES', 'BOUGHT', 'BUILD', 'BUILDING', 'BUILT', 'BURN', 'BURNING', 'BURNT', 'BUY', 'BUYING', 'CAPTURE', 'CAPTURED', 'CAPTURING', 'CAUTION', 'CENTER', 'CHALLENGE ACCEPTED', 'CHASE', 'CHASED', 'CHASING', 'CHECKPOINT', 'CHECKPOINTS', 'CLOUD', 'CLOUDS', 'COME HERE', 'CONDITION', 'CONGRATULATIONS', 'CONNECT', 'CONNECTED', 'CONNECTING', 'CONTROL POINT', 'CONTROL POINTS', 'COOLDOWN', 'COOLDOWNS', 'CORRUPT', 'CORRUPTED', 'CORRUPTING', 'CREDIT', 'CREDITS', 'CRITICAL', 'CROUCH', 'CROUCHED', 'CROUCHING', 'CURRENT', 'CURRENT ALLIES', 'CURRENT ALLY', 'CURRENT ATTEMPT', 'CURRENT CHECKPOINT', 'CURRENT ENEMIES', 'CURRENT ENEMY', 'CURRENT FORM', 'CURRENT GAME', 'CURRENT HERO', 'CURRENT HEROES', 'CURRENT HOSTAGE', 'CURRENT HOSTAGES', 'CURRENT LEVEL', 'CURRENT MISSION', 'CURRENT OBJECT', 'CURRENT OBJECTIVE', 'CURRENT OBJECTS', 'CURRENT PHASE', 'CURRENT PLAYER', 'CURRENT PLAYERS', 'CURRENT ROUND', 'CURRENT TARGET', 'CURRENT TARGETS', 'CURRENT UPGRADE', 'DAMAGE', 'DAMAGED', 'DAMAGING', 'DANGER', 'DEAD', 'DEFEAT', 'DEFEND', 'DEFENDED', 'DEFENDING', 'DELIVER', 'DELIVERED', 'DELIVERING', 'DESTABILIZE', 'DESTABILIZED', 'DESTABILIZING', 'DESTROY', 'DESTROYED', 'DESTROYING', 'DIE', 'DISCONNECT', 'DISCONNECTED', 'DISCONNECTING', 'DISTANCE', 'DISTANCES', 'DODGE', 'DODGED', 'DODGING', 'DOME', 'DOMES', 'DOWN', 'DOWNLOAD', 'DOWNLOADED', 'DOWNLOADING', 'DRAW', 'DROP', 'DROPPED', 'DROPPING', 'DYING', 'EAST', 'ELIMINATE', 'ELIMINATED', 'ELIMINATING', 'ELIMINATION', 'ELIMINATIONS', 'ENEMIES', 'ENEMY', 'ENTRANCE', 'ESCORT', 'ESCORTED', 'ESCORTING', 'EXCELLENT', 'EXIT', 'EXTREME', 'FAILED', 'FAILING', 'FAILURE', 'FALL', 'FALLEN', 'FALLING', 'FAR', 'FAST', 'FASTER', 'FASTEST', 'FAULT', 'FAULTS', 'FINAL', 'FINAL ALLIES', 'FINAL ALLY', 'FINAL ATTEMPT', 'FINAL CHECKPOINT', 'FINAL ENEMIES', 'FINAL ENEMY', 'FINAL FORM', 'FINAL GAME', 'FINAL HERO', 'FINAL HEROES', 'FINAL HOSTAGE', 'FINAL HOSTAGES', 'FINAL ITEM', 'FINAL LEVEL', 'FINAL MISSION', 'FINAL OBJECT', 'FINAL OBJECTIVE', 'FINAL OBJECTS', 'FINAL PHASE', 'FINAL PLAYER', 'FINAL PLAYERS', 'FINAL ROUND', 'FINAL TARGET', 'FINAL TARGETS', 'FINAL TIME', 'FINAL UPGRADE', 'FIND', 'FINDING', 'FINISH', 'FINISHED', 'FINISHING', 'FLOWN', 'FLY', 'FLYING', 'FORM', 'FORMS', 'FORWARD', 'FOUND', 'FREEZE', 'FREEZING', 'FROZEN', 'GAME', 'GAMES', 'GAMES LOST', 'GAMES WON', 'GG', 'GO', 'GOAL', 'GOALS', 'GOING', 'GOOD', 'GOOD LUCK', 'GOODBYE', 'GUILTY', 'HACK', 'HACKED', 'HACKING', 'HEAL', 'HEALED', 'HEALER', 'HEALERS', 'HEALING', 'HELLO', 'HELP', 'HERE', 'HERO', 'HEROES', 'HIDDEN', 'HIDE', 'HIDING', 'HIGH SCORE', 'HIGH SCORES', 'HMMM', 'HOSTAGE', 'HOSTAGES', 'HUH', 'HUNT', 'HUNTED', 'HUNTER', 'HUNTERS', 'HUNTING', 'I GIVE UP', 'I TRIED', 'IN VIEW', 'INCOMING', 'INITIAL', 'INITIAL ALLIES', 'INITIAL ALLY', 'INITIAL ATTEMPT', 'INITIAL CHECKPOINT', 'INITIAL ENEMIES', 'INITIAL ENEMY', 'INITIAL FORM', 'INITIAL GAME', 'INITIAL HERO', 'INITIAL HEROES', 'INITIAL HOSTAGE', 'INITIAL LEVEL', 'INITIAL MISSION', 'INITIAL OBJECT', 'INITIAL OBJECTIVE', 'INITIAL OBJECTS', 'INITIAL PHASE', 'INITIAL PLAYER', 'INITIAL PLAYERS', 'INITIAL ROUND', 'INITIAL TARGET', 'INITIAL TARGETS', 'INITIAL UPGRADE', 'INNOCENT', 'INSIDE', 'INVISIBLE', 'ITEM', 'ITEMS', 'JOIN', 'JOINED', 'JOINING', 'JUMP', 'JUMPING', 'KILL', 'KILLS', 'KILLSTREAK', 'KILLSTREAKS', 'LEADER', 'LEADERS', 'LEAST', 'LEFT', 'LESS', 'LEVEL', 'LEVELS', 'LIFE', 'LIMITED', 'LIVES', 'LOAD', 'LOADED', 'LOADING', 'LOCK', 'LOCKED', 'LOCKING', 'LOSER', 'LOSERS', 'LOSS', 'LOSSES', 'MAX', 'MILD', 'MIN', 'MISSION', 'MISSION', 'MISSION ABORTED', 'MISSION ACCOMPLISHED', 'MISSION FAILED', 'MISSIONS', 'MODERATE', 'MONEY', 'MORE', 'MOST', 'MY MISTAKE', 'NEAR', 'NEW HIGH SCORE', 'NEW RECORD', 'NEXT', 'NEXT ALLIES', 'NEXT ALLY', 'NEXT ATTEMPT', 'NEXT CHECKPOINT', 'NEXT ENEMIES', 'NEXT ENEMY', 'NEXT FORM', 'NEXT GAME', 'NEXT HERO', 'NEXT HEROES', 'NEXT HOSTAGE', 'NEXT HOSTAGES', 'NEXT LEVEL', 'NEXT MISSION', 'NEXT OBJECT', 'NEXT OBJECTIVE', 'NEXT OBJECTS', 'NEXT PHASE', 'NEXT PLAYER', 'NEXT PLAYERS', 'NEXT ROUND', 'NEXT TARGET', 'NEXT TARGETS', 'NEXT UPGRADE', 'NICE TRY', 'NO', 'NO THANKS', 'NONE', 'NORMAL', 'NORTH', 'NORTHEAST', 'NORTHWEST', 'NOT TODAY', 'OBJECT', 'OBJECTIVE', 'OBJECTIVES', 'OBJECTS', 'OBTAIN', 'OBTAINED', 'OBTAINING', 'OFF', 'ON', 'OOF', 'OOPS', 'OPTIMAL', 'OPTIMIZE', 'OPTIMIZED', 'OPTIMIZING', 'OUT OF VIEW', 'OUTGOING', 'OUTSIDE', 'OVER', 'OVERTIME', 'PAYLOAD', 'PAYLOADS', 'PHASE', 'PHASES', 'PICK', 'PICKED', 'PICKING', 'PLAYER', 'PLAYER', 'PLAYERS', 'PLAYERS', 'POINT', 'POINTS', 'POINTS EARNED', 'POINTS LOST', 'POWER-UP', 'POWER-UPS', 'PRICE', 'PROTECT', 'PROTECTED', 'PROTECTING', 'PURIFIED', 'PURIFY', 'PURIFYING', 'RAISE', 'RAISED', 'RAISING', 'RANK', 'RANK A', 'RANK B', 'RANK C', 'RANK D', 'RANK E', 'RANK F', 'RANK S', 'READY', 'RECORD', 'RECORDS', 'RECOVER', 'RECOVERED', 'RECOVERING', 'REMAIN', 'REMAINING', 'RESCUE', 'RESCUED', 'RESCUING', 'RESURRECT', 'RESURRECTED', 'RESURRECTING', 'REVEAL', 'REVEALED', 'REVEALING', 'RIGHT', 'ROUND', 'ROUND {0}', 'ROUNDS', 'ROUNDS LOST', 'ROUNDS WON', 'RUN', 'RUNNING', 'SAFE', 'SAVE', 'SAVED', 'SAVING', 'SCORE', 'SCORES', 'SECURE', 'SECURED', 'SECURING', 'SELL', 'SELLING', 'SEVER', 'SEVERE', 'SEVERED', 'SEVERING', 'SINK', 'SINKING', 'SLEEP', 'SLEEPING', 'SLEPT', 'SLOW', 'SLOWER', 'SLOWEST', 'SOLD', 'SORRY', 'SOUTH', 'SOUTHEAST', 'SOUTHWEST', 'SPARKLES', 'SPAWN', 'SPAWNED', 'SPAWNING', 'SPHERE', 'SPHERES', 'STABILIZE', 'STABILIZED', 'STABILIZING', 'STABLE', 'STAR', 'STARS', 'START', 'STARTED', 'STARTING', 'STATUS', 'STAY AWAY', 'STOP', 'STOPPED', 'STOPPING', 'STUN', 'STUNNED', 'STUNNING', 'SUBOPTIMAL', 'SUCCESS', 'SUDDEN DEATH', 'SUNK', 'SUPERB', 'SURVIVE', 'SURVIVED', 'SURVIVING', 'TARGET', 'TARGETS', 'TEAM', 'TEAMMATE', 'TEAMMATES', 'TEAMS', 'TERRIBLE', 'THANK YOU', 'THANKS', 'THAT WAS AWESOME', 'THREAT', 'THREAT LEVEL', 'THREAT LEVELS', 'THREATS', 'TIEBREAKER', 'TIME', 'TIMES', 'TOTAL', 'TRADE', 'TRADED', 'TRADING', 'TRAITOR', 'TRAITORS', 'TRANSFER', 'TRANSFERRED', 'TRANSFERRING', 'TRY AGAIN', 'TURRET', 'TURRETS', 'UGH', 'ULTIMATE ABILITY', 'UNDER', 'UNKNOWN', 'UNLIMITED', 'UNLOCK', 'UNLOCKED', 'UNLOCKING', 'UNSAFE', 'UNSTABLE', 'UP', 'UPGRADE', 'UPGRADES', 'UPLOAD', 'UPLOADED', 'UPLOADING', 'USE ABILITY 1', 'USE ABILITY 2', 'USE ULTIMATE ABILITY', 'VICTORY', 'VISIBLE', 'VORTEX', 'VORTICES', 'WAIT', 'WAITING', 'WALL', 'WALLS', 'WARNING', 'WELL PLAYED', 'WEST', 'WIN', 'WINNER', 'WINNERS', 'WINS', 'WORSE', 'WOW', 'YES', 'YOU', 'YOU LOSE', 'YOU WIN', 'ZONE', 'ZONES', '¡{0}!', '¿{0}?'] 133 | _extends = [] 134 | prefix = ["#{0}", "-> {0}", "<- {0}", "<-> {0}", "ROUND {0}"] 135 | surround = ["({0})", "¡{0}!", "¿{0}?"] 136 | postfix = ["{0} ->", "{0} <-", "{0} <->", "{0} M", "{0} M/S", "{0} SEC", "{0}!", "{0}!!", "{0}!!!", "{0}%", "{0}:", "{0}?", "{0}??", "{0}???"] 137 | binary = ["{0} - {1}", "{0} -> {1}", "{0} != {1}", "{0} * {1}", "{0} / {1}", "{0} {1}", "{0} + {1}", "{0} <- {1}", "{0} <-> {1}", "{0} < {1}", "{0} <= {1}", "{0} = {1}", "{0} == {1}", "{0} > {1}", "{0} >= {1}", "{0} AND {1}", "{0} VS {1}", "{0}, {1}", "{0}: {1}", "{0}:{1}"] 138 | ternary = ["{0} - {1} - {2}", "{0} {1} {2}", "{0} : {1} : {2}", "{0}, {1}, AND {2}", "{0}: {1} AND {2}"] 139 | normal = ["", "!", "!!", "!!!", "*", "----------", "...", "?", "??", "???", "ABILITIES", "ABILITY", "ABILITY 1", "ABILITY 2", "AGILITY", "ALERT", "ALIVE", "ALLIES", "ALLY", "AMMUNITION", "ANGLE", "ATTACK", "ATTACKED", "ATTACKING", "ATTEMPT", "ATTEMPTS", "AVERAGE", "AVOID", "AVOIDED", "AVOIDING", "BACKWARD", "BAD", "BAN", "BANNED", "BANNING", "BEST", "BETTER", "BID", "BIDS", "BLOCK", "BLOCKED", "BLOCKING", "BLUE", "BONUS", "BONUSES", "BOSS", "BOSSES", "BOUGHT", "BUILD", "BUILDING", "BUILT", "BURN", "BURNING", "BURNT", "BUY", "BUYING", "CAPTURE", "CAPTURED", "CAPTURING", "CAUTION", "CENTER", "CHALLENGE ACCEPTED", "CHARISMA", "CHASE", "CHASED", "CHASING", "CHECKPOINT", "CHECKPOINTS", "CLOUD", "CLOUDS", "CLUB", "CLUBS", "COMBO", "COME HERE", "CONDITION", "CONGRATULATIONS", "CONNECT", "CONNECTED", "CONNECTING", "CONSTITUTION", "CONTROL POINT", "CONTROL POINTS", "COOLDOWN", "COOLDOWNS", "CORRUPT", "CORRUPTED", "CORRUPTING", "CREDIT", "CREDITS", "CRITICAL", "CROUCH", "CROUCHED", "CROUCHING", "CURRENT", "CURRENT ALLIES", "CURRENT ALLY", "CURRENT ATTEMPT", "CURRENT CHECKPOINT", "CURRENT ENEMIES", "CURRENT ENEMY", "CURRENT FORM", "CURRENT GAME", "CURRENT HERO", "CURRENT HEROES", "CURRENT HOSTAGE", "CURRENT HOSTAGES", "CURRENT LEVEL", "CURRENT MISSION", "CURRENT OBJECT", "CURRENT OBJECTIVE", "CURRENT OBJECTS", "CURRENT PHASE", "CURRENT PLAYER", "CURRENT PLAYERS", "CURRENT ROUND", "CURRENT TARGET", "CURRENT TARGETS", "CURRENT UPGRADE", "DAMAGE", "DAMAGED", "DAMAGING", "DANGER", "DEAD", "DEAL", "DEALING", "DEALT", "DECK", "DECKS", "DEFEAT", "DEFEND", "DEFENDED", "DEFENDING", "DEFENSE", "DELIVER", "DELIVERED", "DELIVERING", "DEPTH", "DESTABILIZE", "DESTABILIZED", "DESTABILIZING", "DESTROY", "DESTROYED", "DESTROYING", "DETECT", "DETECTED", "DETECTING", "DEXTERITY", "DIAMOND", "DIAMONDS", "DIE", "DISCARD", "DISCARDED", "DISCARDING", "DISCONNECT", "DISCONNECTED", "DISCONNECTING", "DISTANCE", "DISTANCES", "DODGE", "DODGED", "DODGING", "DOME", "DOMES", "DOWN", "DOWNLOAD", "DOWNLOADED", "DOWNLOADING", "DRAW", "DRAWING", "DRAWN", "DROP", "DROPPED", "DROPPING", "DYING", "EAST", "ELIMINATE", "ELIMINATED", "ELIMINATING", "ELIMINATION", "ELIMINATIONS", "ENEMIES", "ENEMY", "ENTRANCE", "ESCORT", "ESCORTED", "ESCORTING", "EXCELLENT", "EXIT", "EXPERIENCE", "EXTREME", "FACE", "FACES", "FACING", "FAILED", "FAILING", "FAILURE", "FALL", "FALLEN", "FALLING", "FAR", "FAST", "FASTER", "FASTEST", "FAULT", "FAULTS", "FINAL", "FINAL ALLIES", "FINAL ALLY", "FINAL ATTEMPT", "FINAL CHECKPOINT", "FINAL ENEMIES", "FINAL ENEMY", "FINAL FORM", "FINAL GAME", "FINAL HERO", "FINAL HEROES", "FINAL HOSTAGE", "FINAL HOSTAGES", "FINAL ITEM", "FINAL LEVEL", "FINAL MISSION", "FINAL OBJECT", "FINAL OBJECTIVE", "FINAL OBJECTS", "FINAL PHASE", "FINAL PLAYER", "FINAL PLAYERS", "FINAL ROUND", "FINAL TARGET", "FINAL TARGETS", "FINAL TIME", "FINAL UPGRADE", "FIND", "FINDING", "FINISH", "FINISHED", "FINISHING", "FLOWN", "FLY", "FLYING", "FOLD", "FOLDED", "FOLDING", "FORM", "FORMS", "FORWARD", "FOUND", "FREEZE", "FREEZING", "FROZEN", "GAME", "GAMES", "GAMES LOST", "GAMES WON", "GG", "GO", "GOAL", "GOALS", "GOING", "GOOD", "GOOD LUCK", "GOODBYE", "GREEN", "GUILTY", "HACK", "HACKED", "HACKING", "HAND", "HANDS", "HEAL", "HEALED", "HEALER", "HEALERS", "HEALING", "HEART", "HEARTS", "HEIGHT", "HELLO", "HELP", "HERE", "HERO", "HEROES", "HIDDEN", "HIDE", "HIDING", "HIGH SCORE", "HIGH SCORES", "HIT", "HITTING", "HMMM", "HOSTAGE", "HOSTAGES", "HUH", "HUNT", "HUNTED", "HUNTER", "HUNTERS", "HUNTING", "I GIVE UP", "I TRIED", "IN VIEW", "INCOME", "INCOMING", "INITIAL", "INITIAL ALLIES", "INITIAL ALLY", "INITIAL ATTEMPT", "INITIAL CHECKPOINT", "INITIAL ENEMIES", "INITIAL ENEMY", "INITIAL FORM", "INITIAL GAME", "INITIAL HERO", "INITIAL HEROES", "INITIAL HOSTAGE", "INITIAL LEVEL", "INITIAL MISSION", "INITIAL OBJECT", "INITIAL OBJECTIVE", "INITIAL OBJECTS", "INITIAL PHASE", "INITIAL PLAYER", "INITIAL PLAYERS", "INITIAL ROUND", "INITIAL TARGET", "INITIAL TARGETS", "INITIAL UPGRADE", "INNOCENT", "INSIDE", "INTELLIGENCE", "INTERACT", "INVISIBLE", "ITEM", "ITEMS", "JOIN", "JOINED", "JOINING", "JUMP", "JUMPING", "KILL", "KILLS", "KILLSTREAK", "KILLSTREAK", "KILLSTREAKS", "LEADER", "LEADERS", "LEAST", "LEFT", "LESS", "LEVEL", "LEVEL DOWN", "LEVEL UP", "LEVELS", "LIFE", "LIMITED", "LIVES", "LOAD", "LOADED", "LOADING", "LOCK", "LOCKED", "LOCKING", "LOSER", "LOSERS", "LOSS", "LOSSES", "MAX", "MILD", "MIN", "MISSION", "MISSION", "MISSION ABORTED", "MISSION ACCOMPLISHED", "MISSION FAILED", "MISSIONS", "MODERATE", "MONEY", "MONSTER", "MONSTERS", "MORE", "MOST", "MY MISTAKE", "NEAR", "NEW HIGH SCORE", "NEW RECORD", "NEXT", "NEXT ALLIES", "NEXT ALLY", "NEXT ATTEMPT", "NEXT CHECKPOINT", "NEXT ENEMIES", "NEXT ENEMY", "NEXT FORM", "NEXT GAME", "NEXT HERO", "NEXT HEROES", "NEXT HOSTAGE", "NEXT HOSTAGES", "NEXT LEVEL", "NEXT MISSION", "NEXT OBJECT", "NEXT OBJECTIVE", "NEXT OBJECTS", "NEXT PHASE", "NEXT PLAYER", "NEXT PLAYERS", "NEXT ROUND", "NEXT TARGET", "NEXT TARGETS", "NEXT UPGRADE", "NICE TRY", "NO", "NO THANKS", "NONE", "NORMAL", "NORTH", "NORTHEAST", "NORTHWEST", "NOT TODAY", "OBJECT", "OBJECTIVE", "OBJECTIVES", "OBJECTS", "OBTAIN", "OBTAINED", "OBTAINING", "OFF", "ON", "OOF", "OOPS", "OPTIMAL", "OPTIMIZE", "OPTIMIZED", "OPTIMIZING", "OUT OF VIEW", "OUTGOING", "OUTSIDE", "OVER", "OVERTIME", "PARTICIPANT", "PARTICIPANTS", "PAYLOAD", "PAYLOADS", "PHASE", "PHASES", "PICK", "PICKED", "PICKING", "PILE", "PILES", "PLAY", "PLAYED", "PLAYER", "PLAYERS", "POINT", "POINTS", "POINTS EARNED", "POINTS LOST", "POWER", "POWER-UP", "POWER-UPS", "PRICE", "PRIMARY FIRE", "PROJECTILE", "PROJECTILE SPEED", "PROTECT", "PROTECTED", "PROTECTING", "PURIFIED", "PURIFY", "PURIFYING", "PURPLE", "RAISE", "RAISED", "RAISING", "RANK", "RANK A", "RANK B", "RANK C", "RANK D", "RANK E", "RANK F", "RANK S", "REACH", "REACHED", "REACHING", "READY", "RECORD", "RECORDS", "RECOVER", "RECOVERED", "RECOVERING", "RED", "REMAIN", "REMAINING", "RESCUE", "RESCUED", "RESCUING", "RESOURCE", "RESOURCES", "RESURRECT", "RESURRECTED", "RESURRECTING", "REVEAL", "REVEALED", "REVEALING", "REVERSE", "REVERSED", "REVERSING", "RIGHT", "ROUND", "ROUNDS", "ROUNDS LOST", "ROUNDS WON", "RUN", "RUNNING", "SAFE", "SAVE", "SAVED", "SAVING", "SCORE", "SCORES", "SECONDARY FIRE", "SECURE", "SECURED", "SECURING", "SELECT", "SELECTED", "SELECTING", "SELL", "SELLING", "SERVER LOAD", "SERVER LOAD AVERAGE", "SERVER LOAD PEAK", "SEVER", "SEVERE", "SEVERED", "SEVERING", "SINK", "SINKING", "SHOP", "SHOPS", "SHUFFLE", "SHUFFLED", "SHUFFLING", "SINK", "SINKING", "SKIP", "SKIPPED", "SKIPPING", "SLEEP", "SLEEPING", "SLEPT", "SLOW", "SLOWER", "SLOWEST", "SOLD", "SORRY", "SOUTH", "SOUTHEAST", "SOUTHWEST", "SPADE", "SPADES", "SPARKLES", "SPAWN", "SPAWNED", "SPAWNING", "SPEED", "SPEEDS", "SPHERE", "SPHERES", "STABILIZE", "STABILIZED", "STABILIZING", "STABLE", "STAR", "STARS", "START", "STARTED", "STARTING", "STATUS", "STAY", "STAY AWAY", "STAYED", "STAYING", "STOP", "STOPPED", "STOPPING", "STRENGTH", "STUN", "STUNNED", "STUNNING", "SUBOPTIMAL", "SUCCESS", "SUDDEN DEATH", "SUNK", "SUPERB", "SURVIVE", "SURVIVED", "SURVIVING", "TARGET", "TARGETS", "TEAM", "TEAMMATE", "TEAMMATES", "TEAMS", "TERRIBLE", "THANK YOU", "THANKS", "THAT WAS AWESOME", "THREAT", "THREAT LEVEL", "THREAT LEVELS", "THREATS", "TIEBREAKER", "TIME", "TIMES", "TOTAL", "TRADE", "TRADED", "TRADING", "TRAITOR", "TRAITORS", "TRANSFER", "TRANSFERRED", "TRANSFERRING", "TRY AGAIN", "TURRET", "TURRETS", "UGH", "ULTIMATE ABILITY", "UNDER", "UNKNOWN", "UNLIMITED", "UNLOCK", "UNLOCKED", "UNLOCKING", "UNSAFE", "UNSTABLE", "UP", "UPGRADE", "UPGRADES", "UPLOAD", "UPLOADED", "UPLOADING", "USE ABILITY 1", "USE ABILITY 2", "USE ULTIMATE ABILITY", "VICTORY", "VISIBLE", "VORTEX", "VORTICES", "WAIT", "WAITING", "WALL", "WALLS", "WARNING", "WELCOME", "WELL PLAYED", "WEST", "WHITE", "WILD", "WIN", "WINNER", "WINNERS", "WINS", "WISDOM", "WORSE", "WORST", "WOW", "YELLOW", "YES", "YOU", "YOU LOSE", "YOU WIN", "ZONE", "ZONES"] 140 | sorted_values = [sorted(x, key=len, reverse=True) for x in (prefix, surround, postfix, binary, ternary, normal)] 141 | 142 | class TeamConstant(WorkshopType): 143 | _values = ['ALL', 'ALL TEAMS', 'TEAM 1', 'TEAM 2'] 144 | _extends = [] 145 | 146 | class HeroConstant(WorkshopType): 147 | _values = ['SIGMA', 'ANA', 'ASHE', 'BAPTISTE', 'BASTION', 'BRIGITTE', 'D.VA', 'DOOMFIST', 'GENJI', 'HANZO', 'JUNKRAT', 'LÚCIO', 'MCCREE', 'MEI', 'MERCY', 'MOIRA', 'ORISA', 'PHARAH', 'REAPER', 'REINHARDT', 'ROADHOG', 'SOLDIER: 76', 'SOMBRA', 'SYMMETRA', 'TORBJÖRN', 'TRACER', 'WIDOWMAKER', 'WINSTON', 'WRECKING BALL', 'ZARYA', 'ZENYATTA'] 148 | _extends = [] 149 | 150 | class EventPlayer(WorkshopType): 151 | _values = ['ALL', 'SIGMA', 'ANA', 'ASHE', 'BAPTISTE', 'BASTION', 'BRIGITTE', 'D.VA', 'DOOMFIST', 'GENJI', 'HANZO', 'JUNKRAT', 'LÚCIO', 'MCCREE', 'MEI', 'MERCY', 'MOIRA', 'ORISA', 'PHARAH', 'REAPER', 'REINHARDT', 'ROADHOG', 'SLOT 0', 'SLOT 1', 'SLOT 10', 'SLOT 11', 'SLOT 2', 'SLOT 3', 'SLOT 4', 'SLOT 5', 'SLOT 6', 'SLOT 7', 'SLOT 8', 'SLOT 9', 'SOLDIER: 76', 'SOMBRA', 'SYMMETRA', 'TORBJÖRN', 'TRACER', 'WIDOWMAKER', 'WINSTON', 'WRECKING BALL', 'ZARYA', 'ZENYATTA'] 152 | _extends = [] 153 | 154 | class Variable(WorkshopType): 155 | _values = ['B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] 156 | _extends = [] 157 | 158 | def __init__(self, value, index): 159 | self.value = value 160 | self.index = index 161 | 162 | class PlayEffect(WorkshopType): 163 | _values = ['BAD EXPLOSION', 'BAD PICKUP EFFECT', 'BUFF EXPLOSION SOUND', 'BUFF IMPACT SOUND', 'DEBUFF IMPACT SOUND', 'EXPLOSION SOUND', 'GOOD EXPLOSION', 'GOOD PICKUP EFFECT', 'RING EXPLOSION', 'RING EXPLOSION SOUND'] 164 | _extends = [] 165 | 166 | class CreateEffect(WorkshopType): 167 | _values = ['BAD AURA', 'BAD AURA SOUND', 'BEACON SOUND', 'CLOUD', 'DECAL SOUND', 'ENERGY SOUND', 'GOOD AURA', 'GOOD AURA SOUND', 'LIGHT SHAFT', 'ORB', 'PICK-UP SOUND', 'RING', 'SMOKE SOUND', 'SPARKLES', 'SPARKLES SOUND', 'SPHERE'] 168 | _extends = [] 169 | 170 | class Communicate(WorkshopType): 171 | _values = ['ACKNOWLEDGE', 'EMOTE DOWN', 'EMOTE LEFT', 'EMOTE RIGHT', 'EMOTE UP', 'GROUP UP', 'HELLO', 'NEED HEALING', 'THANKS', 'ULTIMATE STATUS', 'VOICE LINE DOWN', 'VOICE LINE LEFT', 'VOICE LINE RIGHT', 'VOICE LINE UP'] 172 | _extends = [] 173 | 174 | class Icon(WorkshopType): 175 | _values = ['ARROW: DOWN', 'ARROW: LEFT', 'ARROW: RIGHT', 'ARROW: UP', 'ASTERISK', 'BOLT', 'CHECKMARK', 'CIRCLE', 'CLUB', 'DIAMOND', 'DIZZY', 'EXCLAMATION MARK', 'EYE', 'FIRE', 'FLAG', 'HALO', 'HAPPY', 'HEART', 'MOON', 'NO', 'PLUS', 'POISON', 'POISON 2', 'QUESTION MARK', 'RADIOACTIVE', 'RECYCLE', 'RING THICK', 'RING THIN', 'SAD', 'SKULL', 'SPADE', 'SPIRAL', 'STOP', 'TRASHCAN', 'WARNING', 'X'] 176 | _extends = [] 177 | 178 | class Relative(WorkshopType): 179 | _values = ['TO PLAYER', 'TO WORLD'] 180 | _extends = [] 181 | 182 | class Motion(WorkshopType): 183 | _values = ['CANCEL CONTRARY MOTION', 'INCORPORATE CONTRARY MOTION'] 184 | _extends = [] 185 | 186 | class RoundingType(WorkshopType): 187 | _values = ['DOWN', 'TO NEAREST', 'UP'] 188 | _extends = [] 189 | 190 | class LosCheck(WorkshopType): 191 | _values = ['OFF', 'SURFACES', 'SURFACES AND ALL BARRIERS', 'SURFACES AND ENEMY BARRIERS'] 192 | _extends = [] 193 | 194 | class WorldTextClipping(WorkshopType): 195 | _values = ['CLIP AGAINST SURFACES', 'DO NOT CLIP'] 196 | _extends = [] 197 | 198 | class HudLocation(WorkshopType): 199 | _values = ['LEFT', 'RIGHT', 'TOP'] 200 | _extends = [] 201 | 202 | class IconReevaluation(WorkshopType): 203 | _values = ['NONE', 'POSITION', 'VISIBLE TO', 'VISIBLE TO AND POSITION'] 204 | _extends = [] 205 | 206 | class EffectReevaluation(WorkshopType): 207 | _values = ['NONE', 'POSITION AND RADIUS', 'VISIBLE TO', 'VISIBLE TO, POSITION, AND RADIUS'] 208 | _extends = [] 209 | 210 | class HudTextReevaluation(WorkshopType): 211 | _values = ['STRING', 'VISIBLE TO AND STRING'] 212 | _extends = [] 213 | 214 | class WorldTextReevaluation(WorkshopType): 215 | _values = ['STRING', 'VISIBLE TO AND STRING', 'VISIBLE TO, POSITION, AND STRING'] 216 | _extends = [] 217 | 218 | class ChaseRateReevaluation(WorkshopType): 219 | _values = ['DESTINATION AND RATE', 'NONE'] 220 | _extends = [] 221 | 222 | class ChaseTimeReevaluation(WorkshopType): 223 | _values = ['DESTINATION AND DURATION', 'NONE'] 224 | _extends = [] 225 | 226 | class ObjectiveDescriptionReevaluation(WorkshopType): 227 | _values = ['STRING', 'VISIBLE TO AND STRING'] 228 | _extends = [] 229 | 230 | class DamageModificationReevaluation(WorkshopType): 231 | _values = ['NONE', 'RECEIVERS AND DAMAGERS', 'RECEIVERS, DAMAGERS, AND DAMAGE PERCENT'] 232 | _extends = [] 233 | 234 | class FacingReevaluation(WorkshopType): 235 | _values = ['NONE', 'DIRECTION AND TURN RATE'] 236 | _extends = [] 237 | 238 | class WaitBehavior(WorkshopType): 239 | _values = ['ABORT WHEN FALSE', 'IGNORE CONDITION', 'RESTART WHEN TRUE'] 240 | _extends = [] 241 | 242 | class BarriersLos(WorkshopType): 243 | _values = ['ALL BARRIERS BLOCK LOS', 'BARRIERS DO NOT BLOCK LOS', 'ENEMY BARRIERS BLOCK LOS'] 244 | _extends = [] 245 | 246 | class Status(WorkshopType): 247 | _values = ['ASLEEP', 'BURNING', 'FROZEN', 'HACKED', 'INVINCIBLE', 'KNOCKED DOWN', 'PHASED OUT', 'ROOTED', 'STUNNED', 'UNKILLABLE'] 248 | _extends = [] 249 | 250 | class CompareOperator(WorkshopType): 251 | _values = ['==', '!=', '<', '<=', '>', '>='] 252 | _extends = [] 253 | 254 | class Any(WorkshopType): 255 | _values = ['ANY'] 256 | _extends = [] 257 | 258 | class Boolean(WorkshopType): 259 | _values = ['AND', 'ARRAY CONTAINS', 'COMPARE', 'ENTITY EXISTS', 'EVENT WAS CRITICAL HIT', 'FALSE', 'HAS SPAWNED', 'HAS STATUS', 'IS ALIVE', 'IS TRUE FOR ALL', 'IS TRUE FOR ANY', 'IS USING ULTIMATE', 'IS ASSEMBLING HEROES', 'IS BETWEEN ROUNDS', 'IS BUTTON HELD', 'IS COMMUNICATING', 'IS COMMUNICATING ANY', 'IS COMMUNICATING ANY EMOTE', 'IS COMMUNICATING ANY VOICE LINE', 'IS CONTROL MODE POINT LOCKED', 'IS CROUCHING', 'IS CTF MODE IN SUDDEN DEATH', 'IS DEAD', 'IS FIRING PRIMARY', 'IS FIRING SECONDARY', 'IS FLAG AT BASE', 'IS FLAG BEING CARRIED', 'IS GAME IN PROGRESS', 'IS HERO BEING PLAYED', 'IS IN AIR', 'IS IN LINE OF SIGHT', 'IS IN SETUP', 'IS IN SPAWN ROOM', 'IS IN VIEW ANGLE', 'IS MATCH COMPLETE', 'IS MOVING', 'IS OBJECTIVE COMPLETE', 'IS ON GROUND', 'IS ON OBJECTIVE', 'IS ON WALL', 'IS PORTRAIT ON FIRE', 'IS STANDING', 'IS TEAM ON DEFENSE', 'IS TEAM ON OFFENSE', 'IS USING ABILITY 1', 'IS USING ABILITY 2', 'IS WAITING FOR PLAYERS', 'NOT', 'OR', 'TRUE'] 260 | _extends = [Any] 261 | 262 | class Hero(WorkshopType): 263 | _values = ['ALL HEROES', 'ALLOWED HEROES', 'HERO', 'HERO OF'] 264 | _extends = [Any] 265 | 266 | class Number(WorkshopType): 267 | _values = ['INDEX OF ARRAY VALUE', 'EVENT HEALING', 'ABSOLUTE VALUE', 'ALTITUDE OF', 'ANGLE DIFFERENCE', 'CONTROL MODE SCORING PERCENTAGE', 'COSINE FROM DEGREES', 'COSINE FROM RADIANS', 'COUNT OF', 'DISTANCE BETWEEN', 'DOT PRODUCT', 'EVENT DAMAGE', 'HEALTH', 'HEALTH PERCENT', 'HORIZONTAL ANGLE FROM DIRECTION', 'HORIZONTAL ANGLE TOWARDS', 'HORIZONTAL FACING ANGLE OF', 'HORIZONTAL SPEED OF', 'LAST DAMAGE MODIFICATION ID', 'LAST DAMAGE OVER TIME ID', 'LAST HEAL OVER TIME ID', 'LAST TEXT ID', 'MATCH ROUND', 'MATCH TIME', 'MAX', 'MAX HEALTH', 'MIN', 'MODULO', 'NUMBER', 'NUMBER OF DEAD PLAYERS', 'NUMBER OF DEATHS', 'NUMBER OF ELIMINATIONS', 'NUMBER OF FINAL BLOWS', 'NUMBER OF HEROES', 'NUMBER OF LIVING PLAYERS', 'NUMBER OF PLAYERS', 'NUMBER OF PLAYERS ON OBJECTIVE', 'OBJECTIVE INDEX', 'PAYLOAD PROGRESS PERCENTAGE', 'POINT CAPTURE PERCENTAGE', 'RAISE TO POWER', 'RANDOM INTEGER', 'RANDOM REAL', 'ROUND TO INTEGER', 'SCORE OF', 'SINE FROM DEGREES', 'SINE FROM RADIANS', 'SLOT OF', 'SPEED OF', 'SPEED OF IN DIRECTION', 'SQUARE ROOT', 'TEAM SCORE', 'TOTAL TIME ELAPSED', 'ULTIMATE CHARGE PERCENT', 'VERTICAL ANGLE FROM DIRECTION', 'VERTICAL ANGLE TOWARDS', 'VERTICAL FACING ANGLE OF', 'VERTICAL SPEED OF', 'X COMPONENT OF', 'Y COMPONENT OF', 'Z COMPONENT OF'] 268 | _extends = [Any] 269 | 270 | def __init__(self, value): 271 | self.value = value 272 | 273 | def __int__(self): 274 | return int(self.value) 275 | 276 | def __add__(self, other): 277 | return float(self.value) + float(other.value) 278 | 279 | def __sub__(self, other): 280 | return float(self.value) - float(other.value) 281 | 282 | def __mul__(self, other): 283 | return float(self.value) * float(other.value) 284 | 285 | def __truediv__(self, other): 286 | return float(self.value) / float(other.value) 287 | 288 | def __pow__(self, other): 289 | return float(self.value) ** float(other.value) 290 | 291 | def __mod__(self, other): 292 | return float(self.value) % float(other.value) 293 | 294 | def __repr__(self): 295 | return '{}'.format(self.value) 296 | 297 | class Vector(WorkshopType): 298 | _values = ['VELOCITY OF'] 299 | _extends = [Any] 300 | 301 | 302 | class Direction(WorkshopType): 303 | _values = ['DIRECTION TOWARDS', 'FACING DIRECTION OF', 'RAY CAST HIT NORMAL', 'VECTOR TOWARDS', 'LEFT', 'RIGHT', 'FORWARD', 'BACKWARD', 'UP', 'DOWN'] 304 | _extends = [Vector] 305 | 306 | class Position(WorkshopType): 307 | _values = ['EYE POSITION', 'FLAG POSITION', 'NEAREST WALKABLE POSITION', 'OBJECTIVE POSITION', 'PAYLOAD POSITION', 'POSITION OF', 'RAY CAST HIT POSITION'] 308 | _extends = [Vector] 309 | 310 | class BaseVector(WorkshopType): 311 | _values = ['CROSS PRODUCT', 'DIRECTION FROM ANGLES', 'LOCAL VECTOR OF', 'NORMALIZE', 'THROTTLE OF', 'VECTOR', 'WORLD VECTOR OF'] 312 | _extends = [Direction, Position] 313 | 314 | class Player(WorkshopType): 315 | _values = ['HEALER', 'HEALEE', 'HOST PLAYER', 'PLAYERS IN VIEW ANGLE', 'PLAYER CLOSEST TO RETICLE', 'ALL DEAD PLAYERS', 'ALL LIVING PLAYERS', 'ALL PLAYERS', 'ALL PLAYERS NOT ON OBJECTIVE', 'ALL PLAYERS ON OBJECTIVE', 'ATTACKER', 'CLOSEST PLAYER TO', 'EVENT PLAYER', 'FARTHEST PLAYER FROM', 'LAST CREATED ENTITY', 'NULL', 'PLAYER CARRYING FLAG', 'PLAYERS IN SLOT', 'PLAYERS ON HERO', 'PLAYERS WITHIN RADIUS', 'RAY CAST HIT PLAYER', 'VICTIM'] 316 | _extends = [BaseVector] 317 | 318 | 319 | class Team(WorkshopType): 320 | _values = ['CONTROL MODE SCORING TEAM', 'OPPOSITE TEAM OF', 'TEAM', 'TEAM OF'] 321 | _extends = [Any] 322 | 323 | class String(WorkshopType): 324 | _values = ['HERO ICON STRING', 'STRING'] 325 | _extends = [Any] 326 | 327 | def __init__(self, value, length=None): 328 | super().__init__() 329 | self.value = value 330 | self.length = length 331 | 332 | def get_length(self): 333 | return self.length + sum(child.get_length for child in self.children) 334 | 335 | def __repr__(self): 336 | return '{}({}){}'.format(self.value, ', '.join(map(repr, self.children)), f'[{self.length}]' if self.length else '') 337 | 338 | class CustomString(WorkshopType): 339 | _values = ['HERO ICON STRING', 'STRING'] 340 | _extends = [Any] 341 | 342 | def __init__(self, value, length=None): 343 | super().__init__() 344 | self.value = value 345 | self.length = length 346 | 347 | def get_length(self): 348 | return self.length + sum(child.get_length for child in self.children) 349 | 350 | def __repr__(self): 351 | return '{}({}){}'.format(self.value, ', '.join(map(repr, self.children)), f'[{self.length}]' if self.length else '') 352 | 353 | class Base(WorkshopType): 354 | _values = ['ADD', 'APPEND TO ARRAY', 'ARRAY SLICE', 'CURRENT ARRAY ELEMENT', 'DIVIDE', 'EMPTY ARRAY', 'FILTERED ARRAY', 'FIRST OF', 'GLOBAL VARIABLE', 'LAST OF', 'MULTIPLY', 'PLAYER VARIABLE', 'RANDOM VALUE IN ARRAY', 'RANDOMIZED ARRAY', 'REMOVE FROM ARRAY', 'SORTED ARRAY', 'SUBTRACT', 'VALUE IN ARRAY'] 355 | _extends = [Any, Boolean, Hero, Number, Direction, Position, Player, Team] 356 | 357 | class Terminal(AST): 358 | def __init__(self, value): 359 | super().__init__() 360 | self.value = value 361 | 362 | def __repr__(self): 363 | return '{}'.format(self.value) 364 | 365 | class Data(AST): 366 | def __init__(self, name): 367 | super().__init__() 368 | self.name = name 369 | 370 | def __repr__(self): 371 | if not self.children: 372 | return self.name 373 | return '{}({})'.format(self.name, self.format_children) 374 | 375 | class OWID(AST): 376 | def __init__(self, name, description='', args=[]): 377 | super().__init__() 378 | self.name = name 379 | self.description = description 380 | self.args = args 381 | 382 | def __repr__(self): 383 | return '{}({})'.format(self.name, ', '.join(map(repr, self.args))) 384 | 385 | class Constant(AST): 386 | def __init__(self, name): 387 | self.name = name 388 | 389 | def __repr__(self): 390 | return self.name 391 | 392 | def halt(self, tp): 393 | return Raw(code='Apply Impulse({}, Down, Multiply(0.001, 0.001), To World, Cancel Contrary Motion)'.format(self.name.title())) 394 | 395 | class BinaryOp(AST): 396 | def __init__(self, left, op, right): 397 | self.left = left 398 | self.op = op 399 | self.right = right 400 | 401 | def __repr__(self): 402 | return '{} {} {}'.format(self.left, self.op, self.right) 403 | 404 | class UnaryOp(AST): 405 | def __init__(self, op, right): 406 | self.op = op 407 | self.right = right 408 | 409 | def __repr__(self): 410 | return '{} {}'.format(self.op, self.right) 411 | 412 | class Trailer(AST): 413 | def __init__(self, parent): 414 | self.parent = parent 415 | 416 | class Script(AST): 417 | pass 418 | 419 | class Rule(Data): 420 | def __init__(self, name, disabled): 421 | super().__init__(name) 422 | self.disabled = disabled 423 | 424 | class Ruleblock(Data): 425 | pass 426 | 427 | class Block(AST): 428 | def __repr__(self): 429 | return '{}'.format(self.format_children) 430 | 431 | class Time(Terminal): 432 | pass 433 | 434 | class Array(AST): 435 | def __init__(self, elements=None): 436 | self.elements = elements or [] 437 | 438 | def append(self, tp, elem): 439 | elem = tp.visit(elem, tp.scope) 440 | if type(elem) != Object: 441 | elem = Raw(code=elem) 442 | self.elements.append(elem) 443 | 444 | def index(self, elem): 445 | return self.elements.index(elem) 446 | 447 | def __iter__(self): 448 | return iter(self.elements) 449 | 450 | def __len__(self): 451 | return len(self.elements) 452 | 453 | def __setitem__(self, index, item): 454 | while index > len(self) - 1: 455 | self.elements.append(Number(value='0')) 456 | self.elements.__setitem__(index, item) 457 | 458 | def __getitem__(self, index): 459 | return self.elements.__getitem__(index) 460 | 461 | def __repr__(self): 462 | return '{}'.format(self.elements) 463 | 464 | class Compare(BinaryOp): 465 | pass 466 | 467 | class Assign(BinaryOp): 468 | pass 469 | 470 | class GlobalVar(AST): 471 | def __init__(self, letter, index=None): 472 | self.letter = letter 473 | self.index = index 474 | 475 | def __repr__(self): 476 | return 'Global.{}[{}]'.format(self.letter, self.index) 477 | 478 | class PlayerVar(AST): 479 | def __init__(self, letter, index=None, player=None): 480 | self.letter = letter 481 | self.index = index 482 | self.player = player or Constant(name='Event Player') 483 | 484 | def __repr__(self): 485 | return 'Player@{}.{}[{}]'.format(self.player, self.letter, self.index) 486 | 487 | class Var(AST): 488 | GLOBAL = 0 489 | PLAYER = 1 490 | INTERNAL = 2 491 | BUILTIN = 3 492 | CONST = 4 493 | STRING = 5 494 | CLASS = 6 495 | OBJECT = 7 496 | METHOD = 8 497 | 498 | def __init__(self, name, type_, value=None, data=None, player=None, chase=False): 499 | self.name = name 500 | self.type = type_ 501 | self.value = value 502 | self.data = data 503 | self.player = player 504 | 505 | @property 506 | def _type(self): 507 | return { 508 | 0: 'GLOBAL', 509 | 1: 'PLAYER', 510 | 2: 'INTERNAL', 511 | 3: 'BUILTIN', 512 | 4: 'CONST', 513 | 5: 'STRING', 514 | 6: 'CLASS', 515 | 7: 'OBJECT', 516 | 8: 'METHOD' 517 | }.get(self.type) 518 | 519 | def __repr__(self): 520 | if self.value or self.data: 521 | return '<{}: type={}, value={}, data={}>'.format(self.name, self._type, self.value, self.data) 522 | return '{}'.format(self.name) 523 | 524 | class If(AST): 525 | def __init__(self, cond, true_block, false_block=None): 526 | self.cond = cond 527 | self.true_block = true_block 528 | self.false_block = false_block 529 | 530 | def __repr__(self): 531 | return 'if {}: {} | else: {}'.format(self.cond, self.true_block, self.false_block) 532 | 533 | class While(AST): 534 | def __init__(self, cond, body): 535 | self.cond = cond 536 | self.body = body 537 | 538 | def __repr__(self): 539 | return 'while {}: {}'.format(self.cond, self.body) 540 | 541 | class For(AST): 542 | def __init__(self, pointer, iterable, body): 543 | self.pointer = pointer 544 | self.iterable = iterable 545 | self.body = body 546 | 547 | def __repr__(self): 548 | return 'for {} in {}: {}'.format(self.pointer, self.iterable, self.body) 549 | 550 | class Function(AST): 551 | def __init__(self, name, params): 552 | super().__init__() 553 | self.name = name 554 | self.params = params 555 | self.closure = None 556 | 557 | @property 558 | def arity(self): 559 | return len(self.params) 560 | 561 | @property 562 | def min_arity(self): 563 | return len([p for p in self.params if not p.optional]) 564 | 565 | def __repr__(self): 566 | return '%{}({}): {}'.format(self.name, ', '.join(map(repr, self.params)), self.format_children) 567 | 568 | class Parameter(AST): 569 | def __init__(self, name, optional=False, default=None): 570 | self.name = name 571 | self.optional = optional 572 | self.default = None or Constant(name='Null') 573 | 574 | def __repr__(self): 575 | return 'param {}{}'.format(self.name, '?=' + repr(self.default) if self.default else '') 576 | 577 | class Class(AST): 578 | def __init__(self, name, body): 579 | self.name = name 580 | self.body = body 581 | self.closure = None 582 | 583 | def __repr__(self): 584 | return 'class {}'.format(self.name) 585 | 586 | class Object(): 587 | def __init__(self, type_): 588 | self.type = type_ 589 | self.env = {} 590 | 591 | def __getattr__(self, attr): 592 | return self.env.get(attr) 593 | 594 | def __repr__(self): 595 | return ''.format(self.type.name) 596 | 597 | class Return(AST): 598 | def __init__(self, value): 599 | self.value = value 600 | 601 | def __repr__(self): 602 | return 'return {}'.format(self.value) 603 | 604 | class Attribute(Trailer): 605 | def __init__(self, name, parent): 606 | super().__init__(parent) 607 | self.name = name 608 | 609 | def __repr__(self): 610 | return '{}.{}'.format(self.parent, self.name) 611 | 612 | class Call(Trailer): 613 | def __init__(self, args, parent): 614 | super().__init__(parent) 615 | self.args = args 616 | 617 | def __repr__(self): 618 | return '{}({})'.format(self.parent, self.args) 619 | 620 | class Item(Trailer): 621 | def __init__(self, index, parent): 622 | super().__init__(parent) 623 | self.index = index 624 | 625 | def __repr__(self): 626 | return '{}[{}]'.format(self.parent, self.index) 627 | -------------------------------------------------------------------------------- /OWScript/Errors.py: -------------------------------------------------------------------------------- 1 | import sys 2 | TEXT = None 3 | 4 | class ExitCode: 5 | CompileError = 1 6 | InputNotFound = 2 7 | OutputNotFound = 3 8 | 9 | class Logger: 10 | INFO = 1 11 | WARN = 2 12 | DEBUG = 3 13 | def __init__(self, log_level=WARN): 14 | self.log_level = log_level 15 | 16 | def info(self, *msg): 17 | if self.log_level >= Logger.INFO: 18 | sys.stderr.write('[INFO] {}\n'.format(' '.join(map(str, msg)))) 19 | 20 | def warn(self, *msg): 21 | if self.log_level >= Logger.WARN: 22 | sys.stderr.write('[WARNING] {}\n'.format(' '.join(map(str, msg)))) 23 | 24 | def debug(self, *msg): 25 | if self.log_level >= Logger.DEBUG: 26 | sys.stderr.write('[DEBUG] {}\n'.format(' '.join(map(str, msg)))) 27 | 28 | class OWSError(Exception): 29 | def __init__(self, msg, pos=None): 30 | global TEXT 31 | if pos: 32 | line, col = pos 33 | text = '\n' + TEXT.split('\n')[line - 1].replace('\t', ' ' * 4) 34 | char = '\n' + ' ' * (col - 1) + '^\n' 35 | msg = 'Line {}'.format(line) + text + char + msg 36 | super().__init__(msg) 37 | 38 | class LexError(OWSError): 39 | pass 40 | 41 | class ParseError(OWSError): 42 | pass 43 | 44 | class ImportError(OWSError): 45 | pass 46 | 47 | class SyntaxError(OWSError): 48 | pass 49 | 50 | class InvalidParameter(SyntaxError): 51 | pass 52 | 53 | class StringError(SyntaxError): 54 | pass 55 | 56 | class NameError(SyntaxError): 57 | pass 58 | 59 | class AttributeError(SyntaxError): 60 | pass 61 | 62 | class FileNotFoundError(OWSError): 63 | pass 64 | 65 | class NotImplementedError(OWSError): 66 | pass 67 | 68 | class ReturnError(Exception): 69 | def __init__(self, value): 70 | self.value = value -------------------------------------------------------------------------------- /OWScript/Importer.py: -------------------------------------------------------------------------------- 1 | from .Lexer import Lexer 2 | from .Parser import Parser 3 | 4 | def import_file(path): 5 | with open(path) as f: 6 | text = f.read() + '\n' 7 | try: 8 | lexer = Lexer(text=text) 9 | tokens = lexer.lex() 10 | parser = Parser(tokens=tokens) 11 | tree = parser.script() 12 | return tree 13 | except Exception as ex: 14 | raise ex -------------------------------------------------------------------------------- /OWScript/Lexer.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import re 3 | 4 | from . import Errors 5 | from .Tokens import Token, Tokens 6 | 7 | class Lexer: 8 | IGNORE = ('WHITESPACE', 'SEMI', 'COMMENT', 'ANNOTATION') 9 | NEWLINE = functools.partial(Token, type='NEWLINE', value='\n') 10 | INDENT = functools.partial(Token, type='INDENT', value='⮡') 11 | DEDENT = functools.partial(Token, type='DEDENT', value='⮢') 12 | EOF = functools.partial(Token, type='EOF', value='') 13 | def __init__(self, text): 14 | self.text = text 15 | self.pos = 0 16 | self.line = 1 17 | self.column = 1 18 | self.indents = [] 19 | self.tokens = [] 20 | 21 | def print_tokens(self): 22 | """Outputs the current token list.""" 23 | tokens = '\n'.join(map(repr, self.tokens)) 24 | print(tokens) 25 | return tokens 26 | 27 | def lex(self): 28 | """Tokenizes input into a list of tokens.""" 29 | expressions = [(token, re.compile(pattern, re.I)) for token, pattern in Tokens.__annotations__.items()] 30 | whitespace_pattern = re.compile(Tokens.__annotations__.get('WHITESPACE')) 31 | self.indents.append(0) 32 | while self.pos < len(self.text): 33 | for token_type, pattern in expressions: 34 | match = pattern.match(self.text, self.pos) 35 | if match: 36 | value = match.group(1) if token_type == 'OWID' else match.group(0) 37 | token = Token(type=token_type, value=value, line=self.line, column=self.column) 38 | if value: 39 | self.pos += len(value) 40 | if token.type == 'NEWLINE': 41 | token.value = r'\n' 42 | self.tokens.append(token) 43 | self.line += match.group(0).count('\n') 44 | match = whitespace_pattern.match(self.text, self.pos) 45 | self.column = len(match.group(0).replace('\t', ' ' * 4)) + 1 if match else 1 46 | spaces = self.column - 1 47 | if spaces > self.indents[-1]: 48 | indent = Lexer.INDENT(line=self.line, column=self.column) 49 | self.tokens.append(indent) 50 | self.indents.append(spaces) 51 | elif spaces < self.indents[-1]: 52 | while spaces < self.indents[-1]: 53 | self.indents.pop() 54 | self.column = spaces + 1 55 | dedent = Lexer.DEDENT(line=self.line, column=self.column) 56 | self.tokens.append(dedent) 57 | else: 58 | self.column = spaces + 1 59 | if match: 60 | self.pos = match.end() 61 | break 62 | elif token.type in Lexer.IGNORE: 63 | self.column += len(token.value) 64 | self.line += token.value.count('\n') 65 | break 66 | else: 67 | self.tokens.append(token) 68 | self.column += len(token.value) 69 | break 70 | else: 71 | Errors.POS = (self.line, self.column) 72 | raise Errors.LexError("Unexpected symbol '{}'".format(self.text[self.pos])) 73 | while self.indents[-1] > 0: 74 | self.column = self.indents.pop() 75 | dedent = Lexer.DEDENT(line=self.line, column=self.column) 76 | self.tokens.append(dedent) 77 | self.tokens.append(Lexer.EOF(line=self.line, column=0)) 78 | return self.tokens -------------------------------------------------------------------------------- /OWScript/Parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import partial 3 | 4 | from . import Errors 5 | from .AST import * 6 | from .Tokens import ALIASES 7 | from .Workshop import * 8 | 9 | class Parser: 10 | def __init__(self, tokens): 11 | self.tokens = tokens 12 | self.pos = 0 13 | self.chase_vars = set() 14 | self.map_rule = False 15 | 16 | @property 17 | def curtoken(self): 18 | """Returns the token at the current index.""" 19 | return self.tokens[self.pos] 20 | 21 | @property 22 | def curtype(self): 23 | """Returns the type of the current token.""" 24 | return self.curtoken.type 25 | 26 | @property 27 | def curpos(self): 28 | """Returns the tuple (line, column) of the current token position.""" 29 | return self.curtoken.line, self.curtoken.column 30 | 31 | @property 32 | def curvalue(self): 33 | """Returns the value of the current token.""" 34 | return self.curtoken.value 35 | 36 | def peek(self, n=1): 37 | """Returns the nth upcoming token.""" 38 | try: 39 | return self.tokens[self.pos + n] 40 | except IndexError: 41 | print('Cannot peek further than token length') 42 | 43 | def eat(self, *tokens): 44 | """Consumes a token and moves on to the next one.""" 45 | if len(tokens) > 1: 46 | for token in tokens: 47 | self.eat(token) 48 | return 49 | # print(self.curtoken) 50 | token_type = tokens[0] 51 | pos = self.curpos 52 | if self.curtype == token_type: 53 | self.pos += 1 54 | else: 55 | raise Errors.ParseError('Expected token of type {}, but received {}'.format(token_type, self.curtype), pos=pos) 56 | 57 | def parse_string(self, string, formats, _pos): 58 | string = re.sub(r'["\'`]', '', string) 59 | null = Constant(name='Null') 60 | if string == '{}': 61 | node = String(value='{0}') 62 | node.children = [formats[0]] + [null] * 2 63 | return node 64 | elif string == '': 65 | node = String(value='') 66 | node.children = [null] * 3 67 | return node 68 | else: 69 | match = re.match(r'^ $| (?: +)$', string) 70 | if match: 71 | empty_string = String(value='') 72 | empty_string.children = [null] * 3 73 | 74 | node = String(value='{0} {1}') 75 | node.children = [ 76 | empty_string if len(match.group(0)) == 1 else self.parse_string(string[1:], formats, _pos), 77 | empty_string, 78 | null 79 | ] 80 | return node 81 | for group in StringConstant.sorted_values: 82 | for pattern in group: 83 | patt = re.sub(r'([^a-zA-Z0-9_\s{}])', r'\\\1', pattern) 84 | patt = re.sub(r'{\d}', r'(.*?)', patt) + '$' 85 | match = re.match(patt, string, re.I) 86 | if match and not match.group(0) == '': 87 | groups = match.groups() 88 | children = [] 89 | try: 90 | for group in groups: 91 | if group == '{}': 92 | child = formats[0] 93 | formats = formats[1:] 94 | else: 95 | child = self.parse_string(group, formats, _pos) 96 | children.append(child) 97 | except Errors.StringError: 98 | continue 99 | node = String(value=pattern) 100 | node.children = children + [null] * (3 - len(children)) 101 | return node 102 | else: 103 | raise Errors.StringError('Invalid string \'{}\''.format(string), pos=_pos) 104 | 105 | def script(self): 106 | """script : (NEWLINE | stmt)* EOF""" 107 | node = Script() 108 | while self.curtype != 'EOF': 109 | if self.curtype == 'NEWLINE': 110 | self.eat('NEWLINE') 111 | else: 112 | node.children.append(self.stmt()) 113 | node.chase_vars = self.chase_vars 114 | node.map_rule = self.map_rule 115 | return node 116 | 117 | def stmt(self): 118 | """stmt : (funcdef | ruledef | importdef | classdef | line)""" 119 | if self.curvalue == '%': 120 | return self.funcdef() 121 | elif self.curtype in ('DISABLED', 'RULE'): 122 | return self.ruledef() 123 | elif self.curtype == 'IMPORT': 124 | return self.importdef() 125 | elif self.curtype == 'CLASS': 126 | return self.classdef() 127 | else: 128 | return self.line() 129 | 130 | def funcdef(self): 131 | """funcdef : % NAME params? funcbody""" 132 | self.eat('MOD') 133 | name = self.curvalue 134 | self.eat('NAME') 135 | params = [] 136 | if self.curtype == 'LPAREN': 137 | params = self.params() 138 | body = self.funcbody() 139 | node = Function(name=name, params=params) 140 | node.children.extend(body) 141 | return node 142 | 143 | def params(self): 144 | """params : (expr ( , expr)*)""" 145 | self.eat('LPAREN') 146 | params = [] 147 | while self.curtype != 'RPAREN': 148 | param = Parameter(name=self.curvalue) 149 | self.eat('NAME') 150 | if self.curtype == 'QUERY': 151 | param.optional = True 152 | self.eat('QUERY') 153 | if self.curvalue == '=': 154 | self.eat('ASSIGN') 155 | param.default = self.expr() 156 | params.append(param) 157 | if self.curtype == 'COMMA': 158 | self.eat('COMMA') 159 | self.eat('RPAREN') 160 | return params 161 | 162 | def funcbody(self): 163 | """funcbody : NEWLINE INDENT (ruledef | ruleblock | block)+ DEDENT""" 164 | self.eat('NEWLINE', 'INDENT') 165 | body = [] 166 | while self.curtype != 'DEDENT': 167 | if self.curtype in ('DISABLED', 'RULE'): 168 | body.append(self.ruledef()) 169 | elif self.curtype == 'RULEBLOCK': 170 | body.append(self.ruleblock()) 171 | else: 172 | body.append(self.block()) 173 | self.eat('DEDENT') 174 | return body 175 | 176 | def ruledef(self): 177 | """ruledef : DISABLED? RULE STRING (+ STRING|NAME)* NEWLINE INDENT (ruleblock | call)+ DEDENT""" 178 | disabled = self.curtype == 'DISABLED' 179 | if disabled: 180 | self.eat('DISABLED') 181 | self.eat('RULE') 182 | name = [] 183 | while self.curtype != 'NEWLINE': 184 | if self.curtype == 'STRING': 185 | name.append(self.curvalue.replace('"', '')) 186 | elif self.curtype == 'NAME': 187 | var = Var(self.curvalue, type_=Var.STRING) 188 | var._pos = self.curpos 189 | name.append(var) 190 | elif self.curtype == 'DOT': 191 | self.eat('DOT') 192 | node = Attribute(name=self.curvalue, parent=name.pop()) 193 | node._pos = self.curpos 194 | name.append(node) 195 | else: 196 | raise Errors.ParseError('Unexpected token \'{}\' in rule name'.format(self.curvalue), pos=self.curpos) 197 | self.eat(self.curtype) 198 | if self.curtype == 'PLUS': 199 | self.eat('PLUS') 200 | node = Rule(name=name, disabled=disabled) 201 | self.eat('NEWLINE', 'INDENT') 202 | while self.curtype != 'DEDENT': 203 | if self.curtype == 'RULEBLOCK': 204 | node.children.append(self.ruleblock()) 205 | else: 206 | node.children.append(self.primary()) 207 | self.eat('NEWLINE') 208 | self.eat('DEDENT') 209 | return node 210 | 211 | def ruleblock(self): 212 | """ruleblock : (RULEBLOCK block)+""" 213 | node = Block() 214 | while self.curtype == 'RULEBLOCK': 215 | ruleblock = Ruleblock(name=self.curvalue) 216 | self.eat('RULEBLOCK') 217 | if self.peek(1).type == 'INDENT': 218 | ruleblock.children.append(self.block()) 219 | else: 220 | self.eat('NEWLINE') 221 | node.children.append(ruleblock) 222 | return node 223 | 224 | def block(self): 225 | """block : NEWLINE INDENT line DEDENT 226 | | line""" 227 | node = Block() 228 | if self.curtype == 'NEWLINE': 229 | self.eat('NEWLINE', 'INDENT') 230 | while self.curtype != 'DEDENT': 231 | line = self.line() 232 | if line is not None: 233 | node.children.append(line) 234 | self.eat('DEDENT') 235 | else: 236 | line = self.line() 237 | node.children.append(line) 238 | return node 239 | 240 | def importdef(self): 241 | """importdef : #import STRING""" 242 | self.eat('IMPORT') 243 | path = self.curvalue.strip('\'').strip('"').replace('/', '\\').replace('.owpy', '') 244 | pos = self.curpos 245 | self.eat('STRING') 246 | node = Import(path=path) 247 | node._pos = pos 248 | return node 249 | 250 | def classdef(self): 251 | """classdef : class NAME : classbody""" 252 | self.eat('CLASS') 253 | name = self.curvalue 254 | self.eat('NAME') 255 | self.eat('COLON') 256 | body = self.classbody() 257 | return Class(name=name, body=body) 258 | 259 | def classbody(self): 260 | """classbody : line | funcdef""" 261 | self.eat('NEWLINE', 'INDENT') 262 | body = [] 263 | while self.curtype != 'DEDENT': 264 | if self.curtype == 'MOD': 265 | body.append(self.funcdef()) 266 | else: 267 | body.append(self.line()) 268 | self.eat('DEDENT') 269 | return body 270 | 271 | def line(self): 272 | """line : 273 | ( if_stmt 274 | | while_stmt 275 | | for_stmt 276 | | return_stmt 277 | | expr ASSIGN expr 278 | | expr 279 | )? NEWLINE""" 280 | if self.curtype == 'NEWLINE': 281 | return self.eat('NEWLINE') 282 | if self.curtype == 'IF': 283 | return self.if_stmt() 284 | if self.curtype == 'WHILE': 285 | return self.while_stmt() 286 | if self.curtype == 'FOR': 287 | return self.for_stmt() 288 | if self.curtype == 'RETURN': 289 | return self.return_stmt() 290 | pos = self.curpos 291 | node = self.expr() 292 | if self.curtype == 'ASSIGN': 293 | op = self.curvalue 294 | self.eat('ASSIGN') 295 | node = Assign(left=node, op=op, right=self.expr()) 296 | while self.curtype == 'NEWLINE': 297 | self.eat('NEWLINE') 298 | node._pos = pos 299 | return node 300 | 301 | def if_stmt(self): 302 | """if_stmt : IF expr : block elif_else""" 303 | self.eat('IF') 304 | cond = self.expr() 305 | self.eat('COLON') 306 | try: 307 | true_block = self.block() 308 | except Errors.ParseError: 309 | raise Errors.SyntaxError('Invalid block after if statement', pos=self.curpos) 310 | false_block = self.elif_else() 311 | node = If(cond=cond, true_block=true_block, false_block=false_block) 312 | return node 313 | 314 | def elif_else(self): 315 | """elif_else : (elif expr : block)* (else : block)?""" 316 | if self.curtype == 'ELIF': 317 | self.eat('ELIF') 318 | elif self.curtype == 'ELSE': 319 | self.eat('ELSE') 320 | self.eat('COLON') 321 | try: 322 | return self.block() 323 | except Errors.ParseError: 324 | raise Errors.SyntaxError('Invalid block after else statement', pos=self.curpos) 325 | else: 326 | return None 327 | cond = self.expr() 328 | self.eat('COLON') 329 | try: 330 | true_block = self.block() 331 | except Errors.ParseError: 332 | raise Errors.SyntaxError('Invalid block after elif statement', pos=self.curpos) 333 | false_block = self.elif_else() 334 | return If(cond=cond, true_block=true_block, false_block=false_block) 335 | 336 | def while_stmt(self): 337 | """while_stmt : WHILE expr : block""" 338 | self.eat('WHILE') 339 | cond = self.expr() 340 | self.eat('COLON') 341 | body = self.block() 342 | return While(cond=cond, body=body) 343 | 344 | def for_stmt(self): 345 | """for_stmt : FOR NAME IN primary : block""" 346 | self.eat('FOR') 347 | pointer = self.variable() 348 | self.eat('IN') 349 | iterable = self.primary() 350 | self.eat('COLON') 351 | body = self.block() 352 | node = For(pointer=pointer, iterable=iterable, body=body) 353 | return node 354 | 355 | def return_stmt(self): 356 | """return_stmt : RETURN expr? NEWLINE""" 357 | self.eat('RETURN') 358 | expr = None 359 | if self.curtype != 'NEWLINE': 360 | expr = self.expr() 361 | self.eat('NEWLINE') 362 | return Return(value=expr) 363 | 364 | def expr(self): 365 | """expr : logic_or""" 366 | node = self.logic_or() 367 | node._pos = self.curpos 368 | return node 369 | 370 | def logic_or(self): 371 | """logic_or : logic_and (OR logic_and)*""" 372 | node = self.logic_and() 373 | while self.curtype == 'OR': 374 | self.eat('OR') 375 | node = BinaryOp(left=node, op='or', right=self.logic_and()) 376 | return node 377 | 378 | def logic_and(self): 379 | """logic_and : logic_not (AND logic_not)*""" 380 | node = self.logic_not() 381 | while self.curtype == 'AND': 382 | self.eat('AND') 383 | node = BinaryOp(left=node, op='and', right=self.logic_not()) 384 | return node 385 | 386 | def logic_not(self): 387 | """logic_not : NOT logic_not 388 | | compare""" 389 | if self.curtype == 'NOT': 390 | self.eat('NOT') 391 | return UnaryOp(op='not', right=self.logic_not()) 392 | return self.compare() 393 | 394 | def compare(self): 395 | """compare : term (COMPARE term)*""" 396 | node = self.term() 397 | while self.curtype in ('COMPARE', 'IN', 'NOT_IN') and self.peek().type not in ('COMMA', 'RPAREN', 'RBRACK', 'NEWLINE'): 398 | op = self.curvalue 399 | self.eat(self.curtype) 400 | node = Compare(left=node, op=op, right=self.compare()) 401 | return node 402 | 403 | def term(self): 404 | """term : factor ((PLUS | MINUS) factor)*""" 405 | node = self.factor() 406 | while self.curtype in ('PLUS', 'MINUS'): 407 | op = self.curvalue 408 | self.eat(self.curtype) 409 | node = BinaryOp(left=node, op=op, right=self.factor()) 410 | return node 411 | 412 | def factor(self): 413 | """factor : powmod ((TIMES | DIVIDE) powmod)*""" 414 | node = self.powmod() 415 | while self.curtype in ('TIMES', 'DIVIDE'): 416 | op = self.curvalue 417 | self.eat(self.curtype) 418 | node = BinaryOp(left=node, op=op, right=self.powmod()) 419 | return node 420 | 421 | def powmod(self): 422 | """powmod : unary ((POW | MOD) unary)*""" 423 | node = self.unary() 424 | while self.curtype in ('POW', 'MOD'): 425 | op = self.curvalue 426 | self.eat(self.curtype) 427 | node = BinaryOp(left=node, op=op, right=self.unary()) 428 | return node 429 | 430 | def unary(self): 431 | """unary : (PLUS | MINUS) unary 432 | | primary""" 433 | if self.curtype in ('PLUS', 'MINUS'): 434 | while self.curtype in ('PLUS', 'MINUS'): 435 | op = self.curvalue 436 | self.eat(self.curtype) 437 | node = UnaryOp(op=op, right=self.unary()) 438 | else: 439 | node = self.primary() 440 | return node 441 | 442 | def primary(self): 443 | """primary : atom trailer*""" 444 | node = self.atom() 445 | while self.curtype in ('DOT', 'LPAREN', 'LBRACK'): 446 | pos = self.curpos 447 | node = self.trailer()(parent=node) 448 | node._pos = pos 449 | return node 450 | 451 | def atom(self): 452 | """atom : variable 453 | | vector 454 | | string 455 | | array 456 | | TIME 457 | | OWID args? 458 | | FLOAT 459 | | INTEGER 460 | | ( expr )""" 461 | pos = self.curpos 462 | if self.curtype == 'OWID': 463 | name = self.curvalue.upper() 464 | for aliases in ALIASES.values(): 465 | name = aliases.get(name, name) 466 | self.eat('OWID') 467 | maps = ['BLACK FOREST', 'BLIZZARD WORLD', 'BUSAN', 'CASTILLO', 'CHÂTEAU GUILLARD', 'DORADO', 'ECOPOINT: ANTARCTICA', 'EICHENWALDE', 'HANAMURA', 'HAVANA', 'HOLLYWOOD', 'HORIZON LUNAR COLONY', 'ILIOS', 'JUNKERTOWN', "KING'S ROW", 'LIJIANG TOWER', 'NECROPOLIS', 'NEPAL', 'NUMBANI', 'OASIS', 'PARIS', 'PETRA', 'RIALTO', 'ROUTE 66', 'TEMPLE OF ANUBIS', 'VOLSKAYA INDUSTRIES', 'WATCHPOINT: GIBRALTAR', 'AYUTTHAYA', 'BUSAN DOWNTOWN', 'BUSAN SANCTUARY', 'ILIOS LIGHTHOUSE', 'ILIOS RUINS', 'ILIOS WELL', 'LIJIANG CONTROL CENTER', 'LIJIANG GARDEN', 'LIJIANG NIGHT MARKET', 'NEPAL SANCTUM', 'NEPAL SHRINE', 'NEPAL VILLAGE', 'OASIS CITY CENTER', 'OASIS GARDENS', 'OASIS UNIVERSITY'] 468 | if name in maps: 469 | return Number(value=str(maps.index(name))) 470 | node = Workshop[name] 471 | if type(node) == OWID: 472 | args = self.args() 473 | if args: 474 | node.children.extend(args) 475 | else: 476 | node = Constant(name=node.name) 477 | if "CHASE" in name: 478 | for child in node.children: 479 | if type(child) == Var: 480 | self.chase_vars.add(child.name) 481 | elif self.curtype in ('GVAR', 'PVAR', 'CONST', 'NAME'): 482 | node = self.variable() 483 | elif self.curvalue == '<': 484 | node = self.vector() 485 | elif self.curtype in ('STRING', 'F_STRING'): 486 | node = self.string() 487 | elif self.curtype in ('C_STRING'): 488 | node = self.string() 489 | elif self.curtype == 'TIME': 490 | node = Time(value=self.curvalue) 491 | self.eat('TIME') 492 | elif self.curtype in ('FLOAT', 'INTEGER'): 493 | node = Number(value=self.curvalue) 494 | self.eat(self.curtype) 495 | elif self.curtype == 'LPAREN': 496 | self.eat('LPAREN') 497 | node = self.expr() 498 | self.eat('RPAREN') 499 | elif self.curtype == 'LBRACK': 500 | node = self.array() 501 | else: 502 | pos = (self.curtoken.line, self.curtoken.column) 503 | raise Errors.ParseError('Unexpected token of type {}'.format(self.curtype), pos=pos) 504 | node._pos = pos 505 | return node 506 | 507 | def args(self): 508 | """args : block 509 | | ( arg_list )""" 510 | node = None 511 | if self.curtype == 'LPAREN': 512 | self.eat('LPAREN') 513 | node = self.arglist() 514 | self.eat('RPAREN') 515 | elif self.peek().type == 'INDENT': 516 | node = self.block().children 517 | return node 518 | 519 | def arglist(self): 520 | """arglist : expr (, NEWLINE* expr)*""" 521 | node = Block() 522 | node.children.append(self.expr()) 523 | while self.curtype == 'COMMA': 524 | self.eat('COMMA') 525 | while self.curtype == 'NEWLINE': 526 | self.eat('NEWLINE') 527 | node.children.append(self.expr()) 528 | return node.children 529 | 530 | def variable(self): 531 | """variable : GVAR NAME 532 | | PVAR NAME (@ atom)? 533 | | CONST NAME 534 | | NAME""" 535 | pos = self.curpos 536 | try: 537 | if self.curvalue == 'get_map': 538 | self.map_rule = True 539 | if self.curtype in ('GVAR', 'NAME'): 540 | if self.curtype == 'GVAR': 541 | self.eat('GVAR') 542 | node = Var(name=self.curvalue, type_=Var.GLOBAL) 543 | elif self.curtype == 'PVAR': 544 | self.eat('PVAR') 545 | node = Var(name=self.curvalue, type_=Var.PLAYER) 546 | elif self.curtype == 'CONST': 547 | self.eat('CONST') 548 | node = Var(name=self.curvalue, type_=Var.CONST) 549 | self.eat('NAME') 550 | if self.curvalue == '@': 551 | self.eat('AT') 552 | node.player = self.atom() 553 | except Errors.ParseError: 554 | raise Errors.SyntaxError('Invalid variable syntax', pos=pos) 555 | node._pos = pos 556 | return node 557 | 558 | def vector(self): 559 | """vector : < term , term , term >""" 560 | node = Vector() 561 | self.eat('COMPARE') 562 | node.children.append(self.term()) 563 | self.eat('COMMA') 564 | node.children.append(self.term()) 565 | self.eat('COMMA') 566 | node.children.append(self.term()) 567 | self.eat('COMPARE') 568 | return node 569 | 570 | def array(self): 571 | """array : [ arglist? ]""" 572 | node = Array() 573 | self.eat('LBRACK') 574 | if self.curtype == 'NEWLINE': 575 | self.eat('NEWLINE', 'INDENT') 576 | if self.curtype != 'RBRACK': 577 | node.elements.extend(self.arglist()) 578 | if self.curtype == 'NEWLINE': 579 | self.eat('NEWLINE', 'DEDENT') 580 | self.eat('RBRACK') 581 | return node 582 | 583 | def string(self): 584 | """string : STRING 585 | | C_STRING 586 | | F_STRING args?""" 587 | pos = self.curpos 588 | if self.curtype == 'STRING': 589 | node = String(value=self.curvalue.strip('"').strip("'")) 590 | self.eat('STRING') 591 | node.children = [Constant(name='Null')] * 3 592 | elif self.curtype == 'C_STRING': 593 | node = CustomString(value=self.curvalue.strip('~"').strip("~'")) 594 | self.eat('C_STRING') 595 | node.children = [Constant(name='Null')] * 3 596 | else: 597 | string = self.curvalue 598 | num_params = string.count('{') 599 | formats = [] 600 | self.eat('F_STRING') 601 | if num_params > 0: 602 | formats = self.args() 603 | try: 604 | assert len(formats) == num_params 605 | except AssertionError: 606 | raise Errors.SyntaxError('String \'{}\' expected {} parameters, received {}'.format(string, num_params, len(formats))) 607 | node = self.parse_string(string, formats, pos) 608 | node._pos = pos 609 | return node 610 | 611 | def trailer(self): 612 | """trailer : DOT NAME 613 | | LPAREN arglist? RPAREN""" 614 | if self.curtype == 'DOT': 615 | self.eat('DOT') 616 | name = self.curvalue 617 | self.eat('NAME') 618 | return partial(Attribute, name=name) 619 | elif self.curtype == 'LPAREN': 620 | self.eat('LPAREN') 621 | args = [] 622 | if self.curtype != 'RPAREN': 623 | args = self.arglist() 624 | self.eat('RPAREN') 625 | return partial(Call, args=args) 626 | elif self.curtype == 'LBRACK': 627 | self.eat('LBRACK') 628 | index = self.expr() 629 | self.eat('RBRACK') 630 | return partial(Item, index=index) 631 | -------------------------------------------------------------------------------- /OWScript/Tokens.py: -------------------------------------------------------------------------------- 1 | class Token: 2 | """Stores token information such as data and line number.""" 3 | def __init__(self, type, value, line, column): 4 | self.type = type 5 | self.value = value 6 | self.line = line 7 | self.column = column 8 | 9 | def __repr__(self): 10 | return f'<{self.type}: {self.value} ({self.line}:{self.column})>' 11 | 12 | ALIASES = { 13 | 'CONST': { 14 | 'CHATEAU GUILLARD': 'CHÂTEAU GUILLARD', 15 | 'CUR ELEM': 'CURRENT ARRAY ELEMENT', 16 | 'EVERYONE': 'ALL PLAYERS(TEAM(ALL))', 17 | 'LUCIO': 'LÚCIO', 18 | 'ON EACH PLAYER': 'ONGOING - EACH PLAYER', 19 | 'ON GLOBAL': 'ONGOING - GLOBAL', 20 | 'RECEIVERS, DAMAGERS, AND DAMAGE PERCENT': 'RECEIVERS DAMAGERS AND DAMAGE PERCENT', 21 | 'TORBJORN': 'TORBJÖRN', 22 | 'VISIBLE TO, POSITION, AND RADIUS': 'VISIBLE TO POSITION AND RADIUS', 23 | 'VISIBLE TO, POSITION, AND STRING': 'VISIBLE TO POSITION AND STRING' 24 | }, 25 | 'VALUE': { 26 | 'ABS': 'ABSOLUTE VALUE', 27 | 'ANY TRUE': 'IS TRUE FOR ANY', 28 | 'ALL TRUE': 'IS TRUE FOR ALL', 29 | 'ARRAY CONTAINS': 'ARRAY CONTAINS', 30 | 'COS': 'COSINE FROM DEGREES', 31 | 'COSR': 'COSINE FROM RADIANS', 32 | 'FILTER': 'FILTERED ARRAY', 33 | 'LOS': 'IS IN LINE OF SIGHT', 34 | 'INDEX': 'INDEX OF ARRAY VALUE', 35 | 'PLAYERS IN RADIUS': 'PLAYERS WITHIN RADIUS', 36 | 'ROUND': 'ROUND TO INTEGER', 37 | 'SIN': 'SINE FROM DEGREES', 38 | 'SINR': 'SINE FROM RADIANS' 39 | }, 40 | 'ACTION': { 41 | 'BIG MSG': 'BIG MESSAGE', 42 | 'HUD': 'CREATE HUD TEXT', 43 | 'MSG': 'SMALL MESSAGE', 44 | 'SET HERO': 'START FORCING PLAYER TO BE HERO', 45 | 'SMALL MSG': 'SMALL MESSAGE', 46 | 'WORLD TEXT': 'CREATE IN-WORLD TEXT' 47 | } 48 | } 49 | MAPS = ["Black Forest", "Blizzard World", "Busan", "Castillo", "Château Guillard", "Dorado", "Ecopoint: Antarctica", "Eichenwalde", "Hanamura", "Havana", "Hollywood", "Horizon Lunar Colony", "Ilios", "Junkertown", "King\'s Row", "Lijiang Tower", "Necropolis", "Nepal", "Numbani", "Oasis", "Paris", "Petra", "Rialto", "Route 66", "Temple of Anubis", "Volskaya Industries", "Watchpoint: Gibraltar", "Ayutthaya", "Busan Downtown", "Busan Sanctuary", "Ilios Lighthouse", "Ilios Ruins", "Ilios Well", "Lijiang Control Center", "Lijiang Garden", "Lijiang Night Market", "Nepal Sanctum", "Nepal Shrine", "Nepal Village", "Oasis City Center", "Oasis Gardens", "Oasis University"] 50 | CONST = ['ABILITY 1', 'ABILITY 2', 'ABORT WHEN FALSE', 'ABSOLUTE VALUE', 'ACKNOWLEDGE', 'ADD', 'ALL BARRIERS BLOCK LOS', 'ALL DEAD PLAYERS', 'ALL HEROES', 'ALL LIVING PLAYERS', 'ALL PLAYERS NOT ON OBJECTIVE', 'ALL PLAYERS ON OBJECTIVE', 'ALL PLAYERS', 'ALL TEAMS', 'ALL', 'ALLOWED HEROES', 'ALTITUDE OF', 'ANA', 'ANGLE DIFFERENCE', 'APPEND TO ARRAY', 'ARRAY CONTAINS', 'ARRAY SLICE', 'ARROW: DOWN', 'ARROW: LEFT', 'ARROW: RIGHT', 'ARROW: UP', 'ASHE', 'ASLEEP', 'ASTERISK', 'ATTACKER', 'BACKWARD', 'BAD AURA SOUND', 'BAD AURA', 'BAD EXPLOSION', 'BAD PICKUP EFFECT', 'BAPTISTE', 'BARRIERS DO NOT BLOCK LOS', 'BASTION', 'BEACON SOUND', 'BLUE', 'BOLT', 'BRIGITTE', 'BUFF EXPLOSION SOUND', 'BUFF IMPACT SOUND', 'BURNING', 'CANCEL CONTRARY MOTION', 'CHECKMARK', 'CIRCLE', 'CLIP AGAINST SURFACES', 'CLOSEST PLAYER TO', 'CLOUD', 'CLUB', 'COMPARE', 'CONTROL MODE SCORING PERCENTAGE', 'CONTROL MODE SCORING TEAM', 'COSINE FROM DEGREES', 'COSINE FROM RADIANS', 'COUNT OF', 'CROSS PRODUCT', 'CROUCH', 'CURRENT ARRAY ELEMENT', 'D.VA', 'DEBUFF IMPACT SOUND', 'DECAL SOUND', 'DESTINATION AND DURATION', 'DESTINATION AND RATE', 'DIAMOND', 'DIRECTION AND TURN RATE', 'DIRECTION FROM ANGLES', 'DIRECTION TOWARDS', 'DISTANCE BETWEEN', 'DIVIDE', 'DIZZY', 'DO NOT CLIP', 'DOOMFIST', 'DOT PRODUCT', 'DOWN', 'EMOTE DOWN', 'EMOTE LEFT', 'EMOTE RIGHT', 'EMOTE UP', 'EMPTY ARRAY', 'ENEMIES', 'ENEMY BARRIERS BLOCK LOS', 'ENERGY SOUND', 'ENTITY EXISTS', 'EVENT DAMAGE', 'EVENT PLAYER', 'EVENT WAS CRITICAL HIT', 'EXCLAMATION MARK', 'EXPLOSION SOUND', 'EYE POSITION', 'EYE', 'FACING DIRECTION OF', 'FALSE', 'FARTHEST PLAYER FROM', 'FILTERED ARRAY', 'FIRE', 'FIRST OF', 'FLAG POSITION', 'FLAG', 'FORWARD', 'FROZEN', 'GENJI', 'GLOBAL VARIABLE', 'GOOD AURA SOUND', 'GOOD AURA', 'GOOD EXPLOSION', 'GOOD PICKUP EFFECT', 'GREEN', 'GROUP UP', 'HACKED', 'HALO', 'HANZO', 'HAPPY', 'HAS SPAWNED', 'HAS STATUS', 'HEALTH PERCENT', 'HEALTH', 'HEART', 'HELLO', 'HERO ICON STRING', 'HERO OF', 'HERO', 'HORIZONTAL ANGLE FROM DIRECTION', 'HORIZONTAL ANGLE TOWARDS', 'HORIZONTAL FACING ANGLE OF', 'HORIZONTAL SPEED OF', 'IGNORE CONDITION', 'INCORPORATE CONTRARY MOTION', 'INDEX OF ARRAY VALUE', 'INTERACT', 'INVINCIBLE', 'IS ALIVE', 'IS ASSEMBLING HEROES', 'IS BETWEEN ROUNDS', 'IS BUTTON HELD', 'IS COMMUNICATING ANY EMOTE', 'IS COMMUNICATING ANY VOICE LINE', 'IS COMMUNICATING ANY', 'IS COMMUNICATING', 'IS CONTROL MODE POINT LOCKED', 'IS CROUCHING', 'IS CTF MODE IN SUDDEN DEATH', 'IS DEAD', 'IS FIRING PRIMARY', 'IS FIRING SECONDARY', 'IS FLAG AT BASE', 'IS FLAG BEING CARRIED', 'IS GAME IN PROGRESS', 'IS HERO BEING PLAYED', 'IS IN AIR', 'IS IN LINE OF SIGHT', 'IS IN SETUP', 'IS IN SPAWN ROOM', 'IS IN VIEW ANGLE', 'IS MATCH COMPLETE', 'IS MOVING', 'IS OBJECTIVE COMPLETE', 'IS ON GROUND', 'IS ON OBJECTIVE', 'IS ON WALL', 'IS PORTRAIT ON FIRE', 'IS STANDING', 'IS TEAM ON DEFENSE', 'IS TEAM ON OFFENSE', 'IS TRUE FOR ALL', 'IS TRUE FOR ANY', 'IS USING ABILITY 1', 'IS USING ABILITY 2', 'IS USING ULTIMATE', 'IS WAITING FOR PLAYERS', 'JUMP', 'JUNKRAT', 'KNOCKED DOWN', 'LAST CREATED ENTITY', 'LAST DAMAGE MODIFICATION ID', 'LAST DAMAGE OVER TIME ID', 'LAST HEAL OVER TIME ID', 'LAST OF', 'LAST TEXT ID', 'LEFT', 'LIGHT SHAFT', 'LOCAL VECTOR OF', 'LÚCIO', 'MATCH ROUND', 'MATCH TIME', 'MAX HEALTH', 'MAX', 'MCCREE', 'MEI', 'MERCY', 'MIN', 'MODULO', 'MOIRA', 'MOON', 'MULTIPLY', 'NEAREST WALKABLE POSITION', 'NEED HEALING', 'NO', 'NONE', 'NORMALIZE', 'NULL', 'NUMBER OF DEAD PLAYERS', 'NUMBER OF DEATHS', 'NUMBER OF ELIMINATIONS', 'NUMBER OF FINAL BLOWS', 'NUMBER OF HEROES', 'NUMBER OF LIVING PLAYERS', 'NUMBER OF PLAYERS ON OBJECTIVE', 'NUMBER OF PLAYERS', 'NUMBER', 'OBJECTIVE INDEX', 'OBJECTIVE POSITION', 'OFF', 'ONGOING - EACH PLAYER', 'ONGOING - GLOBAL', 'OPPOSITE TEAM OF', 'ORB', 'ORISA', 'PAYLOAD POSITION', 'PAYLOAD PROGRESS PERCENTAGE', 'PHARAH', 'PHASED OUT', 'PICK-UP SOUND', 'PLAYER CARRYING FLAG', 'PLAYER CLOSEST TO RETICLE', 'PLAYER DEALT DAMAGE', 'PLAYER DEALT FINAL BLOW', 'PLAYER DIED', 'PLAYER EARNED ELIMINATION', 'PLAYER TOOK DAMAGE', 'PLAYER VARIABLE', 'PLAYERS IN SLOT', 'PLAYERS IN VIEW ANGLE', 'PLAYERS ON HERO', 'PLAYERS WITHIN RADIUS', 'PLUS', 'POINT CAPTURE PERCENTAGE', 'POISON 2', 'POISON', 'POSITION AND RADIUS', 'POSITION OF', 'POSITION', 'PRIMARY FIRE', 'PURPLE', 'QUESTION MARK', 'RADIOACTIVE', 'RAISE TO POWER', 'RANDOM INTEGER', 'RANDOM REAL', 'RANDOM VALUE IN ARRAY', 'RANDOMIZED ARRAY', 'RAY CAST HIT NORMAL', 'RAY CAST HIT PLAYER', 'RAY CAST HIT POSITION', 'REAPER', 'RECEIVERS AND DAMAGERS', 'RECEIVERS DAMAGERS AND DAMAGE PERCENT', 'RECYCLE', 'RED', 'REINHARDT', 'REMOVE FROM ARRAY', 'RESTART WHEN TRUE', 'RIGHT', 'RING EXPLOSION SOUND', 'RING EXPLOSION', 'RING THICK', 'RING THIN', 'RING', 'ROADHOG', 'ROOTED', 'ROTATION AND TRANSLATION', 'ROTATION', 'ROUND TO INTEGER', 'SAD', 'SCORE OF', 'SECONDARY FIRE', 'SINE FROM DEGREES', 'SINE FROM RADIANS', 'SKULL', 'SLOT 0', 'SLOT 1', 'SLOT 10', 'SLOT 11', 'SLOT 2', 'SLOT 3', 'SLOT 4', 'SLOT 5', 'SLOT 6', 'SLOT 7', 'SLOT 8', 'SLOT 9', 'SLOT OF', 'SMOKE SOUND', 'SOLDIER: 76', 'SOMBRA', 'SORTED ARRAY', 'SPADE', 'SPARKLES SOUND', 'SPARKLES', 'SPEED OF IN DIRECTION', 'SPEED OF', 'SPHERE', 'SPIRAL', 'SQUARE ROOT', 'STOP', 'STRING', 'STUNNED', 'SUBTRACT', 'SURFACES AND ALL BARRIERS', 'SURFACES AND ENEMY BARRIERS', 'SURFACES', 'SYMMETRA', 'TEAM 1', 'TEAM 2', 'TEAM OF', 'TEAM SCORE', 'TEAM', 'THANKS', 'THROTTLE OF', 'TO NEAREST', 'TO PLAYER', 'TO WORLD', 'TOP', 'TORBJÖRN', 'TOTAL TIME ELAPSED', 'TRACER', 'TRASHCAN', 'TRUE', 'ULTIMATE CHARGE PERCENT', 'ULTIMATE STATUS', 'ULTIMATE', 'UNKILLABLE', 'UP', 'VALUE IN ARRAY', 'VECTOR TOWARDS', 'VECTOR', 'VELOCITY OF', 'VERTICAL ANGLE FROM DIRECTION', 'VERTICAL ANGLE TOWARDS', 'VERTICAL FACING ANGLE OF', 'VERTICAL SPEED OF', 'VICTIM', 'VISIBLE TO AND POSITION', 'VISIBLE TO AND STRING', 'VISIBLE TO POSITION AND RADIUS', 'VISIBLE TO POSITION AND STRING', 'VISIBLE TO', 'VOICE LINE DOWN', 'VOICE LINE LEFT', 'VOICE LINE RIGHT', 'VOICE LINE UP', 'WARNING', 'WHITE', 'WIDOWMAKER', 'WINSTON', 'WORLD VECTOR OF', 'WRECKING BALL', 'X COMPONENT OF', 'X', 'Y COMPONENT OF', 'YELLOW', 'Z COMPONENT OF', 'ZARYA', 'ZENYATTA'] 51 | CONST.extend(ALIASES.get('CONST').keys()) 52 | CONST.extend(map(lambda x: x.replace('\'', '\'\'').upper(), MAPS)) 53 | VALUE = ['ABSOLUTE VALUE', 'ADD', 'ALL DEAD PLAYERS', 'ALL LIVING PLAYERS', 'ALL PLAYERS NOT ON OBJECTIVE', 'ALL PLAYERS ON OBJECTIVE', 'ALL PLAYERS', 'ALLOWED HEROES', 'ALTITUDE OF', 'ANGLE DIFFERENCE', 'APPEND TO ARRAY', 'ARRAY SLICE', 'CLOSEST PLAYER TO', 'COMPARE', 'CONTROL MODE SCORING PERCENTAGE', 'COSINE FROM DEGREES', 'COSINE FROM RADIANS', 'COUNT OF', 'CROSS PRODUCT', 'DIRECTION FROM ANGLES', 'DIRECTION TOWARDS', 'DISTANCE BETWEEN', 'DIVIDE', 'DOT PRODUCT', 'ENTITY EXISTS', 'EVENT DAMAGE', 'EYE POSITION', 'FACING DIRECTION OF', 'FARTHEST PLAYER FROM', 'FILTERED ARRAY', 'FIRST OF', 'FLAG POSITION', 'GLOBAL VARIABLE', 'HAS SPAWNED', 'HAS STATUS', 'HEALTH PERCENT', 'HEALTH', 'HERO ICON STRING', 'HERO OF', 'HERO', 'HORIZONTAL ANGLE FROM DIRECTION', 'HORIZONTAL ANGLE TOWARDS', 'HORIZONTAL FACING ANGLE OF', 'HORIZONTAL SPEED OF', 'INDEX OF ARRAY VALUE', 'IS ALIVE', 'IS BUTTON HELD', 'IS COMMUNICATING ANY EMOTE', 'IS COMMUNICATING ANY VOICE LINE', 'IS COMMUNICATING ANY', 'IS COMMUNICATING', 'IS CROUCHING', 'IS DEAD', 'IS FIRING PRIMARY', 'IS FIRING SECONDARY', 'IS FLAG AT BASE', 'IS FLAG BEING CARRIED', 'IS HERO BEING PLAYED', 'IS IN AIR', 'IS IN LINE OF SIGHT', 'IS IN SPAWN ROOM', 'IS IN VIEW ANGLE', 'IS MOVING', 'IS OBJECTIVE COMPLETE', 'IS ON GROUND', 'IS ON OBJECTIVE', 'IS ON WALL', 'IS PORTRAIT ON FIRE', 'IS STANDING', 'IS TEAM ON DEFENSE', 'IS TEAM ON OFFENSE', 'IS TRUE FOR ALL', 'IS TRUE FOR ANY', 'IS USING ABILITY 1', 'IS USING ABILITY 2', 'IS USING ULTIMATE', 'LAST DAMAGE MODIFICATION ID', 'LAST DAMAGE OVER TIME ID', 'LAST HEAL OVER TIME ID', 'LAST OF', 'LAST TEXT ID', 'LOCAL VECTOR OF', 'MATCH ROUND', 'MATCH TIME', 'MAX HEALTH', 'MAX', 'MIN', 'MODULO', 'MULTIPLY', 'NEAREST WALKABLE POSITION', 'NORMALIZE', 'NUMBER OF DEAD PLAYERS', 'NUMBER OF DEATHS', 'NUMBER OF ELIMINATIONS', 'NUMBER OF FINAL BLOWS', 'NUMBER OF HEROES', 'NUMBER OF LIVING PLAYERS', 'NUMBER OF PLAYERS ON OBJECTIVE', 'NUMBER OF PLAYERS', 'NUMBER', 'OBJECTIVE INDEX', 'OBJECTIVE POSITION', 'OPPOSITE TEAM OF', 'PAYLOAD PROGRESS PERCENTAGE', 'PLAYER CARRYING FLAG', 'PLAYER CLOSEST TO RETICLE', 'PLAYER VARIABLE', 'PLAYERS IN SLOT', 'PLAYERS IN VIEW ANGLE', 'PLAYERS ON HERO', 'PLAYERS WITHIN RADIUS', 'POINT CAPTURE PERCENTAGE', 'POSITION OF', 'RAISE TO POWER', 'RANDOM INTEGER', 'RANDOM REAL', 'RANDOM VALUE IN ARRAY', 'RANDOMIZED ARRAY', 'RAY CAST HIT NORMAL', 'RAY CAST HIT PLAYER', 'RAY CAST HIT POSITION', 'REMOVE FROM ARRAY', 'ROUND TO INTEGER', 'SCORE OF', 'SINE FROM DEGREES', 'SINE FROM RADIANS', 'SLOT OF', 'SORTED ARRAY', 'SPEED OF IN DIRECTION', 'SPEED OF', 'SQUARE ROOT', 'SUBTRACT', 'TEAM OF', 'TEAM SCORE', 'TEAM', 'THROTTLE OF', 'TOTAL TIME ELAPSED', 'ULTIMATE CHARGE PERCENT', 'VALUE IN ARRAY', 'VECTOR TOWARDS', 'VECTOR', 'VELOCITY OF', 'VERTICAL ANGLE FROM DIRECTION', 'VERTICAL ANGLE TOWARDS', 'VERTICAL FACING ANGLE OF', 'VERTICAL SPEED OF', 'WORLD VECTOR OF', 'X COMPONENT OF', 'Y COMPONENT OF', 'Z COMPONENT OF'] 54 | VALUE.extend(ALIASES.get('VALUE').keys()) 55 | ACTION = ['ABORT IF CONDITION IS FALSE', 'ABORT IF CONDITION IS TRUE', 'ABORT IF', 'ABORT', 'ALLOW BUTTON', 'APPLY IMPULSE', 'BIG MESSAGE', 'CHASE GLOBAL VARIABLE AT RATE', 'CHASE GLOBAL VARIABLE OVER TIME', 'CHASE PLAYER VARIABLE AT RATE', 'CHASE PLAYER VARIABLE OVER TIME', 'CLEAR STATUS', 'COMMUNICATE', 'CREATE EFFECT', 'CREATE HUD TEXT', 'CREATE ICON', 'CREATE IN-WORLD TEXT', 'DAMAGE', 'DECLARE MATCH DRAW', 'DECLARE PLAYER VICTORY', 'DECLARE ROUND VICTORY', 'DECLARE TEAM VICTORY', 'DESTROY ALL EFFECTS', 'DESTROY ALL HUD TEXT', 'DESTROY ALL ICONS', 'DESTROY ALL IN-WORLD TEXT', 'DESTROY EFFECT', 'DESTROY HUD TEXT', 'DESTROY ICON', 'DESTROY IN-WORLD TEXT', 'DISABLE BUILT-IN GAME MODE ANNOUNCER', 'DISABLE BUILT-IN GAME MODE COMPLETION', 'DISABLE BUILT-IN GAME MODE MUSIC', 'DISABLE BUILT-IN GAME MODE RESPAWNING', 'DISABLE BUILT-IN GAME MODE SCORING', 'DISABLE DEATH SPECTATE ALL PLAYERS', 'DISABLE DEATH SPECTATE TARGET HUD', 'DISALLOW BUTTON', 'ENABLE BUILT-IN GAME MODE ANNOUNCER', 'ENABLE BUILT-IN GAME MODE COMPLETION', 'ENABLE BUILT-IN GAME MODE MUSIC', 'ENABLE BUILT-IN GAME MODE RESPAWNING', 'ENABLE BUILT-IN GAME MODE SCORING', 'ENABLE DEATH SPECTATE ALL PLAYERS', 'ENABLE DEATH SPECTATE TARGET HUD', 'GO TO ASSEMBLE HEROES', 'HEAL', 'KILL', 'LOOP IF CONDITION IS FALSE', 'LOOP IF CONDITION IS TRUE', 'LOOP IF', 'LOOP', 'MODIFY GLOBAL VARIABLE', 'MODIFY PLAYER SCORE', 'MODIFY PLAYER VARIABLE', 'MODIFY TEAM SCORE', 'PAUSE MATCH TIME', 'PLAY EFFECT', 'PRELOAD HERO', 'PRESS BUTTON', 'RESET PLAYER HERO AVAILABILITY', 'RESPAWN', 'RESURRECT', 'SET ABILITY 1 ENABLED', 'SET ABILITY 2 ENABLED', 'SET AIM SPEED', 'SET DAMAGE DEALT', 'SET DAMAGE RECEIVED', 'SET FACING', 'SET GLOBAL VARIABLE AT INDEX', 'SET GLOBAL VARIABLE', 'SET GRAVITY', 'SET HEALING DEALT', 'SET HEALING RECEIVED', 'SET INVISIBLE', 'SET MATCH TIME', 'SET MAX HEALTH', 'SET MOVE SPEED', 'SET OBJECTIVE DESCRIPTION', 'SET PLAYER ALLOWED HEROES', 'SET PLAYER SCORE', 'SET PLAYER VARIABLE AT INDEX', 'SET PLAYER VARIABLE', 'SET PRIMARY FIRE ENABLED', 'SET PROJECTILE GRAVITY', 'SET PROJECTILE SPEED', 'SET RESPAWN MAX TIME', 'SET SECONDARY FIRE ENABLED', 'SET SLOW MOTION', 'SET STATUS', 'SET TEAM SCORE', 'SET ULTIMATE ABILITY ENABLED', 'SET ULTIMATE CHARGE', 'SKIP IF', 'SKIP', 'SMALL MESSAGE', 'START ACCELERATING', 'START CAMERA', 'START DAMAGE MODIFICATION', 'START DAMAGE OVER TIME', 'START FACING', 'START FORCING PLAYER TO BE HERO', 'START FORCING SPAWN ROOM', 'START FORCING THROTTLE', 'START HEAL OVER TIME', 'START HOLDING BUTTON', 'STOP ACCELERATING', 'STOP ALL DAMAGE MODIFICATIONS', 'STOP ALL DAMAGE OVER TIME', 'STOP ALL HEAL OVER TIME', 'STOP CAMERA', 'STOP CHASING GLOBAL VARIABLE', 'STOP CHASING PLAYER VARIABLE', 'STOP DAMAGE MODIFICATION', 'STOP DAMAGE OVER TIME', 'STOP FACING', 'STOP FORCING PLAYER TO BE HERO', 'STOP FORCING SPAWN ROOM', 'STOP FORCING THROTTLE', 'STOP HEAL OVER TIME', 'STOP HOLDING BUTTON', 'TELEPORT', 'UNPAUSE MATCH TIME', 'WAIT'] 56 | ACTION.extend(ALIASES.get('ACTION').keys()) 57 | OWID = CONST + VALUE + ACTION + MAPS 58 | OWID.sort(key=len, reverse=True) 59 | 60 | FUNCTIONS = VALUE + ACTION 61 | FUNCTIONS.sort(key=len, reverse=True) 62 | CONST.sort(key=len, reverse=True) 63 | # print('|'.join(FUNCTIONS)) 64 | # print('|'.join(CONST)) 65 | 66 | class Tokens: 67 | """Mapping of token names to regular expressions.""" 68 | COMMENT : r'\s*(\/\*(.|[\n])*?\*\/\n*?|\/\/[^\n]*\n*?)' 69 | COMPARE : r'(>=|<=|==|!=|>|<)' 70 | ASSIGN : r'(=|\+=|-=|\*=|\/=|^=|%=)' 71 | TIME : r'([0-9]+(\.[0-9]+)?)(ms|s|min)' 72 | FLOAT : r'\-?[0-9]+\.[0-9]+' 73 | INTEGER : r'\-?[0-9]+' 74 | PLUS : r'\+' 75 | MINUS : r'\-' 76 | TIMES : r'\*' 77 | DIVIDE : r'\/' 78 | POW : r'\^' 79 | MOD : r'%' 80 | AT : r'@' 81 | QUERY : r'\?' 82 | COMMA : r',' 83 | LPAREN : r'\(' 84 | RPAREN : r'\)' 85 | LBRACK : r'\[' 86 | RBRACK : r'\]' 87 | STRING : r'("[^\\\r\n\f]*?"|\'[^\\\r\n\f]*?\')' 88 | C_STRING: r'(~"[^\\\r\n\f]*?"|~\'[^\\\r\n\f]*?\')' 89 | F_STRING : r'(`[^\\\r\n\f]*?`)' 90 | IMPORT : r'#IMPORT\b' 91 | CLASS : r'CLASS\b' 92 | IF : r'IF\b' 93 | ELIF : r'ELIF\b' 94 | ELSE : r'ELSE\b' 95 | WHILE : r'WHILE\b' 96 | FOR : r'FOR\b' 97 | NOT_IN : r'NOT IN\b' 98 | IN : r'IN\b' 99 | NOT : r'NOT\b' 100 | AND : r'AND\b' 101 | OR : r'OR\b' 102 | PVAR : r'PVAR\b' 103 | GVAR : r'GVAR\b' 104 | CONST : r'CONST\b' 105 | DISABLED : r'DISABLED\b' 106 | RETURN : r'RETURN\b' 107 | RULE : r'RULE\b' 108 | OWID : fr'(?" 47 | 48 | class Builtin: 49 | """The funcionality of built-in functions for OWScript.""" 50 | def range(tp, *args): 51 | args = list(map(int, args)) 52 | elements = list(map(Number, map(str, range(*args)))) 53 | array = Array(elements=elements) 54 | return array 55 | 56 | def ceil(tp, n): 57 | node = OWID(name='Round To Integer', args=(Number, Any)) 58 | node._pos = n._pos 59 | node.children.extend([n, Constant(name='Up')]) 60 | return node 61 | 62 | def floor(tp, n): 63 | node = OWID(name='Round To Integer', args=(Number, Any)) 64 | node._pos = n._pos 65 | node.children.extend([n, Constant(name='Down')]) 66 | return node 67 | 68 | def get_map(tp): 69 | node = OWID(name='Index Of Array Value', args=[None] * 2) 70 | def map_2pos(a, b): 71 | node = Raw(code='First Of(Filtered Array(Append To Array(Append To Array(Empty Array, {}), {}), Compare(Current Array Element, ==, Value In Array(Global Variable(A), 0))))'.format(a, b)) 72 | return tp.visit(node, tp.scope) 73 | elems = list(map(lambda x: Number(value=str(x)), [153, 468, 1196, 135, 139, 477, 184, map_2pos(343, 347), 366, map_2pos(433, 436), 403, map_2pos(382, 384), 993, 386, map_2pos(331, 348), 659, 145, 569, 384, 1150, 371, 179, 497, 374, 312, 324, 434, 297, 276, 330, 376, 347, 480, 310, 342, 360, 364, 372, 370, 450, 356, 305])) 74 | array = Array(elements=elems) 75 | value = Raw(code='Value In Array(Global Variable(A), 0)') 76 | node.children.extend([array, value]) 77 | return node 78 | 79 | range: range 80 | ceil: ceil 81 | floor: floor 82 | get_map: get_map 83 | 84 | class Transpiler: 85 | """Compiles a parse tree into a single string output via the `run` method.""" 86 | def __init__(self, tree, path, logger, credit, indent_size=4): 87 | self.tree = tree 88 | self.path = path 89 | self.logger = logger 90 | self.credit = credit 91 | self.indent_size = indent_size 92 | self.indent_level = 0 93 | # Reserved Global Indices 94 | # 0: Map ID 95 | self.global_reserved = 1 96 | # Generators to return the next available index (or variable letter for chase variables) 97 | self.global_index = count(self.global_reserved) 98 | self.player_index = count() 99 | self.chase_vars = set() 100 | self.letters = defaultdict(lambda x: 'A') 101 | self.global_letters = iter(letters[1:]) 102 | self.player_letters = iter(letters[1:]) 103 | 104 | self.curblock = [] 105 | # Keeps track of absolute import paths to avoid duplicate imports 106 | self.imports = set() 107 | 108 | @property 109 | def tabs(self): 110 | return ' ' * self.indent_size * self.indent_level 111 | 112 | @property 113 | def min_wait(self): 114 | return 'Wait(0.016, Ignore Condition)' 115 | 116 | def base_node(self, node): 117 | """Gets a node that can be evaluated in the current scope (e.g. not an item, property, or call).""" 118 | while hasattr(node, 'parent'): 119 | node = node.parent 120 | return node 121 | 122 | def resolve_skips(self): 123 | """Resolves for/while* continue skips from placeholder text.""" 124 | skips = [] 125 | skip_to = [] 126 | for line_no, line in enumerate(self.curblock): 127 | match = re.match(r'\s*//SKIP TO', line) 128 | if match: 129 | skips.append(line_no) 130 | self.curblock[line_no] = re.sub('//SKIP TO', '', line) 131 | match = re.match(r'\s*//FOR START', line) 132 | if match: 133 | skip_to.append(line_no - 1) 134 | self.curblock[line_no] = re.sub('//FOR START', '', line) 135 | for skip, jump in zip(skips, skip_to[::-1]): 136 | self.curblock[skip] = self.curblock[skip].format(jump) 137 | 138 | def resolve_import(self, node, scope): 139 | """Extends the current parse tree by evaluating the given import path (recursively).""" 140 | children = self.visit(node, scope).children 141 | nodes = [] 142 | for child in children: 143 | if type(child) == Import: 144 | cur_path = self.path 145 | self.path = os.path.join(os.path.dirname(self.path), os.path.dirname(node.path)) + '\\' 146 | result = self.resolve_import(child, scope) 147 | self.path = cur_path 148 | if result: 149 | self.chase_vars.update(result.chase_vars) 150 | nodes = result + nodes 151 | else: 152 | nodes.append(child) 153 | return nodes 154 | 155 | def resolve_name(self, node, scope): 156 | if type(node) == Var: 157 | var = scope.get(node.name) 158 | if not var: 159 | raise Errors.NameError('\'{}\' is undefined'.format(node.name), pos=node._pos) 160 | node = var.value 161 | elif type(node) == BinaryOp: 162 | node.left = self.resolve_name(node.left, scope) 163 | node.right = self.resolve_name(node.right, scope) 164 | elif type(node) == Attribute and type(node.parent) == Var: 165 | node.parent = scope.get(node.parent.name).value 166 | node = self.resolve_name(getattr(node.parent, node.name), node.parent.env) 167 | return node 168 | 169 | def visitScript(self, node, scope): 170 | """Root node generates the final code output and resolves all imports.""" 171 | # Shameless plug + base code for `get_map` functionality 172 | code = '' 173 | if not self.credit: 174 | code += r'rule("Generated by https://github.com/adapap/OWScript") { Event { Ongoing - Global; }}' + '\n' 175 | if node.map_rule: 176 | code += r'rule("Map ID Initialization") { Event { Ongoing - Global; } Actions { Set Global Variable At Index(A, 0, Round To Integer(Add(Distance Between(Nearest Walkable Position(Vector(-500.000, 0, 0)), Nearest Walkable Position(Vector(500, 0, 0))), Distance Between(Nearest Walkable Position(Vector(0, 0, -500.000)), Nearest Walkable Position(Vector(0, 0, 500)))), Down)); }}' + '\n' 177 | self.chase_vars.update(node.chase_vars) 178 | while len(node.children) > 0: 179 | child = node.children[0] 180 | if type(child) == Import: 181 | node.children = self.resolve_import(child, scope) + node.children[1:] 182 | else: 183 | code += self.visit(child, scope) 184 | node.children = node.children[1:] 185 | return code.rstrip('\n') 186 | 187 | def visitImport(self, node, scope): 188 | """Handles `#import` tokens, duplicate imports, and invalid paths.""" 189 | file_dir = os.path.dirname(self.path) 190 | path = os.path.join(file_dir, node.path) + '.owpy' 191 | if not os.path.exists(path): 192 | raise Errors.ImportError('File {} could not be found'.format(node.path), pos=node._pos) 193 | try: 194 | if path not in self.imports: 195 | self.imports.add(path) 196 | result = Importer.import_file(path) 197 | else: 198 | self.logger.info('Skipping duplicate import {}'.format(path)) 199 | result = Script() 200 | return result 201 | except Exception as ex: 202 | raise Errors.ImportError('Failed to import \'{}\' due to the following error:\n{}'.format(node.path, ex), pos=node._pos) 203 | def visitRule(self, node, scope): 204 | """Creates a basic workshop rule.""" 205 | code = '' 206 | if node.disabled: 207 | code += 'disabled ' 208 | code += 'rule(' 209 | code += '"' + ''.join(x if type(x) == str else self.visit(x, scope) for x in node.name) + '"' 210 | self.indent_level += 1 211 | code += ') {\n' + '\n'.join(self.visit_children(node, scope)) + '}\n' 212 | self.indent_level -= 1 213 | return code 214 | 215 | def visitRaw(self, node, scope): 216 | """Returns an exact value for a string without further interpretation.""" 217 | return node.code 218 | 219 | def visitFunction(self, node, scope): 220 | """Defines a user-created function.""" 221 | var = Var(name=node.name, value=node, type_=Var.INTERNAL) 222 | node.closure = scope 223 | scope.assign(node.name, var) 224 | return '' 225 | 226 | def visitClass(self, node, scope): 227 | var = Var(name=node.name, value=node, type_=Var.CLASS) 228 | node.closure = scope 229 | scope.assign(node.name, var) 230 | return '' 231 | 232 | def visitBlock(self, node, scope): 233 | """Visits a collection of statements.""" 234 | code = ''.join(self.visit_children(node, scope)) 235 | return code 236 | 237 | def visitRuleblock(self, node, scope): 238 | """A rule category such as Events, Conditions, or Actions.""" 239 | if not node.children: 240 | return self.tabs + node.name + '{}\n' 241 | code = self.tabs + node.name + ' {' 242 | block = '' 243 | self.indent_level += 1 244 | block = '' 245 | for ruleblock in node.children: 246 | self.curblock = [] 247 | for line in ruleblock.children: 248 | result = self.visit(line, scope) 249 | if result: 250 | result = result.rstrip(';\n').split(';\n') 251 | for x in result: 252 | if x: 253 | child = self.tabs + x 254 | # Automatically compare any condition to true 255 | if node.name.upper() == 'CONDITIONS': 256 | child += ' == True' 257 | self.curblock.append(child) 258 | self.resolve_skips() 259 | block += ';\n'.join(self.curblock) 260 | if block: 261 | code += '\n' + block 262 | else: 263 | return code + '}\n' 264 | self.indent_level -= 1 265 | code += ';\n' + self.tabs + '}\n' 266 | return code 267 | 268 | def visitOWID(self, node, scope): 269 | """A workshop value that takes any number of parameters, such as `Set Facing(...)`.""" 270 | name = node.name.title() 271 | code = name 272 | # Autofill WaitBehavior 273 | if name == 'Wait' and len(node.children) == 1: 274 | node.children.append(Constant(name='Ignore Condition')) 275 | if not len(node.args) == len(node.children): 276 | raise Errors.SyntaxError('\'{}\' expected {} arguments ({}), received {}'.format( 277 | name, len(node.args), ', '.join(map(lambda arg: arg.__name__, node.args)), len(node.children)), 278 | pos=node._pos) 279 | for index, types in enumerate(zip(node.args, node.children[:])): 280 | arg, child = types 281 | if arg is None: 282 | continue 283 | elif arg == Variable: 284 | if not type(child) == Var: 285 | raise Errors.InvalidParameter('Expected variable in chase variable expression, received {}'.format( 286 | child.__class__.__name__), pos=child._pos) 287 | var = scope.get(child.name) 288 | if not var: 289 | raise Errors.NameError('\'{}\' is undefined'.format(child.name), pos=node._pos) 290 | node.children[index] = Raw(code=var.data.letter) 291 | print(var.data.letter) 292 | continue 293 | values = list(map(lambda x: x.replace(',', ''), flatten(arg.get_values()))) 294 | value = self.visit(child, scope).upper() 295 | if value in HeroConstant._values and name != 'Hero': 296 | node.children[index] = Constant(name='Hero({})'.format(value.title())) 297 | if 'ANY' in values: 298 | continue 299 | if value not in values: 300 | raise Errors.InvalidParameter('\'{}\' expected type {} for argument {}'.format( 301 | name, arg.__name__, index + 1), pos=child._pos) 302 | children = [self.visit(child, scope) for child in node.children] 303 | code += '(' + ', '.join(children) + ')' 304 | return code 305 | 306 | def visitConstant(self, node, scope): 307 | """A workshop value with no further parameters, such as `Event Player` or `Yellow`.""" 308 | return node.name.title() 309 | 310 | def visitCompare(self, node, scope): 311 | """Interprets a comparison expression.""" 312 | if node.op.lower() == 'in': 313 | return 'Array Contains(' + self.visit(node.right, scope) + ', ' + self.visit(node.left, scope) + ')' 314 | elif node.op.lower() == 'not in': 315 | return 'Not(Array Contains(' + self.visit(node.right, scope) + ', ' + self.visit(node.left, scope) + '))' 316 | return 'Compare(' + self.visit(node.left, scope) + f', {node.op}, ' + self.visit(node.right, scope) + ')' 317 | 318 | def visitAssign(self, node, scope): 319 | """Handles internal variable definition and assignment.""" 320 | code = '' 321 | value = { 322 | '+=': BinaryOp(left=node.left, op='+', right=node.right), 323 | '-=': BinaryOp(left=node.left, op='-', right=node.right), 324 | '*=': BinaryOp(left=node.left, op='*', right=node.right), 325 | '/=': BinaryOp(left=node.left, op='/', right=node.right), 326 | '^=': BinaryOp(left=node.left, op='^', right=node.right), 327 | '%=': BinaryOp(left=node.left, op='%', right=node.right) 328 | }.get(node.op, node.right) 329 | # Define variables 330 | if type(node.left) == Var: 331 | var = node.left 332 | name = var.name 333 | cur_var = scope.get(name) 334 | if not cur_var: 335 | letter = 'A' 336 | if var.type == Var.GLOBAL: 337 | if name in self.chase_vars: 338 | if name not in self.letters: 339 | try: 340 | self.letters[name] = next(self.global_letters) 341 | except StopIteration: 342 | raise Errors.InvalidParameter('Exceeded maximum number of chase variables (25) for this type.', pos=child._pos) 343 | letter = self.letters[name] 344 | index = None 345 | else: 346 | index = next(self.global_index) 347 | var.data = GlobalVar(letter=letter, index=index) 348 | elif var.type == Var.PLAYER: 349 | if name in self.chase_vars: 350 | if name not in self.letters: 351 | try: 352 | self.letters[name] = next(self.player_letters) 353 | except StopIteration: 354 | raise Errors.InvalidParameter('Exceeded maximum number of chase variables (25) for this type.', pos=child._pos) 355 | letter = self.letters[name] 356 | index = None 357 | else: 358 | index = next(self.global_index) 359 | player = self.resolve_name(var.player, scope) 360 | var.data = PlayerVar(letter=letter, index=index, player=player) 361 | elif var.type != Var.GLOBAL and cur_var.type != var.type: 362 | self.logger.warn('Ignoring type reassign for \'{}\' (Line {}:{})'.format(var.name, *var._pos)) 363 | var = cur_var 364 | elif cur_var.type != Var.CONST: 365 | var = cur_var 366 | else: 367 | raise Errors.SyntaxError('Cannot assign to const \'{}\''.format(var.name), pos=node._pos) 368 | if var.type != Var.PLAYER and var.player is not None: 369 | raise Errors.SyntaxError('Cannot target player for non-player variable \'{}\''.format(var.name), pos=node._pos) 370 | var.value = value 371 | scope.assign(name=name, var=var) 372 | elif type(node.left) == Item: 373 | parent = node.left.parent 374 | name = parent.name 375 | var = scope.get(name) 376 | try: 377 | assert type(var.value) == Array 378 | index = int(self.visit(node.left.index, scope)) 379 | var.value[index] = value 380 | except AssertionError: 381 | raise Errors.SyntaxError('Cannot assign to \'{}\'using indices (value is not an array)'.format(name), pos=node._pos) 382 | except ValueError: 383 | raise Errors.NotImplementedError('Array assignment only supports literal integer indices', pos=node._pos) 384 | scope.assign(name=name, var=var) 385 | elif type(node.left) == Attribute: 386 | if type(node.left.parent) == Object: 387 | obj = node.left.parent 388 | else: 389 | obj = scope.get(node.left.parent.name).value 390 | if not type(obj) == Object: 391 | raise Errors.SyntaxError('Cannot assign value to attributes') 392 | resolved = self.resolve_name(value, scope) 393 | var = Var(name=node.left.name, type_=Var.INTERNAL, value=resolved) 394 | if type(resolved) == String: 395 | var.type = Var.STRING 396 | obj.env.assign(node.left.name, var) 397 | return code 398 | else: 399 | raise Errors.NotImplementedError('Cannot assign value to {}'.format(type(node.left).__name__), pos=node._pos) 400 | var = scope.get(name) 401 | data = var.data 402 | value = self.visit(var.value, scope) 403 | if value == '': 404 | return code 405 | elif type(value) == Object: 406 | var.type = Var.OBJECT 407 | var.value = value 408 | scope.assign(name=name, var=var) 409 | return code 410 | if var.type == Var.GLOBAL: 411 | if data.index is not None: 412 | code += 'Set Global Variable At Index({}, {}, {})'.format(data.letter, data.index, self.visit(var.value, scope)) 413 | else: 414 | code += 'Set Global Variable({}, {})'.format(data.letter, self.visit(var.value, scope)) 415 | elif var.type == Var.PLAYER: 416 | if data.index is not None: 417 | code += 'Set Player Variable At Index({}, {}, {}, {})'.format(self.visit(data.player, scope), data.letter, data.index, self.visit(var.value, scope)) 418 | else: 419 | code += 'Set Player Variable({}, {}, {})'.format(self.visit(data.player, scope), data.letter, self.visit(var.value, scope)) 420 | return code 421 | 422 | def visitIf(self, node, scope): 423 | """If blocks contain a true and false block to evaluate. To simulate this in workshop, the false block 424 | is skipped when the condition is true, and vice-versa.""" 425 | cond = self.visit(node.cond, scope) 426 | skip_code = 'Skip If(Not({}), {});\n' 427 | skip_false = '' 428 | true_code = ';\n'.join(self.visit_children(node.true_block, scope)) + ';\n' 429 | false_code = '' 430 | if node.false_block: 431 | skip_false = 'Skip({});\n' 432 | if type(node.false_block) == If: 433 | false_code += self.visit(node.false_block, scope) 434 | else: 435 | for line in node.false_block.children: 436 | false_code += self.visit(line, scope) + ';\n' 437 | skip_code = skip_code.format(cond, true_code.count(';\n') + bool(node.false_block)) 438 | if false_code: 439 | skip_false = skip_false.format(false_code.count(';\n')) 440 | code = skip_code + true_code + skip_false + false_code 441 | return code 442 | 443 | def visitWhile(self, node, scope): 444 | """While loop is simulated by looping the action list while a condition is met. 445 | Support for while loops is limited.""" 446 | skip_cond = 'Skip If(Not({}), {});\n' 447 | cond = self.visit(node.cond, scope) 448 | block = ';\n'.join(self.visit_children(node.body, scope)) + ';\n' 449 | loop_cond = ';\n{};\nLoop If({})'.format(self.min_wait, cond) 450 | num_skips = block.count(';\n') + 2 # Include wait/loop skip 451 | skip_cond = skip_cond.format(self.visit(node.cond, scope), num_skips) 452 | code = skip_cond + block + loop_cond 453 | return code 454 | 455 | def visitFor(self, node, scope): 456 | """For loops store a pointer to each element in an iterable and loop the action list until the pointer 457 | is at the end of the iterable (length of iterable). If the length is a known value (e.g. user-created array), 458 | then loop unrolling is possible to reduce time and number of actions.""" 459 | code = '' 460 | pointer = node.pointer 461 | iterable = node.iterable 462 | if type(iterable) == Var: 463 | lines = [] 464 | array = scope.get(iterable.name).value 465 | try: 466 | assert type(array) == Array 467 | except AssertionError: 468 | raise Errors.SyntaxError('{} is not iterable'.format(iterable.name), pos=iterable._pos) 469 | for elem in array.elements: 470 | scope = Scope(name='for', parent=scope) 471 | var = Var(name=pointer.name, type_=Var.INTERNAL, value=elem) 472 | scope.assign(pointer.name, var) 473 | lines.append(';\n'.join(self.visit_children(node.body, scope))) 474 | code += ';\n'.join(lines) 475 | elif type(iterable) == Call: 476 | func_name = self.base_node(iterable).name 477 | func = scope.get(func_name).value 478 | try: 479 | self.scope = scope 480 | array = func(*([self] + iterable.args)) 481 | assert type(array) == Array 482 | lines = [] 483 | for elem in array.elements: 484 | for_scope = Scope(name='for', parent=scope) 485 | var = Var(name=pointer.name, type_=Var.INTERNAL, value=elem) 486 | for_scope.assign(pointer.name, var) 487 | result = self.visit_children(node.body, for_scope) 488 | if result: 489 | lines.append(';\n'.join(result)) 490 | code += ';\n'.join(lines) 491 | except AssertionError: 492 | raise Errors.SyntaxError('Function call did not return an array', pos=iterable._pos) 493 | except TypeError as ex: 494 | self.logger.debug('For loop TypeError:', ex) 495 | else: 496 | for_scope = Scope(name='for', parent=scope) 497 | value = Number(value='0') 498 | index = next(self.global_index) 499 | pointer_var = GlobalVar(letter='A', index=index) 500 | var = Var(name=pointer.name, type_=Var.GLOBAL, value=value, data=pointer_var) 501 | for_scope.assign(pointer.name, var) 502 | reset_pointer = 'Set Global Variable At Index(A, {}, 0);\n'.format(index) 503 | code += reset_pointer 504 | skip_code = '//FOR STARTSkip If(Compare(Count Of({}), ==, {}), {})'.format(self.visit(iterable, for_scope), self.visit(pointer, for_scope), '{}') 505 | block = ';\n'.join(self.visit_children(node.body, for_scope) + [ 506 | 'Modify Global Variable At Index(A, {}, Add, 1)'.format(index), 507 | self.min_wait, 508 | 'Loop', 509 | reset_pointer]) 510 | code += skip_code.format(block.count(';\n')) + ';\n' + block 511 | self.curblock.insert(0, self.tabs + '//SKIP TOSkip If(Compare(Value In Array(Global Variable(A), {}), !=, 0), {})'.format(index, '{}')) 512 | return code 513 | 514 | def visitBinaryOp(self, node, scope): 515 | """A binary expression takes two operands and one operator (addition, expontentiation, etc).""" 516 | if type(node.left) == Number and type(node.right) == Number: 517 | func = { 518 | '+': lambda a, b: a + b, 519 | '-': lambda a, b: a - b, 520 | '*': lambda a, b: a * b, 521 | '/': lambda a, b: a / b, 522 | '^': lambda a, b: a ** b, 523 | '%': lambda a, b: a % b 524 | }.get(node.op, None) 525 | if func: 526 | try: 527 | result = func(node.left, node.right) 528 | return self.visit(Number(value='{}'.format(result)), scope) 529 | except ZeroDivisionError: 530 | return self.visit(Number(value='0'), scope) 531 | code = { 532 | '+': 'Add', 533 | '-': 'Subtract', 534 | '*': 'Multiply', 535 | '/': 'Divide', 536 | '^': 'Raise To Power', 537 | '%': 'Modulo', 538 | 'or': 'Or', 539 | 'and': 'And' 540 | }.get(node.op) 541 | try: 542 | code += '(' + self.visit(node.left, scope) + ', ' + self.visit(node.right, scope) + ')' 543 | except RecursionError: 544 | self.logger.debug('Recursion in BinaryOp from: {}'.format(node)) 545 | return code 546 | 547 | def visitUnaryOp(self, node, scope): 548 | """A unary expression takes a single operand and operator (e.g. negation).""" 549 | if node.op == '-': 550 | code = '-' + self.visit(node.right, scope) 551 | elif node.op == '+': 552 | code = 'Abs(' + self.visit(node.right, scope) + ')' 553 | elif node.op == 'not': 554 | code = 'Not(' + self.visit(node.right, scope) + ')' 555 | return code 556 | 557 | def visitVar(self, node, scope): 558 | """Internal variable object detailing its type, value, data (used for player/global variables), and player (for player variables).""" 559 | var = scope.get(node.name) 560 | if not var: 561 | raise Errors.NameError('\'{}\' is undefined'.format(node.name), pos=node._pos) 562 | elif node.type == Var.STRING: 563 | var.type = Var.STRING 564 | if node.type != Var.GLOBAL and var.type != node.type: 565 | self.logger.warn('Ignoring type reassign for \'{}\' (Line {}:{})'.format(node.name, *node._pos)) 566 | code = '' 567 | if var.type == Var.GLOBAL: 568 | if var.data.index is not None: 569 | code += 'Value In Array(Global Variable({}), {})'.format(var.data.letter, var.data.index) 570 | else: 571 | code += 'Global Variable({})'.format(var.data.letter) 572 | elif var.type == Var.PLAYER: 573 | player = self.visit(var.data.player if node.player is None else node.player, scope) 574 | if var.data.index is not None: 575 | code += 'Value In Array(Player Variable({}, {}), {})'.format(player, var.data.letter, var.data.index) 576 | else: 577 | code += 'Player Variable({}, {})'.format(player, var.data.letter) 578 | elif var.type == Var.CONST: 579 | code += self.visit(var.value, scope) 580 | elif var.type == Var.INTERNAL: 581 | code += self.visit(var.value, scope) 582 | elif var.type == Var.STRING: 583 | code += var.value.value 584 | elif var.type == Var.CLASS: 585 | raise Errors.SyntaxError('Cannot assign to class/object literal', pos=node._pos) 586 | elif var.type == Var.OBJECT: 587 | return var.value 588 | else: 589 | raise Errors.NotImplementedError('Unexpected Var type {}'.format(var._type), pos=node._pos) 590 | return code 591 | 592 | def visitCustomString(self, node, scope): 593 | """A custom string field.""" 594 | code = 'Custom String("' + node.value.title() + '", Null, Null, Null)' 595 | return code 596 | 597 | def visitString(self, node, scope): 598 | """A string has three children which can be strings, but each one defaults to null.""" 599 | code = 'String("' + node.value.title() + '", ' 600 | children = ', '.join(self.visit(child, scope) for child in node.children) 601 | code += children + ')' 602 | return code 603 | 604 | def visitNumber(self, node, scope): 605 | """A numeric constant is represented by the value itself in the workshop.""" 606 | return node.value 607 | 608 | def visitTime(self, node, scope): 609 | """Shorthand for writing time values instead of doing integer arithmetic.""" 610 | time = node.value 611 | if time.endswith('ms'): 612 | time = float(time.rstrip('ms')) / 1000 613 | elif time.endswith('s'): 614 | time = float(time.rstrip('s')) 615 | elif time.endswith('min'): 616 | time = float(time.rstrip('min')) * 60 617 | return str(round(time, 3)) 618 | 619 | def visitVector(self, node, scope): 620 | """Convenient way to represent vector values.""" 621 | code = 'Vector(' 622 | components = ', '.join(self.visit(x, scope) for x in node.children) 623 | code += components + ')' 624 | return code 625 | 626 | def visitArray(self, node, scope): 627 | """Arrays in OWScript can take any value, including strings and constants such as heroes.""" 628 | if not node.elements: 629 | return 'Empty Array' 630 | else: 631 | elements = [] 632 | for elem in node.elements: 633 | if type(elem) in (String, Constant, Var): 634 | elements.append(Constant(name='Null')) 635 | else: 636 | elements.append(elem) 637 | num_elems = len(elements) 638 | if num_elems == 0: 639 | return 'Empty Array' 640 | code = 'Append To Array(' * num_elems 641 | code += 'Empty Array, ' + '), '.join(self.visit(elem, scope) for elem in elements) + ')' 642 | return code 643 | 644 | def visitItem(self, node, scope, visit=True): 645 | """An item is accessing an element of an array.""" 646 | # Try to access an array element by interpreting the number? 647 | if type(node.index) == Number and type(node.parent) == Var: 648 | var = scope.get(node.parent.name) 649 | if not var: 650 | raise Errors.NameError('\'{}\' is undefined'.format(node.parent.name), pos=node.parent._pos) 651 | index = int(node.index.value) 652 | array = var.value 653 | if not type(array) == Array: 654 | raise Errors.SyntaxError('Cannot get item from non-array \'{}\''.format(type(array).__name__), pos=node.parent._pos) 655 | if not 0 <= index < len(array): 656 | return self.visit(Number(value='0'), scope) 657 | else: 658 | if not visit: 659 | return var.value[index] 660 | if var.type == Var.GLOBAL: 661 | return 'Value In Array(Value In Array(Global Variable({})), {}, {})'.format(var.data.letter, var.data.index, index) 662 | elif var.type == Var.PLAYER: 663 | player = self.visit(var.data.player if node.parent.player is None else node.parent.player, scope) 664 | return 'Value In Array(Value In Array(Player Variable({}, {})), {}, {})'.format(player, var.data.letter, var.data.index, index) 665 | else: 666 | return self.visit(var.value[index], scope) 667 | else: 668 | try: 669 | index = scope.get(node.index.name) 670 | assert hasattr(index, 'value') 671 | index = int(index.value) 672 | item = scope.get(node.parent.name).value[index] 673 | return self.visit(item, scope) if visit else item 674 | except (ValueError, TypeError, AssertionError): 675 | array = self.visit(node.parent, scope) 676 | index = self.visit(node.index, scope) 677 | return 'Value In Array({}, {})'.format(array, index) 678 | 679 | def visitAttribute(self, node, scope): 680 | """Attributes are properties accessed using the dot operator.""" 681 | attr = node.name.lower() 682 | if type(node.parent) == Var: 683 | parent = scope.get(node.parent.name).value 684 | else: 685 | parent = node.parent 686 | if type(parent) == Item: 687 | item = self.visitItem(parent, scope, visit=False) 688 | node.parent = item 689 | result = self.visitAttribute(node, scope) 690 | return result 691 | try: 692 | attribute = getattr(parent, attr) 693 | except AttributeError: 694 | name = node.parent 695 | if type(parent) != Object and type(name) == str: 696 | name = name.title() 697 | raise Errors.AttributeError('\'{}\' has no attribute \'{}\''.format(name, attr), pos=node._pos) 698 | if type(parent) == Object: 699 | code = self.visit(attribute, parent.env) 700 | else: 701 | code = attribute.format(self.visit(parent, scope)) 702 | return code 703 | 704 | def visitCall(self, node, scope): 705 | """Calls are either made to built-in functions or user-defined functions. Their arguments must be evaluated beforehand.""" 706 | parent = node.parent 707 | base_node = self.base_node(node) 708 | var = scope.get(base_node.name) 709 | is_object = type(var) == Var and var.type == Var.OBJECT 710 | lines = [] 711 | # Handle method (attribute access followed by a call) 712 | if type(parent) == Attribute and not is_object: 713 | if var is not None: 714 | method = getattr(var.value, parent.name) 715 | else: 716 | method = getattr(base_node, parent.name) 717 | try: 718 | self.scope = scope 719 | result = method(self, *node.args) 720 | except TypeError as ex: 721 | print('Invalid method arguments:', ex) 722 | raise Errors.InvalidParameter("'{}' method received invalid arguments".format(parent.name), pos=parent._pos) 723 | if result: 724 | lines.append(self.visit(result, scope)) 725 | return ';\n'.join(lines) 726 | elif type(parent) is not Var: 727 | self.logger.debug('Called by parent of type {}', type(parent)) 728 | if not var: 729 | raise Errors.NameError('Undefined function \'{}\''.format(parent.name), pos=parent._pos) 730 | func = var.value 731 | if type(func) == Var: 732 | func = func.value 733 | elif type(func) == Object: 734 | obj = var.value 735 | func = obj.env.get(parent.name).value 736 | obj_var = Var(name=obj.name, type_=Var.OBJECT, value=obj) 737 | scope.assign('this', obj_var) 738 | # Handle user-defined and built-in functions 739 | if var.type == Var.CLASS: 740 | class_ = var.value 741 | obj = Object(type_=class_) 742 | scope = Scope(name=var.name, parent=scope) 743 | for child in class_.body: 744 | if type(child) == Assign: 745 | left = child.left 746 | if not (type(left) == Var and left.type == Var.GLOBAL): 747 | raise Errors.SyntaxError('Invalid variable for class assignment', pos=var._pos) 748 | var = Var(name=child.left.name, type_=Var.INTERNAL, value=child.right) 749 | scope.assign(left.name, var) 750 | elif type(child) == Function: 751 | var = Var(name=child.name, type_=Var.METHOD, value=child) 752 | scope.assign(var.name, var) 753 | obj.env = scope 754 | init = obj.env.get('init') 755 | if init is not None: 756 | obj_var = Var(name=obj.name, type_=Var.OBJECT, value=obj) 757 | scope.assign('this', obj_var) 758 | init_call = Call(args=node.args, parent=init.value) 759 | self.visit(init_call, scope) 760 | return obj 761 | elif var.type != Var.BUILTIN: 762 | if not func.arity >= len(node.args) >= func.min_arity: 763 | raise Errors.InvalidParameter('\'{}\' expected {} or more arguments, received {}'.format(func.name, func.min_arity, len(node.args)), pos=node._pos) 764 | # Extend default args 765 | default_args = [p.default for p in func.params[len(node.args):]] 766 | # Resolve variables in call 767 | args = [self.resolve_name(arg, scope) for arg in node.args + default_args] 768 | scope = Scope(name=func.name, parent=scope) 769 | for param, arg in zip(func.params, args): 770 | var = Var(name=param.name, type_=Var.INTERNAL, value=arg) 771 | scope.assign(param.name, var) 772 | for child in func.children: 773 | try: 774 | result = self.visit(child, scope=scope) 775 | if result: 776 | lines.append(result) 777 | except Errors.ReturnError as ex: 778 | result = self.visit(ex.value, scope=scope) 779 | if result: 780 | lines.append(result) 781 | elif var.type == Var.BUILTIN: 782 | try: 783 | self.scope = scope 784 | result = func(*([self] + node.args)) 785 | lines.append(self.visit(result, scope)) 786 | except TypeError as ex: 787 | self.logger.debug('TypeError in built-in function {}:'.format(var.name), ex) 788 | return ';\n'.join(lines) 789 | 790 | def visitReturn(self, node, scope): 791 | """Return statements break out of functions early.""" 792 | if node.value is not None: 793 | raise Errors.ReturnError(value=node.value) 794 | return '' 795 | 796 | def visit(self, node, scope): 797 | """Finds the relevant transpiler method for the current node.""" 798 | method_name = 'visit' + type(node).__name__ 799 | visitor = getattr(self, method_name) 800 | return visitor(node, scope) 801 | 802 | def visit_children(self, node, scope): 803 | """Convenience function to visit all children of a node.""" 804 | lines = [] 805 | for child in node.children: 806 | lines.append(self.visit(child, scope)) 807 | return lines 808 | 809 | def run(self): 810 | """Evaluates the parse tree from the parser into workshop code.""" 811 | global_scope = Scope(name='global') 812 | for func_name, func in Builtin.__annotations__.items(): 813 | var = Var(name=func_name, type_=Var.BUILTIN, value=func) 814 | global_scope.assign(func_name, var) 815 | code = self.visit(self.tree, scope=global_scope) 816 | return code 817 | -------------------------------------------------------------------------------- /OWScript/Workshop.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | try: 4 | from .AST import * 5 | except ImportError: 6 | from AST import * 7 | 8 | class WorkshopData: 9 | """Manager for workshop type data.""" 10 | def __init__(self): 11 | try: 12 | with open('Workshop.json') as f: 13 | self.data = json.load(f) 14 | except FileNotFoundError: 15 | with open('OWScript/Workshop.json') as f: 16 | self.data = json.load(f) 17 | 18 | def _gettype(self, type_): 19 | """Returns a WorkshopType object containing data about the argument.""" 20 | if type_ == 'ANY': 21 | return Any 22 | for key in self.data.get('types'): 23 | if key.get('name') == type_: 24 | name = type_.title().replace(' ', '') 25 | return globals().get(name) 26 | 27 | def __getitem__(self, item): 28 | """Returns the instance of the class from the specified Event/Action/Value.""" 29 | for data_type, data_list in self.data.items(): 30 | if data_type in ('events', 'types'): 31 | continue 32 | for key in data_list: 33 | if key.get('name') == item: 34 | name = item 35 | description = key.get('description') 36 | _args = key.get('args', []) 37 | if not _args: 38 | return Constant(name=item) 39 | args = [self._gettype(arg.get('type')) for arg in key.get('args', [])] 40 | node = OWID(name=name, description=description, args=args) 41 | return node 42 | return Constant(name=item) 43 | Workshop = WorkshopData() 44 | # types_ = Workshop.data.get('types') 45 | # print([x.get('name') for x in types_]) 46 | # for type_ in types_: 47 | # extends = ', '.join(map(lambda x: ''.join(x.title().split()), type_.get('extends', []))) 48 | # values = '_values = {}'.format(type_.get('values', [])) 49 | # name = ''.join(type_.get('name').title().split()) 50 | # string = f"""class {name}(WorkshopType): 51 | # {values} 52 | # _extends = [{extends}] 53 | # """ 54 | # print(string) -------------------------------------------------------------------------------- /OWScript/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adapap/OWScript/c7aad13192971637e434d5842d413d7518552df9/OWScript/__init__.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OWScript 2 | Python-like scripting language which transpiles into Overwatch Workshop script rulesets. 3 | 4 | Setup 5 | ===== 6 | ## Installation & Usage 7 | 1. Install Python with `pip` if you have not done so already. 8 | 2. Run the command `python OWScript.py` with the following arguments: 9 | - `input` Path to input file, blank for stdin 10 | - `-m | --min` Optional: minifies the output by stripping whitespace 11 | - `-s | --save [FILE]` Optional: saves to the target output file instead of stdout 12 | - `-c | --copy` Optional: copies code to clipboard (must have *pyperclip* installed: `pip install pyperclip`) 13 | 14 | **NPM Integration** by @MatthewSH 15 | [OWScript NPM Package](https://www.npmjs.com/package/owscript) 16 | 17 | ## Syntax Highlighting 18 | 19 | **Visual Studio Code** 20 | Download the latest [OWScript extension](https://marketplace.visualstudio.com/items?itemName=adapap.owscript) from the marketplace. 21 | 22 | **Sublime Text 3** 23 | In the `Syntax/` folder, you can find the raw Iro code which I used to generate a Sublime Text file with modifications. You can directly import the `OWScript.sublime-syntax` file by putting it in your ST3 `User` folder. 24 | 25 | Projects 26 | ======== 27 | - **Cookie Clicker** [![Discord Shield](https://discordapp.com/api/guilds/572937743114436619/widget.png?style=shield "Made by @adapap")](https://discord.gg/5Nst8g5) 28 | - [**Upgrade Shop**](https://github.com/overwatchworkshop/upgrade-shop) 29 | 30 | Documentation 31 | ============= 32 | *See example code in the `Examples/` folder.* 33 | 34 | **Input** `*.owpy` 35 | 36 | **Output** `*.ows` (standard as agreed upon voting results) 37 | 38 | **Semantic** 39 | * [Values / Actions](#values--actions) 40 | * [Annotations / Comments](#annotations--comments) 41 | * [Assignment / Arithmetic](#assignment--arithmetic) 42 | * [Logic](#logic) 43 | * [Functions](#functions) 44 | * [Loops](#loops) 45 | * [Attributes / Methods](#attributes--methods) 46 | * [Imports](#imports) 47 | 48 | **Data Types & Structures** 49 | * [Variables](#variables) 50 | * [Strings](#strings) 51 | * [Vectors](#vectors) 52 | * [Time](#time) 53 | * [Arrays](#arrays) 54 | * [Alias Table](#alias-table) 55 | 56 | ## Notes 57 | - Be sure not to conflict variable/function names with built-in functions such as `Add`, `Wait`, or `Damage`. 58 | - Many commonly used values have been aliased in order to reduce verbosity. See the table at the bottom for the list of built-in aliases. 59 | - If you have an unexpected error/suggestion, feel free to submit an issue 60 | - Alternatively, I am open to pull requests if you want to contribute 61 | 62 | ## Values / Actions 63 | Values and actions are the main types that come up when working in the Workshop. In general, anything with parameters can be written in two ways (which can be interchanged): 64 | 65 | **Indented Blocks** 66 | ``` 67 | Round 68 | Count Of 69 | All Players 70 | Team 2 71 | Up 72 | ``` 73 | **Parenthesized / Literal** 74 | ``` 75 | Round(Count Of(All Players(Team 2)), Up) /* Same as output */ 76 | ``` 77 | 78 | ## Annotations / Comments 79 | Annotations are ways to remind yourself what the type of a variable. It is written as text followed by a colon. Comments are written as most traditional languages (`// line comment`, `/* multiline comment */`). Both are ignored in the code output. 80 | ``` 81 | Event 82 | /* Set up event attributes */ 83 | Event_Type: Ongoing - Event Player // Event_Type is an annotation (cannot have spaces!) 84 | Annotation_2: All 85 | ``` 86 | 87 | ## Assignment / Arithmetic 88 | Assignment (regular and augmented), as well as most arithmetic operators work as they do in Python or other traditional programming languages. Operators include: `+ - * / ^ %` as well as the augmented equivalents: `+= -= *= /= ^= %=` 89 | ``` 90 | a = 1 91 | a += -1 92 | a *= 3 93 | a = a ^ (a + a) % 3 94 | ``` 95 | 96 | ## Logic 97 | Boolean logic is implemented exactly as in Python. The operators `and`, `or`, and `not` function as C-style `&&`, `| 98 | |`, and `!`. Comparison operators include the traditional `<`, `>`, `<=`, `>=`, `!=`, `==` as well as containment operators `in` and `not in`. 99 | ``` 100 | b = True and not True 101 | Count Of 102 | Everyone 103 | == 12 // The reason why == 12 is here is to distinguish between the constant "Everyone" and the value "Count Of". 104 | // You can choose to write this expression inline for less ambiguity: 105 | Count Of(Everyone) == 12 106 | y = Event Player in Players In Radius(<1, 2, 3>, 15) 107 | ``` 108 | 109 | ## Variables 110 | Variables are ways to reference values using a name. Their type is stored when they are defined. 111 | 112 | **Global Variables (default)** 113 | ``` 114 | gvar hero_index = 1 115 | global_time = 60s // default type is global 116 | ``` 117 | **Player Variables** 118 | ``` 119 | pvar score = 2 // pvar is only needed when defining a variable 120 | pvar score@Event Player = 3 // Event Player (default) is the player which the variable will be bound to 121 | score += 1 // modifies the pvar score 122 | ``` 123 | **Const** 124 | ``` 125 | const cost = 100 126 | /* const cannot be modified and directly outputs the value, 127 | rather than outputting Value In Array(...) */ 128 | ``` 129 | 130 | Using the technique from [@ItsDeltin](https://github.com/ItsDeltin), the limit to 131 | the number of variables that can be created is the maximum length of an array (\~1000 variables). 132 | 133 | ## Strings 134 | String literals are enclosed with quotes. Formatted strings are made with enclosing backticks, using `{}` whenever you want to use a variable instead of a string constant. 135 | ``` 136 | Rule "String Demo" 137 | Event 138 | On Each Player 139 | All 140 | All 141 | Actions 142 | Msg(Event Player, "Hello") // Alias for Small Message 143 | Big Msg(Event Player, `Money: {}`(pvar money)) // Example formatted string 144 | Small Msg(Event Player, `Unlocked {} / {}: Victory!`(5, 5)) // More advanced formatted string 145 | ``` 146 | 147 | ## Vectors 148 | Vectors can be created in 3 ways as well: 149 | 150 | **Literal** 151 | ``` 152 | Vector(1, 2, 3) 153 | ``` 154 | **Block** 155 | ``` 156 | Vector 157 | 1 158 | 2 159 | 3 160 | ``` 161 | **Idiomatic** 162 | ``` 163 | <1, 2, 3> 164 | ``` 165 | 166 | ## Time 167 | Time can be represented in *ms*, *s*, or *min* as a shorthand for the number value. 168 | ``` 169 | Wait(1s + 500ms) 170 | Wait 171 | 0.025min 172 | ``` 173 | 174 | ## Arrays 175 | Arrays are created, modified, and accessed as in Python notation. Arrays can be nested inside the global/player variables, which allows for more complex operations on arrays. (No slice support yet) 176 | 177 | **Creation** 178 | ``` 179 | empty = [] 180 | costs = [5, 15, 30] 181 | ``` 182 | **Modification** 183 | ``` 184 | costs[1] = 20 185 | total = costs[0] + costs[1] + costs[2] 186 | ``` 187 | 188 | ## Functions 189 | Functions allow you to write a block of code once and reuse it many times. They can be used to generate HUDs like a macro or used as a rule factory. All functions must be defined before they are called, and they must be defined at the top level scope (same as where rules are defined). Parameters can be optional, denoted by `?`, which sets the value to `Null` when omitted. Alternatively, specify a default value e.g. `pos?=Event Player.pos`. 190 | 191 | *Note: Functions can access global-scope variables; however, the global scope cannot access variables defined locally in functions* 192 | 193 | ``` 194 | %event_func 195 | Event 196 | On Each Player 197 | All 198 | All 199 | %add_rule(a, b, name_) 200 | Rule name_ 201 | event_func() 202 | c = a + b 203 | %say(text, who?=Everyone) // optional parameter, default to Everyone 204 | Msg(who, text) 205 | Rule "Function Demo" 206 | event_func() 207 | Actions 208 | say("Thanks!") 209 | add_rule(1, 5, "Add Two") 210 | ``` 211 | **Builtin Functions** 212 | 213 | |Function|Parameters|Description| 214 | |:------:|----------|-----------| 215 | |range|*stop* or *start[, stop[, step]]*|Creates an array of numbers from start to stop (exclusive), counting by step| 216 | |floor|*n*|Rounds a numeric expression down to the nearest integer 217 | |ceil|*n*|Rounds a numeric expression up to the nearest integer 218 | |get_map|Returns the current map ID. This can be compared with map names which alias to their respective ID: `get_map() == Dorado`. For the list of maps and their corresponding IDs, please review [@Xerxes post](https://us.forums.blizzard.com/en/overwatch/t/workshop-resource-map-identifier-map-detection-script-v2-0-only-2-actions/341132). 219 | 220 | ## Loops 221 | The while loop is syntactic sugar for using the `Loop` action in the Workshop. At the moment, only use while loops if the purpose of the rule is solely to repeat code until a condition is met. 222 | ``` 223 | while pvar life > 10: 224 | Damage(Event Player, Null, 10) 225 | ``` 226 | A for loop lets you iterate over custom iterables, such as an array of values, a range, or workshop values such as All Players. 227 | ``` 228 | for i in range(1, 10, 2): 229 | Msg(Event Player, i) 230 | for y in [Genji, Tracer, Widowmaker]: 231 | Kill 232 | Players On Hero(y) 233 | ``` 234 | 235 | ## Attributes / Methods 236 | Attributes are properties of an object that can be accessed using the dot operator `.`, which refers to the value before it in order to access a property. A method is simply an attribute followed by a call, which has parameters. Refer to the table below for builtin attributes and methods. 237 | ``` 238 | pvar xpos = Event Player.x // Attribute 239 | y = Victim.jumping and Attacker.moving 240 | scores.append(123) // Method 241 | ``` 242 | 243 | **Attribute Table** 244 | 245 | |Name|Description| 246 | |:--:|-----------| 247 | |x|The X component of a vector| 248 | |y|The Y component of a vector| 249 | |z|The Z component of a vector| 250 | |facing|The facing direction of a player| 251 | |pos|The position of a player| 252 | |eyepos|The eye position of a player| 253 | |hero|The hero of a player| 254 | |team|The team of a player| 255 | |jumping|Check if a player is holding the Jump key| 256 | |crouching|Check if a player is holding the Crouch key| 257 | |moving|Check if the speed of a player is non-zero| 258 | 259 | **Method Table** 260 | 261 | |Name|Parameters|Description| 262 | |:--:|:--------:|-----------| 263 | |append|*element*|Appends an element to the given array| 264 | |index|*element*|Returns the numeric index of an array element| 265 | |halt||Mitigates the motion of a player| 266 | 267 | ## Alias Table 268 | |Alias|Output| 269 | |-----|------| 270 | |Abs|Absolute Value| 271 | |Any True|Is True For Any| 272 | |All True|Is True For All| 273 | |Chateau Guillard|Château Guillard| 274 | |Cos|Cosine From Degrees| 275 | |Cosr|Cosine From Radians| 276 | |Cur Elem|Current Array Element| 277 | |Filter|Filtered Array| 278 | |Everyone|All Players(Team(All))| 279 | |LOS|Is In Line Of Sight| 280 | |Index|Index Of Array Value| 281 | |Lucio|Lúcio| 282 | |On Each Player|Ongoing - Each Player| 283 | |On Global|Ongoing - Global| 284 | |Players In Radius|Players Within Radius| 285 | |Round|Round To Integer| 286 | |Sin|Sine From Degrees| 287 | |Sinr|Sine From Radians| 288 | |Torbjorn|Torbjörn| 289 | 290 | ## Imports 291 | OWScript allows bigger scripts and scripts that use common funcitonality to be broken up into modules and imported into a base file. All the "imported" files are evaluated into a parse tree, which is transpiled to workshop code by the base file. 292 | 293 | You can import a file by using the `#import 'filepath'`. If the file is in a folder, put the relative path to the file as shown in the examples below: 294 | 295 | **Imported File** `lib/functions.owpy` 296 | ``` 297 | %CreateEffect(pos, type, color) 298 | Create Effect 299 | Visible_To: Everyone 300 | Type: type 301 | Color: color 302 | Position: pos 303 | Radius: 1.5 304 | Reevaluation: Visible To 305 | ``` 306 | 307 | **Imported File** `src/setup.owpy` 308 | ``` 309 | Rule "Setup Effects" 310 | Event 311 | On Global 312 | Actions 313 | CreateEffect(<0,0,0>, Ring, Red) 314 | ``` 315 | 316 | **Base File** `src/game.owpy` 317 | ``` 318 | #import 'lib/functions' 319 | #import 'src/setup' 320 | ``` -------------------------------------------------------------------------------- /Syntax/Comments.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Comments 7 | scope 8 | source.OWScript 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START 16 | value 17 | // 18 | 19 | 20 | name 21 | TM_COMMENT_START_2 22 | value 23 | /* 24 | 25 | 26 | name 27 | TM_COMMENT_END_2 28 | value 29 | */ 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Syntax/OWScript.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | name: OWScript 4 | scope: source.OWScript 5 | file_extensions: [ owpy ] 6 | 7 | contexts: 8 | main: 9 | - match: '(\%([_a-zA-Z][_a-zA-Z0-9]*))(?=\()' 10 | push: param_list 11 | captures: 12 | 0: entity.name.function.OWScript 13 | - match: '(\%([_a-zA-Z][_a-zA-Z0-9]*)\b)' 14 | captures: 15 | 0: entity.name.function.OWScript 16 | - match: '(\s+)' 17 | captures: 18 | 0: empty.OWScript 19 | push: 20 | - match: '(?=\S)' 21 | pop: true 22 | captures: 23 | 0: empty.OWScript 24 | - match: '(.)' 25 | captures: 26 | 0: empty.OWScript 27 | - match: '(;)' 28 | captures: 29 | 0: punctuation.OWScript 30 | - match: '(\b([_a-zA-Z][_a-zA-Z0-9]*)\:\s+(?=.))' 31 | push: expr 32 | captures: 33 | 0: comment.annotation.OWScript 34 | - include: rule 35 | param_list: 36 | - match: '(\))' 37 | pop: true 38 | captures: 39 | 0: punctuation.OWScript 40 | - match: '(\()' 41 | captures: 42 | 0: punctuation.OWScript 43 | - match: '(,)' 44 | captures: 45 | 0: punctuation.OWScript 46 | - match: '([_a-zA-Z][_a-zA-Z0-9]*)' 47 | captures: 48 | 0: variable.parameter.OWScript 49 | - include: multiline_comment 50 | rule: 51 | - match: '(?i)(Rule)' 52 | captures: 53 | 0: storage.type.rule.OWScript 54 | - include: expr 55 | expr: 56 | - match: '(\n)' 57 | pop: true 58 | captures: 59 | 0: empty.OWScript 60 | - match: '(\s+)' 61 | captures: 62 | 0: empty.OWScript 63 | push: 64 | - match: '(?=\S)' 65 | pop: true 66 | captures: 67 | 0: empty.OWScript 68 | - match: '(.)' 69 | captures: 70 | 0: empty.OWScript 71 | - match: '(//.*)' 72 | captures: 73 | 0: comment.OWScript 74 | - include: multiline_comment 75 | - match: '([_a-zA-Z][_a-zA-Z0-9]*)(?=\()' 76 | push: arg_list 77 | captures: 78 | 0: variable.function.OWScript 79 | - match: '\b(Team 1|Team 2)\b' 80 | captures: 81 | 0: constant.language.OWScript 82 | - match: '(?i)\b(?=|>|!=|=|\+|\-|\*|\/|\^|%)(?=[^,\)\]]+)' 123 | captures: 124 | 0: keyword.operator.OWScript 125 | vector : 126 | - match: '(>)' 127 | pop: true 128 | captures: 129 | 0: punctuation.vector.OWScript 130 | - match: '(,)' 131 | captures: 132 | 0: punctuation.OWScript 133 | - include: expr 134 | arg_list: 135 | - match: '(\))' 136 | pop: true 137 | captures: 138 | 0: punctuation.OWScript 139 | - match: '(\()' 140 | captures: 141 | 0: punctuation.OWScript 142 | - match: '(,)' 143 | captures: 144 | 0: punctuation.OWScript 145 | - include: expr 146 | fstring: 147 | - match: '(`)' 148 | pop: true 149 | captures: 150 | 0: string.OWScript 151 | - match: '(\{)' 152 | captures: 153 | 0: constant.language.OWScript 154 | push: 155 | - match: '(\})' 156 | pop: true 157 | captures: 158 | 0: constant.language.OWScript 159 | - match: '(.)' 160 | captures: 161 | 0: constant.language.OWScript 162 | - match: '([^\x{007b}\x{007d}\x{0060}]*)' 163 | captures: 164 | 0: string.OWScript 165 | multiline_comment: 166 | - match: '(\s*/\*)' 167 | captures: 168 | 0: comment.OWScript 169 | push: 170 | - match: '(\*/)' 171 | pop: true 172 | captures: 173 | 0: comment.OWScript 174 | - match: '(.)' 175 | captures: 176 | 0: comment.OWScript 177 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyperclip==1.7.0 2 | --------------------------------------------------------------------------------