<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { Link, withRouter } from 'react-router-dom';
import { Trans, t } from '@lingui/macro';

import SPECS from 'game/SPECS';
import ROLES from 'game/ROLES';
import getAverageItemLevel from 'game/getAverageItemLevel';
import getFightName from 'common/getFightName';
import { fetchCombatants, LogNotFoundError } from 'common/fetchWclApi';
import { captureException } from 'common/errorLogger';
import { i18n } from 'interface/RootLocalizationProvider';
import ActivityIndicator from 'interface/common/ActivityIndicator';
import DocumentTitle from 'interface/common/DocumentTitle';
import { setCombatants } from 'interface/actions/combatants';
import { getPlayerId, getPlayerName } from 'interface/selectors/url/report';
import makeAnalyzerUrl from 'interface/common/makeAnalyzerUrl';
import Tooltip from 'common/Tooltip';
import PlayerSelection from 'interface/report/PlayerSelection';
import RaidCompositionDetails from 'interface/report/RaidCompositionDetails';
import { fetchCharacter } from 'interface/actions/characters';
import handleApiError from './handleApiError';

const defaultState = {
  error: null,
  combatants: null,
  combatantsFightId: null,
};

class PlayerLoader extends React.PureComponent {
  tanks = 0;
  healers = 0;
  dps = 0;
  ranged = 0;
  ilvl = 0;
  heartLvl = 0;
  static propTypes = {
    report: PropTypes.shape({
      code: PropTypes.string.isRequired,
      title: PropTypes.string.isRequired,
      friendlies: PropTypes.arrayOf(PropTypes.shape({
        id: PropTypes.number.isRequired,
        type: PropTypes.string.isRequired,
        name: PropTypes.string.isRequired,
      })),
    }).isRequired,
    fight: PropTypes.shape({
      id: PropTypes.number.isRequired,
      start_time: PropTypes.number.isRequired,
      end_time: PropTypes.number.isRequired,
    }).isRequired,
    setCombatants: PropTypes.func.isRequired,
    playerName: PropTypes.string,
    playerId: PropTypes.number,
    children: PropTypes.func.isRequired,
    history: PropTypes.shape({
      push: PropTypes.func.isRequired,
      replace: PropTypes.func.isRequired,
    }).isRequired,
    fetchCharacter: PropTypes.func.isRequired,
  };
  static getDerivedStateFromProps(props, state) {
    if (props.fight.id !== state.combatantsFightId) {
      // When switching fights we need to unset combatants before rendering to avoid children from doing API calls twice
      return defaultState;
    }
    return state;
  }
  state = defaultState;

  componentDidMount() {
    // noinspection JSIgnoredPromiseFromCall
    this.loadCombatants(this.props.report, this.props.fight);
    this.scrollToTop();
  }
  componentDidUpdate(prevProps, prevState, prevContext) {
    const changedReport = this.props.report !== prevProps.report;
    const changedFight = this.props.fight !== prevProps.fight;
    if (changedReport || changedFight) {
      // noinspection JSIgnoredPromiseFromCall
      this.loadCombatants(this.props.report, this.props.fight);
    }
    if (this.props.playerId && (!this.props.playerName || this.props.playerName === `${this.props.playerId}`)) {
      this.props.history.replace(makeAnalyzerUrl(this.props.report, this.props.fight.id, this.props.playerId));
    }
  }

