package opendota; import com.google.gson.Gson; import com.google.protobuf.GeneratedMessage; import skadistats.clarity.decoder.Util; import skadistats.clarity.model.Entity; import skadistats.clarity.model.FieldPath; import skadistats.clarity.model.StringTable; import skadistats.clarity.processor.entities.Entities; import skadistats.clarity.processor.entities.OnEntityEntered; import skadistats.clarity.processor.entities.OnEntityLeft; import skadistats.clarity.processor.entities.UsesEntities; import skadistats.clarity.processor.gameevents.OnCombatLogEntry; import skadistats.clarity.processor.reader.OnMessage; import skadistats.clarity.processor.reader.OnTickStart; import skadistats.clarity.processor.runner.Context; import skadistats.clarity.processor.runner.SimpleRunner; import skadistats.clarity.model.CombatLogEntry; import skadistats.clarity.processor.stringtables.StringTables; import skadistats.clarity.processor.stringtables.UsesStringTable; import skadistats.clarity.source.InputStreamSource; import skadistats.clarity.wire.common.proto.Demo.CDemoFileInfo; import skadistats.clarity.wire.common.proto.DotaUserMessages; import skadistats.clarity.wire.common.proto.DotaUserMessages.CDOTAUserMsg_ChatEvent; import skadistats.clarity.wire.common.proto.DotaUserMessages.CDOTAUserMsg_ChatWheel; import skadistats.clarity.wire.common.proto.DotaUserMessages.CDOTAUserMsg_LocationPing; import skadistats.clarity.wire.common.proto.DotaUserMessages.CDOTAUserMsg_SpectatorPlayerUnitOrders; import skadistats.clarity.wire.common.proto.DotaUserMessages.DOTA_COMBATLOG_TYPES; import skadistats.clarity.wire.s1.proto.S1UserMessages.CUserMsg_SayText2; import skadistats.clarity.wire.s2.proto.S2UserMessages.CUserMessageSayText2; import skadistats.clarity.wire.s2.proto.S2DotaGcCommon.CMsgDOTAMatch; import java.util.*; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import opendota.combatlogvisitors.TrackVisitor; import opendota.combatlogvisitors.GreevilsGreedVisitor; import opendota.combatlogvisitors.TrackVisitor.TrackStatus; import opendota.processors.warding.OnWardExpired; import opendota.processors.warding.OnWardKilled; import opendota.processors.warding.OnWardPlaced; public class Parse { public class Entry { public Integer time = 0; public String type; public Integer team; public String unit; public String key; public Integer value; public Integer slot; public Integer player_slot; //chat event fields public Integer player1; public Integer player2; //combat log fields public String attackername; public String targetname; public String sourcename; public String targetsourcename; public Boolean attackerhero; public Boolean targethero; public Boolean attackerillusion; public Boolean targetillusion; public String inflictor; public Integer gold_reason; public Integer xp_reason; public String valuename; //public Float stun_duration; //public Float slow_duration; //entity fields public Integer gold; public Integer lh; public Integer xp; public Integer x; public Integer y; public Integer z; public Float stuns; public Integer hero_id; public transient List<Item> hero_inventory; public Integer life_state; public Integer level; public Integer kills; public Integer deaths; public Integer assists; public Integer denies; public Boolean entityleft; public Integer ehandle; public Integer obs_placed; public Integer sen_placed; public Integer creeps_stacked; public Integer camps_stacked; public Integer rune_pickups; public Boolean repicked; public Boolean randomed; public Boolean pred_vict; public Float stun_duration; public Float slow_duration; public Boolean tracked_death; public Integer greevils_greed_stack; public String tracked_sourcename; public Integer firstblood_claimed; public Float teamfight_participation; public Integer towers_killed; public Integer roshans_killed; public Integer observers_placed; public Integer draft_order; public Boolean pick; public Integer draft_active_team; public Integer draft_extime0; public Integer draft_extime1; public Entry() { } public Entry(Integer time) { this.time = time; } } private class Item { String id; //Charges can be used to determine how many items are stacked together on stackable items Integer num_charges; //item_ward_dispenser uses num_changes for observer wards //and num_secondary_changes for sentry wards count //and is considered not stackable Integer num_secondary_charges; } private class UnknownItemFoundException extends RuntimeException { public UnknownItemFoundException(String message) { super(message); } } float INTERVAL = 1; float nextInterval = 0; Integer time = 0; int numPlayers = 10; int[] validIndices = new int[numPlayers]; boolean init = false; int gameStartTime = 0; boolean postGame = false; // true when ancient destroyed private Gson g = new Gson(); HashMap<String, Integer> name_to_slot = new HashMap<String, Integer>(); HashMap<Integer, Integer> slot_to_playerslot = new HashMap<Integer, Integer>(); HashMap<Long, Integer> steamid_to_playerslot = new HashMap<Long, Integer>(); HashMap<Integer, Integer> cosmeticsMap = new HashMap<Integer, Integer>(); HashMap<Integer, Integer> dotaplusxpMap = new HashMap<Integer, Integer>(); // playerslot, xp HashMap<Integer, Integer> ward_ehandle_to_slot = new HashMap<Integer, Integer>(); InputStream is = null; OutputStream os = null; private GreevilsGreedVisitor greevilsGreedVisitor; private TrackVisitor trackVisitor; private ArrayList<Boolean> isPlayerStartingItemsWritten; int pingCount = 0; private ArrayList<Entry> logBuffer = new ArrayList<Entry>(); //Draft stage variable boolean[] draftOrderProcessed = new boolean[22]; int order = 1; boolean isDraftStartTimeProcessed = false; //flag to know if draft start time is already handled boolean isDotaPlusProcessed = false; public Parse(InputStream input, OutputStream output) throws IOException { greevilsGreedVisitor = new GreevilsGreedVisitor(name_to_slot); trackVisitor = new TrackVisitor(); is = input; os = output; isPlayerStartingItemsWritten = new ArrayList<>(Arrays.asList(new Boolean[numPlayers])); Collections.fill(isPlayerStartingItemsWritten, Boolean.FALSE); long tStart = System.currentTimeMillis(); new SimpleRunner(new InputStreamSource(is)).runWith(this); long tMatch = System.currentTimeMillis() - tStart; System.err.format("total time taken: %s\n", (tMatch) / 1000.0); } public void output(Entry e) { try { if (gameStartTime == 0) { logBuffer.add(e); } else { e.time -= gameStartTime; this.os.write((g.toJson(e) + "\n").getBytes()); } } catch (IOException ex) { System.err.println(ex); } } public void flushLogBuffer() { for (Entry e : logBuffer) { output(e); } logBuffer = null; } //@OnMessage(GeneratedMessage.class) public void onMessage(Context ctx, GeneratedMessage message) { System.err.println(message.getClass().getName()); System.out.println(message.toString()); } /*@OnMessage(DotaUserMessages.CDOTAUserMsg_SpectatorPlayerClick.class) public void onSpectatorPlayerClick(Context ctx, DotaUserMessages.CDOTAUserMsg_SpectatorPlayerClick message){ Entry entry = new Entry(time); entry.type = "clicks"; //need to get the entity by index entry.key = String.valueOf(message.getOrderType()); Entity e = ctx.getProcessor(Entities.class).getByIndex(message.getEntindex()); entry.x = getEntityProperty(e, "m_iCursor.0000", null); entry.y = getEntityProperty(e, "m_iCursor.0001", null); entry.slot = getEntityProperty(e, "m_iPlayerID", null); //theres also target_index output(entry); } */ @OnMessage(CMsgDOTAMatch.class) public void onDotaMatch(Context ctx, CMsgDOTAMatch message) { //TODO could use this for match overview data for uploads //System.err.println(message); } @OnMessage(CDOTAUserMsg_SpectatorPlayerUnitOrders.class) public void onSpectatorPlayerUnitOrders(Context ctx, CDOTAUserMsg_SpectatorPlayerUnitOrders message) { Entry entry = new Entry(time); entry.type = "actions"; //the entindex points to a CDOTAPlayer. This is probably the player that gave the order. Entity e = ctx.getProcessor(Entities.class).getByIndex(message.getEntindex()); entry.slot = getEntityProperty(e, "m_iPlayerID", null); //Integer handle = (Integer)getEntityProperty(e, "m_hAssignedHero", null); //Entity h = ctx.getProcessor(Entities.class).getByHandle(handle); //System.err.println(h.getDtClass().getDtName()); //break actions into types? entry.key = String.valueOf(message.getOrderType()); //System.err.println(message); output(entry); } @OnMessage(CDOTAUserMsg_LocationPing.class) public void onPlayerPing(Context ctx, CDOTAUserMsg_LocationPing message) { pingCount += 1; if (pingCount > 10000) { return; } Entry entry = new Entry(time); entry.type = "pings"; entry.slot = message.getPlayerId(); /* System.err.println(message); player_id: 7 location_ping { x: 5871 y: 6508 target: -1 direct_ping: false type: 0 } */ //we could get the ping coordinates/type if we cared //entry.key = String.valueOf(message.getOrderType()); output(entry); } @OnMessage(CDOTAUserMsg_ChatEvent.class) public void onChatEvent(Context ctx, CDOTAUserMsg_ChatEvent message) { Integer player1 = message.getPlayerid1(); Integer player2 = message.getPlayerid2(); Integer value = message.getValue(); String type = String.valueOf(message.getType()); Entry entry = new Entry(time); entry.type = type; entry.player1 = player1; entry.player2 = player2; entry.value = value; output(entry); } @OnMessage(CDOTAUserMsg_ChatWheel.class) public void onChatWheel(Context ctx, CDOTAUserMsg_ChatWheel message) { Entry entry = new Entry(time); entry.type = "chatwheel"; entry.slot = message.getPlayerId(); entry.key = String.valueOf(message.getChatMessageId()); output(entry); } @OnMessage(CUserMsg_SayText2.class) public void onAllChatS1(Context ctx, CUserMsg_SayText2 message) { Entry entry = new Entry(time); entry.unit = String.valueOf(message.getPrefix()); entry.key = String.valueOf(message.getText()); entry.type = "chat"; output(entry); } @OnMessage(CUserMessageSayText2.class) public void onAllChatS2(Context ctx, CUserMessageSayText2 message) { Entry entry = new Entry(time); entry.unit = String.valueOf(message.getParam1()); entry.key = String.valueOf(message.getParam2()); Entity e = ctx.getProcessor(Entities.class).getByIndex(message.getEntityindex()); entry.slot = getEntityProperty(e, "m_iPlayerID", null); entry.type = "chat"; output(entry); } @OnMessage(CDemoFileInfo.class) public void onFileInfo(Context ctx, CDemoFileInfo message) { //beware of 4.2b limit! we don't currently do anything with this, so we might be able to just remove this //we can't use the value field since it takes Integers //Entry matchIdEntry = new Entry(); //matchIdEntry.type = "match_id"; //matchIdEntry.value = message.getGameInfo().getDota().getMatchId(); //output(matchIdEntry); // Extracted cosmetics data from CDOTAWearableItem entities Entry cosmeticsEntry = new Entry(); cosmeticsEntry.type = "cosmetics"; cosmeticsEntry.key = new Gson().toJson(cosmeticsMap); output(cosmeticsEntry); // Dota plus hero levels Entry dotaPlusEntry = new Entry(); dotaPlusEntry.type = "dotaplus"; dotaPlusEntry.key = new Gson().toJson(dotaplusxpMap); output(dotaPlusEntry); //emit epilogue event to mark finish Entry epilogueEntry = new Entry(); epilogueEntry.type = "epilogue"; epilogueEntry.key = new Gson().toJson(message); output(epilogueEntry); } @OnCombatLogEntry public void onCombatLogEntry(Context ctx, CombatLogEntry cle) { try { time = Math.round(cle.getTimestamp()); //create a new entry Entry combatLogEntry = new Entry(time); combatLogEntry.type = cle.getType().name(); //translate the fields using string tables if necessary (get*Name methods) combatLogEntry.attackername = cle.getAttackerName(); combatLogEntry.targetname = cle.getTargetName(); combatLogEntry.sourcename = cle.getDamageSourceName(); combatLogEntry.targetsourcename = cle.getTargetSourceName(); combatLogEntry.inflictor = cle.getInflictorName(); combatLogEntry.attackerhero = cle.isAttackerHero(); combatLogEntry.targethero = cle.isTargetHero(); combatLogEntry.attackerillusion = cle.isAttackerIllusion(); combatLogEntry.targetillusion = cle.isTargetIllusion(); combatLogEntry.value = cle.getValue(); float stunDuration = cle.getStunDuration(); if (stunDuration > 0) { combatLogEntry.stun_duration = stunDuration; } float slowDuration = cle.getSlowDuration(); if (slowDuration > 0) { combatLogEntry.slow_duration = slowDuration; } //value may be out of bounds in string table, we can only get valuename if a purchase (type 11) if (cle.getType() == DOTA_COMBATLOG_TYPES.DOTA_COMBATLOG_PURCHASE) { combatLogEntry.valuename = cle.getValueName(); } else if (cle.getType() == DOTA_COMBATLOG_TYPES.DOTA_COMBATLOG_GOLD) { combatLogEntry.gold_reason = cle.getGoldReason(); } else if (cle.getType() == DOTA_COMBATLOG_TYPES.DOTA_COMBATLOG_XP) { combatLogEntry.xp_reason = cle.getXpReason(); } combatLogEntry.greevils_greed_stack = greevilsGreedVisitor.visit(time, cle); TrackStatus trackStatus = trackVisitor.visit(time, cle); if (trackStatus != null) { combatLogEntry.tracked_death = trackStatus.tracked; combatLogEntry.tracked_sourcename = trackStatus.inflictor; } if (combatLogEntry.type.equals("DOTA_COMBATLOG_GAME_STATE") && combatLogEntry.value == 6) { postGame = true; } if (combatLogEntry.type.equals("DOTA_COMBATLOG_GAME_STATE") && combatLogEntry.value == 5) { //alternate to combat log for getting game zero time (looks like this is set at the same time as the game start, so it's not any better for streaming) // int currGameStartTime = Math.round( (float) grp.getProperty("m_pGameRules.m_flGameStartTime")); if (gameStartTime == 0) { gameStartTime = combatLogEntry.time; flushLogBuffer(); } } if (cle.getType().ordinal() <= 19) { output(combatLogEntry); } } catch(Exception e) { System.err.println(e); System.err.println(cle); } } @OnEntityEntered public void onEntityEntered(Context ctx, Entity e) { if (e.getDtClass().getDtName().equals("CDOTAWearableItem")) { Integer accountId = getEntityProperty(e, "m_iAccountID", null); Integer itemDefinitionIndex = getEntityProperty(e, "m_iItemDefinitionIndex", null); Integer ownerHandle = getEntityProperty(e, "m_hOwnerEntity", null); Entity owner = ctx.getProcessor(Entities.class).getByHandle(ownerHandle); //System.err.format("%s,%s\n", accountId, itemDefinitionIndex); if (accountId > 0) { // Get the owner (a hero entity) Integer playerId = getEntityProperty(owner, "m_iPlayerID", null); Long accountId64 = 76561197960265728L + accountId; Integer playerSlot = steamid_to_playerslot.get(accountId64); cosmeticsMap.put(itemDefinitionIndex, playerSlot); } } } @UsesStringTable("EntityNames") @UsesEntities @OnTickStart public void onTickStart(Context ctx, boolean synthetic) { /* Iterator<Entity> cosmetics = ctx.getProcessor(Entities.class).getAllByDtName("CDOTAWearableItem"); while ( cosmetics.hasNext() ) { Entity e = cosmetics.next(); Integer accountId = getEntityProperty(e, "m_iAccountID", null); Integer itemDefinitionIndex = getEntityProperty(e, "m_iItemDefinitionIndex", null); if (itemDefinitionIndex == 7559) { System.err.format("%s,%s\n", accountId, itemDefinitionIndex); } } */ //TODO check engine to decide whether to use s1 or s2 entities //ctx.getEngineType() //s1 DT_DOTAGameRulesProxy Entity grp = ctx.getProcessor(Entities.class).getByDtName("CDOTAGamerulesProxy"); Entity pr = ctx.getProcessor(Entities.class).getByDtName("CDOTA_PlayerResource"); Entity dData = ctx.getProcessor(Entities.class).getByDtName("CDOTA_DataDire"); Entity rData = ctx.getProcessor(Entities.class).getByDtName("CDOTA_DataRadiant"); // Create draftStage variable Integer draftStage = getEntityProperty(grp, "m_pGameRules.m_nGameState", null); if (grp != null) { //System.err.println(grp); //dota_gamerules_data.m_iGameMode = 22 //dota_gamerules_data.m_unMatchID64 = 1193091757 time = Math.round((float) getEntityProperty(grp, "m_pGameRules.m_fGameTime", null)); //draft timings if(draftStage == 2) { //determine the time the draftings start if(!isDraftStartTimeProcessed) { Long iPlayerIDsInControl = getEntityProperty(grp, "m_pGameRules.m_iPlayerIDsInControl", null); boolean isDraftStarted = iPlayerIDsInControl.compareTo(Long.valueOf(0)) != 0; if(isDraftStarted) { Entry draftStartEntry = new Entry(time); draftStartEntry.type = "draft_start"; output(draftStartEntry); isDraftStartTimeProcessed = true; } } //Picks and ban are not in order due to draft change rules changes between patches // Need to listen for the picks and ban to change int[] draftHeroes = new int[22]; draftHeroes[0] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0000", null); draftHeroes[1] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0001", null); draftHeroes[2] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0002", null); draftHeroes[3] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0003", null); draftHeroes[4] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0004", null); draftHeroes[5] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0005", null); draftHeroes[6] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0006", null); draftHeroes[7] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0007", null); draftHeroes[8] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0008", null); draftHeroes[9] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0009", null); // Apparently Drafts go to 6 bans now, but have returns of null draftHeroes[10] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0010", null) == null ? 0 : getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0010", null); draftHeroes[11] = getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0011", null) == null ? 0 : getEntityProperty(grp, "m_pGameRules.m_BannedHeroes.0011", null); draftHeroes[12] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0000", null); draftHeroes[13] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0001", null); draftHeroes[14] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0002", null); draftHeroes[15] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0003", null); draftHeroes[16] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0004", null); draftHeroes[17] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0005", null); draftHeroes[18] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0006", null); draftHeroes[19] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0007", null); draftHeroes[20] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0008", null); draftHeroes[21] = getEntityProperty(grp, "m_pGameRules.m_SelectedHeroes.0009", null); //Once a pick or ban happens grab the time and extra time remaining for both teams for(int i = 0; i < draftHeroes.length; i++) { if(draftHeroes[i] > 0 && draftOrderProcessed[i] == false) { // used to check for new bans and picks draftOrderProcessed[i] = true; Entry draftTimingsEntry = new Entry(time); draftTimingsEntry.type = "draft_timings"; draftTimingsEntry.draft_order = order; order = order + 1; draftTimingsEntry.pick = i < 12 ? false : true; draftTimingsEntry.hero_id = draftHeroes[i]; draftTimingsEntry.draft_active_team = getEntityProperty(grp, "m_pGameRules.m_iActiveTeam", null); draftTimingsEntry.draft_extime0 = Math.round((float) getEntityProperty(grp, "m_pGameRules.m_fExtraTimeRemaining.0000", null)); draftTimingsEntry.draft_extime1 = Math.round((float) getEntityProperty(grp, "m_pGameRules.m_fExtraTimeRemaining.0001", null)); output(draftTimingsEntry); } } } //initialize nextInterval value if (nextInterval == 0) { nextInterval = time; } } if (pr != null) { //Radiant coach shows up in vecPlayerTeamData as position 5 //all the remaining dire entities are offset by 1 and so we miss reading the last one and don't get data for the first dire player //coaches appear to be on team 1, radiant is 2 and dire is 3? //construct an array of valid indices to get vecPlayerTeamData from if (!init) { int added = 0; int i = 0; //according to @Decoud Valve seems to have fixed this issue and players should be in first 10 slots again //sanity check of i to prevent infinite loop when <10 players? while (added < numPlayers && i < 100) { try { //check each m_vecPlayerData to ensure the player's team is radiant or dire int playerTeam = getEntityProperty(pr, "m_vecPlayerData.%i.m_iPlayerTeam", i); int teamSlot = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_iTeamSlot", i); Long steamid = getEntityProperty(pr, "m_vecPlayerData.%i.m_iPlayerSteamID", i); //System.err.format("%s %s %s: %s\n", i, playerTeam, teamSlot, steamid); if (playerTeam == 2 || playerTeam == 3) { //output the player_slot based on team and teamslot Entry entry = new Entry(time); entry.type = "player_slot"; entry.key = String.valueOf(added); entry.value = (playerTeam == 2 ? 0 : 128) + teamSlot; output(entry); //add it to validIndices, add 1 to added validIndices[added] = i; added += 1; slot_to_playerslot.put(added, entry.value); steamid_to_playerslot.put(steamid, entry.value); } } catch(Exception e) { //swallow the exception when an unexpected number of players (!=10) //System.err.println(e); } i += 1; } init = true; } if (!postGame && time >= nextInterval) { //System.err.println(pr); for (int i = 0; i < numPlayers; i++) { Integer hero = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_nSelectedHeroID", validIndices[i]); int handle = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_hSelectedHero", validIndices[i]); int playerTeam = getEntityProperty(pr, "m_vecPlayerData.%i.m_iPlayerTeam", validIndices[i]); int teamSlot = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_iTeamSlot", validIndices[i]); //2 is radiant, 3 is dire, 1 is other? Entity dataTeam = playerTeam == 2 ? rData : dData; Entry entry = new Entry(time); entry.type = "interval"; entry.slot = i; entry.repicked = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_bHasRepicked", validIndices[i]); entry.randomed = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_bHasRandomed", validIndices[i]); entry.pred_vict = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_bHasPredictedVictory", validIndices[i]); entry.firstblood_claimed = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_iFirstBloodClaimed", validIndices[i]); entry.teamfight_participation = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_flTeamFightParticipation", validIndices[i]);; entry.level = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_iLevel", validIndices[i]); entry.kills = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_iKills", validIndices[i]); entry.deaths = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_iDeaths", validIndices[i]); entry.assists = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_iAssists", validIndices[i]); entry.denies = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iDenyCount", teamSlot); entry.obs_placed = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iObserverWardsPlaced", teamSlot); entry.sen_placed = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iSentryWardsPlaced", teamSlot); entry.creeps_stacked = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iCreepsStacked", teamSlot); entry.camps_stacked = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iCampsStacked", teamSlot); entry.rune_pickups = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iRunePickups", teamSlot); entry.towers_killed = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iTowerKills", teamSlot); entry.roshans_killed = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iRoshanKills", teamSlot); entry.observers_placed = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iObserverWardsPlaced", teamSlot); if (teamSlot >= 0) { entry.gold = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iTotalEarnedGold", teamSlot); entry.lh = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iLastHitCount", teamSlot); entry.xp = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_iTotalEarnedXP", teamSlot); entry.stuns = getEntityProperty(dataTeam, "m_vecDataTeam.%i.m_fStuns", teamSlot); } //TODO: gem, rapier time? //need to dump inventory items for each player and possibly keep track of item entity handles //get the player's hero entity Entity e = ctx.getProcessor(Entities.class).getByHandle(handle); //get the hero's coordinates if (e != null) { //System.err.println(e); entry.x = getEntityProperty(e, "CBodyComponent.m_cellX", null); entry.y = getEntityProperty(e, "CBodyComponent.m_cellY", null); //System.err.format("%s, %s\n", entry.x, entry.y); //get the hero's entity name, ex: CDOTA_Hero_Zuus entry.unit = e.getDtClass().getDtName(); entry.hero_id = hero; entry.life_state = getEntityProperty(e, "m_lifeState", null); //check if hero has been assigned to entity if (hero > 0) { //get the hero's entity name, ex: CDOTA_Hero_Zuus String unit = e.getDtClass().getDtName(); //grab the end of the name, lowercase it String ending = unit.substring("CDOTA_Unit_Hero_".length()); //valve is bad at consistency and the combat log name could involve replacing camelCase with _ or not! //double map it so we can look up both cases String combatLogName = "npc_dota_hero_" + ending.toLowerCase(); //don't include final underscore here since the first letter is always capitalized and will be converted to underscore String combatLogName2 = "npc_dota_hero" + ending.replaceAll("([A-Z])", "_$1").toLowerCase(); //System.err.format("%s, %s, %s\n", unit, combatLogName, combatLogName2); //populate for combat log mapping name_to_slot.put(combatLogName, entry.slot); name_to_slot.put(combatLogName2, entry.slot); entry.hero_inventory = getHeroInventory(ctx, e); if (!isPlayerStartingItemsWritten.get(entry.slot) && entry.hero_inventory != null) { // Making something similar to DOTA_COMBATLOG_PURCHASE for each item in the beginning of the game isPlayerStartingItemsWritten.set(entry.slot, true); for (Item item : entry.hero_inventory) { Entry startingItemsEntry = new Entry(time); startingItemsEntry.type = "DOTA_COMBATLOG_PURCHASE"; startingItemsEntry.slot = entry.slot; startingItemsEntry.value = (entry.slot < 5 ? 0 : 123) + entry.slot; startingItemsEntry.valuename = item.id; startingItemsEntry.targetname = combatLogName; output(startingItemsEntry); } } } } output(entry); } nextInterval += INTERVAL; } // When the game is over, get dota plus levels if (postGame && !isDotaPlusProcessed) { for (int i = 0; i < numPlayers; i++) { int xp = getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_unSelectedHeroBadgeXP", i) == null ? 0 : getEntityProperty(pr, "m_vecPlayerTeamData.%i.m_unSelectedHeroBadgeXP", i); Long steamid = getEntityProperty(pr, "m_vecPlayerData.%i.m_iPlayerSteamID", i); int playerslot = steamid_to_playerslot.get(steamid); dotaplusxpMap.put(playerslot, xp); } isDotaPlusProcessed = true; } } } private List<Item> getHeroInventory(Context ctx, Entity eHero) { List<Item> inventoryList = new ArrayList<>(6); for (int i = 0; i < 6; i++) { try { Item item = getHeroItem(ctx, eHero, i); if(item != null) { inventoryList.add(item); } } catch (Exception e) { System.err.println(e); } } return inventoryList; } /** * Uses "EntityNames" string table and Entities processor * @param ctx Context * @param eHero Hero entity * @param idx 0-5 - inventory, 6-8 - backpack, 9-16 - stash * @return {@code null} - empty slot. Throws @{@link UnknownItemFoundException} if item information can't be extracted */ private Item getHeroItem(Context ctx, Entity eHero, int idx) throws UnknownItemFoundException { StringTable stEntityNames = ctx.getProcessor(StringTables.class).forName("EntityNames"); Entities entities = ctx.getProcessor(Entities.class); Integer hItem = eHero.getProperty("m_hItems." + Util.arrayIdxToString(idx)); if (hItem == 0xFFFFFF) { return null; } Entity eItem = entities.getByHandle(hItem); if(eItem == null) { throw new UnknownItemFoundException(String.format("Can't find item by its handle (%d)", hItem)); } String itemName = stEntityNames.getNameByIndex(eItem.getProperty("m_pEntity.m_nameStringableIndex")); if(itemName == null) { throw new UnknownItemFoundException("Can't get item name from EntityName string table"); } Item item = new Item(); item.id = itemName; int numCharges = eItem.getProperty("m_iCurrentCharges"); if(numCharges != 0) { item.num_charges = numCharges; } int numSecondaryCharges = eItem.getProperty("m_iSecondaryCharges"); if(numSecondaryCharges != 0) { item.num_secondary_charges = numSecondaryCharges; } return item; } public <T> T getEntityProperty(Entity e, String property, Integer idx) { try { if (e == null) { return null; } if (idx != null) { property = property.replace("%i", Util.arrayIdxToString(idx)); } FieldPath fp = e.getDtClass().getFieldPathForName(property); return e.getPropertyForFieldPath(fp); } catch (Exception ex) { return null; } } @OnWardKilled public void onWardKilled(Context ctx, Entity e, String killerHeroName) { Entry wardEntry = buildWardEntry(ctx, e); wardEntry.attackername = killerHeroName; output(wardEntry); } @OnWardExpired @OnWardPlaced public void onWardExistenceChanged(Context ctx, Entity e) { output(buildWardEntry(ctx, e)); } private Entry buildWardEntry(Context ctx, Entity e) { Entry entry = new Entry(time); boolean isObserver = !e.getDtClass().getDtName().contains("TrueSight"); Integer x = getEntityProperty(e, "CBodyComponent.m_cellX", null); Integer y = getEntityProperty(e, "CBodyComponent.m_cellY", null); Integer z = getEntityProperty(e, "CBodyComponent.m_cellZ", null); Integer life_state = getEntityProperty(e, "m_lifeState", null); Integer[] pos = {x, y}; entry.x = x; entry.y = y; entry.z = z; entry.type = isObserver ? "obs" : "sen"; entry.entityleft = life_state == 1; entry.key = Arrays.toString(pos); entry.ehandle = e.getHandle(); if (entry.entityleft) { entry.type += "_left"; } Integer owner = getEntityProperty(e, "m_hOwnerEntity", null); Entity ownerEntity = ctx.getProcessor(Entities.class).getByHandle(owner); entry.slot = ownerEntity != null ? (Integer) getEntityProperty(ownerEntity, "m_iPlayerID", null) : null; return entry; } }