Skip to content

xabbo/goearth

Repository files navigation

goearth

Go extension API for the Habbo packet interceptor G-Earth.

Requirements

Requires Go 1.22+.

Usage

Check out the examples for reference.

Getting started

goearth CLI

The goearth CLI can be used to quickly create new extensions.

  1. 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.

  1. Create a new extension.
goearth new -title "New extension" -desc "A new goearth extension" -author "You"
  1. Move into the newly created extension directory: cd "New extension"
  2. 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.

Manual

  1. 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()
}
  1. Initialize a new module and install dependencies:
go mod init your-extension
go mod tidy
  1. 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.

Events

On extension initialized

ext.Initialized(func(e g.InitArgs) {
    log.Printf("Extension initialized (connected=%t)", e.Connected)
})

On game 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)
})

On extension activated

This is when the extension's green "play" button is clicked in G-Earth.

ext.Activated(func() {
    log.Println("Extension clicked in G-Earth")
})

On game disconnected

ext.Disconnected(func() {
    log.Println("Game disconnected")    
})

Intercepting packets

All packets

ext.InterceptAll(func (e *g.Intercept) {
    log.Printf("Intercepted %s message %q\n", e.Dir(), e.Name())
})

By 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)
})

Blocking packets

ext.Intercept(out.MoveAvatar).With(func (e *g.Intercept) {
    // prevent movement
    e.Block()
})

Modifying packets

ext.Intercept(in.Chat, in.Shout).With(func(e *g.Intercept) {
    // make everyone's chat messages uppercase
    e.Packet.ModifyStringAt(4, strings.ToUpper)
})

Reading packets

By type

x := pkt.ReadInt()
y := pkt.ReadInt()
z := pkt.ReadString()

By pointer

var x, y int
var z string
pkt.Read(&x, &y, &z)

Into a struct

type Tile struct {
    X, Y int
    Z    float32
}
tile := Tile{}
pkt.Read(&tile)

Using a custom parser by implementing Parsable

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)

Writing packets

By type

pkt.WriteInt(1)
pkt.WriteInt(2)
pkt.WriteString("3.0")

By values

// writes int, int, string
pkt.Write(1, 2, "3.0")

By struct

type Tile struct {
    X, Y int
    Z    float32
}
tile := Tile{X: 1, Y: 2, Z: 3.0}
pkt.Write(tile)

Using a custom composer by implementing Composable

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))
}

Sending packets

By values

// 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

By packet

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)

Receiving packets

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 Management

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)
}