awkisopen/australium

View on GitHub
lib/australium/parser.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'digest/sha1'

module Australium

  class Parser
    using MatchDataToHash

    # Splits a TF2 logfile into an array of individual lines.
    # @param [String] filename the location of the log file to parse
    # @return [Array<String>] an array of log entries
    def self.parse_file(logfile)
      File.read(logfile).encode('UTF-8', :invalid => :replace, :replace => '').split("\n")
    end

    # Splits a TF2 file log into individual game logs, dumping anything extraneous.
    # @param [Array<String>] file_log the file log to parse
    # @return [Array<Array<String>>] an array of one or more game logs
    def self.parse_file_log(file_log)
      file_log.slice_before(MapLoad::LOG_REGEX).select { |l| l.first =~ MapLoad::LOG_REGEX }
    end

    # Calculates a SHA1 digest of a game_log. Used for identifying unique {Game}s.
    # @param [Array<String>] game_log the game log to hash
    # @return [String] SHA1 hash of the game_log
    def self.game_log_digest(game_log)
      Digest::SHA1.hexdigest(game_log.join("\n"))
    end

    # Parses a log of a full TF2 game.
    # @param [Array<String>] lines the lines to parse
    # @param [Hash] properties an optional hash of properties to pass into the event objects
    # @return [Array<Game>] the game data
    def self.parse_game_log(lines, properties = {})
      state = GameState.new
      state.game_id = game_log_digest(lines)
      events = []

      lines.each_with_index do |line, index|
        event = parse_line(line, index, state)
        unless event.nil?
          properties.each_pair { |key, value| event[key] = value }
          events << event
          state = event.state.clone
        end
      end

      if events.empty?
        nil
      else
        events << GameEnd.new(
          :line_number => events.last.line_number + 1,
          :timestamp => events.last.timestamp,
          :server => events.last.server,
          :game_id => events.last.game_id,
          :state => (events.last.state.clone rescue nil)
        )
        Game.new(events)
      end
    end

    # Parses a single line of TF2 log in the context of a game (if a {GameState} is passed).
    # @param [String] line the line to be parsed
    # @param [Integer] line_number the index of the line
    # @param [GameState] state the {GameState} containing the game context.
    # @return [Event, NilClass] event if an event has been recognized; nil otherwise.
    def self.parse_line(line, line_number, state = nil)
      Event::event_classes.each do |event_class|
        next unless defined?(event_class::LOG_REGEX)
        if event_class::LOG_REGEX =~ line
          # Get timestamp data & timestamp GameState if we are being stateful
          timestamp = Time.strptime(Event::TIMESTAMP_REGEX.match(line)[0], Event::TIMESTAMP_FORMAT)
          state.timestamp = timestamp unless state.nil?

          # Get the meat of the event data
          data = event_class::LOG_REGEX.match(line).to_h

          # Get supplemental property data, if any exists
          property_data = line.scan(Event::PROPERTY_REGEX).map { |x| [x.first.to_sym, x.last] }.to_h
          data.merge!(property_data)

          # Add other useful data
          data.merge!({
            :line_number => line_number,
            :state => state,
            :raw => line,
            :timestamp => timestamp,
            :game_id => (state.nil? ? nil : state.game_id)
          })

          # Construct and return the new Event
          return event_class.new(data)
        end
      end
      nil
    end

  end

end