├── .classpath ├── .gitignore ├── .project ├── LICENSE ├── README.md ├── example.png └── src ├── Main.java ├── boon ├── AbstractBoon.java ├── BoonFactory.java ├── Duration.java └── Intensity.java ├── data ├── AgentData.java ├── AgentItem.java ├── BossData.java ├── CombatData.java ├── CombatItem.java ├── LogData.java ├── SkillData.java └── SkillItem.java ├── enums ├── Activation.java ├── Agent.java ├── Boon.java ├── BuffRemove.java ├── CustomSkill.java ├── IFF.java ├── MenuChoice.java ├── Result.java └── StateChange.java ├── player ├── BoonLog.java ├── DamageLog.java └── Player.java ├── statistics ├── Parse.java └── Statistics.java └── utility ├── TableBuilder.java └── Utility.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | org.eclipse.jdt.core.javabuilder 6 | 7 | 8 | 9 | 10 | 11 | org.eclipse.jdt.core.javanature 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 phoenix-oosd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EVTC Log Parser # 2 | 3 | ## About ## 4 | 5 | The project was started to help re-live boss encounters and identify areas for improvement in my own organised group raids. The resultant program is a parser for ` .evtc ` event chain logs created by [arcdps](https://www.deltaconnected.com/arcdps/). 6 | It is written in Java 8 and requires an installation of [JRE 1.8](https://www.java.com/en/download/), but in most cases you probably already have it installed. 7 | 8 | ## User Manual ## 9 | 10 | ### Setting Up ### 11 | 12 | Because of the way Java works in tandem with Windows, you don't want to run the program by just double-clicking ` evtc_log_parser.jar `. Instead you will have to run using the supplied `run .bat`, but more on this later. First you want to create a folder to contain the two files. 13 | 14 | On the very first run, the following folders are created in the launch directory: `/logs/`, `/graphs/`, and `/tables/`. You will want to re-run the program after it detects `/logs/` is empty. Copy .evtc file(s) for parsing into /logs/. The program will recursively search `/logs/` and its sub-directories for `.evtc files`. Both `/graphs/` and `/tables/` are output folders for the related options. 15 | 16 | ### Basic Use ### 17 | 18 | For basic use you can double click `run .bat` which will open up a console with a menu. Enter the option you want by number (e.g. 1 for Final DPS) and press Enter to confirm. Each ` .evtc ` file in `/logs/` will be processed. The results will be displayed directly into the console, or be directed to files in the sub-directories where appropriate. 19 | 20 | ### Locating the Logs ### 21 | 22 | The ` .evtc ` files are automatically created by ` arcdps ` at the end of encounters. 23 | Your files can be found at ` Documents\arcdps.cbtlogs `, each sub-directory corresponds to a different encounter. 24 | Consult the table below to find the logs you need want to analyse. 25 | 26 | 27 | | Folder | Boss | 28 | | ------------- |---------------------------| 29 | | 15438 | Vale Guardian | 30 | | 15429 | Gorseval the Multifarious | 31 | | 15375 | Sabetha the Saboteur | 32 | | 16123 | Slothasor | 33 | | 16088 | Berg | 34 | | 16137 | Zane | 35 | | 16125 | Narella | 36 | | 16115 | Matthias Gabrel | 37 | | 16235 | Keep Construct | 38 | | 16246 | Xera | 39 | | 17194 | Cairn | 40 | | 17172 | Mursaat Overseer | 41 | | 17188 | Samarog | 42 | | 17154 | Deimos | 43 | 44 | ### File Association ### 45 | 46 | Double clicking any ` .evtc ` file will display an ` Open with... ` dialogue. Tick ` Always use this app to open .evtc files` and choose ` run.bat`. Now, whenever you double-click an ` .evtc ` file, the file will be parsed on the spot based on the `options` command argument. The default argument is ` options=516 ` which displays the ` Miscellaneous Combat Statistics`, ` Final DPS `, and ` Final Boons ` in that order. Option 4 and 8 do not work with file association. 47 | 48 | ### Output Customisation ### 49 | 50 | You can customise the parsing to your liking by opening `run .bat` in a text editor such as `Notepad`. 51 | 52 | You will notice everything is on a single line prefixed by ` start "EVTC Log Parser" /MAX `. If you don't want to maximise the console you can delete this, but it is highly recommended to maximise the console so text does not wrap around. 53 | 54 | The `java -jar "path"` section is required and you *MUST* make sure you have an absolute path to ` evtc_log_parser.jar` (e.g. `C:\Users\JohnDoe\Desktop\EVTC Log Parser\evtc_log_parser.jar`). On Windows, the default path will only work if you create a desktop folder named "EVTC Log Parser" on your desktop, and move the attached files from the latest release into it. 55 | 56 | After the path is specified you can add arguments in any order by space separating `arg_name=value`. For convenience all the arguments have already been written for you. 57 | 58 | The program has the following general arguments that apply to both basic running and file association: 59 | 60 | 1. is_anon 61 | * The default value is `0` 62 | * You can edit this string to `1` to hide all account/player names 63 | 64 | The program has the following arguments specific to file association: 65 | 66 | 1. file_path 67 | * The default value is `%1` 68 | * *NEVER* change or remove this argument as it is required for file association. 69 | 2. options 70 | * The default value is `5126` 71 | * *NEVER* remove this argument as it is required for file association 72 | * You can edit this string to match any of the available options below to display tables in a certain order 73 | 74 | 75 | ### Options ### 76 | 77 | All DPS numbers are derived from the players (and pets) to *ONLY* the boss. Phases are sections of the fight when the boss is vulnerable to damage. This only applies for encounters which consist of predictable phases, so not Keep Construct. 78 | 79 | The program has the following options: 80 | 81 | 0. Text Dump 82 | * For debugging, or if you want to see a human readable version of the log 83 | 1. Final DPS 84 | * DPS by player and group 85 | * Damage dealt by each player and group 86 | 2. Phase DPS 87 | * DPS for each phase where applicable 88 | * Phase duration 89 | 3. Damage Distribution - ranks damage output by skill by each player 90 | * Damage breakdown of each player by skill 91 | * Ranks skills in order of contribution 92 | 4. Total Damage Graph 93 | * Graphs the damage 94 | * Can be used to identify mechanical portions of the fight (e.g. flat lines for Matthias sacrifices) 95 | 5. Miscellaneous Combat Statistics 96 | * Healing, Toughness and Condition damage of each player on a scale of 1-10 97 | * Fight rates such as Scholar up-time, seaweed salad movement, and critical rates 98 | 6. Final Boons 99 | * Show relevant boon up-time 100 | * Show relevant class buff up-time 101 | 7. Phase Boons 102 | * Boons for each phase where applicable 103 | 8. Text Dump Tables 104 | * Saves options 1, 2, 3, 5, 6, and 7 tables into ` /tables/ ` 105 | 106 | ### Interpreting Output ### 107 | 108 | ![example](https://github.com/phoenix-oosd/EVTC-Log-Parser/blob/master/example.png) 109 | 110 | ## Future ## 111 | 112 | ### Known Problems ### 113 | 114 | -Phase related statistics if you don't reach the final phase of the boss 115 | 116 | -This program has only been tested on Windows 10 117 | 118 | -If there are any problems not listed here, contact me via PM on [reddit](https://www.reddit.com/user/ghandi-gandhi) or GW2 (Phoenix.5719) 119 | 120 | ### Working on... ### 121 | 122 | -Dynamic phase detection for non-linear fights like Keep Construct 123 | 124 | -Damage taken statistics 125 | 126 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenix-oosd/EVTC-Log-Parser/dbd36710802f77350ade637d40c82c0a68e34a72/example.png -------------------------------------------------------------------------------- /src/Main.java: -------------------------------------------------------------------------------- 1 | import java.io.File; 2 | import java.io.IOException; 3 | import java.nio.file.Path; 4 | import java.nio.file.Paths; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Scanner; 10 | 11 | import enums.MenuChoice; 12 | import statistics.Parse; 13 | import statistics.Statistics; 14 | import utility.TableBuilder; 15 | import utility.Utility; 16 | 17 | public class Main 18 | { 19 | 20 | // Fields 21 | private static boolean will_quit = false; 22 | private static boolean displaying = true; 23 | private static Map argument_map = new HashMap<>(); 24 | private static String old_version = Utility.boxText("ERROR : This only supports versions 20170218 and onwards"); 25 | private static String current_file; 26 | private static Parse parsed_file; 27 | private static Statistics statistics; 28 | 29 | // Main 30 | public static void main(String[] args) 31 | { 32 | // Scanner 33 | Scanner scan = null; 34 | try 35 | { 36 | scan = new Scanner(System.in); 37 | 38 | // Read arguments 39 | for (String arg : args) 40 | { 41 | if (arg.contains("=")) 42 | { 43 | argument_map.put(arg.substring(0, arg.indexOf('=')), arg.substring(arg.indexOf('=') + 1)); 44 | } 45 | } 46 | String is_anon = argument_map.get("is_anon"); 47 | String is_displaying = argument_map.get("is_displaying"); 48 | String file_path = argument_map.get("file_path"); 49 | String options = argument_map.get("options"); 50 | 51 | // Handle arguments 52 | if (is_anon != null) 53 | { 54 | Statistics.hiding_players = Utility.toBool(Integer.valueOf(is_anon)); 55 | } 56 | if (is_displaying != null) 57 | { 58 | displaying = Utility.toBool(Integer.valueOf(is_displaying)); 59 | } 60 | 61 | // File Association 62 | if (file_path != null && !file_path.isEmpty() && options != null) 63 | { 64 | int[] choices = options.chars().map(x -> x - '0').toArray(); 65 | 66 | StringBuilder output = new StringBuilder(); 67 | if (displaying) 68 | { 69 | System.out.println((Utility.boxText("Log Data"))); 70 | } 71 | for (int i : choices) 72 | { 73 | MenuChoice c = MenuChoice.getEnum(i); 74 | if (c != null && c.canBeAssociated()) 75 | { 76 | String result = parseFileByChoice(c, Paths.get(file_path)); 77 | if (result.contains("ERROR")) 78 | { 79 | System.out.println(old_version); 80 | scan.nextLine(); 81 | return; 82 | } 83 | else 84 | { 85 | output.append(result + System.lineSeparator()); 86 | } 87 | } 88 | } 89 | System.out.println(output.toString()); 90 | scan.nextLine(); 91 | return; 92 | } 93 | 94 | // Menu 95 | else 96 | { 97 | // Create required directories 98 | new File("./logs").mkdir(); 99 | new File("./graphs").mkdirs(); 100 | new File("./tables").mkdirs(); 101 | 102 | // Obtain list of .evtc files in /logs/ 103 | List log_files = new ArrayList(); 104 | File log_folder = new File("./logs"); 105 | Utility.recursiveFileSearch(log_folder, log_files); 106 | 107 | // No logs to process 108 | if (log_files.isEmpty()) 109 | { 110 | try 111 | { 112 | System.out.println(Utility.boxText("ERROR : No log files found at \"" 113 | + log_folder.getCanonicalPath().toString() + "\" ... press Enter to exit")); 114 | } catch (IOException e) 115 | { 116 | e.printStackTrace(); 117 | } 118 | scan.nextLine(); 119 | return; 120 | } 121 | // There are logs to process 122 | else 123 | { 124 | while (!will_quit) 125 | { 126 | // Display menu 127 | System.out.println("\u250C" + Utility.fillWithChar(24, '\u2500') + "\u2510"); 128 | System.out.println("\u2502 EVTC Log Parser \u2502"); 129 | System.out.println("\u251C" + Utility.fillWithChar(24, '\u2500') + "\u2524"); 130 | System.out.println("\u2502 0. Dump EVTC \u2502"); 131 | System.out.println("\u2502 1. Final DPS \u2502"); 132 | System.out.println("\u2502 2. Phase DPS \u2502"); 133 | System.out.println("\u2502 3. Damage Distribution \u2502"); 134 | System.out.println("\u2502 4. Graph Total Damage \u2502"); 135 | System.out.println("\u2502 5. Misc. Combat Stats \u2502"); 136 | System.out.println("\u2502 6. Final Boon Rates \u2502"); 137 | System.out.println("\u2502 7. Phase Boon Rates \u2502"); 138 | System.out.println("\u2502 8. Dump All Tables \u2502"); 139 | System.out.println("\u2502 9. Quit \u2502"); 140 | System.out.println("\u2514" + Utility.fillWithChar(24, '\u2500') + "\u2518"); 141 | 142 | // Read user input 143 | MenuChoice choice = null; 144 | System.out.println(Utility.boxText("Enter an option by number")); 145 | System.out.print(" >> "); 146 | if (scan.hasNextInt()) 147 | { 148 | System.out.println(System.lineSeparator() + Utility.fillWithChar(50, '\u2500') 149 | + System.lineSeparator()); 150 | choice = MenuChoice.getEnum(scan.nextInt()); 151 | } 152 | scan.nextLine(); 153 | 154 | // Invalid option 155 | if (choice == null) 156 | { 157 | System.out.println(Utility.boxText("WARNING : Invalid option")); 158 | } 159 | // Quit 160 | else if (choice.equals(MenuChoice.QUIT)) 161 | { 162 | will_quit = true; 163 | } 164 | // Valid option 165 | else 166 | { 167 | // Apply option to all logs 168 | for (Path log : log_files) 169 | { 170 | System.out.println(Utility.boxText("INPUT : " + log.getFileName().toString())); 171 | String output = parseFileByChoice(choice, log); 172 | System.out.println(output + System.lineSeparator() + System.lineSeparator() 173 | + Utility.fillWithChar(50, '\u2500') + System.lineSeparator()); 174 | } 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | // Close scanner 182 | finally 183 | { 184 | if (scan != null) 185 | { 186 | scan.close(); 187 | } 188 | } 189 | return; 190 | } 191 | 192 | private static String parseFileByChoice(MenuChoice choice, Path path) 193 | { 194 | // Parse a new file 195 | if (current_file == null || !current_file.equals(path.getFileName().toString().split("\\.")[0])) 196 | { 197 | try 198 | { 199 | parsed_file = new Parse(path.toString()); 200 | statistics = new Statistics(parsed_file); 201 | current_file = path.getFileName().toString().split("\\.")[0]; 202 | if (displaying) 203 | { 204 | TableBuilder table = new TableBuilder(); 205 | table.addRow("Build Version", "Point of View", "Start Date", "End Date"); 206 | table.addRow(parsed_file.getLogData().toStringArray()); 207 | System.out.println(table.toString()); 208 | } 209 | if (Integer.valueOf(parsed_file.getLogData().getBuildVersion().replaceAll("EVTC", "")) < 20170218) 210 | { 211 | return old_version; 212 | } 213 | } catch (IOException e) 214 | { 215 | e.printStackTrace(); 216 | } 217 | } 218 | 219 | // Apply option 220 | if (choice.equals(MenuChoice.FINAL_DPS)) 221 | { 222 | return statistics.getFinalDPS(); 223 | } 224 | else if (choice.equals(MenuChoice.PHASE_DPS)) 225 | { 226 | return statistics.getPhaseDPS(); 227 | } 228 | else if (choice.equals(MenuChoice.DMG_DIST)) 229 | { 230 | return statistics.getDamageDistribution(); 231 | } 232 | else if (choice.equals(MenuChoice.G_TOTAL_DMG)) 233 | { 234 | return Utility.boxText("OUTPUT : " + statistics.getTotalDamageGraph(current_file)); 235 | } 236 | else if (choice.equals(MenuChoice.MISC_STATS)) 237 | { 238 | return statistics.getCombatStatistics(); 239 | } 240 | else if (choice.equals(MenuChoice.FINAL_BOONS)) 241 | { 242 | return statistics.getFinalBoons(); 243 | } 244 | else if (choice.equals(MenuChoice.PHASE_BOONS)) 245 | { 246 | return statistics.getPhaseBoons(); 247 | } 248 | else if (choice.equals(MenuChoice.DUMP_EVTC)) 249 | { 250 | File evtc_dump = new File( 251 | "./tables/" + current_file + "_" + parsed_file.getBossData().getName() + "_evtc-dump.txt"); 252 | try 253 | { 254 | Utility.writeToFile(parsed_file.toString(), evtc_dump); 255 | } catch (IOException e) 256 | { 257 | e.printStackTrace(); 258 | } 259 | return Utility.boxText("OUTPUT : " + evtc_dump.getName()); 260 | } 261 | else if (choice.equals(MenuChoice.DUMP_TABLES)) 262 | { 263 | File text_dump = new File( 264 | "./tables/" + current_file + "_" + parsed_file.getBossData().getName() + "_all-tables.txt"); 265 | try 266 | { 267 | Utility.writeToFile(statistics.getFinalDPS() + System.lineSeparator() + statistics.getPhaseDPS() 268 | + System.lineSeparator() + statistics.getCombatStatistics() + System.lineSeparator() 269 | + statistics.getFinalBoons() + System.lineSeparator() + statistics.getPhaseBoons() 270 | + System.lineSeparator() + statistics.getDamageDistribution(), text_dump); 271 | } catch (IOException e) 272 | { 273 | e.printStackTrace(); 274 | } 275 | return Utility.boxText("OUTPUT : " + text_dump.getName()); 276 | } 277 | return ""; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/boon/AbstractBoon.java: -------------------------------------------------------------------------------- 1 | package boon; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | public abstract class AbstractBoon { 8 | 9 | // Fields 10 | protected List boon_stack = new ArrayList(); 11 | protected int capacity; 12 | 13 | // Constructor 14 | public AbstractBoon(int capacity) { 15 | this.capacity = capacity; 16 | } 17 | 18 | // Abstract Methods 19 | public abstract int getStackValue(); 20 | 21 | public abstract void update(int time_passed); 22 | 23 | public abstract void addStacksBetween(List boon_stacks, int time_between); 24 | 25 | // Public Methods 26 | public void add(int boon_duration) { 27 | // Find empty slot 28 | if (!isFull()) { 29 | boon_stack.add(boon_duration); 30 | sort(); 31 | } 32 | // Replace lowest value 33 | else { 34 | int index = boon_stack.size() - 1; 35 | if (boon_stack.get(index) < boon_duration) { 36 | boon_stack.set(index, boon_duration); 37 | sort(); 38 | } 39 | } 40 | } 41 | 42 | // Protected Methods 43 | protected boolean isFull() { 44 | return boon_stack.size() >= capacity; 45 | } 46 | 47 | protected void sort() { 48 | Collections.sort(boon_stack, Collections.reverseOrder()); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/boon/BoonFactory.java: -------------------------------------------------------------------------------- 1 | package boon; 2 | 3 | import enums.Boon; 4 | 5 | public class BoonFactory { 6 | 7 | // Factory 8 | public AbstractBoon makeBoon(Boon boon) { 9 | if (boon.getType().equals("intensity")) { 10 | return new Intensity(boon.getCapacity()); 11 | } else if (boon.getType().equals("duration")) { 12 | return new Duration(boon.getCapacity()); 13 | } else { 14 | return null; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/boon/Duration.java: -------------------------------------------------------------------------------- 1 | package boon; 2 | 3 | import java.util.List; 4 | 5 | public class Duration extends AbstractBoon { 6 | 7 | // Constructor 8 | public Duration(int capacity) { 9 | super(capacity); 10 | } 11 | 12 | // Public Methods 13 | @Override 14 | public int getStackValue() { 15 | return boon_stack.stream().mapToInt(Integer::intValue).sum(); 16 | } 17 | 18 | @Override 19 | public void update(int time_passed) { 20 | 21 | if (!boon_stack.isEmpty()) { 22 | // Clear stack 23 | if (time_passed >= getStackValue()) { 24 | boon_stack.clear(); 25 | return; 26 | } 27 | // Remove from the longest duration 28 | else { 29 | boon_stack.set(0, boon_stack.get(0) - time_passed); 30 | if (boon_stack.get(0) <= 0) { 31 | // Spend leftover time 32 | time_passed = Math.abs(boon_stack.get(0)); 33 | boon_stack.remove(0); 34 | update(time_passed); 35 | } 36 | } 37 | } 38 | } 39 | 40 | @Override 41 | public void addStacksBetween(List boon_stacks, int time_between) { 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/boon/Intensity.java: -------------------------------------------------------------------------------- 1 | package boon; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | 8 | public class Intensity extends AbstractBoon { 9 | 10 | // Constructor 11 | public Intensity(int capacity) { 12 | super(capacity); 13 | } 14 | 15 | // Public Methods 16 | @Override 17 | public int getStackValue() { 18 | return boon_stack.size(); 19 | } 20 | 21 | @Override 22 | public void update(int time_passed) { 23 | 24 | // Subtract from each 25 | for (int i = 0; i < boon_stack.size(); i++) { 26 | boon_stack.set(i, boon_stack.get(i) - time_passed); 27 | } 28 | // Remove negatives 29 | for (Iterator iter = boon_stack.listIterator(); iter.hasNext();) { 30 | Integer stack = iter.next(); 31 | if (stack <= 0) { 32 | iter.remove(); 33 | } 34 | } 35 | } 36 | 37 | @Override 38 | public void addStacksBetween(List boon_stacks, int time_between) { 39 | 40 | // Create copy of the boon 41 | Intensity boon_copy = new Intensity(this.capacity); 42 | boon_copy.boon_stack = new ArrayList(this.boon_stack); 43 | List stacks = boon_copy.boon_stack; 44 | 45 | // Simulate the boon stack decreasing 46 | if (!stacks.isEmpty()) { 47 | 48 | int time_passed = 0; 49 | int min_duration = Collections.min(stacks); 50 | 51 | // Remove minimum duration from stack 52 | for (int i = 1; i < time_between; i++) { 53 | if ((i - time_passed) >= min_duration) { 54 | boon_copy.update(i - time_passed); 55 | if (!stacks.isEmpty()) { 56 | min_duration = Collections.min(stacks); 57 | } 58 | time_passed = i; 59 | } 60 | boon_stacks.add(boon_copy.getStackValue()); 61 | } 62 | } 63 | // Fill in remaining time with 0 values 64 | else { 65 | for (int i = 1; i < time_between; i++) { 66 | boon_stacks.add(0); 67 | } 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/data/AgentData.java: -------------------------------------------------------------------------------- 1 | package data; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import enums.Agent; 7 | 8 | public class AgentData { 9 | 10 | // Fields 11 | private List player_agent_list = new ArrayList(); 12 | private List NPC_agent_list = new ArrayList(); 13 | private List gadget_agent_list = new ArrayList(); 14 | private List all_agents_list = new ArrayList(); 15 | 16 | // Constructors 17 | public AgentData() { 18 | } 19 | 20 | // Public Methods 21 | public void addItem(Agent agent, AgentItem item) { 22 | if (agent.equals(Agent.NPC)) { 23 | NPC_agent_list.add(item); 24 | } else if (agent.equals(Agent.GADGET)) { 25 | gadget_agent_list.add(item); 26 | } else { 27 | player_agent_list.add(item); 28 | } 29 | all_agents_list.add(item); 30 | } 31 | 32 | // Getters 33 | public List getPlayerAgentList() { 34 | return player_agent_list; 35 | } 36 | 37 | public List getNPCAgentList() { 38 | return NPC_agent_list; 39 | } 40 | 41 | public List getGadgetAgentList() { 42 | return gadget_agent_list; 43 | } 44 | 45 | public List getAllAgentsList() { 46 | return all_agents_list; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/data/AgentItem.java: -------------------------------------------------------------------------------- 1 | package data; 2 | 3 | public class AgentItem { 4 | 5 | // Fields 6 | private long agent; 7 | private int instid = 0; 8 | private int first_aware = 0; 9 | private int last_aware = Integer.MAX_VALUE; 10 | private String name; 11 | private String prof; 12 | private int toughness = 0; 13 | private int healing = 0; 14 | private int condition = 0; 15 | 16 | // Constructors 17 | public AgentItem(long agent, String name, String prof) { 18 | this.agent = agent; 19 | this.name = name; 20 | this.prof = prof; 21 | } 22 | 23 | public AgentItem(long agent, String name, String prof, int toughness, int healing, int condition) { 24 | this.agent = agent; 25 | this.name = name; 26 | this.prof = prof; 27 | this.toughness = toughness; 28 | this.healing = healing; 29 | this.condition = condition; 30 | } 31 | 32 | // Public Methods 33 | public String[] toStringArray() { 34 | String[] array = new String[9]; 35 | array[0] = Long.toHexString(agent); 36 | array[1] = String.valueOf(instid); 37 | array[2] = String.valueOf(first_aware); 38 | array[3] = String.valueOf(last_aware); 39 | array[4] = name; 40 | array[5] = prof; 41 | array[6] = String.valueOf(toughness); 42 | array[7] = String.valueOf(healing); 43 | array[8] = String.valueOf(condition); 44 | return array; 45 | } 46 | 47 | // Getters 48 | public long getAgent() { 49 | return agent; 50 | } 51 | 52 | public int getInstid() { 53 | return instid; 54 | } 55 | 56 | public int getFirstAware() { 57 | return first_aware; 58 | } 59 | 60 | public int getLastAware() { 61 | return last_aware; 62 | } 63 | 64 | public String getName() { 65 | return name; 66 | } 67 | 68 | public String getProf() { 69 | return prof; 70 | } 71 | 72 | public int getToughness() { 73 | return toughness; 74 | } 75 | 76 | public int getHealing() { 77 | return healing; 78 | } 79 | 80 | public int getCondition() { 81 | return condition; 82 | } 83 | 84 | // Setters 85 | public void setInstid(int instid) { 86 | this.instid = instid; 87 | } 88 | 89 | public void setFirstAware(int first_aware) { 90 | this.first_aware = first_aware; 91 | } 92 | 93 | public void setLastAware(int last_aware) { 94 | this.last_aware = last_aware; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/data/BossData.java: -------------------------------------------------------------------------------- 1 | package data; 2 | 3 | public class BossData 4 | { 5 | 6 | // Fields 7 | private long agent = 0; 8 | private int instid = 0; 9 | private int first_aware = 0; 10 | private int last_aware = Integer.MAX_VALUE; 11 | private int id; 12 | private String name = "UNKNOWN"; 13 | private int health = -1; 14 | 15 | // Constructors 16 | public BossData(int id) 17 | { 18 | this.id = id; 19 | } 20 | 21 | // Public Methods 22 | public String[] getPhaseNames() 23 | { 24 | if (name.equals("Vale Guardian")) 25 | { 26 | return new String[] { "100% - 66%", "66% - 33%", "33% - 0%" }; 27 | } 28 | else if (name.equals("Gorseval the Multifarious")) 29 | { 30 | return new String[] { "100% - 66%", "66% - 33%", "33% - 0%" }; 31 | } 32 | else if (name.equals("Sabetha the Saboteur")) 33 | { 34 | return new String[] { "100% - 75%", "75% - 50%", "50% - 25%", "25% - 0%" }; 35 | } 36 | else if (name.equals("Slothasor")) 37 | { 38 | return new String[] { "100% - 80%", "80% - 60%", "60% - 40%", "40% - 20%", "20% - 10%", "10% - 0%" }; 39 | } 40 | else if (name.equals("Matthias Gabrel")) 41 | { 42 | return new String[] { "100% - 80%", "80% - 60%", "60% - 40%", "40% - 0%" }; 43 | } 44 | else if (name.equals("Keep Construct")) 45 | { 46 | return new String[] { "100% - 66%", "66% - 33%", "33% - 0%" }; 47 | } 48 | else if (name.equals("Xera")) 49 | { 50 | return new String[] { "100% - 50%", "50% - 0%" }; 51 | } 52 | else if (name.equals("Cairn the Indomitable")) 53 | { 54 | return new String[] { "100% - 75%", "75% - 50%", "50% - 25%", "25% - 0%" }; 55 | } 56 | else if (name.equals("Mursaat Overseer")) 57 | { 58 | return new String[] { "100% - 75%", "75% - 50%", "50% - 25%", "25% - 0%" }; 59 | } 60 | else if (name.equals("Samarog")) 61 | { 62 | return new String[] { "100% - 66%", "66% - 33%", "33% - 0%" }; 63 | } 64 | else if (name.equals("Deimos")) 65 | { 66 | return new String[] { "100% - 75%", "75% - 50%", "50% - 25%", "25% - 10%" }; 67 | } 68 | return new String[] { "100% - 0%" }; 69 | } 70 | 71 | public String[] toStringArray() 72 | { 73 | String[] array = new String[7]; 74 | array[0] = Long.toHexString(agent); 75 | array[1] = String.valueOf(instid); 76 | array[2] = String.valueOf(first_aware); 77 | array[3] = String.valueOf(last_aware); 78 | array[4] = String.valueOf(id); 79 | array[5] = name; 80 | array[6] = String.valueOf(health); 81 | return array; 82 | } 83 | 84 | // Getters 85 | public long getAgent() 86 | { 87 | return agent; 88 | } 89 | 90 | public int getInstid() 91 | { 92 | return instid; 93 | } 94 | 95 | public int getFirstAware() 96 | { 97 | return first_aware; 98 | } 99 | 100 | public int getLastAware() 101 | { 102 | return last_aware; 103 | } 104 | 105 | public int getID() 106 | { 107 | return id; 108 | } 109 | 110 | public String getName() 111 | { 112 | return name; 113 | } 114 | 115 | public int getHealth() 116 | { 117 | return health; 118 | } 119 | 120 | // Setters 121 | public void setAgent(long agent) 122 | { 123 | this.agent = agent; 124 | } 125 | 126 | public void setInstid(int instid) 127 | { 128 | this.instid = instid; 129 | } 130 | 131 | public void setFirstAware(int first_aware) 132 | { 133 | this.first_aware = first_aware; 134 | } 135 | 136 | public void setLastAware(int last_aware) 137 | { 138 | this.last_aware = last_aware; 139 | } 140 | 141 | public void setName(String name) 142 | { 143 | this.name = name; 144 | } 145 | 146 | public void setHealth(int health) 147 | { 148 | this.health = health; 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /src/data/CombatData.java: -------------------------------------------------------------------------------- 1 | package data; 2 | 3 | import java.awt.Point; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import enums.StateChange; 8 | 9 | public class CombatData 10 | { 11 | 12 | // Fields 13 | private List combat_list; 14 | 15 | // Constructors 16 | public CombatData() 17 | { 18 | this.combat_list = new ArrayList(); 19 | } 20 | 21 | // Public Methods 22 | public void addItem(CombatItem item) 23 | { 24 | combat_list.add(item); 25 | } 26 | 27 | public List getStates(int src_instid, StateChange change) 28 | { 29 | List states = new ArrayList(); 30 | for (CombatItem c : combat_list) 31 | { 32 | if (c.getSrcInstid() == src_instid && c.isStateChange().equals(change)) 33 | { 34 | states.add(new Point(c.getTime(), (int) c.getDstAgent())); 35 | } 36 | } 37 | return states; 38 | } 39 | 40 | public int getSkillCount(int src_instid, int skill_id) 41 | { 42 | int count = 0; 43 | for (CombatItem c : combat_list) 44 | { 45 | if (c.getSrcInstid() == src_instid && c.getSkillID() == skill_id) 46 | { 47 | count++; 48 | } 49 | } 50 | return count; 51 | } 52 | 53 | // Getters 54 | public List getCombatList() 55 | { 56 | return combat_list; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/data/CombatItem.java: -------------------------------------------------------------------------------- 1 | package data; 2 | 3 | import enums.Activation; 4 | import enums.BuffRemove; 5 | import enums.IFF; 6 | import enums.Result; 7 | import enums.StateChange; 8 | 9 | public class CombatItem 10 | { 11 | 12 | // Fields 13 | private int time; 14 | private long src_agent; 15 | private long dst_agent; 16 | private int value; 17 | private int buff_dmg; 18 | private int overstack_value; 19 | private int skill_id; 20 | private int src_instid; 21 | private int dst_instid; 22 | private int src_master_instid; 23 | private IFF iff; 24 | private int is_buff; 25 | private Result result; 26 | private Activation is_activation; 27 | private BuffRemove is_buffremove; 28 | private int is_ninety; 29 | private int is_fifty; 30 | private int is_moving; 31 | private StateChange is_statechange; 32 | private int is_flanking; 33 | 34 | // Constructor 35 | public CombatItem(int time, long src_agent, long dst_agent, int value, int buff_dmg, int overstack_value, 36 | int skill_id, int src_instid, int dst_instid, int src_master_instid, IFF iff, int buff, Result result, 37 | Activation is_activation, BuffRemove is_buffremove, int is_ninety, int is_fifty, int is_moving, 38 | StateChange is_statechange, int is_flanking) 39 | { 40 | this.time = time; 41 | this.src_agent = src_agent; 42 | this.dst_agent = dst_agent; 43 | this.value = value; 44 | this.buff_dmg = buff_dmg; 45 | this.overstack_value = overstack_value; 46 | this.skill_id = skill_id; 47 | this.src_instid = src_instid; 48 | this.dst_instid = dst_instid; 49 | this.src_master_instid = src_master_instid; 50 | this.iff = iff; 51 | this.is_buff = buff; 52 | this.result = result; 53 | this.is_activation = is_activation; 54 | this.is_buffremove = is_buffremove; 55 | this.is_ninety = is_ninety; 56 | this.is_fifty = is_fifty; 57 | this.is_moving = is_moving; 58 | this.is_statechange = is_statechange; 59 | this.is_flanking = is_flanking; 60 | } 61 | 62 | // Public Methods 63 | public String[] toStringArray() 64 | { 65 | String[] array = new String[20]; 66 | array[0] = String.valueOf(time); 67 | array[1] = Long.toHexString(src_agent); 68 | array[2] = Long.toHexString(dst_agent); 69 | array[3] = String.valueOf(value); 70 | array[4] = String.valueOf(buff_dmg); 71 | array[5] = String.valueOf(overstack_value); 72 | array[6] = String.valueOf(skill_id); 73 | array[7] = String.valueOf(src_instid); 74 | array[8] = String.valueOf(dst_instid); 75 | array[9] = String.valueOf(src_master_instid); 76 | array[10] = String.valueOf(iff); 77 | array[11] = String.valueOf(is_buff); 78 | array[12] = String.valueOf(result); 79 | array[13] = String.valueOf(is_activation); 80 | array[14] = String.valueOf(is_buffremove); 81 | array[15] = String.valueOf(is_ninety); 82 | array[16] = String.valueOf(is_fifty); 83 | array[17] = String.valueOf(is_moving); 84 | array[18] = String.valueOf(is_statechange); 85 | array[19] = String.valueOf(is_flanking); 86 | return array; 87 | } 88 | 89 | // Getters 90 | public int getTime() 91 | { 92 | return time; 93 | } 94 | 95 | public long getSrcAgent() 96 | { 97 | return src_agent; 98 | } 99 | 100 | public long getDstAgent() 101 | { 102 | return dst_agent; 103 | } 104 | 105 | public int getValue() 106 | { 107 | return value; 108 | } 109 | 110 | public int getBuffDmg() 111 | { 112 | return buff_dmg; 113 | } 114 | 115 | public int getOverstackValue() 116 | { 117 | return overstack_value; 118 | } 119 | 120 | public int getSkillID() 121 | { 122 | return skill_id; 123 | } 124 | 125 | public int getSrcInstid() 126 | { 127 | return src_instid; 128 | } 129 | 130 | public int getDstInstid() 131 | { 132 | return dst_instid; 133 | } 134 | 135 | public int getSrcMasterInstid() 136 | { 137 | return src_master_instid; 138 | } 139 | 140 | public IFF getIFF() 141 | { 142 | return iff; 143 | } 144 | 145 | public int isBuff() 146 | { 147 | return is_buff; 148 | } 149 | 150 | public Result getResult() 151 | { 152 | return result; 153 | } 154 | 155 | public Activation isActivation() 156 | { 157 | return is_activation; 158 | } 159 | 160 | public BuffRemove isBuffremove() 161 | { 162 | return is_buffremove; 163 | } 164 | 165 | public int isNinety() 166 | { 167 | return is_ninety; 168 | } 169 | 170 | public int isFifty() 171 | { 172 | return is_fifty; 173 | } 174 | 175 | public int isMoving() 176 | { 177 | return is_moving; 178 | } 179 | 180 | public int isFlanking() 181 | { 182 | return is_flanking; 183 | } 184 | 185 | public StateChange isStateChange() 186 | { 187 | return is_statechange; 188 | } 189 | 190 | // Setters 191 | public void setSrcAgent(long src_agent) 192 | { 193 | this.src_agent = src_agent; 194 | } 195 | 196 | public void setDstAgent(long dst_agent) 197 | { 198 | this.dst_agent = dst_agent; 199 | } 200 | 201 | public void setSrcInstid(int src_instid) 202 | { 203 | this.src_instid = src_instid; 204 | } 205 | 206 | public void setDstInstid(int dst_instid) 207 | { 208 | this.dst_instid = dst_instid; 209 | } 210 | 211 | } -------------------------------------------------------------------------------- /src/data/LogData.java: -------------------------------------------------------------------------------- 1 | package data; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Date; 5 | import java.util.TimeZone; 6 | 7 | public class LogData 8 | { 9 | 10 | // Fields 11 | private String build_version; 12 | private String pov = "N/A"; 13 | private String log_start = "yyyy-MM-dd HH:mm:ss z"; 14 | private String log_end = "yyyy-MM-dd HH:mm:ss z"; 15 | private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); 16 | 17 | // Constructors 18 | public LogData(String build_version) 19 | { 20 | this.build_version = build_version; 21 | this.sdf.setTimeZone(TimeZone.getDefault()); 22 | } 23 | 24 | // Public Methods 25 | public String[] toStringArray() 26 | { 27 | String[] array = new String[4]; 28 | array[0] = String.valueOf(build_version); 29 | array[1] = String.valueOf(pov); 30 | array[2] = String.valueOf(log_start); 31 | array[3] = String.valueOf(log_end); 32 | return array; 33 | } 34 | 35 | // Getters 36 | public String getBuildVersion() 37 | { 38 | return build_version; 39 | } 40 | 41 | public String getPOV() 42 | { 43 | return pov; 44 | } 45 | 46 | public String getLogStart() 47 | { 48 | return log_start; 49 | } 50 | 51 | public String getLogEnd() 52 | { 53 | return log_end; 54 | } 55 | 56 | // Setters 57 | public void setPOV(String pov) 58 | { 59 | this.pov = pov.substring(0, pov.lastIndexOf('\0')); 60 | } 61 | 62 | public void setLogStart(int unix_seconds) 63 | { 64 | this.log_start = sdf.format(new Date(unix_seconds * 1000L)); 65 | } 66 | 67 | public void setLogEnd(int unix_seconds) 68 | { 69 | this.log_end = sdf.format(new Date(unix_seconds * 1000L)); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/data/SkillData.java: -------------------------------------------------------------------------------- 1 | package data; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import enums.CustomSkill; 7 | 8 | public class SkillData 9 | { 10 | 11 | // Fields 12 | private List skill_list; 13 | 14 | // Constructors 15 | public SkillData() 16 | { 17 | this.skill_list = new ArrayList(); 18 | } 19 | 20 | // Public Methods 21 | public void addItem(SkillItem item) 22 | { 23 | skill_list.add(item); 24 | } 25 | 26 | public String getName(int ID) 27 | { 28 | 29 | // Custom 30 | CustomSkill custom_skill = CustomSkill.getEnum(ID); 31 | if (custom_skill != null) 32 | { 33 | return custom_skill.name(); 34 | } 35 | 36 | // Normal 37 | for (SkillItem s : skill_list) 38 | { 39 | if (s.getID() == ID) 40 | { 41 | return s.getName(); 42 | } 43 | } 44 | 45 | // Unknown 46 | return "uid: " + String.valueOf(ID); 47 | } 48 | 49 | // Getters 50 | public List getSkillList() 51 | { 52 | return skill_list; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/data/SkillItem.java: -------------------------------------------------------------------------------- 1 | package data; 2 | 3 | public class SkillItem { 4 | 5 | // Fields 6 | private int ID; 7 | private String name; 8 | 9 | // Constructor 10 | public SkillItem(int ID, String name) { 11 | this.ID = ID; 12 | this.name = name; 13 | } 14 | 15 | // Public Methods 16 | public String[] toStringArray() { 17 | String[] array = new String[2]; 18 | array[0] = String.valueOf(ID); 19 | array[1] = String.valueOf(name); 20 | return array; 21 | } 22 | 23 | // Getters 24 | public int getID() { 25 | return ID; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/enums/Activation.java: -------------------------------------------------------------------------------- 1 | package enums; 2 | 3 | public enum Activation 4 | { 5 | // Constants 6 | NONE(0), 7 | NORMAL(1), 8 | QUICKNESS(2), 9 | CANCEL_FIRE(3), 10 | CANCEL_CANCEL(4), 11 | RESET(5); 12 | 13 | // Fields 14 | private int ID; 15 | 16 | // Constructors 17 | private Activation(int ID) 18 | { 19 | this.ID = ID; 20 | } 21 | 22 | // Public Methods 23 | public static Activation getEnum(int ID) 24 | { 25 | for (Activation a : values()) 26 | { 27 | if (a.getID() == ID) 28 | { 29 | return a; 30 | } 31 | } 32 | return null; 33 | } 34 | 35 | // Getters 36 | public int getID() 37 | { 38 | return ID; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/enums/Agent.java: -------------------------------------------------------------------------------- 1 | package enums; 2 | 3 | public enum Agent 4 | { 5 | 6 | // Constants 7 | NPC(-1, "NPC"), 8 | GADGET(0, "GDG"), 9 | GUARDIAN(1, "Guardian"), 10 | WARRIOR(2, "Warrior"), 11 | ENGINEER(3, "Engineer"), 12 | RANGER(4, "Ranger"), 13 | THIEF(5, "Thief"), 14 | ELEMENTALIST(6, "Elementalist"), 15 | MESMER(7, "Mesmer"), 16 | NECROMANCER(8, "Necromancer"), 17 | REVENANT(9, "Revenant"), 18 | DRAGONHUNTER(10, "Dragonhunter"), 19 | BERSERKER(11, "Berserker"), 20 | SCRAPPER(12, "Scrapper"), 21 | DRUID(13, "Druid"), 22 | DAREDEVIL(14, "Daredevil"), 23 | TEMPEST(15, "Tempest"), 24 | CHRONOMANCER(16, "Chronomancer"), 25 | REAPER(17, "Reaper"), 26 | HERALD(18, "Herald"); 27 | 28 | // Fields 29 | private String name; 30 | private int ID; 31 | 32 | // Constructor 33 | Agent(int ID, String name) 34 | { 35 | this.name = name; 36 | this.ID = ID; 37 | } 38 | 39 | // Public Methods 40 | public static Agent getEnum(int ID, int is_elite) 41 | { 42 | for (Agent p : values()) 43 | { 44 | if (is_elite == -1) 45 | { 46 | if ((ID & 0xffff0000) == 0xffff0000) 47 | { 48 | return Agent.GADGET; 49 | } 50 | else 51 | { 52 | return Agent.NPC; 53 | } 54 | } 55 | else if (is_elite == 0) 56 | { 57 | if (p.getID() == ID) 58 | { 59 | return p; 60 | } 61 | } 62 | else if (is_elite == 1) 63 | { 64 | if (p.getID() == ID + 9) 65 | { 66 | return p; 67 | } 68 | } 69 | } 70 | return null; 71 | } 72 | 73 | // Getters 74 | public String getName() 75 | { 76 | return name; 77 | } 78 | 79 | public int getID() 80 | { 81 | return ID; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/enums/Boon.java: -------------------------------------------------------------------------------- 1 | package enums; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public enum Boon 7 | { 8 | // Boon 9 | MIGHT("Might", "MGHT", "intensity", 25), 10 | QUICKNESS("Quickness", "QCKN", "duration", 5), 11 | FURY("Fury", "FURY", "duration", 9), 12 | PROTECTION("Protection", "PROT", "duration", 5), 13 | 14 | // Mesmer (also Ventari Revenant o.o) 15 | ALACRITY("Alacrity", "ALAC", "duration", 9), 16 | 17 | // Ranger 18 | SPOTTER("Spotter", "SPOT", "duration", 1), 19 | SPIRIT_OF_FROST("Spirit of Frost", "FRST", "duration", 1), 20 | SUN_SPIRIT("Sun Spirit", "SUNS", "duration", 1), 21 | STONE_SPIRIT("Stone Spirit", "STNE", "duration", 1), 22 | STORM_SPIRIT("Storm Spirit", "STRM", "duration", 1), 23 | GLYPH_OF_EMPOWERMENT("Glyph of Empowerment", "GOFE", "duration", 1), 24 | GRACE_OF_THE_LAND("Grace of the Land", "GOTL", "intensity", 5), 25 | 26 | // Warrior 27 | EMPOWER_ALLIES("Empower Allies", "EALL", "duration", 1), 28 | BANNER_OF_STRENGTH("Banner of Strength", "STRB", "duration", 1), 29 | BANNER_OF_DISCIPLINE("Banner of Discipline", "DISC", "duration", 1), 30 | BANNER_OF_TACTICS("Banner of Tactics", "TACT", "duration", 1), 31 | BANNER_OF_DEFENCE("Banner of Defence", "DEFN", "duration", 1), 32 | 33 | // Revenant 34 | ASSASSINS_PRESENCE("Assassin's Presence", "ASNP", "duration", 1), 35 | NATURALISTIC_RESONANCE("Naturalistic Resonance", "NATR", "duration", 1), 36 | 37 | // Engineer 38 | PINPOINT_PRECISION("Pinpoint Distribution", "PIND", "duration", 1), 39 | 40 | // Elementalist 41 | SOOTHING_MIST("Soothing Mist", "MIST", "duration", 1), 42 | 43 | // Necro 44 | VAMPIRIC_PRESENCE("Vampiric Presence", "VAMP", "duration", 1), 45 | 46 | // Thief 47 | LEAD_ATTACKS("Lead Attacks", "LEAD", "intensity", 15), 48 | LOTUS_TRAINING("Lotus Training", "LOTS", "duration", 1), 49 | BOUNDING_DODGER("Bounding Dodger", "BDOG", "duration", 1), 50 | 51 | // Equipment 52 | MASTERFUL_CONCENTRATION("Masterful Concentration", "CONC", "duration", 1); 53 | // THORNS_RUNE("Thorns", "THRN", "intensity", 5); 54 | 55 | // Fields 56 | private String name; 57 | private String abrv; 58 | private String type; 59 | private int capacity; 60 | 61 | // Constructor 62 | private Boon(String name, String abrv, String type, int capacity) 63 | { 64 | this.name = name; 65 | this.abrv = abrv; 66 | this.type = type; 67 | this.capacity = capacity; 68 | } 69 | 70 | // Public Methods 71 | public static Boon getEnum(String name) 72 | { 73 | for (Boon b : values()) 74 | { 75 | if (b.getName() == name) 76 | { 77 | return b; 78 | } 79 | } 80 | return null; 81 | } 82 | 83 | public static String[] getArray() 84 | { 85 | List boonList = new ArrayList(); 86 | for (Boon b : values()) 87 | { 88 | boonList.add(b.getAbrv()); 89 | } 90 | return boonList.toArray(new String[boonList.size()]); 91 | } 92 | 93 | public static List getList() 94 | { 95 | List boonList = new ArrayList(); 96 | for (Boon b : values()) 97 | { 98 | boonList.add(b.getName()); 99 | } 100 | return boonList; 101 | } 102 | 103 | // Getters 104 | public String getName() 105 | { 106 | return this.name; 107 | } 108 | 109 | public String getAbrv() 110 | { 111 | return this.abrv; 112 | } 113 | 114 | public String getType() 115 | { 116 | return this.type; 117 | } 118 | 119 | public int getCapacity() 120 | { 121 | return this.capacity; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/enums/BuffRemove.java: -------------------------------------------------------------------------------- 1 | package enums; 2 | 3 | public enum BuffRemove 4 | { 5 | // Constants 6 | NONE(0), 7 | ALL(1), 8 | SINGLE(2), 9 | MANUAL(3); 10 | 11 | // Fields 12 | private int ID; 13 | 14 | // Constructors 15 | private BuffRemove(int ID) 16 | { 17 | this.ID = ID; 18 | } 19 | 20 | // Public Methods 21 | public static BuffRemove getEnum(int ID) 22 | { 23 | for (BuffRemove b : values()) 24 | { 25 | if (b.getID() == ID) 26 | { 27 | return b; 28 | } 29 | } 30 | return null; 31 | } 32 | 33 | // Getters 34 | public int getID() 35 | { 36 | return ID; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/enums/CustomSkill.java: -------------------------------------------------------------------------------- 1 | package enums; 2 | 3 | public enum CustomSkill 4 | { 5 | 6 | // Constants 7 | RESURRECT(1066, "Resurrect"), 8 | BANDAGE(1175, "Bandage"), 9 | DODGE(65001, "Dodge"); 10 | 11 | // Fields 12 | private int ID; 13 | private String name; 14 | 15 | // Constructors 16 | private CustomSkill(int ID, String name) 17 | { 18 | this.ID = ID; 19 | this.name = name; 20 | } 21 | 22 | // Public Methods 23 | public static CustomSkill getEnum(int ID) 24 | { 25 | for (CustomSkill c : values()) 26 | { 27 | if (c.getID() == ID) 28 | { 29 | return c; 30 | } 31 | } 32 | return null; 33 | } 34 | 35 | // Getters 36 | public int getID() 37 | { 38 | return ID; 39 | } 40 | 41 | public String getName() 42 | { 43 | return name; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/enums/IFF.java: -------------------------------------------------------------------------------- 1 | package enums; 2 | 3 | public enum IFF { 4 | 5 | // Constants 6 | FRIEND(0), 7 | FOE(1), 8 | UNKNOWN(2); 9 | 10 | // Fields 11 | private int ID; 12 | 13 | // Constructors 14 | private IFF(int ID) { 15 | this.ID = ID; 16 | } 17 | 18 | // Public Methods 19 | public static IFF getEnum(int ID) { 20 | for (IFF i : values()) { 21 | if (i.getID() == ID) { 22 | return i; 23 | } 24 | } 25 | return null; 26 | } 27 | 28 | // Getters 29 | public int getID() { 30 | return ID; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/enums/MenuChoice.java: -------------------------------------------------------------------------------- 1 | package enums; 2 | 3 | public enum MenuChoice { 4 | 5 | // Constants 6 | DUMP_EVTC(0, false), 7 | FINAL_DPS(1, true), 8 | PHASE_DPS(2, true), 9 | DMG_DIST(3, true), 10 | G_TOTAL_DMG(4, false), 11 | MISC_STATS(5, true), 12 | FINAL_BOONS(6, true), 13 | PHASE_BOONS(7, true), 14 | DUMP_TABLES(8, false), 15 | QUIT(9, false); 16 | 17 | // Fields 18 | private int ID; 19 | private boolean can_be_associated; 20 | 21 | // Constructor 22 | MenuChoice(int ID, boolean can_be_associated) { 23 | this.ID = ID; 24 | this.can_be_associated = can_be_associated; 25 | } 26 | 27 | // Public Methods 28 | public static MenuChoice getEnum(int ID) { 29 | for (MenuChoice c : values()) { 30 | if (c.getID() == ID) { 31 | return c; 32 | } 33 | } 34 | return null; 35 | } 36 | 37 | // Getters 38 | public int getID() { 39 | return ID; 40 | } 41 | 42 | public boolean canBeAssociated() { 43 | return can_be_associated; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/enums/Result.java: -------------------------------------------------------------------------------- 1 | package enums; 2 | 3 | public enum Result { 4 | 5 | // Constants 6 | NORMAL(0), 7 | CRIT(1), 8 | GLANCE(2), 9 | BLOCK(3), 10 | EVADE(4), 11 | INTERRUPT(5), 12 | ABSORB(6), 13 | BLIND(7), 14 | KILLING_BLOW(8); 15 | 16 | // Fields 17 | private int ID; 18 | 19 | // Constructors 20 | private Result(int ID) { 21 | this.ID = ID; 22 | } 23 | 24 | // Public Methods 25 | public static Result getEnum(int ID) { 26 | for (Result r : values()) { 27 | if (r.getID() == ID) { 28 | return r; 29 | } 30 | } 31 | return null; 32 | } 33 | 34 | // Getters 35 | public int getID() { 36 | return ID; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/enums/StateChange.java: -------------------------------------------------------------------------------- 1 | package enums; 2 | 3 | public enum StateChange 4 | { 5 | 6 | // Constants 7 | NORMAL(0), 8 | ENTER_COMBAT(1), 9 | EXIT_COMBAT(2), 10 | CHANGE_UP(3), 11 | CHANGE_DEAD(4), 12 | CHANGE_DOWN(5), 13 | SPAWN(6), 14 | DESPAWN(7), 15 | HEALTH_UPDATE(8), 16 | LOG_START(9), 17 | LOG_END(10), 18 | WEAPON_SWAP(11), 19 | MAX_HEALTH_UPDATE(12), 20 | POINT_OF_VIEW(13), 21 | CBTS_LANGUAGE(14); 22 | 23 | // Fields 24 | private int ID; 25 | 26 | // Constructors 27 | private StateChange(int ID) 28 | { 29 | this.ID = ID; 30 | } 31 | 32 | // Public Methods 33 | public static StateChange getEnum(int ID) 34 | { 35 | for (StateChange s : values()) 36 | { 37 | if (s.getID() == ID) 38 | { 39 | return s; 40 | } 41 | } 42 | return null; 43 | } 44 | 45 | // Getters 46 | public int getID() 47 | { 48 | return ID; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/player/BoonLog.java: -------------------------------------------------------------------------------- 1 | package player; 2 | 3 | public class BoonLog { 4 | 5 | // Fields 6 | private int time = 0; 7 | private int value = 0; 8 | 9 | // Constructor 10 | public BoonLog(int time, int value) { 11 | this.time = time; 12 | this.value = value; 13 | } 14 | 15 | // Getters 16 | public int getTime() { 17 | return time; 18 | } 19 | 20 | public int getValue() { 21 | return value; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/player/DamageLog.java: -------------------------------------------------------------------------------- 1 | package player; 2 | 3 | import enums.Result; 4 | 5 | public class DamageLog 6 | { 7 | 8 | // Fields 9 | private int time; 10 | private int damage; 11 | private int skill_id; 12 | private int buff; 13 | private Result result; 14 | private int is_ninety; 15 | private int is_moving; 16 | private int is_flanking; 17 | 18 | // Constructor 19 | public DamageLog(int time, int damage, int skill_id, int buff, Result result, int is_ninety, int is_moving, 20 | int is_flanking) 21 | { 22 | this.time = time; 23 | this.damage = damage; 24 | this.skill_id = skill_id; 25 | this.buff = buff; 26 | this.result = result; 27 | this.is_ninety = is_ninety; 28 | this.is_moving = is_moving; 29 | this.is_flanking = is_flanking; 30 | } 31 | 32 | // Getters 33 | public int getTime() 34 | { 35 | return time; 36 | } 37 | 38 | public int getDamage() 39 | { 40 | return damage; 41 | } 42 | 43 | public int getID() 44 | { 45 | return skill_id; 46 | } 47 | 48 | public int isCondi() 49 | { 50 | return buff; 51 | } 52 | 53 | public Result getResult() 54 | { 55 | return result; 56 | } 57 | 58 | public int isNinety() 59 | { 60 | return is_ninety; 61 | } 62 | 63 | public int isMoving() 64 | { 65 | return is_moving; 66 | } 67 | 68 | public int isFlanking() 69 | { 70 | return is_flanking; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/player/Player.java: -------------------------------------------------------------------------------- 1 | package player; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import data.AgentItem; 9 | import data.BossData; 10 | import data.CombatItem; 11 | import data.SkillData; 12 | import enums.Boon; 13 | import enums.IFF; 14 | import enums.StateChange; 15 | import statistics.Statistics; 16 | 17 | public class Player 18 | { 19 | // Fields 20 | private int instid; 21 | private String account; 22 | private String character; 23 | private String group; 24 | private String prof; 25 | private int toughness; 26 | private int healing; 27 | private int condition; 28 | private List damage_logs = new ArrayList(); 29 | private Map> boon_map = new HashMap<>(); 30 | 31 | // Constructors 32 | public Player(AgentItem agent) 33 | { 34 | this.instid = agent.getInstid(); 35 | String[] name = agent.getName().split(Character.toString('\0')); 36 | this.character = name[0]; 37 | this.account = name[1]; 38 | this.group = name[2]; 39 | if (Statistics.hiding_players) 40 | { 41 | this.character = "P:" + String.format("%04d", instid); 42 | this.account = ":A." + String.format("%04d", instid); 43 | } 44 | this.prof = agent.getProf(); 45 | this.toughness = agent.getToughness(); 46 | this.healing = agent.getHealing(); 47 | this.condition = agent.getCondition(); 48 | } 49 | 50 | // Getters 51 | public int getInstid() 52 | { 53 | return instid; 54 | } 55 | 56 | public String getAccount() 57 | { 58 | return account; 59 | } 60 | 61 | public String getCharacter() 62 | { 63 | return character; 64 | } 65 | 66 | public String getGroup() 67 | { 68 | return group; 69 | } 70 | 71 | public String getProf() 72 | { 73 | return prof; 74 | } 75 | 76 | public int getToughness() 77 | { 78 | return toughness; 79 | } 80 | 81 | public int getHealing() 82 | { 83 | return healing; 84 | } 85 | 86 | public int getCondition() 87 | { 88 | return condition; 89 | } 90 | 91 | public List getDamageLogs(BossData bossData, List combatList) 92 | { 93 | if (damage_logs.isEmpty()) 94 | { 95 | setDamageLogs(bossData, combatList); 96 | } 97 | return damage_logs; 98 | } 99 | 100 | public Map> getBoonMap(BossData bossData, SkillData skillData, List combatList) 101 | { 102 | if (boon_map.isEmpty()) 103 | { 104 | setBoonMap(bossData, skillData, combatList); 105 | } 106 | return boon_map; 107 | } 108 | 109 | // Private Methods 110 | private void setDamageLogs(BossData bossData, List combatList) 111 | { 112 | int time_start = bossData.getFirstAware(); 113 | for (CombatItem c : combatList) 114 | { 115 | if (instid == c.getSrcInstid() || instid == c.getSrcMasterInstid()) 116 | { 117 | StateChange state = c.isStateChange(); 118 | int time = c.getTime() - time_start; 119 | if (bossData.getInstid() == c.getDstInstid() && c.getIFF().equals(IFF.FOE)) 120 | { 121 | if (state.equals(StateChange.NORMAL)) 122 | { 123 | if (c.isBuff() == 1 && c.getBuffDmg() != 0) 124 | { 125 | damage_logs.add(new DamageLog(time, c.getBuffDmg(), c.getSkillID(), c.isBuff(), 126 | c.getResult(), c.isNinety(), c.isMoving(), c.isFlanking())); 127 | } 128 | else if (c.isBuff() == 0 && c.getValue() != 0) 129 | { 130 | damage_logs.add(new DamageLog(time, c.getValue(), c.getSkillID(), c.isBuff(), 131 | c.getResult(), c.isNinety(), c.isMoving(), c.isFlanking())); 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | public void setBoonMap(BossData bossData, SkillData skillData, List combatList) 140 | { 141 | 142 | // Initialize Boon Map with every Boon 143 | for (Boon boon : Boon.values()) 144 | { 145 | boon_map.put(boon.getName(), new ArrayList()); 146 | } 147 | 148 | // Fill in Boon Map 149 | int time_start = bossData.getFirstAware(); 150 | int fight_duration = bossData.getLastAware() - time_start; 151 | for (CombatItem c : combatList) 152 | { 153 | if (instid == c.getDstInstid()) 154 | { 155 | String skill_name = skillData.getName(c.getSkillID()); 156 | if (c.isBuff() == 1 && c.getValue() > 0) 157 | { 158 | if (boon_map.containsKey(skill_name)) 159 | { 160 | int time = c.getTime() - time_start; 161 | if (time < fight_duration) 162 | { 163 | boon_map.get(skill_name).add(new BoonLog(time, c.getValue())); 164 | } 165 | else 166 | { 167 | break; 168 | } 169 | } 170 | } 171 | } 172 | } 173 | 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/statistics/Parse.java: -------------------------------------------------------------------------------- 1 | package statistics; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.UnsupportedEncodingException; 8 | import java.nio.ByteBuffer; 9 | import java.nio.ByteOrder; 10 | import java.util.List; 11 | import java.util.Scanner; 12 | import java.util.zip.ZipEntry; 13 | import java.util.zip.ZipFile; 14 | 15 | import data.AgentData; 16 | import data.AgentItem; 17 | import data.BossData; 18 | import data.CombatData; 19 | import data.CombatItem; 20 | import data.LogData; 21 | import data.SkillData; 22 | import data.SkillItem; 23 | import enums.Activation; 24 | import enums.Agent; 25 | import enums.BuffRemove; 26 | import enums.IFF; 27 | import enums.Result; 28 | import enums.StateChange; 29 | import utility.TableBuilder; 30 | import utility.Utility; 31 | 32 | public class Parse 33 | { 34 | // Fields 35 | private BufferedInputStream f = null; 36 | private LogData log_data; 37 | private BossData boss_data; 38 | private AgentData agent_data = new AgentData(); 39 | private SkillData skill_data = new SkillData(); 40 | private CombatData combat_data = new CombatData(); 41 | 42 | // Constructor 43 | public Parse(String file_path) throws IOException 44 | { 45 | 46 | // Read .evtc file into buffered input stream 47 | ZipFile zip_file = null; 48 | if (file_path.endsWith(".zip")) 49 | { 50 | zip_file = new ZipFile(file_path); 51 | ZipEntry evtc_file = zip_file.entries().nextElement(); 52 | f = new BufferedInputStream(zip_file.getInputStream(evtc_file)); 53 | } 54 | else if (file_path.endsWith(".evtc")) 55 | { 56 | f = new BufferedInputStream(new FileInputStream(new File(file_path))); 57 | } 58 | 59 | // Parse file 60 | try 61 | { 62 | parseBossData(); 63 | parseAgentData(); 64 | parseSkillData(); 65 | parseCombatList(); 66 | fillMissingData(); 67 | } 68 | 69 | // Close streams 70 | finally 71 | { 72 | try 73 | { 74 | if (zip_file != null) 75 | { 76 | zip_file.close(); 77 | } 78 | f.close(); 79 | } catch (IOException e) 80 | { 81 | e.printStackTrace(); 82 | } 83 | } 84 | } 85 | 86 | // Public Methods 87 | public LogData getLogData() 88 | { 89 | return log_data; 90 | } 91 | 92 | public BossData getBossData() 93 | { 94 | return boss_data; 95 | } 96 | 97 | public AgentData getAgentData() 98 | { 99 | return agent_data; 100 | } 101 | 102 | public SkillData getSkillData() 103 | { 104 | return skill_data; 105 | } 106 | 107 | public CombatData getCombatData() 108 | { 109 | return combat_data; 110 | } 111 | 112 | // Private Methods 113 | private void parseBossData() throws IOException 114 | { 115 | // 12 bytes: arc build version 116 | String build_version = getString(12); 117 | this.log_data = new LogData(build_version); 118 | 119 | // 1 byte: skip 120 | safeSkip(1); 121 | 122 | // 2 bytes: boss instance ID 123 | int instid = getShort(); 124 | 125 | // 1 byte: position 126 | safeSkip(1); 127 | 128 | // BossData 129 | this.boss_data = new BossData(instid); 130 | } 131 | 132 | private void parseAgentData() throws IOException 133 | { 134 | // 4 bytes: player count 135 | int player_count = getInt(); 136 | 137 | // 96 bytes: each player 138 | for (int i = 0; i < player_count; i++) 139 | { 140 | // 8 bytes: agent 141 | long agent = getLong(); 142 | 143 | // 4 bytes: profession 144 | int prof = getInt(); 145 | 146 | // 4 bytes: is_elite 147 | int is_elite = getInt(); 148 | 149 | // 4 bytes: toughness 150 | int toughness = getInt(); 151 | 152 | // 4 bytes: healing 153 | int healing = getInt(); 154 | 155 | // 4 bytes: condition 156 | int condition = getInt(); 157 | 158 | // 68 bytes: name 159 | String name = getString(68); 160 | 161 | // Agent 162 | Agent a = Agent.getEnum(prof, is_elite); 163 | 164 | // Add an agent 165 | if (a != null) 166 | { 167 | // NPC 168 | if (a.equals(Agent.NPC)) 169 | { 170 | agent_data.addItem(a, new AgentItem(agent, name, a.getName() + ":" + String.format("%05d", prof))); 171 | } 172 | // Gadget 173 | else if (a.equals(Agent.GADGET)) 174 | { 175 | agent_data.addItem(a, 176 | new AgentItem(agent, name, a.getName() + ":" + String.format("%05d", prof & 0x0000ffff))); 177 | } 178 | // Player 179 | else 180 | { 181 | agent_data.addItem(a, new AgentItem(agent, name, a.getName(), toughness, healing, condition)); 182 | } 183 | } 184 | // Unknown 185 | else 186 | { 187 | agent_data.addItem(a, new AgentItem(agent, name, String.valueOf(prof), toughness, healing, condition)); 188 | } 189 | } 190 | } 191 | 192 | private void parseSkillData() throws IOException 193 | { 194 | // 4 bytes: player count 195 | int skill_count = getInt(); 196 | 197 | // 68 bytes: each skill 198 | for (int i = 0; i < skill_count; i++) 199 | { 200 | // 4 bytes: skill ID 201 | int skill_id = getInt(); 202 | 203 | // 64 bytes: name 204 | String name = getString(64); 205 | 206 | // Add skill 207 | skill_data.addItem(new SkillItem(skill_id, name)); 208 | } 209 | } 210 | 211 | private void parseCombatList() throws IOException 212 | { 213 | // 64 bytes: each combat 214 | while (f.available() >= 64) 215 | { 216 | // 8 bytes: time 217 | int time = (int) getLong(); 218 | 219 | // 8 bytes: src_agent 220 | long src_agent = getLong(); 221 | 222 | // 8 bytes: dst_agent 223 | long dst_agent = getLong(); 224 | 225 | // 4 bytes: value 226 | int value = getInt(); 227 | 228 | // 4 bytes: buff_dmg 229 | int buff_dmg = getInt(); 230 | 231 | // 2 bytes: overstack_value 232 | int overstack_value = getShort(); 233 | 234 | // 2 bytes: skill_id 235 | int skill_id = getShort(); 236 | 237 | // 2 bytes: src_instid 238 | int src_instid = getShort(); 239 | 240 | // 2 bytes: dst_instid 241 | int dst_instid = getShort(); 242 | 243 | // 2 bytes: src_master_instid 244 | int src_master_instid = getShort(); 245 | 246 | // 9 bytes: garbage 247 | safeSkip(9); 248 | 249 | // 1 byte: iff 250 | IFF iff = IFF.getEnum(f.read()); 251 | 252 | // 1 byte: buff 253 | int buff = f.read(); 254 | 255 | // 1 byte: result 256 | Result result = Result.getEnum(f.read()); 257 | 258 | // 1 byte: is_activation 259 | Activation is_activation = Activation.getEnum(f.read()); 260 | 261 | // 1 byte: is_buffremove 262 | BuffRemove is_buffremove = BuffRemove.getEnum(f.read()); 263 | 264 | // 1 byte: is_ninety 265 | int is_ninety = f.read(); 266 | 267 | // 1 byte: is_fifty 268 | int is_fifty = f.read(); 269 | 270 | // 1 byte: is_moving 271 | int is_moving = f.read(); 272 | 273 | // 1 byte: is_statechange 274 | StateChange is_statechange = StateChange.getEnum(f.read()); 275 | 276 | // 1 byte: is_flanking 277 | int is_flanking = f.read(); 278 | 279 | // 3 bytes: garbage 280 | safeSkip(3); 281 | 282 | // Add combat 283 | combat_data.addItem(new CombatItem(time, src_agent, dst_agent, value, buff_dmg, overstack_value, skill_id, 284 | src_instid, dst_instid, src_master_instid, iff, buff, result, is_activation, is_buffremove, 285 | is_ninety, is_fifty, is_moving, is_statechange, is_flanking)); 286 | } 287 | } 288 | 289 | public void fillMissingData() 290 | { 291 | // Set Agent instid, first_aware and last_aware 292 | List player_list = agent_data.getPlayerAgentList(); 293 | List agent_list = agent_data.getAllAgentsList(); 294 | List combat_list = combat_data.getCombatList(); 295 | for (AgentItem a : agent_list) 296 | { 297 | boolean assigned_first = false; 298 | for (CombatItem c : combat_list) 299 | { 300 | if (a.getAgent() == c.getSrcAgent() && c.getSrcInstid() != 0) 301 | { 302 | if (!assigned_first) 303 | { 304 | a.setInstid(c.getSrcInstid()); 305 | a.setFirstAware(c.getTime()); 306 | assigned_first = true; 307 | } 308 | a.setLastAware(c.getTime()); 309 | } 310 | else if (a.getAgent() == c.getDstAgent() && c.getDstInstid() != 0) 311 | { 312 | if (!assigned_first) 313 | { 314 | a.setInstid(c.getDstInstid()); 315 | a.setFirstAware(c.getTime()); 316 | assigned_first = true; 317 | } 318 | a.setLastAware(c.getTime()); 319 | } 320 | else if (c.isStateChange() == StateChange.POINT_OF_VIEW) 321 | { 322 | int pov_instid = c.getSrcInstid(); 323 | for (AgentItem p : player_list) 324 | { 325 | if (pov_instid == p.getInstid()) 326 | { 327 | log_data.setPOV(p.getName()); 328 | } 329 | } 330 | 331 | } 332 | else if (c.isStateChange() == StateChange.LOG_START) 333 | { 334 | log_data.setLogStart(c.getValue()); 335 | } 336 | else if (c.isStateChange() == StateChange.LOG_END) 337 | { 338 | log_data.setLogEnd(c.getValue()); 339 | } 340 | } 341 | } 342 | 343 | // Manual log target selection 344 | if (boss_data.getID() == 1) 345 | { 346 | targetSelection(); 347 | } 348 | 349 | // Set Boss data agent, instid, first_aware, last_aware and name 350 | List NPC_list = agent_data.getNPCAgentList(); 351 | for (AgentItem NPC : NPC_list) 352 | { 353 | if (NPC.getProf().endsWith(String.valueOf(boss_data.getID()))) 354 | { 355 | if (boss_data.getAgent() == 0) 356 | { 357 | boss_data.setAgent(NPC.getAgent()); 358 | boss_data.setInstid(NPC.getInstid()); 359 | boss_data.setFirstAware(NPC.getFirstAware()); 360 | boss_data.setName(NPC.getName()); 361 | } 362 | boss_data.setLastAware(NPC.getLastAware()); 363 | } 364 | } 365 | 366 | // Set Boss health 367 | for (CombatItem c : combat_list) 368 | { 369 | if (c.getSrcInstid() == boss_data.getInstid() && c.isStateChange().equals(StateChange.MAX_HEALTH_UPDATE)) 370 | { 371 | boss_data.setHealth((int) c.getDstAgent()); 372 | break; 373 | } 374 | } 375 | 376 | // Dealing with second half of Xera | ((22611300 * 0.5) + (25560600 * 377 | // 0.5) 378 | int xera_2_instid = 0; 379 | for (AgentItem NPC : NPC_list) 380 | { 381 | if (NPC.getProf().contains("16286")) 382 | { 383 | xera_2_instid = NPC.getInstid(); 384 | boss_data.setHealth(24085950); 385 | boss_data.setLastAware(NPC.getLastAware()); 386 | for (CombatItem c : combat_list) 387 | { 388 | if (c.getSrcInstid() == xera_2_instid) 389 | { 390 | c.setSrcInstid(boss_data.getInstid()); 391 | } 392 | if (c.getDstInstid() == xera_2_instid) 393 | { 394 | c.setDstInstid(boss_data.getInstid()); 395 | } 396 | } 397 | break; 398 | } 399 | } 400 | 401 | } 402 | 403 | @SuppressWarnings("resource") 404 | private void targetSelection() 405 | { 406 | List NPC_list = agent_data.getNPCAgentList(); 407 | TableBuilder target_table = new TableBuilder(); 408 | target_table.addTitle("NPC List"); 409 | target_table.addRow("ID", "Name", "Species"); 410 | 411 | for (AgentItem NPC : NPC_list) 412 | { 413 | target_table.addRow(String.valueOf(NPC.getInstid()), NPC.getName(), NPC.getProf().substring(4)); 414 | } 415 | 416 | System.out.println(target_table.toString()); 417 | 418 | // Read user input 419 | Scanner scan = null; 420 | scan = new Scanner(System.in); 421 | boolean quitting = false; 422 | while (!quitting) 423 | { 424 | System.out.println(Utility.boxText("Select an NPC to target by ID")); 425 | System.out.print(" >> "); 426 | // A number 427 | if (scan.hasNextInt()) 428 | { 429 | int target_id = scan.nextInt(); 430 | for (AgentItem NPC : NPC_list) 431 | { 432 | // Input matches an ID 433 | if (target_id == NPC.getInstid()) 434 | { 435 | boss_data.setAgent(NPC.getAgent()); 436 | boss_data.setInstid(NPC.getInstid()); 437 | boss_data.setFirstAware(NPC.getFirstAware()); 438 | boss_data.setName(NPC.getName()); 439 | boss_data.setLastAware(NPC.getLastAware()); 440 | quitting = true; 441 | break; 442 | } 443 | } 444 | if (!quitting) 445 | { 446 | System.out.println(Utility.boxText("WARNING : Invalid NPC ID")); 447 | } 448 | } 449 | else 450 | { 451 | System.out.println(Utility.boxText("WARNING : Invalid NPC ID")); 452 | } 453 | scan.nextLine(); 454 | } 455 | } 456 | 457 | // Override 458 | @Override 459 | public String toString() 460 | { 461 | // Build tables 462 | StringBuilder output = new StringBuilder(); 463 | TableBuilder table = new TableBuilder(); 464 | 465 | // Log Data Table 466 | table.addTitle("LOG DATA"); 467 | table.addRow("build_version", "point_of_view", "log_start", "log_end"); 468 | table.addRow(log_data.toStringArray()); 469 | output.append(table.toString() + System.lineSeparator()); 470 | table.clear(); 471 | 472 | // Boss Data Table 473 | table.addTitle("BOSS DATA"); 474 | table.addRow("agent", "instid", "first_aware", "last_aware", "id", "name", "health"); 475 | table.addRow(boss_data.toStringArray()); 476 | output.append(table.toString() + System.lineSeparator()); 477 | table.clear(); 478 | 479 | // Player Data 480 | List playerAgents = agent_data.getPlayerAgentList(); 481 | List NPCAgents = agent_data.getNPCAgentList(); 482 | List gadgetAgents = agent_data.getGadgetAgentList(); 483 | table.addTitle("AGENT DATA"); 484 | table.addRow("agent", "instid", "first_aware", "last_aware", "name", "prof", "toughness", "healing", 485 | "condition"); 486 | for (AgentItem player : playerAgents) 487 | { 488 | table.addRow(player.toStringArray()); 489 | } 490 | for (AgentItem npc : NPCAgents) 491 | { 492 | table.addRow(npc.toStringArray()); 493 | } 494 | for (AgentItem gadget : gadgetAgents) 495 | { 496 | table.addRow(gadget.toStringArray()); 497 | } 498 | output.append(table.toString() + System.lineSeparator()); 499 | table.clear(); 500 | 501 | // Skill Data 502 | List skillList = skill_data.getSkillList(); 503 | table.addTitle("SKILL DATA"); 504 | table.addRow("ID", "name"); 505 | for (SkillItem s : skillList) 506 | { 507 | table.addRow(s.toStringArray()); 508 | } 509 | output.append(table.toString() + System.lineSeparator()); 510 | table.clear(); 511 | 512 | // Combat Data Table 513 | List combatList = combat_data.getCombatList(); 514 | table.addTitle("COMBAT DATA"); 515 | table.addRow("time", "src_agent", "dst_agent", "value", "buff_dmg", "overstack_value", "skill_id", "src_instid", 516 | "dst_instid", "src_master_instid", "iff", "buff", "is_crit", "is_activation", "is_buffremove", 517 | "is_ninety", "is_fifty", "is_moving", "is_statechange", "is_flanking"); 518 | for (CombatItem c : combatList) 519 | { 520 | table.addRow(c.toStringArray()); 521 | } 522 | output.append(table.toString() + System.lineSeparator()); 523 | 524 | return output.toString(); 525 | } 526 | 527 | // Private Methods 528 | private void safeSkip(long bytes_to_skip) throws IOException 529 | { 530 | while (bytes_to_skip > 0) 531 | { 532 | long bytes_actually_skipped = f.skip(bytes_to_skip); 533 | if (bytes_actually_skipped > 0) 534 | { 535 | bytes_to_skip -= bytes_actually_skipped; 536 | } 537 | else if (bytes_actually_skipped == 0) 538 | { 539 | if (f.read() == -1) 540 | { 541 | break; 542 | } 543 | else 544 | { 545 | bytes_to_skip--; 546 | } 547 | } 548 | } 549 | } 550 | 551 | private int getShort() throws IOException 552 | { 553 | byte[] bytes = new byte[2]; 554 | f.read(bytes); 555 | return Short.toUnsignedInt(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getShort()); 556 | } 557 | 558 | private int getInt() throws IOException 559 | { 560 | byte[] bytes = new byte[4]; 561 | f.read(bytes); 562 | return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt(); 563 | } 564 | 565 | private long getLong() throws IOException 566 | { 567 | byte[] bytes = new byte[8]; 568 | f.read(bytes); 569 | return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getLong(); 570 | } 571 | 572 | private String getString(int length) throws IOException 573 | { 574 | byte[] bytes = new byte[length]; 575 | f.read(bytes); 576 | try 577 | { 578 | return new String(bytes, "UTF-8").trim(); 579 | } catch (UnsupportedEncodingException e) 580 | { 581 | e.printStackTrace(); 582 | } 583 | return "UNKNOWN"; 584 | } 585 | 586 | } -------------------------------------------------------------------------------- /src/statistics/Statistics.java: -------------------------------------------------------------------------------- 1 | package statistics; 2 | 3 | import java.awt.Font; 4 | import java.awt.Point; 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.ListIterator; 11 | import java.util.Map; 12 | 13 | import org.knowm.xchart.BitmapEncoder; 14 | import org.knowm.xchart.BitmapEncoder.BitmapFormat; 15 | import org.knowm.xchart.XYChart; 16 | import org.knowm.xchart.XYChartBuilder; 17 | import org.knowm.xchart.style.Styler.LegendPosition; 18 | 19 | import boon.AbstractBoon; 20 | import boon.BoonFactory; 21 | import data.AgentItem; 22 | import data.BossData; 23 | import data.CombatData; 24 | import data.SkillData; 25 | import enums.Boon; 26 | import enums.CustomSkill; 27 | import enums.Result; 28 | import enums.StateChange; 29 | import player.BoonLog; 30 | import player.DamageLog; 31 | import player.Player; 32 | import utility.TableBuilder; 33 | import utility.Utility; 34 | 35 | public class Statistics 36 | { 37 | // Fields 38 | public static boolean hiding_players; 39 | BossData b_data; 40 | SkillData s_data; 41 | CombatData c_data; 42 | List p_list; 43 | 44 | // Constructor 45 | public Statistics(Parse parsed) 46 | { 47 | // Data 48 | this.b_data = parsed.getBossData(); 49 | this.s_data = parsed.getSkillData(); 50 | this.c_data = parsed.getCombatData(); 51 | 52 | // Players 53 | p_list = new ArrayList(); 54 | List playerAgentList = parsed.getAgentData().getPlayerAgentList(); 55 | for (AgentItem playerAgent : playerAgentList) 56 | { 57 | this.p_list.add(new Player(playerAgent)); 58 | } 59 | 60 | // Sort 61 | p_list.sort((a, b) -> Integer.parseInt(a.getGroup()) - Integer.parseInt(b.getGroup())); 62 | } 63 | 64 | // Final DPS 65 | public String getFinalDPS() 66 | { 67 | double total_dps = 0.0; 68 | double total_damage = 0.0; 69 | double fight_duration = (b_data.getLastAware() - b_data.getFirstAware()) / 1000.0; 70 | 71 | // Table 72 | TableBuilder table = new TableBuilder(); 73 | table.addTitle("Final DPS - " + b_data.getName()); 74 | 75 | // Header 76 | table.addRow("Name", "Profession", "DPS", "Damage"); 77 | 78 | // Body 79 | for (Player p : p_list) 80 | { 81 | // Damage and DPS 82 | double damage = p.getDamageLogs(b_data, c_data.getCombatList()).stream().mapToDouble(DamageLog::getDamage) 83 | .sum(); 84 | double dps = 0.0; 85 | if (fight_duration > 0) 86 | { 87 | dps = damage / fight_duration; 88 | } 89 | 90 | // Totals 91 | total_dps += dps; 92 | total_damage += damage; 93 | 94 | // Add Row 95 | table.addRow(p.getCharacter(), p.getProf(), String.format("%.2f", dps), String.format("%.0f", damage)); 96 | } 97 | 98 | // Sort by DPS 99 | table.sortAsDouble(2); 100 | 101 | // Footer 102 | table.addSeparator(); 103 | table.addRow("GROUP TOTAL", "-", String.format("%.2f", total_dps), String.format("%.0f", total_damage)); 104 | table.addRow("TARGET HEALTH", "-", "-", String.format("%d", b_data.getHealth())); 105 | 106 | return table.toString(); 107 | } 108 | 109 | // Phase DPS 110 | public String getPhaseDPS() 111 | { 112 | double total_time = 0.0; 113 | List fight_intervals = getPhaseIntervals(); 114 | int n = fight_intervals.size(); 115 | String[] phase_names = b_data.getPhaseNames(); 116 | 117 | // Table 118 | TableBuilder table = new TableBuilder(); 119 | table.addTitle("Phase DPS - " + b_data.getName()); 120 | 121 | // Header 122 | String[] header = new String[n + 3]; 123 | header[0] = "Name"; 124 | header[1] = "Profession"; 125 | for (int i = 2; i < n + 2; i++) 126 | { 127 | header[i] = phase_names[i - 2]; 128 | } 129 | header[header.length - 1] = "Summary"; 130 | table.addRow(header); 131 | 132 | // Body 133 | for (Player p : p_list) 134 | { 135 | total_time = 0.0; 136 | double total_damage = 0.0; 137 | String[] phase_dps = new String[n + 1]; 138 | 139 | for (int i = 0; i < n; i++) 140 | { 141 | List damage_logs = p.getDamageLogs(b_data, c_data.getCombatList()); 142 | Point interval = fight_intervals.get(i); 143 | 144 | // Damage and DPS 145 | double phase_damage = 0.0; 146 | for (DamageLog log : damage_logs) 147 | { 148 | if ((log.getTime() >= interval.x) && (log.getTime() <= interval.y)) 149 | { 150 | phase_damage += log.getDamage(); 151 | } 152 | } 153 | total_time += (interval.getY() - interval.getX()); 154 | total_damage += phase_damage; 155 | double dps = (phase_damage / (interval.getY() - interval.getX())) * 1000.0; 156 | phase_dps[i] = String.format("%.2f", dps); 157 | 158 | } 159 | // Row 160 | phase_dps[n] = String.format("%.2f", (total_damage / total_time) * 1000.0); 161 | table.addRow(Utility.concatStringArray(new String[] { p.getCharacter(), p.getProf() }, phase_dps)); 162 | } 163 | 164 | // Sort 165 | table.sortAsDouble(n + 2); 166 | 167 | // Footer 168 | table.addSeparator(); 169 | String[] durations = new String[n + 3]; 170 | durations[0] = "PHASE DURATION"; 171 | durations[1] = "-"; 172 | for (int i = 2; i < n + 2; i++) 173 | { 174 | Point p = fight_intervals.get(i - 2); 175 | double time = (p.getY() - p.getX()) / 1000.0; 176 | durations[i] = String.format("%.3f", time); 177 | } 178 | durations[durations.length - 1] = String.format("%.3f", total_time / 1000.0); 179 | table.addRow(durations); 180 | 181 | String[] intervals = new String[n + 3]; 182 | intervals[0] = "PHASE INTERVAL"; 183 | intervals[1] = "-"; 184 | for (int i = 2; i < n + 2; i++) 185 | { 186 | Point p = fight_intervals.get(i - 2); 187 | intervals[i] = String.format("%.3f", p.getX() / 1000.0) + " - " + String.format("%.3f", p.getY() / 1000.0); 188 | } 189 | intervals[intervals.length - 1] = String.format("%.3f", fight_intervals.get(0).getX() / 1000.0) + " - " 190 | + String.format("%.3f", fight_intervals.get(fight_intervals.size() - 1).getY() / 1000.0); 191 | table.addRow(intervals); 192 | return table.toString(); 193 | } 194 | 195 | // Damage Distribution 196 | public String getDamageDistribution() 197 | { 198 | // Heading 199 | StringBuilder output = new StringBuilder(); 200 | String title = " Damage Distribution - " + b_data.getName() + ' '; 201 | output.append('\u250C' + Utility.fillWithChar(title.length(), '\u2500') + '\u2510' + System.lineSeparator()); 202 | output.append('\u2502' + title + '\u2502' + System.lineSeparator()); 203 | output.append('\u2514' + Utility.fillWithChar(title.length(), '\u2500') + '\u2518'); 204 | 205 | // Table 206 | TableBuilder table = new TableBuilder(); 207 | 208 | // Body 209 | for (Player p : p_list) 210 | { 211 | List damage_logs = p.getDamageLogs(b_data, c_data.getCombatList()); 212 | 213 | // Skill Damage Map 214 | Map skill_damage = new HashMap(); 215 | for (DamageLog log : damage_logs) 216 | { 217 | if (skill_damage.containsKey(log.getID())) 218 | { 219 | skill_damage.put(log.getID(), skill_damage.get(log.getID()) + log.getDamage()); 220 | } 221 | else 222 | { 223 | skill_damage.put(log.getID(), log.getDamage()); 224 | } 225 | } 226 | 227 | // Title and Header 228 | table.clear(); 229 | table.addTitle(p.getCharacter() + " - " + p.getProf()); 230 | table.addRow("Skill", "Damage", "%"); 231 | 232 | // Sort 233 | skill_damage = Utility.sortByValue(skill_damage); 234 | 235 | // Calculate distribution 236 | double total_damage = skill_damage.values().stream().reduce(0, Integer::sum); 237 | for (Map.Entry entry : skill_damage.entrySet()) 238 | { 239 | String skill_name = s_data.getName(entry.getKey()); 240 | double damage = entry.getValue(); 241 | table.addRow(skill_name, String.format("%.0f", damage), 242 | String.format("%.2f", (damage / total_damage * 100.0))); 243 | } 244 | 245 | // Add table 246 | output.append(System.lineSeparator()); 247 | output.append(table.toString()); 248 | } 249 | return output.toString(); 250 | } 251 | 252 | // Total Damage Graph 253 | public String getTotalDamageGraph(String base) 254 | { 255 | // Build 256 | XYChartBuilder chartBuilder = new XYChartBuilder().width(1600).height(900); 257 | chartBuilder.title("Total Damage - " + b_data.getName()); 258 | chartBuilder.xAxisTitle("Time (seconds)").yAxisTitle("Damage (K)").build(); 259 | XYChart chart = chartBuilder.build(); 260 | 261 | // Style 262 | chart.getStyler().setLegendPosition(LegendPosition.InsideNW); 263 | chart.getStyler().setMarkerSize(1); 264 | chart.getStyler().setXAxisMin(0.0); 265 | chart.getStyler().setYAxisMin(0.0); 266 | chart.getStyler().setLegendFont(new Font("Dialog", Font.PLAIN, 16)); 267 | 268 | // Series 269 | for (Player p : p_list) 270 | { 271 | List damage_logs = p.getDamageLogs(b_data, c_data.getCombatList()); 272 | int n = damage_logs.size(); 273 | if (n > 0) 274 | { 275 | double[] x = new double[n]; 276 | double[] y = new double[n]; 277 | double total_damage = 0.0; 278 | for (int i = 0; i < n; i++) 279 | { 280 | total_damage += damage_logs.get(i).getDamage(); 281 | x[i] = damage_logs.get(i).getTime() / 1000.0; 282 | y[i] = total_damage / 1000.0; 283 | } 284 | chart.addSeries(p.getCharacter() + " - " + p.getProf(), x, y); 285 | } 286 | } 287 | 288 | // Write 289 | try 290 | { 291 | String file_name = "./graphs/" + base + "_" + b_data.getName() + "_TDG.png"; 292 | BitmapEncoder.saveBitmapWithDPI(chart, file_name, BitmapFormat.PNG, 300); 293 | } catch (IOException e) 294 | { 295 | e.printStackTrace(); 296 | } 297 | return base + "_" + b_data.getName() + "_TDG.png"; 298 | } 299 | 300 | // Combat Statistics 301 | public String getCombatStatistics() 302 | { 303 | // Table 304 | TableBuilder table = new TableBuilder(); 305 | table.addTitle("Combat Statistics - " + b_data.getName()); 306 | 307 | // Header 308 | table.addRow("Account", "Character", "Profession", "SUBG", "CRIT", "SCHL", "MOVE", "FLNK", "TGHN", "HEAL", 309 | "COND", "SWAP", "DOGE", "RESS", "DOWN", "DIED"); 310 | 311 | // Body 312 | for (Player p : p_list) 313 | { 314 | List damage_logs = p.getDamageLogs(b_data, c_data.getCombatList()); 315 | int instid = p.getInstid(); 316 | 317 | // Rates 318 | double power_loop_count = 0.0; 319 | double critical_rate = 0.0; 320 | double scholar_rate = 0.0; 321 | double moving_rate = 0.0; 322 | double flanking_rate = 0.0; 323 | for (DamageLog log : damage_logs) 324 | { 325 | if (log.isCondi() == 0) 326 | { 327 | critical_rate += (log.getResult().equals(Result.CRIT)) ? 1 : 0; 328 | scholar_rate += log.isNinety(); 329 | moving_rate += log.isMoving(); 330 | flanking_rate += log.isFlanking(); 331 | power_loop_count++; 332 | } 333 | } 334 | power_loop_count = (power_loop_count == 0) ? 1 : power_loop_count; 335 | 336 | // Counts 337 | int swap = c_data.getStates(instid, StateChange.WEAPON_SWAP).size(); 338 | int down = c_data.getStates(instid, StateChange.CHANGE_DOWN).size(); 339 | int dodge = c_data.getSkillCount(instid, CustomSkill.DODGE.getID()); 340 | int ress = c_data.getSkillCount(instid, CustomSkill.RESURRECT.getID()); 341 | 342 | // R.I.P 343 | List dead = c_data.getStates(instid, StateChange.CHANGE_DEAD); 344 | double died = 0.0; 345 | if (!dead.isEmpty()) 346 | { 347 | died = dead.get(0).getX() - b_data.getFirstAware(); 348 | } 349 | 350 | // Add row 351 | table.addRow(new String[] { p.getAccount(), p.getCharacter(), p.getProf(), p.getGroup(), 352 | String.format("%.2f", critical_rate / power_loop_count), 353 | String.format("%.2f", scholar_rate / power_loop_count), 354 | String.format("%.2f", moving_rate / power_loop_count), 355 | String.format("%.2f", flanking_rate / power_loop_count), String.valueOf(p.getToughness()), 356 | String.valueOf(p.getHealing()), String.valueOf(p.getCondition()), String.valueOf(swap), 357 | String.valueOf(dodge), String.valueOf(ress), String.valueOf(down), 358 | String.format("%.2f", died / 1000.0) }); 359 | } 360 | return table.toString(); 361 | } 362 | 363 | // Final Boons 364 | public String getFinalBoons() 365 | { 366 | // Table 367 | TableBuilder table = new TableBuilder(); 368 | table.addTitle("Final Boon Rates - " + b_data.getName()); 369 | 370 | // Header 371 | String[] boon_array = Boon.getArray(); 372 | table.addRow(Utility.concatStringArray(new String[] { "Name", "Profession" }, boon_array)); 373 | 374 | // Body 375 | List boon_list = Boon.getList(); 376 | int n = boon_list.size(); 377 | BoonFactory boonFactory = new BoonFactory(); 378 | 379 | for (Player p : p_list) 380 | { 381 | Map> boon_logs = p.getBoonMap(b_data, s_data, c_data.getCombatList()); 382 | String[] rates = new String[n]; 383 | for (int i = 0; i < n; i++) 384 | { 385 | Boon boon = Boon.getEnum(boon_list.get(i)); 386 | AbstractBoon boon_object = boonFactory.makeBoon(boon); 387 | List logs = boon_logs.get(boon.getName()); 388 | String rate = "0.00"; 389 | if (!logs.isEmpty()) 390 | { 391 | if (boon.getType().equals("duration")) 392 | { 393 | rate = getBoonDuration(getBoonIntervalsList(boon_object, logs)); 394 | } 395 | else if (boon.getType().equals("intensity")) 396 | { 397 | rate = getAverageStacks(getBoonStacksList(boon_object, logs)); 398 | } 399 | } 400 | rates[i] = rate; 401 | } 402 | table.addRow(Utility.concatStringArray(new String[] { p.getCharacter(), p.getProf() }, rates)); 403 | } 404 | return table.toString(); 405 | } 406 | 407 | // Phase Boons 408 | public String getPhaseBoons() 409 | { 410 | BoonFactory boonFactory = new BoonFactory(); 411 | List all_rates = new ArrayList(); 412 | List boon_list = Boon.getList(); 413 | List fight_intervals = getPhaseIntervals(); 414 | int n = fight_intervals.size(); 415 | int m = boon_list.size(); 416 | 417 | for (Player p : p_list) 418 | { 419 | Map> boon_logs = p.getBoonMap(b_data, s_data, c_data.getCombatList()); 420 | String[][] rates = new String[m][]; 421 | for (int j = 0; j < m; j++) 422 | { 423 | Boon boon = Boon.getEnum(boon_list.get(j)); 424 | String[] rate = new String[fight_intervals.size()]; 425 | Arrays.fill(rate, "0.00"); 426 | List logs = boon_logs.get(boon.getName()); 427 | if (!logs.isEmpty()) 428 | { 429 | AbstractBoon boon_object = boonFactory.makeBoon(boon); 430 | if (boon.getType().equals("duration")) 431 | { 432 | List boon_intervals = getBoonIntervalsList(boon_object, logs); 433 | rate = getBoonDuration(boon_intervals, fight_intervals); 434 | } 435 | else if (boon.getType().equals("intensity")) 436 | { 437 | List boon_stacks = getBoonStacksList(boon_object, logs); 438 | rate = getAverageStacks(boon_stacks, fight_intervals); 439 | } 440 | } 441 | rates[j] = rate; 442 | } 443 | all_rates.add(rates); 444 | } 445 | 446 | // Heading 447 | StringBuilder output = new StringBuilder(); 448 | String title = " Phase Boon Rates - " + b_data.getName() + ' '; 449 | output.append('\u250C' + Utility.fillWithChar(title.length(), '\u2500') + '\u2510' + System.lineSeparator()); 450 | output.append('\u2502' + title + '\u2502' + System.lineSeparator()); 451 | output.append('\u2514' + Utility.fillWithChar(title.length(), '\u2500') + '\u2518'); 452 | 453 | // Table 454 | TableBuilder table = new TableBuilder(); 455 | 456 | // Body 457 | String[] boon_array = Boon.getArray(); 458 | String[] phase_names = b_data.getPhaseNames(); 459 | for (int i = 0; i < n; i++) 460 | { 461 | table.clear(); 462 | table.addTitle(phase_names[i]); 463 | table.addRow(Utility.concatStringArray(new String[] { "Name", "Profession" }, boon_array)); 464 | int l = p_list.size(); 465 | for (int j = 0; j < l; j++) 466 | { 467 | Player p = p_list.get(j); 468 | String[][] player_rates = all_rates.get(j); 469 | String[] row_rates = new String[m]; 470 | for (int k = 0; k < m; k++) 471 | { 472 | row_rates[k] = player_rates[k][i]; 473 | } 474 | table.addRow(Utility.concatStringArray(new String[] { p.getCharacter(), p.getProf() }, row_rates)); 475 | } 476 | output.append(System.lineSeparator() + table.toString()); 477 | } 478 | return output.toString(); 479 | } 480 | 481 | // Private Methods 482 | private List getPhaseIntervals() 483 | { 484 | List fight_intervals = new ArrayList(); 485 | int log_start = c_data.getCombatList().get(0).getTime(); 486 | int time_start = b_data.getFirstAware() - log_start; 487 | int time_end = b_data.getLastAware() - log_start; 488 | 489 | // Thresholds 490 | int time_threshold = 0; 491 | int[] health_thresholds = null; 492 | if (b_data.getName().equals("Vale Guardian")) 493 | { 494 | time_threshold = 20000; 495 | health_thresholds = new int[] { 6600, 3300 }; 496 | } 497 | else if (b_data.getName().equals("Gorseval the Multifarious")) 498 | { 499 | time_threshold = 20000; 500 | health_thresholds = new int[] { 6600, 3300 }; 501 | } 502 | else if (b_data.getName().equals("Sabetha the Saboteur")) 503 | { 504 | time_threshold = 20000; 505 | health_thresholds = new int[] { 7500, 5000, 2500 }; 506 | } 507 | else if (b_data.getName().equals("Slothasor")) 508 | { 509 | health_thresholds = new int[] { 8000, 6000, 4000, 2000, 1000 }; 510 | } 511 | else if (b_data.getName().equals("Matthias Gabrel")) 512 | { 513 | health_thresholds = new int[] { 8000, 6000, 4000 }; 514 | } 515 | else if (b_data.getName().equals("Keep Construct")) 516 | { 517 | time_threshold = 20000; 518 | health_thresholds = new int[] { 6600, 3300 }; 519 | } 520 | else if (b_data.getName().equals("Xera")) 521 | { 522 | time_threshold = 20000; 523 | health_thresholds = new int[] { 5000 }; 524 | } 525 | else if (b_data.getName().equals("Cairn the Indomitable")) 526 | { 527 | health_thresholds = new int[] { 7500, 5000, 2500 }; 528 | } 529 | else if (b_data.getName().equals("Mursaat Overseer")) 530 | { 531 | health_thresholds = new int[] { 7500, 5000, 2500 }; 532 | } 533 | else if (b_data.getName().equals("Samarog")) 534 | { 535 | time_threshold = 20000; 536 | health_thresholds = new int[] { 6600, 3300 }; 537 | } 538 | else if (b_data.getName().equals("Deimos")) 539 | { 540 | health_thresholds = new int[] { 7500, 5000, 2500 }; 541 | } 542 | else 543 | { 544 | fight_intervals.add(new Point(time_start, time_end)); 545 | return fight_intervals; 546 | } 547 | 548 | // Generate intervals with health updates 549 | ListIterator iter = c_data.getStates(b_data.getInstid(), StateChange.HEALTH_UPDATE).listIterator(); 550 | Point previous_update = null; 551 | if (iter.hasNext()) 552 | { 553 | previous_update = iter.next(); 554 | } 555 | if (previous_update != null) 556 | { 557 | main: for (int threshold : health_thresholds) 558 | { 559 | while (iter.hasNext()) 560 | { 561 | Point current_update = iter.next(); 562 | if ((current_update.y <= threshold) && (time_threshold == 0)) 563 | { 564 | fight_intervals.add(new Point(time_start, previous_update.x - log_start)); 565 | time_start = previous_update.x - log_start; 566 | previous_update = current_update; 567 | continue main; 568 | } 569 | else if ((current_update.y <= threshold) 570 | && ((current_update.x - previous_update.x) > time_threshold)) 571 | { 572 | fight_intervals.add(new Point(time_start, previous_update.x - log_start)); 573 | time_start = current_update.x - log_start; 574 | previous_update = current_update; 575 | continue main; 576 | 577 | } 578 | previous_update = current_update; 579 | } 580 | } 581 | } 582 | fight_intervals.add(new Point(time_start, time_end)); 583 | return fight_intervals; 584 | } 585 | 586 | private List getBoonIntervalsList(AbstractBoon boon, List boon_logs) 587 | { 588 | // Initialise variables 589 | int t_prev = 0; 590 | int t_curr = 0; 591 | List boon_intervals = new ArrayList(); 592 | 593 | // Loop: update then add durations 594 | for (BoonLog log : boon_logs) 595 | { 596 | t_curr = log.getTime(); 597 | boon.update(t_curr - t_prev); 598 | boon.add(log.getValue()); 599 | boon_intervals.add(new Point(t_curr, t_curr + boon.getStackValue())); 600 | t_prev = t_curr; 601 | } 602 | 603 | // Merge intervals 604 | boon_intervals = Utility.mergeIntervals(boon_intervals); 605 | 606 | // Trim duration overflow 607 | int fight_duration = b_data.getLastAware() - b_data.getFirstAware(); 608 | int last = boon_intervals.size() - 1; 609 | if (boon_intervals.get(last).getY() > fight_duration) 610 | { 611 | boon_intervals.get(last).y = fight_duration; 612 | } 613 | 614 | return boon_intervals; 615 | } 616 | 617 | private String getBoonDuration(List boon_intervals) 618 | { 619 | // Calculate average duration 620 | double average_duration = 0.0; 621 | for (Point p : boon_intervals) 622 | { 623 | average_duration = average_duration + (p.getY() - p.getX()); 624 | } 625 | return String.format("%.2f", (average_duration / (b_data.getLastAware() - b_data.getFirstAware()))); 626 | } 627 | 628 | private String[] getBoonDuration(List boon_intervals, List fight_intervals) 629 | { 630 | // Phase durations 631 | String[] phase_durations = new String[fight_intervals.size()]; 632 | 633 | // Loop: add intervals in between, merge, calculate duration 634 | for (int i = 0; i < fight_intervals.size(); i++) 635 | { 636 | Point p = fight_intervals.get(i); 637 | List boons_intervals_during_phase = new ArrayList(); 638 | for (Point b : boon_intervals) 639 | { 640 | if (b.x < p.y && p.x < b.y) 641 | { 642 | if (p.x <= b.x && b.y <= p.y) 643 | { 644 | boons_intervals_during_phase.add(b); 645 | } 646 | else if (b.x < p.x && p.y < b.y) 647 | { 648 | boons_intervals_during_phase.add(p); 649 | } 650 | else if (b.x < p.x && b.y <= p.y) 651 | { 652 | boons_intervals_during_phase.add(new Point(p.x, b.y)); 653 | } 654 | else if (p.x <= b.x && p.y < b.y) 655 | { 656 | boons_intervals_during_phase.add(new Point(b.x, p.y)); 657 | } 658 | } 659 | } 660 | double duration = 0.0; 661 | for (Point b : boons_intervals_during_phase) 662 | { 663 | duration = duration + (b.getY() - b.getX()); 664 | } 665 | phase_durations[i] = String.format("%.2f", (duration / (p.getY() - p.getX()))); 666 | } 667 | return phase_durations; 668 | } 669 | 670 | private List getBoonStacksList(AbstractBoon boon, List boon_logs) 671 | { 672 | // Initialise variables 673 | int t_prev = 0; 674 | int t_curr = 0; 675 | List boon_stacks = new ArrayList(); 676 | boon_stacks.add(0); 677 | 678 | // Loop: fill, update, and add to stacks 679 | for (BoonLog log : boon_logs) 680 | { 681 | t_curr = log.getTime(); 682 | boon.addStacksBetween(boon_stacks, t_curr - t_prev); 683 | boon.update(t_curr - t_prev); 684 | boon.add(log.getValue()); 685 | if (t_curr != t_prev) 686 | { 687 | boon_stacks.add(boon.getStackValue()); 688 | } 689 | else 690 | { 691 | boon_stacks.set(boon_stacks.size() - 1, boon.getStackValue()); 692 | } 693 | t_prev = t_curr; 694 | } 695 | 696 | // Fill in remaining stacks 697 | boon.addStacksBetween(boon_stacks, b_data.getLastAware() - b_data.getFirstAware() - t_prev); 698 | boon.update(1); 699 | boon_stacks.add(boon.getStackValue()); 700 | return boon_stacks; 701 | } 702 | 703 | private String getAverageStacks(List boon_stacks) 704 | { 705 | // Calculate average stacks 706 | double average_stacks = boon_stacks.stream().mapToInt(Integer::intValue).sum(); 707 | double average = average_stacks / boon_stacks.size(); 708 | if (average > 10.0) 709 | { 710 | return String.format("%.1f", average); 711 | } 712 | else 713 | { 714 | return String.format("%.2f", average); 715 | } 716 | } 717 | 718 | private String[] getAverageStacks(List boon_stacks, List fight_intervals) 719 | { 720 | // Phase stacks 721 | String[] phase_stacks = new String[fight_intervals.size()]; 722 | 723 | // Loop: get sublist and calculate average stacks 724 | for (int i = 0; i < fight_intervals.size(); i++) 725 | { 726 | Point p = fight_intervals.get(i); 727 | List phase_boon_stacks = new ArrayList(boon_stacks.subList(p.x, p.y)); 728 | double average_stacks = phase_boon_stacks.stream().mapToInt(Integer::intValue).sum(); 729 | double average = average_stacks / phase_boon_stacks.size(); 730 | if (average > 10.0) 731 | { 732 | phase_stacks[i] = String.format("%.1f", average); 733 | } 734 | else 735 | { 736 | phase_stacks[i] = String.format("%.2f", average); 737 | } 738 | } 739 | return phase_stacks; 740 | } 741 | } 742 | -------------------------------------------------------------------------------- /src/utility/TableBuilder.java: -------------------------------------------------------------------------------- 1 | package utility; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.Comparator; 7 | import java.util.List; 8 | import java.util.ListIterator; 9 | 10 | public class TableBuilder 11 | { 12 | 13 | // Fields 14 | private String title = ""; 15 | private List rows = new ArrayList(); 16 | private String nl = System.lineSeparator(); 17 | 18 | // Public Methods 19 | public void addTitle(String title) 20 | { 21 | this.title = ' ' + title + ' '; 22 | } 23 | 24 | public void addRow(String... cols) 25 | { 26 | rows.add(cols); 27 | String[] row = rows.get(rows.size() - 1); 28 | for (int i = 0; i < row.length; i++) 29 | { 30 | row[i] = ' ' + row[i] + ' '; 31 | } 32 | } 33 | 34 | public void addSeparator() 35 | { 36 | if (!rows.isEmpty()) 37 | { 38 | String[] separator = new String[rows.get(0).length]; 39 | Arrays.fill(separator, ""); 40 | rows.add(separator); 41 | } 42 | } 43 | 44 | public void sortAsDouble(int col) 45 | { 46 | // Get body 47 | int separator = 0; 48 | for (String[] row : rows) 49 | { 50 | if (row[0].equals("")) 51 | { 52 | break; 53 | } 54 | separator++; 55 | } 56 | List body = rows.subList(1, separator); 57 | 58 | // Sort by column 59 | Collections.sort(body, new Comparator() 60 | { 61 | @Override 62 | public int compare(String[] x, String[] y) 63 | { 64 | double x_dbl = Double.parseDouble(x[col].replaceAll("\\s+", "")); 65 | double y_dbl = Double.parseDouble(y[col].replaceAll("\\s+", "")); 66 | if (x_dbl > y_dbl) 67 | { 68 | return -1; 69 | } 70 | else if (x_dbl == y_dbl) 71 | { 72 | return 0; 73 | } 74 | else 75 | { 76 | return 1; 77 | } 78 | } 79 | }); 80 | } 81 | 82 | public void clear() 83 | { 84 | title = ""; 85 | rows = new ArrayList(); 86 | } 87 | 88 | @Override 89 | public String toString() 90 | { 91 | // Initialize 92 | removeEmptyColumns(); 93 | StringBuilder output = new StringBuilder(); 94 | int[] colWidths = getWidths(); 95 | int numCols = rows.get(0).length; 96 | 97 | // Title 98 | if (!title.equals("")) 99 | { 100 | output.append('\u250C' + Utility.fillWithChar(title.length(), '\u2500') + '\u2510' + nl); 101 | output.append('\u2502' + title + '\u2502' + nl); 102 | output.append('\u2514' + Utility.fillWithChar(title.length(), '\u2500') + '\u2518' + nl); 103 | } 104 | 105 | // Empty 106 | if (rows.size() <= 1) 107 | { 108 | return output.toString(); 109 | } 110 | 111 | // Header 112 | output.append('\u250C'); 113 | for (int colNum = 0; colNum < rows.get(0).length; colNum++) 114 | { 115 | output.append(Utility.fillWithChar(colWidths[colNum], '\u2500')); 116 | if (colNum != numCols - 1) 117 | { 118 | output.append('\u252C'); 119 | } 120 | else 121 | { 122 | output.append('\u2510' + nl + '\u2502'); 123 | } 124 | } 125 | for (int colNum = 0; colNum < numCols; colNum++) 126 | { 127 | output.append(Utility.centerString(rows.get(0)[colNum], colWidths[colNum]) + '\u2502'); 128 | } 129 | output.append(nl + '\u251C'); 130 | for (int colNum = 0; colNum < rows.get(0).length; colNum++) 131 | { 132 | output.append(Utility.fillWithChar(colWidths[colNum], '\u2550')); 133 | if (colNum != numCols - 1) 134 | { 135 | output.append('\u253C'); 136 | } 137 | else 138 | { 139 | output.append('\u2524' + nl); 140 | } 141 | } 142 | 143 | // Body 144 | boolean footer = false; 145 | for (ListIterator iter = rows.listIterator(1); iter.hasNext();) 146 | { 147 | String[] row = iter.next(); 148 | String text; 149 | 150 | for (int colNum = 0; colNum < row.length; colNum++) 151 | { 152 | if (row[0].equals("")) 153 | { 154 | 155 | footer = true; 156 | if (colNum == 0) 157 | { 158 | output.append('\u251C'); 159 | } 160 | else 161 | { 162 | output.append('\u253C'); 163 | } 164 | output.append(Utility.fillWithChar(colWidths[colNum], '\u2500')); 165 | } 166 | else 167 | { 168 | text = row[colNum]; 169 | if (!footer) 170 | { 171 | if (Utility.isNumeric(text)) 172 | { 173 | output.append('\u2502' + Utility.rightAlignString(text, colWidths[colNum])); 174 | } 175 | else 176 | { 177 | output.append('\u2502' + Utility.leftAlignString(text, colWidths[colNum])); 178 | } 179 | } 180 | else 181 | { 182 | output.append('\u2502' + Utility.centerString(text, colWidths[colNum])); 183 | } 184 | } 185 | } 186 | if (row[0].equals("")) 187 | { 188 | output.append('\u2524' + nl); 189 | } 190 | else 191 | { 192 | output.append('\u2502' + nl); 193 | } 194 | } 195 | output.append('\u2514'); 196 | for (int colNum = 0; colNum < rows.get(0).length; colNum++) 197 | { 198 | output.append(Utility.fillWithChar(colWidths[colNum], '\u2500')); 199 | if (colNum != numCols - 1) 200 | { 201 | output.append('\u2534'); 202 | } 203 | else 204 | { 205 | output.append('\u2518'); 206 | } 207 | } 208 | 209 | return output.toString(); 210 | } 211 | 212 | // Private Methods 213 | private int[] getWidths() 214 | { 215 | int cols = 0; 216 | for (String[] row : rows) 217 | cols = Math.max(cols, row.length); 218 | int[] widths = new int[cols]; 219 | for (String[] row : rows) 220 | { 221 | for (int i = 0; i < row.length; i++) 222 | { 223 | widths[i] = Math.max(widths[i], row[i].length()); 224 | } 225 | } 226 | return widths; 227 | } 228 | 229 | private void removeEmptyColumns() 230 | { 231 | 232 | // Columns that contain all "0.00" will have existance[i] = false 233 | int cols = rows.get(0).length; 234 | boolean[] existance = new boolean[cols]; 235 | for (ListIterator iter = rows.listIterator(1); iter.hasNext();) 236 | { 237 | String[] row = iter.next(); 238 | for (int i = 0; i < cols; i++) 239 | { 240 | if (!existance[i] && !row[i].equals(" 0.00 ")) 241 | { 242 | existance[i] = true; 243 | } 244 | } 245 | } 246 | 247 | // Check if there are any false values 248 | int exist_count = 0; 249 | for (boolean exists : existance) 250 | { 251 | if (exists) 252 | { 253 | exist_count++; 254 | } 255 | } 256 | if (exist_count == cols) 257 | { 258 | return; 259 | } 260 | 261 | // Create new table with no empty columns 262 | List new_rows = new ArrayList(); 263 | for (String[] row : rows) 264 | { 265 | int i = 0; 266 | int j = 0; 267 | String[] new_row = new String[exist_count]; 268 | for (boolean exists : existance) 269 | { 270 | if (exists) 271 | { 272 | new_row[i] = row[j]; 273 | i++; 274 | } 275 | j++; 276 | } 277 | new_rows.add(new_row); 278 | } 279 | rows = new_rows; 280 | 281 | } 282 | 283 | } -------------------------------------------------------------------------------- /src/utility/Utility.java: -------------------------------------------------------------------------------- 1 | package utility; 2 | 3 | import java.awt.Point; 4 | import java.io.BufferedReader; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.PrintWriter; 8 | import java.io.StringReader; 9 | import java.nio.file.Path; 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.LinkedHashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.stream.Collectors; 17 | 18 | public final class Utility 19 | { 20 | 21 | public static boolean toBool(int i) 22 | { 23 | return (i != 0); 24 | } 25 | 26 | public static String boxText(String text) 27 | { 28 | StringBuilder boxedText = new StringBuilder(); 29 | boxedText.append( 30 | "\u250C" + Utility.fillWithChar(text.length() + 2, '\u2500') + "\u2510" + System.lineSeparator()); 31 | boxedText.append("\u2502 " + text + " \u2502 " + System.lineSeparator()); 32 | boxedText.append("\u2514" + Utility.fillWithChar(text.length() + 2, '\u2500') + "\u2518"); 33 | return boxedText.toString(); 34 | } 35 | 36 | public static void writeToFile(String s, File f) throws IOException 37 | { 38 | try (BufferedReader getter = new BufferedReader(new StringReader(s)); 39 | PrintWriter writer = new PrintWriter(f, "UTF-8");) 40 | { 41 | getter.lines().forEach(line -> writer.println(line)); 42 | writer.close(); 43 | } 44 | } 45 | 46 | public static void recursiveFileSearch(File dir, List files) 47 | { 48 | File[] file_array = dir.listFiles(); 49 | for (File f : file_array) 50 | { 51 | if (f.isFile() && f.toString().endsWith(".evtc") || f.toString().endsWith(".zip")) 52 | { 53 | files.add(f.toPath()); 54 | } 55 | else if (f.isDirectory()) 56 | { 57 | recursiveFileSearch(f, files); 58 | } 59 | } 60 | } 61 | 62 | public static > Map sortByValue(Map map) 63 | { 64 | return map.entrySet().stream().sorted(Map.Entry.comparingByValue(Collections.reverseOrder())) 65 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); 66 | } 67 | 68 | public static String centerString(String text, int len) 69 | { 70 | String output = String.format("%" + len + "s%s%" + len + "s", "", text, ""); 71 | float start = (output.length() / 2) - (len / 2); 72 | return output.substring((int) start, (int) (start + len)); 73 | } 74 | 75 | public static boolean isNumeric(String str) 76 | { 77 | try 78 | { 79 | Double.parseDouble(str); 80 | } catch (NumberFormatException nfe) 81 | { 82 | return false; 83 | } 84 | return true; 85 | } 86 | 87 | public static String rightAlignString(String text, int len) 88 | { 89 | while (text.length() < len) 90 | { 91 | text = " " + text; 92 | 93 | } 94 | return text; 95 | } 96 | 97 | public static String leftAlignString(String text, int len) 98 | { 99 | while (text.length() < len) 100 | { 101 | text += " "; 102 | 103 | } 104 | return text; 105 | } 106 | 107 | public static String fillWithChar(int len, char c) 108 | { 109 | if (len > 0) 110 | { 111 | char[] array = new char[len]; 112 | Arrays.fill(array, c); 113 | return new String(array); 114 | } 115 | return ""; 116 | } 117 | 118 | public static List mergeIntervals(List intervals) 119 | { 120 | 121 | if (intervals.size() == 1) 122 | { 123 | return intervals; 124 | } 125 | 126 | List merged = new ArrayList(); 127 | int x = intervals.get(0).x; 128 | int y = intervals.get(0).y; 129 | 130 | for (int i = 1; i < intervals.size(); i++) 131 | { 132 | Point current = intervals.get(i); 133 | if (current.x <= y) 134 | { 135 | y = Math.max(current.y, y); 136 | } 137 | else 138 | { 139 | merged.add(new Point(x, y)); 140 | x = current.x; 141 | y = current.y; 142 | } 143 | } 144 | 145 | merged.add(new Point(x, y)); 146 | 147 | return merged; 148 | } 149 | 150 | public static String[] concatStringArray(String[] a, String[] b) 151 | { 152 | int i = a.length; 153 | int j = b.length; 154 | String[] output = new String[i + j]; 155 | System.arraycopy(a, 0, output, 0, i); 156 | System.arraycopy(b, 0, output, i, j); 157 | return output; 158 | } 159 | 160 | } --------------------------------------------------------------------------------