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
This commit is contained in:
Malckyor
2022-06-15 07:32:45 +09:00
committed by GitHub
parent 642040cb1e
commit 9c6950d200
8 changed files with 1157 additions and 677 deletions

View File

@@ -1,153 +1,156 @@
{ {
"host_ip": "", "host_ip": "",
"bin_path": "bin", "bin_path": "bin",
"devmode": true, "devmode": true,
"devmodeoptions": { "devmodeoptions": {
"serverName" : "", "serverName" : "",
"cleandb": false, "cleandb": false,
"maxlauncherhr": true, "maxlauncherhr": true,
"LogOutboundMessages": false, "LogOutboundMessages": false,
"Event": 0, "Event": 0,
"OpcodeMessages": false, "OpcodeMessages": false,
"SaveDumps": { "SaveDumps": {
"Enabled": true, "Enabled": true,
"OutputDir": "savedata" "OutputDir": "savedata"
} }
}, },
"discord": { "discord": {
"enabled": false, "enabled": false,
"bottoken": "", "bottoken": "",
"channelid": "" "realtimeChannelID": "",
}, "serverId": "",
"database": { "devRoles": [],
"host": "localhost", "devMode": false
"port": 5432, },
"user": "postgres", "database": {
"password": "", "host": "localhost",
"database": "erupe" "port": 5432,
}, "user": "postgres",
"launcher": { "password": "",
"port": 80, "database": "erupe"
"UseOriginalLauncherFiles": false },
}, "launcher": {
"sign": { "port": 80,
"port": 53312 "UseOriginalLauncherFiles": false
}, },
"channel": { "sign": {
"port1": 54001, "port": 53312
"port2": 54002, },
"port3": 54003, "channel": {
"port4": 54004 "port1": 54001,
}, "port2": 54002,
"entrance": { "port3": 54003,
"port": 53310, "port4": 54004
"entries": [ },
{ "entrance": {
"name": " Server #1", "port": 53310,
"ip": "", "entries": [
"unk2": 0, {
"type": 3, "name": " Server #1",
"season": 3, "ip": "",
"unk6": 0, "unk2": 0,
"allowedclientflags": "4096", "type": 3,
"channels": [ "season": 3,
{ "unk6": 0,
"port": 54001, "allowedclientflags": "4096",
"MaxPlayers": 100, "channels": [
"CurrentPlayers": 0, {
"Unk4": 0, "port": 54001,
"Unk5": 0, "MaxPlayers": 100,
"Unk6": 0, "CurrentPlayers": 0,
"Unk7": 0, "Unk4": 0,
"Unk8": 0, "Unk5": 0,
"Unk9": 0, "Unk6": 0,
"Unk10": 319, "Unk7": 0,
"Unk11": 248, "Unk8": 0,
"Unk12": 159, "Unk9": 0,
"Unk13": 12345 "Unk10": 319,
} "Unk11": 248,
] "Unk12": 159,
}, "Unk13": 12345
{ }
"name": " Server #2", ]
"ip": "", },
"unk2": 0, {
"type": 1, "name": " Server #2",
"season": 3, "ip": "",
"unk6": 0, "unk2": 0,
"allowedclientflags": 0, "type": 1,
"channels": [ "season": 3,
{ "unk6": 0,
"port": 54002, "allowedclientflags": 0,
"MaxPlayers": 50, "channels": [
"CurrentPlayers": 0, {
"Unk4": 0, "port": 54002,
"Unk5": 0, "MaxPlayers": 50,
"Unk6": 0, "CurrentPlayers": 0,
"Unk7": 0, "Unk4": 0,
"Unk8": 0, "Unk5": 0,
"Unk9": 0, "Unk6": 0,
"Unk10": 318, "Unk7": 0,
"Unk11": 251, "Unk8": 0,
"Unk12": 155, "Unk9": 0,
"Unk13": 12345 "Unk10": 318,
} "Unk11": 251,
] "Unk12": 155,
}, "Unk13": 12345
{ }
"name": " Server #3", ]
"ip": "", },
"unk2": 0, {
"type": 2, "name": " Server #3",
"season": 1, "ip": "",
"unk6": 0, "unk2": 0,
"allowedclientflags": 0, "type": 2,
"channels": [ "season": 1,
{ "unk6": 0,
"port": 54003, "allowedclientflags": 0,
"MaxPlayers": 50, "channels": [
"CurrentPlayers": 0, {
"Unk4": 0, "port": 54003,
"Unk5": 0, "MaxPlayers": 50,
"Unk6": 0, "CurrentPlayers": 0,
"Unk7": 0, "Unk4": 0,
"Unk8": 0, "Unk5": 0,
"Unk9": 0, "Unk6": 0,
"Unk10": 318, "Unk7": 0,
"Unk11": 251, "Unk8": 0,
"Unk12": 155, "Unk9": 0,
"Unk13": 12345 "Unk10": 318,
} "Unk11": 251,
] "Unk12": 155,
}, "Unk13": 12345
{ }
"name": " Server #4", ]
"ip": "", },
"unk2": 0, {
"type": 4, "name": " Server #4",
"season": 0, "ip": "",
"unk6": 0, "unk2": 0,
"allowedclientflags": 0, "type": 4,
"channels": [ "season": 0,
{ "unk6": 0,
"port": 54004, "allowedclientflags": 0,
"MaxPlayers": 50, "channels": [
"CurrentPlayers": 0, {
"Unk4": 0, "port": 54004,
"Unk5": 0, "MaxPlayers": 50,
"Unk6": 0, "CurrentPlayers": 0,
"Unk7": 0, "Unk4": 0,
"Unk8": 0, "Unk5": 0,
"Unk9": 0, "Unk6": 0,
"Unk10": 318, "Unk7": 0,
"Unk11": 251, "Unk8": 0,
"Unk12": 155, "Unk9": 0,
"Unk13": 12345 "Unk10": 318,
} "Unk11": 251,
] "Unk12": 155,
"Unk13": 12345
} }
] ]
}
}
]
}
} }

