Skip to content

Instantly share code, notes, and snippets.

@alivesay
Last active September 26, 2025 18:43
Show Gist options
  • Save alivesay/bd33d75a6c7d4b42940c04c5aa0bdfd6 to your computer and use it in GitHub Desktop.
Save alivesay/bd33d75a6c7d4b42940c04c5aa0bdfd6 to your computer and use it in GitHub Desktop.
# flake8: noqa: F401
# -----------------------------------------------------------------------------
#
# MrBelvedere's MEGA-AUTO
#
# WARNING: Using auto-attack/target/follow AFK may be bannable on your shard!!!
#
# Notes:
# - Set TazUO -> Mobiles -> Follow Distance = 1 for sticky follow.
# - If radius indicator stays on, run -radius client command.
# -----------------------------------------------------------------------------
from typing import Optional
import API
import time
# ================================= Config ====================================
AA_MAX_DISTANCE = 12 # max distance to search for enemies
AA_SHOW_RADIUS = False # show visual radius circle
AA_ATTACK_STEP_MS = 150 # attack tick interval (ms)
AA_POST_KILL_COMPLETE_LOOT = True # True = stay until corpse has no rule-matching items
AA_POST_KILL_LOOT_MIN_HOLD_MS = 1200 # (used only when COMPLETE_LOOT=False)
AA_POST_KILL_LOOT_QUIET_ROUNDS = 3 # (used only when COMPLETE_LOOT=False)
AA_POST_KILL_LOOT_MAX_HOLD_MS = 4500 # safety cap either way
AA_ABILITY_USE = "secondary" # ability mode: "primary", "secondary", or "off"
TARGETS_MIN_GUMP_W = 240 # Minimum gump width
TARGETS_MIN_GUMP_H = 320 # Minimum gump height
TARGETS_GUMP_WIDTH = TARGETS_MIN_GUMP_W
TARGETS_GUMP_HEIGHT = TARGETS_MIN_GUMP_H
AA_ABILITY_MANA_MIN = 19 # min mana required for ability use
AA_SUMMONS_IGNORE = ["a reaper", "a rising colossus", "a nature's fury", "a blade spirit"]
AA_MOBS_IGNORE = []
AA_IDS_IGNORE = [0x00CB, 0x00CF]
AB_HP_MISSING_MIN = 10 # min HP missing before bandage starts
AB_ALLOW_WHILE_HIDDEN = False # allow bandages while hidden
AB_BANDAGE_POLL_MS = 120 # bandage poll interval (ms)
AB_ETA_MS = 2000 # bandage expected completion time (ms)
AB_MIN_RETRY_GAP_MS = 450 # min gap between bandage retries (ms)
AB_AFTER_HIT_GRACE_MS = 700 # delay after taking damage before bandaging (ms)
AL_ENABLED_DEFAULT = True # enable auto-loot by default
AL_RANGE = 4 # max tile range for loot
AL_LOOT_TICK_MS = 250 # loot tick interval (ms)
AL_RECHECK_COOLDOWN_MS = 300 # cooldown between re-checks of same container (ms)
AL_MOVE_PAUSE_MS = 250 # pause after moving an item (ms)
AL_PASS_BUDGET_MS = 150 # time budget per loot container (ms)
AL_WAIT_READY_SLICE_MS = 25 # wait slice for busy checks (ms)
AL_ONLY_CORPSES = False # True = loot only corpses; False = also scan world containers
AL_SKIP_SECURE = True # skip locked-down / secure containers
AL_OBJECT_NAME_DENY = [
"locked down",
"secure",
"strongbox",
"strong box",
"mailbox",
"commodity",
]
AL_FAIL_LINES = ["you must wait before performing another action"]
AL_RULES = [
{
"label": "Gold",
"enabled": True,
"names": ["gold", "pile of gold", "gold coin", "gold coins"],
"graphics": [0x0EED],
"min_amount": 0,
"dest": lambda: API.Backpack,
},
{
"label": "Gems",
"enabled": False,
"names": [
"amber",
"amethyst",
"citrine",
"diamond",
"emerald",
"ruby",
"sapphire",
"star sapphire",
"tourmaline",
],
"graphics": [],
"min_amount": 0,
"dest": lambda: API.Backpack,
},
{
"label": "Crimson Cincture",
"enabled": True,
"names": ["crimson cincture"],
"graphics": [],
"min_amount": 0,
"dest": lambda: API.Backpack,
},
]
BF_ENABLED_DEFAULT = False
BF_BUFFS_ROTATION = [
{
"label": "Divine Fury",
"action": "Divine Fury",
"enabled": True,
"mana_min": 14,
"type": "CastSpell",
},
{
"label": "Confidence",
"action": "Confidence",
"enabled": True,
"mana_min": 10,
"type": "CastSpell",
},
{
"label": "Consecrate",
"action": "Consecrate Weapon",
"enabled": True,
"mana_min": 10,
"type": "CastSpell",
},
{
"label": "Curse",
"action": "Curse Weapon",
"enabled": True,
"mana_min": 7,
"type": "CastSpell",
},
]
TARGETS_REFRESH_MS = 250
TARGETS_CACHE_MS = 1000
TARGETS_MAX_DISPLAY = 10
TARGETS_SHOW_DISTANCE = False
# ============================== Internals =============================
def purge_loot_denied_serials():
global _loot_denied_serials
present = set()
try:
corpses = API.Corpses()
for c in corpses:
s = int(getattr(c, "Serial", 0) or 0)
if s:
present.add(s)
except:
pass
_loot_denied_serials &= present
_loot_denied_serials = set()
_bf_last_attempt_ms = 0
_bf_cooldown_ms = 2500
bf_enabled = BF_ENABLED_DEFAULT
def bf_enable():
global bf_enabled
bf_enabled = True
def bf_disable():
global bf_enabled
bf_enabled = False
def _is_buff_active(buff_name):
try:
return API.BuffExists(buff_name)
except:
return False
def auto_buff_step():
global _bf_last_attempt_ms
if not bf_enabled:
return
if is_hidden():
return
if api_busy():
return
now = now_ms()
if (now - _bf_last_attempt_ms) < _bf_cooldown_ms:
return
has_cooldown = False
try:
if (API.InJournal("You cannot perform this special move right now.") or
API.InJournal("You have not yet recovered from casting a spell.") or
API.InJournal("You must wait") or
API.InJournal("You are already casting a spell")):
has_cooldown = True
API.ClearJournal()
except:
pass
if has_cooldown:
return
for buff in BF_BUFFS_ROTATION:
if not buff.get("enabled", True):
continue
buff_name = buff["label"]
action = buff["action"]
mana_req = buff.get("mana_min", 0)
buff_type = buff.get("type", "CastSpell")
try:
mana = int(API.Player.Mana)
except:
mana = 0
if not _is_buff_active(buff_name) and mana >= mana_req:
try:
if buff_type == "CastSpell":
API.CastSpell(action)
elif buff_type == "UseSkill":
API.UseSkill(action)
else:
continue
API.Pause(0.05)
_bf_last_attempt_ms = now
break
except Exception as e:
API.SysMsg(f"Auto-buff error for {buff_name}: {str(e)}", 33)
_bf_last_attempt_ms = now
break
for r in AL_RULES:
r["_names_lc"] = [n.lower() for n in (r.get("names") or [])]
_cached_friends_list = None
def reload_friends_list():
global _cached_friends_list
import os, json
try:
friends_path = os.path.join(os.getcwd(), 'Data/UOAlive/friends.json')
with open(friends_path, 'r', encoding='utf-8') as f:
friends = json.load(f)
_cached_friends_list = friends if isinstance(friends, list) else []
except Exception:
_cached_friends_list = []
targets_gump_width = TARGETS_MIN_GUMP_W
targets_gump_height = TARGETS_MIN_GUMP_H
def set_targets_gump_size(width, height):
global targets_gump_width, targets_gump_height
targets_gump_width = max(width, TARGETS_MIN_GUMP_W)
targets_gump_height = max(height, TARGETS_MIN_GUMP_H)
targets_popup_rows = []
targets_popup = None
_targets_popup_last_targets = []
_targets_popup_cache = {"targets": [], "timestamp": 0}
_targets_list_area = None
def refresh_targets_popup():
global targets_popup, _targets_popup_last_targets, _aa_target_serial, _targets_list_area, targets_popup_rows
if not targets_popup:
return
try:
if hasattr(targets_popup, 'IsDisposed') and targets_popup.IsDisposed:
return
except Exception:
return
popup_w, popup_h = targets_gump_width, targets_gump_height
if not targets_popup_rows or len(targets_popup_rows) != TARGETS_MAX_DISPLAY:
targets_popup_rows = []
for i in range(TARGETS_MAX_DISPLAY):
label = API.CreateGumpTTFLabel("", DEFAULT_FONT_SIZE, "#FFFFFF", font=AV_FONT, aligned="left", maxWidth=popup_w-96)
label.SetRect(18, 40 + i * 26 + 4, popup_w - 96, 20)
bar_w = int(80 * 0.75)
bar_h = int(12 * 0.75)
hp_bar = API.CreateGumpSimpleProgressBar(bar_w, bar_h, backgroundColor="#616161", foregroundColor="#21DD21", value=0, max=1)
hp_bar.SetRect(popup_w - bar_w - 24, 40 + i * 26 + 8, bar_w, bar_h)
label.IsVisible = False
hp_bar.IsVisible = False
targets_popup.Add(label)
targets_popup.Add(hp_bar)
targets_popup_rows.append((label, hp_bar))
now = int(time.time() * 1000)
scan_targets = get_attackable_targets()
cache_age = now - _targets_popup_cache["timestamp"]
API.Pause(0.01)
if scan_targets:
_targets_popup_cache["targets"] = scan_targets
_targets_popup_cache["timestamp"] = now
targets = scan_targets
elif _targets_popup_cache["targets"] and cache_age < TARGETS_CACHE_MS:
targets = _targets_popup_cache["targets"]
else:
targets = []
display_targets = sorted(targets, key=lambda m: getattr(m, 'Distance', 9999))
display_targets = display_targets[:TARGETS_MAX_DISPLAY]
col_padding = 2 + 7 + (4 if TARGETS_SHOW_DISTANCE else 0)
max_name_len = max(10, (popup_w - 32) // 9 - col_padding)
for row in targets_popup_rows:
label, hp_bar = row
label.SetText("")
label.IsVisible = False
hp_bar.SetProgress(0, 1)
hp_bar.IsVisible = False
for i, row in enumerate(targets_popup_rows):
if i < len(display_targets):
label, hp_bar = row
mob = display_targets[i]
mob_name = (getattr(mob, 'Name', '') or '').strip()
if len(mob_name) > max_name_len:
mob_name = mob_name[:max_name_len-3] + '...'
mob_dist = getattr(mob, 'Distance', 9999)
mob_serial = int(getattr(mob, 'Serial', 0) or 0)
mob_hp = getattr(mob, 'Hits', None)
mob_maxhp = getattr(mob, 'HitsMax', None)
is_current = (mob_serial == _aa_target_serial)
name_col = mob_name.ljust(max_name_len)
dist_col = f"{mob_dist:>3}" if TARGETS_SHOW_DISTANCE else ""
label_text = f"{name_col}"
if TARGETS_SHOW_DISTANCE:
label_text += f" {dist_col}"
label.SetText(label_text)
label.IsVisible = True
label.Hue = 32 if is_current else 916
def make_onclick(serial, mob_obj, mob_name=mob_name):
def _on_click():
global _aa_target_serial, aa_new_target
_aa_target_serial = serial
aa_new_target = True
try:
API.Target(int(getattr(mob_obj, 'Serial', 0) or 0))
except Exception:
pass
return _on_click
API.AddControlOnClick(label, make_onclick(mob_serial, mob))
if mob_hp is not None and mob_maxhp is not None and mob_maxhp > 0:
hp_bar.SetProgress(int(mob_hp), int(mob_maxhp))
hp_bar.IsVisible = True
else:
hp_bar.SetProgress(0, 1)
hp_bar.IsVisible = False
_targets_popup_last_targets = display_targets
def get_attackable_targets():
try:
mobs = API.NearestMobiles(ENEMY_NOTORIETY, AA_MAX_DISTANCE)
except:
mobs = []
targets = []
if mobs:
for mob in mobs:
s = int(getattr(mob, 'Serial', 0) or 0)
if s and not should_ignore_mob(mob):
targets.append(mob)
targets.sort(key=lambda m: getattr(m, 'Distance', 9999))
return targets
def now_ms() -> int:
return int(time.monotonic() * 1000)
def _safe(fn, *a, **kw):
try:
return fn(*a, **kw)
except:
return None
def api_busy() -> bool:
try:
return API.IsGlobalCooldownActive()
except:
return False
def cancel_motion_and_target():
for fn in (
"CancelAutoFollow",
"CancelPathfinding",
"ClearMoveQueue",
"CancelTarget",
):
_safe(getattr(API, fn))
INSTANCE_KEY = "MEGA_AUTO_INSTANCE_UUID"
import uuid
INSTANCE_CHECK_MS = 1000
def _refresh_lock():
try:
global my_instance_uuid
if not my_instance_uuid:
my_instance_uuid = str(uuid.uuid4())
API.SavePersistentVar(INSTANCE_KEY, my_instance_uuid, API.PersistentVar.Global)
except:
pass
# def _acquire_lock() -> bool:
# try:
# val = API.GetPersistentVar(LOCK_KEY, "", API.PersistentVar.Char)
# if val:
# try:
# ts = int(val)
# except:
# ts = 0
# if (now_ms() - ts) >= LOCK_STALE_MS:
# _release_lock()
# else:
# API.SysMsg(
# "MEGA-AUTO: another instance seems to be running. Aborting.", 32
# )
# return False
# API.SavePersistentVar(LOCK_KEY, str(now_ms()), API.PersistentVar.Char)
# return True
# except:
# _safe(API.SysMsg, "MEGA-AUTO: WARN: lock unavailable, proceeding.", 32)
# return True
def _release_lock():
try:
API.RemovePersistentVar(INSTANCE_KEY, API.PersistentVar.Global)
except:
pass
def player_hits() -> int:
try:
return int(API.Player.Hits)
except:
return 0
def player_hits_max() -> int:
try:
return int(API.Player.HitsMax)
except:
return max(1, player_hits())
def player_mana() -> int:
try:
return int(API.Player.Mana)
except:
return 0
def player_serial() -> int:
try:
return int(API.Player.Serial)
except:
return 0
def is_poisoned() -> bool:
try:
return bool(API.Player.IsPoisoned)
except:
try:
return API.BuffExists("Poison")
except:
return False
def is_hidden() -> bool:
try:
if API.BuffExists("Hiding") or API.BuffExists("Hidden"):
return True
except:
pass
try:
return bool(API.Player.Hidden)
except:
return False
DEFAULT_FONT_SIZE = 15
AV_FONT = "Avadonia"
def Label(text, size=16, color="#FFFFFF", align="left", maxW=0):
return API.CreateGumpTTFLabel(
text,
size if size is not None else DEFAULT_FONT_SIZE,
color,
font=(AV_FONT or ""),
aligned=align,
maxWidth=maxW if maxW else 0,
applyStroke=False
)
_radius_last = {"shown": False, "dist": 0, "hue": 32}
def radius_apply(force: bool = False):
try:
should_show = bool(AA_SHOW_RADIUS)
desired_dist = int(AA_MAX_DISTANCE)
desired_hue = int(_radius_last["hue"])
if should_show:
if (
force
or (not _radius_last["shown"])
or (_radius_last["dist"] != desired_dist)
):
API.DisplayRange(desired_dist, desired_hue)
_radius_last.update({"shown": True, "dist": desired_dist})
else:
if force or _radius_last["shown"]:
API.DisplayRange(0)
_radius_last.update({"shown": False, "dist": 0})
except:
pass
aa_enabled = False
aa_auto_follow = False
aa_honor = False
aa_abilities = True
aa_new_target = False
_aa_target_serial = 0
_loot_lock_until_ms = 0
_target_stuck_count = 0
_target_stuck_threshold = 8
_last_player_pos = None
def clear_stuck_tracking():
global _target_stuck_count, _last_player_pos
_target_stuck_count = 0
_last_player_pos = None
def aa_enable():
global aa_enabled
aa_enabled = True
def aa_disable():
global aa_enabled, _aa_target_serial
aa_enabled = False
_aa_target_serial = 0
clear_stuck_tracking()
cancel_motion_and_target()
def af_enable():
global aa_auto_follow
aa_auto_follow = True
def af_disable():
global aa_auto_follow
aa_auto_follow = False
clear_stuck_tracking()
cancel_motion_and_target()
def hon_enable():
global aa_honor
aa_honor = True
def hon_disable():
global aa_honor
aa_honor = False
def abl_enable():
global aa_abilities
aa_abilities = True
def abl_disable():
global aa_abilities
aa_abilities = False
def aa_request_new_target():
global aa_new_target, _aa_target_serial
targets = get_attackable_targets()
if targets:
closest = targets[0]
new_serial = int(getattr(closest, 'Serial', 0) or 0)
if new_serial:
_aa_target_serial = new_serial
aa_new_target = True
try:
API.Target(closest)
API.SysMsg(f"New target: {getattr(closest, 'Name', 'Unknown')}", 88)
API.Pause(0.08)
except Exception:
pass
else:
_aa_target_serial = 0
aa_new_target = False
API.SysMsg("No valid targets found", 33)
def _honor_target_if_fresh(mob):
try:
if mob and mob.HitsDiff == 0 and mob.Distance < 6:
API.Virtue("honor")
API.WaitForTarget()
API.Target(mob)
API.CancelTarget()
except:
pass
def _use_ability_if_ready():
if not aa_abilities:
return
mode = (AA_ABILITY_USE or "off").lower()
mana_req = int(AA_ABILITY_MANA_MIN or 0)
try:
mana = int(API.Player.Mana)
except:
mana = 0
try:
prim_active = bool(API.PrimaryAbilityActive())
except:
prim_active = False
try:
sec_active = bool(API.SecondaryAbilityActive())
except:
sec_active = False
if mode not in ("primary", "secondary"):
if prim_active:
_safe(API.ToggleAbility, "primary")
prim_active = False
if sec_active:
_safe(API.ToggleAbility, "secondary")
sec_active = False
return
if mode == "primary" and sec_active:
_safe(API.ToggleAbility, "secondary")
sec_active = False
if mode == "secondary" and prim_active:
_safe(API.ToggleAbility, "primary")
prim_active = False
if mana < mana_req:
if mode == "primary" and prim_active:
_safe(API.ToggleAbility, "primary")
elif mode == "secondary" and sec_active:
_safe(API.ToggleAbility, "secondary")
return
if api_busy():
return
if mode == "primary":
if not prim_active:
_safe(API.ToggleAbility, "primary")
else:
if not sec_active:
_safe(API.ToggleAbility, "secondary")
def _is_corpse(ent) -> bool:
try:
if getattr(ent, "IsCorpse", False):
return True
except:
pass
try:
nm = (getattr(ent, "Name", "") or "").lower()
return "corpse" in nm
except:
return False
def _opened(serial) -> bool:
try:
return API.Contents(serial) > 0
except:
return False
def _ensure_open(serial) -> bool:
if _opened(serial):
return True
for fn in ("UseObject", "DoubleClick"):
f = getattr(API, fn, None)
if f:
_safe(f, serial)
API.Pause(0.08)
if _opened(serial):
return True
return _opened(serial)
def _match_rule(it, rule, cached_name=None):
if not rule.get("enabled", True):
return False
try:
amt = int(getattr(it, "Amount", 0) or 0)
except:
amt = 0
if int(rule.get("min_amount", 0)) > 0 and amt < int(rule["min_amount"]):
return False
try:
g = int(getattr(it, "Graphic", -1) or -1)
except:
g = -1
if rule.get("graphics") and g in rule["graphics"]:
return True
if rule.get("names"):
if cached_name is not None:
nm = (cached_name or "").strip().lower()
else:
nm = (getattr(it, "Name", "") or "").strip().lower()
if not nm:
try:
tip = API.GetTooltipText(it)
if isinstance(tip, str) and tip.strip():
nm = tip.strip().lower()
except:
pass
if not nm:
try:
props = API.GetProperties(it)
if isinstance(props, str) and props.strip():
nm = props.strip().lower()
except:
pass
toks = rule.get("_names_lc") or [n.lower() for n in (rule.get("names") or [])]
return any(tok in nm for tok in toks) if nm else False
return False
def _container_has_rule_matches(serial) -> bool:
items = _safe(API.ItemsInContainer, serial, False) or []
for it in items:
try:
if int(it.Serial) == int(serial):
continue
except:
pass
nm = _item_name_lc(it)
for rule in AL_RULES:
if _match_rule(it, rule, nm):
return True
return False
def _loot_container(serial):
items = _safe(API.ItemsInContainer, serial, False) or []
budget_end = now_ms() + AL_PASS_BUDGET_MS
moved = {}
cnt = 0
for it in items:
if now_ms() >= budget_end or cnt >= 5:
break
try:
if int(it.Serial) == int(serial):
continue
except:
pass
nm = _item_name_lc(it)
for rule in AL_RULES:
if _match_rule(it, rule, nm):
dst = (
rule["dest"]()
if callable(rule.get("dest"))
else int(rule.get("dest", API.Backpack))
)
if _move_full_stack(it, dst):
lbl = rule.get("label", "Rule")
moved[lbl] = moved.get(lbl, 0) + 1
cnt += 1
break
return moved
def _wait_ready_ms(max_wait_ms: int = 500) -> bool:
if _bandaging_active:
API.ProcessCallbacks()
return True
deadline = now_ms() + max_wait_ms
slice_ms = AL_WAIT_READY_SLICE_MS
while now_ms() < deadline:
API.ProcessCallbacks()
if not api_busy():
return True
API.Pause(slice_ms / 1000.0)
return True
def _move_full_stack(it, dst) -> bool:
for _ in range(6):
if not _wait_ready_ms():
return False
try:
if hasattr(API, 'GameCursor') and hasattr(API.GameCursor, 'ItemHold'):
if getattr(API.GameCursor.ItemHold, 'Enabled', False) or getattr(API.GameCursor.ItemHold, 'Dropped', False):
API.Pause(0.08)
continue
API.MoveItem(it, dst, 0)
API.Pause(AL_MOVE_PAUSE_MS / 1000.0)
return True
except:
try:
if API.InJournalAny(AL_FAIL_LINES):
API.ClearJournal()
except:
pass
API.Pause(0.02)
return False
def _nearest_openable_corpse(range_tiles: int):
_safe(API.ClearIgnoreList)
best = None
for _ in range(24):
c = _safe(API.NearestCorpse, max(range_tiles, 5))
if not c:
break
s = int(getattr(c, "Serial", 0) or 0)
if s:
best = c
_safe(API.IgnoreObject, s)
break
if s:
_safe(API.IgnoreObject, s)
_safe(API.ClearIgnoreList)
return best
def _loot_corpse_until_done_or_timeout():
global _loot_lock_until_ms
deadline = now_ms() + AA_POST_KILL_LOOT_MAX_HOLD_MS
_loot_lock_until_ms = deadline
c = _nearest_openable_corpse(AL_RANGE)
if not c:
_loot_lock_until_ms = 0
return
serial = int(getattr(c, "Serial", 0) or 0)
if not serial or serial in _loot_denied_serials:
_loot_lock_until_ms = 0
return
_ensure_open(serial)
stagnant_passes = 0
while now_ms() < deadline:
_loot_lock_until_ms = deadline
API.ProcessCallbacks()
cancel_motion_and_target()
auto_bandage_step()
if API.InJournal("You did not earn the right to loot this creature!"):
_loot_denied_serials.add(serial)
API.ClearJournal()
break
if not _opened(serial):
break
if not _container_has_rule_matches(serial):
break
moved = _loot_container(serial)
if moved:
stagnant_passes = 0
API.Pause(0.02)
continue
API.Pause(0.03)
if not _container_has_rule_matches(serial):
break
stagnant_passes += 1
if stagnant_passes >= 2:
break
_loot_lock_until_ms = 0
def _loot_hold_after_kill():
cancel_motion_and_target()
if AA_POST_KILL_COMPLETE_LOOT:
_loot_corpse_until_done_or_timeout()
else:
min_hold = AA_POST_KILL_LOOT_MIN_HOLD_MS
max_hold = AA_POST_KILL_LOOT_MAX_HOLD_MS
need_quiet = AA_POST_KILL_LOOT_QUIET_ROUNDS
start = now_ms()
quiet = 0
while True:
API.ProcessCallbacks()
cancel_motion_and_target()
auto_bandage_step()
_safe(auto_loot_tick)
has_targets = False
try:
if _scan_opened_containers(AL_RANGE):
has_targets = True
except:
pass
elapsed = now_ms() - start
quiet = 0 if has_targets else (quiet + 1)
if elapsed >= min_hold and quiet >= need_quiet:
break
if elapsed >= max_hold:
break
API.Pause(0.07)
ENEMY_NOTORIETY = [
API.Notoriety.Gray,
API.Notoriety.Criminal,
API.Notoriety.Murderer,
]
def load_friends_list():
global _cached_friends_list
if _cached_friends_list is None:
reload_friends_list()
return _cached_friends_list or []
def is_friend(mob):
try:
friends_list = load_friends_list()
if not friends_list:
return False
mob_serial = int(getattr(mob, 'Serial', 0))
mob_name = (getattr(mob, 'Name', '') or '').strip().lower()
matched = False
if mob_serial > 0:
for friend in friends_list:
if friend.get('Serial') == mob_serial:
matched = True
break
if not matched and mob_name:
for friend in friends_list:
friend_name = (friend.get('Name') or '').strip().lower()
if friend_name and mob_name == friend_name:
matched = True
break
return matched
except Exception:
return False
def should_ignore_mob(mob):
try:
mob_name = (getattr(mob, 'Name', '') or '').strip().lower()
if mob_name in [n.lower() for n in AA_SUMMONS_IGNORE + AA_MOBS_IGNORE]:
return True
mob_graphic = int(getattr(mob, 'Graphic', 0))
if mob_graphic in AA_IDS_IGNORE:
return True
if is_friend(mob):
return True
return False
except:
return False
def auto_attack_step():
if not aa_enabled:
return
if AA_POST_KILL_COMPLETE_LOOT:
c = _nearest_openable_corpse(AL_RANGE)
if c:
try:
s = int(getattr(c, "Serial", 0) or 0)
except:
s = 0
if s:
_ensure_open(s)
if _opened(s) and _container_has_rule_matches(s):
cancel_motion_and_target()
_loot_hold_after_kill()
return
global aa_new_target, _aa_target_serial
if aa_new_target:
aa_new_target = False
API.Pause(0.02)
if _aa_target_serial:
requested_mob = _safe(API.FindMobile, _aa_target_serial)
if (requested_mob and
getattr(requested_mob, 'Distance', 9999) < AA_MAX_DISTANCE and
not should_ignore_mob(requested_mob) and
not getattr(requested_mob, 'IsDead', False)):
mob = requested_mob
else:
_aa_target_serial = 0
mob = None
else:
mob = None
else:
mob = None
if _aa_target_serial:
mob = _safe(API.FindMobile, _aa_target_serial)
try:
if (not mob) or (mob.Distance >= AA_MAX_DISTANCE) or should_ignore_mob(mob):
_aa_target_serial = 0
mob = None
except:
_aa_target_serial = 0
mob = None
if not mob:
targets = get_attackable_targets()
mob = targets[0] if targets else None
if mob:
try:
_aa_target_serial = int(mob.Serial)
except:
_aa_target_serial = 0
if aa_honor:
_honor_target_if_fresh(mob)
if not mob:
return
died = False
try:
died = (
bool(getattr(mob, "IsDead", False))
or int(getattr(mob, "Hits", 1) or 1) <= 0
)
except:
pass
if died:
cancel_motion_and_target()
_loot_hold_after_kill()
_aa_target_serial = 0
return
if aa_auto_follow:
if not is_hidden():
global _target_stuck_count, _last_player_pos
try:
current_pos = (API.Player.X, API.Player.Y)
mob_distance = getattr(mob, 'Distance', 9999)
if (_last_player_pos == current_pos and
mob_distance > 1 and
mob_distance <= AA_MAX_DISTANCE):
_target_stuck_count += 1
if _target_stuck_count >= _target_stuck_threshold:
_safe(API.CancelAutoFollow)
_safe(API.CancelPathfinding)
_aa_target_serial = 0
clear_stuck_tracking()
API.SysMsg(f"Target unreachable, finding new target", 33)
return
else:
_target_stuck_count = 0
_last_player_pos = current_pos
except:
pass
_safe(API.AutoFollow, mob)
else:
_safe(API.CancelAutoFollow)
_safe(API.CancelPathfinding)
else:
_safe(API.CancelAutoFollow)
_safe(API.CancelPathfinding)
_use_ability_if_ready()
_safe(API.Attack, mob)
try:
mob.Hue = 32
except:
pass
API.Pause(0.02)
if not aa_auto_follow:
_safe(API.CancelPathfinding)
ab_enabled = True
al_enabled = AL_ENABLED_DEFAULT
_bandaging_active = False
_last_damage_ms = 0
_last_bandage_attempt_ms = 0
_bandage_until_ms = 0
def ab_enable():
global ab_enabled
ab_enabled = True
def ab_disable():
global ab_enabled
ab_enabled = False
def al_enable():
global al_enabled
al_enabled = True
def al_disable():
global al_enabled
al_enabled = False
def _set_bandage_lock(ms):
global _bandage_until_ms
_bandage_until_ms = now_ms() + int(ms)
def auto_bandage_step():
if not ab_enabled:
return
global _last_damage_ms, _last_bandage_attempt_ms, _bandage_until_ms, _bandaging_active
hp = player_hits()
mx = player_hits_max()
nowm = now_ms()
prev = getattr(auto_bandage_step, "_prev_hp", hp)
if hp < prev:
_last_damage_ms = nowm
auto_bandage_step._prev_hp = hp
missing = mx - hp
if missing < AB_HP_MISSING_MIN and not is_poisoned():
return
if is_hidden() and not AB_ALLOW_WHILE_HIDDEN:
return
if nowm < _bandage_until_ms:
return
if (nowm - _last_bandage_attempt_ms) < AB_MIN_RETRY_GAP_MS:
return
if (nowm - _last_damage_ms) < AB_AFTER_HIT_GRACE_MS and not is_poisoned():
return
if api_busy():
return
_safe(API.CancelTarget)
ok = False
try:
ok = bool(API.BandageSelf())
except:
ok = False
if not ok:
_safe(API.SysMsg, "WARNING: No bandages!", 32)
_last_bandage_attempt_ms = nowm
return
_set_bandage_lock(AB_ETA_MS + 150)
_bandaging_active = True
_last_bandage_attempt_ms = nowm
if now_ms() >= _bandage_until_ms and _bandaging_active:
_bandaging_active = False
_al_seen = {}
_last_target_serial = 0
def _looks_secure(ent) -> bool:
if not AL_SKIP_SECURE:
return False
try:
if getattr(ent, "IsLockedDown", False):
return True
except:
pass
try:
if getattr(ent, "IsSecure", False):
return True
except:
pass
try:
txt = None
try:
txt = API.GetTooltipText(ent)
except:
pass
try:
txt = txt or API.GetProperties(ent)
except:
pass
if txt and isinstance(txt, str):
lt = txt.lower()
for tok in AL_OBJECT_NAME_DENY:
if tok in lt:
return True
except:
pass
try:
nm = (getattr(ent, "Name", "") or "").lower()
for tok in AL_OBJECT_NAME_DENY:
if tok in nm:
return True
except:
pass
return False
def _item_name_lc(it) -> str:
try:
nm = (getattr(it, "Name", "") or "").strip()
if nm:
return nm.lower()
except:
pass
try:
tip = API.GetTooltipText(it)
if isinstance(tip, str) and tip.strip():
return tip.strip().lower()
except:
pass
try:
props = API.GetProperties(it)
if isinstance(props, str) and props.strip():
return props.strip().lower()
except:
pass
return ""
def _scan_opened_containers(range_tiles: int):
found = set()
_safe(API.ClearIgnoreList)
for _ in range(24):
c = _safe(API.NearestCorpse, range_tiles)
if not c:
break
s = int(getattr(c, "Serial", 0) or 0)
if s and _opened(s):
found.add(s)
_safe(API.IgnoreObject, s)
_safe(API.ClearIgnoreList)
if not AL_ONLY_CORPSES:
_safe(API.ClearIgnoreList)
for _ in range(36):
e = _safe(API.NearestEntity, API.ScanType.Objects, range_tiles)
if not e:
break
s = int(getattr(e, "Serial", 0) or 0)
if s:
if not _is_corpse(e) and not _looks_secure(e) and _opened(s):
found.add(s)
_safe(API.IgnoreObject, s)
_safe(API.ClearIgnoreList)
return list(found)
def _handle_container(serial):
t = now_ms()
last = _al_seen.get(serial, 0)
if (t - last) < AL_RECHECK_COOLDOWN_MS:
return
if not _opened(serial):
_al_seen[serial] = t
return
_loot_container(serial)
_al_seen[serial] = now_ms()
def auto_loot_tick():
if not al_enabled:
return
global _last_target_serial
cur = int(getattr(API, "LastTargetSerial", 0) or 0)
if cur and cur != _last_target_serial and _opened(cur):
_last_target_serial = cur
_handle_container(cur)
for s in _scan_opened_containers(AL_RANGE):
_handle_container(s)
gump = None
_rows = {}
_local_stop_requested = False
GUMP_W, GUMP_H = 160, 490
_ROW_H = 22
_CB_W = 22
_PAD_X = 10
_CB_OFFSET = 0
def _cleanup_on_stop():
try:
API.DisplayRange(0)
except:
pass
_radius_last.update({"shown": False, "dist": 0})
for fn in (
"CancelAutoFollow",
"CancelPathfinding",
"ClearMoveQueue",
"CancelTarget",
"ClearIgnoreList",
"ClearJournal",
):
_safe(getattr(API, fn))
try:
if API.PrimaryAbilityActive():
API.ToggleAbility("primary")
except:
pass
try:
if API.SecondaryAbilityActive():
API.ToggleAbility("secondary")
except:
pass
try:
gump.Dispose()
except:
pass
_release_lock()
def _set_radius(enabled: bool):
global AA_SHOW_RADIUS
AA_SHOW_RADIUS = bool(enabled)
radius_apply(force=True)
row = _rows.get("showradius")
if row:
try:
row["cb"].IsChecked = AA_SHOW_RADIUS
except:
pass
def _mk_row(g, y, name, text, on_enable, on_disable, checked=True, hue=0, indent=False):
lbl_x = _PAD_X + (18 if indent else 0)
cb_x = GUMP_W - _PAD_X - _CB_W - _CB_OFFSET
cb = API.CreateGumpCheckbox("", hue, checked)
cb.SetRect(cb_x, y, _CB_W, _ROW_H)
lbl = Label(text, DEFAULT_FONT_SIZE, "#FFFFFF", "left")
lbl.SetPos(lbl_x, y)
def _on_click():
if name == "showradius":
_set_radius(cb.IsChecked)
else:
(on_enable if cb.IsChecked else on_disable)()
API.AddControlOnClick(cb, _on_click)
g.Add(lbl)
g.Add(cb)
_rows[name] = {"lbl": lbl, "cb": cb}
return y + _ROW_H
def build_gump():
global gump
gump = API.CreateGump(True, True, True)
gump.SetRect(100, 100, GUMP_W, GUMP_H)
bg = API.CreateGumpColorBox(0.88, "#212121")
bg.SetRect(0, 0, GUMP_W, GUMP_H)
gump.Add(bg)
title = Label("MEGA-AUTO", DEFAULT_FONT_SIZE, "#FFB000", "center", GUMP_W)
title.SetRect(0, 8, GUMP_W, 24)
gump.Add(title)
y = 40
global AA_ABILITY_USE
if AA_ABILITY_USE not in ("primary", "secondary", "off"):
AA_ABILITY_USE = "secondary"
y = _mk_row(
gump, y, "autoattack", "Auto-Attack", aa_enable, aa_disable, checked=aa_enabled
)
y = _mk_row(
gump, y, "follow", "- Follow", af_enable, af_disable, checked=aa_auto_follow, indent=True
)
y = _mk_row(
gump, y, "honor", "- Honor", hon_enable, hon_disable, checked=aa_honor, indent=True
)
y = _mk_row(
gump,
y,
"abilities",
"- Abilities",
abl_enable,
abl_disable,
checked=True,
indent=True,
)
ability_options = ["primary", "secondary"]
ability_labels = ["Primary", "Secondary"]
group_id = 1
radio_x = _PAD_X + 36
radio_y = y
radio_w = 80
radio_h = _ROW_H
for idx, (opt, label) in enumerate(zip(ability_options, ability_labels)):
checked = (AA_ABILITY_USE == opt)
rb = API.CreateGumpRadioButton(label, group_id, 0x00D2, 0x00D3, 0xFFFF, checked)
rb.SetRect(radio_x, radio_y + idx * (radio_h + 2), radio_w, radio_h)
def make_onclick(opt_val):
def _on_click():
global AA_ABILITY_USE
AA_ABILITY_USE = opt_val
return _on_click
API.AddControlOnClick(rb, make_onclick(opt))
gump.Add(rb)
y += len(ability_options) * (radio_h + 2)
y = _mk_row(
gump,
y,
"autobuff",
"Auto-Buff",
bf_enable,
bf_disable,
checked=bool(bf_enabled),
indent=False,
)
radio_x = _PAD_X + 36
radio_y = y
radio_w = 80
radio_h = _ROW_H
for idx, buff in enumerate(BF_BUFFS_ROTATION):
checked = buff["enabled"]
rb = API.CreateGumpRadioButton(buff["label"], 100 + idx, 0x00D2, 0x00D3, 0xFFFF, checked)
rb.SetRect(radio_x, radio_y + idx * (radio_h + 2), radio_w, radio_h)
def make_onclick(buff_ref, rb_ref):
def _on_click():
buff_ref["enabled"] = rb_ref.IsChecked
return _on_click
API.AddControlOnClick(rb, make_onclick(buff, rb))
gump.Add(rb)
y += len(BF_BUFFS_ROTATION) * (radio_h + 2)
y = _mk_row(
gump,
y,
"showradius",
"- Radius",
lambda: _set_radius(True),
lambda: _set_radius(False),
checked=bool(AA_SHOW_RADIUS),
indent=True,
)
y = _mk_row(gump, y, "bandage", "Auto-Bandage", ab_enable, ab_disable, checked=True)
y = _mk_row(
gump,
y,
"autoloot",
"Auto-Loot",
al_enable,
al_disable,
checked=bool(al_enabled),
)
btn_new = API.CreateSimpleButton("[NEW TARGET]", 120, 22)
btn_new.SetPos((GUMP_W - 120) // 2, GUMP_H - 124)
API.AddControlOnClick(btn_new, aa_request_new_target)
gump.Add(btn_new)
btn_targets = API.CreateSimpleButton("[TARGETS]", 120, 22)
btn_targets.SetPos((GUMP_W - 120) // 2, GUMP_H - 94)
def on_targets_button_click():
show_targets_popup()
API.AddControlOnClick(btn_targets, on_targets_button_click)
gump.Add(btn_targets)
stop_btn = API.CreateSimpleButton("[STOP]", 110, 24)
stop_btn.SetBackgroundHue(32)
stop_btn.SetPos((GUMP_W - 110) // 2, GUMP_H - 54)
def on_stop_button_click():
aa_disable()
af_disable()
bf_disable()
ab_disable()
al_disable()
for key in ["autoattack", "autobuff", "bandage", "autoloot"]:
row = _rows.get(key)
if row:
try:
row["cb"].IsChecked = False
except:
pass
API.SysMsg("Auto-attack stopped.", 32)
API.AddControlOnClick(stop_btn, on_stop_button_click)
gump.Add(stop_btn)
API.AddGump(gump)
def show_targets_popup():
global targets_popup, targets_popup_rows
popup_w, popup_h = targets_gump_width, targets_gump_height
try:
if targets_popup and hasattr(targets_popup, 'IsDisposed') and not targets_popup.IsDisposed:
return
except Exception:
pass
targets_popup = API.CreateGump(True, True, False)
targets_popup.SetRect(140, 140, popup_w, popup_h)
bg = API.CreateGumpColorBox(0.92, "#232323")
bg.SetRect(0, 0, popup_w, popup_h)
targets_popup.Add(bg)
title = API.CreateGumpTTFLabel("TARGETS", DEFAULT_FONT_SIZE, "#FFB000", font="Avadonia", aligned="center", maxWidth=popup_w)
title.SetRect(0, 8, popup_w, 24)
targets_popup.Add(title)
targets_popup_rows = []
for i in range(TARGETS_MAX_DISPLAY):
label = API.CreateGumpTTFLabel("", DEFAULT_FONT_SIZE, "#FFFFFF", font=AV_FONT, aligned="left", maxWidth=popup_w-96)
label.SetRect(18, 40 + i * 26 + 4, popup_w - 96, 20)
bar_w = int(80 * 0.75)
bar_h = int(12 * 0.75)
hp_bar = API.CreateGumpSimpleProgressBar(bar_w, bar_h, backgroundColor="#616161", foregroundColor="#21DD21", value=0, max=1)
hp_bar.SetRect(popup_w - bar_w - 24, 40 + i * 26 + 8, bar_w, bar_h)
label.IsVisible = False
hp_bar.IsVisible = False
targets_popup.Add(label)
targets_popup.Add(hp_bar)
targets_popup_rows.append((label, hp_bar))
API.AddGump(targets_popup)
MAIN_TICK_MS = 100
_last_run = {}
def run_every(name: str, interval_ms: int, fn):
nowm = now_ms()
last = _last_run.get(name, 0)
if (nowm - last) >= interval_ms:
_last_run[name] = nowm
fn()
def main():
global aa_new_target, _last_target_serial, _aa_target_serial, my_instance_uuid
my_instance_uuid = str(uuid.uuid4())
_refresh_lock()
try:
_safe(API.SysMsg, "MEGA-AUTO loaded.", 32)
_cleanup_on_stop()
build_gump()
try:
current_char = int(API.Player.Serial)
except:
current_char = 0
try:
last_instance_check = now_ms()
while True:
API.ProcessCallbacks()
run_every("lock_heartbeat", 1000, _refresh_lock)
serial_now = player_serial()
if serial_now and serial_now != current_char:
_cleanup_on_stop()
aa_disable()
af_disable()
hon_disable()
abl_disable()
aa_new_target = False
_last_target_serial = 0
_aa_target_serial = 0
build_gump()
current_char = serial_now
run_every("attack", AA_ATTACK_STEP_MS, auto_attack_step)
run_every("autobuff", 1000, auto_buff_step)
run_every("purge_loot_denied", 60000, purge_loot_denied_serials)
run_every("clear_stuck_tracking_periodic", 30000, lambda: clear_stuck_tracking() if not aa_enabled or not aa_auto_follow else None)
if targets_popup:
run_every("targets_popup_refresh", TARGETS_REFRESH_MS, refresh_targets_popup)
run_every("bandage", AB_BANDAGE_POLL_MS, auto_bandage_step)
run_every("autoloot", AL_LOOT_TICK_MS, auto_loot_tick)
run_every("radius_apply", 300, lambda: radius_apply(False))
if now_ms() - last_instance_check >= INSTANCE_CHECK_MS:
last_instance_check = now_ms()
try:
latest_uuid = API.GetPersistentVar(INSTANCE_KEY, "", API.PersistentVar.Global)
except:
latest_uuid = my_instance_uuid
if latest_uuid and latest_uuid != my_instance_uuid:
_cleanup_on_stop()
_safe(API.SysMsg("MEGA-AUTO: Newer instance detected. Shutting down.", 33))
break
if _local_stop_requested:
_cleanup_on_stop()
_safe(API.SysMsg, "MEGA-AUTO stopped.", 33)
break
API.Pause(MAIN_TICK_MS / 1000.0)
finally:
_cleanup_on_stop()
_safe(API.SysMsg, "MEGA-AUTO stopped.", 33)
finally:
_release_lock()
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment