forked from Apple-2-Tools/jace
Memory view is now performance-optimized and a lot of minor UX issues have been resolved.
This commit is contained in:
parent
eec3fdbcf7
commit
7dada32cbf
@ -314,6 +314,7 @@ public class Apple2e extends Computer {
|
||||
if (cheatEngine.getValue() == null) {
|
||||
if (activeCheatEngine != null) {
|
||||
activeCheatEngine.detach();
|
||||
motherboard.miscDevices.remove(activeCheatEngine);
|
||||
}
|
||||
activeCheatEngine = null;
|
||||
} else {
|
||||
@ -324,6 +325,7 @@ public class Apple2e extends Computer {
|
||||
} else {
|
||||
activeCheatEngine.detach();
|
||||
activeCheatEngine = null;
|
||||
motherboard.miscDevices.remove(activeCheatEngine);
|
||||
}
|
||||
}
|
||||
if (startCheats) {
|
||||
@ -333,6 +335,7 @@ public class Apple2e extends Computer {
|
||||
Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
activeCheatEngine.attach();
|
||||
motherboard.miscDevices.add(activeCheatEngine);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
|
@ -89,6 +89,11 @@ public abstract class Cheats extends Device {
|
||||
listeners.clear();
|
||||
}
|
||||
|
||||
public void removeListener(RAMListener l) {
|
||||
computer.getMemory().removeListener(l);
|
||||
listeners.remove(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reconfigure() {
|
||||
unregisterListeners();
|
||||
|
@ -4,14 +4,21 @@ import jace.Emulator;
|
||||
import jace.JaceApplication;
|
||||
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.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@ -26,6 +33,60 @@ public class MetaCheat extends Cheats {
|
||||
NO_CHANGE, ANY_CHANGE, LESS, GREATER, AMOUNT
|
||||
}
|
||||
|
||||
public static class MemoryCell implements Comparable<MemoryCell>{
|
||||
|
||||
public static ChangeListener<MemoryCell> listener;
|
||||
public int address;
|
||||
public IntegerProperty value = new SimpleIntegerProperty();
|
||||
public IntegerProperty readCount = new SimpleIntegerProperty();
|
||||
public IntegerProperty execCount = new SimpleIntegerProperty();
|
||||
public IntegerProperty writeCount = new SimpleIntegerProperty();
|
||||
public ObservableList<Integer> readInstructions = FXCollections.observableList(new ArrayList<>());
|
||||
public ObservableList<Integer> writeInstructions = FXCollections.observableList(new ArrayList<>());
|
||||
private int x;
|
||||
private int y;
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
public static void setListener(ChangeListener<MemoryCell> l) {
|
||||
listener = l;
|
||||
}
|
||||
|
||||
public MemoryCell() {
|
||||
ChangeListener<Number> changeListener = (ObservableValue<? extends Number> val, Number oldVal, Number newVal) -> {
|
||||
if (listener != null) {
|
||||
listener.changed(null, this, this);
|
||||
}
|
||||
};
|
||||
value.addListener(changeListener);
|
||||
}
|
||||
|
||||
public void setRect(int x, int y, int w, int h) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
public int getY() {
|
||||
return y;
|
||||
}
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(MemoryCell o) {
|
||||
return address - o.address;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SearchResult {
|
||||
|
||||
int address;
|
||||
@ -43,11 +104,16 @@ public class MetaCheat extends Cheats {
|
||||
}
|
||||
|
||||
MetacheatUI ui;
|
||||
|
||||
public int fadeRate = 2;
|
||||
public int lightRate = 20;
|
||||
public int historyLength = 10;
|
||||
|
||||
private int startAddress = 0;
|
||||
private int endAddress = 0x0ffff;
|
||||
private final StringProperty startAddressProperty = new SimpleStringProperty("0");
|
||||
private final StringProperty endAddressProperty = new SimpleStringProperty("FFFF");
|
||||
private boolean byteSized = false;
|
||||
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);
|
||||
@ -64,10 +130,10 @@ public class MetaCheat extends Cheats {
|
||||
addNumericValidator(searchValueProperty);
|
||||
addNumericValidator(changeByProperty);
|
||||
startAddressProperty.addListener((prop, oldVal, newVal) -> {
|
||||
startAddress = parseInt(newVal);
|
||||
startAddress = Math.max(0, Math.min(65535, parseInt(newVal)));
|
||||
});
|
||||
endAddressProperty.addListener((prop, oldVal, newVal) -> {
|
||||
endAddress = parseInt(newVal);
|
||||
endAddress = Math.max(0, Math.min(65535, parseInt(newVal)));
|
||||
});
|
||||
}
|
||||
|
||||
@ -83,6 +149,9 @@ public class MetaCheat extends Cheats {
|
||||
}
|
||||
|
||||
public int parseInt(String s) throws NumberFormatException {
|
||||
if (s == null || s.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
if (s.matches("(\\+|-)?[0-9]+")) {
|
||||
return Integer.parseInt(s);
|
||||
} else if (s.matches("(\\+|-)?[0-9a-fA-F]+")) {
|
||||
@ -116,10 +185,6 @@ public class MetaCheat extends Cheats {
|
||||
return "MetaCheat";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach() {
|
||||
super.detach();
|
||||
@ -210,7 +275,7 @@ public class MetaCheat extends Cheats {
|
||||
? 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;
|
||||
result.lastObservedValue = val;
|
||||
switch (searchType) {
|
||||
case VALUE:
|
||||
int compare = parseInt(searchValueProperty.get());
|
||||
@ -237,4 +302,81 @@ public class MetaCheat extends Cheats {
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
if (fadeCounter-- <= 0) {
|
||||
fadeCounter = FADE_TIMER_VALUE;
|
||||
memoryCells.values().stream().forEach((cell) -> {
|
||||
boolean change = false;
|
||||
if (cell.execCount.get() > 0) {
|
||||
cell.execCount.set(Math.max(0, cell.execCount.get() - fadeRate));
|
||||
change = true;
|
||||
}
|
||||
if (cell.readCount.get() > 0) {
|
||||
cell.readCount.set(Math.max(0, cell.readCount.get() - fadeRate));
|
||||
change = true;
|
||||
}
|
||||
if (cell.writeCount.get() > 0) {
|
||||
cell.writeCount.set(Math.max(0, cell.writeCount.get() - fadeRate));
|
||||
change = true;
|
||||
}
|
||||
if (change) {
|
||||
cell.listener.changed(null, cell, cell);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void processMemoryEvent(RAMEvent e) {
|
||||
MemoryCell cell = getMemoryCell(e.getAddress());
|
||||
if (cell != null) {
|
||||
int programCounter = Emulator.computer.getCpu().getProgramCounter();
|
||||
switch (e.getType()) {
|
||||
case EXECUTE:
|
||||
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));
|
||||
cell.writeInstructions.add(programCounter);
|
||||
if (cell.writeInstructions.size() > historyLength) {
|
||||
cell.writeInstructions.remove(0);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
cell.readCount.set(Math.min(255, cell.readCount.get() + lightRate));
|
||||
cell.readInstructions.add(programCounter);
|
||||
if (cell.readInstructions.size() > historyLength) {
|
||||
cell.readInstructions.remove(0);
|
||||
}
|
||||
}
|
||||
cell.value.set(e.getNewValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,28 +5,38 @@ import jace.Emulator;
|
||||
import jace.cheat.MetaCheat;
|
||||
import jace.cheat.MetaCheat.SearchChangeType;
|
||||
import jace.cheat.MetaCheat.SearchType;
|
||||
import jace.core.RAMEvent;
|
||||
import jace.core.RAMListener;
|
||||
import jace.state.State;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SingleSelectionModel;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.TilePane;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
public class MetacheatUI {
|
||||
|
||||
@ -87,6 +97,9 @@ public class MetacheatUI {
|
||||
@FXML
|
||||
private ListView<MetaCheat.SearchResult> searchResultsListView;
|
||||
|
||||
@FXML
|
||||
private CheckBox showValuesCheckbox;
|
||||
|
||||
@FXML
|
||||
private TilePane watchesPane;
|
||||
|
||||
@ -199,7 +212,6 @@ public class MetacheatUI {
|
||||
searchTypesTabPane.getTabs().get(1).setUserData(SearchType.CHANGE);
|
||||
searchTypesTabPane.getTabs().get(2).setUserData(SearchType.TEXT);
|
||||
searchTypesTabPane.getSelectionModel().selectedItemProperty().addListener((prop, oldVal, newVal) -> {
|
||||
System.out.println("Tab selected: " + newVal.getText());
|
||||
if (cheatEngine != null) {
|
||||
cheatEngine.setSearchType((SearchType) newVal.getUserData());
|
||||
}
|
||||
@ -222,7 +234,7 @@ public class MetacheatUI {
|
||||
if (cheatEngine != null) {
|
||||
cheatEngine.setByteSized((boolean) newVal.getUserData());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
MetaCheat cheatEngine = null;
|
||||
@ -239,21 +251,91 @@ public class MetacheatUI {
|
||||
searchValueField.textProperty().bindBidirectional(cheatEngine.searchValueProperty());
|
||||
searchChangeByField.textProperty().bindBidirectional(cheatEngine.searchChangeByProperty());
|
||||
|
||||
engine.addCheat(RAMEvent.TYPE.ANY, this::processMemoryEvent, 0, 0x0ffff);
|
||||
searchStartAddressField.textProperty().addListener(addressRangeListener);
|
||||
searchEndAddressField.textProperty().addListener(addressRangeListener);
|
||||
|
||||
memoryViewPane.boundsInParentProperty().addListener((prop, oldVal, newVal) -> redrawMemoryView());
|
||||
Application.invokeLater(this::redrawMemoryView);
|
||||
}
|
||||
|
||||
ChangeListener<String> addressRangeListener = (prop, oldVal, newVal) -> Application.invokeLater(this::redrawMemoryView);
|
||||
|
||||
Canvas memoryView = null;
|
||||
public static final int MEMORY_BOX_SIZE = 4;
|
||||
public static final int MEMORY_BOX_GAP = 2;
|
||||
public static final int MEMORY_BOX_TOTAL_SIZE = (MEMORY_BOX_SIZE + MEMORY_BOX_GAP);
|
||||
|
||||
public static Set<MetaCheat.MemoryCell> redrawNodes = new ConcurrentSkipListSet<>();
|
||||
ScheduledExecutorService animationTimer = new ScheduledThreadPoolExecutor(1);
|
||||
ScheduledFuture animationFuture = null;
|
||||
|
||||
private void processMemoryViewUpdates() {
|
||||
Application.invokeLater(() -> {
|
||||
GraphicsContext context = memoryView.getGraphicsContext2D();
|
||||
Set<MetaCheat.MemoryCell> draw = new HashSet<>(redrawNodes);
|
||||
redrawNodes.clear();
|
||||
draw.stream().forEach((cell) -> {
|
||||
if (showValuesCheckbox.isSelected()) {
|
||||
int val = cell.value.get() & 0x0ff;
|
||||
context.setFill(Color.rgb(val, val, val));
|
||||
} else {
|
||||
context.setFill(Color.rgb(
|
||||
cell.writeCount.get(),
|
||||
cell.readCount.get(),
|
||||
cell.execCount.get()));
|
||||
}
|
||||
context.fillRect(cell.getX(), cell.getY(), cell.getWidth(), cell.getHeight());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void redrawMemoryView() {
|
||||
boolean resume = Emulator.computer.pause();
|
||||
if (memoryViewPane.getContent() != null && memoryView != null) {
|
||||
memoryViewPane.setContent(null);
|
||||
}
|
||||
|
||||
if (animationFuture != null) {
|
||||
animationFuture.cancel(false);
|
||||
}
|
||||
|
||||
animationFuture = animationTimer.scheduleAtFixedRate(this::processMemoryViewUpdates, 1000 / 60, 1000 / 60, TimeUnit.MILLISECONDS);
|
||||
|
||||
cheatEngine.initMemoryView();
|
||||
int pixelsPerBlock = 16 * MEMORY_BOX_TOTAL_SIZE;
|
||||
int cols = ((int) memoryViewPane.getWidth()) / pixelsPerBlock * 16;
|
||||
int rows = ((cheatEngine.getEndAddress() - cheatEngine.getStartAddress()) / cols) + 1;
|
||||
memoryView = new Canvas(memoryViewPane.getWidth(), rows * MEMORY_BOX_TOTAL_SIZE);
|
||||
StackPane pane = new StackPane(memoryView);
|
||||
memoryViewPane.setContent(pane);
|
||||
GraphicsContext context = memoryView.getGraphicsContext2D();
|
||||
context.setFill(Color.rgb(40, 40, 40));
|
||||
context.fillRect(0, 0, memoryView.getWidth(), memoryView.getHeight());
|
||||
for (int addr = cheatEngine.getStartAddress(); addr <= cheatEngine.getEndAddress(); addr++) {
|
||||
int col = (addr - cheatEngine.getStartAddress()) % cols;
|
||||
int row = (addr - cheatEngine.getStartAddress()) / cols;
|
||||
MetaCheat.MemoryCell cell = cheatEngine.getMemoryCell(addr);
|
||||
cell.setRect(col * MEMORY_BOX_TOTAL_SIZE, row * MEMORY_BOX_TOTAL_SIZE, MEMORY_BOX_SIZE, MEMORY_BOX_SIZE);
|
||||
redrawNodes.add(cell);
|
||||
}
|
||||
MetaCheat.MemoryCell.setListener((prop, oldCell, newCell) -> {
|
||||
redrawNodes.add(newCell);
|
||||
});
|
||||
|
||||
if (resume) {
|
||||
Emulator.computer.resume();
|
||||
}
|
||||
}
|
||||
|
||||
private void changeZoom(double amount) {
|
||||
double zoom = memoryViewPane.getScaleX();
|
||||
zoom += amount;
|
||||
memoryViewPane.setScaleX(zoom);
|
||||
memoryViewPane.setScaleY(zoom);
|
||||
}
|
||||
|
||||
private void processMemoryEvent(RAMEvent e) {
|
||||
if (e.getAddress() < cheatEngine.getStartAddress() || e.getAddress() > cheatEngine.getEndAddress()) {
|
||||
return;
|
||||
if (memoryView != null) {
|
||||
double zoom = memoryView.getScaleX();
|
||||
zoom += amount;
|
||||
memoryView.setScaleX(zoom);
|
||||
memoryView.setScaleY(zoom);
|
||||
StackPane scrollArea = (StackPane) memoryView.getParent();
|
||||
scrollArea.setPrefSize(memoryView.getWidth() * zoom, memoryView.getHeight() * zoom);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void detach() {
|
||||
|
@ -14,9 +14,10 @@
|
||||
<Button mnemonicParsing="false" onAction="#zoomIn" text="Zoom in" />
|
||||
<Button mnemonicParsing="false" onAction="#zoomOut" text="Zoom out" />
|
||||
<Label text="Start:" />
|
||||
<TextField fx:id="searchStartAddressField" prefHeight="26.0" prefWidth="50.0" text="0000" />
|
||||
<TextField fx:id="searchStartAddressField" prefHeight="26.0" prefWidth="60.0" text="0000" />
|
||||
<Label text="End:" />
|
||||
<TextField fx:id="searchEndAddressField" prefHeight="26.0" prefWidth="48.0" text="FFFF" />
|
||||
<TextField fx:id="searchEndAddressField" prefHeight="26.0" prefWidth="60.0" text="FFFF" />
|
||||
<CheckBox fx:id="showValuesCheckbox" mnemonicParsing="false" text="Show Values" />
|
||||
</items>
|
||||
</ToolBar>
|
||||
<SplitPane dividerPositions="0.6304347826086957" prefHeight="363.0" prefWidth="600.0" VBox.vgrow="ALWAYS">
|
||||
|
Loading…
Reference in New Issue
Block a user