private void SetAgentAwareTimes(LogProcessorState state) { foreach (var ev in state.Events) { if (ev is AgentEvent agentEvent) { if (agentEvent.Agent == null) { continue; } if (agentEvent.Agent.FirstAwareTime == 0 || ev.Time < agentEvent.Agent.FirstAwareTime) { agentEvent.Agent.FirstAwareTime = ev.Time; } if (agentEvent.Agent.LastAwareTime == long.MaxValue || ev.Time > agentEvent.Agent.LastAwareTime) { agentEvent.Agent.LastAwareTime = ev.Time; } } else if (ev is DamageEvent damageEvent) { if (damageEvent.Attacker != null) { if (damageEvent.Attacker.FirstAwareTime == 0 || ev.Time < damageEvent.Attacker.FirstAwareTime) { damageEvent.Attacker.FirstAwareTime = ev.Time; } if (damageEvent.Attacker.LastAwareTime == long.MaxValue || ev.Time > damageEvent.Attacker.LastAwareTime) { damageEvent.Attacker.LastAwareTime = ev.Time; } } if (damageEvent.Defender != null) { if (damageEvent.Defender.FirstAwareTime == 0 || ev.Time < damageEvent.Defender.FirstAwareTime) { damageEvent.Defender.FirstAwareTime = ev.Time; } if (damageEvent.Defender.LastAwareTime == long.MaxValue || ev.Time > damageEvent.Defender.LastAwareTime) { damageEvent.Defender.LastAwareTime = ev.Time; } } } } state.AwareTimesSet = true; }
private Event GetEvent(LogProcessorState state, IReadOnlyDictionary <uint, Skill> skillsById, ParsedCombatItem item) { Debug.Assert(state.AgentsByAddress != null); Agent GetAgentByAddress(ulong address) { if (state.AgentsByAddress.TryGetValue(address, out Agent agent)) { return(agent); } return(null); } Skill GetSkillById(uint id) { if (skillsById.TryGetValue(id, out Skill skill)) { return(skill); } return(null); } if (item.IsStateChange != StateChange.Normal) { switch (item.IsStateChange) { case StateChange.EnterCombat: return(new AgentEnterCombatEvent(item.Time, GetAgentByAddress(item.SrcAgent), (int)item.DstAgent)); case StateChange.ExitCombat: return(new AgentExitCombatEvent(item.Time, GetAgentByAddress(item.SrcAgent))); case StateChange.ChangeUp: return(new AgentRevivedEvent(item.Time, GetAgentByAddress(item.SrcAgent))); case StateChange.ChangeDead: return(new AgentDeadEvent(item.Time, GetAgentByAddress(item.SrcAgent))); case StateChange.ChangeDown: return(new AgentDownedEvent(item.Time, GetAgentByAddress(item.SrcAgent))); case StateChange.Spawn: return(new AgentSpawnEvent(item.Time, GetAgentByAddress(item.SrcAgent))); case StateChange.Despawn: return(new AgentDespawnEvent(item.Time, GetAgentByAddress(item.SrcAgent))); case StateChange.HealthUpdate: var healthFraction = item.DstAgent / 10000f; return(new AgentHealthUpdateEvent(item.Time, GetAgentByAddress(item.SrcAgent), healthFraction)); case StateChange.WeaponSwap: WeaponSet newWeaponSet; switch (item.DstAgent) { case 0: newWeaponSet = WeaponSet.Water1; break; case 1: newWeaponSet = WeaponSet.Water2; break; case 4: newWeaponSet = WeaponSet.Land1; break; case 5: newWeaponSet = WeaponSet.Land2; break; default: newWeaponSet = WeaponSet.Unknown; break; } return(new AgentWeaponSwapEvent(item.Time, GetAgentByAddress(item.SrcAgent), newWeaponSet)); case StateChange.MaxHealthUpdate: return(new AgentMaxHealthUpdateEvent(item.Time, GetAgentByAddress(item.SrcAgent), item.DstAgent)); case StateChange.Reward: return(new RewardEvent(item.Time, item.DstAgent, item.Value)); case StateChange.BuffInitial: return(new InitialBuffEvent(item.Time, GetAgentByAddress(item.SrcAgent), GetSkillById(item.SkillId))); case StateChange.Position: { float x = BitConversions.ToSingle((uint)(item.DstAgent & 0xFFFFFFFF)); float y = BitConversions.ToSingle((uint)(item.DstAgent >> 32 & 0xFFFFFFFF)); float z = BitConversions.ToSingle(item.Value); return(new PositionChangeEvent(item.Time, GetAgentByAddress(item.SrcAgent), x, y, z)); } case StateChange.Velocity: { float x = BitConversions.ToSingle((uint)(item.DstAgent & 0xFFFFFFFF)); float y = BitConversions.ToSingle((uint)(item.DstAgent >> 32 & 0xFFFFFFFF)); float z = BitConversions.ToSingle(item.Value); return(new VelocityChangeEvent(item.Time, GetAgentByAddress(item.SrcAgent), x, y, z)); } case StateChange.Rotation: { float x = BitConversions.ToSingle((uint)(item.DstAgent & 0xFFFFFFFF)); float y = BitConversions.ToSingle((uint)(item.DstAgent >> 32 & 0xFFFFFFFF)); return(new FacingChangeEvent(item.Time, GetAgentByAddress(item.SrcAgent), x, y)); } case StateChange.TeamChange: return(new TeamChangeEvent(item.Time, GetAgentByAddress(item.SrcAgent), item.DstAgent)); case StateChange.Targetable: { var agent = GetAgentByAddress(item.SrcAgent); if (agent is AttackTarget target) { return(new TargetableChangeEvent(item.Time, target, item.DstAgent != 0)); } else { return(new UnknownEvent(item.Time, item)); } } case StateChange.ReplInfo: return(new UnknownEvent(item.Time, item)); case StateChange.StackActive: return(new ActiveBuffStackEvent(item.Time, GetAgentByAddress(item.SrcAgent), (uint)item.DstAgent)); case StateChange.StackReset: return(new ResetBuffStackEvent(item.Time, GetAgentByAddress(item.SrcAgent), item.Padding, item.Value)); case StateChange.BreakbarState: var breakbarState = item.Value switch { 0 => DefianceBarStateUpdateEvent.DefianceBarState.Active, 1 => DefianceBarStateUpdateEvent.DefianceBarState.Recovering, 2 => DefianceBarStateUpdateEvent.DefianceBarState.Immune, 3 => DefianceBarStateUpdateEvent.DefianceBarState.None, _ => DefianceBarStateUpdateEvent.DefianceBarState.Unknown }; return(new DefianceBarStateUpdateEvent(item.Time, GetAgentByAddress(item.SrcAgent), breakbarState)); case StateChange.BreakbarPercent: // This encoding is inconsistent with the health update. float breakbarHealthFraction = BitConversions.ToSingle(item.Value); return(new DefianceBarHealthUpdateEvent(item.Time, GetAgentByAddress(item.SrcAgent), breakbarHealthFraction)); case StateChange.BuffInfo: // TODO: Figure out what the contents are case StateChange.BuffFormula: // TODO: Figure out what the contents are case StateChange.SkillInfo: // TODO: Figure out what the contents are case StateChange.SkillTiming: // TODO: Figure out what the contents are case StateChange.Error: // TODO: Implement return(new UnknownEvent(item.Time, item)); case StateChange.Tag: return(new AgentTagEvent(item.Time, GetAgentByAddress(item.SrcAgent), item.Value)); case StateChange.Unknown: return(new UnknownEvent(item.Time, item)); default: throw new ArgumentOutOfRangeException(); } } else if (item.IsActivation != Activation.None) { switch (item.IsActivation) { case Activation.CancelCancel: return(new EndSkillCastEvent(item.Time, GetAgentByAddress(item.SrcAgent), GetSkillById(item.SkillId), item.Value, EndSkillCastEvent.SkillEndType.Cancel)); case Activation.CancelFire: return(new EndSkillCastEvent(item.Time, GetAgentByAddress(item.SrcAgent), GetSkillById(item.SkillId), item.Value, EndSkillCastEvent.SkillEndType.Fire)); case Activation.Normal: return(new StartSkillCastEvent(item.Time, GetAgentByAddress(item.SrcAgent), GetSkillById(item.SkillId), item.Value, StartSkillCastEvent.SkillCastType.Normal)); case Activation.Quickness: return(new StartSkillCastEvent(item.Time, GetAgentByAddress(item.SrcAgent), GetSkillById(item.SkillId), item.Value, StartSkillCastEvent.SkillCastType.WithQuickness)); case Activation.Reset: return(new ResetSkillCastEvent(item.Time, GetAgentByAddress(item.SrcAgent), GetSkillById(item.SkillId), item.Value)); case Activation.Unknown: return(new UnknownEvent(item.Time, item)); } } else if (item.Buff != 0 && item.IsBuffRemove != BuffRemove.None) { Skill buff = GetSkillById(item.SkillId); int remainingDuration = item.Value; int remainingIntensity = item.BuffDmg; int stacksRemoved = (int)item.Result; var cleansingAgent = GetAgentByAddress(item.DstAgent); var agent = GetAgentByAddress(item.SrcAgent); switch (item.IsBuffRemove) { case BuffRemove.All: return(new AllStacksRemovedBuffEvent(item.Time, agent, buff, cleansingAgent, stacksRemoved)); case BuffRemove.Single: uint stackId = item.Padding; return(new SingleStackRemovedBuffEvent(item.Time, agent, buff, cleansingAgent, remainingDuration, remainingIntensity, stackId)); case BuffRemove.Manual: return(new ManualStackRemovedBuffEvent(item.Time, agent, buff, cleansingAgent, remainingDuration, remainingIntensity)); default: throw new ArgumentOutOfRangeException(); } } else if (item.Buff > 0 && item.BuffDmg == 0) { Skill buff = GetSkillById(item.SkillId); int durationApplied = item.Value; uint durationOfRemovedStack = item.OverstackValue; var agent = GetAgentByAddress(item.DstAgent); var sourceAgent = GetAgentByAddress(item.SrcAgent); return(new BuffApplyEvent(item.Time, agent, buff, sourceAgent, durationApplied, durationOfRemovedStack)); } else if (item.Buff > 0 && item.Value == 0) { Skill buff = GetSkillById(item.SkillId); int buffDamage = item.BuffDmg; bool isOffCycle = item.IsOffCycle > 0; Agent attacker = GetAgentByAddress(item.SrcAgent); Agent defender = GetAgentByAddress(item.DstAgent); bool isMoving = item.IsMoving > 0; bool isNinety = item.IsNinety > 0; bool isFlanking = item.IsFlanking > 0; bool isIgnored = item.Result != 0; if (isIgnored) { var reason = item.Result == (Result)1 ? IgnoredBuffDamageEvent.Reason.InvulnerableBuff : IgnoredBuffDamageEvent.Reason.InvulnerableSkill; return(new IgnoredBuffDamageEvent(item.Time, attacker, defender, buff, buffDamage, isMoving, isNinety, isFlanking, reason)); } else { if (isOffCycle) { return(new OffCycleBuffDamageEvent(item.Time, attacker, defender, buff, buffDamage, isMoving, isNinety, isFlanking)); } else { return(new BuffDamageEvent(item.Time, attacker, defender, buff, buffDamage, isMoving, isNinety, isFlanking)); } } } else if (item.Buff == 0) { int damage = item.Value; uint shieldDamage = item.OverstackValue; Agent attacker = GetAgentByAddress(item.SrcAgent); Agent defender = GetAgentByAddress(item.DstAgent); Skill skill = GetSkillById(item.SkillId); bool isMoving = item.IsMoving > 0; bool isNinety = item.IsNinety > 0; bool isFlanking = item.IsFlanking > 0; // TODO: Rewrite bool ignored = false; var hitResult = PhysicalDamageEvent.Result.Normal; var ignoreReason = IgnoredPhysicalDamageEvent.Reason.Absorbed; switch (item.Result) { case Result.Normal: hitResult = PhysicalDamageEvent.Result.Normal; break; case Result.Critical: hitResult = PhysicalDamageEvent.Result.Critical; break; case Result.Glance: hitResult = PhysicalDamageEvent.Result.Glance; break; case Result.Block: ignored = true; ignoreReason = IgnoredPhysicalDamageEvent.Reason.Blocked; break; case Result.Evade: ignored = true; ignoreReason = IgnoredPhysicalDamageEvent.Reason.Evaded; break; case Result.Interrupt: hitResult = PhysicalDamageEvent.Result.Interrupt; break; case Result.Absorb: ignored = true; ignoreReason = IgnoredPhysicalDamageEvent.Reason.Absorbed; break; case Result.Blind: ignored = true; ignoreReason = IgnoredPhysicalDamageEvent.Reason.Missed; break; case Result.KillingBlow: hitResult = PhysicalDamageEvent.Result.KillingBlow; break; case Result.Downed: hitResult = PhysicalDamageEvent.Result.DowningBlow; break; case Result.Unknown: return(new UnknownEvent(item.Time, item)); default: return(new UnknownEvent(item.Time, item)); } if (!ignored) { return(new PhysicalDamageEvent(item.Time, attacker, defender, skill, damage, isMoving, isNinety, isFlanking, shieldDamage, hitResult)); } else { return(new IgnoredPhysicalDamageEvent(item.Time, attacker, defender, skill, damage, isMoving, isNinety, isFlanking, shieldDamage, ignoreReason)); } } return(new UnknownEvent(item.Time, item)); }
public Log ProcessLog(ParsedLog log) { var context = new LogProcessorState(); context.EvtcVersion = log.LogVersion.BuildVersion; context.Agents = GetAgents(log).ToList(); context.AgentsByAddress = new Dictionary <ulong, Agent>(); context.AgentsById = new Dictionary <int, List <Agent> >(); foreach (var agent in context.Agents) { foreach (var origin in agent.AgentOrigin.OriginalAgentData) { context.AgentsByAddress[origin.Address] = agent; int id = origin.Id; if (!context.AgentsById.TryGetValue(id, out var agentsWithId)) { agentsWithId = new List <Agent>(); context.AgentsById[id] = agentsWithId; } agentsWithId.Add(agent); } } context.Skills = GetSkills(log).ToList(); GetDataFromCombatItems(log, context); Agent mainTarget = null; context.LogType = LogType.PvE; if (log.ParsedBossData.ID == 1) { context.LogType = LogType.WorldVersusWorld; } else { // The boss id be either an NPC species id or a Gadget id. // Conflicts may happen, in that case the first found agent is chosen, // as they are more likely to be the trigger. It is also possible to have // multiple agents with the same id if they are not unique, in that case // we once again choose the first one. foreach (var agent in context.Agents) { if (agent is NPC npc && npc.SpeciesId == log.ParsedBossData.ID || agent is Gadget gadget && gadget.VolatileId == log.ParsedBossData.ID) { mainTarget = agent; break; } } } SetAgentAwareTimes(context); AssignAgentMasters(log, context); // Has to be done after setting aware times context.EncounterData = GetEncounterData(mainTarget, context); foreach (var step in context.EncounterData.ProcessingSteps) { step.Process(context); } return(new Log(mainTarget, context)); }
private void GetDataFromCombatItems(ParsedLog log, LogProcessorState state) { Debug.Assert(state.Agents != null); Debug.Assert(state.Skills != null); Debug.Assert(state.AgentsByAddress != null); var skillsById = new Dictionary <uint, Skill>(); foreach (var skill in state.Skills) { // Rarely, in old logs a skill may be duplicated (typically a skill with id 0), // so we only use the first definition if (!skillsById.ContainsKey(skill.Id)) { skillsById.Add(skill.Id, skill); } } state.GameLanguage = GameLanguage.Other; var events = new List <Event>(); foreach (var item in log.ParsedCombatItems) { if (item.IsStateChange == StateChange.LogStart) { if (state.LogStartTime != null) { throw new LogProcessingException("Multiple log start combat items found"); } var serverTime = DateTimeOffset.FromUnixTimeSeconds(item.Value); var localTime = DateTimeOffset.FromUnixTimeSeconds(item.BuffDmg); state.LogStartTime = new LogTime(localTime, serverTime, item.Time); continue; } if (item.IsStateChange == StateChange.LogEnd) { if (item.Value == 0 && item.BuffDmg == 0) { // This is an erroneous extra log end without any data, // we ignore it. Happened in every log with EVTC20200506. continue; } if (state.LogEndTime != null) { throw new LogProcessingException("Multiple log end combat items found"); } var serverTime = DateTimeOffset.FromUnixTimeSeconds(item.Value); var localTime = DateTimeOffset.FromUnixTimeSeconds(item.BuffDmg); state.LogEndTime = new LogTime(localTime, serverTime, item.Time); continue; } if (item.IsStateChange == StateChange.PointOfView) { if (state.AgentsByAddress.TryGetValue(item.SrcAgent, out var agent)) { state.PointOfView = agent as Player ?? throw new LogProcessingException("The point of view agent is not a player"); } continue; } if (item.IsStateChange == StateChange.Language) { int languageId = (int)item.SrcAgent; state.GameLanguageId = languageId; state.GameLanguage = GameLanguageIds.GetLanguageById(languageId); continue; } if (item.IsStateChange == StateChange.GWBuild) { state.GameBuild = (int)item.SrcAgent; continue; } if (item.IsStateChange == StateChange.ShardId) { state.GameShardId = (int)item.SrcAgent; continue; } if (item.IsStateChange == StateChange.MapId) { state.MapId = (int)item.SrcAgent; continue; } if (item.IsStateChange == StateChange.Guild) { if (state.AgentsByAddress.TryGetValue(item.SrcAgent, out Agent agent)) { var player = (Player)agent; var guid = new byte[16]; // It is unclear how the arcdps values would be stored on a big-endian platform Debug.Assert(BitConverter.IsLittleEndian); var dstBytes = BitConverter.GetBytes(item.DstAgent); var valueBytes = BitConverter.GetBytes(item.Value); var buffDamageBytes = BitConverter.GetBytes(item.BuffDmg); guid[0] = dstBytes[3]; guid[1] = dstBytes[2]; guid[2] = dstBytes[1]; guid[3] = dstBytes[0]; guid[4] = dstBytes[5]; guid[5] = dstBytes[4]; guid[6] = dstBytes[7]; guid[7] = dstBytes[6]; guid[8] = valueBytes[0]; guid[9] = valueBytes[1]; guid[10] = valueBytes[2]; guid[11] = valueBytes[3]; guid[12] = buffDamageBytes[0]; guid[13] = buffDamageBytes[1]; guid[14] = buffDamageBytes[2]; guid[15] = buffDamageBytes[3]; player.GuildGuid = guid; } continue; } if (item.IsStateChange == StateChange.AttackTarget) { // Only used for master assignment // Contains if the attack target is targetable as the value. continue; } var processedEvent = GetEvent(state, skillsById, item); if (!(processedEvent is UnknownEvent) || !IgnoreUnknownEvents) { events.Add(processedEvent); } } state.Events = events; }
private void AssignAgentMasters(ParsedLog log, LogProcessorState state) { // Requires aware times to be set first Debug.Assert(state.AwareTimesSet); Debug.Assert(!state.MastersAssigned); Debug.Assert(state.AgentsByAddress != null); Debug.Assert(state.AgentsById != null); foreach (var combatItem in log.ParsedCombatItems) { if (combatItem.IsStateChange == StateChange.Normal) { if (combatItem.SrcMasterId != 0) { Agent minion = null; if (state.AgentsById.TryGetValue(combatItem.SrcAgentId, out var agentsWithId)) { foreach (var agent in agentsWithId) { if (agent.IsWithinAwareTime(combatItem.Time)) { minion = agent; break; } } } if (minion != null && minion.Master == null) { Agent master = null; if (state.AgentsById.TryGetValue(combatItem.SrcMasterId, out var potentialMasters)) { foreach (var agent in potentialMasters) { if (!(agent is Gadget) && agent.IsWithinAwareTime(combatItem.Time)) { master = agent; break; } } } if (master != null) { bool inCycle = false; var masterParent = master; while (masterParent != null) { if (masterParent == minion) { // A cycle is present in the minion hierarchy, this minion would end up as // a transitive minion of itself, which could cause infinite looping. // This is common in very old logs where minion data is somewhat weird. inCycle = true; break; } masterParent = masterParent.Master; } if (!inCycle) { minion.Master = master; master.MinionList.Add(minion); } } } } } if (combatItem.IsStateChange == StateChange.AttackTarget) { ulong attackTargetAddress = combatItem.SrcAgent; ulong masterGadgetAddress = combatItem.DstAgent; AttackTarget target = null; Gadget master = null; foreach (var agent in state.Agents) { switch (agent) { case Gadget gadget when gadget.AgentOrigin.OriginalAgentData.Any(x => x.Address == masterGadgetAddress): master = gadget; break; case AttackTarget attackTarget when attackTarget.AgentOrigin.OriginalAgentData.Any(x => x.Address == attackTargetAddress): target = attackTarget; break; } } if (master != null && target != null) { master.AddAttackTarget(target); target.Gadget = master; } } } state.MastersAssigned = true; }
private IEncounterData GetEncounterData(Agent mainTarget, LogProcessorState state) { return(EncounterIdentifier.GetEncounterData(mainTarget, state.Events, state.Agents, state.Skills, state.GameBuild, state.LogType)); }