Go extension API for the Habbo packet interceptor G-Earth.
Requires Go 1.22+.
Check out the examples for reference.
The goearth
CLI can be used to quickly create new extensions.
- Install the
goearth
CLI.
go install xabbo.b7c.io/goearth/cmd/goearth@latest
Once it is installed, you can view the command usage with goearth new -h
.
- Create a new extension.
goearth new -title "New extension" -desc "A new goearth extension" -author "You"
- Move into the newly created extension directory:
cd "New extension"
- Run the extension with
go run .
- you should see the extension appear in G-Earth's extension list.
You may specify a target client with the -c
flag, currently either flash
(default) or shockwave
.
- Create a new directory and save the following code as
main.go
.
package main
import g "xabbo.b7c.io/goearth"
var ext = g.NewExt(g.ExtInfo{
Title: "Your extension",
Description: "Go Earth!",
Version: "1.0",
Author: "You",
})
func main() {
// register event handlers and interceptors here
ext.Run()
}
- Initialize a new module and install dependencies:
go mod init your-extension
go mod tidy
- Run the extension:
go run .
Import the in
/out
packages to access the respective incoming/outgoing message identifiers.
import "xabbo.b7c.io/goearth/in"
import "xabbo.b7c.io/goearth/out"
For the Shockwave messages, use the xabbo.b7c.io/goearth/shockwave/in
and out
packages.
ext.Initialized(func(e g.InitArgs) {
log.Printf("Extension initialized (connected=%t)", e.Connected)
})
ext.Connected(func(e g.ConnectArgs) {
log.Printf("Game connected (%s:%d)", e.Host, e.Port)
log.Printf("Client %s (%s)", e.Client.Identifier, e.Client.Version)
})
This is when the extension's green "play" button is clicked in G-Earth.
ext.Activated(func() {
log.Println("Extension clicked in G-Earth")
})
ext.Disconnected(func() {
log.Println("Game disconnected")
})
ext.InterceptAll(func (e *g.Intercept) {
log.Printf("Intercepted %s message %q\n", e.Dir(), e.Name())
})
ext.Intercept(in.Chat, in.Shout, in.Whisper).With(func (e *g.Intercept) {
idx := e.Packet.ReadInt()
msg := e.Packet.ReadString()
log.Printf("Entity #%d said %q", idx, msg)
})
ext.Intercept(out.MoveAvatar).With(func (e *g.Intercept) {
// prevent movement
e.Block()
})
ext.Intercept(in.Chat, in.Shout).With(func(e *g.Intercept) {
// make everyone's chat messages uppercase
e.Packet.ModifyStringAt(4, strings.ToUpper)
})
x := pkt.ReadInt()
y := pkt.ReadInt()
z := pkt.ReadString()
var x, y int
var z string
pkt.Read(&x, &y, &z)
type Tile struct {
X, Y int
Z float32
}
tile := Tile{}
pkt.Read(&tile)
type Tile struct {
X, Y int
Z float32
}
func (v *Tile) Parse(p *g.Packet, pos *int) {
// perform custom parsing logic here
// make sure to use the Read*Ptr variants here
// to ensure the provided position is advanced properly
x := p.ReadIntPtr(pos)
y := p.ReadIntPtr(pos)
zStr := p.ReadStringPtr(pos)
z, err := strconv.ParseFloat(zStr, 32)
if err != nil {
panic(err)
}
*v = Tile{ X: x, Y: y, Z: float32(z) }
}
tile := Tile{}
// Tile.Parse(...) will be invoked
pkt.Read(&tile)
pkt.WriteInt(1)
pkt.WriteInt(2)
pkt.WriteString("3.0")
// writes int, int, string
pkt.Write(1, 2, "3.0")
type Tile struct {
X, Y int
Z float32
}
tile := Tile{X: 1, Y: 2, Z: 3.0}
pkt.Write(tile)
type Tile struct {
X, Y int
Z float32
}
func (v Tile) Compose(p *g.Packet, pos *int) {
// perform custom composing logic here
// make sure to use the Write*Ptr variants here
// to ensure the provided position is advanced properly
p.WriteIntPtr(pos, v.X)
p.WriteIntPtr(pos, v.Y)
p.WriteStringPtr(pos, strconv.FormatFloat(v.Z, 'f', -1, 32))
}
// to server
ext.Send(out.Chat, "hello, world", 0, -1)
// to client
ext.Send(in.Chat, 0, "hello, world", 0, 34, 0, 0)
// take care when sending packets to the client
// as badly formed packets will crash the game client
pkt := ext.NewPacket(in.Chat)
pkt.WriteInt(0)
pkt.WriteString("hello, world")
pkt.WriteInt(0)
pkt.WriteInt(34)
pkt.WriteInt(0)
pkt.WriteInt(0)
ext.SendPacket(pkt)
log.Println("Retrieving user info...")
ext.Send(out.InfoRetrieve)
if pkt := ext.Recv(in.UserObject).Wait(); pkt != nil {
id, name := pkt.ReadInt(), pkt.ReadString()
log.Printf("Got user info (id: %d, name: %q)", id, name)
} else {
log.Println("Timed out")
}
Note: do not perform any long running operations inside an intercept handler.
If you attempt to Wait
for a packet inside an intercept handler,
you will never receive it as the packet's processing loop will be paused until it times out.
Launch a goroutine with the go
keyword if you need to do this inside an intercept handler, for example:
ext.Intercept(in.Chat).With(func(e *g.Intercept) {
// also, do not pass Packets to another goroutine as its buffer may no longer be valid.
// read any values within the intercept handler and then pass those.
msg := e.Packet.ReadStringAt(4)
go func() {
// perform long running operation here...
time.Sleep(10 * time.Second)
ext.Send(out.Shout, msg)
}()
})
Game state managers are currently provided for shockwave in the xabbo.b7c.io/goearth/shockwave/profile
, room
, inventory
, and trade
packages.
These track the state of the game and allow you to subscribe to events, for example, here is a basic chatlog extension:
package main
import (
"fmt"
g "xabbo.b7c.io/goearth"
"xabbo.b7c.io/goearth/shockwave/room"
)
var (
ext = g.NewExt(g.ExtInfo{Title: "Chat log example"})
roomMgr = room.NewManager(ext)
)
func main() {
roomMgr.EntityChat(onEntityChat)
ext.Run()
}
func onEntityChat(e room.EntityChatArgs) {
fmt.Printf("%s: %s\n", e.Entity.Name, e.Message)
}