Files
BFM-decomp/tools/ghidra_scripts/BfmMcpServer.java
T
Drew T bd5e21b366 feat(phase-3): file-loader & overlay-map RE + PCSX-Redux runtime oracle
- RE'd the full file-loader/overlay subsystem in Ghidra via MCP: CdReadRequest ->
  CdReadStateMachine (hand-rolled CdControl SeekL/ReadN reader, NOT PsyQ CdRead);
  LIST.CD RAM cache cdFileLocTable + ResourceGetCdLoc/resourceIdMap lookup; LZSS
  staging buffer 0x80079A70; loadDestPtrTable; ~50 symbols named (T1-T5,T8)
- T6b MILESTONE: resident blob @0x800CEDF8 + location overlay @0x80128158 proven
  byte-identical (sha1-equal contiguous prefixes) vs live PCSX-Redux RAM; blobs
  imported into Ghidra (ImportOverlay.java); overlay-map table -> memory-map.md §4.3
- PCSX-Redux runtime oracle: Windows-native build, WSL reads RAM via the web API
  at 172.17.208.1:8081; ledger #4 resolved
- live-verified player/time state: gold(/10) HP BP day hour tiredness boss-HP;
  save-header template (Q#5) + debug-menu dispatch (Q#9: DEBUG.BIN path is dead)
- overlay map: 13 area-states across SC01/02/03, 2 duplicate pairs, location x
  chapter keying, free-roam vs menu-hub granularity
- resolves Open Q#1/#3/#4/#5/#6/#8/#9/#12; corrects SLUS-90029 (FF8 demo, not BFM)
- 2 US prototypes acquired + CRC-verified (Aug-31 1369DE07, Sep-8 5C24728E)
- rules R9 (verify persistence), R10 (>=3-datapoint live verification),
  R11 (runtime oracle Windows-native, bridged to WSL)
- bumps project version 1.2.0 -> 1.3.0
2026-06-14 02:04:28 -06:00

81 lines
3.6 KiB
Java

// BfmMcpServer.java — headless GhidrAssistMCP server with clean save-on-shutdown.
//
// PERSISTENCE MODEL (verified 2026-06-13):
// * GhidrAssistMCP write tools apply changes immediately, but under headless
// (-Djava.awt.headless=true) they leave the change in an OPEN transaction:
// program.isChanged() reads false and program.save() throws
// "Unable to lock due to active transaction" — so there is NO mid-session save.
// * On a CLEAN shutdown, analyzeHeadless's normal end-of-run save COMMITS that
// pending transaction and writes the program DB (verified: a rename survived a
// full stop + reopen, db.3.gbf -> db.4.gbf, "Save succeeded for processed file").
//
// => To persist MCP work: request a clean stop (touch the stopreq sentinel, e.g.
// via tools/ghidra_mcp_stop.sh). This script stops the MCP server and RETURNS,
// letting analyzeHeadless save+close the project. Resume by restarting (the
// saved db reopens). Checkpoint == stop + restart at task boundaries.
//
// Run as a -preScript (no package decl). Imports the extension server class, which
// resolves because GhidrAssistMCP.jar is on the headless JVM classpath.
//
// Args (key=value): host= port= stopreq=
import ghidra.app.script.GhidraScript;
import ghidra.util.Msg;
import ghidrassistmcp.GhidrAssistMCPHeadlessServer;
import java.io.File;
public class BfmMcpServer extends GhidraScript {
@Override
protected void run() throws Exception {
if (currentProgram == null) {
Msg.warn(this, "BfmMcpServer: no program loaded — aborting");
return;
}
String host = "127.0.0.1";
int port = 8080;
String stopReqPath = System.getProperty("user.home") + "/bfm-decomp/.run/mcp-stop.req"; // project-local default (overridden by stopreq= arg)
String[] scriptArgs = getScriptArgs();
if (scriptArgs != null) {
for (String a : scriptArgs) {
if (a.startsWith("host=")) host = a.substring(5);
else if (a.startsWith("port=")) port = parseIntOr(a.substring(5), port);
else if (a.startsWith("stopreq=")) stopReqPath = a.substring(8);
}
}
File stopReq = new File(stopReqPath);
stopReq.delete(); // clear any stale sentinel so we don't stop immediately
GhidrAssistMCPHeadlessServer srv = GhidrAssistMCPHeadlessServer.getInstance();
if (srv.isRunning()) {
srv.setProgram(currentProgram);
Msg.info(this, "BfmMcpServer: server already running; program reference updated");
} else {
srv.start(currentProgram, host, port);
Msg.info(this, "BfmMcpServer: MCP server started on " + host + ":" + port);
}
Msg.info(this, "BfmMcpServer: serving (stopreq=" + stopReqPath + "). "
+ "MCP writes persist on CLEAN SHUTDOWN only — analyzeHeadless commits+saves on close; "
+ "no mid-session save (GhidrAssist holds an open transaction while serving).");
while (!monitor.isCancelled() && srv.isRunning()) {
if (stopReq.exists()) {
stopReq.delete();
Msg.info(this, "BfmMcpServer: stop requested via sentinel");
break;
}
Thread.sleep(1000);
}
srv.stop();
Msg.info(this, "BfmMcpServer: server stopped; returning so analyzeHeadless saves+closes the project.");
}
private static int parseIntOr(String s, int dflt) {
try { return Integer.parseInt(s.trim()); } catch (Exception e) { return dflt; }
}
}