R/parse.R

#' Parse a DCUO combat log.
#'
#' @param log Character vector of log lines.
#' @param file Name of a log file to read from.
#' @param time_digits Number of decimal places to use for time (seconds).
#' @param type One of "combat", "summary", "crowd_control", "knockout", "dodge" (combat by default).
#' @return Table of parsed combat log entries.
#' @export
parse_dcuo <- function(log, file = NULL, time_digits = 3,
                       type = c("combat", "summary", "crowd_control", "knockout", "dodge")) {

  event_type <- match.arg(type)
  parse_fun <- get(paste0("parse_", event_type))
  parse_fun(log, file = file, time_digits = time_digits)
}

#' Parse general combat events: damage, heal, power, absorb, supercharge.
#'
#' @param log Character vector of log lines.
#' @param file Name of a log file to read from.
#' @param time_digits Number of decimal places to use for time (seconds).
#' @return A `combat_events` object.
#' @export
parse_combat <- function(log, file = NULL, time_digits = 3) {

  pattern <- paste0(
    "^(?<time>[0-9]{16}) ",
    "\\[(?<type>Damage|Healing|Power|Supercharge|Combat) (?>In|Out)\\] ",
    "(?<source>[^']+)'s? ",
    "(?<ability>.+?) ",
    "(?<crit>critically )?",
    "(?:(?:damag|heal)ed (?<target>.+) for|absorbed) ",
    "(?<value>[0-9]+)",
    "(?: Power| Supercharge)?$"
  )

  events <- parse_log(log, pattern, file = file, time_digits = time_digits, class = "combat_events")

  events[type == "Combat", type := "absorb"]
  events[, type := tolower(type)]
  events[, crit := crit != ""]
  setcolorder(events, c("time", "source", "target", "ability", "value", "type", "crit"))

  # Fix names that were capitalized at the beginning of the line
  fix_cap_names(events)

  # Fix incorrect attribution of abilities with aura or chain effects
  fix_attribution(events)

  events[]
}

#' Parse combat parser summary entries.
#'
#' @param log Character vector of log lines.
#' @param file Name of a log file to read from.
#' @param time_digits Number of decimal places to use for time (seconds)
#' @return A `parser_summary` object.
#' @export
parse_summary <- function(log, file = NULL, time_digits = 3) {

  pattern <- paste0(
    "^(?<time>[0-9]{1,16}) ",  # time = 0 bug still exists as of 2/20/17
    "\\[Summary\\] (?<type>Damage|Healing|Power) ",
    "\\[(?<interval>[0-9]+\\.[0-9]+)s\\] ",
    "(?<xps>[0-9]+)/s - ",
    "(?<total>[0-9]+) total - ",
    "(?<hits>[0-9]+) hits? \\((?<max>[0-9]+) max\\) - ",
    "(?<crits>[0-9]+) \\([0-9]+\\.[0-9]+%\\) crits? - ",
    "(?<targets>[0-9]+) targets?$"
  )

  events <- parse_log(log, pattern, file = file, time_digits = time_digits, class = "parser_summary")

  events[, type := tolower(type)]
  events[, crit_pct := round(ifelse(hits > 0, crits/hits, 0), 3)]
  setcolorder(events, c("time", "type", "interval", "xps", "total", "hits", "max",
                        "crits", "crit_pct", "targets"))

  events[]
}

#' Parse crowd control effects, including counters.
#'
#' Effects can be one of the following:
#'   knockdown, juggle, stun, pull, push, ground, encasement, snare, root,
#'   knockback, suppression, counter
#'
#' @param log Character vector of log lines.
#' @param file Name of a log file to read from.
#' @param time_digits Number of decimal places to use for time (seconds).
#' @return A `crowd_control_events` object.
#' @export
parse_crowd_control <- function(log, file = NULL, time_digits = 3) {

  pattern <- paste0(
    "^(?<time>[0-9]{16}) ",
    "\\[Combat (?:In|Out)\\] ",
    "(?<source>.+)'s? ",
    "(?<ability>.+) ",
    "(?<effect>stunned|knocked down|juggled|pulled toward|pushed|grounded|",
    "encased|snared|rooted|knocked back|suppressed) ",
    "(?<target>.+)$"
  )

  events <- parse_log(log, pattern, file = file, time_digits = time_digits, class = "crowd_control_events")

  effects <- c("knocked down"  = "knockdown",
               "juggled"       = "juggle",
               "stunned"       = "stun",
               "pulled toward" = "pull",
               "pushed"        = "push",
               "grounded"      = "grounding",
               "encased"       = "encasement",
               "snared"        = "snare",
               "rooted"        = "root",
               "knocked back"  = "knockback",
               "suppressed"    = "suppression")

  events[, effect := effects[effect]]

  # Only 3 Quantum abilities inflict the actual "suppression" effect - the rest are counters
  suppression_abilities <- c("Time Shift", "Time Bomb", "Temporal Vortex", "Warped Reality")
  events[effect == "suppression" & !ability %in% suppression_abilities, effect := "counter"]

  # Fix names that were capitalized at the beginning of the line
  fix_cap_names(events)

  events[]
}

