Skill v1.0.1
currentAutomated scan100/1003 files
version: "1.0.1" name: SND Core description: Use this skill when creating or editing SND Lua macros for FFXIV. Provides metadata structure, state machine patterns, character conditions, configuration system, Player API, Entity system, Excel data access, and core utility functions.
SND Core Development
This skill covers the foundational patterns for SND (Something Need Doing) Lua macros.
Metadata Structure Requirements
Every SND macro MUST start with this metadata header:
--[=====[[[SND Metadata]]author: 'Your Name'version: 1.0.0description: Brief description of what the macro doesplugin_dependencies:- PluginName1- PluginName2configs:NumberSetting:description: A numeric setting with min/maxdefault: 50min: 1max: 100StringSetting:description: A text settingdefault: SomeValueBooleanSetting:description: A true/false settingdefault: true[[End Metadata]]--]=====]
Required Fields
- author: String - Creator of the macro
- version: String - Semantic versioning (MAJOR.MINOR.PATCH)
- description: String - Brief description of macro functionality
Optional Fields
- plugin_dependencies: Array of required Dalamud plugins
- configs: User-configurable settings object
Configuration Field Properties
Each config setting can have:
- description: What this setting does (REQUIRED - put before default)
- default: Default value (string, number, or boolean)
- min: Minimum value (for numbers)
- max: Maximum value (for numbers)
Configuration Examples
configs:-- Numeric with rangeGameCount:description: Number of games to play. Set to 0 to skip.default: 125min: 0max: 1000-- BooleanEnableFeature:description: Enable this feature (true/false)default: true-- StringTargetName:description: Name of the target NPCdefault: SomeNPC
IMPORTANT: The description field should come BEFORE default in the YAML structure for proper display in SND.
Configuration Access
IMPORTANT: Config values are stored in SND's JSON as strings, regardless of the declared type. Always convert appropriately.
-- Always use Config.Get() to access configuration valueslocal settingValue = Config.Get("SettingName")local numericValue = tonumber(Config.Get("NumericSetting"))local booleanValue = Config.Get("BooleanSetting") == "true" or Config.Get("BooleanSetting") == true-- Type-safe access functionsfunction GetConfigAsString(key, defaultValue)return Config.Get(key) or defaultValue or ""endfunction GetConfigAsNumber(key, defaultValue)local value = Config.Get(key)return value and tonumber(value) or defaultValue or 0endfunction GetConfigAsBoolean(key, defaultValue)local value = Config.Get(key)-- Handle both string "true" and actual boolean trueif value == "true" or value == true then return true endif value == "false" or value == false then return false endreturn defaultValue or falseend
How Configs Work in SND
- Metadata YAML → SND JSON: When syncing, the YAML metadata in your Lua file is parsed and converted to SND's internal JSON format.
- SND JSON Format: Each config becomes a JSON object with these properties:
Value: Current value (string)DefaultValue: Default value from metadata (string)Description: Description textType: "bool", "int", or "string" (auto-detected)MinValue/MaxValue: For numeric types (string or null)
- Type Detection:
true/false→bool- Numeric values →
int - Everything else →
string
- Example JSON output (what SND stores):
{"Debug": {"Value": "true","DefaultValue": "true","Description": "Show debug info","Type": "bool","MinValue": null,"MaxValue": null},"Breakpoint1": {"Value": "50","DefaultValue": "50","Description": "First breakpoint level","Type": "int","MinValue": "1","MaxValue": "100"}}
Logging System
Standardized logging with prefix and optional echo to chat.
-- ConfigurationechoLog = true -- Set to false to disable chat echoPREFIX = "[Script]" -- Prefix for all messages-- Internal helperslocal function _echo(s)yield("/echo " .. tostring(s))endlocal function _log(s)local msg = tostring(s)Dalamud.Log(msg)if echoLog then _echo(msg) endendlocal function _fmt(msg, ...)return string.format("%s %s", PREFIX, string.format(msg, ...))end-- Public logging functionsfunction Logf(msg, ...) _log(_fmt(msg, ...)) endfunction Echof(msg, ...) _echo(_fmt(msg, ...)) end-- Aliases for convenienceLog, log = Logf, LogfEcho, echo = Echof, Echof
Usage Examples
-- Simple loggingLog("Starting script")Echo("Player name: %s", Player.Name)-- With format argumentsLogf("Processing item %d of %d", currentItem, totalItems)Echof("Distance to target: %.2f", distance)-- Conditional loggingif debugMode thenLog("Debug: current state = %s", tostring(State))end
Player API
IMPORTANT: Player vs Svc.ClientState.LocalPlayer
There are TWO ways to access player data, and they return DIFFERENT things:
- `Player.*` - SND wrapper object (some properties return nil or wrapper objects)
- `Svc.ClientState.LocalPlayer` - Direct Dalamud access (more reliable for level/job)
-- WRONG: These may return nil or wrapper objectslocal level = Player.Level -- Returns nil!local job = Player.Job -- Returns wrapper object, not ID!-- CORRECT: Use Svc.ClientState.LocalPlayer for level and job infolocal lp = Svc.ClientState.LocalPlayerif lp thenlocal currentLevel = lp.Level -- Returns actual level numberlocal currentJobId = lp.ClassJob.RowId -- Returns job ID numberend
Player Availability and Basic Info
-- Check if player is available (Player wrapper)if Player.Available then-- Player is available for operationsend-- Check if player is busy (Player wrapper)if Player.IsBusy then-- Player is busy with somethingend-- Get player name (Player wrapper)local playerName = Player.Name-- Get player level - USE LocalPlayer!local lp = Svc.ClientState.LocalPlayerlocal level = lp and lp.Level or 0
Player Position
-- Get player positionlocal pos = Player.Positionlocal x = pos.Xlocal y = pos.Ylocal z = pos.Z-- Example: Log current positionyield("/echo [Script] Position: " .. pos.X .. ", " .. pos.Y .. ", " .. pos.Z)
Player Job/Class
-- CORRECT: Get current job information via LocalPlayerlocal lp = Svc.ClientState.LocalPlayerif lp thenlocal jobId = lp.ClassJob.RowIdlocal jobAbbr = lp.ClassJob.Value.Abbreviation:ToString()local jobName = lp.ClassJob.Value.Name:ToString()yield("/echo [Script] Current job: " .. jobAbbr .. " (ID: " .. jobId .. ") Lv." .. lp.Level)end-- Example: Check if on specific joblocal lp = Svc.ClientState.LocalPlayerif lp and lp.ClassJob.RowId == 14 then -- Carpenteryield("/echo [Script] Currently on Carpenter")end
Gearsets (for listing saved gearsets)
-- Get gearset infolocal gs = Player.GetGearset(1) -- API index 1-100if gs and gs.ClassJob and gs.ClassJob > 0 thenlocal jobId = gs.ClassJoblocal gearsetName = gs.Namelocal itemLevel = gs.ItemLevel -- ITEM LEVEL of the gear, NOT character job level!end-- Iterate all gearsets to find which jobs have gearsetsfor idx = 1, 100 dolocal gs = Player.GetGearset(idx)if gs and gs.ClassJob and gs.ClassJob > 0 and gs.Name and gs.Name ~= "" thenyield("/echo Job ID: " .. gs.ClassJob .. " Gearset: " .. gs.Name .. " iLvl: " .. gs.ItemLevel)endend
CRITICAL WARNING - Gearset Index Offset: Player.GetGearset(idx) returns gearsets where the API index is OFF BY ONE from the UI slot number!
- API index 1 = UI slot 2
- API index 5 = UI slot 6
- etc.
When using /gearset change, you must use the UI slot number, not the API index:
-- CORRECT: Switch to a gearset by job IDfor idx = 1, 100 dolocal gs = Player.GetGearset(idx)if gs and gs.ClassJob == targetJobId thenlocal uiSlot = idx + 1 -- Convert API index to UI slot!yield("/gearset change " .. uiSlot)breakendend-- WRONG: This uses the API index directly (will switch to wrong gearset!)-- yield("/gearset change " .. idx) -- DON'T DO THIS!
WARNING: gs.ItemLevel is the average item level of the GEAR in that gearset, NOT the character's level in that job!
Getting Job Levels
Use Player.GetJob(jobId).Level to get the level for ANY job:
-- Get level for a specific job by IDlocal level = Player.GetJob(15).Level -- e.g., 88 for Machinist (ID 15)-- Get all job levelslocal classJobSheet = Excel.GetSheet("ClassJob")for jobId = 1, 42 dolocal job = Player.GetJob(jobId)if job and job.Level and job.Level > 0 thenlocal jobRow = classJobSheet:GetRow(jobId)local abbr = jobRow and tostring(jobRow.Abbreviation) or "?"yield("/echo " .. abbr .. ": Lv." .. tostring(job.Level))endend
Working approaches:
Player.GetJob(jobId).Level→ Returns level for any job ✓Svc.ClientState.LocalPlayer.Level→ Current job level only ✓
Non-working approaches:
Player.Level→ nilPlayer.GetLevel(x)→ nil (method doesn't exist)Player.ClassJob→ nil
Combat and Casting State
-- Check if player is in combatPlayer.InCombat → boolean-- Check if player is castingPlayer.IsCasting → boolean-- Get current cast info (when casting)Player.CastInfo → table -- Contains spell info when casting-- Check if player is alivePlayer.IsAlive → boolean-- Get current HP/MPPlayer.CurrentHp → numberPlayer.MaxHp → numberPlayer.CurrentMp → numberPlayer.MaxMp → number
Player Status Effects
-- Get player status listlocal statusList = Player.Status-- Check for specific statusfunction HasStatusId(targetId)local statusList = Player.Statusif not statusList thenreturn falseendfor i = 0, statusList.Count - 1 dolocal status = statusList:get_Item(i)if status and status.StatusId == targetId thenreturn trueendendreturn falseend-- Example: Check for Superior Spiritbond Potion (ID 49)if HasStatusId(49) thenyield("/echo [Script] Has potion effect active")end-- Iterate all statusesif Player.Status thenfor i = 0, Player.Status.Count - 1 dolocal status = Player.Status:get_Item(i)local statusId = status.StatusIdlocal statusName = status.Nameyield("/echo [Script] Status: " .. statusName .. " (ID: " .. statusId .. ")")endend
Entity System
Getting Entities
-- Get player entitylocal player = Entity.Player-- Get current target entitylocal target = Entity.Target-- Get entity by namelocal npc = Entity.GetEntityByName("NPC Name")-- Get entity by IDlocal entity = Entity.GetEntityById(entityId)-- Get entity by DataId (for NPCs with specific DataIds)local entity = Entity.GetEntityByDataId(dataId)-- Get nearest entity by namelocal nearest = Entity.GetNearestEntityByName("NPC Name")-- Get all entities matching criterialocal entities = Entity.GetEntitiesByName("NPC Name") -- Returns table
Entity Filtering
-- Get entities within rangelocal nearbyEntities = Entity.GetEntitiesInRange(maxDistance)-- Get targetable entitieslocal targetable = Entity.GetTargetableEntities()-- Get enemy entities in combatlocal enemies = Entity.GetEnemiesInCombat()
Svc.Targets System
Access various target slots through Svc.Targets:
-- Available target slotslocal targetSlots = {"Target", -- Current target"CurrentTarget", -- Same as Target"PreviousTarget", -- Last target"SoftTarget", -- Soft target (hover)"MouseOverTarget", -- Mouse hover target"MouseOverNameplateTarget", -- Nameplate hover target"FocusTarget", -- Focus target"GPoseTarget", -- GPose target}--- Safely get a target slot (avoids getters that can throw)-- @param slotName string - The target slot name-- @return IGameObject|nil - The target object or nillocal function GetTargetSlot(slotName)local ok, val = pcall(function() return Svc.Targets[slotName] end)if not ok then return nil endreturn valend--- Describe an IGameObject briefly-- @param go IGameObject - The game object to describe-- @return table - Object info {name, kind, gameObjectId, dataId, hp, maxHp, position}function DescribeGameObject(go)if not go then return nil endlocal function try(fn) local ok, v = pcall(fn); return ok and v or nil endreturn {name = try(function() return go.Name:ToString() end) or tostring(go),kind = try(function() return go.ObjectKind end),gameObjectId = try(function() return go.GameObjectId end) or try(function() return go.EntityId end),dataId = try(function() return go.DataId end),hp = try(function() return go.CurrentHp end),maxHp = try(function() return go.MaxHp end),position = try(function() return go.Position end),}end-- Example: Get and describe focus targetlocal focusTarget = GetTargetSlot("FocusTarget")if focusTarget thenlocal info = DescribeGameObject(focusTarget)print(("Focus: %s (HP: %d/%d)"):format(info.name, info.hp or 0, info.maxHp or 0))end
Entity Properties
if entity then-- Get entity namelocal name = entity.Name-- Get entity positionlocal position = entity.Positionlocal x = position.Xlocal y = position.Ylocal z = position.Z-- Set entity as targetentity:SetAsTarget()-- Interact with entityentity:Interact()end
Distance to Target
function GetDistanceToTarget()if not Entity.Player or not Entity.Target thenreturn nilendlocal playerPos = Entity.Player.Positionlocal targetPos = Entity.Target.Positionlocal dx = playerPos.X - targetPos.Xlocal dy = playerPos.Y - targetPos.Ylocal dz = playerPos.Z - targetPos.Zreturn math.sqrt(dx * dx + dy * dy + dz * dz)end-- Example usagelocal distance = GetDistanceToTarget()if distance and distance < 3.0 thenyield("/echo [Script] Close enough to interact")end
Target NPC by Name
function TargetNpcByName(npcName)local npc = Entity.GetEntityByName(npcName)if npc thennpc:SetAsTarget()return trueendreturn falseendfunction InteractWithNpc(npcName)local npc = Entity.GetEntityByName(npcName)if npc thennpc:SetAsTarget()yield("/wait 0.5")npc:Interact()return trueendreturn falseend
Advanced Distance Helpers
import("System.Numerics") -- Required for Vector3--- Calculate distance between two positions-- @param pos1 Vector3 - First position-- @param pos2 Vector3 - Second position-- @return number - Distance in yalmsfunction DistanceBetweenPositions(pos1, pos2)if not (pos1 and pos2) then return math.huge endreturn Vector3.Distance(pos1, pos2)end--- Check if two positions are within a maximum distance-- Uses squared distance for efficiency (avoids sqrt)-- @param pos1 Vector3 - First position-- @param pos2 Vector3 - Second position-- @param maxDist number - Maximum distance-- @return boolean - True if within distancefunction IsWithinDistance(pos1, pos2, maxDist)if not (pos1 and pos2 and maxDist) then return false endlocal distSqif Vector3.DistanceSquared thendistSq = Vector3.DistanceSquared(pos1, pos2)elselocal dx = pos1.X - pos2.Xlocal dy = pos1.Y - pos2.Ylocal dz = pos1.Z - pos2.ZdistSq = dx*dx + dy*dy + dz*dzendreturn distSq <= (maxDist * maxDist)end--- Check if current target is within specified distance-- @param maxDist number - Maximum distance-- @return boolean - True if target is within distancefunction IsTargetWithin(maxDist)if not (Entity and Entity.Player and Entity.Target and maxDist) thenreturn falseendreturn IsWithinDistance(Entity.Player.Position, Entity.Target.Position, maxDist)end--- Get target name safely-- @return string - Target name or empty stringfunction GetTargetName()return (Entity and Entity.Target and Entity.Target.Name) or ""end
Robust Entity Interaction
--- Interact with entity by name with timeout and verification-- @param name string - Entity name to interact with-- @param timeout number - Maximum seconds to wait (default: 5)-- @return boolean - True if interaction succeededfunction InteractByName(name, timeout)if type(name) ~= "string" or name == "" thenLog("InteractByName: invalid name '%s'", tostring(name))return falseendtimeout = toNumberSafe(timeout, 5, 0.1)local e = Entity.GetEntityByName(name)if not e thenLog("InteractByName: entity not found '%s'", name)return falseendlocal start = os.clock()while (os.clock() - start) < timeout doe:SetAsTarget()yield("/wait 0.1")local tgt = Entity.Targetif tgt and tgt.Name == name thene:Interact()return trueendyield("/wait 0.1")endLog("InteractByName: timeout '%s'", name)return falseend
Character Info Helpers
--- Get character name from LocalPlayer-- IMPORTANT: Use :ToString() method, NOT tostring() or .TextValue-- tostring() gives weird output with extra IDs like "Name: 12345@World: -67890"-- @return string|nil - Character name or nilfunction GetCharacterName()local lp = Svc and Svc.ClientState and Svc.ClientState.LocalPlayerif not lp then return nil endreturn lp.Name:ToString()end--- Get character home world name-- IMPORTANT: Use :ToString() method for clean output-- @return string|nil - World name or nilfunction GetCharacterWorld()local lp = Svc and Svc.ClientState and Svc.ClientState.LocalPlayerif not lp then return nil endreturn lp.HomeWorld.Value.Name:ToString()end--- Get character unique key (Name@World)-- Useful for persisting per-character data-- @return string|nil - "CharacterName@WorldName" or nilfunction GetCharacterKey()local lp = Svc and Svc.ClientState and Svc.ClientState.LocalPlayerif not lp then return nil endlocal name = lp.Name:ToString()local world = lp.HomeWorld.Value.Name:ToString()return name .. "@" .. worldend--- Get character position from ClientState-- @return Vector3|nil - Position or nilfunction GetCharacterPosition()local player = Svc and Svc.ClientState and Svc.ClientState.LocalPlayerreturn player and player.Position or nilend--- Get current job ID-- @return number|nil - Job ID or nilfunction GetCharacterJob()local lp = Svc and Svc.ClientState and Svc.ClientState.LocalPlayerreturn lp and lp.ClassJob and lp.ClassJob.RowId or nilend--- Get current job level-- @return number - Current job level or 0function GetCharacterLevel()local lp = Svc and Svc.ClientState and Svc.ClientState.LocalPlayerreturn lp and lp.Level or 0end
IMPORTANT: String Conversion Gotcha
When accessing string properties from game objects (like Name, World, etc.):
-- WRONG: tostring() gives weird output with extra metadatalocal name = tostring(lp.Name) -- Returns "Name: 12345@World: -67890"-- WRONG: .TextValue may not exist or return nillocal name = lp.Name.TextValue -- May error or return nil-- CORRECT: Use :ToString() methodlocal name = lp.Name:ToString() -- Returns "Character Name"
This applies to most game string objects like:
lp.Name:ToString()lp.HomeWorld.Value.Name:ToString()lp.ClassJob.Value.Name:ToString()lp.ClassJob.Value.Abbreviation:ToString()
Excel Data Access
Access game data sheets for information lookup. Excel lookups return structured data from the game's internal sheets.
Basic Excel Usage
-- Get territory/zone informationlocal territory = Excel.GetRow("TerritoryType", territoryId)local placeName = territory.PlaceName.Namelocal aetheryteName = territory.Aetheryte.PlaceName.Name-- Get item informationlocal item = Excel.GetRow("Item", itemId)local itemName = item.Name-- Get job informationlocal job = Excel.GetRow("ClassJob", jobId)local jobName = job.Namelocal jobAbbreviation = job.Abbreviation
Get Zone Information
function GetZoneId()return Svc.ClientState.TerritoryTypeend-- Check if in specific zoneif Svc.ClientState.TerritoryType == 129 then -- Limsa Lominsa Lower Decksyield("/echo [Script] Currently in Limsa")end
Safe PlaceName by Territory (Handles Multiple Data Formats)
--[[Get localized place name from territory ID.Handles the various ways PlaceName can be stored (string, userdata, number).Returns: ok (boolean), data/error (table with name or error string)]]function PlaceNameByTerritory(id)local tid = toNumberSafe(id, nil, 1)if not tid then return false, "invalid territory id: "..tostring(id) endlocal terr = Excel.GetSheet("TerritoryType")if not terr then return false, "TerritoryType sheet not found" endlocal row = terr:GetRow(tid)if not row then return false, "TerritoryType row not found for id "..tid endlocal pn = row.PlaceNameif not pn then return false, "PlaceName field missing for territory id "..tid end-- Handle string typeif type(pn) == "string" and #pn > 0 thenreturn true, { name = pn, territoryId = tid, source = "TerritoryType.PlaceName:string" }end-- Handle userdata type (most common)if type(pn) == "userdata" thenlocal okv, val = pcall(function() return pn.Value end)if okv and val thenlocal okn, nm = pcall(function() return val.Singular or val.Name or val:ToString() end)if okn and nm and nm ~= "" thenreturn true, { name = tostring(nm), territoryId = tid, source = "TerritoryType.PlaceName:userdata.Value" }endendlocal okid, rid = pcall(function() return pn.RowId end)if okid and type(rid) == "number" thenlocal place = Excel.GetSheet("PlaceName")if not place then return false, "PlaceName sheet not found (RowId="..tostring(rid)..")" endlocal prow = place:GetRow(rid)if not prow then return false, "PlaceName row not found (RowId="..tostring(rid)..")" endlocal okn2, nm2 = pcall(function() return prow.Singular or prow.Name or prow:ToString() end)if okn2 and nm2 and nm2 ~= "" thenreturn true, { name = tostring(nm2), territoryId = tid, source = "PlaceName(RowId)" }endreturn false, "PlaceName values empty (RowId="..tostring(rid)..")"endreturn false, "unsupported PlaceName userdata shape for territory id "..tidend-- Handle numeric type (direct PlaceName RowId)if type(pn) == "number" thenlocal place = Excel.GetSheet("PlaceName")if not place then return false, "PlaceName sheet not found" endlocal prow = place:GetRow(pn)if not prow then return false, "PlaceName row not found (id="..tostring(pn)..")" endlocal okn, nm = pcall(function() return prow.Singular or prow.Name or prow:ToString() end)if okn and nm and nm ~= "" thenreturn true, { name = tostring(nm), territoryId = tid, source = "PlaceName(numeric)" }endreturn false, "PlaceName values empty (id="..tostring(pn)..")"endreturn false, "unsupported PlaceName type: "..type(pn)end-- Simple wrapper that returns just the namefunction GetZoneName(territoryType)territoryType = territoryType or Svc.ClientState.TerritoryTypelocal ok, dataOrErr = PlaceNameByTerritory(territoryType)if ok thenreturn dataOrErr.nameelsereturn nilendend
ENpcResident Name Lookup
-- Resolve an ENpcResident name directly by DataIdfunction GetENpcResidentName(dataId)local id = toNumberSafe(dataId, nil)if not id then return nil, "ENpcResident: invalid id '"..tostring(dataId).."'" endlocal sheet = Excel.GetSheet("ENpcResident")if not sheet then return nil, "ENpcResident sheet not available" endlocal row = sheet:GetRow(id)if not row then return nil, "ENpcResident: no row for id "..tostring(id) endlocal name = row.Singular or row.Nameif not name or name == "" then return nil, "ENpcResident: name missing for id "..tostring(id) endreturn tostring(name), nilend-- Example usage:-- local npcName = GetENpcResidentName(1052612) -- Returns NPC name
NPC Name Resolution (Multi-Sheet Chain Lookup)
Comprehensive NPC name resolution that handles multiple Excel sheet chains:
- ENpcResident (direct lookup)
- EventNpc → ENpcResident
- BNpcBase → BNpcName
local DEBUG_NPC = false -- Set to true for debug output--- Resolve NPC name from multiple Excel sheet sources-- @param kind string|number - Object kind ("EventNpc", "3", etc.)-- @param dataId number - The NPC's DataId-- @return string|nil, string - Name and source sheet, or nil and "unresolved"local function ResolveNpcName(kind, dataId)local k = tostring(kind):lower()-- ENpcResident fast-path (most common)dolocal en = Excel.GetSheet("ENpcResident")if en thenlocal row = en:GetRow(dataId)if row and (row.Singular or row.Name) thenif DEBUG_NPC then print(("ENpcResident(%d) → %s"):format(dataId, row.Singular or row.Name)) endreturn row.Singular or row.Name, "ENpcResident"endendend-- EventNpc → ENpcResident chainif k == "eventnpc" or k == "3" thenlocal ev = Excel.GetSheet("EventNpc")if ev thenlocal evRow = ev:GetRow(dataId)if evRow thenlocal link = evRow.ENpcResident or evRow.NameId or evRow.ENpcResidentIdlocal linkId = (type(link) == "table" and link.RowId) or linklocal en = Excel.GetSheet("ENpcResident")local enRow = en and linkId and en:GetRow(linkId)if enRow and (enRow.Singular or enRow.Name) thenif DEBUG_NPC then print(("EventNpc(%d) → ENpcResident(%d) → %s"):format(dataId, linkId, enRow.Singular or enRow.Name)) endreturn enRow.Singular or enRow.Name, "EventNpc → ENpcResident"endendendend-- BNpcBase → BNpcName chain (for battle NPCs/enemies)dolocal base = Excel.GetSheet("BNpcBase")local b = base and base:GetRow(dataId)if b thenlocal link = b.BNpcName or b.NameIdlocal nameId = (type(link) == "table" and link.RowId) or linklocal names = Excel.GetSheet("BNpcName")local nm = names and names:GetRow(nameId)if nm and (nm.Singular or nm.Name) thenif DEBUG_NPC then print(("BNpcBase(%d) → BNpcName(%d) → %s"):format(dataId, nameId, nm.Singular or nm.Name)) endreturn nm.Singular or nm.Name, "BNpcBase → BNpcName"endendendif DEBUG_NPC then print(("Unresolved: kind=%s dataId=%s"):format(k, tostring(dataId))) endreturn nil, "unresolved"end-- Cache for resolved NPC nameslocal npcNameCache = {}--- Get NPC name with caching-- @param kind string|number - Object kind-- @param dataId number - The NPC's DataId-- @return string|nil, string - Name and sourcefunction GetNpcName(kind, dataId)local key = tostring(kind) .. ":" .. tostring(dataId)local cached = npcNameCache[key]if cached ~= nil then return cached.name, cached.source endlocal name, source = ResolveNpcName(kind, dataId)npcNameCache[key] = { name = name, source = source }return name, sourceend--- Clear NPC name cachefunction ClearNpcNameCache()npcNameCache = {}end-- Example usage:-- local name, source = GetNpcName("EventNpc", 1052642)-- print("Resolved NPC name:", name or "<not found>", "from sheet:", source)
ClassJob Table Builder
-- Build a table of job information from the ClassJob sheetfunction BuildJobTable(firstId, lastId)firstId = toNumberSafe(firstId, 1, 1)lastId = toNumberSafe(lastId, 42, firstId)local sheet = Excel.GetSheet("ClassJob")if not sheet then return nil, "ClassJob sheet not found" endlocal jobs, missing = {}, {}for id = firstId, lastId dolocal row = sheet:GetRow(id)if row thenlocal name = row.Name or row["Name"]local abbr = row.Abbreviation or row["Abbreviation"]if name and abbr thenjobs[id] = { name = tostring(name), abbr = tostring(abbr) }elsetable.insert(missing, ("id=%d missing Name/Abbreviation"):format(id))endelsetable.insert(missing, ("id=%d row not found"):format(id))endendif next(jobs) == nil thenreturn nil, "ClassJob table empty; " .. table.concat(missing, "; ")endreturn jobs, (#missing > 0) and missing or nilend-- Get specific job infofunction GetJobName(jobId)local sheet = Excel.GetSheet("ClassJob")if not sheet then return "Unknown" endlocal row = sheet:GetRow(jobId)if row and row.Name thenreturn tostring(row.Name)endreturn "Unknown"endfunction GetJobAbbreviation(jobId)local sheet = Excel.GetSheet("ClassJob")if not sheet then return "UNK" endlocal row = sheet:GetRow(jobId)if row and row.Abbreviation thenreturn tostring(row.Abbreviation)endreturn "UNK"end
Gearset Cache System
local _gearsetCache = nillocal _gearsetStamp = nil-- Build a table mapping ClassJob ID to gearset info-- IMPORTANT: Stores uiSlot (idx + 1) for use with /gearset change command!function BuildGearsetTable(force)if _gearsetCache and not force thenreturn _gearsetCacheendlocal gearset = {}for idx = 1, 100 dolocal gs = Player.GetGearset(idx)if gs and gs.ClassJob and gs.ClassJob > 0 and gs.Name and gs.Name ~= "" then-- Store UI slot (idx + 1), not API index!gearset[gs.ClassJob] = { uiSlot = idx + 1, name = gs.Name }endend_gearsetCache = gearset_gearsetStamp = os.clock()return gearsetendfunction InvalidateGearsetCache()_gearsetCache = nil_gearsetStamp = nilDalamud.Log("[Script] Gearset cache invalidated")end-- Get gearset for a specific job-- Returns { uiSlot = N, name = "..." } or nilfunction GetGearsetForJob(jobId)local gearsets = BuildGearsetTable()return gearsets[jobId]end
Eorzea Time Utilities
-- Get current Eorzea hour (0-23)function GetEorzeaHour()local et = os.time() * 1440 / 70return math.floor((et % 86400) / 3600)end-- Convert Eorzea time to Unix timestampfunction EorzeaTimeToUnixTime(eorzeaTime)return Instances.Framework.EorzeaTimeend
State Machine Architecture
Complex macros MUST use this state machine pattern:
-- Define statesCharacterState = {ready = Ready,working = Working,error = Error,recovery = Recovery}-- State functionsfunction Ready()if someCondition thenState = CharacterState.workingelseif errorCondition thenState = CharacterState.errorendendfunction Working()if workComplete thenState = CharacterState.readyelseif errorOccurred thenState = CharacterState.errorendendfunction Error()yield("/echo [Script] ERROR: " .. errorMessage)State = CharacterState.recoveryendfunction Recovery()if recoverySuccessful thenState = CharacterState.readyelseyield("/echo [Script] Recovery failed, stopping")StopFlag = trueendend-- Main execution loopState = CharacterState.readywhile not StopFlag doif not IsCharacterBusy() thenState()endyield("/wait 0.1")end
State Machine Best Practices
- Use descriptive state names (not s1, s2, s3)
- Keep states focused on single responsibility
- Handle all state transitions explicitly
- Include error and recovery states
- Log important state transitions
Character Condition Constants
Complete list of all character conditions (102 entries):
CharacterCondition = {normalConditions = 1, -- moving or standing stilldead = 2,emoting = 3,mounted = 4,crafting = 5,gathering = 6,meldingMateria = 7,operatingSiegeMachine = 8,carryingObject = 9,mounted2 = 10,inThatPosition = 11,chocoboRacing = 12,playingMiniGame = 13,playingLordOfVerminion = 14,participatingInCustomMatch = 15,performing = 16,-- 17-24 unused/unknownoccupied = 25,inCombat = 26,casting = 27,sufferingStatusAffliction = 28,sufferingStatusAffliction2 = 29,occupied30 = 30,occupiedInEvent = 31,occupiedInQuestEvent = 32,occupied33 = 33,boundByDuty34 = 34,occupiedInCutSceneEvent = 35,inDuelingArea = 36,tradeOpen = 37,occupied38 = 38,occupiedMateriaExtractionAndRepair = 39,executingCraftingAction = 40,preparingToCraft = 41,executingGatheringAction = 42,fishing = 43,-- 44 unusedbetweenAreas = 45,stealthed = 46,-- 47 unusedjumping48 = 48,autorunActive = 49,occupiedSummoningBell = 50,betweenAreasForDuty = 51,systemError = 52,loggingOut = 53,conditionLocation = 54,waitingForDuty = 55,boundByDuty56 = 56,mounting57 = 57,watchingCutscene = 58,waitingForDutyFinder = 59,creatingCharacter = 60,jumping61 = 61,pvpDisplayActive = 62,sufferingStatusAffliction63 = 63,mounting64 = 64,carryingItem = 65,usingPartyFinder = 66,usingHousingFunctions = 67,transformed = 68,onFreeTrial = 69,beingMoved = 70,mounting71 = 71,sufferingStatusAffliction72 = 72,sufferingStatusAffliction73 = 73,registeringForRaceOrMatch = 74,waitingForRaceOrMatch = 75,waitingForTripleTriadMatch = 76,flying = 77,watchingCutscene78 = 78,inDeepDungeon = 79,swimming = 80,diving = 81,registeringForTripleTriadMatch = 82,waitingForTripleTriadMatch83 = 83,participatingInCrossWorldPartyOrAlliance = 84,unknown85 = 85, -- Part of gatheringdutyRecorderPlayback = 86,casting87 = 87,inThisState88 = 88,inThisState89 = 89,rolePlaying = 90,inDutyQueue = 91,readyingVisitOtherWorld = 92,waitingToVisitOtherWorld = 93,usingFashionAccessory = 94,boundByDuty95 = 95,unknown96 = 96,disguised = 97,recruitingWorldOnly = 98,unknown99 = 99,editingPortrait = 100,unknown101 = 101,pilotingMech = 102,}-- Check conditionif Svc.Condition[CharacterCondition.casting] then-- Handle castingendif Svc.Condition[CharacterCondition.betweenAreas] then-- Player is teleportingendif Svc.Condition[CharacterCondition.inCombat] then-- Player is in combatend-- Comprehensive busy checkfunction IsCharacterBusy()return Svc.Condition[CharacterCondition.casting] orSvc.Condition[CharacterCondition.betweenAreas] orSvc.Condition[CharacterCondition.beingMoved] orSvc.Condition[CharacterCondition.occupiedInQuestEvent] orSvc.Condition[CharacterCondition.occupiedInCutSceneEvent] orSvc.Condition[CharacterCondition.watchingCutscene] orPlayer.IsBusyend-- Wait for not busyfunction WaitForNotBusy(timeout)timeout = timeout or 30local startTime = os.clock()while IsCharacterBusy() and (os.clock() - startTime) < timeout doyield("/wait 0.1")endreturn not IsCharacterBusy()end-- Common condition checksfunction IsInCombat()return Svc.Condition[CharacterCondition.inCombat]endfunction IsMounted()return Svc.Condition[CharacterCondition.mounted]endfunction IsFlying()return Svc.Condition[CharacterCondition.flying]endfunction IsCrafting()return Svc.Condition[CharacterCondition.crafting]endfunction IsGathering()return Svc.Condition[CharacterCondition.gathering]endfunction IsFishing()return Svc.Condition[CharacterCondition.fishing]endfunction IsBetweenAreas()return Svc.Condition[CharacterCondition.betweenAreas] orSvc.Condition[CharacterCondition.betweenAreasForDuty]end
Core Utility Functions
Plugin Availability Check
function HasPlugin(pluginName)for plugin in luanet.each(Svc.PluginInterface.InstalledPlugins) doif plugin.InternalName == pluginName and plugin.IsLoaded thenreturn trueendendreturn falseend-- Usageif not HasPlugin("RequiredPlugin") thenyield("/echo [Script] Missing required plugin: RequiredPlugin")StopFlag = trueend--- List all installed plugins with their statusfunction GetPlugins()local plugins = {}for plugin in luanet.each(Svc.PluginInterface.InstalledPlugins) dotable.insert(plugins, { name = plugin.InternalName, loaded = plugin.IsLoaded })endtable.sort(plugins, function(a,b) return a.name:lower() < b.name:lower() end)Log("Installed plugins:")for _, p in ipairs(plugins) doLog(" %s | Enabled: %s", p.name, tostring(p.loaded))endreturn pluginsend
Advanced IPC Subscriber Access
For accessing IPC functions not exposed through the standard IPC.* interface, use reflection-based subscribers.
import "System"-- IPC subscriber cachelocal ipc_subscribers = {}--- Get a generic method from a type using reflection-- @param targetType Type - The target .NET type-- @param method_name string - Method name to find-- @param genericTypes table - Array of Type objects for generics-- @return MethodInfo - The constructed generic methodlocal function get_generic_method(targetType, method_name, genericTypes)local genericArgsArr = luanet.make_array(Type, genericTypes)local methods = targetType:GetMethods()for i = 0, methods.Length - 1 dolocal m = methods[i]if m.Name == method_name and m.IsGenericMethodDefinitionand m:GetGenericArguments().Length == genericArgsArr.Length thenlocal ok, constructed = pcall(function()return m:MakeGenericMethod(genericArgsArr)end)if ok then return constructed endendendreturn nilend--- Register an IPC subscriber for a signature-- @param ipc_signature string - The IPC signature (e.g., "PluginName.MethodName")-- @param result_type string|nil - .NET type for return value (nil for actions)-- @param arg_types table - Array of .NET type strings for arguments-- @return boolean - True if registered successfullyfunction RequireIPC(ipc_signature, result_type, arg_types)if ipc_subscribers[ipc_signature] thenreturn true -- Already loadedendlocal pi = Svc.PluginInterfaceif not pi thenLog("RequireIPC: PluginInterface not available")return falseendarg_types = arg_types or {}-- Append result type (System.Object for actions)arg_types[#arg_types + 1] = result_type or 'System.Object'-- Convert string types to Type objectsfor i, v in pairs(arg_types) doarg_types[i] = Type.GetType(v)endlocal method = get_generic_method(pi:GetType(), 'GetIpcSubscriber', arg_types)if not method thenLog("RequireIPC: GetIpcSubscriber not found for %s", ipc_signature)return falseendlocal sig = luanet.make_array(Object, { ipc_signature })local subscriber = method:Invoke(pi, sig)if not subscriber thenLog("RequireIPC: IPC not found: %s", ipc_signature)return falseendlocal kind = (result_type == nil) and "action" or "function"ipc_subscribers[ipc_signature] = { kind = kind, sub = subscriber }Log("RequireIPC: Loaded %s IPC: %s", kind, ipc_signature)return trueend--- Invoke a registered IPC subscriber-- @param ipc_signature string - The IPC signature-- @param ... any - Arguments to pass-- @return any - Result from function IPC, nil from action IPCfunction InvokeIPC(ipc_signature, ...)local entry = ipc_subscribers[ipc_signature]if not entry thenLog("InvokeIPC: IPC not loaded: %s", ipc_signature)return nilendif entry.kind == "function" thenreturn entry.sub:InvokeFunc(...)elseentry.sub:InvokeAction(...)return nilendend--- Clear IPC subscriber cachefunction ResetIPCCache()ipc_subscribers = {}Log("IPC subscriber cache cleared")end
IPC Usage Example
-- Register an IPC function that returns a booleanRequireIPC("SomePlugin.IsReady", "System.Boolean", {})-- Register an IPC action (no return value) with string argumentRequireIPC("SomePlugin.DoSomething", nil, {"System.String"})-- Invoke themlocal ready = InvokeIPC("SomePlugin.IsReady")if ready thenInvokeIPC("SomePlugin.DoSomething", "parameter")end
Distance Calculations
function GetDistanceToPoint(targetX, targetY, targetZ)if not Player.Available or not Player.Position thenreturn math.hugeendlocal px = Player.Position.Xlocal py = Player.Position.Ylocal pz = Player.Position.Zlocal dx = targetX - pxlocal dy = targetY - pylocal dz = targetZ - pzreturn math.sqrt(dx * dx + dy * dy + dz * dz)endfunction IsAtPosition(targetX, targetY, targetZ, tolerance)tolerance = tolerance or 2.0return GetDistanceToPoint(targetX, targetY, targetZ) <= toleranceendfunction DistanceBetween(x1, y1, z1, x2, y2, z2)local dx = x2 - x1local dy = y2 - y1local dz = z2 - z1return math.sqrt(dx * dx + dy * dy + dz * dz)endfunction GetDistanceToEntity(entity)if not entity or not entity.Position thenreturn math.hugeendlocal playerPos = Player.Positionlocal entityPos = entity.Positionlocal dx = entityPos.X - playerPos.Xlocal dy = entityPos.Y - playerPos.Ylocal dz = entityPos.Z - playerPos.Zreturn math.sqrt(dx * dx + dy * dy + dz * dz)end
Inventory Management
-- Get free inventory slotslocal freeSlots = Inventory.GetFreeInventorySlots()-- Get item countlocal itemCount = Inventory.GetItemCount(itemId)-- Get HQ item countlocal hqCount = Inventory.GetHqItemCount(itemId)-- Get collectable item countlocal collectableCount = Inventory.GetCollectableItemCount(itemId, collectability)-- Use itemlocal item = Inventory.GetInventoryItem(itemId)if item thenitem:Use()end-- Helper functionsfunction GetFreeInventorySlots()return Inventory.GetFreeInventorySlots()endfunction HasInventorySpace(requiredSlots)requiredSlots = requiredSlots or 1return Inventory.GetFreeInventorySlots() >= requiredSlotsendfunction GetItemCount(itemId)return Inventory.GetItemCount(itemId)endfunction HasItem(itemId, requiredCount)requiredCount = requiredCount or 1return Inventory.GetItemCount(itemId) >= requiredCountendfunction ValidateInventorySpace(requiredSlots, operation)requiredSlots = requiredSlots or 1operation = operation or "operation"if not HasInventorySpace(requiredSlots) thenyield("/echo [Script] ERROR: Not enough inventory space for " .. operation)return falseendreturn trueend
Timing Constants and Sleep
-- Standard timing constantsTIME = {POLL = 0.10, -- canonical polling stepTIMEOUT = 10.0, -- default time budgetSTABLE = 0.0 -- default stability window}-- Sleep helper (yields with /wait)function Sleep(seconds)local s = seconds or 0s = tonumber(s) or 0if s < 0 then s = 0 ends = math.floor(s * 10 + 0.5) / 10yield("/wait " .. s)end
Number Helper (Safe Parsing with Clamping)
-- Safe number parsing with optional min/max clampingfunction toNumberSafe(s, default, min, max)if s == nil then return default endlocal str = tostring(s):gsub("[^%d%-%.]", "")local n = tonumber(str)if n == nil then return default endif min ~= nil and n < min then n = min endif max ~= nil and n > max then n = max endreturn nend-- Examples:-- toNumberSafe("123", 0) -> 123-- toNumberSafe("abc", 0) -> 0-- toNumberSafe("50", 0, 0, 100) -> 50-- toNumberSafe("150", 0, 0, 100) -> 100 (clamped)-- toNumberSafe("-10", 0, 0, 100) -> 0 (clamped)
Wait and Timeout Patterns
function WaitWithTimeout(condition, timeout, interval)timeout = timeout or 30interval = interval or 0.1local startTime = os.clock()while not condition() and (os.clock() - startTime) < timeout doyield("/wait " .. interval)endreturn condition()end-- Wait until condition with timeout (basic version)function WaitUntil(condition, timeout)local startTime = os.clock()while not condition() and (os.clock() - startTime) < timeout doyield("/wait 0.1")endreturn condition()end
Advanced WaitUntil with Stability Window
--[[WaitUntil with stability window - condition must remain true for stableSeccontinuously before success is returned.Usage:WaitUntil(predicateFn, timeoutSec, pollSec, stableSec)predicateFn : function() -> true/false (checked each poll)timeoutSec : max seconds before giving up (default 10)pollSec : seconds between checks (default 0.10)stableSec : must remain true for this many seconds continuously (default 0)Returns: true if condition satisfied, false on timeoutExamples:-- Wait until addon "Talk" is ready (10s max):WaitUntilStable(function()return Addons.GetAddon("Talk").Readyend, 10.0)-- Wait until crafting condition holds for 2s (15s max):WaitUntilStable(function()return Svc.Condition[CharacterCondition.crafting]end, 15.0, 0.10, 2.0)]]function WaitUntilStable(predicateFn, timeoutSec, pollSec, stableSec)timeoutSec = toNumberSafe(timeoutSec, TIME.TIMEOUT, 0.1)pollSec = toNumberSafe(pollSec, TIME.POLL, 0.01)stableSec = toNumberSafe(stableSec, TIME.STABLE, 0.0)local start = os.clock()local holdStart = nilwhile (os.clock() - start) < timeoutSec dolocal ok, res = pcall(predicateFn)if ok and res thenif not holdStart then holdStart = os.clock() endif (os.clock() - holdStart) >= stableSec then return true endelseholdStart = nilendSleep(pollSec)endreturn falseend-- Wait for condition to be stable for a specified durationfunction WaitConditionStable(conditionIdx, want, stableSec, timeoutSec, pollSec)want = (want ~= false)stableSec = toNumberSafe(stableSec, 2.0, 0.0)timeoutSec = toNumberSafe(timeoutSec, 15.0, 0.1)pollSec = toNumberSafe(pollSec, TIME.POLL, 0.01)if not (Svc and Svc.Condition) thenDalamud.Log("[Script] WaitConditionStable: Svc.Condition unavailable")return falseendlocal ok = WaitUntilStable(function()return Svc.Condition[conditionIdx] == wantend, timeoutSec, pollSec, stableSec)if not ok thenDalamud.Log("[Script] WaitConditionStable: timeout")endreturn okend
Safe Function Execution
function SafeExecute(func, errorMessage)local success, result = pcall(func)if success thenreturn result, nilelsereturn nil, errorMessage or tostring(result)endend
Retry Pattern
function RetryWithBackoff(func, maxRetries, baseDelay)maxRetries = maxRetries or 3baseDelay = baseDelay or 1for attempt = 1, maxRetries dolocal success, result = pcall(func)if success thenreturn result, nilendif attempt < maxRetries thenlocal delay = baseDelay * (2 ^ (attempt - 1))yield("/echo [Script] Attempt " .. attempt .. " failed, retrying in " .. delay .. "s")yield("/wait " .. delay)endendreturn nil, "Function failed after " .. maxRetries .. " attempts"end
Addon Interactions
Addon State Checking
-- Check if addon is readyif Addons.GetAddon("AddonName").Ready then-- Addon is readyend-- Wait for addon to be readywhile not Addons.GetAddon("AddonName").Ready doyield("/wait 0.1")end-- Check if addon is visibleif Addons.GetAddon("AddonName").Visible then-- Addon is visibleend
Addon Callbacks
-- Basic callbackyield("/callback AddonName true 0")-- Callback with parametersyield("/callback AddonName true 1 2 3")-- Close addonyield("/callback AddonName false -1")
Addon Node Access
-- Get addon nodelocal node = Addons.GetAddon("AddonName"):GetNode(1, 2, 3)-- Get node textlocal text = node.Text-- Check if node is visibleif node.Visible then-- Node is visibleend
Safe Addon Functions
function IsAddonReady(addonName)return Addons.GetAddon(addonName).Readyendfunction WaitForAddonReady(addonName, timeout)timeout = timeout or 10local startTime = os.clock()while not Addons.GetAddon(addonName).Ready and (os.clock() - startTime) < timeout doyield("/wait 0.1")endreturn Addons.GetAddon(addonName).Readyendfunction SafeAddonCallback(addonName, ...)if not IsAddonReady(addonName) thenyield("/echo [Script] Addon not ready: " .. addonName)return falseendlocal args = {...}local callbackStr = "/callback " .. addonName .. " true"for _, arg in ipairs(args) docallbackStr = callbackStr .. " " .. tostring(arg)endyield(callbackStr)return trueend
Logging and Echo
-- Standard logging (to Dalamud log)Dalamud.Log("[ScriptName] Message")-- User echo (visible in game)yield("/echo [ScriptName] User message")-- Formatted loggingyield("/echo [Script] Value: " .. tostring(value))-- Error loggingyield("/echo [Script] ERROR: Description")
Teleportation Pattern
function TeleportTo(aetheryteName)yield("/li tp " .. aetheryteName)yield("/wait 1")-- Wait for casting to beginwhile Svc.Condition[CharacterCondition.casting] doyield("/wait 1")end-- Wait for teleport to completewhile Svc.Condition[CharacterCondition.betweenAreas] doyield("/wait 1")endyield("/wait 1")end
Svc.ClientState Access
Access client state information through Svc.ClientState:
-- Basic client state propertieslocal territoryType = Svc.ClientState.TerritoryType -- Current zone IDlocal mapId = Svc.ClientState.MapId -- Current map IDlocal isLoggedIn = Svc.ClientState.IsLoggedIn -- Login statuslocal localContentId = Svc.ClientState.LocalContentId -- Character content ID-- Local player accesslocal lp = Svc.ClientState.LocalPlayerif lp then-- World informationlocal function try(fn) local ok, v = pcall(fn); return ok and v or nil endlocal currentWorld = try(function() return lp.CurrentWorld.Value.Name:ToString() end)local homeWorld = try(function() return lp.HomeWorld.Value.Name:ToString() end)-- ClassJob informationlocal jobId = try(function() return lp.ClassJob.RowId end)or try(function() return lp.ClassJob.Value.RowId end)local jobAbbr = try(function() return lp.ClassJob.Value.Abbreviation:ToString() end)local jobName = try(function() return lp.ClassJob.Value.Name:ToString() end)print(("World: %s (Home: %s)"):format(currentWorld or "?", homeWorld or "?"))print(("Job: %s (%s) ID=%d"):format(jobName or "?", jobAbbr or "?", jobId or 0))end
Debug and Reflection Utilities
Utilities for inspecting objects at runtime (useful for debugging and discovery).
--- Print a formatted headinglocal function head(label) print(("\n== %s =="):format(label)) end--- Reflect & dump an instance's properties (name, CLR type, current value)-- @param label string - Label for the dump-- @param obj any - Object to inspectfunction DumpInstance(label, obj)if obj == nil then print(label .. ": <nil>"); return endlocal okT, t = pcall(function() return obj:GetType() end)if not okT or not t then print(label .. ": no GetType()"); return endhead(label)print("Type:", t.FullName)local props = t:GetProperties()for i = 0, props.Length - 1 dolocal p = props[i]local okV, val = pcall(function() return p:GetValue(obj, nil) end)local v = okV and val or "<error>"local vstr = (v == nil and "<nil>")or ((type(v) == "boolean" or type(v) == "number") and tostring(v))or tostring(v)print(string.format("%-22s : %-12s = %s", p.Name, p.PropertyType.Name, vstr))endend--- List property names & CLR types on an object without invoking getters-- @param label string - Label for the dump-- @param obj any - Object to inspectfunction DumpPropertyTypes(label, obj)if not obj then print(label .. ": <nil>"); return endlocal okT, t = pcall(function() return obj:GetType() end)if not okT or not t then print(label .. ": no GetType()"); return endhead(label .. " (property types)")print("Type:", t.FullName)local props = t:GetProperties()for i = 0, props.Length - 1 dolocal p = props[i]print(string.format("%-22s : %s", p.Name, p.PropertyType.Name))endend--- Dump only the *type* (shape) of a property on a parent object-- @param label string - Label for the dump-- @param parentObj any - Parent object-- @param propName string - Property name to inspectfunction DumpPropertyType(label, parentObj, propName)if not parentObj then print(label .. ": parent <nil>"); return endlocal ok, pt = pcall(function()local t = parentObj:GetType()local p = t:GetProperty(propName)return p and p.PropertyType or nilend)if not ok or not pt then print(label .. ": no type available"); return endhead(label .. " (property type)")print("Type:", pt.FullName)local props = pt:GetProperties()for i = 0, props.Length - 1 dolocal p = props[i]print(string.format("%-22s : %s", p.Name, p.PropertyType.Name))endend-- Example usage:-- DumpInstance("Player.Entity", Player.Entity)-- DumpPropertyTypes("Svc.Targets", Svc.Targets)-- DumpPropertyType("Player.Entity.Target", Player.Entity, "Target")
SND Built-in Global Functions
These are global functions available in SND Lua macros without needing any plugin.
Action Execution
-- Execute a game action safely by IDExecuteActionSafeNumber(number actionId, number actionType) → nil-- Execute action by nameExecuteAction(string actionName) → nil-- Execute action on targetExecuteActionOnTarget(string actionName, number targetId) → nil
String/Variable Storage
-- Get stored string valueGetString(string key) → string-- Set string valueSetString(string key, string value) → nil-- Get stored number valueGetNumber(string key) → number-- Set number valueSetNumber(string key, number value) → nil
Casting Information
-- Get casting-related number valueGetCastingNumber(string key) → number-- Set casting number valueSetCastingNumber(string key, number value) → nil-- Check if player is castingIsCasting() → boolean-- Get current cast time remainingGetCastTimeRemaining() → number
Targeting Functions
-- Get focus targetFocusTarget() → Entity-- Set target by entitySetTarget(Entity entity) → nil-- Get target infoGetTargetInfo() → table
Movement Checks
-- Check if something is nearing (distance check)GetNearing(number x, number y, number z, number distance) → boolean-- Check if player is movingIsMoving() → boolean-- Get distance to coordinatesGetDistanceTo(number x, number y, number z) → number
Addon/UI Access
-- Get addon by name (returns addon wrapper)GetAddon(string addonName) → Addon-- Check if addon is visibleIsAddonVisible(string addonName) → boolean-- Check if addon is readyIsAddonReady(string addonName) → boolean
SND Commands Reference
Macro Control Commands
-- Run a specific macro by nameyield("/pcraft run MacroName")-- Run all macros marked as loopyield("/pcraft runall")-- Run a macro in step mode (pause between steps)yield("/pcraft step MacroName")-- Pause all running macrosyield("/pcraft pause")-- Resume paused macrosyield("/pcraft resume")yield("/pcraft resume all")-- Stop current macroyield("/pcraft stop")-- Stop all running macrosyield("/pcraft stopall")-- Stop loop macros onlyyield("/pcraft stoploop")-- Toggle macro executionyield("/pcraft toggle")-- Show helpyield("/pcraft help")
Yield Commands (In-Macro Commands)
-- Execute a game actionyield("/action ActionName")yield("/ac ActionName") -- Short form-- Click a pre-defined UI buttonyield("/click ButtonName")-- Hold a key with auto-effects (stops at end of macro)yield("/hold KeyName")-- Use an item from inventoryyield("/item ItemName")-- Loop - copy current macro patternyield("/loop")yield("/loop 5") -- Loop 5 times-- Remove held keyboard keysyield("/notify")-- Require specific stats/items before continuingyield("/requirestats")yield("/require ItemName")-- Send key to gameyield("/send KeyName")yield("/sendkey KeyName")-- Target commandsyield("/target TargetName")yield("/targetenemy")-- Wait commandsyield("/wait 1.5") -- Wait 1.5 secondsyield("/waitaddon AddonName") -- Wait for addon to be ready
Available Click Targets
Common /click targets for UI automation:
-- Selection dialogsyield("/click AddonReaderSelect")yield("/click BannerSelect")yield("/click SelectString")yield("/click SelectYesno")-- Banking/Tradingyield("/click Bank")yield("/click Bankroll")-- Duty Finderyield("/click DutyFinder")yield("/click ContentsFinderConfirm")-- Grand Companyyield("/click GrandCompanySupplyList")-- Housingyield("/click HousingAethernet")-- Shopsyield("/click InclusionShop")yield("/click Shop")-- Talk/Dialogyield("/click Talk Click")-- Cancel/Closeyield("/click Cancel")
Best Practices
- Always check prerequisites before starting operations
- Use timeouts for all waiting operations
- Check plugin availability before using plugin APIs
- Use appropriate wait times: 0.1s for condition checks, 1s for movement/teleportation
- Handle all state transitions explicitly in state machines
- Provide clear error messages with
[Script]prefix - Validate configuration values before use
- Use semantic versioning (MAJOR.MINOR.PATCH)
- Update version fingerprint for any changes
- Use pcall for risky operations
- Check Player.Available before player operations
- Use Entity system for NPC interactions
- Use Excel.GetRow for game data lookups