View File

@@ -1,157 +1,159 @@
package config package config
import ( import (
"log" "log"
"net" "net"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// Config holds the global server-wide config. // Config holds the global server-wide config.
type Config struct { type Config struct {
HostIP string `mapstructure:"host_ip"` HostIP string `mapstructure:"host_ip"`
BinPath string `mapstructure:"bin_path"` BinPath string `mapstructure:"bin_path"`
DevMode bool DevMode bool
DevModeOptions DevModeOptions DevModeOptions DevModeOptions
Discord Discord Discord Discord
Database Database Database Database
Launcher Launcher Launcher Launcher
Sign Sign Sign Sign
Channel Channel Channel Channel
Entrance Entrance Entrance Entrance
} }
// DevModeOptions holds various debug/temporary options for use while developing Erupe. // DevModeOptions holds various debug/temporary options for use while developing Erupe.
type DevModeOptions struct { type DevModeOptions struct {
ServerName string // To get specific instance server about (Current Players/Event Week) ServerName string // To get specific instance server about (Current Players/Event Week)
CleanDB bool // Automatically wipes the DB on server reset. 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. 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 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 LogOutboundMessages bool // Log all messages sent to the clients
Event int // Changes the current event Event int // Changes the current event
OpcodeMessages bool // Get all message for Opcodes OpcodeMessages bool // Get all message for Opcodes
SaveDumps SaveDumpOptions SaveDumps SaveDumpOptions
} }
type SaveDumpOptions struct { type SaveDumpOptions struct {
Enabled bool Enabled bool
OutputDir string OutputDir string
} }
// Discord holds the discord integration config. // Discord holds the discord integration config.
type Discord struct { type Discord struct {
Enabled bool Enabled bool
BotToken string BotToken string
ChannelID string ServerID string
} RealtimeChannelID string
DevRoles []string
// Database holds the postgres database config. DevMode bool
type Database struct { }
Host string
Port int // Database holds the postgres database config.
User string type Database struct {
Password string Host string
Database string Port int
} User string
Password string
// Launcher holds the launcher server config. Database string
type Launcher struct { }
Port int
UseOriginalLauncherFiles bool // Launcher holds the launcher server config.
} type Launcher struct {
Port int
// Sign holds the sign server config. UseOriginalLauncherFiles bool
type Sign struct { }
Port int
} // Sign holds the sign server config.
type Sign struct {
// Channel holds the channel server config. Port int
type Channel struct { }
Port1 int
Port2 int // Channel holds the channel server config.
Port3 int type Channel struct {
Port4 int Port1 int
} Port2 int
Port3 int
// Entrance holds the entrance server config. Port4 int
type Entrance struct { }
Port uint16
Entries []EntranceServerInfo // Entrance holds the entrance server config.
} type Entrance struct {
Port uint16
// EntranceServerInfo represents an entry in the serverlist. Entries []EntranceServerInfo
type EntranceServerInfo struct { }
IP string
Unk2 uint16 // EntranceServerInfo represents an entry in the serverlist.
Type uint8 // Server type. 0=?, 1=open, 2=cities, 3=newbie, 4=bar type EntranceServerInfo struct {
Season uint8 // Server activity. 0 = green, 1 = orange, 2 = blue IP string
Unk6 uint8 // Something to do with server recommendation on 0, 3, and 5. Unk2 uint16
Name string // Server name, 66 byte null terminated Shift-JIS(JP) or Big5(TW). Type uint8 // Server type. 0=?, 1=open, 2=cities, 3=newbie, 4=bar
Season uint8 // Server activity. 0 = green, 1 = orange, 2 = blue
// 4096(PC, PS3/PS4)?, 8258(PC, PS3/PS4)?, 8192 == nothing? Unk6 uint8 // Something to do with server recommendation on 0, 3, and 5.
// THIS ONLY EXISTS IF Binary8Header.type == "SV2", NOT "SVR"! Name string // Server name, 66 byte null terminated Shift-JIS(JP) or Big5(TW).
AllowedClientFlags uint32
// 4096(PC, PS3/PS4)?, 8258(PC, PS3/PS4)?, 8192 == nothing?
Channels []EntranceChannelInfo // THIS ONLY EXISTS IF Binary8Header.type == "SV2", NOT "SVR"!
} AllowedClientFlags uint32
// EntranceChannelInfo represents an entry in a server's channel list. Channels []EntranceChannelInfo
type EntranceChannelInfo struct { }
Port uint16
MaxPlayers uint16 // EntranceChannelInfo represents an entry in a server's channel list.
CurrentPlayers uint16 type EntranceChannelInfo struct {
Unk4 uint16 Port uint16
Unk5 uint16 MaxPlayers uint16
Unk6 uint16 CurrentPlayers uint16
Unk7 uint16 Unk4 uint16
Unk8 uint16 Unk5 uint16
Unk9 uint16 Unk6 uint16
Unk10 uint16 Unk7 uint16
Unk11 uint16 Unk8 uint16
Unk12 uint16 Unk9 uint16
Unk13 uint16 Unk10 uint16
} Unk11 uint16
Unk12 uint16
// getOutboundIP4 gets the preferred outbound ip4 of this machine Unk13 uint16
// From https://stackoverflow.com/a/37382208 }
func getOutboundIP4() net.IP {
conn, err := net.Dial("udp4", "8.8.8.8:80") // getOutboundIP4 gets the preferred outbound ip4 of this machine
if err != nil { // From https://stackoverflow.com/a/37382208
log.Fatal(err) func getOutboundIP4() net.IP {
} conn, err := net.Dial("udp4", "8.8.8.8:80")
defer conn.Close() if err != nil {
log.Fatal(err)
localAddr := conn.LocalAddr().(*net.UDPAddr) }
defer conn.Close()
return localAddr.IP.To4()
} localAddr := conn.LocalAddr().(*net.UDPAddr)
// LoadConfig loads the given config toml file. return localAddr.IP.To4()
func LoadConfig() (*Config, error) { }
viper.SetConfigName("config")
viper.AddConfigPath(".") // LoadConfig loads the given config toml file.
func LoadConfig() (*Config, error) {
viper.SetDefault("DevModeOptions.SaveDumps", SaveDumpOptions{ viper.SetConfigName("config")
Enabled: false, viper.AddConfigPath(".")
OutputDir: "savedata",
}) viper.SetDefault("DevModeOptions.SaveDumps", SaveDumpOptions{
Enabled: false,
err := viper.ReadInConfig() OutputDir: "savedata",
if err != nil { })
return nil, err
} err := viper.ReadInConfig()
if err != nil {
c := &Config{} return nil, err
err = viper.Unmarshal(c) }
if err != nil {
return nil, err c := &Config{}
} err = viper.Unmarshal(c)
if err != nil {
if c.HostIP == "" { return nil, err
c.HostIP = getOutboundIP4().To4().String() }
}
if c.HostIP == "" {
return c, nil c.HostIP = getOutboundIP4().To4().String()
} }
return c, nil
}

View File

@@ -1,177 +1,215 @@
package main package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time" "time"
"github.com/Solenataris/Erupe/config" "github.com/Solenataris/Erupe/config"
"github.com/Solenataris/Erupe/server/channelserver" "github.com/Solenataris/Erupe/server/channelserver"
"github.com/Solenataris/Erupe/server/entranceserver" "github.com/Solenataris/Erupe/server/discordbot"
"github.com/Solenataris/Erupe/server/launcherserver" "github.com/Solenataris/Erupe/server/entranceserver"
"github.com/Solenataris/Erupe/server/signserver" "github.com/Solenataris/Erupe/server/launcherserver"
"github.com/jmoiron/sqlx" "github.com/Solenataris/Erupe/server/signserver"
_ "github.com/lib/pq" "github.com/jmoiron/sqlx"
"go.uber.org/zap" _ "github.com/lib/pq"
) "go.uber.org/zap"
)
// Temporary DB auto clean on startup for quick development & testing.
func cleanDB(db *sqlx.DB) { // Temporary DB auto clean on startup for quick development & testing.
_ = db.MustExec("DELETE FROM guild_characters") func cleanDB(db *sqlx.DB) {
_ = db.MustExec("DELETE FROM guilds") _ = db.MustExec("DELETE FROM guild_characters")
_ = db.MustExec("DELETE FROM characters") _ = db.MustExec("DELETE FROM guilds")
_ = db.MustExec("DELETE FROM sign_sessions") _ = db.MustExec("DELETE FROM characters")
_ = db.MustExec("DELETE FROM users") _ = db.MustExec("DELETE FROM sign_sessions")
} _ = db.MustExec("DELETE FROM users")
}
func main() {
zapLogger, _ := zap.NewDevelopment() func main() {
defer zapLogger.Sync() zapLogger, _ := zap.NewDevelopment()
logger := zapLogger.Named("main") defer zapLogger.Sync()
logger := zapLogger.Named("main")
logger.Info("Starting Erupe")
logger.Info("Starting Erupe")
// Load the configuration.
erupeConfig, err := config.LoadConfig() // Load the configuration.
if err != nil { erupeConfig, err := config.LoadConfig()
logger.Fatal("Failed to load config", zap.Error(err)) if err != nil {
} logger.Fatal("Failed to load config", zap.Error(err))
}
// Create the postgres DB pool.
connectString := fmt.Sprintf( // Discord bot
"host=%s port=%d user=%s password=%s dbname= %s sslmode=disable", var discordBot *discordbot.DiscordBot = nil
erupeConfig.Database.Host,
erupeConfig.Database.Port, if erupeConfig.Discord.Enabled {
erupeConfig.Database.User, bot, err := discordbot.NewDiscordBot(discordbot.DiscordBotOptions{
erupeConfig.Database.Password, Logger: logger,
erupeConfig.Database.Database, Config: erupeConfig,
) })
db, err := sqlx.Open("postgres", connectString) if err != nil {
if err != nil { logger.Fatal("Failed to create discord bot", zap.Error(err))
logger.Fatal("Failed to open sql database", zap.Error(err)) }
}
// Discord bot
// Test the DB connection. err = bot.Start()
err = db.Ping()
if err != nil { if err != nil {
logger.Fatal("Failed to ping database", zap.Error(err)) logger.Fatal("Failed to starts discord bot", zap.Error(err))
} }
logger.Info("Connected to database")
discordBot = bot
// Clean the DB if the option is on. } else {
if erupeConfig.DevMode && erupeConfig.DevModeOptions.CleanDB { logger.Info("Discord bot is disabled")
logger.Info("Cleaning DB") }
cleanDB(db)
logger.Info("Done cleaning DB") // Create the postgres DB pool.
} connectString := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname= %s sslmode=disable",
// Now start our server(s). erupeConfig.Database.Host,
erupeConfig.Database.Port,
// Launcher HTTP server. erupeConfig.Database.User,
launcherServer := launcherserver.NewServer( erupeConfig.Database.Password,
&launcherserver.Config{ erupeConfig.Database.Database,
Logger: logger.Named("launcher"), )
ErupeConfig: erupeConfig,
DB: db, db, err := sqlx.Open("postgres", connectString)
UseOriginalLauncherFiles: erupeConfig.Launcher.UseOriginalLauncherFiles, if err != nil {
}) logger.Fatal("Failed to open sql database", zap.Error(err))
err = launcherServer.Start() }
if err != nil {
logger.Fatal("Failed to start launcher server", zap.Error(err)) // Test the DB connection.
} err = db.Ping()
logger.Info("Started launcher server.") if err != nil {
logger.Fatal("Failed to ping database", zap.Error(err))
// Entrance server. }
entranceServer := entranceserver.NewServer( logger.Info("Connected to database")
&entranceserver.Config{
Logger: logger.Named("entrance"), // Clean the DB if the option is on.
ErupeConfig: erupeConfig, if erupeConfig.DevMode && erupeConfig.DevModeOptions.CleanDB {
DB: db, logger.Info("Cleaning DB")
}) cleanDB(db)
err = entranceServer.Start() logger.Info("Done cleaning DB")
if err != nil { }
logger.Fatal("Failed to start entrance server", zap.Error(err))
} // Now start our server(s).
logger.Info("Started entrance server.")
// Launcher HTTP server.
// Sign server. launcherServer := launcherserver.NewServer(
signServer := signserver.NewServer( &launcherserver.Config{
&signserver.Config{ Logger: logger.Named("launcher"),
Logger: logger.Named("sign"), ErupeConfig: erupeConfig,
ErupeConfig: erupeConfig, DB: db,
DB: db, UseOriginalLauncherFiles: erupeConfig.Launcher.UseOriginalLauncherFiles,
}) })
err = signServer.Start() err = launcherServer.Start()
if err != nil { if err != nil {
logger.Fatal("Failed to start sign server", zap.Error(err)) logger.Fatal("Failed to start launcher server", zap.Error(err))
} }
logger.Info("Started sign server.") logger.Info("Started launcher server.")
// Channel Server // Entrance server.
channelServer1 := channelserver.NewServer( entranceServer := entranceserver.NewServer(
&channelserver.Config{ &entranceserver.Config{
Logger: logger.Named("channel"), Logger: logger.Named("entrance"),
ErupeConfig: erupeConfig, ErupeConfig: erupeConfig,
DB: db, DB: db,
}) })
err = entranceServer.Start()
err = channelServer1.Start(erupeConfig.Channel.Port1) if err != nil {
if err != nil { logger.Fatal("Failed to start entrance server", zap.Error(err))
logger.Fatal("Failed to start channel server1", zap.Error(err)) }
} logger.Info("Started entrance server.")
logger.Info("Started channel server.")
// Channel Server // Sign server.
channelServer2 := channelserver.NewServer( signServer := signserver.NewServer(
&channelserver.Config{ &signserver.Config{
Logger: logger.Named("channel"), Logger: logger.Named("sign"),
ErupeConfig: erupeConfig, ErupeConfig: erupeConfig,
DB: db, DB: db,
}) })
err = signServer.Start()
err = channelServer2.Start(erupeConfig.Channel.Port2) if err != nil {
if err != nil { logger.Fatal("Failed to start sign server", zap.Error(err))
logger.Fatal("Failed to start channel server2", zap.Error(err)) }
} logger.Info("Started sign server.")
// Channel Server
channelServer3 := channelserver.NewServer( // Channel Server
&channelserver.Config{ channelServer1 := channelserver.NewServer(
Logger: logger.Named("channel"), &channelserver.Config{
ErupeConfig: erupeConfig, Logger: logger.Named("channel"),
DB: db, ErupeConfig: erupeConfig,
}) DB: db,
Name: erupeConfig.Entrance.Entries[0].Name,
err = channelServer3.Start(erupeConfig.Channel.Port3) Enable: erupeConfig.Entrance.Entries[0].Channels[0].MaxPlayers > 0,
if err != nil { //DiscordBot: discordBot,
logger.Fatal("Failed to start channel server3", zap.Error(err)) })
}
// Channel Server err = channelServer1.Start(erupeConfig.Channel.Port1)
channelServer4 := channelserver.NewServer( if err != nil {
&channelserver.Config{ logger.Fatal("Failed to start channel server1", zap.Error(err))
Logger: logger.Named("channel"), }
ErupeConfig: erupeConfig, logger.Info("Started channel server.")
DB: db, // Channel Server
}) channelServer2 := channelserver.NewServer(
&channelserver.Config{
err = channelServer4.Start(erupeConfig.Channel.Port4) Logger: logger.Named("channel"),
if err != nil { ErupeConfig: erupeConfig,
logger.Fatal("Failed to start channel server4", zap.Error(err)) DB: db,
} Name: erupeConfig.Entrance.Entries[1].Name,
// Wait for exit or interrupt with ctrl+C. Enable: erupeConfig.Entrance.Entries[1].Channels[0].MaxPlayers > 0,
c := make(chan os.Signal, 1) DiscordBot: discordBot,
signal.Notify(c, os.Interrupt, syscall.SIGTERM) })
<-c
err = channelServer2.Start(erupeConfig.Channel.Port2)
logger.Info("Trying to shutdown gracefully.") if err != nil {
channelServer4.Shutdown() logger.Fatal("Failed to start channel server2", zap.Error(err))
channelServer3.Shutdown() }
channelServer2.Shutdown() // Channel Server
channelServer1.Shutdown() channelServer3 := channelserver.NewServer(
signServer.Shutdown() &channelserver.Config{
entranceServer.Shutdown() Logger: logger.Named("channel"),
launcherServer.Shutdown() ErupeConfig: erupeConfig,
DB: db,
time.Sleep(1 * time.Second) 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)
}

View File

@@ -129,9 +129,8 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
fmt.Printf("Got chat message: %+v\n", chatMessage) fmt.Printf("Got chat message: %+v\n", chatMessage)
// Discord integration // Discord integration
if s.server.erupeConfig.Discord.Enabled { if chatMessage.Type == binpacket.ChatTypeLocal || chatMessage.Type == binpacket.ChatTypeParty {
message := fmt.Sprintf("%s: %s", chatMessage.SenderName, chatMessage.Message) s.server.DiscordChannelSend(chatMessage.SenderName, chatMessage.Message)
s.server.discordSession.ChannelMessageSend(s.server.erupeConfig.Discord.ChannelID, message)
} }
// RAVI COMMANDS // RAVI COMMANDS

View File

@@ -2,162 +2,344 @@ package channelserver
import ( import (
"fmt" "fmt"
"os" "sort"
"strings" "strings"
"time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
// onDiscordMessage handles receiving messages from discord and forwarding them ingame. type Character struct {
func (s *Server) onDiscordMessage(ds *discordgo.Session, m *discordgo.MessageCreate) { ID uint32 `db:"id"`
// Ignore messages from our bot, or ones that are not in the correct channel. IsFemale bool `db:"is_female"`
if m.Author.ID == ds.State.User.ID || m.ChannelID != s.erupeConfig.Discord.ChannelID { IsNewCharacter bool `db:"is_new_character"`
return Name string `db:"name"`
} UnkDescString string `db:"unk_desc_string"`
HRP uint16 `db:"hrp"`
// Broadcast to the game clients. GR uint16 `db:"gr"`
data := m.Content WeaponType uint16 `db:"weapon_type"`
LastLogin uint32 `db:"last_login"`
// 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)
} }
func dayConvert(result string) string { var weapons = []string{
var replaceDays 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" { func (s *Server) getCharacterForUser(uid int) (*Character, error) {
replaceDays = "Lundi" character := Character{}
} else if result == "2" { 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)
replaceDays = "Mardi" if err != nil {
} else if result == "3" { return nil, err
replaceDays = "Mercredi" }
} else if result == "4" {
replaceDays = "Jeudi" return &character, nil
} else if result == "5" { }
replaceDays = "Vendredi"
} else if result == "6" { func CountChars(s *Server) string {
replaceDays = "Samedi" count := 0
} else if result == "7" { for _, stage := range s.stages {
replaceDays = "Dimanche" 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 { } 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 { func getPlayerList(s *Server) ([]ListPlayer, int) {
var replaceMonth string list := []ListPlayer{}
questEmojis := []string{
if result == "01" { ":white_circle:",
replaceMonth = "Janvier" ":red_circle:",
} else if result == "02" { ":blue_circle:",
replaceMonth = "Fevrier" ":brown_circle:",
} else if result == "03" { ":green_circle:",
replaceMonth = "Mars" ":purple_circle:",
} else if result == "04" { ":yellow_circle:",
replaceMonth = "Avril" ":orange_circle:",
} 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"
} }
return replaceMonth bigNameLen := 0
}
func (s *Server) TimerUpdate(timer int, typeStop int, disableAutoOff bool) { for _, stage := range s.stages {
timertotal := 0 if len(stage.clients) == 0 {
for timer > 0 { continue
time.Sleep(1 * time.Minute) }
timer -= 1
timertotal += 1 questEmoji := ":black_circle:"
if disableAutoOff {
// Un message s'affiche toutes les 10 minutes pour prévenir de la maintenance. if len(questEmojis) > 0 {
if timertotal == 10 { questEmoji = questEmojis[len(questEmojis)-1]
timertotal = 0 questEmojis = questEmojis[:len(questEmojis)-1]
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)) isQuest := stage.isQuest()
s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas") for client := range stage.clients {
s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous excuser") char, err := s.getCharacterForUser(int(client.charID))
s.BroadcastChatMessage("pour la gene occasionnee. Merci de votre cooperation.") if err == nil {
} else { if len(char.Name) > bigNameLen {
s.BroadcastChatMessage("RAPPEL DE MAINTENANCE EXCEPTIONNELLE: Les serveurs seront") bigNameLen = len(char.Name)
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.")
} }
}
// Déconnecter tous les joueurs du serveur. list = append(list, ListPlayer{
if timer == 0 { CharName: char.Name,
os.Exit(-1) 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 <char name>")
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 <stage id>")
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))
}
} }

View File

@@ -9,16 +9,36 @@ import (
"github.com/Solenataris/Erupe/config" "github.com/Solenataris/Erupe/config"
"github.com/Solenataris/Erupe/network/binpacket" "github.com/Solenataris/Erupe/network/binpacket"
"github.com/Solenataris/Erupe/network/mhfpacket" "github.com/Solenataris/Erupe/network/mhfpacket"
"github.com/bwmarrin/discordgo" "github.com/Solenataris/Erupe/server/discordbot"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"go.uber.org/zap" "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. // Config struct allows configuring the server.
type Config struct { type Config struct {
Logger *zap.Logger Logger *zap.Logger
DB *sqlx.DB DB *sqlx.DB
DiscordBot *discordbot.DiscordBot
ErupeConfig *config.Config ErupeConfig *config.Config
Name string
Enable bool
} }
// Map key type for a user binary part. // Map key type for a user binary part.
@@ -30,14 +50,13 @@ type userBinaryPartID struct {
// Server is a MHF channel server. // Server is a MHF channel server.
type Server struct { type Server struct {
sync.Mutex sync.Mutex
logger *zap.Logger logger *zap.Logger
db *sqlx.DB db *sqlx.DB
erupeConfig *config.Config erupeConfig *config.Config
acceptConns chan net.Conn acceptConns chan net.Conn
deleteConns chan net.Conn deleteConns chan net.Conn
sessions map[net.Conn]*Session sessions map[net.Conn]*Session
listener net.Listener // Listener that is created when Server.Start is called. listener net.Listener // Listener that is created when Server.Start is called.
isShuttingDown bool isShuttingDown bool
stagesLock sync.RWMutex stagesLock sync.RWMutex
@@ -52,12 +71,72 @@ type Server struct {
semaphore map[string]*Semaphore semaphore map[string]*Semaphore
// Discord chat integration // 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. // NewServer creates a new Server type.
func NewServer(config *Config) *Server { func NewServer(config *Config) *Server {
s := &Server{ s := &Server {
logger: config.Logger, logger: config.Logger,
db: config.DB, db: config.DB,
erupeConfig: config.ErupeConfig, erupeConfig: config.ErupeConfig,
@@ -67,7 +146,10 @@ func NewServer(config *Config) *Server {
stages: make(map[string]*Stage), stages: make(map[string]*Stage),
userBinaryParts: make(map[userBinaryPartID][]byte), userBinaryParts: make(map[userBinaryPartID][]byte),
semaphore: make(map[string]*Semaphore), semaphore: make(map[string]*Semaphore),
discordSession: nil, discordBot: config.DiscordBot,
name: config.Name,
enable: config.Enable,
raviente: NewRaviente(),
} }
// Mezeporta // Mezeporta
@@ -88,7 +170,7 @@ func NewServer(config *Config) *Server {
// Rasta bar stage // Rasta bar stage
s.stages["sl1Ns211p0a0u0"] = NewStage("sl1Ns211p0a0u0") s.stages["sl1Ns211p0a0u0"] = NewStage("sl1Ns211p0a0u0")
// Carvane // Pallone Carvan
s.stages["sl1Ns260p0a0u0"] = NewStage("sl1Ns260p0a0u0") s.stages["sl1Ns260p0a0u0"] = NewStage("sl1Ns260p0a0u0")
// Gook Farm // Gook Farm
@@ -103,16 +185,6 @@ func NewServer(config *Config) *Server {
// MezFes // MezFes
s.stages["sl1Ns462p0a0u0"] = NewStage("sl1Ns462p0a0u0") 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 return s
} }
@@ -128,12 +200,8 @@ func (s *Server) Start(port int) error {
go s.manageSessions() go s.manageSessions()
// Start the discord bot for chat integration. // Start the discord bot for chat integration.
if s.erupeConfig.Discord.Enabled { if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
err = s.discordSession.Open() s.discordBot.Session.AddHandler(s.onDiscordMessage)
if err != nil {
s.logger.Warn("Error opening Discord session.", zap.Error(err))
return err
}
} }
return nil return nil
@@ -146,11 +214,8 @@ func (s *Server) Shutdown() {
s.Unlock() s.Unlock()
s.listener.Close() s.listener.Close()
close(s.acceptConns)
if s.erupeConfig.Discord.Enabled { close(s.acceptConns)
s.discordSession.Close()
}
} }
func (s *Server) acceptClients() { func (s *Server) acceptClients() {
@@ -232,7 +297,7 @@ func (s *Server) BroadcastChatMessage(message string) {
Type: 5, Type: 5,
Flags: 0x80, Flags: 0x80,
Message: message, Message: message,
SenderName: "Erupe", SenderName: s.name,
} }
msgBinChat.Build(bf) msgBinChat.Build(bf)
@@ -243,6 +308,13 @@ func (s *Server) BroadcastChatMessage(message string) {
}, nil) }, 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 { func (s *Server) FindSessionByCharID(charID uint32) *Session {
s.stagesLock.RLock() s.stagesLock.RLock()
defer s.stagesLock.RUnlock() defer s.stagesLock.RUnlock()
@@ -259,3 +331,21 @@ func (s *Server) FindSessionByCharID(charID uint32) *Session {
return nil 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
}

View File

@@ -1,9 +1,12 @@
package channelserver package channelserver
import ( import (
"sync"
"time"
"github.com/Andoryuuta/byteframe" "github.com/Andoryuuta/byteframe"
"github.com/Solenataris/Erupe/network/mhfpacket" "github.com/Solenataris/Erupe/network/mhfpacket"
"sync"
) )
// StageObject holds infomation about a specific stage object. // StageObject holds infomation about a specific stage object.
@@ -15,7 +18,7 @@ type StageObject struct {
} }
type ObjectMap struct { type ObjectMap struct {
id uint8 id uint8
charid uint32 charid uint32
status bool status bool
} }
@@ -55,6 +58,7 @@ type Stage struct {
maxPlayers uint16 maxPlayers uint16
hasDeparted bool hasDeparted bool
password string password string
createdAt string
} }
// NewStage creates a new stage with intialized values. // NewStage creates a new stage with intialized values.
@@ -68,6 +72,7 @@ func NewStage(ID string) *Stage {
maxPlayers: 4, maxPlayers: 4,
gameObjectCount: 1, gameObjectCount: 1,
objectList: make(map[uint8]*ObjectMap), objectList: make(map[uint8]*ObjectMap),
createdAt: time.Now().Format("01-02-2006 15:04:05"),
} }
s.InitObjectList() s.InitObjectList()
return s return s
@@ -94,30 +99,71 @@ func (s *Stage) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session) {
} }
func (s *Stage) InitObjectList() { func (s *Stage) InitObjectList() {
for seq:=uint8(0x7f);seq>uint8(0);seq-- { for seq := uint8(0x7f); seq > uint8(0); seq-- {
newObj := &ObjectMap{ newObj := &ObjectMap{
id: seq, id: seq,
charid: uint32(0), charid: uint32(0),
status: false, status: false,
}
s.objectList[seq] = newObj
} }
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 { func (s *Stage) GetNewObjectID(CharID uint32) uint32 {
ObjId:=uint8(0) ObjId := uint8(0)
for seq:=uint8(0x7f);seq>uint8(0);seq--{ for seq := uint8(0x7f); seq > uint8(0); seq-- {
if s.objectList[seq].status == false { if s.objectList[seq].status == false {
ObjId=seq ObjId = seq
break break
} }
} }
s.objectList[ObjId].status=true s.objectList[ObjId].status = true
s.objectList[ObjId].charid=CharID s.objectList[ObjId].charid = CharID
bf := byteframe.NewByteFrame() bf := byteframe.NewByteFrame()
bf.WriteUint8(uint8(0)) bf.WriteUint8(uint8(0))
bf.WriteUint8(ObjId) bf.WriteUint8(ObjId)
bf.WriteUint16(uint16(0)) 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 return obj
} }

View File

@@ -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(`(?:<a?)?:(\w+):(?:\d{18}>)?`)
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)
}