diff --git a/src/main/java/opendota/Parse.java b/src/main/java/opendota/Parse.java index ab51e4224..1440ce797 100644 --- a/src/main/java/opendota/Parse.java +++ b/src/main/java/opendota/Parse.java @@ -38,6 +38,9 @@ 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 { @@ -401,7 +404,6 @@ else if (cle.getType() == DOTA_COMBATLOG_TYPES.DOTA_COMBATLOG_XP) { @OnEntityEntered public void onEntityEntered(Context ctx, Entity e) { - processEntity(ctx, e, false); if (e.getDtClass().getDtName().equals("CDOTAWearableItem")) { Integer accountId = getEntityProperty(e, "m_iAccountID", null); Integer itemDefinitionIndex = getEntityProperty(e, "m_iItemDefinitionIndex", null); @@ -418,11 +420,6 @@ public void onEntityEntered(Context ctx, Entity e) { } } } - - @OnEntityLeft - public void onEntityLeft(Context ctx, Entity e) { - processEntity(ctx, e, true); - } @UsesStringTable("EntityNames") @UsesEntities @@ -727,45 +724,43 @@ public T getEntityProperty(Entity e, String property, Integer idx) { } } - public void processEntity(Context ctx, Entity e, boolean entityLeft) - { - //CDOTA_NPC_Observer_Ward - //CDOTA_NPC_Observer_Ward_TrueSight - //s1 "DT_DOTA_NPC_Observer_Ward" - //s1 "DT_DOTA_NPC_Observer_Ward_TrueSight" - boolean isObserver = e.getDtClass().getDtName().equals("CDOTA_NPC_Observer_Ward"); - boolean isSentry = e.getDtClass().getDtName().equals("CDOTA_NPC_Observer_Ward_TrueSight"); - if (isObserver || isSentry) { - //System.err.println(e); - Entry entry = new Entry(time); - 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[] pos = {x, y}; - entry.x = x; - entry.y = y; - entry.z = z; - if (entityLeft) - { - entry.type = isObserver ? "obs_left" : "sen_left"; - } - else - { - entry.type = isObserver ? "obs" : "sen"; - } - entry.key = Arrays.toString(pos); - entry.entityleft = entityLeft; - entry.ehandle = e.getHandle(); - //System.err.println(entry.key); - 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) : ward_ehandle_to_slot.get(entry.ehandle); - if (entry.slot != null && !ward_ehandle_to_slot.containsKey(entry.ehandle)) { - ward_ehandle_to_slot.put(entry.ehandle, entry.slot); - } - //2/3 radiant/dire - //entry.team = e.getProperty("m_iTeamNum"); - output(entry); + @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; } } diff --git a/src/main/java/opendota/processors/warding/OnWardExpired.java b/src/main/java/opendota/processors/warding/OnWardExpired.java new file mode 100644 index 000000000..ab9bb6cb5 --- /dev/null +++ b/src/main/java/opendota/processors/warding/OnWardExpired.java @@ -0,0 +1,19 @@ +package opendota.processors.warding; + +import java.lang.annotation.Annotation; + +import skadistats.clarity.event.UsagePointMarker; +import skadistats.clarity.event.UsagePointType; +import skadistats.clarity.model.Entity; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = ElementType.METHOD) +@UsagePointMarker(value = UsagePointType.EVENT_LISTENER, parameterClasses = { Entity.class }) +public @interface OnWardExpired { +} + diff --git a/src/main/java/opendota/processors/warding/OnWardKilled.java b/src/main/java/opendota/processors/warding/OnWardKilled.java new file mode 100644 index 000000000..376390a91 --- /dev/null +++ b/src/main/java/opendota/processors/warding/OnWardKilled.java @@ -0,0 +1,19 @@ +package opendota.processors.warding; + +import java.lang.annotation.Annotation; + +import skadistats.clarity.event.UsagePointMarker; +import skadistats.clarity.event.UsagePointType; +import skadistats.clarity.model.Entity; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = ElementType.METHOD) +@UsagePointMarker(value = UsagePointType.EVENT_LISTENER, parameterClasses = { Entity.class, String.class }) +public @interface OnWardKilled { +} + diff --git a/src/main/java/opendota/processors/warding/OnWardPlaced.java b/src/main/java/opendota/processors/warding/OnWardPlaced.java new file mode 100644 index 000000000..b4d5721a5 --- /dev/null +++ b/src/main/java/opendota/processors/warding/OnWardPlaced.java @@ -0,0 +1,19 @@ +package opendota.processors.warding; + +import java.lang.annotation.Annotation; + +import skadistats.clarity.event.UsagePointMarker; +import skadistats.clarity.event.UsagePointType; +import skadistats.clarity.model.Entity; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = ElementType.METHOD) +@UsagePointMarker(value = UsagePointType.EVENT_LISTENER, parameterClasses = { Entity.class }) +public @interface OnWardPlaced { +} + diff --git a/src/main/java/opendota/processors/warding/Wards.java b/src/main/java/opendota/processors/warding/Wards.java new file mode 100644 index 000000000..d0c8f18d7 --- /dev/null +++ b/src/main/java/opendota/processors/warding/Wards.java @@ -0,0 +1,202 @@ +package opendota.processors.warding; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +import skadistats.clarity.event.Event; +import skadistats.clarity.event.EventListener; +import skadistats.clarity.event.Initializer; +import skadistats.clarity.event.Provides; +import skadistats.clarity.model.Entity; +import skadistats.clarity.model.FieldPath; +import skadistats.clarity.model.CombatLogEntry; +import skadistats.clarity.processor.entities.OnEntityCreated; +import skadistats.clarity.processor.entities.OnEntityDeleted; +import skadistats.clarity.processor.entities.OnEntityUpdated; +import skadistats.clarity.processor.entities.UsesEntities; +import skadistats.clarity.processor.gameevents.OnCombatLogEntry; +import skadistats.clarity.processor.reader.OnTickEnd; +import skadistats.clarity.processor.runner.Context; +import skadistats.clarity.wire.common.proto.DotaUserMessages; + +/** + * @author micaelbergeron + */ +@UsesEntities +@Provides({ OnWardKilled.class, OnWardExpired.class, OnWardPlaced.class }) +public class Wards { + + private static final Map WARDS_TARGET_NAME_BY_DT_CLASS; + private static final Set WARDS_DT_CLASSES; + private static final Set WARDS_TARGET_NAMES; + + static { + final String TARGET_NAME_OBSERVER = "npc_dota_observer_wards"; + final String TARGET_NAME_SENTRY = "npc_dota_sentry_wards"; + HashMap target_by_dtclass = new HashMap<>(4); + + target_by_dtclass.put("DT_DOTA_NPC_Observer_Ward", TARGET_NAME_OBSERVER); + target_by_dtclass.put("CDOTA_NPC_Observer_Ward", TARGET_NAME_OBSERVER); + target_by_dtclass.put("DT_DOTA_NPC_Observer_Ward_TrueSight", TARGET_NAME_SENTRY); + target_by_dtclass.put("CDOTA_NPC_Observer_Ward_TrueSight", TARGET_NAME_SENTRY); + + WARDS_TARGET_NAME_BY_DT_CLASS = Collections.unmodifiableMap(target_by_dtclass); + WARDS_DT_CLASSES = Collections.unmodifiableSet(target_by_dtclass.keySet()); + WARDS_TARGET_NAMES = Collections.unmodifiableSet(new HashSet<>(target_by_dtclass.values())); + } + + private final Map lifeStatePaths = new HashMap<>(); + private final Map currentLifeState = new HashMap<>(); + + private final Map> wardKillersByWardClass = new HashMap<>(); + private Queue toProcess = new ArrayDeque<>(); + + private Event evKilled; + private Event evExpired; + private Event evPlaced; + + private class ProcessEntityCommand { + + private final Entity entity; + private final FieldPath fieldPath; + + public ProcessEntityCommand(Entity e, FieldPath p) { + entity = e; + fieldPath = p; + } + } + + @Initializer(OnWardKilled.class) + public void initOnWardKilled(final Context ctx, final EventListener listener) { + evKilled = ctx.createEvent(OnWardKilled.class, Entity.class, String.class); + } + + @Initializer(OnWardExpired.class) + public void initOnWardExpired(final Context ctx, final EventListener listener) { + evExpired = ctx.createEvent(OnWardExpired.class, Entity.class); + } + + @Initializer(OnWardPlaced.class) + public void initOnWardPlaced(final Context ctx, final EventListener listener) { + evPlaced = ctx.createEvent(OnWardPlaced.class, Entity.class); + } + + public Wards() { + WARDS_TARGET_NAMES.forEach((cls) -> { + wardKillersByWardClass.put(cls, new ArrayDeque<>()); + }); + } + + @OnEntityCreated + public void onCreated(Context ctx, Entity e) { + if (!isWard(e)) return; + + FieldPath lifeStatePath; + + clearCachedState(e); + ensureFieldPathForEntityInitialized(e); + if ((lifeStatePath = getFieldPathForEntity(e)) != null) { + processLifeStateChange(e, lifeStatePath); + } + } + + @OnEntityUpdated + public void onUpdated(Context ctx, Entity e, FieldPath[] fieldPaths, int num) { + FieldPath p; + if ((p = getFieldPathForEntity(e)) != null) { + for (int i = 0; i < num; i++) { + if (fieldPaths[i].equals(p)) { + toProcess.add(new ProcessEntityCommand(e, p)); + break; + } + } + } + } + + @OnEntityDeleted + public void onDeleted(Context ctx, Entity e) { + clearCachedState(e); + } + + @OnCombatLogEntry + public void onCombatLogEntry(Context ctx, CombatLogEntry entry) { + if (!isWardDeath(entry)) return; + + String killer; + if ((killer = entry.getDamageSourceName()) != null) { + wardKillersByWardClass.get(entry.getTargetName()).add(killer); + } + } + + @OnTickEnd + public void onTickEnd(Context ctx, boolean synthetic) { + if (!synthetic) return; + + ProcessEntityCommand cmd; + while ((cmd = toProcess.poll()) != null) { + processLifeStateChange(cmd.entity, cmd.fieldPath); + } + } + + private FieldPath getFieldPathForEntity(Entity e) { + return lifeStatePaths.get(e.getDtClass().getClassId()); + } + + private void clearCachedState(Entity e) { + currentLifeState.remove(e.getIndex()); + } + + private void ensureFieldPathForEntityInitialized(Entity e) { + Integer cid = e.getDtClass().getClassId(); + if (!lifeStatePaths.containsKey(cid)) { + lifeStatePaths.put(cid, e.getDtClass().getFieldPathForName("m_lifeState")); + } + } + + private boolean isWard(Entity e) { + return WARDS_DT_CLASSES.contains(e.getDtClass().getDtName()); + } + + private boolean isWardDeath(CombatLogEntry e) { + return e.getType().equals(DotaUserMessages.DOTA_COMBATLOG_TYPES.DOTA_COMBATLOG_DEATH) + && WARDS_TARGET_NAMES.contains(e.getTargetName()); + } + + public void processLifeStateChange(Entity e, FieldPath p) { + int oldState = currentLifeState.containsKey(e.getIndex()) ? currentLifeState.get(e.getIndex()) : 2; + int newState = e.getPropertyForFieldPath(p); + if (oldState != newState) { + switch(newState) { + case 0: + if (evPlaced != null) { + evPlaced.raise(e); + } + break; + case 1: + String killer; + if ((killer = wardKillersByWardClass.get(getWardTargetName(e.getDtClass().getDtName())).poll()) != null) { + if (evKilled != null) { + evKilled.raise(e, killer); + } + } else { + if (evExpired != null) { + evExpired.raise(e); + } + } + break; + } + } + + currentLifeState.put(e.getIndex(), newState); + } + + private String getWardTargetName(String ward_dtclass_name) { + return WARDS_TARGET_NAME_BY_DT_CLASS.get(ward_dtclass_name); + } +} +