package org.mctourney.autoreferee.util; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.text.DateFormat; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import com.google.gson.Gson; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.bukkit.ChatColor; import org.bukkit.DyeColor; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.material.Colorable; import org.mctourney.autoreferee.AutoRefMatch; import org.mctourney.autoreferee.AutoRefPlayer; import org.mctourney.autoreferee.AutoRefTeam; import org.mctourney.autoreferee.AutoReferee; import org.mctourney.autoreferee.AutoRefMatch.TranscriptEvent; import org.mctourney.autoreferee.goals.AutoRefGoal; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.mctourney.autoreferee.listeners.GoalsInventorySnapshot; /** * Formats HTML match reports. * * @author authorblues */ public class ReportGenerator { public ReportGenerator() { } private Map<String, String> customDetails = Maps.newLinkedHashMap(); /** * Adds (or modifies) a custom detail for this match report generator. Custom details will * appear in the order that they are defined. * * @param key text for left column (descriptor) * @param value text for right column (content) */ public void setCustomDetail(String key, String value) { customDetails.put(key, value); } /** * Generates a match report web page from a match object. * * @param match match object * @return HTML for a match report web page, or null if an error occurred */ public String generate(AutoRefMatch match) { String htm, css, js, images = ""; try { htm = getResourceString("webstats/report.htm"); css = getResourceString("webstats/report.css"); js = getResourceString("webstats/report.js" ); images += getResourceString("webstats/image-block.css"); images += getResourceString("webstats/image-header.css"); // images += getResourceString("webstats/image-items.css"); } catch (IOException e) { return null; } StringWriter transcript = new StringWriter(); TranscriptEvent endEvent = null; for (TranscriptEvent e : match.getTranscript()) { transcript.write(transcriptEventHTML(match, e)); if (e.getType() != TranscriptEvent.EventType.MATCH_END) endEvent = e; } AutoRefTeam win = match.getWinningTeam(); String winningTeam = (win == null) ? "??" : String.format("<span class='team team-%s'>%s</span>", getTag(win), ChatColor.stripColor(win.getDisplayName())); Set<String> refList = Sets.newHashSet(); for (Player pl : match.getReferees()) refList.add(String.format("<span class='referee'>%s</span>", pl.getName())); Set<String> streamerList = Sets.newHashSet(); for (Player pl : match.getStreamers()) streamerList.add(String.format("<span class='streamer'>%s</span>", pl.getName())); List<String> extraRows = Lists.newLinkedList(); for (Map.Entry<String, String> e : this.customDetails.entrySet()) extraRows.add(String.format("<tr><th>%s</th><td>%s</td></tr>", e.getKey(), e.getValue())); File mapImage = new File(match.getWorld().getWorldFolder(), "map.png"); if (!mapImage.exists()) match.saveMapImage(); Location ptMin = match.getMapCuboid().getMinimumPoint(); return (htm // base files get replaced first .replaceAll("#base-css#", css.replaceAll("\\s+", " ") + images) .replaceAll("#base-js#", Matcher.quoteReplacement(js)) // followed by the team, player, and block styles .replaceAll("#team-css#", getTeamStyles(match).replaceAll("\\s+", " ")) .replaceAll("#plyr-css#", getPlayerStyles(match).replaceAll("\\s+", " ")) .replaceAll("#blok-css#", getBlockStyles(match).replaceAll("\\s+", " ")) .replaceAll("#map-data#", String.format("{image:'%s', x:%d, z:%d}", MapImageGenerator.imageToDataURI(mapImage, "image/png"), ptMin.getBlockX(), ptMin.getBlockZ())) // then match and map names .replaceAll("#title#", match.getMatchName()) .replaceAll("#map#", match.getMapName()) // date and length of match .replaceAll("#date#", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.FULL).format(new Date())) .replaceAll("#length#", endEvent == null ? "??" : endEvent.getTimestamp()) // team information (all teams, and winning team) .replaceAll("#teams#", getTeamList(match)) .replaceAll("#winners#", winningTeam) // staff information .replaceAll("#referees#", StringUtils.join(refList, ", ")) .replaceAll("#streamers#", StringUtils.join(streamerList, ", ")) // filter settings .replaceAll("#filter-options", getFilterOptions()) // addition details (custom?) .replaceAll("#xtra-details#", StringUtils.join(extraRows, "\n")) // and last, throw in the transcript and stats .replaceAll("#transcript#", transcript.toString()) .replaceAll("#plyr-stats#", getPlayerStats(match)) ); } private static String getFilterOptions() { List<String> options = Lists.newLinkedList(); for (AutoRefMatch.TranscriptEvent.EventType etype : AutoRefMatch.TranscriptEvent.EventType.values()) if (etype.hasFilter()) options.add("<option value='" + etype.getEventClass() + "'>" + etype.getEventName() + "</option>"); return StringUtils.join(options, ""); } // helper method private static String getResourceString(String path) throws IOException { StringWriter buffer = new StringWriter(); IOUtils.copy(AutoReferee.getInstance().getResource(path), buffer); return buffer.toString(); } // generate player.css private static String getPlayerStyles(AutoRefMatch match) { StringWriter css = new StringWriter(); for (AutoRefPlayer apl : match.getPlayers()) css.write(String.format(".player-%s:before { background-image: url(http://minotar.net/avatar/%s/16.png); }\n", getTag(apl), apl.getName())); return css.toString(); } // generate team.css private static String getTeamStyles(AutoRefMatch match) { StringWriter css = new StringWriter(); for (AutoRefTeam team : match.getTeams()) css.write(String.format(".team-%s { color: %s; }\n", getTag(team), ColorConverter.chatToHex(team.getColor()))); return css.toString(); } private static Map<BlockData, Integer> terrain_png = Maps.newHashMap(); private static int terrain_png_size = 16; static { terrain_png.put(new BlockData(Material.WOOL, DyeColor.WHITE.getWoolData()), 4 * 16 + 0); terrain_png.put(new BlockData(Material.WOOL, DyeColor.BLACK.getWoolData()), 7 * 16 + 1); terrain_png.put(new BlockData(Material.WOOL, DyeColor.GRAY.getWoolData()), 7 * 16 + 2); terrain_png.put(new BlockData(Material.WOOL, DyeColor.RED.getWoolData()), 8 * 16 + 1); terrain_png.put(new BlockData(Material.WOOL, DyeColor.PINK.getWoolData()), 8 * 16 + 2); terrain_png.put(new BlockData(Material.WOOL, DyeColor.GREEN.getWoolData()), 9 * 16 + 1); terrain_png.put(new BlockData(Material.WOOL, DyeColor.LIME.getWoolData()), 9 * 16 + 2); terrain_png.put(new BlockData(Material.WOOL, DyeColor.BROWN.getWoolData()), 10 * 16 + 1); terrain_png.put(new BlockData(Material.WOOL, DyeColor.YELLOW.getWoolData()), 10 * 16 + 2); terrain_png.put(new BlockData(Material.WOOL, DyeColor.BLUE.getWoolData()), 11 * 16 + 1); terrain_png.put(new BlockData(Material.WOOL, DyeColor.LIGHT_BLUE.getWoolData()), 11 * 16 + 2); terrain_png.put(new BlockData(Material.WOOL, DyeColor.PURPLE.getWoolData()), 12 * 16 + 1); terrain_png.put(new BlockData(Material.WOOL, DyeColor.MAGENTA.getWoolData()), 12 * 16 + 2); terrain_png.put(new BlockData(Material.WOOL, DyeColor.CYAN.getWoolData()), 13 * 16 + 1); terrain_png.put(new BlockData(Material.WOOL, DyeColor.ORANGE.getWoolData()), 13 * 16 + 2); terrain_png.put(new BlockData(Material.WOOL, DyeColor.SILVER.getWoolData()), 14 * 16 + 1); } private static Map<Material, Integer> items_png = Maps.newHashMap(); private static int items_png_size = 16; static { items_png.put(Material.LEATHER_HELMET, 0 * 16 + 0); items_png.put(Material.LEATHER_CHESTPLATE, 1 * 16 + 0); items_png.put(Material.LEATHER_LEGGINGS, 2 * 16 + 0); items_png.put(Material.LEATHER_BOOTS, 3 * 16 + 0); items_png.put(Material.CHAINMAIL_HELMET, 0 * 16 + 1); items_png.put(Material.CHAINMAIL_CHESTPLATE, 1 * 16 + 1); items_png.put(Material.CHAINMAIL_LEGGINGS, 2 * 16 + 1); items_png.put(Material.CHAINMAIL_BOOTS, 3 * 16 + 1); items_png.put(Material.IRON_HELMET, 0 * 16 + 2); items_png.put(Material.IRON_CHESTPLATE, 1 * 16 + 2); items_png.put(Material.IRON_LEGGINGS, 2 * 16 + 2); items_png.put(Material.IRON_BOOTS, 3 * 16 + 2); items_png.put(Material.DIAMOND_HELMET, 0 * 16 + 3); items_png.put(Material.DIAMOND_CHESTPLATE, 1 * 16 + 3); items_png.put(Material.DIAMOND_LEGGINGS, 2 * 16 + 3); items_png.put(Material.DIAMOND_BOOTS, 3 * 16 + 3); items_png.put(Material.GOLD_HELMET, 0 * 16 + 4); items_png.put(Material.GOLD_CHESTPLATE, 1 * 16 + 4); items_png.put(Material.GOLD_LEGGINGS, 2 * 16 + 4); items_png.put(Material.GOLD_BOOTS, 3 * 16 + 4); items_png.put(Material.WOOD_SWORD, 4 * 16 + 0); items_png.put(Material.WOOD_SPADE, 5 * 16 + 0); items_png.put(Material.WOOD_PICKAXE, 6 * 16 + 0); items_png.put(Material.WOOD_AXE, 7 * 16 + 0); items_png.put(Material.WOOD_HOE, 8 * 16 + 0); items_png.put(Material.STONE_SWORD, 4 * 16 + 1); items_png.put(Material.STONE_SPADE, 5 * 16 + 1); items_png.put(Material.STONE_PICKAXE, 6 * 16 + 1); items_png.put(Material.STONE_AXE, 7 * 16 + 1); items_png.put(Material.STONE_HOE, 8 * 16 + 1); items_png.put(Material.IRON_SWORD, 4 * 16 + 2); items_png.put(Material.IRON_SPADE, 5 * 16 + 2); items_png.put(Material.IRON_PICKAXE, 6 * 16 + 2); items_png.put(Material.IRON_AXE, 7 * 16 + 2); items_png.put(Material.IRON_HOE, 8 * 16 + 2); items_png.put(Material.DIAMOND_SWORD, 4 * 16 + 3); items_png.put(Material.DIAMOND_SPADE, 5 * 16 + 3); items_png.put(Material.DIAMOND_PICKAXE, 6 * 16 + 3); items_png.put(Material.DIAMOND_AXE, 7 * 16 + 3); items_png.put(Material.DIAMOND_HOE, 8 * 16 + 3); items_png.put(Material.GOLD_SWORD, 4 * 16 + 4); items_png.put(Material.GOLD_SPADE, 5 * 16 + 4); items_png.put(Material.GOLD_PICKAXE, 6 * 16 + 4); items_png.put(Material.GOLD_AXE, 7 * 16 + 4); items_png.put(Material.GOLD_HOE, 8 * 16 + 4); items_png.put(Material.BOW, 6 * 16 + 5); items_png.put(Material.POTION, 9 * 16 + 10); } // generate block.css private static String getBlockStyles(AutoRefMatch match) { Set<BlockData> blocks = Sets.newHashSet(); for (AutoRefTeam team : match.getTeams()) for (AutoRefGoal goal : team.getTeamGoals()) if (goal.hasItem()) blocks.add(goal.getItem()); StringWriter css = new StringWriter(); for (BlockData bd : blocks) { Integer x = terrain_png.get(bd); String selector = String.format(".block.mat-%d.data-%d", bd.getMaterial().getId(), (int) bd.getData()); css.write(selector + ":before "); if (x == null) css.write("{ display: none; }\n"); else css.write(String.format("{ background-position: -%dpx -%dpx; }\n", terrain_png_size * (x % 16), terrain_png_size * (x / 16))); if ((bd.getMaterial().getNewData((byte) 0) instanceof Colorable)) { DyeColor color = DyeColor.getByWoolData(bd.getData()); String hex = ColorConverter.dyeToHex(color); css.write(String.format("%s { color: %s; }\n", selector, hex)); } } return css.toString(); } private static String getTeamList(AutoRefMatch match) { StringWriter teamlist = new StringWriter(); for (AutoRefTeam team : match.getTeams()) { Set<String> members = Sets.newHashSet(); for (AutoRefPlayer apl : team.getCachedPlayers()) members.add("<li><input type='checkbox' class='player-toggle' data-player='" + getTag(apl) + "'>" + playerHTML(apl) + "</li>\n"); String memberlist = members.size() == 0 ? "<li>{none}</li>" : StringUtils.join(members, ""); teamlist.write("<div class='span3'>"); teamlist.write(String.format("<h4 class='team team-%s'>%s</h4>", getTag(team), ChatColor.stripColor(team.getDisplayName()))); teamlist.write(String.format("<ul class='teammembers unstyled'>%s</ul></div>\n", memberlist)); } return teamlist.toString(); } private static class NemesisComparator implements Comparator<AutoRefPlayer> { private AutoRefPlayer target = null; public NemesisComparator(AutoRefPlayer target) { this.target = target; } public int compare(AutoRefPlayer apl1, AutoRefPlayer apl2) { if (apl1.getTeam() == target.getTeam()) return -1; if (apl2.getTeam() == target.getTeam()) return +1; // get the number of kills on this player total int k = apl1.getKills(target) - apl2.getKills(target); if (k != 0) return k; // get the relative difference in "focus" return apl1.getKills(target)*apl2.getKills() - apl2.getKills(target)*apl1.getKills(); } }; private static String getPlayerStats(AutoRefMatch match) { List<AutoRefPlayer> players = Lists.newArrayList(match.getCachedPlayers()); Collections.sort(players, new Comparator<AutoRefPlayer>() { public int compare(AutoRefPlayer apl1, AutoRefPlayer apl2) { return apl2.getKDD() - apl1.getKDD(); } }); int rank = 0; StringWriter playerstats = new StringWriter(); for (AutoRefPlayer apl : players) { // get nemesis of this player AutoRefPlayer nms = Collections.max(players, new NemesisComparator(apl)); if (nms != null && nms.getTeam() != null && nms.getTeam() == apl.getTeam()) nms = null; playerstats.write(String.format("<tr><td>%d</td><td>%s</td>", ++rank, playerHTML(apl))); playerstats.write(String.format("<td>%d (%d)</td><td>%d</td><td>%s</td>", apl.getKills(), apl.getAssists(), apl.getDeathCount(), apl.getExtendedAccuracyInfo())); playerstats.write(String.format("<td>%s</td></tr>\n", nms == null ? "none" : playerHTML(nms))); } return playerstats.toString(); } private static String getTag(String s) { return s.toLowerCase().replaceAll("[^a-z0-9]+", ""); } private static String getTag(AutoRefPlayer apl) { return apl == null ? "none" : getTag(apl.getName()); } private static String getTag(AutoRefTeam team) { return team == null ? "none" : getTag(team.getName()); } private static String playerHTML(AutoRefPlayer apl) { return String.format("<span class='player player-%s team-%s'>%s</span>", getTag(apl), getTag(apl.getTeam()), apl.getName()); } private static String transcriptEventHTML(AutoRefMatch match, TranscriptEvent e) { String m = e.getMessage(), tagdata = ""; Set<String> rowClasses = Sets.newHashSet("type-" + e.getType().getEventClass()); for (Object actor : e.getActors()) { if (actor instanceof AutoRefPlayer) { AutoRefPlayer apl = (AutoRefPlayer) actor; tagdata += String.format(" data-player='%s'", apl.getName()); } if (actor instanceof BlockData) { BlockData bd = (BlockData) actor; int mat = bd.getMaterial().getId(); int data = bd.getData(); m = m.replaceAll(bd.getName(), String.format( "<span class='block mat-%d data-%d'>%s</span>", mat, data, bd.getName())); } } for (AutoRefPlayer apl : match.getPlayers()) if (m.contains(apl.getName())) { m = m.replaceAll(apl.getName(), playerHTML(apl)); rowClasses.add("type-player-event"); rowClasses.add("player-" + getTag(apl)); } String coords = LocationUtil.toBlockCoords(e.getLocation()); String fmt = "<tr class='transcript-event %s' data-location='%s'%s>" + "<td class='message'>%s</td><td class='timestamp'>%s</td></tr>\n"; return String.format(fmt, StringUtils.join(rowClasses, " "), coords, tagdata, m, e.getTimestamp()); } public String generateJSONReport(AutoRefMatch match) { JSONReport report = new JSONReport(); report.map = match.getMapName(); report.winner = match.getWinningTeam().getDefaultName(); report.teams = Maps.newHashMap(); for (AutoRefTeam team : match.getTeams()) report.teams.put(team.getDefaultName(), team.getJSONTeam()); report.matchlength = match.getElapsedSeconds(); return new Gson().toJson(report); } } class JSONReport { public Map<String, AutoRefTeam.JSONTeamData> teams; public String map; public String winner; public long matchlength; // seconds public List<String> referees; public List<String> streamers; }