Last active
September 26, 2025 18:43
-
-
Save alivesay/bd33d75a6c7d4b42940c04c5aa0bdfd6 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 = 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