Skip to content

High-level language for the Overwatch Workshop with support for compilation and decompilation.

License

Notifications You must be signed in to change notification settings

Zezombye/overpy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OverPy

OverPy is a high level language for the Overwatch workshop with a Python-like syntax, allowing you to code your gamemodes with modern development practices: multiple files, switches, dictionaries, macros, function macros, enums, built-in JS preprocessing...

It contains both a compiler, and a decompiler, to allow you to quickly convert your existing gamemodes to OverPy.

All in-game languages are supported, meaning you do not need to switch your in-game language to English to use it.

The VS Code extension includes syntax highlighting, autocompletion, and documentation:

Join the discord for help & feedback: https://workshop.codes/discord

Play around with the demo: https://zezombye.github.io/overpy/demo

Thanks to:

Development

  • Install: npm install
  • Build in dev and test with the demo: npm run dev
  • Build out/overpy_standalone.js: npm run package
  • Build .vsix for prod: vsce package (also builds overpy_standalone.js)

Installation

  1. Download and install VS Code: https://code.visualstudio.com/download
  2. In the sidebar, click the "Extensions" button, then search for "overpy" and install the extension:

  1. Make sure you have configured Windows to show file extensions.
  2. Create a new file in VS Code (File -> New Text File or Ctrl+N)

  1. Press Ctrl+S to save the untitled file, then save it wherever you want, but make sure it has an ".opy" extension.

  1. The file should now have the OverPy icon and an ".opy" (not .opy.txt) extension.
  2. Press Ctrl+Shift+P, type "overpy", then select the "Insert Template" command.

  1. You should now have the "OverPy starter pack" in your file. Press Ctrl+S to save it and to compile the starter pack to Workshop.
  2. The gamemode is now compiled and can be pasted directly into Overwatch.

  1. If the text language of Overwatch is set to something other than English (not recommended for workshop) and you are unable to paste the gamemode, go to File -> Preferences -> Settings, type "overpy", then select the workshop language.

  2. To import an existing gamemode, copy the Workshop code from Overwatch using the "copy settings" button.

  1. Then, in an empty .opy file (refer back to steps 4-5 to create one), press Ctrl+Shift+P, type "overpy", then select "Decompile".

  1. Your gamemode is now converted to OverPy and can be compiled by saving the file.

It is not recommended to constantly switch between Workshop and OverPy, as you will not be able to fully use OverPy to its full potential. Once you have decompiled your gamemode, you should not decompile it again.

You may get warnings when compiling; do not ignore them, as they can lead to bugs in your gamemode. If you are sure some warnings can be ignored, see below for how to disable them.

General Syntax

It is recommended to also decompile one of your existing gamemodes to get a better feel of the OverPy syntax.

# Line comments are made with "#".
# Multiline comments are made with /* */.

# A rule is declared with "rule" followed by the rule name and a colon ":".
# The contents of a rule MUST always be indented.
# The default indentation is 4 spaces.
# Do not change it, as a tab is considered equivalent to 4 spaces.
rule "setup":
    disableInspector() #don't forget to add that to your gamemodes


# Rule metadata is specified with annotations, starting with "@".
# The @Event annotation specifies the event of the rule, same for @Team, @Hero and @Slot.
# The @Condition annotation specifies a rule condition. You can have multiple conditions.
rule "player has spawned":
    @Event eachPlayer
    @Condition eventPlayer.hasSpawned()
    @Condition eventPlayer.A == 1
    @Team 1
    @Hero widowmaker
    @Slot 11
    # You can suppress a warning within the rule with the @SuppressWarnings annotation,
    # followed by the warning names separated by spaces.
    @SuppressWarnings w_player_closest_to_reticle w_wait_9999 #don't do this, it's just for the example

    eventPlayer.B = eventPlayer.getPlayerClosestToReticle(Team.ALL)
    wait(9999)


# A subroutine is declared with the "def" keyword, but follows the same format as a rule.
# Note that due to Workshop limitations, no parameters or return values can be specified.
def teleportPlayerToSpawn():
    @Name "Subroutine: Teleport the player to spawn"
    eventPlayer.teleport(vect(10, 0, 10))


# By default, optimization is enabled, meaning empty rules will not appear in the compilation output.
# To prevent this, you can add the @Delimiter annotation.
rule "---------- Boss fight code ----------":
    @Delimiter
    @Disabled
    @NewPage #adds empty rules to put the rule at the top of a page


