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<String, String> WARDS_TARGET_NAME_BY_DT_CLASS; private static final Set<String> WARDS_DT_CLASSES; private static final Set<String> WARDS_TARGET_NAMES; static { final String TARGET_NAME_OBSERVER = "npc_dota_observer_wards"; final String TARGET_NAME_SENTRY = "npc_dota_sentry_wards"; HashMap<String, String> 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<Integer, FieldPath> lifeStatePaths = new HashMap<>(); private final Map<Integer, Integer> currentLifeState = new HashMap<>(); private final Map<String, Queue<String>> wardKillersByWardClass = new HashMap<>(); private Queue<ProcessEntityCommand> toProcess = new ArrayDeque<>(); private Event<OnWardKilled> evKilled; private Event<OnWardExpired> evExpired; private Event<OnWardPlaced> 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<OnWardKilled> listener) { evKilled = ctx.createEvent(OnWardKilled.class, Entity.class, String.class); } @Initializer(OnWardExpired.class) public void initOnWardExpired(final Context ctx, final EventListener<OnWardExpired> listener) { evExpired = ctx.createEvent(OnWardExpired.class, Entity.class); } @Initializer(OnWardPlaced.class) public void initOnWardPlaced(final Context ctx, final EventListener<OnWardPlaced> 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); } }