Compare commits

..

9 Commits
v0.01 ... main

3 changed files with 146 additions and 92 deletions

View File

@ -9,12 +9,21 @@ The latter might include periodically:
* saving the game to a custom, datestamped save * saving the game to a custom, datestamped save
* generating datestamped screenshots * generating datestamped screenshots
## admin.go library
At the moment the library has limited support for anything except managing
responses to date changes. Please tell me your use case and I will be happy
to look at extending it further. Or, pull requests accepted :-)
## openttd_multitool ## openttd_multitool
The openttd_multitool connects to the OpenTTD Admin port (default 3977) The openttd_multitool connects to the OpenTTD Admin port (default 3977)
and stays connected. It monitors the game date, and performs your custom commands and stays connected. It monitors the game date, and performs your custom commands
at periodic intervals. at periodic intervals.
It supports string substitution of commands to embed the current (game) date
into commands or filenames.
Possible intervals are: Possible intervals are:
* daily * daily
@ -23,6 +32,20 @@ Possible intervals are:
These intervals are obviously in game time! These intervals are obviously in game time!
Possible string substitutions are:
* %Y - 4 digit year
* %M - 2 digit month
* %D - 2 digit day of month
## running
If you don't want to build from source, grab a binary release from
https://github.com/tardisx/openttd-admin/releases for your architecture.
The tool is a command line driven executable. It does not require installation.
Just copy it somewhere and run it.
You can configure the tool to send any command that you would type at the OpenTTD You can configure the tool to send any command that you would type at the OpenTTD
console. Here are a few examples: console. Here are a few examples:
@ -46,4 +69,3 @@ Additionally, when using the "screenshot giant" command, the entire server will
certainly kicking off all clients, unless your map is very small or your server is very very fast! certainly kicking off all clients, unless your map is very small or your server is very very fast!
No harm is done in that case, they can just reconnect after the screenshot is finished. No harm is done in that case, they can just reconnect after the screenshot is finished.

View File