# You can declare variables using the globalvar/playervar keywords, followed by an optional index.
# Default variable names (A-Z, AA-DX) do not need to be declared.
globalvar gameStatus 2
playervar score #index is automatically calculated

# You can also directly initialize a variable.
globalvar spawnLocation = vect(10, 0, 10)

rule "functions":
    @Event eachPlayer

    # The vast majority of functions follow these rules:

    # 1. The function name is camelCased.
    # Create Effect() -> createEffect().
    setMatchTime(3600)

    # 2. Function names taking a player, array or string as a first argument are "member functions".
    # Teleport(Event Player, Vector(10,0,10)) -> eventPlayer.teleport(vect(10,0,10))
    eventPlayer.cancelPrimaryAction()

    # 3. Functions to get constants such as Hero(), Map() or Gamemode() are replaced by enums.
    # Map(Hanamura) -> Map.HANAMURA.
    eventPlayer.startForcingHero(Hero.ANA)

    # 4. Operators and some functions have significant syntax changes.
    # You can expand the list below to see all such functions.

    # Some functions also have default values.
    wait() #Equivalent to wait(0.016, Wait.IGNORE_CONDITION)

    # You can also specify individual arguments using the name=value syntax.
    # hudHeader() is a macro for hudText() that only displays the header.
    hudHeader(getAllPlayers(), "Some text", sortOrder=5, color=Color.YELLOW)
    hudHeader(text="Some text")
List of functions with a significant syntax/name change (click to expand)

If a function is not in that list, then the name is the English name in camelCase; simply rely on the autocompletion to find it. (Also type a "." to check if it is a member function.)

Workshop function OverPy function
Abort return
Abort If if condition:
    return
Abort If Condition Is False if not ruleCondition:
    return
Abort If Condition Is True if ruleCondition:
    return
Absolute Value abs()
Add(A, 3) A + 3
And(A == 2, B == 4) A == 2 and B == 4
Append To Array(array, value) array.concat(value)
Arccosine In Degrees acosDeg()
Arccosine In Radians acos()
Arcsine In Degrees asinDeg()
Arcsine In Radians asin()
Arctangent In Degrees atan2Deg()
Arctangent In Radians atan2()
Array(a, b, c) [a, b, c]
Array Contains(array, value) value in array
Array Slice(array, start, count) array.slice(start, count)
Assist Count getNumberOfAssistIds()
Attach Players player.attachTo()
Backward Vector.BACKWARD
Button(Interact) Button.INTERACT
Call Subroutine(subroutine) subroutine()
Char In String(str, index) str.charAt(index)
Color(White) Color.WHITE
Compare(a, ==, b) a == b (same for !=, <, etc)
Cosine From Degrees cosDeg()
Cosine From Radians cos()
Count Of len()
Current Array Element See Filtered Array
Current Array Index See Filtered Array
Custom Color rgb()
Custom String("Score: {0}", Score Of(Event Player)) "Score: {}".format(eventPlayer.getScore())
See Strings for more info.
Damage Modification Count getNumberOfDamageModificationIds()
Damage Over Time Count getNumberOfDamageOverTimeIds()
Detach Players player.detach()
Disable Movement Collision With Environment player.disableEnvironmentCollision()
Disable Movement Collision With Players player.disablePlayerCollision()
Divide(A, 3) A / 3
Down Vector.DOWN
Else else:
Else If elif:
Empty Array []
End Go back a level of indentation
Entity Count getNumberOfEntityIds()
Evaluate Once evalOnce()
Filtered Array(array, Current Array Element == 2)
Filtered Array(array, Current Array Element == 2 && Current Array Index > 4)
[elem for elem in array if elem == 2]
[elem for elem, index in array if elem == 2 and index > 4]
The elem and index variables represent Current Array Element and Current Array Index respectively. You can name them however you like.
First Of(array) array[0]
For Global Variable(A, 0, 10, 1)
For Global Variable(A, 1, 10, 1)
For Global Variable(A, 0, 10, 2)
for A in range(10):
for A in range(1, 10):
for A in range(0, 10, 2)
The step can be omitted if it is 1, and the start can be omitted if it is 0 and step is 1.
For Player Variable(Event Player, A, 1, 10, 2) for eventPlayer.A in range(1, 10, 2):
Forward Vector.FORWARD
Game Mode(Assault) Gamemode.ASSAULT
Global.A A
Global Variable(A) A
Heal Over Time Count getNumberOfHealingOverTimeIds()
Healing Modification Count getNumberOfHealingModificationIds()
Hero(Ana) Hero.ANA
Hero Being Duplicated player.getHeroOfDuplication()
If(A == 2) if A == 2:
If-Then-Else(A == 2, 3, 20) 3 if A == 2 else 20
Note that the if/else operator has a low precedence. This means B + 3 if A == 2 else 20 will be parsed as (B + 3) if A == 2 else 20. It is customary to wrap the if/else operator with parentheses.
Index Of Array Value(array, value) array.index(value)
Index Of String Char(str, char) str.strIndex(char)
Is Button Held player.isHoldingButton()
Is In Line of Sight isInLoS()
Is Portrait On Fire player.isOnFire()
Is True For All(array, Current Array Element == 2) all([elem == 2 for elem in array])
Is True For Any(array, Current Array Element == 2 && Current Array Index > 4) any([elem == 2 and idx > 4 for elem, idx in array])
Also see Filtered Array.
Last Of(array) array.last()
Left Vector.LEFT
Log To Inspector printLog()
Loop If(A == 2) if A == 2:
    loop()
