示例脚本
随程序提供的 8 个脚本,完整源码在应用同级 scripts/ 目录。每个脚本头部 5 行注释说明依赖和加载行为。
keybind_hint_pack
给按键控件挂动态文字与颜色提示。演示 input.format 的三种返回形式:纯字符串、{text, color} table、按数值阈值动态变色。
local tab = gui.tab("KbHints")
local g_pack = tab:group("g", "三种 formatter 返值演示", 0, 0, 300, 0)
g_pack:keybind_control("alarm", "警报开关"):set_hint("返字符串:状态文字")
g_pack:keybind_control("stopwatch", "秒表"):set_hint("返 {text, color}:纯色")
g_pack:keybind_control("counter", "计数器"):set_hint("返 {text, color}:按值变色")
-- 返字符串
local alarm_armed = false
g_pack:button("toggle_alarm", "切换警报状态", function()
alarm_armed = not alarm_armed
gui.notify:info(alarm_armed and "警报已 ARMED" or "警报 OFF", 1.5)
end)
input.format("KbHints.g.alarm", function(id)
return alarm_armed and "ARMED" or "OFF"
end)
-- 返 {text, color} table,颜色固定
local start_t = os.clock()
input.format("KbHints.g.stopwatch", function(id)
return {
text = string.format("%.1fs", os.clock() - start_t),
color = { 0.55, 0.85, 1.0, 1.0 },
}
end)
-- 返 {text, color} table,按阈值变色
local counter = 0
g_pack:button("tick", "计数 +1", function() counter = counter + 1 end)
g_pack:button("reset", "计数清零", function()
counter = 0
gui.notify:warn("计数器已清零", 1.2)
end)
input.format("KbHints.g.counter", function(id)
local label = string.format("× %d", counter)
if counter == 0 then return { text = label, color = { 0.6, 0.6, 0.6, 1.0 } } end
if counter < 10 then return { text = label, color = { 0.3, 1.0, 0.3, 1.0 } } end
return { text = label, color = { 1.0, 0.45, 0.25, 1.0 } }
end)
log.success("keybind_hint_pack 注册完成")用到的 API:gui.tab、input.format、gui.notify、KeybindControl:set_hint
low_health_alert
队友倒地 · 自己 / 队友低血时一次性通知 + ESP 文字标红。配合反抖避免持续触发刷屏。
local tab = gui.tab("LowHpAlert")
local g_cfg = tab:group("g_cfg", "提示阈值(百分比)", 0, 0, 0, 0)
local th_self_el = g_cfg:slider("th_self", "自己血量低于", 1, 100, 30):set_format("%d%%")
local th_team_el = g_cfg:slider("th_teammate", "队友血量低于", 1, 100, 25):set_format("%d%%")
local th_enemy_el = g_cfg:slider("th_enemy", "敌方残血标红", 1, 100, 50):set_format("%d%%")
local notify_el = g_cfg:checkbox("notify_on", "启用通知提示", true)
-- 反抖
local last_alert = {}
local function should_fire(key)
local t = os.clock()
local prev = last_alert[key] or 0.0
if t - prev < 4.0 then return false end
last_alert[key] = t
return true
end
local function hp_pct(p)
local hp = p.health or 0
local max_total = 100.0 + math.max(p.max_shield or 0, 1)
local total = hp + (p.shield or 0)
return math.max(0, math.min(100, (total / max_total) * 100.0))
end
esp.add("self_hp", function(player, ctx)
if not (player.is_teammate and (player.distance or 999) < 1.0) then return nil end
local pct = hp_pct(player)
if pct < th_self_el:get() then
if notify_el:get() and should_fire("__self__") then
gui.notify:error(string.format("自己低血 %.0f%%!", pct), 3.0)
end
return { text = string.format("HP %.0f%% ⚠", pct),
text_color = { 1.0, 0.25, 0.25, 1.0 } }
end
end, { label = "Self HP", kind = "text" })
esp.add("teammate_state", function(player, ctx)
if not player.is_teammate then return nil end
if (player.distance or 0) < 1.0 then return nil end
if player.is_down then
if notify_el:get() and should_fire("DOWN:" .. (player.name or "?")) then
gui.notify:warn(string.format("队友 %s 倒了!", player.name or "?"), 3.5)
end
return { text = "💀 DOWN", text_color = { 1.0, 0.3, 0.3, 1.0 } }
end
local pct = hp_pct(player)
if pct < th_team_el:get() then
if notify_el:get() and should_fire("LOW:" .. (player.name or "?")) then
gui.notify:info(string.format("队友 %s 残血 %.0f%%", player.name or "?", pct), 2.5)
end
return { text = string.format("%.0f%%", pct),
text_color = { 1.0, 0.7, 0.2, 1.0 } }
end
end, { label = "队友状态", kind = "text" })
esp.add("enemy_lowhp", function(player, ctx)
if player.is_teammate then return nil end
local pct = hp_pct(player)
if pct >= th_enemy_el:get() then return nil end
local color = pct < 25 and { 1.0, 0.15, 0.15, 1.0 } or { 1.0, 0.4, 0.4, 1.0 }
return { text = string.format("%.0f%%", pct), text_color = color }
end, {
label = "敌方残血", kind = "text",
dist = { 5.0, 250.0 },
})
log.success("low_health_alert 已注册 3 个 ESP 文本元素")用到的 API:esp.add、game.entities.players、gui.notify
tactical_theme
按战术情境(默认 / 高对比 / 隐身)一键切换 ESP 配色。一行 el.colors = {c0,c1,c2,c3} 批量设 4 个 scenario 颜色。
local THEMES = {
{
name = "Default",
colors = {
{ 0.85, 0.85, 0.85, 1.0 }, -- 不可见敌:白
{ 1.00, 0.35, 0.35, 1.0 }, -- 可见敌:红
{ 1.00, 0.80, 0.20, 1.0 }, -- 倒地:橙
{ 0.35, 0.85, 1.00, 1.0 }, -- 队友:浅蓝
},
},
{
name = "HighContrast",
colors = {
{ 0.40, 0.40, 0.40, 1.0 },
{ 1.00, 1.00, 0.20, 1.0 },
{ 1.00, 0.10, 0.10, 1.0 },
{ 0.20, 1.00, 0.40, 1.0 },
},
},
{
name = "Stealth",
colors = {
{ 1.00, 0.60, 1.00, 1.0 },
{ 1.00, 0.40, 0.10, 1.0 },
{ 0.60, 0.60, 0.60, 1.0 },
{ 0.10, 0.60, 1.00, 1.0 },
},
},
}
local tac_tag = esp.add("tac_tag", function(p, ctx)
if p.is_teammate then return "ALLY" end
if p.is_down then return "DWN" end
if p.is_visible then return "VIS" end
return "INV"
end, {
label = "Tac Tag",
kind = "text",
colors = THEMES[1].colors,
dist = { 5.0, 300.0 },
})
local function apply_theme(idx)
local theme = THEMES[idx]
if not theme then
gui.notify:error("主题索引越界:" .. tostring(idx), 2.0)
return
end
tac_tag.colors = theme.colors
gui.notify:success("已切到主题:" .. theme.name, 2.5)
end
local tab = gui.tab("TacTheme")
local g_main = tab:group("g_main", "战术配色", 0, 0, 0, 0)
local theme_names = {}
for i, t in ipairs(THEMES) do theme_names[i] = t.name end
g_main:dropdown("theme", "当前主题", theme_names, 1)
:on_change(function(idx, name) apply_theme(idx) end)
apply_theme(1)
log.success("tactical_theme 已注册 3 个主题")用到的 API:esp.add、ESPElement.colors、Combobox:on_change
perf_hud
浮动窗口显示 FPS · 帧时(平均 / 最小 / 最大)· loot 数量。win:bind_visible(checkbox) 自动同步窗口可见性。
local tab = gui.tab("PerfHud")
local g_ctrl = tab:group("g_ctrl", "性能 HUD 控制", 0, 0, 350, 0)
local cb_show = g_ctrl:checkbox("show", "显示浮动窗口", false)
local sl_cap = g_ctrl:slider("smooth_frames", "滑窗帧数", 5, 240, 60):set_format("%d frames")
local win = gui.window("perf_hud_window", "Perf HUD", 240.0, 140.0)
win:bind_visible(cb_show)
local ring, ring_head, ring_size = {}, 1, 0
local last_fps, last_ms, last_max, last_min = 0.0, 0.0, 0.0, 999.0
event.on("frame_update", function(e)
local dt = e.delta_time
if dt <= 0.0 then return end
local cap = math.Clamp(sl_cap:get() or 60, 5, 240)
ring[ring_head] = dt
ring_head = ring_head + 1
if ring_head > cap then ring_head = 1 end
if ring_size < cap then ring_size = ring_size + 1 end
local sum, mx, mn = 0.0, 0.0, 9999.0
for i = 1, ring_size do
local v = ring[i]
sum = sum + v
if v > mx then mx = v end
if v < mn then mn = v end
end
local avg = sum / ring_size
last_fps = (avg > 0.0) and (1.0 / avg) or 0.0
last_ms = avg * 1000.0
last_max = mx * 1000.0
last_min = mn * 1000.0
end)
win:live_table("metrics",
{ "指标", "值" },
function()
local loots, players = 0, 0
for _ in pairs(game.entities.loots) do loots = loots + 1 end
for _ in pairs(game.entities.players) do players = players + 1 end
local rows = {
{ cells = { "FPS", string.format("%.1f", last_fps) } },
{ cells = { "帧时 avg", string.format("%.2f ms", last_ms) } },
{ cells = { "帧时 min", string.format("%.2f ms", last_min) } },
{ cells = { "帧时 max", string.format("%.2f ms", last_max) } },
{ cells = { "loots", tostring(loots) } },
{ cells = { "players", tostring(players) } },
}
local lp = game.localplayer
if lp then
table.insert(rows, { cells = { "HP/Shield",
string.format("%d / %d", lp.health or 0, lp.shield or 0) } })
table.insert(rows, { cells = { "Map", game.world.map_name or "?" } })
end
if last_ms > 16.67 then rows[2].color = { 0.55, 0.20, 0.20, 0.35 } end
if last_max > 33.0 then rows[4].color = { 0.55, 0.45, 0.15, 0.35 } end
return rows
end, 180.0)
log.success("perf_hud 已加载")用到的 API:gui.window、Window:bind_visible、event.on frame_update、Window:live_table
loadout_presets
多套配置一键切换,浮窗显示当前预设。批量 gui.set 写控件 + gui.set_visible 控制子页签显隐。
local tab = gui.tab("Loadout")
local g_demo = tab:group("g_demo", "演示控件(被预设修改)", 0, 0, 0, 0)
g_demo:checkbox("aggressive", "开火激进", false)
g_demo:checkbox("auto_heal_prio", "治疗优先", true)
g_demo:slider ("range", "交战距离上限", 10, 300, 100):set_format("%d m")
g_demo:slider ("smoothing", "瞄准平滑", 1, 50, 10)
g_demo:dropdown("playstyle_tag", "Playstyle 标签",
{ "Default", "Holder", "Rusher", "Safe" }, 2)
local sub_adv = gui.sub_tab(tab, "advanced", "进阶(预设可见性)")
local g_adv = sub_adv:group("g_adv", "进阶控件", 0, 0, 0, 0)
g_adv:checkbox("rapid_swap", "快速换弹", false)
g_adv:slider ("trigger_delay", "Trigger 延迟", 0, 200, 80):set_format("%d ms")
local advanced_paths = {
"Loadout.advanced",
"Loadout.advanced.g_adv.rapid_swap",
"Loadout.advanced.g_adv.trigger_delay",
}
local PRESETS = {
["Holder 守点"] = {
tag_index = 2,
values = {
["Loadout.g_demo.aggressive"] = false,
["Loadout.g_demo.auto_heal_prio"] = true,
["Loadout.g_demo.range"] = 250,
["Loadout.g_demo.smoothing"] = 6,
},
advanced_visible = false,
},
["Rusher 突袭"] = {
tag_index = 3,
values = {
["Loadout.g_demo.aggressive"] = true,
["Loadout.g_demo.auto_heal_prio"] = false,
["Loadout.g_demo.range"] = 60,
["Loadout.g_demo.smoothing"] = 18,
},
advanced_visible = true,
},
["Safe 安全运营"] = {
tag_index = 4,
values = {
["Loadout.g_demo.aggressive"] = false,
["Loadout.g_demo.auto_heal_prio"] = true,
["Loadout.g_demo.range"] = 180,
["Loadout.g_demo.smoothing"] = 25,
},
advanced_visible = false,
},
}
local preset_order = { "Holder 守点", "Rusher 突袭", "Safe 安全运营" }
local current_preset = "Holder 守点"
local function apply_preset(name)
local p = PRESETS[name]
if not p then return end
for path, val in pairs(p.values) do gui.set(path, val) end
gui.set("Loadout.g_demo.playstyle_tag", p.tag_index)
gui.set_visible(advanced_paths, p.advanced_visible)
current_preset = name
gui.notify:success("已应用预设:" .. name, 2.5)
end
local g_pick = tab:group("g_pick", "预设选择", 0, 0, 0, 0)
g_pick:dropdown("preset", "当前预设", preset_order, 1)
:on_change(function(idx, name) apply_preset(name) end)
g_pick:button("apply_now", "重新应用当前预设", function() apply_preset(current_preset) end)
local win = gui.window("loadout_status", "Loadout", 220.0, 100.0)
win:set_open(true)
win:group("g_win", "当前预设", 0, 0, 0, 0)
:live_table("status", { "项", "值" }, function()
return {
{ cells = { "预设", current_preset } },
{ cells = { "激进开火", tostring(gui.get("Loadout.g_demo.aggressive")) } },
{ cells = { "交战距离", tostring(gui.get("Loadout.g_demo.range")) } },
{ cells = { "瞄准平滑", tostring(gui.get("Loadout.g_demo.smoothing")) } },
}
end, 110.0)
apply_preset("Holder 守点")
log.success("loadout_presets 已加载 3 套预设")用到的 API:gui.sub_tab、gui.set / gui.get、gui.set_visible、Combobox:on_change、Window:set_open
loot_inspector
实时显示当前掉落物快照——名字、距离、等级。带名字过滤。
local tab = gui.tab("LootInspector")
local cb_show = tab:checkbox("show", "显示窗口", false)
local filter = tab:input_text("filter", "名字过滤", "")
local win = gui.window("loot_inspector_win", "Loot Inspector", 520, 420)
win:bind_visible(cb_show)
win:live_table("loots", { "序号", "名字", "距离", "等级" }, function()
local rows = {}
local f = filter:get():lower()
local idx = 0
for _, l in pairs(game.entities.loots) do
idx = idx + 1
local name = (l.classified_name ~= "" and l.classified_name)
or (l.base_name and l.base_name ~= "" and l.base_name)
or l.model_name
or "?"
if f == "" or name:lower():find(f, 1, true) then
table.insert(rows, {
tostring(idx),
name,
string.format("%.0f m", l.distance / 39.37),
tostring(l.quality_level)
})
end
if #rows >= 200 then break end
end
return rows
end)
log.success("loot_inspector 已加载")用到的 API:game.entities.loots、Window:bind_visible、InputText:get
remote_config_push
WebSocket 接收 path=value 命令热改菜单。配合 HTTP 探针做连通性测试。完整四回调(open / message / error / close)。
local state = {
ws_id = 0,
last_msg = nil,
log_buf = {},
}
local function push_log(dir, text)
if #state.log_buf >= 50 then table.remove(state.log_buf, 1) end
state.log_buf[#state.log_buf + 1] = { ts = os.clock(), dir = dir, text = text }
end
local function parse_value(s)
if s == "true" then return true end
if s == "false" then return false end
local n = tonumber(s)
if n ~= nil then return n end
return s
end
local function apply_path_value(text)
local sep = string.find(text, "=", 1, true)
if not sep then
push_log("!", "格式错误,缺 '=': " .. text)
return
end
local path = string.sub(text, 1, sep - 1)
local val = parse_value(string.sub(text, sep + 1))
if not gui.find(path) then
push_log("!", "找不到控件:" .. path)
return
end
gui.set(path, val)
push_log("<", string.format("apply %s = %s", path, tostring(val)))
end
local tab = gui.tab("RemoteCfg")
local g_conn = tab:group("g_conn", "连接", 0, 0, 0, 0)
g_conn:input_text("url", "WebSocket URL", "ws://127.0.0.1:9000/cfg")
g_conn:input_text("probe_url", "HTTP 探针 URL", "http://127.0.0.1:9000/health")
g_conn:button("probe", "HTTP 探针", function()
local url = gui.get("RemoteCfg.g_conn.probe_url", "")
if type(url) ~= "string" or url == "" then return end
net.http:get(url, function(resp)
if resp.ok then
gui.notify:success(string.format("探针 OK %d", resp.status), 2.0)
push_log("<", string.format("HTTP %d", resp.status))
else
gui.notify:error("探针失败:" .. (resp.error or "unknown"), 3.0)
push_log("!", "HTTP fail: " .. (resp.error or ""))
end
end, 3)
end)
g_conn:button("connect", "Connect", function()
if state.ws_id ~= 0 and net.ws:is_open(state.ws_id) then
gui.notify:warn("已连接,请先 Disconnect", 1.5)
return
end
local url = gui.get("RemoteCfg.g_conn.url", "")
if type(url) ~= "string" or url == "" then return end
state.ws_id = net.ws:connect(url, {
on_open = function(id)
push_log("!", "OPEN id=" .. tostring(id))
gui.notify:success("WebSocket 已连接", 2.0)
end,
on_message = function(id, msg)
state.last_msg = msg
push_log("<", msg)
if string.sub(msg, 1, 4) == "ping" then return end
apply_path_value(msg)
end,
on_error = function(id, err)
push_log("!", "ERR " .. err)
gui.notify:error("WebSocket 错误:" .. err, 3.0)
end,
on_close = function(id, code, reason)
push_log("!", string.format("CLOSE code=%d", code))
gui.notify:info(string.format("连接关闭 (%d)", code), 2.0)
end,
})
end)
g_conn:button("disconnect", "Disconnect", function()
if state.ws_id == 0 then return end
net.ws:close(state.ws_id)
state.ws_id = 0
end)
local g_status = tab:group("g_status", "状态", 0, 0, 0, 0)
g_status:live_table("st", { "key", "value" }, function()
local open = (state.ws_id ~= 0) and net.ws:is_open(state.ws_id) or false
local row_state = open and "OPEN" or (state.ws_id == 0 and "IDLE" or "CLOSED")
return {
{ cells = { "ws_id", tostring(state.ws_id) } },
{ cells = { "state", row_state },
color = open and {0.20, 0.55, 0.30, 0.35} or {0.40, 0.40, 0.40, 0.25} },
{ cells = { "last_msg", state.last_msg or "" } },
}
end, 80.0)
log.success("remote_config_push 已加载,点 Connect 开始")用到的 API:net.ws、net.http:get、gui.find、gui.set
damage_logger
伤害日志本地 CSV + HTTP POST 归档。ESP 文本回调当 polling 钩子追踪 HP / shield 跳变,帧节拍 flush 缓冲,卸载时 final flush。
属于少数需要手动监听 script_unloaded 的脚本——脚本内部缓冲必须显式写盘才会持久化。
local prev = {}
local events_buf = {}
local total_logged = 0
local last_flush_clock = os.clock()
local recent = {}
local function cfg_get(suffix, fallback)
local e = gui.find("DmgLog.g_cfg." .. suffix)
if not e then return fallback end
local v = e:get()
return v == nil and fallback or v
end
local function build_line(ev)
return string.format("%.3f,%s,%s,%d,%d,%d,%d,%s\n",
ev.t, (ev.name or ""):gsub(",", ";"), ev.team,
ev.dmg_hp, ev.dmg_shield, ev.hp_after, ev.shield_after,
ev.is_down and "1" or "0")
end
local function flush_now(silent)
if #events_buf == 0 then
if not silent then gui.notify:info("无待 flush 事件", 1.5) end
return
end
local n = #events_buf
local lines = {}
for i = 1, n do lines[i] = build_line(events_buf[i]) end
local payload = table.concat(lines)
if cfg_get("to_file", false) then
local fp = cfg_get("file_path", "logs/damage.csv")
if type(fp) == "string" and fp ~= "" then
local existing = file.exists(fp) and (file.read(fp) or "")
or "t,name,team,dmg_hp,dmg_shield,hp_after,shield_after,is_down\n"
if not file.write(fp, existing .. payload) and not silent then
gui.notify:error("写盘失败:" .. fp, 3.0)
end
end
end
if cfg_get("to_http", false) then
local url = cfg_get("http_url", "")
if type(url) == "string" and url ~= "" then
net.http:post(url, payload, "text/csv", function(resp)
if not resp.ok then log.warn("HTTP POST 失败:" .. (resp.error or "")) end
end, 5)
end
end
events_buf = {}
total_logged = total_logged + n
last_flush_clock = os.clock()
if not silent then gui.notify:success(string.format("已 flush %d 条", n), 2.0) end
end
local tab = gui.tab("DmgLog")
local g_cfg = tab:group("g_cfg", "归档配置", 0, 0, 0, 0)
g_cfg:checkbox("to_file", "写本地文件", false)
g_cfg:input_text("file_path", "本地路径", "logs/damage.csv")
g_cfg:checkbox("to_http", "POST 到远端", false)
g_cfg:input_text("http_url", "远端 URL", "")
g_cfg:slider("flush_sec", "flush 间隔", 1, 60, 5):set_format("%d s")
g_cfg:checkbox("log_teammate", "记录队友伤害", false)
g_cfg:button("flush_now", "立即 flush", function() flush_now(false) end)
g_cfg:button("clear", "清空缓冲", function()
events_buf = {}
gui.notify:info("缓冲已清空", 1.5)
end)
local g_stat = tab:group("g_stat", "状态", 0, 0, 0, 0)
g_stat:live_table("st", { "key", "value" }, function()
local tracked = 0
for _ in pairs(prev) do tracked = tracked + 1 end
return {
{ cells = { "pending", tostring(#events_buf) } },
{ cells = { "total flushed", tostring(total_logged) } },
{ cells = { "since flush", string.format("%.1f s", os.clock() - last_flush_clock) } },
{ cells = { "tracked players", tostring(tracked) } },
}
end, 100.0)
local g_log = tab:group("g_log", "最近事件", 0, 0, 0, 0)
g_log:live_table("ev_tbl",
{ "#", "t", "对象", "team", "Δhp", "Δsh", "hp", "sh", "down" },
function()
local rows = {}
local n = #recent
for i = n, 1, -1 do
local ev = recent[i]
local color = ev.is_down and {0.55, 0.20, 0.20, 0.35}
or ev.team == "ally" and {0.20, 0.40, 0.55, 0.30}
or nil
rows[#rows + 1] = {
cells = { tostring(n - i + 1), string.format("%.1f", ev.t),
ev.name or "", ev.team,
tostring(ev.dmg_hp), tostring(ev.dmg_shield),
tostring(ev.hp_after), tostring(ev.shield_after),
ev.is_down and "✓" or "·" },
color = color,
}
end
return rows
end, 240.0)
esp.add("dmg_watch", function(p, ctx)
local name = p.name
if not name or name == "" then return "" end
local cur_hp = p.health or 0
local cur_sh = p.shield or 0
local cur_dn = p.is_down or false
local is_team = p.is_teammate or false
local p_prev = prev[name]
if not p_prev then
prev[name] = { hp = cur_hp, shield = cur_sh, down = cur_dn }
return ""
end
local dhp = math.max(0, p_prev.hp - cur_hp)
local dsh = math.max(0, p_prev.shield - cur_sh)
local down_changed = (cur_dn ~= p_prev.down)
if dhp > 0 or dsh > 0 or down_changed then
if (not is_team) or cfg_get("log_teammate", false) then
local ev = {
t = os.clock(), name = name,
team = is_team and "ally" or "enemy",
dmg_hp = dhp, dmg_shield = dsh,
hp_after = cur_hp, shield_after = cur_sh,
is_down = cur_dn,
}
events_buf[#events_buf + 1] = ev
if #recent >= 30 then table.remove(recent, 1) end
recent[#recent + 1] = ev
end
end
p_prev.hp, p_prev.shield, p_prev.down = cur_hp, cur_sh, cur_dn
return ""
end, {
kind = "text",
label = "Damage Watcher",
colors = { {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0} },
})
event.on("frame_update", function(e)
local interval = cfg_get("flush_sec", 5)
if type(interval) ~= "number" then interval = 5 end
if os.clock() - last_flush_clock >= interval and #events_buf > 0 then
flush_now(true)
end
end)
event.on("script_unloaded", function(ue)
if ue.script_path ~= _SCRIPT_PATH then return end
flush_now(true)
end)
log.success("damage_logger 已加载")用到的 API:esp.add、event.on、file.read/write/exists、net.http:post、gui.find