================================================================================ 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-08-24 (link to the version history) Version history and diffs: https://docs.violet.cx/llm-reference/history If this copy is older than the current version, fetch the diff from there and apply it before writing scripts. ================================================================================ PRIVACY AND CONSENT ================================================================================ STRICTLY PROHIBITED: using any Violet Lua API, including core.http and core.get_scripts(), to scrape, collect, exfiltrate, or otherwise harvest a user's script data, settings, credentials, account data, or private gameplay data. Only access or send user data with clear user approval and for the specific workflow the user agreed to. ================================================================================ 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(stage: number) - Called every ~30ms in every game stage - stage is numeric; compare it with core.stage constants, never strings or raw numbers - Continues running during login, Cash Shop, and Auction House - Field-only player, map, movement, and combat logic must require core.stage.FIELD READ-ONLY STAGE CONSTANTS: core.stage.LOGO -- Startup logo core.stage.LOGIN -- Login, world selection, or character selection core.stage.FIELD -- Active field or map core.stage.CASH_SHOP -- Cash Shop core.stage.AUCTION_HOUSE -- Auction House or Global Market core.stage.INTER_STAGE -- Intermediate transition core.stage.LOADING -- Loading core.stage.GATEWAY -- Gateway core.stage.UNKNOWN -- No active recognized stage on_map_load() - Called when loading a new map - Use for map-specific initialization - Called BEFORE on_tick for the new map 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(stage) if stage ~= core.stage.FIELD then return end 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.get_utc_time() -> table - Returns current UTC time as a table (same fields as get_system_time, but UTC instead of local timezone) - 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) - Use for timing against server events regardless of the client's local timezone 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.get_scripts() -> table - Returns all discovered Lua scripts with enabled and loaded booleans - Each loaded script includes settings registered by that script - Each setting includes its full path in id, type, label, and current value - Use this to discover script setting paths instead of guessing them - Disabled scripts have an empty settings table because their code is not loaded - STRICTLY PROHIBITED: using this function to scrape, collect, exfiltrate, or otherwise harvest a user's script data or settings. Only use it for user-approved local discovery and configuration workflows. Return shape: { { filename = "auto_farm.lua", settings_key = "auto_farm.lua", name = "Auto Farm", description = "Automatically farms selected maps", author = "Violet", version = "1.0.0", source = "server", enabled = true, loaded = true, settings = { { id = "lua.auto_farm.enabled", type = "checkbox", label = "Enable Farming", tooltip = "Toggles Auto Farm", tab = "Lua", panel = "Auto Farm", order = 0, sourceScript = "auto_farm.lua", value = true }, { id = "lua.auto_farm.attack_delay", type = "slider_int", label = "Attack Delay", tooltip = "", tab = "Lua", panel = "Auto Farm", order = 1, sourceScript = "auto_farm.lua", min = 50, max = 1000, step = 50, value = 200 } } }, { filename = "boss_helper.lua", settings_key = "boss_helper.lua", name = "Boss Helper", description = "", author = "", version = "", source = "local", enabled = false, loaded = false, settings = {} } } Example: for _, script in ipairs(core.get_scripts()) do for _, setting in ipairs(script.settings) do core.log(setting.id .. " = " .. tostring(setting.value)) end end 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: { enabled = bool, name = string, id = int, delay = int, hits = int, type = int } enabled (default true) and name (display label, default "") are optional 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, player, admin. - Idempotent: re-running with an existing ID returns a handle to it. COMMON OPTIONS (all constructors): id (req), label, panel (req), tab (def "Lua"), section (in-page sub-tab, see SECTIONS), 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 TAB ORGANIZATION (core.ui.tab) - nav metadata for YOUR tabs; returns nothing: core.ui.tab{ name=(req), icon="swords", group="Azura's Scripts", group_icon="crown", order=1 } - icon/group_icon: kebab-case lucide name from the curated set below (unknown -> default icon) - group: your tabs sharing the same label fold into ONE collapsible sidebar entry (submenu); built-in tabs can never be regrouped/re-iconed - order: sort among script tabs / within the group (lower first) - reserved tab names rejected, plus the shared "Lua" tab Icon set: activity, alarm-clock, anchor, archive, award, axe, backpack, banknote, bell, bird, bomb, book, book-open, bot, braces, brain, brush, bug, calendar, camera, car, castle, circle-dollar-sign, clock, cloud-lightning, code-2, cog, coins, compass, cpu, crosshair, crown, database, diamond, dices, dna, droplet, egg, eye, eye-off, feather, file-code, filter, fish, flag, flame, flask-conical, footprints, gamepad-2, gauge, gem, ghost, gift, glasses, hammer, hash, heart, hourglass, house, infinity, key, landmark, layers, layout-grid, leaf, link, list, lock, magnet, mail, map, map-pin, medal, message-square, moon, mountain, music, navigation, package, palette, pause, paw-print, pen, percent, pickaxe, pin, plane, play, puzzle, rabbit, refresh-cw, repeat, rocket, route, ruler, scale, scissors, scroll, scroll-text, search, send, settings, shield, shield-check, ship, shopping-cart, skull, sliders-horizontal, snowflake, sparkles, sprout, star, store, sun, sword, swords, table, target, tent, terminal, test-tube, timer, trending-up, trees, trophy, truck, umbrella, unlock, user, users, wallet, wand-2, watch, waves, wheat, wrench, zap SECTIONS (in-page sub-tabs): pass section="Buffs" on elements -> panels sharing a section render under an in-page tab strip (like Player > Inventory); panels WITHOUT a section stay pinned above the strip (put master toggles there). Sections appear in first-use order; keep all elements of one panel in the same section. Display-only: IDs, values, and profiles are unaffected. 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_section(s) -- "" detaches from strip 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(stage) 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 ================================================================================ FLAME MODULE (core.flame) - AUTO FLAME CONTROL ================================================================================ Drives the Auto Flame state machine (Autos -> Auto Flame 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. Rerolls an equip's additional options (rebirth-flame bonus stats) and stops when a roll raises the item's COMBAT POWER -- not per-stat matching like Cube. The equip's current CP is re-read before each flame as the score to beat. Target must be in the Equip INVENTORY (positive slot). An already-equipped item scores against itself (always a 0 change) and is refused with status item_equipped. core.flame.start([opts: table]) -> boolean, string? - Configures and starts an Auto Flame 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 -- Equip inventory slot; POSITIVE only (equipped/negative is refused) item_id = number -- flame item id to spend; 0 = any flame on hand (default) max_flames = number -- safety cap, stop after this many flames (1..999; default 10) Example: local ok, err = core.flame.start({ slot = 5, item_id = 0, max_flames = 50 }) core.flame.stop() -> boolean - Disables Auto Flame; returns true if toggle flipped - Safe to call when not running core.flame.is_running() -> boolean - True if the Enable Auto Flame checkbox is on core.flame.get_status() -> table - Snapshot of the current run - Fields: running = boolean state = "idle"|"waiting_result"|"evaluating_roll"|"waiting_commit"|"checking_result" slot = number flames_used = number -- flames used this run (the Max Flames cap is measured against this) run_start_cp = number -- equip CP when the run started current_cp = number -- equip CP after the most recent roll baseline_cp = number -- score the next roll must beat (current CP, re-read each flame) result = table -- live status object pushed to the web UI: status = "flaming"|"rolling"|"done"|"item_not_found"|"item_equipped"|"no_flames"|"max_reached" flamesUsed = number oldCp = number? -- equip CP at run start (once scored) newCp = number? -- equip CP after latest roll ================================================================================ AUTO CHAR MODULE (core.autochar) - SCRIPTED CHARACTER CREATION ================================================================================ Wraps the Auto Char machinery (Autos -> Auto Login panel). Writes through the same settings the web UI uses (class, Burning flag, enabled state stay in sync). Two modes: - Ambient (enable): create whenever the configured Auto Login slot is empty, using generated names, unattended. - One-shot (create): a single script-initiated attempt with a terminal outcome you poll via get_status().request. Creation runs from the character-select screen forward (pick empty slot -> class -> name -> intro story). create() drives it from char select; world select stays Auto Login's job. Run at character select or with Auto Login enabled. create() lifecycle -- get_status().request is the single field a retry loop polls: "none" = no create() in play "pending" = in flight (routing/submitting/awaiting verdict); survives disconnects "finishing" = name accepted (character exists); story clicker running "created" = terminal success (latched until next create()/cancel()) "rejected" = terminal: script name refused; flow returned to char select and HOLDS there (won't log into an existing char) until retry/cancel. name.code = server reason. Retry = another create()/set_name() with a new name (class is remembered, so opts can be omitted). Names vs generator: the return-to-char-select-on-reject behavior applies ONLY to a script-supplied name. Omit name and the generator retries names in place until accepted (request goes straight pending -> created, never bounces). core.autochar.enable([opts: table]) -> boolean, string? - Ambient mode: auto-create when the configured Auto Login slot is empty - opts fields (all optional, shared with create): class = string|number -- class label (case-insensitive) or 0-based index burning = boolean -- accept a Burning prompt if it appears (default off) name = string -- specific IGN (1-12 alphanumeric); omit = generated - Returns true, or false + error (unknown class, invalid name, module not ready) core.autochar.disable() -> boolean - Turns off the ambient checkbox; a one-shot create() keeps running (use cancel) - Returns false only if the autos module isn't loaded core.autochar.create([opts: table]) -> boolean, string? - One-shot atomic attempt, independent of the checkbox (opts same as enable) - Omitting name uses the generator; request survives disconnects while pending - Returns true if started, or false + error if a creation is already in flight (pending/finishing), the class/name is invalid, or the module isn't ready. Calling from a terminal state (created/rejected) or none starts a fresh attempt - Example: core.autochar.create({ class = "Kanna", name = "IceFarmer" }) core.autochar.cancel() -> boolean - Withdraws a create() request and clears the name latch (incl. created/rejected) - Does not touch the ambient checkbox core.autochar.set_name(name: string | nil) -> boolean, string? - Arms (or clears, with nil) the script-supplied IGN; verdict via get_status().name - During a create() holding at char select after a rejection, arming a new name resumes it in place (same as calling create() again) - Name must be 1-12 alphanumeric, else returns false + error core.autochar.get_status() -> table | nil - Snapshot of Auto Char state, or nil if the module isn't loaded - Fields: enabled = boolean -- ambient checkbox on class = string -- selected class label class_index = number -- 0-based class index set_burning = boolean request = "none"|"pending"|"finishing"|"created"|"rejected" phase = "idle"|"selecting_class"|"naming"|"story" last_submitted_name = string -- most recent IGN (accepted one after success) name = { value = string, state = "none"|"pending"|"accepted"|"rejected", code = number } -- 0 accepted, else rejection code core.autochar.get_classes() -> table - Array of class label strings in dropdown order - Array index minus one = the 0-based class index enable/create accept - Labels matched case-insensitively ================================================================================ 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_session_stats() -> table | nil - This session's earnings and rates: {runtime_sec, meso_gained, meso_per_hour, exp_gained, exp_per_hour, exp_percent_per_hour}; nil before the first sample - Rates are per hour, matching the web dashboard; divide by 60 for per-minute - Totals are live immediately; rates come off a rolling 5-minute window and read 0 until it spans at least a minute. Rates also return to 0 once the newest sample is over a minute old, so ALWAYS treat a rate of 0 as "no rate available", never as "earning nothing" - Gross totals: spending meso does not reduce meso_gained, and levelling up does not reset exp_gained - Sampled only while in a field; parking in the cash shop or auction house freezes the totals (they do not reset) and zeroes the rates, which need ~1 min of fresh samples after returning player:get_stats() -> table | nil - Snapshot of the primary stats: {str, dex, int, luk, ap}; nil if character data is not loaded - Totals as shown in the stat window (base + gear + buffs); ap is unspent ability points player:is_alive() -> boolean | nil - false while dead (HP 0, tombstone up); nil if the character context is unavailable player:get_pets() -> table - Summoned pets; each entry {repleteness, is_active} - repleteness is the 0-100 fullness meter; it decays and the pet despawns at 0, so watch it and feed 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, unique_id, position: {x, y}, type, own_type, owner_id, source_id} - id = item ID (shared by every drop of that item); unique_id = this one drop on the ground, stable for its lifetime - Already filtered to drops you may loot, so own_type says WHY it is yours: 0 = personal, 1|4 = your party's, else free-for-all - owner_id = your character ID or party ID, per own_type; source_id = mob:get_unique_id() of the mob that dropped it 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_bounds() -> bounds? - Returns the map's boundary rectangle, or nil if the physical space isn't loaded (map transition) - bounds: {left, top, right, bottom, width, height, center: {x, y}} - Same bounds core.input.teleport_safe clamps against; a position inside this rect is never clamped - center is the rect midpoint — where Kami idles when no mobs are alive - center includes empty airspace, so on tall maps it can sit above the floor; cross-reference get_footholds() for walkable ground 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 (species; same for every mob of that type) mob:get_unique_id() -> number | nil - Returns server-assigned unique instance ID of this specific spawned mob - Different for every mob in the map, stable for the mob's lifetime, never reused - Use to track/target one exact mob across ticks mob:get_position() -> {x: number, y: number} | nil - Returns position or nil mob:get_name() -> string | nil - Returns mob name or nil mob:distance() -> number | nil - Euclidean distance from the local player to this mob; nil if the mob or player is unavailable mob:get_health_percent() -> number | nil - Returns remaining health as a whole number 0-100, or nil - The client is never told a mob's real HP; the server broadcasts the HP bar, so a percentage is the finest reading that exists - Multi-body bosses sharing one bar all report that shared bar mob:get_health_gauge() -> number, number | nil - Returns the raw pair behind get_health_percent(): the broadcast bar value, and what it counts up to (NOT real max HP) - Only needed when whole percent is too coarse; a max of 1000 gives 0.1% resolution, a max of 0 means the value is already a percentage - Unlike get_health_percent(), this is the mob's own bar — it does not follow a multi-body boss to the shared bar mob:get_rank() -> number | nil - Elite Monster-system tier: 0 = not elite, 1 = elite monster, 2 = elite champion, 3 = elite boss - Use instead of is_elite() when the tiers matter; is_elite() is true for all three mob:is_elite() -> boolean | nil - Returns true if in the Elite Monster system (elite monster OR elite boss) - True for both a trivial elite and a boss-type elite; use is_boss() to tell them apart - nil if unknown mob:is_boss() -> boolean | nil - Returns true if the mob's template is flagged as a boss (field boss / boss-type elite: boss HP bar + boss mechanics) - Independent of is_elite() (a mob can be both); use to single out a boss-type elite from trivial elites on a farming map - 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.loot() -> boolean - Picks up drops already within reach of the player (same as pressing the loot key) - Reach is the client's pickup box: x +/- 25, y from -50 to +10 around the player - Does NOT auto-walk - move the player onto the drop first; map:get_drops() gives positions - Returns false if no player or nothing in range; true if a pickup was attempted - The client has its own 3s per-drop retry cooldown, so calling every tick is wasted 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 ================================================================================ Use core.login from on_tick(stage) when stage == core.stage.LOGIN to inspect the login flow and manage the character list. Character actions require an idle character-selection state. A true action result means submitted, not completed. Steps: 1=world/channel select, 2=character select, 3=new-character setup, 4=new-char name entry. Other values may appear briefly during login transitions. 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 available character slots for the current character-selection roster - Only valid during step 2; nil otherwise core.login.get_empty_slot_count() -> number | nil - Number of unoccupied character slots - Only valid during step 2; nil otherwise core.login.get_characters() -> table | nil - Array of created characters (see LOGIN_CHARACTER); nil outside step 2 core.login.move_character_to_front(character: login_character | number) -> boolean - Moves a character to the front of the displayed order core.login.soft_delete_character(character: login_character | number) -> boolean - Starts the deletion waiting period for an eligible active character core.login.hard_delete_character(character: login_character | number) -> boolean - Permanently deletes an eligible character after its waiting period core.login.restore_character(character: login_character | number) -> boolean - Restores a character that is pending deletion Character arguments may be a get_characters() record, character ID, or zero-based display index. Prefer the record; numbers resolve as IDs first, then indices. LOGIN_CHARACTER STRUCTURE: { id = number, -- Stable character ID index = number, -- Zero-based display position name = string, -- Character name level = number, -- Character level job = number, -- Job ID ghost = boolean, -- True while deletion is pending delete_deadline = number, -- Unix timestamp; 0 when not pending deletion deletion_ready = boolean -- True when permanent deletion is eligible } ================================================================================ 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). STRICTLY PROHIBITED: using core.http to scrape, collect, exfiltrate, or otherwise harvest a user's script data, settings, credentials, account data, or private gameplay data. Only send data with clear user approval and for the specific workflow the user agreed to. 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(skill_id: number) -> skill | nil - Returns skill object or nil - Skill: {id, name, level, max_level} core.skill_book.get_skills() -> table - Returns all learned skills - Skill: {id, name, level, max_level} core.skill_book.get_skill_level(skill_id: number) -> number - Returns skill level (0 if not learned) core.skill_book.get_skill_max_level(skill_id: number) -> number | nil - Returns effective max (master) level incl. hyper/5th-job caps; nil if not in skill book core.skill_book.is_skill_on_cooldown(skill_id: number) -> boolean - Returns true if on cooldown core.skill_book.get_skill_cooldown_remaining(skill_id: number) -> number - Returns remaining cooldown in ms (0 if off cooldown); matches is_skill_on_cooldown ================================================================================ LINK SKILLS ================================================================================ MODULE: core.link_skills PRESET NUMBERING: - Lua presets are 0, 1, and 2 (in-game Presets 1, 2, and 3) - get_assigned() returns a normal 1-based sequential Lua array - Each preset holds at most 12 skills RETURN CONVENTIONS: - Reads return value, or nil, error_message - Actions return true after dispatch, or false, error_message - true means the client sent the request, not that the server accepted it - Confirm mutations with a later on_tick; immediate reads may be stale core.link_skills.get_active_preset() -> number | nil, string? - Returns active zero-based preset index: 0, 1, or 2 core.link_skills.get_assigned(preset?: number) -> table | nil, string? - Returns assigned skill IDs for a preset - Omit preset or pass nil to read the active preset - Empty preset returns an empty table core.link_skills.assign(skill_id: number) -> boolean, string? - Requests assignment of one skill to the active preset only - Accepts positive signed 32-bit IDs; the constants table is not a whitelist - Fails locally for a duplicate or 12-skill preset in the snapshot at call time core.link_skills.unassign(skill_id: number) -> boolean, string? - Requests removal of one skill from the active preset only - Accepts positive signed 32-bit IDs; the constants table is not a whitelist - Fails locally when the skill is absent from the snapshot at call time core.link_skills.activate_preset(preset: number) -> boolean, string? - Activates preset 0, 1, or 2 - Fails locally when that preset is already active - To edit an inactive preset: activate it, poll until active, then mutate SKILL CONSTANTS (core.link_skills.skills convenience catalog; mutable, not a whitelist): FURY_UNLEASHED=80000001 PHANTOM_INSTINCT=80000002 MOONLIT_BLADE_LEARNINGS=80000003 ELEMENTALISM=80000004 LIGHT_WASH=80000005 IRON_WILL=80000006 HYBRID_LOGIC=80000047 WILD_RAGE=80000050 CYGNUS_BLESSING=80000055 RHINNES_BLESSING=80000110 CLOSE_CALL=80000169 JUDGMENT=80000188 UNFAIR_ADVANTAGE=80000261 TIDE_OF_BATTLE=80000268 SPIRIT_OF_FREEDOM=80000329 RUNE_PERSISTENCE=80000369 COMBO_KILL_BLESSING=80000370 SOLUS=80000514 BRAVADO=80000609 ELVEN_BLESSING=80001040 KNIGHTS_WATCH=80001140 TERMS_AND_CONDITIONS=80001155 INVINCIBLE_BELIEF=80002758 EMPIRICAL_KNOWLEDGE=80002762 ADVENTURERS_CURIOSITY=80002766 THIEFS_CUNNING=80002770 PIRATE_BLESSING=80002774 NOBLESSE=80002857 TIME_TO_PREPARE=80003015 NATURES_FRIEND=80003058 INNATE_GIFT=80003224 GROUNDED_BODY=80003877 FOCUS_SPIRIT=80010006 GUIDING_STARS=80010486 QI_CULTIVATION=80011964 Prefer constants over numeric IDs. Sia source ID 80010343 and Erel source ID 80010473 normalize to the shared assignable GUIDING_STARS ID 80010486. EXAMPLE: local links = core.link_skills local assigned, err = links.get_assigned() if not assigned then core.log(err); return end for slot, skill_id in ipairs(assigned) do core.log(string.format("Link slot %d: %d", slot, skill_id)) end local ok, assign_err = links.assign(links.skills.WILD_RAGE) if not ok then core.log(assign_err) end ================================================================================ 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 } ================================================================================ STORAGE (core.storage) ================================================================================ Requires an open storage dialog. Actions return false when storage is unavailable, busy, or a well-formed transfer is ineligible. Invalid numeric arguments raise a Lua error. Direct-transfer true means submitted, not completed; bulk-action true means the flow started and confirmation may still be required. STORAGE_ITEM STRUCTURE: { id = number, -- Item ID count = number, -- Available quantity position = number, -- Inventory or storage position index = number, -- Zero-based list index accepted by transfer functions tab_id = number, -- Inventory category name = string? -- Omitted when unavailable } core.storage.is_open() -> boolean core.storage.is_busy() -> boolean core.storage.get_meso() -> number | nil - Meso balance held in storage core.storage.get_items() -> table | nil - Items currently held in storage core.storage.get_inventory_items() -> table | nil - Character inventory items shown by the storage interface core.storage.withdraw(item_or_index: storage_item | number, count?: number) -> boolean - Withdraws a returned storage item or zero-based index; count defaults to full stack - Invalid index/count types or ranges raise a Lua error core.storage.deposit(item_or_index: storage_item | number, count?: number) -> boolean - Deposits a returned inventory item or zero-based index; count defaults to full stack - Invalid index/count types or ranges raise a Lua error core.storage.withdraw_meso(amount: number) -> boolean core.storage.deposit_meso(amount: number) -> boolean - Transfers a positive whole-number amount after balance and limit validation - A malformed or non-positive amount raises a Lua error core.storage.withdraw_all() -> boolean core.storage.deposit_all() -> boolean - Takes no parameters; presses the storage window's bulk-transfer control, so storage must be open - False when storage is closed or a request is pending; true means started, and the game may still ask the player to confirm core.storage.close() -> boolean - Closes storage when no action is pending ================================================================================ TRADE (core.trade) ================================================================================ Targets passed to request() or request_cash() are a character name or positive ID present in the current field. True from a networked trade action means submitted, not completed. Opening the native prompt and changing the allowlist complete locally. Item, Meso, and acceptance actions apply to regular trades; cancellation supports regular and cash trades. core.trade.is_open() -> boolean - Whether a regular trade is open core.trade.is_cash_open() -> boolean core.trade.is_trading() -> boolean core.trade.is_accept_pending() -> boolean core.trade.get_state() -> number | nil - Current regular-trade stage; treat as observational core.trade.request(target: string | number) -> boolean, string? core.trade.request_cash(target: string | number) -> boolean, string? - Cash trade initiation requires its entry item core.trade.put_meso(amount: number) -> boolean, string? - Places Mesos over the network; the in-game Meso prompt does not need to be open and is never opened - amount must be positive, <= the current balance, and <= 9999999999999 core.trade.put_item(item_or_tab_id: table | number, count_or_position?: number, count?: number) -> boolean, string? - Two call forms; the position of count differs between them - put_item(item, count?): item is a core.inventory record and must have tab_id and position; count is argument 2 - put_item(tab_id, position, count?): tab_id 1 Equip, 2 Use, 3 Setup, 4 Etc, 5 Cash; position is 1-based; count is argument 3 - An omitted count uses the whole held stack; count may not exceed the held quantity or 65535 core.trade.put_item_by_id(item_id: number, count?: number) -> boolean, string? - Finds and places a matching inventory item core.trade.accept() -> boolean, string? - Accepts when another accept action is not already pending core.trade.cancel() -> boolean, string? - Cancels the current regular or cash trade core.trade.allowlist.add(target: string | number) -> boolean, string? core.trade.allowlist.remove(target: string | number) -> boolean, string? core.trade.allowlist.contains(target: string | number) -> boolean, string? core.trade.allowlist.get_all() -> { names: string[], ids: number[] } core.trade.allowlist.clear() -> boolean - Allowlisted incoming invitations are accepted automatically - Names are case-insensitive; names and IDs are stored separately - The allowlist lasts for the current Violet session ================================================================================ MESO MARKET (core.meso_market) ================================================================================ Reads require the Meso Market to be open and return nil when unavailable. A rate is Maple Points per 100,000,000 Mesos; count is a number of 100,000,000-Meso units. Valid orders use rate 100..50,000 and count 1..50. Invalid arguments raise a Lua error. True means submitted, not completed; false means the open market could not submit the otherwise valid request. core.meso_market.is_open() -> boolean core.meso_market.get_average_rate() -> number | nil - Displayed recent average in Maple Points per 100,000,000 Mesos core.meso_market.get_remaining_listings() -> number | nil - Remaining buy/sell registration count reported by the open market; not market liquidity core.meso_market.get_sell_rate() -> number | nil core.meso_market.get_buy_rate() -> number | nil - Current immediate-trade rates for each side; either may be 0 when no quote is available core.meso_market.buy(rate: number, count: number) -> boolean - Requests count * 100,000,000 Mesos for rate * count Maple Points core.meso_market.sell(rate: number, count: number) -> boolean - Offers count * 100,000,000 Mesos for rate * count Maple Points; server rules and fees still apply ================================================================================ AUCTION HOUSE (core.auction) ================================================================================ Listing actions validate the current sale state and return false when unavailable, incomplete, or busy. True means submitted, not completed. on_tick(stage) continues running here; require stage == core.stage.AUCTION_HOUSE for Auction House-specific logic. core.auction.is_open() -> boolean core.auction.enter() -> boolean - Requests entry; false when unavailable or already open core.auction.leave() -> boolean - Requests departure; false when the Auction House is not open core.auction.submit_current_listing() -> boolean - Takes no parameters; presses Submit on the listing the player prepared, so the sell view must be open with a listing filled in core.auction.cancel_selected_listing() -> boolean - Takes no parameters; presses Cancel on whichever listing the player has selected on screen ================================================================================ CASH SHOP (core.cash_shop) ================================================================================ Locker reads require an open Cash Shop; transfers additionally require the locker to be idle. Prefer passing complete item records returned by Violet rather than constructing records manually. on_tick(stage) continues running here; require stage == core.stage.CASH_SHOP for Cash Shop-specific logic. LOCKER_ITEM STRUCTURE: { cash_sn = number, -- Opaque locker identifier id = number, -- Item ID tab_id = number, -- Destination inventory category name = string?, -- Omitted when unavailable location = "locker", is_in_locker = true, is_in_inventory = false, owner_account_id = number? -- Owning account; omitted when unavailable } core.cash_shop.is_open() -> boolean core.cash_shop.enter() -> boolean - Requests entry; false outside a supported field state or when already open core.cash_shop.leave() -> boolean - Requests departure; false when the Cash Shop is closed or the locker is busy core.cash_shop.locker.is_busy() -> boolean core.cash_shop.locker.get_items() -> table | nil - Returns currently visible locker items, or nil when the locker is unavailable core.cash_shop.locker.withdraw_regular(item: locker_item, destination_slot: number) -> boolean - Withdraws an eligible returned locker item into an inventory slot - True means submitted; wait until is_busy() is false before reading or changing the locker again core.cash_shop.locker.deposit(inventory_item: table) -> boolean - Two call forms: deposit(inventory_item) or deposit(cash_sn, item_id, inventory_type, source_slot) - The record form needs id, tab_id and position from core.inventory.get_cash_items(); tab_id/inventory_type must be 5 or 6 - The four-argument form re-checks cash_sn against the serial live in source_slot and fails if it no longer matches - True means submitted; malformed, stale, or non-cash records raise a Lua error - An otherwise valid request returns false when the locker is unavailable, busy, or rejects it ================================================================================ GUILD (core.guild) ================================================================================ Guild reads return nil plus an error string when current-character data is unavailable. A character outside a guild has ID 0, an empty name, and no members. core.guild.get_id() -> number | nil, string? core.guild.get_name() -> string | nil, string? core.guild.is_in_guild() -> boolean | nil, string? core.guild.get_members() -> table | nil, string? - Returns the guild roster as `{ id = number, name = string }` records - Returns an empty table when the character is not in a guild core.guild.get_pending_applications() -> table | nil, string? - Returns outgoing `{ guild_id = number, guild_name = string }` applications - Returns nil plus an error string until the Guild search view has loaded them core.guild.move_to_castle() -> boolean, string? - Requests entry to the current guild's Castle - Returns false plus an error string when the action is unavailable ================================================================================ 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.summon() -> boolean - Summons the familiars assigned to the currently active preset; takes no SN or preset argument - If familiars are already summoned, returns true without toggling them off - Returns false if the game context, player, or summon skill is unavailable; true means already active or cast dispatched, not server-confirmed core.familiar.pull(inv_pos: number, card_item_id: number, count?: number) -> boolean - Opens familiar cards (item card_item_id) to add familiars. inv_pos is only a hint — the card is looked up by id and whichever USE slot holds it is used, so a stale/wrong inv_pos still works - count defaults to the whole stack and is clamped to the number owned - Returns false (sends nothing) if the card isn't in the USE tab, or if an exclusive request is already pending (throttled ~500ms) — retry next tick; pass the full count in one call rather than looping 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 ================================================================================ V MATRIX (core.vmatrix) ================================================================================ Reads return value or nil,error. Actions return true when the request is sent, or false,error when validation prevents the send. A successful send is not server confirmation. Send one mutation at a time and verify on a later on_tick; on_packet_recv runs before the native V Matrix state update. IDs, counts, and levels must be positive whole numbers. Core IDs, target levels, and craft counts must fit a signed 32-bit integer. POINTS STRUCTURE: { available = number, -- Spendable V Points allocated = number, -- V Points allocated to cores total = number, -- available + allocated nodestone_cost = number, -- V Points to craft one Nodestone activation_cost = number, -- Configured special-core registration cost reset_meso_cost = number -- Mesos to reset allocated V Points } - activation_cost is informational; activate_core does not locally validate or deduct that balance NODESTONE STRUCTURE: { item_id = number, -- Nodestone item ID count = number, -- Inventory count max_use = number, -- Safe batch under inventory/V Point limits points_per_item = number -- V Points granted per item } - max_use may be capped at 1 for single-use flows; it does not bypass quest/map/client-state restrictions V MATRIX CORE STRUCTURE: { id = number, -- ID passed to all core actions level = number, max_level = number, -- 0 when unavailable type = string, -- "skill" | "boost" | "special" | "unknown" active = boolean, -- Special-core activation state expires_at = number, -- Unix timestamp; 0 = no expiration expired = boolean, extension_cost = number -- Quoted V Points; 0 = no configured extension } - Positive extension_cost is not a full eligibility/affordability check; extend_core revalidates core.vmatrix.get_points() -> table | nil, string? - Returns V Point balances plus Nodestone, activation, and reset costs core.vmatrix.get_nodestone(item_id?: number) -> table | nil, string? - Returns Nodestone inventory/use information; item_id defaults to the standard Nodestone core.vmatrix.get_cores() -> table | nil, string? - Returns an array containing every V Matrix core core.vmatrix.get_core(core_id: number) -> vmatrix_core | nil, string? - Returns one core; unknown ID returns nil without an error string core.vmatrix.get_upgrade_cost(core_id: number, target_level: number) -> number | nil, string? - Returns V Point cost to reach the absolute target level - Target must be above current level and no higher than max_level core.vmatrix.craft_nodestones(count: number) -> boolean, string? - Converts available V Points into Nodestones - Validates V Points, item data, and inventory space before sending core.vmatrix.use_nodestones(count?: number, item_id?: number) -> boolean, string? - Uses Nodestones; count defaults to 1, item_id defaults to standard Nodestone - Validates inventory, V Point cap, quests, map, and client state before sending core.vmatrix.upgrade_core(core_id: number, target_level: number) -> boolean, string? - Upgrades a core to the absolute target level after validating cost and V Points - Target must be above the current level and no higher than max_level core.vmatrix.extend_core(core_id: number) -> boolean, string? - Extends an eligible special core; revalidates eligibility and available V Points core.vmatrix.activate_core(core_id: number) -> boolean, string? - Activates a known, eligible, unexpired special core after date and character-state checks core.vmatrix.deactivate_core(core_id: number) -> boolean, string? - Deactivates a known, active, unexpired special core after date and character-state checks core.vmatrix.reset_points() -> boolean, string? - Resets allocated V Points after validating core state, point cap, character state, and Mesos - Live action with no dry-run; cost is get_points().reset_meso_cost Legacy slot/equip V Matrix calls such as get_slots(), get_equipped(), send_*_request(), and singular use_nodestone() are not registered. Use only the point/core APIs above. ================================================================================ HEXA MATRIX (core.hexa) ================================================================================ Reads return value or nil,error. Unknown IDs passed to get_core or get_stat_core return nil without an error. Actions return true when submitted successfully, or false,error when they cannot be submitted. Confirm changes by reading the affected core on a later on_tick. Core IDs and target levels must be positive whole numbers. Actions validate the current state, current-job eligibility, prerequisites, configuration, and required resources before submission. HEXA COST STRUCTURE: { sol_erda = number, fragments = number } HEXA CORE STRUCTURE: { id = number, -- ID passed to other Hexa core functions level = number, -- 0 when inactive max_level = number, type = string, -- "skill" | "mastery" | "enhancement" | "common" | "unknown" position = number, -- Display position active = boolean, skills = table, -- Associated skill IDs activation_cost? = hexa_cost, -- Present for an inactive core when available upgrade_cost? = hexa_cost -- Next-level cost when active and upgradeable } HEXA STAT LINE STRUCTURE: { type = string, -- See supported stat names below level = number } - Supported stat names: critical_damage, boss_damage, ignore_defense, damage, attack_power, magic_attack, main_stat HEXA STAT CORE STRUCTURE: { id = number, position = number, level = number, -- Combined stat-line level; 0 when inactive max_level = number, active = boolean, activation_cost? = hexa_cost, -- Present for an inactive core when available selected_slot? = number, -- Zero-based selected configuration; active only main? = hexa_stat_line, -- Active only additional? = table -- Two lines; active only } core.hexa.get_resources() -> hexa_cost | nil, string? - Returns current Sol Erda and Sol Erda Fragment balances core.hexa.get_cores() -> table | nil, string? - Returns every Hexa core available to the current job, including inactive cores core.hexa.get_core(core_id: number) -> hexa_core | nil, string? - Returns one Hexa core; unknown ID returns nil without an error string core.hexa.get_upgrade_cost(core_id: number, target_level?: number) -> hexa_cost | nil, string? - Returns the total cost from the current level to the absolute target level - target_level defaults to the next level - Requires an active core; target must be above current level and no higher than max_level core.hexa.activate_core(core_id: number) -> boolean, string? - Activates an eligible, inactive Hexa core core.hexa.upgrade_core(core_id: number, target_level?: number) -> boolean, string? - Upgrades an active Hexa core to an absolute target level - target_level defaults to the next level and may span multiple levels - Validates the complete Sol Erda and Fragment cost for the requested level span core.hexa.get_stat_cores() -> table | nil, string? - Returns every available Hexa Stat core, including inactive cores core.hexa.get_stat_core(core_id: number) -> hexa_stat_core | nil, string? - Returns one Hexa Stat core; unknown ID returns nil without an error string core.hexa.activate_stat_core(core_id: number, main_type: string, additional_type_1: string, additional_type_2: string) -> boolean, string? - Activates an eligible, inactive Hexa Stat core with the selected stat types - Names must come from the supported list; unsupported or invalid configurations are rejected Hexa Stat support currently covers inspection and activation. Upgrading, resetting, changing stat lines, and switching configurations are not exposed by core.hexa. ================================================================================ INNER ABILITY (core.inner_ability) - READ + CIRCULATE + AUTO INNER ABILITY ================================================================================ Read access to the character's Inner Ability lines for the active preset, circulate() to re-roll them once (spending Honor EXP), and the Auto Inner Ability engine (start/stop/is_running/get_status) that circulates until the lines meet your goals. 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 - ability name in the client's language, e.g. "Boss Monster Damage Increase". NEVER carries the rolled value - 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 }) --- AUTO INNER ABILITY (same namespace) --- Circulates until the lines meet your goals, then stops itself. Same engine the web menu's Autos > Auto Inner Ability panel drives. core.inner_ability.start(opts?: table) -> boolean, string? - Starts the engine; returns false + an error message when opts are invalid - Every option persists until changed; omit one to keep the menu's value - opts: preset - preset index to circulate (0-31); omit or -1 = active preset min_grade - grade EVERY line must reach (0-3), default 2 (Unique) goals - array of { skill_ids, min_level, min_grade }; each goal must be met by a DIFFERENT line. Empty = grade gate alone skill_ids - ability ids the goal accepts, e.g. {70000035} = Boss Monster Damage Increase. Ids are region-invariant; prefer them over names. Some abilities share a display name (e.g. 70000005 and 70000011 are both "Increases Speed") — list every id you'd accept ability - LEGACY: case-insensitive substring of the localized line name, used only when skill_ids is absent. Breaks on non-English clients; kept for goals saved before ids Rows are validated: a row naming no ability at all, or with a wrong-typed skill_ids/min_level/min_grade, is rejected with false + a message NOTE: min_level is the line's LEVEL. Whether that equals the magnitude shown in the tooltip is NOT verified auto_lock - keep goal-matching lines, re-roll the rest. Only lines that also clear min_grade are kept, and only on a Unique+ preset min_honor - stop once Honor EXP would fall below this (0 = spend all). Checked against the reserved price before paying, so the floor holds from the first circulate onward max_circulates - safety cap on circulates for the run (1-999), or 0 for no cap (min_honor then becomes the only backstop) roll_delay - ms between circulates (0-5000, default 350). Scripts only; deliberately not surfaced in the menu. The only wait the server can see: lower finishes sooner, too low risks a refused roll - WARNING: every circulate spends real Honor EXP. Always set max_circulates or min_honor — an unrollable goal circulates until the cap trips - Circulate cost (Honor EXP), by preset rank x locked lines (0/1/2): rare 100/400/900 epic 200/1100/2600 unique 1500/3000/5500 legendary 8000/11000/16000 Before each circulate the run reserves the LARGER of this table's price for the current rank/lock count and the cost the last circulate actually took, so min_honor holds even when the preset ranks up or locks change mid-run - Ability ids are 70000000-70000062 (63 options). Full name/id table: docs.violet.cx/inner_ability#inner-ability-reference - Example: core.inner_ability.start({ min_grade = 3, -- 70000035 = Boss Monster Damage Increase goals = { { skill_ids = { 70000035 }, min_level = 20, min_grade = 3 } }, min_honor = 50000, max_circulates = 100, }) core.inner_ability.stop() -> boolean - Stops a running Auto Inner Ability run; safe when nothing is running core.inner_ability.is_running() -> boolean - Whether an Auto Inner Ability run is currently active core.inner_ability.get_status() -> table - { running, state, preset, circulates_used, honor_spent, result } state - "idle" | "waiting_result" | "checking_result" | "cooldown" result - { status, circulatesUsed, honorSpent, honor, preset, nextCost, startLines, currentLines }; honor = the character's Honor EXP balance, nextCost = Honor EXP reserved for the next circulate status - "circulating" | "rolling" | "done" | "no_honor" | "max_reached" | "preset_not_found" | "timeout" | "character_changed" startLines/currentLines - arrays of { pos, skillId, level, grade, name } - The top-level circulates_used/honor_spent/preset describe the run IN PROGRESS and are cleared when it ends. For a finished run read result.circulatesUsed / result.honorSpent, which keep the values that stopped it ================================================================================ 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 packet:to_string() -> string - Whole packet as space-separated hex, opcode included ("1D 00 05 00 ...") - Same shape the packet monitor shows, so it can be pasted into the injector - Does not move the decode offset. packet:get_data() is an alias. 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:encode_timestamp() -> self - Encode the client's current update time as 4 bytes. Returns self for chaining. - Same as encode_4(core.get_update_time()). Errors if the game is not loaded. packet:encode_hex(hex: string) -> self - Encode bytes written as hex text, e.g. "05 00 1A FF" (whitespace optional). - Errors on a non-hex character or an odd digit count instead of skipping it. - Body only — the opcode comes from in_packet(opcode). 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:encode_timestamp() -> self - Encode the client's current update time as 4 bytes — the tick most packets carry as their timestamp field - Same as encode_4(core.get_update_time()), and the same value the packet injector's {timestamp} variable expands to - Errors if the game is not loaded - Returns self for chaining packet:encode_hex(hex: string) -> self - Encode bytes written as hex text, e.g. "05 00 1A FF" (whitespace optional) - Errors on a non-hex character or an odd digit count instead of skipping it - Body only — the opcode comes from out_packet(opcode), so a captured string (which starts with the 2-byte opcode) needs its first two bytes dropped - 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(stage) if stage ~= core.stage.FIELD then return end 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(stage) if stage ~= core.stage.FIELD then return end 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(stage) if stage ~= core.stage.FIELD then return end 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 (for plain positions; actors have mob:distance() / npc:distance()) --- 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(stage) if stage ~= core.stage.FIELD then return end 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(stage) if stage ~= core.stage.FIELD then return end 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 a core.stage.FIELD guard at the start of field-only on_tick(stage) logic 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(map) local closest, closest_dist = nil, math.huge for _, mob in ipairs(map:get_mobs()) do if mob and mob:is_valid() then local dist = mob:distance() -- prefer this over M.distance for actors if dist and dist < closest_dist then closest_dist = dist closest = mob 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(stage) if stage ~= core.stage.FIELD then return end 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(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, core.get_utc_time() for UTC) 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 ================================================================================