Loop If Condition Is False if not ruleCondition:
    loop()
Loop If Condition Is True if ruleCondition:
    loop()
Magnitude Of magnitude()
Map(Workshop Island) Map.WORKSHOP_ISLAND
Mapped Array(array, Current Array Element + 2)
Mapped Array(array, Current Array Element * Current Array Index)
[elem+2 for elem in array]
[elem * idx for elem,idx in array]
Modify Global Variable(A, Add, 2) A += 2
Modify Global Variable(A, Append To Array, 2) A.append(2)
Modify Global Variable(A, Divide, 2) A /= 2
Modify Global Variable(A, Max, 2) A max= 2
Modify Global Variable(A, Min, 2) A min= 2
Modify Global Variable(A, Modulo, 2) A %= 2
Modify Global Variable(A, Raise To Power, 2) A **= 2
Modify Global Variable(A, Remove From Array By Index, 2) del A[2]
Modify Global Variable(A, Remove From Array By Value, 2) A.remove(2)
Modify Global Variable(A, Subtract, 2) A -= 2
Modify Global Variable At Index(A, 1, Append To Array, 2) A[1].append(2)
Modify Global Variable At Index(A, 1, Remove From Array By Index, 2) del A[1][2]
Modify Player Score player.addToScore()
Modify Player Variable(Event Player, A, Max, 2) eventPlayer.A max= 2
Modify Player Variable At Index(Event Player, A, 1, Remove From Array By Value, 2) eventPlayer.A[1].remove(2)
Modify Team Score addToTeamScore()
Modulo(A, B) A % B
Move Player to Team moveToTeam()
Multiply(A, B) A * B
Not(a) not a
Number(1234) 1234
Objective Index getCurrentObjective()
Objective Position getObjectivePosition()
Opposite Team Of(Team Of(Event Player)) getOppositeTeam(eventPlayer.getTeam())
Or for a player: eventPlayer.getOppositeTeam()
Or(A == 2, B == 4) A == 2 or B == 4
Player Carrying Flag getFlagCarrier()
Player Variable(Event Player, A) eventPlayer.A
Press Button player.forceButtonPress()
Raise To Power(A, 2) A ** 2
Random Integer random.randint()
Random Real random.uniform()
Random Value In Array random.choice()
Randomized Array random.shuffle()
Raycast Hit Normal(start, end, include, exclude, includePlayerObjects) raycast(start, end, include, exclude, includePlayersObjects).getNormal()
Raycast Hit Player(start, end, include, exclude, includePlayerObjects) raycast(start, end, include, exclude, includePlayersObjects).getPlayerHit()
Raycast Hit Position(start, end, include, exclude, includePlayerObjects) raycast(start, end, include, exclude, includePlayersObjects).getHitPosition()
Remove From Array(array, 2) array.exclude(2)
Remove Player player.removeFromGame()
Right Vector.RIGHT
Round To Integer(A, Up) ceil(A)
Round To Integer(A, Down) floor(A)
Round To Integer(A, To Nearest) round(A)
Set Environment Credit Player player.setEnvironmentalKillCreditor()
Set Global Variable(A, 2) A = 2
Set Global Variable At Index(A, 1, 2) A[1] = 2
Set Player Variable(Event Player, A, 2) eventPlayer.A = 2
Set Player Variable At Index(Event Player, A, 1, 2) eventPlayer.A[1] = 2
Sine From Degrees sinDeg()
Sine From Radians sin()
Skip(A + 3) goto loc+A+3
Skip(2)
A = 1
B = 2
C = 3
goto label
A = 1
B = 2
label:
C = 3
If the argument of the Skip() action is a number, you can instead specify a label within the rule (followed by a colon ":") and OverPy will automatically calculate how many actions to jump.
Skip If(A == 2, 4) if A == 2:
    goto label
