Corpo life has neutered my brain cells#

Looking from waybackmachine, last time I made a post on a website I used to own was around 2013. And damn was I busy creating stuff and having fun 😄

  1. Freelancer (cool space game by microsoft) - Teleporting, XY turn rate hack, speed hack during space travel, warp drive charge up speed
  2. Planetside 2 norecoil/spread & speedhack
  3. Far Cry speedhack (lmao I really dislike that franchise)
  4. Tom Glancy’s Ghost Recon Future - menu language switcher???
  5. Directx 9 Screenshot via hooking CreateOffScreenPlainSurface

I maybe have some of that stuff still available somewhere on my old drives. But anyway, I feel that I need to start having fun again. Working as a software engineer has definitely been rewarding in many aspects, though I feel that the initial reason why I started loving it has really diminished from my life. Corpo life most of the time is just a brain rot. The excitement I get from cracking something open with RE is different.

I still remember the fun I had when I was a teenager, I reverse engineered Quake 2 code for months without understanding anything about it - and then slowly starting to grasp at the straws. Watched Ollydbg tutorials on tuts4you. I think there was a woman named Lena who made really good video tutorials back in the day (I went and googled it now and seems like they are still being shared, awesome: link). That was some “underground” content back in the day 😄.

Not sure I really want to write that much code after work day is over but maybe reverse engineering can be made into a hobby. The past week on my vacation, I’ve been studying new tools (for me) like Binary Ninja and reversing Path of Exile 2. Actually had quite a bit of fun. Which made me think that perhaps kicking up a website for these projects is not that bad idea after all. Then maybe I have something to do behind a computer after I’ve put my kid to sleep.

These days access to information is also so much more readily available. 10 Years back you had script-kiddies gatekeeping methods for RE like they had invented a fusion reactor. These days people are trying to be influencers in all kinds of areas and even on reddit there are some threads popping to my feed how people are doing RE projects.

What I am currently working on#

Path of Exile 2 - 1/3 of trading bot complete#

Here’s a screenshot of Path of Exile 2 hook for chat. These addresses are for version 0.2.1b if you are a gourmet in the field - feel free digg in.

I am thinking of creating a trade bot and then maybe… sell it? lmao not gonna give it free my dude. Gotta afford diapers 😄.

Ghidra Extension Preview_Function_Capabilities ported and optimized to Java#

Also introduced myself to Ghidra. Back in the day I used IDA and never bothered to look at Ghidra. Cool tool but I am more inclined on purchasing a license for Binary Ninja. Allthough with this short encounter with Ghidra, I had the luxury of finding a plugin by AGDCservices called “Preview_Function_Capabilities”. When it had been analyzing Path of Exile executable for 4 hours, I interrupted the task process and ported that thing to Java. I don’t think I plan going back to ghidra so I am not gonna even create a git repository for it.

But anyway, this is not just a port. It’s actually quite big refactor from the original code. The Java plugin builds the full function call graph in a single pass and uses a worklist algorithm to propagate capabilities upward from leaf to root functions. This avoids the multiple nested loops in the Python version and ensures convergent propagation without redundant re-analysis.

IIRC it ran in 30~ secs to rename all methods (or maybe its not working at all 😄). I included some of my own capability detections also. Maybe I will come back to it later. So I am just gonna dump it here for the first bot that ends up reading this website.

Should be put into the same directory with other AGDCServices plugins.

//@author Miko (Original Python by AGDCServices) Refactored to Java
//@category AGDCservices
//@keybinding
//@menupath
//@toolbar
//@runtime Java

