| name | combat-log-parsing |
| description | Guides working with Chronicle's combat log parser for Classic World of Warcraft (Turtle WoW).
Use when: adding new event types, debugging parsing failures, understanding GUID formats,
or working with the vanilla combatlog package. Covers the matcher pattern, message types,
regex-based parsing, and the 64-bit GUID structure with entity type bits.
|
Combat Log Parsing
Chronicle parses vanilla WoW combat logs into structured events. The parser handles text-based combat logs from the Turtle WoW private server, converting raw log lines into typed Go structs.
When to Use This Skill
- Adding support for a new combat log event type
- Debugging why a log line isn't being parsed
- Understanding the GUID format and entity type detection
- Working with message types and their properties
- Modifying regex patterns for log matching
Architecture Overview
io.Reader → Liner → Scanner → Parser → []messages.Message
↑ ↓
CLOCK_INFO Matcher functions (40+)
timestamps try each until match
Key directories:
combatlog/
├── parser/
│ ├── vanilla/
│ │ ├── parser.go # Main Parser struct, Advance() loop
│ │ ├── matchers.go # 40+ matcher functions (fDamage*, fHeal, etc.)
│ │ └── messages/
│ │ └── message.go # Message interface + all message types
│ ├── guid/
│ │ └── guid.go # 64-bit GUID parsing and type detection
│ ├── regexs/
│ │ └── regexs.go # All regex patterns for matching
│ └── types/
│ ├── constants.go # HitType, School, Resource enums
│ └── helper.go # FromRegex() matcher helper
Key Concepts
GUID Format (64-bit)
GUIDs are 18-character hex strings (0x + 16 hex digits) encoding entity type in the high bits:
0xF130000844003FB5
││││
│└┴┴─ Entity type in bits 48-51 (0x00F0 mask after rotation)
└──── High 16 bits
Entity type detection (see combatlog/parser/guid/guid.go):
func (g GUID) GetHigh() uint16 {
return uint16(bits.RotateLeft64(uint64(g), -48))
}
Creature entry ID (24 bits at position 24-47):
func (g GUID) GetEntry() (uint32, bool) {
if g.IsAnyCreature() || g.IsObject() {
rotated := bits.RotateLeft64(uint64(g), -24)
return uint32(rotated & 0x0000000000FFFFFF), true
}
return 0, false
}
Message Interface
All parsed events implement messages.Message (combatlog/parser/vanilla/messages/message.go):
type Message interface {
isMessage()
Date() time.Time
Affects() []guid.GUID
IsSynthetic() bool
}
Core message types:
Damage - All damage events (spell, melee, periodic, environmental)
Heal - Healing events
Aura - Buff/debuff applications and fades
Cast - Spell cast events (from CAST: v2 format)
ResourceChange - Mana/rage/energy gains/losses
Slain - Death events
Interrupt - Spell interrupts
ExtraAttack - Extra attack procs (Windfury, etc.)
Matcher Pattern
Each matcher function in matchers.go follows this pattern:
func (p *Parser) fEventName(ts time.Time, content string) ([]messages.Message, error) {
if !strings.HasPrefix(content, "PREFIX") {
return messages.NotHandled()
}
matches, ok := types.FromRegex(regexs.RePattern).Match(content)
if !ok {
return messages.NotHandled()
}
_, caster := matches.UnitOrGUID()
amount := matches.Int32()
if err := matches.Error(); err != nil {
return nil, fmt.Errorf("EventName: %w", err)
}
if caster.IsZero() || target.IsZero() {
return messages.Skip(ts, "EventName: not using guids"), nil
}
return set(messages.SomeMessage{
MessageBase: messages.Base(ts),
}), nil
}
Important: Matchers are tried in order (see parser.go lines 182-241). Put specific matchers before general ones.
Regex Helper
The types.FromRegex() helper provides sequential field extraction:
matches, ok := types.FromRegex(regexs.ReDamageSpellHitOrCrit).Match(content)
if !ok {
return messages.NotHandled()
}
_, caster := matches.UnitOrGUID()
spellName := matches.String()
hitType := matches.ShortHitType()
_, target := matches.UnitOrGUID()
amount := matches.Int32()
trailer := matches.Trailer()
Adding a New Event Type (Workflow)
Step 1: Identify the Log Format
Find example lines from actual combat logs:
11/18 07:21:45.192 0xF1400844930090A2's Firebolt hits 0xF130000950003FB5 for 38 Fire damage.
Step 2: Create the Regex
Add to combatlog/parser/regexs/regexs.go:
ReNewEventType = regexp.MustCompile(`(.+[^\\s])'s (.+[^\\s]) (cr|h)its (.+[^\\s]) for (\\d+) ([a-zA-Z]+) damage\\.\\s?(.*)`)
Regex tips:
- Use
(.+[^\\s]) for unit/GUID captures (handles both names and 0x...)
- Use
\\s?(.*) at end for optional trailer (absorbed/blocked/resisted)
- Test with
regexp.MustCompile to catch syntax errors at startup
Step 3: Create Message Type (if needed)
If existing types don't fit, add to combatlog/parser/vanilla/messages/message.go:
type NewEvent struct {
MessageBase
Caster guid.GUID
Target guid.GUID
SpellName string
Amount int32
}
func (n NewEvent) Affects() []guid.GUID {
return []guid.GUID{n.Caster, n.Target}
}
Step 4: Create Matcher Function
Add to combatlog/parser/vanilla/matchers.go:
func (p *Parser) fNewEvent(ts time.Time, content string) ([]messages.Message, error) {
matches, ok := types.FromRegex(regexs.ReNewEventType).Match(content)
if !ok {
return messages.NotHandled()
}
_, caster := matches.UnitOrGUID()
spellName := matches.String()
hitType := matches.ShortHitType()
_, target := matches.UnitOrGUID()
amount := matches.Int32()
school := matches.School()
trailer := matches.Trailer()
if err := matches.Error(); err != nil {
return nil, fmt.Errorf("NewEvent: %w", err)
}
if caster.IsZero() || target.IsZero() {
return messages.Skip(ts, "NewEvent: not using guids"), nil
}
return set(messages.Damage{
MessageBase: messages.Base(ts),
Caster: ptr.Ref(caster),
SpellName: ptr.Ref(spellName),
Target: target,
Amount: amount,
HitType: hitType,
School: school,
Trailer: trailer,
}), nil
}
Step 5: Register the Matcher
Add to the matcher list in parser.go ParseContent():
p.matchers = []parseLine{
p.fNewEvent,
}
Ordering matters: More specific patterns must come before general ones.
Step 6: Add Tests
Create test in combatlog/parser/vanilla/matchers_test.go:
func TestNewEvent(t *testing.T) {
t.Parallel()
p := newTestParser(t, `11/18 07:21:45.192 0xF1400844930090A2's Firebolt hits 0xF130000950003FB5 for 38 Fire damage.`)
msgs, err := p.Advance()
require.NoError(t, err)
require.Len(t, msgs, 1)
dmg, ok := msgs[0].(messages.Damage)
require.True(t, ok)
assert.Equal(t, int32(38), dmg.Amount)
assert.Equal(t, "Firebolt", *dmg.SpellName)
}
Code Examples
Parse a GUID
import "github.com/Emyrk/chronicle/combatlog/parser/guid"
gid, err := guid.FromString("0xF1400844930090A2")
if err != nil {
return err
}
if gid.IsPlayer() {
} else if gid.IsPet() {
} else if gid.IsCreature() {
entry, _ := gid.GetEntry()
}
Check HitType Flags
import "github.com/Emyrk/chronicle/combatlog/parser/types"
if dmg.HitType.Has(types.HitTypeCrit) {
}
if dmg.HitType.Has(types.HitTypePartialResist) {
}
Iterate Parser Output
parser, err := vanilla.New(logger, reader)
if err != nil {
return err
}
for {
msgs, err := parser.Advance()
if err == io.EOF {
break
}
if err != nil {
return err
}
for _, msg := range msgs {
switch m := msg.(type) {
case messages.Damage:
case messages.Heal:
case messages.SkippedMessage:
case messages.UnparsedLine:
log.Warn("unmatched line", "content", m.Content)
}
}
}
Anti-Patterns
Don't Use Greedy Patterns Without Anchors
regexp.MustCompile(`(.+) hits (.+) for (\d+)`)
regexp.MustCompile(`(.+[^\\s]) hits (.+[^\\s]) for (\\d+)`)
Don't Forget GUID Validation
return set(messages.Damage{Caster: ptr.Ref(caster), ...})
if caster.IsZero() || target.IsZero() {
return messages.Skip(ts, "not using guids"), nil
}
Don't Return Wrong Error Type
return nil, err
if !ok {
return messages.NotHandled()
}
Don't Duplicate Logic Across Matchers
func (p *Parser) fDamageSchool(...) { }
func (p *Parser) fDamageNoSchool(...) { }
func (p *Parser) fDamageSchool(ts, content) { return p.fDamage(true, ts, content) }
func (p *Parser) fDamageNoSchool(ts, content) { return p.fDamage(false, ts, content) }
Debugging Tips
- Check metrics -
parser.Metrics() shows time spent in each matcher
- Look for UnparsedLine - Messages of this type indicate unmatched lines
- Test regex online - Use regex101.com with Go flavor
- Print raw lines - Add logging in
Advance() to see preprocessing results
- Check GUID format - Old logs use names, new logs use
0x... format
Related Files
combatlog/parser/types/constants.go - HitType, School, Resource enums
combatlog/parser/types/trailer.go - Trailer parsing (absorbed, blocked, etc.)
combatlog/parser/vanilla/synthetic/ - Synthetic event generation
combatlog/parser/vanilla/whoami/ - "You" → player name resolution