Sorted Array(array, Current Array Element.score) sorted(array, key=lambda elem: elem.score)
Sorted Array(array, -1 * Current Array Index) sorted(array, key=lambda elem, idx: -idx)
Or in this specific case: array.reverse()
The elem and idx variables represent the Current Array Element and Current Array Index values respectively. You can name them however you like.
Square Root sqrt()
Start Assist player.startGrantingAssistFor()
Start Forcing Dummy Bot Name player.startForcingName()
Start Holding Button player.startForcingButton()
Start Modifying Hero Voice Lines player.startModifyingVoicelinePitch()
Start Rule(subroutine, behavior) async(subroutine, behavior)
Start Scaling Player player.startScalingSize()
Stop Holding Button player.stopForcingButton()
Stop Modifying Hero Voice Lines player.stopModifyingVoicelinePitch()
String("Hello") l"Hello"
Note: those are "localized strings" and should never be used. Use Custom String instead (don't prefix strings by "l").
String Contains strContains()
String Length strLen()
String Replace(Custom String("abc"), Custom String("a"), Custom String("d")) "abc".replace("a", "d")
String Slice(Custom String("abc"), 1, 2) "abc".substring(1, 2)
String Split(Custom String("abc"), Custom String("b")) "abc".split("b")
Subtract(A, 2) A - 2
Tangent From Degrees tanDeg()
Tangent From Radians tan()
Team(Team 1) Team.1
Text Count getNumberOfTextIds()
Up Vector.UP
Value In Array(A, 3) A[3]
Vector vect()
Weapon player.getCurrentWeapon()
While(A == 2) while A == 2:
X Component Of(vector) vector.x
Y Component Of(vector) vector.y
Z Component Of(vector) vector.z

Custom game settings

OverPy supports including custom game settings, using the settings keyword:

settings {
    "main": {
        "description": "Some awesome game mode"
    },
    "gamemodes": {
        "skirmish": {
            "enabledMaps": [
                "workshopIsland"
            ]
        },
        "general": {
            "heroLimit": "off",
            "respawnTime%": 30
        }
    }
}

However, there are quite a lot of changes regarding the syntax, and it is recommended that you edit settings within Overwatch then use the decompile command to convert to OverPy.

Extensions are activated using the #!extensions compiler directive.

Control flow

If, Elif, Else

An if/elif/else statement is simply represented by the following structure:

if A == 1:
    B = 2
elif A == 2:
    B = 3
elif A == 3:
    B = 4
else:
    B = 5

While loop

A while loop is represented by the following structure:

while A == 1:
    B = 2
    wait()

It compiles to the workshop's While instruction.

For loop

A for loop is represented by the following structure:

globalvar i
rule "for loop":
    for i in range(1, 5, 2):
        B.append(i)
    #B = [1,3,5]

Note that the range(start, stop, step) function, that can only be used here, has other forms:

for i in range(1,5) -> for i in range(1,5,1)
for i in range(5) -> for i in range(0,5,1)

As such, you can use the range(start, stop) and range(stop) signatures.

Gotos

Although not recommended to use, gotos can be used in conjunction with a label. They are the equivalent of the Workshop's Skip instruction.

For example:

if A == 4:
   goto lbl_0
B = 5
lbl_0:
C = 6

Labels are declared on their own line, and must include a colon at the end (but no additional indentation).

Due to the limitations of the workshop, labels must be in the same rule as the goto instruction, and cannot be before it.

Additionally, dynamic gotos can be specified using the special keyword loc:

goto loc+A

This is however not recommended and can lead to very confusing code; you should consider using switches instead.

These statements are compiled to Skip or Skip If instructions.

OverPy guarantees a fixed amount of instructions:

  • Each logical line counts for one instruction, except for the switch statement which counts for 2 instructions.
  • Each unindent counts for one instruction.

This is true regardless of optimizations.

Strings

Strings are written with single or double quotes, using the backslash character to escape: "string \"with quotes\"\nand a newline".

To concatenate strings, the .format() function must be used with {} or {number} placeholders, such as "You have {} money and deal {} damage".format(eventPlayer.money, eventPlayer.damage).

The Workshop applies a limit of 128 characters and 3 placeholders per string. OverPy automatically splits a string if it goes beyond that limit; you can therefore write your strings with as many characters and as many placeholders as you want.

Successive strings are concatenated: "string1""string2" will resolve to "string1string2".

The escape sequence \n can be used to make a newline. Strings are also multiline by default (no need for triple quotes), therefore a literal newline within a string will be compiled as a \n.

The escape sequences \xHH (byte escape) and \uHHHH (unicode escape) are also supported, where H is an hexadecimal digit. For example, "\u3000" will output a fullwidth space.

String entities, using the escape sequence \&, can also be used to not have to look up unicode characters: "\&black_square;" is the same as "■".

The escape sequences \t, \r, and \z map respectively to a tab, a carriage return, and a zero-width space.

String modifiers

String modifiers are placed just before the string, such as w"fullwidth string". There are currently 5 string modifiers:

  • The l string modifier specifies a localized string (String function in the workshop). Localized strings are not recommended as they are limited to a specific subset of predefined strings. They are only included for legacy gamemodes.
  • The w string modifier makes the string fullwidth.
  • The b string modifier forces, when possible, the use of the "big letters" font (Blizzard Global font).
  • The c string modifier makes the string case-sensitive, using special latin characters that are not uppercased. It however shows diacritics above most letters, and only works with the latin alphabet.
  • The t string modifier specifies that the string should be included in translations (see translations.)

Tags

The #!setupTags directive can be used to use textures and colors in strings.

If this directive is used, OverPy will automatically generate the rule to obtain an unsanitized "<" character, and automatically replace it in strings if they are the start of a tag.

For color, use the <fgRRGGBBAA> tag, where RR/GG/BB are the hex color value, and AA is the hex transparency value (00 = transparent, FF = opaque).

Example: print('<fgFF0000FF>Red text</fg>')

For textures, use the <TX> standalone tag, with the texture id as seen in https://workshop.codes/wiki/articles/tx-reference-sheet.

Example: print('<TXC0000000002DD21>') will display the mouse cursor texture.

Additionally, you can use the Texture enum and OverPy will automatically optimize it. For example: "Mouse cursor texture: {}".format(Texture.MOUSE_CURSOR)

OverPy will also replace '<tx1234>' to the correct full texture id, but only if the entire tag is inside a string ('<tx{}>'.format(id) will not work, but '<tx{}>'.format(1234) will).

Compiler options

Compiler options begin with the #! operator, not indented.

#!include

Inserts the text of the specified file. The file path can be relative; if so, it is relative to the main file. For example:

#!include "heroes/zenyatta.opy"

/* includes a file in the parent directory (".." is the parent directory) */
#!include "../main.opy"

/* will include all files in the "heroes" directory */
#!include "heroes/"

#!mainFile

Specifies an .opy file as the main file (implying the current file is a module). This directive MUST be placed at the very beginning of the file. For example:

#!mainFile "../main.opy"

This is so that you can compile your gamemode from any file and OverPy knows the starting point to use.

#!suppressWarnings

Suppresses the specified warnings globally across the program. Warnings must be separated by a space. Example:

#!suppressWarnings w_type_check w_unsuitable_event

Optimizations

By default, OverPy automatically optimizes gamemodes for speed. This means useless code is converted, calculations are done when possible, and function patterns are replaced with builtin functions.

For example:

  • A = A * 1 + 0 will not be included, as multiplying by 1 and adding 0 does nothing, and it is an assignment to itself.
  • A = 10*10+20 will compile to A = 120.
  • not eventPlayer.isDead() will compile to eventPlayer.isAlive().

You can check here for a list of optimizations.

The #!disableOptimizations directive can be used to disable all optimizations done by the compiler. Should be only used for debugging, if you suspect that OverPy has bugs in its optimizations.

The #!optimizeForSize directive prioritizes lowering the number of elements over optimizing the runtime (see here for a list of optimizations).

Replacements

Several compiler options are available to automatically replace some constants, in order to save elements:

#!replaceTeam1ByControlScoringTeam
#!replace0ByCapturePercentage
#!replace0ByPayloadProgressPercentage
#!replace0ByIsMatchComplete
#!replace1ByMatchRound

These replacements should only be used if the requirements for the replacement are met. They are detailed in the autocomplete description of each compiler option.

Size optimizations must be enabled for these replacements to work (with #!optimizeForSize).

Preprocessing

Preprocessing is an important part of OverPy and allows extending its power way beyond the limitations of the workshop.

Normal macros

Normal macros are declared with the #!define directive:

#!define TEAM_HUMANS 1
#!define TEAM_ZOMBIES 2

This is primarly useful for declaring constants.

The #!defineMember directive behaves exactly the same as #!define, except the VS Code extension will put the autocompletion in the dot trigger. This is primarly useful for vectors: if you have a vector that stores 3 distinct numbers, you can do #!defineMember someVar x to do vector.someVar instead of vector.x.

Function macros

Function macros are declared with the #!define directive, but have parameters for the macro:

#!define setUsefulVars(x) hasFirstInfectionPassed = x\
currentSection = x\
firstInfectionLoopIndex = x\
countdownProgress = x\
roundWinners = x

Note the usage of the backslashed lines to make a multi-line macro.

Warning: Always wrap macros and macro arguments with parentheses when possible, as macros are a literal replacement!

For example, given this function:

#!define sum(a,b) a+b

then sum(3, 4) * 3 will be resolved as 3 + 4 * 3 and sum(3, 4 if A else 3) will be resolved as 3 + 4 if A else 3.

This macro should be declared instead as #!define sum(a,b) ((a)+(b)): parentheses around the whole macro definition, and around each argument.

Javascript macros

You can do script macros with the special __script__ function. For example:

#!define addFive(x) __script__("addfive.js")

The content of addfive.js is simply x+5 (no return!)

For the technical details:

  • Arguments are automatically inserted into the script (in this case, var x = 123; would be inserted at the top of the script)
  • A vect() function is automatically inserted, so that vect(1,2,3) returns an object with the correct x, y, and z properties and toString() function
  • The script is then evaluated using a JavaScript interpreter

Advanced constructs

Switches

Switches are a good way to transform an if/elif/else chain into something more optimized:

switch A:
    case Hero.ANA:
        B = 2
        break
    case Hero.ASHE:
        B = 3
        break
    default:
        B = 4

This is equivalent to:

if A == Hero.ANA:
    B = 2
elif A == Hero.ASHE:
    B = 3
else:
    B = 4

However, the switch statement has the advantage of not recalculating A.

Keep in mind that fallthrough is enabled, so if you don't have a break instruction, the execution continues to the next case statement.

Dictionaries

In the above case, we always set the same variable. Therefore, a dictionary can be used:

B = {
    0: 4,
    Hero.ANA: 2,
    Hero.ASHE: 3,
}[A]

If the key is not found in the dictionary, null will be returned. However, you can specify a default key for a default value.

This dictionary is internally converted to:

B = [4, 2, 3][[0, Hero.ANA, Hero.ASHE].index(A)]

Therefore, a dictionary cannot be declared alone; it must always be accessed.

Enums

Enums can be declared to avoid manually declaring several macros:

enum GameStatus:
    GAME_NOT_STARTED = 1
    GAME_IN_PROGRESS
    GAME_FINISHED

enum Team:
    HUMANS = Team.2
    ZOMBIES = Team.1

rule "Kill zombies when game has finished":
    @Condition gameStatus == GameStatus.GAME_FINISHED
    kill(getPlayers(Team.ZOMBIES), null)

If an enum member is not given a value, it will take the previous value plus 1 (or 0 if it is the first value). You can also extend existing enums, such as the Team enum in this example.

You can use len(GameStatus) to have the amount of values in the enum and GameStatus.toArray() to get an array of the values. This is useful to iterate on the values.

Built-in macros

OverPy includes some built-in macros for commonly used patterns.

Lowercase

The createCasedProgressBarIwt function can be used to overlay multiple texts and display pixel-perfect lowercase text. It can however only be used with the Blizzard Global font.

spacesForString

Returns a string made of spaces that is the same length as the provided string. The provided string must be a literal string.

NOTE: The displayed string MUST be in the Blizzard Global font (use the 'b' string modifier on the final string, unless using a progress bar). The casing of the string is also respected.

The spacesForLength and strVisualLength macros can also be used. spacesForString is the combination of these two.

This is useful to do alignment tricks. For example, the following code displays a key:value attribute list:

#We use strVisualLength("M")*20 to specify a string that is longer than the longest key/value.
#!define dictLine(key, value) "{}{}{}{}".format(spacesForLength(strVisualLength("M")*20 - strVisualLength(key)), (key), (value), spacesForLength(strVisualLength("M")*20 - strVisualLength(value)))
rule "iwt":
    createProgressBarInWorldText(text=b" \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n{}\n{}\n{}".format(
        #If not using createCasedProgressBarIwt then you have to put the strings in uppercase
        dictLine("ABILITY 1: ", "KILLAURA"),
        dictLine("ABILITY 2: ", "WALLHACK"),
        dictLine("ULTIMATE: ", "CRASH THE SERVER")
    ), position=vect(0,0,0), scale=2)

ruleCondition

The ruleCondition value compiles to all the conditions of the rule, joined with and.

This is mostly useful in waitUntil, so you can do the following:

rule "":
    @Event eachPlayer
    @Condition ...
    @Condition ...
    @Condition ...
    eventPlayer.startHealingOverTime(null, Math.INFINITY, 30)
    eventPlayer.hotId = getLastHealingOverTimeId()
    waitUntil(not ruleCondition)
    #cleanup
    stopHealingOverTime(eventPlayer.hotId)

String compression

You can use the compressed() function to store a large array of numbers or vectors.

For example:

mapData = compressed([vect(-65.048, -18.007, -80.036), vect(0.92, 12.58, -18.32), vect(194.041, 6.128, -74.097), ...])

OverPy will store the array in a string and automatically decompress it, which takes much fewer elements.

For more control over the compression (eg if you have separate arrays to compress), you can use the compress and decompressNumbers/decompressVectors functions.

splitDictArray

Maps an array of dictionaries to variables. For example:

splitDictArray({
    hero: waveHeroes,
    length: waveLengths
}, [
    {hero: Hero.ANA, length: 3},
    {length: 8, hero: Hero.SOLDIER},
    {hero: Hero.HAMMOND}
])

Will yield the following:

waveHeroes = [Hero.ANA, Hero.SOLDIER, Hero.HAMMOND]
waveLengths = [3, 8, null]

If the third argument is specified and set to true, string compression is automatically used for number and vector arrays.

2d/3d array assignment

The Workshop only allows you to directly assign to a 1d array, but thanks to Mira, assignments to 2d/3d arrays are supported:

  • array[i][j] = value will be compiled to array[i] = array[i].slice(0, j).concat([value]).concat(array[i].slice(j+1))
  • array[i][j][k] = value will be compiled to array[i] = array[i].slice(0, j).concat([array[i][j].slice(0, k).concat([value]).concat(array[i][j].slice(k+1))]).concat(array[i].slice(j+1))

Constants

The Math enum contains useful constants, such as Math.INFINITY and Math.FUCKTON_OF_NEWLINES. See the autocompletion for more info.

Other functions

Function Description
Player.getOppositeTeam() Returns the opposite team of a player.
Player.getEffectiveHero() Returns the hero the player is duplicating (if using Echo ultimate) or the normal hero of a player.
getSign() Returns the sign of a number (-1, 0 or 1).
hudHeader(), hudSubheader(), hudSubtext() Built-in macros for hudText() to reduce the number of arguments. You can specify HudPosition.ACTUALLY_LEFT to fix the bug where HUDs are centered relative to each other, but you have to specify it on each of the left HUDs.
lineIntersectsSphere() Determines if a line intersects a sphere. Can be used to check if a player is looking at a specific point.
print() Creates an orange HUD text for quick debugging of a value.
debug() You will likely want to use this rather than print(). This function displays the value and also displays the arguments passed to it, so debug(A+2) will display A + 2 = 4 instead of just 4.
Array.reverse() Reverses the array.
Array.unique() Removes duplicates from the array.
timeToString() Converts a time (in seconds) to a H:MM:SS string. For example, timeToString(3600+120+37.65) will return 1:02:37.65.
arrayToString() Converts an array to a string (otherwise, only the first element is displayed).
buttonToString() A better version of Input Binding String() which automatically adds brackets (but not if the button is a texture) and replaces LSHIFT/LCONTROL/LALT by SHIFT/CTRL/ALT.
hsl() Specifies a color in HSL format.
log() Calculates the natural logarithm of a given number.

Feel free to open an issue if you'd like an additional macro to be added.

Translations

Use the #!translations directive to setup the translation system. Arguments are the language codes separated by spaces. For example:

#!translations en fr es zh_cn

Only the es_mx, es_es, zh_cn and zh_tw languages can be specified fully. For the rest, you can only specify the first two letters.

To translate a string, wrap it with the _ function, such as _("You have \${} money").format(money). Note that the formatter has to be outside of the function. You can also use the "t" string modifier, such as t"\${} money".format(money).

If two strings are the same but have to be translated differently, you can add a context string as the first argument, such as _("the direction", "left").

Lastly, if a translated string is stored in a variable, you have to use the _ function when displaying it, such as hudHeader(text=_(someVariable)). Else, "TLErr" will be displayed. Note that you also have to use the _ function when storing the string in the variable, else "0" will be displayed.

OverPy will generate and parse .po files for each language based on the name of the main file. You can then use an online editor to edit those files, such as https://pofile.net/free-po-editor or https://localise.biz/free/poeditor. Leading and trailing whitespace is automatically stripped from the string when put into translation files.

WARNING: A translated string cannot be used as a normal string when stored in a variable, as it becomes a string array. This means you cannot use .replace(), .charAt(), etc. When translating your gamemode, look out for these functions.

This also means that, when used in a variable, you cannot use a translated string as an argument of a string: "{}{}".format(t"string", 1234) will not work. Instead, do t"string{}".format(1234). The translated string must always be top-level. You will also get "TLErr" if trying to use a translated string as an argument for another function.

Note: The way string formatting works is via the .replace() function and some constants. This means you cannot have the following in your translated strings if using formatters:

  • (0.00, 1.00, 0.00) (Vector.UP)
  • (0.00, -1.00, 0.00) (Vector.DOWN)
  • (1.00, 0.00, 0.00) (Vector.LEFT)
  • (-1.00, 0.00, 0.00) (Vector.RIGHT)
  • (0.00, 0.00, 1.00) (Vector.FORWARD)
  • (0.00, 0.00, -1.00) (Vector.BACKWARD)
  • 1876650.25
  • 1876651.25
  • 1876652.25
  • 1876653.25
  • 1876654.25
  • 1876655.25
  • 1876656.25
  • 1876657.25
  • 1876658.25
  • 1876659.25

Last, you can use the #!translateWithPlayerVar directive to store the player's language in a variable and save on elements, but it is potentially invasive (although it should work with the vast majority of gamemodes).

In summary:

  • Use the #!translations directive to setup translations, and #!translateWithPlayerVar if you are short on elements.
  • If a string is within a display action (bigMessage(), hudText(), createInWorldText(), etc), simply prefix it with t or wrap it with the _ function, and it will work.
  • Otherwise, if the string is assigned to a variable, you have to wrap the variable with the _ function when displaying it, as well as check if the string is not used as a string formatter or that string operations aren't applied on it. If there are, you need to refactor your code.
Technical details

The way translations work is by casting a value to string (usually Color.WHITE) and checking what is returned. The cast to string is done with the player's language, so it will return "White" if on English, "Blanc" if on French, etc.

However, this cast to string must be done client-side. You cannot do the following:

# Will not work properly, do not use!
eventPlayer.language = ["White", "Blanc"].index("{}".format(Color.WHITE))

This will appear to work, but this is evaluated server-side, and what it actually does is set the language to the language of the host (more precisely the language of whoever last imported or edited the gamemode). This means everyone will play as one single language, which defeats the whole point of translations.

When in a reevaluated HUD text, t"Some string" resolves to:

["Some string", "Une chaîne"][["White", "Blanc"].index("{}").format(Color.WHITE)]

This works as reevaluated text is evaluated client-side. To check if something is evaluated client-side, check if inputBindingString isn't 0 or localPlayer isn't 0.

When in a variable, text = t"Some string" just resolves to text = ["Some string", "Une chaîne"]. We cannot access the array now as we do not know the player's language, and so we need to access it in the HUD text. So _(text) will then resolve to text[["White", "Blanc"].index("{}").format(Color.WHITE)].

Since we are dealing with the array form of the translated string between declaration and display, we cannot perform any operations on it like we would on a normal string.

There is however a way of transferring data from client to server. Most reevaluated actions are evaluated client-side, and this includes .startFacing(). By setting the player's facing direction to a specific angle based on the language, then getting the angle of the player's facing direction, we can know the language index. This is what #!translateWithPlayerVar does, and this avoids repeating the language resolution formula at each translated string.

Note: I used arrays in the example. OverPy concatenates arrays to strings using .split() and does various other tricks to save elements, but the principle is the same.


If you are still confused about something, or want to discuss a feature, please join the discord #hll-scripting :)

About

High-level language for the Overwatch Workshop with support for compilation and decompilation.

Resources

License

Stars

Watchers

Forks

Packages

No packages published