tf2_line_parser 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd8018fe75681bd674febe9c940edf0c91f2da9534e18c75eaf883d6c47088ae
4
- data.tar.gz: 403670b2cf928090827394c4d3a58f7bac3f2baf54bb092c334dd877d7d11504
3
+ metadata.gz: ce1e976cd8122572fc3e7ca491e66064e93a6bf46153c1d8fbc3d47961c1df22
4
+ data.tar.gz: d6cc774e17ab02595f7253e31ed9f436dc163681fd82ca2385d5118d80c5c639
5
5
  SHA512:
6
- metadata.gz: a0a17315b124774163c6de596d18666a710a207223da7300f7836f3d210cecfb3372bb297380860e09ed27830783df45726ff0bb8d27bb09966c398f5d0bc0e7
7
- data.tar.gz: d581273d5657eb48a6b69e38d8afa09f42b044c65a6170ebf40180cfe9fcad2b581cfb0350eaef4705ca1dfa1a42a808ce84d90b43b4d64d243867dbd2790990
6
+ metadata.gz: 0bd2e58a37355dc74faf0f3b3db946e13cf273453d9ed46e6073fd3a15585ecdecfce3caf54279303a16f004141bcb7509d4ece264c82f09e16c3def603b24a7
7
+ data.tar.gz: 74717248ec519c02913b20bb6b4565b147a7b6aa11a211f78917756ab9696a3620eb98ec4c0347988d2a39feed30cc6cf42c2653857665ee871e7f68f0d11eb3
data/.tool-versions ADDED
@@ -0,0 +1,2 @@
1
+ ruby 4.0.0
2
+ nodejs latest
data/Gemfile.lock CHANGED
@@ -1,30 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tf2_line_parser (0.4.1)
5
- activesupport
4
+ tf2_line_parser (0.5.0)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
9
8
  specs:
10
- activesupport (8.0.2)
11
- base64
12
- benchmark (>= 0.3)
13
- bigdecimal
14
- concurrent-ruby (~> 1.0, >= 1.3.1)
15
- connection_pool (>= 2.2.5)
16
- drb
17
- i18n (>= 1.6, < 2)
18
- logger (>= 1.4.2)
19
- minitest (>= 5.1)
20
- securerandom (>= 0.3)
21
- tzinfo (~> 2.0, >= 2.0.5)
22
- uri (>= 0.13.1)
23
- base64 (0.3.0)
24
- benchmark (0.4.1)
25
- bigdecimal (3.2.2)
26
- concurrent-ruby (1.3.5)
27
- connection_pool (2.5.3)
9
+ bigdecimal (4.0.1)
28
10
  coveralls_reborn (0.29.0)
29
11
  simplecov (~> 0.22.0)
30
12
  term-ansicolor (~> 1.7)
@@ -32,42 +14,41 @@ GEM
32
14
  tins (~> 1.32)
33
15
  diff-lcs (1.6.2)
34
16
  docile (1.4.1)
35
- drb (2.2.3)
36
- i18n (1.14.7)
37
- concurrent-ruby (~> 1.0)
38
- json (2.12.2)
39
- logger (1.7.0)
40
- minitest (5.25.5)
41
- rspec (3.13.1)
17
+ io-console (0.8.2)
18
+ json (2.18.0)
19
+ mize (0.6.1)
20
+ readline (0.0.4)
21
+ reline
22
+ reline (0.6.3)
23
+ io-console (~> 0.5)
24
+ rspec (3.13.2)
42
25
  rspec-core (~> 3.13.0)
43
26
  rspec-expectations (~> 3.13.0)
44
27
  rspec-mocks (~> 3.13.0)
45
- rspec-core (3.13.4)
28
+ rspec-core (3.13.6)
46
29
  rspec-support (~> 3.13.0)
47
30
  rspec-expectations (3.13.5)
48
31
  diff-lcs (>= 1.2.0, < 2.0)
49
32
  rspec-support (~> 3.13.0)
50
- rspec-mocks (3.13.5)
33
+ rspec-mocks (3.13.7)
51
34
  diff-lcs (>= 1.2.0, < 2.0)
52
35
  rspec-support (~> 3.13.0)
53
- rspec-support (3.13.4)
54
- securerandom (0.4.1)
36
+ rspec-support (3.13.6)
55
37
  simplecov (0.22.0)
56
38
  docile (~> 1.1)
57
39
  simplecov-html (~> 0.11)
58
40
  simplecov_json_formatter (~> 0.1)
59
- simplecov-html (0.13.1)
41
+ simplecov-html (0.13.2)
60
42
  simplecov_json_formatter (0.1.4)
61
43
  sync (0.5.0)
