The VG Language™ - Electry/VitaGrafix GitHub Wiki
To facilitate user-made game patches without having to recompile the whole plugin, a domain specific language was created. Similar, but somewhat simplified to that of VitaCheat.
Written in patchlist.txt, patches are parsed and applied during game boot-up.
A patch for each game starts with a header. Header identifies SKU/region (TITLE ID), the executable name (because several “collections” use the same TITLE ID for multiple games in their series) and the game version (indirectly, in a form of module NID).
[PCSA00001, eboot.bin, 0x12345678]
| ↑ | ||
|---|---|---|
| PCSA00001 | game's TITLE ID | required |
| eboot.bin | a part of executable's full path | recommended |
| 0x12345678 | main module NID, 8 hex digits with '0x' prefix | recommended |
Executable name/path is used to differentiate between games that share a single TITLE ID (e.g. collections).
NID is used to inform user about potentional version mismatch (e.g. when game gets a new update), instead of applying patches to wrong/old memory addresses and then crashing.
If multiple regions/SKUs share the same offsets, having to duplicate patches would be wasteful. You can freely stack multiple headers after each other, like shown below. Patches will then be applied for all matching games/regions.
[PCSA00001, eboot.bin, 0x12345678]
[PCSB00023, eboot.bin, 0x87654321]
...
Header is followed by patch feature/type marking. This indicates that subsequent patches shall only be applied if user has enabled said feature in their configuration file.
Use one of the following:
@FB
@IB
@FPS
@MSAA
The rules for patches are simple. Each patched memory location is on a separate line, starts with a relative address (segment:offset) followed by the new value.
0:0x1234 0xDEADBEEF
results in 4 bytes at seg000 start + 0x1234 address being patched to EF BE AD DE.
VitaGrafix internally always works with one of the following datatypes:
- Primitive types:
- int (32-bit signed integer)
- uint (32-bit unsigned integer)
- float (32-bit floating point number)
- Raw type:
- bytes (size up to 32 B)
You can cast between those types freely using casting functions (see Explicit type conversion).
-
int – when
+or-sign precedes 1 or more decimal digits (with no space inbetween)-
-
+1,-1 -
-
+ 1
-
-
uint – when:
- hex number is prefixed with
0xor0X - octal number is prefixed with
0 - decimal integer does not have prefix or suffix
-
-
0xCAFE,0876,123 -
-
CAFE
- hex number is prefixed with
-
float – when 1 or more decimal digits (optionally with preceding
+or-sign) are followed by:-
.and optionally:- 1 or more digits after the decimal point and optionally
f f
- 1 or more digits after the decimal point and optionally
f-
-
1.,1.2,1.2f,1f,1.f,-1.0,-2f,+123.4f -
-
.1,.1f,1 . 0 f
-
-
bytes – when 1 or more (up to 32) 8-bit hex values (without the 0x prefix) are followed by
r-
-
DEADr,DE AD r -
-
DEAD,0xDEADr,0xDE 0xAD r
-
The interpreter makes use of implicit conversion when necessary. This is done when applying infix operator or math function (arity >= 2) to following operand types (in no particular order):
| operand 1 | operand 2, 3, ... | result | example | |
|---|---|---|---|---|
| float | int or uint | all casted to float | float |
100 / 2.0 equals 50.0
|
| int | uint | all casted to int | int |
100 / -2 equals -50
|
| uint | uint | ONLY if result is < 0 | int |
1 - 2 equals -1
|
| any | any | ONLY when applying . (concat) |
bytes |
1 . 2 equals 0100000002000000r
|
While working with values, changing the datatype manually might sometimes be desirable.
To convert value of any type to one of the primitive types use following functions:
| function | argument type | result type | example |
|---|---|---|---|
| int(value) | any | int |
int(-2.0) equals -2
|
| uint(value) | any | uint |
uint(FFr) equals 255
|
| float(value) | any | float |
float(2) equals 2.0
|
To convert value of any type to raw type use:
| function | argument type | result type | result size (B) | example |
|---|---|---|---|---|
| int8(value) | any | bytes | 1 |
int8(-128) equals 80 r
|
| int16(value) | any | bytes | 2 |
int16(-32768) equals 00 80 r
|
| int32(value) | any | bytes | 4 |
int32(-2147483648) equals 00 00 00 80 r
|
| uint8(value) | any | bytes | 1 |
uint8(255) equals FF r
|
| uint16(value) | any | bytes | 2 |
uint16(65535) equals FF FF r
|
| uint32(value) | any | bytes | 4 |
uint32(4294967295) equals FF FF FF FF r
|
| fl32(value) | any | bytes | 4 |
fl32(12345.0) equals 00 E4 40 46 r
|
If you don't wish to cast the value, but rather just reinterpret bytes, use:
| function | value type | result type | result size (B) | example |
|---|---|---|---|---|
| raw(value) | any | bytes | sizeof(value) |
raw(1.0 >> 16) equals 80 3F 00 00 r
|
| rawn(value, n) | any | bytes | n |
rawn(1.0 >> 16, 2) equals 80 3F r
|
Operator precedence is honored, order is similar to C-like languages. Brackets (parentheses) are allowed and evaluated first.
| operator | operand type | description | example |
|---|---|---|---|
| + | primitive | addition |
1 + 2 equals 3
|
| - | primitive | subtraction |
1 - 2 equals -1
|
| * | primitive | multiplication |
2 * 3 equals 6
|
| / | primitive | division |
3 / 2.0 equals 1.5
|
| % | int or uint | modulo (division remainder) |
123 % 23 equals 8
|
| | | primitive | bitwise OR |
14 | 5 equals 15
|
| ^ | primitive | bitwise XOR |
14 ^ 5 equals 11
|
| & | primitive | bitwise AND |
14 & 5 equals 4
|
| << | primitive | bitwise left shift |
14 >> 2 equals 3
|
| >> | primitive | bitwise right shift |
14 << 2 equals 56
|
| . | raw | concatenation |
DEr . ADr equals DE AD r
|
| * | primitive * raw | raw repeat |
nop * 2 equals 00 BF 00 BF r
|
| function | description | example |
|---|---|---|
| abs(value) | absolute value |
abs(-1.0) equals 1.0
|
| acos(rad) | arc cosine (inverse cosine) in radians |
acos(-0.5) equals 2.094395
|
| align(value, al) | aligns value to a multiple of al |
align(720, 32) equals 736
|
| asin(rad) | arc sine (inverse sine) in radians |
asin(0.5) equals 0.523599
|
| atan(rad) | arc tangent (inverse tangent) in radians |
atan(1.0) equals 0.785398
|
| atan2(y, x) | arc tangent of y/x in radians |
atan2(0.5, 0.4) equals 0.896055
|
| ceil(value) | nearest integer greater than value |
ceil(1.23) equals 2.0
|
| cos(rad) | cosine in radians |
cos(0.5) equals 0.877583
|
| cosh(rad) | hyperbolic cosine in radians |
cosh(0.5) equals 1.127626
|
| exp(value) | exponential (Euler's number) raised to value |
exp(2.0) equals 7.389056
|
| floor(value) | nearest integer lesser than value |
floor(1.23) equals 1.0
|
| ln(value) | natural logarithm of value |
ln(10.0) equals 2.302585
|
| log10(value) | base 10 logarithm of value |
log10(10.0) equals 1.0
|
| min(a, b) | the smaller of a and b |
min(123, 23) equals 23
|
| max(a, b) | the greater of a and b |
max(123, 23) equals 123
|
| pow(base, power) | base raised to the power |
pow(2, 4) equals 16
|
| round(value) | nearest integer to value (0.5 rounds up) |
round(1.23) equals 1.0
|
| sin(rad) | sine in radians |
sin(0.5) equals 0.479426
|
| sinh(rad) | hyperbolic sine in radians |
sinh(0.5) equals 0.521095
|
| sqrt(value) | square root of value |
sqrt(10.0) equals 3.162278
|
| tan(rad) | tangent in radians |
tan(0.5) equals 0.546302
|
| tanh(rad) | hyperbolic tangent in radians |
tanh(0.5) equals 0.462117
|
| constant | description | defined as |
|---|---|---|
| pi | The number π | 3.141593 |
| e | Euler's number | 2.718282 |
These return values based on user configuration entry (in config.txt) for current game.
| macro | description | config option |
|---|---|---|
| fb_w | Framebuffer width in pixels | FB |
| fb_h | Framebuffer height in pixels | FB |
| ib_w | Internal buf. width in pixels | IB |
| ib_h | Internal buf. height in pixels | IB |
| ib_wi(index) | Internal buf. width in pixels (indexed from 0) | IB (multiple) |
| ib_hi(index) | Internal buf. height in pixels (indexed from 0) | IB (multiple) |
| vblank | Vertical blanking interval (1 for 60Hz, 2 for 30, etc...) | FPS |
| msaa | SceGxmMultisampleMode (0 for no MSAA, 1 for 2x, 2 for 4x) | MSAA |
These encode few of the commonly used instructions. Result type is always raw, size depends on encoder. All are unconditional.
Register arguments are expected to be decimal integers.
ARM within 0 <= reg <= 14 range, where 0 denotes R0 or A1, 13 = SP and 14 = LR.
VFP within 0 <= reg <= 31 range, where 0 denotes S0.
| encoder | description | instr. set (enc.) |
size (B) |
|---|---|---|---|
| t1_mov(reg, imm) | Insert an immediate value to a register. MOV <Rd>,#<imm8> reg - destination imm - immediate value (up to 8-bit) |
Thumb (T1) |
2 |
| t2_mov(sf, reg, imm) | Insert an immediate value to a register. MOV{S}.W <Rd>,#<const> sf - set condition flags (0 = MOV.W, 1 = MOVS.W) reg - destination imm - immediate value (constant) |
Thumb-2 (T2) |
4 |
| t3_mov(reg, imm) | Insert an immediate value to a register. MOVW <Rd>,#<imm16> reg - destination imm - immediate value (up to 16-bit) |
Thumb-2 (T3) |
4 |
| t1_movt(reg, imm) | Insert 16-bit immediate value to the top halfword of a register. Bottom halfword is not affected. MOVT <Rd>,#<imm16> reg - destination imm - immediate value (up to 16-bit) |
Thumb-2 (T1) |
4 |
| mov32(reg, imm, gap) | Pseduo-instruction, generates MOVW, MOVT pair, allows you to load any 32-bit value into a register. MOVW <Rd>,#<imm16> ... MOVT <Rd>,#<imm16> reg - destination imm - immediate value (up to 32-bit) gap - gap size (in bytes) between MOVW and MOVT (max. 24 B) |
Thumb-2 (T3 + T1) |
4 + gap + 4 |
| a1_mov(sf, reg, imm) | Insert an immediate value to a register. MOV{S} <Rd>,#<const> sf - set condition flags (0 = MOV, 1 = MOVS) reg - destination imm - immediate value (constant) |
Arm (A1) |
4 |
| a2_mov(reg, imm) | Insert an immediate value to a register. MOVW <Rd>,#<imm16> reg - destination imm - immediate value (up to 16-bit) |
Arm (A2) |
4 |
| t2_vmov(reg, imm) | Insert a floating-point immediate value in a single-precision register. VMOV.F32 <Sd>, #<imm> reg - destination (VFP) imm - float immediate (+/-n * 2^-r, 16 <= n <= 31, 0 <= r <= 7) |
VFPv3 (T2/A2) |
4 |
| bkpt | Breakpoint (enter debug state). BKPT #0
|
Thumb (T1) |
2 |
| nop | No operation. NOP
|
Thumb-2 (T1) |
2 |
Comments always start with # character. They can, but don't need to be on a separate line.
# This is a comment
[PCSA00001, eboot.bin, 0x12345678] # This is also a valid comment
...
All older patches are compatible with v5.x, however, support for the old syntax/style might be dropped in future versions. It is highly advised to use the new syntax for new patches.
v4.x: nop() and bkpt() encoders must have brackets
v5.x: nop and bkpt encoders are classified as constants, constants do not have brackets
example:
-
nop()=>nop
v4.x: no support for math operators/fns/precedence, macros start with < and end with >
v5.x: infix operators and math functions are available
example:
-
<+,1,2>=>1 + 2 -
<*,<&,<+,<fb_w>,31>,0xFFFFFFE0>,4>=>align(fb_w, 32) * 4
v4.x: config-option macros start with < and end with >
v5.x: config-option macros are used just like regular constants and functions
example:
-
<fb_w>=>fb_w -
<ib_w>=>ib_w -
<ib_w,2>=>ib_wi(2)
v4.x: no extra spaces are allowed, comments have to be on a separate line
v5.x: spaces are ignored, anything after # too (until EOL)
example:
-
t2_mov(1,1,<fb_w>)=>t2_mov(1, 1, fb_w) -
# Comment0:0x1234 nopis valid just like0:0x1234 nop # Comment
v4.x: bytes() is used to parse raw bytes
v5.x: r suffix is used to parse raw bytes, raw() is used for reinterpreting/conversion
example:
-
bytes(DE AD BE EF)=>DEADBEEF r
and more...
Here are some examples of valid patches.
[PCSF00243,eboot.bin] # Killzone Mercenary [EU 1.12]
[PCSF00403,eboot.bin] # Killzone Mercenary [EU 1.12]
[PCSA00107,eboot.bin,0x0F9D3B7C] # Killzone Mercenary [US 1.12]
[PCSC00045,eboot.bin] # Killzone Mercenary [JP 1.12]
[PCSD00071,eboot.bin] # Killzone Mercenary [ASIA 1.12]
@IB
0:0x15A5C8 nop *4
1:0xD728 uint32(ib_w) . uint32(ib_h)
1:0xD730 uint32(ib_w) . uint32(ib_h)
# God of War Collection [EU 1.00]
[PCSF00438,GOW1.self,0x8638ffed]
@FB
0:0x9E212 t2_mov(1, 4, fb_w)
0:0x9E21A t2_mov(1, 2, fb_h)
0:0x9F0F0 t2_mov(1, 0, fb_w)
0:0x9F0F8 t2_mov(1, 1, fb_h)
0:0xA31C6 t2_mov(1, 7, fb_w)
0:0xA31CC t2_mov(1, 1, fb_h)
0:0xCEF06 t2_mov(1, 0, fb_w)
0:0xCEF0E t2_mov(1, 2, fb_h)
0:0xA1098 t2_mov(1, 14, fb_h)
@FPS
0:0x9E228 t1_mov(0, vblank)
# LEGO Star Wars: The Force Awakens [EU 1.00]
[PCSB00877,eboot.bin]
@IB
0:0x2241C4 t2_mov(1, 1, 0xA00000)
0:0x1F313E t2_mov(1, 4, ib_w)
0:0x1F3144 t2_mov(1, 5, ib_h)
1:0x4650 uint32(ib_w) . uint32(ib_h)
# Fix touchscreen/touchpad pos calc
0:0x223AF6 t2_mov(0, 0, 640) . nop *3
0:0x223B36 t2_mov(0, 0, 368) . nop *3
The parser will try to warn you if you make a mistake. Nevertheless, it isn't perfect so you shouldn't rely on it.
-
- 