| 1 | package com.rox.debug.labels; | |
| 2 | ||
| 3 | import com.rox.debug.memory.MemoryEditor; | |
| 4 | ||
| 5 | import java.io.IOException; | |
| 6 | import java.util.ArrayList; | |
| 7 | import java.util.List; | |
| 8 | import java.util.Map; | |
| 9 | import java.util.Optional; | |
| 10 | import java.util.Set; | |
| 11 | import java.util.TreeSet; | |
| 12 | import java.util.function.IntUnaryOperator; | |
| 13 | ||
| 14 | /** | |
| 15 | * Everything a watch list does, minus the Swing: the ROM's labels, each one's live value and lock state | |
| 16 | * as display {@link Row}s, and the actions on them. Label changes are saved straight away; a save that | |
| 17 | * fails is undone, so the list never drifts from the file. | |
| 18 | * | |
| 19 | * <p>If the labels file couldn't be read (e.g. a line being hand-edited), labels can't be added, renamed | |
| 20 | * or removed until a {@link #reload()} succeeds - so saving from the watch list can never overwrite a | |
| 21 | * file the user is part-way through fixing. | |
| 22 | */ | |
| 23 | public final class LabelWatchModel { | |
| 24 | /** One label as displayed: {@code address} like {@code $066D} or {@code $0670-$0671}; values per {@link LabelValues}. */ | |
| 25 | public record Row(String name, String address, String hex, String decimal, boolean locked, boolean editable) { | |
| 26 | } | |
| 27 | ||
| 28 | private final LabelStore store; | |
| 29 | private final LabelWatchTarget target; | |
| 30 | private final List<MemoryLabel> labels = new ArrayList<>(); | |
| 31 | private String loadProblem; | |
| 32 | ||
| 33 | public LabelWatchModel(final LabelStore store, final LabelWatchTarget target){ | |
| 34 | this.store = store; | |
| 35 | this.target = target; | |
| 36 |
1
1. <init> : removed call to com/rox/debug/labels/LabelWatchModel::reload → KILLED |
reload(); |
| 37 | } | |
| 38 | ||
| 39 | /** | |
| 40 | * Re-reads the labels file - e.g. after editing it by hand. If that fails, the current labels stay (so | |
| 41 | * their values can still be set and their locks released) but can't be changed until a reload works. | |
| 42 | * After a successful reload, any locked byte that a label covered before but none covers now is | |
| 43 | * released - otherwise it would stay held with no row left to unlock it. Locks no label ever covered | |
| 44 | * are left alone. | |
| 45 | */ | |
| 46 | public void reload(){ | |
| 47 | final List<MemoryLabel> loaded; | |
| 48 | try { | |
| 49 | loaded = store.load(); | |
| 50 | } catch (IOException | LabelFileException e){ | |
| 51 | loadProblem = e.getMessage(); | |
| 52 | return; | |
| 53 | } | |
| 54 | final List<MemoryLabel> previous = List.copyOf(labels); | |
| 55 |
1
1. reload : removed call to java/util/List::clear → KILLED |
labels.clear(); |
| 56 | labels.addAll(loaded); | |
| 57 | loadProblem = null; | |
| 58 |
1
1. reload : removed call to com/rox/debug/labels/LabelWatchModel::releaseLocksNoLongerCovered → KILLED |
releaseLocksNoLongerCovered(previous); |
| 59 | } | |
| 60 | ||
| 61 | private void releaseLocksNoLongerCovered(final List<MemoryLabel> previous){ | |
| 62 | final Set<Integer> stillCovered = coveredBytes(labels); | |
| 63 | final Map<Integer, Integer> locks = target.locks(); | |
| 64 | for (final int formerlyCovered : coveredBytes(previous)){ | |
| 65 |
2
1. releaseLocksNoLongerCovered : negated conditional → KILLED 2. releaseLocksNoLongerCovered : negated conditional → KILLED |
if (locks.containsKey(formerlyCovered) && !stillCovered.contains(formerlyCovered)){ |
| 66 |
1
1. releaseLocksNoLongerCovered : removed call to com/rox/debug/labels/LabelWatchTarget::unlock → KILLED |
target.unlock(formerlyCovered); |
| 67 | } | |
| 68 | } | |
| 69 | } | |
| 70 | ||
| 71 | /** Every byte the labels cover, by {@link MemoryEditor#canonical} address - so RAM mirrors compare equal. */ | |
| 72 | private static Set<Integer> coveredBytes(final List<MemoryLabel> labels){ | |
| 73 | final Set<Integer> covered = new TreeSet<>(); | |
| 74 | for (final MemoryLabel label : labels){ | |
| 75 |
2
1. coveredBytes : negated conditional → KILLED 2. coveredBytes : changed conditional boundary → KILLED |
for (int address = label.start(); address <= label.end(); address++){ |
| 76 | covered.add(MemoryEditor.canonical(address)); | |
| 77 | } | |
| 78 | } | |
| 79 |
1
1. coveredBytes : replaced return value with Collections.emptySet for com/rox/debug/labels/LabelWatchModel::coveredBytes → KILLED |
return covered; |
| 80 | } | |
| 81 | ||
| 82 | /** Why the labels file couldn't be read, if it couldn't - labels are read-only meanwhile. */ | |
| 83 | public Optional<String> loadProblem(){ | |
| 84 |
1
1. loadProblem : replaced return value with Optional.empty for com/rox/debug/labels/LabelWatchModel::loadProblem → KILLED |
return Optional.ofNullable(loadProblem); |
| 85 | } | |
| 86 | ||
| 87 | public List<MemoryLabel> labels(){ | |
| 88 |
1
1. labels : replaced return value with Collections.emptyList for com/rox/debug/labels/LabelWatchModel::labels → KILLED |
return List.copyOf(labels); |
| 89 | } | |
| 90 | ||
| 91 | /** Every label's current value and lock state, from one consistent read of memory. */ | |
| 92 | public List<Row> rows(){ | |
| 93 | final IntUnaryOperator peek = target.peek(); | |
| 94 | final Map<Integer, Integer> locks = target.locks(); | |
| 95 | final List<Row> rows = new ArrayList<>(labels.size()); | |
| 96 | for (final MemoryLabel label : labels){ | |
| 97 | rows.add(new Row(label.name(), address(label), LabelValues.hex(label, peek), LabelValues.decimal(label, peek), | |
| 98 | isLocked(label, locks), isEditable(label))); | |
| 99 | } | |
| 100 |
1
1. rows : replaced return value with Collections.emptyList for com/rox/debug/labels/LabelWatchModel::rows → KILLED |
return rows; |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * Adds a label written as in the file, e.g. {@code $066D rupees}. | |
| 105 | * | |
| 106 | * @throws LabelFileException if it isn't a valid label | |
| 107 | * @throws IllegalArgumentException if its name is already taken | |
| 108 | * @throws IllegalStateException while the labels file couldn't be read | |
| 109 | */ | |
| 110 | public void add(final String labelText) throws LabelFileException, IOException { | |
| 111 |
1
1. add : removed call to com/rox/debug/labels/LabelWatchModel::requireLabelsEditable → KILLED |
requireLabelsEditable(); |
| 112 | final MemoryLabel label = LabelFile.parseLabel(labelText); | |
| 113 |
1
1. add : removed call to com/rox/debug/labels/LabelWatchModel::requireUnusedName → KILLED |
requireUnusedName(label.name()); |
| 114 | labels.add(label); | |
| 115 |
2
1. add : removed call to com/rox/debug/labels/LabelWatchModel::saveOrUndo → KILLED 2. lambda$add$0 : Replaced integer subtraction with addition → KILLED |
saveOrUndo(() -> labels.remove(labels.size() - 1)); |
| 116 | } | |
| 117 | ||
| 118 | /** | |
| 119 | * @throws IllegalArgumentException if the name is invalid or already taken by another label | |
| 120 | * @throws IllegalStateException while the labels file couldn't be read | |
| 121 | */ | |
| 122 | public void rename(final int index, final String newName) throws IOException { | |
| 123 |
1
1. rename : removed call to com/rox/debug/labels/LabelWatchModel::requireLabelsEditable → KILLED |
requireLabelsEditable(); |
| 124 | final MemoryLabel old = labels.get(index); | |
| 125 | final MemoryLabel renamed = new MemoryLabel(newName.strip(), old.start(), old.length()); | |
| 126 |
1
1. rename : negated conditional → KILLED |
if (!renamed.name().equals(old.name())){ |
| 127 |
1
1. rename : removed call to com/rox/debug/labels/LabelWatchModel::requireUnusedName → KILLED |
requireUnusedName(renamed.name()); |
| 128 | } | |
| 129 | labels.set(index, renamed); | |
| 130 |
1
1. rename : removed call to com/rox/debug/labels/LabelWatchModel::saveOrUndo → KILLED |
saveOrUndo(() -> labels.set(index, old)); |
| 131 | } | |
| 132 | ||
| 133 | /** | |
| 134 | * Removes a label - releasing any lock it held, so nothing stays held with no row left to unlock it. | |
| 135 | * | |
| 136 | * @throws IllegalStateException while the labels file couldn't be read | |
| 137 | */ | |
| 138 | public void remove(final int index) throws IOException { | |
| 139 |
1
1. remove : removed call to com/rox/debug/labels/LabelWatchModel::requireLabelsEditable → KILLED |
requireLabelsEditable(); |
| 140 | final MemoryLabel removed = labels.remove(index); | |
| 141 | try { | |
| 142 |
1
1. remove : removed call to com/rox/debug/labels/LabelStore::save → KILLED |
store.save(labels); |
| 143 | } catch (IOException e){ | |
| 144 |
1
1. remove : removed call to java/util/List::add → KILLED |
labels.add(index, removed); |
| 145 | throw e; | |
| 146 | } | |
| 147 |
1
1. remove : negated conditional → KILLED |
if (isLocked(removed, target.locks())){ |
| 148 |
1
1. remove : removed call to com/rox/debug/labels/LabelWatchModel::unlockBytes → KILLED |
unlockBytes(removed); |
| 149 | } | |
| 150 | } | |
| 151 | ||
| 152 | /** | |
| 153 | * Sets a label's bytes from text per {@link LabelValues#parse} - re-locking at the new value if it's locked. | |
| 154 | * | |
| 155 | * @throws IllegalArgumentException if the text isn't one byte value per byte, or the label isn't editable | |
| 156 | */ | |
| 157 | public void setValue(final int index, final String text){ | |
| 158 | final MemoryLabel label = labels.get(index); | |
| 159 |
1
1. setValue : removed call to com/rox/debug/labels/LabelWatchModel::requireEditable → KILLED |
requireEditable(label); |
| 160 | final int[] values = LabelValues.parse(label, text); | |
| 161 | final boolean locked = isLocked(label, target.locks()); | |
| 162 |
2
1. setValue : changed conditional boundary → KILLED 2. setValue : negated conditional → KILLED |
for (int i = 0; i < values.length; i++){ |
| 163 |
1
1. setValue : negated conditional → KILLED |
if (locked){ |
| 164 |
2
1. setValue : removed call to com/rox/debug/labels/LabelWatchTarget::lock → KILLED 2. setValue : Replaced integer addition with subtraction → KILLED |
target.lock(label.start() + i, values[i]); |
| 165 | } else { | |
| 166 |
2
1. setValue : Replaced integer addition with subtraction → KILLED 2. setValue : removed call to com/rox/debug/labels/LabelWatchTarget::poke → KILLED |
target.poke(label.start() + i, values[i]); |
| 167 | } | |
| 168 | } | |
| 169 | } | |
| 170 | ||
| 171 | /** | |
| 172 | * Locks a label's bytes at their current values, or releases them. | |
| 173 | * | |
| 174 | * @throws IllegalArgumentException if the label isn't editable | |
| 175 | */ | |
| 176 | public void setLocked(final int index, final boolean locked){ | |
| 177 | final MemoryLabel label = labels.get(index); | |
| 178 |
1
1. setLocked : removed call to com/rox/debug/labels/LabelWatchModel::requireEditable → KILLED |
requireEditable(label); |
| 179 |
1
1. setLocked : negated conditional → KILLED |
if (locked){ |
| 180 | final IntUnaryOperator peek = target.peek(); | |
| 181 |
2
1. setLocked : changed conditional boundary → KILLED 2. setLocked : negated conditional → KILLED |
for (int address = label.start(); address <= label.end(); address++){ |
| 182 |
1
1. setLocked : removed call to com/rox/debug/labels/LabelWatchTarget::lock → KILLED |
target.lock(address, peek.applyAsInt(address)); |
| 183 | } | |
| 184 | } else { | |
| 185 |
1
1. setLocked : removed call to com/rox/debug/labels/LabelWatchModel::unlockBytes → KILLED |
unlockBytes(label); |
| 186 | } | |
| 187 | } | |
| 188 | ||
| 189 | private void unlockBytes(final MemoryLabel label){ | |
| 190 |
2
1. unlockBytes : changed conditional boundary → KILLED 2. unlockBytes : negated conditional → KILLED |
for (int address = label.start(); address <= label.end(); address++){ |
| 191 |
1
1. unlockBytes : removed call to com/rox/debug/labels/LabelWatchTarget::unlock → KILLED |
target.unlock(address); |
| 192 | } | |
| 193 | } | |
| 194 | ||
| 195 | private static String address(final MemoryLabel label){ | |
| 196 |
2
1. address : replaced return value with "" for com/rox/debug/labels/LabelWatchModel::address → KILLED 2. address : negated conditional → KILLED |
return label.length() == 1 |
| 197 | ? String.format("$%04X", label.start()) | |
| 198 | : String.format("$%04X-$%04X", label.start(), label.end()); | |
| 199 | } | |
| 200 | ||
| 201 | private static boolean isEditable(final MemoryLabel label){ | |
| 202 |
2
1. isEditable : negated conditional → KILLED 2. isEditable : changed conditional boundary → KILLED |
for (int address = label.start(); address <= label.end(); address++){ |
| 203 |
1
1. isEditable : negated conditional → KILLED |
if (!MemoryEditor.isEditable(address)){ |
| 204 |
1
1. isEditable : replaced boolean return with true for com/rox/debug/labels/LabelWatchModel::isEditable → KILLED |
return false; |
| 205 | } | |
| 206 | } | |
| 207 |
1
1. isEditable : replaced boolean return with false for com/rox/debug/labels/LabelWatchModel::isEditable → KILLED |
return true; |
| 208 | } | |
| 209 | ||
| 210 | private static boolean isLocked(final MemoryLabel label, final Map<Integer, Integer> locks){ | |
| 211 |
2
1. isLocked : changed conditional boundary → KILLED 2. isLocked : negated conditional → KILLED |
for (int address = label.start(); address <= label.end(); address++){ |
| 212 |
1
1. isLocked : negated conditional → KILLED |
if (!locks.containsKey(MemoryEditor.canonical(address))){ |
| 213 |
1
1. isLocked : replaced boolean return with true for com/rox/debug/labels/LabelWatchModel::isLocked → KILLED |
return false; |
| 214 | } | |
| 215 | } | |
| 216 |
1
1. isLocked : replaced boolean return with false for com/rox/debug/labels/LabelWatchModel::isLocked → KILLED |
return true; |
| 217 | } | |
| 218 | ||
| 219 | private void requireEditable(final MemoryLabel label){ | |
| 220 |
1
1. requireEditable : negated conditional → KILLED |
if (!isEditable(label)){ |
| 221 | throw new IllegalArgumentException("'" + label.name() + "' isn't in CPU RAM or PRG-RAM, so it can only be watched"); | |
| 222 | } | |
| 223 | } | |
| 224 | ||
| 225 | private void requireLabelsEditable(){ | |
| 226 |
1
1. requireLabelsEditable : negated conditional → KILLED |
if (loadProblem != null){ |
| 227 | throw new IllegalStateException("The labels file couldn't be read, so labels can't be changed until it's fixed and reloaded: " + loadProblem); | |
| 228 | } | |
| 229 | } | |
| 230 | ||
| 231 | private void requireUnusedName(final String name){ | |
| 232 |
3
1. lambda$requireUnusedName$0 : replaced boolean return with true for com/rox/debug/labels/LabelWatchModel::lambda$requireUnusedName$0 → KILLED 2. requireUnusedName : negated conditional → KILLED 3. lambda$requireUnusedName$0 : replaced boolean return with false for com/rox/debug/labels/LabelWatchModel::lambda$requireUnusedName$0 → KILLED |
if (labels.stream().anyMatch(label -> label.name().equals(name))){ |
| 233 | throw new IllegalArgumentException("'" + name + "' is already used by another label"); | |
| 234 | } | |
| 235 | } | |
| 236 | ||
| 237 | private void saveOrUndo(final Runnable undo) throws IOException { | |
| 238 | try { | |
| 239 |
1
1. saveOrUndo : removed call to com/rox/debug/labels/LabelStore::save → KILLED |
store.save(labels); |
| 240 | } catch (IOException e){ | |
| 241 |
1
1. saveOrUndo : removed call to java/lang/Runnable::run → KILLED |
undo.run(); |
| 242 | throw e; | |
| 243 | } | |
| 244 | } | |
| 245 | } | |
Mutations | ||
| 36 |
1.1 |
|
| 55 |
1.1 |
|
| 58 |
1.1 |
|
| 65 |
1.1 2.2 |
|
| 66 |
1.1 |
|
| 75 |
1.1 2.2 |
|
| 79 |
1.1 |
|
| 84 |
1.1 |
|
| 88 |
1.1 |
|
| 100 |
1.1 |
|
| 111 |
1.1 |
|
| 113 |
1.1 |
|
| 115 |
1.1 2.2 |
|
| 123 |
1.1 |
|
| 126 |
1.1 |
|
| 127 |
1.1 |
|
| 130 |
1.1 |
|
| 139 |
1.1 |
|
| 142 |
1.1 |
|
| 144 |
1.1 |
|
| 147 |
1.1 |
|
| 148 |
1.1 |
|
| 159 |
1.1 |
|
| 162 |
1.1 2.2 |
|
| 163 |
1.1 |
|
| 164 |
1.1 2.2 |
|
| 166 |
1.1 2.2 |
|
| 178 |
1.1 |
|
| 179 |
1.1 |
|
| 181 |
1.1 2.2 |
|
| 182 |
1.1 |
|
| 185 |
1.1 |
|
| 190 |
1.1 2.2 |
|
| 191 |
1.1 |
|
| 196 |
1.1 2.2 |
|
| 202 |
1.1 2.2 |
|
| 203 |
1.1 |
|
| 204 |
1.1 |
|
| 207 |
1.1 |
|
| 211 |
1.1 2.2 |
|
| 212 |
1.1 |
|
| 213 |
1.1 |
|
| 216 |
1.1 |
|
| 220 |
1.1 |
|
| 226 |
1.1 |
|
| 232 |
1.1 2.2 3.3 |
|
| 239 |
1.1 |
|
| 241 |
1.1 |