import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import ghidra.util.exception.DuplicateNameException;
import ghidra.util.exception.InvalidInputException;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class PreviewFunctionsV2 extends GhidraScript {
	private static final String GHIDRA_FUNC_PREFIX = "FUN_";
	private static final String CUSTOM_AUTO_FUNC_PREFIX = "f_p__";
	private static final String CUSTOM_AUTO_THREAD_FUNC_PREFIX = "f_p__TS__";
	private static final int OP_TYPE_PUSH_REGISTER = 512;
	private static final int OP_TYPE_CALL_STATIC_FUNCTION = 8256;
	private static final int OP_TYPE_CALL_DATA_VARIABLE = 8324;

	private Map<String, String> apiPurposeMap;
	private LinkedHashMap<String, List<Character>> categoryMap;
	private Pattern apiNamePattern;

	private static class FunctionInfo {
		Function function;
		int referenceCount;
		Set<String> directPurposes = new HashSet<>();
		Set<String> allPropagatedPurposes = new HashSet<>();
		Set<FunctionInfo> callers = new HashSet<>();
		Set<FunctionInfo> callees = new HashSet<>();

		FunctionInfo(Function f) {
			this.function = f;
		}

		@Override
		public int hashCode() {
			return function.getEntryPoint().hashCode();
		}

		@Override
		public boolean equals(Object o) {
			if (this == o)
				return true;
			if (o == null || getClass() != o.getClass())
				return false;
			return function.getEntryPoint().equals(((FunctionInfo) o).function.getEntryPoint());
		}
	}

	@Override
	public void run() throws Exception {
		initializeMaps();
		println("".repeat(100));
		println("Function_Preview Script Starting (Final Complete & Optimized Version)");

		renameThreadRoots();

		println("Phase 1: Analyzing functions and building call graph...");
		Map<Function, FunctionInfo> analysisMap = new HashMap<>();
		List<Function> funcList = getTargetFunctions();

		for (Function f : funcList) {
			analysisMap.put(f, new FunctionInfo(f));
		}

		for (FunctionInfo info : analysisMap.values()) {
			monitor.checkCancelled();
			Function f = info.function;
			info.referenceCount = getReferencesTo(f.getEntryPoint()).length;
			info.directPurposes = analyzeDirectCapabilities(f);

			for (Function callee : getCalledInternalFunctions(f)) {
				FunctionInfo calleeInfo = analysisMap.get(callee);
				if (calleeInfo != null) {
					info.callees.add(calleeInfo);
					calleeInfo.callers.add(info);
				}
			}
		}

		println("Phase 2: Propagating capabilities up the call graph...");
		Queue<FunctionInfo> worklist = analysisMap.values().stream().filter(info -> info.callees.isEmpty())
				.collect(Collectors.toCollection(LinkedList::new));

		Set<FunctionInfo> inWorklist = new HashSet<>(worklist);

		while (!worklist.isEmpty()) {
			monitor.checkCancelled();
			FunctionInfo info = worklist.poll();
			inWorklist.remove(info);

			Set<String> newPropagatedPurposes = new HashSet<>(info.directPurposes);
			for (FunctionInfo calleeInfo : info.callees) {
				newPropagatedPurposes.addAll(calleeInfo.allPropagatedPurposes);
			}

			if (!newPropagatedPurposes.equals(info.allPropagatedPurposes)) {
				info.allPropagatedPurposes = newPropagatedPurposes;
				for (FunctionInfo callerInfo : info.callers) {
					if (!inWorklist.contains(callerInfo)) {
						worklist.add(callerInfo);
						inWorklist.add(callerInfo);
					}
				}
			}
		}

		println("Phase 3: Renaming functions...");
		int renamedCount = 0;
		for (FunctionInfo info : analysisMap.values()) {
			monitor.checkCancelled();
			String oldName = info.function.getName();
			String newName = buildFinalName(info);
			if (!newName.equals(oldName)) {
				info.function.setName(newName, SourceType.USER_DEFINED);
				renamedCount++;
			}
		}

		println("Function_Preview Script Completed. Renamed " + renamedCount + " functions.");
		println("".repeat(100));
	}

	private Set<String> analyzeDirectCapabilities(Function func) {
		Set<String> usedApiBases = new HashSet<>();
		if (func.getBody() == null || func.getBody().isEmpty()) {
			return usedApiBases;
		}

		InstructionIterator iter = currentProgram.getListing().getInstructions(func.getBody(), true);

		while (iter.hasNext()) {
			Instruction instr = iter.next();
			if (!"call".equalsIgnoreCase(instr.getMnemonicString())) {
				continue;
			}

			String apiBaseName = null;

			// 1) Direct external API calls (DLLs, etc.)
			Reference extRef = instr.getExternalReference(0);
			if (extRef != null) {
				Symbol sym = getSymbolAt(extRef.getToAddress());
				apiBaseName = getBaseNameFromSymbol(sym);

				// 2) Static calls to other functions in the program
			} else if (instr.getOperandType(0) == OP_TYPE_CALL_STATIC_FUNCTION) {
				Reference[] opRefs = instr.getOperandReferences(0);
				if (opRefs.length > 0) {
					Function target = getFunctionAt(opRefs[0].getToAddress());
					if (target != null && !isAutoNamed(target.getName())) {
						Symbol sym = target.getSymbol();
						if (sym != null) {
							apiBaseName = getBaseNameFromSymbol(sym);
						}
					}
				}

				// 3) Indirect calls via function-pointer data variables
			} else if (instr.getOperandType(0) == OP_TYPE_CALL_DATA_VARIABLE) {
				Reference[] opRefs = instr.getOperandReferences(0);
				if (opRefs.length > 0) {
					Address dataAddr = opRefs[0].getToAddress();
					Data d = getDataAt(dataAddr);
					if (d == null) {
						d = getUndefinedDataAt(dataAddr);
					}
					if (d != null) {
						// 3a) First, look for any external references on the data value
						for (Reference valRef : d.getValueReferences()) {
							if (valRef.isExternalReference()) {
								Symbol extSym = getSymbolAt(valRef.getToAddress());
								apiBaseName = getBaseNameFromSymbol(extSym);
								break;
							}
						}
						// 3b) Fallback: use the primary symbol at the data’s address,
						// skipping generic "dat_*" labels
						if (apiBaseName == null) {
							Symbol sym = getSymbolAt(dataAddr);
							if (sym != null && !sym.getName().toLowerCase().startsWith("dat_")) {
								apiBaseName = getBaseNameFromSymbol(sym);
							}
						}
					}
				}
			}

			if (apiBaseName != null) {
				usedApiBases.add(apiBaseName);
			}
		}

		// Map the raw base-names into our netw/reg/file/etc. codes, dropping any nulls
		return usedApiBases.stream().map(apiPurposeMap::get).filter(Objects::nonNull).collect(Collectors.toSet());
	}

	private String buildFinalName(FunctionInfo info) {
		if (info.directPurposes.isEmpty() && info.callees.isEmpty()) {
			return CUSTOM_AUTO_FUNC_PREFIX + "zc_" + GHIDRA_FUNC_PREFIX + info.function.getEntryPoint().toString()
					+ "__xref_" + String.format("%02d", info.referenceCount);
		}

		Map<String, Set<Character>> parentPurposesByCategory = groupPurposes(info.directPurposes);
		Set<String> childOnlyPurposes = new HashSet<>(info.allPropagatedPurposes);
		childOnlyPurposes.removeAll(info.directPurposes);
		Map<String, Set<Character>> childPurposesByCategory = groupPurposes(childOnlyPurposes);

		StringBuilder nameBuilder = new StringBuilder();
		for (Map.Entry<String, List<Character>> category : categoryMap.entrySet()) {
			String cat = category.getKey();

			// Parent capabilities are uppercase
			String parentCaps = parentPurposesByCategory.getOrDefault(cat, Collections.emptySet()).stream()
					.map(c -> Character.toString(Character.toUpperCase(c))).sorted().collect(Collectors.joining());

			// Child-only capabilities are lowercase
			String childCaps = childPurposesByCategory.getOrDefault(cat, Collections.emptySet()).stream()
					.map(c -> Character.toString(Character.toLowerCase(c))).sorted().collect(Collectors.joining());

			if (!parentCaps.isEmpty() || !childCaps.isEmpty()) {
				nameBuilder.append(cat);
				if (!parentCaps.isEmpty())
					nameBuilder.append('_').append(parentCaps);
				if (!childCaps.isEmpty())
					nameBuilder.append('_').append(childCaps);
				nameBuilder.append("__");
			}
		}

		if (nameBuilder.length() > 0) {
			return CUSTOM_AUTO_FUNC_PREFIX + nameBuilder + "xref_" + String.format("%02d", info.referenceCount) + "_"
					+ info.function.getEntryPoint().toString();
		} else {
			return CUSTOM_AUTO_FUNC_PREFIX + GHIDRA_FUNC_PREFIX + info.function.getEntryPoint().toString() + "__xref_"
					+ String.format("%02d", info.referenceCount);
		}
	}


	private void initializeMaps() {
		apiPurposeMap = new HashMap<>();
		// --- Network (netw) ---
	    apiPurposeMap.put("socket", "netwB");        // Bind/listen/connect related
	    apiPurposeMap.put("bind", "netwL");
	    apiPurposeMap.put("listen", "netwL");
	    apiPurposeMap.put("accept", "netwL");
	    apiPurposeMap.put("connect", "netwC");       // Connection establishment
	    apiPurposeMap.put("InternetConnect", "netwC");
	    apiPurposeMap.put("InternetOpen", "netwC");
	    apiPurposeMap.put("InternetOpenURL", "netwC");
	    apiPurposeMap.put("HttpOpenRequest", "netwC");
	    apiPurposeMap.put("WinHttpConnect", "netwC");
	    apiPurposeMap.put("WinHttpOpenRequest", "netwC");
	    apiPurposeMap.put("send", "netwS");          // Sending data
	    apiPurposeMap.put("sendto", "netwS");
	    apiPurposeMap.put("WSASend", "netwS");
	    apiPurposeMap.put("WSASendTo", "netwS");
	    apiPurposeMap.put("InternetWriteFile", "netwS");
	    apiPurposeMap.put("HttpSendRequest", "netwS");
	    apiPurposeMap.put("WinHttpSendRequest", "netwS");
	    apiPurposeMap.put("WinHttpWriteData", "netwS");
	    apiPurposeMap.put("recv", "netwR");          // Receiving data
	    apiPurposeMap.put("recvfrom", "netwR");
	    apiPurposeMap.put("WSARecv", "netwR");
	    apiPurposeMap.put("WSARecvFrom", "netwR");
	    apiPurposeMap.put("InternetReadFile", "netwR");
	    apiPurposeMap.put("HttpReceiveHttpRequest", "netwR");
	    apiPurposeMap.put("WinHttpReceiveResponse", "netwR");
	    apiPurposeMap.put("WinHttpReadData", "netwR");
	    apiPurposeMap.put("URLDownloadToFile", "netwR");
	    apiPurposeMap.put("inet_addr", "netwM");     // Misc net: address/byte-order
	    apiPurposeMap.put("htons", "netwM");
	    apiPurposeMap.put("htonl", "netwM");
	    apiPurposeMap.put("ntohs", "netwM");
	    apiPurposeMap.put("ntohl", "netwM");

	    // --- Registry (reg) ---
	    apiPurposeMap.put("RegOpenKey", "regH");     // Handle (open/create) registry
	    apiPurposeMap.put("RegCreateKey", "regC");
	    apiPurposeMap.put("RegQueryValue", "regR");  // Read registry
	    apiPurposeMap.put("RegGetValue", "regR");
	    apiPurposeMap.put("RegEnumValue", "regR");
	    apiPurposeMap.put("RegSetValue", "regW");    // Write registry
	    apiPurposeMap.put("RegSetKeyValue", "regW");
	    apiPurposeMap.put("RegDeleteValue", "regD"); // Delete registry entries
	    apiPurposeMap.put("RegDeleteKey", "regD");
	    apiPurposeMap.put("RegDeleteKeyValue", "regD");

	    // --- File (file) ---
	    apiPurposeMap.put("CreateFile", "fileH");    // Open file or get handle
	    apiPurposeMap.put("fopen", "fileH");
	    apiPurposeMap.put("FindFirstFile", "fileE"); // Enumerate files
	    apiPurposeMap.put("FindNextFile", "fileE");
	    apiPurposeMap.put("ReadFile", "fileR");      // Read from file
	    apiPurposeMap.put("fread", "fileR");
	    apiPurposeMap.put("fgets", "fileR");
	    apiPurposeMap.put("fscanf", "fileR");
	    apiPurposeMap.put("WriteFile", "fileW");     // Write to file
	    apiPurposeMap.put("fwrite", "fileW");
	    apiPurposeMap.put("fprintf", "fileW");
	    apiPurposeMap.put("flushfilebuffers", "fileW");
	    apiPurposeMap.put("DeleteFile", "fileD");    // Delete file
	    apiPurposeMap.put("CopyFile", "fileC");      // Copy file
	    apiPurposeMap.put("MoveFile", "fileM");      // Move/rename file

	    // --- Process (proc) ---
	    apiPurposeMap.put("CreateToolhelp32Snapshot", "procE"); // Enumerate processes
	    apiPurposeMap.put("Process32First", "procE");
	    apiPurposeMap.put("Process32Next", "procE");
	    apiPurposeMap.put("OpenProcess", "procH");   // Get process handle
	    apiPurposeMap.put("CreateProcess", "procC"); // Create new process
	    apiPurposeMap.put("CreateProcessAsUser", "procC");
	    apiPurposeMap.put("CreateProcessWithLogon", "procC");
	    apiPurposeMap.put("CreateProcessWithToken", "procC");
	    apiPurposeMap.put("ShellExecute", "procC");  // Launch via shell
	    apiPurposeMap.put("TerminateProcess", "procT"); // Terminate a process
	    apiPurposeMap.put("ReadProcessMemory", "procR");   // Read another process memory
	    apiPurposeMap.put("WriteProcessMemory", "procW");  // Write to another process memory
	    apiPurposeMap.put("CreateRemoteThread", "procW");   // (Considered in injection context)

	    // --- Service (serv) ---
	    apiPurposeMap.put("OpenService", "servH");   // Handle to service
	    apiPurposeMap.put("CreateService", "servC"); // Create a new service
	    apiPurposeMap.put("StartService", "servS");  // Start service
	    apiPurposeMap.put("QueryServiceStatus", "servR"); // Read service status/config
	    apiPurposeMap.put("QueryServiceConfig", "servR");
	    apiPurposeMap.put("ChangeServiceConfig", "servW"); // Modify service
	    apiPurposeMap.put("ChangeServiceConfig2", "servW");
	    apiPurposeMap.put("DeleteService", "servD"); // Delete service

	    // --- Thread (thread) ---
	    apiPurposeMap.put("CreateThread", "threadC");    // Create new thread
	    apiPurposeMap.put("_beginthread", "threadC");
	    apiPurposeMap.put("_beginthreadex", "threadC");
	    apiPurposeMap.put("OpenThread", "threadO");      // Open existing thread handle
	    apiPurposeMap.put("SuspendThread", "threadS");   // Suspend/resume threads
	    apiPurposeMap.put("ResumeThread", "threadR");
	    apiPurposeMap.put("TerminateThread", "threadS"); // Terminate thread (if present)
	    // (Thread start functions are handled separately as thread roots)

	    // --- String (str) ---
	    apiPurposeMap.put("strcmp", "strC");         // String compare (case-sensitive)
	    apiPurposeMap.put("strncmp", "strC");
	    apiPurposeMap.put("stricmp", "strC");        // String compare (case-insensitive)
	    apiPurposeMap.put("wcsicmp", "strC");
	    apiPurposeMap.put("mbsicmp", "strC");
	    apiPurposeMap.put("lstrcmp", "strC");
	    apiPurposeMap.put("lstrcmpi", "strC");
	    
	    // =================================================================
	    // =========== NEW: GAME-RELATED API ADDITIONS =====================
	    // =================================================================

	    // --- Graphics (gfx) - DirectX/Vulkan/OpenGL rendering ---
	    // (I) Initialize graphics devices/contexts
	    apiPurposeMap.put("Direct3DCreate9", "gfxI");
	    apiPurposeMap.put("Direct3DCreate9Ex", "gfxI");
	    apiPurposeMap.put("D3D11CreateDevice", "gfxI");
	    apiPurposeMap.put("D3D11CreateDeviceAndSwapChain", "gfxI");
	    apiPurposeMap.put("D3D12CreateDevice", "gfxI");
	    apiPurposeMap.put("vkCreateInstance", "gfxI");
	    apiPurposeMap.put("vkCreateDevice", "gfxI");
	    apiPurposeMap.put("wglCreateContext", "gfxI");
	    apiPurposeMap.put("glXCreateContext", "gfxI");
	    apiPurposeMap.put("eglInitialize", "gfxI");
	    // (D) Draw calls
	    apiPurposeMap.put("Draw", "gfxD");
	    apiPurposeMap.put("DrawIndexed", "gfxD");
	    apiPurposeMap.put("DrawInstanced", "gfxD");
	    apiPurposeMap.put("DrawIndexedInstanced", "gfxD");
	    apiPurposeMap.put("vkCmdDraw", "gfxD");
	    apiPurposeMap.put("vkCmdDrawIndexed", "gfxD");
	    apiPurposeMap.put("glDrawArrays", "gfxD");
	    apiPurposeMap.put("glDrawElements", "gfxD");
	    apiPurposeMap.put("glDrawArraysInstanced", "gfxD");
	    apiPurposeMap.put("glDrawElementsInstanced", "gfxD");
	    // (P) Present/Swap
	    apiPurposeMap.put("Present", "gfxP");        // DXGI SwapChain Present (COM method call via vtable)
	    apiPurposeMap.put("IDXGISwapChain.Present", "gfxP"); // (if symbol includes interface name)
	    apiPurposeMap.put("SwapBuffers", "gfxP");    // OpenGL buffer swap
	    apiPurposeMap.put("eglSwapBuffers", "gfxP");
	    apiPurposeMap.put("vkQueuePresentKHR", "gfxP");
	    // (S) State/Shader setup
	    apiPurposeMap.put("CreateVertexShader", "gfxS");
	    apiPurposeMap.put("CreatePixelShader", "gfxS");
	    apiPurposeMap.put("CreateComputeShader", "gfxS");
	    apiPurposeMap.put("VSSetShader", "gfxS");
	    apiPurposeMap.put("PSSetShader", "gfxS");
	    apiPurposeMap.put("IASetInputLayout", "gfxS");
	    apiPurposeMap.put("OMSetRenderTargets", "gfxS");
	    apiPurposeMap.put("RSSetViewports", "gfxS");
	    apiPurposeMap.put("glCreateShader", "gfxS");
	    apiPurposeMap.put("glShaderSource", "gfxS");
	    apiPurposeMap.put("glCompileShader", "gfxS");
	    apiPurposeMap.put("glLinkProgram", "gfxS");
	    apiPurposeMap.put("glUseProgram", "gfxS");
	    // (T) Texture/Buffer resources
	    apiPurposeMap.put("CreateTexture", "gfxT");      // Generic, covers D3D variants
	    apiPurposeMap.put("CreateTexture1D", "gfxT");
	    apiPurposeMap.put("CreateTexture2D", "gfxT");
	    apiPurposeMap.put("CreateTexture3D", "gfxT");
	    apiPurposeMap.put("CreateBuffer", "gfxT");
	    apiPurposeMap.put("Map", "gfxT");    // ID3D11Resource::Map
	    apiPurposeMap.put("Unmap", "gfxT");
	    apiPurposeMap.put("glGenTextures", "gfxT");
	    apiPurposeMap.put("glBindTexture", "gfxT");
	    apiPurposeMap.put("glBufferData", "gfxT");
	    apiPurposeMap.put("glTexImage2D", "gfxT");

	    // --- Input (input) - Keyboard/Mouse/Controller ---
	    // (K) Keyboard
	    apiPurposeMap.put("GetKeyboardState", "inputK");
	    apiPurposeMap.put("GetAsyncKeyState", "inputK");
	    apiPurposeMap.put("GetKeyState", "inputK");
	    apiPurposeMap.put("MapVirtualKey", "inputK");
	    apiPurposeMap.put("RegisterHotKey", "inputK");
	    // (M) Mouse
	    apiPurposeMap.put("GetCursorPos", "inputM");
	    apiPurposeMap.put("SetCursorPos", "inputM");
	    apiPurposeMap.put("ShowCursor", "inputM");
	    apiPurposeMap.put("GetRawInputData", "inputM");
	    apiPurposeMap.put("GetRawInputBuffer", "inputM");
	    // (C) Controller (Gamepad)
	    apiPurposeMap.put("XInputGetState", "inputC");
	    apiPurposeMap.put("XInputSetState", "inputC");
	    apiPurposeMap.put("XInputGetCapabilities", "inputC");
	    apiPurposeMap.put("DirectInputCreate", "inputC");
	    apiPurposeMap.put("DirectInput8Create", "inputC");

	    // --- Audio (audio) - Init/Playback ---
	    // (I) Initialize audio engines
	    apiPurposeMap.put("DirectSoundCreate", "audioI");
	    apiPurposeMap.put("DirectSoundCreate8", "audioI");
	    apiPurposeMap.put("XAudio2Create", "audioI");
	    apiPurposeMap.put("waveOutOpen", "audioI");
	    apiPurposeMap.put("alcOpenDevice", "audioI");    // OpenAL device
	    apiPurposeMap.put("alcCreateContext", "audioI");
	    apiPurposeMap.put("BASS_Init", "audioI");        // Example: BASS audio library
	    apiPurposeMap.put("FMOD_System_Create", "audioI");
	    apiPurposeMap.put("FMOD_System_Init", "audioI");
	    // (P) Play or produce sound
	    apiPurposeMap.put("CreateSoundBuffer", "audioP");
	    apiPurposeMap.put("CreatePrimaryBuffer", "audioP");
	    apiPurposeMap.put("CreateSourceVoice", "audioP");
	    apiPurposeMap.put("CreateMasteringVoice", "audioP");
	    apiPurposeMap.put("PlaySound", "audioP");
	    apiPurposeMap.put("Play", "audioP");             // IDirectSoundBuffer::Play
	    apiPurposeMap.put("waveOutWrite", "audioP");
	    apiPurposeMap.put("alGenBuffers", "audioP");     // OpenAL generate buffer
	    apiPurposeMap.put("alBufferData", "audioP");     // fill buffer with audio data
	    apiPurposeMap.put("alSourcePlay", "audioP");
	    apiPurposeMap.put("BASS_StreamCreateFile", "audioP");
	    apiPurposeMap.put("BASS_ChannelPlay", "audioP");
	    apiPurposeMap.put("FMOD_System_CreateSound", "audioP");
	    apiPurposeMap.put("FMOD_System_PlaySound", "audioP");

	    // --- Memory (mem) - Allocation/Protection/Free ---
	    // (A) Allocate
	    apiPurposeMap.put("VirtualAlloc", "memA");
	    apiPurposeMap.put("VirtualAllocEx", "memA");
	    apiPurposeMap.put("HeapAlloc", "memA");
	    apiPurposeMap.put("HeapReAlloc", "memA");
	    apiPurposeMap.put("GlobalAlloc", "memA");
	    apiPurposeMap.put("LocalAlloc", "memA");
	    apiPurposeMap.put("malloc", "memA");
	    apiPurposeMap.put("calloc", "memA");
	    apiPurposeMap.put("mmap", "memA");
	    // (P) Protect
	    apiPurposeMap.put("VirtualProtect", "memP");
	    apiPurposeMap.put("VirtualProtectEx", "memP");
	    apiPurposeMap.put("VirtualLock", "memP");
	    apiPurposeMap.put("VirtualUnlock", "memP");
	    apiPurposeMap.put("mprotect", "memP");
	    // (F) Free
	    apiPurposeMap.put("VirtualFree", "memF");
	    apiPurposeMap.put("VirtualFreeEx", "memF");
	    apiPurposeMap.put("HeapFree", "memF");
	    apiPurposeMap.put("GlobalFree", "memF");
	    apiPurposeMap.put("LocalFree", "memF");
	    apiPurposeMap.put("free", "memF");
	    apiPurposeMap.put("munmap", "memF");

	    // --- Steamworks (steam) - Steam API integration ---
	    // (I) Initialize/Shutdown
	    apiPurposeMap.put("SteamAPI_Init", "steamI");
	    apiPurposeMap.put("SteamAPI_RestartAppIfNecessary", "steamI");
	    apiPurposeMap.put("SteamAPI_Shutdown", "steamI");
	    apiPurposeMap.put("SteamAPI_RunCallbacks", "steamI");
	    // (A) Achievements/Stats
	    apiPurposeMap.put("SteamAPI_ISteamUserStats_SetAchievement", "steamA");
	    apiPurposeMap.put("SteamAPI_ISteamUserStats_StoreStats", "steamA");
	    apiPurposeMap.put("SteamAPI_ISteamUserStats_IndicateAchievementProgress", "steamA");
	    apiPurposeMap.put("SteamAPI_ISteamUserStats_GetAchievement", "steamA");
	    // (N) Networking/Multiplayer
	    apiPurposeMap.put("SteamAPI_ISteamNetworking_SendP2PPacket", "steamN");
	    apiPurposeMap.put("SteamAPI_ISteamNetworkingSockets_SendMessageToPeer", "steamN");
	    apiPurposeMap.put("SteamAPI_ISteamMatchmaking_CreateLobby", "steamN");
	    apiPurposeMap.put("SteamAPI_ISteamMatchmaking_JoinLobby", "steamN");
	    apiPurposeMap.put("SteamAPI_ISteamMatchmaking_SetLobbyData", "steamN");

	    // --- Xbox Live (xbl) - Xbox/Microsoft Game Services ---
	    // (I) Initialize
	    apiPurposeMap.put("XblInitialize", "xblI");
	    apiPurposeMap.put("XblCleanup", "xblI");
	    apiPurposeMap.put("XGameRuntimeInitialize", "xblI"); // initialize Game runtime (if applicable)
	    // (U) User Login/Token
	    apiPurposeMap.put("XUserAdd", "xblU");
	    apiPurposeMap.put("XUserAddAsync", "xblU");
	    apiPurposeMap.put("XUserGetTokenAndSignatureAsync", "xblU");
	    apiPurposeMap.put("XUserGetTokenAndSignatureResult", "xblU");
	    apiPurposeMap.put("XUserSignOut", "xblU");
	    // (A) Achievements/Stats
	    apiPurposeMap.put("XblAchievementsWriteAchievementAsync", "xblA");
	    apiPurposeMap.put("XblAchievementsGetAllForTitleIdAsync", "xblA");
	    apiPurposeMap.put("XblPresenceSetPresenceAsync", "xblA"); // e.g., setting rich presence
	    // (Note: additional Xbl* calls for leaderboard or stats could be added similarly)

	    // --- Dynamic Linking (dyn) - Load libraries and resolve symbols ---
	    // (L) Load Library
	    apiPurposeMap.put("LoadLibrary", "dynL");
	    apiPurposeMap.put("LoadLibraryA", "dynL");
	    apiPurposeMap.put("LoadLibraryW", "dynL");
	    apiPurposeMap.put("LoadLibraryEx", "dynL");
	    apiPurposeMap.put("GetModuleHandle", "dynL");    // Getting module handle (if not already loaded)
	    apiPurposeMap.put("dlopen", "dynL");
	    // (G) Get Proc Address
	    apiPurposeMap.put("GetProcAddress", "dynG");
	    apiPurposeMap.put("dlsym", "dynG");
	    // (F) Free Library
	    apiPurposeMap.put("FreeLibrary", "dynF");
	    apiPurposeMap.put("FreeLibraryAndExitThread", "dynF");
	    apiPurposeMap.put("dlclose", "dynF");

	    // --- Scripting (scr) - Embedded scripting engines ---
	    // (P) Python
	    apiPurposeMap.put("Py_Initialize", "scrP");
	    apiPurposeMap.put("Py_InitializeEx", "scrP");
	    apiPurposeMap.put("Py_Finalize", "scrP");
	    apiPurposeMap.put("PyRun_SimpleString", "scrP");
	    apiPurposeMap.put("PyRun_String", "scrP");
	    apiPurposeMap.put("PyImport_Import", "scrP");
	    // (L) Lua
	    apiPurposeMap.put("luaL_newstate", "scrL");
	    apiPurposeMap.put("luaL_openlibs", "scrL");
	    apiPurposeMap.put("luaL_loadstring", "scrL");
	    apiPurposeMap.put("lua_pcall", "scrL");
	    apiPurposeMap.put("lua_close", "scrL");
	    // (J) JavaScript (e.g., V8 or other engines)
	    apiPurposeMap.put("v8::Isolate::New", "scrJ");  // (if C++ symbol names are present, e.g., V8 engine)
	    apiPurposeMap.put("JS_NewRuntime", "scrJ");     // QuickJS initialization
	    apiPurposeMap.put("JS_Eval", "scrJ");

		categoryMap = new LinkedHashMap<>();
	    categoryMap.put("netw", Arrays.asList('b', 'c', 'l', 's', 'r', 'm'));          // network: Bind/Connect, Listen, Send, Receive, Misc
	    categoryMap.put("reg", Arrays.asList('h', 'c', 'r', 'w', 'd'));                // registry: Handle, Create, Read, Write, Delete
	    categoryMap.put("file", Arrays.asList('h', 'e', 'r', 'w', 'd', 'c', 'm'));     // file: Handle, Enum, Read, Write, Delete, Copy, Move
	    categoryMap.put("proc", Arrays.asList('h', 'e', 'c', 't', 'r', 'w'));          // process: Handle, Enum, Create, Terminate, ReadMem, WriteMem
	    categoryMap.put("serv", Arrays.asList('h', 'c', 's', 'r', 'w', 'd'));          // service: Handle, Create, Start, Read, Write, Delete
	    categoryMap.put("thread", Arrays.asList('c', 'o', 's', 'r'));                  // thread: Create, Open, Suspend (or terminate), Resume
	    categoryMap.put("str", Arrays.asList('c'));                                    // string: Compare (could extend if needed)
		
		// New categories below:
	    categoryMap.put("gfx", Arrays.asList('i', 'd', 'p', 's', 't'));                // graphics: Init, Draw, Present, State, Texture
	    categoryMap.put("input", Arrays.asList('k', 'm', 'c'));                        // input: Keyboard, Mouse, Controller
	    categoryMap.put("audio", Arrays.asList('i', 'p'));                             // audio: Init, Play
	    categoryMap.put("mem", Arrays.asList('a', 'p', 'f'));                          // memory: Alloc, Protect, Free
	    categoryMap.put("steam", Arrays.asList('i', 'a', 'n'));                        // steam: Init, Achievements, Network
	    categoryMap.put("xbl", Arrays.asList('i', 'u', 'a'));                          // xbox live: Init, User, Achievements
	    categoryMap.put("dyn", Arrays.asList('l', 'g', 'f'));                          // dynamic library: Load, GetProc, Free
	    categoryMap.put("scr", Arrays.asList('p', 'l', 'j'));                          // scripting: Python, Lua, (Java)Script

		apiNamePattern = Pattern.compile("^(?:FID_conflict:)?(?:_)*(?<base>.+?)(?:A|W|Ex|ExA|ExW)?(?:@[0-9A-Fa-f]+)?$");
	}

	// --- CANCER METHODS ---

	private String getBaseNameFromSymbol(Symbol symbol) {
		if (symbol == null)
			return null;
		Matcher m = apiNamePattern.matcher(symbol.getName());
		return m.matches() ? m.group("base") : null;
	}

	private boolean isAutoNamed(String name) {
		return name.startsWith(GHIDRA_FUNC_PREFIX) || name.startsWith(CUSTOM_AUTO_FUNC_PREFIX) || name.startsWith(CUSTOM_AUTO_THREAD_FUNC_PREFIX);
	}

	private Map<String, Set<Character>> groupPurposes(Set<String> purposes) {
		Map<String, Set<Character>> grouped = new HashMap<>();
		if (purposes == null)
			return grouped;
		for (String purpose : purposes) {
			if (purpose == null || purpose.length() < 2)
				continue;
			String category = getCategory(purpose);
			char capability = getCapability(purpose);
			if (categoryMap.containsKey(category)) {
				grouped.computeIfAbsent(category, k -> new HashSet<>()).add(capability);
			}
		}
		return grouped;
	}

	private String getCategory(String purpose) {
		return purpose.substring(0, purpose.length() - 1).toLowerCase();
	}
	
	private char getCapability(String purpose) {
		return Character.toLowerCase(purpose.charAt(purpose.length() - 1));
	}

	private List<Function> getTargetFunctions() {
		List<Function> funcList = new ArrayList<>();
		FunctionIterator funcs = currentProgram.getListing().getFunctions(true);
		while (funcs.hasNext()) {
			Function f = funcs.next();
			String name = f.getName();
			if ((name.startsWith(GHIDRA_FUNC_PREFIX) || name.startsWith(CUSTOM_AUTO_FUNC_PREFIX))
					&& !name.startsWith(CUSTOM_AUTO_THREAD_FUNC_PREFIX) && !f.isThunk()) {
				funcList.add(f);
			}
		}
		return funcList;
	}

	private Set<Function> getCalledInternalFunctions(Function f) {
		Set<Function> called = new HashSet<>();
		if (f.getBody() == null || f.getBody().isEmpty()) {
			return called;
		}
		InstructionIterator iter = currentProgram.getListing().getInstructions(f.getBody(), true);
		while (iter.hasNext()) {
			Instruction instr = iter.next();
			if ("call".equalsIgnoreCase(instr.getMnemonicString())) {
				Reference[] opRefs = instr.getOperandReferences(0);
				if (opRefs.length > 0) {
					Function targetFunc = getFunctionAt(opRefs[0].getToAddress());
					if (targetFunc != null && !targetFunc.isThunk() && !targetFunc.isExternal()) {
						called.add(targetFunc);
					}
				}
			}
		}
		return called;
	}

	private void renameThreadRoots() throws DuplicateNameException, InvalidInputException {
		Set<Address> threadRoots = getThreadRoots();
		for (Address rootEa : threadRoots) {
			String newFuncName = CUSTOM_AUTO_THREAD_FUNC_PREFIX + GHIDRA_FUNC_PREFIX + rootEa.toString();
			Function func = getFunctionAt(rootEa);
			if (func == null) {
				createFunction(rootEa, newFuncName);
			} else {
				func.setName(newFuncName, SourceType.USER_DEFINED);
			}
		}
	}

	/**
	 * Finds the entry‐point addresses of all thread‐start routines invoked via
	 * CreateThread, _beginthread, etc.
	 */
	private Set<Address> getThreadRoots() {
		String[] threadFuncs = { "CreateThread", "_beginthreadex", "__beginthreadex", "_beginthread", "__beginthread" };
		Set<Address> roots = new HashSet<>();

		for (String fn : threadFuncs) {
			// 1) Try to resolve as an external import:
			Symbol threadApiSym = currentProgram.getSymbolTable().getExternalSymbol(fn);

			// 2) Fallback to any global label/function with that name:
			if (threadApiSym == null) {
				List<Symbol> globals = currentProgram.getSymbolTable().getSymbols(fn, null);
				if (globals.isEmpty()) {
					continue;
				}
				threadApiSym = globals.get(0);
			}

			// Walk all call‐site references to that API
			for (Reference ref : getReferencesTo(threadApiSym.getAddress())) {
				if (!ref.isOperandReference() || !ref.getReferenceType().isCall()) {
					continue;
				}
				Instruction callSite = getInstructionAt(ref.getFromAddress());
				if (callSite == null) {
					continue;
				}

				// Python did: argIndex = (lstrip(_) == "beginthread") ? 1 : 3
				int pushCount = fn.replace("_", "").equals("beginthread") ? 1 : 3;
				Instruction pushIns = getPrevTargetInstruction(callSite, "push", pushCount, 15);
				if (pushIns == null) {
					continue;
				}

				// Extract the address they passed as the thread entry point
				Address entry = extractThreadEntryAddress(pushIns);
				if (entry != null && getFunctionContaining(entry) != null) {
					roots.add(entry);
				}
			}
		}

		return roots;
	}

	/**
	 * Given a 'push' instruction that pushed either a register or an immediate,
	 * chase back to the actual entry‐point address.
	 */
	private Address extractThreadEntryAddress(Instruction pushInstr) {
		// Case A: they pushed a register -> look back for the mov into that register
		if (pushInstr.getOperandType(0) == OP_TYPE_PUSH_REGISTER) {
			String regName = pushInstr.getRegister(0).getName().toLowerCase();
			Instruction probe = pushInstr;
			for (int depth = 0; depth < 10 && probe != null; depth++) {
				probe = probe.getPrevious();
				if (probe == null || !"mov".equalsIgnoreCase(probe.getMnemonicString())) {
					continue;
				}
				// did we mov into our register of interest?
				if (probe.getRegister(0) != null && probe.getRegister(0).getName().toLowerCase().equals(regName)) {
					Reference[] movRefs = probe.getOperandReferences(1);
					if (movRefs.length > 0) {
						return movRefs[0].getToAddress();
					}
					break;
				}
			}

			// Case B: they pushed an immediate address
		} else {
			Reference[] pushRefs = pushInstr.getOperandReferences(0);
			if (pushRefs.length > 0) {
				return pushRefs[0].getToAddress();
			}
		}

		return null;
	}

	private Instruction getPrevTargetInstruction(Instruction start, String mnem, int n, int max) {
		Function func = getFunctionContaining(start.getAddress());
		if (func == null)
			return null;
		AddressSetView body = func.getBody();

		Instruction cur = start;
		int found = 0;
		for (int i = 0; i < max; i++) {
			cur = cur.getPrevious();
			if (cur == null || !body.contains(cur.getAddress())) {
				return null;
			}
			if (cur.getMnemonicString().equalsIgnoreCase(mnem)) {
				found++;
				if (found == n)
					return cur;
			}
		}
		return null;
	}
}