The spark for EchoReplay came from one particular places: a Hypixel dev blog. They were discussing their replay system and the technical hurdles, specifically how recording raw packets creates massive files. While their optimized approach is impressive, I found myself intrigued by the path not taken. I thought, "How cool would it be to actually build a system that saves everything?"
That's what drove me to create this project. It's a hands-on exploration of a pure, packet-based replay system. The goal wasn't to be the most efficient, but to create a proof-of-concept that could capture and reconstruct a player's session with the highest possible fidelity, using the very data the client sees.
Sticking with Minecraft 1.8 gave me a stable foundation to dig into the server's core. This project was my playground for NMS, a deep dive into Netty, and a fun challenge to create my own serialization format. It’s a developer's log of a fun experiment, built for the love of the challenge.
The goal was to create a system that could record a gameplay session and play it back with frame-perfect accuracy, making it look as if the original players were still on the server.
This project is built on four core architectural pillars. Here’s a breakdown of the design philosophy behind each one.
To record anything, you first have to listen. The most efficient and non-intrusive way to access the raw data stream is to hook directly into the server's networking layer.
The Solution: Netty Pipeline Injection
Instead of listening for Bukkit events, EchoReplay injects a custom ChannelDuplexHandler directly into each player's Netty pipeline.
- Injection: When a recording starts (
PacketInterceptor.inject()), the system gets the player's underlying NettyChannel. It then adds a custom handler just before the server's mainpacket_handler. - Listening: This handler silently intercepts two key methods:
channelRead(): Fires for every packet coming from the client (e.g., movement, chat, arm swings).write(): Fires for every packet being sent to the client (e.g., health updates, other entity spawns).
- Passing Through: After recording the packet, the handler simply calls the next element in the pipeline (
super.channelRead()), allowing the server to process the packet as normal. This makes the entire recording process invisible.
Once a packet is intercepted, it needs to be processed and stored in a structured way. Simply saving a massive list of packets is not enough; they need to be grouped by time to allow for synchronized playback.
The Solution: A Frame-Based, Asynchronous Task
The ReplayRecorder is the heart of the recording process. When a recording starts, it kicks off a BukkitRunnable that acts as a master clock.
- The "Frame": Every single server tick (1/20th of a second), the task creates a
ReplayFrameobject. This object is a container, holding the currentticknumber, atimestamp, and a list of all packets that will be recorded during this tick. - Packet Collection: When the
PacketInterceptorcaptures a packet, it doesn't write it to a file immediately. Instead, it serializes the packet into aRecordedPacketobject and adds it to thecurrentFrame. - Flushing: At the start of the next tick, the
flushFrame()method saves the completed frame to a list in memory and a new, empty frame is created for the next tick. This ensures packets are perfectly grouped in the exact tick they occurred.
This "bucket" approach is critical for ensuring that actions that happen at the same time are played back at the same time.
A replay is only as good as the data it's saved in. To store the recording efficiently, EchoReplay uses a custom binary file format with the .erpl extension. This provides complete control over the data structure and is far more space-efficient than text-based formats like JSON or YAML.
The Solution: Sequential Binary Serialization
The ReplayFileWriter and ReplayFileReader classes manage this format. When a recording is stopped, the file is written in a precise order:
// .erpl Custom File Structure
[HEADER]
├─ Magic Bytes ("ECHO")
├─ Format Version (int)
└─ Timestamp (long)
[PLAYER DATA]
├─ Player Count (int)
└─ For each player:
├─ UUID, Name, Entity ID
├─ Initial Location (World, X, Y, Z, Yaw, Pitch)
├─ Initial Health, Inventory, Armor
└─ Skin Data (Value & Signature)
[FRAME DATA]
├─ Frame Count (int)
└─ For each frame:
├─ Tick, Timestamp (longs)
├─ Packet Count (int)
└─ For each packet:
├─ Packet Class Name (String)
├─ Direction (IN/OUT)
├─ Player UUID (who sent/received it)
└─ Serialized Packet Data (byte[])
This structured, binary approach allows the ReplayFileReader to load an entire recording into memory quickly and reliably, ready for playback.
This is where the data is brought to life. The playback system is designed around a "Director" and an "Actor."
-
The Director (
ReplayPlayer): This class is responsible for managing the playback session. It reads the.erplfile, spawns the "Actors," and runs a tick-basedBukkitRunnablethat serves as the playback clock. Each tick, it retrieves the nextReplayFramefrom the loaded data. -
The Actor (
ReplayNPC): This is the entity that re-enacts the recorded player's actions.- Creation: At the start of the replay, a
ReplayNPCis spawned for eachRecordedPlayerfound in the file. It's a true NMSEntityPlayerobject, complete with the original player's skin, name, and starting equipment. - Instruction: The
ReplayPlayer(the Director) loops through the packets in the current frame and passes them to the appropriateReplayNPC. - The Translator (
PacketInterpreter): The NPC doesn't just blindly handle raw packets. It uses aPacketInterpreterutility class. This is a crucial design choice that acts as an abstraction layer. The interpreter's job is to translate the raw, often obfuscated NMS packet data into meaningful information (e.g., extracting aLocationfrom aPacketPlayInFlying). - Action: A large
switchstatement in theReplayPlayerroutes the deserialized packet to the correct method on theReplayNPC(e.g.,npc.move(),npc.animateArm(),npc.chat()). The NPC then executes the action, creating a perfect playback of the original event.
- Creation: At the start of the replay, a
This Director/Actor/Translator model keeps the code clean, readable, and highly organized, separating the playback loop from the NMS entity manipulation.
- Netty Pipeline Injection: A high-performance, non-intrusive method for capturing raw packet data.
- Frame-Based Recording: A tick-synchronized system that groups packets temporally for perfect playback.
- Custom Binary File Format (
.erpl): An efficient, custom-designed file structure for storing replay data. - Full NMS Entity Control: Replays are performed by true
EntityPlayerNPCs, allowing for complete control over appearance, equipment, and animations. - Packet Serialization/Deserialization: A robust system for converting live NMS packet objects into byte arrays and back.
- Clean Architectural Design: Separation of concerns using models, handlers, and a
PacketInterpreterabstraction layer for readable and maintainable code.


