package jace.cheat; import jace.Emulator; import jace.JaceApplication; import jace.core.CPU; import jace.core.Computer; import jace.core.RAM; import jace.core.RAMEvent; import jace.core.RAMListener; import jace.state.State; import jace.ui.MetacheatUI; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; public class MetaCheat extends Cheats { static final ScriptEngine NASHORN_ENGINE = new ScriptEngineManager().getEngineByName("nashorn"); static Invocable NASHORN_INVOCABLE = (Invocable) NASHORN_ENGINE; public static enum SearchType { VALUE, TEXT, CHANGE } public static enum SearchChangeType { NO_CHANGE, ANY_CHANGE, LESS, GREATER, AMOUNT } public static class SearchResult { int address; int lastObservedValue = 0; private SearchResult(int address, int val) { this.address = address; lastObservedValue = val; } @Override public String toString() { return Integer.toHexString(address) + ": " + lastObservedValue + " (" + Integer.toHexString(lastObservedValue) + ")"; } public int getAddress() { return address; } } MetacheatUI ui; public int fadeRate = 1; public int lightRate = 30; public int historyLength = 10; private int startAddress = 0; private int endAddress = 0x0ffff; private final StringProperty startAddressProperty = new SimpleStringProperty(Integer.toHexString(startAddress)); private final StringProperty endAddressProperty = new SimpleStringProperty(Integer.toHexString(endAddress)); private boolean byteSized = true; private SearchType searchType = SearchType.VALUE; private SearchChangeType searchChangeType = SearchChangeType.NO_CHANGE; private final BooleanProperty signedProperty = new SimpleBooleanProperty(false); private final StringProperty searchValueProperty = new SimpleStringProperty("0"); private final StringProperty changeByProperty = new SimpleStringProperty("0"); private final ObservableList<DynamicCheat> cheatList = FXCollections.observableArrayList(); private final ObservableList<SearchResult> resultList = FXCollections.observableArrayList(); private final ObservableList<State> snapshotList = FXCollections.observableArrayList(); public MetaCheat(Computer computer) { super(computer); addNumericValidator(startAddressProperty); addNumericValidator(endAddressProperty); addNumericValidator(searchValueProperty); addNumericValidator(changeByProperty); startAddressProperty.addListener((prop, oldVal, newVal) -> { startAddress = Math.max(0, Math.min(65535, parseInt(newVal))); }); endAddressProperty.addListener((prop, oldVal, newVal) -> { endAddress = Math.max(0, Math.min(65535, parseInt(newVal))); }); } private void addNumericValidator(StringProperty stringProperty) { stringProperty.addListener((ObservableValue<? extends String> prop, String oldVal, String newVal) -> { if (newVal == null || newVal.isEmpty()) { return; } if (!newVal.matches("(\\+|-)?(x|$)?[0-9a-fA-F]*")) { stringProperty.set(""); } }); } public int parseInt(String s) throws NumberFormatException { if (s == null || s.isEmpty()) { return 0; } if (s.matches("(\\+|-)?[0-9]+")) { return Integer.parseInt(s); } else { String upper = s.toUpperCase(); boolean positive = !upper.startsWith("-"); for (int i = 0; i < upper.length(); i++) { char c = upper.charAt(i); if ((c >= '0' && c <= '9') || (c >= 'A' & c <= 'F')) { int value = Integer.parseInt(s.substring(i), 16); if (!positive) { value *= -1; } return value; } } } throw new NumberFormatException("Could not interpret int value " + s); } @Override void registerListeners() { } public void addCheat(DynamicCheat cheat) { cheatList.add(cheat); computer.getMemory().addListener(cheat); cheat.addressProperty().addListener((prop, oldVal, newVal) -> { computer.getMemory().removeListener(cheat); cheat.doConfig(); computer.getMemory().addListener(cheat); }); } public void removeCheat(DynamicCheat cheat) { cheat.active.set(false); computer.getMemory().removeListener(cheat); cheatList.remove(cheat); } @Override protected void unregisterListeners() { super.unregisterListeners(); cheatList.stream().forEach(computer.getMemory()::removeListener); } @Override protected String getDeviceName() { return "MetaCheat"; } @Override public void detach() { super.detach(); ui.detach(); } @Override public void attach() { ui = JaceApplication.getApplication().showMetacheat(); ui.registerMetacheatEngine(this); super.attach(); } public int getStartAddress() { return startAddress; } public int getEndAddress() { return endAddress; } public void setByteSized(boolean b) { byteSized = b; } public void setSearchType(SearchType searchType) { this.searchType = searchType; } public void setSearchChangeType(SearchChangeType searchChangeType) { this.searchChangeType = searchChangeType; } public Property<Boolean> signedProperty() { return signedProperty; } public Property<String> searchValueProperty() { return searchValueProperty; } public Property<String> searchChangeByProperty() { return changeByProperty; } public ObservableList<DynamicCheat> getCheats() { return cheatList; } public ObservableList<SearchResult> getSearchResults() { return resultList; } public ObservableList<State> getSnapshots() { return snapshotList; } public Property<String> startAddressProperty() { return startAddressProperty; } public Property<String> endAddressProperty() { return endAddressProperty; } public void newSearch() { RAM memory = Emulator.computer.getMemory(); resultList.clear(); int compare = parseInt(searchValueProperty.get()); for (int i = 0; i < 0x10000; i++) { boolean signed = signedProperty.get(); int val = byteSized ? signed ? memory.readRaw(i) : memory.readRaw(i) & 0x0ff : signed ? memory.readWordRaw(i) : memory.readWordRaw(i) & 0x0ffff; if (!searchType.equals(SearchType.VALUE) || val == compare) { SearchResult result = new SearchResult(i, val); resultList.add(result); } } } public void performSearch() { RAM memory = Emulator.computer.getMemory(); boolean signed = signedProperty.get(); resultList.removeIf((SearchResult result) -> { int val = byteSized ? signed ? memory.readRaw(result.address) : memory.readRaw(result.address) & 0x0ff : signed ? memory.readWordRaw(result.address) : memory.readWordRaw(result.address) & 0x0ffff; int last = result.lastObservedValue; result.lastObservedValue = val; switch (searchType) { case VALUE: int compare = parseInt(searchValueProperty.get()); return compare != val; case CHANGE: switch (searchChangeType) { case AMOUNT: int amount = parseInt(searchChangeByProperty().getValue()); return (val - last) != amount; case GREATER: return val <= last; case ANY_CHANGE: return val == last; case LESS: return val >= last; case NO_CHANGE: return val != last; } break; case TEXT: break; } return false; }); } RAMListener memoryViewListener = null; private final Map<Integer, MemoryCell> memoryCells = new ConcurrentHashMap<>(); public MemoryCell getMemoryCell(int address) { return memoryCells.get(address); } public void initMemoryView() { RAM memory = Emulator.computer.getMemory(); for (int addr = getStartAddress(); addr <= getEndAddress(); addr++) { if (getMemoryCell(addr) == null) { MemoryCell cell = new MemoryCell(); cell.address = addr; cell.value.set(memory.readRaw(addr)); memoryCells.put(addr, cell); } } if (memoryViewListener == null) { memoryViewListener = memory.observe(RAMEvent.TYPE.ANY, startAddress, endAddress, this::processMemoryEvent); listeners.add(memoryViewListener); } } int fadeCounter = 0; int FADE_TIMER_VALUE = (int) (Emulator.computer.getMotherboard().cyclesPerSecond / 60); @Override public void tick() { computer.cpu.performSingleTrace(); if (fadeCounter-- <= 0) { fadeCounter = FADE_TIMER_VALUE; memoryCells.values().stream() .filter((cell) -> cell.hasCounts()) .forEach((cell) -> { if (cell.execCount.get() > 0) { cell.execCount.set(Math.max(0, cell.execCount.get() - fadeRate)); } if (cell.readCount.get() > 0) { cell.readCount.set(Math.max(0, cell.readCount.get() - fadeRate)); } if (cell.writeCount.get() > 0) { cell.writeCount.set(Math.max(0, cell.writeCount.get() - fadeRate)); } if (MemoryCell.listener != null) { MemoryCell.listener.changed(null, cell, cell); } }); } } AtomicInteger pendingInspectorUpdates = new AtomicInteger(0); public void onInspectorChanged() { pendingInspectorUpdates.set(0); } private void processMemoryEvent(RAMEvent e) { MemoryCell cell = getMemoryCell(e.getAddress()); if (cell != null) { CPU cpu = Emulator.computer.getCpu(); int pc = cpu.getProgramCounter(); String trace = cpu.getLastTrace(); switch (e.getType()) { case EXECUTE: cell.execInstructionsDisassembly.add(trace); if (cell.execInstructionsDisassembly.size() > historyLength) { cell.execInstructionsDisassembly.remove(0); } case READ_OPERAND: cell.execCount.set(Math.min(255, cell.execCount.get() + lightRate)); break; case WRITE: cell.writeCount.set(Math.min(255, cell.writeCount.get() + lightRate)); if (ui.isInspecting(cell.address)) { if (pendingInspectorUpdates.incrementAndGet() < 5) { Platform.runLater(() -> { pendingInspectorUpdates.decrementAndGet(); cell.writeInstructions.add(pc); cell.writeInstructionsDisassembly.add(trace); if (cell.writeInstructions.size() > historyLength) { cell.writeInstructions.remove(0); cell.writeInstructionsDisassembly.remove(0); } }); } } else { cell.writeInstructions.add(cpu.getProgramCounter()); cell.writeInstructionsDisassembly.add(cpu.getLastTrace()); if (cell.writeInstructions.size() > historyLength) { cell.writeInstructions.remove(0); cell.writeInstructionsDisassembly.remove(0); } } break; default: cell.readCount.set(Math.min(255, cell.readCount.get() + lightRate)); if (ui.isInspecting(cell.address)) { if (pendingInspectorUpdates.incrementAndGet() < 5) { Platform.runLater(() -> { pendingInspectorUpdates.decrementAndGet(); cell.readInstructions.add(pc); cell.readInstructionsDisassembly.add(trace); if (cell.readInstructions.size() > historyLength) { cell.readInstructions.remove(0); cell.readInstructionsDisassembly.remove(0); } }); } } else { cell.readInstructions.add(cpu.getProgramCounter()); cell.readInstructionsDisassembly.add(cpu.getLastTrace()); if (cell.readInstructions.size() > historyLength) { cell.readInstructions.remove(0); cell.readInstructionsDisassembly.remove(0); } } } cell.value.set(e.getNewValue()); } } public void saveCheats(File saveFile) { FileWriter writer = null; try { writer = new FileWriter(saveFile); for (DynamicCheat cheat : cheatList) { writer.write(cheat.serialize()); writer.write("\n"); } writer.close(); } catch (IOException ex) { Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex); } finally { try { writer.close(); } catch (IOException ex) { Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex); } } } public void loadCheats(File saveFile) { BufferedReader in = null; try { in = new BufferedReader(new FileReader(saveFile)); StringBuilder guts = new StringBuilder(); String line; while ((line = in.readLine()) != null) { DynamicCheat cheat = DynamicCheat.deserialize(line); addCheat(cheat); } in.close(); } catch (IOException ex) { Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex); } finally { try { in.close(); } catch (IOException ex) { Logger.getLogger(MetaCheat.class.getName()).log(Level.SEVERE, null, ex); } } } }