SDK2 Register & Lifecycle #
Recommended pattern for character scripts. Uses
_SDK.sdk2+Register:submit().
Minimal script #
if isNotCharacter(7) then return end -- Bryan only
local sdk2 = require("_SDK.sdk2")
local enums = sdk2.enums
local utils = sdk2.utils
local script = sdk2.Register:New("My Script", "\xef\x93\xbe")
local config = { enabled = true }
script:onTick(function(self, enemy)
if not utils.CanRun(self) then return end
local fa = getLocalFrameAdvantage()
if isCounterAttackPossible(fa) then
print("Counter at FA", fa)
end
end)
script:onMenu(function()
config.enabled = sdk2.Menu.checkbox("Enable", config.enabled)
end)
script:submit({ config = config })
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Standard character script #
Typical layout used by shipped character scripts (character guard → sdk2 → i18n → Register → callbacks → submit).
-- Wrong character → exit immediately
if isNotCharacter(14) then return end -- Lili = 14
local sdk2 = require("_SDK.sdk2")
local enums = sdk2.enums
local utils = sdk2.utils
local menu = sdk2.Menu
-- Optional: i18n + menu layout (same folder as script)
local _src = debug.getinfo(1, "S").source
local SCRIPT_DIR = (_src:sub(1, 1) == "@" and _src:sub(2) or _src):match("(.*[\\/])") or ""
local i18n = dofile(SCRIPT_DIR .. "lili_i18n.lua")
i18n.init(getLocaleLanguage)
local L, Lf = i18n.L, i18n.Lf
local layout = require("_SDK.menu_layout")
local myScript = sdk2.Register:New("Tekken LILI", "\xef\x93\x9a")
local outModule = {}
local config = {
useAutomaticCounterattack = true,
CounterattackChance = 100,
}
myScript:onTick(function(self, enemy)
-- see "onTick combat logic" below
end)
myScript:onMessage(function(msg, wParam, lParam)
-- msg 256 = key down, 257 = key up (optional)
end)
myScript:onMenu(function()
layout.header(menu, Lf("title_demo", "LILI"), L("help_demo"))
layout.section(menu, L("section_counterattack"), L("help_counterattack"))
layout.counterattack(menu, i18n, config)
layout.footer(menu, L("desc1"), L("desc2"))
end)
if not myScript:submit(outModule) then
return myScript:getModule(myScript.name)
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Register #
| Method | Description |
|---|---|
Register:New(name, icon?) | Create script instance; auto-detects script folder path |
:onTick(fn(self, enemy)) | Combat logic each frame; self/enemy = CharacterState snapshot |
:onMenu(fn) | ImGui settings panel (auto Menu.setCacheName per script) |
:onDraw(fn) | Overlay drawing |
:onMessage(fn(msg, wParam, lParam)) | Windows message hook on worker thread |
:submit(outModule?) | Register with engine; returns success |
:getModule(scriptName) | Get exported table when script already loaded (hot reload) |
Submit fallback when the script name is already registered:
local outModule = {}
if not myScript:submit(outModule) then
return myScript:getModule(myScript.name)
end
2
3
4
Callbacks #
| Method | Engine event | When |
|---|---|---|
onTick(fn) | tick | Every combat frame |
onMenu(fn) | menu | ImGui settings panel |
onDraw(fn) | draw | Overlay draw (non-blocking) |
onMessage(fn) | message | Async worker / Windows messages |
submit(outModule) | — | Register with engine |
onTick combat logic #
Every combat frame the engine calls your tick with local and opponent snapshot tables (same fields as getCharacterState(0).self / .enemy).
Recommended guard chain #
| Step | API / field | Purpose |
|---|---|---|
| 1 | utils.CanRun(self) | Skip when juggled, sidroll, or in startup |
| 2 | self.Distance | Range gate (e.g. < 2000) |
| 3 | getLocalFrameAdvantage() + isCounterAttackPossible(fa) | Frame-advantage counter window |
| 4 | config + getRandomInt(0, 100) | Optional random trigger rate |
| 5 | getOpponentLastAttackType() / utils.GetEnemyAttackTypeNotZero() | Opponent attack height |
| 6 | DisableKeyboardInput() → executeMacro(...) → EnableKeyboardInput() | Inject combo safely |
Common snapshot fields #
| Field | Type | Use |
|---|---|---|
self.Distance / enemy.Distance | number | Spacing |
self.MoveTimer / enemy.MoveTimer | integer | Current move frame counter |
self.EndFrame / enemy.EndFrame | integer | Move end frame; enemy.EndFrame - enemy.MoveTimer = remaining |
self.AttackType / enemy.AttackType | integer | Same as getLocalAttackType() / getters |
self.MoveID / enemy.MoveID | integer | Current move ID |
self.FrameAdvantage | integer | Prefer getLocalFrameAdvantage() in tick |
Counterattack example #
myScript:onTick(function(self, enemy)
if not utils.CanRun(self) then return end
if self.Distance >= 2000 then return end
local fa = getLocalFrameAdvantage()
if not isCounterAttackPossible(fa) then return end
-- Config + random rate
local roll = getRandomInt(0, 100)
if not config.useAutomaticCounterattack or roll > config.CounterattackChance then
return
end
local atk = getOpponentLastAttackType()
if atk == 0 then
atk = utils.GetEnemyAttackTypeNotZero()
end
-- Debug: print(utils.getAttackTypeString(atk), fa)
if atk == enums.AttackType.MID and fa >= 17 then
DisableKeyboardInput()
executeMacro(enums.Input.FORWARD, enums.Input.R_HAND, 5, enums.InputType.KEY_PRESS)
EnableKeyboardInput()
return
end
if atk == enums.AttackType.LOW and fa >= 14 then
DisableKeyboardInput()
executeMacro(0, enums.Input.R_HAND, 5, enums.InputType.KEY_PRESS)
EnableKeyboardInput()
return
end
end)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
executeMacro tips #
- Direction and button args are bitmasks — combine with
+(e.g.FORWARD + DOWN,L_KICK + R_KICK). - Pass
0for direction when no movement needed. InputType.KEY_DOWN/KEY_PRESS/KEY_UP— see Enums.- Always pair
DisableKeyboardInput()/EnableKeyboardInput()around macros to avoid fighting player input.
Related utils #
| Function | When to use |
|---|---|
utils.CanCounter(frames, ...) | Extra counter checks (power crush, blocking, airborne) |
utils.GetEnemyAttackTypeNotZero() | When getOpponentLastAttackType() returns 0 |
utils.getAttackTypeString(type) | Debug label for attack type |
utils.isLocalBlocking() | Skip logic while blocking |
utils.isOpponentAttackWhiffing() | Opponent whiff punish |
onMessage #
Optional Windows message hook on the async worker thread. Useful for global hotkeys outside combat tick.
| Parameter | Meaning |
|---|---|
msg | Windows message ID (256 = key down, 257 = key up) |
wParam | Key / event parameter |
lParam | Extended event data |
myScript:onMessage(function(msg, wParam, lParam)
if msg == 256 then
-- key down
elseif msg == 257 then
-- key up
end
end)
2
3
4
5
6
7
menu_layout (_SDK.menu_layout) #
Shared ImGui section helpers built on sdk2.Menu. Used with a per-script i18n module.
| Function | Description |
|---|---|
layout.header(menu, title, help?) | Title + optional help tooltip + separator |
layout.section(menu, title, help?) | Section heading + inline help |
layout.subsection(menu) | Light separator between sub-blocks |
layout.footer(menu, ...) | Footer description lines |
layout.counterattack(menu, i18n, config) | Standard auto-counter toggles (useAutomaticCounterattack, CounterattackChance) |
layout.counterattack expects config keys and an i18n table exposing checkbox / sliderInt wrappers (see shipped *_i18n.lua).
sdk2.Menu #
ImGui widgets with JSON persistence per script. Widgets call
Menu.beforeConfigautomatically whenconfigKeyis set.
| Method | Description |
|---|---|
Menu.setCacheName(name, path) | Set config file name and load JSON |
Menu.saveConfig() | Write current config to disk |
Menu.migrateConfigKey(key, legacyKeys) | Migrate old config keys |
Menu.checkbox(name, value, configKey?, legacyKeys?) | Checkbox; returns bool |
Menu.sliderFloat(name, value, min, max, ...) | Float slider |
Menu.sliderInt(name, value, min, max, ...) | Int slider |
Menu.combo(name, index, items, ...) | Dropdown |
Menu.colorEdit(name, rgba, ...) | RGBA color picker |
Menu.keySelect(name, keycode, ...) | Key binding picker |
Menu.text(value) / Menu.help(value) | Read-only text / tooltip |
Menu.button(label) | Button; returns clicked |
Menu.separator() / Menu.sameLine() | Layout helpers |
Menu.pushID(id) / Menu.popID() | ImGui ID scope |
Menu.Begin(name, flag?) / Menu.End() | Window begin/end |
Menu.BeginChild(...) / Menu.EndChild() | Child panel |
Demo:
local sdk2 = require("_SDK.sdk2")
local Menu = sdk2.Menu
local config = { enabled = true, faMin = 15 }
script:onMenu(function()
config.enabled = Menu.checkbox("Enable", config.enabled, "enabled")
Menu.separator()
config.faMin = Menu.sliderInt("Min FA", config.faMin, 10, 30, "faMin")
end)
2
3
4
5
6
7
8
9
sdk2.utils #
High-level helpers wrapping engine getters and evasion/bone APIs. Access via
local utils = sdk2.utils.
Attack timing & type #
| Function | Description |
|---|---|
isLocalAttackStarting() / isOpponentAttackStarting() | In startup frames |
isAttackStartingBySide(side) | side = self/enemy |
IsAttackStarting(who?) | Legacy: pass self/enemy table or defaults to local |
isLocalAttackUnblockable() / isOpponentAttackUnblockable() | Unblockable attack type |
isAttackUnblockableBySide(side) / IsAttackUnblockable(who?) | Unblockable by side / legacy snapshot |
isLocalThrowingNow() / isOpponentThrowingNow() | In throw animation |
isAttackThrowBySide(side) / IsAttackThrow(who?) | Throw attack check |
hasLocalThrowTechWindow() / hasOpponentThrowTechWindow() | Throw break window open |
hasThrowTechWindowBySide(side) | Throw break window by side |
getTimeUntilImpactBySide(side) / GetTimeUntilImpact(who?) | Frames until hit connects |
getAttackTypeString(attackType) | Human-readable attack type label |
GetEnemyAttackTypeNotZero() | Opponent attack type if non-zero |
isOpponentAttackWhiffing() / IsAttackWhiffing() | Opponent whiffing |
Move lookup tables #
| Field / Function | Description |
|---|---|
utils.moveNames | byCharMove / byMove tag lookup for Guard-style scripts |
utils.moveBlacklist | Moves that block punish after hit |
isMove(enemy) / isMyMove(self) | Lookup move tag from snapshot table |
fuckThisMove(enemy) | True if opponent move is blacklisted |
Local state checks #
| Function | Description |
|---|---|
CanRun(_) / isLocalCanRun() / isOpponentCanRun() | Can run script logic (not juggled / sidroll / startup) |
isLocalSpecialStatus() / IsSpecialStatus() | Knockdown / juggle / sidroll getup |
isLocalWallSplatted() / IsGettingWallSplatted() | Wall splat state |
isLocalCrouching() / IsCrouching() | Crouching |
isLocalCrouchBack() / IsCrouchBack() | Crouch back / ducking |
isLocalGroundHit() / IsGettingGroundHit() | Ground hitstun |
isLocalCounterHit() / IsGettingCounterHit() | Counter-hit state |
isLocalAirborne() / IsAirborne() | Airborne |
isLocalOnGround() / IsOnGround() | On ground |
isLocalBlocking() / IsBlocking() | Blocking |
isLocalGettingHit() / IsGettingHit() | In hitstun |
isLocalPunish() / IsPunish() | Punish counter state |
isLocalWhileStanding() / IsWhileStanding() | While standing (WS) |
getLocalActiveFrames() / GetActiveFrames() | Active frame window |
CanCounter(frames, ...) | Counter window helper |
IsWSpunish(_self, _enemy) | While-standing punish check |
Spacing #
| Function | Description |
|---|---|
getDistance() | Fighter distance from getters |
WaitDistance(Distance) | Block until distance threshold |
Evasion & bones #
| Function | Description |
|---|---|
getEvasionStateRaw() | Raw C++ getEvasionState() snapshot |
getEvasionState(opts?) | Optional filterEvasionState via opts table |
getEvasionStateForSidestep() | Sidestep: excludes Polaris boundary walls |
filterEvasionState(state, opts) | Filter walls and recompute clearance |
getLocalEvasionFighter(s) / getOpponentEvasionFighter(s) | Fighter table from snapshot |
getEvasionFighter(state, side) | Side = "self" / "enemy" |
isBackToWall(fighter, threshold?) | Backed against wall (default 100 units) |
isNearRingEdge(fighter, threshold?) | Near ring-out edge |
isClearanceLow(fighter, dir, minDist) | Clearance forward/back/left/right too low |
getPolarisWallDistances(side, state?) | Polaris boundary wall distances (debug) |
getSidestepWallDistances(side, state?) | Sidestep-filtered wall distances |
getNearestWallInFacing(me, walls?) | Nearest wall in facing half-plane |
attackBoneName(boneType) | Map 9/13/16/19 → bone substring |
getFighterBoneWorld(slot, nameSub) | Wrapper for global bone lookup |
getFighterBoneWorlds(slot) | All main joints map |
getOpponentAttackLimbWorld(enemy?) | Opponent attacking limb world coords |
hasBoneSdk() | Engine bone API available |
Demo:
local utils = require("_SDK.sdk2").utils
local s = utils.getEvasionState({ excludePolarisStageWalls = true })
if not s.valid then return end
local me = utils.getLocalEvasionFighter(s)
if utils.isBackToWall(me) then print("Back to wall") end
local limb = utils.getOpponentAttackLimbWorld()
if limb and limb.valid then print(limb.x, limb.y, limb.z) end
2
3
4
5
6
7
Input claim #
| Function | Description |
|---|---|
claimInput(owner, frames) | Reserve keyboard input for N frames |
isInputBusy(owner) | Another owner holds input |
getInputOwner() | Current input owner name |
Misc #
| Function | Description |
|---|---|
isS2Edition() | S2 license flag |
POLARIS_STAGE_WALL_FILTERS / POLARIS_STAGE_FILTERS | Polaris stage wall name filters |
wallTail(name) | Extract wall name suffix for filter match |
matchesPolarisStageWall(wallName, filterName) | Match wall against filter entry |
isPolarisStageWallEntry(wall) | True if wall is a Polaris boundary wall |