From 9c6950d2005dcd417627f2b9e666e35eb1127d40 Mon Sep 17 00:00:00 2001 From: Malckyor Date: Wed, 15 Jun 2022 07:32:45 +0900 Subject: [PATCH] Discord Integration Improvement How to config the bot integration: - In the config.json file, find the and do the following in my instruction. "discord": { "enabled": false, // set this value to "true" to enable it "bottoken": "", // add your bot token, for this you need a bot "realtimeChannelID": "", // add the Discord channel ID that will receive and send messages "serverId": "", // add your Discord group ID here "devRoles": [], // add a Discord dev role for a few internal commands, like debug "devMode": false - It is possible to set it to multiple Worlds too, for that just do the following: By default it will only broadcast messages in the Normal World using the Local and Party chat. In the main.go file, find the and do the following. //DiscordBot: discordBot, // just remove the "//" to enable it for that World. - Use the command !players in the Discord chat to see a list of all online players. By : Invasores de Fronteiras https://github.com/Invasor-de-Fronteiras/Erupe --- Erupe/config.json | 307 ++++++------ Erupe/config/config.go | 316 ++++++------ Erupe/main.go | 392 ++++++++------- .../channelserver/handlers_cast_binary.go | 5 +- .../server/channelserver/handlers_discord.go | 458 ++++++++++++------ .../channelserver/sys_channel_server.go | 158 ++++-- Erupe/server/channelserver/sys_stage.go | 78 ++- Erupe/server/discordbot/discord_bot.go | 120 +++++ 8 files changed, 1157 insertions(+), 677 deletions(-) create mode 100644 Erupe/server/discordbot/discord_bot.go diff --git a/Erupe/config.json b/Erupe/config.json index 7a0ef1183..0c78d7bf5 100644 --- a/Erupe/config.json +++ b/Erupe/config.json @@ -1,153 +1,156 @@ -{ - "host_ip": "", - "bin_path": "bin", - "devmode": true, - "devmodeoptions": { - "serverName" : "", - "cleandb": false, - "maxlauncherhr": true, - "LogOutboundMessages": false, - "Event": 0, - "OpcodeMessages": false, - "SaveDumps": { - "Enabled": true, - "OutputDir": "savedata" - } - - }, - "discord": { - "enabled": false, - "bottoken": "", - "channelid": "" - }, - "database": { - "host": "localhost", - "port": 5432, - "user": "postgres", - "password": "", - "database": "erupe" - }, - "launcher": { - "port": 80, - "UseOriginalLauncherFiles": false - }, - "sign": { - "port": 53312 - }, - "channel": { - "port1": 54001, - "port2": 54002, - "port3": 54003, - "port4": 54004 - }, - "entrance": { - "port": 53310, - "entries": [ - { - "name": " Server #1", - "ip": "", - "unk2": 0, - "type": 3, - "season": 3, - "unk6": 0, - "allowedclientflags": "4096", - "channels": [ - { - "port": 54001, - "MaxPlayers": 100, - "CurrentPlayers": 0, - "Unk4": 0, - "Unk5": 0, - "Unk6": 0, - "Unk7": 0, - "Unk8": 0, - "Unk9": 0, - "Unk10": 319, - "Unk11": 248, - "Unk12": 159, - "Unk13": 12345 - } - ] - }, - { - "name": " Server #2", - "ip": "", - "unk2": 0, - "type": 1, - "season": 3, - "unk6": 0, - "allowedclientflags": 0, - "channels": [ - { - "port": 54002, - "MaxPlayers": 50, - "CurrentPlayers": 0, - "Unk4": 0, - "Unk5": 0, - "Unk6": 0, - "Unk7": 0, - "Unk8": 0, - "Unk9": 0, - "Unk10": 318, - "Unk11": 251, - "Unk12": 155, - "Unk13": 12345 - } - ] - }, - { - "name": " Server #3", - "ip": "", - "unk2": 0, - "type": 2, - "season": 1, - "unk6": 0, - "allowedclientflags": 0, - "channels": [ - { - "port": 54003, - "MaxPlayers": 50, - "CurrentPlayers": 0, - "Unk4": 0, - "Unk5": 0, - "Unk6": 0, - "Unk7": 0, - "Unk8": 0, - "Unk9": 0, - "Unk10": 318, - "Unk11": 251, - "Unk12": 155, - "Unk13": 12345 - } - ] - }, - { - "name": " Server #4", - "ip": "", - "unk2": 0, - "type": 4, - "season": 0, - "unk6": 0, - "allowedclientflags": 0, - "channels": [ - { - "port": 54004, - "MaxPlayers": 50, - "CurrentPlayers": 0, - "Unk4": 0, - "Unk5": 0, - "Unk6": 0, - "Unk7": 0, - "Unk8": 0, - "Unk9": 0, - "Unk10": 318, - "Unk11": 251, - "Unk12": 155, - "Unk13": 12345 - } - ] - - } - ] - } +{ + "host_ip": "", + "bin_path": "bin", + "devmode": true, + "devmodeoptions": { + "serverName" : "", + "cleandb": false, + "maxlauncherhr": true, + "LogOutboundMessages": false, + "Event": 0, + "OpcodeMessages": false, + "SaveDumps": { + "Enabled": true, + "OutputDir": "savedata" + } + + }, + "discord": { + "enabled": false, + "bottoken": "", + "realtimeChannelID": "", + "serverId": "", + "devRoles": [], + "devMode": false + }, + "database": { + "host": "localhost", + "port": 5432, + "user": "postgres", + "password": "", + "database": "erupe" + }, + "launcher": { + "port": 80, + "UseOriginalLauncherFiles": false + }, + "sign": { + "port": 53312 + }, + "channel": { + "port1": 54001, + "port2": 54002, + "port3": 54003, + "port4": 54004 + }, + "entrance": { + "port": 53310, + "entries": [ + { + "name": " Server #1", + "ip": "", + "unk2": 0, + "type": 3, + "season": 3, + "unk6": 0, + "allowedclientflags": "4096", + "channels": [ + { + "port": 54001, + "MaxPlayers": 100, + "CurrentPlayers": 0, + "Unk4": 0, + "Unk5": 0, + "Unk6": 0, + "Unk7": 0, + "Unk8": 0, + "Unk9": 0, + "Unk10": 319, + "Unk11": 248, + "Unk12": 159, + "Unk13": 12345 + } + ] + }, + { + "name": " Server #2", + "ip": "", + "unk2": 0, + "type": 1, + "season": 3, + "unk6": 0, + "allowedclientflags": 0, + "channels": [ + { + "port": 54002, + "MaxPlayers": 50, + "CurrentPlayers": 0, + "Unk4": 0, + "Unk5": 0, + "Unk6": 0, + "Unk7": 0, + "Unk8": 0, + "Unk9": 0, + "Unk10": 318, + "Unk11": 251, + "Unk12": 155, + "Unk13": 12345 + } + ] + }, + { + "name": " Server #3", + "ip": "", + "unk2": 0, + "type": 2, + "season": 1, + "unk6": 0, + "allowedclientflags": 0, + "channels": [ + { + "port": 54003, + "MaxPlayers": 50, + "CurrentPlayers": 0, + "Unk4": 0, + "Unk5": 0, + "Unk6": 0, + "Unk7": 0, + "Unk8": 0, + "Unk9": 0, + "Unk10": 318, + "Unk11": 251, + "Unk12": 155, + "Unk13": 12345 + } + ] + }, + { + "name": " Server #4", + "ip": "", + "unk2": 0, + "type": 4, + "season": 0, + "unk6": 0, + "allowedclientflags": 0, + "channels": [ + { + "port": 54004, + "MaxPlayers": 50, + "CurrentPlayers": 0, + "Unk4": 0, + "Unk5": 0, + "Unk6": 0, + "Unk7": 0, + "Unk8": 0, + "Unk9": 0, + "Unk10": 318, + "Unk11": 251, + "Unk12": 155, + "Unk13": 12345 + } + ] + + } + ] + } } \ No newline at end of file diff --git a/Erupe/config/config.go b/Erupe/config/config.go index 9b6d65165..bfb09134e 100644 --- a/Erupe/config/config.go +++ b/Erupe/config/config.go @@ -1,157 +1,159 @@ -package config - -import ( - "log" - "net" - - "github.com/spf13/viper" -) - -// Config holds the global server-wide config. -type Config struct { - HostIP string `mapstructure:"host_ip"` - BinPath string `mapstructure:"bin_path"` - DevMode bool - - DevModeOptions DevModeOptions - Discord Discord - Database Database - Launcher Launcher - Sign Sign - Channel Channel - Entrance Entrance -} - -// DevModeOptions holds various debug/temporary options for use while developing Erupe. -type DevModeOptions struct { - ServerName string // To get specific instance server about (Current Players/Event Week) - CleanDB bool // Automatically wipes the DB on server reset. - MaxLauncherHR bool // Sets the HR returned in the launcher to HR9 so that you can join non-beginner worlds. - FixedStageID bool // Causes all move_stage to use the ID sl1Ns200p0a0u0 to get you into all stages - LogOutboundMessages bool // Log all messages sent to the clients - Event int // Changes the current event - OpcodeMessages bool // Get all message for Opcodes - SaveDumps SaveDumpOptions -} - -type SaveDumpOptions struct { - Enabled bool - OutputDir string -} - -// Discord holds the discord integration config. -type Discord struct { - Enabled bool - BotToken string - ChannelID string -} - -// Database holds the postgres database config. -type Database struct { - Host string - Port int - User string - Password string - Database string -} - -// Launcher holds the launcher server config. -type Launcher struct { - Port int - UseOriginalLauncherFiles bool -} - -// Sign holds the sign server config. -type Sign struct { - Port int -} - -// Channel holds the channel server config. -type Channel struct { - Port1 int - Port2 int - Port3 int - Port4 int -} - -// Entrance holds the entrance server config. -type Entrance struct { - Port uint16 - Entries []EntranceServerInfo -} - -// EntranceServerInfo represents an entry in the serverlist. -type EntranceServerInfo struct { - IP string - Unk2 uint16 - Type uint8 // Server type. 0=?, 1=open, 2=cities, 3=newbie, 4=bar - Season uint8 // Server activity. 0 = green, 1 = orange, 2 = blue - Unk6 uint8 // Something to do with server recommendation on 0, 3, and 5. - Name string // Server name, 66 byte null terminated Shift-JIS(JP) or Big5(TW). - - // 4096(PC, PS3/PS4)?, 8258(PC, PS3/PS4)?, 8192 == nothing? - // THIS ONLY EXISTS IF Binary8Header.type == "SV2", NOT "SVR"! - AllowedClientFlags uint32 - - Channels []EntranceChannelInfo -} - -// EntranceChannelInfo represents an entry in a server's channel list. -type EntranceChannelInfo struct { - Port uint16 - MaxPlayers uint16 - CurrentPlayers uint16 - Unk4 uint16 - Unk5 uint16 - Unk6 uint16 - Unk7 uint16 - Unk8 uint16 - Unk9 uint16 - Unk10 uint16 - Unk11 uint16 - Unk12 uint16 - Unk13 uint16 -} - -// getOutboundIP4 gets the preferred outbound ip4 of this machine -// From https://stackoverflow.com/a/37382208 -func getOutboundIP4() net.IP { - conn, err := net.Dial("udp4", "8.8.8.8:80") - if err != nil { - log.Fatal(err) - } - defer conn.Close() - - localAddr := conn.LocalAddr().(*net.UDPAddr) - - return localAddr.IP.To4() -} - -// LoadConfig loads the given config toml file. -func LoadConfig() (*Config, error) { - viper.SetConfigName("config") - viper.AddConfigPath(".") - - viper.SetDefault("DevModeOptions.SaveDumps", SaveDumpOptions{ - Enabled: false, - OutputDir: "savedata", - }) - - err := viper.ReadInConfig() - if err != nil { - return nil, err - } - - c := &Config{} - err = viper.Unmarshal(c) - if err != nil { - return nil, err - } - - if c.HostIP == "" { - c.HostIP = getOutboundIP4().To4().String() - } - - return c, nil -} - +package config + +import ( + "log" + "net" + + "github.com/spf13/viper" +) + +// Config holds the global server-wide config. +type Config struct { + HostIP string `mapstructure:"host_ip"` + BinPath string `mapstructure:"bin_path"` + DevMode bool + + DevModeOptions DevModeOptions + Discord Discord + Database Database + Launcher Launcher + Sign Sign + Channel Channel + Entrance Entrance +} + +// DevModeOptions holds various debug/temporary options for use while developing Erupe. +type DevModeOptions struct { + ServerName string // To get specific instance server about (Current Players/Event Week) + CleanDB bool // Automatically wipes the DB on server reset. + MaxLauncherHR bool // Sets the HR returned in the launcher to HR9 so that you can join non-beginner worlds. + FixedStageID bool // Causes all move_stage to use the ID sl1Ns200p0a0u0 to get you into all stages + LogOutboundMessages bool // Log all messages sent to the clients + Event int // Changes the current event + OpcodeMessages bool // Get all message for Opcodes + SaveDumps SaveDumpOptions +} + +type SaveDumpOptions struct { + Enabled bool + OutputDir string +} + +// Discord holds the discord integration config. +type Discord struct { + Enabled bool + BotToken string + ServerID string + RealtimeChannelID string + DevRoles []string + DevMode bool +} + +// Database holds the postgres database config. +type Database struct { + Host string + Port int + User string + Password string + Database string +} + +// Launcher holds the launcher server config. +type Launcher struct { + Port int + UseOriginalLauncherFiles bool +} + +// Sign holds the sign server config. +type Sign struct { + Port int +} + +// Channel holds the channel server config. +type Channel struct { + Port1 int + Port2 int + Port3 int + Port4 int +} + +// Entrance holds the entrance server config. +type Entrance struct { + Port uint16 + Entries []EntranceServerInfo +} + +// EntranceServerInfo represents an entry in the serverlist. +type EntranceServerInfo struct { + IP string + Unk2 uint16 + Type uint8 // Server type. 0=?, 1=open, 2=cities, 3=newbie, 4=bar + Season uint8 // Server activity. 0 = green, 1 = orange, 2 = blue + Unk6 uint8 // Something to do with server recommendation on 0, 3, and 5. + Name string // Server name, 66 byte null terminated Shift-JIS(JP) or Big5(TW). + + // 4096(PC, PS3/PS4)?, 8258(PC, PS3/PS4)?, 8192 == nothing? + // THIS ONLY EXISTS IF Binary8Header.type == "SV2", NOT "SVR"! + AllowedClientFlags uint32 + + Channels []EntranceChannelInfo +} + +// EntranceChannelInfo represents an entry in a server's channel list. +type EntranceChannelInfo struct { + Port uint16 + MaxPlayers uint16 + CurrentPlayers uint16 + Unk4 uint16 + Unk5 uint16 + Unk6 uint16 + Unk7 uint16 + Unk8 uint16 + Unk9 uint16 + Unk10 uint16 + Unk11 uint16 + Unk12 uint16 + Unk13 uint16 +} + +// getOutboundIP4 gets the preferred outbound ip4 of this machine +// From https://stackoverflow.com/a/37382208 +func getOutboundIP4() net.IP { + conn, err := net.Dial("udp4", "8.8.8.8:80") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) + + return localAddr.IP.To4() +} + +// LoadConfig loads the given config toml file. +func LoadConfig() (*Config, error) { + viper.SetConfigName("config") + viper.AddConfigPath(".") + + viper.SetDefault("DevModeOptions.SaveDumps", SaveDumpOptions{ + Enabled: false, + OutputDir: "savedata", + }) + + err := viper.ReadInConfig() + if err != nil { + return nil, err + } + + c := &Config{} + err = viper.Unmarshal(c) + if err != nil { + return nil, err + } + + if c.HostIP == "" { + c.HostIP = getOutboundIP4().To4().String() + } + + return c, nil +} diff --git a/Erupe/main.go b/Erupe/main.go index 89cdf39fb..236bbeaeb 100644 --- a/Erupe/main.go +++ b/Erupe/main.go @@ -1,177 +1,215 @@ -package main - -import ( - "fmt" - "os" - "os/signal" - "syscall" - "time" - - "github.com/Solenataris/Erupe/config" - "github.com/Solenataris/Erupe/server/channelserver" - "github.com/Solenataris/Erupe/server/entranceserver" - "github.com/Solenataris/Erupe/server/launcherserver" - "github.com/Solenataris/Erupe/server/signserver" - "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" - "go.uber.org/zap" -) - -// Temporary DB auto clean on startup for quick development & testing. -func cleanDB(db *sqlx.DB) { - _ = db.MustExec("DELETE FROM guild_characters") - _ = db.MustExec("DELETE FROM guilds") - _ = db.MustExec("DELETE FROM characters") - _ = db.MustExec("DELETE FROM sign_sessions") - _ = db.MustExec("DELETE FROM users") -} - -func main() { - zapLogger, _ := zap.NewDevelopment() - defer zapLogger.Sync() - logger := zapLogger.Named("main") - - logger.Info("Starting Erupe") - - // Load the configuration. - erupeConfig, err := config.LoadConfig() - if err != nil { - logger.Fatal("Failed to load config", zap.Error(err)) - } - - // Create the postgres DB pool. - connectString := fmt.Sprintf( - "host=%s port=%d user=%s password=%s dbname= %s sslmode=disable", - erupeConfig.Database.Host, - erupeConfig.Database.Port, - erupeConfig.Database.User, - erupeConfig.Database.Password, - erupeConfig.Database.Database, - ) - - db, err := sqlx.Open("postgres", connectString) - if err != nil { - logger.Fatal("Failed to open sql database", zap.Error(err)) - } - - // Test the DB connection. - err = db.Ping() - if err != nil { - logger.Fatal("Failed to ping database", zap.Error(err)) - } - logger.Info("Connected to database") - - // Clean the DB if the option is on. - if erupeConfig.DevMode && erupeConfig.DevModeOptions.CleanDB { - logger.Info("Cleaning DB") - cleanDB(db) - logger.Info("Done cleaning DB") - } - - // Now start our server(s). - - // Launcher HTTP server. - launcherServer := launcherserver.NewServer( - &launcherserver.Config{ - Logger: logger.Named("launcher"), - ErupeConfig: erupeConfig, - DB: db, - UseOriginalLauncherFiles: erupeConfig.Launcher.UseOriginalLauncherFiles, - }) - err = launcherServer.Start() - if err != nil { - logger.Fatal("Failed to start launcher server", zap.Error(err)) - } - logger.Info("Started launcher server.") - - // Entrance server. - entranceServer := entranceserver.NewServer( - &entranceserver.Config{ - Logger: logger.Named("entrance"), - ErupeConfig: erupeConfig, - DB: db, - }) - err = entranceServer.Start() - if err != nil { - logger.Fatal("Failed to start entrance server", zap.Error(err)) - } - logger.Info("Started entrance server.") - - // Sign server. - signServer := signserver.NewServer( - &signserver.Config{ - Logger: logger.Named("sign"), - ErupeConfig: erupeConfig, - DB: db, - }) - err = signServer.Start() - if err != nil { - logger.Fatal("Failed to start sign server", zap.Error(err)) - } - logger.Info("Started sign server.") - - // Channel Server - channelServer1 := channelserver.NewServer( - &channelserver.Config{ - Logger: logger.Named("channel"), - ErupeConfig: erupeConfig, - DB: db, - }) - - err = channelServer1.Start(erupeConfig.Channel.Port1) - if err != nil { - logger.Fatal("Failed to start channel server1", zap.Error(err)) - } - logger.Info("Started channel server.") - // Channel Server - channelServer2 := channelserver.NewServer( - &channelserver.Config{ - Logger: logger.Named("channel"), - ErupeConfig: erupeConfig, - DB: db, - }) - - err = channelServer2.Start(erupeConfig.Channel.Port2) - if err != nil { - logger.Fatal("Failed to start channel server2", zap.Error(err)) - } - // Channel Server - channelServer3 := channelserver.NewServer( - &channelserver.Config{ - Logger: logger.Named("channel"), - ErupeConfig: erupeConfig, - DB: db, - }) - - err = channelServer3.Start(erupeConfig.Channel.Port3) - if err != nil { - logger.Fatal("Failed to start channel server3", zap.Error(err)) - } - // Channel Server - channelServer4 := channelserver.NewServer( - &channelserver.Config{ - Logger: logger.Named("channel"), - ErupeConfig: erupeConfig, - DB: db, - }) - - err = channelServer4.Start(erupeConfig.Channel.Port4) - if err != nil { - logger.Fatal("Failed to start channel server4", zap.Error(err)) - } - // Wait for exit or interrupt with ctrl+C. - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - <-c - - logger.Info("Trying to shutdown gracefully.") - channelServer4.Shutdown() - channelServer3.Shutdown() - channelServer2.Shutdown() - channelServer1.Shutdown() - signServer.Shutdown() - entranceServer.Shutdown() - launcherServer.Shutdown() - - time.Sleep(1 * time.Second) -} +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/Solenataris/Erupe/config" + "github.com/Solenataris/Erupe/server/channelserver" + "github.com/Solenataris/Erupe/server/discordbot" + "github.com/Solenataris/Erupe/server/entranceserver" + "github.com/Solenataris/Erupe/server/launcherserver" + "github.com/Solenataris/Erupe/server/signserver" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "go.uber.org/zap" +) + +// Temporary DB auto clean on startup for quick development & testing. +func cleanDB(db *sqlx.DB) { + _ = db.MustExec("DELETE FROM guild_characters") + _ = db.MustExec("DELETE FROM guilds") + _ = db.MustExec("DELETE FROM characters") + _ = db.MustExec("DELETE FROM sign_sessions") + _ = db.MustExec("DELETE FROM users") +} + +func main() { + zapLogger, _ := zap.NewDevelopment() + defer zapLogger.Sync() + logger := zapLogger.Named("main") + + logger.Info("Starting Erupe") + + // Load the configuration. + erupeConfig, err := config.LoadConfig() + if err != nil { + logger.Fatal("Failed to load config", zap.Error(err)) + } + + // Discord bot + var discordBot *discordbot.DiscordBot = nil + + if erupeConfig.Discord.Enabled { + bot, err := discordbot.NewDiscordBot(discordbot.DiscordBotOptions{ + Logger: logger, + Config: erupeConfig, + }) + + if err != nil { + logger.Fatal("Failed to create discord bot", zap.Error(err)) + } + + // Discord bot + err = bot.Start() + + if err != nil { + logger.Fatal("Failed to starts discord bot", zap.Error(err)) + } + + discordBot = bot + } else { + logger.Info("Discord bot is disabled") + } + + // Create the postgres DB pool. + connectString := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname= %s sslmode=disable", + erupeConfig.Database.Host, + erupeConfig.Database.Port, + erupeConfig.Database.User, + erupeConfig.Database.Password, + erupeConfig.Database.Database, + ) + + db, err := sqlx.Open("postgres", connectString) + if err != nil { + logger.Fatal("Failed to open sql database", zap.Error(err)) + } + + // Test the DB connection. + err = db.Ping() + if err != nil { + logger.Fatal("Failed to ping database", zap.Error(err)) + } + logger.Info("Connected to database") + + // Clean the DB if the option is on. + if erupeConfig.DevMode && erupeConfig.DevModeOptions.CleanDB { + logger.Info("Cleaning DB") + cleanDB(db) + logger.Info("Done cleaning DB") + } + + // Now start our server(s). + + // Launcher HTTP server. + launcherServer := launcherserver.NewServer( + &launcherserver.Config{ + Logger: logger.Named("launcher"), + ErupeConfig: erupeConfig, + DB: db, + UseOriginalLauncherFiles: erupeConfig.Launcher.UseOriginalLauncherFiles, + }) + err = launcherServer.Start() + if err != nil { + logger.Fatal("Failed to start launcher server", zap.Error(err)) + } + logger.Info("Started launcher server.") + + // Entrance server. + entranceServer := entranceserver.NewServer( + &entranceserver.Config{ + Logger: logger.Named("entrance"), + ErupeConfig: erupeConfig, + DB: db, + }) + err = entranceServer.Start() + if err != nil { + logger.Fatal("Failed to start entrance server", zap.Error(err)) + } + logger.Info("Started entrance server.") + + // Sign server. + signServer := signserver.NewServer( + &signserver.Config{ + Logger: logger.Named("sign"), + ErupeConfig: erupeConfig, + DB: db, + }) + err = signServer.Start() + if err != nil { + logger.Fatal("Failed to start sign server", zap.Error(err)) + } + logger.Info("Started sign server.") + + // Channel Server + channelServer1 := channelserver.NewServer( + &channelserver.Config{ + Logger: logger.Named("channel"), + ErupeConfig: erupeConfig, + DB: db, + Name: erupeConfig.Entrance.Entries[0].Name, + Enable: erupeConfig.Entrance.Entries[0].Channels[0].MaxPlayers > 0, + //DiscordBot: discordBot, + }) + + err = channelServer1.Start(erupeConfig.Channel.Port1) + if err != nil { + logger.Fatal("Failed to start channel server1", zap.Error(err)) + } + logger.Info("Started channel server.") + // Channel Server + channelServer2 := channelserver.NewServer( + &channelserver.Config{ + Logger: logger.Named("channel"), + ErupeConfig: erupeConfig, + DB: db, + Name: erupeConfig.Entrance.Entries[1].Name, + Enable: erupeConfig.Entrance.Entries[1].Channels[0].MaxPlayers > 0, + DiscordBot: discordBot, + }) + + err = channelServer2.Start(erupeConfig.Channel.Port2) + if err != nil { + logger.Fatal("Failed to start channel server2", zap.Error(err)) + } + // Channel Server + channelServer3 := channelserver.NewServer( + &channelserver.Config{ + Logger: logger.Named("channel"), + ErupeConfig: erupeConfig, + DB: db, + Name: erupeConfig.Entrance.Entries[2].Name, + Enable: erupeConfig.Entrance.Entries[2].Channels[0].MaxPlayers > 0, + //DiscordBot: discordBot, + }) + + err = channelServer3.Start(erupeConfig.Channel.Port3) + if err != nil { + logger.Fatal("Failed to start channel server3", zap.Error(err)) + } + // Channel Server + channelServer4 := channelserver.NewServer( + &channelserver.Config{ + Logger: logger.Named("channel"), + ErupeConfig: erupeConfig, + DB: db, + Name: erupeConfig.Entrance.Entries[3].Name, + Enable: erupeConfig.Entrance.Entries[3].Channels[0].MaxPlayers > 0, + //DiscordBot: discordBot, + }) + + err = channelServer4.Start(erupeConfig.Channel.Port4) + if err != nil { + logger.Fatal("Failed to start channel server4", zap.Error(err)) + } + // Wait for exit or interrupt with ctrl+C. + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c + + logger.Info("Trying to shutdown gracefully.") + channelServer4.Shutdown() + channelServer3.Shutdown() + channelServer2.Shutdown() + channelServer1.Shutdown() + signServer.Shutdown() + entranceServer.Shutdown() + launcherServer.Shutdown() + + time.Sleep(1 * time.Second) +} diff --git a/Erupe/server/channelserver/handlers_cast_binary.go b/Erupe/server/channelserver/handlers_cast_binary.go index b4ad6e799..87a10da8b 100644 --- a/Erupe/server/channelserver/handlers_cast_binary.go +++ b/Erupe/server/channelserver/handlers_cast_binary.go @@ -129,9 +129,8 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { fmt.Printf("Got chat message: %+v\n", chatMessage) // Discord integration - if s.server.erupeConfig.Discord.Enabled { - message := fmt.Sprintf("%s: %s", chatMessage.SenderName, chatMessage.Message) - s.server.discordSession.ChannelMessageSend(s.server.erupeConfig.Discord.ChannelID, message) + if chatMessage.Type == binpacket.ChatTypeLocal || chatMessage.Type == binpacket.ChatTypeParty { + s.server.DiscordChannelSend(chatMessage.SenderName, chatMessage.Message) } // RAVI COMMANDS diff --git a/Erupe/server/channelserver/handlers_discord.go b/Erupe/server/channelserver/handlers_discord.go index aaad2472a..5f2ba01ce 100644 --- a/Erupe/server/channelserver/handlers_discord.go +++ b/Erupe/server/channelserver/handlers_discord.go @@ -2,162 +2,344 @@ package channelserver import ( "fmt" - "os" + "sort" "strings" - "time" "github.com/bwmarrin/discordgo" ) -// onDiscordMessage handles receiving messages from discord and forwarding them ingame. -func (s *Server) onDiscordMessage(ds *discordgo.Session, m *discordgo.MessageCreate) { - // Ignore messages from our bot, or ones that are not in the correct channel. - if m.Author.ID == ds.State.User.ID || m.ChannelID != s.erupeConfig.Discord.ChannelID { - return - } - - // Broadcast to the game clients. - data := m.Content - - // Split on comma. - result := strings.Split(data, " ") - - if result[0] == "!maintenancedate" && m.Author.ID == "836027554628370492" { - replaceDays := dayConvert(result[1]) - replaceMonth := MonthConvert(result[3]) - - s.BroadcastChatMessage("MAINTENANCE EXCEPTIONNELLE :") - s.BroadcastChatMessage("Les serveurs seront temporairement inaccessibles le") - s.BroadcastChatMessage(fmt.Sprintf("%s %s %s %s a partir de %s heures et %s minutes.", replaceDays, result[2], replaceMonth, result[4], result[5], result[6])) // Jour Mois Année Heures Minutes - s.BroadcastChatMessage("Evitez de vous connecter durant cette periode. Veuillez nous") - s.BroadcastChatMessage("excuser pour la gene occasionee. Merci de votre cooperation.") - return - } else if result[0] == "!maintenance" && m.Author.ID == "836027554628370492" { - s.BroadcastChatMessage("RAPPEL DE MAINTENANCE DU MARDI (18H-22H): Les serveurs seront") - s.BroadcastChatMessage("temporairement inaccessibles dans 15 minutes. Veuillez ne pas") - s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas") - s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous") - s.BroadcastChatMessage("excuser pour la gene occasionnee. Merci de votre cooperation.") - s.TimerUpdate(15, 0, true) - return - } else if result[0] == "!Rmaintenancedate" && m.Author.ID == "836027554628370492" { - s.BroadcastChatMessage("RAPPEL DE MAINTENANCE EXCEPTIONNELLE: Les serveurs seront") - s.BroadcastChatMessage("temporairement inaccessibles dans 15 minutes. Veuillez ne pas") - s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas") - s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous") - s.BroadcastChatMessage("excuser pour la gene occasionnee. Merci de votre cooperation.") - s.TimerUpdate(15, 1, true) - return - } else if result[0] == "!maintenanceStop" && m.Author.ID == "836027554628370492" { - s.BroadcastChatMessage("INFORMATION: A titre exceptionnel, il n'y aura pas de") - s.BroadcastChatMessage("maintenance de 18h a 22h, vous pouvez continuer de jouer") - s.BroadcastChatMessage("librement jusqu'a la prochaine annonce de maintenance !") - s.BroadcastChatMessage("Bonne chasse !") - s.TimerUpdate(0, 0, false) - return - } else if result[0] == "!maintenanceStopExec" && m.Author.ID == "836027554628370492" { - replaceDays := dayConvert(result[1]) - replaceMonth := MonthConvert(result[3]) - - s.BroadcastChatMessage("INFORMATION: A titre exceptionnel, il n'y aura pas de") - s.BroadcastChatMessage(fmt.Sprintf("maintenance le %s %s %s %s a partir de", replaceDays, result[2], replaceMonth, result[4])) // Jour Mois Année - s.BroadcastChatMessage(fmt.Sprintf("%s heures et %s minutes, vous pouvez continuer de jouer", result[5], result[6])) // Heures Minutes - s.BroadcastChatMessage("librement jusqu'a la prochaine annonce de maintenance !") - s.BroadcastChatMessage("Bonne chasse !") - s.TimerUpdate(0, 1, false) - return - } - - message := fmt.Sprintf("[DISCORD] %s: %s", m.Author.Username, m.Content) - s.BroadcastChatMessage(message) +type Character struct { + ID uint32 `db:"id"` + IsFemale bool `db:"is_female"` + IsNewCharacter bool `db:"is_new_character"` + Name string `db:"name"` + UnkDescString string `db:"unk_desc_string"` + HRP uint16 `db:"hrp"` + GR uint16 `db:"gr"` + WeaponType uint16 `db:"weapon_type"` + LastLogin uint32 `db:"last_login"` } -func dayConvert(result string) string { - var replaceDays string +var weapons = []string{ + "<:gs:970861408227049477>", + "<:hbg:970861408281563206>", + "<:hm:970861408239628308>", + "<:lc:970861408298352660>", + "<:sns:970861408319315988>", + "<:lbg:970861408327725137>", + "<:ds:970861408277368883>", + "<:ls:970861408319311872>", + "<:hh:970861408222863400>", + "<:gl:970861408327720980>", + "<:bw:970861408294174780>", + "<:tf:970861408424177744>", + "<:sw:970861408340283472>", + "<:ms:970861408411594842>", +} - if result == "1" { - replaceDays = "Lundi" - } else if result == "2" { - replaceDays = "Mardi" - } else if result == "3" { - replaceDays = "Mercredi" - } else if result == "4" { - replaceDays = "Jeudi" - } else if result == "5" { - replaceDays = "Vendredi" - } else if result == "6" { - replaceDays = "Samedi" - } else if result == "7" { - replaceDays = "Dimanche" +func (s *Server) getCharacterForUser(uid int) (*Character, error) { + character := Character{} + err := s.db.Get(&character, "SELECT id, is_female, is_new_character, name, unk_desc_string, hrp, gr, weapon_type, last_login FROM characters WHERE id = $1", uid) + if err != nil { + return nil, err + } + + return &character, nil +} + +func CountChars(s *Server) string { + count := 0 + for _, stage := range s.stages { + count += len(stage.clients) + } + + message := fmt.Sprintf("Server [%s]: %d players;", s.name, count) + + return message +} + +type ListPlayer struct { + CharName string + InQuest bool + WeaponEmoji string + QuestEmoji string + StageName string +} + +func (p *ListPlayer) toString(length int) string { + status := "" + if p.InQuest { + status = "(in quest " + p.QuestEmoji + ")" } else { - replaceDays = "NULL" + status = p.StageName } - return replaceDays + missingSpace := length - len(p.CharName) + whitespaces := strings.Repeat(" ", missingSpace+5) + + return fmt.Sprintf("%s %s %s %s", p.WeaponEmoji, p.CharName, whitespaces, status) } -func MonthConvert(result string) string { - var replaceMonth string - - if result == "01" { - replaceMonth = "Janvier" - } else if result == "02" { - replaceMonth = "Fevrier" - } else if result == "03" { - replaceMonth = "Mars" - } else if result == "04" { - replaceMonth = "Avril" - } else if result == "05" { - replaceMonth = "Mai" - } else if result == "06" { - replaceMonth = "Juin" - } else if result == "07" { - replaceMonth = "Juillet" - } else if result == "08" { - replaceMonth = "Aout" - } else if result == "09" { - replaceMonth = "Septembre" - } else if result == "10" { - replaceMonth = "Octobre" - } else if result == "11" { - replaceMonth = "Novembre" - } else if result == "12" { - replaceMonth = "Decembre" - } else { - replaceMonth = "NULL" +func getPlayerList(s *Server) ([]ListPlayer, int) { + list := []ListPlayer{} + questEmojis := []string{ + ":white_circle:", + ":red_circle:", + ":blue_circle:", + ":brown_circle:", + ":green_circle:", + ":purple_circle:", + ":yellow_circle:", + ":orange_circle:", } - return replaceMonth -} + bigNameLen := 0 -func (s *Server) TimerUpdate(timer int, typeStop int, disableAutoOff bool) { - timertotal := 0 - for timer > 0 { - time.Sleep(1 * time.Minute) - timer -= 1 - timertotal += 1 - if disableAutoOff { - // Un message s'affiche toutes les 10 minutes pour prévenir de la maintenance. - if timertotal == 10 { - timertotal = 0 - if typeStop == 0 { - s.BroadcastChatMessage("RAPPEL DE MAINTENANCE DU MARDI (18H-22H): Les serveurs seront") - s.BroadcastChatMessage(fmt.Sprintf("temporairement inaccessibles dans %d minutes. Veuillez ne pas", timer)) - s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas") - s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous excuser") - s.BroadcastChatMessage("pour la gene occasionnee. Merci de votre cooperation.") - } else { - s.BroadcastChatMessage("RAPPEL DE MAINTENANCE EXCEPTIONNELLE: Les serveurs seront") - s.BroadcastChatMessage(fmt.Sprintf("temporairement inaccessibles dans %d minutes. Veuillez ne pas", timer)) - s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas") - s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous excuser") - s.BroadcastChatMessage("pour la gene occasionnee. Merci de votre cooperation.") + for _, stage := range s.stages { + if len(stage.clients) == 0 { + continue + } + + questEmoji := ":black_circle:" + + if len(questEmojis) > 0 { + questEmoji = questEmojis[len(questEmojis)-1] + questEmojis = questEmojis[:len(questEmojis)-1] + } + + isQuest := stage.isQuest() + for client := range stage.clients { + char, err := s.getCharacterForUser(int(client.charID)) + if err == nil { + if len(char.Name) > bigNameLen { + bigNameLen = len(char.Name) } - } - // Déconnecter tous les joueurs du serveur. - if timer == 0 { - os.Exit(-1) + + list = append(list, ListPlayer{ + CharName: char.Name, + InQuest: isQuest, + QuestEmoji: questEmoji, + WeaponEmoji: weapons[char.WeaponType], + StageName: stage.GetName(), + }) + } } } + + return list, bigNameLen +} + +func PlayerList(s *Server) string { + list := "" + count := 0 + listPlayers, bigNameLen := getPlayerList(s) + + sort.SliceStable(listPlayers, func(i, j int) bool { + return listPlayers[i].CharName < listPlayers[j].CharName + }) + + for _, lp := range listPlayers { + list += lp.toString(bigNameLen) + "\n" + count++ + } + + message := fmt.Sprintf("<:SnS:822963937360347148> Frontier Hunters Online: [%s ] <:switcha:822963906401533992> \n============== Total %d =============\n", s.name, count) + message += list + + return message +} + +func debug(s *Server) string { + list := "" + + for _, stage := range s.stages { + if !stage.isQuest() && len(stage.objects) == 0 { + continue + } + + list += fmt.Sprintf(" -> Stage: %s StageId: %s\n", stage.GetName(), stage.id) + isQuest := "false" + hasDeparted := "false" + + if stage.isQuest() { + isQuest = "true" + } + + list += fmt.Sprintf(" '-> isQuest: %s\n", isQuest) + + if stage.isQuest() { + if stage.hasDeparted { + hasDeparted = "true" + } + + list += fmt.Sprintf(" '-> isDeparted: %s\n", hasDeparted) + list += fmt.Sprintf(" '-> reserveSlots (%d/%d)\n", len(stage.reservedClientSlots), stage.maxPlayers) + + for charid, _ := range stage.reservedClientSlots { + char, err := s.getCharacterForUser(int(charid)) + if err == nil { + list += fmt.Sprintf(" '-> %s\n", char.Name) + } + } + } + + list += " '-> objects: \n" + for _, obj := range stage.objects { + objInfo := fmt.Sprintf("X,Y,Z: %f %f %f", obj.x, obj.y, obj.z) + list += fmt.Sprintf(" '-> ObjectId: %d - %s\n", obj.id, objInfo) + } + } + + message := fmt.Sprintf("Objects in Server: [%s ]\n", s.name) + message += list + + return message +} + +func questlist(s *Server) string { + list := "" + + for _, stage := range s.stages { + if !stage.isQuest() { + continue + } + + hasDeparted := "" + if stage.hasDeparted { + hasDeparted = " - departed" + } + list += fmt.Sprintf(" '-> StageId: %s (%d/%d) %s - %s\n", stage.id, len(stage.reservedClientSlots), stage.maxPlayers, hasDeparted, stage.createdAt) + + for charid, _ := range stage.reservedClientSlots { + char, err := s.getCharacterForUser(int(charid)) + if err == nil { + list += fmt.Sprintf(" '-> %s\n", char.Name) + } + } + } + + message := fmt.Sprintf("Quests in Server: [%s ]\n", s.name) + message += list + + return message +} + +func removeStageById(s *Server, stageId string) string { + if s.stages[stageId] != nil { + delete(s.stages, stageId) + return "Stage deleted!" + } + + return "Stage not found!" +} + +func cleanStr(str string) string { + return strings.ToLower(strings.Trim(str, " ")) +} + +func getCharInfo(server *Server, charName string) string { + var s *Stage + var c *Session + + for _, stage := range server.stages { + for client := range stage.clients { + + if client.Name == "" { + continue + } + + if cleanStr(client.Name) == cleanStr(charName) { + s = stage + c = client + } + + } + } + + if s == nil { + return "Character not found" + } + + objInfo := "" + + obj := server.FindStageObjectByChar(c.charID) + // server.logger.Info("Found object: %+v", zap.Object("obj", obj)) + + if obj != nil { + objInfo = fmt.Sprintf("X,Y,Z: %f %f %f", obj.x, obj.y, obj.z) + } + + return fmt.Sprintf("Character: %s\nStage: %s\nStageId: %s\n%s", c.Name, s.GetName(), s.id, objInfo) +} + +func (s *Server) isDiscordAdmin(ds *discordgo.Session, m *discordgo.MessageCreate) bool { + for _, role := range m.Member.Roles { + for _, id := range s.erupeConfig.Discord.DevRoles { + if id == role { + return true + } + } + } + + return false +} + +// onDiscordMessage handles receiving messages from discord and forwarding them ingame. +func (s *Server) onDiscordMessage(ds *discordgo.Session, m *discordgo.MessageCreate) { + // Ignore messages from our bot, or ones that are not in the correct channel. + if m.Author.ID == ds.State.User.ID || !s.enable { + return + } + + // Ignore other channels in devMode + if s.erupeConfig.Discord.DevMode && m.ChannelID != s.erupeConfig.Discord.RealtimeChannelID { + return + } + + args := strings.Split(m.Content, " ") + commandName := args[0] + + // Move to slash commadns + if commandName == "!players" { + ds.ChannelMessageSend(m.ChannelID, PlayerList(s)) + return + } + + if commandName == "-char" { + if len(args) < 2 { + ds.ChannelMessageSend(m.ChannelID, "Usage: !char ") + return + } + + charName := strings.Join(args[1:], " ") + ds.ChannelMessageSend(m.ChannelID, getCharInfo(s, charName)) + return + } + + if commandName == "!debug" && s.isDiscordAdmin(ds, m) { + ds.ChannelMessageSend(m.ChannelID, debug(s)) + return + } + + if commandName == "!questlist" && s.isDiscordAdmin(ds, m) { + ds.ChannelMessageSend(m.ChannelID, questlist(s)) + return + } + + if commandName == "!remove-stage" && s.isDiscordAdmin(ds, m) { + if len(args) < 2 { + ds.ChannelMessageSend(m.ChannelID, "Usage: !remove-stage ") + return + } + + stageId := strings.Join(args[1:], " ") + ds.ChannelMessageSend(m.ChannelID, removeStageById(s, stageId)) + return + } + + if m.ChannelID == s.erupeConfig.Discord.RealtimeChannelID { + message := fmt.Sprintf("[DISCORD] %s: %s", m.Author.Username, m.Content) + s.BroadcastChatMessage(s.discordBot.NormalizeDiscordMessage(message)) + } } diff --git a/Erupe/server/channelserver/sys_channel_server.go b/Erupe/server/channelserver/sys_channel_server.go index ea179e1ee..c5857a575 100644 --- a/Erupe/server/channelserver/sys_channel_server.go +++ b/Erupe/server/channelserver/sys_channel_server.go @@ -9,16 +9,36 @@ import ( "github.com/Solenataris/Erupe/config" "github.com/Solenataris/Erupe/network/binpacket" "github.com/Solenataris/Erupe/network/mhfpacket" - "github.com/bwmarrin/discordgo" + "github.com/Solenataris/Erupe/server/discordbot" "github.com/jmoiron/sqlx" "go.uber.org/zap" ) +type StageIdType = string + +const ( + // GlobalStage is the stage that is used for all users. + MezeportaStageId StageIdType = "sl1Ns200p0a0u0" + GuildHallLv1StageId StageIdType = "sl1Ns202p0a0u0" + GuildHallLv2StageId StageIdType = "sl1Ns203p0a0u0" + GuildHallLv3StageId StageIdType = "sl1Ns204p0a0u0" + PugiFarmStageId StageIdType = "sl1Ns205p0a0u0" + RastaBarStageId StageIdType = "sl1Ns211p0a0u0" + PalloneCaravanStageId StageIdType = "sl1Ns260p0a0u0" + GookFarmStageId StageIdType = "sl1Ns265p0a0u0" + DivaFountainStageId StageIdType = "sl2Ns379p0a0u0" + DivaHallStageId StageIdType = "sl1Ns445p0a0u0" + MezFesStageId StageIdType = "sl1Ns462p0a0u0" +) + // Config struct allows configuring the server. type Config struct { Logger *zap.Logger DB *sqlx.DB + DiscordBot *discordbot.DiscordBot ErupeConfig *config.Config + Name string + Enable bool } // Map key type for a user binary part. @@ -30,14 +50,13 @@ type userBinaryPartID struct { // Server is a MHF channel server. type Server struct { sync.Mutex - logger *zap.Logger - db *sqlx.DB - erupeConfig *config.Config - acceptConns chan net.Conn - deleteConns chan net.Conn - sessions map[net.Conn]*Session - listener net.Listener // Listener that is created when Server.Start is called. - + logger *zap.Logger + db *sqlx.DB + erupeConfig *config.Config + acceptConns chan net.Conn + deleteConns chan net.Conn + sessions map[net.Conn]*Session + listener net.Listener // Listener that is created when Server.Start is called. isShuttingDown bool stagesLock sync.RWMutex @@ -52,12 +71,72 @@ type Server struct { semaphore map[string]*Semaphore // Discord chat integration - discordSession *discordgo.Session + discordBot *discordbot.DiscordBot + + name string + enable bool + + raviente *Raviente +} + +type Raviente struct { + sync.Mutex + + register *RavienteRegister + state *RavienteState + support *RavienteSupport +} + +type RavienteRegister struct { + nextTime uint32 + startTime uint32 + postTime uint32 + killedTime uint32 + ravienteType uint32 + maxPlayers uint32 + carveQuest uint32 + register []uint32 +} + +type RavienteState struct { + damageMultiplier uint32 + stateData []uint32 +} + +type RavienteSupport struct { + supportData []uint32 +} + +// Set up the Raviente variables for the server +func NewRaviente() *Raviente { + ravienteRegister := &RavienteRegister { + nextTime: 0, + startTime: 0, + killedTime: 0, + postTime: 0, + ravienteType: 0, + maxPlayers: 0, + carveQuest: 0, + } + ravienteState := &RavienteState { + damageMultiplier: 1, + } + ravienteSupport := &RavienteSupport { } + ravienteRegister.register = []uint32{0, 0, 0, 0, 0} + ravienteState.stateData = []uint32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + ravienteSupport.supportData = []uint32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + + raviente := &Raviente { + register: ravienteRegister, + state: ravienteState, + support: ravienteSupport, + } + return raviente } // NewServer creates a new Server type. func NewServer(config *Config) *Server { - s := &Server{ + s := &Server { logger: config.Logger, db: config.DB, erupeConfig: config.ErupeConfig, @@ -67,7 +146,10 @@ func NewServer(config *Config) *Server { stages: make(map[string]*Stage), userBinaryParts: make(map[userBinaryPartID][]byte), semaphore: make(map[string]*Semaphore), - discordSession: nil, + discordBot: config.DiscordBot, + name: config.Name, + enable: config.Enable, + raviente: NewRaviente(), } // Mezeporta @@ -88,7 +170,7 @@ func NewServer(config *Config) *Server { // Rasta bar stage s.stages["sl1Ns211p0a0u0"] = NewStage("sl1Ns211p0a0u0") - // Carvane + // Pallone Carvan s.stages["sl1Ns260p0a0u0"] = NewStage("sl1Ns260p0a0u0") // Gook Farm @@ -103,16 +185,6 @@ func NewServer(config *Config) *Server { // MezFes s.stages["sl1Ns462p0a0u0"] = NewStage("sl1Ns462p0a0u0") - // Create the discord session, (not actually connecting to discord servers yet). - if s.erupeConfig.Discord.Enabled { - ds, err := discordgo.New("Bot " + s.erupeConfig.Discord.BotToken) - if err != nil { - s.logger.Fatal("Error creating Discord session.", zap.Error(err)) - } - ds.AddHandler(s.onDiscordMessage) - s.discordSession = ds - } - return s } @@ -128,12 +200,8 @@ func (s *Server) Start(port int) error { go s.manageSessions() // Start the discord bot for chat integration. - if s.erupeConfig.Discord.Enabled { - err = s.discordSession.Open() - if err != nil { - s.logger.Warn("Error opening Discord session.", zap.Error(err)) - return err - } + if s.erupeConfig.Discord.Enabled && s.discordBot != nil { + s.discordBot.Session.AddHandler(s.onDiscordMessage) } return nil @@ -146,11 +214,8 @@ func (s *Server) Shutdown() { s.Unlock() s.listener.Close() - close(s.acceptConns) - if s.erupeConfig.Discord.Enabled { - s.discordSession.Close() - } + close(s.acceptConns) } func (s *Server) acceptClients() { @@ -232,7 +297,7 @@ func (s *Server) BroadcastChatMessage(message string) { Type: 5, Flags: 0x80, Message: message, - SenderName: "Erupe", + SenderName: s.name, } msgBinChat.Build(bf) @@ -243,6 +308,13 @@ func (s *Server) BroadcastChatMessage(message string) { }, nil) } +func (s *Server) DiscordChannelSend(charName string, content string) { + if s.erupeConfig.Discord.Enabled && s.discordBot != nil { + message := fmt.Sprintf("**%s** : %s", charName, content) + s.discordBot.RealtimeChannelSend(message) + } +} + func (s *Server) FindSessionByCharID(charID uint32) *Session { s.stagesLock.RLock() defer s.stagesLock.RUnlock() @@ -259,3 +331,21 @@ func (s *Server) FindSessionByCharID(charID uint32) *Session { return nil } + +func (s *Server) FindStageObjectByChar(charID uint32) *StageObject { + s.stagesLock.RLock() + defer s.stagesLock.RUnlock() + for _, stage := range s.stages { + stage.RLock() + for objId := range stage.objects { + obj := stage.objects[objId] + if obj.ownerCharID == charID { + stage.RUnlock() + return obj + } + } + stage.RUnlock() + } + + return nil +} diff --git a/Erupe/server/channelserver/sys_stage.go b/Erupe/server/channelserver/sys_stage.go index 9887c7e17..2d372c77d 100644 --- a/Erupe/server/channelserver/sys_stage.go +++ b/Erupe/server/channelserver/sys_stage.go @@ -1,9 +1,12 @@ package channelserver import ( + "sync" + + "time" + "github.com/Andoryuuta/byteframe" "github.com/Solenataris/Erupe/network/mhfpacket" - "sync" ) // StageObject holds infomation about a specific stage object. @@ -15,7 +18,7 @@ type StageObject struct { } type ObjectMap struct { - id uint8 + id uint8 charid uint32 status bool } @@ -55,6 +58,7 @@ type Stage struct { maxPlayers uint16 hasDeparted bool password string + createdAt string } // NewStage creates a new stage with intialized values. @@ -68,6 +72,7 @@ func NewStage(ID string) *Stage { maxPlayers: 4, gameObjectCount: 1, objectList: make(map[uint8]*ObjectMap), + createdAt: time.Now().Format("01-02-2006 15:04:05"), } s.InitObjectList() return s @@ -94,30 +99,71 @@ func (s *Stage) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session) { } func (s *Stage) InitObjectList() { - for seq:=uint8(0x7f);seq>uint8(0);seq-- { - newObj := &ObjectMap{ - id: seq, - charid: uint32(0), - status: false, - } - s.objectList[seq] = newObj + for seq := uint8(0x7f); seq > uint8(0); seq-- { + newObj := &ObjectMap{ + id: seq, + charid: uint32(0), + status: false, } + s.objectList[seq] = newObj + } +} + +func (s *Stage) isCharInQuestByID(charID uint32) bool { + if _, exists := s.reservedClientSlots[charID]; exists { + return exists + } + + return false +} + +func (s *Stage) isQuest() bool { + return len(s.reservedClientSlots) > 0 +} + +func (stage *Stage) GetName() string { + switch stage.id { + case MezeportaStageId: + return "Mezeporta" + case GuildHallLv1StageId: + return "Guild Hall Lv1" + case GuildHallLv2StageId: + return "Guild Hall Lv2" + case GuildHallLv3StageId: + return "Guild Hall Lv3" + case PugiFarmStageId: + return "Pugi Farm" + case RastaBarStageId: + return "Rasta Bar" + case PalloneCaravanStageId: + return "Pallone Caravan" + case GookFarmStageId: + return "Gook Farm" + case DivaFountainStageId: + return "Diva Fountain" + case DivaHallStageId: + return "Diva Hall" + case MezFesStageId: + return "Mez Fes" + default: + return "" + } } func (s *Stage) GetNewObjectID(CharID uint32) uint32 { - ObjId:=uint8(0) - for seq:=uint8(0x7f);seq>uint8(0);seq--{ + ObjId := uint8(0) + for seq := uint8(0x7f); seq > uint8(0); seq-- { if s.objectList[seq].status == false { - ObjId=seq + ObjId = seq break } } - s.objectList[ObjId].status=true - s.objectList[ObjId].charid=CharID + s.objectList[ObjId].status = true + s.objectList[ObjId].charid = CharID bf := byteframe.NewByteFrame() bf.WriteUint8(uint8(0)) bf.WriteUint8(ObjId) bf.WriteUint16(uint16(0)) - obj :=uint32(bf.Data()[3]) | uint32(bf.Data()[2])<<8 | uint32(bf.Data()[1])<<16 | uint32(bf.Data()[0])<<32 + obj := uint32(bf.Data()[3]) | uint32(bf.Data()[2])<<8 | uint32(bf.Data()[1])<<16 | uint32(bf.Data()[0])<<32 return obj -} \ No newline at end of file +} diff --git a/Erupe/server/discordbot/discord_bot.go b/Erupe/server/discordbot/discord_bot.go new file mode 100644 index 000000000..b5e35b4aa --- /dev/null +++ b/Erupe/server/discordbot/discord_bot.go @@ -0,0 +1,120 @@ +package discordbot + +import ( + "regexp" + + "github.com/Solenataris/Erupe/config" + "github.com/bwmarrin/discordgo" + "go.uber.org/zap" +) + +type DiscordBot struct { + Session *discordgo.Session + config *config.Config + logger *zap.Logger + MainGuild *discordgo.Guild + RealtimeChannel *discordgo.Channel +} + +type DiscordBotOptions struct { + Config *config.Config + Logger *zap.Logger +} + +func NewDiscordBot(options DiscordBotOptions) (discordBot *DiscordBot, err error) { + session, err := discordgo.New("Bot " + options.Config.Discord.BotToken) + + if err != nil { + options.Logger.Fatal("Discord failed", zap.Error(err)) + return nil, err + } + + mainGuild, err := session.Guild(options.Config.Discord.ServerID) + + if err != nil { + options.Logger.Fatal("Discord failed to get main guild", zap.Error(err)) + return nil, err + } + + realtimeChannel, err := session.Channel(options.Config.Discord.RealtimeChannelID) + + if err != nil { + options.Logger.Fatal("Discord failed to create realtimeChannel", zap.Error(err)) + return nil, err + } + + discordBot = &DiscordBot{ + config: options.Config, + logger: options.Logger, + Session: session, + MainGuild: mainGuild, + RealtimeChannel: realtimeChannel, + } + + return +} + +func (bot *DiscordBot) Start() (err error) { + err = bot.Session.Open() + + return +} + +func (bot *DiscordBot) FindRoleByID(id string) *discordgo.Role { + for _, role := range bot.MainGuild.Roles { + if role.ID == id { + return role + } + } + + return nil +} + +// Replace all mentions to real name from the message. +func (bot *DiscordBot) NormalizeDiscordMessage(message string) string { + userRegex := regexp.MustCompile(`<@!?(\d{17,19})>`) + emojiRegex := regexp.MustCompile(`(?:)?`) + roleRegex := regexp.MustCompile(`<@&(\d{17,19})>`) + + result := ReplaceTextAll(message, userRegex, func(userId string) string { + user, err := bot.Session.User(userId) + + if err != nil { + return "@unknown" // @Unknown + } + + return "@" + user.Username + }) + + result = ReplaceTextAll(result, emojiRegex, func(emojiName string) string { + return ":" + emojiName + ":" + }) + + result = ReplaceTextAll(result, roleRegex, func(roleId string) string { + role := bot.FindRoleByID(roleId) + + if role != nil { + return "@!" + role.Name + } + + return "@!unknown" + }) + + return string(result) +} + +func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) { + _, err = bot.Session.ChannelMessageSend(bot.RealtimeChannel.ID, message) + + return +} + +func ReplaceTextAll(text string, regex *regexp.Regexp, handler func(input string) string) string { + result := regex.ReplaceAllFunc([]byte(text), func(s []byte) []byte { + input := regex.ReplaceAllString(string(s), `$1`) + + return []byte(handler(input)) + }) + + return string(result) +}