Last active
September 6, 2025 00:44
-
-
Save alivesay/9ba31eb7befbcc02b8679d83ab9483c1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 = 30 # max distance to search for enemies | |
AA_SHOW_RADIUS = False # show visual radius circle | |
AA_ATTACK_STEP_MS = 50 # 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" | |
AA_ABILITY_MANA_MIN = 36 # min mana required for ability use | |
AB_HP_MISSING_MIN = 10 # min HP missing before bandage starts | |
AB_ALLOW_WHILE_HIDDEN = False # allow bandages while hidden | |
AB_BANDAGE_POLL_MS = 60 # 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 = 140 # loot tick interval (ms) | |
AL_RECHECK_COOLDOWN_MS = 300 # cooldown between re-checks of same container (ms) | |
AL_MOVE_PAUSE_MS = 350 # 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": True, | |
"names": [ | |
"amber", | |
"amethyst", | |
"citrine", | |
"diamond", | |
"emerald", | |
"ruby", | |
"sapphire", | |
"star sapphire", | |
"tourmaline", | |
], | |
"graphics": [], | |
"min_amount": 0, | |
"dest": lambda: API.Backpack, | |
}, | |
{ | |
"label": "Reagents", | |
"enabled": True, | |
"names": ["grave dust"], | |
"graphics": [], | |
"min_amount": 0, | |
"dest": lambda: API.Backpack, | |
}, | |
] | |
for r in AL_RULES: | |
r["_names_lc"] = [n.lower() for n in (r.get("names") or [])] | |
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)) | |
# For single-instance enforcement: only the most recent instance runs | |
INSTANCE_KEY = "MEGA_AUTO_INSTANCE_TS" | |
INSTANCE_CHECK_MS = 1000 | |
def _refresh_lock(): | |
try: | |
API.SavePersistentVar(LOCK_KEY, str(now_ms()), API.PersistentVar.Char) | |
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(LOCK_KEY, API.PersistentVar.Char) | |
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 = 18 | |
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, | |
) | |
_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 = True | |
aa_abilities = True | |
aa_new_target = False | |
_aa_target_serial = 0 | |
_loot_lock_until_ms = 0 | |
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 | |
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 | |
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_new_target = True | |
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: | |
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: | |
_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 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) | |
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 | |
_aa_target_serial = 0 | |
mob = None | |
if _aa_target_serial: | |
mob = _safe(API.FindMobile, _aa_target_serial) | |
try: | |
if (not mob) or (mob.Distance >= AA_MAX_DISTANCE): | |
_aa_target_serial = 0 | |
mob = None | |
except: | |
_aa_target_serial = 0 | |
mob = None | |
if not mob: | |
mob = _safe( | |
API.NearestMobile, | |
[API.Notoriety.Gray, API.Notoriety.Criminal, API.Notoriety.Murderer], | |
AA_MAX_DISTANCE, | |
) | |
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: | |
_safe(API.AutoFollow, mob) | |
else: | |
_safe(API.CancelAutoFollow) | |
_safe(API.CancelPathfinding) | |
_use_ability_if_ready() | |
_safe(API.Attack, mob) | |
try: | |
mob.Hue = 32 | |
except: | |
pass | |
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 = 200, 320 | |
_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 | |
y = _mk_row( | |
gump, y, "autoattack", "Auto-Attack", aa_enable, aa_disable, checked=False | |
) | |
y = _mk_row( | |
gump, y, "follow", "- Follow", af_enable, af_disable, checked=False, indent=True | |
) | |
y = _mk_row( | |
gump, y, "honor", "- Honor", hon_enable, hon_disable, checked=True, 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, | |
"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 - 64) | |
API.AddControlOnClick(btn_new, aa_request_new_target) | |
gump.Add(btn_new) | |
stop_btn = API.CreateSimpleButton("[STOP]", 110, 24) | |
stop_btn.SetBackgroundHue(32) | |
stop_btn.SetPos((GUMP_W - 110) // 2, GUMP_H - 34) | |
def on_stop_button_click(): | |
aa_disable() | |
API.SysMsg("Auto-attack stopped.", 32) | |
API.AddControlOnClick(stop_btn, on_stop_button_click) | |
gump.Add(stop_btn) | |
API.AddGump(gump) | |
MAIN_TICK_MS = 20 | |
_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_ts = now_ms() | |
try: | |
API.SavePersistentVar(INSTANCE_KEY, str(my_instance_ts), API.PersistentVar.Global) | |
except: | |
pass | |
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("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_ts = int(API.GetPersistentVar(INSTANCE_KEY, "0", API.PersistentVar.Global)) | |
except: | |
latest_ts = my_instance_ts | |
if latest_ts > my_instance_ts: | |
_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