diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 000000000..375aa424a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,21 @@ +--- +name: Bug +about: Bug +title: '' +labels: bug +assignees: '' + +--- + +**Description**: +A clear and concise description of what the bug is. + +**Reproduction Steps**: +1. +2. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 000000000..1d8650d16 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,17 @@ +--- +name: Feature +about: Feature +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to an existing issue?** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/common/pascalstring/pascalstring.go b/common/pascalstring/pascalstring.go index 3640db22d..8ad332018 100644 --- a/common/pascalstring/pascalstring.go +++ b/common/pascalstring/pascalstring.go @@ -11,7 +11,8 @@ func Uint8(bf *byteframe.ByteFrame, x string, t bool) { e := japanese.ShiftJIS.NewEncoder() xt, _, err := transform.String(e, x) if err != nil { - panic(err) + bf.WriteUint8(0) + return } x = xt } @@ -24,7 +25,8 @@ func Uint16(bf *byteframe.ByteFrame, x string, t bool) { e := japanese.ShiftJIS.NewEncoder() xt, _, err := transform.String(e, x) if err != nil { - panic(err) + bf.WriteUint16(0) + return } x = xt } @@ -37,7 +39,8 @@ func Uint32(bf *byteframe.ByteFrame, x string, t bool) { e := japanese.ShiftJIS.NewEncoder() xt, _, err := transform.String(e, x) if err != nil { - panic(err) + bf.WriteUint32(0) + return } x = xt } diff --git a/common/stringsupport/string_convert.go b/common/stringsupport/string_convert.go index 84574375b..ab91311ac 100644 --- a/common/stringsupport/string_convert.go +++ b/common/stringsupport/string_convert.go @@ -120,7 +120,11 @@ func CSVAdd(csv string, v int) string { if len(csv) == 0 { return strconv.Itoa(v) } - return csv + "," + strconv.Itoa(v) + if CSVContains(csv, v) { + return csv + } else { + return csv + "," + strconv.Itoa(v) + } } func CSVRemove(csv string, v int) string { diff --git a/config.json b/config.json index 29af1fb8d..00e541adf 100644 --- a/config.json +++ b/config.json @@ -5,16 +5,16 @@ "devmode": true, "devmodeoptions": { "serverName" : "", + "EnableLauncherServer": false, "hideLoginNotice": false, - "loginNotice": "
Welcome to Erupe SU9 (Patch 1)!
Erupe is experimental software, we are not liable for any
issues caused by installing the software!

■Report bugs on Discord!

■Test everything!

■Don't talk to softlocking NPCs!

■Fork the code on GitHub!

Thank you to all of the contributors,

this wouldn't exist without you.", + "loginNotice": "
Welcome to Erupe SU9.1 Beta!
Erupe is experimental software, we are not liable for any
issues caused by installing the software!

■Report bugs on Discord!

■Test everything!

■Don't talk to softlocking NPCs!

■Fork the code on GitHub!

Thank you to all of the contributors,

this wouldn't exist without you.", "cleandb": false, "maxlauncherhr": false, "LogInboundMessages": false, "LogOutboundMessages": false, "MaxHexdumpLength": 256, - "Event": 0, "DivaEvent": 0, - "FestaEvent": 0, + "FestaEvent": -1, "TournamentEvent": 0, "MezFesEvent": true, "MezFesAlt": false, @@ -28,10 +28,7 @@ "discord": { "enabled": false, "bottoken": "", - "realtimeChannelID": "", - "serverId": "", - "devRoles": [], - "devMode": false + "realtimeChannelID": "" }, "database": { "host": "localhost", diff --git a/config/config.go b/config/config.go index b025e7a0a..bfbf8086a 100644 --- a/config/config.go +++ b/config/config.go @@ -24,23 +24,24 @@ type Config struct { // 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) - HideLoginNotice bool // Hide the Erupe notice on login - LoginNotice string // MHFML string of the login notice displayed - 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 - LogInboundMessages bool // Log all messages sent to the server - LogOutboundMessages bool // Log all messages sent to the clients - MaxHexdumpLength int // Maximum number of bytes printed when logs are enabled - DivaEvent int // Diva Defense event status - FestaEvent int // Hunter's Festa event status - TournamentEvent int // VS Tournament event status - MezFesEvent bool // MezFes status - MezFesAlt bool // Swaps out Volpakkun for Tokotoko - DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) - DisableMailItems bool // Hack to prevent english versions of MHF from crashing - SaveDumps SaveDumpOptions + ServerName string // To get specific instance server about (Current Players/Event Week) + EnableLauncherServer bool // Enables the launcher server to be served on port 80 + HideLoginNotice bool // Hide the Erupe notice on login + LoginNotice string // MHFML string of the login notice displayed + 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 + LogInboundMessages bool // Log all messages sent to the server + LogOutboundMessages bool // Log all messages sent to the clients + MaxHexdumpLength int // Maximum number of bytes printed when logs are enabled + DivaEvent int // Diva Defense event status + FestaEvent int // Hunter's Festa event status + TournamentEvent int // VS Tournament event status + MezFesEvent bool // MezFes status + MezFesAlt bool // Swaps out Volpakkun for Tokotoko + DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) + DisableMailItems bool // Hack to prevent english versions of MHF from crashing + SaveDumps SaveDumpOptions } type SaveDumpOptions struct { diff --git a/go.mod b/go.mod index 59d5c7eb9..8107625a2 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,13 @@ go 1.16 require ( github.com/bwmarrin/discordgo v0.23.2 - github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.4.2 // indirect github.com/jmoiron/sqlx v1.3.4 github.com/lib/pq v1.10.4 - github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/sachaos/lottery v0.0.0-20180520074626-61949d99bd96 github.com/spf13/viper v1.8.1 go.uber.org/atomic v1.9.0 // indirect @@ -23,5 +20,4 @@ require ( golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect golang.org/x/text v0.3.7 golang.org/x/tools v0.1.8 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/main.go b/main.go index c5ac726b3..b9dc62538 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "math/rand" "net" "os" "os/signal" @@ -66,7 +65,7 @@ func main() { var discordBot *discordbot.DiscordBot = nil if erupeConfig.Discord.Enabled { - bot, err := discordbot.NewDiscordBot(discordbot.DiscordBotOptions{ + bot, err := discordbot.NewDiscordBot(discordbot.Options{ Logger: logger, Config: erupeConfig, }) @@ -83,6 +82,7 @@ func main() { } discordBot = bot + logger.Info("Discord bot is enabled") } else { logger.Info("Discord bot is disabled") } @@ -109,9 +109,11 @@ func main() { } logger.Info("Connected to database") - // Clear existing tokens + // Clear stale data _ = db.MustExec("DELETE FROM sign_sessions") _ = db.MustExec("DELETE FROM servers") + _ = db.MustExec("DELETE FROM cafe_accepted") + _ = db.MustExec("UPDATE characters SET cafe_time=0") // Clean the DB if the option is on. if erupeConfig.DevMode && erupeConfig.DevModeOptions.CleanDB { @@ -123,18 +125,21 @@ func main() { // 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 { - preventClose(fmt.Sprintf("Failed to start launcher server: %s", err.Error())) + var launcherServer *launcherserver.Server + if erupeConfig.DevMode && erupeConfig.DevModeOptions.EnableLauncherServer { + launcherServer = launcherserver.NewServer( + &launcherserver.Config{ + Logger: logger.Named("launcher"), + ErupeConfig: erupeConfig, + DB: db, + UseOriginalLauncherFiles: erupeConfig.Launcher.UseOriginalLauncherFiles, + }) + err = launcherServer.Start() + if err != nil { + preventClose(fmt.Sprintf("Failed to start launcher server: %s", err.Error())) + } + logger.Info("Started launcher server") } - logger.Info("Started launcher server") // Entrance server. entranceServer := entranceserver.NewServer( @@ -168,9 +173,6 @@ func main() { ci := 0 count := 1 for _, ee := range erupeConfig.Entrance.Entries { - rand.Seed(time.Now().UnixNano()) - // Randomly generate a season for the World - season := rand.Intn(3) + 1 for _, ce := range ee.Channels { sid := (4096 + si*256) + (16 + ci) c := *channelserver.NewServer(&channelserver.Config{ @@ -190,7 +192,7 @@ func main() { if err != nil { preventClose(fmt.Sprintf("Failed to start channel server: %s", err.Error())) } else { - channelQuery += fmt.Sprintf("INSERT INTO servers (server_id, season, current_players) VALUES (%d, %d, 0);", sid, season) + channelQuery += fmt.Sprintf("INSERT INTO servers (server_id, season, current_players) VALUES (%d, %d, 0);", sid, si%3) channels = append(channels, &c) logger.Info(fmt.Sprintf("Started channel server %d on port %d", count, ce.Port)) ci++ @@ -220,7 +222,9 @@ func main() { } signServer.Shutdown() entranceServer.Shutdown() - launcherServer.Shutdown() + if erupeConfig.DevModeOptions.EnableLauncherServer { + launcherServer.Shutdown() + } time.Sleep(1 * time.Second) } diff --git a/network/binpacket/msg_bin_mail_notify.go b/network/binpacket/msg_bin_mail_notify.go index 5e1687512..125dc57ef 100644 --- a/network/binpacket/msg_bin_mail_notify.go +++ b/network/binpacket/msg_bin_mail_notify.go @@ -16,11 +16,7 @@ func (m MsgBinMailNotify) Parse(bf *byteframe.ByteFrame) error { func (m MsgBinMailNotify) Build(bf *byteframe.ByteFrame) error { bf.WriteUint8(0x01) // Unk - byteName, _ := stringsupport.ConvertUTF8ToShiftJIS(m.SenderName) - - bf.WriteBytes(byteName) - bf.WriteBytes(make([]byte, 21-len(byteName))) - + bf.WriteBytes(stringsupport.PaddedString(m.SenderName, 21, true)) return nil } diff --git a/network/mhfpacket/msg_mhf_acquire_monthly_item.go b/network/mhfpacket/msg_mhf_acquire_monthly_item.go index a8d8e2f04..acc10b42a 100644 --- a/network/mhfpacket/msg_mhf_acquire_monthly_item.go +++ b/network/mhfpacket/msg_mhf_acquire_monthly_item.go @@ -1,15 +1,21 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfAcquireMonthlyItem represents the MSG_MHF_ACQUIRE_MONTHLY_ITEM -type MsgMhfAcquireMonthlyItem struct{} +type MsgMhfAcquireMonthlyItem struct { + AckHandle uint32 + Unk0 uint16 + Unk1 uint16 + Unk2 uint32 + Unk3 uint32 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfAcquireMonthlyItem) Opcode() network.PacketID { @@ -18,7 +24,12 @@ func (m *MsgMhfAcquireMonthlyItem) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfAcquireMonthlyItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Unk0 = bf.ReadUint16() + m.Unk1 = bf.ReadUint16() + m.Unk2 = bf.ReadUint32() + m.Unk3 = bf.ReadUint32() + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_acquire_title.go b/network/mhfpacket/msg_mhf_acquire_title.go index e521734f8..fe3a5ca95 100644 --- a/network/mhfpacket/msg_mhf_acquire_title.go +++ b/network/mhfpacket/msg_mhf_acquire_title.go @@ -1,15 +1,20 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfAcquireTitle represents the MSG_MHF_ACQUIRE_TITLE -type MsgMhfAcquireTitle struct{} +type MsgMhfAcquireTitle struct { + AckHandle uint32 + Unk0 uint16 + Unk1 uint16 + TitleID uint16 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfAcquireTitle) Opcode() network.PacketID { @@ -18,7 +23,11 @@ func (m *MsgMhfAcquireTitle) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfAcquireTitle) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Unk0 = bf.ReadUint16() + m.Unk1 = bf.ReadUint16() + m.TitleID = bf.ReadUint16() + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_check_monthly_item.go b/network/mhfpacket/msg_mhf_check_monthly_item.go index 2e04c3226..d79c65240 100644 --- a/network/mhfpacket/msg_mhf_check_monthly_item.go +++ b/network/mhfpacket/msg_mhf_check_monthly_item.go @@ -1,15 +1,18 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfCheckMonthlyItem represents the MSG_MHF_CHECK_MONTHLY_ITEM -type MsgMhfCheckMonthlyItem struct{} +type MsgMhfCheckMonthlyItem struct { + AckHandle uint32 + Unk uint32 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfCheckMonthlyItem) Opcode() network.PacketID { @@ -18,7 +21,9 @@ func (m *MsgMhfCheckMonthlyItem) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfCheckMonthlyItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Unk = bf.ReadUint32() + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_check_weekly_stamp.go b/network/mhfpacket/msg_mhf_check_weekly_stamp.go index 83f6e5ce8..f03b1d1e7 100644 --- a/network/mhfpacket/msg_mhf_check_weekly_stamp.go +++ b/network/mhfpacket/msg_mhf_check_weekly_stamp.go @@ -1,15 +1,16 @@ package mhfpacket import ( + "errors" + "erupe-ce/common/byteframe" "erupe-ce/network" "erupe-ce/network/clientctx" - "erupe-ce/common/byteframe" ) // MsgMhfCheckWeeklyStamp represents the MSG_MHF_CHECK_WEEKLY_STAMP type MsgMhfCheckWeeklyStamp struct { AckHandle uint32 - Unk0 uint8 + StampType string Unk1 bool Unk2 uint16 // Hardcoded 0 in the binary } @@ -22,7 +23,13 @@ func (m *MsgMhfCheckWeeklyStamp) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfCheckWeeklyStamp) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { m.AckHandle = bf.ReadUint32() - m.Unk0 = bf.ReadUint8() + stampType := bf.ReadUint8() + switch stampType { + case 1: + m.StampType = "hl" + case 2: + m.StampType = "ex" + } m.Unk1 = bf.ReadBool() m.Unk2 = bf.ReadUint16() return nil @@ -30,9 +37,5 @@ func (m *MsgMhfCheckWeeklyStamp) Parse(bf *byteframe.ByteFrame, ctx *clientctx.C // Build builds a binary packet from the current data. func (m *MsgMhfCheckWeeklyStamp) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - bf.WriteUint32(m.AckHandle) - bf.WriteUint8(m.Unk0) - bf.WriteBool(m.Unk1) - bf.WriteUint16(m.Unk2) - return nil + return errors.New("NOT IMPLEMENTED") } diff --git a/network/mhfpacket/msg_mhf_entry_rookie_guild.go b/network/mhfpacket/msg_mhf_entry_rookie_guild.go index d210be01a..e3f12660f 100644 --- a/network/mhfpacket/msg_mhf_entry_rookie_guild.go +++ b/network/mhfpacket/msg_mhf_entry_rookie_guild.go @@ -1,15 +1,18 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfEntryRookieGuild represents the MSG_MHF_ENTRY_ROOKIE_GUILD -type MsgMhfEntryRookieGuild struct{} +type MsgMhfEntryRookieGuild struct { + AckHandle uint32 + Unk uint32 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfEntryRookieGuild) Opcode() network.PacketID { @@ -18,7 +21,9 @@ func (m *MsgMhfEntryRookieGuild) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfEntryRookieGuild) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Unk = bf.ReadUint32() + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_enumerate_rengoku_ranking.go b/network/mhfpacket/msg_mhf_enumerate_rengoku_ranking.go index d35bc7227..21817237d 100644 --- a/network/mhfpacket/msg_mhf_enumerate_rengoku_ranking.go +++ b/network/mhfpacket/msg_mhf_enumerate_rengoku_ranking.go @@ -1,19 +1,19 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfEnumerateRengokuRanking represents the MSG_MHF_ENUMERATE_RENGOKU_RANKING type MsgMhfEnumerateRengokuRanking struct { - AckHandle uint32 - Unk0 uint32 - Unk1 uint16 // Hardcoded 0 in the binary - Unk2 uint16 // Hardcoded 00 01 in the binary + AckHandle uint32 + Leaderboard uint32 + Unk1 uint16 // Hardcoded 0 in the binary + Unk2 uint16 // Hardcoded 00 01 in the binary } // Opcode returns the ID associated with this packet type. @@ -24,7 +24,7 @@ func (m *MsgMhfEnumerateRengokuRanking) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfEnumerateRengokuRanking) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { m.AckHandle = bf.ReadUint32() - m.Unk0 = bf.ReadUint32() + m.Leaderboard = bf.ReadUint32() m.Unk1 = bf.ReadUint16() m.Unk2 = bf.ReadUint16() return nil diff --git a/network/mhfpacket/msg_mhf_enumerate_warehouse.go b/network/mhfpacket/msg_mhf_enumerate_warehouse.go index f567e8bcb..3f1358045 100644 --- a/network/mhfpacket/msg_mhf_enumerate_warehouse.go +++ b/network/mhfpacket/msg_mhf_enumerate_warehouse.go @@ -1,15 +1,19 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfEnumerateWarehouse represents the MSG_MHF_ENUMERATE_WAREHOUSE -type MsgMhfEnumerateWarehouse struct{} +type MsgMhfEnumerateWarehouse struct { + AckHandle uint32 + BoxType string + BoxIndex uint8 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfEnumerateWarehouse) Opcode() network.PacketID { @@ -18,7 +22,17 @@ func (m *MsgMhfEnumerateWarehouse) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfEnumerateWarehouse) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + boxType := bf.ReadUint8() + switch boxType { + case 0: + m.BoxType = "item" + case 1: + m.BoxType = "equip" + } + m.BoxIndex = bf.ReadUint8() + _ = bf.ReadUint16() + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_exchange_weekly_stamp.go b/network/mhfpacket/msg_mhf_exchange_weekly_stamp.go index 6cce19147..918a870ac 100644 --- a/network/mhfpacket/msg_mhf_exchange_weekly_stamp.go +++ b/network/mhfpacket/msg_mhf_exchange_weekly_stamp.go @@ -1,15 +1,20 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfExchangeWeeklyStamp represents the MSG_MHF_EXCHANGE_WEEKLY_STAMP -type MsgMhfExchangeWeeklyStamp struct{} +type MsgMhfExchangeWeeklyStamp struct { + AckHandle uint32 + StampType string + Unk1 uint8 + Unk2 uint16 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfExchangeWeeklyStamp) Opcode() network.PacketID { @@ -18,7 +23,17 @@ func (m *MsgMhfExchangeWeeklyStamp) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfExchangeWeeklyStamp) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + stampType := bf.ReadUint8() + switch stampType { + case 1: + m.StampType = "hl" + case 2: + m.StampType = "ex" + } + m.Unk1 = bf.ReadUint8() + m.Unk2 = bf.ReadUint16() + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_get_achievement.go b/network/mhfpacket/msg_mhf_get_achievement.go index 4b41ce72b..afa49d0d4 100644 --- a/network/mhfpacket/msg_mhf_get_achievement.go +++ b/network/mhfpacket/msg_mhf_get_achievement.go @@ -1,18 +1,18 @@ package mhfpacket import ( - "errors" + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfGetAchievement represents the MSG_MHF_GET_ACHIEVEMENT -type MsgMhfGetAchievement struct{ - AckHandle uint32 - Unk0 uint32 // id? - Unk1 uint32 // char? +type MsgMhfGetAchievement struct { + AckHandle uint32 + CharID uint32 + Unk1 uint32 // char? } // Opcode returns the ID associated with this packet type. @@ -22,8 +22,8 @@ func (m *MsgMhfGetAchievement) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfGetAchievement) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - m.AckHandle = bf.ReadUint32() - m.Unk0 = bf.ReadUint32() + m.AckHandle = bf.ReadUint32() + m.CharID = bf.ReadUint32() m.Unk1 = bf.ReadUint32() return nil } diff --git a/network/mhfpacket/msg_mhf_operate_warehouse.go b/network/mhfpacket/msg_mhf_operate_warehouse.go index 9df4e3770..ba0ee7b78 100644 --- a/network/mhfpacket/msg_mhf_operate_warehouse.go +++ b/network/mhfpacket/msg_mhf_operate_warehouse.go @@ -1,15 +1,22 @@ package mhfpacket -import ( - "errors" +import ( + "errors" + "erupe-ce/common/stringsupport" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfOperateWarehouse represents the MSG_MHF_OPERATE_WAREHOUSE -type MsgMhfOperateWarehouse struct{} +type MsgMhfOperateWarehouse struct { + AckHandle uint32 + Operation uint8 + BoxType string + BoxIndex uint8 + Name string +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfOperateWarehouse) Opcode() network.PacketID { @@ -18,7 +25,20 @@ func (m *MsgMhfOperateWarehouse) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfOperateWarehouse) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Operation = bf.ReadUint8() + boxType := bf.ReadUint8() + switch boxType { + case 0: + m.BoxType = "item" + case 1: + m.BoxType = "equip" + } + m.BoxIndex = bf.ReadUint8() + _ = bf.ReadUint8() // lenName + _ = bf.ReadUint16() // Unk + m.Name = stringsupport.SJISToUTF8(bf.ReadNullTerminatedBytes()) + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_operation_inv_guild.go b/network/mhfpacket/msg_mhf_operation_inv_guild.go index b07311c9d..bcd0b45b8 100644 --- a/network/mhfpacket/msg_mhf_operation_inv_guild.go +++ b/network/mhfpacket/msg_mhf_operation_inv_guild.go @@ -1,15 +1,22 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfOperationInvGuild represents the MSG_MHF_OPERATION_INV_GUILD -type MsgMhfOperationInvGuild struct{} +type MsgMhfOperationInvGuild struct { + AckHandle uint32 + Operation uint8 + ActiveHours uint8 + DaysActive uint8 + PlayStyle uint8 + GuildRequest uint8 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfOperationInvGuild) Opcode() network.PacketID { @@ -18,7 +25,13 @@ func (m *MsgMhfOperationInvGuild) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfOperationInvGuild) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Operation = bf.ReadUint8() + m.ActiveHours = bf.ReadUint8() + m.DaysActive = bf.ReadUint8() + m.PlayStyle = bf.ReadUint8() + m.GuildRequest = bf.ReadUint8() + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_post_cafe_duration_bonus_received.go b/network/mhfpacket/msg_mhf_post_cafe_duration_bonus_received.go index c89159c0c..baa102f61 100644 --- a/network/mhfpacket/msg_mhf_post_cafe_duration_bonus_received.go +++ b/network/mhfpacket/msg_mhf_post_cafe_duration_bonus_received.go @@ -1,15 +1,18 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfPostCafeDurationBonusReceived represents the MSG_MHF_POST_CAFE_DURATION_BONUS_RECEIVED -type MsgMhfPostCafeDurationBonusReceived struct{} +type MsgMhfPostCafeDurationBonusReceived struct { + AckHandle uint32 + CafeBonusID []uint32 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfPostCafeDurationBonusReceived) Opcode() network.PacketID { @@ -18,7 +21,12 @@ func (m *MsgMhfPostCafeDurationBonusReceived) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfPostCafeDurationBonusReceived) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + ids := int(bf.ReadUint32()) + for i := 0; i < ids; i++ { + m.CafeBonusID = append(m.CafeBonusID, bf.ReadUint32()) + } + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_read_mercenary_w.go b/network/mhfpacket/msg_mhf_read_mercenary_w.go index 7cb0117e7..c80afee14 100644 --- a/network/mhfpacket/msg_mhf_read_mercenary_w.go +++ b/network/mhfpacket/msg_mhf_read_mercenary_w.go @@ -1,17 +1,17 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfReadMercenaryW represents the MSG_MHF_READ_MERCENARY_W type MsgMhfReadMercenaryW struct { AckHandle uint32 - Unk0 uint8 + Unk0 bool Unk1 uint8 Unk2 uint16 // Hardcoded 0 in the binary } @@ -24,7 +24,7 @@ func (m *MsgMhfReadMercenaryW) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfReadMercenaryW) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { m.AckHandle = bf.ReadUint32() - m.Unk0 = bf.ReadUint8() + m.Unk0 = bf.ReadBool() m.Unk1 = bf.ReadUint8() m.Unk2 = bf.ReadUint16() return nil diff --git a/network/mhfpacket/msg_mhf_save_mercenary.go b/network/mhfpacket/msg_mhf_save_mercenary.go index a52c973ab..3aa2b0311 100644 --- a/network/mhfpacket/msg_mhf_save_mercenary.go +++ b/network/mhfpacket/msg_mhf_save_mercenary.go @@ -1,18 +1,20 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfSaveMercenary represents the MSG_MHF_SAVE_MERCENARY -type MsgMhfSaveMercenary struct{ - AckHandle uint32 - DataSize uint32 - RawDataPayload []byte +type MsgMhfSaveMercenary struct { + AckHandle uint32 + GCP uint32 + Unk0 uint32 + MercData []byte + Unk1 uint32 } // Opcode returns the ID associated with this packet type. @@ -23,8 +25,11 @@ func (m *MsgMhfSaveMercenary) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfSaveMercenary) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { m.AckHandle = bf.ReadUint32() - m.DataSize = bf.ReadUint32() - m.RawDataPayload = bf.ReadBytes(uint(m.DataSize)) + bf.ReadUint32() // lenData + m.GCP = bf.ReadUint32() + m.Unk0 = bf.ReadUint32() + m.MercData = bf.ReadBytes(uint(bf.ReadUint32())) + m.Unk1 = bf.ReadUint32() return nil } diff --git a/network/mhfpacket/msg_mhf_start_boost_time.go b/network/mhfpacket/msg_mhf_start_boost_time.go index ff424aca9..583ab3ab0 100644 --- a/network/mhfpacket/msg_mhf_start_boost_time.go +++ b/network/mhfpacket/msg_mhf_start_boost_time.go @@ -1,15 +1,18 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfStartBoostTime represents the MSG_MHF_START_BOOST_TIME -type MsgMhfStartBoostTime struct{} +type MsgMhfStartBoostTime struct { + AckHandle uint32 + Unk0 uint32 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfStartBoostTime) Opcode() network.PacketID { @@ -18,7 +21,9 @@ func (m *MsgMhfStartBoostTime) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfStartBoostTime) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Unk0 = bf.ReadUint32() + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_update_warehouse.go b/network/mhfpacket/msg_mhf_update_warehouse.go index ba0321910..962906988 100644 --- a/network/mhfpacket/msg_mhf_update_warehouse.go +++ b/network/mhfpacket/msg_mhf_update_warehouse.go @@ -1,15 +1,28 @@ package mhfpacket -import ( - "errors" - - "erupe-ce/network/clientctx" - "erupe-ce/network" +import ( + "errors" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) +type WarehouseStack struct { + ID uint32 + Index uint16 + EquipType uint16 + ItemID uint16 + Quantity uint16 + Data []byte +} + // MsgMhfUpdateWarehouse represents the MSG_MHF_UPDATE_WAREHOUSE -type MsgMhfUpdateWarehouse struct{} +type MsgMhfUpdateWarehouse struct { + AckHandle uint32 + BoxType string + BoxIndex uint8 + Updates []WarehouseStack +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfUpdateWarehouse) Opcode() network.PacketID { @@ -18,7 +31,37 @@ func (m *MsgMhfUpdateWarehouse) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfUpdateWarehouse) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + boxType := bf.ReadUint8() + switch boxType { + case 0: + m.BoxType = "item" + case 1: + m.BoxType = "equip" + } + m.BoxIndex = bf.ReadUint8() + changes := int(bf.ReadUint16()) + var stackUpdate WarehouseStack + for i := 0; i < changes; i++ { + switch boxType { + case 0: + stackUpdate.ID = bf.ReadUint32() + stackUpdate.Index = bf.ReadUint16() + stackUpdate.ItemID = bf.ReadUint16() + stackUpdate.Quantity = bf.ReadUint16() + _ = bf.ReadUint16() // Unk + m.Updates = append(m.Updates, stackUpdate) + case 1: + stackUpdate.ID = bf.ReadUint32() + stackUpdate.Index = bf.ReadUint16() + stackUpdate.EquipType = bf.ReadUint16() + stackUpdate.ItemID = bf.ReadUint16() + stackUpdate.Data = bf.ReadBytes(56) + m.Updates = append(m.Updates, stackUpdate) + } + } + _ = bf.ReadUint16() + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_sys_update_right.go b/network/mhfpacket/msg_sys_update_right.go index 3a7c95f4e..b343dd0c4 100644 --- a/network/mhfpacket/msg_sys_update_right.go +++ b/network/mhfpacket/msg_sys_update_right.go @@ -37,7 +37,7 @@ type ClientRight struct { // MsgSysUpdateRight represents the MSG_SYS_UPDATE_RIGHT type MsgSysUpdateRight struct { ClientRespAckHandle uint32 // If non-0, requests the client to send back a MSG_SYS_ACK packet with this value. - Unk1 uint32 + Bitfield uint32 Rights []ClientRight UnkSize uint16 // Count of some buf up to 0x800 bytes following it. } @@ -55,7 +55,7 @@ func (m *MsgSysUpdateRight) Parse(bf *byteframe.ByteFrame, ctx *clientctx.Client // Build builds a binary packet from the current data. func (m *MsgSysUpdateRight) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { bf.WriteUint32(m.ClientRespAckHandle) - bf.WriteUint32(m.Unk1) + bf.WriteUint32(m.Bitfield) bf.WriteUint16(uint16(len(m.Rights))) bf.WriteUint16(0) for _, v := range m.Rights { diff --git a/patch-schema/achievements.sql b/patch-schema/achievements.sql new file mode 100644 index 000000000..333ae5ed6 --- /dev/null +++ b/patch-schema/achievements.sql @@ -0,0 +1,41 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS public.achievements +( + id int NOT NULL PRIMARY KEY , + ach0 int DEFAULT 0, + ach1 int DEFAULT 0, + ach2 int DEFAULT 0, + ach3 int DEFAULT 0, + ach4 int DEFAULT 0, + ach5 int DEFAULT 0, + ach6 int DEFAULT 0, + ach7 int DEFAULT 0, + ach8 int DEFAULT 0, + ach9 int DEFAULT 0, + ach10 int DEFAULT 0, + ach11 int DEFAULT 0, + ach12 int DEFAULT 0, + ach13 int DEFAULT 0, + ach14 int DEFAULT 0, + ach15 int DEFAULT 0, + ach16 int DEFAULT 0, + ach17 int DEFAULT 0, + ach18 int DEFAULT 0, + ach19 int DEFAULT 0, + ach20 int DEFAULT 0, + ach21 int DEFAULT 0, + ach22 int DEFAULT 0, + ach23 int DEFAULT 0, + ach24 int DEFAULT 0, + ach25 int DEFAULT 0, + ach26 int DEFAULT 0, + ach27 int DEFAULT 0, + ach28 int DEFAULT 0, + ach29 int DEFAULT 0, + ach30 int DEFAULT 0, + ach31 int DEFAULT 0, + ach32 int DEFAULT 0 +); + +END; \ No newline at end of file diff --git a/patch-schema/festa.sql b/patch-schema/festa.sql new file mode 100644 index 000000000..ce7d03594 --- /dev/null +++ b/patch-schema/festa.sql @@ -0,0 +1,311 @@ +BEGIN; + +CREATE TYPE event_type AS ENUM ('festa', 'diva', 'vs', 'mezfes'); + +DROP TABLE IF EXISTS public.event_week; + +ALTER TABLE IF EXISTS public.guild_characters + ADD COLUMN IF NOT EXISTS souls int DEFAULT 0; + +ALTER TABLE IF EXISTS public.guilds + DROP COLUMN IF EXISTS festival_colour; + +CREATE TABLE IF NOT EXISTS public.events +( + id serial NOT NULL PRIMARY KEY, + event_type event_type NOT NULL, + start_time timestamp without time zone NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS public.festa_registrations +( + guild_id int NOT NULL, + team festival_colour NOT NULL +); + +CREATE TABLE IF NOT EXISTS public.festa_trials +( + id serial NOT NULL PRIMARY KEY, + objective int NOT NULL, + goal_id int NOT NULL, + times_req int NOT NULL, + locale_req int NOT NULL DEFAULT 0, + reward int NOT NULL +); + +CREATE TYPE prize_type AS ENUM ('personal', 'guild'); + +CREATE TABLE IF NOT EXISTS public.festa_prizes +( + id serial NOT NULL PRIMARY KEY, + type prize_type NOT NULL, + tier int NOT NULL, + souls_req int NOT NULL, + item_id int NOT NULL, + num_item int NOT NULL +); + +CREATE TABLE IF NOT EXISTS public.festa_prizes_accepted +( + prize_id int NOT NULL, + character_id int NOT NULL +); + +-- Ripped prizes +INSERT INTO public.festa_prizes + (type, tier, souls_req, item_id, num_item) +VALUES + ('personal', 1, 1, 9647, 7), + ('personal', 2, 1, 9647, 7), + ('personal', 3, 1, 9647, 7), + ('personal', 1, 200, 11284, 4), + ('personal', 2, 200, 11284, 4), + ('personal', 3, 200, 11284, 4), + ('personal', 1, 400, 11381, 3), + ('personal', 2, 400, 11381, 3), + ('personal', 3, 400, 11381, 3), + ('personal', 1, 600, 11284, 8), + ('personal', 2, 600, 11284, 8), + ('personal', 3, 600, 11284, 8), + ('personal', 1, 800, 11384, 3), + ('personal', 2, 800, 11384, 3), + ('personal', 3, 800, 11384, 3), + ('personal', 1, 1000, 11284, 12), + ('personal', 2, 1000, 11284, 12), + ('personal', 3, 1000, 11284, 12), + ('personal', 1, 1200, 11381, 5), + ('personal', 2, 1200, 11381, 5), + ('personal', 3, 1200, 11381, 5), + ('personal', 1, 1400, 11284, 16), + ('personal', 2, 1400, 11284, 16), + ('personal', 3, 1400, 11284, 16), + ('personal', 1, 1700, 11384, 5), + ('personal', 2, 1700, 11384, 5), + ('personal', 3, 1700, 11384, 5), + ('personal', 1, 2000, 11284, 16), + ('personal', 2, 2000, 11284, 16), + ('personal', 3, 2000, 11284, 16), + ('personal', 1, 2500, 11382, 4), + ('personal', 2, 2500, 11382, 4), + ('personal', 3, 2500, 11382, 4), + ('personal', 1, 3000, 11284, 24), + ('personal', 2, 3000, 11284, 24), + ('personal', 3, 3000, 11284, 24), + ('personal', 1, 4000, 11385, 4), + ('personal', 2, 4000, 11385, 4), + ('personal', 3, 4000, 11385, 4), + ('personal', 1, 5000, 11381, 11), + ('personal', 2, 5000, 11381, 11), + ('personal', 3, 5000, 11381, 11), + ('personal', 1, 6000, 5177, 5), + ('personal', 2, 6000, 5177, 5), + ('personal', 3, 6000, 5177, 5), + ('personal', 1, 7000, 11384, 11), + ('personal', 2, 7000, 11384, 11), + ('personal', 3, 7000, 11384, 11), + ('personal', 1, 10000, 11382, 8), + ('personal', 2, 10000, 11382, 8), + ('personal', 3, 10000, 11382, 8), + ('personal', 1, 15000, 11385, 4), + ('personal', 2, 15000, 11385, 4), + ('personal', 3, 15000, 11385, 4), + ('personal', 1, 20000, 11381, 13), + ('personal', 2, 20000, 11381, 13), + ('personal', 3, 20000, 11381, 13), + ('personal', 1, 25000, 11385, 4), + ('personal', 2, 25000, 11385, 4), + ('personal', 3, 25000, 11385, 4), + ('personal', 1, 30000, 11383, 1), + ('personal', 2, 30000, 11383, 1), + ('personal', 3, 30000, 11383, 1); + +INSERT INTO public.festa_prizes +(type, tier, souls_req, item_id, num_item) +VALUES + ('guild', 1, 100, 7468, 5), + ('guild', 2, 100, 7468, 5), + ('guild', 3, 100, 7465, 5), + ('guild', 1, 300, 7469, 5), + ('guild', 2, 300, 7469, 5), + ('guild', 3, 300, 7466, 5), + ('guild', 1, 700, 7470, 5), + ('guild', 2, 700, 7470, 5), + ('guild', 3, 700, 7467, 5), + ('guild', 1, 1500, 13405, 14), + ('guild', 1, 1500, 1520, 3), + ('guild', 2, 1500, 13405, 14), + ('guild', 2, 1500, 1520, 3), + ('guild', 3, 1500, 7011, 3), + ('guild', 3, 1500, 13405, 14), + ('guild', 1, 3000, 10201, 10), + ('guild', 2, 3000, 10201, 10), + ('guild', 3, 3000, 10201, 10), + ('guild', 1, 6000, 13895, 14), + ('guild', 1, 6000, 1520, 6), + ('guild', 2, 6000, 13895, 14), + ('guild', 2, 6000, 1520, 6), + ('guild', 3, 6000, 13895, 14), + ('guild', 3, 6000, 7011, 4), + ('guild', 1, 12000, 13406, 14), + ('guild', 1, 12000, 1520, 9), + ('guild', 2, 12000, 13406, 14), + ('guild', 2, 12000, 1520, 9), + ('guild', 3, 12000, 13406, 14), + ('guild', 3, 12000, 7011, 5), + ('guild', 1, 25000, 10207, 10), + ('guild', 2, 25000, 10207, 10), + ('guild', 3, 25000, 10207, 10), + ('guild', 1, 50000, 1520, 12), + ('guild', 1, 50000, 13896, 14), + ('guild', 2, 50000, 1520, 12), + ('guild', 2, 50000, 13896, 14), + ('guild', 3, 50000, 7011, 6), + ('guild', 3, 50000, 13896, 14), + ('guild', 1, 100000, 10201, 10), + ('guild', 2, 100000, 10201, 10), + ('guild', 3, 100000, 10201, 10), + ('guild', 1, 200000, 13406, 16), + ('guild', 2, 200000, 13406, 16), + ('guild', 3, 200000, 13406, 16), + ('guild', 1, 300000, 13896, 16), + ('guild', 2, 300000, 13896, 16), + ('guild', 3, 300000, 13896, 16), + ('guild', 1, 400000, 10207, 10), + ('guild', 2, 400000, 10207, 10), + ('guild', 3, 400000, 10207, 10), + ('guild', 1, 500000, 13407, 6), + ('guild', 1, 500000, 13897, 6), + ('guild', 2, 500000, 13407, 6), + ('guild', 2, 500000, 13897, 6), + ('guild', 3, 500000, 13407, 6), + ('guild', 3, 500000, 13897, 6); + +-- Ripped trials +INSERT INTO public.festa_trials + (objective, goal_id, times_req, locale_req, reward) +VALUES + (1,27,1,0,1), + (5,53034,0,0,400), + (5,22042,0,0,89), + (5,23397,0,0,89), + (1,28,1,0,1), + (1,68,1,0,1), + (1,6,1,0,2), + (1,38,1,0,2), + (1,20,1,0,3), + (1,39,1,0,4), + (1,48,1,0,4), + (1,67,1,0,4), + (1,93,1,0,4), + (1,22,1,0,5), + (1,52,1,0,5), + (1,101,1,0,5), + (1,1,1,0,5), + (1,37,1,0,5), + (1,15,1,0,5), + (1,45,1,0,5), + (1,74,1,0,5), + (1,78,1,0,5), + (1,103,1,0,5), + (1,51,1,0,6), + (1,17,1,0,6), + (1,21,1,0,6), + (1,92,1,0,6), + (1,47,1,0,7), + (1,46,1,0,7), + (1,26,1,0,7), + (1,14,1,0,7), + (1,11,1,0,7), + (1,44,1,0,8), + (1,43,1,0,8), + (1,49,1,0,8), + (1,40,1,0,8), + (1,76,1,0,8), + (1,89,1,0,8), + (1,94,1,0,8), + (1,96,1,0,8), + (1,75,1,0,8), + (1,91,1,0,8), + (1,53,1,0,9), + (1,80,1,0,9), + (1,42,1,0,9), + (1,79,1,0,9), + (1,81,1,0,10), + (1,41,1,0,10), + (1,82,1,0,10), + (1,90,1,0,10), + (1,149,1,0,10), + (1,85,1,0,11), + (1,95,1,0,11), + (1,121,1,0,11), + (1,142,1,0,11), + (1,141,1,0,11), + (1,146,1,0,12), + (1,147,1,0,12), + (1,148,1,0,12), + (1,151,1,0,12), + (1,152,1,0,12), + (1,159,1,0,12), + (1,153,1,0,12), + (1,162,1,0,12), + (1,111,1,0,13), + (1,110,1,0,13), + (1,112,1,0,13), + (1,109,1,0,14), + (1,169,1,0,15), + (2,33,1,0,6), + (2,104,1,0,8), + (2,119,1,0,8), + (2,120,1,0,8), + (2,54,1,0,8), + (2,59,1,0,8), + (2,64,1,0,8), + (2,65,1,0,8), + (2,99,1,0,9), + (2,83,1,0,9), + (2,84,1,0,10), + (2,77,1,0,10), + (2,106,1,0,10), + (2,55,1,0,10), + (2,58,1,0,10), + (2,7,1,0,10), + (2,50,1,0,11), + (2,131,1,0,11), + (2,129,1,0,11), + (2,140,1,0,11), + (2,122,1,0,11), + (2,126,1,0,11), + (2,127,1,0,11), + (2,128,1,0,11), + (2,130,1,0,11), + (2,139,1,0,11), + (2,144,1,0,11), + (2,150,1,0,11), + (2,158,1,0,11), + (2,164,1,0,15), + (2,165,1,0,15), + (2,2,1,7,15), + (2,36,1,0,15), + (2,71,1,0,15), + (2,108,1,0,15), + (2,116,1,0,15), + (2,107,1,0,15), + (2,154,1,0,17), + (2,166,1,0,17), + (2,170,1,0,18), + (3,31,1,0,1), + (3,8,1,0,3), + (3,123,1,0,8), + (3,105,1,0,9), + (3,125,1,0,11), + (3,115,1,0,12), + (3,114,1,0,12), + (3,161,1,0,12), + (4,670,1,0,1), + (4,671,1,0,1), + (4,672,1,0,1), + (4,675,1,0,1), + (4,673,1,0,1), + (4,674,1,0,1); + +END; \ No newline at end of file diff --git a/patch-schema/mail-system-messages.sql b/patch-schema/mail-system-messages.sql new file mode 100644 index 000000000..4ce8dfaf6 --- /dev/null +++ b/patch-schema/mail-system-messages.sql @@ -0,0 +1,13 @@ +BEGIN; + +ALTER TABLE IF EXISTS public.mail + ADD COLUMN IF NOT EXISTS is_sys_message bool DEFAULT false; + +UPDATE mail SET is_sys_message=false; + +ALTER TABLE IF EXISTS public.mail + DROP CONSTRAINT IF EXISTS mail_sender_id_fkey; + +INSERT INTO public.characters (id, name) VALUES (0, ''); + +END; \ No newline at end of file diff --git a/patch-schema/mercenary.sql b/patch-schema/mercenary.sql new file mode 100644 index 000000000..9000db9f3 --- /dev/null +++ b/patch-schema/mercenary.sql @@ -0,0 +1,7 @@ +BEGIN; + +CREATE SEQUENCE IF NOT EXISTS public.rasta_id_seq; + +UPDATE characters SET savemercenary=NULL; + +END; \ No newline at end of file diff --git a/patch-schema/netcafe.sql b/patch-schema/netcafe.sql new file mode 100644 index 000000000..e284742ce --- /dev/null +++ b/patch-schema/netcafe.sql @@ -0,0 +1,40 @@ +BEGIN; + +ALTER TABLE IF EXISTS public.characters + ADD COLUMN IF NOT EXISTS cafe_time integer DEFAULT 0; + +ALTER TABLE IF EXISTS public.characters + DROP COLUMN IF EXISTS netcafe_points; + +ALTER TABLE IF EXISTS public.characters + ADD COLUMN IF NOT EXISTS netcafe_points int DEFAULT 0; + +ALTER TABLE IF EXISTS public.characters + ADD COLUMN IF NOT EXISTS boost_time timestamp without time zone; + +CREATE TABLE IF NOT EXISTS public.cafebonus +( + id serial NOT NULL PRIMARY KEY, + time_req integer NOT NULL, + item_type integer NOT NULL, + item_id integer NOT NULL, + quantity integer NOT NULL +); + +CREATE TABLE IF NOT EXISTS public.cafe_accepted +( + cafe_id integer NOT NULL, + character_id integer NOT NULL +); + +INSERT INTO public.cafebonus (time_req, item_type, item_id, quantity) +VALUES + (1800, 17, 0, 250), + (3600, 17, 0, 500), + (7200, 17, 0, 1000), + (10800, 17, 0, 1500), + (18000, 17, 0, 1750), + (28800, 17, 0, 3000), + (43200, 17, 0, 4000); + +END; \ No newline at end of file diff --git a/patch-schema/return.sql b/patch-schema/return.sql new file mode 100644 index 000000000..4e09d7e47 --- /dev/null +++ b/patch-schema/return.sql @@ -0,0 +1,9 @@ +BEGIN; + +ALTER TABLE IF EXISTS public.users + ADD COLUMN IF NOT EXISTS last_login timestamp without time zone; + +ALTER TABLE IF EXISTS public.users + ADD COLUMN IF NOT EXISTS return_expires timestamp without time zone; + +END; \ No newline at end of file diff --git a/patch-schema/road-leaderboard.sql b/patch-schema/road-leaderboard.sql new file mode 100644 index 000000000..8dfea2875 --- /dev/null +++ b/patch-schema/road-leaderboard.sql @@ -0,0 +1,11 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS rengoku_score ( + character_id integer PRIMARY KEY, + max_stages_mp integer, + max_points_mp integer, + max_stages_sp integer, + max_points_sp integer +); + +END; \ No newline at end of file diff --git a/patch-schema/stamps.sql b/patch-schema/stamps.sql new file mode 100644 index 000000000..2b940fa8c --- /dev/null +++ b/patch-schema/stamps.sql @@ -0,0 +1,13 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS public.stamps ( + character_id integer PRIMARY KEY, + hl_total integer DEFAULT 0, + hl_redeemed integer DEFAULT 0, + hl_next timestamp without time zone, + ex_total integer DEFAULT 0, + ex_redeemed integer DEFAULT 0, + ex_next timestamp without time zone +); + +END; \ No newline at end of file diff --git a/patch-schema/titles.sql b/patch-schema/titles.sql new file mode 100644 index 000000000..e4a87dc86 --- /dev/null +++ b/patch-schema/titles.sql @@ -0,0 +1,11 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS public.titles +( + id int NOT NULL, + char_id int NOT NULL, + unlocked_at timestamp without time zone, + updated_at timestamp without time zone +); + +END; \ No newline at end of file diff --git a/patch-schema/warehouse.sql b/patch-schema/warehouse.sql new file mode 100644 index 000000000..2f2a5adde --- /dev/null +++ b/patch-schema/warehouse.sql @@ -0,0 +1,49 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS public.warehouse ( + character_id integer PRIMARY KEY, + item0 bytea, + item1 bytea, + item2 bytea, + item3 bytea, + item4 bytea, + item5 bytea, + item6 bytea, + item7 bytea, + item8 bytea, + item9 bytea, + item10 bytea, + item0name text, + item1name text, + item2name text, + item3name text, + item4name text, + item5name text, + item6name text, + item7name text, + item8name text, + item9name text, + equip0 bytea, + equip1 bytea, + equip2 bytea, + equip3 bytea, + equip4 bytea, + equip5 bytea, + equip6 bytea, + equip7 bytea, + equip8 bytea, + equip9 bytea, + equip10 bytea, + equip0name text, + equip1name text, + equip2name text, + equip3name text, + equip4name text, + equip5name text, + equip6name text, + equip7name text, + equip8name text, + equip9name text +); + +END; \ No newline at end of file diff --git a/server/channelserver/handlers.go b/server/channelserver/handlers.go index 1cfe9e3ff..6c49ba0af 100644 --- a/server/channelserver/handlers.go +++ b/server/channelserver/handlers.go @@ -1,25 +1,21 @@ package channelserver import ( - "bytes" "encoding/binary" "encoding/hex" "erupe-ce/common/stringsupport" "fmt" "io" + "math" "net" "strings" - - "io/ioutil" - "math/bits" - "math/rand" "time" "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" "go.uber.org/zap" - "golang.org/x/text/encoding/japanese" - "golang.org/x/text/transform" + "math/bits" + "math/rand" ) // Temporary function to just return no results for a MSG_MHF_ENUMERATE* packet @@ -78,40 +74,32 @@ func doAckSimpleFail(s *Session, ackHandle uint32, data []byte) { } func updateRights(s *Session) { + s.rights = uint32(0x0E) + s.server.db.QueryRow("SELECT rights FROM users u INNER JOIN characters c ON u.id = c.user_id WHERE c.id = $1", s.charID).Scan(&s.rights) + + rights := make([]mhfpacket.ClientRight, 0) + tempRights := s.rights + for i := 30; i > 0; i-- { + right := uint32(math.Pow(2, float64(i))) + if tempRights-right < 0x80000000 { + if i == 1 { + continue + } + rights = append(rights, mhfpacket.ClientRight{ID: uint16(i), Timestamp: 0x70DB59F0}) + tempRights -= right + } + } + rights = append(rights, mhfpacket.ClientRight{ID: 1, Timestamp: 0}) + update := &mhfpacket.MsgSysUpdateRight{ ClientRespAckHandle: 0, - Unk1: s.rights, - Rights: []mhfpacket.ClientRight{ - { - ID: 1, - Timestamp: 0, - }, - { - ID: 2, - Timestamp: 0x5FEA1781, - }, - { - ID: 3, - Timestamp: 0x5FEA1781, - }, - }, - UnkSize: 0, + Bitfield: s.rights, + Rights: rights, + UnkSize: 0, } s.QueueSendMHF(update) } -func fixedSizeShiftJIS(text string, size int) []byte { - r := bytes.NewBuffer([]byte(text)) - encoded, err := ioutil.ReadAll(transform.NewReader(r, japanese.ShiftJIS.NewEncoder())) - if err != nil { - panic(err) - } - - out := make([]byte, size) - copy(out, encoded) - out[len(out)-1] = 0 - return out -} func handleMsgHead(s *Session, p mhfpacket.MHFPacket) {} func handleMsgSysExtendThreshold(s *Session, p mhfpacket.MHFPacket) { @@ -149,14 +137,11 @@ func handleMsgSysLogin(s *Session, p mhfpacket.MHFPacket) { } } - rights := uint32(0x0E) - s.server.db.QueryRow("SELECT rights FROM users u INNER JOIN characters c ON u.id = c.user_id WHERE c.id = $1", pkt.CharID0).Scan(&rights) - s.Lock() s.charID = pkt.CharID0 - s.rights = rights s.token = pkt.LoginTokenString s.Unlock() + updateRights(s) bf := byteframe.NewByteFrame() bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // Unix timestamp @@ -192,8 +177,12 @@ func handleMsgSysLogout(s *Session, p mhfpacket.MHFPacket) { } func logoutPlayer(s *Session) { - delete(s.server.sessions, s.rawConn) - s.rawConn.Close() + if _, exists := s.server.sessions[s.rawConn]; exists { + delete(s.server.sessions, s.rawConn) + s.rawConn.Close() + } else { + return // Prevent re-running logout logic on real logouts + } _, err := s.server.db.Exec("UPDATE sign_sessions SET server_id=NULL, char_id=NULL WHERE token=$1", s.token) if err != nil { @@ -206,13 +195,13 @@ func logoutPlayer(s *Session) { } var timePlayed int + var sessionTime int _ = s.server.db.QueryRow("SELECT time_played FROM characters WHERE id = $1", s.charID).Scan(&timePlayed) - - timePlayed = (int(Time_Current_Adjusted().Unix()) - int(s.sessionStart)) + timePlayed + sessionTime = int(Time_Current_Adjusted().Unix()) - int(s.sessionStart) + timePlayed += sessionTime var rpGained int - - if s.rights > 0x40000000 { // N Course + if s.rights >= 0x40000000 { // N Course rpGained = timePlayed / 900 timePlayed = timePlayed % 900 } else { @@ -220,10 +209,10 @@ func logoutPlayer(s *Session) { timePlayed = timePlayed % 1800 } - _, err = s.server.db.Exec("UPDATE characters SET time_played = $1 WHERE id = $2", timePlayed, s.charID) - if err != nil { - panic(err) - } + s.server.db.Exec("UPDATE characters SET time_played = $1 WHERE id = $2", timePlayed, s.charID) + s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID) + + treasureHuntUnregister(s) if s.stage == nil { return @@ -243,7 +232,6 @@ func logoutPlayer(s *Session) { removeSessionFromSemaphore(s) removeSessionFromStage(s) - treasureHuntUnregister(s) saveData, err := GetCharacterSaveData(s, s.charID) if err != nil { @@ -421,6 +409,9 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { } for session := range stage.clients { count++ + hrp := uint16(1) + gr := uint16(0) + s.server.db.QueryRow("SELECT hrp, gr FROM characters WHERE id=$1", session.charID).Scan(&hrp, &gr) sessionStage := stringsupport.UTF8ToSJIS(session.stageID) sessionName := stringsupport.UTF8ToSJIS(session.Name) resp.WriteUint32(binary.LittleEndian.Uint32(net.ParseIP(c.IP).To4())) @@ -433,8 +424,8 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { resp.WriteBytes(make([]byte, 48)) resp.WriteNullTerminatedBytes(sessionStage) resp.WriteNullTerminatedBytes(sessionName) - resp.WriteUint16(999) // HR - resp.WriteUint16(999) // GR + resp.WriteUint16(hrp) + resp.WriteUint16(gr) resp.WriteBytes([]byte{0x06, 0x10, 0x00}) // Unk } } @@ -443,12 +434,23 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { } case 4: // Find Party bf := byteframe.NewByteFrameFromBytes(pkt.MessageData) - bf.ReadUint8() + setting := bf.ReadUint8() maxResults := bf.ReadUint16() - bf.ReadUint8() - bf.ReadUint8() + bf.Seek(2, 1) partyType := bf.ReadUint16() - _ = bf.DataFromCurrent() // Restrictions + rankRestriction := uint16(0) + if setting >= 2 { + bf.Seek(2, 1) + rankRestriction = bf.ReadUint16() + } + targets := make([]uint16, 4) + if setting >= 3 { + bf.Seek(1, 1) + lenTargets := int(bf.ReadUint8()) + for i := 0; i < lenTargets; i++ { + targets[i] = bf.ReadUint16() + } + } var stagePrefix string switch partyType { case 0: // Public Bar @@ -468,31 +470,41 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { break } if strings.HasPrefix(stage.id, stagePrefix) { + sb3 := byteframe.NewByteFrameFromBytes(stage.rawBinaryData[stageBinaryKey{1, 3}]) + sb3.Seek(4, 0) + stageRankRestriction := sb3.ReadUint16() + stageTarget := sb3.ReadUint16() + if rankRestriction != 0xFFFF && stageRankRestriction < rankRestriction { + continue + } count++ sessionStage := stringsupport.UTF8ToSJIS(stage.id) resp.WriteUint32(binary.LittleEndian.Uint32(net.ParseIP(c.IP).To4())) resp.WriteUint16(c.Port) - - // TODO: This is half right, could be trimmed - resp.WriteUint16(0) - resp.WriteUint16(uint16(len(stage.clients))) + resp.WriteUint16(0) // Static? + resp.WriteUint16(0) // Unk resp.WriteUint16(uint16(len(stage.clients))) resp.WriteUint16(stage.maxPlayers) - resp.WriteUint16(0) - resp.WriteUint16(uint16(len(stage.clients))) - // - - resp.WriteUint16(uint16(len(sessionStage) + 1)) - resp.WriteUint8(1) + resp.WriteUint16(0) // Num clients entered from stage + resp.WriteUint16(stage.maxPlayers) + resp.WriteUint8(1) // Static? + resp.WriteUint8(uint8(len(sessionStage) + 1)) + resp.WriteUint8(uint8(len(stage.rawBinaryData[stageBinaryKey{1, 0}]))) resp.WriteUint8(uint8(len(stage.rawBinaryData[stageBinaryKey{1, 1}]))) - resp.WriteBytes(make([]byte, 16)) + resp.WriteUint16(stageRankRestriction) + resp.WriteUint16(stageTarget) + resp.WriteBytes(make([]byte, 12)) resp.WriteNullTerminatedBytes(sessionStage) - resp.WriteBytes([]byte{0x00}) + resp.WriteBytes(stage.rawBinaryData[stageBinaryKey{1, 0}]) resp.WriteBytes(stage.rawBinaryData[stageBinaryKey{1, 1}]) } } } } + if (pkt.SearchType == 1 || pkt.SearchType == 3) && count == 0 { + doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) + return + } resp.Seek(0, io.SeekStart) resp.WriteUint16(count) doAckBufSucceed(s, pkt.AckHandle, resp.Data()) @@ -500,8 +512,6 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { func handleMsgCaExchangeItem(s *Session, p mhfpacket.MHFPacket) {} -func handleMsgMhfResetTitle(s *Session, p mhfpacket.MHFPacket) {} - func handleMsgMhfPresentBox(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfServerCommand(s *Session, p mhfpacket.MHFPacket) {} @@ -556,8 +566,6 @@ func handleMsgMhfEnumerateOrder(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetExtraInfo(s *Session, p mhfpacket.MHFPacket) {} -func handleMsgMhfAcquireTitle(s *Session, p mhfpacket.MHFPacket) {} - func handleMsgMhfEnumerateUnionItem(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateUnionItem) var boxContents []byte @@ -645,86 +653,57 @@ func handleMsgMhfUpdateUnionItem(s *Session, p mhfpacket.MHFPacket) { doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } -func handleMsgMhfAcquireCafeItem(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfAcquireCafeItem) - var netcafe_points int - err := s.server.db.QueryRow("UPDATE characters SET netcafe_points = netcafe_points - $1 WHERE id = $2 RETURNING netcafe_points", pkt.PointCost, s.charID).Scan(&netcafe_points) - if err != nil { - s.logger.Fatal("Failed to get plate data savedata from db", zap.Error(err)) - } - resp := byteframe.NewByteFrame() - resp.WriteUint32(uint32(netcafe_points)) - doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) -} - -func handleMsgMhfUpdateCafepoint(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfUpdateCafepoint) - var netcafe_points int - err := s.server.db.QueryRow("SELECT COALESCE(netcafe_points, 0) FROM characters WHERE id = $1", s.charID).Scan(&netcafe_points) - if err != nil { - s.logger.Fatal("Failed to get plate data savedata from db", zap.Error(err)) - } - resp := byteframe.NewByteFrame() - resp.WriteUint32(0) - resp.WriteUint32(uint32(netcafe_points)) - doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) -} - -func handleMsgMhfCheckDailyCafepoint(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfCheckDailyCafepoint) - - // I am not sure exactly what this does, but all responses I have seen include this exact sequence of bytes - // 1 daily, 5 daily halk pots, 3 point boosted quests, also adds 5 netcafe points but not sent to client - // available once after midday every day - - // get next midday - var t = Time_static() - year, month, day := t.Date() - midday := time.Date(year, month, day, 12, 0, 0, 0, t.Location()) - if t.After(midday) { - midday = midday.Add(24 * time.Hour) - } - - // get time after which daily claiming would be valid from db - var dailyTime time.Time - err := s.server.db.QueryRow("SELECT COALESCE(daily_time, $2) FROM characters WHERE id = $1", s.charID, time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)).Scan(&dailyTime) - if err != nil { - s.logger.Fatal("Failed to get daily_time savedata from db", zap.Error(err)) - } - - if t.After(dailyTime) { - // +5 netcafe points and setting next valid window - _, err := s.server.db.Exec("UPDATE characters SET daily_time=$1, netcafe_points=netcafe_points::int + 5 WHERE id=$2", midday, s.charID) - if err != nil { - s.logger.Fatal("Failed to update daily_time and netcafe_points savedata in db", zap.Error(err)) - } - doAckBufSucceed(s, pkt.AckHandle, []byte{0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01}) - } else { - doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) - } -} - func handleMsgMhfGetCogInfo(s *Session, p mhfpacket.MHFPacket) {} -func handleMsgMhfCheckMonthlyItem(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfAcquireMonthlyItem(s *Session, p mhfpacket.MHFPacket) {} - func handleMsgMhfCheckWeeklyStamp(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfCheckWeeklyStamp) - - resp := byteframe.NewByteFrame() - resp.WriteUint16(0x000E) - resp.WriteUint16(0x0001) - resp.WriteUint16(0x0000) - resp.WriteUint16(0x0000) // 0x0000 stops the vaguely annoying log in pop up - resp.WriteUint32(0) - resp.WriteUint32(0x5dddcbb3) // Timestamp - - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) + weekCurrentStart := TimeWeekStart() + weekNextStart := TimeWeekNext() + var total, redeemed, updated uint16 + var nextClaim time.Time + err := s.server.db.QueryRow(fmt.Sprintf("SELECT %s_next FROM stamps WHERE character_id=$1", pkt.StampType), s.charID).Scan(&nextClaim) + if err != nil { + s.server.db.Exec("INSERT INTO stamps (character_id, hl_next, ex_next) VALUES ($1, $2, $2)", s.charID, weekNextStart) + nextClaim = weekNextStart + } + if nextClaim.Before(weekCurrentStart) { + s.server.db.Exec(fmt.Sprintf("UPDATE stamps SET %s_total=%s_total+1, %s_next=$1 WHERE character_id=$2", pkt.StampType, pkt.StampType, pkt.StampType), weekNextStart, s.charID) + updated = 1 + } + s.server.db.QueryRow(fmt.Sprintf("SELECT %s_total, %s_redeemed FROM stamps WHERE character_id=$1", pkt.StampType, pkt.StampType), s.charID).Scan(&total, &redeemed) + bf := byteframe.NewByteFrame() + bf.WriteUint16(total) + bf.WriteUint16(redeemed) + bf.WriteUint16(updated) + bf.WriteUint32(0) // Unk + bf.WriteUint32(uint32(weekCurrentStart.Unix())) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } -func handleMsgMhfExchangeWeeklyStamp(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfExchangeWeeklyStamp(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfExchangeWeeklyStamp) + var total, redeemed uint16 + var tktStack mhfpacket.WarehouseStack + if pkt.Unk1 == 0xA { // Yearly Sub Ex + s.server.db.QueryRow("UPDATE stamps SET hl_total=hl_total-48, hl_redeemed=hl_redeemed-48 WHERE character_id=$1 RETURNING hl_total, hl_redeemed", s.charID).Scan(&total, &redeemed) + tktStack = mhfpacket.WarehouseStack{ItemID: 0x08A2, Quantity: 1} + } else { + s.server.db.QueryRow(fmt.Sprintf("UPDATE stamps SET %s_redeemed=%s_redeemed+8 WHERE character_id=$1 RETURNING %s_total, %s_redeemed", pkt.StampType, pkt.StampType, pkt.StampType, pkt.StampType), s.charID).Scan(&total, &redeemed) + if pkt.StampType == "hl" { + tktStack = mhfpacket.WarehouseStack{ItemID: 0x065E, Quantity: 5} + } else { + tktStack = mhfpacket.WarehouseStack{ItemID: 0x065F, Quantity: 5} + } + } + addWarehouseGift(s, "item", tktStack) + bf := byteframe.NewByteFrame() + bf.WriteUint16(total) + bf.WriteUint16(redeemed) + bf.WriteUint16(0) + bf.WriteUint32(0) // Unk, but has possible values + bf.WriteUint32(uint32(TimeWeekStart().Unix())) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} func getGookData(s *Session, cid uint32) (uint16, []byte) { var data []byte diff --git a/server/channelserver/handlers_achievement.go b/server/channelserver/handlers_achievement.go index 7887ab71d..5db3853e1 100644 --- a/server/channelserver/handlers_achievement.go +++ b/server/channelserver/handlers_achievement.go @@ -3,86 +3,139 @@ package channelserver import ( "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" + "fmt" + "io" ) +var achievementCurves = [][]int32{ + // 0: HR weapon use, Class use, Tore dailies + {5, 15, 30, 50, 100, 150, 200, 300}, + // 1: Weapon collector, G wep enhances + {1, 5, 10, 15, 30, 50, 75, 100}, + // 2: Festa wins + {1, 2, 3, 4, 5, 6, 7, 8}, + // 3: GR weapon use, Sigil crafts + {10, 50, 100, 200, 350, 500, 750, 999}, +} + +var achievementCurveMap = map[uint8][]int32{ + 0: achievementCurves[0], 1: achievementCurves[0], 2: achievementCurves[0], 3: achievementCurves[0], + 4: achievementCurves[0], 5: achievementCurves[0], 6: achievementCurves[0], 7: achievementCurves[1], + 8: achievementCurves[2], 9: achievementCurves[0], 10: achievementCurves[0], 11: achievementCurves[0], + 12: achievementCurves[0], 13: achievementCurves[0], 14: achievementCurves[0], 15: achievementCurves[0], + 16: achievementCurves[3], 17: achievementCurves[3], 18: achievementCurves[3], 19: achievementCurves[3], + 20: achievementCurves[3], 21: achievementCurves[3], 22: achievementCurves[3], 23: achievementCurves[3], + 24: achievementCurves[3], 25: achievementCurves[3], 26: achievementCurves[3], 27: achievementCurves[1], + 28: achievementCurves[1], 29: achievementCurves[3], 30: achievementCurves[3], 31: achievementCurves[3], + 32: achievementCurves[3], +} + +type Achievement struct { + Level uint8 + Value uint32 + NextValue uint16 + Required uint32 + Updated bool + Progress uint32 + Trophy uint8 +} + +func GetAchData(id uint8, score int32) Achievement { + curve := achievementCurveMap[id] + var ach Achievement + for i, v := range curve { + temp := score - v + if temp < 0 { + ach.Progress = uint32(score) + ach.Required = uint32(curve[i]) + switch ach.Level { + case 0: + ach.NextValue = 5 + case 1, 2, 3: + ach.NextValue = 10 + case 4, 5: + ach.NextValue = 15 + case 6: + ach.NextValue = 15 + ach.Trophy = 0x40 + case 7: + ach.NextValue = 20 + ach.Trophy = 0x60 + } + return ach + } else { + score = temp + ach.Level++ + switch ach.Level { + case 1: + ach.Value += 5 + case 2, 3, 4: + ach.Value += 10 + case 5, 6, 7: + ach.Value += 15 + case 8: + ach.Value += 20 + } + } + } + ach.Required = uint32(curve[7]) + ach.Trophy = 0x7F + ach.Progress = ach.Required + return ach +} + func handleMsgMhfGetAchievement(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetAchievement) - achievementStruct := []struct { - ID uint8 // Main ID - Unk0 uint8 // always FF - Unk1 uint16 // 0x05 0x00 - Unk2 uint32 // 0x01 0x0A 0x05 0x00 - }{ - {ID: 0, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 1, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 2, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 3, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 4, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 5, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 6, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 7, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 8, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 9, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 10, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 11, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 12, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 13, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 14, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 15, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 16, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 17, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 18, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 19, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 20, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 21, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 22, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 23, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 24, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 25, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 26, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 27, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 28, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 29, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 30, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 31, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 32, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 33, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 34, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 35, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 36, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 37, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 38, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 39, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 40, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 41, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 42, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 43, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 44, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 45, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 46, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 47, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 48, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 49, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 50, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 51, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 52, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 53, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 54, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 55, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 56, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 57, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 58, Unk0: 0xFF, Unk1: 0, Unk2: 0}, - {ID: 59, Unk0: 0xFF, Unk1: 0, Unk2: 0}, + var exists int + err := s.server.db.QueryRow("SELECT id FROM achievements WHERE id=$1", pkt.CharID).Scan(&exists) + if err != nil { + s.server.db.Exec("INSERT INTO achievements (id) VALUES ($1)", pkt.CharID) } + + var scores [33]int32 + err = s.server.db.QueryRow("SELECT * FROM achievements WHERE id=$1", pkt.CharID).Scan(&scores[0], + &scores[0], &scores[1], &scores[2], &scores[3], &scores[4], &scores[5], &scores[6], &scores[7], &scores[8], + &scores[9], &scores[10], &scores[11], &scores[12], &scores[13], &scores[14], &scores[15], &scores[16], + &scores[17], &scores[18], &scores[19], &scores[20], &scores[21], &scores[22], &scores[23], &scores[24], + &scores[25], &scores[26], &scores[27], &scores[28], &scores[29], &scores[30], &scores[31], &scores[32]) + if err != nil { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 20)) + return + } + resp := byteframe.NewByteFrame() - resp.WriteUint8(uint8(len(achievementStruct))) // Entry count - for _, entry := range achievementStruct { - resp.WriteUint8(entry.ID) - resp.WriteUint8(entry.Unk0) - resp.WriteUint16(entry.Unk1) - resp.WriteUint32(entry.Unk2) + var points uint32 + resp.WriteBytes(make([]byte, 16)) + resp.WriteBytes([]byte{0x02, 0x00, 0x00}) // Unk + + var id uint8 + entries := uint8(33) + resp.WriteUint8(entries) // Entry count + for id = 0; id < entries; id++ { + achData := GetAchData(id, scores[id]) + points += achData.Value + resp.WriteUint8(id) + resp.WriteUint8(achData.Level) + resp.WriteUint16(achData.NextValue) + resp.WriteUint32(achData.Required) + resp.WriteBool(false) // TODO: Notify on rank increase since last checked, see MhfDisplayedAchievement + resp.WriteUint8(achData.Trophy) + /* Trophy bitfield + 0000 0000 + abcd efgh + B - Bronze (0x40) + B-C - Silver (0x60) + B-H - Gold (0x7F) + */ + resp.WriteUint16(0) // Unk + resp.WriteUint32(achData.Progress) } + resp.Seek(0, io.SeekStart) + resp.WriteUint32(points) + resp.WriteUint32(points) + resp.WriteUint32(points) + resp.WriteUint32(points) doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } @@ -93,11 +146,23 @@ func handleMsgMhfSetCaAchievementHist(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfResetAchievement(s *Session, p mhfpacket.MHFPacket) {} -func handleMsgMhfAddAchievement(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfAddAchievement(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfAddAchievement) + + var exists int + err := s.server.db.QueryRow("SELECT id FROM achievements WHERE id=$1", s.charID).Scan(&exists) + if err != nil { + s.server.db.Exec("INSERT INTO achievements (id) VALUES ($1)", s.charID) + } + + s.server.db.Exec(fmt.Sprintf("UPDATE achievements SET ach%d=ach%d+1 WHERE id=$1", pkt.AchievementID, pkt.AchievementID), s.charID) +} func handleMsgMhfPaymentAchievement(s *Session, p mhfpacket.MHFPacket) {} -func handleMsgMhfDisplayedAchievement(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfDisplayedAchievement(s *Session, p mhfpacket.MHFPacket) { + // This is how you would figure out if the rank-up notification needs to occur +} func handleMsgMhfGetCaAchievementHist(s *Session, p mhfpacket.MHFPacket) {} diff --git a/server/channelserver/handlers_cafe.go b/server/channelserver/handlers_cafe.go new file mode 100644 index 000000000..c7d826429 --- /dev/null +++ b/server/channelserver/handlers_cafe.go @@ -0,0 +1,239 @@ +package channelserver + +import ( + "erupe-ce/common/byteframe" + ps "erupe-ce/common/pascalstring" + "erupe-ce/network/mhfpacket" + "go.uber.org/zap" + "io" + "time" +) + +func handleMsgMhfAcquireCafeItem(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfAcquireCafeItem) + var netcafePoints uint32 + err := s.server.db.QueryRow("UPDATE characters SET netcafe_points = netcafe_points - $1 WHERE id = $2 RETURNING netcafe_points", pkt.PointCost, s.charID).Scan(&netcafePoints) + if err != nil { + s.logger.Fatal("Failed to get netcafe points from db", zap.Error(err)) + } + resp := byteframe.NewByteFrame() + resp.WriteUint32(netcafePoints) + doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) +} + +func handleMsgMhfUpdateCafepoint(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfUpdateCafepoint) + var netcafePoints uint32 + err := s.server.db.QueryRow("SELECT COALESCE(netcafe_points, 0) FROM characters WHERE id = $1", s.charID).Scan(&netcafePoints) + if err != nil { + s.logger.Fatal("Failed to get netcate points from db", zap.Error(err)) + } + resp := byteframe.NewByteFrame() + resp.WriteUint32(netcafePoints) + doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) +} + +func handleMsgMhfCheckDailyCafepoint(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfCheckDailyCafepoint) + + // I am not sure exactly what this does, but all responses I have seen include this exact sequence of bytes + // 1 daily, 5 daily halk pots, 3 point boosted quests, also adds 5 netcafe points but not sent to client + // available once after midday every day + + // get next midday + var t = Time_static() + year, month, day := t.Date() + midday := time.Date(year, month, day, 12, 0, 0, 0, t.Location()) + if t.After(midday) { + midday = midday.Add(24 * time.Hour) + } + + // get time after which daily claiming would be valid from db + var dailyTime time.Time + err := s.server.db.QueryRow("SELECT COALESCE(daily_time, $2) FROM characters WHERE id = $1", s.charID, time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)).Scan(&dailyTime) + if err != nil { + s.logger.Fatal("Failed to get daily_time savedata from db", zap.Error(err)) + } + + if t.After(dailyTime) { + // +5 netcafe points and setting next valid window + _, err := s.server.db.Exec("UPDATE characters SET daily_time=$1, netcafe_points=netcafe_points+5 WHERE id=$2", midday, s.charID) + if err != nil { + s.logger.Fatal("Failed to update daily_time and netcafe_points savedata in db", zap.Error(err)) + } + doAckBufSucceed(s, pkt.AckHandle, []byte{0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01}) + } else { + doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + } +} + +func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetCafeDuration) + bf := byteframe.NewByteFrame() + + var cafeTime uint32 + err := s.server.db.QueryRow("SELECT cafe_time FROM characters WHERE id = $1", s.charID).Scan(&cafeTime) + if err != nil { + panic(err) + } + cafeTime = uint32(Time_Current_Adjusted().Unix()) - uint32(s.sessionStart) + cafeTime + bf.WriteUint32(cafeTime) // Total cafe time + bf.WriteUint16(0) + ps.Uint16(bf, "Resets at next maintenance", true) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +type CafeBonus struct { + ID uint32 `db:"id"` + TimeReq uint32 `db:"time_req"` + ItemType uint32 `db:"item_type"` + ItemID uint32 `db:"item_id"` + Quantity uint32 `db:"quantity"` + Claimed bool `db:"claimed"` +} + +func handleMsgMhfGetCafeDurationBonusInfo(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetCafeDurationBonusInfo) + bf := byteframe.NewByteFrame() + + var count uint32 + rows, err := s.server.db.Queryx(` + SELECT cb.id, time_req, item_type, item_id, quantity, + ( + SELECT count(*) + FROM cafe_accepted ca + WHERE cb.id = ca.cafe_id AND ca.character_id = $1 + )::int::bool AS claimed + FROM cafebonus cb ORDER BY id ASC;`, s.charID) + if err != nil { + s.logger.Error("Error getting cafebonus", zap.Error(err)) + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + } else { + for rows.Next() { + count++ + cafeBonus := &CafeBonus{} + err = rows.StructScan(&cafeBonus) + if err != nil { + s.logger.Error("Error scanning cafebonus", zap.Error(err)) + } + bf.WriteUint32(cafeBonus.TimeReq) + bf.WriteUint32(cafeBonus.ItemType) + bf.WriteUint32(cafeBonus.ItemID) + bf.WriteUint32(cafeBonus.Quantity) + bf.WriteBool(cafeBonus.Claimed) + } + resp := byteframe.NewByteFrame() + resp.WriteUint32(0) + resp.WriteUint32(uint32(time.Now().Unix())) + resp.WriteUint32(count) + resp.WriteBytes(bf.Data()) + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) + } +} + +func handleMsgMhfReceiveCafeDurationBonus(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfReceiveCafeDurationBonus) + bf := byteframe.NewByteFrame() + var count uint32 + bf.WriteUint32(0) + rows, err := s.server.db.Queryx(` + SELECT c.id, time_req, item_type, item_id, quantity + FROM cafebonus c + WHERE ( + SELECT count(*) + FROM cafe_accepted ca + WHERE c.id = ca.cafe_id AND ca.character_id = $1 + ) < 1 AND ( + SELECT ch.cafe_time + $2 + FROM characters ch + WHERE ch.id = $1 + ) >= time_req`, s.charID, Time_Current_Adjusted().Unix()-s.sessionStart) + if err != nil { + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + } else { + for rows.Next() { + cafeBonus := &CafeBonus{} + err = rows.StructScan(cafeBonus) + if err != nil { + continue + } + count++ + bf.WriteUint32(cafeBonus.ID) + bf.WriteUint32(cafeBonus.ItemType) + bf.WriteUint32(cafeBonus.ItemID) + bf.WriteUint32(cafeBonus.Quantity) + } + bf.Seek(0, io.SeekStart) + bf.WriteUint32(count) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + } +} + +func handleMsgMhfPostCafeDurationBonusReceived(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfPostCafeDurationBonusReceived) + var cafeBonus CafeBonus + for _, cbID := range pkt.CafeBonusID { + err := s.server.db.QueryRow(` + SELECT cb.id, item_type, quantity FROM cafebonus cb WHERE cb.id=$1 + `, cbID).Scan(&cafeBonus.ID, &cafeBonus.ItemType, &cafeBonus.Quantity) + if err == nil { + if cafeBonus.ItemType == 17 { + s.server.db.Exec("UPDATE characters SET netcafe_points=netcafe_points+$1 WHERE id=$2", cafeBonus.Quantity, s.charID) + } + } + s.server.db.Exec("INSERT INTO public.cafe_accepted VALUES ($1, $2)", cbID, s.charID) + } + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} + +func handleMsgMhfStartBoostTime(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfStartBoostTime) + bf := byteframe.NewByteFrame() + boostLimit := Time_Current_Adjusted().Add(100 * time.Minute) + s.server.db.Exec("UPDATE characters SET boost_time=$1 WHERE id=$2", boostLimit, s.charID) + bf.WriteUint32(uint32(boostLimit.Unix())) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfGetBoostTime(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetBoostTime) + doAckBufSucceed(s, pkt.AckHandle, []byte{}) +} + +func handleMsgMhfGetBoostTimeLimit(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetBoostTimeLimit) + bf := byteframe.NewByteFrame() + var boostLimit time.Time + err := s.server.db.QueryRow("SELECT boost_time FROM characters WHERE id=$1", s.charID).Scan(&boostLimit) + if err != nil { + bf.WriteUint32(0) + } else { + bf.WriteUint32(uint32(boostLimit.Unix())) + } + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} + +func handleMsgMhfGetBoostRight(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetBoostRight) + var boostLimit time.Time + err := s.server.db.QueryRow("SELECT boost_time FROM characters WHERE id=$1", s.charID).Scan(&boostLimit) + if err != nil { + doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) + return + } + if boostLimit.Unix() > Time_Current_Adjusted().Unix() { + doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x01}) + } else { + doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x02}) + } +} + +func handleMsgMhfPostBoostTimeQuestReturn(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfPostBoostTimeQuestReturn) + doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) +} + +func handleMsgMhfPostBoostTime(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgMhfPostBoostTimeLimit(s *Session, p mhfpacket.MHFPacket) {} diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 304369295..35538e0fb 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -16,6 +16,7 @@ import ( const ( BinaryMessageTypeState = 0 BinaryMessageTypeChat = 1 + BinaryMessageTypeData = 3 BinaryMessageTypeMailNotify = 4 BinaryMessageTypeEmote = 6 ) @@ -164,6 +165,66 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { fmt.Printf("Got chat message: %+v\n", chatMessage) + // Flush all objects and users and reload + if strings.HasPrefix(chatMessage.Message, "!reload") { + sendServerChatMessage(s, "Reloading players...") + var temp mhfpacket.MHFPacket + deleteNotif := byteframe.NewByteFrame() + for _, object := range s.stage.objects { + if object.ownerCharID == s.charID { + continue + } + temp = &mhfpacket.MsgSysDeleteObject{ObjID: object.id} + deleteNotif.WriteUint16(uint16(temp.Opcode())) + temp.Build(deleteNotif, s.clientContext) + } + for _, session := range s.server.sessions { + if s == session { + continue + } + temp = &mhfpacket.MsgSysDeleteUser{CharID: session.charID} + deleteNotif.WriteUint16(uint16(temp.Opcode())) + temp.Build(deleteNotif, s.clientContext) + } + deleteNotif.WriteUint16(0x0010) + s.QueueSend(deleteNotif.Data()) + time.Sleep(500 * time.Millisecond) + reloadNotif := byteframe.NewByteFrame() + for _, session := range s.server.sessions { + if s == session { + continue + } + temp = &mhfpacket.MsgSysInsertUser{CharID: session.charID} + reloadNotif.WriteUint16(uint16(temp.Opcode())) + temp.Build(reloadNotif, s.clientContext) + for i := 0; i < 3; i++ { + temp = &mhfpacket.MsgSysNotifyUserBinary{ + CharID: session.charID, + BinaryType: uint8(i + 1), + } + reloadNotif.WriteUint16(uint16(temp.Opcode())) + temp.Build(reloadNotif, s.clientContext) + } + } + for _, obj := range s.stage.objects { + if obj.ownerCharID == s.charID { + continue + } + temp = &mhfpacket.MsgSysDuplicateObject{ + ObjID: obj.id, + X: obj.x, + Y: obj.y, + Z: obj.z, + Unk0: 0, + OwnerCharID: obj.ownerCharID, + } + reloadNotif.WriteUint16(uint16(temp.Opcode())) + temp.Build(reloadNotif, s.clientContext) + } + reloadNotif.WriteUint16(0x0010) + s.QueueSend(reloadNotif.Data()) + } + // Set account rights if strings.HasPrefix(chatMessage.Message, "!rights") { var v uint32 @@ -179,7 +240,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { } // Discord integration - if chatMessage.Type == binpacket.ChatTypeLocal || chatMessage.Type == binpacket.ChatTypeParty { + if (pkt.BroadcastType == BroadcastTypeStage && s.stage.id == "sl1Ns200p0a0u0") || pkt.BroadcastType == BroadcastTypeWorld { s.server.DiscordChannelSend(chatMessage.SenderName, chatMessage.Message) } diff --git a/server/channelserver/handlers_data.go b/server/channelserver/handlers_data.go index 1f18fe262..8ed55c54a 100644 --- a/server/channelserver/handlers_data.go +++ b/server/channelserver/handlers_data.go @@ -56,7 +56,7 @@ func handleMsgMhfSavedata(s *Session, p mhfpacket.MHFPacket) { s.logger.Fatal("Failed to update savedata in db", zap.Error(err)) } s.logger.Info("Wrote recompressed savedata back to DB.") - dumpSaveData(s, pkt.RawDataPayload, "") + dumpSaveData(s, pkt.RawDataPayload, "savedata") _, err = s.server.db.Exec("UPDATE characters SET weapon_type=$1 WHERE id=$2", uint16(decompressedData[128789]), s.charID) if err != nil { @@ -71,8 +71,8 @@ func handleMsgMhfSavedata(s *Session, p mhfpacket.MHFPacket) { s.myseries.toreData = decompressedData[130228:130468] // 0x1FCB4 + 240 s.myseries.gardenData = decompressedData[142424:142492] // 0x22C58 + 68 - isMale := uint8(decompressedData[80]) // 0x50 - if isMale == 1 { + isFemale := decompressedData[81] // 0x51 + if isFemale == 1 { _, err = s.server.db.Exec("UPDATE characters SET is_female=true WHERE id=$1", s.charID) } else { _, err = s.server.db.Exec("UPDATE characters SET is_female=false WHERE id=$1", s.charID) @@ -275,8 +275,8 @@ func dumpSaveData(s *Session, data []byte, suffix string) { if !s.server.erupeConfig.DevModeOptions.SaveDumps.Enabled { return } else { - dir := filepath.Join(s.server.erupeConfig.DevModeOptions.SaveDumps.OutputDir, fmt.Sprintf("%s_", s.Name)) - path := filepath.Join(s.server.erupeConfig.DevModeOptions.SaveDumps.OutputDir, fmt.Sprintf("%s_", s.Name), fmt.Sprintf("%d_%s_%s%s.bin", s.charID, s.Name, Time_Current().Format("2006-01-02_15.04.05"), suffix)) + dir := filepath.Join(s.server.erupeConfig.DevModeOptions.SaveDumps.OutputDir, fmt.Sprintf("%d_%s", s.charID, s.Name)) + path := filepath.Join(s.server.erupeConfig.DevModeOptions.SaveDumps.OutputDir, fmt.Sprintf("%d_%s", s.charID, s.Name), fmt.Sprintf("%d_%s_%s.bin", s.charID, s.Name, suffix)) if _, err := os.Stat(dir); os.IsNotExist(err) { os.Mkdir(dir, os.ModeDir) @@ -297,10 +297,11 @@ func handleMsgMhfLoaddata(s *Session, p mhfpacket.MHFPacket) { } var data []byte - err := s.server.db.QueryRow("SELECT savedata FROM characters WHERE id = $1", s.charID).Scan(&data) - if err != nil { - s.logger.Fatal("Failed to get savedata from db", zap.Error(err)) + if err != nil || len(data) == 0 { + s.logger.Warn(fmt.Sprintf("Failed to load savedata (CID: %d)", s.charID), zap.Error(err)) + s.rawConn.Close() // Terminate the connection + return } doAckBufSucceed(s, pkt.AckHandle, data) @@ -310,11 +311,11 @@ func handleMsgMhfLoaddata(s *Session, p mhfpacket.MHFPacket) { } bf := byteframe.NewByteFrameFromBytes(decompSaveData) bf.Seek(88, io.SeekStart) - binary1 := bf.ReadNullTerminatedBytes() + name := bf.ReadNullTerminatedBytes() s.server.userBinaryPartsLock.Lock() - s.server.userBinaryParts[userBinaryPartID{charID: s.charID, index: 1}] = append(binary1, []byte{0x00}...) + s.server.userBinaryParts[userBinaryPartID{charID: s.charID, index: 1}] = append(name, []byte{0x00}...) s.server.userBinaryPartsLock.Unlock() - s.Name = stringsupport.SJISToUTF8(binary1) + s.Name = stringsupport.SJISToUTF8(name) } func handleMsgMhfSaveScenarioData(s *Session, p mhfpacket.MHFPacket) { diff --git a/server/channelserver/handlers_discord.go b/server/channelserver/handlers_discord.go index 38619e079..1f3398d18 100644 --- a/server/channelserver/handlers_discord.go +++ b/server/channelserver/handlers_discord.go @@ -2,87 +2,45 @@ package channelserver import ( "fmt" + "github.com/bwmarrin/discordgo" "sort" "strings" - - "github.com/bwmarrin/discordgo" + "unicode" ) -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"` +type Player struct { + CharName string + QuestID int } -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>", -} +func getPlayerSlice(s *Server) []Player { + var p []Player + var questIndex int -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 + for _, channel := range s.Channels { + for _, stage := range channel.stages { + if len(stage.clients) == 0 { + continue + } + questID := 0 + if stage.isQuest() { + questIndex++ + questID = questIndex + } + for client := range stage.clients { + p = append(p, Player{ + CharName: client.Name, + QuestID: questID, + }) + } + } } - - return &character, nil + return p } -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 { - status = p.StageName - } - - missingSpace := length - len(p.CharName) - whitespaces := strings.Repeat(" ", missingSpace+5) - - return fmt.Sprintf("%s %s %s %s", p.WeaponEmoji, p.CharName, whitespaces, status) -} - -func getPlayerList(s *Server) ([]ListPlayer, int) { - list := []ListPlayer{} +func getCharacterList(s *Server) string { questEmojis := []string{ + ":person_in_lotus_position:", ":white_circle:", ":red_circle:", ":blue_circle:", @@ -91,255 +49,41 @@ func getPlayerList(s *Server) ([]ListPlayer, int) { ":purple_circle:", ":yellow_circle:", ":orange_circle:", + ":black_circle:", } - bigNameLen := 0 + playerSlice := getPlayerSlice(s) - 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) - } - - 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 + sort.SliceStable(playerSlice, func(i, j int) bool { + return playerSlice[i].QuestID < playerSlice[j].QuestID }) - for _, lp := range listPlayers { - list += lp.toString(bigNameLen) + "\n" - count++ + message := fmt.Sprintf("===== Online: %d =====\n", len(playerSlice)) + for _, player := range playerSlice { + message += fmt.Sprintf("%s %s", questEmojis[player.QuestID], player.CharName) } - 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 len(stage.clients) > 0 { - 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 len(stage.clients) > 0 { - 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.FindObjectByChar(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 { + if m.Author.Bot || m.ChannelID != s.erupeConfig.Discord.RealtimeChannelID { 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 + paddedName := strings.TrimSpace(strings.Map(func(r rune) rune { + if r > unicode.MaxASCII { + return -1 } + return r + }, m.Author.Username)) - charName := strings.Join(args[1:], " ") - ds.ChannelMessageSend(m.ChannelID, getCharInfo(s, charName)) - return + for i := 0; i < 8-len(m.Author.Username); i++ { + paddedName += " " } - 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)) - } + message := fmt.Sprintf("[D] %s > %s", paddedName, m.Content) + s.BroadcastChatMessage(s.discordBot.NormalizeDiscordMessage(message)) } diff --git a/server/channelserver/handlers_diva.go b/server/channelserver/handlers_diva.go index 8927cd792..48e6dcecf 100644 --- a/server/channelserver/handlers_diva.go +++ b/server/channelserver/handlers_diva.go @@ -2,11 +2,116 @@ package channelserver import ( "encoding/hex" + "erupe-ce/common/stringsupport" + "time" "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" ) +func cleanupDiva(s *Session) { + s.server.db.Exec("DELETE FROM events WHERE event_type='diva'") +} + +func generateDivaTimestamps(s *Session, start uint32, debug bool) []uint32 { + timestamps := make([]uint32, 6) + midnight := Time_Current_Midnight() + if debug && start <= 3 { + midnight := uint32(midnight.Unix()) + switch start { + case 1: + timestamps[0] = midnight + timestamps[1] = timestamps[0] + 601200 + timestamps[2] = timestamps[1] + 3900 + timestamps[3] = timestamps[1] + 604800 + timestamps[4] = timestamps[3] + 3900 + timestamps[5] = timestamps[3] + 604800 + case 2: + timestamps[0] = midnight - 605100 + timestamps[1] = midnight - 3900 + timestamps[2] = midnight + timestamps[3] = timestamps[1] + 604800 + timestamps[4] = timestamps[3] + 3900 + timestamps[5] = timestamps[3] + 604800 + case 3: + timestamps[0] = midnight - 1213800 + timestamps[1] = midnight - 608700 + timestamps[2] = midnight - 604800 + timestamps[3] = midnight - 3900 + timestamps[4] = midnight + timestamps[5] = timestamps[3] + 604800 + } + return timestamps + } + if start == 0 || Time_Current_Adjusted().Unix() > int64(start)+2977200 { + cleanupDiva(s) + // Generate a new diva defense, starting midnight tomorrow + start = uint32(midnight.Add(24 * time.Hour).Unix()) + s.server.db.Exec("INSERT INTO events (event_type, start_time) VALUES ('diva', to_timestamp($1)::timestamp without time zone)", start) + } + timestamps[0] = start + timestamps[1] = timestamps[0] + 601200 + timestamps[2] = timestamps[1] + 3900 + timestamps[3] = timestamps[1] + 604800 + timestamps[4] = timestamps[3] + 3900 + timestamps[5] = timestamps[3] + 604800 + return timestamps +} + +func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetUdSchedule) + bf := byteframe.NewByteFrame() + + id, start := uint32(0xCAFEBEEF), uint32(0) + rows, _ := s.server.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='diva'") + for rows.Next() { + rows.Scan(&id, &start) + } + + var timestamps []uint32 + if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.DivaEvent >= 0 { + if s.server.erupeConfig.DevModeOptions.DivaEvent == 0 { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 36)) + return + } + timestamps = generateDivaTimestamps(s, uint32(s.server.erupeConfig.DevModeOptions.DivaEvent), true) + } else { + timestamps = generateDivaTimestamps(s, start, false) + } + + bf.WriteUint32(id) + for _, timestamp := range timestamps { + bf.WriteUint32(timestamp) + } + + bf.WriteUint16(0x19) // Unk 00011001 + bf.WriteUint16(0x2D) // Unk 00101101 + bf.WriteUint16(0x02) // Unk 00000010 + bf.WriteUint16(0x02) // Unk 00000010 + + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfGetUdInfo(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetUdInfo) + // Message that appears on the Diva Defense NPC and triggers the green exclamation mark + udInfos := []struct { + Text string + StartTime time.Time + EndTime time.Time + }{} + + resp := byteframe.NewByteFrame() + resp.WriteUint8(uint8(len(udInfos))) + for _, udInfo := range udInfos { + resp.WriteBytes(stringsupport.PaddedString(udInfo.Text, 1024, true)) + resp.WriteUint32(uint32(udInfo.StartTime.Unix())) + resp.WriteUint32(uint32(udInfo.EndTime.Unix())) + } + + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) +} + func handleMsgMhfGetKijuInfo(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetKijuInfo) // Temporary canned response diff --git a/server/channelserver/handlers_event.go b/server/channelserver/handlers_event.go index af16799c7..b6765f998 100644 --- a/server/channelserver/handlers_event.go +++ b/server/channelserver/handlers_event.go @@ -7,7 +7,6 @@ import ( "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" - timeServerFix "erupe-ce/server/channelserver/timeserver" ) func handleMsgMhfRegisterEvent(s *Session, p mhfpacket.MHFPacket) { @@ -234,103 +233,6 @@ func handleMsgMhfUseKeepLoginBoost(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } -func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfGetUdSchedule) - var t = timeServerFix.Tstatic_midnight() - var event int = s.server.erupeConfig.DevModeOptions.DivaEvent - - year, month, day := t.Date() - midnight := time.Date(year, month, day, 0, 0, 0, 0, t.Location()) - // Events with time limits are Festival with Sign up, Soul Week and Winners Weeks - // Diva Defense with Prayer, Interception and Song weeks - // Mezeporta Festival with simply 'available' being a weekend thing - resp := byteframe.NewByteFrame() - resp.WriteUint32(0x1d5fda5c) // Unk (1d5fda5c, 0b5397df) - - if event == 1 { - resp.WriteUint32(uint32(midnight.Add(24 * 21 * time.Hour).Unix())) // Week 1 Timestamp, Festi start? - } else { - resp.WriteUint32(uint32(midnight.Add(-24 * 21 * time.Hour).Unix())) // Week 1 Timestamp, Festi start? - } - - if event == 2 { - resp.WriteUint32(uint32(midnight.Add(24 * 14 * time.Hour).Unix())) // Week 2 Timestamp - resp.WriteUint32(uint32(midnight.Add(24 * 14 * time.Hour).Unix())) // Week 2 Timestamp - } else { - resp.WriteUint32(uint32(midnight.Add(-24 * 14 * time.Hour).Unix())) // Week 2 Timestamp - resp.WriteUint32(uint32(midnight.Add(-24 * 14 * time.Hour).Unix())) // Week 2 Timestamp - } - - if event == 3 { - resp.WriteUint32(uint32(midnight.Add((24) * 7 * time.Hour).Unix())) // Diva Defense Interception - resp.WriteUint32(uint32(midnight.Add((24) * 14 * time.Hour).Unix())) // Diva Defense Greeting Song - } else { - resp.WriteUint32(uint32(midnight.Add((-24) * 7 * time.Hour).Unix())) // Diva Defense Interception - resp.WriteUint32(uint32(midnight.Add((-24) * 14 * time.Hour).Unix())) // Diva Defense Greeting Song - } - - resp.WriteUint16(0x19) // Unk 00011001 - resp.WriteUint16(0x2d) // Unk 00101101 - resp.WriteUint16(0x02) // Unk 00000010 - resp.WriteUint16(0x02) // Unk 00000010 - - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) -} - -func handleMsgMhfGetUdInfo(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfGetUdInfo) - // Message that appears on the Diva Defense NPC and triggers the green exclamation mark - udInfos := []struct { - Text string - StartTime time.Time - EndTime time.Time - }{ - /*{ - Text: " ~C17【Erupe】 is dead event!\n\n■Features\n~C18 Dont bother walking around!\n~C17 Take down your DB by doing \n~C17 nearly anything!", - StartTime: Time_static().Add(time.Duration(-5) * time.Minute), // Event started 5 minutes ago, - EndTime: Time_static().Add(time.Duration(24) * time.Hour), // Event ends in 5 minutes, - }, */ - } - - resp := byteframe.NewByteFrame() - resp.WriteUint8(uint8(len(udInfos))) - for _, udInfo := range udInfos { - resp.WriteBytes(fixedSizeShiftJIS(udInfo.Text, 1024)) - resp.WriteUint32(uint32(udInfo.StartTime.Unix())) - resp.WriteUint32(uint32(udInfo.EndTime.Unix())) - } - - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) -} - -func handleMsgMhfGetBoostTime(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfGetBoostTime) - - doAckBufSucceed(s, pkt.AckHandle, []byte{}) - updateRights(s) -} - -func handleMsgMhfGetBoostTimeLimit(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfGetBoostTimeLimit) - doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) -} - -func handleMsgMhfGetBoostRight(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfGetBoostRight) - doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) -} - -func handleMsgMhfPostBoostTimeQuestReturn(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfPostBoostTimeQuestReturn) - doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) -} - -func handleMsgMhfStartBoostTime(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfPostBoostTime(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfPostBoostTimeLimit(s *Session, p mhfpacket.MHFPacket) {} - func handleMsgMhfGetRestrictionEvent(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfSetRestrictionEvent(s *Session, p mhfpacket.MHFPacket) { diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index c1f3cecfb..6e5080ac3 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -2,12 +2,11 @@ package channelserver import ( "encoding/hex" - "math/rand" - "time" - "erupe-ce/common/byteframe" ps "erupe-ce/common/pascalstring" "erupe-ce/network/mhfpacket" + "math/rand" + "time" ) func handleMsgMhfSaveMezfesData(s *Session, p mhfpacket.MHFPacket) { @@ -43,99 +42,200 @@ func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) { case 1: bf.WriteUint32(uint32(midnight.Unix())) bf.WriteUint32(uint32(midnight.Add(3 * 24 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(12 * 24 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(21 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(13 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(20 * 24 * time.Hour).Unix())) case 2: bf.WriteUint32(uint32(midnight.Add(-3 * 24 * time.Hour).Unix())) bf.WriteUint32(uint32(midnight.Unix())) - bf.WriteUint32(uint32(midnight.Add(9 * 24 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(16 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(10 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(17 * 24 * time.Hour).Unix())) case 3: - bf.WriteUint32(uint32(midnight.Add(-12 * 24 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(-9 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(-13 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(-10 * 24 * time.Hour).Unix())) bf.WriteUint32(uint32(midnight.Unix())) bf.WriteUint32(uint32(midnight.Add(7 * 24 * time.Hour).Unix())) default: bf.WriteBytes(make([]byte, 16)) bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // TS Current Time - bf.WriteUint16(1) - bf.WriteUint32(0) + bf.WriteUint8(3) + bf.WriteBytes(make([]byte, 4)) doAckBufSucceed(s, pkt.AckHandle, bf.Data()) return } bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // TS Current Time - d, _ := hex.DecodeString("031491E631353089F18CF68EAE8EEB97C291E589EF00001200000A54001000000000ED130D949A96B697B393A294B081490000000A55001000010000ED130D949A96B697B393A294B081490000000A56001000020000ED130D949A96B697B393A294B081490000000A57001000030000ED130D949A96B697B393A294B081490000000A58001000040000ED130D949A96B697B393A294B081490000000A59001000050000ED130D949A96B697B393A294B081490000000A5A001000060000ED130D949A96B697B393A294B081490000000A5B001000070000ED130D949A96B697B393A294B081490000000A5C001000080000ED130D949A96B697B393A294B081490000000A5D001000090000ED130D949A96B697B393A294B081490000000A5E0010000A0000ED130D949A96B697B393A294B081490000000A5F0010000B0000ED130D949A96B697B393A294B081490000000A600010000C0000ED130D949A96B697B393A294B081490000000A610010000D0000ED130D949A96B697B393A294B081490000000A620011FFFF0000ED121582DD82F182C882C5949A96B697B393A294B081490000000A63000600EA0000000009834C838C834183570000000A64000600ED000000000B836E838A837D834F838D0000000A65000600EF0000000011834A834E8354839383668381834C83930003000002390006000600000E8CC2906C208B9091E58B9B94740001617E43303581798BA38B5A93E09765817A0A7E433030834E83478358836782C592DE82C182BD8B9B82CC83548343835982F08BA382A40A7E433034817991CE8FDB8B9B817A0A7E433030834C838C8341835781410A836E838A837D834F838D8141834A834E8354839383668381834C83930A7E433037817993FC8FDC8FDC9569817A0A7E4330308B9B947482CC82B582E982B58141835E838B836C835290B68E598C9481410A834F815B834E90B68E598C948141834F815B834E91AB90B68E598C9481410A834F815B834E89F095FA8C94283181603388CA290A2F97C29263837C8343839383672831816031303088CA290A2F8FA08360835083628367817B836E815B8374836083508362836794920A2831816035303088CA290A7E43303381798A4A8DC38AFA8AD4817A0A7E43303032303139944E31318C8E323293FA2031343A303082A982E70A32303139944E31318C8E323593FA2031343A303082DC82C5000000023A0011000700001297C292632082668B89E8E891CA935694740000ED7E43303581798BA38B5A93E09765817A0A7E43303081E182DD82F182C882C5949A96B697B393A294B0814981E282F00A93AF82B697C2926382C98F8A91AE82B782E934906C82DC82C582CC0A97C2926388F582C582A282A982C9918182AD834E838A834182B782E982A90A82F08BA382A40A0A7E433037817993FC8FDC8FDC9569817A0A7E43303091E631343789F18EEB906C8DD582CC8DB02831816032303088CA290A0A7E43303381798A4A8DC38AFA8AD4817A0A7E43303032303139944E31318C8E323293FA2031343A303082A982E70A32303139944E31318C8E323593FA2031343A303082DC82C50A000000023B001000070000128CC2906C2082668B89E8E891CA935694740001497E43303581798BA38B5A93E09765817A0A7E43303081E1949A96B697B393A294B0814981E282F00A82A282A982C9918182AD834E838A834182B782E982A982F08BA382A40A0A7E433037817993FC8FDC8FDC9569817A0A7E43303089A48ED282CC8381835F838B283188CA290A2F8CF68EAE82CC82B582E982B58141835E838B836C835290B68E598C9481410A834F815B834E90B68E598C948141834F815B834E91AB90B68E598C9481410A834F815B834E89F095FA8C94283181603388CA290A2F97C29263837C8343839383672831816031303088CA290A2F8FA08360835083628367817B836E815B8374836083508362836794920A2831816035303088CA290A7E43303381798A4A8DC38AFA8AD4817A0A7E43303032303139944E31318C8E323293FA2031343A303082A982E70A32303139944E31318C8E323593FA2031343A303082DC82C500") - bf.WriteBytes(d) + bf.WriteUint8(3) + ps.Uint8(bf, "", false) + bf.WriteUint16(0) // numEvents + bf.WriteUint8(0) // numCups + + /* + struct event + uint32 eventID + uint16 unk + uint16 unk + uint32 unk + psUint8 name + + struct cup + uint32 cupID + uint16 unk + uint16 unk + uint16 unk + psUint8 name + psUint16 desc + */ + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } +func cleanupFesta(s *Session) { + s.server.db.Exec("DELETE FROM events WHERE event_type='festa'") + s.server.db.Exec("DELETE FROM festa_registrations") + s.server.db.Exec("DELETE FROM festa_prizes_accepted") + s.server.db.Exec("UPDATE guild_characters SET souls=0") +} + +func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 { + timestamps := make([]uint32, 5) + midnight := Time_Current_Midnight() + if debug && start <= 3 { + midnight := uint32(midnight.Unix()) + switch start { + case 1: + timestamps[0] = midnight + timestamps[1] = timestamps[0] + 604800 + timestamps[2] = timestamps[1] + 604800 + timestamps[3] = timestamps[2] + 9000 + timestamps[4] = timestamps[3] + 1240200 + case 2: + timestamps[0] = midnight - 604800 + timestamps[1] = midnight + timestamps[2] = timestamps[1] + 604800 + timestamps[3] = timestamps[2] + 9000 + timestamps[4] = timestamps[3] + 1240200 + case 3: + timestamps[0] = midnight - 1209600 + timestamps[1] = midnight - 604800 + timestamps[2] = midnight + timestamps[3] = timestamps[2] + 9000 + timestamps[4] = timestamps[3] + 1240200 + } + return timestamps + } + if start == 0 || Time_Current_Adjusted().Unix() > int64(start)+2977200 { + cleanupFesta(s) + // Generate a new festa, starting midnight tomorrow + start = uint32(midnight.Add(24 * time.Hour).Unix()) + s.server.db.Exec("INSERT INTO events (event_type, start_time) VALUES ('festa', to_timestamp($1)::timestamp without time zone)", start) + } + timestamps[0] = start + timestamps[1] = timestamps[0] + 604800 + timestamps[2] = timestamps[1] + 604800 + timestamps[3] = timestamps[2] + 9000 + timestamps[4] = timestamps[3] + 1240200 + return timestamps +} + +type Trial struct { + ID uint32 `db:"id"` + Objective uint8 `db:"objective"` + GoalID uint32 `db:"goal_id"` + TimesReq uint16 `db:"times_req"` + Locale uint16 `db:"locale_req"` + Reward uint16 `db:"reward"` +} + func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfInfoFesta) bf := byteframe.NewByteFrame() - state := s.server.erupeConfig.DevModeOptions.FestaEvent - bf.WriteUint32(0xdeadbeef) // festaID - // Registration Week Start - // Introductory Week Start - // Totalling Time - // Reward Festival Start (2.5hrs after totalling) - // 2 weeks after RewardFes (next fes?) - midnight := Time_Current_Midnight() - switch state { - case 1: - bf.WriteUint32(uint32(midnight.Unix())) - bf.WriteUint32(uint32(midnight.Add(24 * 7 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(24 * 14 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(24*14*time.Hour + 150*time.Minute).Unix())) - bf.WriteUint32(uint32(midnight.Add(24*28*time.Hour + 11*time.Hour).Unix())) - case 2: - bf.WriteUint32(uint32(midnight.Add(-24 * 7 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Unix())) - bf.WriteUint32(uint32(midnight.Add(24 * 7 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(24*7*time.Hour + 150*time.Minute).Unix())) - bf.WriteUint32(uint32(midnight.Add(24 * 21 * time.Hour).Add(11 * time.Hour).Unix())) - case 3: - bf.WriteUint32(uint32(midnight.Add(-24 * 14 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(-24*7*time.Hour + 11*time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Unix())) - bf.WriteUint32(uint32(midnight.Add(150 * time.Minute).Unix())) - bf.WriteUint32(uint32(midnight.Add(24*14*time.Hour + 11*time.Hour).Unix())) - default: + + id, start := uint32(0xDEADBEEF), uint32(0) + rows, _ := s.server.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='festa'") + for rows.Next() { + rows.Scan(&id, &start) + } + + var timestamps []uint32 + if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.FestaEvent >= 0 { + if s.server.erupeConfig.DevModeOptions.FestaEvent == 0 { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } + timestamps = generateFestaTimestamps(s, uint32(s.server.erupeConfig.DevModeOptions.FestaEvent), true) + } else { + timestamps = generateFestaTimestamps(s, start, false) + } + + if timestamps[0] > uint32(Time_Current_Adjusted().Unix()) { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) return } - bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // TS Current Time + + var blueSouls, redSouls uint32 + s.server.db.QueryRow("SELECT SUM(gc.souls) FROM guild_characters gc INNER JOIN festa_registrations fr ON fr.guild_id = gc.guild_id WHERE fr.team = 'blue'").Scan(&blueSouls) + s.server.db.QueryRow("SELECT SUM(gc.souls) FROM guild_characters gc INNER JOIN festa_registrations fr ON fr.guild_id = gc.guild_id WHERE fr.team = 'red'").Scan(&redSouls) + + bf.WriteUint32(id) + for _, timestamp := range timestamps { + bf.WriteUint32(timestamp) + } + bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) bf.WriteUint8(4) ps.Uint8(bf, "", false) bf.WriteUint32(0) - bf.WriteUint32(0) // Blue souls - bf.WriteUint32(0) // Red souls + bf.WriteUint32(blueSouls) + bf.WriteUint32(redSouls) - trials := 0 - bf.WriteUint16(uint16(trials)) - for i := 0; i < trials; i++ { - bf.WriteUint32(uint32(i + 1)) // trialID - bf.WriteUint8(0xFF) // unk - bf.WriteUint8(uint8(i)) // objective - bf.WriteUint32(0x1B) // monID, itemID if deliver - bf.WriteUint16(1) // huntsRemain? - bf.WriteUint16(0) // location - bf.WriteUint16(1) // numSoulsReward - bf.WriteUint8(0xFF) // unk - bf.WriteUint8(0xFF) // monopolised - bf.WriteUint16(0) // unk + rows, _ = s.server.db.Queryx("SELECT * FROM festa_trials") + trialData := byteframe.NewByteFrame() + var count uint16 + for rows.Next() { + trial := &Trial{} + err := rows.StructScan(&trial) + if err != nil { + continue + } + count++ + trialData.WriteUint32(trial.ID) + trialData.WriteUint8(0) // Unk + trialData.WriteUint8(trial.Objective) + trialData.WriteUint32(trial.GoalID) + trialData.WriteUint16(trial.TimesReq) + trialData.WriteUint16(trial.Locale) + trialData.WriteUint16(trial.Reward) + trialData.WriteUint8(0xFF) // Unk + trialData.WriteUint8(0xFF) // MonopolyState + trialData.WriteUint16(0) // Unk + } + bf.WriteUint16(count) + bf.WriteBytes(trialData.Data()) + + // Static bonus rewards + rewards, _ := hex.DecodeString("001901000007015E05F000000000000100000703E81B6300000000010100000C03E8000000000000000100000D0000000000000000000100000100000000000000000002000007015E05F000000000000200000703E81B6300000000010200000C03E8000000000000000200000D0000000000000000000200000400000000000000000003000007015E05F000000000000300000703E81B6300000000010300000C03E8000000000000000300000D0000000000000000000300000100000000000000000004000007015E05F000000000000400000703E81B6300000000010400000C03E8000000000000000400000D0000000000000000000400000400000000000000000005000007015E05F000000000000500000703E81B6300000000010500000C03E8000000000000000500000D00000000000000000005000001000000000000000000") + bf.WriteBytes(rewards) + + bf.WriteUint16(0x0001) + bf.WriteUint32(0xD4C001F4) + + categoryWinners := uint16(0) // NYI + bf.WriteUint16(categoryWinners) + for i := uint16(0); i < categoryWinners; i++ { + bf.WriteUint32(0) // Guild ID + bf.WriteUint16(i + 1) // Category ID + bf.WriteUint16(0) // Festa Team + ps.Uint8(bf, "", true) // Guild Name } - unk := 0 // static rewards? - bf.WriteUint16(uint16(unk)) - for i := 0; i < unk; i++ { - bf.WriteUint32(0) - bf.WriteUint16(0) - bf.WriteUint16(0) - bf.WriteUint32(0) - bf.WriteBool(false) + dailyWinners := uint16(0) // NYI + bf.WriteUint16(dailyWinners) + for i := uint16(0); i < dailyWinners; i++ { + bf.WriteUint32(0) // Guild ID + bf.WriteUint16(i + 1) // Category ID + bf.WriteUint16(0) // Festa Team + ps.Uint8(bf, "", true) // Guild Name } - d, _ := hex.DecodeString("0001D4C001F4000411B6648100010001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100020001152A81F589A497A793C196B18B528E6D926381F52A000C952CE10003000109E54BE54E89B38F970029FDCE04000400001381818D84836C8352819993A294B091D1818100000811B6648100010001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100020001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100030001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100040001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100050001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100060001152A81F589A497A793C196B18B528E6D926381F52A000C952CE10007000109E54BE54E89B38F9700000000000008000001000000000100001388000007D0000003E800000064012C00C8009600640032") + d, _ := hex.DecodeString("000000000000000100001388000007D0000003E800000064012C00C8009600640032") bf.WriteBytes(d) ps.Uint16(bf, "", false) doAckBufSucceed(s, pkt.AckHandle, bf.Data()) @@ -144,8 +244,19 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { // state festa (U)ser func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfStateFestaU) + guild, err := GetGuildInfoByCharacterId(s, s.charID) + applicant := false + if guild != nil { + applicant, _ = guild.HasApplicationForCharID(s, s.charID) + } + if err != nil || guild == nil || applicant { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } + var souls uint32 + s.server.db.QueryRow("SELECT souls FROM guild_characters WHERE character_id=$1", s.charID).Scan(&souls) bf := byteframe.NewByteFrame() - bf.WriteUint32(0) // souls + bf.WriteUint32(souls) bf.WriteUint32(0) // unk doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } @@ -153,80 +264,156 @@ func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) { // state festa (G)uild func handleMsgMhfStateFestaG(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfStateFestaG) + guild, err := GetGuildInfoByCharacterId(s, s.charID) + applicant := false + if guild != nil { + applicant, _ = guild.HasApplicationForCharID(s, s.charID) + } resp := byteframe.NewByteFrame() - resp.WriteUint32(0) // souls - resp.WriteUint32(1) // unk - resp.WriteUint32(1) // unk - resp.WriteUint32(1) // unk, rank? - resp.WriteUint32(1) // unk + if err != nil || guild == nil || applicant { + resp.WriteUint32(0) + resp.WriteUint32(0) + resp.WriteUint32(0xFFFFFFFF) + resp.WriteUint32(0) + resp.WriteUint32(0) + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) + return + } + resp.WriteUint32(guild.Souls) + resp.WriteUint32(0) // unk + resp.WriteUint32(0) // unk, rank? + resp.WriteUint32(0) // unk + resp.WriteUint32(0) // unk doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } func handleMsgMhfEnumerateFestaMember(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateFestaMember) + guild, err := GetGuildInfoByCharacterId(s, s.charID) + if err != nil || guild == nil { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } + members, err := GetGuildMembers(s, guild.ID, false) + if err != nil { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } bf := byteframe.NewByteFrame() - bf.WriteUint16(0) // numMembers - // uint16 unk - // uint32 charID - // uint32 souls + bf.WriteUint16(uint16(len(members))) + bf.WriteUint16(0) // Unk + for _, member := range members { + bf.WriteUint32(member.CharID) + bf.WriteUint32(member.Souls) + } doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } func handleMsgMhfVoteFesta(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfEntryFesta) + pkt := p.(*mhfpacket.MsgMhfVoteFesta) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEntryFesta) - bf := byteframe.NewByteFrame() + guild, err := GetGuildInfoByCharacterId(s, s.charID) + if err != nil || guild == nil { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } rand.Seed(time.Now().UnixNano()) - bf.WriteUint32(uint32(rand.Intn(2))) - // Update guild table + team := uint32(rand.Intn(2)) + switch team { + case 0: + s.server.db.Exec("INSERT INTO festa_registrations VALUES ($1, 'blue')", guild.ID) + case 1: + s.server.db.Exec("INSERT INTO festa_registrations VALUES ($1, 'red')", guild.ID) + } + bf := byteframe.NewByteFrame() + bf.WriteUint32(team) doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) } func handleMsgMhfChargeFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfChargeFesta) - // Update festa state table + s.server.db.Exec("UPDATE guild_characters SET souls=souls+$1 WHERE character_id=$2", pkt.Souls, s.charID) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfAcquireFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireFesta) - // Mark festa as claimed - // Update guild table? doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfAcquireFestaPersonalPrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireFestaPersonalPrize) - // Set prize as claimed + s.server.db.Exec("INSERT INTO public.festa_prizes_accepted VALUES ($1, $2)", pkt.PrizeID, s.charID) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfAcquireFestaIntermediatePrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireFestaIntermediatePrize) - // Set prize as claimed + s.server.db.Exec("INSERT INTO public.festa_prizes_accepted VALUES ($1, $2)", pkt.PrizeID, s.charID) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } -// uint32 numPrizes -// struct festaPrize -// uint32 prizeID -// uint32 prizeTier (1/2/3, 3 = GR) -// uint32 soulsReq -// uint32 unk (00 00 00 07) -// uint32 itemID -// uint32 numItem -// bool claimed +type Prize struct { + ID uint32 `db:"id"` + Tier uint32 `db:"tier"` + SoulsReq uint32 `db:"souls_req"` + ItemID uint32 `db:"item_id"` + NumItem uint32 `db:"num_item"` + Claimed int `db:"claimed"` +} func handleMsgMhfEnumerateFestaPersonalPrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateFestaPersonalPrize) - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + rows, _ := s.server.db.Queryx("SELECT id, tier, souls_req, item_id, num_item, (SELECT count(*) FROM festa_prizes_accepted fpa WHERE fp.id = fpa.prize_id AND fpa.character_id = 4) AS claimed FROM festa_prizes fp WHERE type='personal'") + var count uint32 + prizeData := byteframe.NewByteFrame() + for rows.Next() { + prize := &Prize{} + err := rows.StructScan(&prize) + if err != nil { + continue + } + count++ + prizeData.WriteUint32(prize.ID) + prizeData.WriteUint32(prize.Tier) + prizeData.WriteUint32(prize.SoulsReq) + prizeData.WriteUint32(7) // Unk + prizeData.WriteUint32(prize.ItemID) + prizeData.WriteUint32(prize.NumItem) + prizeData.WriteBool(prize.Claimed > 0) + } + bf := byteframe.NewByteFrame() + bf.WriteUint32(count) + bf.WriteBytes(prizeData.Data()) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } func handleMsgMhfEnumerateFestaIntermediatePrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateFestaIntermediatePrize) - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + rows, _ := s.server.db.Queryx("SELECT id, tier, souls_req, item_id, num_item, (SELECT count(*) FROM festa_prizes_accepted fpa WHERE fp.id = fpa.prize_id AND fpa.character_id = 4) AS claimed FROM festa_prizes fp WHERE type='guild'") + var count uint32 + prizeData := byteframe.NewByteFrame() + for rows.Next() { + prize := &Prize{} + err := rows.StructScan(&prize) + if err != nil { + continue + } + count++ + prizeData.WriteUint32(prize.ID) + prizeData.WriteUint32(prize.Tier) + prizeData.WriteUint32(prize.SoulsReq) + prizeData.WriteUint32(7) // Unk + prizeData.WriteUint32(prize.ItemID) + prizeData.WriteUint32(prize.NumItem) + prizeData.WriteBool(prize.Claimed > 0) + } + bf := byteframe.NewByteFrame() + bf.WriteUint32(count) + bf.WriteBytes(prizeData.Data()) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } diff --git a/server/channelserver/handlers_guild.go b/server/channelserver/handlers_guild.go index fcaddbaf9..6bbee7eba 100644 --- a/server/channelserver/handlers_guild.go +++ b/server/channelserver/handlers_guild.go @@ -56,6 +56,7 @@ type Guild struct { PugiName3 string `db:"pugi_name_3"` Recruiting bool `db:"recruiting"` FestivalColour FestivalColour `db:"festival_colour"` + Souls uint32 `db:"souls"` Rank uint16 `db:"rank"` AllianceID uint32 `db:"alliance_id"` Icon *GuildIcon `db:"icon"` @@ -121,11 +122,12 @@ SELECT leader_id, lc.name as leader_name, comment, - pugi_name_1, - pugi_name_2, - pugi_name_3, + COALESCE(pugi_name_1, '') AS pugi_name_1, + COALESCE(pugi_name_2, '') AS pugi_name_2, + COALESCE(pugi_name_3, '') AS pugi_name_3, recruiting, - festival_colour, + COALESCE((SELECT team FROM festa_registrations fr WHERE fr.guild_id = g.id), 'none') AS festival_colour, + (SELECT SUM(souls) FROM guild_characters gc WHERE gc.guild_id = g.id) AS souls, CASE WHEN rank_rp <= 48 THEN rank_rp/24 WHEN rank_rp <= 288 THEN rank_rp/48+1 @@ -134,23 +136,14 @@ SELECT WHEN rank_rp < 1200 THEN 16 ELSE 17 END rank, - CASE WHEN ( + COALESCE(( SELECT id FROM guild_alliances ga WHERE ga.parent_id = g.id OR ga.sub1_id = g.id OR ga.sub2_id = g.id - ) IS NULL THEN 0 - ELSE ( - SELECT id FROM guild_alliances ga WHERE - ga.parent_id = g.id OR - ga.sub1_id = g.id OR - ga.sub2_id = g.id - ) - END alliance_id, + ), 0) AS alliance_id, icon, - ( - SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id - ) AS member_count + (SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count FROM guilds g JOIN guild_characters lgc ON lgc.character_id = leader_id JOIN characters lc on leader_id = lc.id @@ -158,8 +151,8 @@ SELECT func (guild *Guild) Save(s *Session) error { _, err := s.server.db.Exec(` - UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7, festival_colour=$8, icon=$9 WHERE id=$1 - `, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3, guild.FestivalColour, guild.Icon) + UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7, icon=$8, leader_id=$9 WHERE id=$1 + `, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3, guild.Icon, guild.GuildLeader.LeaderCharID) if err != nil { s.logger.Error("failed to update guild data", zap.Error(err), zap.Uint32("guildID", guild.ID)) @@ -643,6 +636,33 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint32(uint32(response)) doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) return + case mhfpacket.OPERATE_GUILD_RESIGN: + guildMembers, err := GetGuildMembers(s, guild.ID, false) + success := false + if err == nil { + sort.Slice(guildMembers[:], func(i, j int) bool { + return guildMembers[i].OrderIndex < guildMembers[j].OrderIndex + }) + for i := 1; i < len(guildMembers); i++ { + if !guildMembers[i].AvoidLeadership { + guild.LeaderCharID = guildMembers[i].CharID + guildMembers[0].OrderIndex = guildMembers[i].OrderIndex + guildMembers[i].OrderIndex = 1 + guildMembers[0].Save(s) + guildMembers[i].Save(s) + bf.WriteUint32(guildMembers[i].CharID) + success = true + break + } + } + guild.Save(s) + doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) + } + if !success { + bf.WriteUint32(0) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + } + return case mhfpacket.OPERATE_GUILD_APPLY: err = guild.CreateApplication(s, s.charID, GuildApplicationTypeApplied, nil) @@ -667,6 +687,14 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { if err != nil { // All successful acks return 0x01, assuming 0x00 is failure response = 0x00 + } else { + mail := Mail{ + RecipientID: s.charID, + Subject: "Withdrawal", + Body: fmt.Sprintf("You have withdrawn from 「%s」.", guild.Name), + IsSystemMessage: true, + } + mail.Send(s, nil) } bf.WriteUint32(uint32(response)) @@ -790,14 +818,14 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) { guild, err := GetGuildInfoByCharacterId(s, pkt.CharID) if err != nil || guild == nil { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } actorCharacter, err := GetCharacterGuildData(s, s.charID) if err != nil || (!actorCharacter.IsSubLeader() && guild.LeaderCharID != s.charID) { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } @@ -806,41 +834,45 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) { case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT: err = guild.AcceptApplication(s, pkt.CharID) mail = Mail{ - SenderID: s.charID, - RecipientID: pkt.CharID, - Subject: "Accepted!", - Body: fmt.Sprintf("Your application to join 「%s」 was accepted.", guild.Name), - IsGuildInvite: false, + RecipientID: pkt.CharID, + Subject: "Accepted!", + Body: fmt.Sprintf("Your application to join 「%s」 was accepted.", guild.Name), + IsSystemMessage: true, } case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_REJECT: err = guild.RejectApplication(s, pkt.CharID) mail = Mail{ - SenderID: s.charID, - RecipientID: pkt.CharID, - Subject: "Rejected", - Body: fmt.Sprintf("Your application to join 「%s」 was rejected.", guild.Name), - IsGuildInvite: false, + RecipientID: pkt.CharID, + Subject: "Rejected", + Body: fmt.Sprintf("Your application to join 「%s」 was rejected.", guild.Name), + IsSystemMessage: true, } case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_KICK: err = guild.RemoveCharacter(s, pkt.CharID) mail = Mail{ - SenderID: s.charID, - RecipientID: pkt.CharID, - Subject: "Kicked", - Body: fmt.Sprintf("You were kicked from 「%s」.", guild.Name), - IsGuildInvite: false, + RecipientID: pkt.CharID, + Subject: "Kicked", + Body: fmt.Sprintf("You were kicked from 「%s」.", guild.Name), + IsSystemMessage: true, } default: - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) s.logger.Warn(fmt.Sprintf("unhandled operateGuildMember action '%d'", pkt.Action)) } if err != nil { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) } else { mail.Send(s, nil) + for _, channel := range s.server.Channels { + for _, session := range channel.sessions { + if session.charID == pkt.CharID { + SendMailNotification(s, &mail, session) + } + } + } + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } - doAckSimpleSucceed(s, pkt.AckHandle, nil) } func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { @@ -914,24 +946,12 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { bf.WriteBytes(guildLeaderName) bf.WriteBytes([]byte{0x00, 0x00, 0x00, 0x00}) // Unk bf.WriteBool(false) // isReturnGuild - bf.WriteBytes([]byte{0x01, 0x02, 0x02}) // Unk + bf.WriteBool(false) // earnedSpecialHall + bf.WriteBytes([]byte{0x02, 0x02}) // Unk bf.WriteUint32(guild.EventRP) - - if guild.PugiName1 == "" { - bf.WriteUint16(0x0100) - } else { - ps.Uint8(bf, guild.PugiName1, true) - } - if guild.PugiName2 == "" { - bf.WriteUint16(0x0100) - } else { - ps.Uint8(bf, guild.PugiName2, true) - } - if guild.PugiName3 == "" { - bf.WriteUint16(0x0100) - } else { - ps.Uint8(bf, guild.PugiName3, true) - } + ps.Uint8(bf, guild.PugiName1, true) + ps.Uint8(bf, guild.PugiName2, true) + ps.Uint8(bf, guild.PugiName3, true) // probably guild pugi properties, should be status, stamina and luck outfits bf.WriteBytes([]byte{ @@ -952,7 +972,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { } else { bf.WriteUint32(alliance.ID) bf.WriteUint32(uint32(alliance.CreatedAt.Unix())) - bf.WriteUint16(uint16(alliance.TotalMembers)) + bf.WriteUint16(alliance.TotalMembers) bf.WriteUint16(0) // Unk0 ps.Uint16(bf, alliance.Name, true) if alliance.SubGuild1ID > 0 { @@ -1015,7 +1035,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } - if err != nil { + if err != nil || characterGuildData.IsApplicant { bf.WriteUint16(0) } else { bf.WriteUint16(uint16(len(applicants))) @@ -1063,15 +1083,11 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { } else { bf.WriteUint8(0x00) } + bf.WriteUint8(0) // Unk doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } else { - //// REALLY large/complex format... stubbing it out here for simplicity. - //resp := byteframe.NewByteFrame() - //resp.WriteUint32(0) // Count - //resp.WriteUint8(0) // Unk, read if count == 0. - - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 8)) + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 5)) } } @@ -1260,6 +1276,14 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { guild, err = GetGuildInfoByCharacterId(s, s.charID) } + if guild != nil { + isApplicant, _ := guild.HasApplicationForCharID(s, s.charID) + if isApplicant { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } + } + if guild == nil && s.prevGuildID > 0 { guild, err = GetGuildInfoByID(s, s.prevGuildID) } @@ -1305,7 +1329,8 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint16(0x0600) } bf.WriteUint8(member.OrderIndex) - ps.Uint16(bf, member.Name, true) + bf.WriteBool(member.AvoidLeadership) + ps.Uint8(bf, member.Name, true) } for _, member := range guildMembers { @@ -1414,12 +1439,8 @@ func handleMsgMhfGetGuildTargetMemberNum(s *Session, p mhfpacket.MHFPacket) { guild, err = GetGuildInfoByID(s, pkt.GuildID) } - if err != nil { - s.logger.Warn("failed to find guild", zap.Error(err), zap.Uint32("guildID", pkt.GuildID)) - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) - return - } else if guild == nil { - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + if err != nil || guild == nil { + doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x02}) return } @@ -1865,7 +1886,10 @@ func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } -func handleMsgMhfEntryRookieGuild(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfEntryRookieGuild(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfEntryRookieGuild) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) +} func handleMsgMhfUpdateForceGuildRank(s *Session, p mhfpacket.MHFPacket) {} @@ -1883,8 +1907,18 @@ func handleMsgMhfUpdateGuild(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfSetGuildManageRight(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSetGuildManageRight) s.server.db.Exec("UPDATE guild_characters SET recruiter=$1 WHERE character_id=$2", pkt.Allowed, pkt.CharID) - // TODO: What is this supposed to return? This works for now - doAckBufSucceed(s, pkt.AckHandle, []byte{0x01}) + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) +} + +func handleMsgMhfCheckMonthlyItem(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfCheckMonthlyItem) + doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x01}) + // TODO: Implement month-by-month tracker, 0 = Not claimed, 1 = Claimed +} + +func handleMsgMhfAcquireMonthlyItem(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfAcquireMonthlyItem) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfEnumerateInvGuild(s *Session, p mhfpacket.MHFPacket) { @@ -1892,6 +1926,9 @@ func handleMsgMhfEnumerateInvGuild(s *Session, p mhfpacket.MHFPacket) { stubEnumerateNoResults(s, pkt.AckHandle) } -func handleMsgMhfOperationInvGuild(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfOperationInvGuild(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfOperationInvGuild) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) +} func handleMsgMhfUpdateGuildcard(s *Session, p mhfpacket.MHFPacket) {} diff --git a/server/channelserver/handlers_guild_member.go b/server/channelserver/handlers_guild_member.go index 5fa83fb6b..2c3f0cd1a 100644 --- a/server/channelserver/handlers_guild_member.go +++ b/server/channelserver/handlers_guild_member.go @@ -12,6 +12,7 @@ type GuildMember struct { GuildID uint32 `db:"guild_id"` CharID uint32 `db:"character_id"` JoinedAt *time.Time `db:"joined_at"` + Souls uint32 `db:"souls"` Name string `db:"name"` IsApplicant bool `db:"is_applicant"` OrderIndex uint8 `db:"order_index"` @@ -25,12 +26,25 @@ type GuildMember struct { WeaponType uint16 `db:"weapon_type"` } +func (gm *GuildMember) CanRecruit() bool { + if gm.Recruiter { + return true + } + if gm.OrderIndex <= 3 { + return true + } + if gm.IsLeader { + return true + } + return false +} + func (gm *GuildMember) IsSubLeader() bool { - return gm.OrderIndex <= 3 && !gm.AvoidLeadership + return gm.OrderIndex <= 3 } func (gm *GuildMember) Save(s *Session) error { - _, err := s.server.db.Exec("UPDATE guild_characters SET avoid_leadership=$1 WHERE character_id=$2", gm.AvoidLeadership, gm.CharID) + _, err := s.server.db.Exec("UPDATE guild_characters SET avoid_leadership=$1, order_index=$2 WHERE character_id=$3", gm.AvoidLeadership, gm.OrderIndex, gm.CharID) if err != nil { s.logger.Error( @@ -48,6 +62,7 @@ const guildMembersSelectSQL = ` SELECT g.id as guild_id, joined_at, + coalesce(souls, 0) as souls, c.name, character.character_id, coalesce(gc.order_index, 0) as order_index, diff --git a/server/channelserver/handlers_guild_scout.go b/server/channelserver/handlers_guild_scout.go index 4bcbb6b91..f47c28bc6 100644 --- a/server/channelserver/handlers_guild_scout.go +++ b/server/channelserver/handlers_guild_scout.go @@ -21,7 +21,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { panic(err) } - if actorCharGuildData == nil || !actorCharGuildData.Recruiter { + if actorCharGuildData == nil || !actorCharGuildData.CanRecruit() { doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) return } @@ -59,19 +59,13 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { panic(err) } - senderName, err := getCharacterName(s, s.charID) - - if err != nil { - panic(err) - } - mail := &Mail{ SenderID: s.charID, RecipientID: pkt.CharID, - Subject: "Guild! ヽ(・∀・)ノ", + Subject: "Invitation!", Body: fmt.Sprintf( - "%s has invited you to join the wonderful guild %s, do you accept this challenge?", - senderName, + "%s has invited you to join 「%s」\nDo you want to accept?", + getCharacterName(s, s.charID), guildInfo.Name, ), IsGuildInvite: true, @@ -104,7 +98,7 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) { panic(err) } - if guildCharData == nil || !guildCharData.Recruiter { + if guildCharData == nil || !guildCharData.CanRecruit() { doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) return } @@ -128,61 +122,72 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAnswerGuildScout) - + bf := byteframe.NewByteFrame() guild, err := GetGuildInfoByCharacterId(s, pkt.LeaderID) if err != nil { panic(err) } - _, err = guild.GetApplicationForCharID(s, s.charID, GuildApplicationTypeInvited) + app, err := guild.GetApplicationForCharID(s, s.charID, GuildApplicationTypeInvited) - if err != nil { + if app == nil || err != nil { s.logger.Warn( - "could not retrieve guild invitation", + "Guild invite missing, deleted?", zap.Error(err), zap.Uint32("guildID", guild.ID), zap.Uint32("charID", s.charID), ) - doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) + bf.WriteUint32(7) + bf.WriteUint32(guild.ID) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) return } + var mail []Mail if pkt.Answer { err = guild.AcceptApplication(s, s.charID) + mail = append(mail, Mail{ + RecipientID: s.charID, + Subject: "Success!", + Body: fmt.Sprintf("You successfully joined 「%s」.", guild.Name), + IsSystemMessage: true, + }) + mail = append(mail, Mail{ + SenderID: s.charID, + RecipientID: pkt.LeaderID, + Subject: "Accepted", + Body: fmt.Sprintf("%s accepted your invitation to join 「%s」.", s.Name, guild.Name), + IsSystemMessage: true, + }) } else { err = guild.RejectApplication(s, s.charID) + mail = append(mail, Mail{ + RecipientID: s.charID, + Subject: "Declined", + Body: fmt.Sprintf("You declined the invitation to join 「%s」.", guild.Name), + IsSystemMessage: true, + }) + mail = append(mail, Mail{ + SenderID: s.charID, + RecipientID: pkt.LeaderID, + Subject: "Declined", + Body: fmt.Sprintf("%s declined your invitation to join 「%s」.", s.Name, guild.Name), + IsSystemMessage: true, + }) } - if err != nil { - doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) - return + bf.WriteUint32(7) + bf.WriteUint32(guild.ID) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + } else { + bf.WriteUint32(0) + bf.WriteUint32(guild.ID) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + for _, m := range mail { + m.Send(s, nil) + } } - - senderName, err := getCharacterName(s, pkt.LeaderID) - - if err != nil { - doAckSimpleFail(s, pkt.AckHandle, nil) - panic(err) - } - - successMail := Mail{ - SenderID: pkt.LeaderID, - RecipientID: s.charID, - Subject: "Happy days!", - Body: fmt.Sprintf("You successfully joined %s and should be proud of all you have accomplished.", guild.Name), - IsGuildInvite: false, - SenderName: senderName, - } - - err = successMail.Send(s, nil) - - if err != nil { - doAckSimpleFail(s, pkt.AckHandle, nil) - panic(err) - } - - doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x81, 0x7e}) } func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) { @@ -191,12 +196,12 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) { guildInfo, err := GetGuildInfoByCharacterId(s, s.charID) if guildInfo == nil && s.prevGuildID == 0 { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } else { guildInfo, err = GetGuildInfoByID(s, s.prevGuildID) if guildInfo == nil || err != nil { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } } @@ -210,7 +215,7 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) { if err != nil { s.logger.Error("failed to retrieve scouted characters", zap.Error(err)) - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } diff --git a/server/channelserver/handlers_house.go b/server/channelserver/handlers_house.go index 92d7c853e..280f27238 100644 --- a/server/channelserver/handlers_house.go +++ b/server/channelserver/handlers_house.go @@ -5,9 +5,37 @@ import ( ps "erupe-ce/common/pascalstring" "erupe-ce/common/stringsupport" "erupe-ce/network/mhfpacket" + "fmt" "go.uber.org/zap" + "io" + "time" ) +const warehouseNamesQuery = ` +SELECT +COALESCE(item0name, ''), +COALESCE(item1name, ''), +COALESCE(item2name, ''), +COALESCE(item3name, ''), +COALESCE(item4name, ''), +COALESCE(item5name, ''), +COALESCE(item6name, ''), +COALESCE(item7name, ''), +COALESCE(item8name, ''), +COALESCE(item9name, ''), +COALESCE(equip0name, ''), +COALESCE(equip1name, ''), +COALESCE(equip2name, ''), +COALESCE(equip3name, ''), +COALESCE(equip4name, ''), +COALESCE(equip5name, ''), +COALESCE(equip6name, ''), +COALESCE(equip7name, ''), +COALESCE(equip8name, ''), +COALESCE(equip9name, '') +FROM warehouse +` + func handleMsgMhfUpdateInterior(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUpdateInterior) _, err := s.server.db.Exec("UPDATE characters SET house=$1 WHERE id=$2", pkt.InteriorData, s.charID) @@ -64,7 +92,7 @@ func handleMsgMhfEnumerateHouse(s *Session, p mhfpacket.MHFPacket) { } case 3: house := HouseData{} - row := s.server.db.QueryRowx("SELECT id, hrp, gr, name FROM characters WHERE name=$1", pkt.Name) + row := s.server.db.QueryRowx("SELECT id, hrp, gr, name FROM characters WHERE name ILIKE $1", fmt.Sprintf(`%%%s%%`, pkt.Name)) err := row.StructScan(&house) if err != nil { panic(err) @@ -291,27 +319,238 @@ func handleMsgMhfSaveDecoMyset(s *Session, p mhfpacket.MHFPacket) { doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } +type Title struct { + ID uint16 `db:"id"` + Acquired time.Time `db:"unlocked_at"` + Updated time.Time `db:"updated_at"` +} + func handleMsgMhfEnumerateTitle(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateTitle) + var count uint16 bf := byteframe.NewByteFrame() - if pkt.CharID == 0 { - titleCount := 114 // all titles unlocked - bf.WriteUint16(uint16(titleCount)) // title count - bf.WriteUint16(0) // unk - for i := 0; i < titleCount; i++ { - bf.WriteUint16(uint16(i)) - bf.WriteUint16(0) // unk - bf.WriteUint32(0) // timestamp acquired - bf.WriteUint32(0) // timestamp updated - } - } else { - bf.WriteUint16(0) + bf.WriteUint16(0) + bf.WriteUint16(0) // Unk + rows, err := s.server.db.Queryx("SELECT id, unlocked_at, updated_at FROM titles WHERE char_id=$1", s.charID) + if err != nil { + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + return } + for rows.Next() { + title := &Title{} + err = rows.StructScan(&title) + if err != nil { + continue + } + count++ + bf.WriteUint16(title.ID) + bf.WriteUint16(0) // Unk + bf.WriteUint32(uint32(title.Acquired.Unix())) + bf.WriteUint32(uint32(title.Updated.Unix())) + } + bf.Seek(0, io.SeekStart) + bf.WriteUint16(count) doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } -func handleMsgMhfOperateWarehouse(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfAcquireTitle(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfAcquireTitle) + var exists int + err := s.server.db.QueryRow("SELECT count(*) FROM titles WHERE id=$1 AND char_id=$2", pkt.TitleID, s.charID).Scan(&exists) + if err != nil || exists == 0 { + s.server.db.Exec("INSERT INTO titles VALUES ($1, $2, now(), now())", pkt.TitleID, s.charID) + } else { + s.server.db.Exec("UPDATE titles SET updated_at=now()") + } + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} -func handleMsgMhfEnumerateWarehouse(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfResetTitle(s *Session, p mhfpacket.MHFPacket) {} -func handleMsgMhfUpdateWarehouse(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfOperateWarehouse(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfOperateWarehouse) + var t int + err := s.server.db.QueryRow("SELECT character_id FROM warehouse WHERE character_id=$1", s.charID).Scan(&t) + if err != nil { + s.server.db.Exec("INSERT INTO warehouse (character_id) VALUES ($1)", s.charID) + } + bf := byteframe.NewByteFrame() + bf.WriteUint8(pkt.Operation) + switch pkt.Operation { + case 0: + var count uint8 + itemNames := make([]string, 10) + equipNames := make([]string, 10) + s.server.db.QueryRow(fmt.Sprintf("%s WHERE character_id=$1", warehouseNamesQuery), s.charID).Scan(&itemNames[0], + &itemNames[1], &itemNames[2], &itemNames[3], &itemNames[4], &itemNames[5], &itemNames[6], &itemNames[7], &itemNames[8], &itemNames[9], &equipNames[0], + &equipNames[1], &equipNames[2], &equipNames[3], &equipNames[4], &equipNames[5], &equipNames[6], &equipNames[7], &equipNames[8], &equipNames[9]) + bf.WriteUint32(0) + bf.WriteUint16(10000) // Usages + temp := byteframe.NewByteFrame() + for i, name := range itemNames { + if len(name) > 0 { + count++ + temp.WriteUint8(0) + temp.WriteUint8(uint8(i)) + ps.Uint8(temp, name, true) + } + } + for i, name := range equipNames { + if len(name) > 0 { + count++ + temp.WriteUint8(1) + temp.WriteUint8(uint8(i)) + ps.Uint8(temp, name, true) + } + } + bf.WriteUint8(count) + bf.WriteBytes(temp.Data()) + case 1: + bf.WriteUint8(0) + case 2: + s.server.db.Exec(fmt.Sprintf("UPDATE warehouse SET %s%dname=$1 WHERE character_id=$2", pkt.BoxType, pkt.BoxIndex), pkt.Name, s.charID) + case 3: + bf.WriteUint32(0) // Usage renewal time, >1 = disabled + bf.WriteUint16(10000) // Usages + case 4: + bf.WriteUint32(0) + bf.WriteUint16(10000) // Usages + bf.WriteUint8(0) + } + // Opcodes + // 0 = Get box names + // 1 = Commit usage + // 2 = Rename + // 3 = Get usage limit + // 4 = Get gift box names (doesn't do anything?) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func addWarehouseGift(s *Session, boxType string, giftStack mhfpacket.WarehouseStack) { + giftBox := getWarehouseBox(s, boxType, 10) + if boxType == "item" { + exists := false + for i, stack := range giftBox { + if stack.ItemID == giftStack.ItemID { + exists = true + giftBox[i].Quantity += giftStack.Quantity + break + } + } + if exists == false { + giftBox = append(giftBox, giftStack) + } + } else { + giftBox = append(giftBox, giftStack) + } + s.server.db.Exec(fmt.Sprintf("UPDATE warehouse SET %s10=$1 WHERE character_id=$2", boxType), boxToBytes(giftBox, boxType), s.charID) +} + +func getWarehouseBox(s *Session, boxType string, boxIndex uint8) []mhfpacket.WarehouseStack { + var data []byte + s.server.db.QueryRow(fmt.Sprintf("SELECT %s%d FROM warehouse WHERE character_id=$1", boxType, boxIndex), s.charID).Scan(&data) + if len(data) > 0 { + box := byteframe.NewByteFrameFromBytes(data) + numStacks := box.ReadUint16() + stacks := make([]mhfpacket.WarehouseStack, numStacks) + for i := 0; i < int(numStacks); i++ { + if boxType == "item" { + stacks[i].ID = box.ReadUint32() + stacks[i].Index = box.ReadUint16() + stacks[i].ItemID = box.ReadUint16() + stacks[i].Quantity = box.ReadUint16() + box.ReadUint16() + } else { + stacks[i].ID = box.ReadUint32() + stacks[i].Index = box.ReadUint16() + stacks[i].EquipType = box.ReadUint16() + stacks[i].ItemID = box.ReadUint16() + stacks[i].Data = box.ReadBytes(56) + } + } + return stacks + } else { + return make([]mhfpacket.WarehouseStack, 0) + } +} + +func boxToBytes(stacks []mhfpacket.WarehouseStack, boxType string) []byte { + bf := byteframe.NewByteFrame() + bf.WriteUint16(uint16(len(stacks))) + for i, stack := range stacks { + if boxType == "item" { + bf.WriteUint32(stack.ID) + bf.WriteUint16(uint16(i + 1)) + bf.WriteUint16(stack.ItemID) + bf.WriteUint16(stack.Quantity) + bf.WriteUint16(0) + } else { + bf.WriteUint32(stack.ID) + bf.WriteUint16(uint16(i + 1)) + bf.WriteUint16(stack.EquipType) + bf.WriteUint16(stack.ItemID) + bf.WriteBytes(stack.Data) + } + } + bf.WriteUint16(0) + return bf.Data() +} + +func handleMsgMhfEnumerateWarehouse(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfEnumerateWarehouse) + box := getWarehouseBox(s, pkt.BoxType, pkt.BoxIndex) + if len(box) > 0 { + doAckBufSucceed(s, pkt.AckHandle, boxToBytes(box, pkt.BoxType)) + } else { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + } +} + +func handleMsgMhfUpdateWarehouse(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfUpdateWarehouse) + box := getWarehouseBox(s, pkt.BoxType, pkt.BoxIndex) + // Update existing stacks + var newStacks []mhfpacket.WarehouseStack + for _, update := range pkt.Updates { + exists := false + if pkt.BoxType == "item" { + for i, stack := range box { + if stack.Index == update.Index { + exists = true + box[i].Quantity = update.Quantity + break + } + } + } else { + for i, stack := range box { + if stack.Index == update.Index { + exists = true + box[i].ItemID = update.ItemID + break + } + } + } + if exists == false { + newStacks = append(newStacks, update) + } + } + // Append new stacks + for _, stack := range newStacks { + box = append(box, stack) + } + // Slice empty stacks + var cleanedBox []mhfpacket.WarehouseStack + for _, stack := range box { + if pkt.BoxType == "item" { + if stack.Quantity > 0 { + cleanedBox = append(cleanedBox, stack) + } + } else { + if stack.ItemID != 0 { + cleanedBox = append(cleanedBox, stack) + } + } + } + s.server.db.Exec(fmt.Sprintf("UPDATE warehouse SET %s%d=$1 WHERE character_id=$2", pkt.BoxType, pkt.BoxIndex), boxToBytes(cleanedBox, pkt.BoxType), s.charID) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} diff --git a/server/channelserver/handlers_mail.go b/server/channelserver/handlers_mail.go index 1e060b245..e31e90eed 100644 --- a/server/channelserver/handlers_mail.go +++ b/server/channelserver/handlers_mail.go @@ -25,21 +25,22 @@ type Mail struct { AttachedItemAmount uint16 `db:"attached_item_amount"` CreatedAt time.Time `db:"created_at"` IsGuildInvite bool `db:"is_guild_invite"` + IsSystemMessage bool `db:"is_sys_message"` SenderName string `db:"sender_name"` } func (m *Mail) Send(s *Session, transaction *sql.Tx) error { query := ` - INSERT INTO mail (sender_id, recipient_id, subject, body, attached_item, attached_item_amount, is_guild_invite) - VALUES ($1, $2, $3, $4, $5, $6, $7) + INSERT INTO mail (sender_id, recipient_id, subject, body, attached_item, attached_item_amount, is_guild_invite, is_sys_message) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ` var err error if transaction == nil { - _, err = s.server.db.Exec(query, m.SenderID, m.RecipientID, m.Subject, m.Body, m.AttachedItemID, m.AttachedItemAmount, m.IsGuildInvite) + _, err = s.server.db.Exec(query, m.SenderID, m.RecipientID, m.Subject, m.Body, m.AttachedItemID, m.AttachedItemAmount, m.IsGuildInvite, m.IsSystemMessage) } else { - _, err = transaction.Exec(query, m.SenderID, m.RecipientID, m.Subject, m.Body, m.AttachedItemID, m.AttachedItemAmount, m.IsGuildInvite) + _, err = transaction.Exec(query, m.SenderID, m.RecipientID, m.Subject, m.Body, m.AttachedItemID, m.AttachedItemAmount, m.IsGuildInvite, m.IsSystemMessage) } if err != nil { @@ -53,6 +54,7 @@ func (m *Mail) Send(s *Session, transaction *sql.Tx) error { zap.Uint16("itemID", m.AttachedItemID), zap.Uint16("itemAmount", m.AttachedItemAmount), zap.Bool("isGuildInvite", m.IsGuildInvite), + zap.Bool("isSystemMessage", m.IsSystemMessage), ) return err } @@ -141,6 +143,7 @@ func GetMailListForCharacter(s *Session, charID uint32) ([]Mail, error) { m.attached_item_amount, m.created_at, m.is_guild_invite, + m.is_sys_message, m.deleted, m.locked, c.name as sender_name @@ -189,6 +192,7 @@ func GetMailByID(s *Session, ID int) (*Mail, error) { m.attached_item_amount, m.created_at, m.is_guild_invite, + m.is_sys_message, m.deleted, m.locked, c.name as sender_name @@ -215,16 +219,10 @@ func GetMailByID(s *Session, ID int) (*Mail, error) { } func SendMailNotification(s *Session, m *Mail, recipient *Session) { - senderName, err := getCharacterName(s, m.SenderID) - - if err != nil { - panic(err) - } - bf := byteframe.NewByteFrame() notification := &binpacket.MsgBinMailNotify{ - SenderName: senderName, + SenderName: getCharacterName(s, m.SenderID), } notification.Build(bf) @@ -241,7 +239,7 @@ func SendMailNotification(s *Session, m *Mail, recipient *Session) { recipient.QueueSendMHF(castedBinary) } -func getCharacterName(s *Session, charID uint32) (string, error) { +func getCharacterName(s *Session, charID uint32) string { row := s.server.db.QueryRow("SELECT name FROM characters WHERE id = $1", charID) charName := "" @@ -249,10 +247,9 @@ func getCharacterName(s *Session, charID uint32) (string, error) { err := row.Scan(&charName) if err != nil { - return "", err + return "" } - - return charName, nil + return charName } func handleMsgMhfReadMail(s *Session, p mhfpacket.MHFPacket) { @@ -325,8 +322,9 @@ func handleMsgMhfListMail(s *Session, p mhfpacket.MHFPacket) { flags |= 0x02 } - // System message, hides ID - // flags |= 0x04 + if m.IsSystemMessage { + flags |= 0x04 + } // Workaround until EN mail items are patched if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.DisableMailItems { diff --git a/server/channelserver/handlers_mercenary.go b/server/channelserver/handlers_mercenary.go index 606579fe5..31bc26249 100644 --- a/server/channelserver/handlers_mercenary.go +++ b/server/channelserver/handlers_mercenary.go @@ -9,7 +9,6 @@ import ( "go.uber.org/zap" "io" "io/ioutil" - "math/rand" "os" "path/filepath" ) @@ -40,7 +39,7 @@ func handleMsgMhfLoadPartner(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfSavePartner(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSavePartner) - dumpSaveData(s, pkt.RawDataPayload, "_partner") + dumpSaveData(s, pkt.RawDataPayload, "partner") _, err := s.server.db.Exec("UPDATE characters SET partner=$1 WHERE id=$2", pkt.RawDataPayload, s.charID) if err != nil { @@ -76,7 +75,7 @@ func handleMsgMhfLoadHunterNavi(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSaveHunterNavi) - dumpSaveData(s, pkt.RawDataPayload, "_hunternavi") + dumpSaveData(s, pkt.RawDataPayload, "hunternavi") if pkt.IsDataDiff { var data []byte @@ -122,75 +121,87 @@ func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfMercenaryHuntdata(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfMercenaryHuntdata) - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0x0A)) + if pkt.Unk0 == 1 { + // Format: + // uint8 Hunts + // struct Hunt + // uint32 HuntID + // uint32 MonID + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1)) + } else { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0)) + } } -func handleMsgMhfEnumerateMercenaryLog(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfEnumerateMercenaryLog(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfEnumerateMercenaryLog) + bf := byteframe.NewByteFrame() + bf.WriteUint32(0) + // Format: + // struct Log + // uint32 Timestamp + // []byte Name (len 18) + // uint8 Unk + // uint8 Unk + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} func handleMsgMhfCreateMercenary(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfCreateMercenary) bf := byteframe.NewByteFrame() - bf.WriteUint32(0x00) // Unk - bf.WriteUint32(rand.Uint32()) // Partner ID? + var nextID uint32 + s.server.db.QueryRow("SELECT nextval('rasta_id_seq')").Scan(&nextID) + + bf.WriteUint32(nextID) // New MercID + bf.WriteUint32(0xDEADBEEF) // Unk doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) } func handleMsgMhfSaveMercenary(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSaveMercenary) - bf := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload) - GCPValue := bf.ReadUint32() - _ = bf.ReadUint32() // unk - MercDataSize := bf.ReadUint32() - MercData := bf.ReadBytes(uint(MercDataSize)) - _ = bf.ReadUint32() // unk - - if MercDataSize > 0 { - // the save packet has an extra null byte after its size - _, err := s.server.db.Exec("UPDATE characters SET savemercenary=$1 WHERE id=$2", MercData[:MercDataSize], s.charID) - if err != nil { - s.logger.Fatal("Failed to update savemercenary and gcp in db", zap.Error(err)) - } - } - // gcp value is always present regardless - _, err := s.server.db.Exec("UPDATE characters SET gcp=$1 WHERE id=$2", GCPValue, s.charID) - if err != nil { - s.logger.Fatal("Failed to update savemercenary and gcp in db", zap.Error(err)) + if len(pkt.MercData) > 0 { + s.server.db.Exec("UPDATE characters SET savemercenary=$1 WHERE id=$2", pkt.MercData, s.charID) } + s.server.db.Exec("UPDATE characters SET gcp=$1 WHERE id=$2", pkt.GCP, s.charID) doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfReadMercenaryW) + if pkt.Unk0 { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 2)) + return + } var data []byte var gcp uint32 - // still has issues - err := s.server.db.QueryRow("SELECT savemercenary FROM characters WHERE id = $1", s.charID).Scan(&data) - if err != nil { - s.logger.Fatal("Failed to get savemercenary data from db", zap.Error(err)) - } - - err = s.server.db.QueryRow("SELECT COALESCE(gcp, 0) FROM characters WHERE id = $1", s.charID).Scan(&gcp) - if err != nil { - panic(err) - } - if len(data) == 0 { - data = []byte{0x00} - } + s.server.db.QueryRow("SELECT savemercenary FROM characters WHERE id = $1", s.charID).Scan(&data) + s.server.db.QueryRow("SELECT COALESCE(gcp, 0) FROM characters WHERE id = $1", s.charID).Scan(&gcp) resp := byteframe.NewByteFrame() - resp.WriteBytes(data) - resp.WriteUint16(0) + if len(data) == 0 { + resp.WriteBytes(make([]byte, 3)) + } else { + resp.WriteBytes(data[1:]) + resp.WriteUint32(0) // Unk + } resp.WriteUint32(gcp) doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } func handleMsgMhfReadMercenaryM(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfReadMercenaryM) - // accessing actual rasta data of someone else still unsure of the formatting of this - doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) + var data []byte + s.server.db.QueryRow("SELECT savemercenary FROM characters WHERE id = $1", pkt.CharID).Scan(&data) + resp := byteframe.NewByteFrame() + if len(data) == 0 { + resp.WriteBool(false) + } else { + resp.WriteBytes(data[4:]) + } + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } func handleMsgMhfContractMercenary(s *Session, p mhfpacket.MHFPacket) {} @@ -217,6 +228,7 @@ func handleMsgMhfLoadOtomoAirou(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfSaveOtomoAirou(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSaveOtomoAirou) + dumpSaveData(s, pkt.RawDataPayload, "otomoairou") decomp, err := nullcomp.Decompress(pkt.RawDataPayload[1:]) if err != nil { s.logger.Error("Failed to decompress airou", zap.Error(err)) diff --git a/server/channelserver/handlers_register.go b/server/channelserver/handlers_register.go index 3dd65cf6a..2c44cc6db 100644 --- a/server/channelserver/handlers_register.go +++ b/server/channelserver/handlers_register.go @@ -11,7 +11,7 @@ func handleMsgSysOperateRegister(s *Session, p mhfpacket.MHFPacket) { bf := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload) s.server.raviente.Lock() switch pkt.SemaphoreID { - case 3: + case 4: resp := byteframe.NewByteFrame() size := 6 for i := 0; i < len(bf.Data())-1; i += size { @@ -49,7 +49,7 @@ func handleMsgSysOperateRegister(s *Session, p mhfpacket.MHFPacket) { } resp.WriteUint8(0) doAckBufSucceed(s, pkt.AckHandle, resp.Data()) - case 4: + case 5: resp := byteframe.NewByteFrame() size := 6 for i := 0; i < len(bf.Data())-1; i += size { @@ -74,7 +74,7 @@ func handleMsgSysOperateRegister(s *Session, p mhfpacket.MHFPacket) { } resp.WriteUint8(0) doAckBufSucceed(s, pkt.AckHandle, resp.Data()) - case 5: + case 6: resp := byteframe.NewByteFrame() size := 6 for i := 0; i < len(bf.Data())-1; i += size { @@ -242,15 +242,15 @@ func handleMsgSysLoadRegister(s *Session, p mhfpacket.MHFPacket) { func (s *Session) notifyRavi() { var temp mhfpacket.MHFPacket raviNotif := byteframe.NewByteFrame() - temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 3} - raviNotif.WriteUint16(uint16(temp.Opcode())) - temp.Build(raviNotif, s.clientContext) temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 4} raviNotif.WriteUint16(uint16(temp.Opcode())) temp.Build(raviNotif, s.clientContext) temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 5} raviNotif.WriteUint16(uint16(temp.Opcode())) temp.Build(raviNotif, s.clientContext) + temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 6} + raviNotif.WriteUint16(uint16(temp.Opcode())) + temp.Build(raviNotif, s.clientContext) raviNotif.WriteUint16(0x0010) // End it. sema := getRaviSemaphore(s) if sema != "" { @@ -262,7 +262,7 @@ func (s *Session) notifyRavi() { func getRaviSemaphore(s *Session) string { for _, semaphore := range s.server.semaphore { - if strings.HasPrefix(semaphore.id_semaphore, "hs_l0u3B5") && strings.HasSuffix(semaphore.id_semaphore, "3") { + if strings.HasPrefix(semaphore.id_semaphore, "hs_l0u3B5") && strings.HasSuffix(semaphore.id_semaphore, "4") { return semaphore.id_semaphore } } diff --git a/server/channelserver/handlers_rengoku.go b/server/channelserver/handlers_rengoku.go index d7b60686b..6fdce7c22 100644 --- a/server/channelserver/handlers_rengoku.go +++ b/server/channelserver/handlers_rengoku.go @@ -1,6 +1,8 @@ package channelserver import ( + ps "erupe-ce/common/pascalstring" + "fmt" "io/ioutil" "path/filepath" @@ -17,7 +19,19 @@ func handleMsgMhfSaveRengokuData(s *Session, p mhfpacket.MHFPacket) { if err != nil { s.logger.Fatal("Failed to update rengokudata savedata in db", zap.Error(err)) } - + bf := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload) + bf.Seek(71, 0) + maxStageMp := bf.ReadUint32() + maxScoreMp := bf.ReadUint32() + bf.Seek(4, 1) + maxStageSp := bf.ReadUint32() + maxScoreSp := bf.ReadUint32() + var t int + err = s.server.db.QueryRow("SELECT character_id FROM rengoku_score WHERE character_id=$1", s.charID).Scan(&t) + if err != nil { + s.server.db.Exec("INSERT INTO rengoku_score (character_id) VALUES ($1)", s.charID) + } + s.server.db.Exec("UPDATE rengoku_score SET max_stages_mp=$1, max_points_mp=$2, max_stages_sp=$3, max_points_sp=$4 WHERE character_id=$5", maxStageMp, maxScoreMp, maxStageSp, maxScoreSp, s.charID) doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } @@ -81,16 +95,191 @@ func handleMsgMhfGetRengokuBinary(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, data) } +const rengokuScoreQuery = ` +SELECT max_stages_mp, max_points_mp, max_stages_sp, max_points_sp, c.name, gc.guild_id +FROM rengoku_score rs +LEFT JOIN characters c ON c.id = rs.character_id +LEFT JOIN guild_characters gc ON gc.character_id = rs.character_id +` + +type RengokuScore struct { + Name string `db:"name"` + GuildID int `db:"guild_id"` + MaxStagesMP uint32 `db:"max_stages_mp"` + MaxPointsMP uint32 `db:"max_points_mp"` + MaxStagesSP uint32 `db:"max_stages_sp"` + MaxPointsSP uint32 `db:"max_points_sp"` +} + func handleMsgMhfEnumerateRengokuRanking(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateRengokuRanking) - doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) + + guild, _ := GetGuildInfoByCharacterId(s, s.charID) + isApplicant, _ := guild.HasApplicationForCharID(s, s.charID) + if isApplicant { + guild = nil + } + + var score RengokuScore + i := uint32(1) + bf := byteframe.NewByteFrame() + scoreData := byteframe.NewByteFrame() + switch pkt.Leaderboard { + case 0: // Max stage overall MP + rows, _ := s.server.db.Queryx(fmt.Sprintf("%s ORDER BY max_stages_mp DESC", rengokuScoreQuery)) + for rows.Next() { + rows.StructScan(&score) + if score.Name == s.Name { + bf.WriteUint32(i) + bf.WriteUint32(score.MaxStagesMP) + ps.Uint8(bf, s.Name, true) + ps.Uint8(bf, "", false) + } + scoreData.WriteUint32(i) + scoreData.WriteUint32(score.MaxStagesMP) + ps.Uint8(scoreData, score.Name, true) + ps.Uint8(scoreData, "", false) + i++ + } + case 1: // Max RdP overall MP + rows, _ := s.server.db.Queryx(fmt.Sprintf("%s ORDER BY max_points_mp DESC", rengokuScoreQuery)) + for rows.Next() { + rows.StructScan(&score) + if score.Name == s.Name { + bf.WriteUint32(i) + bf.WriteUint32(score.MaxPointsMP) + ps.Uint8(bf, s.Name, true) + ps.Uint8(bf, "", false) + } + scoreData.WriteUint32(i) + scoreData.WriteUint32(score.MaxPointsMP) + ps.Uint8(scoreData, score.Name, true) + ps.Uint8(scoreData, "", false) + i++ + } + case 2: // Max stage guild MP + if guild != nil { + rows, _ := s.server.db.Queryx(fmt.Sprintf("%s WHERE guild_id=$1 ORDER BY max_stages_mp DESC", rengokuScoreQuery), guild.ID) + for rows.Next() { + rows.StructScan(&score) + if score.Name == s.Name { + bf.WriteUint32(i) + bf.WriteUint32(score.MaxStagesMP) + ps.Uint8(bf, s.Name, true) + ps.Uint8(bf, "", false) + } + scoreData.WriteUint32(i) + scoreData.WriteUint32(score.MaxStagesMP) + ps.Uint8(scoreData, score.Name, true) + ps.Uint8(scoreData, "", false) + i++ + } + } else { + bf.WriteBytes(make([]byte, 11)) + } + case 3: // Max RdP guild MP + if guild != nil { + rows, _ := s.server.db.Queryx(fmt.Sprintf("%s WHERE guild_id=$1 ORDER BY max_points_mp DESC", rengokuScoreQuery), guild.ID) + for rows.Next() { + rows.StructScan(&score) + if score.Name == s.Name { + bf.WriteUint32(i) + bf.WriteUint32(score.MaxPointsMP) + ps.Uint8(bf, s.Name, true) + ps.Uint8(bf, "", false) + } + scoreData.WriteUint32(i) + scoreData.WriteUint32(score.MaxPointsMP) + ps.Uint8(scoreData, score.Name, true) + ps.Uint8(scoreData, "", false) + i++ + } + } else { + bf.WriteBytes(make([]byte, 11)) + } + case 4: // Max stage overall SP + rows, _ := s.server.db.Queryx(fmt.Sprintf("%s ORDER BY max_stages_sp DESC", rengokuScoreQuery)) + for rows.Next() { + rows.StructScan(&score) + if score.Name == s.Name { + bf.WriteUint32(i) + bf.WriteUint32(score.MaxStagesSP) + ps.Uint8(bf, s.Name, true) + ps.Uint8(bf, "", false) + } + scoreData.WriteUint32(i) + scoreData.WriteUint32(score.MaxStagesSP) + ps.Uint8(scoreData, score.Name, true) + ps.Uint8(scoreData, "", false) + i++ + } + case 5: // Max RdP overall SP + rows, _ := s.server.db.Queryx(fmt.Sprintf("%s ORDER BY max_points_sp DESC", rengokuScoreQuery)) + for rows.Next() { + rows.StructScan(&score) + if score.Name == s.Name { + bf.WriteUint32(i) + bf.WriteUint32(score.MaxPointsSP) + ps.Uint8(bf, s.Name, true) + ps.Uint8(bf, "", false) + } + scoreData.WriteUint32(i) + scoreData.WriteUint32(score.MaxPointsSP) + ps.Uint8(scoreData, score.Name, true) + ps.Uint8(scoreData, "", false) + i++ + } + case 6: // Max stage guild SP + if guild != nil { + rows, _ := s.server.db.Queryx(fmt.Sprintf("%s WHERE guild_id=$1 ORDER BY max_stages_sp DESC", rengokuScoreQuery), guild.ID) + for rows.Next() { + rows.StructScan(&score) + if score.Name == s.Name { + bf.WriteUint32(i) + bf.WriteUint32(score.MaxStagesSP) + ps.Uint8(bf, s.Name, true) + ps.Uint8(bf, "", false) + } + scoreData.WriteUint32(i) + scoreData.WriteUint32(score.MaxStagesSP) + ps.Uint8(scoreData, score.Name, true) + ps.Uint8(scoreData, "", false) + i++ + } + } else { + bf.WriteBytes(make([]byte, 11)) + } + case 7: // Max RdP guild SP + if guild != nil { + rows, _ := s.server.db.Queryx(fmt.Sprintf("%s WHERE guild_id=$1 ORDER BY max_points_sp DESC", rengokuScoreQuery), guild.ID) + for rows.Next() { + rows.StructScan(&score) + if score.Name == s.Name { + bf.WriteUint32(i) + bf.WriteUint32(score.MaxPointsSP) + ps.Uint8(bf, s.Name, true) + ps.Uint8(bf, "", false) + } + scoreData.WriteUint32(i) + scoreData.WriteUint32(score.MaxPointsSP) + ps.Uint8(scoreData, score.Name, true) + ps.Uint8(scoreData, "", false) + i++ + } + } else { + bf.WriteBytes(make([]byte, 11)) + } + } + bf.WriteUint8(uint8(i) - 1) + bf.WriteBytes(scoreData.Data()) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } func handleMsgMhfGetRengokuRankingRank(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetRengokuRankingRank) - - resp := byteframe.NewByteFrame() - resp.WriteBytes([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) - - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) + // What is this for? + bf := byteframe.NewByteFrame() + bf.WriteUint32(0) // Max stage overall MP rank + bf.WriteUint32(0) // Max RdP overall MP rank + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } diff --git a/server/channelserver/handlers_semaphore.go b/server/channelserver/handlers_semaphore.go index 6659bd2da..cbf1a0a7f 100644 --- a/server/channelserver/handlers_semaphore.go +++ b/server/channelserver/handlers_semaphore.go @@ -45,12 +45,10 @@ func destructEmptySemaphores(s *Session) { } func releaseRaviSemaphore(s *Session, sema *Semaphore) { - if !strings.HasSuffix(sema.id_semaphore, "5") { - delete(sema.reservedClientSlots, s.charID) - delete(sema.clients, s) - } - if len(sema.reservedClientSlots) == 0 && len(sema.clients) == 0 { - s.logger.Debug("Raviente semaphore is empty, resetting") + delete(sema.reservedClientSlots, s.charID) + delete(sema.clients, s) + if strings.HasSuffix(sema.id_semaphore, "2") && len(sema.clients) == 0 { + s.logger.Debug("Main raviente semaphore is empty, resetting") resetRavi(s) } } @@ -91,7 +89,7 @@ func handleMsgSysCreateAcquireSemaphore(s *Session, p mhfpacket.MHFPacket) { suffix, _ := strconv.ParseUint(pkt.SemaphoreID[len(pkt.SemaphoreID)-1:], 10, 32) s.server.semaphore[SemaphoreID] = &Semaphore{ id_semaphore: pkt.SemaphoreID, - id: uint32(suffix), + id: uint32(suffix + 1), clients: make(map[*Session]uint32), reservedClientSlots: make(map[uint32]interface{}), maxPlayers: 32, @@ -119,7 +117,7 @@ func handleMsgSysCreateAcquireSemaphore(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint32(newSemaphore.id) doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) } else { - doAckSimpleFail(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) + doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } } diff --git a/server/channelserver/handlers_stage.go b/server/channelserver/handlers_stage.go index 4bf19d114..906480be7 100644 --- a/server/channelserver/handlers_stage.go +++ b/server/channelserver/handlers_stage.go @@ -135,7 +135,6 @@ func removeSessionFromStage(s *Session) { // Remove client from old stage. s.stage.Lock() delete(s.stage.clients, s) - delete(s.stage.reservedClientSlots, s.charID) // Delete old stage objects owned by the client. s.logger.Info("Sending notification to old stage clients") @@ -157,6 +156,7 @@ func handleMsgSysEnterStage(s *Session, p mhfpacket.MHFPacket) { if s.stageID == "" { s.stageMoveStack.Set(pkt.StageID) } else { + s.stage.reservedClientSlots[s.charID] = false s.stageMoveStack.Push(s.stageID) s.stageMoveStack.Lock() } @@ -175,11 +175,18 @@ func handleMsgSysBackStage(s *Session, p mhfpacket.MHFPacket) { // Transfer back to the saved stage ID before the previous move or enter. s.stageMoveStack.Unlock() backStage, err := s.stageMoveStack.Pop() - if err != nil { panic(err) } + if _, exists := s.stage.reservedClientSlots[s.charID]; exists { + delete(s.stage.reservedClientSlots, s.charID) + } + + if _, exists := s.server.stages[backStage].reservedClientSlots[s.charID]; exists { + delete(s.server.stages[backStage].reservedClientSlots, s.charID) + } + doStageTransfer(s, pkt.AckHandle, backStage) } diff --git a/server/channelserver/handlers_tactics.go b/server/channelserver/handlers_tactics.go index 585c0ef79..4991ce846 100644 --- a/server/channelserver/handlers_tactics.go +++ b/server/channelserver/handlers_tactics.go @@ -2,7 +2,6 @@ package channelserver import ( "encoding/hex" - "erupe-ce/network/mhfpacket" ) @@ -58,11 +57,3 @@ func handleMsgMhfGetUdTacticsRanking(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfSetUdTacticsFollower(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfGetUdTacticsLog(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfGetCafeDurationBonusInfo(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfReceiveCafeDurationBonus(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfPostCafeDurationBonusReceived(s *Session, p mhfpacket.MHFPacket) {} diff --git a/server/channelserver/handlers_users.go b/server/channelserver/handlers_users.go index 6db6613c0..3bebcc97a 100644 --- a/server/channelserver/handlers_users.go +++ b/server/channelserver/handlers_users.go @@ -3,7 +3,6 @@ package channelserver import ( "fmt" - "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" ) @@ -17,7 +16,8 @@ func handleMsgSysSetUserBinary(s *Session, p mhfpacket.MHFPacket) { s.server.userBinaryParts[userBinaryPartID{charID: s.charID, index: pkt.BinaryType}] = pkt.RawDataPayload s.server.userBinaryPartsLock.Unlock() - err := s.server.db.QueryRow("SELECT type2 FROM user_binaries WHERE id=$1", s.charID) + var exists []byte + err := s.server.db.QueryRow("SELECT type2 FROM user_binaries WHERE id=$1", s.charID).Scan(&exists) if err != nil { s.server.db.Exec("INSERT INTO user_binaries (id) VALUES ($1)", s.charID) } @@ -39,25 +39,18 @@ func handleMsgSysGetUserBinary(s *Session, p mhfpacket.MHFPacket) { s.server.userBinaryPartsLock.RLock() defer s.server.userBinaryPartsLock.RUnlock() data, ok := s.server.userBinaryParts[userBinaryPartID{charID: pkt.CharID, index: pkt.BinaryType}] - resp := byteframe.NewByteFrame() // If we can't get the real data, try to get it from the database. if !ok { - var data []byte - rows, _ := s.server.db.Queryx(fmt.Sprintf("SELECT type%d FROM user_binaries WHERE id=$1", pkt.BinaryType), pkt.CharID) - for rows.Next() { - rows.Scan(&data) - resp.WriteBytes(data) - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) - return + err := s.server.db.QueryRow(fmt.Sprintf("SELECT type%d FROM user_binaries WHERE id=$1", pkt.BinaryType), pkt.CharID).Scan(&data) + if err != nil { + doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) + } else { + doAckBufSucceed(s, pkt.AckHandle, data) } - doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) - return } else { - resp.WriteBytes(data) + doAckBufSucceed(s, pkt.AckHandle, data) } - - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } func handleMsgSysNotifyUserBinary(s *Session, p mhfpacket.MHFPacket) {} diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index a23ec2834..82f29d03a 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -11,27 +11,11 @@ import ( "erupe-ce/network/binpacket" "erupe-ce/network/mhfpacket" "erupe-ce/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 { ID uint16 @@ -80,8 +64,7 @@ type Server struct { // Discord chat integration discordBot *discordbot.DiscordBot - name string - enable bool + name string raviente *Raviente } @@ -154,43 +137,30 @@ func NewServer(config *Config) *Server { stages: make(map[string]*Stage), userBinaryParts: make(map[userBinaryPartID][]byte), semaphore: make(map[string]*Semaphore), - semaphoreIndex: 5, + semaphoreIndex: 7, discordBot: config.DiscordBot, name: config.Name, - enable: config.Enable, raviente: NewRaviente(), } // Mezeporta s.stages["sl1Ns200p0a0u0"] = NewStage("sl1Ns200p0a0u0") - // Guild Hall LV1 - s.stages["sl1Ns202p0a0u0"] = NewStage("sl1Ns202p0a0u0") - - // Guild Hall LV2 - s.stages["sl1Ns203p0a0u0"] = NewStage("sl1Ns203p0a0u0") - - // Guild Hall LV3 - s.stages["sl1Ns204p0a0u0"] = NewStage("sl1Ns204p0a0u0") - - // Pugi Farm - s.stages["sl1Ns205p0a0u0"] = NewStage("sl1Ns205p0a0u0") - // Rasta bar stage s.stages["sl1Ns211p0a0u0"] = NewStage("sl1Ns211p0a0u0") // Pallone Carvan s.stages["sl1Ns260p0a0u0"] = NewStage("sl1Ns260p0a0u0") - // Gook Farm - s.stages["sl1Ns265p0a0u0"] = NewStage("sl1Ns265p0a0u0") + // Pallone Guest House 1st Floor + s.stages["sl1Ns262p0a0u0"] = NewStage("sl1Ns262p0a0u0") + + // Pallone Guest House 2nd Floor + s.stages["sl1Ns263p0a0u0"] = NewStage("sl1Ns263p0a0u0") // Diva fountain / prayer fountain. s.stages["sl2Ns379p0a0u0"] = NewStage("sl2Ns379p0a0u0") - // Diva Hall - s.stages["sl1Ns445p0a0u0"] = NewStage("sl1Ns445p0a0u0") - // MezFes s.stages["sl1Ns462p0a0u0"] = NewStage("sl1Ns462p0a0u0") @@ -367,26 +337,18 @@ func (s *Server) BroadcastRaviente(ip uint32, port uint16, stage []byte, _type u func (s *Server) DiscordChannelSend(charName string, content string) { if s.erupeConfig.Discord.Enabled && s.discordBot != nil { - message := fmt.Sprintf("**%s** : %s", charName, content) + message := fmt.Sprintf("**%s**: %s", charName, content) s.discordBot.RealtimeChannelSend(message) } } func (s *Server) FindSessionByCharID(charID uint32) *Session { for _, c := range s.Channels { - c.stagesLock.RLock() - for _, stage := range c.stages { - stage.RLock() - for client := range stage.clients { - if client.charID == charID { - stage.RUnlock() - c.stagesLock.RUnlock() - return client - } + for _, session := range c.sessions { + if session.charID == charID { + return session } - stage.RUnlock() } - c.stagesLock.RUnlock() } return nil } @@ -413,6 +375,9 @@ func (s *Server) NextSemaphoreID() uint32 { for { exists := false s.semaphoreIndex = s.semaphoreIndex + 1 + if s.semaphoreIndex == 0 { + s.semaphoreIndex = 7 // Skip reserved indexes + } for _, semaphore := range s.semaphore { if semaphore.id == s.semaphoreIndex { exists = true diff --git a/server/channelserver/sys_stage.go b/server/channelserver/sys_stage.go index 9a968bf3f..827b2d6be 100644 --- a/server/channelserver/sys_stage.go +++ b/server/channelserver/sys_stage.go @@ -98,35 +98,6 @@ func (s *Stage) isQuest() bool { return len(s.reservedClientSlots) > 0 } -func (s *Stage) GetName() string { - switch s.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) NextObjectID() uint32 { s.objectIndex = s.objectIndex + 1 // Objects beyond 127 do not duplicate correctly diff --git a/server/channelserver/sys_timefix.go b/server/channelserver/sys_timefix.go index 839382053..4cde6a319 100644 --- a/server/channelserver/sys_timefix.go +++ b/server/channelserver/sys_timefix.go @@ -31,6 +31,16 @@ func Time_Current_Midnight() time.Time { return time.Date(baseTime.Year(), baseTime.Month(), baseTime.Day(), 0, 0, 0, 0, baseTime.Location()) } +func TimeWeekStart() time.Time { + midnight := Time_Current_Midnight() + offset := (int(midnight.Weekday()) - 1) * -24 + return midnight.Add(time.Hour * time.Duration(offset)) +} + +func TimeWeekNext() time.Time { + return TimeWeekStart().Add(time.Hour * 24 * 7) +} + func Time_Current_Week_uint8() uint8 { baseTime := time.Now().In(time.FixedZone(fmt.Sprintf("UTC+%d", Offset), Offset*60*60)).AddDate(YearAdjust, MonthAdjust, DayAdjust) diff --git a/server/discordbot/discord_bot.go b/server/discordbot/discord_bot.go index df817d50a..fc5c41ce8 100644 --- a/server/discordbot/discord_bot.go +++ b/server/discordbot/discord_bot.go @@ -1,11 +1,10 @@ package discordbot import ( - "regexp" - "erupe-ce/config" "github.com/bwmarrin/discordgo" "go.uber.org/zap" + "regexp" ) type DiscordBot struct { @@ -16,12 +15,12 @@ type DiscordBot struct { RealtimeChannel *discordgo.Channel } -type DiscordBotOptions struct { +type Options struct { Config *config.Config Logger *zap.Logger } -func NewDiscordBot(options DiscordBotOptions) (discordBot *DiscordBot, err error) { +func NewDiscordBot(options Options) (discordBot *DiscordBot, err error) { session, err := discordgo.New("Bot " + options.Config.Discord.BotToken) if err != nil { @@ -29,13 +28,6 @@ func NewDiscordBot(options DiscordBotOptions) (discordBot *DiscordBot, err error 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 { @@ -47,7 +39,6 @@ func NewDiscordBot(options DiscordBotOptions) (discordBot *DiscordBot, err error config: options.Config, logger: options.Logger, Session: session, - MainGuild: mainGuild, RealtimeChannel: realtimeChannel, } @@ -60,21 +51,10 @@ func (bot *DiscordBot) Start() (err error) { 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) @@ -90,17 +70,7 @@ func (bot *DiscordBot) NormalizeDiscordMessage(message 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) + return result } func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) { diff --git a/server/signserver/dbutils.go b/server/signserver/dbutils.go index 8f1b89a01..d39d96c92 100644 --- a/server/signserver/dbutils.go +++ b/server/signserver/dbutils.go @@ -29,7 +29,7 @@ func (s *Server) newUserChara(username string) error { INSERT INTO characters ( user_id, is_female, is_new_character, name, unk_desc_string, hrp, gr, weapon_type, last_login) - VALUES($1, False, True, '', '', 1, 0, 0, $2)`, + VALUES($1, False, True, '', '', 0, 0, 0, $2)`, id, uint32(time.Now().Unix()), ) @@ -47,7 +47,7 @@ func (s *Server) registerDBAccount(username string, password string) error { return err } - _, err = s.db.Exec("INSERT INTO users (username, password) VALUES ($1, $2)", username, string(passwordHash)) + _, err = s.db.Exec("INSERT INTO users (username, password, return_expires) VALUES ($1, $2, $3)", username, string(passwordHash), time.Now().Add(time.Hour*24*30)) if err != nil { return err } @@ -88,13 +88,30 @@ type character struct { func (s *Server) getCharactersForUser(uid int) ([]character, error) { characters := make([]character, 0) - err := s.db.Select(&characters, "SELECT id, is_female, is_new_character, name, unk_desc_string, hrp, gr, weapon_type, last_login FROM characters WHERE user_id = $1 AND deleted = false", uid) + err := s.db.Select(&characters, "SELECT id, is_female, is_new_character, name, unk_desc_string, hrp, gr, weapon_type, last_login FROM characters WHERE user_id = $1 AND deleted = false ORDER BY id ASC", uid) if err != nil { return nil, err } return characters, nil } +func (s *Server) getReturnExpiry(uid int) time.Time { + var returnExpiry, lastLogin time.Time + s.db.Get(&lastLogin, "SELECT COALESCE(last_login, now()) FROM users WHERE id=$1", uid) + if time.Now().Add((time.Hour * 24) * -90).After(lastLogin) { + returnExpiry = time.Now().Add(time.Hour * 24 * 30) + s.db.Exec("UPDATE users SET return_expires=$1 WHERE id=$2", returnExpiry, uid) + } else { + err := s.db.Get(&returnExpiry, "SELECT return_expires FROM users WHERE id=$1", uid) + if err != nil { + returnExpiry = time.Now() + s.db.Exec("UPDATE users SET return_expires=$1 WHERE id=$2", returnExpiry, uid) + } + } + s.db.Exec("UPDATE users SET last_login=$1 WHERE id=$2", time.Now(), uid) + return returnExpiry +} + func (s *Server) getLastCID(uid int) uint32 { var lastPlayed uint32 _ = s.db.QueryRow("SELECT last_character FROM users WHERE id=$1", uid).Scan(&lastPlayed) diff --git a/server/signserver/dsgn_resp.go b/server/signserver/dsgn_resp.go index 650188000..a169d93a1 100644 --- a/server/signserver/dsgn_resp.go +++ b/server/signserver/dsgn_resp.go @@ -28,6 +28,8 @@ func randSeq(n int) string { } func (s *Session) makeSignInResp(uid int) []byte { + returnExpiry := s.server.getReturnExpiry(uid) + // Get the characters from the DB. chars, err := s.server.getCharactersForUser(uid) if err != nil { @@ -115,15 +117,7 @@ func (s *Session) makeSignInResp(uid int) []byte { bf.WriteUint8(0x00) bf.WriteUint32(0xCA110001) bf.WriteUint32(0x4E200000) - - returning := false - // return course end time - if returning { - bf.WriteUint32(uint32(channelserver.Time_Current_Adjusted().Add(30 * 24 * time.Hour).Unix())) - } else { - bf.WriteUint32(0) - } - + bf.WriteUint32(uint32(returnExpiry.Unix())) bf.WriteUint32(0x00000000) bf.WriteUint32(0x0A5197DF) diff --git a/www/erupe/index.html b/www/erupe/index.html index 57aa1c568..0b130c684 100644 --- a/www/erupe/index.html +++ b/www/erupe/index.html @@ -118,11 +118,11 @@
-
+