@ -1,23 +1,18 @@
package main package main
import ( import (
"github.com/tardisx/openttd-admin/pkg"
"flag" "flag"
"strings" "github.com/tardisx/openttd-admin/pkg/admin"
"os" "os"
"strings"
) )
const currentVersion = "0.02"
// ./openttd_multitool --monthly "say \"hi it is a new month\"" --daily "say \"wow a new day %D"
const currentVersion = "0.01"
type dailyFlags []string type dailyFlags []string
type monthlyFlags []string type monthlyFlags []string
type yearlyFlags []string type yearlyFlags []string
func (i *dailyFlags) String() string { func (i *dailyFlags) String() string {
// change this, this is just can example to satisfy the interface // change this, this is just can example to satisfy the interface
return "my string representation" return "my string representation"

View File

@ -1,3 +1,5 @@
// Package admin provides an interface to connect to and manage a running
// OpenTTD dedicated server.
package admin package admin
// references // references
@ -9,21 +11,32 @@ package admin
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"log"
"net" "net"
"strings" "strings"
"time" "time"
"log"
) )
// OpenTTDServer - an object representing the server connection // OpenTTDServer - an object representing the server connection
type OpenTTDServer struct { type OpenTTDServer struct {
connection net.Conn connection net.Conn
serverName string ServerName string
ServerVersion string
ServerDedicated bool // is this a dedicated server?
MapName string
MapSeed uint32
MapLandscape byte
MapX uint16
MapY uint16
MapCreationDate time.Time
rconDaily []string rconDaily []string
rconMonthly []string rconMonthly []string
rconYearly []string rconYearly []string
connected chan bool connected chan bool
disconnected chan bool disconnected chan bool
frequencies [adminUpdateEND]uint16
} }
const ( const (
@ -88,7 +101,9 @@ const (
adminFrequencyAUTOMATIC = 0x40 ///< The admin gets information about this when it changes. adminFrequencyAUTOMATIC = 0x40 ///< The admin gets information about this when it changes.
) )
// Connect to the OpenTTD server on the admin port // Connect to the OpenTTD server on the admin port. Requires that the server
// is listening on the admin port (admin_password must be specified in the config).
// This method will block, and automatically attempt to reconnect if disconnected.
func (server *OpenTTDServer) Connect(host string, port int, password string, botName string, botVersion string) { func (server *OpenTTDServer) Connect(host string, port int, password string, botName string, botVersion string) {
for { for {
@ -160,7 +175,11 @@ func (server *OpenTTDServer) Connect(host string, port int, password string, bot
} }
// RegisterDateChange to send a command periodically // RegisterDateChange sends an arbitrary number of rcon commands when certain
// (game time) date changes occur. The possible periods are 'daily', 'monthly'
// and 'yearly'.
//
// Note that this must be called before Connect.
func (server *OpenTTDServer) RegisterDateChange(period string, command string) { func (server *OpenTTDServer) RegisterDateChange(period string, command string) {
if period == "daily" { if period == "daily" {
server.rconDaily = append(server.rconDaily, command) server.rconDaily = append(server.rconDaily, command)
@ -213,23 +232,17 @@ func processCommand(command string, dt time.Time) string {
} }
func (server *OpenTTDServer) sendSocket(protocol int, data []byte) { func (server *OpenTTDServer) sendSocket(protocol int, data []byte) {
// fmt.Printf("Going to send using protocol %v this data: %v\n", protocol, data)
toSend := make([]byte, 3) // start with 3 bytes for the length and protocol toSend := make([]byte, 3) // start with 3 bytes for the length and protocol
size := uint16(len(data) + 3) // size 2 bytes, plus protocol size := uint16(len(data) + 3) // size 2 bytes, plus protocol
binary.LittleEndian.PutUint16(toSend, size) binary.LittleEndian.PutUint16(toSend, size)
// toSend = append(toSend[:],
toSend[2] = byte(protocol) toSend[2] = byte(protocol)
toSend = append(toSend, data...) toSend = append(toSend, data...)
// fmt.Printf("Going to send this: %v\n", toSend)
server.connection.Write(toSend) server.connection.Write(toSend)
} }
func (server *OpenTTDServer) listenSocket() { func (server *OpenTTDServer) listenSocket() {
// fmt.Println("waiting for connection...")
// fmt.Printf("Listening to socket...\n")
<-server.connected <-server.connected
var chunk []byte var chunk []byte
SocketLoop: SocketLoop:
@ -273,23 +286,54 @@ SocketLoop:
packetSize := binary.LittleEndian.Uint16(chunk[0:2]) packetSize := binary.LittleEndian.Uint16(chunk[0:2])
// if we don't have enough bytes yet, just loop around // if we don't have enough bytes yet, just loop around
// fmt.Printf("current chunk %d bytes, indicated packet size %d\n", len(chunk), packetSize)
if packetSize > uint16(len(chunk)) { if packetSize > uint16(len(chunk)) {
// fmt.Printf("incomplete data, waiting for more from socket\n")
continue SocketLoop continue SocketLoop
} }
// so we are good to continue processing // so we are good to continue processing
packetType := int(chunk[2]) packetType := int(chunk[2])
packetData := chunk[3:packetSize] packetData := chunk[3:packetSize]
// fmt.Printf("packet type %d and size is %v bytes, I read %d from socket\n", packetType, len(packetData), s)
// figure out what type of packet this is and process it
if packetType == adminPacketServerPROTOCOL { if packetType == adminPacketServerPROTOCOL {
// fmt.Print(" - Got a adminPacketServerPROTOCOL packet\n") log.Println("received protocol packet")
adminUpdateDataStart := 1
var frequencies [adminUpdateEND]uint16
for i := 0; i < adminUpdateEND; i++ {
indexPos := adminUpdateDataStart+(i*5)+1
index := binary.LittleEndian.Uint16(packetData[indexPos:indexPos+2])
freqPos := adminUpdateDataStart+(i*5)+3
allowedFreq := binary.LittleEndian.Uint16(packetData[freqPos:freqPos+2])
frequencies[index] = allowedFreq
}
server.frequencies = frequencies
} else if packetType == adminPacketServerWELCOME { } else if packetType == adminPacketServerWELCOME {
log.Println("received welcome packet") log.Println("received welcome packet")
server.serverName = extractString(packetData[0:]) var next int
// fmt.Printf(" * server name: %s\n", serverName) server.ServerName, next = extractString(packetData[:], 0)
server.ServerVersion, next = extractString(packetData[:], next)
if packetData[next] == 0000 {
server.ServerDedicated = false
} else if packetData[next] == 0001 {
server.ServerDedicated = true
} else {
panic(fmt.Sprintf("not bool %v?\n", packetData[next]))
}
server.MapName, next = extractString(packetData[:], next+1)
server.MapSeed = binary.LittleEndian.Uint32(packetData[next : next+4])
server.MapLandscape = packetData[next+4]
creationDate := binary.LittleEndian.Uint32(packetData[next+5:next+9])
epochDate := time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC)
dt := epochDate.AddDate(0, 0, int(creationDate))
server.MapCreationDate = dt
server.MapX = binary.LittleEndian.Uint16(packetData[next+9 : next+11])
server.MapY = binary.LittleEndian.Uint16(packetData[next+11 : next+13])
log.Printf("server: %v\n", server)
} else if packetType == adminPacketServerSHUTDOWN { } else if packetType == adminPacketServerSHUTDOWN {
log.Println("server shutting down - will try to reconnect") log.Println("server shutting down - will try to reconnect")
server.connection = nil server.connection = nil
@ -297,39 +341,32 @@ SocketLoop:
return return
} else if packetType == adminPacketServerDATE { } else if packetType == adminPacketServerDATE {
// [[7 0 107 84 252 10 0 0 0
date := binary.LittleEndian.Uint32(packetData[0:4]) date := binary.LittleEndian.Uint32(packetData[0:4])
epochDate := time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC) epochDate := time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC)
dt := epochDate.AddDate(0, 0, int(date)) dt := epochDate.AddDate(0, 0, int(date))
// fmt.Printf(" * Date is %v\n", dt)
server.dateChanged(dt) server.dateChanged(dt)
// uint32
} else if packetType == adminPacketServerCHAT { } else if packetType == adminPacketServerCHAT {
// fmt.Printf(" - Got a chat packet:\n%v", packetData)
// [3 0 3 0 0 0 98 105 116 104 99 105 110 103 0 0 0 0 0 0 0 0 0]
chatAction := int8(packetData[0]) chatAction := int8(packetData[0])
chatDestType := int8(packetData[1]) chatDestType := int8(packetData[1])
chatClientID := binary.LittleEndian.Uint32(packetData[2:6]) chatClientID := binary.LittleEndian.Uint32(packetData[2:6])
chatMsg := extractString(packetData[6:]) chatMsg, _ := extractString(packetData[:], 6)
chatData := binary.LittleEndian.Uint64(packetData[len(packetData)-8:]) chatData := binary.LittleEndian.Uint64(packetData[len(packetData)-8:])
log.Printf("chat message: action %v desttype %v, client id %v msg %v data %v\n", chatAction, chatDestType, chatClientID, string(chatMsg), chatData) log.Printf("chat message: action %v desttype %v, client id %v msg %v data %v\n", chatAction, chatDestType, chatClientID, string(chatMsg), chatData)
} else if packetType == adminPacketServerRCON { } else if packetType == adminPacketServerRCON {
colour := binary.LittleEndian.Uint16(packetData[0:2]) colour := binary.LittleEndian.Uint16(packetData[0:2])
string := extractString(packetData[2:]) rconRecvString, _ := extractString(packetData[:], 2)
log.Printf("rcon: colour %v : %s\n", colour, string) log.Printf("rcon: colour %v : %s\n", colour, rconRecvString)
} else if packetType == adminPacketServerRCON_END { } else if packetType == adminPacketServerRCON_END {
string := extractString(packetData[0:]) rconEndRecvString, _ := extractString(packetData[:], 0)
log.Printf("rcon end : %s\n", string) log.Printf("rcon end : %s\n", rconEndRecvString)
} else { } else {
log.Printf("unknown packet received from server: %v [%v]\n", string(packetData), packetData) log.Printf("unknown packet received from server: %v [%v]\n", string(packetData), packetData)
} }
// fmt.Printf("removing the chunk we have processed\n")
chunk = chunk[packetSize:] chunk = chunk[packetSize:]
} }
// check if there is data left to process in the current data // check if there is data left to process in the current data
// fmt.Printf("remaining in chunk %d bytes", len(chunk))
if len(chunk) < 3 { if len(chunk) < 3 {
// we don't even have enough for a length and protocol type, so may // we don't even have enough for a length and protocol type, so may
// as well go sit on the socket // as well go sit on the socket
@ -340,13 +377,13 @@ SocketLoop:
} }
func extractString(bytes []byte) string { func extractString(bytes []byte, start int) (string, int) {
var str []byte var buildString []byte
for i := 0; i <= len(bytes); i++ { for i := start; i <= len(bytes); i++ {
if bytes[i] == 0 { if bytes[i] == 0 {
return string(str) return string(buildString), i + 1
} }
str = append(str, bytes[i]) buildString = append(buildString, bytes[i])
} }
return "" return "", -1
} }