From 5fad9f085e23bd15f4315d18dbe287ab4f29c5b8 Mon Sep 17 00:00:00 2001 From: shoopea Date: Sat, 6 Nov 2021 23:33:16 +0800 Subject: [PATCH] big revamp --- bot.go | 229 +++++++++++--- config.go | 118 ++++++++ config.sample.json | 18 ++ generate.go | 3 + go.mod | 5 +- go.sum | 2 + main.go | 337 ++------------------- packet.go | 136 +++++++++ ttd.go | 729 +++++++++++++++++++++++++++++++++++++++++++++ version.go | 6 + version.sh | 13 + 11 files changed, 1245 insertions(+), 351 deletions(-) create mode 100644 config.go create mode 100644 config.sample.json create mode 100644 generate.go create mode 100644 ttd.go create mode 100644 version.go create mode 100644 version.sh diff --git a/bot.go b/bot.go index 2e64aa2..c99c9f9 100644 --- a/bot.go +++ b/bot.go @@ -2,57 +2,209 @@ package main import ( "encoding/json" + "fmt" + "regexp" + "strconv" + "time" tb "gopkg.in/tucnak/telebot.v2" ) -func BotHandlers(b *tb.Bot) { +type Bot struct { + bot *tb.Bot + Config *TelegramConfig +} - b.Handle("/pause", botPause) - b.Handle("/unpause", botUnpause) +func (b *Bot) Start() { + var err error + b.bot, err = tb.NewBot(tb.Settings{ + Token: b.Config.Token, + URL: b.Config.URL, + Poller: &tb.LongPoller{Timeout: 10 * time.Second}, + }) + failError(err, "Bot.Start() : registering bot") - b.Handle(tb.OnPhoto, botPhoto) - b.Handle(tb.OnChannelPost, botChannelPost) - b.Handle(tb.OnQuery, botQuery) - b.Handle(tb.OnText, botText) - b.Handle(tb.OnDocument, botDocument) + b.BotHandlers() +} - b.Start() +func (b *Bot) SendUser(id int64, msg string) { + u := tb.User{ + ID: int(id), + } + _, err := b.bot.Send(&u, msg) + logErrorDebug(err, "Bot.SendUser()") +} + +func (b *Bot) SendChat(chatID int64, text string) { + opt := tb.SendOptions{ + ParseMode: tb.ModeDefault, + } + + ch := tb.Chat{ + ID: chatID, + } + _, err := b.bot.Send(&ch, text, &opt) + logErrorDebug(err, "Bot.SendChat()") +} + +func (b *Bot) BotHandlers() { + + b.bot.Handle("/pause", botPause) + b.bot.Handle("/unpause", botUnpause) + b.bot.Handle("/register", botRegister) + b.bot.Handle("/delete", botDelete) + b.bot.Handle("/companies", botCompanies) + b.bot.Handle("/clients", botClients) + b.bot.Handle("/players", botPlayers) + + b.bot.Handle(tb.OnPhoto, botPhoto) + b.bot.Handle(tb.OnChannelPost, botChannelPost) + b.bot.Handle(tb.OnQuery, botQuery) + b.bot.Handle(tb.OnText, botText) + b.bot.Handle(tb.OnDocument, botDocument) + + go func() { + time.Sleep(time.Second) + b.SendUser(b.Config.AdminID, fmt.Sprintf("Started (%s)", version)) + }() + + b.bot.Start() } func botPause(m *tb.Message) { - forcePaused = true - if !paused { - paused = true - px := PacketAdminRCon{ - Packet: Packet{PType: AdminPacketAdminRCon}, - Command: "pause", + PrintText(m) + for userID, cc := range cfg.Clients { + if userID == m.Sender.ID { + if co, ok := srv.Status.Companies[cc.CompanyID]; ok { + if clt, ok2 := srv.Status.Clients[co.ClientID]; ok2 { + clt.Paused = true + } + } } - _, err := conn.Write(px.Bytes()) - logErrorDebug(err, "botPause : conn.Write") - sendChat(-436055948, "Game paused.") + } + if !srv.Status.Paused { + srv.Pause() + bot.SendChat(bot.Config.ChatID, "Game paused.") } else { - sendChat(-436055948, "Game already paused.") + bot.SendChat(bot.Config.ChatID, "Game already paused.") } return } func botUnpause(m *tb.Message) { - forcePaused = false - if paused && len(clients) > 1 { - paused = false - px := PacketAdminRCon{ - Packet: Packet{PType: AdminPacketAdminRCon}, - Command: "unpause", + PrintText(m) + for userID, cc := range cfg.Clients { + if userID == m.Sender.ID { + if co, ok := srv.Status.Companies[cc.CompanyID]; ok { + if clt, ok2 := srv.Status.Clients[co.ClientID]; ok2 { + clt.Paused = false + } + } } - _, err := conn.Write(px.Bytes()) - logErrorDebug(err, "botUnpause : conn.Write") - sendChat(-436055948, "Game unpaused.") - } else if len(clients) == 1 { - sendChat(-436055948, "No players, cannot unpause.") - } else { - sendChat(-436055948, "Game already unpaused.") } + if !srv.NeedPause() { + srv.Unpause() + bot.SendChat(bot.Config.ChatID, "Game unpaused.") + } else { + bot.SendChat(bot.Config.ChatID, fmt.Sprintf("Cannot unpause : %s", srv.NeedPauseReason())) + } + return +} + +func botDelete(m *tb.Message) { + logInfoDebug("[%d] %s(%d) | %s(%d) : delete : %s\n", m.ID, m.Chat.Title, m.Chat.ID, m.Sender.Username, m.Sender.ID, m.Text) + if m.Sender.ID == int(bot.Config.AdminID) { + r := regexp.MustCompile("/delete (?P[0-9]+)") + ID64, _ := strconv.ParseInt(r.ReplaceAllString(m.Text, "${CompanyID}"), 10, 64) + srv.DeleteCompany(uint8(ID64)) + bot.SendChat(m.Chat.ID, "Deleting") + } else { + bot.SendChat(m.Chat.ID, "Not authorized to delete") + } + return +} + +func botCompanies(m *tb.Message) { + str := "Companies :" + for k, v := range srv.Status.Companies { + str = str + "\r\n" + fmt.Sprintf(" - %s (%d)", v.Name, k) + } + bot.SendChat(m.Chat.ID, str) +} + +func botClients(m *tb.Message) { + str := "Clients :" + for k, v := range srv.Status.Clients { + str = str + "\r\n" + fmt.Sprintf(" - %s (%d) : company #%d", v.Name, k, v.CompanyID) + } + bot.SendChat(m.Chat.ID, str) +} + +func botPlayers(m *tb.Message) { + online := "" + for _, cc := range cfg.Clients { + if cc.Online { + online = online + fmt.Sprintf(" - @%s (%s) : %s", cc.Username, cc.TimeLeft, srv.Status.Companies[cc.CompanyID].Name) + "\r\n" + } + } + offline := "" + for _, cc := range cfg.Clients { + if !cc.Online { + offline = offline + fmt.Sprintf(" - @%s (%s) : %s", cc.Username, cc.TimeLeft, srv.Status.Companies[cc.CompanyID].Name) + "\r\n" + } + } + str := "" + if len(online) > 0 { + str = str + "Players online :\r\n" + online + } + if len(offline) > 0 { + str = str + "Players offline :\r\n" + offline + } + if str == "" { + str = "No players." + } + bot.SendChat(m.Chat.ID, str) +} + +func botRegister(m *tb.Message) { + cc, ok := cfg.Clients[m.Sender.ID] + if !ok { + cc = &ClientConfig{ + UserID: m.Sender.ID, + Username: m.Sender.Username, + CompanyID: 255, + } + cfg.Clients[m.Sender.ID] = cc + } + coList := make(map[uint8]struct{}) + for coID, _ := range srv.Status.Companies { + coList[coID] = struct{}{} + } + for _, c := range cfg.Clients { + if c.CompanyID != 255 { + if _, ok := coList[c.CompanyID]; !ok { + logInfoAlert("botRegister : %s : no such CompanyID : %d", c.Username, c.CompanyID) + c.CompanyID = 255 + } else { + delete(coList, c.CompanyID) + } + + } + } + if len(coList) == 0 { + bot.SendChat(m.Chat.ID, "No company to register") + return + } + if len(coList) == 1 { + for id, _ := range coList { + cc.CompanyID = id + days := int(time.Now().Sub(cfg.Game.StartDate).Hours() / 24) + cc.TimeLeft = cfg.Game.StartingAllotment + cfg.Game.DailyAllotment*time.Duration(days) + bot.SendChat(m.Chat.ID, fmt.Sprintf("@%s registered %s (with %s playable)", cc.Username, srv.Status.Companies[cc.CompanyID].Name, cc.TimeLeft)) + return + } + } + bot.SendChat(m.Chat.ID, "More than one company unregistered. Wait for bot update (poke @tiennou)") return } @@ -75,7 +227,6 @@ func botChannelPost(m *tb.Message) { PrintText(m) b, _ := json.Marshal(m) logInfoDebug("botChannelPost : %s\n", string(b)) - // channel posts only } @@ -87,15 +238,3 @@ func botQuery(q *tb.Query) { func botText(m *tb.Message) { PrintText(m) } - -func sendChat(chatID int64, text string) { - opt := tb.SendOptions{ - ParseMode: tb.ModeDefault, - } - - ch := tb.Chat{ - ID: chatID, - } - _, err := bot.Send(&ch, text, &opt) - logErrorDebug(err, "sendChat") -} diff --git a/config.go b/config.go new file mode 100644 index 0000000..3046ba8 --- /dev/null +++ b/config.go @@ -0,0 +1,118 @@ +package main + +import ( + _ "embed" + "encoding/json" + "io/ioutil" + "time" + + "github.com/tidwall/pretty" +) + +//go:embed config.sample.json +var cfgSample []byte + +type ServerConfig struct { + Addr string `json:"addr"` + Passwd string `json:"passwd"` +} + +type TelegramConfig struct { + AdminID int64 `json:"admin_id"` + ChatID int64 `json:"chat_id"` + URL string `json:"url"` + Token string `json:"token"` +} + +type GameConfig struct { + TimeZone string `json:"timezone"` + DailyAllotment time.Duration `json:"daily_allotment"` + StartingAllotment time.Duration `json:"starting_allotment"` + Threshold time.Duration `json:"threshold"` + StartDate time.Time `json:"start_date"` +} + +type ClientConfig struct { + UserID int `json:"user_id"` + Username string `json:"username"` + Online bool `json:"online"` + TimeLeft time.Duration `json:"time_left"` + CompanyID uint8 `json:"company_id` +} + +type Config struct { + Server *ServerConfig `json:"server"` + Telegram *TelegramConfig `json:"telegram"` + Game *GameConfig `json:"game"` + Clients map[int]*ClientConfig `json:"clients"` +} + +// Init values for a config based on package defaults +func (c *Config) Init() error { + err := json.Unmarshal(cfgSample, &c) + c.Clients = make(map[int]*ClientConfig) + if err != nil { + return err + } + return nil +} + +// Load config values from a given file +func (c *Config) Load(path string) error { + err := c.Init() + if err != nil { + return err + } + + b, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + err = json.Unmarshal(b, &c) + if err != nil { + return err + } + + return nil +} + +// Save config values to a given file +func (c *Config) Save(path string) error { + b, err := c.Export() + if err != nil { + return nil + } + + err = ioutil.WriteFile(path, b, 0644) + + return err +} + +// Export config values as a bytestream +func (c *Config) Export() ([]byte, error) { + b, err := json.Marshal(c) + if err != nil { + return b, err + } + return pretty.Pretty(b), nil + +} + +func (c *Config) CompanyIsRegistered(id uint8) bool { + for _, cc := range c.Clients { + if cc.CompanyID == id { + return true + } + } + return false +} + +func (c *Config) GetCompanyClient(id uint8) *ClientConfig { + for _, cc := range c.Clients { + if cc.CompanyID == id { + return cc + } + } + return nil +} diff --git a/config.sample.json b/config.sample.json new file mode 100644 index 0000000..c021423 --- /dev/null +++ b/config.sample.json @@ -0,0 +1,18 @@ +{ + "server":{ + "addr":"heidi.siteop.biz:3977", + "passwd":"plop" + }, + "telegram":{ + "admin_id":32173684, + "chat_id":-436055948, + "url":"https://api.telegram.org", + "token":"954090437:AAEMYeUWGluKZRwXi_K3-T23ZVpFoqQAmu0" + }, + "game":{ + "daily_allotment":1200000000000, + "starting_allotment":6000000000000, + "threshold":600000000000, + "start_date":"2021-11-01T17:22:28+02:00" + } +} \ No newline at end of file diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..8b92de7 --- /dev/null +++ b/generate.go @@ -0,0 +1,3 @@ +package main + +//go:generate bash ./version.sh diff --git a/go.mod b/go.mod index 14e8c86..b5c94d5 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module git.siteop.biz/shoopea/gottdad go 1.16 -require gopkg.in/tucnak/telebot.v2 v2.4.0 +require ( + github.com/tidwall/pretty v1.2.0 + gopkg.in/tucnak/telebot.v2 v2.4.0 +) diff --git a/go.sum b/go.sum index c142393..ffd1e07 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tucnak/telebot.v2 v2.4.0 h1:nOeqOWnOAD3dzbKW+NRumd8zjj5vrWwSa0WRTxvgfag= gopkg.in/tucnak/telebot.v2 v2.4.0/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= diff --git a/main.go b/main.go index 6fb740f..84c6533 100644 --- a/main.go +++ b/main.go @@ -1,328 +1,55 @@ package main import ( - "bufio" - "encoding/binary" - "fmt" - "net" - "regexp" - "strconv" + "flag" "time" - tb "gopkg.in/tucnak/telebot.v2" + _ "embed" ) -type Client struct { - ClientID uint32 - Name string - Address string - CompanyID uint8 -} - var ( - bot *tb.Bot - clients map[uint32]*Client - paused bool = true - forcePaused bool = true - pausedBy string + cfg *Config + srv *ServerTTD + bot *Bot - githash string - buildstamp string - commits string - - conn net.Conn + configFlag = flag.String("config", "config.json", "config file") + initFlag = flag.Bool("init", false, "init config") ) func main() { var err error - clients = make(map[uint32]*Client) + flag.Parse() - conn, err = net.Dial("tcp", "poop.siteop.biz:3977") - failError(err, "net.Dial") - logInfoDebug("Connected to poop.siteop.biz:3977") - - //send auth - p := PacketAdminJoin{ - Packet: Packet{PType: AdminPacketAdminJoin}, - Password: "plop", - AppName: "gottdad", - AppVersion: "alpha", + cfg = &Config{} + if *initFlag { + err = cfg.Init() + failError(err, "Cannot init config") + err = cfg.Save(*configFlag) + failError(err, "Cannot save config") + } else { + err = cfg.Load(*configFlag) + failError(err, "Cannot open config") } - _, err = conn.Write(p.Bytes()) - failError(err, "conn.Write") - r := bufio.NewReader(conn) - b := make([]byte, 0xFFFF) - read := 0 - n := 0 + + logInfoWarn("Starting up (%s) ...", version) // Registering bot - bot, err = tb.NewBot(tb.Settings{ - Token: "954090437:AAEMYeUWGluKZRwXi_K3-T23ZVpFoqQAmu0", - URL: "https://api.telegram.org", - Poller: &tb.LongPoller{Timeout: 10 * time.Second}, - }) - failError(err, "Registering bot") - logInfoDebug("Connected to Telegram") - - go BotHandlers(bot) - - u := tb.User{ - ID: int(32173684), + bot = &Bot{ + Config: cfg.Telegram, } - bot.Send(&u, fmt.Sprintf("Started (%s-b%s - %s)", githash, commits, buildstamp)) + go bot.Start() + + time.Sleep(1 * time.Second) + + srv = &ServerTTD{ + Config: cfg.Server, + Data: &ServerDataTTD{}, + Status: &ServerStatusTTD{}, + } + go srv.Start() for { - p := Packet{} - - for { - if read >= 3 { - //logInfoDebug("Packet read") - break - } - n, err = r.Read(b[read:]) - logErrorDebug(err, "r.Read") - read += n - //logInfoDebug("Waiting for packet, read %d bytes.", read) - - } - p.PLength = binary.LittleEndian.Uint16(b[0:]) - p.PType = b[2] - if p.PLength <= 3 { - logInfoAlert("Wrong packet length") - break - } - - //logInfoDebug("Waiting for packet data : len : %d / type : %d", p.PLength, p.PType) - - for { - if read >= int(p.PLength) { - //logInfoDebug("Data read") - break - } - n, err = r.Read(b[read:]) - logErrorDebug(err, "r.Read") - read += n - //logInfoDebug("Waiting for data, read %d/%d bytes.", read, p.PLength) - - } - - switch p.PType { - case AdminPacketServerProtocol: - sp := PacketServerProtocol{ - Packet: p, - } - sp.Read(b[:p.PLength]) - logInfoDebug("AdminPacketServerProtocol :\n- ProtocolVersion: %v\n- FurtherData: %v\n- UpdatePacketType: %v\n- FrequenciesAllowed: %b", sp.ProtocolVersion, sp.FurtherData, sp.UpdatePacketType, sp.FrequenciesAllowed) - case AdminPacketServerWelcome: - sp := PacketServerWelcome{ - Packet: p, - } - sp.Read(b[:p.PLength]) - logInfoDebug("AdminPacketServerWelcome :\n- ServerName: %v\n- OpenTTDVersion: %v\n- Dedicated: %v\n- MapSeed: %x\n- MapLandscape: %v\n- MapStartDate: %v\n- Size: %v x %v", sp.ServerName, sp.OpenTTDVersion, sp.Dedicated, sp.MapSeed, sp.MapLandscape, sp.MapStartDate, sp.MapX, sp.MapY) - px := PacketAdminUpdateFrequency{ - Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, - UpdateType: AdminUpdateDate, - UpdateFrequency: AdminFrequencyDaily, - } - _, err = conn.Write(px.Bytes()) - - px = PacketAdminUpdateFrequency{ - Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, - UpdateType: AdminUpdateClientInfo, - UpdateFrequency: AdminFrequencyAutomatic, - } - _, err = conn.Write(px.Bytes()) - - px = PacketAdminUpdateFrequency{ - Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, - UpdateType: AdminUpdateCompanyInfo, - UpdateFrequency: AdminFrequencyAutomatic, - } - _, err = conn.Write(px.Bytes()) - - px = PacketAdminUpdateFrequency{ - Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, - UpdateType: AdminUpdateChat, - UpdateFrequency: AdminFrequencyAutomatic, - } - _, err = conn.Write(px.Bytes()) - - px = PacketAdminUpdateFrequency{ - Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, - UpdateType: AdminUpdateConsole, - UpdateFrequency: AdminFrequencyAutomatic, - } - _, err = conn.Write(px.Bytes()) - case AdminPacketServerDate: - sp := PacketServerDate{ - Packet: p, - } - sp.Read(b[:p.PLength]) - logInfoDebug("AdminPacketServerDate :\n- Date: %d\n- RealDate : %v", sp.Date, toDate(sp.Date)) - t := toDate(sp.Date) - if t.Day() == 1 && t.Month() == 1 { - sendChat(-436055948, fmt.Sprintf("Year %d.", t.Year())) - } - paused = false - case AdminPacketServerClientJoin: - sp := PacketServerClientJoin{ - Packet: p, - } - sp.Read(b[:p.PLength]) - logInfoDebug("AdminPacketServerClientJoin :\n- ClientID: %d", sp.ClientID) - sendChat(-436055948, fmt.Sprintf("%s joining.", clients[sp.ClientID].Name)) - case AdminPacketServerClientInfo: - sp := PacketServerClientInfo{ - Packet: p, - } - sp.Read(b[:p.PLength]) - //logInfoDebug("AdminPacketServerClientInfo :\n- ClientID: %d\n- Address: %s\n- Name: %s\n- Lang: %d\n- Date: %d\n- CompanyID: %d", sp.ClientID, sp.Address, sp.Name, sp.Lang, sp.Date, sp.CompanyID) - clt := Client{ - ClientID: sp.ClientID, - Name: sp.Name, - Address: sp.Address, - CompanyID: sp.CompanyID, - } - clients[sp.ClientID] = &clt - logInfoDebug("Clients : %v", clients) - case AdminPacketServerClientError: - sp := PacketServerClientError{ - Packet: p, - } - sp.Read(b[:p.PLength]) - logInfoDebug("AdminPacketServerClientError :\n- ClientID: %d\n- ErrorID: %d", sp.ClientID, sp.ErrorID) - case AdminPacketServerClientQuit: - sp := PacketServerClientQuit{ - Packet: p, - } - sp.Read(b[:p.PLength]) - logInfoDebug("AdminPacketServerClientQuit :\n- ClientID: %d", sp.ClientID) - if len(clients) == 2 && !paused { - sendChat(-436055948, fmt.Sprintf("%s leaving. Game paused.", clients[sp.ClientID].Name)) - } else { - sendChat(-436055948, fmt.Sprintf("%s leaving.", clients[sp.ClientID].Name)) - } - delete(clients, sp.ClientID) - case AdminPacketServerChat: - sp := PacketServerChat{ - Packet: p, - } - sp.Read(b[:p.PLength]) - - if sp.Message == "!unpause" { - logInfoDebug("AdminPacketServerChat : Unpausing") - forcePaused = false - pausedBy = clients[sp.ClientID].Name - } else if sp.Message == "!pause" { - logInfoDebug("AdminPacketServerChat : Pausing") - forcePaused = true - pausedBy = clients[sp.ClientID].Name - } else { - logInfoDebug("AdminPacketServerChat :\n- ActionID: %d\n- DestinationID: %d\n- ClientID: %d\n- Message: %s\n- Amount: %d", sp.ActionID, sp.DestinationID, sp.ClientID, sp.Message, sp.Amount) - } - case AdminPacketServerConsole: - sp := PacketServerConsole{ - Packet: p, - } - sp.Read(b[:p.PLength]) - - ok, err := regexp.MatchString("\\[udp\\] queried from .*", sp.Text) - logErrorDebug(err, "queried from") - if sp.Origin != "net" || ok == false { - logInfoDebug("AdminPacketServerConsole :\n- Origin: %q\n- Text: %s", sp.Origin, sp.Text) - } - case AdminPacketServerRCon: - sp := PacketServerRCon{ - Packet: p, - } - sp.Read(b[:p.PLength]) - - ok, _ := regexp.MatchString("Client #[0-9]+ name: '.*' company: [0-9]+ IP: .*", sp.Output) - if ok { - clt := Client{} - r, _ := regexp.Compile("Client #(?P[0-9]+) name: '(?P.*)' company: (?P[0-9]+) IP: (?P
.*)") - ID64, _ := strconv.ParseInt(r.ReplaceAllString(sp.Output, "${ClientID}"), 10, 32) - clt.ClientID = uint32(ID64) - clt.Name = r.ReplaceAllString(sp.Output, "${Name}") - if clt.Name == "" { - clt.Name = "server" - } - ID64, _ = strconv.ParseInt(r.ReplaceAllString(sp.Output, "${CompanyID}"), 10, 8) - clt.CompanyID = uint8(ID64) - clt.Address = r.ReplaceAllString(sp.Output, "${Address}") - clients[clt.ClientID] = &clt - } else { - logInfoDebug("AdminPacketServerRCon :\n- ColorID: %d\n- Output: %s", sp.ColorID, sp.Output) - } - case AdminPacketServerRConEnd: - sp := PacketServerRConEnd{ - Packet: p, - } - sp.Read(b[:p.PLength]) - - if sp.Command == "clients" { - for k, v := range clients { - logInfoDebug("Client[%d] : %s - %d (%s)", k, v.Name, v.CompanyID, v.Address) - } - } else { - logInfoDebug("AdminPacketServerRConEnd :\n- Command: %s", sp.Command) - } - default: - logInfoDebug("Packet fully read : len : %d / type : %d", p.PLength, p.PType) - } - - c := make([]byte, 0xFFFF) - copy(c, b[p.PLength:]) - b = c - read -= int(p.PLength) - - if len(clients) == 0 { - px := PacketAdminRCon{ - Packet: Packet{PType: AdminPacketAdminRCon}, - Command: "clients", - } - _, err = conn.Write(px.Bytes()) - } - - if !paused && forcePaused { - paused = true - px := PacketAdminRCon{ - Packet: Packet{PType: AdminPacketAdminRCon}, - Command: "pause", - } - _, err = conn.Write(px.Bytes()) - logInfoDebug("Force pausing") - if pausedBy != "" { - sendChat(-436055948, fmt.Sprintf("Game paused by %s", pausedBy)) - pausedBy = "" - } - } - if paused && !forcePaused && len(clients) > 1 { // server is client #1 - paused = false - px := PacketAdminRCon{ - Packet: Packet{PType: AdminPacketAdminRCon}, - Command: "unpause", - } - _, err = conn.Write(px.Bytes()) - logInfoDebug("Force unpausing") - if pausedBy != "" { - sendChat(-436055948, fmt.Sprintf("Game unpaused by %s", pausedBy)) - pausedBy = "" - } else { - sendChat(-436055948, "Game unpaused") - } - } - if !paused && len(clients) == 1 { // server is client #1 - paused = true - px := PacketAdminRCon{ - Packet: Packet{PType: AdminPacketAdminRCon}, - Command: "pause", - } - _, err = conn.Write(px.Bytes()) - logInfoDebug("Pausing") - - } + time.Sleep(1 * time.Second) } - } diff --git a/packet.go b/packet.go index eb1493c..79c6175 100644 --- a/packet.go +++ b/packet.go @@ -109,6 +109,11 @@ type PacketAdminUpdateFrequency struct { UpdateFrequency uint16 } +type PacketAdminPoll struct { + Packet + UpdateType uint8 + UpdateID uint32 +} type PacketServerDate struct { Packet Date uint32 @@ -134,12 +139,52 @@ type PacketServerClientInfo struct { CompanyID uint8 } +type PacketServerClientUpdate struct { + Packet + ClientID uint32 + Name string + CompanyID uint8 +} + type PacketServerClientError struct { Packet ClientID uint32 ErrorID uint8 } +type PacketServerCompanyNew struct { + Packet + CompanyID uint8 +} + +type PacketServerCompanyInfo struct { + Packet + CompanyID uint8 + Name string + President string + Color uint8 + Inauguration uint32 + AI bool + Protected bool + Bankruptcies uint8 +} + +type PacketServerCompanyUpdate struct { + Packet + CompanyID uint8 + Name string + President string + Color uint8 + Protected bool + Bankruptcies uint8 +} + +type PacketServerCompanyRemove struct { + Packet + CompanyID uint8 + Reason uint8 +} + type PacketServerChat struct { Packet ActionID uint8 @@ -206,6 +251,18 @@ func (p *PacketAdminUpdateFrequency) Bytes() []byte { return buf.Bytes() } +func (p *PacketAdminPoll) Bytes() []byte { + buf := new(bytes.Buffer) + p.PLength = 8 + + binary.Write(buf, binary.LittleEndian, p.PLength) + binary.Write(buf, binary.LittleEndian, p.PType) + binary.Write(buf, binary.LittleEndian, p.UpdateType) + binary.Write(buf, binary.LittleEndian, p.UpdateID) + + return buf.Bytes() +} + func (p *PacketServerWelcome) Read(b []byte) { r := bufio.NewReader(bytes.NewReader(b)) r.Discard(3) @@ -285,6 +342,18 @@ func (p *PacketServerClientInfo) Read(b []byte) { p.CompanyID = uint8(c) } +func (p *PacketServerClientUpdate) Read(b []byte) { + r := bufio.NewReader(bytes.NewReader(b)) + r.Discard(3) + bs := make([]byte, 4) + _, _ = r.Read(bs) + p.ClientID = binary.LittleEndian.Uint32(bs[0:]) + p.Name, _ = r.ReadString(0) + p.Name = p.Name[:len(p.Name)-1] + c, _ := r.ReadByte() + p.CompanyID = uint8(c) +} + func (p *PacketServerClientError) Read(b []byte) { r := bufio.NewReader(bytes.NewReader(b)) r.Discard(3) @@ -303,6 +372,73 @@ func (p *PacketServerClientQuit) Read(b []byte) { p.ClientID = binary.LittleEndian.Uint32(bs[0:]) } +func (p *PacketServerCompanyNew) Read(b []byte) { + r := bufio.NewReader(bytes.NewReader(b)) + r.Discard(3) + c, _ := r.ReadByte() + p.CompanyID = uint8(c) +} + +func (p *PacketServerCompanyInfo) Read(b []byte) { + r := bufio.NewReader(bytes.NewReader(b)) + r.Discard(3) + c, _ := r.ReadByte() + p.CompanyID = uint8(c) + p.Name, _ = r.ReadString(0) + p.Name = p.Name[:len(p.Name)-1] + p.President, _ = r.ReadString(0) + p.President = p.Name[:len(p.Name)-1] + c, _ = r.ReadByte() + p.Color = uint8(c) + c, _ = r.ReadByte() + if c > 0 { + p.Protected = true + } else { + p.Protected = false + } + bs := make([]byte, 4) + _, _ = r.Read(bs) + p.Inauguration = binary.LittleEndian.Uint32(bs[0:]) + c, _ = r.ReadByte() + if c > 0 { + p.AI = true + } else { + p.AI = false + } + c, _ = r.ReadByte() + p.Bankruptcies = uint8(c) +} + +func (p *PacketServerCompanyUpdate) Read(b []byte) { + r := bufio.NewReader(bytes.NewReader(b)) + r.Discard(3) + c, _ := r.ReadByte() + p.CompanyID = uint8(c) + p.Name, _ = r.ReadString(0) + p.Name = p.Name[:len(p.Name)-1] + p.President, _ = r.ReadString(0) + p.President = p.Name[:len(p.Name)-1] + c, _ = r.ReadByte() + p.Color = uint8(c) + c, _ = r.ReadByte() + if c > 0 { + p.Protected = true + } else { + p.Protected = false + } + c, _ = r.ReadByte() + p.Bankruptcies = uint8(c) +} + +func (p *PacketServerCompanyRemove) Read(b []byte) { + r := bufio.NewReader(bytes.NewReader(b)) + r.Discard(3) + c, _ := r.ReadByte() + p.CompanyID = uint8(c) + c, _ = r.ReadByte() + p.Reason = uint8(c) +} + func (p *PacketServerChat) Read(b []byte) { r := bufio.NewReader(bytes.NewReader(b)) r.Discard(3) diff --git a/ttd.go b/ttd.go new file mode 100644 index 0000000..a3e999b --- /dev/null +++ b/ttd.go @@ -0,0 +1,729 @@ +package main + +import ( + "bufio" + "encoding/binary" + "fmt" + "net" + "os" + "regexp" + "strconv" + "time" +) + +type ClientTTD struct { + ClientID uint32 + Name string + Address string + CompanyID uint8 + Paused bool + LastSeen time.Time +} + +type CompanyTTD struct { + CompanyID uint8 + Name string + Protected bool + ClientID uint32 + FirstSeen time.Time + LastSeen time.Time +} + +type ServerTTD struct { + Config *ServerConfig + Status *ServerStatusTTD + Data *ServerDataTTD +} + +type ServerStatusTTD struct { + Connected bool + Paused bool + UpdateCompanies time.Time + UpdateClients time.Time + UpdateDate time.Time + Initialized bool + GameDate time.Time + Clients map[uint32]*ClientTTD + Companies map[uint8]*CompanyTTD +} + +type ServerDataTTD struct { + LastClientCompute time.Time + Conn net.Conn + Stop chan struct{} +} + +var updateHeartBeat = 5 * time.Second + +func (s *ServerTTD) Connect() (err error) { + t := time.Now() + s.Status.Connected = false + s.Data.Conn, err = net.Dial("tcp", s.Config.Addr) + logErrorDebug(err, "Server.Connect() : net.Dial") + if err != nil { + return err + } + logInfoDebug("Server.Connect() : Connected to " + s.Config.Addr) + s.Data.LastClientCompute = time.Now() + s.Status.Connected = true + s.Status.Initialized = false + s.Status.UpdateDate = t + s.Status.UpdateClients = t + s.Status.UpdateCompanies = t + s.Status.Clients = make(map[uint32]*ClientTTD) + s.Status.Companies = make(map[uint8]*CompanyTTD) + return err +} + +func (s *ServerTTD) Send(b []byte) (err error) { + if !s.Status.Connected { + return fmt.Errorf("not connected") + } + + _, err = s.Data.Conn.Write(b) + return err +} + +func (s *ServerTTD) Start() { + for { + err := s.Connect() + if err != nil { + time.Sleep(5 * time.Second) + } + break + } + + //send auth + p := PacketAdminJoin{ + Packet: Packet{PType: AdminPacketAdminJoin}, + Password: s.Config.Passwd, + AppName: "gottdad", + AppVersion: version, + } + err := s.Send(p.Bytes()) + failError(err, "Server.Start() : Cannot send authentication packet") + + stopPoll := make(chan struct{}) + go s.Poll(stopPoll) + + stopHeartBeat := make(chan struct{}) + go s.HeartBeat(stopHeartBeat) + + for { + select { + // call to stop polling + case <-s.Data.Stop: + close(stopPoll) + close(stopHeartBeat) + return + } + } +} + +// Stop gracefully shuts the poller down. +func (s *ServerTTD) Stop() { + s.Data.Stop <- struct{}{} +} + +func (s *ServerTTD) HeartBeat(stop chan struct{}) { + for { + select { + case <-stop: + return + default: + } + s.UpdateDate() + s.UpdateClients() + s.UpdateCompanies() + s.PruneClients() + s.PruneCompanies() + s.ComputeClientTime() + if !s.Status.Paused && s.NeedPause() { + s.Pause() + } else if s.Status.Paused && !s.NeedPause() { + s.Unpause() + } + cfg.Save(*configFlag) + time.Sleep(updateHeartBeat) + } +} + +func (s *ServerTTD) Poll(stop chan struct{}) { + var err error + reader := bufio.NewReader(s.Data.Conn) + buffer := make([]byte, 0xFFFF) + var n, read int + + for { + select { + case <-stop: + return + default: + } + + p := Packet{} + + for { + if read >= 3 { + //logInfoDebug("Server.Poll() : packet read") + break + } + n, err = reader.Read(buffer[read:]) + logErrorDebug(err, "Server.Poll() : reader.Read") + read += n + //logInfoDebug("Server.Poll() : waiting for packet, read %d bytes.", read) + } + + p.PLength = binary.LittleEndian.Uint16(buffer[0:]) + p.PType = buffer[2] + if p.PLength <= 3 { + logInfoAlert("Server.Poll() : wrong packet length") + break + } + + //logInfoDebug("Server.Poll() : waiting for packet data : len : %d / type : %d", p.PLength, p.PType) + + for { + if read >= int(p.PLength) { + //logInfoDebug("Server.Poll() : data read") + break + } + n, err = reader.Read(buffer[read:]) + logErrorDebug(err, "Server.Poll() : reader.Read") + read += n + //logInfoDebug("Server.Poll() : waiting for data, read %d/%d bytes.", read, p.PLength) + + } + + switch p.PType { + case AdminPacketServerProtocol: + sp := PacketServerProtocol{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + //logInfoDebug("Server.Poll() : AdminPacketServerProtocol :\n- ProtocolVersion: %v\n- FurtherData: %v\n- UpdatePacketType: %v\n- FrequenciesAllowed: %b", sp.ProtocolVersion, sp.FurtherData, sp.UpdatePacketType, sp.FrequenciesAllowed) + case AdminPacketServerWelcome: + sp := PacketServerWelcome{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + //logInfoDebug("Server.Poll() : AdminPacketServerWelcome :\n- ServerName: %v\n- OpenTTDVersion: %v\n- Dedicated: %v\n- MapSeed: %x\n- MapLandscape: %v\n- MapStartDate: %v\n- Size: %v x %v", sp.ServerName, sp.OpenTTDVersion, sp.Dedicated, sp.MapSeed, sp.MapLandscape, sp.MapStartDate, sp.MapX, sp.MapY) + s.Initialize() + case AdminPacketServerDate: + sp := PacketServerDate{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + //logInfoDebug("Server.Poll() : AdminPacketServerDate :\n- Date: %d\n- RealDate : %v", sp.Date, toDate(sp.Date)) + gameDate := toDate(sp.Date) + if gameDate.Day() == 1 && gameDate.Month() == 1 && gameDate != s.Status.GameDate { + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("Year %d.", gameDate.Year())) + } + s.Status.GameDate = gameDate + s.Status.UpdateDate = time.Now() + case AdminPacketServerClientJoin: + sp := PacketServerClientJoin{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + logInfoDebug("Server.Poll() : AdminPacketServerClientJoin :\n- ClientID: %d", sp.ClientID) + case AdminPacketServerClientInfo: + sp := PacketServerClientInfo{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + //logInfoDebug("Server.Poll() : AdminPacketServerClientInfo :\n- ClientID: %d\n- Address: %s\n- Name: %s\n- Lang: %d\n- Date: %d\n- CompanyID: %d", sp.ClientID, sp.Address, sp.Name, sp.Lang, sp.Date, sp.CompanyID) + clt := &ClientTTD{ + ClientID: sp.ClientID, + } + if _, ok := s.Status.Clients[sp.ClientID]; ok { + clt = s.Status.Clients[sp.ClientID] + } else { + s.Status.Clients[sp.ClientID] = clt + } + clt.Address = sp.Address + clt.Name = sp.Name + clt.CompanyID = sp.CompanyID + clt.LastSeen = time.Now() + if clt.CompanyID != 255 { + if co, ok := s.Status.Companies[clt.CompanyID]; ok { + if co.ClientID == 0 { + co.ClientID = clt.ClientID + } + } else { + //FIXME company doesn't exist ? + } + if cfg.CompanyIsRegistered(clt.CompanyID) { + cc := cfg.GetCompanyClient(clt.CompanyID) + if !cc.Online { + cc.Online = true + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("@%s now playing.", cc.Username)) + } + } + } + case AdminPacketServerClientUpdate: + sp := PacketServerClientUpdate{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + logInfoDebug("Server.Poll() : AdminPacketServerClientUpdate :\n- ClientID: %d\n- Name: %s\n- CompanyID: %d", sp.ClientID, sp.Name, sp.CompanyID) + clt := s.Status.Clients[sp.ClientID] + clt.Name = sp.Name + if sp.CompanyID != 255 { + if cfg.CompanyIsRegistered(sp.CompanyID) { + cc := cfg.GetCompanyClient(sp.CompanyID) + cc.Online = true + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("@%s playing.", cc.Username)) + } else { + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("%s unclaimed. Please /register to claim the company", s.Status.Companies[sp.CompanyID].Name)) + } + } else if clt.CompanyID != 255 && cfg.CompanyIsRegistered(clt.CompanyID) { + cc := cfg.GetCompanyClient(clt.CompanyID) + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("@%s taking a break.", cc.Username)) + cc.Online = false + } + clt.LastSeen = time.Now() + clt.CompanyID = sp.CompanyID + s.Unpause() + s.Pause() + case AdminPacketServerClientError: + sp := PacketServerClientError{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + logInfoDebug("Server.Poll() : AdminPacketServerClientError :\n- ClientID: %d\n- ErrorID: %d", sp.ClientID, sp.ErrorID) + case AdminPacketServerClientQuit: + sp := PacketServerClientQuit{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + logInfoDebug("Server.Poll() : AdminPacketServerClientQuit :\n- ClientID: %d", sp.ClientID) + if len(s.Status.Clients) == 2 && !srv.Status.Paused { + if cfg.CompanyIsRegistered(s.Status.Clients[sp.ClientID].CompanyID) { + cc := cfg.GetCompanyClient(s.Status.Clients[sp.ClientID].CompanyID) + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("@%s leaving. Game paused.", cc.Username)) + cc.Online = false + } else { + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("%s leaving. Game paused.", s.Status.Clients[sp.ClientID].Name)) + } + } else { + if cfg.CompanyIsRegistered(s.Status.Clients[sp.ClientID].CompanyID) { + cc := cfg.GetCompanyClient(s.Status.Clients[sp.ClientID].CompanyID) + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("@%s leaving.", cc.Username)) + cc.Online = false + } else { + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("%s leaving.", s.Status.Clients[sp.ClientID].Name)) + } + } + delete(s.Status.Clients, sp.ClientID) + case AdminPacketServerCompanyNew: + sp := PacketServerCompanyNew{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + //logInfoDebug("Server.Poll() : PacketServerCompanyNew :\n- CompanyID: %d", sp.CompanyID) + c := &CompanyTTD{ + CompanyID: sp.CompanyID, + FirstSeen: time.Now(), + } + s.Status.Companies[sp.CompanyID] = c + case AdminPacketServerCompanyInfo: + sp := PacketServerCompanyInfo{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + //logInfoDebug("Server.Poll() : PacketServerCompanyInfo :\n- CompanyID: %d\n- Name: %s\n- President: %s\n- Protected: %t\n- Inauguration: %d\n- AI: %t", sp.CompanyID, sp.Name, sp.President, sp.Protected, sp.Inauguration, sp.AI) + c := &CompanyTTD{ + CompanyID: sp.CompanyID, + FirstSeen: time.Now(), + } + if _, ok := s.Status.Companies[sp.CompanyID]; !ok { + s.Status.Companies[sp.CompanyID] = c + } else { + c = s.Status.Companies[sp.CompanyID] + } + c.Name = sp.Name + c.LastSeen = time.Now() + c.Protected = sp.Protected + case AdminPacketServerCompanyUpdate: + sp := PacketServerCompanyUpdate{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + //logInfoDebug("Server.Poll() : PacketServerCompanyUpdate :\n- CompanyID: %d\n- Name: %s\n- President: %s\n- Protected: %t", sp.CompanyID, sp.Name, sp.President, sp.Protected) + c := s.Status.Companies[sp.CompanyID] + c.Name = sp.Name + c.FirstSeen = time.Now() + c.LastSeen = time.Now() + c.Protected = sp.Protected + case AdminPacketServerCompanyRemove: + sp := PacketServerCompanyRemove{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + //logInfoDebug("Server.Poll() : PacketServerCompanyRemove :\n- CompanyID: %d\n- Reason: %d", sp.CompanyID, sp.Reason) + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("Company #%d deleted (%s)", sp.CompanyID, s.Status.Companies[sp.CompanyID].Name)) + delete(s.Status.Companies, sp.CompanyID) + case AdminPacketServerChat: + sp := PacketServerChat{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + + if sp.Message == "!unpause" { + logInfoDebug("Server.Poll() : AdminPacketServerChat : Unpausing") + clt := s.Status.Clients[sp.ClientID] + clt.Paused = false + s.Unpause() + } else if sp.Message == "!pause" { + logInfoDebug("Server.Poll() : AdminPacketServerChat : Pausing") + clt := s.Status.Clients[sp.ClientID] + clt.Paused = true + s.Pause() + } else { + logInfoDebug("Server.Poll() : AdminPacketServerChat :\n- ActionID: %d\n- DestinationID: %d\n- ClientID: %d\n- Message: %s\n- Amount: %d", sp.ActionID, sp.DestinationID, sp.ClientID, sp.Message, sp.Amount) + } + case AdminPacketServerConsole: + sp := PacketServerConsole{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + + udp_queried_from, err := regexp.MatchString("\\[udp\\] queried from .*", sp.Text) + logErrorDebug(err, "Server.Poll() : queried from") + if udp_queried_from && sp.Origin == "net" { + break + } + + joined_company, err := regexp.MatchString("\\*\\*\\* .* has joined company #[0-9]+", sp.Text) + logErrorDebug(err, "Server.Poll() : joined company") + if joined_company && sp.Origin == "console" { + r := regexp.MustCompile("\\*\\*\\* (?P.*) has joined company #(?P[0-9]+)") + clientName := r.ReplaceAllString(sp.Text, "${Client}") + ID8, _ := strconv.ParseInt(r.ReplaceAllString(sp.Text, "${CompanyID}"), 10, 8) + companyID := uint8(ID8) - 1 + for _, clt := range s.Status.Clients { + if clt.Name == clientName { + clt.CompanyID = companyID + } + } + break + } + + joined_spectators, err := regexp.MatchString("\\*\\*\\* .* has joined spectators", sp.Text) + if joined_spectators && sp.Origin == "console" { + r := regexp.MustCompile("\\*\\*\\* (?P.*) has joined spectators") + clientName := r.ReplaceAllString(sp.Text, "${Client}") + for _, clt := range s.Status.Clients { + if clt.Name == clientName { + clt.CompanyID = 255 + } + } + break + } + + command, err := regexp.MatchString("\\[admin\\] Rcon command from 'gottdad' \\(.*\\): .*", sp.Text) + if command && sp.Origin == "net" { + break + } + + logInfoDebug("Server.Poll() : AdminPacketServerConsole :\n- Origin: %q\n- Text: %s", sp.Origin, sp.Text) + + case AdminPacketServerRCon: + sp := PacketServerRCon{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + logInfoDebug("Server.Poll() : AdminPacketServerRCon :\n- ColorID: %d\n- Output: %s", sp.ColorID, sp.Output) + + case AdminPacketServerRConEnd: + sp := PacketServerRConEnd{ + Packet: p, + } + sp.Read(buffer[:p.PLength]) + + switch sp.Command { + case "clients": + for k, v := range s.Status.Clients { + logInfoDebug("Server.Poll() : Client[%d] : %s - %d (%s)", k, v.Name, v.CompanyID, v.Address) + } + + case "companies": + for k, v := range s.Status.Companies { + logInfoDebug("Server.Poll() : Company[%d] : %s", k, v.Name) + } + case "pause": + default: + logInfoDebug("Server.Poll() : AdminPacketServerRConEnd :\n- Command: %s", sp.Command) + } + default: + logInfoDebug("Server.Poll() : Packet fully read : len : %d / type : %d", p.PLength, p.PType) + os.Exit(0) + } + + c := make([]byte, 0xFFFF) + copy(c, buffer[p.PLength:]) + buffer = c + read -= int(p.PLength) + } + +} + +func (s *ServerTTD) UpdateDate() { + //logInfoDebug("Server.UpdateDate") + px := PacketAdminPoll{ + Packet: Packet{PType: AdminPacketAdminPoll}, + UpdateType: AdminUpdateDate, + UpdateID: 0, + } + err := s.Send(px.Bytes()) + logErrorDebug(err, "Server.UpdateDate() : Send(PacketAdminPoll) : AdminUpdateDate") + s.Status.UpdateDate = time.Now() +} + +func (s *ServerTTD) UpdateClients() { + //logInfoDebug("Server.UpdateClients") + px := PacketAdminPoll{ + Packet: Packet{PType: AdminPacketAdminPoll}, + UpdateType: AdminUpdateClientInfo, + UpdateID: uint32(4294967295), + } + err := s.Send(px.Bytes()) + logErrorDebug(err, "Server.UpdateClients() : Send(PacketAdminPoll) : AdminUpdateClientInfo") + s.Status.UpdateClients = time.Now() +} + +func (s *ServerTTD) UpdateCompanies() { + //logInfoDebug("Server.UpdateCompanies") + px := PacketAdminPoll{ + Packet: Packet{PType: AdminPacketAdminPoll}, + UpdateType: AdminUpdateCompanyInfo, + UpdateID: uint32(4294967295), + } + err := s.Send(px.Bytes()) + logErrorDebug(err, "Server.UpdateCompanies() : Send(UpdateCompanies) : AdminUpdateClientInfo") + s.Status.UpdateCompanies = time.Now() +} + +func (s *ServerTTD) PruneClients() { + for cltID, clt := range s.Status.Clients { + if clt.LastSeen.Add(2 * updateHeartBeat).Before(time.Now()) { + logInfoDebug("ServerTTD.PruneClients : deleting client #%d", cltID) + delete(s.Status.Clients, cltID) + } + } +} + +func (s *ServerTTD) PruneCompanies() { + for coID, co := range s.Status.Companies { + if co.LastSeen.Add(2 * updateHeartBeat).Before(time.Now()) { + logInfoDebug("ServerTTD.PruneCompanies : deleting company #%d", coID) + delete(s.Status.Companies, coID) + } + } +} + +func (s *ServerTTD) ComputeClientTime() { + t := time.Now() + daysNow := int(t.Sub(cfg.Game.StartDate).Hours() / 24) + daysLast := int(s.Data.LastClientCompute.Sub(cfg.Game.StartDate).Hours() / 24) + if daysLast != daysNow { + bot.SendChat(cfg.Telegram.ChatID, "New daily allotment available.") + logInfoDebug("Server.ComputeClientTime : newDay : now : %d vs %d : last", daysNow, daysLast) + for _, cc := range cfg.Clients { + cc.TimeLeft = cc.TimeLeft + cfg.Game.DailyAllotment + } + } + if !s.Status.Paused { + diff := t.Sub(s.Data.LastClientCompute) + for _, cc := range cfg.Clients { + if cc.Online { + if cc.TimeLeft > 0 && cc.TimeLeft < diff { + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("@%s got not time left. Grace period of %s.", cc.Username, cfg.Game.Threshold)) + } + if (cc.TimeLeft+cfg.Game.Threshold) > 0 && (cc.TimeLeft+cfg.Game.Threshold) < diff { + bot.SendChat(cfg.Telegram.ChatID, fmt.Sprintf("@%s got not time left. Grace period over.", cc.Username)) + } + cc.TimeLeft = cc.TimeLeft - diff + } + } + } + + s.Data.LastClientCompute = t + return + +} + +func (s *ServerTTD) DeleteCompany(id uint8) { + if _, ok := s.Status.Companies[id]; ok { + logInfoDebug("Server.DeleteCompany : deleting #%d", id) + px := PacketAdminRCon{ + Packet: Packet{PType: AdminPacketAdminRCon}, + Command: fmt.Sprintf("reset_company %d", id), + } + s.Send(px.Bytes()) + px = PacketAdminRCon{ + Packet: Packet{PType: AdminPacketAdminRCon}, + Command: "companies", + } + s.Send(px.Bytes()) + } else { + logInfoDebug("Server.DeleteCompany : cannot find company #%d", id) + px := PacketAdminRCon{ + Packet: Packet{PType: AdminPacketAdminRCon}, + Command: "companies", + } + s.Send(px.Bytes()) + } +} + +func (s *ServerTTD) Pause() { + if s.Status.Paused { + return + } + if !s.NeedPause() { + return + } + + px := PacketAdminRCon{ + Packet: Packet{PType: AdminPacketAdminRCon}, + Command: "pause", + } + err := s.Send(px.Bytes()) + + s.ComputeClientTime() + + s.Status.Paused = true + logErrorDebug(err, "Server.Pause : Send()") + logInfoDebug("Server.Pause : pausing") +} + +func (s *ServerTTD) Unpause() { + if !s.Status.Paused { + return + } + if s.NeedPause() { + return + } + + px := PacketAdminRCon{ + Packet: Packet{PType: AdminPacketAdminRCon}, + Command: "unpause", + } + err := s.Send(px.Bytes()) + + s.ComputeClientTime() + + s.Status.Paused = false + logErrorDebug(err, "Server.Unpause : Send()") + logInfoDebug("Server.Unpause : unpausing") +} + +func (s *ServerTTD) NeedPause() bool { + if !s.Status.Initialized { + logInfoDebug("Server.NeedPause : not initialized yet") + return false + } + reason := s.NeedPauseReason() + if reason == "" { + return false + } else { + //logInfoDebug("Server.NeedPause : %s", reason) + return true + } +} + +func (s *ServerTTD) NeedPauseReason() string { + if !s.Status.Initialized { + return "" + } + dupl := make(map[uint8]struct{}) + online := 0 + for cltID, clt := range s.Status.Clients { + if cltID != 1 && clt.CompanyID != 255 { + online++ + if _, ok := s.Status.Companies[clt.CompanyID]; !ok { + return "company unidentified" + } + } + + if clt.Paused { + return "user paused" + } + + if _, ok := dupl[clt.CompanyID]; ok && clt.CompanyID != 255 { + return "more than one user in company" + } + dupl[clt.CompanyID] = struct{}{} + } + for coID, co := range s.Status.Companies { + if !co.Protected { + return "company unprotected" + } + if !cfg.CompanyIsRegistered(coID) { + return "company unregistered" + } + } + for _, cc := range cfg.Clients { + if cc.Online && (cc.TimeLeft+cfg.Game.Threshold) < 0 { + return "no time left" + } + } + if online == 0 { + return "no players online" + } + return "" +} + +func (s *ServerTTD) Initialize() { + var err error + + if s.Status.Initialized { + return + } + + px := PacketAdminUpdateFrequency{ + Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, + UpdateType: AdminUpdateDate, + UpdateFrequency: AdminFrequencyDaily, + } + err = s.Send(px.Bytes()) + logErrorDebug(err, "Server.Initialize() : Send(AdminUpdateDate)") + + px = PacketAdminUpdateFrequency{ + Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, + UpdateType: AdminUpdateClientInfo, + UpdateFrequency: AdminFrequencyAutomatic, + } + err = s.Send(px.Bytes()) + logErrorDebug(err, "Server.Initialize() : Send(AdminUpdateClientInfo)") + + px = PacketAdminUpdateFrequency{ + Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, + UpdateType: AdminUpdateCompanyInfo, + UpdateFrequency: AdminFrequencyAutomatic, + } + err = s.Send(px.Bytes()) + logErrorDebug(err, "Server.Initialize() : Send(AdminUpdateCompanyInfo)") + + px = PacketAdminUpdateFrequency{ + Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, + UpdateType: AdminUpdateChat, + UpdateFrequency: AdminFrequencyAutomatic, + } + err = s.Send(px.Bytes()) + logErrorDebug(err, "Server.Initialize() : Send(AdminUpdateChat)") + + px = PacketAdminUpdateFrequency{ + Packet: Packet{PType: AdminPacketAdminUpdateFrequency}, + UpdateType: AdminUpdateConsole, + UpdateFrequency: AdminFrequencyAutomatic, + } + err = s.Send(px.Bytes()) + logErrorDebug(err, "Server.Initialize() : Send(AdminUpdateConsole)") + + s.Status.Initialized = true + + s.UpdateCompanies() + s.UpdateClients() +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..531bed0 --- /dev/null +++ b/version.go @@ -0,0 +1,6 @@ +// Code generated by version.sh (@generated) DO NOT EDIT. +package main +var githash = "90e584f" +var buildstamp = "2021-11-06_15:32:44" +var commits = "156" +var version = "90e584f-b156 - 2021-11-06_15:32:44" diff --git a/version.sh b/version.sh new file mode 100644 index 0000000..a436a5c --- /dev/null +++ b/version.sh @@ -0,0 +1,13 @@ +# Get the version. +githash=`git rev-parse --short HEAD` +buildstamp=`date -u '+%Y-%m-%d_%H:%M:%S'` +commits=`git rev-list --count master` +# Write out the package. +cat << EOF > version.go +// Code generated by version.sh (@generated) DO NOT EDIT. +package main +var githash = "$githash" +var buildstamp = "$buildstamp" +var commits = "$commits" +var version = "$githash-b$commits - $buildstamp" +EOF \ No newline at end of file