62
- term-ansicolor (1.11.2)
63
- tins (~> 1.0)
64
- thor (1.3.2)
65
- tins (1.38.0)
44
+ term-ansicolor (1.11.3)
45
+ tins (~> 1)
46
+ thor (1.4.0)
47
+ tins (1.51.0)
66
48
  bigdecimal
49
+ mize (~> 0.6)
50
+ readline
67
51
  sync
68
- tzinfo (2.0.6)
69
- concurrent-ruby (~> 1.0)
70
- uri (1.0.3)
71
52
 
72
53
  PLATFORMS
73
54
  ruby
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # TF2 line parser [![Build Status](https://travis-ci.org/Arie/tf2_line_parser.png?branch=master)](http://travis-ci.org/Arie/tf2_line_parser) [![Dependency Status](https://gemnasium.com/Arie/tf2_line_parser.png)](https://gemnasium.com/Arie/tf2_line_parser) [![Code Climate](https://codeclimate.com/github/Arie/tf2_line_parser.png)](https://codeclimate.com/github/Arie/tf2_line_parser) [![Coverage Status](https://coveralls.io/repos/Arie/tf2_line_parser/badge.png?branch=master)](https://coveralls.io/r/Arie/tf2_line_parser)
1
+ # TF2 line parser
2
2
 
3
3
  A TF2 log line parser. Unlike the other log parsers I've found, this one parses the line and returns a plain old ruby object to describe the event of the line.
4
4
  I plan to use this for my own stats parsing tool.
5
5
 
6
6
  ## Requirements
7
- * Ruby
7
+ * Ruby (no runtime dependencies)
8
8
 
9
9
  ## Credits
