-- ============================================================ -- recentFiles.lua (Rule Matching + Fuzzy Fallback) -- Ver 2.1.39 (Added ENABLE_AUTO_LOAD_DELAY switch) -- ============================================================ local recentFiles = {} -- ★ Cache variables local _cachedRawTitle = nil local _cachedCleanTitle = nil local _wordCache = setmetatable({}, {__mode = "k"}) -- ★ Process name index local _processIndex = nil -- -------------------- Configuration -------------------- recentFiles.FileName = 'recentFiles.txt' recentFiles.NumberOfEntries = 50000 recentFiles.IgnoreCETRAINER = false recentFiles.AUTO_LOAD_LATEST = true -- ★★★ Auto‑load on process attach (enabled by default) ★★★ recentFiles.ENABLE_AUTO_LOAD_ON_ATTACH = true -- ★★★ Enable delayed auto‑load (enabled by default) ★★★ -- Set to false to revert to 2.1.35 old behaviour: synchronous execution, no delay, -- but may override CE's own prompt about duplicate table loading. recentFiles.ENABLE_AUTO_LOAD_DELAY = false -- ★★★ Auto‑load delay in milliseconds ★★★ recentFiles.AUTO_LOAD_DELAY = 1100 -- User‑adjustable -- ★★★ Show overwrite/merge confirmation before loading (enabled by default) ★★★ -- Set to false to revert to 2.1.35 old behaviour: load without asking, overwrite directly recentFiles.ENABLE_LOAD_CONFIRMATION = false -- ★★★ Custom save filename feature (disabled by default) ★★★ recentFiles.ENABLE_CUSTOM_SAVE = false -- ★★★ Smart Save ★★★ recentFiles.SMART_SAVE = true -- ★★★ Title cache switch ★★★ recentFiles.ENABLE_TITLE_CACHE = true -- ★★★ Stop‑word filter switch ★★★ recentFiles.ENABLE_STOP_WORDS = true -- ★★★ Fuzzy matching parameters ★★★ recentFiles.FUZZY_MIN_WORDS = 2 recentFiles.FUZZY_THRESHOLD = 0.4 -- ★★★ CamelCase splitting ★★★ recentFiles.SPLIT_CAMEL_CASE = true -- ★★★ Stop‑word list (Chinese common words, can be customised) ★★★ recentFiles.STOP_WORDS = { "汉", "化", "版", -- Chinese localisation "简", "体", "中", "文", -- Simplified Chinese "修", "正", -- Revision "补", "丁", -- Patch "加", "强", -- Enhanced "完", "整", -- Complete "测", "试", -- Test "正", "式", -- Official "发", "布", -- Release } recentFiles.BLACKLIST = { --"melonDS.CT", --"NDS_银河漫步.CT", } -- Title preprocessing recentFiles.TITLE_CLEANUP = function(title) if not title then return "" end local lastPipe = title:match("^.*|%s*(.+)$") if lastPipe then title = lastPipe:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "") end title = title:gsub("%(%s*WinBuild%s*%.?%s*[%d%.]+%s*%)", "") title = title:gsub("[Vv]ersion%s*:?%s*[%d%.]+", "") title = title:gsub("[Bb]uild%s*:?%s*[%d%.]+", "") title = title:gsub("%(%s*%d+%.%d+%.%d+%.%d+%s*%)", "") title = title:gsub("[Vv]%s*%d+%.%d+%.?%d*%.?%d*", "") title = title:gsub("%s*%-%s*%d+%.%d+%.%d+[%w%.+%-]*", "") title = title:gsub("%s*%d+%.%d+%.%d+%.?%d*%s*", " ") title = title:gsub("%s*%d+%.%d+%.%d+[%-%+][%w%.+]+", "") title = title:gsub("%s*[Bb]uild%s*%d+", "") title = title:gsub("%s*[Rr]%s*%d+", "") title = title:gsub("%[%s*[Ee]mulation%s*%]", "") title = title:gsub("%[%s*[Pp]aused%s*%]", "") title = title:gsub("[%(%[【(]%s*已暂停%s*[%]%)】)]", "") title = title:gsub("%s*[Mm]em%s*:?%s*%d+%s*[Mm][Bb]%s*,?", "") title = title:gsub("%s*[Cc][Pp][Uu]%s*:?%s*%d+%%%s*,?", "") title = title:gsub("%(%s*%d+%.?%d*%s*[Ff][Pp][Ss]%s*%)", "") title = title:gsub("%(%s*%d+%.?%d*%s*/%s*%d+%.?%d*%s*%)", "") title = title:gsub("%[%s*%d+%.?%d*%s*[Ff][Pp][Ss]%s*%]", "") title = title:gsub("%[%s*%d+%.?%d*%s*/%s*%d+%.?%d*%s*%]", "") title = title:gsub("[Ff][Pp][Ss]%s*:?%s*%d+%.?%d*", "") title = title:gsub("帧率%s*:?%s*%d+%.?%d*", "") title = title:gsub("[Ff][Pp][Ss]%s*%d+%.?%d*", "") title = title:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "") return title end -- Process name normalisation recentFiles.PROCESS_NORMALIZE = function(processName) if not processName then return processName end local name = processName:gsub("%.exe$", "") name = name:gsub("[-_][0-9]+[%.]?[0-9]*$", "") if name == "" then return processName end return name end -- ------------------------------------------------- -- ★★★ safeLoadTable: supports ENABLE_LOAD_CONFIRMATION ★★★ local function safeLoadTable(path, forceMerge) if not path or not fileExists(path) then print("[recentFiles] File not found: " .. tostring(path)) return false, "file_not_found" end local merge = forceMerge or false if recentFiles.ENABLE_LOAD_CONFIRMATION then local hasTable = recentFiles.CurrentFilePath and fileExists(recentFiles.CurrentFilePath) local addressListCount = 0 local addrList = getAddressList() if addrList then addressListCount = addrList.Count end if hasTable or addressListCount > 0 then local msg = "A CT table is currently loaded.\nPlease choose an action:\n[Yes] Overwrite current table\n[No] Merge into current table\n[Cancel] Cancel loading" local choice = messageDialog(msg, mtConfirmation, mbYes, mbNo, mbCancel) if choice == mrYes then merge = false elseif choice == mrNo then merge = true else --print("[recentFiles] User cancelled loading: " .. path) return false, "cancelled" end end else merge = false end local success, err = pcall(function() loadTable(path, merge) end) if not success then print("[recentFiles] Failed to load CT table: " .. err) return false, "load_error" end return true, "ok" end -- ★★★ Improved process name retrieval ★★★ local function getCurrentProcessName() local rawName = process if not rawName or rawName == "" then local pid = getOpenedProcessID() if pid ~= 0 then local procs = getProcesslist() if procs and procs[pid] then rawName = procs[pid] end end if not rawName or rawName == "" then local caption = getMainForm().Caption if caption then rawName = caption:match("^Cheat Engine %- (.+)") end end end if rawName and recentFiles.PROCESS_NORMALIZE then return recentFiles.PROCESS_NORMALIZE(rawName) end return rawName end -- ★★★ Get the main window title of the attached process ★★★ local function getMainWindowTitle() local pid = getOpenedProcessID() if pid == 0 then return nil end local all = getWindowlist() if not all or not all[pid] then return nil end local processName = getCurrentProcessName() if not processName then return nil end local blacklist = { "Default IME", "MSCTFIME UI", "GDI+ Window", "DDE Window", "MCI","MSCTFIME", "__wglDummyWindowFodder", "wglDummy", "DummyWindow", "CTrayNotifyIcon Helper Window", "TrayNotifyIcon", "BluetoothNotificationAreaIconWindowClass", "NvContainerWindowClass", ".NET-BroadcastEventWindow","MediaContextNotificationWindow", "SystemResourceNotifyWindow","AppBarDetectFullScreen", "TRAYAGENTWINDOW", "UDiskVirtualWindow", "Window_tsvulfw_man_window_0","BroadcastListenerWindow","UxdService", "CalcMsgPumpWnd", "MS_WebcheckMonitor","DDE Server Window", "NVOGLDC invisible", "CiceroUIWndFrame", "QTrayIconMessageWindow", "WinEventWindow", "_WINDOWTOP_INTERNAL_UI_", "BroadcastListenerWindow", "com.pot-app.desktop-siw", "ms_sqlce_se_notify_wndproc", "Secondary Window", } local candidates = {} for _, win in ipairs(all[pid]) do local title if type(win) == "string" then title = win elseif type(win) == "table" then title = win[2] or "" else title = tostring(win) or "" end if title ~= "" then local isBlack = false for _, b in ipairs(blacklist) do if title:find(b, 1, true) then isBlack = true break end end if not isBlack then table.insert(candidates, title) end end end if #candidates == 0 then return nil end local processLower = processName:lower() local bestMatch = nil local bestLength = 0 for _, title in ipairs(candidates) do if title:lower():find(processLower, 1, true) then if #title > bestLength then bestLength = #title bestMatch = title end end end if bestMatch then return bestMatch end local best = candidates[1] for _, title in ipairs(candidates) do if #title > #best then best = title end end return best end -- ★★★ Preprocessed window title ★★★ local function getCurrentWindowTitle() local rawTitle = getMainWindowTitle() if not rawTitle then return "" end if recentFiles.ENABLE_TITLE_CACHE then if _cachedRawTitle == rawTitle then return _cachedCleanTitle or "" end end local cleanTitle = rawTitle if recentFiles.TITLE_CLEANUP then cleanTitle = recentFiles.TITLE_CLEANUP(rawTitle) end if recentFiles.ENABLE_TITLE_CACHE then _cachedRawTitle = rawTitle _cachedCleanTitle = cleanTitle end return cleanTitle end local function isBlacklisted(path) if not path then return false end local baseName = extractFileName(path) for _, pattern in ipairs(recentFiles.BLACKLIST) do if baseName == pattern then return true end end return false end -- ★★★ CamelCase splitting ★★★ local function splitCamelCase(str) local result = {} local len = #str local i = 1 while i <= len do local b = string.byte(str, i) if (b >= 65 and b <= 90) or (b >= 97 and b <= 122) or (b >= 48 and b <= 57) then local start = i while i <= len do local b2 = string.byte(str, i) if not ((b2 >= 65 and b2 <= 90) or (b2 >= 97 and b2 <= 122) or (b2 >= 48 and b2 <= 57)) then break end i = i + 1 end local block = string.sub(str, start, i - 1) local parts = {} for word in block:gmatch("%u?%l+") do table.insert(parts, word) end local remaining = block:gsub("%u?%l+", "") if remaining ~= "" then table.insert(parts, remaining) end if #parts > 0 then table.insert(result, table.concat(parts, " ")) else table.insert(result, block) end else local start = i i = i + 1 while i <= len do local b2 = string.byte(str, i) if (b2 >= 65 and b2 <= 90) or (b2 >= 97 and b2 <= 122) or (b2 >= 48 and b2 <= 57) then break end i = i + 1 end table.insert(result, string.sub(str, start, i - 1)) end end return table.concat(result, " ") end -- ★★★ Extract words ★★★ local function extractWords(str, splitCamel, filterStopWords) if not str or str == "" then return {} end if not recentFiles.ENABLE_STOP_WORDS then filterStopWords = false end local key = str .. (splitCamel and "_c" or "_n") .. (filterStopWords and "_f" or "_u") if _wordCache[key] then return _wordCache[key] end if splitCamel == nil then splitCamel = recentFiles.SPLIT_CAMEL_CASE or false end local processedStr = str if splitCamel then processedStr = splitCamelCase(str) end local words = {} local len = #processedStr local i = 1 while i <= len do local b = string.byte(processedStr, i) if (b >= 65 and b <= 90) or (b >= 97 and b <= 122) or (b >= 48 and b <= 57) then local start = i while i <= len do local b2 = string.byte(processedStr, i) if not ((b2 >= 65 and b2 <= 90) or (b2 >= 97 and b2 <= 122) or (b2 >= 48 and b2 <= 57)) then break end i = i + 1 end local word = string.sub(processedStr, start, i - 1) if #word > 0 then table.insert(words, word:lower()) end elseif b >= 224 and b <= 239 then local b2 = string.byte(processedStr, i+1) local b3 = string.byte(processedStr, i+2) if b2 and b3 and b2 >= 128 and b2 <= 191 and b3 >= 128 and b3 <= 191 then while i <= len do local cb = string.byte(processedStr, i) if not (cb >= 224 and cb <= 239 and string.byte(processedStr, i+1) and string.byte(processedStr, i+2) and string.byte(processedStr, i+1) >= 128 and string.byte(processedStr, i+1) <= 191 and string.byte(processedStr, i+2) >= 128 and string.byte(processedStr, i+2) <= 191) then break end local char = string.sub(processedStr, i, i+2) table.insert(words, char) i = i + 3 end else i = i + 1 end else i = i + 1 end end if filterStopWords and recentFiles.ENABLE_STOP_WORDS then local stopWords = recentFiles.STOP_WORDS or {} local filtered = {} for _, w in ipairs(words) do local isStop = false for _, sw in ipairs(stopWords) do if w == sw then isStop = true break end end if not isStop then table.insert(filtered, w) end end words = filtered end _wordCache[key] = words return words end -- ★★★ Fuzzy matching ★★★ local function getFuzzyMatch(entries, title) local useStopWords = recentFiles.ENABLE_STOP_WORDS local titleWords = extractWords(title, nil, useStopWords) if #titleWords == 0 then return nil end local filteredTitleWords = {} for _, w in ipairs(titleWords) do if not w:match("^%d+$") then table.insert(filteredTitleWords, w) end end if #filteredTitleWords == 0 then return nil end titleWords = filteredTitleWords local bestEntry = nil local bestScore = 0 for _, entry in ipairs(entries) do local filename = extractFileName(entry.path) local name = filename:gsub("%.ct$", "", 1) local nameWords = extractWords(name, nil, useStopWords) if #nameWords > 0 then local filteredNameWords = {} for _, w in ipairs(nameWords) do if not w:match("^%d+$") then table.insert(filteredNameWords, w) end end if #filteredNameWords > 0 then nameWords = filteredNameWords local matchCount = 0 for _, w in ipairs(nameWords) do for _, tw in ipairs(titleWords) do if w == tw then matchCount = matchCount + 1 break end end end if matchCount >= recentFiles.FUZZY_MIN_WORDS then local score = matchCount / math.max(#titleWords, #nameWords) if score > bestScore then bestScore = score bestEntry = entry end end end end end if bestEntry and bestScore >= recentFiles.FUZZY_THRESHOLD then return bestEntry end return nil end -- Data storage recentFiles.Entries = {} recentFiles.ClearMenuItem = nil recentFiles.SwitchMenuItem = nil recentFiles.CurrentFilePath = nil -- ★ Build process name index local function buildProcessIndex() _processIndex = {} for _, entry in ipairs(recentFiles.Entries) do local p = entry.process if not _processIndex[p] then _processIndex[p] = {} end table.insert(_processIndex[p], entry) end end function recentFiles.Load() recentFiles.Entries = {} local file = io.open(getCheatEngineDir() .. recentFiles.FileName, "r") local invalidCount = 0 if file then for line in file:lines() do local process, path, rule = line:match("^(.-)\t(.+)\t(.*)$") if process and path then rule = rule or "" if fileExists(path) then table.insert(recentFiles.Entries, {process = process, path = path, rule = rule}) else invalidCount = invalidCount + 1 end else process, path = line:match("^(.-)\t(.+)$") if process and path then if fileExists(path) then table.insert(recentFiles.Entries, {process = process, path = path, rule = ""}) else invalidCount = invalidCount + 1 end else invalidCount = invalidCount + 1 end end end file:close() end if invalidCount > 0 then recentFiles.Save() end if #recentFiles.Entries > recentFiles.NumberOfEntries then for i = recentFiles.NumberOfEntries + 1, #recentFiles.Entries do recentFiles.Entries[i] = nil end recentFiles.Save() end buildProcessIndex() end function recentFiles.Save() local file = io.open(getCheatEngineDir() .. recentFiles.FileName, "w") if not file then return end for _, entry in ipairs(recentFiles.Entries) do file:write(entry.process .. "\t" .. entry.path .. "\t" .. (entry.rule or "") .. "\n") end file:close() end function recentFiles.AddEntry(process, path, rule) rule = rule or "" if not process or not path then return end for i = #recentFiles.Entries, 1, -1 do local e = recentFiles.Entries[i] if e.process == process and e.path == path then if rule ~= "" then e.rule = rule end table.remove(recentFiles.Entries, i) break end end table.insert(recentFiles.Entries, 1, {process = process, path = path, rule = rule}) while #recentFiles.Entries > recentFiles.NumberOfEntries do table.remove(recentFiles.Entries) end recentFiles.Save() buildProcessIndex() end function recentFiles.ClearAll() recentFiles.Entries = {} recentFiles.Save() _processIndex = {} end -- ★★★ Index‑accelerated GetEntriesForProcess ★★★ function recentFiles.GetEntriesForProcess(process) recentFiles.Load() local result = {} local processLower = process:lower() local exactList = _processIndex[process] if exactList then for _, entry in ipairs(exactList) do if not isBlacklisted(entry.path) and fileExists(entry.path) then table.insert(result, entry) end end end if #result == 0 and _processIndex then for procName, entryList in pairs(_processIndex) do local entryLower = procName:lower() if entryLower:find(processLower, 1, true) or processLower:find(entryLower, 1, true) then for _, entry in ipairs(entryList) do if not isBlacklisted(entry.path) and fileExists(entry.path) then table.insert(result, entry) end end end end end return result end -- Rule matching local function matchByRule(entries, currentTitle) if currentTitle == "" then return nil end local lowerTitle = currentTitle:lower() for _, entry in ipairs(entries) do if entry.rule and entry.rule ~= "" then if lowerTitle:find(entry.rule:lower(), 1, true) then return entry end end end return nil end -- ★★★ Record CT table ★★★ local function onPostLoadTable(filename) if filename == '' or not fileExists(filename) then return end recentFiles.CurrentFilePath = filename if recentFiles.IgnoreCETRAINER and filename:sub(-3):upper() ~= '.CT' then return end local process = getCurrentProcessName() or "unknown" local existingRule = "" for _, e in ipairs(recentFiles.Entries) do if e.process == process and e.path == filename then existingRule = e.rule or "" break end end recentFiles.AddEntry(process, filename, existingRule) end -- Popup dialog local function showSelectionDialogAlways() local processName = getCurrentProcessName() if not processName then return end local entries = recentFiles.GetEntriesForProcess(processName) if #entries == 0 then return end local paths = {} local rules = {} for _, e in ipairs(entries) do table.insert(paths, e.path) table.insert(rules, e.rule or "") end local form = createForm(true) form.Caption = "Recent CT Tables (Process: " .. processName .. ")" form.setSize(610, 400) form.centerScreen() form.OnClose = function(sender) sender.destroy() end local listBox = createListBox(form) listBox.setSize(600, 300) listBox.setPosition(5, 50) listBox.MultiSelect = true local items = listBox.Items items.clear() for i, p in ipairs(paths) do local display = extractFileName(p) if rules[i] ~= "" then display = display .. " [Rule: " .. rules[i] .. "]" end items.add(display) end listBox.ItemIndex = 0 local updateButtons local function refreshList() items.clear() for i, p in ipairs(paths) do local display = extractFileName(p) if rules[i] ~= "" then display = display .. " [Rule: " .. rules[i] .. "]" end items.add(display) end if #paths > 0 then listBox.ItemIndex = 0 else form.hide() end if updateButtons then updateButtons() end end listBox.OnDblClick = function() local idx = listBox.ItemIndex if idx >= 0 and idx < #paths then local selectedPath = paths[idx + 1] if fileExists(selectedPath) then local ok, reason = safeLoadTable(selectedPath) if ok then onPostLoadTable(selectedPath) else if reason == "cancelled" then --print("[recentFiles] User cancelled loading") else --print("[recentFiles] Load failed: " .. reason) end end else showMessage("File not found: " .. selectedPath) end form.hide() end end listBox.OnSelectionChange = function() if updateButtons then updateButtons() end end local btnLoad = createButton(form) btnLoad.Caption = "Load" btnLoad.setSize(90, 34) btnLoad.setPosition(200, 358) btnLoad.OnClick = function() local idx = listBox.ItemIndex if idx >= 0 and idx < #paths then local selectedPath = paths[idx + 1] if fileExists(selectedPath) then local ok, reason = safeLoadTable(selectedPath) if ok then onPostLoadTable(selectedPath) --print("[recentFiles] Loaded " .. extractFileName(selectedPath)) else if reason == "cancelled" then --print("[recentFiles] User cancelled loading") else --print("[recentFiles] Load failed: " .. reason) end end else showMessage("File not found: " .. selectedPath) end end form.hide() end local btnSetRule = createButton(form) btnSetRule.Caption = "Set Auto‑Match Rule" btnSetRule.setSize(310, 34) btnSetRule.setPosition(7, 8) btnSetRule.Enabled = false btnSetRule.OnClick = function() local idx = listBox.ItemIndex if idx < 0 or idx >= #paths then showMessage("Please select a CT table record to set a rule for") return end local currentPath = paths[idx + 1] local currentRule = rules[idx + 1] or "" local defaultRule = getCurrentWindowTitle() if defaultRule == "" then defaultRule = "No window title detected" end if currentRule ~= "" then defaultRule = currentRule end local newRule = inputQuery("Set Rule", "Enter match rule (window title contains this text; leave empty for process‑only match):", defaultRule) if newRule == nil then return end newRule = newRule:gsub("^%s+", ""):gsub("%s+$", "") rules[idx + 1] = newRule for _, e in ipairs(recentFiles.Entries) do if e.path == currentPath then e.rule = newRule break end end recentFiles.Save() refreshList() print("[recentFiles] Set rule for " .. extractFileName(currentPath) .. ": " .. (newRule == "" and "(none)" or newRule)) end local btnDelete = createButton(form) btnDelete.Caption = "Delete Selected CT Record(s)" btnDelete.setSize(280, 34) btnDelete.setPosition(325, 8) btnDelete.Enabled = false btnDelete.OnClick = function() local selectedIndices = {} for i = 0, items.Count - 1 do if listBox.Selected[i] then table.insert(selectedIndices, i) end end if #selectedIndices == 0 then showMessage("Please select CT table record(s) to delete (multi‑select allowed)") return end if messageDialog("Are you sure you want to delete " .. #selectedIndices .. " selected record(s)?\n(This will NOT delete the CT files from disk)", mtConfirmation, mbYes, mbNo) ~= mrYes then return end local pathsToRemove = {} for _, idx in ipairs(selectedIndices) do table.insert(pathsToRemove, paths[idx + 1]) end for i = #recentFiles.Entries, 1, -1 do for _, p in ipairs(pathsToRemove) do if recentFiles.Entries[i].path == p then table.remove(recentFiles.Entries, i) break end end end recentFiles.Save() for i = #paths, 1, -1 do for _, p in ipairs(pathsToRemove) do if paths[i] == p then table.remove(paths, i) table.remove(rules, i) break end end end refreshList() end local btnCancel = createButton(form) btnCancel.Caption = "Cancel" btnCancel.setSize(90, 34) btnCancel.setPosition(320, 358) btnCancel.OnClick = function() form.hide() end updateButtons = function() local selectedCount = 0 for i = 0, items.Count - 1 do if listBox.Selected[i] then selectedCount = selectedCount + 1 end end btnSetRule.Enabled = (selectedCount == 1) btnDelete.Enabled = (selectedCount > 0) end updateButtons() form.showModal() end -- Auto‑load local function showRecentSelectionDialog() if not recentFiles.ENABLE_AUTO_LOAD_ON_ATTACH then return end local processName = getCurrentProcessName() if not processName then return end local entries = recentFiles.GetEntriesForProcess(processName) if #entries == 0 then return end if recentFiles.AUTO_LOAD_LATEST then local title = getCurrentWindowTitle() if title ~= "" then local matched = matchByRule(entries, title) if matched then local ok, reason = safeLoadTable(matched.path) if ok then onPostLoadTable(matched.path) print("[recentFiles] Rule match succeeded: " .. extractFileName(matched.path) .. " (Rule: " .. matched.rule .. ")") return else --print("[recentFiles] Rule match load cancelled or failed: " .. (reason or "unknown reason")) return end end local fuzzyMatched = getFuzzyMatch(entries, title) if fuzzyMatched then local ok, reason = safeLoadTable(fuzzyMatched.path) if ok then onPostLoadTable(fuzzyMatched.path) print("[recentFiles] Fuzzy match succeeded: " .. extractFileName(fuzzyMatched.path) .. " (based on filename‑title similarity)") return else --print("[recentFiles] Fuzzy match load cancelled or failed: " .. (reason or "unknown reason")) return end end end local latest = entries[1] if latest and fileExists(latest.path) then local ok, reason = safeLoadTable(latest.path) if ok then onPostLoadTable(latest.path) print("[recentFiles] Auto‑loaded latest (no rule/fuzzy match): " .. extractFileName(latest.path)) else --print("[recentFiles] Latest file load cancelled or failed: " .. (reason or "unknown reason")) end else --print("[recentFiles] Latest file does not exist: " .. tostring(latest and latest.path)) end return end showSelectionDialogAlways() end -- Menu creation function recentFiles.ShowMenu() if not recentFiles.SwitchMenuItem then local mainMenu = getMainForm().Menu if mainMenu then local switchItem = createMenuItem(mainMenu) switchItem.Caption = 'Switch CT' switchItem.OnClick = function() showSelectionDialogAlways() end local pos = mainMenu.Items.Count mainMenu.Items.insert(pos, switchItem) recentFiles.SwitchMenuItem = switchItem end end if recentFiles.ClearMenuItem then return end local FileMenuItem = getMainForm().Menu.Items[0] if not FileMenuItem then return end local position = FileMenuItem.Count local sep = createMenuItem(FileMenuItem) sep.Caption = '-' FileMenuItem.insert(position, sep) position = position + 1 local clearItem = createMenuItem(FileMenuItem) clearItem.Caption = 'Clear Recent File List' clearItem.OnClick = function() if messageDialog("Are you sure you want to clear all recent file records?", mtConfirmation, mbYes, mbNo) ~= mrYes then return end recentFiles.ClearAll() end FileMenuItem.insert(position, clearItem) recentFiles.ClearMenuItem = clearItem end -- ★ Reset title cache on process switch, and decide whether to delay execution local oldOnProcessOpened = MainForm.OnProcessOpened MainForm.OnProcessOpened = function(pid, handle, caption) _cachedRawTitle = nil _cachedCleanTitle = nil if oldOnProcessOpened then oldOnProcessOpened(pid, handle, caption) end if recentFiles.ENABLE_AUTO_LOAD_DELAY then -- Delayed execution local timer = createTimer(nil, false) timer.Interval = recentFiles.AUTO_LOAD_DELAY or 150 timer.OnTimer = function() timer.Enabled = false timer.destroy() showRecentSelectionDialog() end timer.Enabled = true else -- Synchronous execution (2.1.35 old behaviour) showRecentSelectionDialog() end end -- Custom open function local function openWithButtonOrMenu(sender) local dialog = getMainForm().OpenDialog1 if not dialog then print("[recentFiles] OpenDialog1 not found, cannot open file") return end if dialog:Execute() then local filename = dialog.FileName if filename ~= "" and fileExists(filename) then local ok, reason = safeLoadTable(filename) if ok then onPostLoadTable(filename) else if reason == "cancelled" then --print("[recentFiles] User cancelled loading") else print("[recentFiles] Failed to open file: " .. reason) end end end end end -- Replace CE's "Open" button and menu items getMainForm().LoadButton.OnClick = openWithButtonOrMenu getMainForm().Load1.OnClick = openWithButtonOrMenu getMainForm().LoadButton.Action = nil getMainForm().Load1.Action = nil -- ============================================================ -- ★★★ Custom Save ★★★ -- ============================================================ local function getSuggestedFileName() local title = getCurrentWindowTitle() if title == "" then local proc = getCurrentProcessName() if proc and proc ~= "" then return proc:gsub("[\\/:*?\"<>|]", "_") .. ".CT" else return "CheatTable.CT" end end local base = title:match("^.*[\\/]([^\\/]+)$") if base then title = base end local fileName = title:gsub("[\\/:*?\"<>|]", "_") .. ".CT" return fileName end -- ★★★ Smart save check ★★★ local function isCurrentFileMatchingTitle() local current = recentFiles.CurrentFilePath if not current or not fileExists(current) then return false end local title = getCurrentWindowTitle() if title == "" then return false end local name = extractFileName(current):gsub("%.ct$", "", 1) local useStopWords = recentFiles.ENABLE_STOP_WORDS local nameWords = extractWords(name, nil, useStopWords) if #nameWords == 0 then return false end local titleWords = extractWords(title, nil, useStopWords) local filteredTitle = {} for _, w in ipairs(titleWords) do if not w:match("^%d+$") then table.insert(filteredTitle, w) end end if #filteredTitle == 0 then return false end local matchCount = 0 for _, nw in ipairs(nameWords) do for _, tw in ipairs(filteredTitle) do if nw == tw then matchCount = matchCount + 1 break end end end if matchCount < recentFiles.FUZZY_MIN_WORDS then return false end local score = matchCount / math.max(#filteredTitle, #nameWords) return score >= recentFiles.FUZZY_THRESHOLD end local function customSave() local defaultPath = nil local defaultDir = nil local defaultName = nil if recentFiles.ENABLE_CUSTOM_SAVE then defaultName = getSuggestedFileName() elseif recentFiles.SMART_SAVE then if isCurrentFileMatchingTitle() then defaultPath = recentFiles.CurrentFilePath else defaultName = getSuggestedFileName() end else defaultPath = recentFiles.CurrentFilePath if not defaultPath or not fileExists(defaultPath) then defaultName = getSuggestedFileName() end end if defaultPath and fileExists(defaultPath) then defaultDir = extractFilePath(defaultPath) defaultName = extractFileName(defaultPath) elseif not defaultName then defaultName = getSuggestedFileName() end local saveDialog = createSaveDialog() saveDialog.Title = "Save Cheat Table" if defaultDir then saveDialog.InitialDir = defaultDir end if defaultName then saveDialog.FileName = defaultName end saveDialog.Filter = "Cheat Table (*.CT)|*.CT|All Files (*.*)|*.*" saveDialog.DefaultExt = "CT" if saveDialog.Execute() then local filePath = saveDialog.FileName if filePath and filePath ~= "" then if fileExists(filePath) then local answer = messageDialog(string.format("File %s already exists.\nDo you want to replace it?", filePath), mtConfirmation, mbYes, mbNo) if answer ~= mrYes then return end end local success = saveTable(filePath) if success then onPostLoadTable(filePath) else messageDialog(string.format("Save failed: %s\nPlease check the file path or permissions.", filePath), mtError, mbOk) end end end saveDialog.destroy() end local function replaceSaveControls() local mainForm = getMainForm() if not mainForm then return end local saveButton = mainForm.SaveButton if saveButton then saveButton.OnClick = customSave saveButton.Action = nil end local menuItems = { mainForm.miSaveFile, mainForm.Save1, mainForm.miSave, mainForm.SaveAs1, } for _, item in ipairs(menuItems) do if item then item.OnClick = customSave item.Action = nil end end local actSave = mainForm.actSave if actSave then actSave.OnExecute = customSave actSave.OnUpdate = nil end end replaceSaveControls() -- ============================================================ -- ★★★ Handle drag‑and‑drop of CT files ★★★ -- ============================================================ local function onFormDropFiles(sender, fileNames) if fileNames and #fileNames > 0 then local firstFile = fileNames[1] if firstFile and fileExists(firstFile) then onPostLoadTable(firstFile) end end end local mainForm = getMainForm() if mainForm then mainForm.AllowDropFiles = true local oldOnDropFiles = mainForm.OnDropFiles mainForm.OnDropFiles = function(sender, fileNames) if oldOnDropFiles then oldOnDropFiles(sender, fileNames) end onFormDropFiles(sender, fileNames) end end -- Initialisation recentFiles.Load() recentFiles.ShowMenu()