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 })
1
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
1
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
1
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).

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)
1
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 0 for 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.
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)
1
2
3
4
5
6
7

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.beforeConfig automatically when configKey is 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)
1
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
1
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