  async loadCombatants(report, fight) {
    let numberOfCombatantsWithLoadedHeart = 0;
    try {
      const { fetchCharacter } = this.props;
      const combatants = await fetchCombatants(report.code, fight.start_time, fight.end_time);
      const characterDataPromises = combatants.map(player => {
        const friendly = report.friendlies.find(friendly => friendly.id === player.sourceID);
        const exportedCharacter = report.exportedCharacters ? report.exportedCharacters.find(char => char.name === friendly.name) : null;
        if (!exportedCharacter) {
          return Promise.resolve();
        }
        return fetchCharacter(friendly.guid, exportedCharacter.region, friendly.realm, friendly.name).then(data => {
          return Promise.resolve(data);
        }).catch(err => {
          // This guy failed to load - this is nice to have data
          // We can ignore this and we'll just drop him from the overall averages later
          return Promise.resolve();
        });
      });
      let characterDatas = await Promise.all(characterDataPromises);
      // Filter for only loaded characterDatas
      characterDatas = characterDatas.filter(value => value);
      combatants.forEach(player => {
        if (player.error || player.specID === -1) {
          return;
        }
        const friendly = report.friendlies.find(friendly => friendly.id === player.sourceID);
        const characterData = characterDatas ? characterDatas.find(data => data.id === friendly.guid) : null;
        switch (SPECS[player.specID].role) {
          case ROLES.TANK:
            this.tanks += 1;
            break;
          case ROLES.HEALER:
            this.healers += 1;
            break;
          case ROLES.DPS.MELEE:
            this.dps += 1;
            break;
          case ROLES.DPS.RANGED:
            this.ranged += 1;
            break;
          default: break;
        }
        // Gear may be null for broken combatants
        this.ilvl += player.gear ? getAverageItemLevel(player.gear) : 0;
        if (characterData && characterData.heartOfAzeroth) {
          numberOfCombatantsWithLoadedHeart++;
          this.heartLvl += characterData.heartOfAzeroth.azeriteItemLevel;
        }
      });
      this.ilvl /= combatants.length;
      if (numberOfCombatantsWithLoadedHeart > 0) {
        this.heartLvl /= numberOfCombatantsWithLoadedHeart;
      }
      if (this.props.report !== report || this.props.fight !== fight) {
        return; // the user switched report/fight already
      }<commit_msg>fix crash in player loader when a friendly is missing<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { Link, withRouter } from 'react-router-dom';
import { Trans, t } from '@lingui/macro';

import SPECS from 'game/SPECS';
import ROLES from 'game/ROLES';
import getAverageItemLevel from 'game/getAverageItemLevel';
import getFightName from 'common/getFightName';
import { fetchCombatants, LogNotFoundError } from 'common/fetchWclApi';
import { captureException } from 'common/errorLogger';
import { i18n } from 'interface/RootLocalizationProvider';
import ActivityIndicator from 'interface/common/ActivityIndicator';
import DocumentTitle from 'interface/common/DocumentTitle';
import { setCombatants } from 'interface/actions/combatants';
import { getPlayerId, getPlayerName } from 'interface/selectors/url/report';
import makeAnalyzerUrl from 'interface/common/makeAnalyzerUrl';
import Tooltip from 'common/Tooltip';
import PlayerSelection from 'interface/report/PlayerSelection';
import RaidCompositionDetails from 'interface/report/RaidCompositionDetails';
import { fetchCharacter } from 'interface/actions/characters';
import handleApiError from './handleApiError';

const defaultState = {
  error: null,
  combatants: null,
  combatantsFightId: null,
};

class PlayerLoader extends React.PureComponent {
  tanks = 0;
  healers = 0;
  dps = 0;
  ranged = 0;
  ilvl = 0;
  heartLvl = 0;
  static propTypes = {
    report: PropTypes.shape({
      code: PropTypes.string.isRequired,
      title: PropTypes.string.isRequired,
      friendlies: PropTypes.arrayOf(PropTypes.shape({
        id: PropTypes.number.isRequired,
        type: PropTypes.string.isRequired,
        name: PropTypes.string.isRequired,
      })),
    }).isRequired,
    fight: PropTypes.shape({
      id: PropTypes.number.isRequired,
      start_time: PropTypes.number.isRequired,
      end_time: PropTypes.number.isRequired,
    }).isRequired,
    setCombatants: PropTypes.func.isRequired,
    playerName: PropTypes.string,
    playerId: PropTypes.number,
    children: PropTypes.func.isRequired,
    history: PropTypes.shape({
      push: PropTypes.func.isRequired,
      replace: PropTypes.func.isRequired,
    }).isRequired,
    fetchCharacter: PropTypes.func.isRequired,
  };
  static getDerivedStateFromProps(props, state) {
    if (props.fight.id !== state.combatantsFightId) {
      // When switching fights we need to unset combatants before rendering to avoid children from doing API calls twice
      return defaultState;
    }
    return state;
  }
  state = defaultState;

  componentDidMount() {
    // noinspection JSIgnoredPromiseFromCall
    this.loadCombatants(this.props.report, this.props.fight);
    this.scrollToTop();
  }
  componentDidUpdate(prevProps, prevState, prevContext) {
    const changedReport = this.props.report !== prevProps.report;
    const changedFight = this.props.fight !== prevProps.fight;
    if (changedReport || changedFight) {
      // noinspection JSIgnoredPromiseFromCall
      this.loadCombatants(this.props.report, this.props.fight);
    }
    if (this.props.playerId && (!this.props.playerName || this.props.playerName === `${this.props.playerId}`)) {
      this.props.history.replace(makeAnalyzerUrl(this.props.report, this.props.fight.id, this.props.playerId));
    }
  }

  async loadCombatants(report, fight) {
    let numberOfCombatantsWithLoadedHeart = 0;
    try {
      const { fetchCharacter } = this.props;
      const combatants = await fetchCombatants(report.code, fight.start_time, fight.end_time);
      const characterDataPromises = combatants.map(player => {
        const friendly = report.friendlies.find(friendly => friendly.id === player.sourceID);
        if(!friendly) {
          // unsure why this happens, but it can
          return Promise.resolve();
        }
        const exportedCharacter = report.exportedCharacters ? report.exportedCharacters.find(char => char.name === friendly.name) : null;
        if (!exportedCharacter) {
          return Promise.resolve();
        }
        return fetchCharacter(friendly.guid, exportedCharacter.region, friendly.realm, friendly.name).then(data => {
          return Promise.resolve(data);
        }).catch(err => {
          // This guy failed to load - this is nice to have data
          // We can ignore this and we'll just drop him from the overall averages later
          return Promise.resolve();
        });
      });
      let characterDatas = await Promise.all(characterDataPromises);
      // Filter for only loaded characterDatas
      characterDatas = characterDatas.filter(value => value);
      combatants.forEach(player => {
        if (player.error || player.specID === -1) {
          return;
        }
        const friendly = report.friendlies.find(friendly => friendly.id === player.sourceID);
        if(!friendly) {
          console.error("friendly missing from report for player", player.sourceID);
          return;
        }
        const characterData = characterDatas ? characterDatas.find(data => data.id === friendly.guid) : null;
        switch (SPECS[player.specID].role) {
          case ROLES.TANK:
            this.tanks += 1;
            break;
          case ROLES.HEALER:
            this.healers += 1;
            break;
          case ROLES.DPS.MELEE:
            this.dps += 1;
            break;
          case ROLES.DPS.RANGED:
            this.ranged += 1;
            break;
          default: break;
        }
        // Gear may be null for broken combatants
        this.ilvl += player.gear ? getAverageItemLevel(player.gear) : 0;
        if (characterData && characterData.heartOfAzeroth) {
          numberOfCombatantsWithLoadedHeart++;
          this.heartLvl += characterData.heartOfAzeroth.azeriteItemLevel;
        }
      });
      this.ilvl /= combatants.length;
      if (numberOfCombatantsWithLoadedHeart > 0) {
        this.heartLvl /= numberOfCombatantsWithLoadedHeart;
      }
      if (this.props.report !== report || this.props.fight !== fight) {
        return; // the user switched report/fight already
      }
{'fraction_non_alphanumeric': 0.10860553805339082, 'fraction_numerical': 0.002652959708174432, 'mean_word_length': 3.096095076400679, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 23, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2348193', 'n_tokens_mistral': 3457, 'n_tokens_neox': 3442, 'n_words': 796}