================================================================================ 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-07-14 (added familiar write actions: pull, fuse, rank_up, reveal_potential, set_lock, decompose) ================================================================================ 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 changelog = "What changed in this version" -- optional; auto-recorded as a marketplace release note on publish } ================================================================================ 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 core.log_computer(message: string [, event_type: string]) - Write a line to your computer's activity log on the web dashboard (the computer view), separate from the in-client console - event_type is an optional tag shown as a label (default "script"); frequent calls are rate-limited 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.get_game_version() -> string - Returns the game client version string (e.g. "8.269.1.1") - The version the client is currently running; changes with each game patch 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.logout() -> boolean - In game: log out to the character-select screen (client stays open; does NOT close the game) - At character select: jumps the login flow back to world select instead - Returns true if issued, false (no-op) otherwise (e.g. mid step-transition; retry next tick) core.terminate() - Close the game client immediately; does NOT return (process exits) - Use to actually CLOSE the client; use core.logout() to return to character select instead core.set_auto_restart(enabled: boolean) -> boolean - Enable/disable auto-restart for the game profile this client was launched from - When on, the launcher re-opens the client with the same account if it closes; saved to your account, persists across launches - Returns true if the request was sent 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. CUSTOM STORAGE (core.settings.storage) - free-form, script-owned values Key/value storage for data NOT tied to any menu setting (counters, timestamps, per-character state). Saved as part of the active profile: persists across sessions, travels with the profile, and swaps when you switch profiles. Keys use dot-paths; values can be boolean/number/string/table. Namespace keys with your script name (e.g. "my_script.runs") to avoid collisions. core.settings.storage.get(path: string) -> value | nil - Reads a stored value; nil if nothing is stored at that path core.settings.storage.set(path: string, value: boolean|number|string|table) -> boolean - Stores a value (creates the path). Any key and any type accepted (no schema) - Returns true on success core.settings.storage.delete(path: string) -> boolean - Removes a stored value; returns true if one existed core.settings.storage.all() -> table - The whole storage namespace as a table (empty if nothing stored) Example: local runs = core.settings.storage.get("my_script.runs") or 0 core.settings.storage.set("my_script.runs", runs + 1) NOTE: Storage is live immediately but only PERSISTED when the profile is saved (core.profiles.save()), like any other setting. 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 ================================================================================ PROFILES MODULE (core.profiles) - SAVED SETTINGS PROFILES ================================================================================ Read the user's saved settings profiles and switch the session between them (the same profiles shown in the web menu's profile bar). Metadata (id + name) is mirrored on the client and kept current, so list() and get_active() are instant and safe to call every tick. Switching applies a profile's full settings and takes a moment to propagate. core.profiles.list() -> table - Array of profiles, each: { id = number, name = string, active = boolean } - active = true for the profile currently applied to this session - Reads from the local metadata cache (instant) core.profiles.get_active() -> table | nil - The profile applied to this session: { id = number, name = string } - nil if no profile is active core.profiles.switch(profile: string | number) -> boolean - Switch the session to a saved profile, by name or by id - Returns true if the profile exists and the switch started (or it is already active); false if the name/id is unknown or a switch is already in progress - Asynchronous: true means accepted, not yet applied. While a switch is in flight, further switch() calls return false (reads stay instant). Poll get_active() until it reports the target to act after the swap lands. Example: if not core.profiles.switch("Bossing") then core.log_error("switch rejected (unknown profile or one already running)") end core.profiles.save([options: table]) -> boolean - Saves current settings to the ACTIVE profile (same as web menu Save button) - Returns true if the save request was sent (asynchronous, like switch()) - Full save by default. options selects a subset (everything else in the profile is left intact): sections = { "lua", ... } -- whole top-level categories keys = { "hacks.godmode", ... } -- individual setting paths Example: core.profiles.save() -- full save to active core.profiles.save({ sections = { "lua" } }) -- only the lua section core.profiles.save({ keys = { "hacks.godmode" } }) core.profiles.save_as(name: string [, options: table]) -> boolean - Creates a NEW profile from current settings (auto-deduplicates the name, e.g. "Build (1)"). Same options filter as save(). Async; returns true if sent. Example: core.profiles.save_as("Bossing Build") core.profiles.save_as("Lua Only", { sections = { "lua" } }) ================================================================================ 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_exp_percent() -> number player:get_meso() -> number player:get_sol_erda() -> 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:get_skill_sp(tier: number) -> number - SP available to spend on the given job-advancement tier (skill-window tab) - Most jobs use per-tier Extended SP; tier 0 = beginner, 1+ = job advancements player:level_up_skill(skill_id: number, count: number) - Sends a skill-up request, spending SP to raise skill_id by count levels - Server-validated (same as in-game "+"); invalid requests are rejected player:get_arcane_symbols() -> table - Array of equipped Arcane symbols (Arcane River region order; unequipped skipped) - symbol: {index 0-5, position, level, exp, max_level (20), exp_to_next, can_level_up, is_max} player:get_sacred_symbols() -> table - Array of equipped Sacred/Authentic symbols (Grandis region order) - symbol: {index 0-5, position, level, exp, max_level (11), exp_to_next, can_level_up, is_max} 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_reactors() -> table - Returns all reactors currently in the field - reactor: {id, template_id, name, state, flipped, position: {x, y}} map:get_rune() -> rune? - Returns the active rune on the map, or nil if there is none - Maps only ever have one rune at a time (single table, not a list) - rune: {position: {x, y}, solvable} - solvable is false while the rune-solved cooldown is active (a rune was solved recently) map:get_players() -> table - Returns all other players (remote characters) currently in the field - Excludes the local player - player: {id, name, position: {x, y}} 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: routes through the game's keyboard handler AND briefly pulses the key on the GetAsyncKeyState hook (so movement/up-held polling sees it too) - PREFERRED over call_wndproc for key presses (no Win32 message building, no crash risk) - One-shot tap (single key-down), so a movement key only nudges before releasing; use hold_key/release_key to walk/climb/up-jump, or move_x/move_y for coordinate moves - 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 (through the keyboard handler AND the GetAsyncKeyState hook) until release_key is called for the same key - Because it feeds GetAsyncKeyState, this is the primitive that CAN drive movement: hold an arrow to walk/climb, or hold Up before Jump to up jump - 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 - For simple key presses prefer press_key (safer, no Win32 building); use call_wndproc only for messages press_key can't send - wparam/lparam use standard Win32 layout (e.g. VK code in wparam for WM_KEYDOWN); both default to 0 - NOT for movement: directional walk/climb is read via GetAsyncKeyState and this bypasses it; use move_x/move_y. Up jumps fail too (Up-held read via GetAsyncKeyState), though a plain jump works. Fine for non-movement input (skills, pot/item hotkeys) - 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 core.login.get_total_slot_count() -> number | nil - Total purchased character slots for the account - Only valid during step 2; nil otherwise core.login.get_empty_slot_count() -> number | nil - Slots not occupied by a character (total_slot_count - char_count) - Only valid during step 2; nil otherwise core.login.get_characters() -> table | nil - Array of created characters (see CHARACTER_SLOT); nil outside step 2 CHARACTER_SLOT STRUCTURE: { index = number, -- Slot index (pass to slot-select actions) name = string, -- Character name (IGN) level = number, -- Character level job = number -- Job code } ================================================================================ 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 ================================================================================ HOW MOVEMENT INPUT WORKS: the game reads directional movement (walking left/right, climbing ladders/ropes up/down) via GetAsyncKeyState. move_x/move_y hold the arrow keys through that path and stop at a target coordinate -- the right tool for navigation, kiting, patrolling. MANUAL CONTROL: hold_key/release_key (and press_key) feed the same GetAsyncKeyState path, so they CAN drive movement -- hold an arrow to walk/climb, or hold Up before Jump to up jump. press_key only pulses briefly (a nudge), so use hold_key to sustain. EXCEPTION: call_wndproc bypasses GetAsyncKeyState entirely, so it CANNOT drive movement and an up jump through it falls back to a plain jump; use it only for non-movement, one-shot input. 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.set_facing(left: boolean) - Turn the character in place without moving; true = face left, false = face right - Resolves the move action like a direction-key tap, so the new facing is sent to the server - Only the facing flips (no step); most reliable while standing on a foothold - Read current facing with player:is_left() 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 - Equip entries (tab_id=1) also carry sn / essence_id / essence_sn / cash_item_sn core.inventory.get_equip_items() -> table - Returns unequipped equip items in inventory (tab_id=1) - These are equips in your bag, NOT currently worn - Each entry carries sn / essence_id / essence_sn / cash_item_sn (Equip only) 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 - Each entry carries sn / essence_id / essence_sn / cash_item_sn (Equip only) - 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) sn = number, -- Unique serial number; stable identity for this equip instance (Equip only, full 64-bit precision) essence_id = number, -- Essence id, or 0 if none (Equip only) essence_sn = number, -- Essence serial number, or 0 if none (Equip only) cash_item_sn = number, -- Cash-shop serial number for cash equips, or 0 if none (Equip only) 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) } ================================================================================ SHOP (core.shop) - NPC SHOP DIALOG ================================================================================ All functions require an NPC shop dialog to be open. core.shop.is_open() -> boolean - Returns true if an NPC shop dialog is currently open core.shop.get_items() -> table | nil - Returns every item the NPC sells across all tabs (i.e. what you can buy) - nil if no shop is open - Each entry's `position` is its slot in the shop list, passed to buy() core.shop.buy(item: shop_item, count: number, price?: number) -> boolean - Buys `count` of `item` (an entry from get_items(); its position/id are read for you) - Also accepts the explicit form buy(index, item_id, count [, price]) with raw numbers - For multi-tab shops, acts on the currently open tab; `index` is the slot in it - price is the PER-UNIT price; auto-resolved from the matching shop entry when omitted - Returns true if the request was sent; false (+ error string) if no shop is open or the item is no longer at that slot in the open tab and no price was given core.shop.sell(position: number, item_id: number, count: number) -> boolean - Sells `count` of the inventory item at slot `position` - Returns false if no shop is open or a previous sell is still awaiting server ack - The server rejects/DCs on quest, not-for-sale, and cash items — only sell sellable items core.shop.close() -> boolean - Closes the open shop dialog; true if a shop was open, false otherwise SHOP_ITEM STRUCTURE: { id = number, -- Item template ID position = number, -- Item's slot in the shop list (pass as `index` to buy()) count = number, -- Stock quantity price = number, -- Unit price in mesos unit_price = number, -- Per-charge unit price for rechargeables, else 0 max_per_slot = number, -- Max quantity per slot name = string -- Display name } ================================================================================ PARTY (core.party) - PARTY STATE & ACTIONS ================================================================================ Reads come from the client's party state; when not in a party they return empty/zero. Member actions (leave/kick/promote) return false when not applicable. core.party.is_in_party() -> boolean - True if currently in a party core.party.is_party_leader() -> boolean - True if you are the party leader (boss) core.party.get_party_id() -> number - Current party ID, or 0 when not in a party core.party.get_party_name() -> string - Party name, or "" when not in a party core.party.get_party_boss_id() -> number - Leader's character ID, or 0 when not in a party core.party.get_members() -> table - Array of current members (see PARTY_MEMBER); empty when not in a party core.party.is_member(who: string | number) -> boolean - True if `who` (character name or character id) is in your party core.party.create(name?: string) -> boolean - Creates a party; name optional (server applies a default when empty) - Returns false if already in a party core.party.leave() -> boolean - Leaves the party; false if not in a party core.party.invite(name: string) -> boolean - Invites the player named `name` (leader-only; server enforced) core.party.kick(name: string) -> boolean - Kicks the member named `name`; false if not a current member core.party.promote(name: string) -> boolean - Promotes the member named `name` to leader; false if not a current member core.party.apply(party_id: number) -> boolean - Applies to a listed party by id (party search) core.party.accept_invite(inviter_id: number) - Accepts a pending invite (inviter_id from on_party_invite) core.party.decline_invite(inviter_id: number) - Declines a pending invite (inviter_id from on_party_invite) on_party_invite(inviter: string, inviter_id: number, level: number, job: number, sub_job: number) -> boolean | nil - Callback fired when a party invite arrives, BEFORE the client shows its invite dialog - Return true to accept (no dialog), false to decline (no dialog), nil/no return to let the normal dialog appear - inviter: inviter's IGN; inviter_id: their character ID (keep it to answer later via accept_invite/decline_invite) - level / job / sub_job describe the inviter - First script to return a non-nil value decides; later scripts don't see that invite - Requires plugin metadata structure PARTY_MEMBER STRUCTURE: { id = number, -- Member character ID name = string, -- Character name (IGN) level = number, -- Character level job = number, -- Job code sub_job = number, -- Sub-job code (0 for most jobs) channel = number, -- Channel the member is on field_id = number, -- Map ID the member is in is_boss = boolean -- True for the party leader } ================================================================================ PROFESSION (core.profession) - LIFE SKILL LEVELS & EXP ================================================================================ Level/EXP for each gathering and crafting profession, plus shared crafting fatigue. Every getter returns 0 when the profession isn't learned or character data isn't available yet. core.profession.get_herbalism_level() -> number core.profession.get_herbalism_exp() -> number core.profession.get_mining_level() -> number core.profession.get_mining_exp() -> number core.profession.get_smithing_level() -> number core.profession.get_smithing_exp() -> number core.profession.get_accessory_crafting_level() -> number core.profession.get_accessory_crafting_exp() -> number core.profession.get_alchemy_level() -> number core.profession.get_alchemy_exp() -> number core.profession.get_fatigue() -> number - Current crafting fatigue (shared across crafting professions) ================================================================================ 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 register_date = number -- When obtained, as a Unix timestamp (seconds); 0 if unset } 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, last_fatigue_time - last_fatigue_time is a Unix timestamp (seconds); 0 if unset core.familiar.get_active_familiar_max_count() -> number - Max simultaneously-active familiars (dynamic cap: 1, 2, or 3); raised by familiar slot-expansion quests - Returns 0 if character data is unavailable 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 core.familiar.pull(inv_pos: number, card_item_id: number, count: number) -> boolean - Consumes `count` familiar cards (item card_item_id) from USE-tab slot inv_pos to add familiars - count defaults to 1; exclusive request — returns false (sends nothing) when a request is already pending, retry next tick core.familiar.fuse(target_sn: number, fodder: number | table) -> boolean - Feeds fodder familiar(s) into target_sn to raise its fusion gauge (grade exp) - fodder is an SN or array of SNs (max 255/call); validates target + fodder, returns false on unknown SN core.familiar.rank_up(sn: number) -> boolean - Ranks the familiar up a grade (needs max level + full gauge in-game) - Rerolls the grade's potential, so reveal/lock keepers first; returns false if sn not owned core.familiar.reveal_potential(sn: number) -> boolean - Reveals hidden potential lines on an Epic+ familiar (appears in `potentials` after round-trip) - Returns false if sn not owned core.familiar.set_lock(sn: number, locked: boolean) -> boolean - Locks/unlocks a familiar; locked defaults to true. Toggle under the hood, so it sends only on change (no-op returns true) - Returns false if sn not owned core.familiar.decompose(familiars: number | table) -> boolean - Decomposes (extracts) familiar(s) for materials; familiars is an SN or array of SNs - Validates each against owned-familiar map, returns false on unknown SN ================================================================================ INNER ABILITY (core.inner_ability) - READ + CIRCULATE ACTIVE PRESET ================================================================================ Read access to the character's Inner Ability lines for the active preset, plus circulate() to re-roll them (spending Honor EXP). 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 core.inner_ability.get_active_preset() -> number - Returns the index of the currently active Inner Ability preset (0-based), or -1 if none - This is the preset get_current() reads and the default target of circulate() core.inner_ability.get_honor_exp() -> number - Returns the character's current Honor EXP — the currency spent circulating Inner Ability (capped at 9,999,999) core.inner_ability.circulate(preset_index?: number, lock_positions?: table) -> boolean - Re-rolls (circulates) the Inner Ability lines, spending Honor EXP - preset_index (0-based) defaults to the active preset - lock_positions is an optional array of slot numbers (1-3) to keep — the rest are re-rolled; omit to re-roll all lines - Locking is only allowed once the preset is Unique grade or higher, and at least one slot must be left to re-roll - Example: -- Keep slot 1, re-roll the rest core.inner_ability.circulate(core.inner_ability.get_active_preset(), { 1 }) ================================================================================ 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.get_quest_progress(quest_id: number) -> table | nil - Read-only view of a quest's completion requirements + live progress; nil if no demand - Fields: id, name, state ("available"/"in_progress"/"complete"/"not_available"), objective ("kill"/"collect"/"reach"/"talk"/"condition"/"unknown"), target_npc (number|nil), target_map (number|nil, for "reach"), min_level/max_level (number|nil), items (array), mobs (array), conditions (array) - items[i] = { id, need, have } -- collect; have = live inventory count - mobs[i] = { id, need, have, order } -- kill; id = monster id, have = live kill count (server-updated), order = quest step the requirement belongs to (1-based) - conditions[i] = { key, need, have, cond } -- record rule: record[key] need cond is "=="/">="/"<="; have = current record value story/scenario demand: key = a scripted flag (objective = "condition") rare: some quests express kills as conditions keyed by a monster id - a condition is met when have need holds - meso cost is NOT included (not in the current client's demand data) 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 modules live under libs/ in your Lua tree (managed in the web menu) and are imported with require(). require("module_name") -- loads libs/module_name.lua from your Lua tree require("combat.targeting") -- loads 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: -- 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: -- 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 (in your Lua tree): libs/*.lua for modules, top-level *.lua for plugins ================================================================================ MARKETPLACE LIBRARIES (publishing & the @ convention) ================================================================================ Two kinds of "library": - PUBLIC library: a standalone marketplace product others require() in their own scripts (free or paid, reviewed before listing). - PRIVATE library: module files bundled INSIDE your own script's package. Not sold or listed; only that script uses them; they ship to the script's buyers. Your unique publisher HANDLE (e.g. "acme") owns the folder libs//. Both kinds live there, so nothing collides with another developer. A PUBLIC library named "combat" by handle "acme": entry file: libs/acme/combat.lua (a module: returns a table, no plugin) submodule: libs/acme/combat/targeting.lua consumers: require("@acme.combat") and require("@acme.combat.targeting") THE @ CONVENTION: require("@acme.combat") -- a marketplace (public) library require("acme.util") -- your own bundled private module The leading "@" marks marketplace code. It is OPTIONAL: require("@acme.combat") and require("acme.combat") resolve to the same file. Use "@" for marketplace libraries so third-party imports stand out. A PRIVATE library is just modules under libs// in a script's package: my_script.lua -- the runnable plugin libs/acme/my_script/helpers.lua -- private, require("acme.my_script.helpers") DEPENDENCIES: to use someone else's public library, declare it as a dependency (don't copy its files). It auto-installs for your buyers. Only FREE public libraries can be dependencies; paid libraries are bought directly. ================================================================================ 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 your Lua tree's 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 ================================================================================