Watches any number of ARK:SE servers (RCON for players and chat, the query port for map, version and player cap), records joins, leaves, chat and an audit trail, and keeps a player database across servers. Control: save, broadcast, chat, whisper, kick, time of day, MOTD, wild dino wipe, a raw console with history and macros, restarts with timed warnings and a world save, optional start command and auto-start. Bans are kept by Megalodon: timed, cluster-wide and re-enforced when the player returns. Automation: schedules (every N or at times of day), Discord/ntfy/webhook notifications, welcome message, chat word filter. Access: viewer, moderator and admin roles, API tokens, Prometheus metrics. cmd/fakeark simulates a server for trying it out. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
108 lines
3.2 KiB
Go
108 lines
3.2 KiB
Go
package ark
|
|
|
|
import (
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParsePlayers(t *testing.T) {
|
|
got := ParsePlayers("0. Alex, the Tamer, 76561198000000001\r\n1. Bo, 76561198000000002 \n")
|
|
if len(got) != 2 || got[0].Name != "Alex, the Tamer" || got[0].SteamID != "76561198000000001" || got[1].Name != "Bo" {
|
|
t.Fatalf("got %+v", got)
|
|
}
|
|
if n := len(ParsePlayers("No Players Connected \n")); n != 0 {
|
|
t.Fatalf("expected nobody, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestParseChat(t *testing.T) {
|
|
got := ParseChat("Alex (Rex Rider): hello: world\nSERVER: restarting soon\nsomething odd\n" + NoResponse + "\n")
|
|
if len(got) != 3 {
|
|
t.Fatalf("got %+v", got)
|
|
}
|
|
if a := got[0]; a.Sender != "Alex" || a.Character != "Rex Rider" || a.Message != "hello: world" || a.Server {
|
|
t.Fatalf("player line: %+v", a)
|
|
}
|
|
if !got[1].Server || got[1].Message != "restarting soon" {
|
|
t.Fatalf("server line: %+v", got[1])
|
|
}
|
|
if !got[2].Server || got[2].Message != "something odd" {
|
|
t.Fatalf("odd line: %+v", got[2])
|
|
}
|
|
if n := len(ParseChat("[2026.01.02-03.04.05] Bo: hi")); n != 1 {
|
|
t.Fatalf("timestamped line not parsed")
|
|
}
|
|
}
|
|
|
|
func TestCommandsAreSafe(t *testing.T) {
|
|
if _, err := ForPlayer("KickPlayer", "123; DoExit"); err == nil {
|
|
t.Fatal("a non-numeric id was accepted")
|
|
}
|
|
c, err := ForPlayer("KickPlayer", "76561198000000001")
|
|
if err != nil || c != "KickPlayer 76561198000000001" {
|
|
t.Fatalf("got %q, %v", c, err)
|
|
}
|
|
c, err = Broadcast("hi \"there\"\nDoExit")
|
|
if err != nil || c != "Broadcast hi 'there' DoExit" {
|
|
t.Fatalf("got %q, %v", c, err)
|
|
}
|
|
if _, err := Broadcast(" \n "); err == nil {
|
|
t.Fatal("empty message accepted")
|
|
}
|
|
if c, _ := SetTimeOfDay("6:30"); c != "SetTimeOfDay 6:30:00" {
|
|
t.Fatalf("got %q", c)
|
|
}
|
|
if _, err := SetTimeOfDay("25:00"); err == nil {
|
|
t.Fatal("bad time accepted")
|
|
}
|
|
if c, _ := ServerChatTo("76561198000000001", "hello"); c != `ServerChatTo "76561198000000001" hello` {
|
|
t.Fatalf("got %q", c)
|
|
}
|
|
}
|
|
|
|
func TestDuration(t *testing.T) {
|
|
for d, want := range map[time.Duration]string{
|
|
10 * time.Second: "10 seconds", time.Minute: "1 minute", 5 * time.Minute: "5 minutes", 3 * time.Hour: "3 hours",
|
|
} {
|
|
if got := Duration(d); got != want {
|
|
t.Errorf("%v: got %q, want %q", d, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A fake game server that asks for a challenge first, as current servers do.
|
|
func TestQuery(t *testing.T) {
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer pc.Close()
|
|
go func() {
|
|
buf := make([]byte, 100)
|
|
for {
|
|
n, from, err := pc.ReadFrom(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if n == len(a2sRequest) {
|
|
pc.WriteTo([]byte{0xff, 0xff, 0xff, 0xff, 0x41, 1, 2, 3, 4}, from)
|
|
continue
|
|
}
|
|
reply := []byte{0xff, 0xff, 0xff, 0xff, 0x49, 17}
|
|
for _, s := range []string{"Test Island - (v350.2)", "TheIsland", "ark_survival_evolved", "ARK: Survival Evolved"} {
|
|
reply = append(append(reply, s...), 0)
|
|
}
|
|
reply = append(reply, 0, 0, 3, 20, 0, 'd', 'w', 0, 1, '1', '.', '0', 0)
|
|
pc.WriteTo(reply, from)
|
|
}
|
|
}()
|
|
in, err := Query(pc.LocalAddr().String(), 2*time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if in.Name != "Test Island - (v350.2)" || in.Map != "TheIsland" || in.Players != 3 || in.MaxPlayers != 20 || in.Version != "350.2" {
|
|
t.Fatalf("got %+v", in)
|
|
}
|
|
}
|