/// <summary> /// Parses combat related data /// </summary> private IEnumerable <ParsedCombatItem> ParseCombatItems(int revision, ByteArrayBinaryReader reader) { switch (revision) { case 0: // 64 bytes: each combat item while (reader.Length - reader.Position >= 64) { ParsedCombatItem combatItem = ReadCombatItemRevision0(reader); yield return(combatItem); } break; case 1: // 64 bytes: each combat item while (reader.Length - reader.Position >= 64) { ParsedCombatItem combatItem = ReadCombatItemRevision1(reader); yield return(combatItem); } break; default: throw new NotSupportedException("Only EVTC revisions 0 and 1 are supported."); } }
/// <summary> /// Update the names of players in order to hide their identity. Also removes guild data. /// </summary> /// <param name="log">The log data that will be updated.</param> /// <exception cref="NotSupportedException">Thrown if the log is not a supported revision.</exception> public void AnonymizePlayers(ParsedLog log) { EnsureRevisionIsSupported(log.LogVersion); int playerIndex = 1; for (int i = 0; i < log.ParsedAgents.Count; i++) { var agent = log.ParsedAgents[i]; if (agent.IsElite == 0xFFFFFFFF) { // This agent is not a player continue; } // The subgroup is encoded within the name, so we need to reconstruct this. // It is also possible that some parts may be missing in case the players in // the log are enemies, in that case we maintain that. var nameParts = agent.Name.Split(new[] { '\0' }, StringSplitOptions.RemoveEmptyEntries); string characterName = nameParts[0]; string accountName = nameParts.Length > 1 ? nameParts[1] : null; string subgroupLiteral = nameParts.Length > 2 ? nameParts[2] : null; if (accountName != null) { accountName = $":Anonymous.{playerIndex:0000}"; } characterName = $"Player {playerIndex}"; string updatedName = $"{characterName}\0{accountName ?? ""}\0{subgroupLiteral ?? ""}"; var updatedAgent = new ParsedAgent(agent.Address, updatedName, agent.Prof, agent.IsElite, agent.Toughness, agent.Concentration, agent.Healing, agent.Condition, agent.HitboxWidth, agent.HitboxHeight); log.ParsedAgents[i] = updatedAgent; playerIndex++; } for (int i = 0; i < log.ParsedCombatItems.Count; i++) { var item = log.ParsedCombatItems[i]; if (item.IsStateChange != StateChange.Guild) { continue; } // We cannot just remove the events as that would break expectations for present for a specific log version. // Instead we set the guild guid to zero, which corresponds to having no guild. // dst, value and buffdmg have to be zeroed var updatedItem = new ParsedCombatItem(item.Time, item.SrcAgent, 0, 0, 0, item.OverstackValue, item.SkillId, item.SrcAgentId, item.DstAgentId, item.SrcMasterId, item.DstMasterId, item.Iff, item.Buff, item.Result, item.IsActivation, item.IsBuffRemove, item.IsNinety, item.IsFifty, item.IsMoving, item.IsStateChange, item.IsFlanking, item.IsShields, item.IsOffCycle, item.Padding); log.ParsedCombatItems[i] = updatedItem; } }
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)); }