From 9c6950d2005dcd417627f2b9e666e35eb1127d40 Mon Sep 17 00:00:00 2001 From: Malckyor Date: Wed, 15 Jun 2022 07:32:45 +0900 Subject: [PATCH 1/4] 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) +} From 2d7f55600f2a963900781a6dc588334979a7e943 Mon Sep 17 00:00:00 2001 From: Malckyor Date: Wed, 15 Jun 2022 07:58:28 +0900 Subject: [PATCH 2/4] Road Shop Rotation Added Road Shop inventory's weekly dynamic rotation. Thank you everyone who helped with this! --- Erupe/road-shop-rotation.sql | 39 +++++++++++++++ .../channelserver/handlers_shop_gacha.go | 50 +++++++++++++++---- 2 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 Erupe/road-shop-rotation.sql diff --git a/Erupe/road-shop-rotation.sql b/Erupe/road-shop-rotation.sql new file mode 100644 index 000000000..034d6bfce --- /dev/null +++ b/Erupe/road-shop-rotation.sql @@ -0,0 +1,39 @@ +BEGIN; +CREATE TABLE IF NOT EXISTS public.normal_shop_items +( + shoptype integer, + shopid integer, + itemhash integer not null, + itemid integer, + points integer, + tradequantity integer, + rankreqlow integer, + rankreqhigh integer, + rankreqg integer, + storelevelreq integer, + maximumquantity integer, + boughtquantity integer, + roadfloorsrequired integer, + weeklyfataliskills integer, + enable_weeks character varying(8) +); + +ALTER TABLE IF EXISTS public.normal_shop_items +( + ADD COLUMN enable_weeks character varying(8) +); + +CREATE TABLE IF NOT EXISTS public.shop_item_state +( + char_id bigint REFERENCES characters (id), + itemhash int UNIQUE NOT NULL, + usedquantity int, + week int +); + +ALTER TABLE IF EXISTS public.shop_item_state +( + ADD COLUMN week int +); + +END; \ No newline at end of file diff --git a/Erupe/server/channelserver/handlers_shop_gacha.go b/Erupe/server/channelserver/handlers_shop_gacha.go index be0ec9e04..ac786cccf 100644 --- a/Erupe/server/channelserver/handlers_shop_gacha.go +++ b/Erupe/server/channelserver/handlers_shop_gacha.go @@ -2,16 +2,28 @@ package channelserver import ( "encoding/hex" + "fmt" + "strings" "time" //"github.com/Solenataris/Erupe/common/stringsupport" - "github.com/Solenataris/Erupe/network/mhfpacket" "github.com/Andoryuuta/byteframe" + "github.com/Solenataris/Erupe/network/mhfpacket" "github.com/lib/pq" "github.com/sachaos/lottery" "go.uber.org/zap" ) +func contains(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + + return false +} + func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateShop) // SHOP TYPES: @@ -149,20 +161,28 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) { } else { doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } - } else { - shopEntries, err := s.server.db.Query("SELECT itemhash,itemID,Points,TradeQuantity,rankReqLow,rankReqHigh,rankReqG,storeLevelReq,maximumQuantity,boughtQuantity,roadFloorsRequired,weeklyFatalisKills FROM normal_shop_items WHERE shoptype=$1 AND shopid=$2", pkt.ShopType, pkt.ShopID) + } else { + _, week := time.Now().ISOWeek() + season := fmt.Sprintf("%d", week%4) + shopEntries, err := s.server.db.Query("SELECT itemhash,itemID,Points,TradeQuantity,rankReqLow,rankReqHigh,rankReqG,storeLevelReq,maximumQuantity,boughtQuantity,roadFloorsRequired,weeklyFatalisKills, COALESCE(enable_weeks, '') FROM normal_shop_items WHERE shoptype=$1 AND shopid=$2", pkt.ShopType, pkt.ShopID) if err != nil { panic(err) } var ItemHash, entryCount int var itemID, Points, TradeQuantity, rankReqLow, rankReqHigh, rankReqG, storeLevelReq, maximumQuantity, boughtQuantity, roadFloorsRequired, weeklyFatalisKills, charQuantity uint16 + var itemWeeks string resp := byteframe.NewByteFrame() resp.WriteUint32(0) // total defs for shopEntries.Next() { - err = shopEntries.Scan(&ItemHash, &itemID, &Points, &TradeQuantity, &rankReqLow, &rankReqHigh, &rankReqG, &storeLevelReq, &maximumQuantity, &boughtQuantity, &roadFloorsRequired, &weeklyFatalisKills) + err = shopEntries.Scan(&ItemHash, &itemID, &Points, &TradeQuantity, &rankReqLow, &rankReqHigh, &rankReqG, &storeLevelReq, &maximumQuantity, &boughtQuantity, &roadFloorsRequired, &weeklyFatalisKills, &itemWeeks) if err != nil { panic(err) } + + if len(itemWeeks) > 0 && !contains(strings.Split(itemWeeks, ","), season) { + continue + } + resp.WriteUint32(uint32(ItemHash)) resp.WriteUint16(0) // unk, always 0 in existing packets resp.WriteUint16(itemID) @@ -175,9 +195,13 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) { resp.WriteUint16(storeLevelReq) resp.WriteUint16(maximumQuantity) if maximumQuantity > 0 { - err = s.server.db.QueryRow("SELECT COALESCE(usedquantity,0) FROM shop_item_state WHERE itemhash=$1 AND char_id=$2", ItemHash, s.charID).Scan(&charQuantity) + var itemWeek int + err = s.server.db.QueryRow("SELECT COALESCE(usedquantity,0), COALESCE(week,-1) FROM shop_item_state WHERE itemhash=$1 AND char_id=$2", ItemHash, s.charID).Scan(&charQuantity, &itemWeek) if err != nil { resp.WriteUint16(0) + } else if pkt.ShopID == 7 && itemWeek >= 0 && itemWeek != week { + clearShopItemState(s, s.charID, uint32(ItemHash)) + resp.WriteUint16(0) } else { resp.WriteUint16(charQuantity) } @@ -200,6 +224,7 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) { } func handleMsgMhfAcquireExchangeShop(s *Session, p mhfpacket.MHFPacket) { + _, week := time.Now().ISOWeek() // writing out to an editable shop enumeration pkt := p.(*mhfpacket.MsgMhfAcquireExchangeShop) if pkt.DataSize == 10 { @@ -207,10 +232,10 @@ func handleMsgMhfAcquireExchangeShop(s *Session, p mhfpacket.MHFPacket) { _ = bf.ReadUint16() // unk, always 1 in examples itemHash := bf.ReadUint32() buyCount := bf.ReadUint32() - _, err := s.server.db.Exec(`INSERT INTO shop_item_state (char_id, itemhash, usedquantity) - VALUES ($1,$2,$3) ON CONFLICT (char_id, itemhash) + _, err := s.server.db.Exec(`INSERT INTO shop_item_state (char_id, itemhash, usedquantity, week) + VALUES ($1,$2,$3,$4) ON CONFLICT (char_id, itemhash) DO UPDATE SET usedquantity = shop_item_state.usedquantity + $3 - WHERE EXCLUDED.char_id=$1 AND EXCLUDED.itemhash=$2`, s.charID, itemHash, buyCount) + WHERE EXCLUDED.char_id=$1 AND EXCLUDED.itemhash=$2`, s.charID, itemHash, buyCount, week) if err != nil { s.logger.Fatal("Failed to update shop_item_state in db", zap.Error(err)) } @@ -218,6 +243,13 @@ func handleMsgMhfAcquireExchangeShop(s *Session, p mhfpacket.MHFPacket) { doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } +func clearShopItemState(s *Session, charId uint32, itemHash uint32) { + _, err := s.server.db.Exec(`DELETE FROM shop_item_state WHERE char_id=$1 AND itemhash=$2`, charId, itemHash) + if err != nil { + s.logger.Fatal("Failed to delete shop_item_state in db", zap.Error(err)) + } +} + func handleMsgMhfGetGachaPlayHistory(s *Session, p mhfpacket.MHFPacket) { // returns number of times the gacha was played, will need persistent db stuff pkt := p.(*mhfpacket.MsgMhfGetGachaPlayHistory) @@ -703,4 +735,4 @@ func handleMsgMhfResetBoxGachaInfo(s *Session, p mhfpacket.MHFPacket) { panic(err) } doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) -} +} \ No newline at end of file From f05391ecba12aa702879a40648c80277be1a69ab Mon Sep 17 00:00:00 2001 From: Malckyor Date: Wed, 15 Jun 2022 08:11:03 +0900 Subject: [PATCH 3/4] Item Distribution Doing the PR for wish, it's his code. Also this is the one used by Chakratos' Save Manager. P.S.: Sadly it is still crashing for some reason. --- Erupe/distitem.sql | 26 +++ .../mhfpacket/msg_mhf_apply_dist_item.go | 14 +- .../mhfpacket/msg_mhf_get_dist_description.go | 10 +- .../server/channelserver/handlers_distitem.go | 168 +++++++++++++----- 4 files changed, 157 insertions(+), 61 deletions(-) create mode 100644 Erupe/distitem.sql diff --git a/Erupe/distitem.sql b/Erupe/distitem.sql new file mode 100644 index 000000000..4325b51e4 --- /dev/null +++ b/Erupe/distitem.sql @@ -0,0 +1,26 @@ +BEGIN; +CREATE TABLE public.distribution +( + id serial NOT NULL PRIMARY KEY, + character_id int, + type int NOT NULL, + deadline timestamp without time zone, + event_name text NOT NULL DEFAULT 'GM Gift!', + description text NOT NULL DEFAULT '~C05You received a gift!', + times_acceptable int NOT NULL DEFAULT 1, + min_hr int NOT NULL DEFAULT 65535, + max_hr int NOT NULL DEFAULT 65535, + min_sr int NOT NULL DEFAULT 65535, + max_sr int NOT NULL DEFAULT 65535, + min_gr int NOT NULL DEFAULT 65535, + max_gr int NOT NULL DEFAULT 65535, + data bytea NOT NULL +); + +CREATE TABLE public.distributions_accepted +( + distribution_id int, + character_id int +); + +END; \ No newline at end of file diff --git a/Erupe/network/mhfpacket/msg_mhf_apply_dist_item.go b/Erupe/network/mhfpacket/msg_mhf_apply_dist_item.go index 8e5f43558..b3603bfeb 100644 --- a/Erupe/network/mhfpacket/msg_mhf_apply_dist_item.go +++ b/Erupe/network/mhfpacket/msg_mhf_apply_dist_item.go @@ -9,8 +9,8 @@ import ( // MsgMhfApplyDistItem represents the MSG_MHF_APPLY_DIST_ITEM type MsgMhfApplyDistItem struct { AckHandle uint32 - Unk0 uint8 - RequestType uint32 + DistributionType uint8 + DistributionID uint32 Unk2 uint32 Unk3 uint32 } @@ -23,8 +23,8 @@ func (m *MsgMhfApplyDistItem) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfApplyDistItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { m.AckHandle = bf.ReadUint32() - m.Unk0 = bf.ReadUint8() - m.RequestType = bf.ReadUint32() + m.DistributionType = bf.ReadUint8() + m.DistributionID = bf.ReadUint32() m.Unk2 = bf.ReadUint32() m.Unk3 = bf.ReadUint32() return nil @@ -33,9 +33,9 @@ func (m *MsgMhfApplyDistItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.Clie // Build builds a binary packet from the current data. func (m *MsgMhfApplyDistItem) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { bf.WriteUint32(m.AckHandle) - bf.WriteUint8(m.Unk0) - bf.WriteUint32(m.RequestType) + bf.WriteUint8(m.DistributionType) + bf.WriteUint32(m.DistributionID) bf.WriteUint32(m.Unk2) bf.WriteUint32(m.Unk3) return nil -} +} \ No newline at end of file diff --git a/Erupe/network/mhfpacket/msg_mhf_get_dist_description.go b/Erupe/network/mhfpacket/msg_mhf_get_dist_description.go index fd8f664a7..d3edf6cc0 100644 --- a/Erupe/network/mhfpacket/msg_mhf_get_dist_description.go +++ b/Erupe/network/mhfpacket/msg_mhf_get_dist_description.go @@ -1,7 +1,7 @@ package mhfpacket -import ( - "errors" +import ( + "errors" "github.com/Solenataris/Erupe/network/clientctx" "github.com/Solenataris/Erupe/network" @@ -12,7 +12,7 @@ import ( type MsgMhfGetDistDescription struct{ AckHandle uint32 Unk0 uint8 - EntryID uint32 + DistributionID uint32 } // Opcode returns the ID associated with this packet type. @@ -24,10 +24,10 @@ func (m *MsgMhfGetDistDescription) Opcode() network.PacketID { func (m *MsgMhfGetDistDescription) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { m.AckHandle = bf.ReadUint32() m.Unk0 = bf.ReadUint8() - m.EntryID = bf.ReadUint32() + m.DistributionID = bf.ReadUint32() return nil } // Build builds a binary packet from the current data. func (m *MsgMhfGetDistDescription) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { return errors.New("NOT IMPLEMENTED") -} +} \ No newline at end of file diff --git a/Erupe/server/channelserver/handlers_distitem.go b/Erupe/server/channelserver/handlers_distitem.go index aeefc4a41..b2d987610 100644 --- a/Erupe/server/channelserver/handlers_distitem.go +++ b/Erupe/server/channelserver/handlers_distitem.go @@ -1,67 +1,137 @@ package channelserver import ( - "encoding/hex" - // "io/ioutil" - // "path/filepath" - "github.com/Solenataris/Erupe/network/mhfpacket" + "github.com/Solenataris/Erupe/common/stringsupport" + "github.com/Andoryuuta/byteframe" + "go.uber.org/zap" ) -func handleMsgMhfEnumerateDistItem(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfEnumerateDistItem) - // uint16 number of entries - // 446 entry block - // uint32 claimID - // 00 00 00 00 00 00 - // uint16 timesClaimable - // 00 00 00 00 FF FF FF FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00 00 00 - // uint8 stringLength - // string nullTermString - data, _ := hex.DecodeString("0001000000010000000000000000002000000000FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000002F323020426F7820457870616E73696F6E73000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") - doAckBufSucceed(s, pkt.AckHandle, data) +type ItemDist struct { + ID uint32 `db:"id"` + Deadline uint32 `db:"deadline"` + TimesAcceptable uint16 `db:"times_acceptable"` + TimesAccepted uint16 `db:"times_accepted"` + MinHR uint16 `db:"min_hr"` + MaxHR uint16 `db:"max_hr"` + MinSR uint16 `db:"min_sr"` + MaxSR uint16 `db:"max_sr"` + MinGR uint16 `db:"min_gr"` + MaxGR uint16 `db:"max_gr"` + EventName string `db:"event_name"` + Description string `db:"description"` + Data []byte `db:"data"` +} +func handleMsgMhfEnumerateDistItem(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfEnumerateDistItem) + bf := byteframe.NewByteFrame() + + distCount := 0 + dists, err := s.server.db.Queryx(` + SELECT d.id, event_name, description, times_acceptable, + min_hr, max_hr, min_sr, max_sr, min_gr, max_gr, + ( + SELECT count(*) + FROM distributions_accepted da + WHERE d.id = da.distribution_id + AND da.character_id = $1 + ) AS times_accepted, + CASE + WHEN (EXTRACT(epoch FROM deadline)::int) IS NULL THEN 0 + ELSE (EXTRACT(epoch FROM deadline)::int) + END deadline + FROM distribution d + WHERE character_id = $1 AND type = $2 OR character_id IS NULL AND type = $2 ORDER BY id DESC; + `, s.charID, pkt.Unk0) + if err != nil { + s.logger.Error("Error getting distribution data from db", zap.Error(err)) + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + } else { + for dists.Next() { + distCount++ + distData := &ItemDist{} + err = dists.StructScan(&distData) + if err != nil { + s.logger.Error("Error parsing item distribution data", zap.Error(err)) + } + + bf.WriteUint32(distData.ID) + bf.WriteUint32(distData.Deadline) + bf.WriteUint32(0) // Unk + bf.WriteUint16(distData.TimesAcceptable) + bf.WriteUint16(distData.TimesAccepted) + bf.WriteUint16(0) // Unk + bf.WriteUint16(distData.MinHR) + bf.WriteUint16(distData.MaxHR) + bf.WriteUint16(distData.MinSR) + bf.WriteUint16(distData.MaxSR) + bf.WriteUint16(distData.MinGR) + bf.WriteUint16(distData.MaxGR) + bf.WriteUint32(0) // Unk + bf.WriteUint32(0) // Unk + eventName, _ := stringsupport.ConvertUTF8ToShiftJIS(distData.EventName) + bf.WriteUint16(uint16(len(eventName)+1)) + bf.WriteNullTerminatedBytes(eventName) + bf.WriteBytes(make([]byte, 391)) + } + resp := byteframe.NewByteFrame() + resp.WriteUint16(uint16(distCount)) + resp.WriteBytes(bf.Data()) + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) + } } func handleMsgMhfApplyDistItem(s *Session, p mhfpacket.MHFPacket) { - // 0052a49100011f00000000000000010274db99 equipment box page - // 0052a48f00011e0000000000000001195dda5c item box page - // 0052a49400010700003ae30000000132d3a4d6 Item ID 3AE3 - // HEADER: - // int32: Unique item hash for tracking server side purchases? Swapping across items didn't change image/cost/function etc. - // int16: Number of distributed item types - // ITEM ENTRY - // int8: distribution type - // 00 = legs, 01 = Head, 02 = Chest, 03 = Arms, 04 = Waist, 05 = Melee, 06 = Ranged, 07 = Item, 08 == furniture - // ids are wrong shop displays in random order - // 09 = Nothing, 10 = Null Point, 11 = Festi Point, 12 = Zeny, 13 = Null, 14 = Null Points, 15 = My Tore points - // 16 = Restyle Point, 17 = N Points, 18 = Nothing, 19 = Gacha Coins, 20 = Trial Gacha Coins, 21 = Frontier points - // 22 = ?, 23 = Guild Points, 30 = Item Box Page, 31 = Equipment Box Page - // int16: Unk - // int16: Item when type 07 - // int16: Unk - // int16: Number delivered in batch - // int32: Unique item hash for tracking server side purchases? Swapping across items didn't change image/cost/function etc. - pkt := p.(*mhfpacket.MsgMhfApplyDistItem) - if pkt.RequestType == 0 { - doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) - } else if pkt.RequestType == 0x00000001 { - data, _ := hex.DecodeString("0052a494001e0f000000010000000132d3a4d60f000000020000000132d3a4d60f000000030000000132d3a4d60f000000040000000132d3a4d60f000000050000000132d3a4d60f000000060000000132d3a4d60f000000070000000132d3a4d60f000000080000000132d3a4d60f000000090000000132d3a4d60f0000000a0000000132d3a4d60f0000000b0000000132d3a4d60f0000000c0000000132d3a4d60f0000000d0000000132d3a4d60f0000000e0000000132d3a4d60f0000000f0000000132d3a4d60f000000100000000132d3a4d60f000000110000000132d3a4d60f000000120000000132d3a4d60f000000130000000132d3a4d60f000000140000000132d3a4d60f000000150000000132d3a4d60f000000160000000132d3a4d60f000000170000000132d3a4d60f000000180000000132d3a4d60f000000190000000132d3a4d60f0000001a0000000132d3a4d60f0000001b0000000132d3a4d60f0000001c0000000132d3a4d60f0000001d0000000132d3a4d60f0000001e0000000132d3a4d6") - doAckBufSucceed(s, pkt.AckHandle, data) - } else { - doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) - } + pkt := p.(*mhfpacket.MsgMhfApplyDistItem) + + if pkt.DistributionID == 0 { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 6)) + } else { + row := s.server.db.QueryRowx("SELECT data FROM distribution WHERE id = $1", pkt.DistributionID) + dist := &ItemDist{} + err := row.StructScan(dist) + if err != nil { + s.logger.Error("Error parsing item distribution data", zap.Error(err)) + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 6)) + return + } + + bf := byteframe.NewByteFrame() + bf.WriteUint32(0) + bf.WriteBytes(dist.Data) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + + _, err = s.server.db.Exec(` + INSERT INTO public.distributions_accepted + VALUES ($1, $2) + `, pkt.DistributionID, s.charID) + if err != nil { + s.logger.Error("Error updating accepted dist count", zap.Error(err)) + } + } } func handleMsgMhfAcquireDistItem(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireDistItem) - doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfGetDistDescription(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetDistDescription) - // string for the associated message - data, _ := hex.DecodeString("007E43303547656E65726963204974656D20436C61696D204D6573736167657E4330300D0A596F752067657420736F6D65206B696E64206F66206974656D732070726F6261626C792E00000100") - //data, _ := hex.DecodeString("0075b750c1c2b17ac1cab652a1757e433035b8cbb3c6bd63c258b169aa41b0c87e433030a1760a0aa175b8cbb3c6bd63c258b169aa41b0c8a176a843c1caa44a31a6b8a141a569c258b169a2b0add30aa8a4a6e2aabaa175b8cbb3c6bd63a176a2b0adb6a143b3cca668a569c258b169a2b4adb6a14300000100") - doAckBufSucceed(s, pkt.AckHandle, data) -} + + var itemDesc string + err := s.server.db.QueryRow("SELECT description FROM distribution WHERE id = $1", pkt.DistributionID).Scan(&itemDesc) + + if err != nil { + s.logger.Error("Error parsing item distribution description", zap.Error(err)) + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } + + bf := byteframe.NewByteFrame() + description, _ := stringsupport.ConvertUTF8ToShiftJIS(itemDesc) + bf.WriteUint16(uint16(len(description)+1)) + bf.WriteNullTerminatedBytes(description) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} \ No newline at end of file From 33bb960e55f936707b827103ddee1de961d6e2b1 Mon Sep 17 00:00:00 2001 From: Malckyor Date: Thu, 16 Jun 2022 01:07:34 +0900 Subject: [PATCH 4/4] Guild Post JP Shift-JIS Support --- Erupe/server/channelserver/handlers_guild.go | 616 +++++++++++-------- 1 file changed, 355 insertions(+), 261 deletions(-) diff --git a/Erupe/server/channelserver/handlers_guild.go b/Erupe/server/channelserver/handlers_guild.go index f6a9c2929..04677449d 100644 --- a/Erupe/server/channelserver/handlers_guild.go +++ b/Erupe/server/channelserver/handlers_guild.go @@ -1,6 +1,7 @@ package channelserver import ( + "bytes" "database/sql" "database/sql/driver" "encoding/binary" @@ -8,10 +9,13 @@ import ( "encoding/json" "errors" "fmt" + "io" + "io/ioutil" + "log" "sort" - "time" - "strings" "strconv" + "strings" + "time" "github.com/Andoryuuta/byteframe" "github.com/Solenataris/Erupe/common/bfutil" @@ -19,6 +23,8 @@ import ( "github.com/Solenataris/Erupe/network/mhfpacket" "github.com/jmoiron/sqlx" "go.uber.org/zap" + "golang.org/x/text/encoding/japanese" + "golang.org/x/text/transform" ) type FestivalColour string @@ -611,124 +617,124 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { bf := byteframe.NewByteFrame() switch pkt.Action { - case mhfpacket.OPERATE_GUILD_DISBAND: - if guild.LeaderCharID != s.charID { - s.logger.Warn(fmt.Sprintf("character '%d' is attempting to manage guild '%d' without permission", s.charID, guild.ID)) - return - } - - err = guild.Disband(s) - response := 0x01 - - if err != nil { - // All successful acks return 0x01, assuming 0x00 is failure - response = 0x00 - } - - bf.WriteUint32(uint32(response)) - case mhfpacket.OPERATE_GUILD_APPLY: - err = guild.CreateApplication(s, s.charID, GuildApplicationTypeApplied, nil) - - if err != nil { - // All successful acks return 0x01, assuming 0x00 is failure - bf.WriteUint32(0x00) - } else { - bf.WriteUint32(guild.LeaderCharID) - } - case mhfpacket.OPERATE_GUILD_LEAVE: - var err error - - if characterGuildInfo.IsApplicant { - err = guild.RejectApplication(s, s.charID) - } else { - err = guild.RemoveCharacter(s, s.charID) - } - - response := 0x01 - if err != nil { - // All successful acks return 0x01, assuming 0x00 is failure - response = 0x00 - } - - bf.WriteUint32(uint32(response)) - case mhfpacket.OPERATE_GUILD_DONATE_RANK: - handleDonateRP(s, pkt, bf, guild, false) - case mhfpacket.OPERATE_GUILD_SET_APPLICATION_DENY: - // TODO: close applications for guild - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + case mhfpacket.OPERATE_GUILD_DISBAND: + if guild.LeaderCharID != s.charID { + s.logger.Warn(fmt.Sprintf("character '%d' is attempting to manage guild '%d' without permission", s.charID, guild.ID)) return - case mhfpacket.OPERATE_GUILD_SET_APPLICATION_ALLOW: - // TODO: open applications for guild - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) - return - case mhfpacket.OPERATE_GUILD_SET_AVOID_LEADERSHIP_TRUE: - handleAvoidLeadershipUpdate(s, pkt, true) - case mhfpacket.OPERATE_GUILD_SET_AVOID_LEADERSHIP_FALSE: - handleAvoidLeadershipUpdate(s, pkt, false) - case mhfpacket.OPERATE_GUILD_UPDATE_COMMENT: - pbf := byteframe.NewByteFrameFromBytes(pkt.UnkData) + } - if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() { - doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) - return - } + err = guild.Disband(s) + response := 0x01 - commentLength := pbf.ReadUint8() - _ = pbf.ReadUint32() + if err != nil { + // All successful acks return 0x01, assuming 0x00 is failure + response = 0x00 + } - guild.Comment, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(pbf.ReadBytes(uint(commentLength)))) - - if err != nil { - s.logger.Warn("failed to convert guild comment to UTF8", zap.Error(err)) - doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) - break - } - - err = guild.Save(s) - - if err != nil { - doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) - return - } + bf.WriteUint32(uint32(response)) + case mhfpacket.OPERATE_GUILD_APPLY: + err = guild.CreateApplication(s, s.charID, GuildApplicationTypeApplied, nil) + if err != nil { + // All successful acks return 0x01, assuming 0x00 is failure bf.WriteUint32(0x00) - case mhfpacket.OPERATE_GUILD_UPDATE_MOTTO: - if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() { - doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) - return - } + } else { + bf.WriteUint32(guild.LeaderCharID) + } + case mhfpacket.OPERATE_GUILD_LEAVE: + var err error - guild.SubMotto = pkt.UnkData[3] - guild.MainMotto = pkt.UnkData[4] + if characterGuildInfo.IsApplicant { + err = guild.RejectApplication(s, s.charID) + } else { + err = guild.RemoveCharacter(s, s.charID) + } - err := guild.Save(s) + response := 0x01 + if err != nil { + // All successful acks return 0x01, assuming 0x00 is failure + response = 0x00 + } - if err != nil { - doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) - return - } - case mhfpacket.OPERATE_GUILD_RENAME_PUGI_1: - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + bf.WriteUint32(uint32(response)) + case mhfpacket.OPERATE_GUILD_DONATE_RANK: + handleDonateRP(s, pkt, bf, guild, false) + case mhfpacket.OPERATE_GUILD_SET_APPLICATION_DENY: + // TODO: close applications for guild + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + case mhfpacket.OPERATE_GUILD_SET_APPLICATION_ALLOW: + // TODO: open applications for guild + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + case mhfpacket.OPERATE_GUILD_SET_AVOID_LEADERSHIP_TRUE: + handleAvoidLeadershipUpdate(s, pkt, true) + case mhfpacket.OPERATE_GUILD_SET_AVOID_LEADERSHIP_FALSE: + handleAvoidLeadershipUpdate(s, pkt, false) + case mhfpacket.OPERATE_GUILD_UPDATE_COMMENT: + pbf := byteframe.NewByteFrameFromBytes(pkt.UnkData) + + if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return - case mhfpacket.OPERATE_GUILD_RENAME_PUGI_2: - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + } + + commentLength := pbf.ReadUint8() + _ = pbf.ReadUint32() + + guild.Comment, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(pbf.ReadBytes(uint(commentLength)))) + + if err != nil { + s.logger.Warn("failed to convert guild comment to UTF8", zap.Error(err)) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + break + } + + err = guild.Save(s) + + if err != nil { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return - case mhfpacket.OPERATE_GUILD_RENAME_PUGI_3: - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + } + + bf.WriteUint32(0x00) + case mhfpacket.OPERATE_GUILD_UPDATE_MOTTO: + if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return - case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_1: - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + } + + guild.SubMotto = pkt.UnkData[3] + guild.MainMotto = pkt.UnkData[4] + + err := guild.Save(s) + + if err != nil { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return - case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_2: - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) - return - case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_3: - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) - return - case mhfpacket.OPERATE_GUILD_DONATE_EVENT: - handleDonateRP(s, pkt, bf, guild, true) - default: - panic(fmt.Sprintf("unhandled operate guild action '%d'", pkt.Action)) + } + case mhfpacket.OPERATE_GUILD_RENAME_PUGI_1: + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + case mhfpacket.OPERATE_GUILD_RENAME_PUGI_2: + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + case mhfpacket.OPERATE_GUILD_RENAME_PUGI_3: + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_1: + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_2: + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_3: + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + case mhfpacket.OPERATE_GUILD_DONATE_EVENT: + handleDonateRP(s, pkt, bf, guild, true) + default: + panic(fmt.Sprintf("unhandled operate guild action '%d'", pkt.Action)) } doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) @@ -914,8 +920,8 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint32(guild.RankRP) bf.WriteNullTerminatedBytes(leaderName) bf.WriteBytes([]byte{0x00, 0x00, 0x00, 0x00}) // Unk - bf.WriteBool(false) // isReturnGuild - bf.WriteBytes([]byte{0x01, 0x02, 0x02}) // Unk + bf.WriteBool(false) // isReturnGuild + bf.WriteBytes([]byte{0x01, 0x02, 0x02}) // Unk bf.WriteUint32(guild.EventRP) // Pugi's names, probably expected as null until you have them with levels? Null gives them a default japanese name @@ -1001,120 +1007,120 @@ func handleMsgMhfEnumerateGuild(s *Session, p mhfpacket.MHFPacket) { bf := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload) switch pkt.Type { - case mhfpacket.ENUMERATE_GUILD_TYPE_GUILD_NAME: - bf.ReadBytes(8) - searchTermLength := bf.ReadUint16() - bf.ReadBytes(1) - searchTerm := bf.ReadBytes(uint(searchTermLength)) - var searchTermSafe string - searchTermSafe, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(searchTerm)) - if err != nil { - panic(err) + case mhfpacket.ENUMERATE_GUILD_TYPE_GUILD_NAME: + bf.ReadBytes(8) + searchTermLength := bf.ReadUint16() + bf.ReadBytes(1) + searchTerm := bf.ReadBytes(uint(searchTermLength)) + var searchTermSafe string + searchTermSafe, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(searchTerm)) + if err != nil { + panic(err) + } + guilds, err = FindGuildsByName(s, searchTermSafe) + case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_NAME: + bf.ReadBytes(8) + searchTermLength := bf.ReadUint16() + bf.ReadBytes(1) + searchTerm := bf.ReadBytes(uint(searchTermLength)) + var searchTermSafe string + searchTermSafe, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(searchTerm)) + if err != nil { + panic(err) + } + rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE lc.name ILIKE $1`, guildInfoSelectQuery), searchTermSafe) + if err != nil { + s.logger.Error("Failed to retrieve guild by leader name", zap.Error(err)) + } else { + for rows.Next() { + guild, _ := buildGuildObjectFromDbResult(rows, err, s) + guilds = append(guilds, guild) } - guilds, err = FindGuildsByName(s, searchTermSafe) - case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_NAME: - bf.ReadBytes(8) - searchTermLength := bf.ReadUint16() - bf.ReadBytes(1) - searchTerm := bf.ReadBytes(uint(searchTermLength)) - var searchTermSafe string - searchTermSafe, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(searchTerm)) - if err != nil { - panic(err) + } + case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_ID: + bf.ReadBytes(3) + ID := bf.ReadUint32() + rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE leader_id = $1`, guildInfoSelectQuery), ID) + if err != nil { + s.logger.Error("Failed to retrieve guild by leader ID", zap.Error(err)) + } else { + for rows.Next() { + guild, _ := buildGuildObjectFromDbResult(rows, err, s) + guilds = append(guilds, guild) } - rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE lc.name ILIKE $1`, guildInfoSelectQuery), searchTermSafe) - if err != nil { - s.logger.Error("Failed to retrieve guild by leader name", zap.Error(err)) - } else { - for rows.Next() { - guild, _ := buildGuildObjectFromDbResult(rows, err, s) - guilds = append(guilds, guild) - } + } + case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_MEMBERS: + sorting := bf.ReadUint16() + if sorting == 1 { + rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY member_count DESC`, guildInfoSelectQuery)) + } else { + rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY member_count ASC`, guildInfoSelectQuery)) + } + if err != nil { + s.logger.Error("Failed to retrieve guild by member count", zap.Error(err)) + } else { + for rows.Next() { + guild, _ := buildGuildObjectFromDbResult(rows, err, s) + guilds = append(guilds, guild) } - case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_ID: - bf.ReadBytes(3) - ID := bf.ReadUint32() - rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE leader_id = $1`, guildInfoSelectQuery), ID) - if err != nil { - s.logger.Error("Failed to retrieve guild by leader ID", zap.Error(err)) - } else { - for rows.Next() { - guild, _ := buildGuildObjectFromDbResult(rows, err, s) - guilds = append(guilds, guild) - } + } + case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_REGISTRATION: + sorting := bf.ReadUint16() + if sorting == 1 { + rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY id DESC`, guildInfoSelectQuery)) + } else { + rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY id ASC`, guildInfoSelectQuery)) + } + if err != nil { + s.logger.Error("Failed to retrieve guild by registration date", zap.Error(err)) + } else { + for rows.Next() { + guild, _ := buildGuildObjectFromDbResult(rows, err, s) + guilds = append(guilds, guild) } - case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_MEMBERS: - sorting := bf.ReadUint16() - if sorting == 1 { - rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY member_count DESC`, guildInfoSelectQuery)) - } else { - rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY member_count ASC`, guildInfoSelectQuery)) + } + case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_RANK: + sorting := bf.ReadUint16() + if sorting == 1 { + rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY rank_rp DESC`, guildInfoSelectQuery)) + } else { + rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY rank_rp ASC`, guildInfoSelectQuery)) + } + if err != nil { + s.logger.Error("Failed to retrieve guild by rank", zap.Error(err)) + } else { + for rows.Next() { + guild, _ := buildGuildObjectFromDbResult(rows, err, s) + guilds = append(guilds, guild) } - if err != nil { - s.logger.Error("Failed to retrieve guild by member count", zap.Error(err)) - } else { - for rows.Next() { - guild, _ := buildGuildObjectFromDbResult(rows, err, s) - guilds = append(guilds, guild) - } + } + case mhfpacket.ENUMERATE_GUILD_TYPE_MOTTO: + bf.ReadBytes(3) + mainMotto := bf.ReadUint16() + subMotto := bf.ReadUint16() + rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE main_motto = $1 AND sub_motto = $2`, guildInfoSelectQuery), mainMotto, subMotto) + if err != nil { + s.logger.Error("Failed to retrieve guild by motto", zap.Error(err)) + } else { + for rows.Next() { + guild, _ := buildGuildObjectFromDbResult(rows, err, s) + guilds = append(guilds, guild) } - case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_REGISTRATION: - sorting := bf.ReadUint16() - if sorting == 1 { - rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY id DESC`, guildInfoSelectQuery)) - } else { - rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY id ASC`, guildInfoSelectQuery)) - } - if err != nil { - s.logger.Error("Failed to retrieve guild by registration date", zap.Error(err)) - } else { - for rows.Next() { - guild, _ := buildGuildObjectFromDbResult(rows, err, s) - guilds = append(guilds, guild) - } - } - case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_RANK: - sorting := bf.ReadUint16() - if sorting == 1 { - rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY rank_rp DESC`, guildInfoSelectQuery)) - } else { - rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY rank_rp ASC`, guildInfoSelectQuery)) - } - if err != nil { - s.logger.Error("Failed to retrieve guild by rank", zap.Error(err)) - } else { - for rows.Next() { - guild, _ := buildGuildObjectFromDbResult(rows, err, s) - guilds = append(guilds, guild) - } - } - case mhfpacket.ENUMERATE_GUILD_TYPE_MOTTO: - bf.ReadBytes(3) - mainMotto := bf.ReadUint16() - subMotto := bf.ReadUint16() - rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE main_motto = $1 AND sub_motto = $2`, guildInfoSelectQuery), mainMotto, subMotto) - if err != nil { - s.logger.Error("Failed to retrieve guild by motto", zap.Error(err)) - } else { - for rows.Next() { - guild, _ := buildGuildObjectFromDbResult(rows, err, s) - guilds = append(guilds, guild) - } - } - case mhfpacket.ENUMERATE_GUILD_TYPE_RECRUITING: - // - case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ALLIANCE_NAME: - // - case mhfpacket.ENUMERATE_ALLIANCE_TYPE_LEADER_NAME: - // - case mhfpacket.ENUMERATE_ALLIANCE_TYPE_LEADER_ID: - // - case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ORDER_MEMBERS: - // - case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ORDER_REGISTRATION: - // - default: - panic(fmt.Sprintf("no handler for guild search type '%d'", pkt.Type)) + } + case mhfpacket.ENUMERATE_GUILD_TYPE_RECRUITING: + // + case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ALLIANCE_NAME: + // + case mhfpacket.ENUMERATE_ALLIANCE_TYPE_LEADER_NAME: + // + case mhfpacket.ENUMERATE_ALLIANCE_TYPE_LEADER_ID: + // + case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ORDER_MEMBERS: + // + case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ORDER_REGISTRATION: + // + default: + panic(fmt.Sprintf("no handler for guild search type '%d'", pkt.Type)) } if err != nil || guilds == nil { @@ -1133,13 +1139,13 @@ func handleMsgMhfEnumerateGuild(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint32(guild.ID) bf.WriteUint32(guild.LeaderCharID) bf.WriteUint16(guild.MemberCount) - bf.WriteUint8(0x00) // Unk - bf.WriteUint8(0x00) // Unk + bf.WriteUint8(0x00) // Unk + bf.WriteUint8(0x00) // Unk bf.WriteUint16(guild.Rank) bf.WriteUint32(uint32(guild.CreatedAt.Unix())) - bf.WriteUint8(uint8(len(guildName)+1)) + bf.WriteUint8(uint8(len(guildName) + 1)) bf.WriteNullTerminatedBytes(guildName) - bf.WriteUint8(uint8(len(leaderName)+1)) + bf.WriteUint8(uint8(len(leaderName) + 1)) bf.WriteNullTerminatedBytes(leaderName) bf.WriteUint8(0x01) // Unk } @@ -1336,8 +1342,8 @@ func handleMsgMhfEnumerateGuildItem(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint32(0x00) bf.WriteUint16(0x00) for i := 0; i < amount; i++ { - bf.WriteUint32(binary.BigEndian.Uint32(boxContents[i*4:i*4+4])) - if i + 1 != amount { + bf.WriteUint32(binary.BigEndian.Uint32(boxContents[i*4 : i*4+4])) + if i+1 != amount { bf.WriteUint64(0x00) } } @@ -1346,7 +1352,7 @@ func handleMsgMhfEnumerateGuildItem(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } -type Item struct{ +type Item struct { ItemId uint16 Amount uint16 } @@ -1364,8 +1370,8 @@ func handleMsgMhfUpdateGuildItem(s *Session, p mhfpacket.MHFPacket) { amount := len(boxContents) / 4 oldItems = make([]Item, amount) for i := 0; i < amount; i++ { - oldItems[i].ItemId = binary.BigEndian.Uint16(boxContents[i*4:i*4+2]) - oldItems[i].Amount = binary.BigEndian.Uint16(boxContents[i*4+2:i*4+4]) + oldItems[i].ItemId = binary.BigEndian.Uint16(boxContents[i*4 : i*4+2]) + oldItems[i].Amount = binary.BigEndian.Uint16(boxContents[i*4+2 : i*4+4]) } } @@ -1391,9 +1397,9 @@ func handleMsgMhfUpdateGuildItem(s *Session, p mhfpacket.MHFPacket) { // Delete empty item stacks for i := len(newItems) - 1; i >= 0; i-- { if int(newItems[i].Amount) == 0 { - copy(newItems[i:], newItems[i + 1:]) - newItems[len(newItems) - 1] = make([]Item, 1)[0] - newItems = newItems[:len(newItems) - 1] + copy(newItems[i:], newItems[i+1:]) + newItems[len(newItems)-1] = make([]Item, 1)[0] + newItems = newItems[:len(newItems)-1] } } @@ -1515,7 +1521,13 @@ func handleMsgMhfLoadGuildCooking(s *Session, p mhfpacket.MHFPacket) { // uint32 expiration timestamp // encourage food - data := []byte{0x00, 0x01, 0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0xC4, 0x00, 0x00, 0x00, 0x03, 0x5F, 0xFC, 0x0B, 0x51} + data := []byte{0x00, 0x06, + 0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0xc4, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x0B, 0x51, + 0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0x9c, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x0B, 0x52, + 0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x0B, 0x51, + 0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x01, 0x8b, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFD, 0x0B, 0x51, + 0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0x54, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x0B, 0x51, + 0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x03, 0xF3, 0xFC, 0x0B, 0x51} doAckBufSucceed(s, pkt.AckHandle, data) //data := []byte{0x00, 0x01, 0x1C, 0x72, 0x54, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x5F, 0xF8, 0x2F, 0xE1} //doAckBufSucceed(s, pkt.AckHandle, data) @@ -1566,7 +1578,7 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { msgs, err := s.server.db.Queryx("SELECT post_type, stamp_id, title, body, author_id, (EXTRACT(epoch FROM created_at)::int) as created_at, liked_by FROM guild_posts WHERE guild_id = $1 AND post_type = $2 ORDER BY created_at DESC", guild.ID, int(pkt.BoardType)) if err != nil { - s.logger.Fatal("Failed to get guild messages from db", zap.Error(err)) + log.Println("Failed to get guild messages from db", zap.Error(err)) } bf := byteframe.NewByteFrame() @@ -1575,22 +1587,38 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { for msgs.Next() { noMsgs = false postCount++ + var str_title, str_body string + var timestampst, timecomp uint64 + timestampst = 32400 postData := &MessageBoardPost{} err = msgs.StructScan(&postData) if err != nil { - s.logger.Error("Failed to read guild message board post") + log.Println("Failed to get guild messages from db", zap.Error(err)) } + t := japanese.ShiftJIS.NewEncoder() + str_title, _, err := transform.String(t, postData.Title) + if err != nil { + log.Println(err) + } + str_body, _, err = transform.String(t, postData.Body) + if err != nil { + log.Println(err) + } + + timecomp = postData.Timestamp - timestampst + bf.WriteUint32(postData.Type) bf.WriteUint32(postData.AuthorID) - bf.WriteUint64(postData.Timestamp) + bf.WriteUint64(timecomp) liked := false likedBySlice := strings.Split(postData.LikedBy, ",") for i := 0; i < len(likedBySlice); i++ { j, _ := strconv.ParseInt(likedBySlice[i], 10, 64) if int(j) == int(s.charID) { - liked = true; break + liked = true + break } } if likedBySlice[0] == "" { @@ -1598,12 +1626,13 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { } else { bf.WriteUint32(uint32(len(likedBySlice))) } + bf.WriteBool(liked) bf.WriteUint32(postData.StampID) - bf.WriteUint32(uint32(len(postData.Title))) - bf.WriteBytes([]byte(postData.Title)) - bf.WriteUint32(uint32(len(postData.Body))) - bf.WriteBytes([]byte(postData.Body)) + bf.WriteUint32(uint32(len(str_title))) + bf.WriteBytes([]byte(str_title)) + bf.WriteUint32(uint32(len(str_body))) + bf.WriteBytes([]byte(str_body)) } if noMsgs { doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) @@ -1615,6 +1644,19 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { } } +func transformEncoding(rawReader io.Reader, trans transform.Transformer) (string, error) { + ret, err := ioutil.ReadAll(transform.NewReader(rawReader, trans)) + if err == nil { + return string(ret), nil + } else { + return "", err + } +} + +func BytesFromShiftJIS(b []byte) (string, error) { + return transformEncoding(bytes.NewReader(b), japanese.ShiftJIS.NewDecoder()) +} + func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUpdateGuildMessageBoard) bf := byteframe.NewByteFrameFromBytes(pkt.Request) @@ -1623,57 +1665,109 @@ func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) return } + switch pkt.MessageOp { case 0: // Create message + var title_str, body_str string + postType := bf.ReadUint32() // 0 = message, 1 = news stampId := bf.ReadUint32() titleLength := bf.ReadUint32() bodyLength := bf.ReadUint32() title := bf.ReadBytes(uint(titleLength)) body := bf.ReadBytes(uint(bodyLength)) - _, err := s.server.db.Exec("INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)", guild.ID, s.charID, int(stampId), int(postType), string(title), string(body)) + + title_str, err := BytesFromShiftJIS(title) if err != nil { - s.logger.Fatal("Failed to add new guild message to db", zap.Error(err)) + log.Println(err) + } + fmt.Println(title_str) + + body_str, err = BytesFromShiftJIS(body) + if err != nil { + log.Println(err) + } + fmt.Println(body_str) + + _, err = s.server.db.Exec("INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)", guild.ID, s.charID, int(stampId), int(postType), string(title_str), string(body_str)) + if err != nil { + log.Println("Failed to add new guild message to db", zap.Error(err)) } // TODO: if there are too many messages, purge excess _, err = s.server.db.Exec("") if err != nil { - s.logger.Fatal("Failed to remove excess guild messages from db", zap.Error(err)) + log.Println("Failed to remove excess guild messages from db", zap.Error(err)) } case 1: // Delete message + var timestampst, timecomp uint64 + timestampst = 32400 + postType := bf.ReadUint32() timestamp := bf.ReadUint64() - _, err := s.server.db.Exec("DELETE FROM guild_posts WHERE post_type = $1 AND (EXTRACT(epoch FROM created_at)::int) = $2 AND guild_id = $3", int(postType), int(timestamp), guild.ID) + + timecomp = timestamp + timestampst + _, err := s.server.db.Exec("DELETE FROM guild_posts WHERE post_type = $1 AND (EXTRACT(epoch FROM created_at)::int) = $2 AND guild_id = $3", int(postType), int(timecomp), guild.ID) if err != nil { - s.logger.Fatal("Failed to delete guild message from db", zap.Error(err)) + log.Println("Failed to delete guild message from db", zap.Error(err)) } case 2: // Update message + var title_str, body_str string + var timestampst, timecomp uint64 + timestampst = 32400 + postType := bf.ReadUint32() timestamp := bf.ReadUint64() titleLength := bf.ReadUint32() bodyLength := bf.ReadUint32() title := bf.ReadBytes(uint(titleLength)) body := bf.ReadBytes(uint(bodyLength)) - _, err := s.server.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE post_type = $3 AND (EXTRACT(epoch FROM created_at)::int) = $4 AND guild_id = $5", string(title), string(body), int(postType), int(timestamp), guild.ID) + + title_str, err := BytesFromShiftJIS(title) if err != nil { - s.logger.Fatal("Failed to update guild message in db", zap.Error(err)) + log.Println(err) + } + fmt.Println(title_str) + + body_str, err = BytesFromShiftJIS(body) + if err != nil { + log.Println(err) + } + fmt.Println(body_str) + + timecomp = timestamp + timestampst + + _, err = s.server.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE post_type = $3 AND (EXTRACT(epoch FROM created_at)::int) = $4 AND guild_id = $5", string(title_str), string(body_str), int(postType), int(timecomp), guild.ID) + if err != nil { + log.Println("Failed to update guild message in db", zap.Error(err)) } case 3: // Update stamp + var timestampst, timecomp uint64 + timestampst = 32400 + postType := bf.ReadUint32() timestamp := bf.ReadUint64() stampId := bf.ReadUint32() - _, err := s.server.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", int(stampId), int(postType), int(timestamp), guild.ID) + + timecomp = timestamp + timestampst + + _, err := s.server.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", int(stampId), int(postType), int(timecomp), guild.ID) if err != nil { - s.logger.Fatal("Failed to update guild message stamp in db", zap.Error(err)) + log.Println("Failed to update guild message stamp in db", zap.Error(err)) } case 4: // Like message + var timestampst, timecomp uint64 + timestampst = 32400 + postType := bf.ReadUint32() timestamp := bf.ReadUint64() likeState := bf.ReadBool() + + timecomp = timestamp + timestampst + var likedBy string - err := s.server.db.QueryRow("SELECT liked_by FROM guild_posts WHERE post_type = $1 AND (EXTRACT(epoch FROM created_at)::int) = $2 AND guild_id = $3", int(postType), int(timestamp), guild.ID).Scan(&likedBy) + err := s.server.db.QueryRow("SELECT liked_by FROM guild_posts WHERE post_type = $1 AND (EXTRACT(epoch FROM created_at)::int) = $2 AND guild_id = $3", int(postType), int(timecomp), guild.ID).Scan(&likedBy) if err != nil { - s.logger.Fatal("Failed to get guild message like data from db", zap.Error(err)) + log.Println("Failed to get guild message like data from db", zap.Error(err)) } else { if likeState { if len(likedBy) == 0 { @@ -1681,22 +1775,22 @@ func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { } else { likedBy += "," + strconv.Itoa(int(s.charID)) } - _, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", likedBy, int(postType), int(timestamp), guild.ID) + _, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", likedBy, int(postType), int(timecomp), guild.ID) if err != nil { - s.logger.Fatal("Failed to like guild message in db", zap.Error(err)) + log.Println("Failed to like guild message in db", zap.Error(err)) } } else { likedBySlice := strings.Split(likedBy, ",") for i, e := range likedBySlice { if e == strconv.Itoa(int(s.charID)) { - likedBySlice[i] = likedBySlice[len(likedBySlice) - 1] - likedBySlice = likedBySlice[:len(likedBySlice) - 1] + likedBySlice[i] = likedBySlice[len(likedBySlice)-1] + likedBySlice = likedBySlice[:len(likedBySlice)-1] } } likedBy = strings.Join(likedBySlice, ",") - _, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", likedBy, int(postType), int(timestamp), guild.ID) + _, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", likedBy, int(postType), int(timecomp), guild.ID) if err != nil { - s.logger.Fatal("Failed to unlike guild message in db", zap.Error(err)) + log.Println("Failed to unlike guild message in db", zap.Error(err)) } } } @@ -1705,15 +1799,15 @@ func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { var newPosts int err := s.server.db.QueryRow("SELECT (EXTRACT(epoch FROM guild_post_checked)::int) FROM characters WHERE id = $1", s.charID).Scan(&timeChecked) if err != nil { - s.logger.Fatal("Failed to get last guild post check timestamp from db", zap.Error(err)) + log.Println("Failed to get last guild post check timestamp from db", zap.Error(err)) } else { _, err = s.server.db.Exec("UPDATE characters SET guild_post_checked = $1 WHERE id = $2", time.Now(), s.charID) if err != nil { - s.logger.Fatal("Failed to update guild post check timestamp in db", zap.Error(err)) + log.Println("Failed to update guild post check timestamp in db", zap.Error(err)) } else { err = s.server.db.QueryRow("SELECT COUNT(*) FROM guild_posts WHERE guild_id = $1 AND (EXTRACT(epoch FROM created_at)::int) > $2", guild.ID, timeChecked).Scan(&newPosts) if err != nil { - s.logger.Fatal("Failed to check for new guild posts in db", zap.Error(err)) + log.Println("Failed to check for new guild posts in db", zap.Error(err)) } else { if newPosts > 0 { doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x01}) @@ -1764,8 +1858,8 @@ func handleMsgMhfOperationInvGuild(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfUpdateGuildcard(s *Session, p mhfpacket.MHFPacket) {} -func handleMsgMhfCreateJoint(s *Session, p mhfpacket.MHFPacket) { } +func handleMsgMhfCreateJoint(s *Session, p mhfpacket.MHFPacket) {} -func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) { } +func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) {} -func handleMsgMhfInfoJoint(s *Session, p mhfpacket.MHFPacket) {} \ No newline at end of file +func handleMsgMhfInfoJoint(s *Session, p mhfpacket.MHFPacket) {}