Help us shape the future of Go! Take the 2026 Go Developer Survey

a2kit

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 21 Imported by: 0

README

a2kit

A passive AION 2 protocol toolkit, library and CLI (capture, decode, telemetry, analysis) with a structured a2log session format. No injection. No Npcap/WinPcap required.

CLI

Download a2k for your system from the releases, or install it as a CLI tool:

go install github.com/nuriland/a2kit/cmd/a2k@latest

On Linux, a2k needs libpcap to capture live, and libpcap-dev to build. A build without cgo reads recordings, but cannot capture.

Use
a2k help                                      # list all available commands
a2k watch                                     # what happens in the game, live
a2k record -log fight.jsonl                   # save the game's session
a2k show fight.pcap                           # read the saved game session
a2k export fight.pcap -o fight.jsonl          # a log to share
a2k timeline fight.pcap -o fight.trace.json   # a timeline reconstruction via ui.perfetto.dev
a2k stats fight.pcap                          # which message types came, and which a2k decodes
a2k conns down.pcap                           # the game's connections, -all for all TCP connections

Example usage of a2k show:

   21.050  Hit        actor=37365 target=15943 skill=11020000 damage=36 type=2 speed=10000

From the server list on, before a world server is picked:

    1.006  Servers    list=[1301:LIVE_Light_001 3245/7500 restricted 1302:LIVE_Light_002 2374/7000 ...]
    5.015  Redirect   server=1304 host=10.0.0.2 port=13328

Live capture

watch and record find the game's connection by themselves. Needs priviledged access to run on all platforms (Win, Linux, MacOS).

Windows
  • Doesn't require Npcap or WinPcap installed, it runs on native pktmon monitor.
  • It captures on every adapter at once (nics), a VPN's tunnel included, by default. a2k adapters lists the adapters.

Recording and sharing

a2k record saves a session two ways, and can do both at once:

  • -log fight.jsonl, a log of the game's messages only, with their raw bytes
  • fight.pcap, a recording of all the adapter's TCP traffic. DO NOT SHARE THESE PUBLICLY.

The log, a2k export and a2k timeline leave out the messages that name your account (Login, Account, Characters). -account keeps them. What show and watch print still includes them.

a2log

a2k record -log and a2k export write an a2log file format.

{"schema":"a2log/v0.1","decoder":"github.com/nuriland/a2kit@v0.3.0","source":{"kind":"pcap","path":"fight.pcap"},"t0":"2026-09-23T18:00:00.123Z"}
{"t":0,"opcode":"04 38","flags":["server"],"src":"10.0.0.2:13328","dst":"10.0.0.1:10000","payload":"x3wEAPWjAuAmqAAAAkvtn0EBAAAAkE4kAQA="}
{"t":51,"opcode":"42 36","flags":["server"],"src":"10.0.0.2:13328","dst":"10.0.0.1:10000","payload":"x3wAAw=="}

The header:

key
schema a2log/v0.1
decoder the a2kit module and version that wrote the log
source kind is pcap, live or feed -- path is the file, or the adapter
t0 the first message's time, UTC

Each message:

key
t milliseconds after t0, negative for a message completed out of order
opcode the message type, two bytes in wire order
flags server or client, then lz4, bundled, resynced
src, dst the endpoints
payload the bytes after the type, base64
a2k show fight.jsonl                                                                   # what the log says
jq -r 'select(.opcode) | .opcode' fight.jsonl | sort | uniq -c | sort -rn              # messages by type
jq -c 'select(.opcode == "04 38")' fight.jsonl                                         # the hits
jq -r 'select(.opcode == "04 38") | .payload' fight.jsonl | head -1 | base64 -d | xxd  # one hit's bytes

a2k show on the three lines above:

    0.000  Hit        actor=37365 target=15943 skill=11020000 damage=36 type=2 speed=10000
    0.051  Death      entity=15943 flag=3

a2kit.Open reads a log the same way it reads a recording. a2log.NewReader reads one on its own, and gives the frames game.Parse takes:

r, err := a2log.NewReader(file)
if err != nil {
	return err
}
for f, err := range r.Frames() {
	if err != nil {
		return err
	}
	e, _ := game.Parse(f) // nil when game does not read it
	if hit, ok := e.(game.Hit); ok {
		fmt.Println(hit.Actor, hit.Target, hit.Damage)
	}
}

Warning: A log holds what the game sent, players' names among it. Your account ID and character list are left out unless it was written with -account.

As a library

go get github.com/nuriland/a2kit
r, err := a2kit.Open("fight.pcap", a2kit.Config{})
if err != nil {
	return err
}
defer r.Close()

for m, err := range r.Messages() {
	if err != nil {
		return err
	}
	if hit, ok := m.Event.(game.Hit); ok {
		fmt.Println(hit.Actor, hit.Target, hit.Skill, hit.Damage)
	}
}
fmt.Printf("%+v\n", r.Summary()) // how many were decoded, the game's connection, what was lost

Work in progress.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func WriteTrace

func WriteTrace(w io.Writer, msgs iter.Seq2[Message, error]) error

WriteTrace writes msgs to w as a Chrome trace, to open in ui.perfetto.dev

Types

type Action

type Action struct {
	At       time.Time
	Sent     []int   // the sizes of the client's packets
	Answered []Count // the types the server sent after
}

Action is a moment the player acted, as the client's packets show it.

type Analysis