#' Parse knockouts.
#'
#' @param log Character vector of log lines.
#' @param file Name of a log file to read from.
#' @param time_digits Number of decimal places to use for time (seconds).
#' @return A `knockout_events` object.
#' @export
parse_knockout <- function(log, file = NULL, time_digits = 3) {

  pattern <- "^(?<time>[0-9]{16}) \\[Damage (?:In|Out)\\] (?<source>.+) knocked out (?<target>.+)$"
  events <- parse_log(log, pattern, file = file, time_digits = time_digits, class = "knockout_events")
  matched <- attr(events, "matched")

  # Restoration Barrels don't count as real knockouts
  barrel_knockouts <- events[, target == "Restoration Barrel"]

  if (any(barrel_knockouts)) {
    matched <- matched[!barrel_knockouts]
    events <- events[!barrel_knockouts]
    setattr(events, "matched", matched)
  }

  # Names are properly formatted in knockouts

  events[]
}

#' Parse dodges.
#'
#' @param log Character vector of log lines.
#' @param file Name of a log file to read from.
#' @param time_digits Number of decimal places to use for time (seconds).
#' @return A `dodge_events` object.
#' @export
parse_dodge <- function(log, file = NULL, time_digits = 3) {

  pattern <- "^(?<time>[0-9]{16}) \\[Damage (?:In|Out)\\] (?<source>.+) dodged (?<target>.+)'s? (?<ability>.+)$"
  events <- parse_log(log, pattern, file = file, time_digits = time_digits, class = "dodge_events")

  # Fix names that were capitalized at the beginning of the line
  fix_cap_names(events)

  events[]
}

#' Parse a combat log given a pattern.
#' @keywords internal
parse_log <- function(log, pattern, file = NULL, time_digits = 3, class = "events") {

  if (!is.null(file)) {
    log <- read_lines(file)
  }

  if (!is.character(log)) {
    stop("'log' must be a character vector")
  }

  if (!grepl("?<time>", pattern)) {
    stop("pattern must contain a 'time' group")
  }

  matches <- str_match_named(log, pattern)
  events <- as.data.table(matches)
  setattr(events, "matched", attr(matches, "matched"))
  setattr(events, "class", c(class, class(events)))

  numeric_cols <- get_numeric_groups(pattern)
  if (length(numeric_cols) > 0) {
    events[, (numeric_cols) := lapply(.SD, as.numeric), .SDcols = numeric_cols]
  }

  events[, time := as.POSIXct(round(as.numeric(time)/1e6, time_digits), origin = "1970-01-01")]

  events
}

#' Fix incorrect attribution of abilities with aura or chain reaction effects.
#' @keywords internal
fix_attribution <- function(combat_events) {

  # Reattribute Sparring Target damage if there's only one other damage source in the log.
  sources <- combat_events[type == "damage", unique(source)]
  if (length(sources) == 2 && "Sparring Target" %in% sources) {
    true_source <- sources[sources != "Sparring Target"]
    combat_events[type == "damage" & source == "Sparring Target", source := true_source]
    return(combat_events[])
  }

  # Aura/chain abilities in each powerset
  aura_abilities <- list(
    electricity = c("Electrogenesis", "Voltaic Bolt", "Arc Lightning", "Overcharge"),
    nature = c("Harvest"),
    fire = c("Spontaneous Combustion", "Overheat", "Inferno"),
    gadgets = c("Sticky Bomb"),
    celestial = c("Corrupted Retribution", "Curse", "Cleansed Curse"),
    munitions = c("Chain Grenade Launcher")
  )

  for (powerset in names(aura_abilities)) {
    if (any(aura_abilities[[powerset]] %in% combat_events$ability)) {
      # Most aura attacks show up in the combat log attributed to both the source and the target.
      # Sticky Bomb is probably the only exception here.
      # Eliminate any targets that appear to be damaging themselves.
      aura_dmg_events <- combat_events[type == "damage" & ability %in% aura_abilities[[powerset]]]
      sources <- aura_dmg_events[, unique(source)]
      targets <- aura_dmg_events[, unique(target)]

      # If there's only one remaining source, it's likely to be the true source.
      true_source <- sources[!sources %in% targets]
      if (length(true_source) == 1) {
        combat_events[type == "damage" & ability %in% aura_abilities[[powerset]],
                      source := true_source]
      }
    }
  }

  combat_events[]
}

#' Fix capitalization of source names.
#' @importFrom stats setNames
#' @keywords internal
fix_cap_names <- function(events) {
  # Check the 'target' field for the original capitalization
  targets <- unique(events$target)
  lowercase_names <- targets[substr(targets, 0, 1) %in% base::letters]
  cap_names <- paste0(toupper(substr(lowercase_names, 0, 1)), substring(lowercase_names, 2))
  lowercase_names <- setNames(lowercase_names, cap_names)

  events[source %in% names(lowercase_names), source := lowercase_names[source]]

  events[]
}
glin/dcuolog documentation built on May 25, 2019, 5:25 p.m.