78 lines
5.2 KiB
Swift
78 lines
5.2 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
import Darwin
|
|
|
|
@MainActor final class GameAnalysisModel:ObservableObject {
|
|
@Published var running=false
|
|
@Published var transcript=""
|
|
@Published var failed=false
|
|
@Published var started:Date?
|
|
private var task:Process?
|
|
private var activity=BuildActivity()
|
|
private var logURL:URL?
|
|
private(set) var workspace:URL?
|
|
func start(repo:URL,game:URL,run:Bool,ghidra:String="") {
|
|
guard !running else {return}
|
|
let out=game.appendingPathComponent("nxnative-tools/analysis/"+UUID().uuidString)
|
|
workspace=out;failed=false;transcript="";activity=BuildActivity();started=Date()
|
|
let worker=Process(),pipe=Pipe()
|
|
worker.executableURL=URL(fileURLWithPath:"/usr/bin/python3")
|
|
worker.arguments=[repo.appendingPathComponent("desktop/NxNativeEasy/build_session.py").path,repo.appendingPathComponent("scripts/analyze_game.py").path,game.path,"--output",out.path]+(run ? ["--run"] : [])
|
|
worker.standardOutput=pipe;worker.standardError=pipe
|
|
var env=ProcessInfo.processInfo.environment;env["PYTHONUNBUFFERED"]="1"
|
|
logURL=game.appendingPathComponent("nxnative-tools/logs/analysis-"+UUID().uuidString+".log")
|
|
env["NXE_BUILD_LOG"]=logURL?.path
|
|
if !ghidra.isEmpty {env["GHIDRA_INSTALL_DIR"]=ghidra}
|
|
worker.environment=env
|
|
pipe.fileHandleForReading.readabilityHandler={ [weak self] handle in
|
|
let data=handle.availableData
|
|
if data.isEmpty {handle.readabilityHandler=nil;return}
|
|
Task {@MainActor in guard let self,self.running else {return};self.activity.receive(data);self.transcript=self.activity.transcript}
|
|
}
|
|
worker.terminationHandler={ [weak self] worker in
|
|
Task {@MainActor in guard let self else {return};self.activity.finish(logURL:self.logURL);self.transcript=self.activity.transcript;self.failed=worker.terminationStatus != 0;self.running=false;self.task=nil}
|
|
}
|
|
do {task=worker;running=true;try worker.run()}
|
|
catch {transcript=error.localizedDescription;running=false;failed=true;task=nil}
|
|
}
|
|
func cancel() {if let task,task.isRunning {if kill(-task.processIdentifier,SIGTERM) != 0 {task.terminate()}}}
|
|
func reveal() {if let workspace,FileManager.default.fileExists(atPath:workspace.path) {NSWorkspace.shared.open(workspace)}}
|
|
}
|
|
|
|
struct GameAnalysisView:View {
|
|
let repo:URL
|
|
let game:URL
|
|
let text:(String)->String
|
|
@StateObject private var model=GameAnalysisModel()
|
|
@AppStorage("analysisGhidraDirectory") private var ghidra=""
|
|
@Environment(\.dismiss) private var dismiss
|
|
var body:some View {
|
|
VStack(alignment:.leading,spacing:16) {
|
|
HStack {Text(text("Analizar con REA")).font(.title2.bold());Spacer();Button(text("Cerrar")){dismiss()}.disabled(model.running)}
|
|
Text(text("Análisis opcional del código original extraído. No modifica ni recompila el juego y no lo convierte automáticamente en un juego compatible."))
|
|
Text(text("REA necesita Node/npm y un motor local compatible: Ghidra, Hopper o IDA. Su licencia e instalación son independientes. El análisis usa versiones fijas en una caché de este juego; no cambia la configuración de tu agente."))
|
|
.font(.callout).foregroundStyle(.secondary)
|
|
HStack {
|
|
Button(text("Exportar para analizar")){model.start(repo:repo,game:game,run:false)}.disabled(model.running)
|
|
Button(text("Exportar y ejecutar REA")){model.start(repo:repo,game:game,run:true,ghidra:ghidra)}.disabled(model.running)
|
|
Link(text("Configurar motor"),destination:URL(string:"https://github.com/morluto/rea/blob/main/docs/installation.md")!)
|
|
}
|
|
HStack {
|
|
Button(text("Seleccionar Ghidra…")) {
|
|
let panel=NSOpenPanel();panel.canChooseDirectories=true;panel.canChooseFiles=false;panel.allowsMultipleSelection=false
|
|
if panel.runModal() == .OK,let path=panel.url,FileManager.default.fileExists(atPath:path.appendingPathComponent("support/analyzeHeadless").path) {ghidra=path.path}
|
|
}.disabled(model.running)
|
|
Text(ghidra.isEmpty ? text("Opcional: usa tu instalación local de Ghidra.") : URL(fileURLWithPath:ghidra).lastPathComponent).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
if model.running {
|
|
ProgressView().progressViewStyle(.linear)
|
|
TimelineView(.periodic(from:.now,by:1)) {context in Text(text("Tiempo transcurrido")+" "+String(Int(context.date.timeIntervalSince(model.started ?? context.date)))+" s").font(.caption)}
|
|
Button(text("Cancelar")){model.cancel()}
|
|
}
|
|
ScrollView {Text(model.transcript).font(.system(size:11,design:.monospaced)).textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading)}.frame(minHeight:180)
|
|
if model.failed {Text(text("El análisis no terminó. Revisa los logs; tus archivos originales se conservaron.")).foregroundStyle(.orange)}
|
|
if model.workspace != nil && !model.running {Button(text("Mostrar análisis y logs")){model.reveal()}}
|
|
}.padding(24).frame(width:720).interactiveDismissDisabled(model.running)
|
|
}
|
|
}
|