type Analysis struct {
	Server, Client netip.AddrPort

	Pairs       int           // server and client pairs the decoder locked on
	Start       time.Time     // the recording's first message, which a2k show counts from
	First, Last time.Time     // the pair's first and last message or client packet
	Messages    []Message     // the server's, by time
	Packets     int           // the client's packets that carried data
	Usual       []int         // the sizes the client sends every second or so, in order
	Background  []wire.Opcode // what the server sends whether the player acts or not, in order
	Summary     Summary       // of the whole recording
	// contains filtered or unexported fields
}

Analysis is a recording read for what its message types are, and what answered the player.

func Analyze

func Analyze(name string, cfg Config) (*Analysis, error)

func (*Analysis) Actions

func (a *Analysis) Actions(window time.Duration) []Action

Actions finds when the client sent packets of a size it does not send all the time, one within burstGap of the last joining its action, and what the server sent within window after, or until the next action.

func (*Analysis) Type

func (a *Analysis) Type(op wire.Opcode) Detail

Type is op's messages, position by position.

func (*Analysis) Types

func (a *Analysis) Types(marks []time.Time, window time.Duration) []TypeStat

Types adds up each type the server sent, most first. With marks, moments the player acted, it also counts how each type followed them within window, and sorts by that.

type Config

type Config struct {
	Client bool         // keep the client's messages too
	Record io.Writer    // Capture, also write every packet to it, as a pcap
	Logger *slog.Logger // what the decoder does -- resyncs, locks, lost gaps, nil is silent
}

type Connection added in v0.5.0

type Connection struct {
	Client, Server netip.AddrPort
	IfIndex        int // the adapter it was seen on, a packet seen on two is two connections
	Start, End     time.Time
	SYN, SYNACK    int
	FIN, RST       bool
	Sent, Received int
	Game           bool
	Status         string
}

Connection is one TCP connection as a recording shows it, from its first packet to its last.

func Connections added in v0.5.0

func Connections(name string, cfg Config) ([]Connection, error)

Connections lists a recording's TCP connections in the order they began, with what became of each, and marks the game's.

func (Connection) Failed added in v0.5.0

func (c Connection) Failed() bool

type Count

type Count struct {
	Type wire.Opcode
	N    int
}

type Detail

type Detail struct {
	Messages []Message      // the type's, by time
	Sizes    map[int]int    // payload size to messages
	Longest  int            // the longest payload
	Bytes    []map[byte]int // for each of the first positions, how often it took each value
	Values   int            // the values the first varint of a payload takes
	Shared   []Count        // other types that begin with some of those values, and how many, most first
}

Detail is one type's messages, position by position.

type Message

type Message struct {
	wire.Frame
	Event game.Event // nil unless game reads it
	Err   error      // game.ErrUnread, or one wrapping game.ErrLayout
}

Message is one of the game's frames, and what game reads of it.

func (Message) Failed

func (m Message) Failed() bool

Failed reports whether m's bytes do not fit the layout game reads for its type, most likely after a game patch.

type Reader

type Reader struct {
	Source a2log.Source // where the frames came from. For a log, what its header says
	Log    bool         // it reads a2log, not packets, there is no decoder, so no Stats
	// contains filtered or unexported fields
}

func Capture

func Capture(ctx context.Context, adapter string, cfg Config) (*Reader, error)

func New

func New(d *wire.Decoder, src wire.SegmentReader) *Reader

func Open

func Open(name string, cfg Config) (*Reader, error)

func OpenReader

func OpenReader(r io.Reader, cfg Config) (*Reader, error)

func (*Reader) Close

func (r *Reader) Close() error

func (*Reader) Messages

func (r *Reader) Messages() iter.Seq2[Message, error]

func (*Reader) Summary

func (r *Reader) Summary() Summary

type Summary

type Summary struct {
	wire.Stats // Analytical stats of the source read

	Messages int   // the game's messages
	Decoded  int   // those game reads
	Unread   int   // those it does not read yet
	Failed   int   // those whose bytes do not fit what it reads, most likely after a game patch
	CutShort int   // segments the recording cut short, whose bytes are lost
	Lost     error // what a live capture lost without failing, once it is closed
}

type TypeStat

type TypeStat struct {
	Type wire.Opcode

	Sizes      map[int]int // payload size to messages
	Count      int         // the total number of messages of this type
	Decoded    int         // those game reads
	Failed     int         // those off the layout game reads
	Background bool        // it comes whether the player acts or not

	// with marks
	Marks  int     // the marks it followed within the window
	Within int     // its messages inside a mark's window
	Idle   float64 // its messages a second outside the windows, NaN if they cover the session
}

TypeStat is what the messages of one type add up to.

Directories

Path Synopsis
Package a2log is the fight log: a Header line, then one Frame a line, each a JSON object.
Package a2log is the fight log: a Header line, then one Frame a line, each a JSON object.
Package capture reads TCP segments from pcap and pcapng files, and from a live device.
Package capture reads TCP segments from pcap and pcapng files, and from a live device.
cmd
a2k command
Package game reads the meaning of AION 2 frames, like hits, casts, spawns, deaths, positions and the clocks.
Package game reads the meaning of AION 2 frames, like hits, casts, spawns, deaths, positions and the clocks.
internal
genfixtures command
Genfixtures writes the synthesized test fixtures: TCP payload streams for package wire, and a pcap for package capture, each with the frames it holds.
Genfixtures writes the synthesized test fixtures: TCP payload streams for package wire, and a pcap for package capture, each with the frames it holds.
score command
Score measures the decoder on synthesized captures whose frames are known: how many of them it finds, and how many it makes up.
Score measures the decoder on synthesized captures whose frames are known: how many of them it finds, and how many it makes up.
Package wire turns TCP segments into AION 2 game frames.
Package wire turns TCP segments into AION 2 game frames.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL