Description: Rolling my own bot, as everyone else should!
Currently has:
Simple player detection,
Bandaging and using health pots on low health
Disarm detection and rearming,
Cure poison with potion or bandage,
Scavenging,
Buffs, Song of Healing
Pop pouch,
Moongate,
Stamina drinking
Supports manually curing or bandaging while its running.
Configure what you want at the top.
Will update the list when more is added.
https://i.imgur.com/yKAkSA7.gif
-- Scripted by Halesluker, Knurr on Ultima Online Sagas
---
--- CONFIG
---
-- TODO: Use immutable fields and values
local Config = {
-- Adjust the ActionWaitTime if you experience issues, set it longer, ex. 1500 on high ping
ActionWaitTime = 1000, -- in milliseconds, how long to wait for actions like using items, targeting etc.
EnableOverheadMessages = true, -- Enables overhead messages, if false then messages will be printed in journal
EnableCure = true, -- Cures poison with potions or bandages
EnableBandage = true, -- Bandages player if HP is below BandageHP or if poisoned and no cure potions
EnableScavenging = false, -- Scavenges items from the ground, only arrows, add more if needed
EnablePopPouch = true, -- Pops pouch if you are paralyzed in PvP mode
EnableDisarmDetection = true, -- Rearms your weapon if you are disarmed
-- DO NOT STORE POTIONS IN BANK IF YOU ENABLE THIS, SCRIPT WILL LOOP TO TRY TO DRINK THEM
EnableBuffs = true, -- Enables buffs like Stamina, Strength, Agility, Food and Song of Healing
EnableMoongate = true, -- Opens moongate if you are near one
EnableHunting = false, -- Alerts you when a player from the hunt list is visible
EnableCancel = false, -- Stops the script from running by saying a unique command
EnableEscape = false, -- Saying escape and the escape command in the escape config will port you
EnableOpenCorpses = false, -- Opens corpses if you are near one, even if previously closed, not implemented yet
EnableIdentification = false, -- Starts identifcation after standing still for 30 seconds, not implemented yet
Debug = false,
DebugTick = 1000, -- Overrides MainTick in debug mode
DatePattern = "%H:%M:%S",
InfoTextColor = 88,
WarningTextColor = 34,
ErrorTextColor = 53,
DebugTextColor = 1153,
MainTick = 60, -- in milliseconds
JournalTick = 0, -- milliseconds, zero means immediate
}
local CancelConfig = {
Command = "YOUR UNIQUE COMMAND HERE", -- The command to say, make it unique to you
}
local CureConfig = {
Potions = {0x0f07},
Tick = 0, -- milliseconds, zero means immediate
}
local BandageConfig = {
BandageHP = 99, -- in percentage, when to use bandage
Bandages = { 0x00e21 },
HealPotionHP = 20, -- in percentage, when to use heal potion
HealPotions = { 0x0f0c },
HealthPotionRecentTime = 15 * 1000,
OverheadPauseTime = 0, -- in ms, zero means only when beginning bandage
WarningPauseTime = 60 * 1000
}
local HuntConfig = {
Players = { "oFrizz", "FloodgateUO", "Lespunk Strange", "Vector", "BTK", "RDY", "BRG", "URK" },
AlertPauseTime = 10 * 1000 -- alert once per 10 seconds
}
local DisarmConfig = {
AlwaysRearm = false, -- rearm without moving, warning will spam messages if you drag from hands
AlertPauseTime = 10 * 1000, -- alert once per 10 seconds
}
local ScavengeConfig = {
Tick = 0, -- milliseconds, zero means immediate
items = {
0x0F3F
}
}
local GateConfig = {
LookupPauseTime = 1000, -- in milliseconds, how often to look for moongate
gumpId = 585180759,
OverheadPauseTime = 5 * 1000,
}
local EscapeConfig = {
Command = "I shall return!", -- The command to say, make it unique to you
Callback = function() -- Use the assistant and record your way of escaping and paste it below
Player.UseObject('1110433901')
Gumps.WaitForGump(1498407526, 1000)
Gumps.PressButton(1498407526, 26)
return true
end,
}
local BuffsConfig = {
buffs = {
SongOfHealing = {
Enable = true,
FailWait = 30 * 1000, -- in ms, how long to retry if already under effects by manual cast
},
Stamina = {
Enable = true,
DrinkBelowPercentage = 50, -- in percentage, when to drink stamina potion
Potions = {0x0f0b},
},
Strength = {
Enable = true,
Potions = {0x0f09},
},
Agility = {
Enable = true,
Potions = {0x0f08},
},
NightSight = {
Enable = false,
},
Food = {
Enable = true,
EatCooldown = 15 * 60 * 1000, -- in ms, how often to eat food
},
},
debuffs = {
Peacemaking = {
Enable = false,
},
},
Instruments = {"Drum", "Lute", "Tambourine", "Lap Harp" }
}
---
--- STATE
---
-- TODO: Use immutable fields and mutable values
local state = {
currentTickTime = math.floor(os.clock() * 1000),
lastJournalTickTime = 0,
flaggedForPvp = false,
disarmed = nil,
cure = {
useBandages = false,
},
bandage = {
lastTickTime = nil,
lastOverheadTime = 0,
isBandaging = false,
lastPotionDrinkTime = 0,
},
hunt = {
lastTickTime = nil,
lastOverheadTime = 0,
},
scavenge = {
lastTickTime = nil,
},
-- TODO: Implement states below
inventory = {
bandages = nil,
healthPotions = nil,
curePotions = nil,
agilityPotions = nil,
strengthPotions = nil,
nightSightPotions = nil,
},
buffs = {
songOfHealing = {
isActive = false,
startTime = nil,
endTime = nil,
duration = 163 * 1000, -- Need to calculate based on music skill?
lastWarningTickTime = nil,
instrument = nil,
},
strength = {
lastStr = 999,
isActive = false,
startTime = nil,
endTime = nil,
duration = 120 * 1000, -- Need to calculate based on alchemy skill
},
agility = {
lastDex = 999,
isActive = false,
startTime = nil,
endTime = nil,
duration = 120 * 1000, -- Need to calculate based on alchemy skill
},
nightSight = {
isActive = false,
startTime = nil,
endTime = nil,
duration = 120,
},
},
debuffs = {
peacemaking = {
enemies = {
--[[
{
serial = "12345678", -- Serial ID of the enemy
name = "Enemy Name", -- Name of the enemy
lastTickTime = timestamp, -- Last time we checked this enemy
endTime = timestamp, -- When the Peacemaking effect ends
},
]]
},
isActive = false,
startTime = nil,
endTime = nil,
duration = 163, -- Need to calculate based on music skill?
lastWarningTickTime = nil,
},
},
moongate = {
lastTickTime = nil,
lastOverheadTime = nil,
serial = nil,
gateChangeDistance = 0,
},
food = {
lastEatTime = nil,
}
}
---
--- DATA
---
local data = {
foods = {
[0x1602] = "Bowl Of Potatoes",
[0x1601] = "Bowl Of Peas",
[0x1600] = "Bowl Of Lettuce",
[0x15ff] = "Bowl Of Corn",
[0x15fc] = "Bowl Of Peas",
[0x15fb] = "Bowl Of Lettuce",
[0x15fa] = "Bowl Of Corn",
[0x15f9] = "Bowl Of Carrots",
[0x9ec] = "Jar Of Honey",
[0x9bb] = "Roast Pig",
[0x1606] = "Tomato Soup",
[0x1604] = "Bowl Of Stew",
[0x15f8] = "Wooden Bowl",
[0x1608] = "Chicken Leg",
[0x160a] = "Leg Of Lamb",
[0x9b7] = "Cooked Bird",
[0x97e] = "Wheel Of Cheese",
[0x9eb] = "Muffins",
[0x9e9] = "Cake",
[0x1041] = "Baked Apple Pie",
[0x103b] = "Bread Loaf",
[0x09d0] = "Apple",
[0x09d1] = "Grape Bunch",
[0x09d2] = "Peach",
[0x0994] = "Pear",
[0x0c5c] = "Watermelon",
[0x0c64] = "Gourd",
[0x0c6a] = "Pumpkin",
[0x0c6d] = "Onion",
[0x0c70] = "Head Of Lettuce",
[0x0c72] = "Squash",
[0x0c74] = "Honeydew Melon",
[0x0c78] = "Carrot",
[0x0c79] = "Canteloupe",
[0x0c7b] = "Head Of Cabbage",
[0x09ad] = "Pitcher Of Milk",
[0x09b5] = "Eggs"
}
}
---
--- PRINT UTILS
---
local function message(text, color, sendFunc)
if not text or (type(text) ~= "string" and type(text) ~= "table") then
text = "Variabel message to print needs to be a string or table"
end
if not color or type(color) ~= "number" then
color = Config.InfoTextColor
end
if type(text) == "table" then
text = table.concat(text, ", ")
end
sendFunc(text, color, Player.Serial)
end
local function print(text, color)
message(text, color, Messages.Print)
end
local function overhead(text, color)
local printFn = Messages.Overhead
if not Config.EnableOverheadMessages then
printFn = Messages.Print
end
message(text, color, printFn)
end
local function info(text)
overhead(text, Config.InfoTextColor)
end
local function warning(text)
overhead(text, Config.WarningTextColor)
end
---@diagnostic disable-next-line: unused-local, unused-function
local function error(text)
overhead(text, Config.ErrorTextColor)
end
local function debug(text)
if not Config.Debug then return end
local ok, timestamp = pcall(function()
return os.date(Config.DatePattern, os.time()) .. "." .. string.format("%03d", os.clock() * 1000 % 1000)
end)
if not ok then
timestamp = os.time()
end
if Config.DebugTick > Config.MainTick then
Pause(Config.DebugTick - Config.MainTick)
end
print("[" .. timestamp .. "] " .. text, Config.DebugTextColor)
end
---
--- UTILITY FUNCTIONS
---
local function pauseUntil(callback, interval, timeout)
local startTime = math.floor(os.clock() * 1000)
while (math.floor(os.clock() * 1000) - startTime) < timeout do
if callback() then
return true
end
Pause(interval)
end
return false
end
local function findInInventory(itemType)
local items = Items.FindByFilter({ graphics = itemType, onground = false })
if not items or #items == 0 then
return nil
end
-- Filter out items that are not on player
for i = #items, 1, -1 do
if items[i].RootContainer ~= Player.Serial then
table.remove(items, i)
end
end
return items
end
---@diagnostic disable-next-line: unused-local, unused-function
local function findInSurroundings(itemType, range)
local items = Items.FindByFilter({ graphics = itemType, range = range })
if not items or #items == 0 then
return nil
end
return items[1]
end
local function exceedsDuration(startTime, endTime, duration)
if startTime == nil then
return true
end
if endTime == nil then
endTime = math.floor(os.clock() * 1000)
end
if duration == nil then
duration = 1000
end
return (endTime - startTime) >= duration
end
local function bandageEndTime(start)
local delayMs = math.ceil((9.0 + 0.85 * ((130 - Player.Dex) / 20)) * 1000)
local baseTime = start or state.currentTickTime or math.floor(os.clock() * 1000)
return baseTime + delayMs
end
local function disarmPlayer()
if state.disarmed then
debug("Player is already disarmed, skipping disarm.")
return state.disarmed
end
local disarmState = { weapon = nil, hand = nil }
local weapon = Items.FindByLayer(2)
if weapon then
Player.ClearHands("right")
-- Wait for hands to be cleared
Pause(Config.ActionWaitTime)
end
disarmState = { weapon = weapon, hand = function() return Items.FindByLayer(2) end }
state.disarmed = disarmState
return disarmState
end
local function rearmPlayer()
if not state.disarmed or not state.disarmed.weapon then
debug("Player is not disarmed, skipping rearm.")
return
end
while state.disarmed.hand() == nil do
Player.Equip(state.disarmed.weapon.Serial)
Pause(Config.ActionWaitTime) -- Wait for weapon to be equipped
end
state.disarmed = nil
end
---
--- CURE
---
local function cure()
if not Config.EnableCure then
return
end
debug("Trying to cure...")
if Player.IsHidden then
debug("Player is hiding, skipping cure.")
return
end
debug("Player is poisoned: " .. tostring(Player.IsPoisoned))
if not Player.IsPoisoned then
if state.cure.isPoisoned then
info("Cured")
end
state.cure.isPoisoned = false
state.cure.useBandages = false
debug("Player is not poisoned")
return
end
state.cure.isPoisoned = true
if state.cure.useBandages then
debug("Using bandages to cure poison.")
return
end
warning("Poisoned")
local tick = CureConfig.Tick > 0 and CureConfig.Tick or 1000
if not exceedsDuration(state.bandage.lastPotionDrinkTime, state.currentTickTime, tick) then
debug("A potion was recently used, skipping.")
return
end
debug("Player is poisoned, looking for cure potion.")
local potions = findInInventory(CureConfig.Potions)
if not potions or #potions == 0 then
if not state.cure.useBandages then
warning("No cure potions")
end
state.cure.useBandages = true
return
else
state.cure.useBandages = false
debug("Found " .. #potions .. " cure potions in inventory.")
end
local alchemySkill = Skills.GetValue("Alchemy")
if alchemySkill and alchemySkill < 80 then
debug("Alchemy skill is below 80, disarming weapon to use health potion.")
disarmPlayer()
end
local hasUsedPotion = false
for _, potion in ipairs(potions) do
if not potion and not potion.Serial then
goto endcure
end
debug("Using cure potion: " .. (potion.Name or "No Potion Name"))
if not Player.UseObject(potion.Serial) then
debug("Failed to use cure potion: " .. (potion.Name or "No Potion Name"))
goto endcure
end
local cured = pauseUntil(function () return Journal.Contains("You feel cured of poison") end, 50, 500)
if cured then
debug("Successfully cured poison.")
hasUsedPotion = true
state.bandage.lastPotionDrinkTime = state.currentTickTime
break
end
:: endcure ::
end
if not hasUsedPotion then
warning("Cure failed")
state.cure.useBandages = true
end
debug("Cure process completed.")
end
---
--- BANDAGE
---
local function getHpPercentage()
return (Player.Hits / Player.HitsMax) * 100
end
local function getHealthPotions()
local healthPotions = findInInventory(BandageConfig.HealPotions)
if not healthPotions or #healthPotions == 0 then
if exceedsDuration(state.bandage.lastOverHeadTime, state.currentTickTime, BandageConfig.WarningPauseTime) then
warning("No health potions")
state.bandage.lastOverHeadTime = state.currentTickTime
end
end
return healthPotions
end
local function drinkHealthPotion(forced)
forced = forced or false
if not Config.EnableBandage then
return
end
local playerHpPercentage = getHpPercentage()
if not forced and (playerHpPercentage > BandageConfig.HealPotionHP) then
debug("Player HP is above health potion threshold, skipping health potion.")
return
end
debug("Player HP is below health potion threshold, drinking health potion.")
if Player.IsPoisoned then
debug("Player is poisoned, skipping health potion.")
return
end
debug("Drinking health potion...")
local healthPotions = getHealthPotions()
if not healthPotions or #healthPotions == 0 then
if exceedsDuration(state.bandage.lastOverHeadTime, state.currentTickTime, BandageConfig.WarningPauseTime) then
warning("No health potions")
state.bandage.lastOverHeadTime = state.currentTickTime
end
return
end
if not exceedsDuration(state.bandage.lastPotionDrinkTime, state.currentTickTime, BandageConfig.HealthPotionRecentTime) then
debug("Health potion recently drunk, skipping.")
return
end
local alchemySkill = Skills.GetValue("Alchemy")
if alchemySkill and alchemySkill < 80 then
debug("Alchemy skill is below 80, disarming weapon to use health potion.")
disarmPlayer()
end
for _, potion in ipairs(healthPotions) do
if not potion or not potion.Serial then
goto enddrink
end
debug("Using health potion: " .. (potion.Name or "No Potion Name"))
if not Player.UseObject(potion.Serial) then
debug("Failed to use health potion: " .. (potion.Name or "No Potion Name"))
goto enddrink
end
local drank = pauseUntil(function () return Journal.Contains("You feel better") end, 50, Config.ActionWaitTime)
if drank then
debug("Successfully drank health potion.")
info("Drank health pot")
state.bandage.lastPotionDrinkTime = state.currentTickTime
break
end
:: enddrink ::
end
debug("Health potion process completed.")
end
local function bandage()
if Config.EnableBandage == false then
return
end
debug("Bandage running with main tick time")
if Player.IsHidden then
debug("Player is hiding, skipping bandage.")
return
end
drinkHealthPotion()
local bandageState = state and state.bandage
if bandageState.isBandaging then
debug("Already healing, skipping bandage.")
local timeLeft = bandageState.bandageTimeEnd - state.currentTickTime
if timeLeft > 0 and BandageConfig.OverheadPauseTime > 0 then
if exceedsDuration(bandageState.lastOverHeadTime, state.currentTickTime, BandageConfig.OverheadPauseTime) then
local countdown = math.floor(timeLeft / 1000)
if countdown >= 1 then
info("Bandaging " .. countdown .. "s")
end
bandageState.lastOverHeadTime = state.currentTickTime
end
end
if state.currentTickTime > bandageState.bandageTimeEnd then
bandageState.isBandaging = false
end
return
end
debug("Checking if bandaging is needed...")
if Player.IsDead then
debug("Cannot bandage while dead.")
return
end
if Journal.Contains("You begin applying the bandages") then
debug("Already manually bandaging, skipping.")
bandageState.bandageTimeEnd = bandageEndTime(state.currentTickTime)
bandageState.isBandaging = true
return
end
local playerHpPercentage = getHpPercentage()
if not Player.IsPoisoned and (playerHpPercentage >= BandageConfig.BandageHP) then
debug("Player not poisoned or HP is above threshold, no bandage needed.")
return
end
if Player.IsPoisoned and bandageState.useBandages then
debug("Using bandages due to previous poison.")
info("Curing with bandage")
bandageState.useBandages = false
end
debug("Looking for bandages...")
local bandages = findInInventory(0x00E21)
if not bandages or #bandages == 0 then
if exceedsDuration(bandageState.lastOverheadTime, state.currentTickTime, BandageConfig.WarningPauseTime) then
warning("No bandages found")
bandageState.lastOverheadTime = state.currentTickTime
end
return
end
debug("Attempting to bandage...")
-- Print size of bandages or 1 if only one item
local bandageCount = #bandages > 1 and #bandages or 1
debug("Bandaging with " .. bandageCount .. " bandage(s)...")
-- Loop in case you got item from bank or other "player" container
local isBandagingSuccessful = false
for _, item in ipairs(bandages) do
if not Player.UseObject(item.Serial) then
debug("Unable to use bandage item.")
goto continue
end
if not Targeting.WaitForTarget(500) then
debug("Targeting failed, unable to bandage.")
goto continue
end
if Targeting.TargetSelf() then
isBandagingSuccessful = true
break
end
:: continue ::
end
if not isBandagingSuccessful then
debug("Failed to bandage, the bandages found are probably in bank?")
return
end
local bandaging = pauseUntil(function()
return Journal.Contains("You begin applying the bandages.")
end, 50, 500)
if not bandaging then
bandageState.isBandaging = false
bandageState.lastBandageStart = nil
return
end
debug("Bandaging")
if BandageConfig.OverheadPauseTime == 0 then
info("Bandaging...")
bandageState.lastOverHeadTime = state.currentTickTime
end
bandageState.isBandaging = bandaging
bandageState.lastBandageStart = state.currentTickTime
bandageState.bandageTimeEnd = bandageEndTime(state.bandage.lastBandageStart)
end
---
--- PLAYER DETECTION
---
local function hunt()
if Config.EnableHunting == false then
return
end
debug("Hunting for players...")
state.hunt.lastTickTime = state.currentTickTime
local isWarningTimeExceeded = exceedsDuration(state.hunt.lastOverheadTime, state.currentTickTime,
HuntConfig.AlertPauseTime)
if not isWarningTimeExceeded then
debug("Last player detection notification was too recent, skipping")
return
end
for index, playerName in ipairs(HuntConfig.Players) do
debug("Looking for player " .. index .. "... ")
if Journal.Contains(playerName) then
info("Hunted player " .. playerName)
state.hunt.lastOverheadTime = state.currentTickTime
end
end
end
---
--- DISARM DETECTION
---
-- TODO: Use config and state
local LAYER_ONE_HANDED = 1
local LAYER_TWO_HANDED = 2
local lastRightHand = nil
local lastLeftHand = nil
local disarm = { x = 0, y = 0 }
local function disarmed()
if not Config.EnableDisarmDetection then
return
end
debug("Disarm detection running")
if lastRightHand == nil then
local rightHand = Items.FindByLayer(LAYER_ONE_HANDED)
if not rightHand then
debug("No weapon in right hand")
else
lastRightHand = rightHand
debug("Weapon " .. (rightHand.Name or "No Weapon Name") .. " used as right hand")
end
else
local rightHand = Items.FindByLayer(LAYER_ONE_HANDED)
if rightHand and rightHand.Serial ~= lastRightHand.Serial then
debug("Right hand weapon changed from: " .. (lastRightHand.Name or "No Weapon Name") .. " to: "
.. (rightHand.Name or "No Weapon Name"))
-- Since user changed weapon we are not disarmed and need to reset both hands in case of two hander
lastRightHand = nil
lastLeftHand = nil
disarm.x = 0
disarm.y = 0
end
end
if lastLeftHand == nil then
local leftHand = Items.FindByLayer(LAYER_TWO_HANDED)
if not leftHand then
debug("No weapon in left hand")
else
lastLeftHand = leftHand
debug("Weapon " .. (leftHand.Name or "No Weapon Name") .. " used as left hand")
end
else
local leftHand = Items.FindByLayer(LAYER_TWO_HANDED)
if leftHand and leftHand.Serial ~= lastLeftHand.Serial then
debug("Left hand weapon changed from: " .. (lastLeftHand.Name or "No Weapon Name") .. " to: "
.. (leftHand.Name or "No Weapon Name"))
-- Since user changed weapon we are not disarmed and need to reset both hands in case of two hander
lastLeftHand = nil
lastRightHand = nil
disarm.x = 0
disarm.y = 0
end
end
local isDisarmed = disarm.x > 0 or disarm.y > 0
local playerMoved = (Player.X ~= disarm.x or Player.Y ~= disarm.y) or DisarmConfig.AlwaysRearm
if isDisarmed and playerMoved then
-- Right hand
local alreadyHasRightHand = Items.FindByLayer(LAYER_ONE_HANDED)
if alreadyHasRightHand then
debug("Weapon " ..
((lastRightHand and lastRightHand.Name) or "No Weapon Name") .. " already equipped in right hand")
end
local canEquipRightHand = not alreadyHasRightHand and lastRightHand and lastRightHand.Serial
if canEquipRightHand then
debug("Trying to re-equip right hand weapon: " ..
((lastRightHand and lastRightHand.Name) or "No Weapon Name"))
---@diagnostic disable-next-line: need-check-nil
if Player.Equip(lastRightHand.Serial) then
info("Equipping right hand")
disarm.x = 0
disarm.y = 0
--lastDisarmRightHand = nil -- To refresh serial
Pause(Config.ActionWaitTime) -- Wait to allow the weapon to equip
else
warning("Equipping right hand failed")
end
end
-- Left hand
local alreadyHasLeftHand = Items.FindByLayer(LAYER_TWO_HANDED)
if alreadyHasLeftHand then
debug("Weapon " ..
((lastLeftHand and lastLeftHand.Name) or "No Weapon Name") .. " already equipped in left hand")
end
local canEquipLeftHand = not alreadyHasLeftHand and lastLeftHand and lastLeftHand.Serial
if canEquipLeftHand then
debug("Trying to re-equip left hand weapon: " .. ((lastLeftHand and lastLeftHand.Name) or "No Weapon Name"))
---@diagnostic disable-next-line: need-check-nil
if Player.Equip(lastLeftHand.Serial) then
info("Equipping left hand")
disarm.x = 0
disarm.y = 0
--lastDisarmLeftHand = nil -- To refresh serial
Pause(Config.ActionWaitTime)
else
warning("Equipping left hand failed")
end
end
end
if not isDisarmed then
if lastRightHand and not Items.FindByLayer(LAYER_ONE_HANDED) then
warning("Right hand disarmed, move to equip")
disarm.x = Player.X
disarm.y = Player.Y
elseif lastLeftHand and not Items.FindByLayer(LAYER_TWO_HANDED) then
warning("Left hand disarmed, move to equip")
disarm.x = Player.X
disarm.y = Player.Y
end
end
end
---
--- Scavenging
---
local function scavenge()
if not Config.EnableScavenging then
return
end
if not exceedsDuration(state.scavenge.lastTickTime, state.currentTickTime, ScavengeConfig.Tick) then
debug("Scavenging is not ready yet, skipping this tick.")
return
end
debug("Trying to scavenge...")
state.scavenge.lastTickTime = state.currentTickTime
local filter = { onground = true, rangemax = 2, graphics = ScavengeConfig.items }
local list = Items.FindByFilter(filter)
for _, item in ipairs(list) do
if not Player.PickUp(item.Serial, 1000) then
debug("Scavenging failed to pick up item: " .. (item.Name or "No Item Name"))
goto continue
end
Pause(250)
if not Player.DropInBackpack() then
debug("Scavenging failed to drop item in backpack: " .. (item.Name or "No Item Name"))
goto continue
end
debug("Scavenged item: " .. (item.Name or "No Item Name"))
Pause(250)
::continue::
end
end
---
--- BUFFS
---
local function startBuff(buffState, duration)
buffState.isActive = true
buffState.startTime = state.currentTickTime
buffState.endTime = buffState.startTime + duration
end
local function recentCast()
return Journal.Contains("You play your hypnotic music, stopping the battle.") or
Journal.Contains("You must wait a few seconds before you can play another song.") or
Journal.Contains("Your song creates a healing aura around you.")
end
local function eatFood()
local FoodConfig = BuffsConfig and BuffsConfig.buffs.Food
local foodState = state and state.food
if not exceedsDuration(foodState.lastEatTime, state.currentTickTime, Config.ActionWaitTime) then
debug("Food check time is not ready, skipping.")
return
end
if not exceedsDuration(foodState.lastEatTime, state.currentTickTime, FoodConfig.EatCooldown) then
debug("Eat cooldown not met, skipping.")
return
end
debug("Lookin for food...")
local foodItems = {}
for graphic, name in pairs(data.foods) do
local found = findInInventory({graphic})
if found and #found > 0 then
for _, item in ipairs(found) do
table.insert(foodItems, item)
end
end
end
if #foodItems == 0 then
debug("No food items found in inventory.")
return
end
debug("Starting to eat")
for _, item in ipairs(foodItems) do
debug("Attempting to eat: " .. (item.Name or "Unknown"))
Player.UseObject(item.Serial)
local full = pauseUntil(function()
return Journal.Contains("You are simply too full")
end, 50, Config.ActionWaitTime)
if full then
debug("Player is full.")
break
end
end
info("Finished eating")
foodState.lastEatTime = state.currentTickTime
end
local function stamina()
local StaminaConfig = BuffsConfig and BuffsConfig.buffs.Stamina
if not StaminaConfig or not StaminaConfig.Enable then
return
end
-- Debug stamina and max stamina
debug("Player stamina: " .. Player.Stam .. ", Max Stamina: " .. Player.MaxStam)
if Player.Stam >= Player.MaxStam then
debug("Player stamina is full, skipping stamina buff.")
return
end
local staminaPercentage = (Player.Stam / Player.MaxStam) * 100
if staminaPercentage > StaminaConfig.DrinkBelowPercentage then
debug("Player stamina is above " .. staminaPercentage .. "%, skipping stamina buff.")
return
end
debug("Player stamina is below " .. staminaPercentage .. "%, looking for stamina potion...")
local staminaPotions = findInInventory(StaminaConfig.Potions)
if not staminaPotions or #staminaPotions == 0 then
debug("No stamina potions found in inventory.")
return
end
debug("Found " .. #staminaPotions .. " stamina potions in inventory.")
local alchemySkill = Skills.GetValue("Alchemy")
if alchemySkill and alchemySkill < 80 then
debug("Alchemy skill is below 80, disarming weapon to use stamina potion.")
disarmPlayer()
end
for _, potion in ipairs(staminaPotions) do
if not potion then
goto endstamina
end
debug("Using stamina potion: " .. (potion.Name or "No Potion Name"))
if not Player.UseObject(potion.Serial) then
debug("Failed to use stamina potion: " .. (potion.Name or "No Potion Name"))
goto endstamina
end
local drank = pauseUntil(function() return Journal.Contains("You feel invigorated") end, 50, Config.ActionWaitTime)
if drank then
debug("Successfully drank stamina potion.")
break
end
:: endstamina ::
end
end
local function agility()
local AgilityConfig = BuffsConfig and BuffsConfig.buffs.Agility
if not AgilityConfig or not AgilityConfig.Enable then
return
end
local buffState = state and state.buffs and state.buffs.agility
if Player.Dex < buffState.lastDex then
debug("Detected dexterity debuff.")
buffState.lastDex = Player.Dex
else
debug("Player dexterity is above last known dexterity, skipping agility buff.")
return
end
debug("Looking for agility potion...")
local agilityPotions = findInInventory(AgilityConfig.Potions)
if not agilityPotions or #agilityPotions == 0 then
debug("No agility potions found in inventory.")
return
end
debug("Found " .. #agilityPotions .. " agility potions in inventory.")
local alchemySkill = Skills.GetValue("Alchemy")
if alchemySkill and alchemySkill < 80 then
debug("Alchemy skill is below 80, disarming weapon to use agility potion.")
disarmPlayer()
end
for _, potion in ipairs(agilityPotions) do
if not potion and not potion.Serial then
goto endagility
end
debug("Using agility potion: " .. (potion.Name or "No Potion Name"))
if not Player.UseObject(potion.Serial) then
debug("Failed to use agility potion: " .. (potion.Name or "No Potion Name"))
goto endagility
end
local buffed = pauseUntil(function()
return Player.Dex > buffState.lastDex
end, 50, Config.ActionWaitTime)
if buffed then
debug("Successfully drank agility potion.")
Pause(Config.ActionWaitTime)
break
end
:: endagility ::
end
buffState.lastDex = Player.Dex
end
local function strength()
local StrengthConfig = BuffsConfig and BuffsConfig.buffs.Strength
if not StrengthConfig or not StrengthConfig.Enable then
return
end
local buffState = state and state.buffs and state.buffs.strength
debug("Checking if strength is debuffed or dropped")
if Player.Str < buffState.lastStr then
debug("Detected strength debuff")
buffState.lastStr = Player.Str
else
debug("Player strength is above last known strength, skipping strength buff.")
return
end
debug("Looking for strength potion...")
local strengthPotions = findInInventory(StrengthConfig.Potions)
if not strengthPotions or #strengthPotions == 0 then
debug("No strength potions found in inventory.")
return
end
debug("Found " .. #strengthPotions .. " strength potions in inventory.")
local alchemySkill = Skills.GetValue("Alchemy")
if alchemySkill and alchemySkill < 80 then
debug("Alchemy skill is below 80, disarming weapon to use strength potion.")
disarmPlayer()
end
for _, potion in ipairs(strengthPotions) do
if (not potion) and (not potion.Serial) then
goto endstrength
end
debug("Using strength potion: " .. (potion.Name or "No Potion Name"))
if not Player.UseObject(potion.Serial) then
debug("Failed to use strength potion: " .. (potion.Name or "No Potion Name"))
goto endstrength
end
local buffed = pauseUntil(function()
return Player.Str > buffState.lastStr
end, 50, Config.ActionWaitTime)
if buffed then
debug("Successfully drank strength potion.")
Pause(Config.ActionWaitTime)
drinkHealthPotion(true)
break
end
:: endstrength ::
end
buffState.lastStr = Player.Str
end
local function songOfHealing()
local SongConfig = BuffsConfig and BuffsConfig.buffs.SongOfHealing
if not SongConfig.Enable then
return
end
local musicSkill = Skills.GetValue("Musicianship")
if musicSkill and musicSkill > 0 then
debug("Musicianship skill is " .. musicSkill .. ", proceeding with Song of Healing.")
else
debug("Musicianship skill is 0, skipping Song of Healing.")
return
end
local songState = state and state.buffs and state.buffs.songOfHealing
if songState.isActive then
if state.currentTickTime > songState.endTime then
songState.isActive = false
debug("Song of Healing ended.")
return
end
debug("Waiting for Song of Healing to end in: " ..
((songState.endTime - state.currentTickTime) / 1000) .. " seconds.")
return
end
if recentCast() then
debug("Buff was recently cast, wait to retry")
songState.isActive = true
songState.startTime = state.currentTickTime
local recastWaitTime = 8
songState.endTime = songState.startTime + recastWaitTime
return
end
if not songState.instrument then
local instrument = nil
for _, instrumentName in ipairs(BuffsConfig.Instruments) do
debug("Looking for instrument: " .. instrumentName)
instrument = Items.FindByName(instrumentName)
if instrument then
debug("Found instrument: " .. instrument.Name)
break
end
end
if not instrument then
debug("No instrument found in inventory")
return
end
songState.instrument = instrument
end
debug("Casting Song of Healing...")
if not Spells.Cast("SongOfHealing") then
if exceedsDuration(songState.lastWarningTickTime, state.currentTickTime, SongConfig.FailWait) then
info("Recasting Song of Healing")
debug("Failed to cast Song of Healing, waiting " .. (SongConfig.FailWait / 1000) .. " seconds to retry.")
songState.lastWarningTickTime = state.currentTickTime
end
startBuff(songState, SongConfig.FailWait)
return
end
local castSuccess = pauseUntil(function()
if Journal.Contains("You are already under the effects") then
debug("Song was already active.")
return false
elseif Journal.Contains("Your song creates a healing aura around you.") then
return true
elseif Journal.Contains("What instrument shall you play?") then
debug("Instrument depleeted, will look for a new one")
songState.instrument = nil
return false
end
return false
end, 50, Config.ActionWaitTime)
if not castSuccess then
if exceedsDuration(songState.lastWarningTickTime, state.currentTickTime, SongConfig.FailWait) then
info("Recasting Song of Healing")
debug("Journal did not contain expectations for Song of Healing, waiting " .. (SongConfig.FailWait / 1000)
.. " seconds to retry.")
songState.lastWarningTickTime = state.currentTickTime
end
startBuff(songState, SongConfig.FailWait)
return
end
startBuff(songState, songState.duration)
info("Casted Song of Healing")
debug("Song of Healing started.")
end
local function buffs()
if not Config.EnableBuffs then
return
end
if Player.IsDead then
debug("Player is dead, skipping buffs.")
return
end
if Player.IsHidden then
debug("Player is hiding, skipping buffs.")
return
end
songOfHealing()
stamina()
strength()
agility()
eatFood()
end
local function peacemaking()
local PeaceConfig = BuffsConfig and BuffsConfig.debuffs.Peacemaking
if not PeaceConfig.Enable then
return
end
local skill = Skills.GetValue("Peacemaking")
if not skill or not (skill > 0) then
debug("No skill in peacemaking...")
return
end
local songState = state and state.debuffs and state.debuffs.peacemaking
local target = Player.Serial
if recentCast() then
debug("Resent cast, waiting to retry peacemaking.")
songState.isActive = true
songState.startTime = state.currentTickTime
local recastWaitTime = 8
songState.endTime = songState.startTime + recastWaitTime
return
end
-- Whom do you wish to calm?
if not Journal.Contains("You begin to play a soothing melody") or
not Journal.Contains("That creature is already being calmed.") then
debug("No Peacemaking song in progress, starting...")
Spells.Cast("Peacemaking")
end
end
local function debuffs()
if not Config.EnableDebuffs then
return
end
debug("Buffs running")
if Player.IsHidden then
debug("Player is hiding, skipping buffs.")
return
end
peacemaking()
end
---
--- POP POUCH
---
local function popPouch()
if not Config.EnablePopPouch then
return
end
if Journal.Contains("You are now PvP-Combat flagged!") then
state.flaggedForPvp = true
end
if Journal.Contains("You are no longer PvP-Combat flagged!") then
state.flaggedForPvp = false
end
if state.flaggedForPvp and Player.IsParalyzed and Journal.Contains("You cannot move!") then
debug("Player is paralyzed, popping pouch.")
info("Popping pouch")
Player.PopPouch()
end
end
---
--- Moongate
---
-- Based on Jase's moongate script: https://uoaddicts.com/script/escape-moongate-m-cm6micvh
local function moongate()
if not Config.EnableMoongate then
return
end
local gateState = state and state.moongate
if not exceedsDuration(gateState.lastTickTime, state.currentTickTime, 1000) then
debug("Moongate check is not ready yet, skipping")
return
end
gateState.lastTickTime = state.currentTickTime
local gate = Items.FindByName('Moongate')
if not gate then
gateState.serial = nil
gateState.previousDistance = nil
gateState.messageShown = false
return
end
if gateState.serial ~= gate.Serial then
gateState.serial = gate.Serial
gateState.previousDistance = gate.Distance
gateState.messageShown = false
return
end
if gateState.previousDistance == nil then
gateState.previousDistance = gate.Distance
end
local movingTowardGate = gate.Distance < gateState.previousDistance
local isNearGate = gate.Distance <= 10
local movedAway = gate.Distance > 10
if movedAway and gateState.messageShown then
gateState.messageShown = false
debug("Moved away from moongate, resetting message flag")
end
if (movingTowardGate or isNearGate) and not gateState.messageShown then
if not exceedsDuration(gateState.lastMessageTime, state.currentTickTime, GateConfig.OverheadPauseTime) then
info("Found moongate")
end
gateState.messageShown = true
end
gateState.previousDistance = gate.Distance
if gate.Distance > 2 then
debug("Moongate is too far away, skipping")
return
end
if Gumps.IsActive(GateConfig.gumpId) then
info("Click destination")
else
Player.UseObject(gate.Serial)
end
if Gumps.WaitForGump(GateConfig.gumpId, Config.ActionWaitTime) then
info("Trying to travel")
Gumps.PressButton(GateConfig.gumpId, 1)
end
end
---
--- ESCAPE
---
local function escape()
if not Config.EnableEscape then
return
end
if Player.IsDead then
debug("Player is dead, skipping escape.")
return
end
if Player.IsHidden then
debug("Player is hiding, skipping escape.")
return
end
local command = EscapeConfig.Command
if not Journal.Contains(command) then
return
end
local callback = EscapeConfig.Callback
if callback and type(callback) == "function" then
debug("Running escape callback function")
pauseUntil(callback, 50, Config.ActionWaitTime)
end
end
---
--- CANCEL
---
local function cancel()
return Journal.Contains(CancelConfig.Command)
end
---
--- MAIN LOOP
---
info("Halesluker's Sagas Bot")
debug("Halesluker's Sagas Bot started") -- Debug messages has timestamps
if Config.Debug then
print("Debug mode is enabled.")
for key, _ in pairs(Config) do
if type(Config[key]) == "boolean" then
Config[key] = false
end
end
Config.Debug = true
Config.EnableCure = true
Config.EnableBandage = true
Config.EnableBuffs = true
end
Journal.Clear() -- Start with a clean journal
while true do
state.currentTickTime = math.floor(os.clock() * 1000)
debug("Main tick loop start")
if Player.IsDead then
debug("Player is dead, skipping main loop.")
goto mainloopend
end
-- Journal dependent functions
debug("Before journal tick.")
if exceedsDuration(state.lastJournalTickTime, state.currentTickTime, Config.JournalTick) then
debug("Journal tick time exceeded, processing journal...")
if cancel() then
debug("Cancel command detected, exiting main loop.")
return
end
popPouch()
escape()
cure()
bandage()
buffs()
rearmPlayer()
hunt()
state.lastJournalTickTime = state.currentTickTime
end
-- Journal independent functions
disarmed()
scavenge()
moongate()
debug("Main tick loop end")
:: mainloopend ::
Journal.Clear()
Pause(Config.MainTick)
end
Version History
Version 1 - 6/13/2025, 6:18:12 PM - about 16 hours ago
Halesluker's Sagas Bot
Original Version Saved - 6/13/2025, 6:18:12 PM - about 16 hours ago