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 +4 -4
- data/.tool-versions +2 -0
- data/Gemfile.lock +20 -39
- data/README.md +2 -2
- data/lib/tf2_line_parser/events/airshot.rb +11 -2
- data/lib/tf2_line_parser/events/airshot_heal.rb +34 -0
- data/lib/tf2_line_parser/events/built_object.rb +1 -1
- data/lib/tf2_line_parser/events/event.rb +3 -3
- data/lib/tf2_line_parser/events/headshot_damage.rb +10 -3
- data/lib/tf2_line_parser/events/joined_team.rb +4 -0
- data/lib/tf2_line_parser/events/position_report.rb +23 -0
- data/lib/tf2_line_parser/events/shot_fired.rb +29 -0
- data/lib/tf2_line_parser/events/shot_hit.rb +29 -0
- data/lib/tf2_line_parser/events/world_intermission_win_limit.rb +11 -0
- data/lib/tf2_line_parser/line.rb +160 -12
- data/lib/tf2_line_parser/parser.rb +5 -0
- data/lib/tf2_line_parser/version.rb +1 -1
- data/lib/tf2_line_parser.rb +1 -0
- data/spec/fixtures/logs/airshot.log +6 -0
- data/spec/lib/tf2_line_parser/parser_spec.rb +110 -1
- data/tf2_line_parser.gemspec +0 -1
- metadata +9 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce1e976cd8122572fc3e7ca491e66064e93a6bf46153c1d8fbc3d47961c1df22
|
|
4
|
+
data.tar.gz: d6cc774e17ab02595f7253e31ed9f436dc163681fd82ca2385d5118d80c5c639
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0bd2e58a37355dc74faf0f3b3db946e13cf273453d9ed46e6073fd3a15585ecdecfce3caf54279303a16f004141bcb7509d4ece264c82f09e16c3def603b24a7
|
|
7
|
+
data.tar.gz: 74717248ec519c02913b20bb6b4565b147a7b6aa11a211f78917756ab9696a3620eb98ec4c0347988d2a39feed30cc6cf42c2653857665ee871e7f68f0d11eb3
|
data/.tool-versions
ADDED
data/Gemfile.lock
CHANGED
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
tf2_line_parser (0.
|
|
5
|
-
activesupport
|
|
4
|
+
tf2_line_parser (0.5.0)
|
|
6
5
|
|
|
7
6
|
GEM
|
|
8
7
|
remote: https://rubygems.org/
|
|
9
8
|
specs:
|
|
10
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
63
|
-
tins (~> 1
|
|
64
|
-
thor (1.
|
|
65
|
-
tins (1.
|
|
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
|
|
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
|
-
|
|
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 ||=
|
|
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
|
|
@@ -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
|
data/lib/tf2_line_parser/line.rb
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
data/lib/tf2_line_parser.rb
CHANGED
|
@@ -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__)
|
data/tf2_line_parser.gemspec
CHANGED
|
@@ -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
|
+
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:
|
|
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:
|
|
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:
|