================================================================================ VIOLET LUA SDK - COMPLETE API REFERENCE FOR AI/LLM ================================================================================ Ultra-comprehensive plain text reference optimized for AI consumption. Maximum information density. Token-efficient format. Last Updated: 2026-06-07 (Doc sync pass: documented legacy core.inventory.starforce_start/stop aliases; npc:get_id/get_position/get_name now correctly return nil when the NPC is invalid/disabled; noted core.vmatrix.send_equip_request drag arg is optional) ================================================================================ LANGUAGE: LUAU (not Lua 5.4) ================================================================================ Violet runs scripts on Luau (Lua 5.1 base + Luau extensions). Differences that matter when porting or generating code: GAINED over Lua 5.1: - continue inside loops - compound assignments: x += 1, s ..= "foo", etc. - string interpolation: `Hello, ${name}!` - generalized iteration: for k, v in t do ... end (no need for pairs()) - optional type annotations: local x: number = 1 - buffer type (native mutable byte array) — see buffer library MISSING vs Lua 5.2+/5.4: - no native bitwise operators (|, &, ~, <<, >>) — use bit32.* library - no string.pack / string.unpack - no utf8 library - no or variable attributes - no integer subtype — all numbers are doubles (// still does integer division) All plugins share a single global environment. Top-level globals are visible across scripts. Hooks (on_tick, on_packet_recv, etc.) from every plugin are CHAINED — each plugin's hook is stored separately and all of them fire in load order every tick/event. Defining on_tick in two plugins runs BOTH, not just the last one. Use `local` at file scope for per-script private state. For hooks with blocking semantics (on_packet_recv/send return false to drop the packet, on_dialog returns non-nil to intercept, on_wndproc returns false or a number to block input), the first plugin to block wins and later plugins don't see the event. ================================================================================ MANDATORY PLUGIN STRUCTURE ================================================================================ Every script MUST begin with this structure: plugin = { name = "Script Name", version = "1.0.0", author = "Author Name", description = "Brief description", load = true -- Auto-load on injection } ================================================================================ CORE CALLBACKS ================================================================================ Scripts execute through callbacks. Define only what you need: on_tick() - Called every ~30ms during gameplay - Primary location for game logic, movement, combat - Runs when player is loaded and game active on_map_load() - Called when loading a new map - Use for map-specific initialization - Called BEFORE on_tick for the new map on_login_tick() - Called during login/character selection screen - Use for pre-game automation on_packet_recv(packet: in_packet) -> boolean - Called when receiving packet from server - Return true to allow, false to block - Requires plugin metadata structure on_packet_send(packet: in_packet) -> boolean - Called when sending packet to server - Packet is provided as in_packet for read-only decoding (same type as on_packet_recv) - Return true to allow, false to block - Requires plugin metadata structure on_evasion(player_names: table) - Called once when evasion first triggers - player_names: array of strings (detected non-whitelisted player names) - Fires BEFORE the evasion action begins (CC, logout, etc.) - Use for logging, notifications, or custom evasion logic on_wndproc(event: table) -> nil | boolean | number - Called for keyboard and mouse WndProc messages only - Non-input WndProc messages are NOT passed to Lua - WM_MOUSEMOVE is NOT delivered (fires too frequently); use core.input.get_mouse_position() - Return nil/true to allow the original WndProc - Return false to block the input message with result 0 - Return number to block the input message with that LRESULT - event fields: hwnd, msg, w_param, l_param, blockable=true - Mouse messages also include x and y decoded from l_param on_chat(sender: string, message: string, channel: string, type: number) - Called for each chat message the client displays - sender: IGN that sent it (empty string for system lines) - message: the chat text - channel: "public" | "whisper" | "party" | "buddy" | "guild" | "alliance" | "megaphone" - type: raw chat-log type code (disambiguates megaphone sub-kinds per build) - System notices / item-drop lines are NOT reported; your own messages are included - Observe-only (cannot block) - Requires plugin metadata structure on_dialog(dialog: table) -> nil | boolean | number | string - Intercepts NPC dialogs and blue boxes BEFORE core auto-handling - Called ONCE per dialog -- result is cached until dialog closes - Requires plugin metadata structure DIALOG TABLE FIELDS: dialog.kind string "npc" or "bluebox" dialog.type number NPC: 0=close/prev, 1=next/ok, 5=text_input, 6=list_select Bluebox: 1000=yes/no, other=notice dialog.text string The NPC message or blue box text dialog.choices table Array of {text=string, index=number} (present when dialog has list options) index = game's internal selection value for each choice RETURN VALUES: nil or true -> pass to core (auto_npc / auto_blue_boxes handles if enabled) false -> block (do nothing, leave dialog open) 6 -> click yes / ok / next 7 -> click no / close number -> select choice matching this index value (requires choices) string -> type 5: submit as text input; with choices: select by substring match NOTE: 6 and 7 are always yes/no. If a choice has index 6 or 7, use string selection instead. EXAMPLE: function on_dialog(dialog) if dialog.kind == "npc" then -- Type 6 (list): select by index or text if dialog.type == 6 and dialog.choices then return dialog.choices[1].index -- select by index -- return "Complete" -- or select by text substring end -- Type 5 (text input): return the string to enter if dialog.type == 5 then return "answer" -- submits "answer" as text input end if dialog.type == 1 then return 6 -- click next end end -- nil = let core handle it end NOTES: - Replaces queue_npc_selection for scripts that need full dialog control - queue_npc_selection still works for simple automation via core auto_npc - Result is cached per dialog pointer -- false blocks for the dialog's lifetime - Works independently of the auto_npc / auto_blue_boxes settings ================================================================================ GUARD CLAUSE PATTERN (CRITICAL) ================================================================================ ALWAYS validate objects before use: function on_tick() local player = core.object_manager.get_local_player() if not player or not player:is_valid() then return end local map = core.object_manager.get_current_map() if not map or not map:is_valid() then return end -- Safe to use player and map here end ================================================================================ CORE MODULE - LOGGING ================================================================================ core.log(message: string) - Log white message with [-] prefix core.log_error(message: string) - Log red message with [!] prefix core.log_warning(message: string) - Log yellow message with [?] prefix NOTE: Use tostring() to convert numbers/booleans for logging ================================================================================ CORE MODULE - GAME INFORMATION ================================================================================ core.get_world_id() -> number | nil - Returns current world/server ID - nil if not in game core.get_channel_id() -> number | nil - Returns current channel number - nil if not in game core.change_channel(channel_id: number) -> boolean - Switch to specified channel - Must be out of combat - Returns true if issued, false if rejected - Excessive changes in a short period are blocked — always check the return value core.is_evading() -> boolean - Returns true if currently evading core.is_solving_rune() -> boolean - Returns true if solving rune puzzle - Use to pause automation during runes core.is_solving_lie_detector() -> boolean - Returns true while the figure-tracking lie detector (anti-macro challenge) is being solved - All automated systems (kami, rushing, skill injection, rune solving, auto attack, evasion, antimacro tracking) auto-suspend while this is true - Gate your own automation on it too: if core.is_solving_lie_detector() then return end core.is_in_cutscene() -> boolean | nil - Returns true if in cutscene/dialogue - nil if state unknown core.get_update_time() -> number - Returns current game update time in milliseconds - Use for timing operations and delays core.get_system_time() -> table - Returns system (wall clock) time as a table - Fields: year, month (1-12), day (1-31), hour (0-23), min (0-59), sec (0-59), ms (0-999), epoch (ms since Unix epoch) - Example: local t = core.get_system_time(); core.log(t.hour .. ":" .. t.min) core.is_rushing() -> boolean - Returns true if currently rushing to a map core.is_auto_selling() -> boolean - Returns true while the auto-sell flow is active (rushing to shop, walking, talking to NPC, selling) - Returns false when idle - Use to gate logic while the auto-sell state machine drives the player core.use_hyper_rock(map_id: number) -> boolean - Teleport to map using Hyper Teleport Rock - Returns true on success, false if unavailable core.chat.send(message: string [, channel: string]) -> boolean [, error] - Sends a chat message - channel defaults to "public"; also "party" | "buddy" | "guild" | "alliance" - For "public", a leading "/" is sent as a chat command - Returns true, or false plus an error string core.chat.whisper(target: string, message: string) -> boolean [, error] - Whispers message to the player named target - Must be in a field; whispering yourself is ignored by the game - Returns true, or false plus an error string (empty target/message, or not in a field) ================================================================================ CORE MODULE - SETTINGS API ================================================================================ Dynamic runtime access to game settings. Changes sync to web UI automatically. FINDING SETTING PATHS: Developer Mode (Recommended): 1. Open Settings in web menu 2. Press Ctrl+Shift+D for Developer Mode 3. Click any setting badge to copy its path Path Format: "category.subcategory.property" Examples: "autos.auto_pot.hp.enabled", "hacks.godmode", "combat.kami.enabled" core.get_setting(path: string) -> value | nil - Retrieves setting value by path - Returns boolean, number, string, or table - Array/object settings (e.g. skill injection lists) returned as Lua tables - Returns nil if setting not found Example: local auto_hp = core.get_setting("autos.auto_pot.hp.enabled") local threshold = core.get_setting("autos.auto_pot.hp.value") local skills = core.get_setting("combat.skill_injection.skills") -- skills is a table: {{id=123, delay=200, hits=1, type=0}, ...} core.set_setting(path: string, value: boolean|number|string|table) -> boolean - Updates setting value - Value type MUST match setting's expected type - Lua tables are converted to JSON arrays/objects for array settings - Returns true on success, false on failure - Changes sync automatically to web UI Example: core.set_setting("autos.auto_pot.hp.enabled", true) core.set_setting("autos.auto_pot.hp.value", 75) core.set_setting("hacks.godmode", false) core.set_setting("combat.skill_injection.skills", { { id = 2321006, delay = 200, hits = 1, type = 0 }, { id = 2321007, delay = 150, hits = 2, type = 1 } }) TYPE MISMATCH ERROR: -- WRONG: Will throw Lua error core.set_setting("autos.auto_pot.hp.enabled", 123) -- CORRECT: core.set_setting("autos.auto_pot.hp.enabled", true) NOTE: Dropdown settings store a 0-based INDEX, not the display string. COMMON SETTING PATHS (75 total -- see Settings Reference page for complete list): Autos: "autos.auto_pot.hp.enabled" -- boolean "autos.auto_pot.hp.value" -- number (0-100) "autos.auto_pot.hp.keybind" -- string "autos.auto_pot.mp.enabled" -- boolean "autos.auto_pot.mp.value" -- number (0-100) "autos.auto_pot.mp.keybind" -- string "autos.auto_login.enabled" -- boolean "autos.auto_login.world_id" -- number (dropdown index, see world table) "autos.auto_login.channel" -- number (dropdown: 0=random, 1-40=specific) "autos.auto_login.char_index" -- number "autos.evasion.type" -- number (dropdown: 0=Next Map CC, 1=Disable, 2=Logout, 3=Terminate) "autos.evasion.whitelisted_igns" -- table (string array) Hacks: "hacks.godmode" -- boolean "hacks.bossing_godmode" -- boolean "hacks.pet_loot" -- boolean "hacks.speedy_fma" -- boolean Combat: "combat.kami.enabled" -- boolean "combat.kami.type" -- number (dropdown: 0=Closest, 1=Random, 2=Random Speedy) "combat.kami.kami_exp" -- boolean "combat.kami.kami_loot" -- boolean "combat.kami.x_offset" -- number "combat.kami.y_offset" -- number "combat.skill_injection.enabled" -- boolean "combat.skill_injection.safe_mode" -- boolean "combat.skill_injection.skills" -- table (structured array, see below) Items: "items.filter_enabled" -- boolean "items.filter_mode" -- number (dropdown: 0=blacklist, 1=whitelist) "items.filtered_items" -- table (int array) "items.meso_filter_enabled" -- boolean "items.min_meso_amount" -- number Map: "map.rush_by_level" -- table (structured array, see below) "map.spawn_points" -- table (structured array, see below) Packets: "packets.streaming_enabled" -- boolean "packets.blocked_incoming_opcodes" -- table (int array) "packets.blocked_outgoing_opcodes" -- table (int array) "packets.ignored_incoming_opcodes" -- table (int array) "packets.ignored_outgoing_opcodes" -- table (int array) Macros: "macros.list" -- table (structured array, see below) WORLD INDEX TABLE (for autos.auto_login.world_id dropdown): 0 - Scania (NA, default) 1 - Bera (NA) 2 - Kronos (NA, Reboot, 40 channels) 3 - Hyperion (NA) 4 - NA CW Heroic (10 channels) 5 - NA CW Interactive (10 channels) 6 - Luna (EU) 7 - Solis (EU) 8 - EU CW Heroic (10 channels) 9 - EU CW Interactive (10 channels) Example - Auto login to Kronos, Channel 5, Character 2: core.set_setting("autos.auto_login.world_id", 2) -- Kronos (index 2) core.set_setting("autos.auto_login.channel", 5) -- Channel 5 core.set_setting("autos.auto_login.char_index", 2) -- 3rd character ARRAY SETTING STRUCTURES: skill_injection.skills: { id = int, delay = int, hits = int, type = int } type: 0=Generic, 1=Melee, 2=Magic, 3=Shoot, 4=Use Skill, 5=Safe Mode map.rush_by_level: { min_level = int, max_level = int, map_id = int } map.spawn_points: { map_id = int, x = int, y = int, label = string } macros.list: { name = string, key = string, delay = int, enabled = bool } Simple arrays (int): items.filtered_items, packets.blocked_*_opcodes, packets.ignored_*_opcodes Simple arrays (string): autos.evasion.whitelisted_igns ================================================================================ MENU / UI MODULE (core.ui) - CUSTOM SETTINGS UI ================================================================================ Build settings UI from scripts. Each core.ui.* constructor takes ONE options table and returns an element HANDLE you read/mutate at runtime. Values persist and are also reachable via core.get_setting/core.set_setting. IDS & TABS: - IDs are namespaced per script: id="enabled" in my_script.lua -> "lua.my_script.enabled". An ID already starting with "lua." is kept as-is. Prefer the handle (elem:get()) so you never type the full ID. - Omit `tab` to use the shared "Lua" tab, or pass any name for your own tab. Reserved tabs rejected: autos, combat, hacks, items, map, macros, keybinds, packets, debug, settings. - Idempotent: re-running with an existing ID returns a handle to it. COMMON OPTIONS (all constructors): id (req), label, panel (req), tab (def "Lua"), tooltip, order, description, visible_when, hidden, disabled, dangerous, transient, critical (bool|string). CONSTRUCTORS (extra options in parentheses): core.ui.checkbox{...} (default:bool) -> element core.ui.slider_int{...} (default,min,max,step) -> element core.ui.slider_float{...} (default,min,max,step,rounding) -> element core.ui.dropdown{...} (options:string[], default:index) -> element -- value=index core.ui.radio_group{...} (options:string[], default:index) -> element -- value=index core.ui.multi_select{...} (options:string[], default:index[]) -> element -- value=index[] core.ui.input_text{...} (default:string, flags:string[]) -> element core.ui.keybind{...} (default:string) -> element core.ui.color{...} (default:{r,g,b,a} 0..1) -> element core.ui.button{...} (variant, confirm) -> element -- no value; poll elem:pressed() core.ui.static_text{...} (text, style:"muted"|"body"|"heading", markdown:bool) -> element -- display only core.ui.progress{...} (value, min, max, display:"percent"|"value", color) -> element -- read-only; elem:set_value() core.ui.separator{...} () -> element -- divider; label = heading ELEMENT HANDLE METHODS (mutators return the handle for chaining): elem:get() -> any -- current value elem:set(value) -> boolean -- set value (coerced to element type) elem:id() -> string -- full namespaced ID elem:set_label(s) / elem:set_tooltip(s) elem:set_visible(bool) / elem:visible() -> bool elem:set_enabled(bool) / elem:enabled() -> bool elem:set_visible_when(other_id) elem:set_order(n) / elem:set_panel(s) elem:set_options(string[]) -- dropdown/radio_group/multi_select elem:get_index() / elem:set_index(i) -- dropdown/radio_group elem:get_selected() -- dropdown/radio: string; multi_select: index[] elem:set_selected(index[]) -- multi_select elem:set_min(n) / elem:set_max(n) / elem:set_step(n) -- sliders elem:set_text(s) -- static_text elem:set_value(n) -- progress elem:pressed() -> bool -- button: true once per click, then clears elem:remove() BUTTONS: no event callbacks -- poll elem:pressed() in on_tick. Example: local enabled = core.ui.checkbox{ id="enabled", label="Enable", panel="General", default=true } local mode = core.ui.radio_group{ id="mode", label="Mode", panel="General", options={"Safe","Fast"}, default=0 } local run = core.ui.button{ id="run", label="Run Once", panel="Actions" } function on_tick() if run:pressed() and enabled:get() then core.log("mode="..mode:get_selected()) end end LEGACY core.menu (still works; thin wrappers over core.ui, forced "Lua" tab, legacy "lua." naming, return boolean -- read via core.get_setting): core.menu.create_checkbox(id, label, tab, panel, default_value [, tooltip]) -> boolean core.menu.create_slider_int(id, label, tab, panel, default, min, max [, step [, tooltip]]) -> boolean core.menu.create_slider_float(id, label, tab, panel, default, min, max [, step [, tooltip [, rounding]]]) -> boolean core.menu.create_dropdown(id, label, tab, panel, options_table, default_index [, tooltip]) -> boolean core.menu.create_keybind(id, label, tab, panel [, default_key [, tooltip]]) -> boolean core.menu.create_input_text(id, label, tab, panel [, default_value [, tooltip]]) -> boolean ================================================================================ STAR FORCE MODULE (core.starforce) - AUTO STAR FORCE CONTROL ================================================================================ Drives the Auto Star Force state machine (Autos -> Auto Starforce panel). Writes through the same settings the web UI uses, so changes show up in the menu. Slot encoding: negative = equipped (e.g. -11 = weapon), positive = Equip inventory slot. Insufficient meso protection: state machine refuses catch packets it can't pay for (would DC the client). Status reports "insufficient_meso" and the run stops. core.starforce.start([opts: table]) -> boolean, string? - Configures and starts an Auto Star Force run - All opts fields optional; omitted fields keep their current setting - Slot must be non-zero (in opts or already configured) or returns false + error - Returns true on success, or false + error string opts fields: slot = number -- equipment slot target = number -- stop at this star level (1-30) safeguard = boolean -- safeguard for 15-17 stars (2x base surcharge) sunny_sunday = boolean -- 30% Sunny Sunday discount mvp_tier = string|number -- "none"|"silver"|"gold"|"diamond" or 0-3 Example: local ok, err = core.starforce.start({ slot = -11, target = 17, safeguard = true, mvp_tier = "gold" }) core.starforce.stop() -> boolean - Disables Auto Star Force; returns true if toggle flipped - Safe to call when not running core.starforce.is_running() -> boolean - True if the Enable Auto SF checkbox is on core.starforce.get_status() -> table - Snapshot of the current run - Fields: running = boolean state = "idle"|"waiting_catch"|"waiting_result"|"next_item"|"done" slot = number current_stars = number target_stars = number attempts = number spent = number -- mesos billed in this run result = table -- live status object (see below) - result fields (not all present every state): status = "enhancing"|"done"|"item_not_found"|"item_boomed"|"insufficient_meso"|"timeout" stars = number attempts = number spent = number lastCost = number? -- cost of last attempt nextCost = number? -- present on "insufficient_meso" currentMeso = number? -- present on "insufficient_meso" core.starforce.estimate_cost(item_level: number, current_star: number [, opts: table]) -> number - Meso cost of one attempt at current_star on an item of given required level - GMS formula with discounts and safeguard surcharge applied - opts omitted -> uses live settings - opts fields: safeguard, sunny_sunday, mvp_tier (same encoding as start()) Example: local cost = core.starforce.estimate_cost(150, 16, { safeguard = true, mvp_tier = "gold" }) ================================================================================ CUBE MODULE (core.cube) - AUTO CUBE CONTROL ================================================================================ Drives the Auto Cube state machine (Autos -> Auto Cube panel). Writes through the same settings the web UI uses, so changes show up in the menu and persist. The frontend widget and Lua share state. Slot encoding: negative = equipped (e.g. -11 = weapon), positive = Equip slot. Matching: each roll's three potential lines are resolved to displayed strings and parsed into canonical {stat, value} pairs. A roll matches when any condition group is fully satisfied (OR-of-AND). core.cube.start([opts: table]) -> boolean, string? - Configures and starts an Auto Cube run - All opts fields optional; omitted fields keep their current setting - Slot must be non-zero (in opts or already configured) or returns false + error - Returns true on success, or false + error string opts fields: slot = number -- equipment slot cube_type = string|number -- "glowing"|"bright"|"mystical"|"hard"|"solid" or 0..4 (mystical/hard/solid cap at Epic/Unique/Legendary) min_grade = number -- 0=Common, 1=Rare, 2=Epic, 3=Unique, 4=Legendary Example: local ok, err = core.cube.start({ slot = -11, cube_type = "bright", min_grade = 3 }) core.cube.stop() -> boolean - Disables Auto Cube; returns true if toggle flipped - Safe to call when not running core.cube.is_running() -> boolean - True if the Enable Auto Cube checkbox is on core.cube.get_status() -> table - Snapshot of the current run - Fields: running = boolean state = "idle"|"sending_cube"|"waiting_result"|"checking_result" |"accepting"|"rejecting"|"waiting_reject_confirm"|"cooldown" slot = number cubes_used = number last_rolled = { p1=number, p2=number, p3=number, grade=number } result = table -- live status object pushed to the web UI core.cube.set_conditions(spec: table) -> boolean, string? - Sets the "good roll" definition. OR-of-AND: roll accepted when any group is fully satisfied; each group is a list of conditions that must all hold - Accepts two shapes: Flat (single AND group): { {stat="STR %", min=21}, {stat="Attack Power %", min=18} } OR-of-AND: { { {stat=..., min=...}, ... }, { {...}, ... } } - Each leaf is one of: { stat = "", min = } -- summed across all 3 lines { exact = "", count = } -- literal tooltip match - All Stats % automatically counts toward STR %/DEX %/INT %/LUK % thresholds Common canonical stat names (use parse_potential to discover others): STR, STR %, DEX, DEX %, INT, INT %, LUK, LUK %, All Stats, All Stats % Attack Power, Attack Power %, Magic ATT, Magic ATT %, DEF, DEF % Max HP, Max HP %, Max MP, Max MP % Boss Damage %, Critical Damage %, Critical Rate %, Damage % Ignore Defense %, Item Drop Rate %, Mesos Obtained %, EXP Obtained % Cooldown Reduction (sec), MP Cost Reduction % core.cube.get_conditions() -> table | nil - Returns stored conditions in OR-of-AND shape, or nil if module unavailable core.cube.clear_conditions() -> boolean - Removes all conditions; with none set, every roll passing min_grade is kept core.cube.resolve_potential(pot_id: number [, level: number]) -> string | nil - Resolves a raw potential ID to its displayed tooltip line (no [Grade] suffix) - If level omitted, looks up level by matching pot_id against the configured equip slot's three potentials - Returns nil for pot_id 0 or unresolvable core.cube.parse_potential(str: string) -> table | nil - Parses a displayed line into { stat = "", value = } - Returns nil for non-stat lines (e.g. proc chances) -- those need exact match core.cube.inspect_current_item() -> table | nil - Reads the equip at the configured slot and returns: grade = number -- 0=Common .. 4=Legendary potentials = table -- array of 3 entries each: { id, level, line, stat?, value? } - Returns nil if slot is 0 or no item present ================================================================================ OBJECT MANAGER ================================================================================ core.object_manager.get_local_player() -> Player | nil - Returns local player object - nil if player not loaded - ALWAYS validate with is_valid() before use core.object_manager.get_current_map() -> Map | nil - Returns current map object - nil if map not loaded - ALWAYS validate with is_valid() before use ================================================================================ PLAYER OBJECT ================================================================================ player:is_valid() -> boolean - ALWAYS call before using other methods player:get_id() -> number player:get_name() -> string player:get_health() -> number player:get_max_health() -> number player:get_mana() -> number player:get_max_mana() -> number player:get_level() -> number player:get_exp() -> number player:get_meso() -> number player:get_job() -> number player:get_position() -> {x: number, y: number} - Returns position table - Example: local pos = player:get_position(); print(pos.x, pos.y) player:get_server_position() -> {x: number, y: number} - Position the server believes the character is at - Use when client-side and server can drift (teleport/rush automation) player:get_move_action() -> number - Returns raw character move action value player:is_left() -> boolean - Returns true when the character is facing left player:get_hyperstat_sp() -> number | nil - Returns hyperstat SP available on the currently active preset - Returns nil when no character data is available player:has_buff(buff_id: number) -> boolean - Returns true if buff active - More efficient than get_buff() for checking existence player:get_buff(buff_id: number) -> buff | nil - Returns buff object or nil - Buff: {id, type, name, time_remaining} - time_remaining in milliseconds player:get_buffs() -> table - Returns array of all active buffs - Buff: {id, type, name, time_remaining} ================================================================================ MAP OBJECT ================================================================================ map:is_valid() -> boolean - ALWAYS call before using other methods map:get_id() -> number - Returns map ID (e.g., 100000000 = Henesys) map:is_town() -> boolean - Returns true if map is a town map:get_burning_stage() -> number | nil - Returns burning XP stage (0-10) - nil if not burning field map:get_drops() -> table - Returns all drops on map - Drop: {id, position: {x, y}, type} map:get_portals() -> table - Returns all portals - Portal: {name, position: {x, y}, type, target_map_id, target_name} map:get_mobs() -> table - Returns all mobs on map map:get_bosses() -> table - Returns all boss mobs map:get_mob_count() -> number - Returns total mob count - More efficient than iterating get_mobs() map:get_npcs() -> table - Returns all NPCs on map map:get_footholds() -> table - Returns all platforms - Foothold: {x1, y1, x2, y2} map:get_ladders() -> table - Returns all ladders/ropes - Ladder: {x, y1, y2} ================================================================================ MOB OBJECT ================================================================================ mob:is_valid() -> boolean - ALWAYS call before using other methods mob:get_id() -> number | nil - Returns mob template ID mob:get_position() -> {x: number, y: number} | nil - Returns position or nil mob:get_name() -> string | nil - Returns mob name or nil mob:is_elite() -> boolean | nil - Returns true if elite/champion - nil if unknown ================================================================================ NPC OBJECT ================================================================================ npc:is_valid() -> boolean - ALWAYS call before using other methods npc:get_id() -> number | nil - Returns NPC template ID, or nil if the NPC is invalid or disabled npc:get_position() -> {x: number, y: number} | nil - Returns NPC position, or nil if the NPC is invalid or disabled npc:get_name() -> string | nil - Returns NPC name, or nil if the NPC is invalid or disabled npc:distance() -> number | nil - Distance from local player to NPC - nil if NPC or player is unavailable npc:talk() -> boolean - Attempts to talk to NPC - Returns false if NPC/player unavailable or NPC is out of interaction range ================================================================================ INPUT MODULE - GAME INTERACTION ================================================================================ core.input.use_skill(skill_id: number) - Cast skill by ID - Check cooldown first with skill_book.is_skill_on_cooldown() core.input.use_item(item_id: number) - Use item from inventory - Verify existence first with inventory.has_item() core.input.talk_to_npc(npc_id: number) -> boolean - Initiate NPC dialogue - Returns true if sent, false if NPC not on map or player out of range - Does NOT auto-teleport — position the player within range first core.input.enter_portal() - Enter portal at player position - Ensure player is on portal first core.input.press_key(key_code: number) - Simulate key press (a one-shot tap: single key-down, releases immediately) - Use hold_key/release_key to hold a key down instead - Requires valid local player - See: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes core.input.hold_key(key_code: number) - Send a key-down and keep it held until release_key is called for the same key - Use to hold movement/skill keys for a controlled duration instead of tapping - Requires valid local player core.input.release_key(key_code: number) - Send a key-up for a key held with hold_key, ending the hold - Requires valid local player core.input.call_wndproc(msg: number, wparam?: number, lparam?: number) -> number | nil - Calls the game's window procedure directly with a synthesized message - Bypasses the OS message queue and the menu's own input hooks - wparam/lparam use standard Win32 layout (e.g. VK code in wparam for WM_KEYDOWN); both default to 0 - Returns the LRESULT, or nil if the game window is unavailable - WARNING: malformed messages can crash the client; only use valid Win32 combinations core.input.get_mouse_position() -> {x: number, y: number} | nil - Returns current cursor position relative to the game window client area - Returns nil if the game window is unavailable or cursor is outside it ================================================================================ INPUT MODULE - KEYBOARD STATE & CALLBACKS ================================================================================ core.input.is_key_down(key_code: number) -> boolean - True while the engine sees the virtual key as held down - Works for modifier keys too (VK_CONTROL 0x11, VK_SHIFT 0x10, VK_MENU 0x12) for combos core.input.is_key_up(key_code: number) -> boolean - Convenience inverse of is_key_down core.input.on_key_press(key_code: number, callback: function) -> number - Registers callback to run when key_code is pressed; callback receives the key code - Only the registered key fires the callback (no per-keypress dispatch in your script) - Edge-triggered: holding the key fires once, not on OS auto-repeat - Returns a handle for remove_key_callback; auto-removed when the script unloads core.input.on_key_release(key_code: number, callback: function) -> number - Like on_key_press, but fires when the key is released core.input.remove_key_callback(handle: number) -> boolean - Unregisters a callback from on_key_press/on_key_release - Returns true if a matching callback was found and removed ================================================================================ LOGIN MODULE ================================================================================ core.login is read-only state for the login screen. Useful from on_login_tick. Steps: 1=world/channel select, 2=character select, 3=new-char race/subjob, 4=new-char name entry. core.login.get_step() -> number | nil - Current login step, or nil if not on the login screen (e.g. in field) core.login.get_char_count() -> number | nil - Number of characters in the selected world's slot list - Only valid during step 2; nil otherwise ================================================================================ KEYS MODULE ================================================================================ core.keys edits the game keyboard layout for the active key preset. Virtual-key codes are translated to the game's keyboard scan-code keymap index. Mapping types: 0=empty, 1=skill, 2=item. core.keys.get(vkey: number) -> {type: number, id: number} - Returns the current game key mapping for a keyboard virtual-key code core.keys.set(vkey: number, type: number, id: number) - Sets a game key mapping - type: 1=skill, 2=item - Use core.keys.unset to clear a key core.keys.set_skill(vkey: number, skill_id: number) - Sets a keyboard key to a skill core.keys.set_item(vkey: number, item_id: number) - Sets a keyboard key to an item core.keys.unset(vkey: number) - Clears a keyboard key mapping ================================================================================ INPUT MODULE - MOVEMENT ================================================================================ core.input.teleport(x: number, y: number) - Instant teleport to coordinates - WARNING: Spamming triggers anti-cheat - Use only for one-time teleports to portals/NPCs core.input.teleport_safe(x: number, y: number, x_offset?: number, y_offset?: number) - Safe teleport with foothold validation - Moves toward position over time (like Kami) - Optional x_offset and y_offset parameters core.input.is_moving() -> boolean - Returns true if player moving core.input.stop_moving() - Stop all movement immediately core.input.move_x(x_coord: number, force?: boolean, timeout_ms?: number) - Move to X coordinate (maintains Y) - force=false (default): holds arrow keys for natural movement (the only legit path); can be blocked by what the character is doing (e.g. mid-attack) - force=true: engine cutscene move; WILL move even when regular is blocked, but looks unnatural - timeout_ms (default 10000): caps how long the move runs; force/timeout order-independent core.input.move_y(y_coord: number, force?: boolean, timeout_ms?: number) - Move to Y coordinate (maintains X); regular movement climbs ladders/ropes via Up/Down - Same force / timeout_ms options as move_x NOTE: Movement functions don't account for manual player input NOTE: Prefer regular movement (force=false) — natural arrow-key input is the only legit path ================================================================================ HTTP MODULE (core.http) - ASYNC HTTP REQUESTS ================================================================================ All functions are async. Callback fires on the next tick after completion. Last argument is always the callback function. ISOLATION: Each script has its own HTTP state. Cookies (Set-Cookie) and auth state persist within a script's own requests but never leak between scripts. Two scripts hitting the same host keep separate cookie jars. Scripts can also spin up additional isolated clients via core.http.client() (see below). core.http.get(url, [headers,] callback) - GET request - headers: optional table of string key-value pairs core.http.post(url, body, [content_type, [headers,]] callback) - POST request - content_type defaults to "application/json" core.http.put(url, body, [content_type, [headers,]] callback) - PUT request - content_type defaults to "application/json" core.http.delete(url, [headers,] callback) - DELETE request core.http.request(method, url, [body, [headers,]] callback) - Any HTTP method RESPONSE OBJECT (passed to callback): { status = number, -- HTTP status code (200, 404, etc.) or 0 if failed body = string, -- Response body headers = table -- Response headers as key-value pairs } EXAMPLES: -- Simple GET core.http.get("https://httpbin.org/get", function(response) core.log("Status: " .. response.status) core.log("Body: " .. response.body) end) -- POST with headers core.http.post("https://api.example.com/webhook", '{"event":"test"}', "application/json", { ["Authorization"] = "Bearer token" }, function(response) core.log("Status: " .. response.status) end ) -- Discord webhook from on_evasion function on_evasion(player_names) local names = table.concat(player_names, ", ") core.http.post("https://discord.com/api/webhooks/YOUR_URL", '{"content":"Evasion: ' .. names .. '"}', "application/json", function(response) end ) end EXPLICIT CLIENTS (core.http.client()): Creates a brand-new isolated client with its own cookie jar, auth state, and connection pool. Use when one script needs multiple independent HTTP contexts (e.g. two separate auth sessions, isolated bot vs telemetry traffic). Methods mirror the free functions but use `:` instead of `.`: client:get(url, [headers,] callback) client:post(url, body, [content_type, [headers,]] callback) client:put(url, body, [content_type, [headers,]] callback) client:delete(url, [headers,] callback) client:request(method, url, [body, [headers,]] callback) EXAMPLE: local api = core.http.client() local logs = core.http.client() api:get("https://api.example.com/me", function(r) end) logs:post("https://logs.example.com/ingest", payload, function(r) end) -- cookies set on `api` are NOT sent by `logs`, and vice versa ================================================================================ SKILL BOOK ================================================================================ core.skill_book.get_skill_level(skill_id: number) -> number - Returns skill level (0 if not learned) core.skill_book.get_skill(skill_id: number) -> skill | nil - Returns skill object or nil - Skill: {id, type, name} core.skill_book.get_skills() -> table - Returns all learned skills - Skill: {id, type, name} core.skill_book.is_skill_on_cooldown(skill_id: number) -> boolean - Returns true if on cooldown ================================================================================ INVENTORY ================================================================================ TAB IDs: 1 = Equip 2 = Use 3 = Setup 4 = Etc 5 = Cash core.inventory.get_total_slots(tab_id: number) -> number - Returns total slots in tab core.inventory.get_free_slots(tab_id: number) -> number - Returns free slots in tab core.inventory.has_item(item_id: number) -> boolean - Returns true if item exists in any tab core.inventory.get_all_items() -> table - Returns all items across all tabs core.inventory.get_equip_items() -> table - Returns unequipped equip items in inventory (tab_id=1) - These are equips in your bag, NOT currently worn core.inventory.get_equipped_items() -> table - Returns items currently worn by the character - Position values are NEGATIVE (equipment slot IDs) - Includes potentials and star counts - Example: for _, item in ipairs(core.inventory.get_equipped_items()) do ... end core.inventory.get_use_items() -> table - Returns Use tab items (tab_id=2) core.inventory.get_etc_items() -> table - Returns Etc tab items (tab_id=3) core.inventory.get_setup_items() -> table - Returns Setup tab items (tab_id=4) core.inventory.get_cash_items() -> table - Returns Cash tab items (tab_id=5) core.inventory.get_required_level(item_id: number) -> number | nil - Returns the required level to equip/use the item - nil if item info unavailable core.inventory.can_equip(item_id: number) -> boolean | nil - Returns true if current character meets all requirements (level, job, stats) - nil if item info or character context unavailable core.inventory.get_item_combat_power(tab_id: number, position: number) -> number | nil - Estimated combat power delta from equipping the item in that tab/slot - Positive means stronger than current equipment, negative means weaker - nil if item or character context unavailable core.inventory.change_slot_position(tab_id: number, old_pos: number, new_pos: number, count?: number) - Moves an item from old_pos to new_pos within the specified tab - Can equip items by moving to negative positions (e.g. -1 = hat, -5 = top) - Use new_pos=0 to drop/discard items - count defaults to 1 core.inventory.starforce_start(equip_slot: number, target_stars: number, use_safeguard?: boolean) -> boolean - LEGACY ALIAS of core.starforce.start — prefer core.starforce (adds sunny_sunday, mvp_tier, validation, status/cost) - Configures and starts an Auto Star Force run; writes the same Autos -> Auto Starforce settings - equip_slot: negative = equipped (e.g. -11 = weapon), positive = Equip tab slot - target_stars: stop at this star level (1-30); use_safeguard optional, defaults false - Returns true on success, false if the Autos module isn't ready core.inventory.starforce_stop() -> boolean - LEGACY ALIAS of core.starforce.stop - Clears the Enable Auto SF toggle and resets state to idle; safe to call when not running - Returns true (no-op when Autos module isn't loaded) ITEM STRUCTURE: { id = number, -- Item template ID position = number, -- Slot index count = number, -- Quantity name = string, -- Display name tab_id = number, -- Tab (1-5) potentials = table, -- Array of strings (Equip only, up to 7) current_star_count = number, -- Star force (Equip only) max_star_count = number, -- Max star force (Equip only) remaining_upgrade_count = number, -- Remaining scroll upgrade slots (Equip only, nRUC) current_upgrade_count = number, -- Successful scroll upgrades applied (Equip only, nCUC) is_dead = boolean, -- Pet past expiration date (Pet items only, false for non-pets) is_active = boolean -- Pet currently active/summoned (Pet items only, false for non-pets) } ================================================================================ FAMILIAR ================================================================================ FAMILIAR STRUCTURE: { sn = number, -- Unique serial number (instance ID) id = number, -- Mob template ID character_id = number, -- Owner character ID name = string, -- Custom familiar name level = number, -- Current level max_level = number, -- Max level cap exp = number, -- Experience grade = number, -- 0=Common, 1=Rare, 2=Epic, 3=Unique, 4=Legendary grade_exp = number, -- Grade experience add_pad = number, -- Bonus physical attack add_pdd = number, -- Bonus physical defense option1 = number, -- Potential line 1 ID (raw) option2 = number, -- Potential line 2 ID (raw) potentials = table, -- Array of resolved potential strings (e.g. "Boss Damage +30%") locked = boolean -- Whether familiar is locked } core.familiar.get_all() -> table - Returns all familiars owned by the character core.familiar.get_by_sn(serial_number: number) -> familiar | nil - Look up a specific familiar by serial number core.familiar.get_count() -> number - Returns total number of familiars owned core.familiar.get_summoned() -> familiar | nil - Returns currently summoned familiar, or nil if none core.familiar.get_equipped() -> table - Returns up to 3 familiars in active preset's equipped slots - Empty slots are nil core.familiar.get_badges() -> table - Returns active badge entries (non-empty slots) - Entry: {slot = number (0-7), badge_id = number} core.familiar.get_user_info() -> table | nil - Returns familiar system state - Fields: fatigue, inventory_size, summoned_sn, version, active_preset, active_badge_preset core.familiar.get_preset_familiars(preset_index: number) -> table - Returns 3 familiar SNs for the specified preset (0-4) - SNs of 0 indicate empty slots core.familiar.get_preset_badges(preset_index: number) -> table - Returns 8 badge IDs for the specified badge preset (0-4) core.familiar.apply_preset(preset_index: number, sn1: number, sn2: number, sn3: number) -> boolean - Writes 3 familiar SNs into preset_index (0-4); 0 = empty slot - Does NOT change which preset is active — use set_active_preset for that - Validates each non-zero SN against owned-familiar map (stale SNs disconnect); returns false on bad input core.familiar.set_active_preset(preset_index: number) -> boolean - Switches which familiar preset is currently active (0-4) - Does NOT modify the preset's familiar layout ================================================================================ V MATRIX ================================================================================ SLOT STRUCTURE (from get_slots / get_slot_by_index — one owned V Core): { inventory_index = number, -- Position in owned-cores array (stable). USE THIS as data_index in send_*_request item_sn = number, -- Unique core serial number id = number, -- V Core template ID grade = number, -- Core grade exp = number, -- Current experience slot = number, -- Inventory slot skill1 = number, -- Skill ID in slot 1 skill2 = number, -- Skill ID in slot 2 skill3 = number, -- Skill ID in slot 3 slot_index = number, -- Board position when equipped, -1 if not on board. NOT the right key for send_*_request expire_date = number, -- FILETIME (100ns ticks since 1601); a 2079-01-01 value = non-expiring sentinel protect_lock = boolean -- Protect-locked from disassembly } CRITICAL — VMATRIX ACTION SEMANTICS: - All send_* return true on PACKET DISPATCH, not on server accept. Server can silently reject (equipped core, lock, rate limit, stale index). - inventory_index is POSITIONAL and SHIFTS when the server applies a remove / disassemble / craft: every higher inventory_index decrements by 1. Stale snapshots from a previous tick will point at wrong cores. Re-read get_slots() after every confirmed action. - item_sn is STABLE across all inventory mutations — use it as the identity key for per-core cooldowns or "already tried" sets. - Verify success by observed state, not return value: send_disassemble_single_request / send_craft_request → get_shard_count() rises send_equip_request / send_remove_request → get_equipped() entry changes send_enhance_slot_request → get_matrix_point() drops, target slot's enhance rises - ONE MUTATION PER TICK. Real UI gates these behind confirm dialogs; server does not expect bursts. Pattern: 1. on_tick: if pending request, wait until expected state changes (or N-ms timeout). If still pending, return. 2. Otherwise pick the next target, snapshot the "before" value (shard count etc.), send the packet, store target's item_sn as the pending request. EQUIP_SLOT STRUCTURE (from get_equipped — one V Matrix board slot): { data_index = number, -- inventory_index of equipped core (-1 if empty) slot_index = number, -- Position on the V Matrix board enhance = number, -- Slot enhancement level extension = boolean -- Whether the board slot has been extended } core.vmatrix.get_slots() -> table - Returns all owned V Core slots core.vmatrix.get_equipped() -> table - Returns all equipped V Matrix board slots core.vmatrix.get_slot_count() -> number - Number of owned V Core slots core.vmatrix.get_equip_count() -> number - Number of equipped V Matrix board slots core.vmatrix.get_slot_by_index(slot_index: number) -> slot | nil - Look up an EQUIPPED core by its board position (slot.slot_index) - For inventory lookup, just index get_slots()[inventory_index + 1] core.vmatrix.get_shard_count() -> number - V Core Shard count (currency for crafting/enhancement) - Reads quest record QR_VCoreShard "count" key; 0 if char not loaded core.vmatrix.send_craft_request(v_core_id: number, craft_count: number) -> boolean - Craft N cores of template v_core_id - Returns true if packet was sent (server validates) core.vmatrix.send_equip_request(data_index1, data_index2, equip_index1, equip_index2, drag?: boolean) -> boolean - Equip / swap / move cores between board slots - data_index1 = source core's inventory_index - data_index2 = current occupant's inventory_index, or -1 if dest is empty - equip_index1 = source board pos, or -1 if source is unequipped - equip_index2 = destination board pos - drag = true for drag-drop, false for click-equip (optional; omitted/nil = false) core.vmatrix.send_remove_request(data_index: number) -> boolean - Unequip the core with the given inventory_index core.vmatrix.send_enhance_request(data_index: number, src_data_index: number) -> boolean - Enhance core at inventory_index `data_index` by consuming inventory_index `src_data_index` core.vmatrix.send_disassemble_single_request(data_index: number) -> boolean - Disassemble a single unequipped core (inventory_index) back into V Core Shards - Server rejects if the core is currently equipped — call send_remove_request first core.vmatrix.send_disassemble_multiple_request(data_indices: table) -> boolean - Bulk disassemble — takes an array of inventory_index values, one packet for the whole batch - Returns false if the table is empty - Build indices from a single get_slots() snapshot; do NOT call repeatedly (the server reshuffles inventory_index after the batch lands) core.vmatrix.send_enhance_slot_request(slot_index: number) -> boolean - Raise the enhance level of the board slot at slot_index, spending Matrix Points - Returns false (and does nothing) if the player has no Matrix Points core.vmatrix.get_matrix_point() -> number - Player's current Matrix Point balance (currency for slot enhance/extension) - Returns 0 if char not loaded core.vmatrix.use_nodestone(amount: number) -> boolean - Use `amount` Nodestones from the player's inventory ================================================================================ INNER ABILITY (core.inner_ability) - READ ACTIVE PRESET ================================================================================ Read-only access to the character's Inner Ability lines for the active preset. core.inner_ability.get_current() -> table - Returns an array of the active preset's Inner Ability lines (up to 3) - Returns an empty table if unavailable (char not loaded) - Each line is a table: { skill_id: number, pos: number, grade: number, name: string } skill_id - option ID pos - slot index (1-3) grade - rarity: 0=Rare, 1=Epic, 2=Unique, 3=Legendary name - option name (e.g. "Boss Damage +10%") - Example: for _, line in ipairs(core.inner_ability.get_current()) do core.log(string.format("Slot %d (grade %d): %s", line.pos, line.grade, line.name)) end ================================================================================ QUESTER ================================================================================ core.quester.get_quest_state(quest_id: number) -> number - Returns quest state: -1 = Not available 0 = Available 1 = In progress 2 = Completed core.quester.start_quest(npc_id: number, quest_id: number) -> boolean - Start quest with NPC - Check state first with get_quest_state() - Returns false if NPC is on map but player is out of interaction range - If NPC is not on the map, the call proceeds (auto-complete flows) - Does NOT auto-teleport — position the player within range first core.quester.complete_quest(npc_id: number, quest_id: number) -> boolean - Complete quest with NPC - Check if completable first with can_complete_quest() - Returns false if NPC is on map but player is out of interaction range - If NPC is not on the map, the call proceeds (auto-complete flows) - Does NOT auto-teleport — position the player within range first core.quester.can_complete_quest(npc_id: number, quest_id: number) -> boolean - Returns true if quest requirements met core.quester.queue_npc_selection(selection: string) - Queue dialogue option substring - When NPC dialogue appears, option containing substring is selected - For text input, queued string is used as answer core.quester.clear_npc_selection() - Clear entire NPC selection queue ================================================================================ RUSHER ================================================================================ core.rusher.rush(map_id: number) - Auto-navigate to target map - Handles pathfinding and portals automatically core.rusher.stop() - Stop current rushing immediately core.rusher.is_rushing() -> boolean - Returns true if currently rushing ================================================================================ IN PACKET (RECEIVING + INJECTION) ================================================================================ Two flavors: 1. Read-only view passed to on_packet_recv / on_packet_send callbacks. 2. Builder created via in_packet(opcode) for injecting synthetic packets. --- READ SIDE (callback parameter) --- packet:get_opcode() -> number - Returns packet opcode packet:decode_1() -> number - Read 1 byte (0-255) packet:decode_2() -> number - Read 2 bytes (0-65535) packet:decode_4() -> number - Read 4 bytes (0-4294967295) packet:decode_8() -> integer - Read 8 bytes as a Luau int64 (LUA_TINTEGER), NOT a regular number - int64 is a distinct numeric type — does NOT compare/arithmetic against plain numbers - For initial state, use `integer.new(0)` not `0` - Cast with `tonumber(v)` if you need a regular number (loses precision past 2^53) packet:decode_string() -> string - Read length-prefixed string packet:decode_buffer(size: number) -> buffer | nil - Read N bytes as a native Luau buffer (mutable, byte-addressable) - Access with buffer.readu8(buf, offset) — 0-indexed, NOT buf[i+1] - Returns nil if not enough data remains - Pass straight to out_packet:encode_buffer or in_packet:encode_buffer to forward bytes without copy packet:get_length() -> number - Returns total packet length in bytes packet:get_offset() -> number - Returns current read offset position packet:set_offset(offset: number) - Set read offset to specific position - Allows rewinding to re-read data NOTE: Decode methods read sequentially. Each call advances read position. Use set_offset() to rewind if you need to re-read data. --- INJECTION SIDE (builder) --- Constructor: in_packet(opcode: number) -> InPacketBuilder - Creates a builder for injecting a synthetic incoming packet. - Server never sees this — packet is dispatched LOCALLY as if received. - Use out_packet(...):send() if you actually want to send to the server. packet:encode_1(value: number) -> self - Encode 1 byte. Returns self for chaining. packet:encode_2(value: number) -> self - Encode 2 bytes. Returns self for chaining. packet:encode_4(value: number) -> self - Encode 4 bytes. Returns self for chaining. packet:encode_8(value: number) -> self - Encode 8 bytes. Returns self for chaining. packet:encode_string(text: string) -> self - Encode length-prefixed string. Returns self for chaining. packet:encode_buffer(data: string | buffer) -> self - Encode raw bytes from a Lua string or Luau buffer. Returns self for chaining. packet:inject() - Queue the packet for dispatch on next main tick. - Safe to call from inside on_packet_recv (deferred — no re-entrancy). packet:to_string() -> string - Hex representation of contents (debug helper). packet:get_opcode() -> number - The opcode passed to in_packet(opcode). packet:get_data() -> string - Alias of to_string(). EXAMPLE (injection): in_packet(0x00A2) :encode_1(0) :encode_string("hi") :inject() EXAMPLE (forward bytes from a hook): function on_packet_recv(packet) if packet:get_opcode() == 0x1234 then local body = packet:decode_buffer(packet:get_length() - 4) in_packet(0x5678):encode_buffer(body):inject() end return true end ================================================================================ OUT PACKET (SENDING) ================================================================================ Constructor: out_packet(opcode: number) -> OutPacket packet:encode_1(value: number) -> self - Encode 1 byte (0-255) - Returns self for chaining packet:encode_2(value: number) -> self - Encode 2 bytes (0-65535) - Returns self for chaining packet:encode_4(value: number) -> self - Encode 4 bytes (0-4294967295) - Returns self for chaining packet:encode_8(value: number) -> self - Encode 8 bytes - Returns self for chaining packet:encode_string(text: string) -> self - Encode length-prefixed string - Returns self for chaining packet:encode_buffer(data: string | buffer) -> self - Encode raw binary data from either a Lua string or a Luau buffer - Use the buffer form to forward bytes from in_packet:decode_buffer - Returns self for chaining packet:to_string() -> string - Returns hex string for debugging packet:get_opcode() -> number - Returns the packet opcode packet:get_data() -> string - Returns hex string (alias of to_string) packet:send() - Send packet to server EXAMPLE: out_packet(0x0029) :encode_1(1) :encode_4(map_id) :encode_string("test") :send() ================================================================================ COMMON PATTERNS ================================================================================ --- HP/MP MONITORING --- function on_tick() local player = core.object_manager.get_local_player() if not player or not player:is_valid() then return end local hp_percent = (player:get_health() / player:get_max_health()) * 100 if hp_percent < 50 then core.input.use_item(2000001) -- Red Potion end end --- MOB HUNTING --- function on_tick() local player = core.object_manager.get_local_player() if not player or not player:is_valid() then return end local map = core.object_manager.get_current_map() if not map or not map:is_valid() then return end local mobs = map:get_mobs() for _, mob in ipairs(mobs) do if mob:is_valid() then local pos = mob:get_position() if pos then core.input.teleport_safe(pos.x, pos.y) core.input.use_skill(2001004) break end end end end --- BUFF MAINTENANCE --- function on_tick() local player = core.object_manager.get_local_player() if not player or not player:is_valid() then return end local buff_id = 2001002 if not player:has_buff(buff_id) then if not core.skill_book.is_skill_on_cooldown(buff_id) then core.input.use_skill(buff_id) end end end --- DISTANCE CALCULATION --- function distance(pos1, pos2) local dx = pos1.x - pos2.x local dy = pos1.y - pos2.y return math.sqrt(dx * dx + dy * dy) end --- COOLDOWN MANAGER --- local cooldowns = {} function can_use(key, cooldown_ms) local now = core.get_update_time() if not cooldowns[key] or now - cooldowns[key] >= cooldown_ms then cooldowns[key] = now return true end return false end --- STATE MACHINE --- local state = "idle" function on_tick() if state == "idle" then state = "hunting" elseif state == "hunting" then -- hunting logic if condition then state = "looting" end elseif state == "looting" then -- looting logic state = "idle" end end --- MAP CHANGE DETECTION --- local last_map_id = nil function on_tick() local map = core.object_manager.get_current_map() if not map or not map:is_valid() then return end local current_map_id = map:get_id() if last_map_id ~= current_map_id then core.log("Map changed to: " .. current_map_id) last_map_id = current_map_id -- Reset state for new map end end ================================================================================ CRITICAL SAFETY RULES ================================================================================ 1. ALWAYS validate objects with is_valid() before calling methods 2. ALWAYS check for nil returns before using values 3. NEVER assume map or player exists - validate every tick 4. Use guard clauses at start of on_tick() to exit early if invalid 5. Wrap risky operations in pcall(): local success, result = pcall(function() return operation() end) 6. Avoid infinite loops - always have exit conditions 7. Be mindful of packet manipulation - can cause disconnects 8. Test scripts in safe environments first 9. Use core.log() liberally for debugging 10. Handle nil positions from mobs/npcs gracefully ================================================================================ PERFORMANCE OPTIMIZATION ================================================================================ 1. Cache frequently accessed objects within tick 2. Limit loop iterations: for i = 1, math.min(#mobs, 10) do 3. Use early returns: if core.is_solving_rune() then return end 4. Throttle expensive operations: tick_count = tick_count + 1 if tick_count % 10 == 0 then ... end 5. Prefer has_buff() over get_buff() for existence checks 6. Use local variables for frequently accessed values 7. Break out of loops early when target found 8. Minimize packet parsing - decode only what you need 9. Batch operations when possible ================================================================================ COMMON MAP IDS ================================================================================ Henesys: 100000000 Ellinia: 101000000 Perion: 102000000 Kerning City: 103000000 Lith Harbor: 104000000 Sleepywood: 105000000 Nautilus: 120000000 Orbis: 200000000 El Nath: 211000000 Ludibrium: 220000000 Leafre: 240000000 Mu Lung: 250000000 Ariant: 260000000 ================================================================================ COMMON JOB IDS ================================================================================ Beginner: 0 Warrior: 100 Fighter: 110, Hero: 111 Page: 120, Paladin: 121 Spearman: 130, Dark Knight: 131 Magician: 200 Fire/Poison: 210, Arch Mage (F/P): 211 Ice/Lightning: 220, Arch Mage (I/L): 221 Cleric: 230, Bishop: 231 Bowman: 300 Hunter: 310, Bow Master: 311 Crossbowman: 320, Marksman: 321 Thief: 400 Assassin: 410, Night Lord: 411 Bandit: 420, Shadower: 421 Pirate: 500 Brawler: 510, Buccaneer: 511 Gunslinger: 520, Corsair: 521 ================================================================================ MODULES (require) ================================================================================ Shared utility scripts live in scripts/libs/ and are imported with require(). require("module_name") -- loads scripts/libs/module_name.lua require("combat.targeting") -- loads scripts/libs/combat/targeting.lua Module names: alphanumeric, underscores, dots only (no paths like ../). Results are CACHED -- each module executes once, subsequent calls return cached value. Cache clears when any script is toggled (enabled/disabled), so edits to libs are picked up. Modules return a table of functions/values. No plugin table needed. WRITING A MODULE: -- scripts/libs/utils.lua local M = {} function M.distance(pos1, pos2) local dx = pos1.x - pos2.x local dy = pos1.y - pos2.y return math.sqrt(dx * dx + dy * dy) end function M.find_closest_mob(player_pos, map) local closest, closest_dist = nil, math.huge for _, mob in ipairs(map:get_mobs()) do if mob and mob:is_valid() then local mpos = mob:get_position() if mpos then local dist = M.distance(player_pos, mpos) if dist < closest_dist then closest_dist = dist closest = mob end end end end return closest, closest_dist end return M USING A MODULE IN A SCRIPT: local utils = require("utils") plugin = {name="Hunter", version="1.0.0", author="You", description="...", load=true} function on_tick() local player = core.object_manager.get_local_player() if not player or not player:is_valid() then return end local map = core.object_manager.get_current_map() if not map or not map:is_valid() then return end local mob = utils.find_closest_mob(player:get_position(), map) if mob then core.input.teleport_safe(mob:get_position().x, mob:get_position().y) end end COOLDOWN MANAGER MODULE: -- scripts/libs/cooldowns.lua local M = {} local timers = {} function M.ready(key, cooldown_ms) local now = core.get_update_time() if not timers[key] or now - timers[key] >= cooldown_ms then timers[key] = now return true end return false end return M MODULE RULES: - Same sandbox: no io, os, dofile, loadfile - Place require() at script top level (outside callbacks) - Modules run in global environment (shared across all scripts) - File layout: scripts/libs/*.lua for modules, scripts/*.lua for plugins ================================================================================ LUA STANDARD LIBRARY ================================================================================ AVAILABLE: Math: math.abs, math.sqrt, math.sin, math.cos, math.floor, math.ceil, etc. String: string.format, string.sub, string.len, string.find, string.upper Table: table.insert, table.remove, table.sort Pairs/IPairs: for iteration require(name): import modules from scripts/libs/ (see MODULES section) NOT AVAILABLE: os library (use core.get_update_time() for game timing, core.get_system_time() for wall clock) io library (file operations not supported) debug library loadfile / dofile (use require() for shared code instead) ================================================================================ DEBUGGING TECHNIQUES ================================================================================ 1. Log everything during development 2. Use descriptive error messages 3. Print table contents with pairs/ipairs 4. Track execution flow with log statements 5. Monitor packet opcodes in packet handlers 6. Measure execution time with get_update_time() 7. Test edge cases (empty arrays, nil values, zero HP) 8. Validate data types before operations 9. Use pcall for error handling ================================================================================ END OF REFERENCE ================================================================================ This reference covers 100% of documented Violet Lua SDK APIs. Optimized for AI/LLM consumption with maximum information density. For support: Violet community Discord ================================================================================