--[[ ==================================================================== Automatic Activator for Tagged Auto Assembler Scripts (v1.2.1) ==================================================================== Features: - Automatically activates AA scripts that contain the {!activate} or {!activate, delay} tag - Supports recursive batch add/remove of tags for all child AA scripts under group headers (right-click menu) - Configurable three-level logging (silent / errors only / info+errors / debug) Triggers: process attach, table load, manual load (button/menu/drag&drop), hotkey Ctrl+Shift+A ==================================================================== --]] -- =================================================================== -- ★★★ User-configurable section ★★★ -- =================================================================== AutoActivator = { -- ★ Log level: 0=silent, 1=errors only, 2=info+errors, 3=debug (all) logLevel = 1, -- ★ Delay before scanning (ms). After loading a CT, the address list may be filled -- with a delay; a suitable delay improves scan success. Recommended 200~1000ms. delay = 500, -- ★ Auto-rescan interval (seconds). Set to 0 to disable; if >0, re-scans every N seconds, -- useful for scripts that change dynamically or need continuous monitoring. scanInterval = 0, -- ★ Parent-child dependency check. If true, child scripts will only be activated if all AA -- scripts in the parent chain are already active; useful for scripts with dependencies. requireParentActivated = true, -- ★ Detailed debug log switch (only effective when logLevel >= 3). debug = false, -- ★ Recursion maximum depth. Prevents stack overflow from accidental cycles; generally no need to change. maxDepth = 100, -- ★ Single script activation timeout (ms). If a script takes longer than this to activate, -- it will be skipped and the next one processed to avoid hanging. timeout = 5000, -- ★ Hotkey toggle. enableHotkey = true, -- ★ Hotkey combination (default Ctrl+Shift+A). hotkey = { keys = {VK_CONTROL, VK_SHIFT, VK_A} }, -- ★ Auto Assembler script type constant (auto-detected, no need to modify). vtAA = vtAutoAssembler or 11, -- ======== Internal variables – do not modify ======== processing = false, queue = {}, index = 0, menuAdded = false, loaded = false, retryTimer = nil, scanTimer = nil, sessionId = 0, -- ★ Verification callback (can be overridden by advanced users). Returning false will immediately deactivate the activated script. verify = function(memrec) return true end, -- Internal cache for detecting address list changes _lastAddrSig = nil, } -- =================================================================== -- No need to modify anything below -- =================================================================== local TAG_PREFIX = "{!activate" -- Manual trigger function (can be executed in the Lua engine) function forceActivateScripts() AutoActivator:printInfo("Manual trigger (hotkey)") AutoActivator:start(true) end -- Logging functions (output according to level) function AutoActivator:printError(...) if self.logLevel >= 1 then print("[ERROR]", ...) end end function AutoActivator:printInfo(...) if self.logLevel >= 2 then print("[INFO]", ...) end end function AutoActivator:printDebug(...) if self.logLevel >= 3 and self.debug then print("[DEBUG]", ...) end end -- Legacy log method kept, points to printDebug (compatibility) function AutoActivator:log(...) self:printDebug(...) end -- =================================================================== -- Parse activation tag -- =================================================================== function AutoActivator:parseTag(script) if not script or script == "" then return nil end local firstLine = script:match("^[ \t]*(.-)[\n\r]") or script if firstLine:sub(1, #TAG_PREFIX) ~= TAG_PREFIX then return nil end local param = firstLine:match("^" .. TAG_PREFIX .. "%s*,?%s*(.*)}") if not param then return 0 end param = param:match("^%s*(.-)%s*$") if param == "" then return 0 end local delay = tonumber(param) if delay and delay >= 0 then return math.floor(delay) else self:printDebug(string.format("Invalid delay parameter in tag '%s', treated as 0", firstLine)) return 0 end end -- =================================================================== -- Recursively collect scripts to activate -- =================================================================== function AutoActivator:collect(memrec, list, depth) depth = depth or 0 if depth > self.maxDepth then return end if memrec.Type == self.vtAA then local script = memrec.Script local delay = self:parseTag(script) if delay ~= nil and not memrec.Active then table.insert(list, { memrec = memrec, delay = delay }) self:printDebug(string.format("Found script to activate: %s (delay %dms)", memrec.Description, delay)) end end for i = 0, memrec.Count - 1 do self:collect(memrec.Child[i], list, depth + 1) end end -- =================================================================== -- Check whether all scripts in the parent chain are active -- =================================================================== function AutoActivator:isParentChainActive(memrec) local parent = memrec.Parent while parent do if parent.Type == self.vtAA and not parent.Active then return false end parent = parent.Parent end return true end -- =================================================================== -- Sequential activation processor -- =================================================================== function AutoActivator:processNext() if not self.processing then self:printDebug("Processing aborted, skipping") return end if self.index >= #self.queue then self.processing = false self.queue = {} self:printInfo("All scripts processed") self:scheduleNextScan() return end local item = self.queue[self.index + 1] self.index = self.index + 1 local memrec = item.memrec local delay = item.delay if self.requireParentActivated and not self:isParentChainActive(memrec) then self:printDebug(string.format("Skipping '%s' (parent not active)", memrec.Description)) self:processNext() return end local currentSession = self.sessionId local function doActivate() if self.sessionId ~= currentSession then self:printDebug("New task detected, old activation callback aborted") return end if not self.processing then self:printDebug("Processing stopped, aborting current activation") return end if memrec.Active then self:processNext() return end local oldOnActivate = memrec.OnActivate local activated = false local function onActivateCallback(mr, before, currentstate) if not before then activated = true mr.OnActivate = oldOnActivate self:printInfo(string.format("Activated: %s", mr.Description)) local ok, err = pcall(function() if not self.verify(mr) then error("Verification failed") end end) if not ok then self:printError(string.format("Verification failed, deactivating '%s': %s", mr.Description, err)) mr.Active = false mr.OnActivate = oldOnActivate activated = false self:processNext() return true end self:processNext() return true end return true end memrec.OnActivate = onActivateCallback local ok, err = pcall(function() memrec.Active = true end) if not ok then self:printError(string.format("Error activating '%s': %s", memrec.Description, err)) memrec.OnActivate = oldOnActivate self:processNext() return end createTimer(self.timeout, function() if self.sessionId ~= currentSession then return end if not activated and memrec.OnActivate == onActivateCallback then memrec.OnActivate = oldOnActivate self:printError(string.format("Warning: %s activation timeout, skipping", memrec.Description)) self:processNext() end end) end if delay > 0 then createTimer(delay, doActivate) else createTimer(1, doActivate) end end -- =================================================================== -- Rescan scheduling -- =================================================================== function AutoActivator:scheduleNextScan() if self.scanTimer then self.scanTimer = nil end local interval = self.scanInterval if not interval or interval <= 0 then return end self:printDebug(string.format("Will rescan in %d seconds", interval)) local timerId = createTimer(interval * 1000, function() if self.scanTimer == timerId then self:printDebug("Timer-triggered rescan") self:start(true) end end) self.scanTimer = timerId end -- =================================================================== -- Core start function (force=true can interrupt an old task) -- =================================================================== function AutoActivator:start(force, retry) self:addMenu() force = force or false retry = retry or 0 self:printInfo(string.format("start() called, force=%s, retry=%d, sessionId=%d", tostring(force), retry, self.sessionId)) -- Address list change detection (fallback trigger) local addrList = getAddressList() if addrList then local sig = tostring(addrList.Count) .. ":" .. (addrList.Count > 0 and addrList.MemoryRecord[0].ID or "nil") if self._lastAddrSig and self._lastAddrSig ~= sig then self:printInfo(string.format("Address list change detected (%s -> %s)", self._lastAddrSig, sig)) if self.processing then self.processing = false self.queue = {} self.index = 0 self.retryTimer = nil end end self._lastAddrSig = sig end if self.processing and not force then self:printInfo("A task is already running, skipping this scan") self:scheduleNextScan() return end if self.processing and force then self:printInfo("Force mode: interrupting old task, starting new scan") self.processing = false self.queue = {} self.index = 0 self.retryTimer = nil self.sessionId = self.sessionId + 1 end if not self.processing then self.sessionId = self.sessionId + 1 end local currentSession = self.sessionId if self.retryTimer then self.retryTimer = nil end if not addrList or addrList.Count == 0 then if retry < 5 then self:printInfo(string.format("Address list empty, retry in %ds (%d/5)", retry + 1, retry + 1)) local timerId = createTimer(1000, function() if self.sessionId == currentSession and self.retryTimer == timerId then self:start(force, retry + 1) end end) self.retryTimer = timerId else self:printInfo("Address list still empty after multiple retries, giving up") self:scheduleNextScan() end return end self:printInfo("Address list is not empty, collecting scripts to activate...") local list = {} for i = 0, addrList.Count - 1 do self:collect(addrList.MemoryRecord[i], list) end if #list == 0 then self:printInfo("No tagged and inactive scripts found") self:scheduleNextScan() return end self:printInfo(string.format("Found %d script(s) to activate", #list)) self.queue = list self.index = 0 self.processing = true local waitTime = (self.delay > 0) and self.delay or 1 createTimer(waitTime, function() if self.sessionId == currentSession then self:processNext() end end) end -- =================================================================== -- Right-click menu (supports recursive add/remove tags) -- =================================================================== function AutoActivator:addMenu() if self.menuAdded then return end local addrList = getAddressList() if not addrList then self:printDebug("Address list not ready, cannot add right-click menu") return end local popup = addrList.PopupMenu if not popup then self:printDebug("Cannot get address list popup menu") return end local items = popup.Items for i = 0, items.Count - 1 do if items[i].Caption == "Add auto-activation tag" then self.menuAdded = true return end end local sep = createMenuItem(popup) sep.Caption = "-" items.add(sep) -- ============================================================ -- Add tag (supports recursive processing of group headers) -- ============================================================ local addItem = createMenuItem(popup) addItem.Caption = "Add auto-activation tag" addItem.OnClick = function() local selected = addrList.SelectedRecord if not selected then showMessage("Please select a memory record first") return end -- Check if it's a group (has children and is not an AA script itself) local isGroup = (selected.Type ~= self.vtAA and selected.Count > 0) if not isGroup then -- Original single-record handling if selected.Type ~= self.vtAA then showMessage("Selected record is not an Auto Assembler script") return end local script = selected.Script or "" local firstLine = script:match("^[ \t]*(.-)[\n\r]") or script if firstLine:sub(1, #TAG_PREFIX) == TAG_PREFIX then showMessage("This script already has an activation tag") return end local delayStr = inputQuery("Enter delay", "Enter delay in milliseconds (leave empty for immediate):", "0") if delayStr == nil then return end local delay = tonumber(delayStr) if not delay or delay < 0 then delay = 0 end local tag = (delay > 0) and string.format(TAG_PREFIX .. ", %d}", delay) or TAG_PREFIX .. "}" local ok, err = pcall(function() selected.Script = tag .. "\n" .. script end) if ok then showMessage("Activation tag added") else showMessage("Failed to add tag: " .. err) end return end -- Group handling: batch add tags local delayStr = inputQuery("Batch add delay", "Enter delay in milliseconds (applied to all child AA scripts, leave empty for immediate):", "0") if delayStr == nil then return end local delay = tonumber(delayStr) if not delay or delay < 0 then delay = 0 end local function addTagToRecord(memrec, d) if memrec.Type == self.vtAA then local script = memrec.Script or "" local firstLine = script:match("^[ \t]*(.-)[\n\r]") or script if firstLine:sub(1, #TAG_PREFIX) == TAG_PREFIX then return false end local tag = (d > 0) and string.format(TAG_PREFIX .. ", %d}", d) or TAG_PREFIX .. "}" memrec.Script = tag .. "\n" .. script return true end return false end local function recursiveAdd(memrec, d) local count = 0 if memrec.Type == self.vtAA then if addTagToRecord(memrec, d) then count = count + 1 end end for i = 0, memrec.Count - 1 do count = count + recursiveAdd(memrec.Child[i], d) end return count end local total = recursiveAdd(selected, delay) if total == 0 then showMessage("No AA scripts found to tag (they may already have tags)") else showMessage(string.format("Successfully added activation tags to %d AA script(s)", total)) end end items.add(addItem) -- ============================================================ -- Remove tag (supports recursive processing of group headers) -- ============================================================ local removeItem = createMenuItem(popup) removeItem.Caption = "Remove auto-activation tag" removeItem.OnClick = function() local selected = addrList.SelectedRecord if not selected then showMessage("Please select a memory record first") return end local isGroup = (selected.Type ~= self.vtAA and selected.Count > 0) if not isGroup then -- Single record if selected.Type ~= self.vtAA then showMessage("Selected record is not an Auto Assembler script") return end local script = selected.Script or "" local firstLine, rest = script:match("^[ \t]*(.-)([\n\r].*)") if not firstLine then firstLine = script rest = "" end if firstLine:sub(1, #TAG_PREFIX) ~= TAG_PREFIX then showMessage("This script does not have an activation tag") return end local newScript = rest:match("^[\n\r]*(.*)") or "" local ok, err = pcall(function() selected.Script = newScript end) if ok then showMessage("Activation tag removed") else showMessage("Failed to remove tag: " .. err) end return end -- Batch remove local function recursiveRemove(memrec) local count = 0 if memrec.Type == self.vtAA then local script = memrec.Script or "" local firstLine, rest = script:match("^[ \t]*(.-)([\n\r].*)") if firstLine and firstLine:sub(1, #TAG_PREFIX) == TAG_PREFIX then local newScript = (rest and rest:match("^[\n\r]*(.*)")) or "" memrec.Script = newScript count = 1 end end for i = 0, memrec.Count - 1 do count = count + recursiveRemove(memrec.Child[i]) end return count end local total = recursiveRemove(selected) if total == 0 then showMessage("No AA scripts with activation tags found") else showMessage(string.format("Successfully removed activation tags from %d AA script(s)", total)) end end items.add(removeItem) self.menuAdded = true self:printDebug("Right-click menu added") end -- =================================================================== -- Manual load hooks (override buttons, menus, drag&drop, dialogs, etc.) -- =================================================================== function AutoActivator:hookManualLoad() local mainForm = getMainForm() if not mainForm then self:printDebug("MainForm not available, cannot hook manual load") return end local function wrapHandler(name, control, eventName) if not control then return end local oldHandler = control[eventName] control[eventName] = function(...) if oldHandler then oldHandler(...) end self:printInfo("Detected " .. name .. " trigger") createTimer(100, function() self:start(true) end) end self:printDebug("Hooked " .. name) end wrapHandler("LoadButton", mainForm.LoadButton, "OnClick") wrapHandler("Load1", mainForm.Load1, "OnClick") wrapHandler("actOpen", mainForm.actOpen, "OnExecute") wrapHandler("OpenDialog1.OnClose", mainForm.OpenDialog1, "OnClose") -- Drag & drop local oldOnDropFiles = mainForm.OnDropFiles mainForm.AllowDropFiles = true mainForm.OnDropFiles = function(sender, fileNames) if oldOnDropFiles then oldOnDropFiles(sender, fileNames) end self:printInfo("Detected file drag&drop") createTimer(100, function() self:start(true) end) end self:printDebug("Hooked OnDropFiles") end -- =================================================================== -- Event registration (supports re-registration) -- =================================================================== function AutoActivator:registerEvents() if not MainForm then self:printDebug("MainForm not available, delaying registration") createTimer(500, function() self:registerEvents() end) return end if self.loaded and MainForm.OnTableLoad and MainForm.OnTableLoad ~= self._tableLoadHandler then self.loaded = false end if self.loaded then self:printDebug("Event listeners already registered, skipping") return end local selfRef = self local function onTableLoadHandler(before) if selfRef._oldOnTableLoad then selfRef._oldOnTableLoad(before) end if not before then selfRef:printInfo("Table load event detected") selfRef:start(true) end end self._oldOnTableLoad = MainForm.OnTableLoad MainForm.OnTableLoad = onTableLoadHandler self._tableLoadHandler = onTableLoadHandler local function onProcessOpenedHandler(pid, handle, caption) if selfRef._oldOnProcessOpened then selfRef._oldOnProcessOpened(pid, handle, caption) end selfRef:printInfo("Process attach event detected") createTimer(500, function() selfRef:start(true) end) end self._oldOnProcessOpened = MainForm.OnProcessOpened MainForm.OnProcessOpened = onProcessOpenedHandler self.loaded = true self:printInfo("Event listeners registered") self:hookManualLoad() end -- =================================================================== -- Address list polling detection (fallback, uses a Timer object to poll continuously) -- =================================================================== function AutoActivator:startPolling() local timer = createTimer() timer.Interval = 500 timer.OnTimer = function() local addrList = getAddressList() if addrList and addrList.Count > 0 then local sig = tostring(addrList.Count) .. ":" .. (addrList.Count > 0 and addrList.MemoryRecord[0].ID or "nil") if AutoActivator._lastAddrSig and AutoActivator._lastAddrSig ~= sig then AutoActivator:printInfo("[Poll] Address list change detected, triggering scan") AutoActivator:start(true) end AutoActivator._lastAddrSig = sig end end timer.Enabled = true return timer end -- =================================================================== -- Initialization -- =================================================================== if not AutoActivator.initialized then AutoActivator.initialized = true if AutoActivator.enableHotkey then local hotkeyObj = createHotkey(function() forceActivateScripts() end, AutoActivator.hotkey.keys) if hotkeyObj then AutoActivator:printInfo("Hotkey registered: Ctrl+Shift+A") else AutoActivator:printError("Hotkey registration failed") end end AutoActivator:registerEvents() local function rehook() AutoActivator:printDebug("Re-hooking manual load...") AutoActivator:hookManualLoad() end createTimer(100, rehook) createTimer(500, rehook) createTimer(1000, rehook) AutoActivator:startPolling() -- Start continuous polling createTimer(300, function() if getAddressList() and getAddressList().Count > 0 then AutoActivator:printInfo("Table already present on script load, scanning immediately") AutoActivator:start(true) else AutoActivator:printInfo("No table present on script load, waiting for events") end AutoActivator:addMenu() end) createTimer(2000, function() AutoActivator:addMenu() end) AutoActivator:printInfo("Script loaded (v1.2.1)") AutoActivator:printInfo("Log level: " .. AutoActivator.logLevel .. " (0=silent,1=errors,2=info,3=debug)") AutoActivator:printInfo("Global delay: " .. AutoActivator.delay .. "ms") AutoActivator:printInfo("Auto-rescan interval: " .. (AutoActivator.scanInterval > 0 and AutoActivator.scanInterval .. " seconds" or "disabled")) AutoActivator:printInfo("Parent-child dependency check: " .. (AutoActivator.requireParentActivated and "enabled" or "disabled")) AutoActivator:printInfo("Hotkey: " .. (AutoActivator.enableHotkey and "enabled" or "disabled")) AutoActivator:printInfo("Manual trigger: forceActivateScripts()") else AutoActivator:printInfo("Script already initialized, skipping duplicate load") end