10
10
  * nTraum, I stole most of the regexes from his [TF2Stats](https://github.com/nTraum/tf2stats/) project.
@@ -4,11 +4,16 @@ module TF2LineParser
4
4
  module Events
5
5
  class Airshot < Damage
6
6
  def self.regex
7
- @regex ||= /#{regex_time} #{regex_player} triggered "damage" #{regex_damage_against}\(damage "(?'value'\d+)"\)#{regex_realdamage}#{regex_weapon}#{regex_healing}#{regex_crit}#{regex_headshot} #{regex_airshot}$/.freeze
7
+ # Airshot can appear after weapon, with optional height at the end
8
+ @regex ||= /#{regex_time} #{regex_player} triggered "damage" #{regex_damage_against}\(damage "(?'value'\d+)"\)#{regex_realdamage}#{regex_weapon}#{regex_airshot}#{regex_height}/.freeze
8
9
  end
9
10
 
10
11
  def self.regex_airshot
11
- @regex_airshot ||= '\(airshot "(?\'airshot\'\w*)"\)'
12
+ @regex_airshot ||= / \(airshot "(?'airshot'\w*)"\)/
13
+ end
14
+
15
+ def self.regex_height
16
+ @regex_height ||= /(?: \(height "(?'height'\d*)"\))?/
12
17
  end
13
18
 
14
19
  def self.attributes
@@ -40,7 +45,11 @@ module TF2LineParser
40
45
  @player = Player.new(player_name, player_uid, player_steamid, player_team)
41
46
  @target = Player.new(target_name, target_uid, target_steamid, target_team) if target_name
42
47
  @value = value.to_i
48
+ @damage = @value
43
49
  @weapon = weapon
50
+ @healing = nil
51
+ @crit = nil
52
+ @headshot = nil
44
53
  @airshot = (airshot.to_i == 1)
45
54
  end
46
55
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TF2LineParser
4
+ module Events
5
+ class AirshotHeal < Heal
6
+ attr_reader :airshot
7
+
8
+ def self.regex
9
+ @regex ||= /#{regex_time} #{regex_player} triggered "healed" against #{regex_target} \(healing "(?'value'\d+)"\)#{regex_airshot}#{regex_height}/.freeze
10
+ end
11
+
12
+ def self.regex_airshot
13
+ @regex_airshot ||= / \(airshot "(?'airshot'\w*)"\)/
14
+ end
15
+
16
+ def self.regex_height
17
+ @regex_height ||= /(?: \(height "(?'height'\d*)"\))?/
18
+ end
19
+
20
+ def self.attributes
21
+ @attributes ||= %i[time player_section target_section value airshot]
22
+ end
23
+
24
+ def initialize(time, player_name, player_uid, player_steam_id, player_team, target_name, target_uid, target_steam_id, target_team, value, airshot)
25
+ @time = parse_time(time)
26
+ @player = Player.new(player_name, player_uid, player_steam_id, player_team)
27
+ @target = Player.new(target_name, target_uid, target_steam_id, target_team)
28
+ @value = value.to_i
29
+ @healing = @value
30
+ @airshot = (airshot.to_i == 1)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -4,7 +4,7 @@ module TF2LineParser
4
4
  attr_reader :time, :player, :object
5
5
 
6
6
  def self.regex
7
- @regex ||= /#{regex_time} #{regex_player} triggered "builtobject" #{regex_object_info}#{regex_position}?/.freeze
7
+ @regex ||= /#{regex_time} #{regex_player} triggered "(?:player_)?builtobject" #{regex_object_info}#{regex_position}?/.freeze
8
8
  end
9
9
 
10
10
  def self.regex_object_info
@@ -11,7 +11,7 @@ module TF2LineParser
11
11
  end
12
12
 
13
13
  def self.regex_time
14
- @regex_time ||= 'L (?\'time\'.*):'
14
+ @regex_time ||= 'L (?\'time\'\\d{2}/\\d{2}/\\d{4} - \\d{2}:\\d{2}:\\d{2}):'
15
15
  end
16
16
 
17
17
  def self.regex_player
@@ -36,9 +36,9 @@ module TF2LineParser
36
36
 
37
37
  def self.types
38
38
  # ordered by how common the messages are
39
- @types ||= [HeadshotDamage, Airshot, Damage, Heal, PickupItem, Assist, Kill, CaptureBlock, PointCapture, ChargeDeployed,
39
+ @types ||= [ShotFired, ShotHit, PositionReport, HeadshotDamage, Airshot, Damage, AirshotHeal, Heal, PickupItem, Assist, Kill, CaptureBlock, PointCapture, ChargeDeployed,
40
40
  MedicDeath, MedicDeathEx, EmptyUber, ChargeReady, ChargeEnded, FirstHealAfterSpawn, LostUberAdvantage, BuiltObject, PlayerExtinguished, JoinedTeam, EnteredGame, RoleChange, Spawn, Suicide, KilledObject, Say, TeamSay, Domination, Revenge, RoundWin, CurrentScore,
41
- RoundLength, RoundStart, RoundSetupBegin, RoundSetupEnd, MiniRoundStart, MiniRoundSelected, IntermissionWinLimit, Connect, Disconnect, RconCommand, ConsoleSay, MatchEnd, FinalScore,
41
+ RoundLength, RoundStart, RoundSetupBegin, RoundSetupEnd, MiniRoundStart, MiniRoundSelected, WorldIntermissionWinLimit, IntermissionWinLimit, Connect, Disconnect, RconCommand, ConsoleSay, MatchEnd, FinalScore,
42
42
  RoundStalemate, Unknown].freeze
43
43
  end
44
44
 
@@ -12,7 +12,7 @@ module TF2LineParser
12
12
  end
13
13
 
14
14
  def self.attributes
15
- @attributes ||= %i[time player_section target_section value weapon]
15
+ @attributes ||= %i[time player_section target_section value weapon healing crit headshot]
16
16
  end
17
17
 
18
18
  def self.regex_results(matched_line)
@@ -21,6 +21,9 @@ module TF2LineParser
21
21
  target_section = matched_line['target_section']
22
22
  value = matched_line['value']
23
23
  weapon = matched_line['weapon']
24
+ healing = matched_line['healing']
25
+ crit = matched_line['crit']
26
+ headshot = matched_line['headshot']
24
27
 
25
28
  # Parse player section
26
29
  player_name, player_uid, player_steamid, player_team = parse_player_section(player_section)
@@ -31,15 +34,19 @@ module TF2LineParser
31
34
  target_name, target_uid, target_steamid, target_team = parse_target_section(target_section)
32
35
  end
33
36
 
34
- [time, player_name, player_uid, player_steamid, player_team, target_name, target_uid, target_steamid, target_team, value, weapon]
37
+ [time, player_name, player_uid, player_steamid, player_team, target_name, target_uid, target_steamid, target_team, value, weapon, healing, crit, headshot]
35
38
  end
36
39
 
37
- def initialize(time, player_name, player_uid, player_steamid, player_team, target_name, target_uid, target_steamid, target_team, value, weapon)
40
+ def initialize(time, player_name, player_uid, player_steamid, player_team, target_name, target_uid, target_steamid, target_team, value, weapon, healing, crit, headshot)
38
41
  @time = parse_time(time)
39
42
  @player = Player.new(player_name, player_uid, player_steamid, player_team)
40
43
  @target = Player.new(target_name, target_uid, target_steamid, target_team) if target_name
41
44
  @value = value.to_i
45
+ @damage = @value
42
46
  @weapon = weapon
47
+ @healing = healing.to_i if healing
48
+ @crit = crit
49
+ @headshot = true # Always true for HeadshotDamage events
43
50
  end
44
51
  end
45
52
  end
@@ -16,6 +16,10 @@ module TF2LineParser
16
16
  def self.attributes
17
17
  @attributes ||= %i[time player_section team_name]
18
18
  end
19
+
20
+ def team
21
+ @team_name
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TF2LineParser
4
+ module Events
5
+ class PositionReport < Event
6
+ attr_reader :time, :player, :position
7
+
8
+ def self.regex
9
+ @regex ||= /#{regex_time} #{regex_player} position_report \(position "(?'position'[^"]+)"\)/.freeze
10
+ end
11
+
12
+ def self.attributes
13
+ @attributes ||= %i[time player_section position]
14
+ end
15
+
16
+ def initialize(time, name, uid, steam_id, team, position)
17
+ @time = parse_time(time)
18
+ @player = Player.new(name, uid, steam_id, team)
19
+ @position = position
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TF2LineParser
4
+ module Events
5
+ class ShotFired < PlayerActionEvent
6
+ def self.action_text
7
+ @action_text ||= 'triggered "shot_fired"'
8
+ end
9
+
10
+ def self.regex_action
11
+ @regex_action ||= '\(weapon "(?\'weapon\'[^\"]+)"\)'
12
+ end
13
+
14
+ def self.item
15
+ :weapon
16
+ end
17
+
18
+ def self.attributes
19
+ @attributes ||= %i[time player_section weapon]
20
+ end
21
+
22
+ def initialize(time, player_name, player_uid, player_steam_id, player_team, weapon)
23
+ @time = parse_time(time)
24
+ @player = Player.new(player_name, player_uid, player_steam_id, player_team)
25
+ @weapon = weapon
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TF2LineParser
4
+ module Events
5
+ class ShotHit < PlayerActionEvent
6
+ def self.action_text
7
+ @action_text ||= 'triggered "shot_hit"'
8
+ end
9
+
10
+ def self.regex_action
11
+ @regex_action ||= '\(weapon "(?\'weapon\'[^\"]+)"\)'
12
+ end
13
+
14
+ def self.item
15
+ :weapon
16
+ end
17
+
18
+ def self.attributes
19
+ @attributes ||= %i[time player_section weapon]
20
+ end
21
+
22
+ def initialize(time, player_name, player_uid, player_steam_id, player_team, weapon)
23
+ @time = parse_time(time)
24
+ @player = Player.new(player_name, player_uid, player_steam_id, player_team)
25
+ @weapon = weapon
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TF2LineParser
4
+ module Events
5
+ class WorldIntermissionWinLimit < RoundEventWithoutVariables
6
+ def self.round_type
7
+ @round_type ||= 'Intermission_Win_Limit'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,31 +1,179 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'time'
4
- require 'active_support/multibyte/chars'
5
- require 'active_support/multibyte/unicode'
6
4
 
7
5
  module TF2LineParser
8
6
  class Line
9
7
  attr_accessor :line
10
8
 
9
+ # Keyword to event types mapping (order matters for subtypes!)
10
+ KEYWORD_DISPATCH = {
11
+ 'shot_fired' => [Events::ShotFired],
12
+ 'shot_hit' => [Events::ShotHit],
13
+ 'damage' => :check_damage_subtypes,
14
+ 'healed' => :check_heal_subtypes,
15
+ 'picked up' => [Events::PickupItem],
16
+ 'kill assist' => [Events::Assist],
17
+ 'killed' => [Events::Kill],
18
+ 'killedobject' => [Events::KilledObject],
19
+ 'captureblocked' => [Events::CaptureBlock],
20
+ 'pointcaptured' => [Events::PointCapture],
21
+ 'chargedeployed' => [Events::ChargeDeployed],
22
+ 'medic_death_ex' => [Events::MedicDeathEx],
23
+ 'medic_death' => [Events::MedicDeath],
24
+ 'empty_uber' => [Events::EmptyUber],
25
+ 'chargeready' => [Events::ChargeReady],
26
+ 'chargeended' => [Events::ChargeEnded],
27
+ 'first_heal_after_spawn' => [Events::FirstHealAfterSpawn],
28
+ 'lost_uber_advantage' => [Events::LostUberAdvantage],
29
+ 'builtobject' => [Events::BuiltObject],
30
+ 'player_builtobject' => [Events::BuiltObject],
31
+ 'player_extinguished' => [Events::PlayerExtinguished],
32
+ 'joined team' => [Events::JoinedTeam],
33
+ 'entered the game' => [Events::EnteredGame],
34
+ 'changed role' => [Events::RoleChange],
35
+ 'spawned as' => [Events::Spawn],
36
+ 'committed suicide' => [Events::Suicide],
37
+ 'domination' => [Events::Domination],
38
+ 'revenge' => [Events::Revenge],
39
+ 'Round_Win' => [Events::RoundWin],
40
+ 'Round_Length' => [Events::RoundLength],
41
+ 'Round_Start' => [Events::RoundStart],
42
+ 'Round_Setup_Begin' => [Events::RoundSetupBegin],
43
+ 'Round_Setup_End' => [Events::RoundSetupEnd],
44
+ 'Mini_Round_Start' => [Events::MiniRoundStart],
45
+ 'Mini_Round_Selected' => [Events::MiniRoundSelected],
46
+ 'Intermission_Win_Limit' => [Events::WorldIntermissionWinLimit, Events::IntermissionWinLimit],
47
+ 'Round_Stalemate' => [Events::RoundStalemate],
48
+ 'Game_Over' => [Events::MatchEnd],
49
+ 'connected, address' => [Events::Connect],
50
+ 'disconnected' => [Events::Disconnect],
51
+ 'say_team' => [Events::TeamSay],
52
+ 'say "' => [Events::Say],
53
+ 'position_report' => [Events::PositionReport],
54
+ }.freeze
55
+
56
+ # Fallback types when no keyword matches
57
+ FALLBACK_TYPES = [
58
+ Events::CurrentScore, Events::FinalScore, Events::RconCommand,
59
+ Events::ConsoleSay, Events::Unknown
60
+ ].freeze
61
+
11
62
  def initialize(line)
12
63
  @line = line
13
64
  end
14
65
 
15
66
  def parse
16
- Events::Event.types.each do |type|
17
- begin
18
- match = line.match(type.regex)
19
- rescue ArgumentError
20
- tidied_line = ActiveSupport::Multibyte::Chars.new(line).tidy_bytes
21
- match = tidied_line.match(type.regex)
67
+ self.class.parse(@line)
68
+ end
69
+
70
+ # Class method to parse without object allocation
71
+ def self.parse(line)
72
+ types = find_candidate_types(line)
73
+ result = try_parse_types(line, types)
74
+ return result if result
75
+
76
+ # If candidate types didn't match, fall back to Unknown (unless we already tried fallbacks)
77
+ return nil if types == FALLBACK_TYPES
78
+
79
+ try_parse_types(line, [Events::Unknown])
80
+ end
81
+
82
+ class << self
83
+ private
84
+
85
+ def find_candidate_types(line)
86
+ # Fast path: check for "triggered" which covers most events
87
+ if line.include?('triggered "')
88
+ start_idx = line.index('triggered "')
89
+ if start_idx
90
+ end_idx = line.index('"', start_idx + 11)
91
+ if end_idx
92
+ keyword = line[start_idx + 11...end_idx]
93
+ result = KEYWORD_DISPATCH[keyword]
94
+ if result
95
+ case result
96
+ when :check_damage_subtypes
97
+ return check_damage_subtypes(line)
98
+ when :check_heal_subtypes
99
+ return check_heal_subtypes(line)
100
+ else
101
+ return result
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ # Check other common patterns with simple string operations
109
+ if line.include?('" killed "')
110
+ [Events::Kill]
111
+ elsif line.include?('position_report')
112
+ [Events::PositionReport]
113
+ elsif line.include?('" say_team "')
114
+ [Events::TeamSay]
115
+ elsif line.include?('" say "')
116
+ [Events::Say]
117
+ elsif line.include?('picked up item')
118
+ [Events::PickupItem]
119
+ elsif line.include?('joined team "')
120
+ [Events::JoinedTeam]
121
+ elsif line.include?('entered the game')
122
+ [Events::EnteredGame]
123
+ elsif line.include?('changed role to')
124
+ [Events::RoleChange]
125
+ elsif line.include?('spawned as "')
126
+ [Events::Spawn]
127
+ elsif line.include?('committed suicide')
128
+ [Events::Suicide]
129
+ elsif line.include?('connected, address')
130
+ [Events::Connect]
131
+ elsif line.include?('disconnected (reason')
132
+ [Events::Disconnect]
133
+ else
134
+ FALLBACK_TYPES
22
135
  end
23
- if match
24
- @match ||= type.new(*type.regex_results(match))
25
- break
136
+ end
137
+
138
+ def try_parse_types(line, types)
139
+ types.each do |type|
140
+ begin
141
+ match = line.match(type.regex)
142
+ rescue ArgumentError
143
+ # Handle invalid byte sequences by forcing UTF-8 encoding
144
+ tidied_line = line.encode('UTF-8', 'UTF-8', invalid: :replace, undef: :replace, replace: '')
145
+ match = tidied_line.match(type.regex)
146
+ end
147
+ return type.new(*type.regex_results(match)) if match
148
+ end
149
+ nil
150
+ end
151
+
152
+ def check_damage_subtypes(line)
153
+ damage_attr_pos = line.index('(damage "')
154
+ return [Events::Damage] unless damage_attr_pos
155
+
156
+ suffix = line[damage_attr_pos..-1]
157
+ if suffix.include?('(headshot "')
158
+ [Events::HeadshotDamage]
159
+ elsif suffix.include?('(airshot "')
160
+ [Events::Airshot]
161
+ else
162
+ [Events::Damage]
163
+ end
164
+ end
165
+
166
+ def check_heal_subtypes(line)
167
+ heal_attr_pos = line.index('(healing "')
168
+ return [Events::Heal] unless heal_attr_pos
169
+
170
+ suffix = line[heal_attr_pos..-1]
171
+ if suffix.include?('(airshot "')
172
+ [Events::AirshotHeal]
173
+ else
174
+ [Events::Heal]
26
175
  end
27
176
  end
28
- @match
29
177
  end
30
178
  end
31
179
  end
@@ -11,5 +11,10 @@ module TF2LineParser
11
11
  def parse
12
12
  line.parse
13
13
  end
14
+
15
+ # Class method to parse without object allocation overhead
16
+ def self.parse(line)
17
+ Line.parse(line.to_s)
18
+ end
14
19
  end
15
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TF2LineParser
4
- VERSION = '0.4.1'
4
+ VERSION = '0.5.1'
5
5
  end
@@ -13,6 +13,7 @@ require 'tf2_line_parser/events/connect'
13
13
  require 'tf2_line_parser/events/score'
14
14
  require 'tf2_line_parser/events/role_change'
15
15
  require 'tf2_line_parser/events/damage'
16
+ require 'tf2_line_parser/events/heal'
16
17
  Dir["#{File.dirname(__FILE__)}/tf2_line_parser/events/*.rb"].sort.each { |file| require file }
17
18
  require 'tf2_line_parser/line'
18
19
 
@@ -1 +1,7 @@
1
1
  L 05/15/2014 - 18:22:27: "super sanic<5><STEAM_0:0:64268661><Red>" triggered "damage" against "kerrieebOb<11><STEAM_0:0:11431287><Blue>" (damage "47") (weapon "tf_projectile_rocket") (airshot "1")
2
+ L 12/18/2025 - 22:06:01: "tantwo<17><[U:1:191375689]><Blue>" triggered "damage" against "ryftomania(big/guy)<8><[U:1:468872526]><Red>" (damage "105") (realdamage "88") (weapon "quake_rl") (airshot "1") (height "353")
3
+ L 12/18/2025 - 22:07:05: "merkules<11><[U:1:86331856]><Blue>" triggered "healed" against "fy<14><[U:1:442791013]><Blue>" (healing "82") (airshot "1") (height "261")
4
+ L 12/18/2025 - 22:10:55: "fy<14><[U:1:442791013]><Blue>" triggered "damage" against "keicam<9><[U:1:163231154]><Red>" (damage "50") (realdamage "26") (weapon "tf_projectile_rocket") (airshot "1") (height "292")
5
+ L 12/18/2025 - 22:12:57: "tantwo<17><[U:1:191375689]><Blue>" triggered "damage" against "keicam<9><[U:1:163231154]><Red>" (damage "105") (weapon "quake_rl") (airshot "1") (height "318")
6
+ L 12/18/2025 - 22:12:58: "tantwo<17><[U:1:191375689]><Blue>" triggered "damage" against "keicam<9><[U:1:163231154]><Red>" (damage "99") (realdamage "9") (weapon "quake_rl") (airshot "1") (height "427")
7
+ L 12/18/2025 - 22:15:21: "fy<14><[U:1:442791013]><Blue>" triggered "damage" against "gibusmain<19><[U:1:258908204]><Red>" (damage "109") (realdamage "66") (weapon "tf_projectile_rocket") (airshot "1") (height "228")
@@ -96,11 +96,71 @@ module TF2LineParser
96
96
  parse(line).inspect
97
97
  end
98
98
 
99
+ it 'recognizes airshots with realdamage and height' do
100
+ line = airshot_log_lines[1]
101
+ result = parse(line)
102
+ expect(result).to be_a(Events::Airshot)
103
+ expect(result.player.name).to eq('tantwo')
104
+ expect(result.player.steam_id).to eq('[U:1:191375689]')
105
+ expect(result.player.team).to eq('Blue')
106
+ expect(result.target.name).to eq('ryftomania(big/guy)')
107
+ expect(result.target.steam_id).to eq('[U:1:468872526]')
108
+ expect(result.target.team).to eq('Red')
109
+ expect(result.damage).to eq(105)
110
+ expect(result.weapon).to eq('quake_rl')
111
+ expect(result.airshot).to eq(true)
112
+ end
113
+
114
+ it 'recognizes airshots without realdamage' do
115
+ line = airshot_log_lines[4]
116
+ result = parse(line)
117
+ expect(result).to be_a(Events::Airshot)
118
+ expect(result.player.name).to eq('tantwo')
119
+ expect(result.damage).to eq(105)
120
+ expect(result.weapon).to eq('quake_rl')
121
+ expect(result.airshot).to eq(true)
122
+ end
123
+
124
+ it 'recognizes airshot heals (Crusader Crossbow mid-air)' do
125
+ line = airshot_log_lines[2]
126
+ result = parse(line)
127
+ expect(result).to be_a(Events::AirshotHeal)
128
+ expect(result.player.name).to eq('merkules')
129
+ expect(result.player.steam_id).to eq('[U:1:86331856]')
130
+ expect(result.player.team).to eq('Blue')
131
+ expect(result.target.name).to eq('fy')
132
+ expect(result.target.steam_id).to eq('[U:1:442791013]')
133
+ expect(result.target.team).to eq('Blue')
134
+ expect(result.healing).to eq(82)
135
+ expect(result.airshot).to eq(true)
136
+ end
137
+
138
+ it 'parses all damage airshot log lines correctly' do
139
+ damage_airshot_lines = airshot_log_lines.reject { |l| l.include?('healed') }
140
+ damage_airshot_lines.each do |line|
141
+ result = parse(line)
142
+ expect(result).to be_a(Events::Airshot), "Expected Airshot for: #{line}"
143
+ expect(result.airshot).to eq(true)
144
+ end
145
+ end
146
+
147
+ it 'parses all heal airshot log lines correctly' do
148
+ heal_airshot_lines = airshot_log_lines.select { |l| l.include?('healed') }
149
+ heal_airshot_lines.each do |line|
150
+ result = parse(line)
151
+ expect(result).to be_a(Events::AirshotHeal), "Expected AirshotHeal for: #{line}"
152
+ expect(result.airshot).to eq(true)
153
+ end
154
+ end
155
+
99
156
  it 'recognizes sniper headshot damage' do
100
157
  line = detailed_log_lines[3645]
101
158
  weapon = 'sniperrifle'
159
+ healing = nil
160
+ crit = nil
161
+ headshot = '1'
102
162
  expect(Events::HeadshotDamage).to receive(:new).with(anything, anything, anything, anything, anything, anything,
103
- anything, anything, anything, anything, weapon)
163
+ anything, anything, anything, anything, weapon, healing, crit, headshot)
104
164
  parse(line).inspect
105
165
  end
106
166
 
@@ -396,6 +456,12 @@ module TF2LineParser
396
456
  parse(line)
397
457
  end
398
458
 
459
+ it 'recognizes world intermission win limit' do
460
+ line = 'L 01/24/2026 - 15:30:45: World triggered "Intermission_Win_Limit"'
461
+ expect(Events::WorldIntermissionWinLimit).to receive(:new).with(anything)
462
+ parse(line)
463
+ end
464
+
399
465
  it 'recognizes ubercharges' do
400
466
  line = log_lines[1416]
401
467
  name = 'broder mirelin'
@@ -585,6 +651,39 @@ module TF2LineParser
585
651
  parse(line)
586
652
  end
587
653
 
654
+ it 'recognizes shot_fired' do
655
+ line = 'L 12/30/2025 - 18:34:19: "Jib<34><[U:1:367944796]><Blue>" triggered "shot_fired" (weapon "tf_projectile_rocket")'
656
+ result = parse(line)
657
+ expect(result).to be_a(Events::ShotFired)
658
+ expect(result.player.name).to eq('Jib')
659
+ expect(result.player.uid).to eq('34')
660
+ expect(result.player.steam_id).to eq('[U:1:367944796]')
661
+ expect(result.player.team).to eq('Blue')
662
+ expect(result.weapon).to eq('tf_projectile_rocket')
663
+ end
664
+
665
+ it 'recognizes shot_hit' do
666
+ line = 'L 12/30/2025 - 18:34:19: "Jib<34><[U:1:367944796]><Blue>" triggered "shot_hit" (weapon "tf_projectile_rocket")'
667
+ result = parse(line)
668
+ expect(result).to be_a(Events::ShotHit)
669
+ expect(result.player.name).to eq('Jib')
670
+ expect(result.player.uid).to eq('34')
671
+ expect(result.player.steam_id).to eq('[U:1:367944796]')
672
+ expect(result.player.team).to eq('Blue')
673
+ expect(result.weapon).to eq('tf_projectile_rocket')
674
+ end
675
+
676
+ it 'recognizes position_report' do
677
+ line = 'L 12/30/2025 - 18:34:19: "Jib<34><[U:1:367944796]><Blue>" position_report (position "306 -1464 -237")'
678
+ result = parse(line)
679
+ expect(result).to be_a(Events::PositionReport)
680
+ expect(result.player.name).to eq('Jib')
681
+ expect(result.player.uid).to eq('34')
682
+ expect(result.player.steam_id).to eq('[U:1:367944796]')
683
+ expect(result.player.team).to eq('Blue')
684
+ expect(result.position).to eq('306 -1464 -237')
685
+ end
686
+
588
687
  it 'recognizes suicides' do
589
688
  line = log_lines[76]
590
689
  name = '.schocky'
@@ -638,6 +737,16 @@ module TF2LineParser
638
737
  parse(line)
639
738
  end
640
739
 
740
+ it 'falls back to Unknown when keyword matches but regex does not' do
741
+ # A triggered event with a recognized keyword but unrecognized format should
742
+ # fall back to Unknown instead of returning nil
743
+ line = 'L 01/24/2026 - 15:30:45: Something triggered "Round_Win" with unexpected format'
744
+ time = '01/24/2026 - 15:30:45'
745
+ unknown = 'Something triggered "Round_Win" with unexpected format'
746
+ expect(Events::Unknown).to receive(:new).with(time, unknown)
747
+ parse(line)
748
+ end
749
+
641
750
  it 'can parse all lines in the example log files without exploding' do
642
751
  broder_vs_epsilon = File.expand_path('../../fixtures/logs/broder_vs_epsilon.log', __dir__)
643
752
  special_characters = File.expand_path('../../fixtures/logs/special_characters.log', __dir__)
@@ -15,7 +15,6 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ['lib']
16
16
  gem.homepage = 'http://github.com/Arie/tf2_line_parser'
17
17
 
18
- gem.add_dependency 'activesupport'
19
18
  gem.add_development_dependency 'coveralls_reborn'
20
19
  gem.add_development_dependency 'rspec'
21
20
  gem.add_development_dependency 'simplecov'
metadata CHANGED
@@ -1,29 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tf2_line_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arie
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-12-31 00:00:00.000000000 Z
10
+ date: 2026-01-24 00:00:00.000000000 Z
12
11
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: activesupport
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
12
  - !ruby/object:Gem::Dependency
28
13
  name: coveralls_reborn
29
14
  requirement: !ruby/object:Gem::Requirement
@@ -89,12 +74,14 @@ files:
89
74
  - ".coverage"
90
75
  - ".gitignore"
91
76
  - ".rspec"
77
+ - ".tool-versions"
92
78
  - ".travis.yml"
93
79
  - Gemfile
94
80
  - Gemfile.lock
95
81
  - README.md
96
82
  - lib/tf2_line_parser.rb
97
83
  - lib/tf2_line_parser/events/airshot.rb
84
+ - lib/tf2_line_parser/events/airshot_heal.rb
98
85
  - lib/tf2_line_parser/events/assist.rb
99
86
  - lib/tf2_line_parser/events/built_object.rb
100
87
  - lib/tf2_line_parser/events/capture_block.rb
@@ -129,6 +116,7 @@ files:
129
116
  - lib/tf2_line_parser/events/player_action_event.rb
130
117
  - lib/tf2_line_parser/events/player_extinguished.rb
131
118
  - lib/tf2_line_parser/events/point_capture.rb
119
+ - lib/tf2_line_parser/events/position_report.rb
132
120
  - lib/tf2_line_parser/events/pvp_event.rb
133
121
  - lib/tf2_line_parser/events/rcon_command.rb
134
122
  - lib/tf2_line_parser/events/revenge.rb
@@ -142,9 +130,12 @@ files:
142
130
  - lib/tf2_line_parser/events/round_start.rb
143
131
  - lib/tf2_line_parser/events/round_win.rb
144
132
  - lib/tf2_line_parser/events/score.rb
133
+ - lib/tf2_line_parser/events/shot_fired.rb
134
+ - lib/tf2_line_parser/events/shot_hit.rb
145
135
  - lib/tf2_line_parser/events/spawn.rb
146
136
  - lib/tf2_line_parser/events/suicide.rb
147
137
  - lib/tf2_line_parser/events/unknown.rb
138
+ - lib/tf2_line_parser/events/world_intermission_win_limit.rb
148
139
  - lib/tf2_line_parser/line.rb
149
140
  - lib/tf2_line_parser/parser.rb
150
141
  - lib/tf2_line_parser/player.rb
@@ -166,7 +157,6 @@ files:
166
157
  homepage: http://github.com/Arie/tf2_line_parser
167
158
  licenses: []
168
159
  metadata: {}
169
- post_install_message:
170
160
  rdoc_options: []
171
161
  require_paths:
172
162
  - lib
@@ -181,8 +171,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
171
  - !ruby/object:Gem::Version
182
172
  version: '0'
183
173
  requirements: []
184
- rubygems_version: 3.5.11
185
- signing_key:
174
+ rubygems_version: 4.0.3
186
175
  specification_version: 4
187
176
  summary: TF2 log line parser
188
177
  test_files: