diff --git a/src/main/java/com/github/manolo8/darkbot/core/IDarkBotAPI.java b/src/main/java/com/github/manolo8/darkbot/core/IDarkBotAPI.java index 5857e14eb..74b0bf420 100644 --- a/src/main/java/com/github/manolo8/darkbot/core/IDarkBotAPI.java +++ b/src/main/java/com/github/manolo8/darkbot/core/IDarkBotAPI.java @@ -217,6 +217,12 @@ default boolean readBoolean(long address) { return readMemoryBoolean(address); } + String readStringDirect(long address); + + default String readStringDirect(long address, int... offsets) { + return readStringDirect(readLong(address, offsets)); + } + @Override default String readString(long address) { return readMemoryString(address); diff --git a/src/main/java/com/github/manolo8/darkbot/core/api/GameAPIImpl.java b/src/main/java/com/github/manolo8/darkbot/core/api/GameAPIImpl.java index debe4473b..e547b9942 100644 --- a/src/main/java/com/github/manolo8/darkbot/core/api/GameAPIImpl.java +++ b/src/main/java/com/github/manolo8/darkbot/core/api/GameAPIImpl.java @@ -351,7 +351,9 @@ public String readMemoryStringFallback(long address, String fallback) { @Override public byte[] readMemory(long address, int length) { if (!ByteUtils.isValidPtr(address)) return new byte[0]; - return memory.readBytes(address, length); + synchronized (memory) { + return memory.readBytes(address, length); + } } @Override @@ -360,7 +362,9 @@ public void readMemory(long address, byte[] buffer, int length) { Arrays.fill(buffer, 0, length, (byte) 0); return; } - memory.readBytes(address, buffer, length); + synchronized (memory) { + memory.readBytes(address, buffer, length); + } } @Override @@ -616,4 +620,12 @@ public void setQuality(GameAPI.Handler.GameQuality quality) { public long lastInternetReadTime() { return handler.lastInternetReadTime(); } + + @Override + public String readStringDirect(long address) { + if (!ByteUtils.isValidPtr(address)) return FALLBACK_STRING; + String s = ByteUtils.readStringDirect(address); + + return s == null ? FALLBACK_STRING : s; + } } diff --git a/src/main/java/com/github/manolo8/darkbot/core/itf/NativeUpdatable.java b/src/main/java/com/github/manolo8/darkbot/core/itf/NativeUpdatable.java index bd20eb77b..5b79f2ecf 100644 --- a/src/main/java/com/github/manolo8/darkbot/core/itf/NativeUpdatable.java +++ b/src/main/java/com/github/manolo8/darkbot/core/itf/NativeUpdatable.java @@ -8,6 +8,9 @@ public interface NativeUpdatable { long getAddress(); + default void update() {} + default void update(long address) {} + default int modifyOffset(int offset) { return offset; } diff --git a/src/main/java/com/github/manolo8/darkbot/core/objects/swf/AtomKind.java b/src/main/java/com/github/manolo8/darkbot/core/objects/swf/AtomKind.java new file mode 100644 index 000000000..84432846b --- /dev/null +++ b/src/main/java/com/github/manolo8/darkbot/core/objects/swf/AtomKind.java @@ -0,0 +1,80 @@ +package com.github.manolo8.darkbot.core.objects.swf; + +import com.github.manolo8.darkbot.Main; +import com.github.manolo8.darkbot.core.itf.Updatable; +import com.github.manolo8.darkbot.core.utils.ByteUtils; + +import java.util.function.BiFunction; +import java.util.function.LongFunction; + +public enum AtomKind { + UNUSED(), + OBJECT(Long.class, atom -> atom & ByteUtils.ATOM_MASK, ByteUtils::getLong), + STRING(String.class, atom -> Main.API.readString(atom & ByteUtils.ATOM_MASK), + (b, i) -> Main.API.readString(ByteUtils.getLong(b, i))), + NAMESPACE(), // ? + SPECIAL(Float.class),// prob not supported + BOOLEAN(Boolean.class, atom -> atom == 0xD, (b, i) -> b[i] == 1), + INTEGER(Integer.class, atom -> (int) ((atom & ByteUtils.ATOM_MASK) >> 3), ByteUtils::getInt), + DOUBLE(Double.class, atom -> Double.longBitsToDouble(atom & ByteUtils.ATOM_MASK), ByteUtils::getDouble); + + private static final AtomKind[] VALUES = values(); + + private final Class javaType; + private final LongFunction readAtom; + private final BiFunction readBuffer; + + AtomKind() { + this(null); + } + + AtomKind(Class javaType) { + this(javaType, null, null); + } + + AtomKind(Class javaType, LongFunction readAtom, BiFunction readBuffer) { + this.javaType = javaType; + this.readAtom = readAtom; + this.readBuffer = readBuffer; + } + + @SuppressWarnings("unchecked") + public T readAtom(long atom, boolean threadSafe) { + if (threadSafe && this == STRING) + return (T) Main.API.readStringDirect(atom & ByteUtils.ATOM_MASK); + return (T) readAtom.apply(atom); + } + + @SuppressWarnings("unchecked") + public T readBuffer(byte[] buffer, int offset) { + return (T) readBuffer.apply(buffer, offset); + } + + public boolean isNotSupported() { + return readAtom == null; + } + + public static AtomKind of(long atom) { + int kind = (int) (atom & ByteUtils.ATOM_KIND); + if (kind >= VALUES.length) return UNUSED; + + return VALUES[kind]; + } + + public static AtomKind of(Class type) { + if (Updatable.class.isAssignableFrom(type)) return OBJECT; + for (AtomKind kind : VALUES) { + if (kind.javaType == type) + return kind; + } + return UNUSED; + } + + public static boolean isNullAtom(long atom) { + return atom <= 4; + } + + public static boolean isString(long atom) { + return AtomKind.of(atom) == STRING; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/manolo8/darkbot/core/objects/swf/FlashList.java b/src/main/java/com/github/manolo8/darkbot/core/objects/swf/FlashList.java new file mode 100644 index 000000000..8d7de813c --- /dev/null +++ b/src/main/java/com/github/manolo8/darkbot/core/objects/swf/FlashList.java @@ -0,0 +1,347 @@ +package com.github.manolo8.darkbot.core.objects.swf; + +import com.github.manolo8.darkbot.Main; +import com.github.manolo8.darkbot.core.itf.NativeUpdatable; +import com.github.manolo8.darkbot.core.itf.Updatable; +import com.github.manolo8.darkbot.core.manager.HeroManager; +import com.github.manolo8.darkbot.core.utils.ByteUtils; + +import java.lang.reflect.Array; +import java.lang.reflect.Modifier; +import java.util.AbstractList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +// int denseStart = Main.API.readInt(address + 48); +// int denseUsed = Main.API.readInt(address + 52); +// int length = Main.API.readInt(address + 56); +// int lengthIfSimple = Main.API.readInt(address + 60); +// int canBeSimple = Main.API.readInt(address + 64); +public abstract class FlashList extends AbstractList implements NativeUpdatable { + private static final int MAX_SIZE = 8192; + private static final byte[] BUFFER = new byte[MAX_SIZE * Long.BYTES]; //64kb + + private static final long ATOM_NOT_FOUND = 0; + + protected final Class valueType; + protected final boolean valueUpdatable; + protected AtomKind valueKind; + protected long address; + protected boolean threadSafe; + + private int size; + private ElementWrapper[] elements; + private Map updatables; + private E lastElement; + + private FlashList(Class valueType) { + if (valueType == null) { + this.valueKind = null; + this.valueType = null; + this.valueUpdatable = false; + } else { + this.valueKind = AtomKind.of(valueType); + this.valueType = valueType; + + if (valueKind.isNotSupported()) + throw new IllegalArgumentException("Given valueType is not supported!"); + this.valueUpdatable = Updatable.class.isAssignableFrom(valueType); + + if (valueUpdatable && Modifier.isAbstract(this.valueType.getModifiers())) + throw new IllegalArgumentException("Updatable must not be abstract!"); + } + clearInternal(); + } + + /** + * Array + */ + public static FlashList ofArrayUnknown() { + return ofArray(null); + } + + /** + * Array + */ + public static FlashList ofArray(Class valueType) { + return new FlashArray<>(valueType); + } + + /** + * Vector + */ + public static FlashList ofVectorUnknown() { + return ofVector(null); + } + + /** + * Vector + */ + public static FlashList ofVector(Class valueType) { + return new FlashVector<>(valueType); + } + + public abstract void update(); + + public FlashList makeThreadSafe() { + this.threadSafe = true; + return this; + } + + public void update(long address) { + if (this.address != address) { + clearInternal(); + } + this.address = address; + } + + @Override + public long getAddress() { + return address; + } + + @Override + public void clear() { + throw new UnsupportedOperationException("clear"); + } + + @SuppressWarnings("unchecked") + private void clearInternal() { + size = 0; + lastElement = null; + elements = (ElementWrapper[]) Array.newInstance(ElementWrapper.class, 0); + + if (updatables != null) { + updatables.forEach((integer, elementWrapper) -> elementWrapper.reset()); + } + } + + @Override + public int size() { + return size; + } + + protected void setSize(int size) { + for (int i = size; i < this.size; i++) { + this.elements[i].reset(); + } + this.size = size; + } + + @Override + public E get(int index) { + return elements[index].value; + } + + @Override + public void add(int index, E element) { + if (!valueUpdatable) throw new IllegalStateException("E type is not updatable!"); + if (updatables == null) updatables = new HashMap<>(); + updatables.put(index, new ElementWrapper(element)); + } + + @Override + public int lastIndexOf(Object value) { + for (int i = size() - 1; i >= 0; i--) + if (value.equals(get(i))) return i; + return -1; + } + + public void forEachIncremental(Consumer consumer) { + for (int i = lastIndexOf(lastElement) + 1; i < size(); i++) + consumer.accept(lastElement = get(i)); + } + + protected boolean ensureCapacity(int size) { + if (size <= 0 || size > MAX_SIZE) { + this.size = 0; + return false; + } + if (size > elements.length) + elements = Arrays.copyOf(elements, Math.min((int) (size * 1.25), MAX_SIZE)); + return true; + } + + protected ElementWrapper checkElement(int idx) { + ElementWrapper element = elements[idx]; + if (element == null) { + if (updatables != null) + element = updatables.get(idx); + + if (element == null) + element = new ElementWrapper(); + + elements[idx] = element; + } + + return element; + } + + protected void readBuffer(long table, int length) { + if (!threadSafe) { + Main.API.readMemory(table, BUFFER, length); + } + } + + protected long readAtom(long table, int offset) { + if (threadSafe) return Main.API.readLong(table + offset); + return ByteUtils.getLong(BUFFER, offset); + } + + private static class FlashArray extends FlashList { + + private FlashArray(Class valueType) { + super(valueType); + } + + public void update() { + if (address == 0) return; + + int size = readInt(40); + if (!ensureCapacity(size)) + return; + + int length = size * Long.BYTES; + long table = readLong(32) + 16; + readBuffer(table, length); + + int realSize = 0; + for (int offset = 0; offset < length; offset += 8) { + long atom = readAtom(table, offset); + if (atom == ATOM_NOT_FOUND) continue; + + AtomKind valueKind = AtomKind.of(atom); + if (this.valueKind != null && this.valueKind != valueKind) { + System.out.println("Invalid valueKind! expected: " + this.valueKind + ", read: " + this.valueKind); + break; + } + + checkElement(realSize++).set(atom, valueKind); + } + setSize(realSize); + } + + @Override + public String toString() { + return "FlashArray[" + size() + "]"; + } + } + + private static class FlashVector extends FlashList { + + private FlashVector(Class valueType) { + super(valueType); + } + + @Override + public void update(long address) { + if (this.address != address && valueType == null) { + String type = threadSafe + ? Main.API.readStringDirect(address, 32, 48, 144) + : Main.API.readString(address, 32, 48, 144); + switch (type) { + case "uint": + case "int": + valueKind = AtomKind.INTEGER; + break; + case "Number": + valueKind = AtomKind.DOUBLE; + break; + case "String": + valueKind = AtomKind.STRING; + break; + case "Boolean": + valueKind = AtomKind.BOOLEAN; + break; + default: + valueKind = AtomKind.OBJECT; + } + } + super.update(address); + } + + public void update() { + if (address == 0) return; + int valueLength = valueKind == AtomKind.INTEGER ? Integer.BYTES : Long.BYTES; + boolean isObjectType = valueKind == AtomKind.OBJECT || valueKind == AtomKind.STRING; + + int size = readInt(56 + (isObjectType ? 0 : 8)); + if (!ensureCapacity(size)) + return; + + int length = size * valueLength; + long table = readLong(48) + valueLength + (isObjectType ? 8 : 0); + readBuffer(table, length); + + int realSize = 0; + for (int offset = 0; offset < length; offset += valueLength) { + ElementWrapper elementWrapper = checkElement(realSize++); + + if (isObjectType) { + elementWrapper.set(readAtom(table, offset), valueKind); + } else { + elementWrapper.set(readElement(table, offset)); + } + } + setSize(realSize); + } + + @SuppressWarnings("unchecked") + private E readElement(long table, int offset) { + if (!threadSafe) return valueKind.readBuffer(BUFFER, offset); + if (valueKind == AtomKind.DOUBLE) return (E) Double.valueOf(Main.API.readDouble(table + offset)); + if (valueKind == AtomKind.BOOLEAN) return (E) Boolean.valueOf(Main.API.readInt(table + offset) == 1); + return (E) Integer.valueOf(Main.API.readInt(table + offset)); + } + + @Override + public String toString() { + return "FlashVector[" + size() + "]"; + } + } + + protected class ElementWrapper { + private E value; + private long atomCache; + + private ElementWrapper() { + if (valueUpdatable) + this.value = HeroManager.instance.main.pluginAPI.requireInstance(valueType); + } + + private ElementWrapper(E value) { + this.value = value; + } + + protected void set(long atom, AtomKind valueKind) { + if (atomCache != atom) { + atomCache = atom; + + if (value instanceof Updatable) { + ((Updatable) value).update(atom & ByteUtils.ATOM_MASK); + } else { + value = valueKind.readAtom(atom, threadSafe); + } + } + if (atom == 0) return; + if (value instanceof Updatable) + ((Updatable) value).update(); + } + + private void set(E value) { + this.value = value; + } + + private void reset() { + if (this.value instanceof Updatable) { + Updatable u = (Updatable) this.value; + if (u.address != 0) + u.update(0); + } + this.value = null; + this.atomCache = 0; + } + } +} diff --git a/src/main/java/com/github/manolo8/darkbot/core/objects/swf/FlashMap.java b/src/main/java/com/github/manolo8/darkbot/core/objects/swf/FlashMap.java new file mode 100644 index 000000000..925646741 --- /dev/null +++ b/src/main/java/com/github/manolo8/darkbot/core/objects/swf/FlashMap.java @@ -0,0 +1,463 @@ +package com.github.manolo8.darkbot.core.objects.swf; + +import com.github.manolo8.darkbot.core.itf.NativeUpdatable; +import com.github.manolo8.darkbot.core.itf.Updatable; +import com.github.manolo8.darkbot.core.manager.HeroManager; +import com.github.manolo8.darkbot.core.utils.ByteUtils; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Array; +import java.lang.reflect.Modifier; +import java.util.*; + +import static com.github.manolo8.darkbot.Main.API; + +/** + * Object[K] = V, Array[K] = V, Dictionary[K] = V + */ +public class FlashMap extends AbstractMap implements NativeUpdatable { + /** + * since identifiers are always interned strings, they can't be 0, + * so we can use 0 as the empty value. + */ + private static final int EMPTY_ITEM = 0x0; + + // DELETED is stored as the key for deleted items + private static final int DELETED_ITEM = 0x4; + + private static final int DONT_ENUM_BIT = 0x01; + private static final int HAS_DELETED_ITEMS = 0x02; + private static final int HAS_ITER_INDEX = 0x04; + + private static final int DICTIONARY_FLAG = 1 << 4; + + private static final int MAX_SIZE = 2048; + private static final int MAX_CAPACITY = MAX_SIZE * Long.BYTES * 2; + private static final byte[] BUFFER = new byte[MAX_CAPACITY]; //32kb + + private final AtomKind keyKind; + private final AtomKind valueKind; + + private final Class keyType; + private final Class valueType; + + private final boolean keyUpdatable, valueUpdatable; + + private int size; + private Entry[] entries; + + private long address; + private EntrySet entrySet; + private Map updatables; + private boolean threadSafe; + + private FlashMap(Class keyType, Class valueType) { + if (keyType == null || valueType == null) { + this.keyKind = null; + this.valueKind = null; + + this.keyType = null; + this.valueType = null; + + this.keyUpdatable = false; + this.valueUpdatable = false; + + } else { + this.keyKind = AtomKind.of(keyType); + this.valueKind = AtomKind.of(valueType); + + this.keyType = keyType; + this.valueType = valueType; + if (keyKind.isNotSupported() || valueKind.isNotSupported()) + throw new UnsupportedOperationException("Provided java types are not supported!"); + + this.keyUpdatable = Updatable.class.isAssignableFrom(keyType); + this.valueUpdatable = Updatable.class.isAssignableFrom(valueType); + + if (keyUpdatable) + throw new UnsupportedOperationException("Key as updatable is not supported!"); + if (valueUpdatable && Modifier.isAbstract(valueType.getModifiers())) + throw new UnsupportedOperationException("Abstract updatable value type is not supported!"); + } + + clearMap(); + } + + public static boolean hasHashMap(long address) { + int tableOffset = API.readInt(address, 16, 40, 236); + return tableOffset != 0 && API.readLong(address + tableOffset) != 0; + } + + public static FlashMap of(Class keyType, Class valueType) { + return new FlashMap<>(keyType, valueType); + } + + public static FlashMap ofUnknown() { + return new FlashMap<>(null, null); + } + + private static void updateIfChanged(Updatable u, long address) { + if (u.address != address) + u.update(address); + } + + public FlashMap makeThreadSafe() { + this.threadSafe = true; + return this; + } + + public void update() { + if (address == 0) return; + + long traits = readLong(16, 40); + + int tableOffset = API.readInt(traits, 236); + if (tableOffset == 0) return; + + boolean isDictionary = (API.readInt(traits, 248) & DICTIONARY_FLAG) != 0; + if (isDictionary) { + readHashTable(readLong(tableOffset) + 8); //read hash table ptr & skip cpp vtable + } else { + readHashTable(address + tableOffset); + } + } + + public void update(long address) { + if (this.address != address) { + clearMap(); + } + + this.address = address; + } + + private int getCapacity(int logCapacity, @SuppressWarnings("unused") boolean hasIterIndexes) { + if (logCapacity <= 0) return 0; + + // Math.pow(2, logCapacity - 1) + logCapacity = 1 << (logCapacity - 1); + //if (hasIterIndexes) logCapacity += 2; // for tests only + + return logCapacity * Long.BYTES; + } + + private void ensureBuffer(long atoms, int capacity) { + if (!threadSafe) { + API.readMemory(atoms, BUFFER, capacity); + } + } + + private long getLong(long atoms, int offset) { + if (threadSafe) return API.readLong(atoms + offset); + return ByteUtils.getLong(BUFFER, offset); + } + + private void readHashTable(long table) { + long atomsAndFlags = API.readLong(table); + long atoms = (atomsAndFlags & ByteUtils.ATOM_MASK) + 8; + + int size = API.readMemoryInt(table + 8); // includes deleted items + int capacity = getCapacity(API.readMemoryInt(table + 12), (atomsAndFlags & HAS_ITER_INDEX) != 0); + + //noinspection unused + boolean hasDeletedItems = (atomsAndFlags & HAS_DELETED_ITEMS) != 0; + + if (size <= 0 || size > MAX_SIZE || capacity <= 0 || capacity > MAX_CAPACITY) { + resetOldEntries(0); + resetUpdatablesIfNoRef(); + this.size = 0; + return; + } + if (entries.length < size) { + entries = Arrays.copyOf(entries, (int) Math.min(size * 1.25, MAX_SIZE)); + } + + ensureBuffer(atoms, capacity); + int currentSize = 0, realSize = 0; + for (int offset = 0; offset < capacity && currentSize < size; offset += 8) { + long keyAtom = getLong(atoms, offset); + + if (keyAtom == EMPTY_ITEM) continue; + if (keyAtom == DELETED_ITEM) { + offset += 8; + currentSize++; + continue; // skip deleted pair + } + + AtomKind keyKind = AtomKind.of(keyAtom); + + // if keyKind is Double, most-likely it is a weak key + if (keyKind == AtomKind.DOUBLE) { + keyKind = AtomKind.OBJECT; + + // it is no more an atom tagged pointer + keyAtom = API.readLong(keyAtom & ByteUtils.ATOM_MASK); + } + if (this.keyKind != null && keyKind != this.keyKind) { + System.out.println("Invalid keyKind! expected: " + this.keyKind + ", read: " + keyKind); + break; + } + + long valueAtom = getLong(atoms, (offset += 8)); + AtomKind valueKind = AtomKind.of(valueAtom); + if (this.valueKind != null && valueKind != this.valueKind) { + System.out.println("Invalid valueKind! expected: " + this.valueKind + ", read: " + valueKind); + break; + } + + Entry entry = entries[realSize]; + if (entry == null) + entry = entries[realSize] = new Entry(); + + entry.set(keyAtom, valueAtom, keyKind, valueKind); + + realSize++; + currentSize++; + } + + resetOldEntries(realSize); + resetUpdatablesIfNoRef(); + + this.size = realSize; + } + + private void clearMap() { + this.size = 0; + //noinspection unchecked + this.entries = (Entry[]) Array.newInstance(Entry.class, 0); + if (updatables != null) + updatables.forEach((k, wrapper) -> wrapper.resetReferences()); + } + + public T putUpdatable(K key, T updatable) { + if (valueType == null || !valueType.isInstance(updatable)) + throw new IllegalArgumentException("value type is unknown or given updatable is not instance of value type!"); + if (updatables == null) updatables = new HashMap<>(); + updatables.put(key, new UpdatableWrapper(updatable)); + + return updatable; + } + + public Updatable removeUpdatable(K key) { + if (updatables == null) return null; + UpdatableWrapper v = updatables.remove(key); + return v == null ? null : v.value; + } + + private int indexOf(Object key) { + for (int i = 0; i < size(); i++) { + Entry entry = entries[i]; + if (entry.getKey() == null) continue; + if (entry.getKey().equals(key)) return i; + } + + return -1; + } + + private void resetOldEntries(int realSize) { + for (int i = realSize; i < size(); i++) { + Entry entry = entries[i]; + if (entry != null) entry.reset(); + } + } + + private void resetUpdatablesIfNoRef() { + if (updatables != null) + updatables.forEach((k, wrapper) -> wrapper.resetIfNoReferences()); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set> entrySet() { + return entrySet == null ? entrySet = new EntrySet() : entrySet; + } + + @Override + public int size() { + return size; + } + + @Override + public V get(Object key) { + int i = indexOf(key); + return i == -1 ? null : entries[i].getValue(); + } + + @Override + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public long getAddress() { + return address; + } + + @Override + public String toString() { + return "FlashMap[" + size() + "]"; + } + + private static class UpdatableWrapper { + private final Updatable value; + + private int references; + + private UpdatableWrapper(Updatable value) { + this.value = value; + } + + private void resetIfNoReferences() { + if (references <= 0) + updateIfChanged(value, 0); + } + + private void resetReferences() { + references = 0; + updateIfChanged(value, 0); + } + + private void removeReference() { + references--; + } + + private void addReference() { + references++; + } + } + + private class Entry implements Map.Entry { + private long keyAtomCache, valueAtomCache; + + private K key; + private V value; + + private UpdatableWrapper wrapper; + + public Entry() { + if (keyUpdatable) + this.key = HeroManager.instance.main.pluginAPI.requireInstance(keyType); + if (valueUpdatable) + this.value = HeroManager.instance.main.pluginAPI.requireInstance(valueType); + } + + private void set(long keyAtom, long valueAtom, AtomKind keyKind, AtomKind valueKind) { + boolean keyChanged = keyAtomCache != keyAtom; + if (keyChanged) { + setKey(keyKind.readAtom(keyAtom, threadSafe)); + keyAtomCache = keyAtom; + } + + if (keyChanged || (valueAtomCache != valueAtom)) { + setVal(valueKind.readAtom(valueAtom, threadSafe)); + valueAtomCache = valueAtom; + } + + if (key instanceof Updatable) { + ((Updatable) key).update(); + } + + if (wrapper != null) { + wrapper.value.update(); + } else if (value instanceof Updatable) { + ((Updatable) value).update(); + } + } + + public K getKey() { + return key; + } + + private void setKey(K key) { + if (this.key instanceof Updatable) + updateIfChanged((Updatable) this.key, (long) key); + else this.key = key; + } + + public V getValue() { + //noinspection unchecked + return wrapper == null ? value : (V) wrapper.value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException("FlashEntry#setValue"); + } + + private void setVal(V value) { + if (keyKind != AtomKind.OBJECT && valueKind == AtomKind.OBJECT) { + + UpdatableWrapper v = updatables == null ? null : updatables.get(key); + if (v != null) { + if (v != wrapper) { + if (wrapper != null) + wrapper.removeReference(); + + v.addReference(); + wrapper = v; + } + } else if (wrapper != null) { + this.wrapper.removeReference(); + this.wrapper = null; + } + } + + if (wrapper != null) { + updateIfChanged(this.wrapper.value, (long) value); + } else if (this.value instanceof Updatable) { + updateIfChanged((Updatable) this.value, (long) value); + } else { + this.value = value; + } + } + + private void reset() { + if (key instanceof Updatable) + updateIfChanged((Updatable) key, 0); + + if (value instanceof Updatable) + updateIfChanged((Updatable) value, 0); + + if (wrapper != null) { + this.wrapper.removeReference(); + this.wrapper = null; + } + } + + @Override + public String toString() { + return "Entry{" + "key=" + key + ", value=" + getValue() + '}'; + } + } + + private class EntrySet extends AbstractSet> { + + @Override + public @NotNull Iterator> iterator() { + return new EntryIterator(); + } + + @Override + public int size() { + return size; + } + } + + private class EntryIterator implements Iterator> { + private int nextIdx; + + @Override + public boolean hasNext() { + return nextIdx < FlashMap.this.size; + } + + @Override + public Map.Entry next() { + return FlashMap.this.entries[nextIdx++]; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/manolo8/darkbot/core/utils/ByteUtils.java b/src/main/java/com/github/manolo8/darkbot/core/utils/ByteUtils.java index 0a97f10b1..c8edd3394 100644 --- a/src/main/java/com/github/manolo8/darkbot/core/utils/ByteUtils.java +++ b/src/main/java/com/github/manolo8/darkbot/core/utils/ByteUtils.java @@ -144,6 +144,31 @@ public static String readObjectName(long object) { return Main.API.readString(object, 0x10, 0x28, 0x90); } + public static String readObjectNameDirect(long object) { + return Main.API.readStringDirect(object, 0x10, 0x28, 0x90); + } + + public static String readStringDirect(long address) { + long sizeAndFlags = Main.API.readLong(address + 32); + + int size = (int) sizeAndFlags; // lower 32bits + if (size == 0) return ""; + + int flags = (int) (sizeAndFlags >> 32); // high 32bits + int width = (flags & 0b001); + + size <<= width; + if (size > 1024 || size < 0) return null; + + boolean dependent = ((flags & 0b110) >> 1) == ExtraMemoryReader.TYPE_DEPENDENT; + address = dependent + ? Main.API.readLong(address, 24, 16) + Main.API.readInt(address + 16) + : Main.API.readLong(address + 16); + + return new String(Main.API.readBytes(address, size), + width == ExtraMemoryReader.WIDTH_8 ? StandardCharsets.ISO_8859_1 : StandardCharsets.UTF_16LE); + } + public static class ExtraMemoryReader implements GameAPI.ExtraMemoryReader { private static final int TYPE_DYNAMIC = 0, TYPE_STATIC = 1, TYPE_DEPENDENT = 2; private static final int WIDTH_AUTO = -1, WIDTH_8 = 0, WIDTH_16 = 1; diff --git a/src/main/java/com/github/manolo8/darkbot/gui/titlebar/ExtraButton.java b/src/main/java/com/github/manolo8/darkbot/gui/titlebar/ExtraButton.java index 0a0c54983..4807f06d5 100644 --- a/src/main/java/com/github/manolo8/darkbot/gui/titlebar/ExtraButton.java +++ b/src/main/java/com/github/manolo8/darkbot/gui/titlebar/ExtraButton.java @@ -165,8 +165,8 @@ public Collection getExtraMenuItems(PluginAPI api) { list.add(create("Save SWF", e -> main.addTask(SWFUtils::dumpMainSWF))); list.add(create("Reset keybinds", e -> main.addTask(() -> main.guiManager.settingsGui.setKeyBinds(false)))); list.add(create("Object inspector", e -> { - JFrame frame = new ObjectInspectorUI(); - frame.setSize(600, 600); + JFrame frame = new ObjectInspectorUI((JMenuItem) e.getSource()); + frame.setSize(800, 600); frame.setVisible(true); })); } diff --git a/src/main/java/com/github/manolo8/darkbot/gui/titlebar/PinButton.java b/src/main/java/com/github/manolo8/darkbot/gui/titlebar/PinButton.java index dcca5ca92..0dc4477d6 100644 --- a/src/main/java/com/github/manolo8/darkbot/gui/titlebar/PinButton.java +++ b/src/main/java/com/github/manolo8/darkbot/gui/titlebar/PinButton.java @@ -11,7 +11,7 @@ public class PinButton extends TitleBarToggleButton { private static final Icon PIN = UIUtils.getIcon("pin"), UNPIN = UIUtils.getIcon("unpin"); - PinButton(JFrame frame) { + public PinButton(JFrame frame) { super(PIN, frame); setSelectedIcon(UNPIN); setToolTipText(I18n.get("gui.pin_button")); diff --git a/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ClassGenerator.java b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ClassGenerator.java new file mode 100644 index 000000000..9c0a4728e --- /dev/null +++ b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ClassGenerator.java @@ -0,0 +1,60 @@ +package com.github.manolo8.darkbot.gui.utils.inspector; + +import com.github.manolo8.darkbot.core.itf.Updatable; +import com.github.manolo8.darkbot.utils.debug.ObjectInspector; + +import java.util.List; +import java.util.StringJoiner; + +class ClassGenerator { + + static String generate(String name, List slots) { + String template = + "import {import};\n" + + "\n" + + "public class {className} extends {updatable} {\n" + + "\n" + + " {members};\n" + + "\n" + + " @Override\n" + + " public void update() {\n" + + " {membersRead};\n" + + " }\n" + + "}"; + template = template.replace("{className}", name); + template = template.replace("{import}", Updatable.class.getName()); + template = template.replace("{updatable}", Updatable.class.getSimpleName()); + + StringJoiner members = new StringJoiner(";\n "); + StringJoiner membersRead = new StringJoiner(";\n "); + + for (ObjectInspector.Slot slot : slots) { + String slotName = slot.name.replace("-", ""); + String read = "this." + slotName + " = "; + switch (slot.slotType) { + case INT: + case UINT: + case BOOLEAN: + members.add("private int " + slotName); + read += "readInt"; + break; + case DOUBLE: + members.add("private double " + slotName); + read += "readDouble"; + break; + case STRING: + members.add("private String " + slotName); + read += "readString"; + break; + default: + members.add("private long " + slotName); + read += "readLong"; + } + membersRead.add(read + "(" + slot.offset + ")"); + } + + template = template.replace("{members}", members.toString()); + template = template.replace("{membersRead}", membersRead.toString()); + return template; + } +} diff --git a/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/InspectorTitleBar.java b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/InspectorTitleBar.java new file mode 100644 index 000000000..18d9f5f30 --- /dev/null +++ b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/InspectorTitleBar.java @@ -0,0 +1,23 @@ +package com.github.manolo8.darkbot.gui.utils.inspector; + +import com.github.manolo8.darkbot.gui.titlebar.PinButton; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +public class InspectorTitleBar extends JMenuBar { + + public InspectorTitleBar(JFrame frame) { + setLayout(new MigLayout("fill, ins 0, gap 0")); + setBorder(BorderFactory.createEmptyBorder()); + + add(Box.createGlue(), "grow, push"); + add(new PinButton(frame) { + @Override + public void actionPerformed(ActionEvent e) { + frame.setAlwaysOnTop(isSelected()); + } + }); + } +} diff --git a/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/InspectorTree.java b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/InspectorTree.java index f387f9d21..d590f3f8b 100644 --- a/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/InspectorTree.java +++ b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/InspectorTree.java @@ -1,5 +1,6 @@ package com.github.manolo8.darkbot.gui.utils.inspector; +import com.github.manolo8.darkbot.core.utils.ByteUtils; import com.github.manolo8.darkbot.utils.SystemUtils; import com.github.manolo8.darkbot.utils.debug.ObjectInspector; @@ -10,10 +11,14 @@ import javax.swing.event.TreeWillExpandListener; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.ExpandVetoException; +import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; -import java.awt.*; +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -43,7 +48,7 @@ public void mouseClicked(MouseEvent e) { addTreeWillExpandListener(new TreeWillExpandListener() { @Override - public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { + public void treeWillExpand(TreeExpansionEvent event) { TreePath path = event.getPath(); if (path.getLastPathComponent() instanceof ObjectTreeNode) { ObjectTreeNode node = (ObjectTreeNode) path.getLastPathComponent(); @@ -52,7 +57,12 @@ public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException } @Override - public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { + public void treeWillCollapse(TreeExpansionEvent event) { + TreePath path = event.getPath(); + if (path.getLastPathComponent() instanceof ObjectTreeNode) { + ObjectTreeNode node = (ObjectTreeNode) path.getLastPathComponent(); + node.unloadChildren(InspectorTree.this); + } } }); @@ -62,6 +72,9 @@ public void ancestorAdded(AncestorEvent event) { if (timer == null) { timer = new Timer(delay, e -> { ((ObjectTreeNode) model.getRoot()).update(InspectorTree.this); + if (ObjectTreeNode.maxTextLengthChanged()) + notifyNodesChanged(model, (TreeNode) model.getRoot()); + invalidate(); }); timer.setRepeats(true); @@ -81,9 +94,19 @@ public void ancestorMoved(AncestorEvent event) { } }); - DefaultTreeCellRenderer cellRender = new DefaultTreeCellRenderer(); - cellRender.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); - setCellRenderer(cellRender); + setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + setCellRenderer(new InspectorTreeCellRenderer()); + } + + private static void notifyNodesChanged(DefaultTreeModel treeModel, TreeNode node) { + treeModel.nodeChanged(node); + node.children().asIterator() + .forEachRemaining(c -> notifyNodesChanged(treeModel, c)); + } + + @Override + public boolean hasBeenExpanded(TreePath path) { + return false; } @Override @@ -106,7 +129,7 @@ private ObjectTreeNode getSelectedNode() { private void editValue(boolean primitivesOnly) { ObjectTreeNode node = getSelectedNode(); if (node != null && node.isMemoryWritable()) { - ObjectInspector.Slot slot = (ObjectInspector.Slot) node.getUserObject(); + ObjectInspector.Slot slot = node.getSlot(); if (primitivesOnly && slot.slotType == ObjectInspector.Slot.Type.OBJECT) return; String result = JOptionPane.showInputDialog(getRootPane(), @@ -116,6 +139,29 @@ private void editValue(boolean primitivesOnly) { } } + private static class InspectorTreeCellRenderer extends DefaultTreeCellRenderer { + private Color bgColor; + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { + Component treeCellRendererComponent = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + ObjectTreeNode node = (ObjectTreeNode) value; + + bgColor = node.getBackgroundColor(sel ? super.getBackgroundSelectionColor() : super.getBackgroundNonSelectionColor()); + return treeCellRendererComponent; + } + + @Override + public Color getBackgroundNonSelectionColor() { + return bgColor == null ? super.getBackgroundNonSelectionColor() : bgColor; + } + + @Override + public Color getBackgroundSelectionColor() { + return bgColor == null ? super.getBackgroundSelectionColor() : bgColor; + } + } + private class ContextMenu extends JPopupMenu { public ContextMenu() { @@ -123,20 +169,68 @@ public ContextMenu() { JMenuItem copyValueItem = new JMenuItem("Copy value"); JMenuItem copyAddressItem = new JMenuItem("Copy address"); + JMenuItem copyClosureAddressItem = new JMenuItem("Copy class address"); + JMenuItem copyClassStructureItem = new JMenuItem("Copy class structure"); JMenuItem editValueItem = new JMenuItem("Edit value"); + JMenuItem editTypeNameItem = new JMenuItem("Edit type name"); copyValueItem.addActionListener(a -> { ObjectTreeNode node = getSelectedNode(); - if (node != null) SystemUtils.toClipboard(node.strValue); + if (node != null) SystemUtils.toClipboard(node.getText()); }); copyAddressItem.addActionListener(a -> { ObjectTreeNode node = getSelectedNode(); - if (node != null) SystemUtils.toClipboard(String.format("0x%x", node.address.get())); + if (node != null) SystemUtils.toClipboard(String.format("0x%x", node.address.getAsLong())); + }); + + copyClosureAddressItem.addActionListener(a -> { + ObjectTreeNode node = getSelectedNode(); + if (node != null) { + ObjectInspector.Slot slot = node.getSlot(); + if (slot.slotType == ObjectInspector.Slot.Type.OBJECT && !slot.name.contains("$")) { + SystemUtils.toClipboard(String.format("0x%x", ByteUtils.getClassClosure(node.value))); + } + } }); + + copyClassStructureItem.addActionListener(this::generateClass); editValueItem.addActionListener(a -> editValue(false)); + editTypeNameItem.addActionListener(a -> { + ObjectTreeNode node = getSelectedNode(); + if (node != null) { + ObjectInspector.Slot slot = node.getSlot(); + String result = JOptionPane.showInputDialog(getRootPane(), + "Edit type name of " + slot.type + " " + slot.name, "Edit type name", JOptionPane.PLAIN_MESSAGE); + if (result != null && !result.isEmpty()) + slot.setReplacement(result); + } + }); + + copyValueItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, 0)); + copyAddressItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0)); + copyClosureAddressItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, 0)); + copyClassStructureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0)); + editValueItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, 0)); + add(copyValueItem); add(copyAddressItem); + add(copyClosureAddressItem); + add(copyClassStructureItem); add(editValueItem); + add(editTypeNameItem); } - } + private void generateClass(ActionEvent e) { + ObjectTreeNode node = getSelectedNode(); + if (node != null) { + ObjectInspector.Slot slot = node.getSlot(); + if (slot.slotType == ObjectInspector.Slot.Type.OBJECT + || slot.slotType == ObjectInspector.Slot.Type.PLAIN_OBJECT) { + + String generated = ClassGenerator.generate(slot.name.replace("-", ""), + ObjectInspector.getObjectSlots(node.value)); + SystemUtils.toClipboard(generated); + } + } + } + } } \ No newline at end of file diff --git a/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ObjectInspectorUI.java b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ObjectInspectorUI.java index 52451b28a..049faf365 100644 --- a/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ObjectInspectorUI.java +++ b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ObjectInspectorUI.java @@ -6,27 +6,38 @@ import com.github.manolo8.darkbot.gui.MainGui; import net.miginfocom.swing.MigLayout; -import javax.swing.*; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JMenuItem; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.UIManager; import javax.swing.tree.DefaultTreeModel; -import java.awt.*; +import java.awt.Insets; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; import java.util.Map; import java.util.Objects; import java.util.function.BiConsumer; +import java.util.function.LongSupplier; import java.util.function.Supplier; public class ObjectInspectorUI extends JFrame { - public ObjectInspectorUI() { + public ObjectInspectorUI(JMenuItem menuItem) { super("Object Inspector"); setLayout(new MigLayout("ins 0, gap 0")); setSize(600, 600); setIconImage(MainGui.ICON); setLocationRelativeTo(null); - setAlwaysOnTop(true); - + setJMenuBar(new InspectorTitleBar(this)); DefaultTreeModel treeModel = new DefaultTreeModel(ObjectTreeNode.root("-", () -> 0L)); + + Object paintLines = UIManager.put("Tree.paintLines", true); InspectorTree treeView = new InspectorTree(treeModel); + UIManager.put("Tree.paintLines", paintLines); JComboBox addressCombo = new AddressCombo((name, addr) -> { ObjectTreeNode root = ObjectTreeNode.root(name, addr); @@ -41,26 +52,33 @@ public ObjectInspectorUI() { add(addressCombo, "grow"); add(delaySpinner, "wrap"); add(new JScrollPane(treeView), "push, grow, span"); + + menuItem.setEnabled(false); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + menuItem.setEnabled(true); + } + }); } private static class AddressCombo extends JComboBox { - public AddressCombo(BiConsumer> onRootUpdate) { + public AddressCombo(BiConsumer onRootUpdate) { putClientProperty("FlatLaf.style", Map.of("padding", new Insets(0, 5, 0, 5))); addActionListener(ae -> { - Supplier addrSupplier = getSelectedAddr(); - Long addr; + LongSupplier addrSupplier = getSelectedAddr(); + long addr; - if (addrSupplier == null || (addr = addrSupplier.get()) == null || addr == 0) { + if (addrSupplier == null || (addr = addrSupplier.getAsLong()) == 0) { putClientProperty("JComponent.outline", "error"); return; } else { putClientProperty("JComponent.outline", null); } - - String objectName = ByteUtils.readObjectName(addr); + String objectName = ByteUtils.readObjectNameDirect(addr); if (!Objects.equals(objectName, "ERROR")) { onRootUpdate.accept(objectName, addrSupplier); } @@ -79,10 +97,13 @@ public AddressCombo(BiConsumer> onRootUpdate) { addItem(new AddressEntry("Settings Address", b.settingsAddress::get)); } - private Supplier getSelectedAddr() { + private LongSupplier getSelectedAddr() { Object selectedItem = getSelectedItem(); if (selectedItem instanceof AddressEntry) { - return ((AddressEntry) selectedItem).address; + return () -> { + Long l = ((AddressEntry) selectedItem).address.get(); + return l == null ? 0 : l; + }; } else if (selectedItem instanceof String) { Long addr = parseAddress((String) selectedItem); if (addr != null) return () -> addr; diff --git a/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ObjectTreeNode.java b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ObjectTreeNode.java index 4faaebd1e..2cb11ee30 100644 --- a/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ObjectTreeNode.java +++ b/src/main/java/com/github/manolo8/darkbot/gui/utils/inspector/ObjectTreeNode.java @@ -1,74 +1,143 @@ package com.github.manolo8.darkbot.gui.utils.inspector; -import com.github.manolo8.darkbot.core.objects.swf.IntArray; -import com.github.manolo8.darkbot.core.objects.swf.ObjArray; -import com.github.manolo8.darkbot.core.objects.swf.PairArray; +import com.github.manolo8.darkbot.core.objects.swf.FlashList; +import com.github.manolo8.darkbot.core.objects.swf.FlashMap; import com.github.manolo8.darkbot.core.utils.ByteUtils; +import com.github.manolo8.darkbot.gui.utils.UIUtils; import com.github.manolo8.darkbot.utils.debug.ObjectInspector; import eu.darkbot.util.Popups; +import lombok.Getter; +import org.jetbrains.annotations.Nullable; -import javax.swing.*; +import javax.swing.JOptionPane; import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.TreePath; -import java.util.Locale; -import java.util.Vector; -import java.util.function.Supplier; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreeNode; +import java.awt.*; +import java.util.*; +import java.util.List; +import java.util.function.LongSupplier; import static com.github.manolo8.darkbot.Main.API; public class ObjectTreeNode extends DefaultMutableTreeNode { + private static int nameLastLength = 25, nameCurrentLength = 25; + private static int typeLastLength = 25, typeCurrentLength = 25; - public final Supplier address; - private final boolean addressIsValue; + @Getter protected String text; + @Getter protected ObjectInspector.Slot slot; - protected long value = -1; - protected String strValue; + protected LongSupplier address; + protected boolean staticAddress, readChildren; + protected long value = Long.MIN_VALUE; - public ObjectTreeNode(ObjectInspector.Slot slot, Supplier address, boolean addressIsValue) { - super(slot); + private double percentChange; + private long lastChange; + private int slotsCount; + + protected ObjectTreeNode(ObjectInspector.Slot slot, LongSupplier address, boolean staticAddress) { + this.slot = slot; this.address = address; - this.addressIsValue = addressIsValue; + this.staticAddress = staticAddress; + + //todo should keep slot count? + if (slot.size == 8 && slot.slotType != ObjectInspector.Slot.Type.DOUBLE + && slot.slotType != ObjectInspector.Slot.Type.STRING) { + long adr = staticAddress ? API.readLong(address.getAsLong()) : address.getAsLong(); + List objectSlots = ObjectInspector.getObjectSlots(adr); + slotsCount = objectSlots.size(); + } } - public static ObjectTreeNode root(String name, Supplier addr) { - return new ObjectTreeNode( - new ObjectInspector.Slot(name, "Object", null, 0, 8), - addr, true); + public static boolean maxTextLengthChanged() { + boolean changed = nameLastLength != nameCurrentLength || typeLastLength != typeCurrentLength; + nameLastLength = nameCurrentLength; + typeLastLength = typeCurrentLength; + + typeCurrentLength = 25; + nameCurrentLength = 25; + return changed; } - @Override - public boolean isLeaf() { - ObjectInspector.Slot slot = (ObjectInspector.Slot) (this.getUserObject()); - return value == 0 || slot.size < 8 || slot.type.equals("Number") || slot.type.equals("String"); + public static ObjectTreeNode root(String type, LongSupplier address) { + return new ObjectTreeNode(new ObjectInspector.Slot("-", type, null, 0, 8), address, false); } - @Override - public String toString() { - ObjectInspector.Slot slot = (ObjectInspector.Slot) (this.getUserObject()); - return String.format("%03X %-30s %-30s %s", - slot.offset, (slot.name.length() >= 30) ? (slot.name.substring(0, 27) + "...") : slot.name, slot.getType(), strValue); + private static String extractType(String slotName) { + int i = slotName.indexOf(".<"); + return i == -1 ? slotName : slotName.substring(0, i); + } + + private static String extractTemplateType(String slotName) { + int i = slotName.indexOf(".<"); + if (i == -1) return null; + + int j = slotName.lastIndexOf("::"); + return slotName.substring((j == -1 ? i : j) + 2, slotName.length() - 1); } - public void trimChildren(int maxSize) { - if (children == null) return; - while (children.size() > maxSize) { - remove(children.size() - 1); + private static ObjectTreeNode createNode(LongSupplier address, ObjectInspector.Slot slot, boolean staticAddress) { + long object = staticAddress ? API.readMemoryPtr(address.getAsLong()) : address.getAsLong(); + if (object == 0) new ObjectTreeNode(slot, address, staticAddress); + + boolean hasHashMap = FlashMap.hasHashMap(object); + + //todo move & fix in ObjectInspector#getObjectSlots method + if (slot.slotType == ObjectInspector.Slot.Type.ARRAY + || slot.slotType == ObjectInspector.Slot.Type.VECTOR + || slot.slotType == ObjectInspector.Slot.Type.DICTIONARY + || slot.slotType == ObjectInspector.Slot.Type.PLAIN_OBJECT + || slot.slotType == ObjectInspector.Slot.Type.OBJECT) { + + String name = ByteUtils.readObjectNameDirect(object); + if (!name.equals("ERROR")) { + String type = extractType(name); + String templateType = extractTemplateType(name); + + slot.setType(type); + if (templateType != null) + slot.setTemplateType(templateType); + slot.slotType = ObjectInspector.Slot.Type.of(slot); + } + } + + switch (slot.slotType) { + case ARRAY: + boolean denseUsed = API.readInt(object + 52) != 0; + return new CollectionNode(slot, address, staticAddress, hasHashMap && !denseUsed, false); + case DICTIONARY: + return new CollectionNode(slot, address, staticAddress, true, false); + case VECTOR: + return new CollectionNode(slot, address, staticAddress, false, true); + case PLAIN_OBJECT: + if (hasHashMap) + return new CollectionNode(slot, address, staticAddress, true, false); + default: + return new ObjectTreeNode(slot, address, staticAddress); } } + @Override + public boolean isLeaf() { + return address.getAsLong() <= 0xFFFF || (staticAddress && value < 0xFFFF) + || slot.size < 8 + || slot.slotType == ObjectInspector.Slot.Type.DOUBLE + || slot.slotType == ObjectInspector.Slot.Type.STRING; + } + public boolean isMemoryWritable() { - ObjectInspector.Slot slot = (ObjectInspector.Slot) getUserObject(); - return slot.slotType == ObjectInspector.Slot.Type.INT + return address.getAsLong() > 0xFFFF && + (slot.slotType == ObjectInspector.Slot.Type.INT || slot.slotType == ObjectInspector.Slot.Type.UINT || slot.slotType == ObjectInspector.Slot.Type.DOUBLE || slot.slotType == ObjectInspector.Slot.Type.BOOLEAN - || slot.slotType == ObjectInspector.Slot.Type.OBJECT; + || slot.slotType == ObjectInspector.Slot.Type.OBJECT); } public void memoryWrite(String text) { text = text.trim(); - ObjectInspector.Slot slot = (ObjectInspector.Slot) getUserObject(); switch (slot.slotType) { case BOOLEAN: Boolean result = null; @@ -77,217 +146,271 @@ public void memoryWrite(String text) { if (text.equalsIgnoreCase("false") || text.equalsIgnoreCase("0")) result = false; if (result != null) { - API.writeInt(address.get(), result ? 1 : 0); + API.writeInt(address.getAsLong(), result ? 1 : 0); return; } break; case INT: case UINT: try { - int i = Integer.parseInt(text); - API.writeInt(address.get(), i); + API.writeInt(address.getAsLong(), Integer.parseInt(text)); return; - } catch (NumberFormatException ignore) {} + } catch (NumberFormatException ignore) { + } break; case DOUBLE: try { - double v = Double.parseDouble(text); - API.writeLong(address.get(), Double.doubleToLongBits(v)); + API.writeLong(address.getAsLong(), Double.doubleToLongBits(Double.parseDouble(text))); return; - } catch (NumberFormatException ignore) {} + } catch (NumberFormatException ignore) { + } break; case OBJECT: Long l = ObjectInspectorUI.parseAddress(text); if (l != null) { - API.writeLong(address.get(), l); + API.writeLong(address.getAsLong(), l); return; } break; } String name = slot.slotType.name().toLowerCase(Locale.ROOT); Popups.of("Invalid " + name + " value", - "Expected a " + name + " value, but got '" + text + "'", - JOptionPane.ERROR_MESSAGE).showAsync(); + "Expected a " + name + " value, but got '" + text + "'", JOptionPane.ERROR_MESSAGE).showAsync(); } - public void update(InspectorTree tree) { - long address = this.address.get(); - if (address == 0) { - return; - } - - ObjectInspector.Slot slot = (ObjectInspector.Slot) (this.getUserObject()); + public @Nullable Color getBackgroundColor(Color bg) { + return percentChange > 0 ? UIUtils.darker(bg, 1 - percentChange * 0.25) : null; + } + public void update(InspectorTree tree) { + long address = this.address.getAsLong(); long oldValue = value; - if (addressIsValue) { - value = address; - } else { + if (staticAddress) { if (slot.size == 8) { - value = API.readMemoryLong(address); + value = API.readMemoryPtr(address); } else if (slot.size == 4) { value = API.readMemoryInt(address); } else if (slot.size == 1) { value = API.readInt(address); } - - // Non-leaf means it's an object, which we need to remove atom mask from. - if (!isLeaf()) value &= ByteUtils.ATOM_MASK; - } - - boolean changed = oldValue != value; - if (changed) { - if (slot.type.equals("Number")) { - strValue = String.format("%.2f", Double.longBitsToDouble(value)); - } else if (slot.type.equals("String")) { - strValue = value == 0 ? "null" : String.format("%s", API.readString(value)); - } else if (slot.type.equals("Boolean")) { - strValue = (value == 1) ? "true" : (value == 0 ? "false" : String.format("Boolean(%d)", this.value)); + } else value = address; + + if (oldValue != value) { + if (slot.slotType == ObjectInspector.Slot.Type.DOUBLE) { + text = String.format("%.3f", Double.longBitsToDouble(value)); + } else if (slot.slotType == ObjectInspector.Slot.Type.STRING) { + text = value == 0 ? "null" : String.format("%s", API.readStringDirect(value)); + } else if (slot.slotType == ObjectInspector.Slot.Type.BOOLEAN) { + text = (value == 1) ? "true" : (value == 0 ? "false" : String.format("Boolean(%d)", this.value)); } else if (slot.size == 4) { - strValue = String.format("%d", this.value); + text = String.format("%d", this.value); } else { - strValue = value == 0 ? "null" : String.format("0x%x", this.value); + text = value == 0 ? "null" : String.format("0x%x", this.value); + } + + // do not change color on init + if (lastChange == 0) { + lastChange = 1; + } else { + lastChange = System.currentTimeMillis(); + percentChange = 1; + } + tree.getModel().nodeChanged(this); + } else { + double percent = Math.max(0, (double) (lastChange + 1000 - System.currentTimeMillis()) / 1000); + if (this.percentChange != percent) { + this.percentChange = percent; + tree.getModel().nodeChanged(this); } } - if (children != null && tree.isExpanded(new TreePath(tree.getModel().getPathToRoot(this)))) { - children.forEach(child -> ((ObjectTreeNode) child).update(tree)); + updateChildren(tree, readChildren); + + if (children != null) { + for (TreeNode child : children) { + ObjectTreeNode node = (ObjectTreeNode) child; + node.update(tree); + } } - if (changed) tree.getModel().nodeChanged(this); + + nameCurrentLength = Math.max(nameCurrentLength, slot.name.length()); + typeCurrentLength = Math.max(typeCurrentLength, slot.getType().length()); + } + + protected void updateChildren(InspectorTree tree, boolean update) { } public void loadChildren(InspectorTree tree) { - if (children != null) return; - children = new Vector<>(); - - if (value == -1) update(tree); - - for (ObjectInspector.Slot childSlot : ObjectInspector.getObjectSlots(this.value)) { - ObjectTreeNode child; - switch (childSlot.type) { - case "Dictionary": - child = new DictionaryTreeNode(childSlot, () -> this.value + childSlot.offset); - break; - case "Array": - child = new ArrayTreeNode(childSlot, () -> API.readLong(this.value + childSlot.offset)); - break; - case "Vector": - child = new VectorTreeNode(childSlot, () -> this.value + childSlot.offset); - break; - default: - child = new ObjectTreeNode(childSlot, () -> this.value + childSlot.offset, false); - break; + update(tree); + for (ObjectInspector.Slot slot : ObjectInspector.getObjectSlots(this.value)) { + ObjectTreeNode child = createNode(() -> this.value + slot.offset, slot, true); + add(child); + } + + if (getChildCount() > 0) { + tree.getModel().nodeStructureChanged(this); + + for (TreeNode child : children) { + ObjectTreeNode node = (ObjectTreeNode) child; + node.update(tree); } - child.setParent(this); - children.add(child); } + } + + public void unloadChildren(InspectorTree tree) { + if (children != null) { + remove(tree, new ArrayList<>(children)); + } + } + + protected void remove(InspectorTree tree, List toRemove) { + for (TreeNode node : toRemove) { + tree.getModel().removeNodeFromParent((MutableTreeNode) node); + } + } + + protected void trimChildren(InspectorTree tree, int maxSize) { + if (children == null || maxSize >= children.size()) return; + + List list = new ArrayList<>(); + for (int i = maxSize; i < children.size(); i++) { + list.add(children.get(i)); + } + remove(tree, list); + } - tree.getModel().nodeStructureChanged(this); - children.forEach(ch -> ((ObjectTreeNode) ch).update(tree)); + @Override + public String toString() { + int nameLength = nameLastLength; + int typeLength = typeLastLength; + String format = (slot.isInArray ? "%03d" : "%03X") + " %-" + nameLength + "s %-" + typeLength + "s %s"; + + return String.format(format, slot.offset, + (slot.name.length() > nameLength) ? (slot.name.substring(0, nameLength - 3) + "...") : slot.name, + (slot.getType().length() > typeLength) ? (slot.getType().substring(0, typeLength - 3) + "...") : slot.getType(), + slot.isInArray ? slot.valueText : text) + + (slotsCount > 0 ? " [" + slotsCount + "]" : ""); } - private static class DictionaryTreeNode extends ObjectTreeNode { - public DictionaryTreeNode(ObjectInspector.Slot slot, Supplier address) { - super(slot, address, false); + private static class CollectionNode extends ObjectTreeNode { + private final FlashList list; + private final FlashMap map; + + private int oldSize; + + protected CollectionNode(ObjectInspector.Slot slot, LongSupplier address, boolean staticAddress, + boolean isMap, boolean isVector) { + super(slot, address, staticAddress); + if (isMap) { + this.map = FlashMap.ofUnknown().makeThreadSafe(); + this.list = null; + } else { + this.map = null; + this.list = (isVector ? FlashList.ofVectorUnknown() : FlashList.ofArrayUnknown()).makeThreadSafe(); + } } @Override public void loadChildren(InspectorTree tree) { - PairArray dict = PairArray.ofDictionary(); - dict.update(this.value); - dict.update(); - - for (int index = 0; index < dict.getSize(); index++) { - PairArray.Pair p = dict.get(index); - ObjectInspector.Slot valueSlot = new ObjectInspector.Slot(p.key, ByteUtils.readObjectName(p.value), null, index, 8); - ObjectTreeNode child = new ObjectTreeNode(valueSlot, () -> p.value, true); - tree.getModel().insertNodeInto(child, this, index); - child.update(tree); - } + readChildren = true; + update(tree); } - } - private static class ArrayTreeNode extends ObjectTreeNode { - public ArrayTreeNode(ObjectInspector.Slot slot, Supplier address) { - super(slot, address, false); + @Override + public void unloadChildren(InspectorTree tree) { + readChildren = false; + super.unloadChildren(tree); } @Override - public void update(InspectorTree tree) { - int denseLength = API.readInt(this.value + 0x28); - - if (denseLength != 0) { - IntArray arr = IntArray.ofArray(true); - arr.update(this.value); - - for (int index = 0; index < arr.elements.length; index++) { - ObjectInspector.Slot valueSlot = new ObjectInspector.Slot("", "int", null, index, 4); - ObjectTreeNode child = new ObjectTreeNode(valueSlot, () -> this.value, false); - tree.getModel().insertNodeInto(child, this, index); - child.update(tree); + protected void updateChildren(InspectorTree tree, boolean update) { + int size; + if (map != null) { + map.update(value); + map.update(); // read size of map; + size = map.size(); + } else { + list.update(value); + list.update(); + size = list.size(); + } + + if (oldSize != size) { + tree.getModel().nodeChanged(this); + oldSize = size; + } + + if (!update) return; + List insertedIndexes = new ArrayList<>(); + + int i = 0; + if (map != null) { + for (Map.Entry entry : map.entrySet()) { + if (updateChild(i, entry.getValue(), entry.getKey().toString(), tree.getModel())) + insertedIndexes.add(i); + i++; } - trimChildren(arr.getSize()); } else { - PairArray dict = PairArray.ofArray(); - dict.setAutoUpdatable(true); - dict.setIgnoreEmpty(false); - dict.update(this.value); - - int c = 0; - - for (int index = 0; index < dict.getSize(); index++) { - PairArray.Pair p = dict.get(index); - if (p == null) { - break; - } - c++; - String objectName = ByteUtils.readObjectName(p.value); - ObjectInspector.Slot valueSlot = new ObjectInspector.Slot(p.key, objectName, null, index, 8); - ObjectTreeNode child = new ObjectTreeNode(valueSlot, () -> p.value, true); - tree.getModel().insertNodeInto(child, this, index); + for (; i < list.size(); i++) { + if (updateChild(i, list.get(i), "", tree.getModel())) + insertedIndexes.add(i); } - trimChildren(c); } - } - @Override - public void loadChildren(InspectorTree model) { - // Children are populated in update method for arrays + if (!insertedIndexes.isEmpty()) + tree.getModel().nodesWereInserted(this, insertedIndexes.stream().mapToInt(Integer::intValue).toArray()); + trimChildren(tree, i); } - } - private static class VectorTreeNode extends ObjectTreeNode { - public VectorTreeNode(ObjectInspector.Slot slot, Supplier address) { - super(slot, address, false); + // true -> inserted + private boolean updateChild(int i, Object o, String name, DefaultTreeModel treeModel) { + ObjectInspector.Slot valueSlot = createSlot(name, o, i + 1); + if (getChildCount() > i) { + ObjectTreeNode child = (ObjectTreeNode) getChildAt(i); + + child.address = () -> o instanceof Number ? ((Number) o).longValue() : 0; + if (!child.slot.equals(valueSlot)) { + child.slot = valueSlot; + treeModel.nodeChanged(child); + } + } else { + ObjectTreeNode child = createNode(() -> o instanceof Number ? ((Number) o).longValue() : 0L, valueSlot, false); + add(child); + return true; + } + return false; } - @Override - public void loadChildren(InspectorTree tree) { - ObjectInspector.Slot slot = (ObjectInspector.Slot) (this.getUserObject()); - - if (slot.templateType.equals("uint") || slot.templateType.equals("int")) { - IntArray vec = IntArray.ofVector(this.value); - vec.update(); - for (int index = 0; index < vec.elements.length; index++) { - ObjectInspector.Slot valueSlot = new ObjectInspector.Slot("", slot.templateType, null, index, 4); - ObjectTreeNode child = new ObjectTreeNode(valueSlot, () -> this.value + slot.offset, false); - tree.getModel().insertNodeInto(child, this, index); - child.update(tree); + private ObjectInspector.Slot createSlot(String name, Object value, int offset) { + String type = ""; + String templateType = null; + + if (value instanceof Long) { + String s = ByteUtils.readObjectNameDirect((Long) value); + if (!s.equals("ERROR")) { + type = extractType(s); + templateType = extractTemplateType(s); } - trimChildren(vec.getSize()); } else { - ObjArray vec = ObjArray.ofVector(); - vec.update(this.value); - vec.update(); - for (int index = 0; index < vec.getSize(); index++) { - long object = vec.getPtr(index); - ObjectInspector.Slot valueSlot = new ObjectInspector.Slot("", ByteUtils.readObjectName(object), null, index, 8); - ObjectTreeNode child = new ObjectTreeNode(valueSlot, () -> object & ByteUtils.ATOM_MASK, true); - tree.getModel().insertNodeInto(child, this, index); - child.update(tree); + if (this.slot.templateType != null && !this.slot.templateType.equals("ERROR")) { + type = this.slot.templateType; + } else { + type = value.getClass().getSimpleName(); } - trimChildren(vec.getSize()); } + + int size = value instanceof Integer || value instanceof Boolean ? 4 : 8; + ObjectInspector.Slot slot = new ObjectInspector.Slot(name, type, templateType, offset, size); + + slot.isInArray = true; + slot.valueText = value instanceof Long ? String.format("0x%x", value) : value.toString(); + return slot; } - } + @Override + public String toString() { + if (value <= 0xFFFF) return super.toString(); + return super.toString() + " " + (map == null ? list : map); + } + } } diff --git a/src/main/java/com/github/manolo8/darkbot/utils/debug/ObjectInspector.java b/src/main/java/com/github/manolo8/darkbot/utils/debug/ObjectInspector.java index 793c59cb2..688650dec 100644 --- a/src/main/java/com/github/manolo8/darkbot/utils/debug/ObjectInspector.java +++ b/src/main/java/com/github/manolo8/darkbot/utils/debug/ObjectInspector.java @@ -4,160 +4,124 @@ import com.github.manolo8.darkbot.utils.OSUtil; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import static com.github.manolo8.darkbot.Main.API; public class ObjectInspector { - public enum TraitKind { - SLOT(0x00), - METHOD(0x01), - GETTER(0x02), - SETTER(0x03), - CLASS(0x04), - PAD(0x07), - CONST(0x06), - COUNT(0x07); - - private final int kind; - - TraitKind(int kind) { - this.kind = kind; - } - }; - - public static class Trait { - int name; - TraitKind kind; - int typeId = 0; - int id = 0; - int temp; + public static List getObjectSlots(long address) { + address = address & ByteUtils.ATOM_MASK; + // obj -> vtable -> vtable init -> vtable scope -> abc env -> const pool + long pool = API.readLong(address, 0x10, 0x10, 0x18, 0x10, 0x8); + long traits = API.readLong(address, 0x10, 0x28); + if (pool == 0 || traits == 0) + return Collections.emptyList(); + return getTraitsBinding(traits, pool); } - public static class Slot { - - public String name; - public String type; - public String templateType; - public long offset; - public long size; - public Type slotType; - - long getOffset() { - return offset; - } + private static List getTraitsBinding(long traits, long pool) { + List result = new ArrayList<>(); - public String getType() { - String type = this.type; - if (templateType != null && !templateType.equals("ERROR")) - type += "<" + templateType + ">"; + long offset = API.readInt(traits + 0xea) & 0xffff; + long base = API.readLong(traits + 0x10); - return type; - } + if (offset == 0 && base != 0) { + int hashTableOffset = API.readInt(base + 0xec); + int totalSize = API.readInt(base + 0xf0); - public String toString() { - return String.format("%03X - %s %s", offset, name, type); + offset = (hashTableOffset != 0) ? hashTableOffset : totalSize; + result = getTraitsBinding(base, pool); } - public Slot(String name, String type, String templateType, long offset, long size) { - this.name = name; - this.type = type; - this.templateType = templateType; - this.offset = offset; - this.size = size; - - this.slotType = Type.of(this); - } + int precompMnSize = API.readInt(pool + 0x98 + (OSUtil.isWindows() ? -0x18 : 0)); + long precompMn = API.readLong(pool + 0xe8 + (OSUtil.isWindows() ? -0x20 : 0)); - public enum Type { - INT("int"), - UINT("uint"), - BOOLEAN("Boolean"), - STRING("String"), - DOUBLE("Number"), - ARRAY("Array"), - VECTOR("Vector"), - DICTIONARY("Dictionary"), - OBJECT(null); + int slot32Count = 0; + int slotPointerCount = 0; + int slot64Count = 0; - private final String typeName; + List parsed_traits = parseTraitsInternal(traits); - Type(String typeName) { - this.typeName = typeName; + for (Trait trait : parsed_traits) { + if ((trait.kind != TraitKind.SLOT && trait.kind != TraitKind.CONST) || trait.name > precompMnSize) { + continue; } - static Type of(Slot slot) { - for (Type value : values()) { - if (slot.type.equals(value.typeName)) - return value; - } + long typeAddr = API.readLong(precompMn + (trait.typeId + 1) * 0x18L); - return slot.size >= 8 ? OBJECT : INT; + String typeName = API.readStringDirect(typeAddr); + + if (typeName.equals("Boolean") || typeName.equals("int") || typeName.equals("uint")) { + slot32Count++; + } else if (typeName.equals("Number")) { + slot64Count++; + } else { + slotPointerCount++; } } - } - private static class TraitsPtr { - long address; - byte[] bs = new byte[5]; - - TraitsPtr(long address) { - this.address = address; - } + long next32 = offset; + long nextPointer = offset + ((slot32Count * 4L + 4) & (~7)); + long next64 = nextPointer + slotPointerCount * 8L; - byte readByte() { - bs[0] = 0; - API.readMemory(this.address++, bs, 1); - return bs[0]; - } + for (Trait trait : parsed_traits) { + if ((trait.kind != TraitKind.SLOT && trait.kind != TraitKind.CONST) || trait.name > precompMnSize) { + continue; + } + long nameAddr = API.readLong(precompMn + (trait.name + 1) * 0x18L); + long typeAddr = API.readLong(precompMn + (trait.typeId + 1) * 0x18L); + String typeName = API.readStringDirect(typeAddr); + String templateType = null; - int readU32() { - Arrays.fill(bs, (byte) 0); - API.readMemory(this.address, bs,5); + long slotSize; - int result = bs[0]; - if ((result & 0x00000080) == 0) { - this.address++; - return result; - } - result = (result & 0x0000007f) | bs[1]<<7; - if ((result & 0x00004000) == 0) { - this.address += 2; - return result; - } - result = (result & 0x00003fff) | (int)bs[2]<<14; - if ((result & 0x00200000) == 0) { - this.address += 3; - return result; + // Get template type + if (typeName.equals("Vector") || typeName.equals("Dictionary")) { + int vectorTypeId = API.readInt(precompMn + (trait.typeId + 1) * 0x18L + 0x14); + templateType = API.readStringDirect(precompMn + (vectorTypeId + 1) * 0x18L, 0); } - result = (result & 0x001fffff) | (int)bs[3]<<21; - if ((result & 0x10000000) == 0) { - this.address+=4; - return result; + + if (typeName.equals("Boolean") || typeName.equals("int") || typeName.equals("uint")) { + offset = next32; + next32 += 4; + slotSize = 4; + } else if (typeName.equals("Number")) { + offset = next64; + next64 += 8; + slotSize = 8; + } else { + offset = nextPointer; + nextPointer += 8; + slotSize = 8; } - result = (result & 0x0fffffff) | (int)bs[4]<<28; - this.address += 5; - return result; + result.add(new Slot(API.readStringDirect(nameAddr), typeName, templateType, offset, slotSize)); } + + result.sort(Comparator.comparing(Slot::getOffset)); + return result; } private static List parseTraitsInternal(long address) { long base = API.readMemoryLong(address + 0x10); - List traits = new ArrayList(); + List traits = new ArrayList<>(); long traitsPos = API.readLong(address + 0xb0); - int posType = API.readInt(address + 0xf5) & 0xff; + int posType = API.readInt(address + 0xf5) & 0xff; TraitsPtr traitsPtr = new TraitsPtr(traitsPos); - if (posType == 0) - { - /* auto qname = */traitsPtr.readU32(); - /* auto sname = */traitsPtr.readU32(); + if (posType == 0) { + /* auto qname = */ + traitsPtr.readU32(); + /* auto sname = */ + traitsPtr.readU32(); byte flags = traitsPtr.readByte(); @@ -181,16 +145,16 @@ private static List parseTraitsInternal(long address) { int name = traitsPtr.readU32(); byte tag = traitsPtr.readByte(); - TraitKind kind = TraitKind.values()[((int)tag & 0xf)]; + TraitKind kind = TraitKind.values()[((int) tag & 0xf)]; trait.name = name; trait.kind = kind; - switch(kind) { + switch (kind) { case SLOT: - case CONST: - { - /* int slot_id = */ traitsPtr.readU32(); + case CONST: { + /* int slot_id = */ + traitsPtr.readU32(); int typeName = traitsPtr.readU32(); int vindex = traitsPtr.readU32(); // references one of the tables in the constant pool, depending on the value of vkind @@ -202,9 +166,9 @@ private static List parseTraitsInternal(long address) { } break; } - case CLASS: - { - /* int slot_id = */ traitsPtr.readU32(); + case CLASS: { + /* int slot_id = */ + traitsPtr.readU32(); int class_index = traitsPtr.readU32(); // is an index that points into the class array of the abcFile entry trait.id = class_index; @@ -212,12 +176,12 @@ private static List parseTraitsInternal(long address) { } case METHOD: case GETTER: - case SETTER: - { + case SETTER: { // The disp_id field is a compiler assigned integer that is used by the AVM2 to optimize the resolution of // virtual function calls. An overridden method must have the same disp_id as that of the method in the // base class. A value of zero disables this optimization. - /* int disp_id = */ traitsPtr.readU32(); + /* int disp_id = */ + traitsPtr.readU32(); int method_index = traitsPtr.readU32(); // is an index that points into the method array of the abcFile e trait.id = method_index; @@ -230,9 +194,10 @@ private static List parseTraitsInternal(long address) { // ATTR_metadata if ((tag & 0x40) != 0) { - int metadata_count = traitsPtr.readU32(); + int metadata_count = traitsPtr.readU32(); for (int j = 0; j < metadata_count; j++) { - /* int index = */ traitsPtr.readU32(); + /* int index = */ + traitsPtr.readU32(); } } traits.add(trait); @@ -240,93 +205,162 @@ private static List parseTraitsInternal(long address) { return traits; } - public static List getObjectSlots(long address) { - address = address & ByteUtils.ATOM_MASK; - // obj -> vtable -> vtable init -> vtable scope -> abc env -> const pool - long pool = API.readLong(address, 0x10, 0x10, 0x18, 0x10, 0x8); - long traits = API.readLong(address, 0x10, 0x28); - return getTraitsBinding(traits, pool); + public enum TraitKind { + SLOT(0x00), + METHOD(0x01), + GETTER(0x02), + SETTER(0x03), + CLASS(0x04), + PAD(0x07), // unused, exist only as a padding for next value + CONST(0x06), + COUNT(0x07); + + TraitKind(int kind) {} } - private static List getTraitsBinding (long traits, long pool) { - List result = new ArrayList(); + public static class Trait { + int name; + TraitKind kind; + int typeId = 0; + int id = 0; + int temp; + } - long offset = API.readInt(traits + 0xea) & 0xffff; - long base = API.readLong(traits + 0x10); + public static class Slot { + private static final Map TYPE_REPLACEMENTS = new HashMap<>(); - if (offset == 0 && base != 0) { - int hashTableOffset = API.readInt(base + 0xec); - int totalSize = API.readInt(base + 0xf0); + public String name; + public String type; + public String templateType; + public long offset; + public long size; + public Type slotType; - offset = (hashTableOffset != 0) ? hashTableOffset : totalSize; - result = getTraitsBinding(base, pool); + public boolean isInArray; + public String valueText; + + public Slot(String name, String type, String templateType, long offset, long size) { + this.name = name; + setType(type); + this.templateType = templateType; + this.offset = offset; + this.size = size; + + this.slotType = Type.of(this); } - int precompMnSize = API.readInt(pool + 0x98 + (OSUtil.isWindows() ? -0x18 : 0)); - long precompMn = API.readLong(pool + 0xe8 + (OSUtil.isWindows() ? -0x20 : 0)); + public void setType(String type) { + this.type = TYPE_REPLACEMENTS.getOrDefault(type, type); + } - int slot32Count = 0; - int slotPointerCount = 0; - int slot64Count = 0; + public void setTemplateType(String templateType) { + this.templateType = templateType; + } - List parsed_traits = parseTraitsInternal(traits); + public void setReplacement(String replacement) { + TYPE_REPLACEMENTS.put(type, replacement); + setType(replacement); + } - for (Trait trait : parsed_traits) { - if ((trait.kind != TraitKind.SLOT && trait.kind != TraitKind.CONST) || trait.name > precompMnSize) { - continue; - } + long getOffset() { + return offset; + } - long typeAddr = API.readLong(precompMn + (trait.typeId + 1) * 0x18); + public String getType() { + String type = this.type; + if (templateType != null && !templateType.equals("ERROR")) + type += "<" + templateType + ">"; - String typeName = API.readString(typeAddr); + return type; + } - if (typeName.equals("Boolean") || typeName.equals("int") || typeName.equals("uint")) { - slot32Count++; - } else if (typeName.equals("Number")) { - slot64Count++; - } else { - slotPointerCount++; - } + public String toString() { + return String.format("%03X - %s %s", offset, name, type); } - long next32 = offset; - long nextPointer = offset + ((slot32Count * 4 + 4) & (~7)); - long next64 = nextPointer + slotPointerCount * 8; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; - for (Trait trait : parsed_traits) { - if ((trait.kind != TraitKind.SLOT && trait.kind != TraitKind.CONST) || trait.name > precompMnSize) { - continue; - } - long nameAddr = API.readLong(precompMn + (trait.name + 1) * 0x18); - long typeAddr = API.readLong(precompMn + (trait.typeId + 1) * 0x18); - String typeName = API.readString(typeAddr); - String templateType = null; + Slot slot = (Slot) o; - long slotSize; + if (offset != slot.offset) return false; + if (size != slot.size) return false; + if (!Objects.equals(name, slot.name)) return false; + if (!Objects.equals(type, slot.type)) return false; + if (!Objects.equals(templateType, slot.templateType)) + return false; + return slotType == slot.slotType; + } - // Get template type - if (typeName.equals("Vector") || typeName.equals("Dictionary")) { - int vectorTypeId = API.readInt(precompMn + (trait.typeId + 1) * 0x18 + 0x14); - templateType = API.readString(precompMn + (vectorTypeId + 1) * 0x18, 0); + public enum Type { + INT("int"), + UINT("uint"), + BOOLEAN("Boolean"), + STRING("String"), + DOUBLE("Number"), + ARRAY("Array"), + VECTOR("Vector"), + DICTIONARY("Dictionary"), + PLAIN_OBJECT("Object"), + OBJECT(null); + + private final String typeName; + + Type(String typeName) { + this.typeName = typeName; } - if (typeName.equals("Boolean") || typeName.equals("int") || typeName.equals("uint")) { - offset = next32; - next32 += 4; - slotSize = 4; - } else if (typeName.equals("Number")) { - offset = next64; - next64 += 8; - slotSize = 8; - } else { - offset = nextPointer; - nextPointer += 8; - slotSize = 8; + public static Type of(Slot slot) { + for (Type value : values()) { + if (slot.type.equals(value.typeName)) + return value; + } + + return slot.size >= 8 ? OBJECT : INT; } - result.add(new Slot(API.readString(nameAddr), typeName, templateType, offset, slotSize)); } + } - result.sort(Comparator.comparing(Slot::getOffset)); - return result; + private static class TraitsPtr { + private long address; + + private TraitsPtr(long address) { + this.address = address; + } + + private byte readByte() { + byte[] bs = ByteUtils.getBytes(API.readInt(this.address++)); + return bs[0]; + } + + private int readU32() { + byte[] bs = ByteUtils.getBytes(API.readLong(this.address)); + + int result = bs[0]; + if ((result & 0x00000080) == 0) { + this.address++; + return result; + } + result = (result & 0x0000007f) | bs[1] << 7; + if ((result & 0x00004000) == 0) { + this.address += 2; + return result; + } + result = (result & 0x00003fff) | (int) bs[2] << 14; + if ((result & 0x00200000) == 0) { + this.address += 3; + return result; + } + result = (result & 0x001fffff) | (int) bs[3] << 21; + if ((result & 0x10000000) == 0) { + this.address += 4; + return result; + } + result = (result & 0x0fffffff) | (int) bs[4] << 28; + this.address += 5; + return result; + } } }