├── README.md ├── Variable.java ├── PackageTrie.java └── ScrollOfDebug.java /README.md: -------------------------------------------------------------------------------- 1 | # ScrollOfDebug 2 | A scroll that dynamically detects and provides an interface to create classes for the game [Shattered Pixel Dungeon](https://github.com/00-Evan/shattered-pixel-dungeon) and its mods while running the game. 3 | 4 | This repository can be added without impacting the functionality of the implementing project if desired. 5 | 6 | ## Installation 7 | 8 | 1. Obtain a fork of that is updated to be at least consistent with [v1.0.0](https://github.com/00-Evan/shattered-pixel-dungeon/releases/tag/v1.0.0). 9 | 10 | 2. Implement Scroll of Debug via one of the following methods described below. 11 | 3. Ensure the code compiles. 12 | * If the directory `shatteredpixel.shatteredpixeldungeon` was changed in the fork, a mass find and replace must be done for the scroll to work properly. 13 | 14 | ### Via `git pull` 15 | 16 | #### Identify The Correct Branch 17 | 18 | Scroll of Debug has 4 active "implementation" branches based on various places in Shattered's commit history as well as the history of Scroll of Debug itself. 19 | 20 | ```bash 21 | git pull https://github.com/zrp200/ScrollOfDebug shpd/VERSION/IMPLEMENTATION 22 | ``` 23 | Where: 24 | * `VERSION` is one of `1.0` or `1.3` 25 | * `IMPLEMENTATION` is one of `pc-only` or `apk_support` 26 | 27 | It is possible to upgrade from `1.0` to `1.3` and from `pc-only` to `apk_support` with no Scroll of Debug-related conflicts due to the nature of how the branches are implemented. 28 | 29 | If you want to avoid pulling in Scroll of Debug's admittably cursed commit history, add a `--squash` argument to the command. 30 | 31 | ##### Version 32 | Currently, the version ranges supported are: 33 | * [[v1.0.0](https://github.com/00-Evan/shattered-pixel-dungeon/releases/tag/v1.0.0), 34 | [v1.3.0](https://github.com/00-Evan/shattered-pixel-dungeon/releases/tag/v1.3.0)) 35 | (`shpd/1.0/` branches) 36 | * [v1.3.0](https://github.com/00-Evan/shattered-pixel-dungeon/releases/tag/v1.3.0) and newer. (`shpd/1.3/` branches) 37 | 38 | *Note*: Scroll of Debug is developed on the latest Shattered by default, so the `1.0` branches may be updated a bit more slowly when features have to be changed during backporting to ensure compatibility. 39 | 40 | ##### Implementation 41 | There are two variants of Scroll of Debug: the minimal `pc-only` implementation, and the invasive `apk_support` implementation that allows Scroll of Debug to work properly on Android devices. 42 | 43 | ###### `pc-only` 44 | The `pc-only` branch is a direct implementation of the project via the subtree method (described later). 45 | 46 | The only added change is to `GameScene.java`, where the Scroll of Debug is automatically added to the hero's inventory whenever `GameScene` is loaded when the game is in debug mode (`-INDEV` suffix), and removes it automatically if the game is not in debug mode. This specific implementation ensures that Scroll of Debug is always available to the developer while preventing unintended leaks of the scroll to players. 47 | 48 | However, this implementation does not have full functionality in Android builds --- the base implementation of Scroll of Debug will be unable to detect classes in `.apk` builds of the game, causing Scroll of Debug's reflection-based commands (`give`, `spawn`, `set`, `seed`, `use`, `inspect`) to not work properly (though variables may still work). 49 | 50 | **`pc-only` implementation is best when**: 51 | * You do not intend to share Scroll of Debug builds with others. 52 | * You want to keep Scroll of Debug confined to one directory. 53 | 54 | ###### `apk_support` (NEW) 55 | 56 | The `apk-support` implementation spreads out Scroll of Debug files across the implementing repository to ensure that Android builds have full functionality of Scroll of Debug. 57 | 58 | It moves one of the core files (`PackageTrie`) to `SPD-CLASSES`, amends `PlatformSupport` to allow a custom implementation of class retrieval, and then provides a custom Android implementation of this to ensure classes are read correctly. 59 | 60 | It is entirely incompatible with the subtree-based implementation of Scroll of Debug. 61 | 62 | *Note:* You can upgrade from the `pc-only` implementation (or subtree implementation) to the `apk_support` implementation by pulling in the `apk_support` branch at any time. The `apk_support` branch is based on `pc-only`. 63 | 64 | ### Via `git subtree` 65 | 66 | The master branch of the repository can be directly pulled using git subtrees: 67 | ```bash 68 | git subtree -P core/src/main/java/com/zrp200/scrollofdebug add https://github.com/zrp200/ScrollOfDebug master 69 | ``` 70 | It can then also be updated by replacing `add` with `pull` in the previous command. 71 | 72 | Implementing it this way is effectively identical to pulling in the `shpd/1.3/pc-only` branch, but it will lack the `GameScene.java` changes that let Scroll of Debug actually get added. 73 | 74 | As such, it is not the preferred way to implement it, but if you lack Shattered's commit history (or desire a custom method of giving access to scroll of debug in-game) for any reason it may be the only viable option for implementation. 75 | 76 | ### Via copy-paste 77 | 78 | There is virtually no difference between the subtree method and straight copy-pasting the files into `core/src/main/java/com/zrp200/scrollofdebug`. 79 | 80 | ## Usage 81 | 82 | Scroll of Debug lets the reader create virtually any game-related object with a single command. 83 | 84 | A more actively updated documentation can be found in the [wiki](https://github.com/Zrp200/ScrollOfDebug/wiki). 85 | 86 | The main commands currently are: 87 | * `give [+]` --- places an item of the corresponding `item` class into your inventory. The level can be optionally specified. 88 | * `spawn [-p]` --- spawns a given `mob` somewhere on the level. `-p` lets you place the mob. 89 | * `affect [duration]` applies the specified `buff` to a selected mob. 90 | * `seed ` --- 91 | * `use [ARGS...]` --- which uses the corresponding method of the provided class. A method can also be appended to the end of the `give`, `spawn`, and `affect` commands to act on the created entity. 92 | * `inspect ` gives a list of defined methods and fields for the specified class. 93 | * `goto ` --- warps you to the target depth immediately. 94 | * `help [command]` --- gives an overview on all supported commands. Specifying the command will give more information about that specific command. 95 | 96 | ### Autofilled entities 97 | The following classes are autofilled with the corresponding Dungeon object by default. 98 | * `hero` < `Dungeon.hero` 99 | * `level` < `Dungeon.level` 100 | 101 | ### Variables 102 | Sometimes we need to run multiple methods on a created object or otherwise store an object for later commands. 103 | 104 | Scroll of Debug supports **variables** to make this possible. 105 | 106 | #### Storage of Variables 107 | 108 | The result of methods that create objects can be stored by starting the command with `@` where `` is the name you want it to be stored as. 109 | 110 | Alternatively, existing entities can be selected directly by using `@ cell` to select a cell or `@ inv` to select from the inventory. 111 | 112 | #### Usage of variables 113 | Once created, the variable can be used by using `@` in place of any class or argument of a command. -------------------------------------------------------------------------------- /Variable.java: -------------------------------------------------------------------------------- 1 | package com.zrp200.scrollofdebug; 2 | 3 | import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; 4 | import com.shatteredpixel.shatteredpixeldungeon.items.Item; 5 | import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; 6 | import com.shatteredpixel.shatteredpixeldungeon.scenes.CellSelector; 7 | import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; 8 | import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; 9 | import com.shatteredpixel.shatteredpixeldungeon.windows.WndBag; 10 | import com.shatteredpixel.shatteredpixeldungeon.windows.WndOptions; 11 | import com.watabou.noosa.Game; 12 | 13 | import java.lang.reflect.Method; 14 | import java.util.ArrayList; 15 | import java.util.HashMap; 16 | 17 | abstract public class Variable { 18 | @SuppressWarnings("rawtypes") 19 | public static final HashMap assigned = new HashMap<>(); 20 | 21 | public static final String MARKER = "@"; 22 | 23 | static void putFromInventory(String key) { 24 | GameScene.selectItem(new WndBag.ItemSelector() { 25 | @Override 26 | public String textPrompt() { return "Select an item"; } 27 | @Override 28 | public boolean itemSelectable(Item item) { return !(item instanceof ScrollOfDebug); } 29 | @Override 30 | public void onSelect(Item item) { put(key, item); } 31 | }); 32 | } 33 | 34 | static void putFromCell(String key) { 35 | GameScene.selectCell(new CellSelector.Listener() { 36 | @SuppressWarnings("unchecked") 37 | @Override 38 | public void onSelect(Integer cell) { 39 | if (cell == null || cell == -1) return; 40 | // determine objects of interest. 41 | // find stuff of interest 42 | // accessing private methods :/ 43 | final ArrayList objects = new ArrayList<>(); 44 | final ArrayList objectNames = new ArrayList<>(); 45 | try { 46 | Method 47 | getObjectsAtCell = GameScene.class.getDeclaredMethod("getObjectsAtCell", int.class), 48 | getObjectNames = GameScene.class.getDeclaredMethod("getObjectNames", ArrayList.class); 49 | // getting around the private declaration 50 | // fixme this may not work properly on mobile. 51 | getObjectNames.setAccessible(true); 52 | getObjectsAtCell.setAccessible(true); 53 | // now using the functions 54 | objects.addAll((ArrayList) getObjectsAtCell.invoke(null, cell)); 55 | objectNames.addAll((ArrayList) getObjectNames.invoke(null, objects)); 56 | } catch (Exception e) { 57 | // maybe copy paste the shattered implementation? 58 | // currently we just pretend nothing else is there. 59 | Game.reportException(e); 60 | } 61 | if (objects.isEmpty()) { 62 | put(key, cell); 63 | } else { 64 | objects.add(0, cell); 65 | objectNames.add(0, "cell (" + cell + ")"); // include the actual value of the cell 66 | GameScene.show(new WndOptions(Messages.get(GameScene.class, "multiple"), 67 | "", objectNames.toArray(new String[0])) { 68 | @Override protected void onSelect(int index) { put(key, objects.get(index)); } 69 | }); 70 | } 71 | } 72 | 73 | @Override public String prompt() { return "Choose a location to target"; } 74 | }); 75 | } 76 | 77 | static void put(String key, T o) { 78 | if (key == null || o == null) return; // no variable to store. 79 | Variable v = wrap(o); 80 | assigned.put(key, v); 81 | GLog.p("%s = %s", key, v); 82 | } 83 | 84 | static Object get(String key) { 85 | if(key.startsWith(MARKER)) { 86 | Variable v = assigned.get(key); 87 | if(v != null) return v.getTarget(); 88 | } 89 | return null; 90 | } 91 | 92 | // note this can still have situational weird behavior, but this should mostly prevent unintentional variable usage. 93 | static T get(String key, Class expectedClass) { 94 | Object o = get(key); 95 | if(expectedClass.isPrimitive()) { 96 | // wrap it. 97 | Class wrapper; 98 | if(expectedClass == int.class) wrapper = Integer.class; 99 | else if(expectedClass == char.class) wrapper = Character.class; 100 | else try { 101 | // all other wrappers are just capitalized primitives 102 | wrapper = Class.forName("java.lang." + Messages.capitalize(expectedClass.getSimpleName())); 103 | } catch (Exception e) { wrapper = expectedClass; } 104 | //change expected class to the wrapper. 105 | //noinspection unchecked 106 | expectedClass = (Class)wrapper; 107 | } 108 | //noinspection unchecked 109 | return expectedClass.isInstance(o) ? (T)o : null; 110 | } 111 | 112 | abstract T getTarget(); 113 | 114 | boolean isActive() { 115 | return getTarget() != null; 116 | } 117 | 118 | @Override 119 | public int hashCode() { 120 | T target = getTarget(); 121 | return target != null ? target.hashCode() : 0; 122 | } 123 | 124 | @Override 125 | public boolean equals(Object obj) { 126 | return obj != null && obj.getClass() == getClass() && hashCode() == obj.hashCode(); 127 | } 128 | 129 | static String toString(String key) { 130 | Variable v = assigned.get(key); 131 | return v != null ? v.toString() : null; 132 | } 133 | 134 | @Override 135 | public String toString() { 136 | T target = getTarget(); 137 | if (target == null) return null; 138 | Class c = target.getClass(); 139 | try { 140 | // if we have a pretty toString, use that instead. 141 | if (c.isPrimitive() || c.getMethod("toString").getDeclaringClass() != Object.class) { 142 | return target.toString(); 143 | } else { 144 | // check for dedicated "name" method that is often implemented by game objects 145 | return (String)c.getMethod("name").invoke(target); 146 | } 147 | } catch (Exception e) { return c.getSimpleName(); } 148 | } 149 | 150 | @SuppressWarnings("unchecked") 151 | static Variable wrap(T target) { 152 | return target instanceof Actor ? (Variable) new ActorVariable((Actor)target) : 153 | new Variable() { @Override T getTarget() { return target; } }; 154 | } 155 | 156 | // actors can be retrieved by id instead of by reference 157 | public static class ActorVariable extends Variable { 158 | private final int id; 159 | 160 | ActorVariable(Actor actor) { 161 | id = actor.id(); 162 | } 163 | 164 | @Override 165 | Actor getTarget() { return Actor.findById(id); } 166 | 167 | @Override 168 | public String toString() { 169 | String toString = super.toString(); 170 | return toString != null ? toString + " (id=" + id + ")" : null; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /PackageTrie.java: -------------------------------------------------------------------------------- 1 | package com.zrp200.scrollofdebug; 2 | 3 | import static java.util.Collections.*; 4 | 5 | import com.badlogic.gdx.utils.reflect.ReflectionException; 6 | import com.watabou.noosa.Game; 7 | import com.watabou.utils.Reflection; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.io.UnsupportedEncodingException; 12 | import java.net.*; 13 | import java.util.*; 14 | import java.util.jar.JarEntry; 15 | import java.util.jar.JarFile; 16 | 17 | public class PackageTrie { 18 | 19 | /** package of core game files (for example com.shatteredpixel.shatteredpixeldungeon) **/ 20 | private final String ROOT; 21 | public PackageTrie(String ROOT) {this.ROOT = ROOT;} 22 | 23 | public PackageTrie() { this("com.shatteredpixel.shatteredpixeldungeon"); } // backwards compatibility 24 | 25 | private final HashMap subTries = new HashMap<>(); 26 | private final ArrayList> classes = new ArrayList<>(); 27 | 28 | public final Map getSubtries() { return unmodifiableMap(subTries); } 29 | public final List> getClasses() { return unmodifiableList(classes); } 30 | 31 | protected void add(String pkg, PackageTrie tree) { 32 | if(!tree.isEmpty()) subTries.put(pkg, tree); 33 | } 34 | 35 | /** finds a package somewhere in the trie. 36 | * 37 | * fixme/todo This is not used for #findClass because it stops at first match. If it returned a list it would work, probably. 38 | * fixme this (and #findClass) do not handle duplicated results well at all. This isn't an issue for me, but it COULD be an issue. 39 | **/ 40 | public PackageTrie findPackage(String name) { 41 | return findPackage(name.split("\\."), 0); 42 | } 43 | public PackageTrie findPackage(String[] path, int index) { 44 | if(index == path.length) return this; 45 | 46 | PackageTrie 47 | match = getPackage(path[index]), 48 | found = match != null ? match.findPackage(path, index+1) : null; 49 | 50 | if(found != null) return found; 51 | for(PackageTrie trie : subTries.values()) { 52 | if(trie == match) continue; 53 | found = trie.findPackage(path, 0); 54 | if(found != null) return found; 55 | } 56 | return null; 57 | } 58 | 59 | public Class findClass(String name, Class parent) { 60 | // first attempt to blindly match the class 61 | Class match = null; 62 | try { 63 | match = Reflection.forNameUnhandled(name); 64 | } catch (ReflectionException e) { 65 | if(ROOT != null && !name.startsWith(ROOT)) { 66 | try { 67 | match = Reflection.forNameUnhandled(ROOT + "." + name); 68 | } 69 | catch (ReflectionException ignored) {/* do nothing */} 70 | catch (Exception e1) { 71 | e1.addSuppressed(e); 72 | Game.reportException(e1); 73 | } 74 | } 75 | } catch(Exception e) {Game.reportException(e);} 76 | if (match != null && parent.isAssignableFrom(match)) { 77 | // add it to the trie if possible 78 | String pkg = match.getPackage().getName(); 79 | addClass(match, pkg.substring(pkg.indexOf(ROOT + ".") + 1)); 80 | return match; 81 | } 82 | // now match it from stored classes 83 | match = findClass(name.split("\\."), parent, 0); 84 | return match; 85 | } 86 | // known issues: duplicated classes may mask each other. 87 | public Class findClass(String[] path, Class parent, int i) { 88 | if(i == path.length) return null; 89 | 90 | Class found = null; 91 | PackageTrie match = null; 92 | if(i+1 < path.length) { 93 | match = getPackage(path[i]); 94 | if (match != null) { 95 | found = match.findClass(path, parent, i + 1); 96 | if (found != null && (parent == null || parent.isAssignableFrom(found)) ) return found; 97 | } 98 | } else if( ( found = getClass(path[i]) ) != null && (parent == null || parent.isAssignableFrom(found))) return found; 99 | else found = null; 100 | ArrayList toSearch = new ArrayList(subTries.values()); 101 | toSearch.remove(match); 102 | for(PackageTrie tree : toSearch) if( (found = tree.findClass(path,parent,i)) != null ) break; 103 | return found; 104 | } 105 | 106 | // does not deep search 107 | public PackageTrie getPackage(String packageName) { 108 | return subTries.get(packageName); 109 | } 110 | // this is probably not efficient or even taking advantage of what I've done. 111 | public Class getClass(String className) { 112 | boolean hasQualifiers = className.contains("$") || className.contains("."); 113 | for(Class cls : classes) { 114 | boolean match = hasQualifiers 115 | ? cls.getName().toLowerCase(Locale.ROOT).endsWith( className.toLowerCase(Locale.ROOT) ) 116 | : cls.getSimpleName().equalsIgnoreCase(className); 117 | if(match) return cls; 118 | } 119 | return null; 120 | } 121 | 122 | public ArrayList getAllClasses() { 123 | ArrayList classes = new ArrayList(this.classes); 124 | for(PackageTrie tree : subTries.values()) classes.addAll(tree.getAllClasses()); 125 | return classes; 126 | } 127 | 128 | public boolean isEmpty() { return subTries.isEmpty() && classes.isEmpty(); } 129 | 130 | protected final PackageTrie getOrCreate(String pkg) { 131 | if(pkg == null || pkg.isEmpty()) return this; 132 | String[] split = pkg.split("\\.", 2); 133 | // [0] is stored, [1] is recursively added. 134 | PackageTrie stored = subTries.get(split[0]); 135 | if(stored == null) subTries.put(split[0], stored = new PackageTrie()); 136 | return split.length == 1 ? stored : stored.getOrCreate(split[1]); 137 | } 138 | protected final void addClass(Class cls, String pkg) { 139 | String clsPkg = cls.getPackage().getName(); 140 | if(clsPkg.equals(pkg)) classes.add(cls); 141 | else if(clsPkg.startsWith(pkg)) getOrCreate(clsPkg.substring(pkg.length()+1)).classes.add(cls); 142 | } 143 | 144 | /** 145 | * Attempts to list all the classes in the specified package as determined 146 | * by the context class loader 147 | * 148 | * @link https://stackoverflow.com/a/22462785/4258976 149 | * 150 | * @implNote I modified it to work with a trie, but that implementation will still get all classes. 151 | * 152 | * @param pckgname 153 | * the package name to search 154 | * @return a trie of classes found in that package. 155 | * @throws ClassNotFoundException 156 | * if something went wrong 157 | */ 158 | public static PackageTrie getClassesForPackage(String pckgname) 159 | throws ClassNotFoundException { 160 | PackageTrie root = new PackageTrie(); 161 | ClassLoader loader = PackageTrie.class.getClassLoader(); 162 | 163 | try { 164 | if (loader == null) throw new ClassNotFoundException("Can't get class loader."); 165 | 166 | final Enumeration resources = loader.getResources(pckgname.replace('.', '/')); 167 | URLConnection connection; 168 | 169 | while(resources.hasMoreElements()) { 170 | URL url = resources.nextElement(); 171 | if(url == null) break; 172 | try { 173 | connection = url.openConnection(); 174 | 175 | if (connection instanceof JarURLConnection) { 176 | checkJarFile((JarURLConnection) connection, pckgname, root); 177 | } else if (url.getProtocol().equals("file")) { 178 | try { 179 | checkDirectory( 180 | new File(URLDecoder.decode(url.getPath(), 181 | "UTF-8")), pckgname, root); 182 | } catch (final UnsupportedEncodingException ex) { 183 | throw new ClassNotFoundException( 184 | pckgname + " does not appear to be a valid package (Unsupported encoding)", 185 | ex); 186 | } 187 | } else 188 | throw new ClassNotFoundException( 189 | pckgname +" ("+ url.getPath() +") does not appear to be a valid package"); 190 | } catch (final IOException ioex) { 191 | throw new ClassNotFoundException( 192 | "IOException was thrown when trying to get all resources for " 193 | + pckgname, ioex); 194 | } 195 | } 196 | } catch (final NullPointerException ex) { 197 | throw new ClassNotFoundException( 198 | pckgname+" does not appear to be a valid package (Null pointer exception)", 199 | ex); 200 | } catch (final IOException ioex) { 201 | throw new ClassNotFoundException( 202 | "IOException was thrown when trying to get all resources for " 203 | + pckgname, ioex); 204 | } 205 | return root; 206 | } 207 | private static PackageTrie checkDirectory(File directory, String pckgname, PackageTrie trie) throws ClassNotFoundException { 208 | File tmpDirectory; 209 | 210 | if (directory.exists() && directory.isDirectory()) { 211 | final String[] files = directory.list(); 212 | 213 | for (final String file : files) { 214 | if (file.endsWith(".class")) { 215 | try { 216 | Class cls = Class.forName(pckgname + '.' 217 | + file.substring(0, file.length() - 6)); 218 | //if(canInstantiate(cls)) 219 | trie.classes.add(cls); 220 | } catch (final NoClassDefFoundError e) { 221 | // do nothing. this class hasn't been found by the 222 | // loader, and we don't care. 223 | } 224 | } else if ( (tmpDirectory = new File(directory, file) ).isDirectory()) { 225 | trie.add(file, checkDirectory(tmpDirectory, pckgname + "." + file, new PackageTrie())); 226 | } 227 | } 228 | } 229 | return trie; 230 | } 231 | private static void checkJarFile(JarURLConnection connection, 232 | String pckgname, 233 | PackageTrie tree) 234 | throws ClassNotFoundException, IOException { 235 | final JarFile jarFile = connection.getJarFile(); 236 | final Enumeration entries = jarFile.entries(); 237 | 238 | while(entries.hasMoreElements()) { 239 | JarEntry jarEntry = entries.nextElement(); 240 | if(jarEntry == null) break; 241 | 242 | String name = jarEntry.getName(); 243 | int index = name.indexOf(".class"); 244 | if(index == -1) continue; 245 | 246 | name = name.substring(0, index) 247 | .replace('/', '.'); 248 | if (name.contains(pckgname) /*&& canInstantiate(cls = Class.forName(name))*/) { 249 | tree.addClass(Class.forName(name),pckgname); 250 | } 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /ScrollOfDebug.java: -------------------------------------------------------------------------------- 1 | package com.zrp200.scrollofdebug; 2 | 3 | import static com.shatteredpixel.shatteredpixeldungeon.Dungeon.*; 4 | // backwards compatible until v0.9.4, before, returned void (21de6d38) 5 | import static com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation.teleportToLocation; 6 | import static java.util.Arrays.copyOfRange; 7 | 8 | import com.shatteredpixel.shatteredpixeldungeon.GamesInProgress; 9 | import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.Scroll; 10 | 11 | import com.badlogic.gdx.utils.StringBuilder; 12 | import com.shatteredpixel.shatteredpixeldungeon.Dungeon; 13 | // Commands 14 | import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; 15 | import com.shatteredpixel.shatteredpixeldungeon.actors.Char; 16 | import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob; 17 | import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; 18 | import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.FlavourBuff; 19 | import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; 20 | import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob; 21 | import com.shatteredpixel.shatteredpixeldungeon.items.Item; 22 | import com.shatteredpixel.shatteredpixeldungeon.items.bags.Bag; 23 | import com.shatteredpixel.shatteredpixeldungeon.items.potions.Potion; 24 | import com.shatteredpixel.shatteredpixeldungeon.levels.Level; 25 | import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain; 26 | import com.shatteredpixel.shatteredpixeldungeon.levels.traps.Trap; 27 | import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; 28 | import com.shatteredpixel.shatteredpixeldungeon.scenes.CellSelector; 29 | import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; 30 | // needed for HelpWindow 31 | import com.shatteredpixel.shatteredpixeldungeon.scenes.PixelScene; 32 | 33 | import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; 34 | import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet; 35 | import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator; 36 | import com.shatteredpixel.shatteredpixeldungeon.ui.RenderedTextBlock; 37 | import com.shatteredpixel.shatteredpixeldungeon.ui.ScrollPane; 38 | import com.shatteredpixel.shatteredpixeldungeon.ui.Window; 39 | // WndTextInput (added in v0.9.4) 40 | import com.shatteredpixel.shatteredpixeldungeon.windows.WndTextInput; 41 | // Output 42 | import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; 43 | 44 | import com.watabou.noosa.Game; 45 | import com.watabou.noosa.ui.Component; 46 | import com.watabou.utils.Bundle; 47 | import com.watabou.utils.Callback; 48 | import com.watabou.utils.FileUtils; 49 | import com.watabou.utils.Reflection; 50 | 51 | import java.io.IOException; 52 | import java.io.PrintWriter; 53 | import java.io.StringWriter; 54 | import java.lang.reflect.*; 55 | import java.util.*; 56 | import java.util.regex.Matcher; 57 | import java.util.regex.Pattern; 58 | 59 | /** 60 | * Scroll of Debug uses ClassLoader to get every class that can be directly created and provides a command interface with which to interact with them. 61 | * 62 | * 63 | * @author 64 | * Zrp200 65 | * @version v2.1.0 66 | * 67 | * @apiNote Compatible with Shattered Pixel Dungeon v1.3.0+, and compatible with any LibGDX Shattered Pixel Dungeon version (post v0.8) with minimal changes. 68 | * **/ 69 | @SuppressWarnings({"rawtypes", "unchecked"}) 70 | public class ScrollOfDebug extends Scroll { 71 | { 72 | image = ItemSpriteSheet.SCROLL_HOLDER; 73 | } 74 | 75 | static String lastCommand = ""; // used with '!!' 76 | 77 | /** this is where all the game files are supposed to be located. **/ 78 | private static final String ROOT = "com.shatteredpixel.shatteredpixeldungeon"; 79 | 80 | private enum Command { 81 | HELP(null, // ... 82 | "[COMMAND | all]", 83 | "Gives more information on commands", 84 | "Specifying a command after the help will give an explanation for how to use that command."), 85 | // todo add more debug-oriented commands 86 | CHANGES(null, "", "Gives a history of changes to Scroll of Debug."), 87 | // generation commands. 88 | GIVE(Item.class, 89 | " [+] [x] [-f|--force] [ [] ]", 90 | "Creates and puts into your inventory the generated item", 91 | "Any method specified will be called prior to collection.", 92 | "Specifying _level_ will set the level of the item to the indicated amount using Item#level. This is the method called when restoring items from a save file. If it's not giving you want you want, please try passing \"upgrade\" as your method.", 93 | "_--force_ (or _-f_ for short) will disable all on-pickup logic (specifically Item#doPickUp) that may be affecting how the item gets collected into your inventory."), 94 | SPAWN(Mob.class, 95 | " [x|(-p|--place)] []", 96 | "Creates the indicated mob and places them on the depth.", 97 | "Specifying [quantity] will attempt to spawn that many mobs ", 98 | "_-p_ allows manual placement, though it cannot be combined with a quantity argument."), 99 | SET(Trap.class, 100 | "", 101 | "Sets a trap at an indicated position"), 102 | AFFECT(Buff.class, 103 | " [] [ []]", 104 | "Allows you to attach a buff to a character in sight.", 105 | "This can be potentially hazardous if a buff is applied to something that it was not designed for.", 106 | "Specifying _duration_ will attempt to set the duration of the buff. In the cases of buffs that are active in nature (e.g. buffs.Burning), you may need to call a method to properly set its duration.", 107 | "The method is called after the buff is attached, or on the existing buff if one existed already. This means you can say \"affect doom detach\" to remove doom from that character."), 108 | SEED(Blob.class, 109 | " []", 110 | "Seed a blob of the specified amount to a targeted tile"), 111 | USE(Object.class, " method [args]", "Use a specified method from a desired class.", 112 | "It may be handy to see _inspect_ to see usable methods for your object", 113 | "If you set a variable from this command, the return value of the method will be stored into the variable."), 114 | INSPECT(Object.class, "", "Gives a list of supported methods for the indicated class."), 115 | GOTO(null, "", "Sends your character to the indicated depth."), 116 | WARP(null, "[]", "Targeted teleportation. Optionally takes a cell location, most easily assigned by variable"), 117 | MACRO(null, "", 118 | "Store a sequence of scroll of debug commands to a single name", 119 | "Macros are a way to store and reproduce multiple scroll of debug commands at once.", 120 | "This is an experimental feature. Anything that prompts the player should be at the last line of a macro.", 121 | "Macros can call other macros", 122 | "To take parameters, write '%n', where n is the nth input after the macro name when calling it. For example `mymacro rat` can reference 'rat' via '%1'.", 123 | "Macros are saved and are kept independent of run." 124 | ), 125 | VARIABLES(null, 126 | "_@_ [ [COMMAND ...] | i[nv] | c[ell] ]", 127 | "store game objects for later use as method targets or parameters", 128 | "The variables can be referenced later with their names for the purposes of methods from commands, as well as the _use_ and _inspect_ commands.", 129 | "You can see all active variable names by typing _@_.", 130 | "Specifying \"inv\" (or \"i\") will have the game prompt you to select an item from your inventory.", 131 | "Specifying \"cell\" (or \"c\") will allow you to select a tile. ", 132 | "When selecting a cell, you may or may not be able to directly select things in the tile you select, depending on the Scroll of Debug implementation.", 133 | "Please note that variables are not saved when you close the game." 134 | ); 135 | 136 | final Class paramClass; 137 | final String syntax; 138 | // a short description intended to fit on one line. 139 | final String summary; 140 | // more details on usage. a length of 1 will be treated as an extended description, more will be treated as a list. 141 | final String[] notes; 142 | 143 | Command(Class paramClass, String syntax, String summary, String... notes) { 144 | this.paramClass = paramClass; 145 | this.syntax = syntax; 146 | this.summary = summary; 147 | this.notes = notes; 148 | } 149 | 150 | @Override public String toString() { return name().toLowerCase(); } 151 | 152 | String documentation() { return documentation(this, syntax, summary); } 153 | static String documentation(Object command, String syntax, String description) { 154 | return String.format("_%s_ %s\n%s", command, syntax, description); 155 | } 156 | 157 | // adds more information depending on what the paramClass actually is. 158 | String fullDocumentation(PackageTrie trie, boolean showClasses) { 159 | String documentation = documentation(); 160 | if(notes.length > 0) { 161 | documentation += "\n"; 162 | if(notes.length == 1) documentation += "\n" + notes[0]; 163 | else for(String note : notes) documentation += "\n_-_ " + note; 164 | } 165 | if(showClasses && paramClass != null && !paramClass.isPrimitive() && paramClass != Object.class) { 166 | documentation += "\n\n_Valid Classes_:" + listAllClasses(trie,paramClass); 167 | } 168 | return documentation; 169 | } 170 | String fullDocumentation(PackageTrie trie) { return fullDocumentation(trie, true); } 171 | 172 | 173 | static Command get(String string) { try { 174 | return string.equals("@") ? VARIABLES : 175 | valueOf(string.toUpperCase()); 176 | } catch (Exception e) { return null; } } 177 | } 178 | 179 | // -- macro logic 180 | private static final String MACRO_FILE = "debug-macros.dat", KEYS="KEYS", VALUES="VALUES"; 181 | 182 | private static HashMap macros = null; 183 | 184 | // always returns non-null value 185 | private static Map getMacros() { 186 | if (macros == null) try { 187 | Bundle macroBundle = FileUtils.bundleFromFile(MACRO_FILE); 188 | String[] keys=macroBundle.getStringArray(KEYS), values=macroBundle.getStringArray(VALUES); 189 | if (keys == null || values == null) throw new IOException("bad macro bundle!"); 190 | macros = new HashMap<>(keys.length); 191 | for (int i=0; i < keys.length; i++) macros.put(keys[i], values[i]); 192 | } catch (IOException e) { 193 | // just... yea. Assuming the file just isn't there or something? 194 | Game.reportException(new IOException("Failed to retrieve Scroll of Debug macros", e)); 195 | macros = new HashMap<>(); // initialize empty array 196 | } 197 | return macros; 198 | } 199 | 200 | /** creates or modifies macro with value. If value is empty, delete the macro. **/ 201 | public static void setMacro(String macro, String value) { 202 | getMacros(); 203 | if(value.isEmpty() ? macros.remove(macro) == null : value.equals(macros.put(macro, value))) return; 204 | // only run this if we actually changed macros 205 | Bundle bundle = new Bundle(); 206 | String[] a = {}; 207 | bundle.put(KEYS, macros.keySet().toArray(a)); 208 | bundle.put(VALUES, macros.values().toArray(a)); 209 | try { 210 | FileUtils.bundleToFile(MACRO_FILE, bundle); 211 | } catch (IOException e) { 212 | Game.reportException(new IOException("Failed to save Scroll of Debug macros", e)); 213 | } 214 | } 215 | 216 | // -- general logic 217 | 218 | // fixme should be able to buffer a store location for a macro 219 | private String storeLocation; 220 | 221 | @Override 222 | public void doRead() { 223 | collect(); // you don't lose scroll of debug. 224 | GameScene.show(new WndTextInput("Enter Command:", null, "", 100, false, 225 | "Execute", "Cancel") { 226 | 227 | private String[] handleVariables(String[] input) { 228 | storeLocation = null; 229 | if (input.length > 0 && input[0].startsWith(Variable.MARKER)) { 230 | // drop from the start, save for later. 231 | storeLocation = input[0]; 232 | if (storeLocation.length() == 1) { 233 | if (input.length > 1) 234 | GLog.w("warning: remaining arguments were discarded"); 235 | // list them all 236 | StringBuilder s = new StringBuilder(); 237 | for (Map.Entry e : Variable.assigned.entrySet()) 238 | if (e.getValue().isActive()) { 239 | s.append("\n_").append(e.getKey()).append("_ - ").append(e.getValue()); 240 | } 241 | GameScene.show(new HelpWindow("Active Variables: \n" + s)); 242 | return null; 243 | } 244 | input = Arrays.copyOfRange(input, 1, input.length); 245 | 246 | // variable-specific actions 247 | if (input.length == 0) { 248 | GLog.p("%s = %s", storeLocation, Variable.toString(storeLocation)); 249 | return input; 250 | } 251 | String vCommand = input[0].toLowerCase(); 252 | if (vCommand.matches("i(nv(entory)?)?")) { 253 | Variable.putFromInventory(storeLocation); 254 | return null; 255 | } else if (vCommand.matches("c(ell)?")) { 256 | Variable.putFromCell(storeLocation); 257 | return null; 258 | } 259 | 260 | } 261 | return input; 262 | } 263 | 264 | @Override public void onSelect(boolean positive, String text) { 265 | if(!positive) return; 266 | 267 | // !! handling 268 | { 269 | Matcher m = Pattern.compile("!!").matcher(text); 270 | if (m.find()) { 271 | GLog.newLine(); 272 | GLog.i("> %s", text = m.replaceAll(lastCommand)); 273 | GLog.newLine(); 274 | } 275 | } 276 | lastCommand = text; 277 | 278 | String[] initialInput = text.split(" "); 279 | Callback init = null; 280 | 281 | final String[] input = handleVariables(initialInput); 282 | 283 | if (input == null || input.length == 0) return; 284 | 285 | interpret(input); 286 | } 287 | 288 | // returns whether a macro exists 289 | private boolean handleMacro(String[] input) { 290 | String macro = getMacros().get(input[0]); 291 | if(macro == null) return false; // only false output of handleMacro 292 | 293 | Pattern argPattern = Pattern.compile("%(\\d)"); 294 | // avoid stupid infinite loops caused by parameter substitution 295 | // I want to allow it but infinite loops are dumb 296 | int[] placeholders = new int[input.length]; 297 | Arrays.fill(placeholders, -2); // -2 is unprocessed 298 | for (int i = 0; i < input.length; i++) { 299 | if (placeholders[i] > -2) continue; // already processed 300 | int cur = i; 301 | StringBuilder loop = new StringBuilder(); 302 | do { 303 | if (!loop.isEmpty()) loop.append("->"); 304 | loop.append(cur); 305 | if (placeholders[cur] != -2) { 306 | GLog.n("infinite parameter loop: " + loop); 307 | return true; 308 | } 309 | Matcher matcher = argPattern.matcher(input[cur]); 310 | cur = placeholders[cur] = matcher.matches() ? Integer.parseInt(matcher.group(1)) : -1; 311 | } while(cur >= 0 && placeholders[cur] != -1); 312 | } 313 | String[] lines = macro.split("\n"); 314 | for (String line : lines) { 315 | try { 316 | while (true) { 317 | Matcher argMatcher = argPattern.matcher(line); 318 | if (argMatcher.find()) { 319 | int index = Integer.parseInt(argMatcher.group(1)); 320 | argMatcher.reset(); 321 | line = argMatcher.replaceFirst(input[index]); 322 | continue; 323 | } 324 | break; 325 | } 326 | String[] line_input = handleVariables(line.split(" ")); 327 | if (line_input == null) break; // fixme should also indicate end of parsing 328 | // todo fix for when command isn't actually...given 329 | GLog.newLine(); 330 | GLog.i("> " + line); 331 | 332 | // interpret until we can't 333 | if (!interpret(line_input)) { 334 | return true; 335 | } 336 | } catch (Exception ex) { 337 | reportException(ex); 338 | break; 339 | } 340 | } 341 | return true; 342 | } 343 | 344 | // todo have redirect-able output for better logging 345 | // command logic 346 | // returns true if another command is safely called after it. 347 | // errors generally return false to stop macro flow. 348 | private boolean interpret(String... input) { 349 | Command command = Command.get(input[0]); 350 | 351 | if (command == null) { 352 | // fixme drawbacks of the current system make it impossible to verify macro call safety 353 | if (handleMacro(input)) { 354 | return true; // dig your own grave... 355 | } 356 | GLog.w("\"" + input[0] + "\" is not a valid command."); 357 | return false; 358 | } 359 | 360 | if(command == Command.CHANGES) { 361 | GameScene.show(new HelpWindow(CHANGELOG)); 362 | } 363 | else if(command == Command.HELP) { 364 | String output = null; 365 | boolean all = false; 366 | if (input.length > 1) { 367 | // we only care about the initial argument. 368 | Command cmd = Command.get(input[1]); 369 | if (cmd != null) output = cmd.fullDocumentation(trie); 370 | else all = input[1].equalsIgnoreCase("all"); 371 | } 372 | if (output == null) { 373 | StringBuilder builder = new StringBuilder(); 374 | for (Command cmd : Command.values()) { 375 | if (all) { 376 | // extensive. help is omitted because we are using help. 377 | if (cmd != Command.HELP) { 378 | builder.append("\n\n") 379 | .append(cmd.fullDocumentation(trie, false)); 380 | } 381 | } else { 382 | // use documentation. (show syntax in addition to description) 383 | builder.append('\n').appendLine(cmd.documentation()); 384 | } 385 | } 386 | output = builder.toString().trim(); 387 | } 388 | GameScene.show(new HelpWindow(output)); 389 | return false; 390 | } 391 | else if (command == Command.MACRO) { 392 | getMacros(); 393 | if (input.length == 1) { 394 | StringBuilder msg = new StringBuilder(); 395 | msg.append(command.documentation()); 396 | if(!macros.isEmpty()) { 397 | msg.append("\n_Defined macros:_"); 398 | for(String macro : macros.keySet()) { 399 | msg.append("\n_-_ ").append(macro); 400 | } 401 | } 402 | GameScene.show(new HelpWindow(msg.toString())); 403 | return false; 404 | } 405 | final String macro = input[1]; 406 | boolean macroExists = macros.containsKey(macro); 407 | String failureReason = 408 | macroExists ? null : // avoid checks if it already exists 409 | Command.get(macro) != null ? "existing command" : 410 | // should I print out the offending part??? 411 | !macro.matches("[A-Za-z_][\\w$_]*") ? "must be valid java variable name (alphanumeric, first character must be a letter or underscore)" 412 | : null; 413 | if (failureReason != null) { 414 | GLog.n("Invalid macro name - " + failureReason); 415 | } else GameScene.show(new WndTextInput( 416 | "Macro " + input[1], "Enter macro.\n\nMacros consist of chains of scroll of debug commands separated by new lines. Please refrain from commands that prompt for input outside of the last line.", 417 | macroExists ? macros.get(macro) : "", 418 | Integer.MAX_VALUE, // ???? 419 | true, "Confirm", "Cancel" 420 | ) { 421 | @Override public void onSelect(boolean positive, String text) { 422 | if (positive) setMacro(macro, text); 423 | } 424 | }); 425 | return false; 426 | } 427 | else if (command == Command.WARP) { 428 | Object storedVariable = input.length > 1 ? Variable.get(input[1]) : null; 429 | if (storedVariable instanceof Integer) { 430 | // backport note: prior to 1.0.0 there was no return value 431 | return teleportToLocation(hero, (int)storedVariable); 432 | } 433 | else if (input.length > 1) { 434 | GLog.w("Invalid argument provided: " + (storedVariable == null ? input[1] : storedVariable)); 435 | } else { 436 | GameScene.selectCell(new CellSelector.Listener() { 437 | @Override 438 | public void onSelect(Integer cell) { 439 | if (cell != null) teleportToLocation(hero, cell); 440 | } 441 | 442 | @Override 443 | public String prompt() { 444 | return "Choose a location to teleport"; 445 | } 446 | }); 447 | } 448 | return false; 449 | } 450 | else if(input.length > 1) { 451 | Object storedVariable = Variable.get(input[1]); 452 | 453 | if(command == Command.GOTO) { 454 | if(storedVariable instanceof Integer) { 455 | gotoDepth((Integer)storedVariable); 456 | } 457 | else try { 458 | gotoDepth(Integer.parseInt(input[1])); 459 | } catch (NumberFormatException e) { 460 | GLog.w("Invalid depth provided: " + input[1]); 461 | // should I report this exception too? 462 | // false to stop at failure 463 | return false; 464 | } 465 | return true; 466 | } 467 | 468 | Class _cls = storedVariable != null ? storedVariable.getClass() 469 | : trie.findClass(input[1], command.paramClass); 470 | 471 | if(command == Command.INSPECT || command == Command.USE && input.length == 2) { 472 | Class cls = _cls; 473 | if(cls == null) { 474 | Command c = Command.get(input[1]); 475 | if(c != null) cls = c.paramClass; 476 | } 477 | if(cls != null) { 478 | boolean isGameClass = cls.getName().contains(ROOT); // dirty hack to allow seeing methods for out of package stuff 479 | StringBuilder message = new StringBuilder(); 480 | for(Map.Entry> entry : hierarchy(cls).entrySet()) { 481 | Class inspecting = entry.getKey(); 482 | String className = inspecting.getName(); 483 | if (isGameClass) { 484 | int i = className.indexOf(ROOT); 485 | if(i == -1) continue; 486 | className = className.substring(i+ROOT.length()+1); 487 | } 488 | message.append("\n\n_").append(className).append("_"); 489 | Object[] enumConstants = inspecting.getEnumConstants(); 490 | if(enumConstants != null) for(Object member : entry.getKey().getEnumConstants()) { 491 | message.append("\n_->_ ").append(member.toString().replaceAll("_"," ")); 492 | } 493 | for(Field f : inspecting.getFields()) { 494 | if(f.isEnumConstant()) continue; 495 | if(f.getDeclaringClass() != inspecting) continue; 496 | int modifiers = f.getModifiers(); 497 | Class t = f.getType(); 498 | // wonder if this should be sorted (possibly static -> instance) 499 | // also need to revisit the use of symbols, - is duplicated inappropriately. 500 | message.append("\n_") 501 | .append(Modifier.isStatic(modifiers) ? '-' : '#') 502 | .append('_').append(f.getName().replaceAll("_"," ")); 503 | if(Modifier.isFinal(modifiers)) { 504 | boolean showValue = Modifier.isStatic(modifiers); 505 | if(showValue) try { 506 | // no point in showing if we're just going to get a meaningless hash 507 | showValue = t.isPrimitive() 508 | || t.getMethod("toString") 509 | .getDeclaringClass() != Object.class; 510 | } catch (NoSuchMethodException e) { showValue = false; } 511 | if(showValue) try { 512 | message.append("=").append(f.get(null)); 513 | } catch (IllegalAccessException e) {/* do nothing*/} 514 | else { 515 | message.append(": ").append(t.getSimpleName()); 516 | } 517 | } else { 518 | // this signifies that the getter can be accessed this way. hopefully no one was dumb enough to duplicate the name. 519 | message.append(" [<") 520 | .append(f.getType().getSimpleName()) 521 | .append(">]"); 522 | } 523 | } 524 | for(Method m : entry.getValue()) { 525 | message.append("\n_").append(Modifier.isStatic(m.getModifiers()) ? '*' : '-').append("_") 526 | .append(m.getName()); 527 | Class[] types = m.getParameterTypes(); 528 | int left = types.length; 529 | for(Class c : m.getParameterTypes()) { 530 | StringBuilder param = new StringBuilder("<"); 531 | param.append(c.getSimpleName().toLowerCase()); 532 | // varargs handling. Not supported, but...maybe someday? 533 | if(--left == 0 && m.isVarArgs()) param.append(".."); 534 | param.append('>'); 535 | // optional handling, currently only hero is handled. 536 | // todo have similar methods be merged, with the offending parameters marked as optional. 537 | if(c == Hero.class || c != Object.class && c.isInstance(Dungeon.level)) { 538 | param.insert(0,'[').append(']'); 539 | } 540 | message.append(' ').append(param); 541 | } 542 | } 543 | } 544 | GameScene.show(new HelpWindow( 545 | "inspection of _"+input[1]+"_:" 546 | + message.toString() )); 547 | return false; 548 | } 549 | } 550 | 551 | final Class cls = _cls; 552 | 553 | if(command == Command.USE && input.length > 2) { 554 | Object o = 555 | storedVariable != null ? storedVariable : // use the variable if available. 556 | cls == Hero.class ? Dungeon.hero : 557 | cls != Object.class && cls != null && cls.isInstance(Dungeon.level) ? Dungeon.level : 558 | cls != null && canInstantiate(cls) ? Reflection.newInstance(cls) : 559 | null; 560 | if(!executeMethod(o, cls, input, 2)) { 561 | GLog.w(String.format("No method '%s' was found for %s", input[2], cls)); 562 | return false; 563 | } 564 | return true; 565 | } 566 | 567 | boolean valid = true; 568 | Object o = null; try { 569 | o = Reflection.newInstanceUnhandled(cls); 570 | if(o != null) Variable.put(storeLocation, o); 571 | } catch (Exception e) { valid = false; } 572 | if (valid) switch (command) { 573 | case SPAWN: Mob mob = (Mob)o; 574 | // process args 575 | int quantity = 1; 576 | boolean manualPlace = false; 577 | boolean qSpecified = false; 578 | if(input.length > 2) { 579 | String opt = input[2]; 580 | // is this a forced use of regex? 581 | Matcher matcher = Pattern.compile("x(\\d+)").matcher(opt); 582 | if(matcher.find()) { 583 | quantity = Integer.parseInt(matcher.group(1)); 584 | qSpecified = true; 585 | } else if(opt.matches("-p|--place")) { 586 | manualPlace = true; 587 | } 588 | } 589 | if(manualPlace) { 590 | GameScene.selectCell(new CellSelector.Listener() { 591 | @Override public String prompt() { 592 | return "Select a tile to place " + mob.name(); 593 | } 594 | @Override public void onSelect(Integer cell) { 595 | if(cell == null) return; 596 | // damn it evan for making me copy paste this 597 | if(level.findMob(cell) != null 598 | || !level.passable[cell] 599 | || level.solid[cell] 600 | || !level.openSpace[cell] && mob.properties().contains(Char.Property.LARGE) 601 | ) { 602 | GLog.w("You cannot place %s here.", mob.name()); 603 | return; 604 | } 605 | mob.pos = cell; 606 | GameScene.add(mob); 607 | // doing this means that I can't actually let you select cells for methods; it'll be immediately cancelled. 608 | executeMethod(mob,input,3); 609 | GLog.w("Spawned " + mob.name()); 610 | } 611 | }); 612 | return false; // DO NOT USE THIS IN MACROS DO NOT USE THIS IN MACROS 613 | } else { 614 | int spawned = 0; 615 | boolean canExecute = true; 616 | // nonstandard for loop that generates mobs. first mob is the original one. 617 | for(Mob m = mob; m != null && spawned++ < quantity; m = (Mob)Reflection.newInstance(cls)) { 618 | m.pos = level.randomRespawnCell(m); 619 | if(m.pos == -1) break; 620 | GameScene.add(m); 621 | // if it fails we don't want to flood the screen with messages. 622 | if(canExecute) canExecute = executeMethod(m, input, qSpecified?3:2); 623 | } 624 | spawned--; 625 | GLog.w("Spawned " 626 | + mob.name() 627 | + (spawned == 1 ? "" : " x" + spawned) 628 | ); 629 | } 630 | return true; 631 | case SET: 632 | Trap t = (Trap)o; 633 | GameScene.selectCell(new CellSelector.Listener() { 634 | @Override 635 | public void onSelect(Integer cell) { 636 | if(cell == null || cell == -1) return; 637 | // currently manually set traps are always revealed. 638 | Dungeon.level.setTrap(t.set(cell).reveal(), cell); 639 | Level.set(cell, Terrain.TRAP); 640 | } 641 | @Override public String prompt() { 642 | return "Select location of trap:"; 643 | } 644 | }); 645 | return false; // game selectors do not stack well 646 | case GIVE: Item item = (Item)o; 647 | item.identify(); 648 | // todo add enchants/glyphs for weapons/armor? 649 | // process modifiers left to right (so later ones have higher precedence) 650 | boolean collect = false; 651 | for(int i=2; i < input.length; i++) { 652 | if(input[i].startsWith("--force") || input[i].equalsIgnoreCase("-f")) { 653 | collect = true; 654 | } 655 | else if(input[i].matches("[\\-x+]\\d+")) { 656 | switch (input[i].charAt(0)) { 657 | case 'x': 658 | item.quantity(Integer.parseInt(input[i].substring(1))); 659 | break; 660 | case '-': 661 | case '+': 662 | item.level(Integer.parseInt(input[i])); 663 | break; 664 | } 665 | } 666 | else { 667 | if(!executeMethod(item,input,i)) { 668 | GLog.w("Unrecognized option or method '%s'", input[i]); 669 | return interpret("help", input[0]); 670 | } 671 | break; 672 | } 673 | } 674 | Item toPickUp = collect ? new Item() { 675 | // create wrapper item that simulates doPickUp while actually just calling collect. 676 | { image = item.image; } 677 | @Override public boolean collect(Bag container) { 678 | return item.collect(container); 679 | } 680 | } : item; 681 | String itemName = item.name(); 682 | if (toPickUp.doPickUp(curUser)) { 683 | // ripped from Hero#actPickUp, kinda. 684 | boolean important = item.unique && (item instanceof Scroll || item instanceof Potion); 685 | String pickupMessage = Messages.get(curUser, "you_now_have", itemName); 686 | if(important) GLog.p(pickupMessage); else GLog.i(pickupMessage); 687 | // attempt to nullify turn usage. 688 | curUser.spend(-curUser.cooldown()); 689 | } else { 690 | GLog.n(Messages.get(curUser, "you_cant_have", itemName)); 691 | } 692 | return true; 693 | case AFFECT: 694 | Buff buff = (Buff)o; 695 | // fixme perhaps have special logic for when additional arguments in general are passed to non-flavor buffs. 696 | GameScene.selectCell(new CellSelector.Listener() { 697 | @Override public String prompt() { 698 | return "Select the character to apply the buff to:"; 699 | } 700 | @Override public void onSelect(Integer cell) { 701 | Char target; 702 | if(cell == null || cell == -1 || (target = Actor.findChar(cell)) == null) return; 703 | Buff added = null; 704 | int index = 2; 705 | 706 | boolean success = false; 707 | 708 | if(index >= input.length) 709 | { 710 | // no additional arguments. 711 | Buff.affect(target, cls); 712 | } 713 | else { 714 | if(buff instanceof FlavourBuff) { 715 | try { 716 | added = Buff.affect(target,cls,Float.parseFloat(input[index])); 717 | index++; 718 | } catch (NumberFormatException e) { 719 | added = Buff.affect(target,cls); 720 | } 721 | } else { 722 | added = Buff.affect(target, cls); 723 | // check some common methods for active buffs 724 | String[] methodNames = {"set", "reset", "prolong", "extend"}; 725 | for(String methodName : methodNames) { 726 | if(success = executeMethod(added, methodName, copyOfRange(input,index,input.length))) 727 | break; 728 | } 729 | } 730 | // attempt to call a specified method. 731 | if(!success && 732 | index < input.length 733 | && !executeMethod(added, input, index) 734 | ) GLog.w("Warning: No supported method matching "+input[index]+" was found."); 735 | } 736 | if(added == null) { 737 | added = Buff.affect(target, cls); 738 | } 739 | // manual announce. 740 | if(added.icon() == BuffIndicator.NONE && !added.announced) { 741 | int color; switch(added.type) { 742 | case POSITIVE: 743 | color = CharSprite.POSITIVE; 744 | break; 745 | case NEGATIVE: 746 | color = CharSprite.NEGATIVE; 747 | break; 748 | default: 749 | color = CharSprite.NEUTRAL; 750 | } 751 | String buffName; try { 752 | // Evan attempted to screw me over by changing toString implementations of buff to a new name() method (see be01254) 753 | // Unfortunately for him, I can just check for it. 754 | buffName = (String)added.getClass() 755 | .getMethod("name") 756 | .invoke(added); 757 | } catch(Exception e) { buffName = added.toString(); } 758 | target.sprite.showStatus(color, buffName); 759 | } 760 | } 761 | }); 762 | return false; 763 | case SEED: 764 | int a = 1; 765 | if(input.length > 2) try { 766 | a = Integer.parseInt(input[2]); 767 | } catch (Exception e) {/*do nothing*/} 768 | final int amount = a; 769 | GameScene.selectCell(new CellSelector.Listener() { 770 | @Override public String prompt() { 771 | return "Select the tile to seed the blob:"; 772 | } 773 | @Override public void onSelect(Integer cell) { 774 | if(cell == null) return; 775 | GameScene.add(Blob.seed(cell, amount, (Class)cls)); 776 | } 777 | }); 778 | return false; 779 | } else { 780 | GLog.w( "%s \"%s\" not found.", command.paramClass.getSimpleName(), input[1]); 781 | return false; 782 | } 783 | } else { 784 | // fixme should be able to just call help directly... 785 | return interpret("help", input[0]); // bring up help for command 786 | } 787 | return true; 788 | } 789 | }); 790 | } 791 | 792 | /** level transition was implemented in 1.3.0 **/ 793 | private static final boolean before1_3_0; 794 | static { 795 | boolean preRework = false; 796 | try { 797 | Class.forName(ROOT + ".levels.features.LevelTransition"); 798 | } catch (ClassNotFoundException e) { preRework = true; } 799 | before1_3_0 = preRework; 800 | } 801 | // force sends you to the corresponding depth. 802 | private static void gotoDepth(int targetDepth) { 803 | Mob.holdAllies( Dungeon.level ); 804 | try { saveAll(); } catch (IOException e) { 805 | reportException("Unable to save game!", e); 806 | return; 807 | } 808 | try { 809 | // needed for certain implementations of this mechanic. 810 | Game.scene().destroy(); 811 | } catch (Exception e) { 812 | // if it fails for some unknown reason I really don't care, move on. 813 | Game.reportException(e); 814 | } 815 | // if ascending, don't bother loading levels in between 816 | final int startDepth = depth; 817 | Level level; 818 | // attempt to load it directly 819 | depth = targetDepth; 820 | try { 821 | level = loadLevel(GamesInProgress.curSlot); 822 | } catch (IOException needToGenerateLevel) { 823 | // load each intermediate level to preserve seed generation logic if descending 824 | depth = startDepth; 825 | final Level origLevel = level = Dungeon.level; 826 | final int increment = targetDepth < depth ? targetDepth - depth : 1; 827 | while (depth != targetDepth) { 828 | depth += increment; 829 | try { 830 | level = loadLevel(GamesInProgress.curSlot); 831 | } catch (IOException e) { 832 | // generating a new level before the feature rework incremented the level automatically. 833 | if (before1_3_0) depth--; 834 | level = newLevel(); 835 | if (depth != targetDepth) try { 836 | // need to overwrite Dungeon.level to save a level's generation 837 | Dungeon.level = level; 838 | Dungeon.saveLevel(GamesInProgress.curSlot); 839 | } catch (IOException ex) { 840 | // skip to dest level 841 | Game.reportException(e); 842 | depth = targetDepth - increment; 843 | } finally { 844 | Dungeon.level = origLevel; 845 | } 846 | } 847 | } 848 | } 849 | switchLevel(level, -1); 850 | Game.switchScene(GameScene.class); 851 | } 852 | 853 | @Override public String name() { 854 | return "Scroll of Debug"; 855 | } 856 | @Override public String desc() { 857 | StringBuilder builder = new StringBuilder(); 858 | builder.appendLine("A scroll that gives you great power, letting you create virtually any item or mob in the game.") 859 | .appendLine("\nSupported Commands:"); 860 | for(Command cmd : Command.values()) builder.appendLine( 861 | // this should hopefully fit on one line. 862 | String.format("_- %s_: %s", cmd, cmd.summary) 863 | ); 864 | return builder.append("\nPlease note that some possible inputs may crash the game or cause other unexpected behavior, especially if their targets weren't intended to be created or otherwise used arbitrarily.") 865 | .toString(); 866 | } 867 | @Override public boolean isIdentified() { 868 | return true; 869 | } 870 | @Override public boolean isKnown() { return true; } 871 | { 872 | unique = true; 873 | } 874 | 875 | 876 | // todo change return type to integer to indicate how many spaces were used, possibly add option to force all to be used. This would allow stacking. 877 | // variant that derives class from the object given 878 | boolean executeMethod(T obj, String methodName, String... args) { 879 | return executeMethod(obj, (Class)obj.getClass(), methodName, args); 880 | } 881 | // fixme there's no way to know how many arguments were actually used, which forces this to be the last command. 882 | /** dynamic method execution logic **/ 883 | boolean executeMethod(T obj, Class cls, String methodName, String... args) { 884 | ArrayList methods = new ArrayList<>(); 885 | for(Method method : cls.getMethods()) { 886 | if(args.length > method.getParameterTypes().length) continue; // prevents arbitrary hiding. 887 | if(method.getName().equalsIgnoreCase(methodName)) methods.add(method); 888 | } 889 | Collections.sort(methods, (m1, m2) -> m2.getParameterTypes().length - m1.getParameterTypes().length ); 890 | for(Method method : methods) { 891 | Object[] arguments; try { arguments = getArguments(method.getParameterTypes(), args); } 892 | catch (Exception e) { continue; } 893 | try { 894 | Object result = method.invoke(obj, arguments); 895 | if(result != null) { 896 | printMethodOutput(cls,method,method.getModifiers(),result,arguments); 897 | if(storeLocation != null) Variable.put(storeLocation, result); 898 | } 899 | return true; 900 | } catch (Exception e) { 901 | // fixme distinguish properly between methods that don't exist and methods that failed to call so errors can be reported here 902 | // this is a straight up guess, and if it doesn't work as expected, remove the if-else clause entirely and just call Game.reportException 903 | if (e instanceof IllegalArgumentException) { 904 | Game.reportException(e); 905 | } else { 906 | reportException(e); 907 | break; 908 | } 909 | } 910 | } 911 | // check if it is actually a field. 912 | try { 913 | Field field = null; 914 | // this is needed because it's currently not case sensitive, while getField() is. 915 | for(Field f : cls.getFields()) { 916 | if(f.getName().equalsIgnoreCase(methodName)) { 917 | field = f; 918 | break; 919 | } 920 | } 921 | if(field == null) return false; 922 | Object result; 923 | if(args.length == 0) { 924 | result = field.get(obj); 925 | } 926 | // fixme this will need to be revisited when I implement stacking of methods 927 | else if(args.length == 1) { 928 | // convert the argument to a proper object and assign 929 | // fixme should not have to do this much wrangling 930 | field.set(obj, result=getArguments(new Class[]{field.getType()}, args)[0]); 931 | } else throw new IllegalArgumentException(); 932 | if(storeLocation != null) Variable.put(storeLocation, result); 933 | printMethodOutput(cls,field,field.getModifiers(), result); 934 | return true; 935 | } catch(Exception e) {/*not a valid match*/} 936 | return false; 937 | } 938 | // shortcut methods that interpret input to get the arguments needed 939 | boolean executeMethod(T obj, Class cls, String[] input, int startIndex) { 940 | return startIndex < input.length && executeMethod(obj, cls, input[startIndex++], startIndex < input.length 941 | ? copyOfRange(input, startIndex, input.length) 942 | : new String[0] 943 | ); 944 | } 945 | boolean executeMethod(T obj, String[] input, int startIndex) { return executeMethod(obj, (Class)obj.getClass(), input, startIndex); } 946 | 947 | // prints out the result of a method call. 948 | static void printMethodOutput(Class cls, Member m, int modifiers, Object result, Object... arguments) { 949 | String argsAsString = Arrays.deepToString(arguments); 950 | String argFormat = m instanceof Method ? "(%5$s):" : " ="; 951 | GLog.w("%s%s%s"+argFormat+" %4$s", 952 | cls.getSimpleName(), 953 | Modifier.isStatic(modifiers) ? '.' : '#', 954 | m.getName(), 955 | // this displays arrays properly. 956 | result.getClass().isArray() ? Arrays.deepToString((Object[])result) : result, 957 | // snip first and last brace 958 | argsAsString.substring(1,argsAsString.length()-1) 959 | ); 960 | } 961 | 962 | // throws an exception if it fails. This removes the need for me to handle errors at all. 963 | Object[] getArguments(Class[] params, String[] input) throws Exception { 964 | // todo make a #getArgument(Class, String... input) 965 | Object[] args = new Object[params.length]; 966 | int j = 0; 967 | for(int i=0; i < params.length; i++) { 968 | Class type = params[i]; 969 | args[i] = Variable.get(input[j], type); 970 | if(args[i] != null) j++; // successful variable call. 971 | // primitive type checks 972 | else if (type == int.class || type == Integer.class) { 973 | args[i] = Integer.parseInt(input[j++]); 974 | } 975 | else if (type == char.class || type == Character.class) { 976 | // check if it's a length of 1. If it is, just use that, otherwise fail. 977 | String fullStr = input[j++]; 978 | if (fullStr.length() != 1) throw new NumberFormatException("Unable to coerce " + fullStr + "to char"); 979 | args[i] = fullStr.charAt(0); 980 | } 981 | else if (type == long.class || type == Long.class) 982 | args[i] = Long.parseLong(input[j++]); 983 | // being through, nothing actually uses these (I hope) 984 | else if (type == short.class || type == Short.class) 985 | args[i] = Short.parseShort(input[j++]); 986 | else if (type == byte.class || type == Byte.class) 987 | args[i] = Byte.parseByte(input[j++]); 988 | else if (type == double.class || type == Double.class) 989 | args[i] = Double.parseDouble(input[j++]); 990 | else if (type == float.class || type == Float.class) 991 | args[i] = Float.parseFloat(input[j++]); 992 | else if (type == String.class) 993 | args[i] = input[j++]; 994 | else if (type == Boolean.class || type == boolean.class) { 995 | boolean result = Boolean.parseBoolean(input[j]); 996 | // parseBoolean returns false if given invalid input 997 | if (!result && !"false".equalsIgnoreCase(input[j])) { 998 | throw new NumberFormatException(input[j]); 999 | } 1000 | args[i] = result; 1001 | j++; 1002 | } 1003 | 1004 | else if(input[j].equalsIgnoreCase("null")) { 1005 | // sometimes you want this. 1006 | args[i] = null; 1007 | j++; 1008 | continue; 1009 | } 1010 | else if (Enum.class.isAssignableFrom(type)) { 1011 | for (String name : new String[]{ 1012 | input[j], input[j].toUpperCase(), input[j].toLowerCase() 1013 | }) 1014 | try { 1015 | args[i] = Enum.valueOf(type, name); 1016 | } catch (IllegalArgumentException e) {/*continue*/} 1017 | j++; 1018 | } 1019 | else { 1020 | // note: the only drawback of this is that it makes it harder to pass the class version of hero or level as an Object. 1021 | // -- substitution logic for major dungeon objects 1022 | if(type.isInstance(curUser) && input[j].equalsIgnoreCase("hero")) { 1023 | type = Hero.class; 1024 | j++; 1025 | } 1026 | if(type.isInstance(Dungeon.level) && input[j].equalsIgnoreCase("level")) { 1027 | type = Dungeon.level.getClass(); 1028 | j++; // for easier understanding. 1029 | } 1030 | 1031 | args[i] = 1032 | type == Hero.class ? curUser :// autofill hero 1033 | Class.class.isAssignableFrom(type) ? trie.findClass(input[j++], Object.class) : 1034 | type == Dungeon.level.getClass() ? Dungeon.level : // level autofill 1035 | // blindly instantiate, any error indicates invalid method. 1036 | Reflection.newInstanceUnhandled(trie.findClass(input[j++], type)); 1037 | } 1038 | // todo determine the exact cases where this is reached. 1039 | if (args[i] == null) throw new IllegalArgumentException("No argument for " + type.getName()); 1040 | } 1041 | return args; 1042 | } 1043 | 1044 | TreeMap> hierarchy(Class base) { 1045 | TreeMap> map = new TreeMap<>((c1, c2) -> { 1046 | int res = 0; 1047 | if (c1.isAssignableFrom(c2)) res++; 1048 | if (c2.isAssignableFrom(c1)) res--; 1049 | return res; 1050 | }); 1051 | for (Method m : base.getMethods()) { 1052 | Class key = m.getDeclaringClass(); 1053 | Set value = map.get(key); 1054 | if(value == null) map.put(key, value = new HashSet<>()); 1055 | value.add(m); 1056 | } 1057 | return map; 1058 | } 1059 | 1060 | // ensures name uniqueness for help display. treemap so things are sorted. 1061 | private static class ClassNameMap extends HashMap { 1062 | ArrayList getNames() { 1063 | ArrayList names = new ArrayList<>(); 1064 | for(Map.Entry entry : entrySet()) { 1065 | if(entry.getValue() != null) names.add(entry.getKey()); 1066 | } 1067 | Collections.sort(names); 1068 | return names; 1069 | } 1070 | 1071 | @Override public Class put(String key, Class cls) { 1072 | String newKey = key; 1073 | if(containsKey(key)) { 1074 | // null means it's been moved. 1075 | Class existing = get(key); 1076 | if(existing != null) { // if it hasn't already been moved. 1077 | // assumes we can't create conflicts this way. 1078 | super.put(key, null); 1079 | put(extendPath(key,existing), existing); 1080 | } 1081 | newKey = extendPath(key, cls); 1082 | } 1083 | //noinspection StringEquality 1084 | return key == newKey ? super.put(key, cls) : put(newKey, cls); 1085 | } 1086 | 1087 | private static String extendPath(String name, Class cls) { 1088 | if(cls == null) return name; 1089 | 1090 | String fullName = cls.getName(); 1091 | 1092 | int right = fullName.indexOf(name); 1093 | if(right == 0) return name; 1094 | 1095 | int left = fullName.lastIndexOf('$', right-2); 1096 | if(left == -1) left = fullName.lastIndexOf('.', right-2); 1097 | 1098 | return fullName.substring(left+1, right) + name; 1099 | } 1100 | } 1101 | 1102 | // reflection logic. 1103 | 1104 | public static ClassLoader loader = ScrollOfDebug.class.getClassLoader(); 1105 | public static PackageTrie trie = null; // loaded when needed. 1106 | static { 1107 | try { 1108 | trie = PackageTrie.getClassesForPackage(ROOT); 1109 | } catch (ClassNotFoundException e) { Game.reportException(e); } 1110 | } 1111 | 1112 | static String listAllClasses(PackageTrie trie, Class parent) { 1113 | ClassNameMap names = new ClassNameMap(); 1114 | for(Class cls : trie.getAllClasses()) { 1115 | if(parent.isAssignableFrom(cls)) names.put(cls.getSimpleName(), cls); 1116 | } 1117 | StringBuilder result = new StringBuilder(); 1118 | if(!names.isEmpty()) { 1119 | for(String name : names.getNames()) if(canInstantiate(names.get(name))) result.append("\n_-_ ").append(name); 1120 | } 1121 | return result.toString(); 1122 | } 1123 | 1124 | 1125 | // including RKPD2 scrolling window code. 1126 | private static class HelpWindow extends Window { 1127 | private static final int WIDTH_MIN=120, WIDTH_MAX=220; 1128 | ScrollPane scrollPane; 1129 | HelpWindow(String message) { 1130 | int width = WIDTH_MIN; 1131 | 1132 | RenderedTextBlock text = PixelScene.renderTextBlock(6); 1133 | text.text(message, width); 1134 | while (PixelScene.landscape() 1135 | && text.bottom() > (PixelScene.MIN_HEIGHT_L - 10) 1136 | && width < WIDTH_MAX) { 1137 | text.maxWidth(width += 20); 1138 | } 1139 | 1140 | int height = (int)text.bottom(); 1141 | int maxHeight = (int)(PixelScene.uiCamera.height * 0.9); 1142 | boolean needScrollPane = height > maxHeight; 1143 | if(needScrollPane) height = maxHeight; 1144 | resize((int)text.width(), height); 1145 | if(needScrollPane) { 1146 | add(scrollPane = new ScrollPane(new Component()) { 1147 | { 1148 | content.add(text); 1149 | } 1150 | // vertical margin is required to prevent text from getting cut off. 1151 | final float VERTICAL_MARGIN = 1; 1152 | @Override 1153 | protected void layout() { 1154 | text.setPos(0, VERTICAL_MARGIN); 1155 | // also set the width of the scroll pane 1156 | content.setSize(width = text.right(), text.bottom()+VERTICAL_MARGIN); 1157 | width += 2; // padding on the right to cause the controller to be flush against the window. 1158 | super.layout(); 1159 | } 1160 | }); 1161 | scrollPane.setSize(width, height); 1162 | } 1163 | else { 1164 | add(text); 1165 | } 1166 | } 1167 | 1168 | @Override // this should be removed for pre-v1.2 builds, this method was added in v1.2 1169 | public void offset(int xOffset, int yOffset) { 1170 | super.offset(xOffset, yOffset); 1171 | // this prevents issues in the full ui mode. 1172 | if(scrollPane != null) scrollPane.setSize(scrollPane.width(), scrollPane.height()); 1173 | } 1174 | } 1175 | 1176 | // report exception via HelpWindow 1177 | // should only be used if it terminates execution of whatever command was running 1178 | // wonder if I should split this into another file... 1179 | public static void reportException(CharSequence msg, Exception e) { 1180 | Game.reportException(e); // also log normally 1181 | // print stack trace directly to help window for faster error identification 1182 | StringWriter s = new StringWriter(); 1183 | PrintWriter p = new PrintWriter(s); 1184 | if (msg != null) p.print(msg + "\n\n"); 1185 | e.printStackTrace(p); 1186 | GameScene.show(new HelpWindow(s.toString())); 1187 | } 1188 | public static void reportException(Exception e) { reportException(null, e);} 1189 | 1190 | /** this checks if we can create this class using Reflection. **/ 1191 | public static boolean canInstantiate(Class c) { 1192 | // check if there's a valid constructor 1193 | try { c.getConstructor(); } catch (NoSuchMethodException e) { return false; } 1194 | return !( Modifier.isAbstract(c.getModifiers()) || Reflection.isMemberClass(c) && !Reflection.isStatic(c) ); 1195 | } 1196 | 1197 | private static final String CHANGELOG 1198 | = "" 1199 | +"_2.1.0_:" 1200 | +"\n_-_ Goto now loads intermediate depths. Load time is increased slightly, but is now seed-stable" 1201 | +"\n_-_ Add warp command" 1202 | +"_2.0.0_:" 1203 | +"\n_-_ Added experimental macro support; macros are chains of commands stored together under an alias, saved between sessions" 1204 | +"\n_-_ Implemented workaround allowing scroll of debug to work even when it can't find any classes" 1205 | +"\n_-_ argument autofill now works with Object parameters, autofills level in more cases, and works with all primitives" 1206 | +"\n_-_ some minor changes to method execution logic, will eventually print errors directly as stack trace in a scrollable window" 1207 | +"\n_-_ fixed formatting in this command" 1208 | +"\n" 1209 | +"_1.2.2_:" 1210 | +"\n_-_ Goto no longer relies on version code in any form." 1211 | +"\n_-_ Variables now attempt to show their ingame name rather than built-in toString." 1212 | +"\n_-_ Seeing the value of a specific variable now uses the same template as when setting them." 1213 | +"\n_-_ Fixed variables being cleared when cancelling a command to set them." 1214 | +"\n_-_ Fixed goto crash when warping to post-v1.3.0 demon halls." 1215 | +"\n_-_ Fixed erroneous assertion in goto description; it does not generate depths in between." 1216 | +"\n" 1217 | +"_1.2.1_:" 1218 | +"\n_-_ Implemented goto, which immediately sends the hero to the targeted depth." 1219 | +"\n_-_ Fixed 1.4.X shattered changes breaking give command text output." 1220 | +"\n" 1221 | +"_1.2.0_:" 1222 | +"\n_-_ Implemented variables! You are now able to store the result of commands that create game objects, as well as anything generated from the use command. You can also store stuff from the map (variable name followed by 'cell' or 'c') and your inventory (variable name followed by 'inv' or 'i')." 1223 | +"\n_-_ Adjusted some descriptions of commands, and added more detail to their extended descriptions." 1224 | +"\n_- help all_ no longer displays all usable classes for every command. To get the functionality, please use _help _." 1225 | +"\n_-_ For methods, Level arguments are now optional (autofilled with Dungeon.level)" 1226 | +"\n_-_ You can now pass 'null' to methods." 1227 | +"\n_-_ Fixed info window for Scroll of Debug being too big for most devices." 1228 | +"\n_-_ Fixed cases where hero wouldn't be inferred when used in methods" 1229 | +"\n_-_ Fixed not being able to pass true to methods expecting true or false" 1230 | +"\n\n" 1231 | +"_1.1.1_:" 1232 | +"\n_-_ methods that have less parameters than given arguments are now ignored, preventing inappropriate hiding of fields" 1233 | +"\n_-_ fields are no longer case sensitive" 1234 | +"\n_-_ fixed bug making inspection field types way longer" 1235 | +"\n_-_ Fixed scrollpane issues. Really." 1236 | +"\n_-_ There is, however, a bug caused by resizing your window while a help window is active. So don't do that." 1237 | +"\n\n_1.1.0_:" 1238 | +"\n_-_ Actually fixed scrollpane issues this time" 1239 | +"\n_-_ Added the ability to retrieve and set public fields of objects, though such functionality cannot be used to pass them to methods at this time." 1240 | +"\n\n_1.0.0_:" 1241 | +"\n_-_ Changes to Shattered Pixel Dungeon in v1.3.0 mean scroll of debug no longer directly supports versions before it." 1242 | +"\n_-_ Changed formatting style of commands." 1243 | +"\n_-_ Enumerated types now list their values in _inspect_" 1244 | +"\n_- give_ now only requires a - to give degraded items, rather than +-" 1245 | +"\n_- give_ now rejects more invalid inputs." 1246 | +"\n_-_ Scroll of Debug's add implementation only triggers when it is not in the player's inventory." 1247 | +"\n_-_ Fixed scrollpanes being offset incorrectly in full shpd view" 1248 | +"\n\n\n_0.4.0_:" 1249 | +"\n_-_ Added this command." 1250 | +"\n_-_ Added _use_ command, which can call a desired method on any game class that supports it (see _inspect_ for valid methods)." 1251 | +"\n_-_ Including _!!_ in a command will replace it with the previously written command." 1252 | +"\n_- spawn_ command now supports either a quantity argument or a --place (-p) option for manual placing of the mob." 1253 | +"\n_- spawn_ command now supports methods, which are called directly after placing the mob." 1254 | +"\n_-_ When calling methods that yield output, the output is now displayed in the game log." 1255 | +"\n_-_ Scroll of Debug is now considered unique, and thus will not burn." 1256 | +"\n_-_ Fixed more bugs in class finding for jar version caused by 0.3." 1257 | +"\n_-_ Fixed Hero method arguments not being automatically resolved to the hero." 1258 | +"\n\n\n_0.3.3_:" 1259 | +"\n_-_ Scroll Of Debug now automatically adds itself to the first open quickslot, rather than always quickslot #3." 1260 | +"\n_0.3.1, 0.3.2_:" 1261 | +"\n_-_ Fixed faulty package logic caused by 0.3.0" 1262 | +"\n_0.3.0_:" 1263 | +"\n_-_ Scroll of Debug now works on Android"; 1264 | } --------------------------------------------------------------------------------