From 33665130cf1b04d1fab560b3efc08c841f5d3a4b Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 26 Nov 2023 01:22:51 -0500 Subject: [PATCH 01/70] feat: Discord basic implementation --- patch-schema/15-discord-password-resets.sql | 6 ++++++ server/channelserver/handlers_cast_binary.go | 10 ++++++++++ server/channelserver/handlers_discord.go | 21 ++++++++++++++++++++ server/channelserver/sys_channel_server.go | 11 ++++++++++ 4 files changed, 48 insertions(+) create mode 100644 patch-schema/15-discord-password-resets.sql diff --git a/patch-schema/15-discord-password-resets.sql b/patch-schema/15-discord-password-resets.sql new file mode 100644 index 000000000..bd2e83fea --- /dev/null +++ b/patch-schema/15-discord-password-resets.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE IF EXISTS public.users ADD COLUMN discord_token text; +ALTER TABLE IF EXISTS public.users ADD COLUMN discord_id text; + +END; \ No newline at end of file diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index dbfafc68c..36edbbadb 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -1,6 +1,7 @@ package channelserver import ( + "crypto" "encoding/hex" "erupe-ce/common/byteframe" "erupe-ce/common/mhfcourse" @@ -318,6 +319,15 @@ func parseChatCommand(s *Session, command string) { } else { sendDisabledCommandMessage(s, commands["Teleport"]) } + case commands["Discord"].Prefix: + if commands["Discord"].Enabled { + token := crypto.MD5.New() + _, err := s.server.db.Exec("UPDATE users SET discord_token = ?", token) + if err != nil { + return + } + sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscord"], token)) + } } } diff --git a/server/channelserver/handlers_discord.go b/server/channelserver/handlers_discord.go index 1f3398d18..551c74146 100644 --- a/server/channelserver/handlers_discord.go +++ b/server/channelserver/handlers_discord.go @@ -66,6 +66,27 @@ func getCharacterList(s *Server) string { return message } +// onInteraction handles slash commands +func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCreate) { + switch i.Interaction.ApplicationCommandData().Name { + case "verify": + _, err := s.db.Exec("UPDATE users SET discord_id = ? WHERE discord_token = ?", i.User.ID, i.Interaction.ApplicationCommandData().Options[0].StringValue()) + if err != nil { + return + } + err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Account successfully linked", + }, + }) + if err != nil { + return + } + break + } +} + // 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. diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 70f52e461..f7658c5cd 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -2,6 +2,7 @@ package channelserver import ( "fmt" + "github.com/bwmarrin/discordgo" "net" "strings" "sync" @@ -210,7 +211,17 @@ func (s *Server) Start() error { // Start the discord bot for chat integration. if s.erupeConfig.Discord.Enabled && s.discordBot != nil { + _, err := s.discordBot.Session.ApplicationCommandBulkOverwrite(s.discordBot.Session.State.User.ID, "", []*discordgo.ApplicationCommand{ + { + Name: "verify", + Description: "Verify your account with Discord", + }, + }) + if err != nil { + return err + } s.discordBot.Session.AddHandler(s.onDiscordMessage) + s.discordBot.Session.AddHandler(s.onInteraction) } return nil From 226adddc43b5c639ae230ff46b0681dc9fcb55fd Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 26 Nov 2023 16:47:54 -0500 Subject: [PATCH 02/70] feat: Generate hashes for Discord and allow password resets --- config.json | 20 ++++++---- main.go | 32 +++++++++++++++ server/channelserver/handlers_cast_binary.go | 17 ++++++-- server/channelserver/handlers_discord.go | 23 ++++++++++- server/channelserver/handlers_stage.go | 41 +++++++++----------- server/channelserver/sys_channel_server.go | 10 ----- server/channelserver/sys_language.go | 2 + server/channelserver/sys_session.go | 1 + 8 files changed, 100 insertions(+), 46 deletions(-) diff --git a/config.json b/config.json index da75463ee..4063a6df2 100644 --- a/config.json +++ b/config.json @@ -19,9 +19,9 @@ "AutoCreateAccount": true, "CleanDB": false, "MaxLauncherHR": false, - "LogInboundMessages": false, - "LogOutboundMessages": false, - "LogMessageData": false, + "LogInboundMessages": true, + "LogOutboundMessages": true, + "LogMessageData": true, "MaxHexdumpLength": 256, "DivaEvent": 0, "FestaEvent": -1, @@ -73,9 +73,9 @@ "SeasonOverride": false }, "Discord": { - "Enabled": false, - "BotToken": "", - "RealtimeChannelID": "" + "Enabled": true, + "BotToken": "MTAzMTQ2MDI4MDYxOTc2NTgwMA.GGe824._OxF9rtv1O8EjOZI26hATruaF_VZ9YBwuAdS1Y", + "RealtimeChannelID": "645108836423958540" }, "Commands": [ { @@ -106,6 +106,10 @@ "Name": "PSN", "Enabled": true, "Prefix": "psn" + }, { + "Name": "Discord", + "Enabled": true, + "Prefix": "discord" } ], "Courses": [ @@ -125,7 +129,7 @@ "Host": "localhost", "Port": 5432, "User": "postgres", - "Password": "", + "Password": "admin", "Database": "erupe" }, "Sign": { @@ -133,7 +137,7 @@ "Port": 53312 }, "SignV2": { - "Enabled": false, + "Enabled": true, "Port": 8080, "PatchServer": "", "Banners": [], diff --git a/main.go b/main.go index c56d90b0f..7a21a4b7b 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( _config "erupe-ce/config" "fmt" + "github.com/bwmarrin/discordgo" "net" "os" "os/signal" @@ -98,6 +99,37 @@ func main() { } discordBot = bot + + _, err = discordBot.Session.ApplicationCommandBulkOverwrite(discordBot.Session.State.User.ID, "", []*discordgo.ApplicationCommand{ + { + Name: "verify", + Description: "Verify your account with Discord", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "token", + Description: "The access token provided by !discord command within the game client.", + Required: true, + }, + }, + }, + { + Name: "passwordreset", + Description: "Reset your account password on Erupe", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "password", + Description: "The password to change your account to.", + Required: true, + }, + }, + }, + }) + if err != nil { + preventClose(fmt.Sprintf("Discord: Failed to start, %s", err.Error())) + } + logger.Info("Discord: Started successfully") } else { logger.Info("Discord: Disabled") diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 36edbbadb..ea3b291d2 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -2,6 +2,7 @@ package channelserver import ( "crypto" + "encoding/binary" "encoding/hex" "erupe-ce/common/byteframe" "erupe-ce/common/mhfcourse" @@ -321,12 +322,22 @@ func parseChatCommand(s *Session, command string) { } case commands["Discord"].Prefix: if commands["Discord"].Enabled { - token := crypto.MD5.New() - _, err := s.server.db.Exec("UPDATE users SET discord_token = ?", token) + tokenHash := crypto.MD5.New() + tokenSalt := fmt.Sprint(s.charID) + fmt.Sprint(s.server.ID) + tokenData := make([]byte, 4) + binary.LittleEndian.PutUint32(tokenData, uint32(time.Now().Second())) + tokenHash.Write([]byte(fmt.Sprintf("%s%s", tokenSalt, tokenData))) + discordToken := fmt.Sprint(tokenHash)[4:12] + s.logger.Info(discordToken) + _, err := s.server.db.Exec("UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", discordToken, s.charID) if err != nil { + sendServerChatMessage(s, fmt.Sprint("An error occurred while processing this command")) + s.logger.Error(fmt.Sprint(err)) return } - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscord"], token)) + sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscordSuccess"], discordToken)) + } else { + sendDisabledCommandMessage(s, commands["Discord"]) } } } diff --git a/server/channelserver/handlers_discord.go b/server/channelserver/handlers_discord.go index 551c74146..7482332e0 100644 --- a/server/channelserver/handlers_discord.go +++ b/server/channelserver/handlers_discord.go @@ -3,6 +3,7 @@ package channelserver import ( "fmt" "github.com/bwmarrin/discordgo" + "golang.org/x/crypto/bcrypt" "sort" "strings" "unicode" @@ -70,14 +71,32 @@ func getCharacterList(s *Server) string { func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCreate) { switch i.Interaction.ApplicationCommandData().Name { case "verify": - _, err := s.db.Exec("UPDATE users SET discord_id = ? WHERE discord_token = ?", i.User.ID, i.Interaction.ApplicationCommandData().Options[0].StringValue()) + _, err := s.db.Exec("UPDATE users SET discord_id = $1 WHERE discord_token = $2", i.Member.User.ID, i.ApplicationCommandData().Options[0].StringValue()) if err != nil { return } err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: "Account successfully linked", + Content: "Erupe account successfully linked to Discord account.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + return + } + break + case "passwordreset": + password, _ := bcrypt.GenerateFromPassword([]byte(i.ApplicationCommandData().Options[0].StringValue()), 10) + _, err := s.db.Exec("UPDATE users SET password = $1 WHERE discord_id = $2", password, i.Member.User.ID) + if err != nil { + return + } + err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Password has been reset, you may login now.", + Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { diff --git a/server/channelserver/handlers_stage.go b/server/channelserver/handlers_stage.go index 7a12d453e..cf8757b00 100644 --- a/server/channelserver/handlers_stage.go +++ b/server/channelserver/handlers_stage.go @@ -55,6 +55,7 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) { // Save our new stage ID and pointer to the new stage itself. s.Lock() + s.stageID = stageID s.stage = s.server.stages[stageID] s.Unlock() @@ -152,13 +153,13 @@ func handleMsgSysEnterStage(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysEnterStage) // Push our current stage ID to the movement stack before entering another one. - if s.stage.id == "" { + if s.stageID == "" { s.stageMoveStack.Set(pkt.StageID) } else { s.stage.Lock() s.stage.reservedClientSlots[s.charID] = false s.stage.Unlock() - s.stageMoveStack.Push(s.stage.id) + s.stageMoveStack.Push(s.stageID) s.stageMoveStack.Lock() } @@ -205,12 +206,9 @@ func handleMsgSysLeaveStage(s *Session, p mhfpacket.MHFPacket) {} func handleMsgSysLockStage(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysLockStage) - if stage, exists := s.server.stages[pkt.StageID]; exists { - stage.Lock() - stage.locked = true - stage.Unlock() - } - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) + // TODO(Andoryuuta): What does this packet _actually_ do? + // I think this is supposed to mark a stage as no longer able to accept client reservations + doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } func handleMsgSysUnlockStage(s *Session, p mhfpacket.MHFPacket) { @@ -220,9 +218,7 @@ func handleMsgSysUnlockStage(s *Session, p mhfpacket.MHFPacket) { for charID := range s.reservationStage.reservedClientSlots { session := s.server.FindSessionByCharID(charID) - if session != nil { - session.QueueSendMHF(&mhfpacket.MsgSysStageDestruct{}) - } + session.QueueSendMHF(&mhfpacket.MsgSysStageDestruct{}) } delete(s.server.stages, s.reservationStage.id) @@ -245,10 +241,6 @@ func handleMsgSysReserveStage(s *Session, p mhfpacket.MHFPacket) { } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } else if uint16(len(stage.reservedClientSlots)) < stage.maxPlayers { - if stage.locked { - doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) - return - } if len(stage.password) > 0 { if stage.password != s.stagePass { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) @@ -391,17 +383,20 @@ func handleMsgSysEnumerateStage(s *Session, p mhfpacket.MHFPacket) { joinable++ bf.WriteUint16(uint16(len(stage.reservedClientSlots))) - bf.WriteUint16(uint16(len(stage.clients))) - bf.WriteUint16(uint16(len(stage.clients))) + bf.WriteUint16(0) // Unk + if len(stage.clients) > 0 { + bf.WriteUint16(1) + } else { + bf.WriteUint16(0) + } bf.WriteUint16(stage.maxPlayers) - var flags uint8 - if stage.locked { - flags |= 1 - } if len(stage.password) > 0 { - flags |= 2 + // This byte has also been seen as 1 + // The quest is also recognised as locked when this is 2 + bf.WriteUint8(2) + } else { + bf.WriteUint8(0) } - bf.WriteUint8(flags) ps.Uint8(bf, sid, false) stage.RUnlock() } diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index f7658c5cd..119cbf420 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -2,7 +2,6 @@ package channelserver import ( "fmt" - "github.com/bwmarrin/discordgo" "net" "strings" "sync" @@ -211,15 +210,6 @@ func (s *Server) Start() error { // Start the discord bot for chat integration. if s.erupeConfig.Discord.Enabled && s.discordBot != nil { - _, err := s.discordBot.Session.ApplicationCommandBulkOverwrite(s.discordBot.Session.State.User.ID, "", []*discordgo.ApplicationCommand{ - { - Name: "verify", - Description: "Verify your account with Discord", - }, - }) - if err != nil { - return err - } s.discordBot.Session.AddHandler(s.onDiscordMessage) s.discordBot.Session.AddHandler(s.onInteraction) } diff --git a/server/channelserver/sys_language.go b/server/channelserver/sys_language.go index dbd48cfb8..cc17ee6a1 100644 --- a/server/channelserver/sys_language.go +++ b/server/channelserver/sys_language.go @@ -76,6 +76,8 @@ func getLangStrings(s *Server) map[string]string { strings["commandPSNSuccess"] = "Connected PSN ID: %s" strings["commandPSNExists"] = "PSN ID is connected to another account!" + strings["commandDiscordSuccess"] = "Discord token has been generated: %s" + strings["commandRaviNoCommand"] = "No Raviente command specified!" strings["commandRaviStartSuccess"] = "The Great Slaying will begin in a moment" strings["commandRaviStartError"] = "The Great Slaying has already begun!" diff --git a/server/channelserver/sys_session.go b/server/channelserver/sys_session.go index 5034f38c2..32004151d 100644 --- a/server/channelserver/sys_session.go +++ b/server/channelserver/sys_session.go @@ -36,6 +36,7 @@ type Session struct { objectIndex uint16 userEnteredStage bool // If the user has entered a stage before + stageID string stage *Stage reservationStage *Stage // Required for the stateful MsgSysUnreserveStage packet. stagePass string // Temporary storage From d2e9e3d1a9c8e7947745c677c3eabf305c10164e Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 26 Nov 2023 16:49:11 -0500 Subject: [PATCH 03/70] fix: Fix default config (whoops) --- config.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config.json b/config.json index 4063a6df2..87686a173 100644 --- a/config.json +++ b/config.json @@ -73,9 +73,9 @@ "SeasonOverride": false }, "Discord": { - "Enabled": true, - "BotToken": "MTAzMTQ2MDI4MDYxOTc2NTgwMA.GGe824._OxF9rtv1O8EjOZI26hATruaF_VZ9YBwuAdS1Y", - "RealtimeChannelID": "645108836423958540" + "Enabled": false, + "BotToken": "", + "RealtimeChannelID": "" }, "Commands": [ { @@ -129,7 +129,7 @@ "Host": "localhost", "Port": 5432, "User": "postgres", - "Password": "admin", + "Password": "", "Database": "erupe" }, "Sign": { From 38b57c6d98c0b6d2ee7b5d2984c2cc8473f657c9 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 27 Nov 2023 01:08:40 -0500 Subject: [PATCH 04/70] refactor: Change to using rand.Read instead of whatever the hell else was before --- main.go | 2 +- server/channelserver/handlers_cast_binary.go | 15 +++++---------- server/channelserver/handlers_discord.go | 3 ++- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 7a21a4b7b..35ca98043 100644 --- a/main.go +++ b/main.go @@ -114,7 +114,7 @@ func main() { }, }, { - Name: "passwordreset", + Name: "password", Description: "Reset your account password on Erupe", Options: []*discordgo.ApplicationCommandOption{ { diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index ea3b291d2..89e978ffd 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -1,8 +1,7 @@ package channelserver import ( - "crypto" - "encoding/binary" + "crypto/rand" "encoding/hex" "erupe-ce/common/byteframe" "erupe-ce/common/mhfcourse" @@ -322,14 +321,10 @@ func parseChatCommand(s *Session, command string) { } case commands["Discord"].Prefix: if commands["Discord"].Enabled { - tokenHash := crypto.MD5.New() - tokenSalt := fmt.Sprint(s.charID) + fmt.Sprint(s.server.ID) - tokenData := make([]byte, 4) - binary.LittleEndian.PutUint32(tokenData, uint32(time.Now().Second())) - tokenHash.Write([]byte(fmt.Sprintf("%s%s", tokenSalt, tokenData))) - discordToken := fmt.Sprint(tokenHash)[4:12] - s.logger.Info(discordToken) - _, err := s.server.db.Exec("UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", discordToken, s.charID) + discordToken := make([]byte, 8) + _, err := rand.Read(discordToken) + s.logger.Info(fmt.Sprint(discordToken)) + _, err = s.server.db.Exec("UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", fmt.Sprint(discordToken), s.charID) if err != nil { sendServerChatMessage(s, fmt.Sprint("An error occurred while processing this command")) s.logger.Error(fmt.Sprint(err)) diff --git a/server/channelserver/handlers_discord.go b/server/channelserver/handlers_discord.go index 7482332e0..9a7dec2a0 100644 --- a/server/channelserver/handlers_discord.go +++ b/server/channelserver/handlers_discord.go @@ -86,10 +86,11 @@ func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCr return } break - case "passwordreset": + case "password": password, _ := bcrypt.GenerateFromPassword([]byte(i.ApplicationCommandData().Options[0].StringValue()), 10) _, err := s.db.Exec("UPDATE users SET password = $1 WHERE discord_id = $2", password, i.Member.User.ID) if err != nil { + s.logger.Error(fmt.Sprint(err)) return } err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ From 523266fc6834634a82c9e4fc2ec6dbdde992587d Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 27 Nov 2023 02:18:01 -0500 Subject: [PATCH 05/70] refactor: Move realtime channels to its own config --- config.json | 5 ++++- config/config.go | 7 ++++++- server/channelserver/handlers_discord.go | 2 +- server/discordbot/discord_bot.go | 10 +++++++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/config.json b/config.json index 87686a173..dcccdf069 100644 --- a/config.json +++ b/config.json @@ -75,7 +75,10 @@ "Discord": { "Enabled": false, "BotToken": "", - "RealtimeChannelID": "" + "RealTimeChannel": { + "Enabled": false, + "RealTimeChannelID": "" + } }, "Commands": [ { diff --git a/config/config.go b/config/config.go index 31fedd246..4c1809e40 100644 --- a/config/config.go +++ b/config/config.go @@ -161,8 +161,13 @@ type GameplayOptions struct { // Discord holds the discord integration config. type Discord struct { + Enabled bool + BotToken string + RealTimeChannel DiscordRealTime +} + +type DiscordRealTime struct { Enabled bool - BotToken string RealtimeChannelID string } diff --git a/server/channelserver/handlers_discord.go b/server/channelserver/handlers_discord.go index 9a7dec2a0..61c7a2a57 100644 --- a/server/channelserver/handlers_discord.go +++ b/server/channelserver/handlers_discord.go @@ -110,7 +110,7 @@ func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCr // 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.Bot || m.ChannelID != s.erupeConfig.Discord.RealtimeChannelID { + if m.Author.Bot || m.ChannelID != s.erupeConfig.Discord.RealTimeChannel.RealtimeChannelID { return } diff --git a/server/discordbot/discord_bot.go b/server/discordbot/discord_bot.go index c082faf70..0d774fff7 100644 --- a/server/discordbot/discord_bot.go +++ b/server/discordbot/discord_bot.go @@ -28,7 +28,11 @@ func NewDiscordBot(options Options) (discordBot *DiscordBot, err error) { return nil, err } - realtimeChannel, err := session.Channel(options.Config.Discord.RealtimeChannelID) + var realtimeChannel *discordgo.Channel + + if options.Config.Discord.RealTimeChannel.Enabled { + realtimeChannel, err = session.Channel(options.Config.Discord.RealTimeChannel.RealtimeChannelID) + } if err != nil { options.Logger.Fatal("Discord failed to create realtimeChannel", zap.Error(err)) @@ -74,6 +78,10 @@ func (bot *DiscordBot) NormalizeDiscordMessage(message string) string { } func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) { + if bot.RealtimeChannel == nil { + return + } + _, err = bot.Session.ChannelMessageSend(bot.RealtimeChannel.ID, message) return From a4745f05d5d210c49c282411f7f63c2a8e625e09 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 27 Nov 2023 03:03:48 -0500 Subject: [PATCH 06/70] refactor: Clean up random token implementation --- server/channelserver/handlers_cast_binary.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 89e978ffd..1fe81c323 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -321,9 +321,16 @@ func parseChatCommand(s *Session, command string) { } case commands["Discord"].Prefix: if commands["Discord"].Enabled { - discordToken := make([]byte, 8) - _, err := rand.Read(discordToken) - s.logger.Info(fmt.Sprint(discordToken)) + randToken := make([]byte, 4) + + _, err := rand.Read(randToken) + if err != nil { + sendServerChatMessage(s, fmt.Sprint("An error occurred while processing this command")) + s.logger.Error(fmt.Sprint(err)) + return + } + + discordToken := fmt.Sprintf("%x-%x", randToken[:2], randToken[2:]) _, err = s.server.db.Exec("UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", fmt.Sprint(discordToken), s.charID) if err != nil { sendServerChatMessage(s, fmt.Sprint("An error occurred while processing this command")) From 7d630088a4893210a60b80f5fc6f0dc4ae64571d Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 27 Nov 2023 03:23:30 -0500 Subject: [PATCH 07/70] refactor: Fix code formatting and reset config --- config.json | 8 ++++---- server/channelserver/handlers_discord.go | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/config.json b/config.json index dcccdf069..aabc3d62a 100644 --- a/config.json +++ b/config.json @@ -19,9 +19,9 @@ "AutoCreateAccount": true, "CleanDB": false, "MaxLauncherHR": false, - "LogInboundMessages": true, - "LogOutboundMessages": true, - "LogMessageData": true, + "LogInboundMessages": false, + "LogOutboundMessages": false, + "LogMessageData": false, "MaxHexdumpLength": 256, "DivaEvent": 0, "FestaEvent": -1, @@ -140,7 +140,7 @@ "Port": 53312 }, "SignV2": { - "Enabled": true, + "Enabled": false, "Port": 8080, "PatchServer": "", "Banners": [], diff --git a/server/channelserver/handlers_discord.go b/server/channelserver/handlers_discord.go index 61c7a2a57..1772e2d06 100644 --- a/server/channelserver/handlers_discord.go +++ b/server/channelserver/handlers_discord.go @@ -75,6 +75,7 @@ func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCr if err != nil { return } + err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -82,17 +83,20 @@ func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCr Flags: discordgo.MessageFlagsEphemeral, }, }) + if err != nil { return } break case "password": password, _ := bcrypt.GenerateFromPassword([]byte(i.ApplicationCommandData().Options[0].StringValue()), 10) + _, err := s.db.Exec("UPDATE users SET password = $1 WHERE discord_id = $2", password, i.Member.User.ID) if err != nil { s.logger.Error(fmt.Sprint(err)) return } + err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ From a77d6d53aa642bf8ed7c6e5fd5ae6d0d02a00b0e Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 27 Nov 2023 03:27:20 -0500 Subject: [PATCH 08/70] refactor: Remove reverted mutex changes --- server/channelserver/handlers_stage.go | 43 ++++++++++++++------------ server/channelserver/sys_session.go | 1 - 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/server/channelserver/handlers_stage.go b/server/channelserver/handlers_stage.go index cf8757b00..7a12d453e 100644 --- a/server/channelserver/handlers_stage.go +++ b/server/channelserver/handlers_stage.go @@ -55,7 +55,6 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) { // Save our new stage ID and pointer to the new stage itself. s.Lock() - s.stageID = stageID s.stage = s.server.stages[stageID] s.Unlock() @@ -153,13 +152,13 @@ func handleMsgSysEnterStage(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysEnterStage) // Push our current stage ID to the movement stack before entering another one. - if s.stageID == "" { + if s.stage.id == "" { s.stageMoveStack.Set(pkt.StageID) } else { s.stage.Lock() s.stage.reservedClientSlots[s.charID] = false s.stage.Unlock() - s.stageMoveStack.Push(s.stageID) + s.stageMoveStack.Push(s.stage.id) s.stageMoveStack.Lock() } @@ -206,9 +205,12 @@ func handleMsgSysLeaveStage(s *Session, p mhfpacket.MHFPacket) {} func handleMsgSysLockStage(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysLockStage) - // TODO(Andoryuuta): What does this packet _actually_ do? - // I think this is supposed to mark a stage as no longer able to accept client reservations - doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) + if stage, exists := s.server.stages[pkt.StageID]; exists { + stage.Lock() + stage.locked = true + stage.Unlock() + } + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgSysUnlockStage(s *Session, p mhfpacket.MHFPacket) { @@ -218,7 +220,9 @@ func handleMsgSysUnlockStage(s *Session, p mhfpacket.MHFPacket) { for charID := range s.reservationStage.reservedClientSlots { session := s.server.FindSessionByCharID(charID) - session.QueueSendMHF(&mhfpacket.MsgSysStageDestruct{}) + if session != nil { + session.QueueSendMHF(&mhfpacket.MsgSysStageDestruct{}) + } } delete(s.server.stages, s.reservationStage.id) @@ -241,6 +245,10 @@ func handleMsgSysReserveStage(s *Session, p mhfpacket.MHFPacket) { } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } else if uint16(len(stage.reservedClientSlots)) < stage.maxPlayers { + if stage.locked { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } if len(stage.password) > 0 { if stage.password != s.stagePass { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) @@ -383,20 +391,17 @@ func handleMsgSysEnumerateStage(s *Session, p mhfpacket.MHFPacket) { joinable++ bf.WriteUint16(uint16(len(stage.reservedClientSlots))) - bf.WriteUint16(0) // Unk - if len(stage.clients) > 0 { - bf.WriteUint16(1) - } else { - bf.WriteUint16(0) - } + bf.WriteUint16(uint16(len(stage.clients))) + bf.WriteUint16(uint16(len(stage.clients))) bf.WriteUint16(stage.maxPlayers) - if len(stage.password) > 0 { - // This byte has also been seen as 1 - // The quest is also recognised as locked when this is 2 - bf.WriteUint8(2) - } else { - bf.WriteUint8(0) + var flags uint8 + if stage.locked { + flags |= 1 } + if len(stage.password) > 0 { + flags |= 2 + } + bf.WriteUint8(flags) ps.Uint8(bf, sid, false) stage.RUnlock() } diff --git a/server/channelserver/sys_session.go b/server/channelserver/sys_session.go index 32004151d..5034f38c2 100644 --- a/server/channelserver/sys_session.go +++ b/server/channelserver/sys_session.go @@ -36,7 +36,6 @@ type Session struct { objectIndex uint16 userEnteredStage bool // If the user has entered a stage before - stageID string stage *Stage reservationStage *Stage // Required for the stateful MsgSysUnreserveStage packet. stagePass string // Temporary storage From 26438306c613308840f686f465e900cd04778b79 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 4 Dec 2023 13:48:17 -0500 Subject: [PATCH 09/70] fix: Missing closing bracket --- server/channelserver/handlers_cast_binary.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index b0646fcce..5f548feab 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -344,6 +344,7 @@ func parseChatCommand(s *Session, command string) { sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscordSuccess"], discordToken)) } else { sendDisabledCommandMessage(s, commands["Discord"]) + } case commands["Help"].Prefix: if commands["Help"].Enabled { for _, command := range commands { From b8f431ae66f17a8b080d51bd5c034fd2210a385c Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 17 Dec 2023 17:47:35 +1100 Subject: [PATCH 10/70] parse CapLink responses --- config.json | 6 ++++++ config/config.go | 8 ++++++++ server/signserver/dsgn_resp.go | 34 ++++++++++++++++++++++++++-------- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/config.json b/config.json index 5d66b9e33..1d5fe76d7 100644 --- a/config.json +++ b/config.json @@ -36,6 +36,12 @@ "Enabled": true, "RawEnabled": false, "OutputDir": "save-backups" + }, + "CapLink": { + "Values": [51728, 20000, 51729, 1, 20000], + "Key": "", + "Host": "", + "Port": 80 } }, "GameplayOptions": { diff --git a/config/config.go b/config/config.go index 153cfedb3..4995c042d 100644 --- a/config/config.go +++ b/config/config.go @@ -114,6 +114,7 @@ type DevModeOptions struct { EarthIDOverride int32 EarthMonsterOverride []int32 SaveDumps SaveDumpOptions + CapLink CapLinkOptions } type SaveDumpOptions struct { @@ -122,6 +123,13 @@ type SaveDumpOptions struct { OutputDir string } +type CapLinkOptions struct { + Values []uint16 + Key string + Host string + Port int +} + // GameplayOptions has various gameplay modifiers type GameplayOptions struct { FeaturedWeapons int // Number of Active Feature weapons to generate daily diff --git a/server/signserver/dsgn_resp.go b/server/signserver/dsgn_resp.go index 34cc371e2..25b5650c4 100644 --- a/server/signserver/dsgn_resp.go +++ b/server/signserver/dsgn_resp.go @@ -144,14 +144,32 @@ func (s *Session) makeSignResponse(uid uint32) []byte { s.server.db.QueryRow("SELECT psn_id FROM users WHERE id = $1", uid).Scan(&psnUser) bf.WriteBytes(stringsupport.PaddedString(psnUser, 20, true)) } - bf.WriteUint16(0xCA10) - bf.WriteUint16(0x4E20) - ps.Uint16(bf, "", false) // unk key - bf.WriteUint8(0x00) - bf.WriteUint16(0xCA11) - bf.WriteUint16(0x0001) - bf.WriteUint16(0x4E20) - ps.Uint16(bf, "", false) // unk ipv4 + + bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[0]) + if s.server.erupeConfig.DevModeOptions.CapLink.Values[0] == 51728 { + bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[1]) + if s.server.erupeConfig.DevModeOptions.CapLink.Values[1] == 20000 || s.server.erupeConfig.DevModeOptions.CapLink.Values[1] == 20002 { + ps.Uint16(bf, s.server.erupeConfig.DevModeOptions.CapLink.Key, false) + } + } + caStruct := []struct { + Unk0 uint8 + Unk1 uint32 + Unk2 string + }{} + bf.WriteUint8(uint8(len(caStruct))) + for i := range caStruct { + bf.WriteUint8(caStruct[i].Unk0) + bf.WriteUint32(caStruct[i].Unk1) + ps.Uint8(bf, caStruct[i].Unk2, false) + } + bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[2]) + bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[3]) + bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[4]) + if s.server.erupeConfig.DevModeOptions.CapLink.Values[2] == 51729 && s.server.erupeConfig.DevModeOptions.CapLink.Values[3] == 1 && s.server.erupeConfig.DevModeOptions.CapLink.Values[4] == 20000 { + ps.Uint16(bf, fmt.Sprintf(`%s:%d`, s.server.erupeConfig.DevModeOptions.CapLink.Host, s.server.erupeConfig.DevModeOptions.CapLink.Port), false) + } + bf.WriteUint32(uint32(s.server.getReturnExpiry(uid).Unix())) bf.WriteUint32(0) From c71557851960b7e94908733036e9f7dc33fc046a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 23:59:56 +0000 Subject: [PATCH 11/70] Bump golang.org/x/crypto from 0.15.0 to 0.17.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.15.0 to 0.17.0. - [Commits](https://github.com/golang/crypto/compare/v0.15.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 410eb88b0..5047c6609 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/lib/pq v1.10.9 github.com/spf13/viper v1.17.0 go.uber.org/zap v1.26.0 - golang.org/x/crypto v0.15.0 + golang.org/x/crypto v0.17.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/text v0.14.0 ) @@ -32,7 +32,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1c6fbeb53..d2cc39776 100644 --- a/go.sum +++ b/go.sum @@ -220,8 +220,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -345,8 +345,8 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 293122b9a17d29184e244b2cdfa788801fd45a68 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 20 Dec 2023 13:47:08 -0800 Subject: [PATCH 12/70] rm: Unintentional file inclusion --- bin/quests/desktop.ini | 2372 ---------------------------------------- 1 file changed, 2372 deletions(-) delete mode 100644 bin/quests/desktop.ini diff --git a/bin/quests/desktop.ini b/bin/quests/desktop.ini deleted file mode 100644 index a71744976..000000000 --- a/bin/quests/desktop.ini +++ /dev/null @@ -1,2372 +0,0 @@ -[LocalizedFileNames] -23484d1.bin=@23484d1.bin,0 -23484d2.bin=@23484d2.bin,0 -23484n0.bin=@23484n0.bin,0 -23484n1.bin=@23484n1.bin,0 -23484n2.bin=@23484n2.bin,0 -23485d0.bin=@23485d0.bin,0 -23485d1.bin=@23485d1.bin,0 -23485d2.bin=@23485d2.bin,0 -23485n0.bin=@23485n0.bin,0 -23485n1.bin=@23485n1.bin,0 -23485n2.bin=@23485n2.bin,0 -23486d0.bin=@23486d0.bin,0 -23486d1.bin=@23486d1.bin,0 -23486d2.bin=@23486d2.bin,0 -23486n0.bin=@23486n0.bin,0 -23486n1.bin=@23486n1.bin,0 -23486n2.bin=@23486n2.bin,0 -23487d0.bin=@23487d0.bin,0 -23487d1.bin=@23487d1.bin,0 -23487d2.bin=@23487d2.bin,0 -23487n0.bin=@23487n0.bin,0 -23487n1.bin=@23487n1.bin,0 -23487n2.bin=@23487n2.bin,0 -23488d0.bin=@23488d0.bin,0 -23488d1.bin=@23488d1.bin,0 -23488d2.bin=@23488d2.bin,0 -23488n0.bin=@23488n0.bin,0 -23488n1.bin=@23488n1.bin,0 -23488n2.bin=@23488n2.bin,0 -23489d0.bin=@23489d0.bin,0 -23489d1.bin=@23489d1.bin,0 -23489d2.bin=@23489d2.bin,0 -23489n0.bin=@23489n0.bin,0 -23489n1.bin=@23489n1.bin,0 -23489n2.bin=@23489n2.bin,0 -23490d0.bin=@23490d0.bin,0 -23490d1.bin=@23490d1.bin,0 -23490d2.bin=@23490d2.bin,0 -23490n0.bin=@23490n0.bin,0 -23490n1.bin=@23490n1.bin,0 -23490n2.bin=@23490n2.bin,0 -23491d0.bin=@23491d0.bin,0 -23491d1.bin=@23491d1.bin,0 -23491d2.bin=@23491d2.bin,0 -23491n0.bin=@23491n0.bin,0 -23491n1.bin=@23491n1.bin,0 -23491n2.bin=@23491n2.bin,0 -23492d0.bin=@23492d0.bin,0 -23492d1.bin=@23492d1.bin,0 -23492d2.bin=@23492d2.bin,0 -23492n0.bin=@23492n0.bin,0 -23492n1.bin=@23492n1.bin,0 -23492n2.bin=@23492n2.bin,0 -23484d0.bin=@23484d0.bin,0 -23585d0.bin=@23585d0.bin,0 -23585d1.bin=@23585d1.bin,0 -23585d2.bin=@23585d2.bin,0 -23585n0.bin=@23585n0.bin,0 -23585n1.bin=@23585n1.bin,0 -23585n2.bin=@23585n2.bin,0 -23586d0.bin=@23586d0.bin,0 -23586d1.bin=@23586d1.bin,0 -23586d2.bin=@23586d2.bin,0 -23586n0.bin=@23586n0.bin,0 -23586n1.bin=@23586n1.bin,0 -23586n2.bin=@23586n2.bin,0 -23587d0.bin=@23587d0.bin,0 -23587d1.bin=@23587d1.bin,0 -23587d2.bin=@23587d2.bin,0 -23587n0.bin=@23587n0.bin,0 -23587n1.bin=@23587n1.bin,0 -23587n2.bin=@23587n2.bin,0 -23588d0.bin=@23588d0.bin,0 -23588d1.bin=@23588d1.bin,0 -23588d2.bin=@23588d2.bin,0 -23588n0.bin=@23588n0.bin,0 -23588n1.bin=@23588n1.bin,0 -23588n2.bin=@23588n2.bin,0 -23589d0.bin=@23589d0.bin,0 -23589d1.bin=@23589d1.bin,0 -23589d2.bin=@23589d2.bin,0 -23589n0.bin=@23589n0.bin,0 -23589n1.bin=@23589n1.bin,0 -23589n2.bin=@23589n2.bin,0 -23590d0.bin=@23590d0.bin,0 -23590d1.bin=@23590d1.bin,0 -23590d2.bin=@23590d2.bin,0 -23590n0.bin=@23590n0.bin,0 -23590n1.bin=@23590n1.bin,0 -23590n2.bin=@23590n2.bin,0 -23591d0.bin=@23591d0.bin,0 -23591d1.bin=@23591d1.bin,0 -23591d2.bin=@23591d2.bin,0 -23591n0.bin=@23591n0.bin,0 -23591n1.bin=@23591n1.bin,0 -23591n2.bin=@23591n2.bin,0 -23592d0.bin=@23592d0.bin,0 -23592d1.bin=@23592d1.bin,0 -23592d2.bin=@23592d2.bin,0 -23592n0.bin=@23592n0.bin,0 -23592n1.bin=@23592n1.bin,0 -23592n2.bin=@23592n2.bin,0 -23593d0.bin=@23593d0.bin,0 -23593d1.bin=@23593d1.bin,0 -23593d2.bin=@23593d2.bin,0 -23593n0.bin=@23593n0.bin,0 -23593n1.bin=@23593n1.bin,0 -23593n2.bin=@23593n2.bin,0 -23594d0.bin=@23594d0.bin,0 -23594d1.bin=@23594d1.bin,0 -23594d2.bin=@23594d2.bin,0 -23594n0.bin=@23594n0.bin,0 -23594n1.bin=@23594n1.bin,0 -23594n2.bin=@23594n2.bin,0 -23595d0.bin=@23595d0.bin,0 -23595d1.bin=@23595d1.bin,0 -23595d2.bin=@23595d2.bin,0 -23595n0.bin=@23595n0.bin,0 -23595n1.bin=@23595n1.bin,0 -23595n2.bin=@23595n2.bin,0 -23596d0.bin=@23596d0.bin,0 -23596d1.bin=@23596d1.bin,0 -23596d2.bin=@23596d2.bin,0 -23596n0.bin=@23596n0.bin,0 -23596n1.bin=@23596n1.bin,0 -23596n2.bin=@23596n2.bin,0 -23597d0.bin=@23597d0.bin,0 -23597d1.bin=@23597d1.bin,0 -23597d2.bin=@23597d2.bin,0 -23597n0.bin=@23597n0.bin,0 -23597n1.bin=@23597n1.bin,0 -23597n2.bin=@23597n2.bin,0 -23598d0.bin=@23598d0.bin,0 -23598d1.bin=@23598d1.bin,0 -23598d2.bin=@23598d2.bin,0 -23598n0.bin=@23598n0.bin,0 -23598n1.bin=@23598n1.bin,0 -23598n2.bin=@23598n2.bin,0 -23599d0.bin=@23599d0.bin,0 -23599d1.bin=@23599d1.bin,0 -23599d2.bin=@23599d2.bin,0 -23599n0.bin=@23599n0.bin,0 -23599n1.bin=@23599n1.bin,0 -23599n2.bin=@23599n2.bin,0 -23601d0.bin=@23601d0.bin,0 -23601d1.bin=@23601d1.bin,0 -23601d2.bin=@23601d2.bin,0 -23601n0.bin=@23601n0.bin,0 -23601n1.bin=@23601n1.bin,0 -23601n2.bin=@23601n2.bin,0 -23602d0.bin=@23602d0.bin,0 -23602d1.bin=@23602d1.bin,0 -23602d2.bin=@23602d2.bin,0 -23602n0.bin=@23602n0.bin,0 -23602n1.bin=@23602n1.bin,0 -23602n2.bin=@23602n2.bin,0 -23603d0.bin=@23603d0.bin,0 -23603d1.bin=@23603d1.bin,0 -23603d2.bin=@23603d2.bin,0 -23603n0.bin=@23603n0.bin,0 -23603n1.bin=@23603n1.bin,0 -23603n2.bin=@23603n2.bin,0 -23604d0.bin=@23604d0.bin,0 -23604d1.bin=@23604d1.bin,0 -23604d2.bin=@23604d2.bin,0 -23604n0.bin=@23604n0.bin,0 -23604n1.bin=@23604n1.bin,0 -23604n2.bin=@23604n2.bin,0 -23605d0.bin=@23605d0.bin,0 -23605d1.bin=@23605d1.bin,0 -23605d2.bin=@23605d2.bin,0 -23605n0.bin=@23605n0.bin,0 -23605n1.bin=@23605n1.bin,0 -23605n2.bin=@23605n2.bin,0 -40241n0.bin=@40241n0.bin,0 -40241n1.bin=@40241n1.bin,0 -40241n2.bin=@40241n2.bin,0 -54594d0.bin=@54594d0.bin,0 -54594d1.bin=@54594d1.bin,0 -54594d2.bin=@54594d2.bin,0 -54594n0.bin=@54594n0.bin,0 -54594n1.bin=@54594n1.bin,0 -54594n2.bin=@54594n2.bin,0 -54603d0.bin=@54603d0.bin,0 -54603d1.bin=@54603d1.bin,0 -54603d2.bin=@54603d2.bin,0 -54603n0.bin=@54603n0.bin,0 -54603n1.bin=@54603n1.bin,0 -54603n2.bin=@54603n2.bin,0 -40241d0.bin=@40241d0.bin,0 -40241d1.bin=@40241d1.bin,0 -40241d2.bin=@40241d2.bin,0 -23493n0.bin=@23493n0.bin,0 -23493n1.bin=@23493n1.bin,0 -23493n2.bin=@23493n2.bin,0 -23494d0.bin=@23494d0.bin,0 -23494d1.bin=@23494d1.bin,0 -23494d2.bin=@23494d2.bin,0 -23494n0.bin=@23494n0.bin,0 -23494n1.bin=@23494n1.bin,0 -23494n2.bin=@23494n2.bin,0 -23495d0.bin=@23495d0.bin,0 -23495d1.bin=@23495d1.bin,0 -23495d2.bin=@23495d2.bin,0 -23495n0.bin=@23495n0.bin,0 -23495n1.bin=@23495n1.bin,0 -23495n2.bin=@23495n2.bin,0 -23496d0.bin=@23496d0.bin,0 -23496d1.bin=@23496d1.bin,0 -23496d2.bin=@23496d2.bin,0 -23496n0.bin=@23496n0.bin,0 -23496n1.bin=@23496n1.bin,0 -23496n2.bin=@23496n2.bin,0 -23497d0.bin=@23497d0.bin,0 -23497d1.bin=@23497d1.bin,0 -23497d2.bin=@23497d2.bin,0 -23497n0.bin=@23497n0.bin,0 -23497n1.bin=@23497n1.bin,0 -23497n2.bin=@23497n2.bin,0 -23498d0.bin=@23498d0.bin,0 -23498d1.bin=@23498d1.bin,0 -23498d2.bin=@23498d2.bin,0 -23498n0.bin=@23498n0.bin,0 -23498n1.bin=@23498n1.bin,0 -23498n2.bin=@23498n2.bin,0 -23499d0.bin=@23499d0.bin,0 -23499d1.bin=@23499d1.bin,0 -23499d2.bin=@23499d2.bin,0 -23499n0.bin=@23499n0.bin,0 -23499n1.bin=@23499n1.bin,0 -23499n2.bin=@23499n2.bin,0 -23513d0.bin=@23513d0.bin,0 -23513d1.bin=@23513d1.bin,0 -23513d2.bin=@23513d2.bin,0 -23513n0.bin=@23513n0.bin,0 -23513n1.bin=@23513n1.bin,0 -23513n2.bin=@23513n2.bin,0 -23528d0.bin=@23528d0.bin,0 -23528d1.bin=@23528d1.bin,0 -23528d2.bin=@23528d2.bin,0 -23528n0.bin=@23528n0.bin,0 -23528n1.bin=@23528n1.bin,0 -23528n2.bin=@23528n2.bin,0 -23643d0.bin=@23643d0.bin,0 -23643d1.bin=@23643d1.bin,0 -23643d2.bin=@23643d2.bin,0 -23643n0.bin=@23643n0.bin,0 -23643n1.bin=@23643n1.bin,0 -23643n2.bin=@23643n2.bin,0 -23667d0.bin=@23667d0.bin,0 -23667d1.bin=@23667d1.bin,0 -23667d2.bin=@23667d2.bin,0 -23667n0.bin=@23667n0.bin,0 -23667n1.bin=@23667n1.bin,0 -23667n2.bin=@23667n2.bin,0 -55679d0.bin=@55679d0.bin,0 -55679d1.bin=@55679d1.bin,0 -55679d2.bin=@55679d2.bin,0 -55679n0.bin=@55679n0.bin,0 -55679n1.bin=@55679n1.bin,0 -55679n2.bin=@55679n2.bin,0 -55680d0.bin=@55680d0.bin,0 -55680d1.bin=@55680d1.bin,0 -55680d2.bin=@55680d2.bin,0 -55680n0.bin=@55680n0.bin,0 -55680n1.bin=@55680n1.bin,0 -55680n2.bin=@55680n2.bin,0 -55772d0.bin=@55772d0.bin,0 -55772d1.bin=@55772d1.bin,0 -55772d2.bin=@55772d2.bin,0 -55772n0.bin=@55772n0.bin,0 -55772n1.bin=@55772n1.bin,0 -55772n2.bin=@55772n2.bin,0 -55916d0.bin=@55916d0.bin,0 -55916d1.bin=@55916d1.bin,0 -55916d2.bin=@55916d2.bin,0 -55916n0.bin=@55916n0.bin,0 -55916n1.bin=@55916n1.bin,0 -55916n2.bin=@55916n2.bin,0 -23493d0.bin=@23493d0.bin,0 -23493d1.bin=@23493d1.bin,0 -23493d2.bin=@23493d2.bin,0 -23199n2.bin=@23199n2.bin,0 -23201d0.bin=@23201d0.bin,0 -23201d1.bin=@23201d1.bin,0 -23201d2.bin=@23201d2.bin,0 -23201n0.bin=@23201n0.bin,0 -23201n1.bin=@23201n1.bin,0 -23201n2.bin=@23201n2.bin,0 -23202d0.bin=@23202d0.bin,0 -23202d1.bin=@23202d1.bin,0 -23202d2.bin=@23202d2.bin,0 -23202n0.bin=@23202n0.bin,0 -23202n1.bin=@23202n1.bin,0 -23202n2.bin=@23202n2.bin,0 -23203d0.bin=@23203d0.bin,0 -23203d1.bin=@23203d1.bin,0 -23203d2.bin=@23203d2.bin,0 -23203n0.bin=@23203n0.bin,0 -23203n1.bin=@23203n1.bin,0 -23203n2.bin=@23203n2.bin,0 -23204d0.bin=@23204d0.bin,0 -23204d1.bin=@23204d1.bin,0 -23204d2.bin=@23204d2.bin,0 -23204n0.bin=@23204n0.bin,0 -23204n1.bin=@23204n1.bin,0 -23204n2.bin=@23204n2.bin,0 -23205d0.bin=@23205d0.bin,0 -23205d1.bin=@23205d1.bin,0 -23205d2.bin=@23205d2.bin,0 -23205n0.bin=@23205n0.bin,0 -23205n1.bin=@23205n1.bin,0 -23205n2.bin=@23205n2.bin,0 -23206d0.bin=@23206d0.bin,0 -23206d1.bin=@23206d1.bin,0 -23206d2.bin=@23206d2.bin,0 -23206n0.bin=@23206n0.bin,0 -23206n1.bin=@23206n1.bin,0 -23206n2.bin=@23206n2.bin,0 -23207d0.bin=@23207d0.bin,0 -23207d1.bin=@23207d1.bin,0 -23207d2.bin=@23207d2.bin,0 -23207n0.bin=@23207n0.bin,0 -23207n1.bin=@23207n1.bin,0 -23207n2.bin=@23207n2.bin,0 -23208d0.bin=@23208d0.bin,0 -23208d1.bin=@23208d1.bin,0 -23208d2.bin=@23208d2.bin,0 -23208n0.bin=@23208n0.bin,0 -23208n1.bin=@23208n1.bin,0 -23208n2.bin=@23208n2.bin,0 -23209d0.bin=@23209d0.bin,0 -23209d1.bin=@23209d1.bin,0 -23209d2.bin=@23209d2.bin,0 -23209n0.bin=@23209n0.bin,0 -23209n1.bin=@23209n1.bin,0 -23209n2.bin=@23209n2.bin,0 -23210d0.bin=@23210d0.bin,0 -23210d1.bin=@23210d1.bin,0 -23210d2.bin=@23210d2.bin,0 -23210n0.bin=@23210n0.bin,0 -23210n1.bin=@23210n1.bin,0 -23210n2.bin=@23210n2.bin,0 -23253d0.bin=@23253d0.bin,0 -23253d1.bin=@23253d1.bin,0 -23253d2.bin=@23253d2.bin,0 -23253n0.bin=@23253n0.bin,0 -23253n1.bin=@23253n1.bin,0 -23253n2.bin=@23253n2.bin,0 -23254d0.bin=@23254d0.bin,0 -23254d1.bin=@23254d1.bin,0 -23254d2.bin=@23254d2.bin,0 -23254n0.bin=@23254n0.bin,0 -23254n1.bin=@23254n1.bin,0 -23254n2.bin=@23254n2.bin,0 -23269d0.bin=@23269d0.bin,0 -23269d1.bin=@23269d1.bin,0 -23269d2.bin=@23269d2.bin,0 -23269n0.bin=@23269n0.bin,0 -23269n1.bin=@23269n1.bin,0 -23269n2.bin=@23269n2.bin,0 -23270d0.bin=@23270d0.bin,0 -23270d1.bin=@23270d1.bin,0 -23270d2.bin=@23270d2.bin,0 -23270n0.bin=@23270n0.bin,0 -23270n1.bin=@23270n1.bin,0 -23270n2.bin=@23270n2.bin,0 -23271d0.bin=@23271d0.bin,0 -23271d1.bin=@23271d1.bin,0 -23271d2.bin=@23271d2.bin,0 -23271n0.bin=@23271n0.bin,0 -23271n1.bin=@23271n1.bin,0 -23271n2.bin=@23271n2.bin,0 -23272d0.bin=@23272d0.bin,0 -23272d1.bin=@23272d1.bin,0 -23272d2.bin=@23272d2.bin,0 -23272n0.bin=@23272n0.bin,0 -23272n1.bin=@23272n1.bin,0 -23272n2.bin=@23272n2.bin,0 -23273d0.bin=@23273d0.bin,0 -23273d1.bin=@23273d1.bin,0 -23273d2.bin=@23273d2.bin,0 -23273n0.bin=@23273n0.bin,0 -23273n1.bin=@23273n1.bin,0 -23273n2.bin=@23273n2.bin,0 -23274d0.bin=@23274d0.bin,0 -23274d1.bin=@23274d1.bin,0 -23274d2.bin=@23274d2.bin,0 -23274n0.bin=@23274n0.bin,0 -23274n1.bin=@23274n1.bin,0 -23274n2.bin=@23274n2.bin,0 -23280d0.bin=@23280d0.bin,0 -23280d1.bin=@23280d1.bin,0 -23280d2.bin=@23280d2.bin,0 -23280n0.bin=@23280n0.bin,0 -23280n1.bin=@23280n1.bin,0 -23280n2.bin=@23280n2.bin,0 -23281d0.bin=@23281d0.bin,0 -23281d1.bin=@23281d1.bin,0 -23281d2.bin=@23281d2.bin,0 -23281n0.bin=@23281n0.bin,0 -23281n1.bin=@23281n1.bin,0 -23281n2.bin=@23281n2.bin,0 -23303d0.bin=@23303d0.bin,0 -23303d1.bin=@23303d1.bin,0 -23303d2.bin=@23303d2.bin,0 -23303n0.bin=@23303n0.bin,0 -23303n1.bin=@23303n1.bin,0 -23303n2.bin=@23303n2.bin,0 -23304d0.bin=@23304d0.bin,0 -23304d1.bin=@23304d1.bin,0 -23304d2.bin=@23304d2.bin,0 -23304n0.bin=@23304n0.bin,0 -23304n1.bin=@23304n1.bin,0 -23304n2.bin=@23304n2.bin,0 -23323d0.bin=@23323d0.bin,0 -23323d1.bin=@23323d1.bin,0 -23323d2.bin=@23323d2.bin,0 -23323n0.bin=@23323n0.bin,0 -23323n1.bin=@23323n1.bin,0 -23323n2.bin=@23323n2.bin,0 -23326d0.bin=@23326d0.bin,0 -23326d1.bin=@23326d1.bin,0 -23326d2.bin=@23326d2.bin,0 -23326n0.bin=@23326n0.bin,0 -23326n1.bin=@23326n1.bin,0 -23326n2.bin=@23326n2.bin,0 -23199d0.bin=@23199d0.bin,0 -23199d1.bin=@23199d1.bin,0 -23199d2.bin=@23199d2.bin,0 -23199n0.bin=@23199n0.bin,0 -23199n1.bin=@23199n1.bin,0 -23211d2.bin=@23211d2.bin,0 -23211n0.bin=@23211n0.bin,0 -23211n1.bin=@23211n1.bin,0 -23211n2.bin=@23211n2.bin,0 -23212d0.bin=@23212d0.bin,0 -23212d1.bin=@23212d1.bin,0 -23212d2.bin=@23212d2.bin,0 -23212n0.bin=@23212n0.bin,0 -23212n1.bin=@23212n1.bin,0 -23212n2.bin=@23212n2.bin,0 -23213d0.bin=@23213d0.bin,0 -23213d1.bin=@23213d1.bin,0 -23213d2.bin=@23213d2.bin,0 -23213n0.bin=@23213n0.bin,0 -23213n1.bin=@23213n1.bin,0 -23213n2.bin=@23213n2.bin,0 -23214d0.bin=@23214d0.bin,0 -23214d1.bin=@23214d1.bin,0 -23214d2.bin=@23214d2.bin,0 -23214n0.bin=@23214n0.bin,0 -23214n1.bin=@23214n1.bin,0 -23214n2.bin=@23214n2.bin,0 -23215d0.bin=@23215d0.bin,0 -23215d1.bin=@23215d1.bin,0 -23215d2.bin=@23215d2.bin,0 -23215n0.bin=@23215n0.bin,0 -23215n1.bin=@23215n1.bin,0 -23215n2.bin=@23215n2.bin,0 -23216d0.bin=@23216d0.bin,0 -23216d1.bin=@23216d1.bin,0 -23216d2.bin=@23216d2.bin,0 -23216n0.bin=@23216n0.bin,0 -23216n1.bin=@23216n1.bin,0 -23216n2.bin=@23216n2.bin,0 -23217d0.bin=@23217d0.bin,0 -23217d1.bin=@23217d1.bin,0 -23217d2.bin=@23217d2.bin,0 -23217n0.bin=@23217n0.bin,0 -23217n1.bin=@23217n1.bin,0 -23217n2.bin=@23217n2.bin,0 -23218d0.bin=@23218d0.bin,0 -23218d1.bin=@23218d1.bin,0 -23218d2.bin=@23218d2.bin,0 -23218n0.bin=@23218n0.bin,0 -23218n1.bin=@23218n1.bin,0 -23218n2.bin=@23218n2.bin,0 -23219d0.bin=@23219d0.bin,0 -23219d1.bin=@23219d1.bin,0 -23219d2.bin=@23219d2.bin,0 -23219n0.bin=@23219n0.bin,0 -23219n1.bin=@23219n1.bin,0 -23219n2.bin=@23219n2.bin,0 -23220d0.bin=@23220d0.bin,0 -23220d1.bin=@23220d1.bin,0 -23220d2.bin=@23220d2.bin,0 -23220n0.bin=@23220n0.bin,0 -23220n1.bin=@23220n1.bin,0 -23220n2.bin=@23220n2.bin,0 -23221d0.bin=@23221d0.bin,0 -23221d1.bin=@23221d1.bin,0 -23221d2.bin=@23221d2.bin,0 -23221n0.bin=@23221n0.bin,0 -23221n1.bin=@23221n1.bin,0 -23221n2.bin=@23221n2.bin,0 -23223d0.bin=@23223d0.bin,0 -23223d1.bin=@23223d1.bin,0 -23223d2.bin=@23223d2.bin,0 -23223n0.bin=@23223n0.bin,0 -23223n1.bin=@23223n1.bin,0 -23223n2.bin=@23223n2.bin,0 -23255d0.bin=@23255d0.bin,0 -23255d1.bin=@23255d1.bin,0 -23255d2.bin=@23255d2.bin,0 -23255n0.bin=@23255n0.bin,0 -23255n1.bin=@23255n1.bin,0 -23255n2.bin=@23255n2.bin,0 -23256d0.bin=@23256d0.bin,0 -23256d1.bin=@23256d1.bin,0 -23256d2.bin=@23256d2.bin,0 -23256n0.bin=@23256n0.bin,0 -23256n1.bin=@23256n1.bin,0 -23256n2.bin=@23256n2.bin,0 -23282d0.bin=@23282d0.bin,0 -23282d1.bin=@23282d1.bin,0 -23282d2.bin=@23282d2.bin,0 -23282n0.bin=@23282n0.bin,0 -23282n1.bin=@23282n1.bin,0 -23282n2.bin=@23282n2.bin,0 -23283d0.bin=@23283d0.bin,0 -23283d1.bin=@23283d1.bin,0 -23283d2.bin=@23283d2.bin,0 -23283n0.bin=@23283n0.bin,0 -23283n1.bin=@23283n1.bin,0 -23283n2.bin=@23283n2.bin,0 -23305d0.bin=@23305d0.bin,0 -23305d1.bin=@23305d1.bin,0 -23305d2.bin=@23305d2.bin,0 -23305n0.bin=@23305n0.bin,0 -23305n1.bin=@23305n1.bin,0 -23305n2.bin=@23305n2.bin,0 -23320d0.bin=@23320d0.bin,0 -23320d1.bin=@23320d1.bin,0 -23320d2.bin=@23320d2.bin,0 -23320n0.bin=@23320n0.bin,0 -23320n1.bin=@23320n1.bin,0 -23320n2.bin=@23320n2.bin,0 -23321d0.bin=@23321d0.bin,0 -23321d1.bin=@23321d1.bin,0 -23321d2.bin=@23321d2.bin,0 -23321n0.bin=@23321n0.bin,0 -23321n1.bin=@23321n1.bin,0 -23321n2.bin=@23321n2.bin,0 -23324d0.bin=@23324d0.bin,0 -23324d1.bin=@23324d1.bin,0 -23324d2.bin=@23324d2.bin,0 -23324n0.bin=@23324n0.bin,0 -23324n1.bin=@23324n1.bin,0 -23324n2.bin=@23324n2.bin,0 -23350d0.bin=@23350d0.bin,0 -23350d1.bin=@23350d1.bin,0 -23350d2.bin=@23350d2.bin,0 -23350n0.bin=@23350n0.bin,0 -23350n1.bin=@23350n1.bin,0 -23350n2.bin=@23350n2.bin,0 -23351d0.bin=@23351d0.bin,0 -23351d1.bin=@23351d1.bin,0 -23351d2.bin=@23351d2.bin,0 -23351n0.bin=@23351n0.bin,0 -23351n1.bin=@23351n1.bin,0 -23351n2.bin=@23351n2.bin,0 -23369d0.bin=@23369d0.bin,0 -23369d1.bin=@23369d1.bin,0 -23369d2.bin=@23369d2.bin,0 -23369n0.bin=@23369n0.bin,0 -23369n1.bin=@23369n1.bin,0 -23369n2.bin=@23369n2.bin,0 -23370d0.bin=@23370d0.bin,0 -23370d1.bin=@23370d1.bin,0 -23370d2.bin=@23370d2.bin,0 -23370n0.bin=@23370n0.bin,0 -23370n1.bin=@23370n1.bin,0 -23370n2.bin=@23370n2.bin,0 -23399d0.bin=@23399d0.bin,0 -23399d1.bin=@23399d1.bin,0 -23399d2.bin=@23399d2.bin,0 -23399n0.bin=@23399n0.bin,0 -23399n1.bin=@23399n1.bin,0 -23399n2.bin=@23399n2.bin,0 -23401d0.bin=@23401d0.bin,0 -23401d1.bin=@23401d1.bin,0 -23401d2.bin=@23401d2.bin,0 -23401n0.bin=@23401n0.bin,0 -23401n1.bin=@23401n1.bin,0 -23401n2.bin=@23401n2.bin,0 -23211d0.bin=@23211d0.bin,0 -23211d1.bin=@23211d1.bin,0 -23224n1.bin=@23224n1.bin,0 -23224n2.bin=@23224n2.bin,0 -23225d0.bin=@23225d0.bin,0 -23225d1.bin=@23225d1.bin,0 -23225d2.bin=@23225d2.bin,0 -23225n0.bin=@23225n0.bin,0 -23225n1.bin=@23225n1.bin,0 -23225n2.bin=@23225n2.bin,0 -23226d0.bin=@23226d0.bin,0 -23226d1.bin=@23226d1.bin,0 -23226d2.bin=@23226d2.bin,0 -23226n0.bin=@23226n0.bin,0 -23226n1.bin=@23226n1.bin,0 -23226n2.bin=@23226n2.bin,0 -23227d0.bin=@23227d0.bin,0 -23227d1.bin=@23227d1.bin,0 -23227d2.bin=@23227d2.bin,0 -23227n0.bin=@23227n0.bin,0 -23227n1.bin=@23227n1.bin,0 -23227n2.bin=@23227n2.bin,0 -23228d0.bin=@23228d0.bin,0 -23228d1.bin=@23228d1.bin,0 -23228d2.bin=@23228d2.bin,0 -23228n0.bin=@23228n0.bin,0 -23228n1.bin=@23228n1.bin,0 -23228n2.bin=@23228n2.bin,0 -23229d0.bin=@23229d0.bin,0 -23229d1.bin=@23229d1.bin,0 -23229d2.bin=@23229d2.bin,0 -23229n0.bin=@23229n0.bin,0 -23229n1.bin=@23229n1.bin,0 -23229n2.bin=@23229n2.bin,0 -23230d0.bin=@23230d0.bin,0 -23230d1.bin=@23230d1.bin,0 -23230d2.bin=@23230d2.bin,0 -23230n0.bin=@23230n0.bin,0 -23230n1.bin=@23230n1.bin,0 -23230n2.bin=@23230n2.bin,0 -23231d0.bin=@23231d0.bin,0 -23231d1.bin=@23231d1.bin,0 -23231d2.bin=@23231d2.bin,0 -23231n0.bin=@23231n0.bin,0 -23231n1.bin=@23231n1.bin,0 -23231n2.bin=@23231n2.bin,0 -23232d0.bin=@23232d0.bin,0 -23232d1.bin=@23232d1.bin,0 -23232d2.bin=@23232d2.bin,0 -23232n0.bin=@23232n0.bin,0 -23232n1.bin=@23232n1.bin,0 -23232n2.bin=@23232n2.bin,0 -23233d0.bin=@23233d0.bin,0 -23233d1.bin=@23233d1.bin,0 -23233d2.bin=@23233d2.bin,0 -23233n0.bin=@23233n0.bin,0 -23233n1.bin=@23233n1.bin,0 -23233n2.bin=@23233n2.bin,0 -23238d0.bin=@23238d0.bin,0 -23238d1.bin=@23238d1.bin,0 -23238d2.bin=@23238d2.bin,0 -23238n0.bin=@23238n0.bin,0 -23238n1.bin=@23238n1.bin,0 -23238n2.bin=@23238n2.bin,0 -23239d0.bin=@23239d0.bin,0 -23239d1.bin=@23239d1.bin,0 -23239d2.bin=@23239d2.bin,0 -23239n0.bin=@23239n0.bin,0 -23239n1.bin=@23239n1.bin,0 -23239n2.bin=@23239n2.bin,0 -23257d0.bin=@23257d0.bin,0 -23257d1.bin=@23257d1.bin,0 -23257d2.bin=@23257d2.bin,0 -23257n0.bin=@23257n0.bin,0 -23257n1.bin=@23257n1.bin,0 -23257n2.bin=@23257n2.bin,0 -23258d0.bin=@23258d0.bin,0 -23258d1.bin=@23258d1.bin,0 -23258d2.bin=@23258d2.bin,0 -23258n0.bin=@23258n0.bin,0 -23258n1.bin=@23258n1.bin,0 -23258n2.bin=@23258n2.bin,0 -23284d0.bin=@23284d0.bin,0 -23284d1.bin=@23284d1.bin,0 -23284d2.bin=@23284d2.bin,0 -23284n0.bin=@23284n0.bin,0 -23284n1.bin=@23284n1.bin,0 -23284n2.bin=@23284n2.bin,0 -23285d0.bin=@23285d0.bin,0 -23285d1.bin=@23285d1.bin,0 -23285d2.bin=@23285d2.bin,0 -23285n0.bin=@23285n0.bin,0 -23285n1.bin=@23285n1.bin,0 -23285n2.bin=@23285n2.bin,0 -23311d0.bin=@23311d0.bin,0 -23311d1.bin=@23311d1.bin,0 -23311d2.bin=@23311d2.bin,0 -23311n0.bin=@23311n0.bin,0 -23311n1.bin=@23311n1.bin,0 -23311n2.bin=@23311n2.bin,0 -23346d0.bin=@23346d0.bin,0 -23346d1.bin=@23346d1.bin,0 -23346d2.bin=@23346d2.bin,0 -23346n0.bin=@23346n0.bin,0 -23346n1.bin=@23346n1.bin,0 -23346n2.bin=@23346n2.bin,0 -23347d0.bin=@23347d0.bin,0 -23347d1.bin=@23347d1.bin,0 -23347d2.bin=@23347d2.bin,0 -23347n0.bin=@23347n0.bin,0 -23347n1.bin=@23347n1.bin,0 -23347n2.bin=@23347n2.bin,0 -23371d0.bin=@23371d0.bin,0 -23371d1.bin=@23371d1.bin,0 -23371d2.bin=@23371d2.bin,0 -23371n0.bin=@23371n0.bin,0 -23371n1.bin=@23371n1.bin,0 -23371n2.bin=@23371n2.bin,0 -23372d0.bin=@23372d0.bin,0 -23372d1.bin=@23372d1.bin,0 -23372d2.bin=@23372d2.bin,0 -23372n0.bin=@23372n0.bin,0 -23372n1.bin=@23372n1.bin,0 -23372n2.bin=@23372n2.bin,0 -23386d0.bin=@23386d0.bin,0 -23386d1.bin=@23386d1.bin,0 -23386d2.bin=@23386d2.bin,0 -23386n0.bin=@23386n0.bin,0 -23386n1.bin=@23386n1.bin,0 -23386n2.bin=@23386n2.bin,0 -23387d0.bin=@23387d0.bin,0 -23387d1.bin=@23387d1.bin,0 -23387d2.bin=@23387d2.bin,0 -23387n0.bin=@23387n0.bin,0 -23387n1.bin=@23387n1.bin,0 -23387n2.bin=@23387n2.bin,0 -23224d0.bin=@23224d0.bin,0 -23224d1.bin=@23224d1.bin,0 -23224d2.bin=@23224d2.bin,0 -23224n0.bin=@23224n0.bin,0 -23236n0.bin=@23236n0.bin,0 -23236n1.bin=@23236n1.bin,0 -23236n2.bin=@23236n2.bin,0 -23237d0.bin=@23237d0.bin,0 -23237d1.bin=@23237d1.bin,0 -23237d2.bin=@23237d2.bin,0 -23237n0.bin=@23237n0.bin,0 -23237n1.bin=@23237n1.bin,0 -23237n2.bin=@23237n2.bin,0 -23240d0.bin=@23240d0.bin,0 -23240d1.bin=@23240d1.bin,0 -23240d2.bin=@23240d2.bin,0 -23240n0.bin=@23240n0.bin,0 -23240n1.bin=@23240n1.bin,0 -23240n2.bin=@23240n2.bin,0 -23241d0.bin=@23241d0.bin,0 -23241d1.bin=@23241d1.bin,0 -23241d2.bin=@23241d2.bin,0 -23241n0.bin=@23241n0.bin,0 -23241n1.bin=@23241n1.bin,0 -23241n2.bin=@23241n2.bin,0 -23250d0.bin=@23250d0.bin,0 -23250d1.bin=@23250d1.bin,0 -23250d2.bin=@23250d2.bin,0 -23250n0.bin=@23250n0.bin,0 -23250n1.bin=@23250n1.bin,0 -23250n2.bin=@23250n2.bin,0 -23251d0.bin=@23251d0.bin,0 -23251d1.bin=@23251d1.bin,0 -23251d2.bin=@23251d2.bin,0 -23251n0.bin=@23251n0.bin,0 -23251n1.bin=@23251n1.bin,0 -23251n2.bin=@23251n2.bin,0 -23252d0.bin=@23252d0.bin,0 -23252d1.bin=@23252d1.bin,0 -23252d2.bin=@23252d2.bin,0 -23252n0.bin=@23252n0.bin,0 -23252n1.bin=@23252n1.bin,0 -23252n2.bin=@23252n2.bin,0 -23259d0.bin=@23259d0.bin,0 -23259d1.bin=@23259d1.bin,0 -23259d2.bin=@23259d2.bin,0 -23259n0.bin=@23259n0.bin,0 -23259n1.bin=@23259n1.bin,0 -23259n2.bin=@23259n2.bin,0 -23260d0.bin=@23260d0.bin,0 -23260d1.bin=@23260d1.bin,0 -23260d2.bin=@23260d2.bin,0 -23260n0.bin=@23260n0.bin,0 -23260n1.bin=@23260n1.bin,0 -23260n2.bin=@23260n2.bin,0 -23286d0.bin=@23286d0.bin,0 -23286d1.bin=@23286d1.bin,0 -23286d2.bin=@23286d2.bin,0 -23286n0.bin=@23286n0.bin,0 -23286n1.bin=@23286n1.bin,0 -23286n2.bin=@23286n2.bin,0 -23287d0.bin=@23287d0.bin,0 -23287d1.bin=@23287d1.bin,0 -23287d2.bin=@23287d2.bin,0 -23287n0.bin=@23287n0.bin,0 -23287n1.bin=@23287n1.bin,0 -23287n2.bin=@23287n2.bin,0 -23288d0.bin=@23288d0.bin,0 -23288d1.bin=@23288d1.bin,0 -23288d2.bin=@23288d2.bin,0 -23288n0.bin=@23288n0.bin,0 -23288n1.bin=@23288n1.bin,0 -23288n2.bin=@23288n2.bin,0 -23289d0.bin=@23289d0.bin,0 -23289d1.bin=@23289d1.bin,0 -23289d2.bin=@23289d2.bin,0 -23289n0.bin=@23289n0.bin,0 -23289n1.bin=@23289n1.bin,0 -23289n2.bin=@23289n2.bin,0 -23325d0.bin=@23325d0.bin,0 -23325d1.bin=@23325d1.bin,0 -23325d2.bin=@23325d2.bin,0 -23325n0.bin=@23325n0.bin,0 -23325n1.bin=@23325n1.bin,0 -23325n2.bin=@23325n2.bin,0 -23327d0.bin=@23327d0.bin,0 -23327d1.bin=@23327d1.bin,0 -23327d2.bin=@23327d2.bin,0 -23327n0.bin=@23327n0.bin,0 -23327n1.bin=@23327n1.bin,0 -23327n2.bin=@23327n2.bin,0 -23348d0.bin=@23348d0.bin,0 -23348d1.bin=@23348d1.bin,0 -23348d2.bin=@23348d2.bin,0 -23348n0.bin=@23348n0.bin,0 -23348n1.bin=@23348n1.bin,0 -23348n2.bin=@23348n2.bin,0 -23349d0.bin=@23349d0.bin,0 -23349d1.bin=@23349d1.bin,0 -23349d2.bin=@23349d2.bin,0 -23349n0.bin=@23349n0.bin,0 -23349n1.bin=@23349n1.bin,0 -23349n2.bin=@23349n2.bin,0 -23382d0.bin=@23382d0.bin,0 -23382d1.bin=@23382d1.bin,0 -23382d2.bin=@23382d2.bin,0 -23382n0.bin=@23382n0.bin,0 -23382n1.bin=@23382n1.bin,0 -23382n2.bin=@23382n2.bin,0 -23383d0.bin=@23383d0.bin,0 -23383d1.bin=@23383d1.bin,0 -23383d2.bin=@23383d2.bin,0 -23383n0.bin=@23383n0.bin,0 -23383n1.bin=@23383n1.bin,0 -23383n2.bin=@23383n2.bin,0 -23384d0.bin=@23384d0.bin,0 -23384d1.bin=@23384d1.bin,0 -23384d2.bin=@23384d2.bin,0 -23384n0.bin=@23384n0.bin,0 -23384n1.bin=@23384n1.bin,0 -23384n2.bin=@23384n2.bin,0 -23385d0.bin=@23385d0.bin,0 -23385d1.bin=@23385d1.bin,0 -23385d2.bin=@23385d2.bin,0 -23385n0.bin=@23385n0.bin,0 -23385n1.bin=@23385n1.bin,0 -23385n2.bin=@23385n2.bin,0 -23394d0.bin=@23394d0.bin,0 -23394d1.bin=@23394d1.bin,0 -23394d2.bin=@23394d2.bin,0 -23394n0.bin=@23394n0.bin,0 -23394n1.bin=@23394n1.bin,0 -23394n2.bin=@23394n2.bin,0 -23395d0.bin=@23395d0.bin,0 -23395d1.bin=@23395d1.bin,0 -23395d2.bin=@23395d2.bin,0 -23395n0.bin=@23395n0.bin,0 -23395n1.bin=@23395n1.bin,0 -23395n2.bin=@23395n2.bin,0 -23396d0.bin=@23396d0.bin,0 -23396d1.bin=@23396d1.bin,0 -23396d2.bin=@23396d2.bin,0 -23396n0.bin=@23396n0.bin,0 -23396n1.bin=@23396n1.bin,0 -23396n2.bin=@23396n2.bin,0 -23236d0.bin=@23236d0.bin,0 -23236d1.bin=@23236d1.bin,0 -23236d2.bin=@23236d2.bin,0 -23234d2.bin=@23234d2.bin,0 -23234n0.bin=@23234n0.bin,0 -23234n1.bin=@23234n1.bin,0 -23234n2.bin=@23234n2.bin,0 -23235d0.bin=@23235d0.bin,0 -23235d1.bin=@23235d1.bin,0 -23235d2.bin=@23235d2.bin,0 -23235n0.bin=@23235n0.bin,0 -23235n1.bin=@23235n1.bin,0 -23235n2.bin=@23235n2.bin,0 -23246d0.bin=@23246d0.bin,0 -23246d1.bin=@23246d1.bin,0 -23246d2.bin=@23246d2.bin,0 -23246n0.bin=@23246n0.bin,0 -23246n1.bin=@23246n1.bin,0 -23246n2.bin=@23246n2.bin,0 -23247d0.bin=@23247d0.bin,0 -23247d1.bin=@23247d1.bin,0 -23247d2.bin=@23247d2.bin,0 -23247n0.bin=@23247n0.bin,0 -23247n1.bin=@23247n1.bin,0 -23247n2.bin=@23247n2.bin,0 -23248d0.bin=@23248d0.bin,0 -23248d1.bin=@23248d1.bin,0 -23248d2.bin=@23248d2.bin,0 -23248n0.bin=@23248n0.bin,0 -23248n1.bin=@23248n1.bin,0 -23248n2.bin=@23248n2.bin,0 -23249d0.bin=@23249d0.bin,0 -23249d1.bin=@23249d1.bin,0 -23249d2.bin=@23249d2.bin,0 -23249n0.bin=@23249n0.bin,0 -23249n1.bin=@23249n1.bin,0 -23249n2.bin=@23249n2.bin,0 -23261d0.bin=@23261d0.bin,0 -23261d1.bin=@23261d1.bin,0 -23261d2.bin=@23261d2.bin,0 -23261n0.bin=@23261n0.bin,0 -23261n1.bin=@23261n1.bin,0 -23261n2.bin=@23261n2.bin,0 -23262d0.bin=@23262d0.bin,0 -23262d1.bin=@23262d1.bin,0 -23262d2.bin=@23262d2.bin,0 -23262n0.bin=@23262n0.bin,0 -23262n1.bin=@23262n1.bin,0 -23262n2.bin=@23262n2.bin,0 -23267d0.bin=@23267d0.bin,0 -23267d1.bin=@23267d1.bin,0 -23267d2.bin=@23267d2.bin,0 -23267n0.bin=@23267n0.bin,0 -23267n1.bin=@23267n1.bin,0 -23267n2.bin=@23267n2.bin,0 -23268d0.bin=@23268d0.bin,0 -23268d1.bin=@23268d1.bin,0 -23268d2.bin=@23268d2.bin,0 -23268n0.bin=@23268n0.bin,0 -23268n1.bin=@23268n1.bin,0 -23268n2.bin=@23268n2.bin,0 -23275d0.bin=@23275d0.bin,0 -23275d1.bin=@23275d1.bin,0 -23275d2.bin=@23275d2.bin,0 -23275n0.bin=@23275n0.bin,0 -23275n1.bin=@23275n1.bin,0 -23275n2.bin=@23275n2.bin,0 -23309d0.bin=@23309d0.bin,0 -23309d1.bin=@23309d1.bin,0 -23309d2.bin=@23309d2.bin,0 -23309n0.bin=@23309n0.bin,0 -23309n1.bin=@23309n1.bin,0 -23309n2.bin=@23309n2.bin,0 -23310d0.bin=@23310d0.bin,0 -23310d1.bin=@23310d1.bin,0 -23310d2.bin=@23310d2.bin,0 -23310n0.bin=@23310n0.bin,0 -23310n1.bin=@23310n1.bin,0 -23310n2.bin=@23310n2.bin,0 -23345d0.bin=@23345d0.bin,0 -23345d1.bin=@23345d1.bin,0 -23345d2.bin=@23345d2.bin,0 -23345n0.bin=@23345n0.bin,0 -23345n1.bin=@23345n1.bin,0 -23345n2.bin=@23345n2.bin,0 -23397d0.bin=@23397d0.bin,0 -23397d1.bin=@23397d1.bin,0 -23397d2.bin=@23397d2.bin,0 -23397n0.bin=@23397n0.bin,0 -23397n1.bin=@23397n1.bin,0 -23397n2.bin=@23397n2.bin,0 -23398d0.bin=@23398d0.bin,0 -23398d1.bin=@23398d1.bin,0 -23398d2.bin=@23398d2.bin,0 -23398n0.bin=@23398n0.bin,0 -23398n1.bin=@23398n1.bin,0 -23398n2.bin=@23398n2.bin,0 -23403d0.bin=@23403d0.bin,0 -23403d1.bin=@23403d1.bin,0 -23403d2.bin=@23403d2.bin,0 -23403n0.bin=@23403n0.bin,0 -23403n1.bin=@23403n1.bin,0 -23403n2.bin=@23403n2.bin,0 -23404d0.bin=@23404d0.bin,0 -23404d1.bin=@23404d1.bin,0 -23404d2.bin=@23404d2.bin,0 -23404n0.bin=@23404n0.bin,0 -23404n1.bin=@23404n1.bin,0 -23404n2.bin=@23404n2.bin,0 -23425d0.bin=@23425d0.bin,0 -23425d1.bin=@23425d1.bin,0 -23425d2.bin=@23425d2.bin,0 -23425n0.bin=@23425n0.bin,0 -23425n1.bin=@23425n1.bin,0 -23425n2.bin=@23425n2.bin,0 -23426d0.bin=@23426d0.bin,0 -23426d1.bin=@23426d1.bin,0 -23426d2.bin=@23426d2.bin,0 -23426n0.bin=@23426n0.bin,0 -23426n1.bin=@23426n1.bin,0 -23426n2.bin=@23426n2.bin,0 -23433d0.bin=@23433d0.bin,0 -23433d1.bin=@23433d1.bin,0 -23433d2.bin=@23433d2.bin,0 -23433n0.bin=@23433n0.bin,0 -23433n1.bin=@23433n1.bin,0 -23433n2.bin=@23433n2.bin,0 -23434d0.bin=@23434d0.bin,0 -23434d1.bin=@23434d1.bin,0 -23434d2.bin=@23434d2.bin,0 -23434n0.bin=@23434n0.bin,0 -23434n1.bin=@23434n1.bin,0 -23434n2.bin=@23434n2.bin,0 -23234d0.bin=@23234d0.bin,0 -23234d1.bin=@23234d1.bin,0 -23242n0.bin=@23242n0.bin,0 -23242n1.bin=@23242n1.bin,0 -23242n2.bin=@23242n2.bin,0 -23243d0.bin=@23243d0.bin,0 -23243d1.bin=@23243d1.bin,0 -23243d2.bin=@23243d2.bin,0 -23243n0.bin=@23243n0.bin,0 -23243n1.bin=@23243n1.bin,0 -23243n2.bin=@23243n2.bin,0 -23244d0.bin=@23244d0.bin,0 -23244d1.bin=@23244d1.bin,0 -23244d2.bin=@23244d2.bin,0 -23244n0.bin=@23244n0.bin,0 -23244n1.bin=@23244n1.bin,0 -23244n2.bin=@23244n2.bin,0 -23245d0.bin=@23245d0.bin,0 -23245d1.bin=@23245d1.bin,0 -23245d2.bin=@23245d2.bin,0 -23245n0.bin=@23245n0.bin,0 -23245n1.bin=@23245n1.bin,0 -23245n2.bin=@23245n2.bin,0 -23263d0.bin=@23263d0.bin,0 -23263d1.bin=@23263d1.bin,0 -23263d2.bin=@23263d2.bin,0 -23263n0.bin=@23263n0.bin,0 -23263n1.bin=@23263n1.bin,0 -23263n2.bin=@23263n2.bin,0 -23264d0.bin=@23264d0.bin,0 -23264d1.bin=@23264d1.bin,0 -23264d2.bin=@23264d2.bin,0 -23264n0.bin=@23264n0.bin,0 -23264n1.bin=@23264n1.bin,0 -23264n2.bin=@23264n2.bin,0 -23277d0.bin=@23277d0.bin,0 -23277d1.bin=@23277d1.bin,0 -23277d2.bin=@23277d2.bin,0 -23277n0.bin=@23277n0.bin,0 -23277n1.bin=@23277n1.bin,0 -23277n2.bin=@23277n2.bin,0 -23278d0.bin=@23278d0.bin,0 -23278d1.bin=@23278d1.bin,0 -23278d2.bin=@23278d2.bin,0 -23278n0.bin=@23278n0.bin,0 -23278n1.bin=@23278n1.bin,0 -23278n2.bin=@23278n2.bin,0 -23279d0.bin=@23279d0.bin,0 -23279d1.bin=@23279d1.bin,0 -23279d2.bin=@23279d2.bin,0 -23279n0.bin=@23279n0.bin,0 -23279n1.bin=@23279n1.bin,0 -23279n2.bin=@23279n2.bin,0 -23307d0.bin=@23307d0.bin,0 -23307d1.bin=@23307d1.bin,0 -23307d2.bin=@23307d2.bin,0 -23307n0.bin=@23307n0.bin,0 -23307n1.bin=@23307n1.bin,0 -23307n2.bin=@23307n2.bin,0 -23312d0.bin=@23312d0.bin,0 -23312d1.bin=@23312d1.bin,0 -23312d2.bin=@23312d2.bin,0 -23312n0.bin=@23312n0.bin,0 -23312n1.bin=@23312n1.bin,0 -23312n2.bin=@23312n2.bin,0 -23313d0.bin=@23313d0.bin,0 -23313d1.bin=@23313d1.bin,0 -23313d2.bin=@23313d2.bin,0 -23313n0.bin=@23313n0.bin,0 -23313n1.bin=@23313n1.bin,0 -23313n2.bin=@23313n2.bin,0 -23318d0.bin=@23318d0.bin,0 -23318d1.bin=@23318d1.bin,0 -23318d2.bin=@23318d2.bin,0 -23318n0.bin=@23318n0.bin,0 -23318n1.bin=@23318n1.bin,0 -23318n2.bin=@23318n2.bin,0 -23319d0.bin=@23319d0.bin,0 -23319d1.bin=@23319d1.bin,0 -23319d2.bin=@23319d2.bin,0 -23319n0.bin=@23319n0.bin,0 -23319n1.bin=@23319n1.bin,0 -23319n2.bin=@23319n2.bin,0 -23322d0.bin=@23322d0.bin,0 -23322d1.bin=@23322d1.bin,0 -23322d2.bin=@23322d2.bin,0 -23322n0.bin=@23322n0.bin,0 -23322n1.bin=@23322n1.bin,0 -23322n2.bin=@23322n2.bin,0 -23342d0.bin=@23342d0.bin,0 -23342d1.bin=@23342d1.bin,0 -23342d2.bin=@23342d2.bin,0 -23342n0.bin=@23342n0.bin,0 -23342n1.bin=@23342n1.bin,0 -23342n2.bin=@23342n2.bin,0 -23343d0.bin=@23343d0.bin,0 -23343d1.bin=@23343d1.bin,0 -23343d2.bin=@23343d2.bin,0 -23343n0.bin=@23343n0.bin,0 -23343n1.bin=@23343n1.bin,0 -23343n2.bin=@23343n2.bin,0 -23344d0.bin=@23344d0.bin,0 -23344d1.bin=@23344d1.bin,0 -23344d2.bin=@23344d2.bin,0 -23344n0.bin=@23344n0.bin,0 -23344n1.bin=@23344n1.bin,0 -23344n2.bin=@23344n2.bin,0 -23373d0.bin=@23373d0.bin,0 -23373d1.bin=@23373d1.bin,0 -23373d2.bin=@23373d2.bin,0 -23373n0.bin=@23373n0.bin,0 -23373n1.bin=@23373n1.bin,0 -23373n2.bin=@23373n2.bin,0 -23429d0.bin=@23429d0.bin,0 -23429d1.bin=@23429d1.bin,0 -23429d2.bin=@23429d2.bin,0 -23429n0.bin=@23429n0.bin,0 -23429n1.bin=@23429n1.bin,0 -23429n2.bin=@23429n2.bin,0 -23430d0.bin=@23430d0.bin,0 -23430d1.bin=@23430d1.bin,0 -23430d2.bin=@23430d2.bin,0 -23430n0.bin=@23430n0.bin,0 -23430n1.bin=@23430n1.bin,0 -23430n2.bin=@23430n2.bin,0 -23431d0.bin=@23431d0.bin,0 -23431d1.bin=@23431d1.bin,0 -23431d2.bin=@23431d2.bin,0 -23431n0.bin=@23431n0.bin,0 -23431n1.bin=@23431n1.bin,0 -23431n2.bin=@23431n2.bin,0 -23432d0.bin=@23432d0.bin,0 -23432d1.bin=@23432d1.bin,0 -23432d2.bin=@23432d2.bin,0 -23432n0.bin=@23432n0.bin,0 -23432n1.bin=@23432n1.bin,0 -23432n2.bin=@23432n2.bin,0 -23242d0.bin=@23242d0.bin,0 -23242d1.bin=@23242d1.bin,0 -23242d2.bin=@23242d2.bin,0 -23265n2.bin=@23265n2.bin,0 -23266d0.bin=@23266d0.bin,0 -23266d1.bin=@23266d1.bin,0 -23266d2.bin=@23266d2.bin,0 -23266n0.bin=@23266n0.bin,0 -23266n1.bin=@23266n1.bin,0 -23266n2.bin=@23266n2.bin,0 -23306d0.bin=@23306d0.bin,0 -23306d1.bin=@23306d1.bin,0 -23306d2.bin=@23306d2.bin,0 -23306n0.bin=@23306n0.bin,0 -23306n1.bin=@23306n1.bin,0 -23306n2.bin=@23306n2.bin,0 -23308d0.bin=@23308d0.bin,0 -23308d1.bin=@23308d1.bin,0 -23308d2.bin=@23308d2.bin,0 -23308n0.bin=@23308n0.bin,0 -23308n1.bin=@23308n1.bin,0 -23308n2.bin=@23308n2.bin,0 -23314d0.bin=@23314d0.bin,0 -23314d1.bin=@23314d1.bin,0 -23314d2.bin=@23314d2.bin,0 -23314n0.bin=@23314n0.bin,0 -23314n1.bin=@23314n1.bin,0 -23314n2.bin=@23314n2.bin,0 -23367d0.bin=@23367d0.bin,0 -23367d1.bin=@23367d1.bin,0 -23367d2.bin=@23367d2.bin,0 -23367n0.bin=@23367n0.bin,0 -23367n1.bin=@23367n1.bin,0 -23367n2.bin=@23367n2.bin,0 -23368d0.bin=@23368d0.bin,0 -23368d1.bin=@23368d1.bin,0 -23368d2.bin=@23368d2.bin,0 -23368n0.bin=@23368n0.bin,0 -23368n1.bin=@23368n1.bin,0 -23368n2.bin=@23368n2.bin,0 -23380d0.bin=@23380d0.bin,0 -23380d1.bin=@23380d1.bin,0 -23380d2.bin=@23380d2.bin,0 -23380n0.bin=@23380n0.bin,0 -23380n1.bin=@23380n1.bin,0 -23380n2.bin=@23380n2.bin,0 -23381d0.bin=@23381d0.bin,0 -23381d1.bin=@23381d1.bin,0 -23381d2.bin=@23381d2.bin,0 -23381n0.bin=@23381n0.bin,0 -23381n1.bin=@23381n1.bin,0 -23381n2.bin=@23381n2.bin,0 -23392d0.bin=@23392d0.bin,0 -23392d1.bin=@23392d1.bin,0 -23392d2.bin=@23392d2.bin,0 -23392n0.bin=@23392n0.bin,0 -23392n1.bin=@23392n1.bin,0 -23392n2.bin=@23392n2.bin,0 -23393d0.bin=@23393d0.bin,0 -23393d1.bin=@23393d1.bin,0 -23393d2.bin=@23393d2.bin,0 -23393n0.bin=@23393n0.bin,0 -23393n1.bin=@23393n1.bin,0 -23393n2.bin=@23393n2.bin,0 -23402d0.bin=@23402d0.bin,0 -23402d1.bin=@23402d1.bin,0 -23402d2.bin=@23402d2.bin,0 -23402n0.bin=@23402n0.bin,0 -23402n1.bin=@23402n1.bin,0 -23402n2.bin=@23402n2.bin,0 -23424d0.bin=@23424d0.bin,0 -23424d1.bin=@23424d1.bin,0 -23424d2.bin=@23424d2.bin,0 -23424n0.bin=@23424n0.bin,0 -23424n1.bin=@23424n1.bin,0 -23424n2.bin=@23424n2.bin,0 -23427d0.bin=@23427d0.bin,0 -23427d1.bin=@23427d1.bin,0 -23427d2.bin=@23427d2.bin,0 -23427n0.bin=@23427n0.bin,0 -23427n1.bin=@23427n1.bin,0 -23427n2.bin=@23427n2.bin,0 -23428d0.bin=@23428d0.bin,0 -23428d1.bin=@23428d1.bin,0 -23428d2.bin=@23428d2.bin,0 -23428n0.bin=@23428n0.bin,0 -23428n1.bin=@23428n1.bin,0 -23428n2.bin=@23428n2.bin,0 -23514d0.bin=@23514d0.bin,0 -23514d1.bin=@23514d1.bin,0 -23514d2.bin=@23514d2.bin,0 -23514n0.bin=@23514n0.bin,0 -23514n1.bin=@23514n1.bin,0 -23514n2.bin=@23514n2.bin,0 -23515d0.bin=@23515d0.bin,0 -23515d1.bin=@23515d1.bin,0 -23515d2.bin=@23515d2.bin,0 -23515n0.bin=@23515n0.bin,0 -23515n1.bin=@23515n1.bin,0 -23515n2.bin=@23515n2.bin,0 -23626d0.bin=@23626d0.bin,0 -23626d1.bin=@23626d1.bin,0 -23626d2.bin=@23626d2.bin,0 -23626n0.bin=@23626n0.bin,0 -23626n1.bin=@23626n1.bin,0 -23626n2.bin=@23626n2.bin,0 -55016d0.bin=@55016d0.bin,0 -55016d1.bin=@55016d1.bin,0 -55016d2.bin=@55016d2.bin,0 -55016n0.bin=@55016n0.bin,0 -55016n1.bin=@55016n1.bin,0 -55016n2.bin=@55016n2.bin,0 -55202d0.bin=@55202d0.bin,0 -55202d1.bin=@55202d1.bin,0 -55202d2.bin=@55202d2.bin,0 -55202n0.bin=@55202n0.bin,0 -55202n1.bin=@55202n1.bin,0 -55202n2.bin=@55202n2.bin,0 -58073d0.bin=@58073d0.bin,0 -58073d1.bin=@58073d1.bin,0 -58073d2.bin=@58073d2.bin,0 -58073n0.bin=@58073n0.bin,0 -58073n1.bin=@58073n1.bin,0 -58073n2.bin=@58073n2.bin,0 -64551d0.bin=@64551d0.bin,0 -64551d1.bin=@64551d1.bin,0 -64551d2.bin=@64551d2.bin,0 -23265d0.bin=@23265d0.bin,0 -23265d1.bin=@23265d1.bin,0 -23265d2.bin=@23265d2.bin,0 -23265n0.bin=@23265n0.bin,0 -23265n1.bin=@23265n1.bin,0 -23648n1.bin=@23648n1.bin,0 -23648n2.bin=@23648n2.bin,0 -23649d0.bin=@23649d0.bin,0 -23649d1.bin=@23649d1.bin,0 -23649d2.bin=@23649d2.bin,0 -23649n0.bin=@23649n0.bin,0 -23649n1.bin=@23649n1.bin,0 -23649n2.bin=@23649n2.bin,0 -54926d0.bin=@54926d0.bin,0 -54926d1.bin=@54926d1.bin,0 -54926d2.bin=@54926d2.bin,0 -54926n0.bin=@54926n0.bin,0 -54926n1.bin=@54926n1.bin,0 -54926n2.bin=@54926n2.bin,0 -55195d0.bin=@55195d0.bin,0 -55195d1.bin=@55195d1.bin,0 -55195d2.bin=@55195d2.bin,0 -55195n0.bin=@55195n0.bin,0 -55195n1.bin=@55195n1.bin,0 -55195n2.bin=@55195n2.bin,0 -55529d0.bin=@55529d0.bin,0 -55529d1.bin=@55529d1.bin,0 -55529d2.bin=@55529d2.bin,0 -55529n0.bin=@55529n0.bin,0 -55529n1.bin=@55529n1.bin,0 -55529n2.bin=@55529n2.bin,0 -55530d0.bin=@55530d0.bin,0 -55530d1.bin=@55530d1.bin,0 -55530d2.bin=@55530d2.bin,0 -55530n0.bin=@55530n0.bin,0 -55530n1.bin=@55530n1.bin,0 -55530n2.bin=@55530n2.bin,0 -55531d0.bin=@55531d0.bin,0 -55531d1.bin=@55531d1.bin,0 -55531d2.bin=@55531d2.bin,0 -55531n0.bin=@55531n0.bin,0 -55531n1.bin=@55531n1.bin,0 -55531n2.bin=@55531n2.bin,0 -55532d0.bin=@55532d0.bin,0 -55532d1.bin=@55532d1.bin,0 -55532d2.bin=@55532d2.bin,0 -55532n0.bin=@55532n0.bin,0 -55532n1.bin=@55532n1.bin,0 -55532n2.bin=@55532n2.bin,0 -55534d0.bin=@55534d0.bin,0 -55534d1.bin=@55534d1.bin,0 -55534d2.bin=@55534d2.bin,0 -55534n0.bin=@55534n0.bin,0 -55534n1.bin=@55534n1.bin,0 -55534n2.bin=@55534n2.bin,0 -55535d0.bin=@55535d0.bin,0 -55535d1.bin=@55535d1.bin,0 -55535d2.bin=@55535d2.bin,0 -55535n0.bin=@55535n0.bin,0 -55535n1.bin=@55535n1.bin,0 -55535n2.bin=@55535n2.bin,0 -55714d0.bin=@55714d0.bin,0 -55714d1.bin=@55714d1.bin,0 -55714d2.bin=@55714d2.bin,0 -55714n0.bin=@55714n0.bin,0 -55714n1.bin=@55714n1.bin,0 -55714n2.bin=@55714n2.bin,0 -55917d0.bin=@55917d0.bin,0 -55917d1.bin=@55917d1.bin,0 -55917d2.bin=@55917d2.bin,0 -55917n0.bin=@55917n0.bin,0 -55917n1.bin=@55917n1.bin,0 -55917n2.bin=@55917n2.bin,0 -55918d0.bin=@55918d0.bin,0 -55918d1.bin=@55918d1.bin,0 -55918d2.bin=@55918d2.bin,0 -55918n0.bin=@55918n0.bin,0 -55918n1.bin=@55918n1.bin,0 -55918n2.bin=@55918n2.bin,0 -55920d0.bin=@55920d0.bin,0 -55920d1.bin=@55920d1.bin,0 -55920d2.bin=@55920d2.bin,0 -55920n0.bin=@55920n0.bin,0 -55920n1.bin=@55920n1.bin,0 -55920n2.bin=@55920n2.bin,0 -55921d0.bin=@55921d0.bin,0 -55921d1.bin=@55921d1.bin,0 -55921d2.bin=@55921d2.bin,0 -55921n0.bin=@55921n0.bin,0 -55921n1.bin=@55921n1.bin,0 -55921n2.bin=@55921n2.bin,0 -55922d0.bin=@55922d0.bin,0 -55922d1.bin=@55922d1.bin,0 -55922d2.bin=@55922d2.bin,0 -55922n0.bin=@55922n0.bin,0 -55922n1.bin=@55922n1.bin,0 -55922n2.bin=@55922n2.bin,0 -55935d0.bin=@55935d0.bin,0 -55935d1.bin=@55935d1.bin,0 -55935d2.bin=@55935d2.bin,0 -55935n0.bin=@55935n0.bin,0 -55935n1.bin=@55935n1.bin,0 -55935n2.bin=@55935n2.bin,0 -55936d0.bin=@55936d0.bin,0 -55936d1.bin=@55936d1.bin,0 -55936d2.bin=@55936d2.bin,0 -55936n0.bin=@55936n0.bin,0 -55936n1.bin=@55936n1.bin,0 -55936n2.bin=@55936n2.bin,0 -55948d0.bin=@55948d0.bin,0 -55948d1.bin=@55948d1.bin,0 -55948d2.bin=@55948d2.bin,0 -55948n0.bin=@55948n0.bin,0 -55948n1.bin=@55948n1.bin,0 -55948n2.bin=@55948n2.bin,0 -55949d0.bin=@55949d0.bin,0 -55949d1.bin=@55949d1.bin,0 -55949d2.bin=@55949d2.bin,0 -55949n0.bin=@55949n0.bin,0 -55949n1.bin=@55949n1.bin,0 -55949n2.bin=@55949n2.bin,0 -55950d0.bin=@55950d0.bin,0 -55950d1.bin=@55950d1.bin,0 -55950d2.bin=@55950d2.bin,0 -55950n0.bin=@55950n0.bin,0 -55950n1.bin=@55950n1.bin,0 -55950n2.bin=@55950n2.bin,0 -55951d0.bin=@55951d0.bin,0 -55951d1.bin=@55951d1.bin,0 -55951d2.bin=@55951d2.bin,0 -55951n0.bin=@55951n0.bin,0 -55951n1.bin=@55951n1.bin,0 -55951n2.bin=@55951n2.bin,0 -56106d0.bin=@56106d0.bin,0 -56106d1.bin=@56106d1.bin,0 -56106d2.bin=@56106d2.bin,0 -56106n0.bin=@56106n0.bin,0 -56106n1.bin=@56106n1.bin,0 -56106n2.bin=@56106n2.bin,0 -56115d0.bin=@56115d0.bin,0 -56115d1.bin=@56115d1.bin,0 -56115d2.bin=@56115d2.bin,0 -56115n0.bin=@56115n0.bin,0 -56115n1.bin=@56115n1.bin,0 -56115n2.bin=@56115n2.bin,0 -56126d0.bin=@56126d0.bin,0 -56126d1.bin=@56126d1.bin,0 -56126d2.bin=@56126d2.bin,0 -56126n0.bin=@56126n0.bin,0 -56126n1.bin=@56126n1.bin,0 -56126n2.bin=@56126n2.bin,0 -56127d0.bin=@56127d0.bin,0 -56127d1.bin=@56127d1.bin,0 -56127d2.bin=@56127d2.bin,0 -56127n0.bin=@56127n0.bin,0 -56127n1.bin=@56127n1.bin,0 -56127n2.bin=@56127n2.bin,0 -56128d0.bin=@56128d0.bin,0 -56128d1.bin=@56128d1.bin,0 -56128d2.bin=@56128d2.bin,0 -56128n0.bin=@56128n0.bin,0 -56128n1.bin=@56128n1.bin,0 -56128n2.bin=@56128n2.bin,0 -56130d0.bin=@56130d0.bin,0 -56130d1.bin=@56130d1.bin,0 -56130d2.bin=@56130d2.bin,0 -56130n0.bin=@56130n0.bin,0 -56130n1.bin=@56130n1.bin,0 -56130n2.bin=@56130n2.bin,0 -56131d0.bin=@56131d0.bin,0 -56131d1.bin=@56131d1.bin,0 -56131d2.bin=@56131d2.bin,0 -56131n0.bin=@56131n0.bin,0 -56131n1.bin=@56131n1.bin,0 -56131n2.bin=@56131n2.bin,0 -56133d0.bin=@56133d0.bin,0 -56133d1.bin=@56133d1.bin,0 -56133d2.bin=@56133d2.bin,0 -56133n0.bin=@56133n0.bin,0 -56133n1.bin=@56133n1.bin,0 -56133n2.bin=@56133n2.bin,0 -56152d0.bin=@56152d0.bin,0 -56152d1.bin=@56152d1.bin,0 -56152d2.bin=@56152d2.bin,0 -56152n0.bin=@56152n0.bin,0 -56152n1.bin=@56152n1.bin,0 -56152n2.bin=@56152n2.bin,0 -56153d0.bin=@56153d0.bin,0 -56153d1.bin=@56153d1.bin,0 -56153d2.bin=@56153d2.bin,0 -56153n0.bin=@56153n0.bin,0 -56153n1.bin=@56153n1.bin,0 -56153n2.bin=@56153n2.bin,0 -56158d0.bin=@56158d0.bin,0 -56158d1.bin=@56158d1.bin,0 -56158d2.bin=@56158d2.bin,0 -56158n0.bin=@56158n0.bin,0 -56158n1.bin=@56158n1.bin,0 -56158n2.bin=@56158n2.bin,0 -23648d0.bin=@23648d0.bin,0 -23648d1.bin=@23648d1.bin,0 -23648d2.bin=@23648d2.bin,0 -23648n0.bin=@23648n0.bin,0 -23468n0.bin=@23468n0.bin,0 -23468n1.bin=@23468n1.bin,0 -23468n2.bin=@23468n2.bin,0 -23472d0.bin=@23472d0.bin,0 -23472d1.bin=@23472d1.bin,0 -23472d2.bin=@23472d2.bin,0 -23472n0.bin=@23472n0.bin,0 -23472n1.bin=@23472n1.bin,0 -23472n2.bin=@23472n2.bin,0 -23476d0.bin=@23476d0.bin,0 -23476d1.bin=@23476d1.bin,0 -23476d2.bin=@23476d2.bin,0 -23476n0.bin=@23476n0.bin,0 -23476n1.bin=@23476n1.bin,0 -23476n2.bin=@23476n2.bin,0 -23480d0.bin=@23480d0.bin,0 -23480d1.bin=@23480d1.bin,0 -23480d2.bin=@23480d2.bin,0 -23480n0.bin=@23480n0.bin,0 -23480n1.bin=@23480n1.bin,0 -23480n2.bin=@23480n2.bin,0 -23516d0.bin=@23516d0.bin,0 -23516d1.bin=@23516d1.bin,0 -23516d2.bin=@23516d2.bin,0 -23516n0.bin=@23516n0.bin,0 -23516n1.bin=@23516n1.bin,0 -23516n2.bin=@23516n2.bin,0 -23520d0.bin=@23520d0.bin,0 -23520d1.bin=@23520d1.bin,0 -23520d2.bin=@23520d2.bin,0 -23520n0.bin=@23520n0.bin,0 -23520n1.bin=@23520n1.bin,0 -23520n2.bin=@23520n2.bin,0 -23536d0.bin=@23536d0.bin,0 -23536d1.bin=@23536d1.bin,0 -23536d2.bin=@23536d2.bin,0 -23536n0.bin=@23536n0.bin,0 -23536n1.bin=@23536n1.bin,0 -23536n2.bin=@23536n2.bin,0 -23540d0.bin=@23540d0.bin,0 -23540d1.bin=@23540d1.bin,0 -23540d2.bin=@23540d2.bin,0 -23540n0.bin=@23540n0.bin,0 -23540n1.bin=@23540n1.bin,0 -23540n2.bin=@23540n2.bin,0 -23606d0.bin=@23606d0.bin,0 -23606d1.bin=@23606d1.bin,0 -23606d2.bin=@23606d2.bin,0 -23606n0.bin=@23606n0.bin,0 -23606n1.bin=@23606n1.bin,0 -23606n2.bin=@23606n2.bin,0 -23610d0.bin=@23610d0.bin,0 -23610d1.bin=@23610d1.bin,0 -23610d2.bin=@23610d2.bin,0 -23610n0.bin=@23610n0.bin,0 -23610n1.bin=@23610n1.bin,0 -23610n2.bin=@23610n2.bin,0 -23614d0.bin=@23614d0.bin,0 -23614d1.bin=@23614d1.bin,0 -23614d2.bin=@23614d2.bin,0 -23614n0.bin=@23614n0.bin,0 -23614n1.bin=@23614n1.bin,0 -23614n2.bin=@23614n2.bin,0 -23618d0.bin=@23618d0.bin,0 -23618d1.bin=@23618d1.bin,0 -23618d2.bin=@23618d2.bin,0 -23618n0.bin=@23618n0.bin,0 -23618n1.bin=@23618n1.bin,0 -23618n2.bin=@23618n2.bin,0 -23622d0.bin=@23622d0.bin,0 -23622d1.bin=@23622d1.bin,0 -23622d2.bin=@23622d2.bin,0 -23622n0.bin=@23622n0.bin,0 -23622n1.bin=@23622n1.bin,0 -23622n2.bin=@23622n2.bin,0 -23644d0.bin=@23644d0.bin,0 -23644d1.bin=@23644d1.bin,0 -23644d2.bin=@23644d2.bin,0 -23644n0.bin=@23644n0.bin,0 -23644n1.bin=@23644n1.bin,0 -23644n2.bin=@23644n2.bin,0 -23655d0.bin=@23655d0.bin,0 -23655d1.bin=@23655d1.bin,0 -23655d2.bin=@23655d2.bin,0 -23655n0.bin=@23655n0.bin,0 -23655n1.bin=@23655n1.bin,0 -23655n2.bin=@23655n2.bin,0 -23659d0.bin=@23659d0.bin,0 -23659d1.bin=@23659d1.bin,0 -23659d2.bin=@23659d2.bin,0 -23659n0.bin=@23659n0.bin,0 -23659n1.bin=@23659n1.bin,0 -23659n2.bin=@23659n2.bin,0 -23668d0.bin=@23668d0.bin,0 -23668d1.bin=@23668d1.bin,0 -23668d2.bin=@23668d2.bin,0 -23668n0.bin=@23668n0.bin,0 -23668n1.bin=@23668n1.bin,0 -23668n2.bin=@23668n2.bin,0 -23705d0.bin=@23705d0.bin,0 -23705d1.bin=@23705d1.bin,0 -23705d2.bin=@23705d2.bin,0 -23705n0.bin=@23705n0.bin,0 -23705n1.bin=@23705n1.bin,0 -23705n2.bin=@23705n2.bin,0 -23709d0.bin=@23709d0.bin,0 -23709d1.bin=@23709d1.bin,0 -23709d2.bin=@23709d2.bin,0 -23709n0.bin=@23709n0.bin,0 -23709n1.bin=@23709n1.bin,0 -23709n2.bin=@23709n2.bin,0 -23713d0.bin=@23713d0.bin,0 -23713d1.bin=@23713d1.bin,0 -23713d2.bin=@23713d2.bin,0 -23713n0.bin=@23713n0.bin,0 -23713n1.bin=@23713n1.bin,0 -23713n2.bin=@23713n2.bin,0 -23718d0.bin=@23718d0.bin,0 -23718d1.bin=@23718d1.bin,0 -23718d2.bin=@23718d2.bin,0 -23718n0.bin=@23718n0.bin,0 -23718n1.bin=@23718n1.bin,0 -23718n2.bin=@23718n2.bin,0 -40236d0.bin=@40236d0.bin,0 -40236d1.bin=@40236d1.bin,0 -40236d2.bin=@40236d2.bin,0 -40236n0.bin=@40236n0.bin,0 -40236n1.bin=@40236n1.bin,0 -40236n2.bin=@40236n2.bin,0 -55691d0.bin=@55691d0.bin,0 -55691d1.bin=@55691d1.bin,0 -55691d2.bin=@55691d2.bin,0 -55691n0.bin=@55691n0.bin,0 -55691n1.bin=@55691n1.bin,0 -55691n2.bin=@55691n2.bin,0 -55692d0.bin=@55692d0.bin,0 -55692d1.bin=@55692d1.bin,0 -55692d2.bin=@55692d2.bin,0 -55692n0.bin=@55692n0.bin,0 -55692n1.bin=@55692n1.bin,0 -55692n2.bin=@55692n2.bin,0 -55693d0.bin=@55693d0.bin,0 -55693d1.bin=@55693d1.bin,0 -55693d2.bin=@55693d2.bin,0 -55693n0.bin=@55693n0.bin,0 -55693n1.bin=@55693n1.bin,0 -55693n2.bin=@55693n2.bin,0 -55694d0.bin=@55694d0.bin,0 -55694d1.bin=@55694d1.bin,0 -55694d2.bin=@55694d2.bin,0 -55694n0.bin=@55694n0.bin,0 -55694n1.bin=@55694n1.bin,0 -55694n2.bin=@55694n2.bin,0 -55695d0.bin=@55695d0.bin,0 -55695d1.bin=@55695d1.bin,0 -55695d2.bin=@55695d2.bin,0 -55695n0.bin=@55695n0.bin,0 -55695n1.bin=@55695n1.bin,0 -55695n2.bin=@55695n2.bin,0 -55696d0.bin=@55696d0.bin,0 -55696d1.bin=@55696d1.bin,0 -55696d2.bin=@55696d2.bin,0 -55696n0.bin=@55696n0.bin,0 -55696n1.bin=@55696n1.bin,0 -55696n2.bin=@55696n2.bin,0 -55697d0.bin=@55697d0.bin,0 -55697d1.bin=@55697d1.bin,0 -55697d2.bin=@55697d2.bin,0 -55697n0.bin=@55697n0.bin,0 -55697n1.bin=@55697n1.bin,0 -55697n2.bin=@55697n2.bin,0 -55698d0.bin=@55698d0.bin,0 -55698d1.bin=@55698d1.bin,0 -55698d2.bin=@55698d2.bin,0 -55698n0.bin=@55698n0.bin,0 -55698n1.bin=@55698n1.bin,0 -55698n2.bin=@55698n2.bin,0 -55728d0.bin=@55728d0.bin,0 -55728d1.bin=@55728d1.bin,0 -55728d2.bin=@55728d2.bin,0 -55728n0.bin=@55728n0.bin,0 -55728n1.bin=@55728n1.bin,0 -55728n2.bin=@55728n2.bin,0 -55738d0.bin=@55738d0.bin,0 -55738d1.bin=@55738d1.bin,0 -55738d2.bin=@55738d2.bin,0 -55738n0.bin=@55738n0.bin,0 -55738n1.bin=@55738n1.bin,0 -55738n2.bin=@55738n2.bin,0 -55923d0.bin=@55923d0.bin,0 -55923d1.bin=@55923d1.bin,0 -55923d2.bin=@55923d2.bin,0 -55923n0.bin=@55923n0.bin,0 -55923n1.bin=@55923n1.bin,0 -55923n2.bin=@55923n2.bin,0 -55929d0.bin=@55929d0.bin,0 -55929d1.bin=@55929d1.bin,0 -55929d2.bin=@55929d2.bin,0 -55929n0.bin=@55929n0.bin,0 -55929n1.bin=@55929n1.bin,0 -55929n2.bin=@55929n2.bin,0 -56076d0.bin=@56076d0.bin,0 -56076d1.bin=@56076d1.bin,0 -56076d2.bin=@56076d2.bin,0 -56076n0.bin=@56076n0.bin,0 -56076n1.bin=@56076n1.bin,0 -56076n2.bin=@56076n2.bin,0 -56077d0.bin=@56077d0.bin,0 -56077d1.bin=@56077d1.bin,0 -56077d2.bin=@56077d2.bin,0 -56077n0.bin=@56077n0.bin,0 -56077n1.bin=@56077n1.bin,0 -56077n2.bin=@56077n2.bin,0 -56078d0.bin=@56078d0.bin,0 -56078d1.bin=@56078d1.bin,0 -56078d2.bin=@56078d2.bin,0 -56078n0.bin=@56078n0.bin,0 -56078n1.bin=@56078n1.bin,0 -56078n2.bin=@56078n2.bin,0 -56079d0.bin=@56079d0.bin,0 -56079d1.bin=@56079d1.bin,0 -56079d2.bin=@56079d2.bin,0 -56079n0.bin=@56079n0.bin,0 -56079n1.bin=@56079n1.bin,0 -56079n2.bin=@56079n2.bin,0 -56080d0.bin=@56080d0.bin,0 -56080d1.bin=@56080d1.bin,0 -56080d2.bin=@56080d2.bin,0 -56080n0.bin=@56080n0.bin,0 -56080n1.bin=@56080n1.bin,0 -56080n2.bin=@56080n2.bin,0 -56125d0.bin=@56125d0.bin,0 -56125d1.bin=@56125d1.bin,0 -56125d2.bin=@56125d2.bin,0 -56125n0.bin=@56125n0.bin,0 -56125n1.bin=@56125n1.bin,0 -56125n2.bin=@56125n2.bin,0 -56144d0.bin=@56144d0.bin,0 -56144d1.bin=@56144d1.bin,0 -56144d2.bin=@56144d2.bin,0 -56144n0.bin=@56144n0.bin,0 -56144n1.bin=@56144n1.bin,0 -56144n2.bin=@56144n2.bin,0 -56145d0.bin=@56145d0.bin,0 -56145d1.bin=@56145d1.bin,0 -56145d2.bin=@56145d2.bin,0 -56145n0.bin=@56145n0.bin,0 -56145n1.bin=@56145n1.bin,0 -56145n2.bin=@56145n2.bin,0 -56150d0.bin=@56150d0.bin,0 -56150d1.bin=@56150d1.bin,0 -56150d2.bin=@56150d2.bin,0 -56150n0.bin=@56150n0.bin,0 -56150n1.bin=@56150n1.bin,0 -56150n2.bin=@56150n2.bin,0 -56151d0.bin=@56151d0.bin,0 -56151d1.bin=@56151d1.bin,0 -56151d2.bin=@56151d2.bin,0 -56151n0.bin=@56151n0.bin,0 -56151n1.bin=@56151n1.bin,0 -56151n2.bin=@56151n2.bin,0 -56154d0.bin=@56154d0.bin,0 -56154d1.bin=@56154d1.bin,0 -56154d2.bin=@56154d2.bin,0 -56154n0.bin=@56154n0.bin,0 -56154n1.bin=@56154n1.bin,0 -56154n2.bin=@56154n2.bin,0 -56155d0.bin=@56155d0.bin,0 -56155d1.bin=@56155d1.bin,0 -56155d2.bin=@56155d2.bin,0 -56155n0.bin=@56155n0.bin,0 -56155n1.bin=@56155n1.bin,0 -56155n2.bin=@56155n2.bin,0 -56156d0.bin=@56156d0.bin,0 -56156d1.bin=@56156d1.bin,0 -56156d2.bin=@56156d2.bin,0 -56156n0.bin=@56156n0.bin,0 -56156n1.bin=@56156n1.bin,0 -56156n2.bin=@56156n2.bin,0 -23468d0.bin=@23468d0.bin,0 -23468d1.bin=@23468d1.bin,0 -23468d2.bin=@23468d2.bin,0 -23469n2.bin=@23469n2.bin,0 -23473d0.bin=@23473d0.bin,0 -23473d1.bin=@23473d1.bin,0 -23473d2.bin=@23473d2.bin,0 -23473n0.bin=@23473n0.bin,0 -23473n1.bin=@23473n1.bin,0 -23473n2.bin=@23473n2.bin,0 -23477d0.bin=@23477d0.bin,0 -23477d1.bin=@23477d1.bin,0 -23477d2.bin=@23477d2.bin,0 -23477n0.bin=@23477n0.bin,0 -23477n1.bin=@23477n1.bin,0 -23477n2.bin=@23477n2.bin,0 -23481d0.bin=@23481d0.bin,0 -23481d1.bin=@23481d1.bin,0 -23481d2.bin=@23481d2.bin,0 -23481n0.bin=@23481n0.bin,0 -23481n1.bin=@23481n1.bin,0 -23481n2.bin=@23481n2.bin,0 -23517d0.bin=@23517d0.bin,0 -23517d1.bin=@23517d1.bin,0 -23517d2.bin=@23517d2.bin,0 -23517n0.bin=@23517n0.bin,0 -23517n1.bin=@23517n1.bin,0 -23517n2.bin=@23517n2.bin,0 -23521d0.bin=@23521d0.bin,0 -23521d1.bin=@23521d1.bin,0 -23521d2.bin=@23521d2.bin,0 -23521n0.bin=@23521n0.bin,0 -23521n1.bin=@23521n1.bin,0 -23521n2.bin=@23521n2.bin,0 -23537d0.bin=@23537d0.bin,0 -23537d1.bin=@23537d1.bin,0 -23537d2.bin=@23537d2.bin,0 -23537n0.bin=@23537n0.bin,0 -23537n1.bin=@23537n1.bin,0 -23537n2.bin=@23537n2.bin,0 -23541d0.bin=@23541d0.bin,0 -23541d1.bin=@23541d1.bin,0 -23541d2.bin=@23541d2.bin,0 -23541n0.bin=@23541n0.bin,0 -23541n1.bin=@23541n1.bin,0 -23541n2.bin=@23541n2.bin,0 -23607d0.bin=@23607d0.bin,0 -23607d1.bin=@23607d1.bin,0 -23607d2.bin=@23607d2.bin,0 -23607n0.bin=@23607n0.bin,0 -23607n1.bin=@23607n1.bin,0 -23607n2.bin=@23607n2.bin,0 -23611d0.bin=@23611d0.bin,0 -23611d1.bin=@23611d1.bin,0 -23611d2.bin=@23611d2.bin,0 -23611n0.bin=@23611n0.bin,0 -23611n1.bin=@23611n1.bin,0 -23611n2.bin=@23611n2.bin,0 -23615d0.bin=@23615d0.bin,0 -23615d1.bin=@23615d1.bin,0 -23615d2.bin=@23615d2.bin,0 -23615n0.bin=@23615n0.bin,0 -23615n1.bin=@23615n1.bin,0 -23615n2.bin=@23615n2.bin,0 -23619d0.bin=@23619d0.bin,0 -23619d1.bin=@23619d1.bin,0 -23619d2.bin=@23619d2.bin,0 -23619n0.bin=@23619n0.bin,0 -23619n1.bin=@23619n1.bin,0 -23619n2.bin=@23619n2.bin,0 -23623d0.bin=@23623d0.bin,0 -23623d1.bin=@23623d1.bin,0 -23623d2.bin=@23623d2.bin,0 -23623n0.bin=@23623n0.bin,0 -23623n1.bin=@23623n1.bin,0 -23623n2.bin=@23623n2.bin,0 -23645d0.bin=@23645d0.bin,0 -23645d1.bin=@23645d1.bin,0 -23645d2.bin=@23645d2.bin,0 -23645n0.bin=@23645n0.bin,0 -23645n1.bin=@23645n1.bin,0 -23645n2.bin=@23645n2.bin,0 -23656d0.bin=@23656d0.bin,0 -23656d1.bin=@23656d1.bin,0 -23656d2.bin=@23656d2.bin,0 -23656n0.bin=@23656n0.bin,0 -23656n1.bin=@23656n1.bin,0 -23656n2.bin=@23656n2.bin,0 -23660d0.bin=@23660d0.bin,0 -23660d1.bin=@23660d1.bin,0 -23660d2.bin=@23660d2.bin,0 -23660n0.bin=@23660n0.bin,0 -23660n1.bin=@23660n1.bin,0 -23660n2.bin=@23660n2.bin,0 -23669d0.bin=@23669d0.bin,0 -23669d1.bin=@23669d1.bin,0 -23669d2.bin=@23669d2.bin,0 -23669n0.bin=@23669n0.bin,0 -23669n1.bin=@23669n1.bin,0 -23669n2.bin=@23669n2.bin,0 -23706d0.bin=@23706d0.bin,0 -23706d1.bin=@23706d1.bin,0 -23706d2.bin=@23706d2.bin,0 -23706n0.bin=@23706n0.bin,0 -23706n1.bin=@23706n1.bin,0 -23706n2.bin=@23706n2.bin,0 -23710d0.bin=@23710d0.bin,0 -23710d1.bin=@23710d1.bin,0 -23710d2.bin=@23710d2.bin,0 -23710n0.bin=@23710n0.bin,0 -23710n1.bin=@23710n1.bin,0 -23710n2.bin=@23710n2.bin,0 -23714d0.bin=@23714d0.bin,0 -23714d1.bin=@23714d1.bin,0 -23714d2.bin=@23714d2.bin,0 -23714n0.bin=@23714n0.bin,0 -23714n1.bin=@23714n1.bin,0 -23714n2.bin=@23714n2.bin,0 -23719d0.bin=@23719d0.bin,0 -23719d1.bin=@23719d1.bin,0 -23719d2.bin=@23719d2.bin,0 -23719n0.bin=@23719n0.bin,0 -23719n1.bin=@23719n1.bin,0 -23719n2.bin=@23719n2.bin,0 -55513d0.bin=@55513d0.bin,0 -55513d1.bin=@55513d1.bin,0 -55513d2.bin=@55513d2.bin,0 -55513n0.bin=@55513n0.bin,0 -55513n1.bin=@55513n1.bin,0 -55513n2.bin=@55513n2.bin,0 -55924d0.bin=@55924d0.bin,0 -55924d1.bin=@55924d1.bin,0 -55924d2.bin=@55924d2.bin,0 -55924n0.bin=@55924n0.bin,0 -55924n1.bin=@55924n1.bin,0 -55924n2.bin=@55924n2.bin,0 -55930d0.bin=@55930d0.bin,0 -55930d1.bin=@55930d1.bin,0 -55930d2.bin=@55930d2.bin,0 -55930n0.bin=@55930n0.bin,0 -55930n1.bin=@55930n1.bin,0 -55930n2.bin=@55930n2.bin,0 -23469d0.bin=@23469d0.bin,0 -23469d1.bin=@23469d1.bin,0 -23469d2.bin=@23469d2.bin,0 -23469n0.bin=@23469n0.bin,0 -23469n1.bin=@23469n1.bin,0 -23470n2.bin=@23470n2.bin,0 -23474d0.bin=@23474d0.bin,0 -23474d1.bin=@23474d1.bin,0 -23474d2.bin=@23474d2.bin,0 -23474n0.bin=@23474n0.bin,0 -23474n1.bin=@23474n1.bin,0 -23474n2.bin=@23474n2.bin,0 -23478d0.bin=@23478d0.bin,0 -23478d1.bin=@23478d1.bin,0 -23478d2.bin=@23478d2.bin,0 -23478n0.bin=@23478n0.bin,0 -23478n1.bin=@23478n1.bin,0 -23478n2.bin=@23478n2.bin,0 -23482d0.bin=@23482d0.bin,0 -23482d1.bin=@23482d1.bin,0 -23482d2.bin=@23482d2.bin,0 -23482n0.bin=@23482n0.bin,0 -23482n1.bin=@23482n1.bin,0 -23482n2.bin=@23482n2.bin,0 -23518d0.bin=@23518d0.bin,0 -23518d1.bin=@23518d1.bin,0 -23518d2.bin=@23518d2.bin,0 -23518n0.bin=@23518n0.bin,0 -23518n1.bin=@23518n1.bin,0 -23518n2.bin=@23518n2.bin,0 -23522d0.bin=@23522d0.bin,0 -23522d1.bin=@23522d1.bin,0 -23522d2.bin=@23522d2.bin,0 -23522n0.bin=@23522n0.bin,0 -23522n1.bin=@23522n1.bin,0 -23522n2.bin=@23522n2.bin,0 -23538d0.bin=@23538d0.bin,0 -23538d1.bin=@23538d1.bin,0 -23538d2.bin=@23538d2.bin,0 -23538n0.bin=@23538n0.bin,0 -23538n1.bin=@23538n1.bin,0 -23538n2.bin=@23538n2.bin,0 -23542d0.bin=@23542d0.bin,0 -23542d1.bin=@23542d1.bin,0 -23542d2.bin=@23542d2.bin,0 -23542n0.bin=@23542n0.bin,0 -23542n1.bin=@23542n1.bin,0 -23542n2.bin=@23542n2.bin,0 -23608d0.bin=@23608d0.bin,0 -23608d1.bin=@23608d1.bin,0 -23608d2.bin=@23608d2.bin,0 -23608n0.bin=@23608n0.bin,0 -23608n1.bin=@23608n1.bin,0 -23608n2.bin=@23608n2.bin,0 -23612d0.bin=@23612d0.bin,0 -23612d1.bin=@23612d1.bin,0 -23612d2.bin=@23612d2.bin,0 -23612n0.bin=@23612n0.bin,0 -23612n1.bin=@23612n1.bin,0 -23612n2.bin=@23612n2.bin,0 -23616d0.bin=@23616d0.bin,0 -23616d1.bin=@23616d1.bin,0 -23616d2.bin=@23616d2.bin,0 -23616n0.bin=@23616n0.bin,0 -23616n1.bin=@23616n1.bin,0 -23616n2.bin=@23616n2.bin,0 -23620d0.bin=@23620d0.bin,0 -23620d1.bin=@23620d1.bin,0 -23620d2.bin=@23620d2.bin,0 -23620n0.bin=@23620n0.bin,0 -23620n1.bin=@23620n1.bin,0 -23620n2.bin=@23620n2.bin,0 -23624d0.bin=@23624d0.bin,0 -23624d1.bin=@23624d1.bin,0 -23624d2.bin=@23624d2.bin,0 -23624n0.bin=@23624n0.bin,0 -23624n1.bin=@23624n1.bin,0 -23624n2.bin=@23624n2.bin,0 -23646d0.bin=@23646d0.bin,0 -23646d1.bin=@23646d1.bin,0 -23646d2.bin=@23646d2.bin,0 -23646n0.bin=@23646n0.bin,0 -23646n1.bin=@23646n1.bin,0 -23646n2.bin=@23646n2.bin,0 -23657d0.bin=@23657d0.bin,0 -23657d1.bin=@23657d1.bin,0 -23657d2.bin=@23657d2.bin,0 -23657n0.bin=@23657n0.bin,0 -23657n1.bin=@23657n1.bin,0 -23657n2.bin=@23657n2.bin,0 -23661d0.bin=@23661d0.bin,0 -23661d1.bin=@23661d1.bin,0 -23661d2.bin=@23661d2.bin,0 -23661n0.bin=@23661n0.bin,0 -23661n1.bin=@23661n1.bin,0 -23661n2.bin=@23661n2.bin,0 -23670d0.bin=@23670d0.bin,0 -23670d1.bin=@23670d1.bin,0 -23670d2.bin=@23670d2.bin,0 -23670n0.bin=@23670n0.bin,0 -23670n1.bin=@23670n1.bin,0 -23670n2.bin=@23670n2.bin,0 -23707d0.bin=@23707d0.bin,0 -23707d1.bin=@23707d1.bin,0 -23707d2.bin=@23707d2.bin,0 -23707n0.bin=@23707n0.bin,0 -23707n1.bin=@23707n1.bin,0 -23707n2.bin=@23707n2.bin,0 -23711d0.bin=@23711d0.bin,0 -23711d1.bin=@23711d1.bin,0 -23711d2.bin=@23711d2.bin,0 -23711n0.bin=@23711n0.bin,0 -23711n1.bin=@23711n1.bin,0 -23711n2.bin=@23711n2.bin,0 -23715d0.bin=@23715d0.bin,0 -23715d1.bin=@23715d1.bin,0 -23715d2.bin=@23715d2.bin,0 -23715n0.bin=@23715n0.bin,0 -23715n1.bin=@23715n1.bin,0 -23715n2.bin=@23715n2.bin,0 -23720d0.bin=@23720d0.bin,0 -23720d1.bin=@23720d1.bin,0 -23720d2.bin=@23720d2.bin,0 -23720n0.bin=@23720n0.bin,0 -23720n1.bin=@23720n1.bin,0 -23720n2.bin=@23720n2.bin,0 -55925d0.bin=@55925d0.bin,0 -55925d1.bin=@55925d1.bin,0 -55925d2.bin=@55925d2.bin,0 -55925n0.bin=@55925n0.bin,0 -55925n1.bin=@55925n1.bin,0 -55925n2.bin=@55925n2.bin,0 -55931d0.bin=@55931d0.bin,0 -55931d1.bin=@55931d1.bin,0 -55931d2.bin=@55931d2.bin,0 -55931n0.bin=@55931n0.bin,0 -55931n1.bin=@55931n1.bin,0 -55931n2.bin=@55931n2.bin,0 -56042d0.bin=@56042d0.bin,0 -56042d1.bin=@56042d1.bin,0 -56042d2.bin=@56042d2.bin,0 -56042n0.bin=@56042n0.bin,0 -56042n1.bin=@56042n1.bin,0 -56042n2.bin=@56042n2.bin,0 -56063d0.bin=@56063d0.bin,0 -56063d1.bin=@56063d1.bin,0 -56063d2.bin=@56063d2.bin,0 -56063n0.bin=@56063n0.bin,0 -56063n1.bin=@56063n1.bin,0 -56063n2.bin=@56063n2.bin,0 -56064d0.bin=@56064d0.bin,0 -56064d1.bin=@56064d1.bin,0 -56064d2.bin=@56064d2.bin,0 -56064n0.bin=@56064n0.bin,0 -56064n1.bin=@56064n1.bin,0 -56064n2.bin=@56064n2.bin,0 -23470d0.bin=@23470d0.bin,0 -23470d1.bin=@23470d1.bin,0 -23470d2.bin=@23470d2.bin,0 -23470n0.bin=@23470n0.bin,0 -23470n1.bin=@23470n1.bin,0 -23475d0.bin=@23475d0.bin,0 -23475d1.bin=@23475d1.bin,0 -23475d2.bin=@23475d2.bin,0 -23475n0.bin=@23475n0.bin,0 -23475n1.bin=@23475n1.bin,0 -23475n2.bin=@23475n2.bin,0 -23479d0.bin=@23479d0.bin,0 -23479d1.bin=@23479d1.bin,0 -23479d2.bin=@23479d2.bin,0 -23479n0.bin=@23479n0.bin,0 -23479n1.bin=@23479n1.bin,0 -23479n2.bin=@23479n2.bin,0 -23483d0.bin=@23483d0.bin,0 -23483d1.bin=@23483d1.bin,0 -23483d2.bin=@23483d2.bin,0 -23483n0.bin=@23483n0.bin,0 -23483n1.bin=@23483n1.bin,0 -23483n2.bin=@23483n2.bin,0 -23519d0.bin=@23519d0.bin,0 -23519d1.bin=@23519d1.bin,0 -23519d2.bin=@23519d2.bin,0 -23519n0.bin=@23519n0.bin,0 -23519n1.bin=@23519n1.bin,0 -23519n2.bin=@23519n2.bin,0 -23523d0.bin=@23523d0.bin,0 -23523d1.bin=@23523d1.bin,0 -23523d2.bin=@23523d2.bin,0 -23523n0.bin=@23523n0.bin,0 -23523n1.bin=@23523n1.bin,0 -23523n2.bin=@23523n2.bin,0 -23539d0.bin=@23539d0.bin,0 -23539d1.bin=@23539d1.bin,0 -23539d2.bin=@23539d2.bin,0 -23539n0.bin=@23539n0.bin,0 -23539n1.bin=@23539n1.bin,0 -23539n2.bin=@23539n2.bin,0 -23543d0.bin=@23543d0.bin,0 -23543d1.bin=@23543d1.bin,0 -23543d2.bin=@23543d2.bin,0 -23543n0.bin=@23543n0.bin,0 -23543n1.bin=@23543n1.bin,0 -23543n2.bin=@23543n2.bin,0 -23609d0.bin=@23609d0.bin,0 -23609d1.bin=@23609d1.bin,0 -23609d2.bin=@23609d2.bin,0 -23609n0.bin=@23609n0.bin,0 -23609n1.bin=@23609n1.bin,0 -23609n2.bin=@23609n2.bin,0 -23613d0.bin=@23613d0.bin,0 -23613d1.bin=@23613d1.bin,0 -23613d2.bin=@23613d2.bin,0 -23613n0.bin=@23613n0.bin,0 -23613n1.bin=@23613n1.bin,0 -23613n2.bin=@23613n2.bin,0 -23617d0.bin=@23617d0.bin,0 -23617d1.bin=@23617d1.bin,0 -23617d2.bin=@23617d2.bin,0 -23617n0.bin=@23617n0.bin,0 -23617n1.bin=@23617n1.bin,0 -23617n2.bin=@23617n2.bin,0 -23621d0.bin=@23621d0.bin,0 -23621d1.bin=@23621d1.bin,0 -23621d2.bin=@23621d2.bin,0 -23621n0.bin=@23621n0.bin,0 -23621n1.bin=@23621n1.bin,0 -23621n2.bin=@23621n2.bin,0 -23625d0.bin=@23625d0.bin,0 -23625d1.bin=@23625d1.bin,0 -23625d2.bin=@23625d2.bin,0 -23625n0.bin=@23625n0.bin,0 -23625n1.bin=@23625n1.bin,0 -23625n2.bin=@23625n2.bin,0 -23647d0.bin=@23647d0.bin,0 -23647d1.bin=@23647d1.bin,0 -23647d2.bin=@23647d2.bin,0 -23647n0.bin=@23647n0.bin,0 -23647n1.bin=@23647n1.bin,0 -23647n2.bin=@23647n2.bin,0 -23658d0.bin=@23658d0.bin,0 -23658d1.bin=@23658d1.bin,0 -23658d2.bin=@23658d2.bin,0 -23658n0.bin=@23658n0.bin,0 -23658n1.bin=@23658n1.bin,0 -23658n2.bin=@23658n2.bin,0 -23662d0.bin=@23662d0.bin,0 -23662d1.bin=@23662d1.bin,0 -23662d2.bin=@23662d2.bin,0 -23662n0.bin=@23662n0.bin,0 -23662n1.bin=@23662n1.bin,0 -23662n2.bin=@23662n2.bin,0 -23671d0.bin=@23671d0.bin,0 -23671d1.bin=@23671d1.bin,0 -23671d2.bin=@23671d2.bin,0 -23671n0.bin=@23671n0.bin,0 -23671n1.bin=@23671n1.bin,0 -23671n2.bin=@23671n2.bin,0 -23708d0.bin=@23708d0.bin,0 -23708d1.bin=@23708d1.bin,0 -23708d2.bin=@23708d2.bin,0 -23708n0.bin=@23708n0.bin,0 -23708n1.bin=@23708n1.bin,0 -23708n2.bin=@23708n2.bin,0 -23712d0.bin=@23712d0.bin,0 -23712d1.bin=@23712d1.bin,0 -23712d2.bin=@23712d2.bin,0 -23712n0.bin=@23712n0.bin,0 -23712n1.bin=@23712n1.bin,0 -23712n2.bin=@23712n2.bin,0 -23716d0.bin=@23716d0.bin,0 -23716d1.bin=@23716d1.bin,0 -23716d2.bin=@23716d2.bin,0 -23716n0.bin=@23716n0.bin,0 -23716n1.bin=@23716n1.bin,0 -23716n2.bin=@23716n2.bin,0 -23721d0.bin=@23721d0.bin,0 -23721d1.bin=@23721d1.bin,0 -23721d2.bin=@23721d2.bin,0 -23721n0.bin=@23721n0.bin,0 -23721n1.bin=@23721n1.bin,0 -23721n2.bin=@23721n2.bin,0 -55926d0.bin=@55926d0.bin,0 -55926d1.bin=@55926d1.bin,0 -55926d2.bin=@55926d2.bin,0 -55926n0.bin=@55926n0.bin,0 -55926n1.bin=@55926n1.bin,0 -55926n2.bin=@55926n2.bin,0 -55932d0.bin=@55932d0.bin,0 -55932d1.bin=@55932d1.bin,0 -55932d2.bin=@55932d2.bin,0 -55932n0.bin=@55932n0.bin,0 -55932n1.bin=@55932n1.bin,0 -55932n2.bin=@55932n2.bin,0 -23471d0.bin=@23471d0.bin,0 -23471d1.bin=@23471d1.bin,0 -23471d2.bin=@23471d2.bin,0 -23471n0.bin=@23471n0.bin,0 -23471n1.bin=@23471n1.bin,0 -23471n2.bin=@23471n2.bin,0 -21731n0.bin=@21731n0.bin,0 -21731n1.bin=@21731n1.bin,0 -21731n2.bin=@21731n2.bin,0 -21746d0.bin=@21746d0.bin,0 -21746d1.bin=@21746d1.bin,0 -21746d2.bin=@21746d2.bin,0 -21746n0.bin=@21746n0.bin,0 -21746n1.bin=@21746n1.bin,0 -21746n2.bin=@21746n2.bin,0 -21731d0.bin=@21731d0.bin,0 -21731d1.bin=@21731d1.bin,0 -21731d2.bin=@21731d2.bin,0 -54633d0.bin=@54633d0.bin,0 -54633d1.bin=@54633d1.bin,0 -54633d2.bin=@54633d2.bin,0 -54633n0.bin=@54633n0.bin,0 -54633n1.bin=@54633n1.bin,0 -54633n2.bin=@54633n2.bin,0 -54801d0.bin=@54801d0.bin,0 -54801d1.bin=@54801d1.bin,0 -54801d2.bin=@54801d2.bin,0 -54801n0.bin=@54801n0.bin,0 -54801n1.bin=@54801n1.bin,0 -54801n2.bin=@54801n2.bin,0 -55203d0.bin=@55203d0.bin,0 -55203d1.bin=@55203d1.bin,0 -55203d2.bin=@55203d2.bin,0 -55203n0.bin=@55203n0.bin,0 -55203n1.bin=@55203n1.bin,0 -55203n2.bin=@55203n2.bin,0 -55204d0.bin=@55204d0.bin,0 -55204d1.bin=@55204d1.bin,0 -55204d2.bin=@55204d2.bin,0 -55204n0.bin=@55204n0.bin,0 -55204n1.bin=@55204n1.bin,0 -55204n2.bin=@55204n2.bin,0 -55464d0.bin=@55464d0.bin,0 -55464d1.bin=@55464d1.bin,0 -55464d2.bin=@55464d2.bin,0 -55464n0.bin=@55464n0.bin,0 -55464n1.bin=@55464n1.bin,0 -55464n2.bin=@55464n2.bin,0 -55619d0.bin=@55619d0.bin,0 -55619d1.bin=@55619d1.bin,0 -55619d2.bin=@55619d2.bin,0 -55619n0.bin=@55619n0.bin,0 -55619n1.bin=@55619n1.bin,0 -55619n2.bin=@55619n2.bin,0 -55730d0.bin=@55730d0.bin,0 -55730d1.bin=@55730d1.bin,0 -55730d2.bin=@55730d2.bin,0 -55730n0.bin=@55730n0.bin,0 -55730n1.bin=@55730n1.bin,0 -55730n2.bin=@55730n2.bin,0 -55771d0.bin=@55771d0.bin,0 -55771d1.bin=@55771d1.bin,0 -55771d2.bin=@55771d2.bin,0 -55771n0.bin=@55771n0.bin,0 -55771n1.bin=@55771n1.bin,0 -55771n2.bin=@55771n2.bin,0 -55896d0.bin=@55896d0.bin,0 -55896d1.bin=@55896d1.bin,0 -55896d2.bin=@55896d2.bin,0 -55896n0.bin=@55896n0.bin,0 -55896n1.bin=@55896n1.bin,0 -55896n2.bin=@55896n2.bin,0 -55937d0.bin=@55937d0.bin,0 -55937d1.bin=@55937d1.bin,0 -55937d2.bin=@55937d2.bin,0 -55937n0.bin=@55937n0.bin,0 -55937n1.bin=@55937n1.bin,0 -55937n2.bin=@55937n2.bin,0 -55938d0.bin=@55938d0.bin,0 -55938d1.bin=@55938d1.bin,0 -55938d2.bin=@55938d2.bin,0 -55938n0.bin=@55938n0.bin,0 -55938n1.bin=@55938n1.bin,0 -55938n2.bin=@55938n2.bin,0 -55939d0.bin=@55939d0.bin,0 -55939d1.bin=@55939d1.bin,0 -55939d2.bin=@55939d2.bin,0 -55939n0.bin=@55939n0.bin,0 -55939n1.bin=@55939n1.bin,0 -55939n2.bin=@55939n2.bin,0 -55964d0.bin=@55964d0.bin,0 -55964d1.bin=@55964d1.bin,0 -55964d2.bin=@55964d2.bin,0 -55964n0.bin=@55964n0.bin,0 -55964n1.bin=@55964n1.bin,0 -55964n2.bin=@55964n2.bin,0 -55967d0.bin=@55967d0.bin,0 -55967d1.bin=@55967d1.bin,0 -55967d2.bin=@55967d2.bin,0 -55967n0.bin=@55967n0.bin,0 -55967n1.bin=@55967n1.bin,0 -55967n2.bin=@55967n2.bin,0 -56109d0.bin=@56109d0.bin,0 -56109d1.bin=@56109d1.bin,0 -56109d2.bin=@56109d2.bin,0 -56109n0.bin=@56109n0.bin,0 -56109n1.bin=@56109n1.bin,0 -56109n2.bin=@56109n2.bin,0 -56110d0.bin=@56110d0.bin,0 -56110d1.bin=@56110d1.bin,0 -56110d2.bin=@56110d2.bin,0 -56110n0.bin=@56110n0.bin,0 -56110n1.bin=@56110n1.bin,0 -56110n2.bin=@56110n2.bin,0 -56143d0.bin=@56143d0.bin,0 -56143d1.bin=@56143d1.bin,0 -56143d2.bin=@56143d2.bin,0 -56143n0.bin=@56143n0.bin,0 -56143n1.bin=@56143n1.bin,0 -56143n2.bin=@56143n2.bin,0 -56147d0.bin=@56147d0.bin,0 -56147d1.bin=@56147d1.bin,0 -56147d2.bin=@56147d2.bin,0 -56147n0.bin=@56147n0.bin,0 -56147n1.bin=@56147n1.bin,0 -56147n2.bin=@56147n2.bin,0 -56157d0.bin=@56157d0.bin,0 -56157d1.bin=@56157d1.bin,0 -56157d2.bin=@56157d2.bin,0 -56157n0.bin=@56157n0.bin,0 -56157n1.bin=@56157n1.bin,0 -56157n2.bin=@56157n2.bin,0 -56159d0.bin=@56159d0.bin,0 -56159d1.bin=@56159d1.bin,0 -56159d2.bin=@56159d2.bin,0 -56159n0.bin=@56159n0.bin,0 -56159n1.bin=@56159n1.bin,0 -56159n2.bin=@56159n2.bin,0 -64552d0.bin=@64552d0.bin,0 -64552d1.bin=@64552d1.bin,0 -64552d2.bin=@64552d2.bin,0 -64555d0.bin=@64555d0.bin,0 -64555d1.bin=@64555d1.bin,0 -64555d2.bin=@64555d2.bin,0 -64556d0.bin=@64556d0.bin,0 -64556d1.bin=@64556d1.bin,0 -64556d2.bin=@64556d2.bin,0 -51122d0.bin=@51122d0.bin,0 -51122d1.bin=@51122d1.bin,0 -51122d2.bin=@51122d2.bin,0 -51122n0.bin=@51122n0.bin,0 -51122n1.bin=@51122n1.bin,0 -51122n2.bin=@51122n2.bin,0 -55732d0.bin=@55732d0.bin,0 -55732d1.bin=@55732d1.bin,0 -55732d2.bin=@55732d2.bin,0 -55732n0.bin=@55732n0.bin,0 -55732n1.bin=@55732n1.bin,0 -55732n2.bin=@55732n2.bin,0 -55731d0.bin=@55731d0.bin,0 -55731d1.bin=@55731d1.bin,0 -55731d2.bin=@55731d2.bin,0 -55731n0.bin=@55731n0.bin,0 -55731n1.bin=@55731n1.bin,0 -55731n2.bin=@55731n2.bin,0 -55818d0.bin=@55818d0.bin,0 -55818d1.bin=@55818d1.bin,0 -55818d2.bin=@55818d2.bin,0 -55818n0.bin=@55818n0.bin,0 -55818n1.bin=@55818n1.bin,0 -55818n2.bin=@55818n2.bin,0 -56112d0.bin=@56112d0.bin,0 -56112d1.bin=@56112d1.bin,0 -56112d2.bin=@56112d2.bin,0 -56112n0.bin=@56112n0.bin,0 -56112n1.bin=@56112n1.bin,0 -56112n2.bin=@56112n2.bin,0 -56105d0.bin=@56105d0.bin,0 -56105d1.bin=@56105d1.bin,0 -56105d2.bin=@56105d2.bin,0 -56105n0.bin=@56105n0.bin,0 -56105n1.bin=@56105n1.bin,0 -56105n2.bin=@56105n2.bin,0 -58007d2.bin=@58007d2.bin,0 -58008d2.bin=@58008d2.bin,0 -58009d2.bin=@58009d2.bin,0 -58010d2.bin=@58010d2.bin,0 -58129d0.bin=@58129d0.bin,0 -58130d0.bin=@58130d0.bin,0 -58139d0.bin=@58139d0.bin,0 From fd02a12ae9783be85a97aef8776215266a73c0fa Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 31 Dec 2023 10:22:27 +1100 Subject: [PATCH 13/70] add DefaultCourses DevModeOption --- common/mhfcourse/mhfcourse.go | 6 +++++- config.json | 1 + config/config.go | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/common/mhfcourse/mhfcourse.go b/common/mhfcourse/mhfcourse.go index f838e64a5..272d3e3d4 100644 --- a/common/mhfcourse/mhfcourse.go +++ b/common/mhfcourse/mhfcourse.go @@ -1,6 +1,7 @@ package mhfcourse import ( + _config "erupe-ce/config" "math" "sort" "time" @@ -66,7 +67,10 @@ func CourseExists(ID uint16, c []Course) bool { // GetCourseStruct returns a slice of Course(s) from a rights integer func GetCourseStruct(rights uint32) ([]Course, uint32) { - resp := []Course{{ID: 1}, {ID: 23}, {ID: 24}} + var resp []Course + for _, c := range _config.ErupeConfig.DevModeOptions.DefaultCourses { + resp = append(resp, Course{ID: c}) + } s := Courses() sort.Slice(s, func(i, j int) bool { return s[i].ID > s[j].ID diff --git a/config.json b/config.json index 1d5fe76d7..a099936bc 100644 --- a/config.json +++ b/config.json @@ -29,6 +29,7 @@ "TournamentEvent": 0, "DisableTokenCheck": false, "QuestDebugTools": false, + "DefaultCourses": [1, 23, 24], "EarthStatusOverride": 0, "EarthIDOverride": 0, "EarthMonsterOverride": [0, 0, 0, 0], diff --git a/config/config.go b/config/config.go index 4995c042d..7e87fd8b8 100644 --- a/config/config.go +++ b/config/config.go @@ -110,6 +110,7 @@ type DevModeOptions struct { TournamentEvent int // VS Tournament event status DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) QuestDebugTools bool // Enable various quest debug logs + DefaultCourses []uint16 EarthStatusOverride int32 EarthIDOverride int32 EarthMonsterOverride []int32 From 0069a5029f8d5af8d90a5049bbd4bb3e052c21aa Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 31 Dec 2023 11:42:44 +1100 Subject: [PATCH 14/70] decode Festa stuff --- server/channelserver/handlers_festa.go | 31 +++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index 0ea67fee2..f92b8d3fc 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -309,8 +309,8 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { ps.Uint8(bf, "", true) // Guild Name } + bf.WriteUint32(0) // Clan goal // Final bonus rates - bf.WriteUint32(1) // 5000-Infinity? bf.WriteUint32(5000) // 5000+ souls bf.WriteUint32(2000) // 2000-4999 souls bf.WriteUint32(1000) // 1000-1999 souls @@ -349,7 +349,6 @@ func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) { bf.WriteBool(false) bf.WriteBool(true) } - bf.WriteUint16(0) // Unk doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } @@ -364,18 +363,18 @@ func handleMsgMhfStateFestaG(s *Session, p mhfpacket.MHFPacket) { resp := byteframe.NewByteFrame() if err != nil || guild == nil || applicant { resp.WriteUint32(0) - resp.WriteUint32(0) - resp.WriteUint32(0xFFFFFFFF) - resp.WriteUint32(0) - resp.WriteUint32(0) + resp.WriteInt32(0) + resp.WriteInt32(-1) + resp.WriteInt32(0) + resp.WriteInt32(0) doAckBufSucceed(s, pkt.AckHandle, resp.Data()) return } resp.WriteUint32(guild.Souls) - resp.WriteUint32(1) // unk - resp.WriteUint32(1) // unk - resp.WriteUint32(1) // unk, rank? - resp.WriteUint32(1) // unk + resp.WriteInt32(0) // unk + resp.WriteInt32(1) // unk, rank? + resp.WriteInt32(0) // unk + resp.WriteInt32(0) // unk doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } @@ -391,13 +390,19 @@ func handleMsgMhfEnumerateFestaMember(s *Session, p mhfpacket.MHFPacket) { doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } - bf := byteframe.NewByteFrame() - bf.WriteUint16(uint16(len(members))) - bf.WriteUint16(0) // Unk sort.Slice(members, func(i, j int) bool { return members[i].Souls > members[j].Souls }) + var validMembers []*GuildMember for _, member := range members { + if member.Souls > 0 { + validMembers = append(validMembers, member) + } + } + bf := byteframe.NewByteFrame() + bf.WriteUint16(uint16(len(validMembers))) + bf.WriteUint16(0) // Unk + for _, member := range validMembers { bf.WriteUint32(member.CharID) bf.WriteUint32(member.Souls) } From 52082aaf06f2ed11b43cdea3934968f2d33c2fb6 Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 31 Dec 2023 11:43:10 +1100 Subject: [PATCH 15/70] use correct GuildMember length --- server/channelserver/handlers_guild.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/channelserver/handlers_guild.go b/server/channelserver/handlers_guild.go index ad5221281..43629e49a 100644 --- a/server/channelserver/handlers_guild.go +++ b/server/channelserver/handlers_guild.go @@ -1427,7 +1427,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { bf := byteframe.NewByteFrame() - bf.WriteUint16(guild.MemberCount) + bf.WriteUint16(uint16(len(guildMembers))) sort.Slice(guildMembers[:], func(i, j int) bool { return guildMembers[i].OrderIndex < guildMembers[j].OrderIndex @@ -1460,7 +1460,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { } if guild.AllianceID > 0 { - bf.WriteUint16(alliance.TotalMembers - guild.MemberCount) + bf.WriteUint16(alliance.TotalMembers - uint16(len(guildMembers))) if guild.ID != alliance.ParentGuildID { mems, err := GetGuildMembers(s, alliance.ParentGuildID, false) if err != nil { From 0ea0dc217b816706782a4f323dd573c835b10e17 Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 31 Dec 2023 12:51:24 +1100 Subject: [PATCH 16/70] simplify config --- common/mhfcourse/mhfcourse.go | 2 +- config.json | 31 ++++----- config/config.go | 72 ++++++++++---------- main.go | 17 ++--- server/channelserver/handlers.go | 10 +-- server/channelserver/handlers_cast_binary.go | 2 +- server/channelserver/handlers_data.go | 8 +-- server/channelserver/handlers_diva.go | 6 +- server/channelserver/handlers_festa.go | 8 +-- server/channelserver/handlers_object.go | 2 +- server/channelserver/handlers_quest.go | 4 +- server/channelserver/handlers_tower.go | 6 +- server/channelserver/sys_session.go | 12 ++-- server/entranceserver/entrance_server.go | 2 +- server/entranceserver/make_resp.go | 8 +-- server/signserver/dbutils.go | 2 +- server/signserver/dsgn_resp.go | 26 +++---- server/signserver/session.go | 6 +- server/signv2server/endpoints.go | 8 +-- 19 files changed, 109 insertions(+), 123 deletions(-) diff --git a/common/mhfcourse/mhfcourse.go b/common/mhfcourse/mhfcourse.go index 272d3e3d4..332c684dc 100644 --- a/common/mhfcourse/mhfcourse.go +++ b/common/mhfcourse/mhfcourse.go @@ -68,7 +68,7 @@ func CourseExists(ID uint16, c []Course) bool { // GetCourseStruct returns a slice of Course(s) from a rights integer func GetCourseStruct(rights uint32) ([]Course, uint32) { var resp []Course - for _, c := range _config.ErupeConfig.DevModeOptions.DefaultCourses { + for _, c := range _config.ErupeConfig.DefaultCourses { resp = append(resp, Course{ID: c}) } s := Courses() diff --git a/config.json b/config.json index a099936bc..23105d55a 100644 --- a/config.json +++ b/config.json @@ -13,11 +13,18 @@ "DeleteOnSaveCorruption": false, "ClientMode": "ZZ", "QuestCacheExpiry": 300, - "ProxyPort": 0, "CommandPrefix": "!", - "DevMode": true, - "DevModeOptions": { - "AutoCreateAccount": true, + "AutoCreateAccount": true, + "DefaultCourses": [1, 23, 24], + "EarthStatus": 0, + "EarthID": 0, + "EarthMonsters": [0, 0, 0, 0], + "SaveDumps": { + "Enabled": true, + "RawEnabled": false, + "OutputDir": "save-backups" + }, + "DebugOptions": { "CleanDB": false, "MaxLauncherHR": false, "LogInboundMessages": false, @@ -28,16 +35,8 @@ "FestaEvent": -1, "TournamentEvent": 0, "DisableTokenCheck": false, - "QuestDebugTools": false, - "DefaultCourses": [1, 23, 24], - "EarthStatusOverride": 0, - "EarthIDOverride": 0, - "EarthMonsterOverride": [0, 0, 0, 0], - "SaveDumps": { - "Enabled": true, - "RawEnabled": false, - "OutputDir": "save-backups" - }, + "QuestTools": false, + "ProxyPort": 0, "CapLink": { "Values": [51728, 20000, 51729, 1, 20000], "Key": "", @@ -58,8 +57,6 @@ "ClanMemberLimits": [[0, 30], [3, 40], [7, 50], [10, 60]], "BonusQuestAllowance": 3, "DailyQuestAllowance": 1, - "MezfesSoloTickets": 10, - "MezfesGroupTickets": 4, "LowLatencyRaviente": false, "RegularRavienteMaxPlayers": 8, "ViolentRavienteMaxPlayers": 8, @@ -74,6 +71,8 @@ "MaterialMultiplier": 1.00, "ExtraCarves": 0, "DisableHunterNavi": false, + "MezFesSoloTickets": 5, + "MezFesGroupTickets": 1, "MezFesDuration": 172800, "MezFesSwitchMinigame": false, "EnableKaijiEvent": false, diff --git a/config/config.go b/config/config.go index 7e87fd8b8..02c94b030 100644 --- a/config/config.go +++ b/config/config.go @@ -80,42 +80,23 @@ type Config struct { ClientMode string RealClientMode Mode QuestCacheExpiry int // Number of seconds to keep quest data cached - ProxyPort uint16 // Forces the game to connect to a channel server proxy CommandPrefix string // The prefix for commands - DevMode bool - - DevModeOptions DevModeOptions - GameplayOptions GameplayOptions - Discord Discord - Commands []Command - Courses []Course - Database Database - Sign Sign - SignV2 SignV2 - Channel Channel - Entrance Entrance -} - -// DevModeOptions holds various debug/temporary options for use while developing Erupe. -type DevModeOptions struct { - AutoCreateAccount bool // Automatically create accounts if they don't exist - CleanDB bool // Automatically wipes the DB on server reset. - MaxLauncherHR bool // Sets the HR returned in the launcher to HR7 so that you can join non-beginner worlds. - LogInboundMessages bool // Log all messages sent to the server - LogOutboundMessages bool // Log all messages sent to the clients - LogMessageData bool // Log all bytes transferred as a hexdump - 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 - DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) - QuestDebugTools bool // Enable various quest debug logs - DefaultCourses []uint16 - EarthStatusOverride int32 - EarthIDOverride int32 - EarthMonsterOverride []int32 - SaveDumps SaveDumpOptions - CapLink CapLinkOptions + AutoCreateAccount bool // Automatically create accounts if they don't exist + DefaultCourses []uint16 + EarthStatus int32 + EarthID int32 + EarthMonsters []int32 + SaveDumps SaveDumpOptions + DebugOptions DebugOptions + GameplayOptions GameplayOptions + Discord Discord + Commands []Command + Courses []Course + Database Database + Sign Sign + SignV2 SignV2 + Channel Channel + Entrance Entrance } type SaveDumpOptions struct { @@ -124,6 +105,23 @@ type SaveDumpOptions struct { OutputDir string } +// DebugOptions holds various debug/temporary options for use while developing Erupe. +type DebugOptions struct { + CleanDB bool // Automatically wipes the DB on server reset. + MaxLauncherHR bool // Sets the HR returned in the launcher to HR7 so that you can join non-beginner worlds. + LogInboundMessages bool // Log all messages sent to the server + LogOutboundMessages bool // Log all messages sent to the clients + LogMessageData bool // Log all bytes transferred as a hexdump + 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 + DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) + QuestTools bool // Enable various quest debug logs + ProxyPort uint16 // Forces the game to connect to a channel server proxy + CapLink CapLinkOptions +} + type CapLinkOptions struct { Values []uint16 Key string @@ -146,8 +144,6 @@ type GameplayOptions struct { ClanMemberLimits [][]uint8 // Array of maximum Clan Members -> [Rank, Members] BonusQuestAllowance uint32 // Number of Bonus Point Quests to allow daily DailyQuestAllowance uint32 // Number of Daily Quests to allow daily - MezfesSoloTickets uint32 // Number of solo tickets given weekly - MezfesGroupTickets uint32 // Number of group tickets given weekly LowLatencyRaviente bool // Toggles low latency mode for Raviente, can be network intensive RegularRavienteMaxPlayers uint8 ViolentRavienteMaxPlayers uint8 @@ -162,6 +158,8 @@ type GameplayOptions struct { MaterialMultiplier float32 // Adjusts the multiplier of Monster Materials rewarded for quest completion ExtraCarves uint16 // Grant n extra chances to carve ALL carcasses DisableHunterNavi bool // Disables the Hunter Navi + MezFesSoloTickets uint32 // Number of solo tickets given weekly + MezFesGroupTickets uint32 // Number of group tickets given weekly MezFesDuration int // Seconds that MezFes will last for weekly (from 12AM Mon backwards) MezFesSwitchMinigame bool // Swaps out Volpakkun Together for Tokotoko Partnya EnableKaijiEvent bool // Enables the Kaiji event in the Rasta Bar diff --git a/main.go b/main.go index c56d90b0f..42da5a7fe 100644 --- a/main.go +++ b/main.go @@ -22,13 +22,10 @@ import ( ) // Temporary DB auto clean on startup for quick development & testing. -func cleanDB(db *sqlx.DB, config *_config.Config) { +func cleanDB(db *sqlx.DB) { _ = db.MustExec("DELETE FROM guild_characters") _ = db.MustExec("DELETE FROM guilds") _ = db.MustExec("DELETE FROM characters") - if config.ProxyPort == 0 { - _ = db.MustExec("DELETE FROM sign_sessions") - } _ = db.MustExec("DELETE FROM users") } @@ -48,11 +45,7 @@ func main() { var zapLogger *zap.Logger config := _config.ErupeConfig - if config.DevMode { - zapLogger, _ = zap.NewDevelopment() - } else { - zapLogger, _ = zap.NewProduction() - } + zapLogger, _ = zap.NewDevelopment() defer zapLogger.Sync() logger := zapLogger.Named("main") @@ -126,16 +119,16 @@ func main() { logger.Info("Database: Started successfully") // Clear stale data - if config.ProxyPort == 0 { + if config.DebugOptions.ProxyPort == 0 { _ = db.MustExec("DELETE FROM sign_sessions") } _ = db.MustExec("DELETE FROM servers") _ = db.MustExec(`UPDATE guild_characters SET treasure_hunt=NULL`) // Clean the DB if the option is on. - if config.DevMode && config.DevModeOptions.CleanDB { + if config.DebugOptions.CleanDB { logger.Info("Database: Started clearing...") - cleanDB(db, config) + cleanDB(db) logger.Info("Database: Finished clearing") } diff --git a/server/channelserver/handlers.go b/server/channelserver/handlers.go index 76d085175..e2c15a4f8 100644 --- a/server/channelserver/handlers.go +++ b/server/channelserver/handlers.go @@ -31,7 +31,7 @@ func stubEnumerateNoResults(s *Session, ackHandle uint32) { func doAckEarthSucceed(s *Session, ackHandle uint32, data []*byteframe.ByteFrame) { bf := byteframe.NewByteFrame() - bf.WriteUint32(uint32(s.server.erupeConfig.DevModeOptions.EarthIDOverride)) + bf.WriteUint32(uint32(s.server.erupeConfig.EarthID)) bf.WriteUint32(0) bf.WriteUint32(0) bf.WriteUint32(uint32(len(data))) @@ -127,7 +127,7 @@ func handleMsgSysTerminalLog(s *Session, p mhfpacket.MHFPacket) { func handleMsgSysLogin(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysLogin) - if !s.server.erupeConfig.DevModeOptions.DisableTokenCheck { + if !s.server.erupeConfig.DebugOptions.DisableTokenCheck { var token string err := s.server.db.QueryRow("SELECT token FROM sign_sessions ss INNER JOIN public.users u on ss.user_id = u.id WHERE token=$1 AND ss.id=$2 AND u.id=(SELECT c.user_id FROM characters c WHERE c.id=$3)", pkt.LoginTokenString, pkt.LoginTokenNumber, pkt.CharID0).Scan(&token) if err != nil { @@ -1147,9 +1147,9 @@ func handleMsgMhfGetEarthStatus(s *Session, p mhfpacket.MHFPacket) { bf := byteframe.NewByteFrame() bf.WriteUint32(uint32(TimeWeekStart().Unix())) // Start bf.WriteUint32(uint32(TimeWeekNext().Unix())) // End - bf.WriteInt32(s.server.erupeConfig.DevModeOptions.EarthStatusOverride) - bf.WriteInt32(s.server.erupeConfig.DevModeOptions.EarthIDOverride) - for i, m := range s.server.erupeConfig.DevModeOptions.EarthMonsterOverride { + bf.WriteInt32(s.server.erupeConfig.EarthStatus) + bf.WriteInt32(s.server.erupeConfig.EarthID) + for i, m := range s.server.erupeConfig.EarthMonsters { if _config.ErupeConfig.RealClientMode <= _config.G9 { if i == 3 { break diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 71a8a6f07..51141a47f 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -348,7 +348,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { } } - if s.server.erupeConfig.DevModeOptions.QuestDebugTools == true && s.server.erupeConfig.DevMode { + if s.server.erupeConfig.DebugOptions.QuestTools { if pkt.BroadcastType == 0x03 && pkt.MessageType == 0x02 && len(pkt.RawDataPayload) > 32 { // This is only correct most of the time tmp.ReadBytes(20) diff --git a/server/channelserver/handlers_data.go b/server/channelserver/handlers_data.go index 805fa59f5..8844dbdd8 100644 --- a/server/channelserver/handlers_data.go +++ b/server/channelserver/handlers_data.go @@ -45,7 +45,7 @@ func handleMsgMhfSavedata(s *Session, p mhfpacket.MHFPacket) { doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } - if s.server.erupeConfig.DevModeOptions.SaveDumps.RawEnabled { + if s.server.erupeConfig.SaveDumps.RawEnabled { dumpSaveData(s, saveData, "raw-savedata") } s.logger.Info("Updating save with blob") @@ -112,11 +112,11 @@ func grpToGR(n int) uint16 { } func dumpSaveData(s *Session, data []byte, suffix string) { - if !s.server.erupeConfig.DevModeOptions.SaveDumps.Enabled { + if !s.server.erupeConfig.SaveDumps.Enabled { return } else { - dir := filepath.Join(s.server.erupeConfig.DevModeOptions.SaveDumps.OutputDir, fmt.Sprintf("%d", s.charID)) - path := filepath.Join(s.server.erupeConfig.DevModeOptions.SaveDumps.OutputDir, fmt.Sprintf("%d", s.charID), fmt.Sprintf("%d_%s.bin", s.charID, suffix)) + dir := filepath.Join(s.server.erupeConfig.SaveDumps.OutputDir, fmt.Sprintf("%d", s.charID)) + path := filepath.Join(s.server.erupeConfig.SaveDumps.OutputDir, fmt.Sprintf("%d", s.charID), fmt.Sprintf("%d_%s.bin", s.charID, suffix)) _, err := os.Stat(dir) if err != nil { if os.IsNotExist(err) { diff --git a/server/channelserver/handlers_diva.go b/server/channelserver/handlers_diva.go index 42b70f064..d7c60331a 100644 --- a/server/channelserver/handlers_diva.go +++ b/server/channelserver/handlers_diva.go @@ -70,8 +70,8 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { } var timestamps []uint32 - if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.DivaEvent >= 0 { - if s.server.erupeConfig.DevModeOptions.DivaEvent == 0 { + if s.server.erupeConfig.DebugOptions.DivaEvent >= 0 { + if s.server.erupeConfig.DebugOptions.DivaEvent == 0 { if s.server.erupeConfig.RealClientMode >= _config.Z2 { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 36)) } else { @@ -79,7 +79,7 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { } return } - timestamps = generateDivaTimestamps(s, uint32(s.server.erupeConfig.DevModeOptions.DivaEvent), true) + timestamps = generateDivaTimestamps(s, uint32(s.server.erupeConfig.DebugOptions.DivaEvent), true) } else { timestamps = generateDivaTimestamps(s, start, false) } diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index f92b8d3fc..d200240e4 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -36,7 +36,7 @@ func handleMsgMhfLoadMezfesData(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateRanking) bf := byteframe.NewByteFrame() - state := s.server.erupeConfig.DevModeOptions.TournamentEvent + state := s.server.erupeConfig.DebugOptions.TournamentEvent // Unk // Unk // Start? @@ -173,12 +173,12 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { } var timestamps []uint32 - if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.FestaEvent >= 0 { - if s.server.erupeConfig.DevModeOptions.FestaEvent == 0 { + if s.server.erupeConfig.DebugOptions.FestaEvent >= 0 { + if s.server.erupeConfig.DebugOptions.FestaEvent == 0 { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) return } - timestamps = generateFestaTimestamps(s, uint32(s.server.erupeConfig.DevModeOptions.FestaEvent), true) + timestamps = generateFestaTimestamps(s, uint32(s.server.erupeConfig.DebugOptions.FestaEvent), true) } else { timestamps = generateFestaTimestamps(s, start, false) } diff --git a/server/channelserver/handlers_object.go b/server/channelserver/handlers_object.go index 241505c0d..41f28e5d3 100644 --- a/server/channelserver/handlers_object.go +++ b/server/channelserver/handlers_object.go @@ -42,7 +42,7 @@ func handleMsgSysDeleteObject(s *Session, p mhfpacket.MHFPacket) {} func handleMsgSysPositionObject(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysPositionObject) - if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogInboundMessages { + if s.server.erupeConfig.DebugOptions.LogInboundMessages { fmt.Printf("[%s] with objectID [%d] move to (%f,%f,%f)\n\n", s.Name, pkt.ObjID, pkt.X, pkt.Y, pkt.Z) } s.stage.Lock() diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index 98226c777..138199ea4 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -20,7 +20,7 @@ func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysGetFile) if pkt.IsScenario { - if s.server.erupeConfig.DevModeOptions.QuestDebugTools && s.server.erupeConfig.DevMode { + if s.server.erupeConfig.DebugOptions.QuestTools { s.logger.Debug( "Scenario", zap.Uint8("CategoryID", pkt.ScenarioIdentifer.CategoryID), @@ -40,7 +40,7 @@ func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) { } doAckBufSucceed(s, pkt.AckHandle, data) } else { - if s.server.erupeConfig.DevModeOptions.QuestDebugTools && s.server.erupeConfig.DevMode { + if s.server.erupeConfig.DebugOptions.QuestTools { s.logger.Debug( "Quest", zap.String("Filename", pkt.Filename), diff --git a/server/channelserver/handlers_tower.go b/server/channelserver/handlers_tower.go index 4ce0bcc9f..7f6bdb3b9 100644 --- a/server/channelserver/handlers_tower.go +++ b/server/channelserver/handlers_tower.go @@ -105,7 +105,7 @@ func handleMsgMhfGetTowerInfo(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfPostTowerInfo(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfPostTowerInfo) - if s.server.erupeConfig.DevModeOptions.QuestDebugTools { + if s.server.erupeConfig.DebugOptions.QuestTools { s.logger.Debug( p.Opcode().String(), zap.Uint32("InfoType", pkt.InfoType), @@ -328,7 +328,7 @@ func handleMsgMhfGetTenrouirai(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfPostTenrouirai(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfPostTenrouirai) - if s.server.erupeConfig.DevModeOptions.QuestDebugTools { + if s.server.erupeConfig.DebugOptions.QuestTools { s.logger.Debug( p.Opcode().String(), zap.Uint8("Unk0", pkt.Unk0), @@ -442,7 +442,7 @@ func handleMsgMhfGetGemInfo(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfPostGemInfo(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfPostGemInfo) - if s.server.erupeConfig.DevModeOptions.QuestDebugTools { + if s.server.erupeConfig.DebugOptions.QuestTools { s.logger.Debug( p.Opcode().String(), zap.Uint32("Op", pkt.Op), diff --git a/server/channelserver/sys_session.go b/server/channelserver/sys_session.go index 5034f38c2..d49e5264a 100644 --- a/server/channelserver/sys_session.go +++ b/server/channelserver/sys_session.go @@ -253,13 +253,9 @@ func ignored(opcode network.PacketID) bool { } func (s *Session) logMessage(opcode uint16, data []byte, sender string, recipient string) { - if !s.server.erupeConfig.DevMode { + if sender == "Server" && !s.server.erupeConfig.DebugOptions.LogOutboundMessages { return - } - - if sender == "Server" && !s.server.erupeConfig.DevModeOptions.LogOutboundMessages { - return - } else if sender != "Server" && !s.server.erupeConfig.DevModeOptions.LogInboundMessages { + } else if sender != "Server" && !s.server.erupeConfig.DebugOptions.LogInboundMessages { return } @@ -277,8 +273,8 @@ func (s *Session) logMessage(opcode uint16, data []byte, sender string, recipien fmt.Printf("[%s] -> [%s]\n", sender, recipient) } fmt.Printf("Opcode: %s\n", opcodePID) - if s.server.erupeConfig.DevModeOptions.LogMessageData { - if len(data) <= s.server.erupeConfig.DevModeOptions.MaxHexdumpLength { + if s.server.erupeConfig.DebugOptions.LogMessageData { + if len(data) <= s.server.erupeConfig.DebugOptions.MaxHexdumpLength { fmt.Printf("Data [%d bytes]:\n%s\n", len(data), hex.Dump(data)) } else { fmt.Printf("Data [%d bytes]: (Too long!)\n\n", len(data)) diff --git a/server/entranceserver/entrance_server.go b/server/entranceserver/entrance_server.go index 8b06be0e0..18869304b 100644 --- a/server/entranceserver/entrance_server.go +++ b/server/entranceserver/entrance_server.go @@ -111,7 +111,7 @@ func (s *Server) handleEntranceServerConnection(conn net.Conn) { return } - if s.erupeConfig.DevMode && s.erupeConfig.DevModeOptions.LogInboundMessages { + if s.erupeConfig.DebugOptions.LogInboundMessages { fmt.Printf("[Client] -> [Server]\nData [%d bytes]:\n%s\n", len(pkt), hex.Dump(pkt)) } diff --git a/server/entranceserver/make_resp.go b/server/entranceserver/make_resp.go index 4b478fa24..f7f2f433e 100644 --- a/server/entranceserver/make_resp.go +++ b/server/entranceserver/make_resp.go @@ -69,8 +69,8 @@ func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte { for channelIdx, ci := range si.Channels { sid = (4096 + serverIdx*256) + (16 + channelIdx) - if _config.ErupeConfig.DevMode && _config.ErupeConfig.ProxyPort != 0 { - bf.WriteUint16(_config.ErupeConfig.ProxyPort) + if _config.ErupeConfig.DebugOptions.ProxyPort != 0 { + bf.WriteUint16(_config.ErupeConfig.DebugOptions.ProxyPort) } else { bf.WriteUint16(ci.Port) } @@ -136,7 +136,7 @@ func makeSv2Resp(config *_config.Config, s *Server, local bool) []byte { } rawServerData := encodeServerInfo(config, s, local) - if s.erupeConfig.DevMode && s.erupeConfig.DevModeOptions.LogOutboundMessages { + if s.erupeConfig.DebugOptions.LogOutboundMessages { fmt.Printf("[Server] -> [Client]\nData [%d bytes]:\n%s\n", len(rawServerData), hex.Dump(rawServerData)) } @@ -168,7 +168,7 @@ func makeUsrResp(pkt []byte, s *Server) []byte { } } - if s.erupeConfig.DevMode && s.erupeConfig.DevModeOptions.LogOutboundMessages { + if s.erupeConfig.DebugOptions.LogOutboundMessages { fmt.Printf("[Server] -> [Client]\nData [%d bytes]:\n%s\n", len(resp.Data()), hex.Dump(resp.Data())) } diff --git a/server/signserver/dbutils.go b/server/signserver/dbutils.go index 751862a49..f23d3bdcc 100644 --- a/server/signserver/dbutils.go +++ b/server/signserver/dbutils.go @@ -231,7 +231,7 @@ func (s *Server) validateLogin(user string, pass string) (uint32, RespID) { if err != nil { if err == sql.ErrNoRows { s.logger.Info("User not found", zap.String("User", user)) - if s.erupeConfig.DevMode && s.erupeConfig.DevModeOptions.AutoCreateAccount { + if s.erupeConfig.AutoCreateAccount { uid, err = s.registerDBAccount(user, pass) if err == nil { return uid, SIGN_SUCCESS diff --git a/server/signserver/dsgn_resp.go b/server/signserver/dsgn_resp.go index 25b5650c4..715299f8f 100644 --- a/server/signserver/dsgn_resp.go +++ b/server/signserver/dsgn_resp.go @@ -72,7 +72,7 @@ func (s *Session) makeSignResponse(uid uint32) []byte { bf.WriteUint32(char.ID) // Exp, HR[x] is split by 0, 1, 30, 50, 99, 299, 998, 999 - if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.MaxLauncherHR { + if s.server.erupeConfig.DebugOptions.MaxLauncherHR { bf.WriteUint16(999) } else { bf.WriteUint16(char.HRP) @@ -145,11 +145,11 @@ func (s *Session) makeSignResponse(uid uint32) []byte { bf.WriteBytes(stringsupport.PaddedString(psnUser, 20, true)) } - bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[0]) - if s.server.erupeConfig.DevModeOptions.CapLink.Values[0] == 51728 { - bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[1]) - if s.server.erupeConfig.DevModeOptions.CapLink.Values[1] == 20000 || s.server.erupeConfig.DevModeOptions.CapLink.Values[1] == 20002 { - ps.Uint16(bf, s.server.erupeConfig.DevModeOptions.CapLink.Key, false) + bf.WriteUint16(s.server.erupeConfig.DebugOptions.CapLink.Values[0]) + if s.server.erupeConfig.DebugOptions.CapLink.Values[0] == 51728 { + bf.WriteUint16(s.server.erupeConfig.DebugOptions.CapLink.Values[1]) + if s.server.erupeConfig.DebugOptions.CapLink.Values[1] == 20000 || s.server.erupeConfig.DebugOptions.CapLink.Values[1] == 20002 { + ps.Uint16(bf, s.server.erupeConfig.DebugOptions.CapLink.Key, false) } } caStruct := []struct { @@ -163,19 +163,19 @@ func (s *Session) makeSignResponse(uid uint32) []byte { bf.WriteUint32(caStruct[i].Unk1) ps.Uint8(bf, caStruct[i].Unk2, false) } - bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[2]) - bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[3]) - bf.WriteUint16(s.server.erupeConfig.DevModeOptions.CapLink.Values[4]) - if s.server.erupeConfig.DevModeOptions.CapLink.Values[2] == 51729 && s.server.erupeConfig.DevModeOptions.CapLink.Values[3] == 1 && s.server.erupeConfig.DevModeOptions.CapLink.Values[4] == 20000 { - ps.Uint16(bf, fmt.Sprintf(`%s:%d`, s.server.erupeConfig.DevModeOptions.CapLink.Host, s.server.erupeConfig.DevModeOptions.CapLink.Port), false) + bf.WriteUint16(s.server.erupeConfig.DebugOptions.CapLink.Values[2]) + bf.WriteUint16(s.server.erupeConfig.DebugOptions.CapLink.Values[3]) + bf.WriteUint16(s.server.erupeConfig.DebugOptions.CapLink.Values[4]) + if s.server.erupeConfig.DebugOptions.CapLink.Values[2] == 51729 && s.server.erupeConfig.DebugOptions.CapLink.Values[3] == 1 && s.server.erupeConfig.DebugOptions.CapLink.Values[4] == 20000 { + ps.Uint16(bf, fmt.Sprintf(`%s:%d`, s.server.erupeConfig.DebugOptions.CapLink.Host, s.server.erupeConfig.DebugOptions.CapLink.Port), false) } bf.WriteUint32(uint32(s.server.getReturnExpiry(uid).Unix())) bf.WriteUint32(0) tickets := []uint32{ - s.server.erupeConfig.GameplayOptions.MezfesSoloTickets, - s.server.erupeConfig.GameplayOptions.MezfesGroupTickets, + s.server.erupeConfig.GameplayOptions.MezFesSoloTickets, + s.server.erupeConfig.GameplayOptions.MezFesGroupTickets, } stalls := []uint8{ 10, 3, 6, 9, 4, 8, 5, 7, diff --git a/server/signserver/session.go b/server/signserver/session.go index 5806f1108..164ab70e2 100644 --- a/server/signserver/session.go +++ b/server/signserver/session.go @@ -37,7 +37,7 @@ type Session struct { func (s *Session) work() { pkt, err := s.cryptConn.ReadPacket() - if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogInboundMessages { + if s.server.erupeConfig.DebugOptions.LogInboundMessages { fmt.Printf("\n[Client] -> [Server]\nData [%d bytes]:\n%s\n", len(pkt), hex.Dump(pkt)) } @@ -78,7 +78,7 @@ func (s *Session) handlePacket(pkt []byte) error { } default: s.logger.Warn("Unknown request", zap.String("reqType", reqType)) - if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogInboundMessages { + if s.server.erupeConfig.DebugOptions.LogInboundMessages { fmt.Printf("\n[Client] -> [Server]\nData [%d bytes]:\n%s\n", len(pkt), hex.Dump(pkt)) } } @@ -102,7 +102,7 @@ func (s *Session) authenticate(username string, password string) { default: bf.WriteUint8(uint8(resp)) } - if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogOutboundMessages { + if s.server.erupeConfig.DebugOptions.LogOutboundMessages { fmt.Printf("\n[Server] -> [Client]\nData [%d bytes]:\n%s\n", len(bf.Data()), hex.Dump(bf.Data())) } _ = s.cryptConn.SendPacket(bf.Data()) diff --git a/server/signv2server/endpoints.go b/server/signv2server/endpoints.go index b6cc03515..5a0a6c8e7 100644 --- a/server/signv2server/endpoints.go +++ b/server/signv2server/endpoints.go @@ -80,7 +80,7 @@ func (s *Server) newAuthData(userID uint32, userRights uint32, userTokenID uint3 PatchServer: s.erupeConfig.SignV2.PatchServer, Notices: []string{}, } - if s.erupeConfig.DevModeOptions.MaxLauncherHR { + if s.erupeConfig.DebugOptions.MaxLauncherHR { for i := range resp.Characters { resp.Characters[i].HR = 7 } @@ -93,8 +93,8 @@ func (s *Server) newAuthData(userID uint32, userRights uint32, userTokenID uint3 ID: uint32(channelserver.TimeWeekStart().Unix()), Start: uint32(channelserver.TimeWeekStart().Add(-time.Duration(s.erupeConfig.GameplayOptions.MezFesDuration) * time.Second).Unix()), End: uint32(channelserver.TimeWeekNext().Unix()), - SoloTickets: s.erupeConfig.GameplayOptions.MezfesSoloTickets, - GroupTickets: s.erupeConfig.GameplayOptions.MezfesGroupTickets, + SoloTickets: s.erupeConfig.GameplayOptions.MezFesSoloTickets, + GroupTickets: s.erupeConfig.GameplayOptions.MezFesGroupTickets, Stalls: stalls, } if !s.erupeConfig.HideLoginNotice { @@ -226,7 +226,7 @@ func (s *Server) CreateCharacter(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) return } - if s.erupeConfig.DevModeOptions.MaxLauncherHR { + if s.erupeConfig.DebugOptions.MaxLauncherHR { character.HR = 7 } w.Header().Add("Content-Type", "application/json") From 32dee9039ed3b83d687ac96bd43cd10683fd03fc Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 31 Dec 2023 12:54:18 +1100 Subject: [PATCH 17/70] simplify config --- config.json | 6 +++--- config/config.go | 6 +++--- server/channelserver/handlers_diva.go | 6 +++--- server/channelserver/handlers_festa.go | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/config.json b/config.json index 23105d55a..63034a452 100644 --- a/config.json +++ b/config.json @@ -31,9 +31,9 @@ "LogOutboundMessages": false, "LogMessageData": false, "MaxHexdumpLength": 256, - "DivaEvent": 0, - "FestaEvent": -1, - "TournamentEvent": 0, + "DivaOverride": 0, + "FestaOverride": -1, + "TournamentOverride": 0, "DisableTokenCheck": false, "QuestTools": false, "ProxyPort": 0, diff --git a/config/config.go b/config/config.go index 02c94b030..e1248e1f9 100644 --- a/config/config.go +++ b/config/config.go @@ -113,9 +113,9 @@ type DebugOptions struct { LogOutboundMessages bool // Log all messages sent to the clients LogMessageData bool // Log all bytes transferred as a hexdump 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 + DivaOverride int // Diva Defense event status + FestaOverride int // Hunter's Festa event status + TournamentOverride int // VS Tournament event status DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) QuestTools bool // Enable various quest debug logs ProxyPort uint16 // Forces the game to connect to a channel server proxy diff --git a/server/channelserver/handlers_diva.go b/server/channelserver/handlers_diva.go index d7c60331a..7f5b33992 100644 --- a/server/channelserver/handlers_diva.go +++ b/server/channelserver/handlers_diva.go @@ -70,8 +70,8 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { } var timestamps []uint32 - if s.server.erupeConfig.DebugOptions.DivaEvent >= 0 { - if s.server.erupeConfig.DebugOptions.DivaEvent == 0 { + if s.server.erupeConfig.DebugOptions.DivaOverride >= 0 { + if s.server.erupeConfig.DebugOptions.DivaOverride == 0 { if s.server.erupeConfig.RealClientMode >= _config.Z2 { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 36)) } else { @@ -79,7 +79,7 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { } return } - timestamps = generateDivaTimestamps(s, uint32(s.server.erupeConfig.DebugOptions.DivaEvent), true) + timestamps = generateDivaTimestamps(s, uint32(s.server.erupeConfig.DebugOptions.DivaOverride), true) } else { timestamps = generateDivaTimestamps(s, start, false) } diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index d200240e4..4fc5370e1 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -36,7 +36,7 @@ func handleMsgMhfLoadMezfesData(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateRanking) bf := byteframe.NewByteFrame() - state := s.server.erupeConfig.DebugOptions.TournamentEvent + state := s.server.erupeConfig.DebugOptions.TournamentOverride // Unk // Unk // Start? @@ -173,12 +173,12 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { } var timestamps []uint32 - if s.server.erupeConfig.DebugOptions.FestaEvent >= 0 { - if s.server.erupeConfig.DebugOptions.FestaEvent == 0 { + if s.server.erupeConfig.DebugOptions.FestaOverride >= 0 { + if s.server.erupeConfig.DebugOptions.FestaOverride == 0 { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) return } - timestamps = generateFestaTimestamps(s, uint32(s.server.erupeConfig.DebugOptions.FestaEvent), true) + timestamps = generateFestaTimestamps(s, uint32(s.server.erupeConfig.DebugOptions.FestaOverride), true) } else { timestamps = generateFestaTimestamps(s, start, false) } From 5a8bc3b67afac4cd17c8757b65e16d5407b64edd Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 31 Dec 2023 15:12:20 +1100 Subject: [PATCH 18/70] fix EntranceServer response on G5 --- server/entranceserver/make_resp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/entranceserver/make_resp.go b/server/entranceserver/make_resp.go index f7f2f433e..481ccae93 100644 --- a/server/entranceserver/make_resp.go +++ b/server/entranceserver/make_resp.go @@ -51,13 +51,13 @@ func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte { combined := append(stringsupport.UTF8ToSJIS(si.Name), []byte{0x00}...) combined = append(combined, stringsupport.UTF8ToSJIS(si.Description)...) bf.WriteBytes(stringsupport.PaddedString(string(combined), 65, false)) - } else if s.erupeConfig.RealClientMode <= _config.GG { + } else if s.erupeConfig.RealClientMode <= _config.G5 { combined := append(stringsupport.UTF8ToSJIS(si.Name), []byte{0x00}...) combined = append(combined, stringsupport.UTF8ToSJIS(si.Description)...) bf.WriteUint8(uint8(len(combined))) bf.WriteBytes(combined) } else { - bf.WriteUint8(0) // Prevents malformed server name + bf.WriteUint8(0) // Ignored combined := append(stringsupport.UTF8ToSJIS(si.Name), []byte{0x00}...) combined = append(combined, stringsupport.UTF8ToSJIS(si.Description)...) bf.WriteBytes(stringsupport.PaddedString(string(combined), 65, false)) From 1aa2e360876516bb0e5ef73ea82bc648fe35b396 Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 31 Dec 2023 16:30:11 +1100 Subject: [PATCH 19/70] simplify World name concatenation --- server/entranceserver/make_resp.go | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/server/entranceserver/make_resp.go b/server/entranceserver/make_resp.go index 481ccae93..164f63871 100644 --- a/server/entranceserver/make_resp.go +++ b/server/entranceserver/make_resp.go @@ -47,20 +47,15 @@ func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte { bf.WriteUint8(si.Recommended) } - if s.erupeConfig.RealClientMode <= _config.F5 { - combined := append(stringsupport.UTF8ToSJIS(si.Name), []byte{0x00}...) - combined = append(combined, stringsupport.UTF8ToSJIS(si.Description)...) - bf.WriteBytes(stringsupport.PaddedString(string(combined), 65, false)) - } else if s.erupeConfig.RealClientMode <= _config.G5 { - combined := append(stringsupport.UTF8ToSJIS(si.Name), []byte{0x00}...) - combined = append(combined, stringsupport.UTF8ToSJIS(si.Description)...) - bf.WriteUint8(uint8(len(combined))) - bf.WriteBytes(combined) + fullName := append(append(stringsupport.UTF8ToSJIS(si.Name), []byte{0x00}...), stringsupport.UTF8ToSJIS(si.Description)...) + if s.erupeConfig.RealClientMode >= _config.G1 && s.erupeConfig.RealClientMode <= _config.G5 { + bf.WriteUint8(uint8(len(fullName))) + bf.WriteBytes(fullName) } else { - bf.WriteUint8(0) // Ignored - combined := append(stringsupport.UTF8ToSJIS(si.Name), []byte{0x00}...) - combined = append(combined, stringsupport.UTF8ToSJIS(si.Description)...) - bf.WriteBytes(stringsupport.PaddedString(string(combined), 65, false)) + if s.erupeConfig.RealClientMode >= _config.G51 { + bf.WriteUint8(0) // Ignored + } + bf.WriteBytes(stringsupport.PaddedString(string(fullName), 65, false)) } if s.erupeConfig.RealClientMode >= _config.GG { From 57bf1ca6e481dc452f1a39974f08eb09fcc17299 Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 31 Dec 2023 19:12:33 +1100 Subject: [PATCH 20/70] simplify EntranceServer crypto --- server/entranceserver/crypto.go | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/server/entranceserver/crypto.go b/server/entranceserver/crypto.go index 20a361e7e..5cbc94604 100644 --- a/server/entranceserver/crypto.go +++ b/server/entranceserver/crypto.go @@ -12,45 +12,40 @@ var ( // CalcSum32 calculates the custom MHF "sum32" checksum of the given data. func CalcSum32(data []byte) uint32 { - tableIdx0 := int(len(data) & 0xFF) - tableIdx1 := int(data[len(data)>>1] & 0xFF) - + tableIdx0 := (len(data) + 1) & 0xFF + tableIdx1 := int((data[len(data)>>1] + 1) & 0xFF) out := make([]byte, 4) for i := 0; i < len(data); i++ { - tableIdx0++ - tableIdx1++ - - tmp := byte((_sum32Table1[tableIdx1%9] ^ _sum32Table0[tableIdx0%7]) ^ data[i]) - out[i&3] = (out[i&3] + tmp) & 0xFF + key := _sum32Table0[(tableIdx0+i)%7] ^ _sum32Table1[(tableIdx1+i)%9] + out[i&3] = (out[i&3] + (data[i] ^ key)) & 0xFF } - return binary.BigEndian.Uint32(out) } +func rotate(k *uint32) { + *k = uint32(((54323 * uint(*k)) + 1) & 0xFFFFFFFF) +} + // EncryptBin8 encrypts the given data using MHF's "binary8" encryption. func EncryptBin8(data []byte, key byte) []byte { - curKey := uint32(((54323 * uint(key)) + 1) & 0xFFFFFFFF) - + _key := uint32(key) var output []byte for i := 0; i < len(data); i++ { - tmp := (_bin8Key[i&7] ^ byte((curKey>>13)&0xFF)) + rotate(&_key) + tmp := _bin8Key[i&7] ^ byte((_key>>13)&0xFF) output = append(output, data[i]^tmp) - curKey = uint32(((54323 * uint(curKey)) + 1) & 0xFFFFFFFF) } - return output } // DecryptBin8 decrypts the given MHF "binary8" data. func DecryptBin8(data []byte, key byte) []byte { - curKey := uint32(((54323 * uint(key)) + 1) & 0xFFFFFFFF) - + _key := uint32(key) var output []byte for i := 0; i < len(data); i++ { - tmp := (data[i] ^ byte((curKey>>13)&0xFF)) + rotate(&_key) + tmp := data[i] ^ byte((_key>>13)&0xFF) output = append(output, tmp^_bin8Key[i&7]) - curKey = uint32(((54323 * uint(curKey)) + 1) & 0xFFFFFFFF) } - return output } From 3db6ee7b25134dd580354a1f25f2792232238dd3 Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 31 Dec 2023 19:56:20 +1100 Subject: [PATCH 21/70] simplify EntranceServer crypto --- server/entranceserver/crypto.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/entranceserver/crypto.go b/server/entranceserver/crypto.go index 5cbc94604..e9e76f9fa 100644 --- a/server/entranceserver/crypto.go +++ b/server/entranceserver/crypto.go @@ -16,8 +16,8 @@ func CalcSum32(data []byte) uint32 { tableIdx1 := int((data[len(data)>>1] + 1) & 0xFF) out := make([]byte, 4) for i := 0; i < len(data); i++ { - key := _sum32Table0[(tableIdx0+i)%7] ^ _sum32Table1[(tableIdx1+i)%9] - out[i&3] = (out[i&3] + (data[i] ^ key)) & 0xFF + key := data[i] ^ _sum32Table0[(tableIdx0+i)%7] ^ _sum32Table1[(tableIdx1+i)%9] + out[i&3] = (out[i&3] + key) & 0xFF } return binary.BigEndian.Uint32(out) } From 4f5eeb1508d0732ac73e16258d1cf6119ca169af Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 1 Jan 2024 01:03:20 +1100 Subject: [PATCH 22/70] add ico resources --- erupe.ico | Bin 0 -> 99678 bytes rsrc_windows_amd64.syso | Bin 0 -> 100166 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 erupe.ico create mode 100644 rsrc_windows_amd64.syso diff --git a/erupe.ico b/erupe.ico new file mode 100644 index 0000000000000000000000000000000000000000..e1358c741f3bffeca6e1550e42f912ec9af94ce8 GIT binary patch literal 99678 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%0D0f_5|;(%60D2WjFFf{SwY@|7uxu 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 QyYsTWWOh@A Date: Mon, 1 Jan 2024 02:10:27 +1100 Subject: [PATCH 23/70] reuse existing token, JP string & command description --- config.json | 1 + server/channelserver/handlers_cast_binary.go | 22 +++++++------------- server/channelserver/sys_language.go | 4 +++- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/config.json b/config.json index 08bf46fe1..20da75a87 100644 --- a/config.json +++ b/config.json @@ -133,6 +133,7 @@ }, { "Name": "Discord", "Enabled": true, + "Description": "Generate a token to link your Discord account", "Prefix": "discord" } ], diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 7fdfbe437..08ae74d7e 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -325,23 +325,15 @@ func parseChatCommand(s *Session, command string) { } case commands["Discord"].Prefix: if commands["Discord"].Enabled { - randToken := make([]byte, 4) - - _, err := rand.Read(randToken) + var _token string + err := s.server.db.QueryRow(`SELECT discord_token FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&_token) if err != nil { - sendServerChatMessage(s, fmt.Sprint("An error occurred while processing this command")) - s.logger.Error(fmt.Sprint(err)) - return + randToken := make([]byte, 4) + rand.Read(randToken) + _token = fmt.Sprintf("%x-%x", randToken[:2], randToken[2:]) + s.server.db.Exec(`UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, _token, s.charID) } - - discordToken := fmt.Sprintf("%x-%x", randToken[:2], randToken[2:]) - _, err = s.server.db.Exec("UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", fmt.Sprint(discordToken), s.charID) - if err != nil { - sendServerChatMessage(s, fmt.Sprint("An error occurred while processing this command")) - s.logger.Error(fmt.Sprint(err)) - return - } - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscordSuccess"], discordToken)) + sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscordSuccess"], _token)) } else { sendDisabledCommandMessage(s, commands["Discord"]) } diff --git a/server/channelserver/sys_language.go b/server/channelserver/sys_language.go index a5f326487..d173637f2 100644 --- a/server/channelserver/sys_language.go +++ b/server/channelserver/sys_language.go @@ -25,6 +25,8 @@ func getLangStrings(s *Server) map[string]string { strings["commandPSNSuccess"] = "PSN「%s」が連携されています" strings["commandPSNExists"] = "PSNは既存のユーザに接続されています" + strings["commandDiscordSuccess"] = "あなたのDiscordトークン:%s" + strings["commandRaviNoCommand"] = "ラヴィコマンドが指定されていません" strings["commandRaviStartSuccess"] = "大討伐を開始します" strings["commandRaviStartError"] = "大討伐は既に開催されています" @@ -78,7 +80,7 @@ func getLangStrings(s *Server) map[string]string { strings["commandPSNSuccess"] = "Connected PSN ID: %s" strings["commandPSNExists"] = "PSN ID is connected to another account!" - strings["commandDiscordSuccess"] = "Discord token has been generated: %s" + strings["commandDiscordSuccess"] = "Your Discord token: %s" strings["commandRaviNoCommand"] = "No Raviente command specified!" strings["commandRaviStartSuccess"] = "The Great Slaying will begin in a moment" From 0d2863709591a7487e3e23f2b42d2218e5cb15b9 Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 1 Jan 2024 21:22:51 +1100 Subject: [PATCH 24/70] support long messages, rename to RelayChannel, move commands out of main --- config.json | 1 + config/config.go | 13 +++--- main.go | 28 +---------- server/channelserver/handlers_discord.go | 23 +++++++-- server/discordbot/discord_bot.go | 59 +++++++++++++++++------- 5 files changed, 70 insertions(+), 54 deletions(-) diff --git a/config.json b/config.json index 20da75a87..15f51d9df 100644 --- a/config.json +++ b/config.json @@ -86,6 +86,7 @@ "BotToken": "", "RealTimeChannel": { "Enabled": false, + "MaxMessageLength": 183, "RealTimeChannelID": "" } }, diff --git a/config/config.go b/config/config.go index 8b1c0764d..3c6f6a0a7 100644 --- a/config/config.go +++ b/config/config.go @@ -171,14 +171,15 @@ type GameplayOptions struct { // Discord holds the discord integration config. type Discord struct { - Enabled bool - BotToken string - RealTimeChannel DiscordRealTime + Enabled bool + BotToken string + RelayChannel DiscordRelay } -type DiscordRealTime struct { - Enabled bool - RealtimeChannelID string +type DiscordRelay struct { + Enabled bool + MaxMessageLength int + RelayChannelID string } // Command is a channelserver chat command diff --git a/main.go b/main.go index 94f3b6e24..a7d368930 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( _config "erupe-ce/config" "fmt" - "github.com/bwmarrin/discordgo" "net" "os" "os/signal" @@ -93,32 +92,7 @@ func main() { discordBot = bot - _, err = discordBot.Session.ApplicationCommandBulkOverwrite(discordBot.Session.State.User.ID, "", []*discordgo.ApplicationCommand{ - { - Name: "verify", - Description: "Verify your account with Discord", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "token", - Description: "The access token provided by !discord command within the game client.", - Required: true, - }, - }, - }, - { - Name: "password", - Description: "Reset your account password on Erupe", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "password", - Description: "The password to change your account to.", - Required: true, - }, - }, - }, - }) + _, err = discordBot.Session.ApplicationCommandBulkOverwrite(discordBot.Session.State.User.ID, "", discordbot.Commands) if err != nil { preventClose(fmt.Sprintf("Discord: Failed to start, %s", err.Error())) } diff --git a/server/channelserver/handlers_discord.go b/server/channelserver/handlers_discord.go index 1772e2d06..8e3ed3cb9 100644 --- a/server/channelserver/handlers_discord.go +++ b/server/channelserver/handlers_discord.go @@ -113,8 +113,8 @@ func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCr // 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.Bot || m.ChannelID != s.erupeConfig.Discord.RealTimeChannel.RealtimeChannelID { + // Ignore messages from bots, or messages that are not in the correct channel. + if m.Author.Bot || m.ChannelID != s.erupeConfig.Discord.RelayChannel.RelayChannelID { return } @@ -124,11 +124,24 @@ func (s *Server) onDiscordMessage(ds *discordgo.Session, m *discordgo.MessageCre } return r }, m.Author.Username)) - for i := 0; i < 8-len(m.Author.Username); i++ { paddedName += " " } + message := s.discordBot.NormalizeDiscordMessage(fmt.Sprintf("[D] %s > %s", paddedName, m.Content)) + if len(message) > s.erupeConfig.Discord.RelayChannel.MaxMessageLength { + return + } - message := fmt.Sprintf("[D] %s > %s", paddedName, m.Content) - s.BroadcastChatMessage(s.discordBot.NormalizeDiscordMessage(message)) + var messages []string + lineLength := 61 + for i := 0; i < len(message); i += lineLength { + end := i + lineLength + if end > len(message) { + end = len(message) + } + messages = append(messages, message[i:end]) + } + for i := range messages { + s.BroadcastChatMessage(messages[i]) + } } diff --git a/server/discordbot/discord_bot.go b/server/discordbot/discord_bot.go index 0d774fff7..a9b327cc3 100644 --- a/server/discordbot/discord_bot.go +++ b/server/discordbot/discord_bot.go @@ -7,12 +7,39 @@ import ( "regexp" ) +var Commands = []*discordgo.ApplicationCommand{ + { + Name: "link", + Description: "Link your Erupe account to Discord", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "token", + Description: "The token provided by the Discord command in-game", + Required: true, + }, + }, + }, + { + Name: "password", + Description: "Change your Erupe account password", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "password", + Description: "Your new password", + Required: true, + }, + }, + }, +} + type DiscordBot struct { - Session *discordgo.Session - config *_config.Config - logger *zap.Logger - MainGuild *discordgo.Guild - RealtimeChannel *discordgo.Channel + Session *discordgo.Session + config *_config.Config + logger *zap.Logger + MainGuild *discordgo.Guild + RelayChannel *discordgo.Channel } type Options struct { @@ -28,22 +55,22 @@ func NewDiscordBot(options Options) (discordBot *DiscordBot, err error) { return nil, err } - var realtimeChannel *discordgo.Channel + var relayChannel *discordgo.Channel - if options.Config.Discord.RealTimeChannel.Enabled { - realtimeChannel, err = session.Channel(options.Config.Discord.RealTimeChannel.RealtimeChannelID) + if options.Config.Discord.RelayChannel.Enabled { + relayChannel, err = session.Channel(options.Config.Discord.RelayChannel.RelayChannelID) } if err != nil { - options.Logger.Fatal("Discord failed to create realtimeChannel", zap.Error(err)) + options.Logger.Fatal("Discord failed to create relayChannel", zap.Error(err)) return nil, err } discordBot = &DiscordBot{ - config: options.Config, - logger: options.Logger, - Session: session, - RealtimeChannel: realtimeChannel, + config: options.Config, + logger: options.Logger, + Session: session, + RelayChannel: relayChannel, } return @@ -55,7 +82,7 @@ func (bot *DiscordBot) Start() (err error) { return } -// Replace all mentions to real name from the message. +// NormalizeDiscordMessage replaces all mentions to real name from the message. func (bot *DiscordBot) NormalizeDiscordMessage(message string) string { userRegex := regexp.MustCompile(`<@!?(\d{17,19})>`) emojiRegex := regexp.MustCompile(`(?:)?`) @@ -78,11 +105,11 @@ func (bot *DiscordBot) NormalizeDiscordMessage(message string) string { } func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) { - if bot.RealtimeChannel == nil { + if bot.RelayChannel == nil { return } - _, err = bot.Session.ChannelMessageSend(bot.RealtimeChannel.ID, message) + _, err = bot.Session.ChannelMessageSend(bot.RelayChannel.ID, message) return } From 1ed8b97347f1c202d730bb5fc737720ca1510399 Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 1 Jan 2024 23:39:29 +1100 Subject: [PATCH 25/70] update onInteraction process --- server/channelserver/handlers_discord.go | 70 ++++++++++++------------ 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/server/channelserver/handlers_discord.go b/server/channelserver/handlers_discord.go index 8e3ed3cb9..3144b5e7b 100644 --- a/server/channelserver/handlers_discord.go +++ b/server/channelserver/handlers_discord.go @@ -70,44 +70,46 @@ func getCharacterList(s *Server) string { // onInteraction handles slash commands func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCreate) { switch i.Interaction.ApplicationCommandData().Name { - case "verify": - _, err := s.db.Exec("UPDATE users SET discord_id = $1 WHERE discord_token = $2", i.Member.User.ID, i.ApplicationCommandData().Options[0].StringValue()) - if err != nil { - return + case "link": + var temp string + err := s.db.QueryRow(`UPDATE users SET discord_id = $1 WHERE discord_token = $2 RETURNING discord_id`, i.Member.User.ID, i.ApplicationCommandData().Options[0].StringValue()).Scan(&temp) + if err == nil { + ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Your Erupe account was linked successfully.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + } else { + ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Failed to link Erupe account.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) } - - err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Erupe account successfully linked to Discord account.", - Flags: discordgo.MessageFlagsEphemeral, - }, - }) - - if err != nil { - return - } - break case "password": password, _ := bcrypt.GenerateFromPassword([]byte(i.ApplicationCommandData().Options[0].StringValue()), 10) - - _, err := s.db.Exec("UPDATE users SET password = $1 WHERE discord_id = $2", password, i.Member.User.ID) - if err != nil { - s.logger.Error(fmt.Sprint(err)) - return + _, err := s.db.Exec(`UPDATE users SET password = $1 WHERE discord_id = $2`, password, i.Member.User.ID) + if err == nil { + ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Your Erupe account password has been updated.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + } else { + ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Failed to update Erupe account password.", + Flags: discordgo.MessageFlagsEphemeral, + }, + }) } - - err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Password has been reset, you may login now.", - Flags: discordgo.MessageFlagsEphemeral, - }, - }) - if err != nil { - return - } - break } } From 63388aa4f7c23291292bbaa242ca6c8ce46a3b41 Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 2 Jan 2024 19:53:27 +1100 Subject: [PATCH 26/70] re-index patch-schema --- ...discord-password-resets.sql => 16-discord-password-resets.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patch-schema/{15-discord-password-resets.sql => 16-discord-password-resets.sql} (100%) diff --git a/patch-schema/15-discord-password-resets.sql b/patch-schema/16-discord-password-resets.sql similarity index 100% rename from patch-schema/15-discord-password-resets.sql rename to patch-schema/16-discord-password-resets.sql From 1766b6f2bda1a362baee24503585a3c25af6ce15 Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 2 Jan 2024 20:04:40 +1100 Subject: [PATCH 27/70] rewrite EntranceServer response --- server/entranceserver/make_resp.go | 35 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/server/entranceserver/make_resp.go b/server/entranceserver/make_resp.go index 164f63871..ec0281abc 100644 --- a/server/entranceserver/make_resp.go +++ b/server/entranceserver/make_resp.go @@ -29,7 +29,6 @@ func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte { } } - sid := (4096 + serverIdx*256) * 6000 if si.IP == "" { si.IP = config.Host } @@ -38,8 +37,8 @@ func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte { } else { bf.WriteUint32(binary.LittleEndian.Uint32(net.ParseIP(si.IP).To4())) } - bf.WriteUint16(16 + uint16(serverIdx)) - bf.WriteUint16(0x0000) + bf.WriteUint16(uint16(serverIdx | 16)) + bf.WriteUint16(0) bf.WriteUint16(uint16(len(si.Channels))) bf.WriteUint8(si.Type) bf.WriteUint8(uint8(((channelserver.TimeAdjusted().Unix() / 86400) + int64(serverIdx)) % 3)) @@ -63,31 +62,31 @@ func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte { } for channelIdx, ci := range si.Channels { - sid = (4096 + serverIdx*256) + (16 + channelIdx) + sid := (serverIdx<<8 | 4096) + (channelIdx | 16) if _config.ErupeConfig.DebugOptions.ProxyPort != 0 { bf.WriteUint16(_config.ErupeConfig.DebugOptions.ProxyPort) } else { bf.WriteUint16(ci.Port) } - bf.WriteUint16(16 + uint16(channelIdx)) + bf.WriteUint16(uint16(channelIdx | 16)) bf.WriteUint16(ci.MaxPlayers) var currentPlayers uint16 s.db.QueryRow("SELECT current_players FROM servers WHERE server_id=$1", sid).Scan(¤tPlayers) bf.WriteUint16(currentPlayers) - bf.WriteUint16(0) // Unk - bf.WriteUint16(0) // Unk - bf.WriteUint16(0) // Unk - bf.WriteUint16(0) // Unk - bf.WriteUint16(0) // Unk - bf.WriteUint16(0) // Unk - bf.WriteUint16(319) // Unk - bf.WriteUint16(252) // Unk - bf.WriteUint16(248) // Unk - bf.WriteUint16(12345) // Unk + bf.WriteUint16(0) + bf.WriteUint16(0) + bf.WriteUint16(0) + bf.WriteUint16(0) + bf.WriteUint16(0) + bf.WriteUint16(0) + bf.WriteUint16(319) // Unk + bf.WriteUint16(254 - currentPlayers) // Unk + bf.WriteUint16(255 - currentPlayers) // Unk + bf.WriteUint16(12345) } } bf.WriteUint32(uint32(channelserver.TimeAdjusted().Unix())) - bf.WriteUint32(0x0000003C) + bf.WriteUint32(60) return bf.Data() } @@ -156,11 +155,11 @@ func makeUsrResp(pkt []byte, s *Server) []byte { var sid uint16 err := s.db.QueryRow("SELECT(SELECT server_id FROM sign_sessions WHERE char_id=$1) AS _", cid).Scan(&sid) if err != nil { - resp.WriteBytes(make([]byte, 4)) + resp.WriteUint16(0) } else { resp.WriteUint16(sid) - resp.WriteUint16(0) } + resp.WriteUint16(0) } if s.erupeConfig.DebugOptions.LogOutboundMessages { From ca80a98141a1441dcc630b3daf9bbb55775b711f Mon Sep 17 00:00:00 2001 From: wish Date: Wed, 3 Jan 2024 04:22:25 +1100 Subject: [PATCH 28/70] i18n proposal --- server/channelserver/handlers_cafe.go | 2 +- server/channelserver/handlers_cast_binary.go | 60 ++-- server/channelserver/handlers_guild_scout.go | 20 +- server/channelserver/sys_channel_server.go | 12 +- server/channelserver/sys_language.go | 274 ++++++++++++------- 5 files changed, 228 insertions(+), 140 deletions(-) diff --git a/server/channelserver/handlers_cafe.go b/server/channelserver/handlers_cafe.go index 9c4b0b732..b87bac5ff 100644 --- a/server/channelserver/handlers_cafe.go +++ b/server/channelserver/handlers_cafe.go @@ -94,7 +94,7 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) { } bf.WriteUint32(cafeTime) // Total cafe time bf.WriteUint16(0) - ps.Uint16(bf, fmt.Sprintf(s.server.dict["cafeReset"], int(cafeReset.Month()), cafeReset.Day()), true) + ps.Uint16(bf, fmt.Sprintf(s.server.i18n.cafe.reset, int(cafeReset.Month()), cafeReset.Day()), true) doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 08ae74d7e..043403216 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -59,7 +59,7 @@ func init() { } func sendDisabledCommandMessage(s *Session, cmd _config.Command) { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDisabled"], cmd.Name)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.disabled, cmd.Name)) } func sendServerChatMessage(s *Session, message string) { @@ -95,20 +95,20 @@ func parseChatCommand(s *Session, command string) { if exists == 0 { _, err := s.server.db.Exec(`UPDATE users u SET psn_id=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, args[1], s.charID) if err == nil { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandPSNSuccess"], args[1])) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.psn.success, args[1])) } } else { - sendServerChatMessage(s, s.server.dict["commandPSNExists"]) + sendServerChatMessage(s, s.server.i18n.commands.psn.exists) } } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandPSNError"], commands["PSN"].Prefix)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.psn.error, commands["PSN"].Prefix)) } } else { sendDisabledCommandMessage(s, commands["PSN"]) } case commands["Reload"].Prefix: if commands["Reload"].Enabled { - sendServerChatMessage(s, s.server.dict["commandReload"]) + sendServerChatMessage(s, s.server.i18n.commands.reload) var temp mhfpacket.MHFPacket deleteNotif := byteframe.NewByteFrame() for _, object := range s.stage.objects { @@ -170,19 +170,19 @@ func parseChatCommand(s *Session, command string) { case commands["KeyQuest"].Prefix: if commands["KeyQuest"].Enabled { if s.server.erupeConfig.RealClientMode < _config.G10 { - sendServerChatMessage(s, s.server.dict["commandKqfVersion"]) + sendServerChatMessage(s, s.server.i18n.commands.kqf.version) } else { if len(args) > 1 { if args[1] == "get" { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandKqfGet"], s.kqf)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.kqf.get, s.kqf)) } else if args[1] == "set" { if len(args) > 2 && len(args[2]) == 16 { hexd, _ := hex.DecodeString(args[2]) s.kqf = hexd s.kqfOverride = true - sendServerChatMessage(s, s.server.dict["commandKqfSetSuccess"]) + sendServerChatMessage(s, s.server.i18n.commands.kqf.set.success) } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandKqfSetError"], commands["KeyQuest"].Prefix)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.kqf.set.error, commands["KeyQuest"].Prefix)) } } } @@ -196,12 +196,12 @@ func parseChatCommand(s *Session, command string) { v, _ := strconv.Atoi(args[1]) _, err := s.server.db.Exec("UPDATE users u SET rights=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", v, s.charID) if err == nil { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandRightsSuccess"], v)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.success, v)) } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandRightsError"], commands["Rights"].Prefix)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.error, commands["Rights"].Prefix)) } } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandRightsError"], commands["Rights"].Prefix)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.error, commands["Rights"].Prefix)) } } else { sendDisabledCommandMessage(s, commands["Rights"]) @@ -225,11 +225,11 @@ func parseChatCommand(s *Session, command string) { }) if ei != -1 { delta = uint32(-1 * math.Pow(2, float64(course.ID))) - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseDisabled"], course.Aliases()[0])) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.disabled, course.Aliases()[0])) } } else { delta = uint32(math.Pow(2, float64(course.ID))) - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseEnabled"], course.Aliases()[0])) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.enabled, course.Aliases()[0])) } err := 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(&rightsInt) if err == nil { @@ -237,14 +237,14 @@ func parseChatCommand(s *Session, command string) { } updateRights(s) } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseLocked"], course.Aliases()[0])) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.locked, course.Aliases()[0])) } return } } } } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseError"], commands["Course"].Prefix)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.error, commands["Course"].Prefix)) } } else { sendDisabledCommandMessage(s, commands["Course"]) @@ -257,45 +257,45 @@ func parseChatCommand(s *Session, command string) { case "start": if s.server.raviente.register[1] == 0 { s.server.raviente.register[1] = s.server.raviente.register[3] - sendServerChatMessage(s, s.server.dict["commandRaviStartSuccess"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.start.success) s.notifyRavi() } else { - sendServerChatMessage(s, s.server.dict["commandRaviStartError"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.start.error) } case "cm", "check", "checkmultiplier", "multiplier": - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandRaviMultiplier"], s.server.GetRaviMultiplier())) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ravi.multiplier, s.server.GetRaviMultiplier())) case "sr", "sendres", "resurrection", "ss", "sendsed", "rs", "reqsed": if s.server.erupeConfig.RealClientMode == _config.ZZ { switch args[1] { case "sr", "sendres", "resurrection": if s.server.raviente.state[28] > 0 { - sendServerChatMessage(s, s.server.dict["commandRaviResSuccess"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.res.success) s.server.raviente.state[28] = 0 } else { - sendServerChatMessage(s, s.server.dict["commandRaviResError"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.res.error) } case "ss", "sendsed": - sendServerChatMessage(s, s.server.dict["commandRaviSedSuccess"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.sed.success) // Total BerRavi HP HP := s.server.raviente.state[0] + s.server.raviente.state[1] + s.server.raviente.state[2] + s.server.raviente.state[3] + s.server.raviente.state[4] s.server.raviente.support[1] = HP case "rs", "reqsed": - sendServerChatMessage(s, s.server.dict["commandRaviRequest"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.request) // Total BerRavi HP HP := s.server.raviente.state[0] + s.server.raviente.state[1] + s.server.raviente.state[2] + s.server.raviente.state[3] + s.server.raviente.state[4] s.server.raviente.support[1] = HP + 1 } } else { - sendServerChatMessage(s, s.server.dict["commandRaviVersion"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.version) } default: - sendServerChatMessage(s, s.server.dict["commandRaviError"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.error) } } else { - sendServerChatMessage(s, s.server.dict["commandRaviNoPlayers"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.noPlayers) } } else { - sendServerChatMessage(s, s.server.dict["commandRaviError"]) + sendServerChatMessage(s, s.server.i18n.commands.ravi.error) } } else { sendDisabledCommandMessage(s, commands["Raviente"]) @@ -316,9 +316,9 @@ func parseChatCommand(s *Session, command string) { MessageType: BinaryMessageTypeState, RawDataPayload: payloadBytes, }) - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandTeleportSuccess"], x, y)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.teleport.success, x, y)) } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandTeleportError"], commands["Teleport"].Prefix)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.teleport.error, commands["Teleport"].Prefix)) } } else { sendDisabledCommandMessage(s, commands["Teleport"]) @@ -333,7 +333,7 @@ func parseChatCommand(s *Session, command string) { _token = fmt.Sprintf("%x-%x", randToken[:2], randToken[2:]) s.server.db.Exec(`UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, _token, s.charID) } - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscordSuccess"], _token)) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.discord.success, _token)) } else { sendDisabledCommandMessage(s, commands["Discord"]) } diff --git a/server/channelserver/handlers_guild_scout.go b/server/channelserver/handlers_guild_scout.go index fceeb037d..3c73b5bec 100644 --- a/server/channelserver/handlers_guild_scout.go +++ b/server/channelserver/handlers_guild_scout.go @@ -60,9 +60,9 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { mail := &Mail{ SenderID: s.charID, RecipientID: pkt.CharID, - Subject: s.server.dict["guildInviteName"], + Subject: s.server.i18n.guild.invite.title, Body: fmt.Sprintf( - s.server.dict["guildInvite"], + s.server.i18n.guild.invite.body, guildInfo.Name, ), IsGuildInvite: true, @@ -146,30 +146,30 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) { err = guild.AcceptApplication(s, s.charID) mail = append(mail, Mail{ RecipientID: s.charID, - Subject: s.server.dict["guildInviteSuccessName"], - Body: fmt.Sprintf(s.server.dict["guildInviteSuccess"], guild.Name), + Subject: s.server.i18n.guild.invite.success.title, + Body: fmt.Sprintf(s.server.i18n.guild.invite.success.body, guild.Name), IsSystemMessage: true, }) mail = append(mail, Mail{ SenderID: s.charID, RecipientID: pkt.LeaderID, - Subject: s.server.dict["guildInviteAcceptedName"], - Body: fmt.Sprintf(s.server.dict["guildInviteAccepted"], guild.Name), + Subject: s.server.i18n.guild.invite.accepted.title, + Body: fmt.Sprintf(s.server.i18n.guild.invite.accepted.body, guild.Name), IsSystemMessage: true, }) } else { err = guild.RejectApplication(s, s.charID) mail = append(mail, Mail{ RecipientID: s.charID, - Subject: s.server.dict["guildInviteRejectName"], - Body: fmt.Sprintf(s.server.dict["guildInviteReject"], guild.Name), + Subject: s.server.i18n.guild.invite.rejected.title, + Body: fmt.Sprintf(s.server.i18n.guild.invite.rejected.body, guild.Name), IsSystemMessage: true, }) mail = append(mail, Mail{ SenderID: s.charID, RecipientID: pkt.LeaderID, - Subject: s.server.dict["guildInviteDeclined"], - Body: fmt.Sprintf(s.server.dict["guildInviteDeclined"], guild.Name), + Subject: s.server.i18n.guild.invite.declined.title, + Body: fmt.Sprintf(s.server.i18n.guild.invite.declined.body, guild.Name), IsSystemMessage: true, }) } diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 119cbf420..30d63241f 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -57,7 +57,7 @@ type Server struct { stages map[string]*Stage // Used to map different languages - dict map[string]string + i18n i18n // UserBinary userBinaryPartsLock sync.RWMutex @@ -192,7 +192,7 @@ func NewServer(config *Config) *Server { // MezFes s.stages["sl1Ns462p0a0u0"] = NewStage("sl1Ns462p0a0u0") - s.dict = getLangStrings(s) + s.i18n = getLangStrings(s) return s } @@ -337,13 +337,13 @@ func (s *Server) BroadcastRaviente(ip uint32, port uint16, stage []byte, _type u var text string switch _type { case 2: - text = s.dict["ravienteBerserk"] + text = s.i18n.raviente.berserk case 3: - text = s.dict["ravienteExtreme"] + text = s.i18n.raviente.extreme case 4: - text = s.dict["ravienteExtremeLimited"] + text = s.i18n.raviente.extremeLimited case 5: - text = s.dict["ravienteBerserkSmall"] + text = s.i18n.raviente.berserkSmall default: s.logger.Error("Unk raviente type", zap.Uint8("_type", _type)) } diff --git a/server/channelserver/sys_language.go b/server/channelserver/sys_language.go index d173637f2..5feb76444 100644 --- a/server/channelserver/sys_language.go +++ b/server/channelserver/sys_language.go @@ -1,118 +1,206 @@ package channelserver -func getLangStrings(s *Server) map[string]string { - strings := make(map[string]string) +type i18n struct { + language string + cafe struct { + reset string + } + commands struct { + disabled string + reload string + kqf struct { + get string + set struct { + error string + success string + } + version string + } + rights struct { + error string + success string + } + course struct { + error string + disabled string + enabled string + locked string + } + teleport struct { + error string + success string + } + psn struct { + error string + success string + exists string + } + discord struct { + success string + } + ravi struct { + noCommand string + start struct { + success string + error string + } + multiplier string + res struct { + success string + error string + } + sed struct { + success string + } + request string + error string + noPlayers string + version string + } + } + raviente struct { + berserk string + extreme string + extremeLimited string + berserkSmall string + } + guild struct { + invite struct { + title string + body string + success struct { + title string + body string + } + accepted struct { + title string + body string + } + rejected struct { + title string + body string + } + declined struct { + title string + body string + } + } + } +} + +func getLangStrings(s *Server) i18n { + var i i18n switch s.erupeConfig.Language { case "jp": - strings["language"] = "日本語" - strings["cafeReset"] = "%d/%dにリセット" + i.language = "日本語" + i.cafe.reset = "%d/%dにリセット" - strings["commandDisabled"] = "%sのコマンドは無効です" - strings["commandReload"] = "リロードします" - strings["commandKqfGet"] = "現在のキークエストフラグ:%x" - strings["commandKqfSetError"] = "キークエコマンドエラー 例:%s set xxxxxxxxxxxxxxxx" - strings["commandKqfSetSuccess"] = "キークエストのフラグが更新されました。ワールド/ランドを移動してください" - strings["commandKqfVersion"] = "This command is disabled prior to MHFG10" - strings["commandRightsError"] = "コース更新コマンドエラー 例:%s x" - strings["commandRightsSuccess"] = "コース情報を更新しました:%d" - strings["commandCourseError"] = "コース確認コマンドエラー 例:%s " - strings["commandCourseDisabled"] = "%sコースは無効です" - strings["commandCourseEnabled"] = "%sコースは有効です" - strings["commandCourseLocked"] = "%sコースはロックされています" - strings["commandTeleportError"] = "テレポートコマンドエラー 構文:%s x y" - strings["commandTeleportSuccess"] = "%d %dにテレポート" - strings["commandPSNError"] = "PSN連携コマンドエラー 例:%s " - strings["commandPSNSuccess"] = "PSN「%s」が連携されています" - strings["commandPSNExists"] = "PSNは既存のユーザに接続されています" + i.commands.disabled = "%sのコマンドは無効です" + i.commands.reload = "リロードします" + i.commands.kqf.get = "現在のキークエストフラグ:%x" + i.commands.kqf.set.error = "キークエコマンドエラー 例:%s set xxxxxxxxxxxxxxxx" + i.commands.kqf.set.success = "キークエストのフラグが更新されました。ワールド/ランドを移動してください" + i.commands.kqf.version = "This command is disabled prior to MHFG10" + i.commands.rights.error = "コース更新コマンドエラー 例:%s x" + i.commands.rights.success = "コース情報を更新しました:%d" + i.commands.course.error = "コース確認コマンドエラー 例:%s " + i.commands.course.disabled = "%sコースは無効です" + i.commands.course.enabled = "%sコースは有効です" + i.commands.course.locked = "%sコースはロックされています" + i.commands.teleport.error = "テレポートコマンドエラー 構文:%s x y" + i.commands.teleport.success = "%d %dにテレポート" + i.commands.psn.error = "PSN連携コマンドエラー 例:%s " + i.commands.psn.success = "PSN「%s」が連携されています" + i.commands.psn.exists = "PSNは既存のユーザに接続されています" - strings["commandDiscordSuccess"] = "あなたのDiscordトークン:%s" + i.commands.discord.success = "あなたのDiscordトークン:%s" - strings["commandRaviNoCommand"] = "ラヴィコマンドが指定されていません" - strings["commandRaviStartSuccess"] = "大討伐を開始します" - strings["commandRaviStartError"] = "大討伐は既に開催されています" - strings["commandRaviMultiplier"] = "ラヴィダメージ倍率:x%.2f" - strings["commandRaviResSuccess"] = "復活支援を実行します" - strings["commandRaviResError"] = "復活支援は実行されませんでした" - strings["commandRaviSedSuccess"] = "鎮静支援を実行します" - strings["commandRaviRequest"] = "鎮静支援を要請します" - strings["commandRaviError"] = "ラヴィコマンドが認識されません" - strings["commandRaviNoPlayers"] = "誰も大討伐に参加していません" - strings["commandRaviVersion"] = "This command is disabled outside of MHFZZ" + i.commands.ravi.noCommand = "ラヴィコマンドが指定されていません" + i.commands.ravi.start.success = "大討伐を開始します" + i.commands.ravi.start.error = "大討伐は既に開催されています" + i.commands.ravi.multiplier = "ラヴィダメージ倍率:x%.2f" + i.commands.ravi.res.success = "復活支援を実行します" + i.commands.ravi.res.error = "復活支援は実行されませんでした" + i.commands.ravi.sed.success = "鎮静支援を実行します" + i.commands.ravi.request = "鎮静支援を要請します" + i.commands.ravi.error = "ラヴィコマンドが認識されません" + i.commands.ravi.noPlayers = "誰も大討伐に参加していません" + i.commands.ravi.version = "This command is disabled outside of MHFZZ" - strings["ravienteBerserk"] = "<大討伐:猛狂期>が開催されました!" - strings["ravienteExtreme"] = "<大討伐:猛狂期【極】>が開催されました!" - strings["ravienteExtremeLimited"] = "<大討伐:猛狂期【極】(制限付)>が開催されました!" - strings["ravienteBerserkSmall"] = "<大討伐:猛狂期(小数)>が開催されました!" + i.raviente.berserk = "<大討伐:猛狂期>が開催されました!" + i.raviente.extreme = "<大討伐:猛狂期【極】>が開催されました!" + i.raviente.extremeLimited = "<大討伐:猛狂期【極】(制限付)>が開催されました!" + i.raviente.berserkSmall = "<大討伐:猛狂期(小数)>が開催されました!" - strings["guildInviteName"] = "猟団勧誘のご案内" - strings["guildInvite"] = "猟団「%s」からの勧誘通知です。\n「勧誘に返答」より、返答を行ってください。" + i.guild.invite.title = "猟団勧誘のご案内" + i.guild.invite.body = "猟団「%s」からの勧誘通知です。\n「勧誘に返答」より、返答を行ってください。" - strings["guildInviteSuccessName"] = "成功" - strings["guildInviteSuccess"] = "あなたは「%s」に参加できました。" + i.guild.invite.success.title = "成功" + i.guild.invite.success.body = "あなたは「%s」に参加できました。" - strings["guildInviteAcceptedName"] = "承諾されました" - strings["guildInviteAccepted"] = "招待した狩人が「%s」への招待を承諾しました。" + i.guild.invite.accepted.title = "承諾されました" + i.guild.invite.accepted.body = "招待した狩人が「%s」への招待を承諾しました。" - strings["guildInviteRejectName"] = "却下しました" - strings["guildInviteReject"] = "あなたは「%s」への参加を却下しました。" + i.guild.invite.rejected.title = "却下しました" + i.guild.invite.rejected.body = "あなたは「%s」への参加を却下しました。" - strings["guildInviteDeclinedName"] = "辞退しました" - strings["guildInviteDeclined"] = "招待した狩人が「%s」への招待を辞退しました。" + i.guild.invite.declined.title = "辞退しました" + i.guild.invite.declined.body = "招待した狩人が「%s」への招待を辞退しました。" default: - strings["language"] = "English" - strings["cafeReset"] = "Resets on %d/%d" + i.language = "English" + i.cafe.reset = "Resets on %d/%d" - strings["commandDisabled"] = "%s command is disabled" - strings["commandReload"] = "Reloading players..." - strings["commandKqfGet"] = "KQF: %x" - strings["commandKqfSetError"] = "Error in command. Format: %s set xxxxxxxxxxxxxxxx" - strings["commandKqfSetSuccess"] = "KQF set, please switch Land/World" - strings["commandKqfVersion"] = "This command is disabled prior to MHFG10" - strings["commandRightsError"] = "Error in command. Format: %s x" - strings["commandRightsSuccess"] = "Set rights integer: %d" - strings["commandCourseError"] = "Error in command. Format: %s " - strings["commandCourseDisabled"] = "%s Course disabled" - strings["commandCourseEnabled"] = "%s Course enabled" - strings["commandCourseLocked"] = "%s Course is locked" - strings["commandTeleportError"] = "Error in command. Format: %s x y" - strings["commandTeleportSuccess"] = "Teleporting to %d %d" - strings["commandPSNError"] = "Error in command. Format: %s " - strings["commandPSNSuccess"] = "Connected PSN ID: %s" - strings["commandPSNExists"] = "PSN ID is connected to another account!" + i.commands.disabled = "%s command is disabled" + i.commands.reload = "Reloading players..." + i.commands.kqf.get = "KQF: %x" + i.commands.kqf.set.error = "Error in command. Format: %s set xxxxxxxxxxxxxxxx" + i.commands.kqf.set.success = "KQF set, please switch Land/World" + i.commands.kqf.version = "This command is disabled prior to MHFG10" + i.commands.rights.error = "Error in command. Format: %s x" + i.commands.rights.success = "Set rights integer: %d" + i.commands.course.error = "Error in command. Format: %s " + i.commands.course.disabled = "%s Course disabled" + i.commands.course.enabled = "%s Course enabled" + i.commands.course.locked = "%s Course is locked" + i.commands.teleport.error = "Error in command. Format: %s x y" + i.commands.teleport.success = "Teleporting to %d %d" + i.commands.psn.error = "Error in command. Format: %s " + i.commands.psn.success = "Connected PSN ID: %s" + i.commands.psn.exists = "PSN ID is connected to another account!" - strings["commandDiscordSuccess"] = "Your Discord token: %s" + i.commands.discord.success = "Your Discord token: %s" - strings["commandRaviNoCommand"] = "No Raviente command specified!" - strings["commandRaviStartSuccess"] = "The Great Slaying will begin in a moment" - strings["commandRaviStartError"] = "The Great Slaying has already begun!" - strings["commandRaviMultiplier"] = "Raviente multiplier is currently %.2fx" - strings["commandRaviResSuccess"] = "Sending resurrection support!" - strings["commandRaviResError"] = "Resurrection support has not been requested!" - strings["commandRaviSedSuccess"] = "Sending sedation support if requested!" - strings["commandRaviRequest"] = "Requesting sedation support!" - strings["commandRaviError"] = "Raviente command not recognised!" - strings["commandRaviNoPlayers"] = "No one has joined the Great Slaying!" - strings["commandRaviVersion"] = "This command is disabled outside of MHFZZ" + i.commands.ravi.noCommand = "No Raviente command specified!" + i.commands.ravi.start.success = "The Great Slaying will begin in a moment" + i.commands.ravi.start.error = "The Great Slaying has already begun!" + i.commands.ravi.multiplier = "Raviente multiplier is currently %.2fx" + i.commands.ravi.res.success = "Sending resurrection support!" + i.commands.ravi.res.error = "Resurrection support has not been requested!" + i.commands.ravi.sed.success = "Sending sedation support if requested!" + i.commands.ravi.request = "Requesting sedation support!" + i.commands.ravi.error = "Raviente command not recognised!" + i.commands.ravi.noPlayers = "No one has joined the Great Slaying!" + i.commands.ravi.version = "This command is disabled outside of MHFZZ" - strings["ravienteBerserk"] = " is being held!" - strings["ravienteExtreme"] = " is being held!" - strings["ravienteExtremeLimited"] = " is being held!" - strings["ravienteBerserkSmall"] = " is being held!" + i.raviente.berserk = " is being held!" + i.raviente.extreme = " is being held!" + i.raviente.extremeLimited = " is being held!" + i.raviente.berserkSmall = " is being held!" - strings["guildInviteName"] = "Invitation!" - strings["guildInvite"] = "You have been invited to join\n「%s」\nDo you want to accept?" + i.guild.invite.title = "Invitation!" + i.guild.invite.body = "You have been invited to join\n「%s」\nDo you want to accept?" - strings["guildInviteSuccessName"] = "Success!" - strings["guildInviteSuccess"] = "You have successfully joined\n「%s」." + i.guild.invite.success.title = "Success!" + i.guild.invite.success.body = "You have successfully joined\n「%s」." - strings["guildInviteAcceptedName"] = "Accepted" - strings["guildInviteAccepted"] = "The recipient accepted your invitation to join\n「%s」." + i.guild.invite.accepted.title = "Accepted" + i.guild.invite.accepted.body = "The recipient accepted your invitation to join\n「%s」." - strings["guildInviteRejectName"] = "Rejected" - strings["guildInviteReject"] = "You rejected the invitation to join\n「%s」." + i.guild.invite.rejected.title = "Rejected" + i.guild.invite.rejected.body = "You rejected the invitation to join\n「%s」." - strings["guildInviteDeclinedName"] = "Declined" - strings["guildInviteDeclined"] = "The recipient declined your invitation to join\n「%s」." + i.guild.invite.declined.title = "Declined" + i.guild.invite.declined.body = "The recipient declined your invitation to join\n「%s」." } - return strings + return i } From e0615dcd0c76f46a4062aec25027457ecd5b774b Mon Sep 17 00:00:00 2001 From: wish Date: Wed, 3 Jan 2024 19:08:45 +1100 Subject: [PATCH 29/70] add support for operator accounts & bans --- patch-schema/op-accounts.sql | 12 ++++++++++++ server/channelserver/handlers_cast_binary.go | 20 ++++++++++---------- server/channelserver/sys_channel_server.go | 20 ++++++++++++++++++++ server/channelserver/sys_session.go | 9 +++++++++ server/signserver/dbutils.go | 9 +++++++++ 5 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 patch-schema/op-accounts.sql diff --git a/patch-schema/op-accounts.sql b/patch-schema/op-accounts.sql new file mode 100644 index 000000000..bdf5dccd8 --- /dev/null +++ b/patch-schema/op-accounts.sql @@ -0,0 +1,12 @@ +BEGIN; + +ALTER TABLE IF EXISTS public.users ADD COLUMN op boolean; + +CREATE TABLE public.bans +( + user_id integer NOT NULL, + expires timestamp with time zone, + PRIMARY KEY (user_id) +); + +END; \ No newline at end of file diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 043403216..016e1f70c 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -88,7 +88,7 @@ func parseChatCommand(s *Session, command string) { args := strings.Split(command[len(s.server.erupeConfig.CommandPrefix):], " ") switch args[0] { case commands["PSN"].Prefix: - if commands["PSN"].Enabled { + if commands["PSN"].Enabled || s.isOp() { if len(args) > 1 { var exists int s.server.db.QueryRow(`SELECT count(*) FROM users WHERE psn_id = $1`, args[1]).Scan(&exists) @@ -107,7 +107,7 @@ func parseChatCommand(s *Session, command string) { sendDisabledCommandMessage(s, commands["PSN"]) } case commands["Reload"].Prefix: - if commands["Reload"].Enabled { + if commands["Reload"].Enabled || s.isOp() { sendServerChatMessage(s, s.server.i18n.commands.reload) var temp mhfpacket.MHFPacket deleteNotif := byteframe.NewByteFrame() @@ -168,7 +168,7 @@ func parseChatCommand(s *Session, command string) { sendDisabledCommandMessage(s, commands["Reload"]) } case commands["KeyQuest"].Prefix: - if commands["KeyQuest"].Enabled { + if commands["KeyQuest"].Enabled || s.isOp() { if s.server.erupeConfig.RealClientMode < _config.G10 { sendServerChatMessage(s, s.server.i18n.commands.kqf.version) } else { @@ -191,7 +191,7 @@ func parseChatCommand(s *Session, command string) { sendDisabledCommandMessage(s, commands["KeyQuest"]) } case commands["Rights"].Prefix: - if commands["Rights"].Enabled { + if commands["Rights"].Enabled || s.isOp() { if len(args) > 1 { v, _ := strconv.Atoi(args[1]) _, err := s.server.db.Exec("UPDATE users u SET rights=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", v, s.charID) @@ -207,7 +207,7 @@ func parseChatCommand(s *Session, command string) { sendDisabledCommandMessage(s, commands["Rights"]) } case commands["Course"].Prefix: - if commands["Course"].Enabled { + if commands["Course"].Enabled || s.isOp() { if len(args) > 1 { for _, course := range mhfcourse.Courses() { for _, alias := range course.Aliases() { @@ -250,7 +250,7 @@ func parseChatCommand(s *Session, command string) { sendDisabledCommandMessage(s, commands["Course"]) } case commands["Raviente"].Prefix: - if commands["Raviente"].Enabled { + if commands["Raviente"].Enabled || s.isOp() { if len(args) > 1 { if s.server.getRaviSemaphore() != nil { switch args[1] { @@ -301,7 +301,7 @@ func parseChatCommand(s *Session, command string) { sendDisabledCommandMessage(s, commands["Raviente"]) } case commands["Teleport"].Prefix: - if commands["Teleport"].Enabled { + if commands["Teleport"].Enabled || s.isOp() { if len(args) > 2 { x, _ := strconv.ParseInt(args[1], 10, 16) y, _ := strconv.ParseInt(args[2], 10, 16) @@ -324,7 +324,7 @@ func parseChatCommand(s *Session, command string) { sendDisabledCommandMessage(s, commands["Teleport"]) } case commands["Discord"].Prefix: - if commands["Discord"].Enabled { + if commands["Discord"].Enabled || s.isOp() { var _token string err := s.server.db.QueryRow(`SELECT discord_token FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&_token) if err != nil { @@ -338,9 +338,9 @@ func parseChatCommand(s *Session, command string) { sendDisabledCommandMessage(s, commands["Discord"]) } case commands["Help"].Prefix: - if commands["Help"].Enabled { + if commands["Help"].Enabled || s.isOp() { for _, command := range commands { - if command.Enabled { + if command.Enabled || s.isOp() { sendServerChatMessage(s, fmt.Sprintf("%s%s: %s", s.server.erupeConfig.CommandPrefix, command.Prefix, command.Description)) } } diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 30d63241f..19bd04123 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -378,6 +378,26 @@ func (s *Server) FindSessionByCharID(charID uint32) *Session { return nil } +func (s *Server) DisconnectUser(uid uint32) { + var cid uint32 + var cids []uint32 + rows, _ := s.db.Query(`SELECT id FROM characters WHERE user_id=$1`, uid) + for rows.Next() { + rows.Scan(&cid) + cids = append(cids, cid) + } + for _, c := range s.Channels { + for _, session := range c.sessions { + for _, cid := range cids { + if session.charID == cid { + session.rawConn.Close() + break + } + } + } + } +} + func (s *Server) FindObjectByChar(charID uint32) *Object { s.stagesLock.RLock() defer s.stagesLock.RUnlock() diff --git a/server/channelserver/sys_session.go b/server/channelserver/sys_session.go index d49e5264a..a0b30adb3 100644 --- a/server/channelserver/sys_session.go +++ b/server/channelserver/sys_session.go @@ -309,3 +309,12 @@ func (s *Session) NextObjectID() uint32 { bf.Seek(0, 0) return bf.ReadUint32() } + +func (s *Session) isOp() bool { + var op bool + err := s.server.db.QueryRow(`SELECT op FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&op) + if err == nil && op { + return true + } + return false +} diff --git a/server/signserver/dbutils.go b/server/signserver/dbutils.go index f23d3bdcc..dadf3f074 100644 --- a/server/signserver/dbutils.go +++ b/server/signserver/dbutils.go @@ -244,6 +244,15 @@ func (s *Server) validateLogin(user string, pass string) (uint32, RespID) { return 0, SIGN_EABORT } else { if bcrypt.CompareHashAndPassword([]byte(passDB), []byte(pass)) == nil { + var bans int + err = s.db.QueryRow(`SELECT count(*) FROM bans WHERE user_id=$1 AND expires IS NULL`, uid).Scan(&bans) + if err == nil && bans > 0 { + return uid, SIGN_EELIMINATE + } + err = s.db.QueryRow(`SELECT count(*) FROM bans WHERE user_id=$1 AND expires > now()`, uid).Scan(&bans) + if err == nil && bans > 0 { + return uid, SIGN_ESUSPEND + } return uid, SIGN_SUCCESS } return 0, SIGN_EPASS From 2135c443d8b53dd54df5d7815824e320c4002922 Mon Sep 17 00:00:00 2001 From: wish Date: Wed, 3 Jan 2024 19:30:57 +1100 Subject: [PATCH 30/70] add ban chat command --- common/mhfcid/mhfcid.go | 54 +++++++++++++++++++ config.json | 5 ++ server/channelserver/handlers_cast_binary.go | 57 ++++++++++++++++++++ server/channelserver/sys_language.go | 22 ++++++++ 4 files changed, 138 insertions(+) create mode 100644 common/mhfcid/mhfcid.go diff --git a/common/mhfcid/mhfcid.go b/common/mhfcid/mhfcid.go new file mode 100644 index 000000000..8b951fd4e --- /dev/null +++ b/common/mhfcid/mhfcid.go @@ -0,0 +1,54 @@ +package mhfcid + +import ( + "math" +) + +// ConvertCID converts a MHF Character ID String to integer +// +// Banned characters: 0, I, O, S +func ConvertCID(ID string) (r uint32) { + if len(ID) != 6 { + return + } + + m := map[rune]uint32{ + '1': 0, + '2': 1, + '3': 2, + '4': 3, + '5': 4, + '6': 5, + '7': 6, + '8': 7, + '9': 8, + 'A': 9, + 'B': 10, + 'C': 11, + 'D': 12, + 'E': 13, + 'F': 14, + 'G': 15, + 'H': 16, + 'J': 17, + 'K': 18, + 'L': 19, + 'M': 20, + 'N': 21, + 'P': 22, + 'Q': 23, + 'R': 24, + 'T': 25, + 'U': 26, + 'V': 27, + 'W': 28, + 'X': 29, + 'Y': 30, + 'Z': 31, + } + + for i, c := range ID { + r += m[c] * uint32(math.Pow(32, float64(i))) + } + return +} diff --git a/config.json b/config.json index 15f51d9df..c0247b9d3 100644 --- a/config.json +++ b/config.json @@ -136,6 +136,11 @@ "Enabled": true, "Description": "Generate a token to link your Discord account", "Prefix": "discord" + }, { + "Name": "Ban", + "Enabled": false, + "Description": "Ban/Temp Ban a user", + "Prefix": "ban" } ], "Courses": [ diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 016e1f70c..771a7be39 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "encoding/hex" "erupe-ce/common/byteframe" + "erupe-ce/common/mhfcid" "erupe-ce/common/mhfcourse" "erupe-ce/common/token" "erupe-ce/config" @@ -87,6 +88,62 @@ func sendServerChatMessage(s *Session, message string) { func parseChatCommand(s *Session, command string) { args := strings.Split(command[len(s.server.erupeConfig.CommandPrefix):], " ") switch args[0] { + case commands["Ban"].Prefix: + if s.isOp() { + if len(args) > 1 { + var expiry time.Time + if len(args) > 2 { + var length int + var unit string + n, err := fmt.Sscanf(args[2], `%d%s`, &length, &unit) + if err == nil && n == 2 { + switch unit { + case "s", "second", "seconds": + expiry = time.Now().Add(time.Duration(length) * time.Second) + case "m", "mi", "minute", "minutes": + expiry = time.Now().Add(time.Duration(length) * time.Minute) + case "h", "hour", "hours": + expiry = time.Now().Add(time.Duration(length) * time.Hour) + case "d", "day", "days": + expiry = time.Now().Add(time.Duration(length) * time.Hour * 24) + case "mo", "month", "months": + expiry = time.Now().Add(time.Duration(length) * time.Hour * 24 * 30) + case "y", "year", "years": + expiry = time.Now().Add(time.Duration(length) * time.Hour * 24 * 365) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.ban.error) + return + } + } + cid := mhfcid.ConvertCID(args[1]) + if cid > 0 { + var uid uint32 + var uname string + err := s.server.db.QueryRow(`SELECT id, username FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, cid).Scan(&uid, &uname) + if err == nil { + if expiry.IsZero() { + s.server.db.Exec(`INSERT INTO bans VALUES ($1) + ON CONFLICT (user_id) DO UPDATE SET expires=NULL`, uid) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)) + } else { + s.server.db.Exec(`INSERT INTO bans VALUES ($1, $2) + ON CONFLICT (user_id) DO UPDATE SET expires=$2`, uid, expiry) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)+fmt.Sprintf(s.server.i18n.commands.ban.length, expiry.Format(time.DateTime))) + } + s.server.DisconnectUser(uid) + } else { + sendServerChatMessage(s, s.server.i18n.commands.ban.noUser) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.ban.invalid) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.ban.error) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.noOp) + } case commands["PSN"].Prefix: if commands["PSN"].Enabled || s.isOp() { if len(args) > 1 { diff --git a/server/channelserver/sys_language.go b/server/channelserver/sys_language.go index 5feb76444..eae96dc85 100644 --- a/server/channelserver/sys_language.go +++ b/server/channelserver/sys_language.go @@ -6,6 +6,7 @@ type i18n struct { reset string } commands struct { + noOp string disabled string reload string kqf struct { @@ -38,6 +39,13 @@ type i18n struct { discord struct { success string } + ban struct { + success string + noUser string + invalid string + error string + length string + } ravi struct { noCommand string start struct { @@ -95,6 +103,7 @@ func getLangStrings(s *Server) i18n { i.language = "日本語" i.cafe.reset = "%d/%dにリセット" + i.commands.noOp = "You don't have permission to use this command" i.commands.disabled = "%sのコマンドは無効です" i.commands.reload = "リロードします" i.commands.kqf.get = "現在のキークエストフラグ:%x" @@ -115,6 +124,12 @@ func getLangStrings(s *Server) i18n { i.commands.discord.success = "あなたのDiscordトークン:%s" + i.commands.ban.noUser = "Could not find user" + i.commands.ban.success = "Successfully banned %s" + i.commands.ban.invalid = "Invalid Character ID" + i.commands.ban.error = "Error in command. Format: %s [length]" + i.commands.ban.length = " until %s" + i.commands.ravi.noCommand = "ラヴィコマンドが指定されていません" i.commands.ravi.start.success = "大討伐を開始します" i.commands.ravi.start.error = "大討伐は既に開催されています" @@ -150,6 +165,7 @@ func getLangStrings(s *Server) i18n { i.language = "English" i.cafe.reset = "Resets on %d/%d" + i.commands.noOp = "You don't have permission to use this command" i.commands.disabled = "%s command is disabled" i.commands.reload = "Reloading players..." i.commands.kqf.get = "KQF: %x" @@ -170,6 +186,12 @@ func getLangStrings(s *Server) i18n { i.commands.discord.success = "Your Discord token: %s" + i.commands.ban.noUser = "Could not find user" + i.commands.ban.success = "Successfully banned %s" + i.commands.ban.invalid = "Invalid Character ID" + i.commands.ban.error = "Error in command. Format: %s [length]" + i.commands.ban.length = " until %s" + i.commands.ravi.noCommand = "No Raviente command specified!" i.commands.ravi.start.success = "The Great Slaying will begin in a moment" i.commands.ravi.start.error = "The Great Slaying has already begun!" From f73bdd7445ca2a5e210d81a1ee2c21a40dcfa052 Mon Sep 17 00:00:00 2001 From: wish Date: Fri, 5 Jan 2024 02:39:25 +1100 Subject: [PATCH 31/70] rewrite CastBinary payload handling --- network/binpacket/msg_bin_chat.go | 3 +- server/channelserver/handlers_cast_binary.go | 40 +++++++++----------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/network/binpacket/msg_bin_chat.go b/network/binpacket/msg_bin_chat.go index 44bd587ca..b39a43795 100644 --- a/network/binpacket/msg_bin_chat.go +++ b/network/binpacket/msg_bin_chat.go @@ -11,7 +11,8 @@ type ChatType uint8 // Chat types const ( - ChatTypeLocal ChatType = 1 + ChatTypeWorld ChatType = 0 + ChatTypeStage = 1 ChatTypeGuild = 2 ChatTypeAlliance = 3 ChatTypeParty = 4 diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 043403216..0f6b736e7 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -377,24 +377,20 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { // Parse out the real casted binary payload var msgBinTargeted *binpacket.MsgBinTargeted - var authorLen, msgLen uint16 - var msg []byte - - isDiceCommand := false + var message, author string + var returnToSender bool if pkt.MessageType == BinaryMessageTypeChat { tmp.SetLE() - tmp.Seek(int64(0), 0) - _ = tmp.ReadUint32() - authorLen = tmp.ReadUint16() - msgLen = tmp.ReadUint16() - msg = tmp.ReadNullTerminatedBytes() + tmp.Seek(8, 0) + message = string(tmp.ReadNullTerminatedBytes()) + author = string(tmp.ReadNullTerminatedBytes()) } // Customise payload realPayload := pkt.RawDataPayload if pkt.BroadcastType == BroadcastTypeTargeted { tmp.SetBE() - tmp.Seek(int64(0), 0) + tmp.Seek(0, 0) msgBinTargeted = &binpacket.MsgBinTargeted{} err := msgBinTargeted.Parse(tmp) if err != nil { @@ -403,18 +399,18 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { } realPayload = msgBinTargeted.RawDataPayload } else if pkt.MessageType == BinaryMessageTypeChat { - if msgLen == 6 && string(msg) == "@dice" { - isDiceCommand = true - roll := byteframe.NewByteFrame() - roll.WriteInt16(1) // Unk - roll.SetLE() - roll.WriteUint16(4) // Unk - roll.WriteUint16(authorLen) - dice := fmt.Sprintf("%d", token.RNG().Intn(100)+1) - roll.WriteUint16(uint16(len(dice) + 1)) - roll.WriteNullTerminatedBytes([]byte(dice)) - roll.WriteNullTerminatedBytes(tmp.ReadNullTerminatedBytes()) - realPayload = roll.Data() + if message == "@dice" { + returnToSender = true + m := binpacket.MsgBinChat{ + Type: BinaryMessageTypeChat, + Flags: 4, + Message: fmt.Sprintf(`%d`, token.RNG().Intn(100)+1), + SenderName: author, + } + bf := byteframe.NewByteFrame() + bf.SetLE() + m.Build(bf) + realPayload = bf.Data() } else { bf := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload) bf.SetLE() From c8e21387c0f8b5b38027e180e6b60d1d4fb8c595 Mon Sep 17 00:00:00 2001 From: wish Date: Sat, 6 Jan 2024 17:43:25 +1100 Subject: [PATCH 32/70] rewrite CastBinary payload handling --- server/channelserver/handlers_cast_binary.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 0f6b736e7..97fc34c95 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -439,8 +439,8 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { case BroadcastTypeWorld: s.server.WorldcastMHF(resp, s, nil) case BroadcastTypeStage: - if isDiceCommand { - s.stage.BroadcastMHF(resp, nil) // send dice result back to caller + if returnToSender { + s.stage.BroadcastMHF(resp, nil) } else { s.stage.BroadcastMHF(resp, s) } From ba7321b535d01e4f95d3640a4767c7a30906588b Mon Sep 17 00:00:00 2001 From: wish Date: Wed, 10 Jan 2024 19:29:51 +1100 Subject: [PATCH 33/70] fix stringstack & MoveStage error --- common/stringstack/stringstack.go | 17 +---------------- server/channelserver/handlers_stage.go | 8 -------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/common/stringstack/stringstack.go b/common/stringstack/stringstack.go index 9f6b646ae..a7ddef31e 100644 --- a/common/stringstack/stringstack.go +++ b/common/stringstack/stringstack.go @@ -6,8 +6,7 @@ import ( // StringStack is a basic LIFO "stack" for storing strings. type StringStack struct { - Locked bool - stack []string + stack []string } // New creates a new instance of StringStack @@ -20,20 +19,6 @@ func (s *StringStack) Set(v string) { s.stack = []string{v} } -// Lock freezes the StringStack -func (s *StringStack) Lock() { - if !s.Locked { - s.Locked = true - } -} - -// Unlock unfreezes the StringStack -func (s *StringStack) Unlock() { - if s.Locked { - s.Locked = false - } -} - // Push pushes a string onto the stack. func (s *StringStack) Push(v string) { s.stack = append(s.stack, v) diff --git a/server/channelserver/handlers_stage.go b/server/channelserver/handlers_stage.go index a9de55b28..bc0b3689e 100644 --- a/server/channelserver/handlers_stage.go +++ b/server/channelserver/handlers_stage.go @@ -157,7 +157,6 @@ func handleMsgSysEnterStage(s *Session, p mhfpacket.MHFPacket) { s.stage.reservedClientSlots[s.charID] = false s.stage.Unlock() s.stageMoveStack.Push(s.stage.id) - s.stageMoveStack.Lock() } if s.reservationStage != nil { @@ -171,7 +170,6 @@ func handleMsgSysBackStage(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysBackStage) // Transfer back to the saved stage ID before the previous move or enter. - s.stageMoveStack.Unlock() backStage, err := s.stageMoveStack.Pop() if backStage == "" || err != nil { backStage = "sl1Ns200p0a0u0" @@ -190,12 +188,6 @@ func handleMsgSysBackStage(s *Session, p mhfpacket.MHFPacket) { func handleMsgSysMoveStage(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysMoveStage) - - // Set a new move stack from the given stage ID - if !s.stageMoveStack.Locked { - s.stageMoveStack.Set(pkt.StageID) - } - doStageTransfer(s, pkt.AckHandle, pkt.StageID) } From 4ccb3af5acec2ee79eab4b139e0733d7bf64d172 Mon Sep 17 00:00:00 2001 From: wish Date: Thu, 11 Jan 2024 23:00:44 +1100 Subject: [PATCH 34/70] simplify cryptography --- network/crypt_conn.go | 13 +++----- network/crypto/crypto.go | 60 +++++++++++++---------------------- network/crypto/crypto_test.go | 4 +-- 3 files changed, 29 insertions(+), 48 deletions(-) diff --git a/network/crypt_conn.go b/network/crypt_conn.go index 2fc302b18..de9181855 100644 --- a/network/crypt_conn.go +++ b/network/crypt_conn.go @@ -4,11 +4,10 @@ import ( "encoding/hex" "errors" _config "erupe-ce/config" + "erupe-ce/network/crypto" "fmt" "io" "net" - - "erupe-ce/network/crypto" ) // CryptConn represents a MHF encrypted two-way connection, @@ -67,7 +66,7 @@ func (cc *CryptConn) ReadPacket() ([]byte, error) { cc.readKeyRot = uint32(cph.KeyRotDelta) * (cc.readKeyRot + 1) } - out, combinedCheck, check0, check1, check2 := crypto.Decrypt(encryptedPacketBody, cc.readKeyRot, nil) + out, combinedCheck, check0, check1, check2 := crypto.Crypto(encryptedPacketBody, cc.readKeyRot, false, nil) if cph.Check0 != check0 || cph.Check1 != check1 || cph.Check2 != check2 { fmt.Printf("got c0 %X, c1 %X, c2 %X\n", check0, check1, check2) fmt.Printf("want c0 %X, c1 %X, c2 %X\n", cph.Check0, cph.Check1, cph.Check2) @@ -77,7 +76,7 @@ func (cc *CryptConn) ReadPacket() ([]byte, error) { // Attempt to bruteforce it. fmt.Println("Crypto out of sync? Attempting bruteforce") for key := byte(0); key < 255; key++ { - out, combinedCheck, check0, check1, check2 = crypto.Decrypt(encryptedPacketBody, 0, &key) + out, combinedCheck, check0, check1, check2 = crypto.Crypto(encryptedPacketBody, 0, false, &key) //fmt.Printf("Key: 0x%X\n%s\n", key, hex.Dump(out)) if cph.Check0 == check0 && cph.Check1 == check1 && cph.Check2 == check2 { fmt.Printf("Bruceforce successful, override key: 0x%X\n", key) @@ -106,7 +105,7 @@ func (cc *CryptConn) SendPacket(data []byte) error { } // Encrypt the data - encData, combinedCheck, check0, check1, check2 := crypto.Encrypt(data, cc.sendKeyRot, nil) + encData, combinedCheck, check0, check1, check2 := crypto.Crypto(data, cc.sendKeyRot, true, nil) header := &CryptPacketHeader{} header.Pf0 = byte(((uint(len(encData)) >> 12) & 0xF3) | 3) @@ -123,9 +122,7 @@ func (cc *CryptConn) SendPacket(data []byte) error { return err } - cc.conn.Write(headerBytes) - cc.conn.Write(encData) - + cc.conn.Write(append(headerBytes, encData...)) cc.sentPackets++ cc.prevSendPacketCombinedCheck = combinedCheck diff --git a/network/crypto/crypto.go b/network/crypto/crypto.go index 87746fa90..59e631786 100644 --- a/network/crypto/crypto.go +++ b/network/crypto/crypto.go @@ -6,46 +6,30 @@ var ( _sharedCryptKey = []byte{0xDD, 0xA8, 0x5F, 0x1E, 0x57, 0xAF, 0xC0, 0xCC, 0x43, 0x35, 0x8F, 0xBB, 0x6F, 0xE6, 0xA1, 0xD6, 0x60, 0xB9, 0x1A, 0xAE, 0x20, 0x49, 0x24, 0x81, 0x21, 0xFE, 0x86, 0x2B, 0x98, 0xB7, 0xB3, 0xD2, 0x91, 0x01, 0x3A, 0x4C, 0x65, 0x92, 0x1C, 0xF4, 0xBE, 0xDD, 0xD9, 0x08, 0xE6, 0x81, 0x98, 0x1B, 0x8D, 0x60, 0xF3, 0x6F, 0xA1, 0x47, 0x24, 0xF1, 0x53, 0x45, 0xC8, 0x7B, 0x88, 0x80, 0x4E, 0x36, 0xC3, 0x0D, 0xC9, 0xD6, 0x8B, 0x08, 0x19, 0x0B, 0xA5, 0xC1, 0x11, 0x4C, 0x60, 0xF8, 0x5D, 0xFC, 0x15, 0x68, 0x7E, 0x32, 0xC0, 0x50, 0xAB, 0x64, 0x1F, 0x8A, 0xD4, 0x08, 0x39, 0x7F, 0xC2, 0xFB, 0xBA, 0x6C, 0xF0, 0xE6, 0xB0, 0x31, 0x10, 0xC1, 0xBF, 0x75, 0x43, 0xBB, 0x18, 0x04, 0x0D, 0xD1, 0x97, 0xF7, 0x23, 0x21, 0x83, 0x8B, 0xCA, 0x25, 0x2B, 0xA3, 0x03, 0x13, 0xEA, 0xAE, 0xFE, 0xF0, 0xEB, 0xFD, 0x85, 0x57, 0x53, 0x65, 0x41, 0x2A, 0x40, 0x99, 0xC0, 0x94, 0x65, 0x7E, 0x7C, 0x93, 0x82, 0xB0, 0xB3, 0xE5, 0xC0, 0x21, 0x09, 0x84, 0xD5, 0xEF, 0x9F, 0xD1, 0x7E, 0xDC, 0x4D, 0xF5, 0x7E, 0xCD, 0x45, 0x3C, 0x7F, 0xF5, 0x59, 0x98, 0xC6, 0x55, 0xFC, 0x9F, 0xA3, 0xB7, 0x74, 0xEE, 0x31, 0x98, 0xE6, 0xB7, 0xBE, 0x26, 0xF4, 0x3C, 0x76, 0xF1, 0x23, 0x7E, 0x02, 0x4E, 0x3C, 0xD1, 0xC7, 0x28, 0x23, 0x73, 0xC4, 0xD9, 0x5E, 0x0D, 0xA1, 0x80, 0xA5, 0xAA, 0x26, 0x0A, 0xA3, 0x44, 0x82, 0x74, 0xE6, 0x3C, 0x44, 0x27, 0x51, 0x0D, 0x5F, 0xC7, 0x9C, 0xD6, 0x63, 0x67, 0xA5, 0x27, 0x97, 0x38, 0xFB, 0x2D, 0xD3, 0xD6, 0x60, 0x25, 0x83, 0x4D, 0x37, 0x5B, 0x40, 0x59, 0x11, 0x77, 0x51, 0x11, 0x14, 0x18, 0x07, 0x63, 0xB1, 0x34, 0x3D, 0xB8, 0x60, 0x13, 0xC2, 0xE8, 0x13, 0x82} ) -// Encrypt encrypts the given data using MHF's custom encryption+checksum method. -// if a overrideByteKey value is supplied (!= nil), it will be used to override the derived/truncated key byte. -func Encrypt(data []byte, key uint32, overrideByteKey *byte) (outputData []byte, combinedCheck uint16, check0 uint16, check1 uint16, check2 uint16) { - return _generalCrypt(data, key, 0, overrideByteKey) -} - -// Decrypt decrypts the given data using MHF's custom decryption+checksum method. -// if a overrideByteKey value is supplied (!= nil), it will be used to override the derived/truncated key byte. -func Decrypt(data []byte, key uint32, overrideByteKey *byte) (outputData []byte, combinedCheck uint16, check0 uint16, check1 uint16, check2 uint16) { - return _generalCrypt(data, key, 1, overrideByteKey) -} - -// _generalCrypt is a generalized MHF crypto function that can perform both encryption and decryption, +// Crypto is a generalized MHF crypto function that can perform both encryption and decryption, // these two crypto operations are combined into a single function because they shared most of their logic. -// encrypt: cryptType==0 -// decrypt: cryptType==1 -func _generalCrypt(data []byte, rotKey uint32, cryptType int, overrideByteKey *byte) ([]byte, uint16, uint16, uint16, uint16) { +func Crypto(data []byte, rotKey uint32, encrypt bool, overrideByteKey *byte) ([]byte, uint16, uint16, uint16, uint16) { cryptKeyTruncByte := byte(((rotKey >> 1) % 999983) & 0xFF) if overrideByteKey != nil { cryptKeyTruncByte = *overrideByteKey } - derivedCryptKey := int32((uint32(len(data)) * (uint32(cryptKeyTruncByte) + 1)) & 0xFFFFFFFF) + derivedCryptKey := (uint32(len(data)) * (uint32(cryptKeyTruncByte) + 1)) & 0xFFFFFFFF sharedBufIdx := byte(1) - accumulator0 := uint32(0) - accumulator1 := uint32(0) - accumulator2 := uint32(0) + var accumulator0, accumulator1, accumulator2 uint32 var outputData []byte - if cryptType == 0 { + if encrypt { for i := 0; i < len(data); i++ { // Do the encryption for this iteration - encKeyIdx := int32(((uint32(derivedCryptKey) >> 10) ^ uint32(data[i])) & 0xFF) - derivedCryptKey = (0x4FD * (derivedCryptKey + 1)) + encKeyIdx := ((derivedCryptKey >> 10) ^ uint32(data[i])) & 0xFF + derivedCryptKey = 1277*derivedCryptKey + 1277 encKeyByte := _encryptKey[encKeyIdx] // Update the checksum accumulators. - accumulator2 = uint32((accumulator2 + (uint32(sharedBufIdx) * uint32(data[i]))) & 0xFFFFFFFF) - accumulator1 = uint32((accumulator1 + uint32(encKeyIdx)) & 0xFFFFFFFF) - accumulator0 = uint32((accumulator0 + (uint32(encKeyByte)<<(i&7))&0xFFFFFFFF) & 0xFFFFFFFF) + accumulator2 = accumulator2 + (uint32(sharedBufIdx) * uint32(data[i])) + accumulator1 = accumulator1 + encKeyIdx + accumulator0 = accumulator0 + uint32(encKeyByte)<<(i&7) // Append the output. outputData = append(outputData, _sharedCryptKey[sharedBufIdx]^encKeyByte) @@ -53,32 +37,32 @@ func _generalCrypt(data []byte, rotKey uint32, cryptType int, overrideByteKey *b // Update the sharedBufIdx for the next iteration. sharedBufIdx = data[i] } - - } else if cryptType == 1 { + } else { for i := 0; i < len(data); i++ { // Do the decryption for this iteration oldSharedBufIdx := sharedBufIdx tIdx := data[i] ^ _sharedCryptKey[sharedBufIdx] decKeyByte := _decryptKey[tIdx] - sharedBufIdx = byte(((uint32(derivedCryptKey) >> 10) ^ uint32(decKeyByte)) & 0xFF) + sharedBufIdx = byte((derivedCryptKey >> 10) ^ uint32(decKeyByte)) // Update the checksum accumulators. - accumulator0 = (accumulator0 + ((uint32(tIdx) << (i & 7)) & 0xFFFFFFFF)) - accumulator1 = (accumulator1 + uint32(decKeyByte)) & 0xFFFFFFFF - accumulator2 = (accumulator2 + ((uint32(oldSharedBufIdx) * uint32(sharedBufIdx)) & 0xFFFFFFFF)) & 0xFFFFFFFF + accumulator0 = accumulator0 + uint32(tIdx)<<(i&7) + accumulator1 = accumulator1 + uint32(decKeyByte) + accumulator2 = accumulator2 + uint32(oldSharedBufIdx)*uint32(sharedBufIdx) // Append the output. outputData = append(outputData, sharedBufIdx) // Update the key pos for next iteration. - derivedCryptKey = (0x4FD * (derivedCryptKey + 1)) + derivedCryptKey = 1277*derivedCryptKey + 1277 } } - combinedCheck := uint16((accumulator1 + (accumulator0 >> 1) + (accumulator2 >> 2)) & 0xFFFF) - check0 := uint16((accumulator0 ^ ((accumulator0 & 0xFFFF0000) >> 16)) & 0xFFFF) - check1 := uint16((accumulator1 ^ ((accumulator1 & 0xFFFF0000) >> 16)) & 0xFFFF) - check2 := uint16((accumulator2 ^ ((accumulator2 & 0xFFFF0000) >> 16)) & 0xFFFF) + var check [4]uint16 + check[0] = uint16(accumulator1 + (accumulator0 >> 1) + (accumulator2 >> 2)) + check[1] = uint16(accumulator0 ^ ((accumulator0 & 0xFFFF0000) >> 16)) + check[2] = uint16(accumulator1 ^ ((accumulator1 & 0xFFFF0000) >> 16)) + check[3] = uint16(accumulator2 ^ ((accumulator2 & 0xFFFF0000) >> 16)) - return outputData, combinedCheck, check0, check1, check2 + return outputData, check[0], check[1], check[2], check[3] } diff --git a/network/crypto/crypto_test.go b/network/crypto/crypto_test.go index 32ff7ee7c..5093e429f 100644 --- a/network/crypto/crypto_test.go +++ b/network/crypto/crypto_test.go @@ -65,7 +65,7 @@ func TestEncrypt(t *testing.T) { for k, tt := range tests { testname := fmt.Sprintf("encrypt_test_%d", k) t.Run(testname, func(t *testing.T) { - out, cc, c0, c1, c2 := Encrypt(tt.decryptedData, tt.key, nil) + out, cc, c0, c1, c2 := Crypto(tt.decryptedData, tt.key, true, nil) if cc != tt.ecc { t.Errorf("got cc 0x%X, want 0x%X", cc, tt.ecc) } else if c0 != tt.ec0 { @@ -86,7 +86,7 @@ func TestDecrypt(t *testing.T) { for k, tt := range tests { testname := fmt.Sprintf("decrypt_test_%d", k) t.Run(testname, func(t *testing.T) { - out, cc, c0, c1, c2 := Decrypt(tt.encryptedData, tt.key, nil) + out, cc, c0, c1, c2 := Crypto(tt.decryptedData, tt.key, false, nil) if cc != tt.ecc { t.Errorf("got cc 0x%X, want 0x%X", cc, tt.ecc) } else if c0 != tt.ec0 { From a7bf38388c899b7413134917270f1ddaf53c5562 Mon Sep 17 00:00:00 2001 From: wish Date: Thu, 11 Jan 2024 23:01:32 +1100 Subject: [PATCH 35/70] fix package collision --- server/signserver/dbutils.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/signserver/dbutils.go b/server/signserver/dbutils.go index f23d3bdcc..c8e140ce6 100644 --- a/server/signserver/dbutils.go +++ b/server/signserver/dbutils.go @@ -198,17 +198,17 @@ func (s *Server) checkToken(uid uint32) (bool, error) { } func (s *Server) registerUidToken(uid uint32) (uint32, string, error) { - token := token.Generate(16) + _token := token.Generate(16) var tid uint32 - err := s.db.QueryRow(`INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2) RETURNING id`, uid, token).Scan(&tid) - return tid, token, err + err := s.db.QueryRow(`INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2) RETURNING id`, uid, _token).Scan(&tid) + return tid, _token, err } func (s *Server) registerPsnToken(psn string) (uint32, string, error) { - token := token.Generate(16) + _token := token.Generate(16) var tid uint32 - err := s.db.QueryRow(`INSERT INTO sign_sessions (psn_id, token) VALUES ($1, $2) RETURNING id`, psn, token).Scan(&tid) - return tid, token, err + err := s.db.QueryRow(`INSERT INTO sign_sessions (psn_id, token) VALUES ($1, $2) RETURNING id`, psn, _token).Scan(&tid) + return tid, _token, err } func (s *Server) validateToken(token string, tokenID uint32) bool { From 26854760223fbcc1c10fc459b8d108a277f1cd29 Mon Sep 17 00:00:00 2001 From: wish Date: Thu, 11 Jan 2024 23:01:53 +1100 Subject: [PATCH 36/70] check against unwrapped error --- server/signserver/dbutils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/signserver/dbutils.go b/server/signserver/dbutils.go index c8e140ce6..96503117c 100644 --- a/server/signserver/dbutils.go +++ b/server/signserver/dbutils.go @@ -229,7 +229,7 @@ func (s *Server) validateLogin(user string, pass string) (uint32, RespID) { var passDB string err := s.db.QueryRow(`SELECT id, password FROM users WHERE username = $1`, user).Scan(&uid, &passDB) if err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { s.logger.Info("User not found", zap.String("User", user)) if s.erupeConfig.AutoCreateAccount { uid, err = s.registerDBAccount(user, pass) From af29ee637e1b97837cd78fb5a168cdf95d9f79be Mon Sep 17 00:00:00 2001 From: wish Date: Thu, 11 Jan 2024 23:03:53 +1100 Subject: [PATCH 37/70] minor session optimisations --- server/channelserver/sys_session.go | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/server/channelserver/sys_session.go b/server/channelserver/sys_session.go index d49e5264a..6c958b155 100644 --- a/server/channelserver/sys_session.go +++ b/server/channelserver/sys_session.go @@ -86,13 +86,11 @@ func NewSession(server *Server, conn net.Conn) *Session { // Start starts the session packet send and recv loop(s). func (s *Session) Start() { - go func() { - s.logger.Debug("New connection", zap.String("RemoteAddr", s.rawConn.RemoteAddr().String())) - // Unlike the sign and entrance server, - // the client DOES NOT initalize the channel connection with 8 NULL bytes. - go s.sendLoop() - s.recvLoop() - }() + s.logger.Debug("New connection", zap.String("RemoteAddr", s.rawConn.RemoteAddr().String())) + // Unlike the sign and entrance server, + // the client DOES NOT initalize the channel connection with 8 NULL bytes. + go s.sendLoop() + go s.recvLoop() } // QueueSend queues a packet (raw []byte) to be sent. @@ -150,16 +148,19 @@ func (s *Session) QueueAck(ackHandle uint32, data []byte) { } func (s *Session) sendLoop() { + var pkt packet for { if s.closed { return } - pkt := <-s.sendPackets - err := s.cryptConn.SendPacket(append(pkt.data, []byte{0x00, 0x10}...)) - if err != nil { - s.logger.Warn("Failed to send packet") + for len(s.sendPackets) > 0 { + pkt = <-s.sendPackets + err := s.cryptConn.SendPacket(append(pkt.data, []byte{0x00, 0x10}...)) + if err != nil { + s.logger.Warn("Failed to send packet") + } } - time.Sleep(10 * time.Millisecond) + time.Sleep(5 * time.Millisecond) } } @@ -178,14 +179,13 @@ func (s *Session) recvLoop() { s.logger.Info(fmt.Sprintf("[%s] Disconnected", s.Name)) logoutPlayer(s) return - } - if err != nil { + } else if err != nil { s.logger.Warn("Error on ReadPacket, exiting recv loop", zap.Error(err)) logoutPlayer(s) return } s.handlePacketGroup(pkt) - time.Sleep(10 * time.Millisecond) + time.Sleep(5 * time.Millisecond) } } From 1a6a9da308881d4242ef739a0ff83aa779ec705b Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 14 Jan 2024 14:09:51 +1100 Subject: [PATCH 38/70] add logging to EnumerateQuest --- server/channelserver/handlers_quest.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index 138199ea4..699830ce4 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -249,6 +249,7 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { err = rows.Scan(&id, &maxPlayers, &questType, &questId, &mark, &flags, &startTime, &activeDays, &inactiveDays) if err != nil { + s.logger.Error("Failed to scan event quest row", zap.Error(err)) continue } @@ -283,9 +284,11 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { data, err := makeEventQuest(s, rows) if err != nil { + s.logger.Error("Failed to make event quest", zap.Error(err)) continue } else { if len(data) > 896 || len(data) < 352 { + s.logger.Error("Invalid quest data length", zap.Int("len", len(data))) continue } else { totalCount++ From 6b54e40cc6891257f020b0e598cd814c38b1a184 Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 14 Jan 2024 14:13:11 +1100 Subject: [PATCH 39/70] add tuneValue comments --- server/channelserver/handlers_quest.go | 54 +++++++++++++------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index 699830ce4..b403f134d 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -322,40 +322,40 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { {ID: 67, Value: 1}, {ID: 80, Value: 1}, {ID: 94, Value: 1}, - {ID: 1010, Value: 300}, - {ID: 1011, Value: 300}, - {ID: 1012, Value: 300}, - {ID: 1013, Value: 300}, - {ID: 1014, Value: 200}, - {ID: 1015, Value: 200}, - {ID: 1021, Value: 400}, + {ID: 1010, Value: 300}, // get_hrp_rate_netcafe + {ID: 1011, Value: 300}, // get_zeny_rate_netcafe + {ID: 1012, Value: 300}, // get_hrp_rate_ncource + {ID: 1013, Value: 300}, // get_zeny_rate_ncource + {ID: 1014, Value: 200}, // get_hrp_rate_premium + {ID: 1015, Value: 200}, // get_zeny_rate_premium + {ID: 1021, Value: 400}, // get_gcp_rate_assist {ID: 1023, Value: 8}, - {ID: 1024, Value: 150}, + {ID: 1024, Value: 150}, // get_hrp_rate_ptbonus {ID: 1025, Value: 1}, {ID: 1026, Value: 999}, // get_grank_cap - {ID: 1027, Value: 100}, - {ID: 1028, Value: 100}, - {ID: 1030, Value: 8}, - {ID: 1031, Value: 100}, + {ID: 1027, Value: 100}, // get_exchange_rate_festa + {ID: 1028, Value: 100}, // get_exchange_rate_cafe + {ID: 1030, Value: 8}, // get_gquest_cap + {ID: 1031, Value: 100}, // get_exchange_rate_guild (GCP) {ID: 1032, Value: 0}, // isValid_partner {ID: 1044, Value: 200}, // get_rate_tload_time_out {ID: 1045, Value: 0}, // get_rate_tower_treasure_preset - {ID: 1046, Value: 99}, - {ID: 1048, Value: 0}, // get_rate_tower_log_disable - {ID: 1049, Value: 10}, // get_rate_tower_gem_max - {ID: 1050, Value: 1}, // get_rate_tower_gem_set + {ID: 1046, Value: 99}, // get_hunter_life_cap + {ID: 1048, Value: 0}, // get_rate_tower_log_disable + {ID: 1049, Value: 10}, // get_rate_tower_gem_max + {ID: 1050, Value: 1}, // get_rate_tower_gem_set {ID: 1051, Value: 200}, - {ID: 1052, Value: 200}, - {ID: 1063, Value: 50000}, - {ID: 1064, Value: 50000}, - {ID: 1065, Value: 25000}, - {ID: 1066, Value: 25000}, - {ID: 1067, Value: 90}, // get_lobby_member_upper_for_making_room Lv1? - {ID: 1068, Value: 80}, // get_lobby_member_upper_for_making_room Lv2? - {ID: 1069, Value: 70}, // get_lobby_member_upper_for_making_room Lv3? - {ID: 1072, Value: 300}, // get_rate_premium_ravi_tama - {ID: 1073, Value: 300}, // get_rate_premium_ravi_ax_tama - {ID: 1074, Value: 300}, // get_rate_premium_ravi_g_tama + {ID: 1052, Value: 200}, // get_trp_rate_premium + {ID: 1063, Value: 50000}, // get_nboost_quest_point_from_hrank + {ID: 1064, Value: 50000}, // get_nboost_quest_point_from_srank + {ID: 1065, Value: 25000}, // get_nboost_quest_point_from_grank + {ID: 1066, Value: 25000}, // get_nboost_quest_point_from_gsrank + {ID: 1067, Value: 90}, // get_lobby_member_upper_for_making_room Lv1? + {ID: 1068, Value: 80}, // get_lobby_member_upper_for_making_room Lv2? + {ID: 1069, Value: 70}, // get_lobby_member_upper_for_making_room Lv3? + {ID: 1072, Value: 300}, // get_rate_premium_ravi_tama + {ID: 1073, Value: 300}, // get_rate_premium_ravi_ax_tama + {ID: 1074, Value: 300}, // get_rate_premium_ravi_g_tama {ID: 1078, Value: 0}, {ID: 1079, Value: 1}, {ID: 1080, Value: 1}, From 59eafbe3d5caca3a2686d144a6648aed72ce377d Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 16 Jan 2024 19:50:41 +1100 Subject: [PATCH 40/70] fix EnumerateQuest rotation --- server/channelserver/handlers_quest.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index b403f134d..084fab95f 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -163,7 +163,7 @@ func makeEventQuest(s *Session, rows *sql.Rows) ([]byte, error) { data := loadQuestFile(s, questId) if data == nil { - return nil, fmt.Errorf("failed to load quest file") + return nil, fmt.Errorf(fmt.Sprintf("failed to load quest file (%d)", questId)) } bf := byteframe.NewByteFrame() @@ -278,7 +278,7 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { // Check if the quest is currently active if currentTime.Before(startTime) || currentTime.After(startTime.Add(time.Duration(activeDays)*24*time.Hour)) { - break + continue } } From 4a7f7b80410d577b0278a4caf2d012c7255b7857 Mon Sep 17 00:00:00 2001 From: stratic-dev Date: Mon, 22 Jan 2024 20:06:40 +0000 Subject: [PATCH 41/70] Added docker and docker-compose --- Dockerfile | 14 +++++++++++ docker/docker-compose.yml | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 Dockerfile create mode 100644 docker/docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..37015b19d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.21-alpine3.19 + +ENV GO111MODULE=on + +WORKDIR /app/erupe + +COPY go.mod . +COPY go.sum . + +RUN go mod download + +COPY . . + +CMD [ "go", "run", "." ] \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 000000000..e7ff0e960 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,49 @@ +version: "3.9" +# 1. docker-compose up db pgadmin +# 2. Use pgadmin to restore db and also apply patch-schema +# 3. Configure the config.json example. in docker you can point to the service name for the database i.e db +# 4. In seperate terminal docker-compose up server +# 5. If all went well happy hunting! +services: + db: + image: postgres + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=erupe + ports: + - "5432:5432" + pgadmin: + image: dpage/pgadmin4 + restart: always + environment: + PGADMIN_DEFAULT_EMAIL: test@test.com + PGADMIN_DEFAULT_PASSWORD: root + ports: + - "5050:80" + depends_on: + - db + server: + depends_on: + - db + # If using prebuilt container change paths and config + build: + context: ../ + volumes: + - ../config.json:/app/erupe/config.json + - ../bin:/app/erupe/bin + - ../savedata:/app/erupe/savedata + ports: + - "53312:53312" #Sign V1 + - "8080:8080" #Sign V2 + - "53310:53310" #Entrance + # Channels + - "54001:54001" + - "54002:54002" + - "54003:54003" + - "54004:54004" + - "54005:54005" + - "54006:54006" + - "54007:54007" + - "54008:54008" + From 76e62c6af29f103a0edc8b2a66b5c1d5c51f7605 Mon Sep 17 00:00:00 2001 From: stratic-dev Date: Mon, 22 Jan 2024 22:35:45 +0000 Subject: [PATCH 42/70] Added readme and comments to docker-compose --- docker/README.md | 64 +++++++++++++++++++++++++++++++++++++++ docker/docker-compose.yml | 6 ++-- 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 docker/README.md diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..11a5cb04a --- /dev/null +++ b/docker/README.md @@ -0,0 +1,64 @@ +# Docker for erupe + +## Building the container +Run the following from the route of the soruce folder. In this example we give it the tag of dev to seperate it from any other container verions. +```bash +docker build . -t erupe:dev +``` +## Running the container in isolation +This is just running the container. You can do volume mounts into the container for the `config.json` to tell it to communicate to a database. You will need to do this also for other folders such as `bin` and `savedata` +```bash +docker run erupe:dev +``` + +## Docker compose +Docker compose allows you to run multiple containers at once. The docker compose in this folder has 3 things set up. +- postgres +- pg admin (Admin interface to make db changes) +- erupe + +Before we get started you should make sure the database info matches whats in the docker compose file for the environment variables `POSTGRES_PASSWORD`,`POSTGRES_USER` and `POSTGRES_DB`. You can set the host to be the service name `db`. + +Here is a example of what you would put in the config.json if you was to leave the defaults. It is strongly recommended to change the password. +```txt +"Database": { + "Host": "db", + "Port": 5432, + "User": "postgres", + "Password": "password", + "Database": "erupe" + }, +``` + +### Running up the database for the first time +First we need to set up the database. This requires the schema and the patch schemas to be applied. This can be done by runnnig up both the db and pgadmin. + +1. Pull the remote images and build a container image for erupe +```bash +docker-compose pull +docker-compose build +``` +2. Run up pgadmin and login using the username and password provided in `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` note you will need to set up a new connection to the database internally. You will use the same host, database, username and password as above. +```bash +docker-compose run db pgadmin -d +``` +3. Use pgadmin to restore the schema using the restore functionaltiy and they query tool for the patch-schemas. + +4. Now run up the server you should see the server start correctly now. +```bash +docker-compose run server -d +``` + +## Turning off the server safely +```bash +docker-compose stop +``` +## Turning on the server again +This boots the db pgadmin and the server in a detached state +```bash +docker-compose up -d +``` +if you want all the logs and you want it to be in an attached state +```bash +docker-compose up +``` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e7ff0e960..3503326db 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -8,6 +8,7 @@ services: db: image: postgres environment: + # (Make sure these match config.json) - POSTGRES_USER=postgres - POSTGRES_PASSWORD=password - POSTGRES_DB=erupe @@ -17,8 +18,8 @@ services: image: dpage/pgadmin4 restart: always environment: - PGADMIN_DEFAULT_EMAIL: test@test.com - PGADMIN_DEFAULT_PASSWORD: root + PGADMIN_DEFAULT_EMAIL: user@pgadmin.com + PGADMIN_DEFAULT_PASSWORD: password ports: - "5050:80" depends_on: @@ -34,6 +35,7 @@ services: - ../bin:/app/erupe/bin - ../savedata:/app/erupe/savedata ports: + # (Make sure these match config.json) - "53312:53312" #Sign V1 - "8080:8080" #Sign V2 - "53310:53310" #Entrance From 463ceba555a13e27af64029371021b6a5c44bbc7 Mon Sep 17 00:00:00 2001 From: stratic-dev Date: Mon, 22 Jan 2024 22:45:31 +0000 Subject: [PATCH 43/70] Added github action --- .github/workflows/docker.yml | 48 ++++++++++++++++++++++++++++++++++++ config.json | 4 +-- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..f49ec5d7c --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,48 @@ +name: Create and publish a Docker image + +# Configures this workflow to run every time a tag is created. +on: + push: + tags: + - '*' + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/config.json b/config.json index c0247b9d3..d866c3855 100644 --- a/config.json +++ b/config.json @@ -157,10 +157,10 @@ {"Name": "EXRenewing", "Enabled": true} ], "Database": { - "Host": "localhost", + "Host": "db", "Port": 5432, "User": "postgres", - "Password": "", + "Password": "password", "Database": "erupe" }, "Sign": { From abe47445876c1ddcfa08756679af80d885e1195b Mon Sep 17 00:00:00 2001 From: stratic-dev Date: Wed, 24 Jan 2024 04:35:24 +0000 Subject: [PATCH 44/70] Added db init script. Combined all schemas under schemas. Persisted updates and init --- .gitignore | 3 +- docker/README.md | 34 +-- docker/docker-compose.yml | 32 ++- docker/init/setup.sh | 22 ++ schemas/9.1-init.sql | Bin 0 -> 64231 bytes .../bundled-schema}/DistributionDemo.sql | 0 .../bundled-schema}/DivaShops.sql | 0 .../bundled-schema}/EventQuests.sql | 0 .../bundled-schema}/FPointItems.sql | 0 .../bundled-schema}/FestaDefaults.sql | 0 .../bundled-schema}/GachaDemo.sql | 0 .../bundled-schema}/NetcafeDefaults.sql | 0 .../bundled-schema}/OtherShops.sql | 0 .../bundled-schema}/RoadShopItems.sql | 0 .../bundled-schema}/ScenarioDefaults.sql | 0 .../patch-schema}/.gitkeep | 0 .../patch-schema}/00-psn-id.sql | 0 .../patch-schema}/01-wiiu-key.sql | 0 .../patch-schema}/02-tower.sql | 0 .../patch-schema}/03-event_quests.sql | 0 .../patch-schema}/04-trend-weapons.sql | 0 .../patch-schema}/05-gacha-roll-name.sql | 0 .../patch-schema}/06-goocoo-rename.sql | 0 .../patch-schema}/07-scenarios-counter.sql | 0 .../patch-schema}/08-kill-counts.sql | 0 .../patch-schema}/09-fix-guild-treasure.sql | 0 .../patch-schema}/10-rework-distributions.sql | 0 .../patch-schema}/11-event-quest-flags.sql | 0 .../patch-schema}/12-event_quest_cycling.sql | 0 .../patch-schema}/13-festa-trial-votes.sql | 0 .../patch-schema}/14-fix-fpoint-trades.sql | 0 .../patch-schema}/15-reset-goocoos.sql | 0 .../16-discord-password-resets.sql | 0 .../patch-schema}/op-accounts.sql | 0 .../patch-schema}/psn-link.sql | 0 schemas/update-schema/9.2-update.sql | 241 ++++++++++++++++++ 36 files changed, 309 insertions(+), 23 deletions(-) create mode 100644 docker/init/setup.sh create mode 100644 schemas/9.1-init.sql rename {bundled-schema => schemas/bundled-schema}/DistributionDemo.sql (100%) rename {bundled-schema => schemas/bundled-schema}/DivaShops.sql (100%) rename {bundled-schema => schemas/bundled-schema}/EventQuests.sql (100%) rename {bundled-schema => schemas/bundled-schema}/FPointItems.sql (100%) rename {bundled-schema => schemas/bundled-schema}/FestaDefaults.sql (100%) rename {bundled-schema => schemas/bundled-schema}/GachaDemo.sql (100%) rename {bundled-schema => schemas/bundled-schema}/NetcafeDefaults.sql (100%) rename {bundled-schema => schemas/bundled-schema}/OtherShops.sql (100%) rename {bundled-schema => schemas/bundled-schema}/RoadShopItems.sql (100%) rename {bundled-schema => schemas/bundled-schema}/ScenarioDefaults.sql (100%) rename {patch-schema => schemas/patch-schema}/.gitkeep (100%) rename {patch-schema => schemas/patch-schema}/00-psn-id.sql (100%) rename {patch-schema => schemas/patch-schema}/01-wiiu-key.sql (100%) rename {patch-schema => schemas/patch-schema}/02-tower.sql (100%) rename {patch-schema => schemas/patch-schema}/03-event_quests.sql (100%) rename {patch-schema => schemas/patch-schema}/04-trend-weapons.sql (100%) rename {patch-schema => schemas/patch-schema}/05-gacha-roll-name.sql (100%) rename {patch-schema => schemas/patch-schema}/06-goocoo-rename.sql (100%) rename {patch-schema => schemas/patch-schema}/07-scenarios-counter.sql (100%) rename {patch-schema => schemas/patch-schema}/08-kill-counts.sql (100%) rename {patch-schema => schemas/patch-schema}/09-fix-guild-treasure.sql (100%) rename {patch-schema => schemas/patch-schema}/10-rework-distributions.sql (100%) rename {patch-schema => schemas/patch-schema}/11-event-quest-flags.sql (100%) rename {patch-schema => schemas/patch-schema}/12-event_quest_cycling.sql (100%) rename {patch-schema => schemas/patch-schema}/13-festa-trial-votes.sql (100%) rename {patch-schema => schemas/patch-schema}/14-fix-fpoint-trades.sql (100%) rename {patch-schema => schemas/patch-schema}/15-reset-goocoos.sql (100%) rename {patch-schema => schemas/patch-schema}/16-discord-password-resets.sql (100%) rename {patch-schema => schemas/patch-schema}/op-accounts.sql (100%) rename {patch-schema => schemas/patch-schema}/psn-link.sql (100%) create mode 100644 schemas/update-schema/9.2-update.sql diff --git a/.gitignore b/.gitignore index a36e91559..4101960d2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ vendor/ savedata/*/ *.exe *.lnk -*.bat \ No newline at end of file +*.bat +/docker/db-data \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 11a5cb04a..3f45a12c0 100644 --- a/docker/README.md +++ b/docker/README.md @@ -17,6 +17,8 @@ Docker compose allows you to run multiple containers at once. The docker compose - pg admin (Admin interface to make db changes) - erupe +We automatically populate the database to the latest version on start. If you you are updating you will need to apply the new schemas manually. + Before we get started you should make sure the database info matches whats in the docker compose file for the environment variables `POSTGRES_PASSWORD`,`POSTGRES_USER` and `POSTGRES_DB`. You can set the host to be the service name `db`. Here is a example of what you would put in the config.json if you was to leave the defaults. It is strongly recommended to change the password. @@ -30,29 +32,29 @@ Here is a example of what you would put in the config.json if you was to leave t }, ``` -### Running up the database for the first time -First we need to set up the database. This requires the schema and the patch schemas to be applied. This can be done by runnnig up both the db and pgadmin. +Place this file within ./docker/config.json + +You will need to do the same for your bins place these in ./docker/bin + +# Setting up the web hosted materials +Clone the Severs repo into ./docker/Severs + +Make sure your hosts are pointing to where this is hosted -1. Pull the remote images and build a container image for erupe -```bash -docker-compose pull -docker-compose build -``` -2. Run up pgadmin and login using the username and password provided in `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` note you will need to set up a new connection to the database internally. You will use the same host, database, username and password as above. -```bash -docker-compose run db pgadmin -d -``` -3. Use pgadmin to restore the schema using the restore functionaltiy and they query tool for the patch-schemas. -4. Now run up the server you should see the server start correctly now. -```bash -docker-compose run server -d -``` ## Turning off the server safely ```bash docker-compose stop ``` + +## Turning off the server destructive +```bash +docker-compose down +``` +Make sure if you want to delete your data you delete the folders that persisted +- ./docker/savedata +- ./docker/db-data ## Turning on the server again This boots the db pgadmin and the server in a detached state ```bash diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3503326db..a610836ee 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -14,6 +14,15 @@ services: - POSTGRES_DB=erupe ports: - "5432:5432" + volumes: + - ./db-data/:/var/lib/postgresql/data/ + - ../schemas/:/schemas/ + - ./init/setup.sh:/docker-entrypoint-initdb.d/setup.sh + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 pgadmin: image: dpage/pgadmin4 restart: always @@ -23,17 +32,19 @@ services: ports: - "5050:80" depends_on: - - db + db: + condition: service_healthy server: depends_on: - - db + db: + condition: service_healthy # If using prebuilt container change paths and config build: context: ../ volumes: - - ../config.json:/app/erupe/config.json - - ../bin:/app/erupe/bin - - ../savedata:/app/erupe/savedata + - ./config.json:/app/erupe/config.json + - ./bin:/app/erupe/bin + - ./savedata:/app/erupe/savedata ports: # (Make sure these match config.json) - "53312:53312" #Sign V1 @@ -48,4 +59,13 @@ services: - "54006:54006" - "54007:54007" - "54008:54008" - + web: + image: httpd:latest + container_name: my-apache-app + ports: + - '80:80' + volumes: + - ./Servers:/usr/local/apache2/htdocs + depends_on: + db: + condition: service_healthy \ No newline at end of file diff --git a/docker/init/setup.sh b/docker/init/setup.sh new file mode 100644 index 000000000..d9fda8d59 --- /dev/null +++ b/docker/init/setup.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e +echo "INIT!" +pg_restore --username="$POSTGRES_USER" --dbname="$POSTGRES_DB" --verbose /schemas/9.1-init.sql + + + +echo "Updating!" + +for file in /schemas/update-schema/* +do + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -1 -f $file +done + + + +echo "Patching!" + +for file in /schemas/patch-schema/* +do + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -1 -f $file +done \ No newline at end of file diff --git a/schemas/9.1-init.sql b/schemas/9.1-init.sql new file mode 100644 index 0000000000000000000000000000000000000000..3ae0ca127112d22ee882d7d7c9bfdebedf6362e4 GIT binary patch literal 64231 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 literal 0 HcmV?d00001 diff --git a/bundled-schema/DistributionDemo.sql b/schemas/bundled-schema/DistributionDemo.sql similarity index 100% rename from bundled-schema/DistributionDemo.sql rename to schemas/bundled-schema/DistributionDemo.sql diff --git a/bundled-schema/DivaShops.sql b/schemas/bundled-schema/DivaShops.sql similarity index 100% rename from bundled-schema/DivaShops.sql rename to schemas/bundled-schema/DivaShops.sql diff --git a/bundled-schema/EventQuests.sql b/schemas/bundled-schema/EventQuests.sql similarity index 100% rename from bundled-schema/EventQuests.sql rename to schemas/bundled-schema/EventQuests.sql diff --git a/bundled-schema/FPointItems.sql b/schemas/bundled-schema/FPointItems.sql similarity index 100% rename from bundled-schema/FPointItems.sql rename to schemas/bundled-schema/FPointItems.sql diff --git a/bundled-schema/FestaDefaults.sql b/schemas/bundled-schema/FestaDefaults.sql similarity index 100% rename from bundled-schema/FestaDefaults.sql rename to schemas/bundled-schema/FestaDefaults.sql diff --git a/bundled-schema/GachaDemo.sql b/schemas/bundled-schema/GachaDemo.sql similarity index 100% rename from bundled-schema/GachaDemo.sql rename to schemas/bundled-schema/GachaDemo.sql diff --git a/bundled-schema/NetcafeDefaults.sql b/schemas/bundled-schema/NetcafeDefaults.sql similarity index 100% rename from bundled-schema/NetcafeDefaults.sql rename to schemas/bundled-schema/NetcafeDefaults.sql diff --git a/bundled-schema/OtherShops.sql b/schemas/bundled-schema/OtherShops.sql similarity index 100% rename from bundled-schema/OtherShops.sql rename to schemas/bundled-schema/OtherShops.sql diff --git a/bundled-schema/RoadShopItems.sql b/schemas/bundled-schema/RoadShopItems.sql similarity index 100% rename from bundled-schema/RoadShopItems.sql rename to schemas/bundled-schema/RoadShopItems.sql diff --git a/bundled-schema/ScenarioDefaults.sql b/schemas/bundled-schema/ScenarioDefaults.sql similarity index 100% rename from bundled-schema/ScenarioDefaults.sql rename to schemas/bundled-schema/ScenarioDefaults.sql diff --git a/patch-schema/.gitkeep b/schemas/patch-schema/.gitkeep similarity index 100% rename from patch-schema/.gitkeep rename to schemas/patch-schema/.gitkeep diff --git a/patch-schema/00-psn-id.sql b/schemas/patch-schema/00-psn-id.sql similarity index 100% rename from patch-schema/00-psn-id.sql rename to schemas/patch-schema/00-psn-id.sql diff --git a/patch-schema/01-wiiu-key.sql b/schemas/patch-schema/01-wiiu-key.sql similarity index 100% rename from patch-schema/01-wiiu-key.sql rename to schemas/patch-schema/01-wiiu-key.sql diff --git a/patch-schema/02-tower.sql b/schemas/patch-schema/02-tower.sql similarity index 100% rename from patch-schema/02-tower.sql rename to schemas/patch-schema/02-tower.sql diff --git a/patch-schema/03-event_quests.sql b/schemas/patch-schema/03-event_quests.sql similarity index 100% rename from patch-schema/03-event_quests.sql rename to schemas/patch-schema/03-event_quests.sql diff --git a/patch-schema/04-trend-weapons.sql b/schemas/patch-schema/04-trend-weapons.sql similarity index 100% rename from patch-schema/04-trend-weapons.sql rename to schemas/patch-schema/04-trend-weapons.sql diff --git a/patch-schema/05-gacha-roll-name.sql b/schemas/patch-schema/05-gacha-roll-name.sql similarity index 100% rename from patch-schema/05-gacha-roll-name.sql rename to schemas/patch-schema/05-gacha-roll-name.sql diff --git a/patch-schema/06-goocoo-rename.sql b/schemas/patch-schema/06-goocoo-rename.sql similarity index 100% rename from patch-schema/06-goocoo-rename.sql rename to schemas/patch-schema/06-goocoo-rename.sql diff --git a/patch-schema/07-scenarios-counter.sql b/schemas/patch-schema/07-scenarios-counter.sql similarity index 100% rename from patch-schema/07-scenarios-counter.sql rename to schemas/patch-schema/07-scenarios-counter.sql diff --git a/patch-schema/08-kill-counts.sql b/schemas/patch-schema/08-kill-counts.sql similarity index 100% rename from patch-schema/08-kill-counts.sql rename to schemas/patch-schema/08-kill-counts.sql diff --git a/patch-schema/09-fix-guild-treasure.sql b/schemas/patch-schema/09-fix-guild-treasure.sql similarity index 100% rename from patch-schema/09-fix-guild-treasure.sql rename to schemas/patch-schema/09-fix-guild-treasure.sql diff --git a/patch-schema/10-rework-distributions.sql b/schemas/patch-schema/10-rework-distributions.sql similarity index 100% rename from patch-schema/10-rework-distributions.sql rename to schemas/patch-schema/10-rework-distributions.sql diff --git a/patch-schema/11-event-quest-flags.sql b/schemas/patch-schema/11-event-quest-flags.sql similarity index 100% rename from patch-schema/11-event-quest-flags.sql rename to schemas/patch-schema/11-event-quest-flags.sql diff --git a/patch-schema/12-event_quest_cycling.sql b/schemas/patch-schema/12-event_quest_cycling.sql similarity index 100% rename from patch-schema/12-event_quest_cycling.sql rename to schemas/patch-schema/12-event_quest_cycling.sql diff --git a/patch-schema/13-festa-trial-votes.sql b/schemas/patch-schema/13-festa-trial-votes.sql similarity index 100% rename from patch-schema/13-festa-trial-votes.sql rename to schemas/patch-schema/13-festa-trial-votes.sql diff --git a/patch-schema/14-fix-fpoint-trades.sql b/schemas/patch-schema/14-fix-fpoint-trades.sql similarity index 100% rename from patch-schema/14-fix-fpoint-trades.sql rename to schemas/patch-schema/14-fix-fpoint-trades.sql diff --git a/patch-schema/15-reset-goocoos.sql b/schemas/patch-schema/15-reset-goocoos.sql similarity index 100% rename from patch-schema/15-reset-goocoos.sql rename to schemas/patch-schema/15-reset-goocoos.sql diff --git a/patch-schema/16-discord-password-resets.sql b/schemas/patch-schema/16-discord-password-resets.sql similarity index 100% rename from patch-schema/16-discord-password-resets.sql rename to schemas/patch-schema/16-discord-password-resets.sql diff --git a/patch-schema/op-accounts.sql b/schemas/patch-schema/op-accounts.sql similarity index 100% rename from patch-schema/op-accounts.sql rename to schemas/patch-schema/op-accounts.sql diff --git a/patch-schema/psn-link.sql b/schemas/patch-schema/psn-link.sql similarity index 100% rename from patch-schema/psn-link.sql rename to schemas/patch-schema/psn-link.sql diff --git a/schemas/update-schema/9.2-update.sql b/schemas/update-schema/9.2-update.sql new file mode 100644 index 000000000..e7dbf699b --- /dev/null +++ b/schemas/update-schema/9.2-update.sql @@ -0,0 +1,241 @@ +BEGIN; + +DROP TABLE IF EXISTS public.fpoint_items; + +CREATE TABLE IF NOT EXISTS public.fpoint_items ( + id serial PRIMARY KEY, + item_type integer, + item_id integer, + quantity integer, + fpoints integer, + trade_type integer +); + +ALTER TABLE IF EXISTS public.characters ADD bonus_quests INT NOT NULL DEFAULT 0; + +ALTER TABLE IF EXISTS public.characters ADD daily_quests INT NOT NULL DEFAULT 0; + +ALTER TABLE IF EXISTS public.characters ADD promo_points INT NOT NULL DEFAULT 0; + +ALTER TABLE IF EXISTS public.guild_characters ADD rp_today INT DEFAULT 0; + +ALTER TABLE IF EXISTS public.guild_characters ADD rp_yesterday INT DEFAULT 0; + +UPDATE public.characters SET savemercenary = NULL; + +ALTER TABLE IF EXISTS public.characters ADD rasta_id INT; + +ALTER TABLE IF EXISTS public.characters ADD pact_id INT; + +ALTER TABLE IF EXISTS public.characters ADD stampcard INT NOT NULL DEFAULT 0; + +ALTER TABLE IF EXISTS public.characters DROP COLUMN IF EXISTS gacha_prem; + +ALTER TABLE IF EXISTS public.characters DROP COLUMN IF EXISTS gacha_trial; + +ALTER TABLE IF EXISTS public.characters DROP COLUMN IF EXISTS frontier_points; + +ALTER TABLE IF EXISTS public.users ADD IF NOT EXISTS gacha_premium INT; + +ALTER TABLE IF EXISTS public.users ADD IF NOT EXISTS gacha_trial INT; + +ALTER TABLE IF EXISTS public.users ADD IF NOT EXISTS frontier_points INT; + +DROP TABLE IF EXISTS public.gacha_shop; + +CREATE TABLE IF NOT EXISTS public.gacha_shop ( + id SERIAL PRIMARY KEY, + min_gr INTEGER, + min_hr INTEGER, + name TEXT, + url_banner TEXT, + url_feature TEXT, + url_thumbnail TEXT, + wide BOOLEAN, + recommended BOOLEAN, + gacha_type INTEGER, + hidden BOOLEAN +); + +DROP TABLE IF EXISTS public.gacha_shop_items; + +CREATE TABLE IF NOT EXISTS public.gacha_entries ( + id SERIAL PRIMARY KEY, + gacha_id INTEGER, + entry_type INTEGER, + item_type INTEGER, + item_number INTEGER, + item_quantity INTEGER, + weight INTEGER, + rarity INTEGER, + rolls INTEGER, + frontier_points INTEGER, + daily_limit INTEGER +); + +CREATE TABLE IF NOT EXISTS public.gacha_items ( + id SERIAL PRIMARY KEY, + entry_id INTEGER, + item_type INTEGER, + item_id INTEGER, + quantity INTEGER +); + +DROP TABLE IF EXISTS public.stepup_state; + +CREATE TABLE IF NOT EXISTS public.gacha_stepup ( + gacha_id INTEGER, + step INTEGER, + character_id INTEGER +); + +DROP TABLE IF EXISTS public.lucky_box_state; + +CREATE TABLE IF NOT EXISTS public.gacha_box ( + gacha_id INTEGER, + entry_id INTEGER, + character_id INTEGER +); + +DROP TABLE IF EXISTS public.login_boost_state; + +CREATE TABLE IF NOT EXISTS public.login_boost ( + char_id INTEGER, + week_req INTEGER, + expiration TIMESTAMP WITH TIME ZONE, + reset TIMESTAMP WITH TIME ZONE +); + +ALTER TABLE IF EXISTS public.characters ADD COLUMN mezfes BYTEA; + +ALTER TABLE IF EXISTS public.characters ALTER COLUMN daily_time TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.characters ALTER COLUMN guild_post_checked TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.characters ALTER COLUMN boost_time TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.characters ADD COLUMN IF NOT EXISTS cafe_reset TIMESTAMP WITHOUT TIME ZONE; + +ALTER TABLE IF EXISTS public.characters ALTER COLUMN cafe_reset TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.distribution ALTER COLUMN deadline TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.events ALTER COLUMN start_time TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.feature_weapon ALTER COLUMN start_time TYPE TIMESTAMP WITH TIME ZONE; + +CREATE TABLE IF NOT EXISTS public.feature_weapon +( + start_time TIMESTAMP WITH TIME ZONE NOT NULL, + featured INTEGER NOT NULL +); + +ALTER TABLE IF EXISTS public.guild_alliances ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.guild_applications ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.guild_characters ALTER COLUMN joined_at TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.guild_posts ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.characters ALTER COLUMN daily_time TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.guilds ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.mail ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.stamps ALTER COLUMN hl_next TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.stamps ALTER COLUMN ex_next TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.titles ALTER COLUMN unlocked_at TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.titles ALTER COLUMN updated_at TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.users ALTER COLUMN last_login TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.users ALTER COLUMN return_expires TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE IF EXISTS public.guild_meals DROP COLUMN IF EXISTS expires; + +ALTER TABLE IF EXISTS public.guild_meals ADD COLUMN IF NOT EXISTS created_at TIMESTAMP WITH TIME ZONE; + +DROP TABLE IF EXISTS public.account_ban; + +DROP TABLE IF EXISTS public.account_history; + +DROP TABLE IF EXISTS public.account_moderation; + +DROP TABLE IF EXISTS public.account_sub; + +DROP TABLE IF EXISTS public.history; + +DROP TABLE IF EXISTS public.questlists; + +DROP TABLE IF EXISTS public.schema_migrations; + +DROP TABLE IF EXISTS public.user_binaries; + +DROP PROCEDURE IF EXISTS raviinit; + +DROP PROCEDURE IF EXISTS ravireset; + +ALTER TABLE IF EXISTS public.normal_shop_items RENAME TO shop_items; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN shoptype TO shop_type; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN shopid TO shop_id; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN itemhash TO id; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN itemid TO item_id; + +ALTER TABLE IF EXISTS public.shop_items ALTER COLUMN points TYPE integer; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN points TO cost; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN tradequantity TO quantity; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN rankreqlow TO min_hr; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN rankreqhigh TO min_sr; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN rankreqg TO min_gr; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN storelevelreq TO store_level; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN maximumquantity TO max_quantity; + +ALTER TABLE IF EXISTS public.shop_items DROP COLUMN IF EXISTS boughtquantity; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN roadfloorsrequired TO road_floors; + +ALTER TABLE IF EXISTS public.shop_items RENAME COLUMN weeklyfataliskills TO road_fatalis; + +ALTER TABLE public.shop_items RENAME CONSTRAINT normal_shop_items_pkey TO shop_items_pkey; + +ALTER TABLE IF EXISTS public.shop_items DROP CONSTRAINT IF EXISTS normal_shop_items_itemhash_key; + +CREATE SEQUENCE IF NOT EXISTS public.shop_items_id_seq; + +ALTER SEQUENCE IF EXISTS public.shop_items_id_seq OWNER TO postgres; + +ALTER TABLE IF EXISTS public.shop_items ALTER COLUMN id SET DEFAULT nextval('shop_items_id_seq'::regclass); + +ALTER SEQUENCE IF EXISTS public.shop_items_id_seq OWNED BY shop_items.id; + +SELECT setval('shop_items_id_seq', (SELECT MAX(id) FROM public.shop_items)); + +DROP TABLE IF EXISTS public.shop_item_state; + +CREATE TABLE IF NOT EXISTS public.shop_items_bought ( + character_id INTEGER, + shop_item_id INTEGER, + bought INTEGER +); + +UPDATE users SET rights = rights-2; + +ALTER TABLE IF EXISTS public.users ALTER COLUMN rights SET DEFAULT 12; + +END; \ No newline at end of file From 851301b088302a955cfa292c448313dc78689376 Mon Sep 17 00:00:00 2001 From: stratic-dev Date: Fri, 26 Jan 2024 17:59:34 +0000 Subject: [PATCH 45/70] Updated readme and added init schemas to folder --- README.md | 14 ++++++++++++++ docker/init/setup.sh | 2 +- .../{ => initialisation-schema}/9.1-init.sql | Bin schemas/initialisation-schema/9.2-init.sql | Bin 0 -> 55992 bytes 4 files changed, 15 insertions(+), 1 deletion(-) rename schemas/{ => initialisation-schema}/9.1-init.sql (100%) create mode 100644 schemas/initialisation-schema/9.2-init.sql diff --git a/README.md b/README.md index 034e2a4ea..b9be1fb05 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,20 @@ If you want to modify or compile Erupe yourself, please read on. 3. Edit [config.json](./config.json) such that the database password matches your PostgreSQL setup. 4. Run `go build` or `go run .` to compile Erupe. +## Docker + +Please see the readme in [docker/README.md](./docker/README.md). At the moment this is only really good for quick installs and checking out development not for production. + +## Schemas + +We source control the following schemas: +- Initialisation Schemas: These initialise the application database to a clean install from a specific version. +- Update Schemas: These are update files they should be ran in order of version to get to the latest schema. +- Patch Schemas: These are for development and should be ran from the lastest available update schema or initial schema. These eventually get condensed into `Update Schemas` and then deleted when updated to a new version. +- Bundled Schemas: These are demo reference files to allow servers to be able to roll their own shops, distributions gachas and scenarios set ups. + +Note: Patch schemas are subject to change! You should only be using them if you are following along with development. + ## Resources - [Quest and Scenario Binary Files](https://files.catbox.moe/xf0l7w.7z) diff --git a/docker/init/setup.sh b/docker/init/setup.sh index d9fda8d59..b84f83b4d 100644 --- a/docker/init/setup.sh +++ b/docker/init/setup.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e echo "INIT!" -pg_restore --username="$POSTGRES_USER" --dbname="$POSTGRES_DB" --verbose /schemas/9.1-init.sql +pg_restore --username="$POSTGRES_USER" --dbname="$POSTGRES_DB" --verbose /schemas/initialisation-schema/9.1-init.sql diff --git a/schemas/9.1-init.sql b/schemas/initialisation-schema/9.1-init.sql similarity index 100% rename from schemas/9.1-init.sql rename to schemas/initialisation-schema/9.1-init.sql diff --git a/schemas/initialisation-schema/9.2-init.sql b/schemas/initialisation-schema/9.2-init.sql new file mode 100644 index 0000000000000000000000000000000000000000..229b860a5812817c77b313bae6bee700756d8892 GIT binary patch literal 55992 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 jE2<;(I1-iE| Date: Mon, 29 Jan 2024 21:58:36 -0500 Subject: [PATCH 46/70] fix config mismatch --- config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index c0247b9d3..1e9e6486d 100644 --- a/config.json +++ b/config.json @@ -84,10 +84,10 @@ "Discord": { "Enabled": false, "BotToken": "", - "RealTimeChannel": { + "RelayChannel": { "Enabled": false, "MaxMessageLength": 183, - "RealTimeChannelID": "" + "RelayChannelID": "" } }, "Commands": [ From 08e6fd2cda3e853beebc88c36239b375420359ab Mon Sep 17 00:00:00 2001 From: stratic-dev Date: Thu, 1 Feb 2024 21:54:40 +0000 Subject: [PATCH 47/70] Delete 9.2-init.sql --- schemas/initialisation-schema/9.2-init.sql | Bin 55992 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 schemas/initialisation-schema/9.2-init.sql diff --git a/schemas/initialisation-schema/9.2-init.sql b/schemas/initialisation-schema/9.2-init.sql deleted file mode 100644 index 229b860a5812817c77b313bae6bee700756d8892..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55992 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 jE2<;(I1-iE| Date: Thu, 1 Feb 2024 21:56:43 +0000 Subject: [PATCH 48/70] Create AUTHORS.md --- AUTHORS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 AUTHORS.md diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 000000000..618d34264 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,29 @@ +# List of AUTHORS who contributed over time to the Erupe project + +## Point of current development +The project is currently developed under https://github.com/ZeruLight/Erupe + +## History of development +Development of this project dates back to 2019, and was developed under various umbrellas over time: +* Cappuccino (Fist/Ando/Ellie42) (The Erupe Developers), 2019-2020 (https://github.com/Ellie42/Erupe / https://github.com/ricochhet/Erupe-Legacy) (Still active closed source) +* Einherjar Team, ????-2022 Feb (There is no git history for this period, this team's work was taken and used as a foundation for future repositories) +* Community Edition, 2022 (https://github.com/xl3lackout/Erupe) +* Zerulight, 2022-2023 (https://github.com/ZeruLight/Erupe) + +## Authorship of the code +Authorship is assigned for each commit within the git history, which is stored in these git repos: +* https://github.com/ZeruLight/Erupe +* https://github.com/Ellie42/Erupe +* https://github.com/ricochhet/Erupe-Legacy +* https://github.com/xl3lackout/Erupe + +Note there is a divergence between Ellie42s branch and xl3lackout where history has been lost. + +Unfortunately, we have no detailed information on the history of the Erupe pre-2022 +if somebody can provide information, please contact us, so that we can make this history available. + +## Exceptions with third-party libraries +The third-party libraries have their own way of addressing authorship and the authorship of commits importing/updating +a third-party library reflects who did the importing instead of who wrote the code within the commit. + +The Authors of third-party libraries are not explicitly mentioned, and usually is possible to obtain from the files belonging to the third-party libraries. \ No newline at end of file From df9e33bdccc464e2ba7e947d9b4c992188fe95dd Mon Sep 17 00:00:00 2001 From: wish Date: Thu, 8 Feb 2024 18:29:04 +1100 Subject: [PATCH 49/70] fix dirty pr --- config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index bffde254a..1e9e6486d 100644 --- a/config.json +++ b/config.json @@ -157,10 +157,10 @@ {"Name": "EXRenewing", "Enabled": true} ], "Database": { - "Host": "db", + "Host": "localhost", "Port": 5432, "User": "postgres", - "Password": "password", + "Password": "", "Database": "erupe" }, "Sign": { From bbf4fa247200d8776fd42665126026949d1828a1 Mon Sep 17 00:00:00 2001 From: wish Date: Sat, 10 Feb 2024 03:07:13 +1100 Subject: [PATCH 50/70] further tune value configuration --- config.json | 15 + config/config.go | 15 + server/channelserver/handlers_quest.go | 410 ++++++------------------- 3 files changed, 125 insertions(+), 315 deletions(-) diff --git a/config.json b/config.json index 1e9e6486d..9bb641f72 100644 --- a/config.json +++ b/config.json @@ -65,11 +65,26 @@ "SmallBerserkRavienteMaxPlayers": 8, "GUrgentRate": 0.10, "GCPMultiplier": 1.00, + "HRPMultiplier": 1.00, + "HRPMultiplierNC": 1.00, + "SRPMultiplier": 1.00, + "SRPMultiplierNC": 1.00, "GRPMultiplier": 1.00, + "GRPMultiplierNC": 1.00, "GSRPMultiplier": 1.00, + "GSRPMultiplierNC": 1.00, + "ZennyMultiplier": 1.00, + "ZennyMultiplierNC": 1.00, "GZennyMultiplier": 1.00, + "GZennyMultiplierNC": 1.00, "MaterialMultiplier": 1.00, + "MaterialMultiplierNC": 1.00, + "GMaterialMultiplier": 1.00, + "GMaterialMultiplierNC": 1.00, "ExtraCarves": 0, + "ExtraCarvesNC": 0, + "GExtraCarves": 0, + "GExtraCarvesNC": 0, "DisableHunterNavi": false, "MezFesSoloTickets": 5, "MezFesGroupTickets": 1, diff --git a/config/config.go b/config/config.go index 3c6f6a0a7..d2038f150 100644 --- a/config/config.go +++ b/config/config.go @@ -152,11 +152,26 @@ type GameplayOptions struct { SmallBerserkRavienteMaxPlayers uint8 GUrgentRate float32 // Adjusts the rate of G Urgent quests spawning GCPMultiplier float32 // Adjusts the multiplier of GCP rewarded for quest completion + HRPMultiplier float32 // Adjusts the multiplier of Hunter Rank Points rewarded for quest completion + HRPMultiplierNC float32 // Adjusts the multiplier of Hunter Rank Points rewarded for quest completion in a NetCafe + SRPMultiplier float32 // Adjusts the multiplier of Skill Rank Points rewarded for quest completion + SRPMultiplierNC float32 // Adjusts the multiplier of Skill Rank Points rewarded for quest completion in a NetCafe GRPMultiplier float32 // Adjusts the multiplier of G Rank Points rewarded for quest completion + GRPMultiplierNC float32 // Adjusts the multiplier of G Rank Points rewarded for quest completion in a NetCafe GSRPMultiplier float32 // Adjusts the multiplier of G Skill Rank Points rewarded for quest completion + GSRPMultiplierNC float32 // Adjusts the multiplier of G Skill Rank Points rewarded for quest completion in a NetCafe + ZennyMultiplier float32 // Adjusts the multiplier of Zenny rewarded for quest completion + ZennyMultiplierNC float32 // Adjusts the multiplier of Zenny rewarded for quest completion in a NetCafe GZennyMultiplier float32 // Adjusts the multiplier of G Zenny rewarded for quest completion + GZennyMultiplierNC float32 // Adjusts the multiplier of G Zenny rewarded for quest completion in a NetCafe MaterialMultiplier float32 // Adjusts the multiplier of Monster Materials rewarded for quest completion + MaterialMultiplierNC float32 // Adjusts the multiplier of Monster Materials rewarded for quest completion in a NetCafe + GMaterialMultiplier float32 // Adjusts the multiplier of G Rank Monster Materials rewarded for quest completion + GMaterialMultiplierNC float32 // Adjusts the multiplier of G Rank Monster Materials rewarded for quest completion in a NetCafe ExtraCarves uint16 // Grant n extra chances to carve ALL carcasses + ExtraCarvesNC uint16 // Grant n extra chances to carve ALL carcasses in a NetCafe + GExtraCarves uint16 // Grant n extra chances to carve ALL G Rank carcasses + GExtraCarvesNC uint16 // Grant n extra chances to carve ALL G Rank carcasses in a NetCafe DisableHunterNavi bool // Disables the Hunter Navi MezFesSoloTickets uint32 // Number of solo tickets given weekly MezFesGroupTickets uint32 // Number of group tickets given weekly diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index 084fab95f..c8bd41f3b 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -16,6 +16,11 @@ import ( "go.uber.org/zap" ) +type tuneValue struct { + ID uint16 + Value uint16 +} + func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysGetFile) @@ -305,11 +310,6 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { tx.Commit() } - type tuneValue struct { - ID uint16 - Value uint16 - } - tuneValues := []tuneValue{ {ID: 20, Value: 1}, {ID: 26, Value: 1}, @@ -322,29 +322,30 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { {ID: 67, Value: 1}, {ID: 80, Value: 1}, {ID: 94, Value: 1}, - {ID: 1010, Value: 300}, // get_hrp_rate_netcafe - {ID: 1011, Value: 300}, // get_zeny_rate_netcafe - {ID: 1012, Value: 300}, // get_hrp_rate_ncource - {ID: 1013, Value: 300}, // get_zeny_rate_ncource - {ID: 1014, Value: 200}, // get_hrp_rate_premium - {ID: 1015, Value: 200}, // get_zeny_rate_premium - {ID: 1021, Value: 400}, // get_gcp_rate_assist - {ID: 1023, Value: 8}, - {ID: 1024, Value: 150}, // get_hrp_rate_ptbonus - {ID: 1025, Value: 1}, - {ID: 1026, Value: 999}, // get_grank_cap - {ID: 1027, Value: 100}, // get_exchange_rate_festa - {ID: 1028, Value: 100}, // get_exchange_rate_cafe - {ID: 1030, Value: 8}, // get_gquest_cap - {ID: 1031, Value: 100}, // get_exchange_rate_guild (GCP) - {ID: 1032, Value: 0}, // isValid_partner - {ID: 1044, Value: 200}, // get_rate_tload_time_out - {ID: 1045, Value: 0}, // get_rate_tower_treasure_preset - {ID: 1046, Value: 99}, // get_hunter_life_cap - {ID: 1048, Value: 0}, // get_rate_tower_log_disable - {ID: 1049, Value: 10}, // get_rate_tower_gem_max - {ID: 1050, Value: 1}, // get_rate_tower_gem_set - {ID: 1051, Value: 200}, + {ID: 1001, Value: 100}, // get_hrp_rate + {ID: 1010, Value: 300}, // get_hrp_rate_netcafe + {ID: 1011, Value: 300}, // get_zeny_rate_netcafe + {ID: 1012, Value: 300}, // get_hrp_rate_ncource + {ID: 1013, Value: 300}, // get_zeny_rate_ncource + {ID: 1014, Value: 200}, // get_hrp_rate_premium + {ID: 1015, Value: 200}, // get_zeny_rate_premium + {ID: 1021, Value: 400}, // get_gcp_rate_assist + {ID: 1023, Value: 8}, // unused? + {ID: 1024, Value: 150}, // get_hrp_rate_ptbonus + {ID: 1025, Value: 1}, // isValid_stampcard + {ID: 1026, Value: 999}, // get_grank_cap + {ID: 1027, Value: 100}, // get_exchange_rate_festa + {ID: 1028, Value: 100}, // get_exchange_rate_cafe + {ID: 1030, Value: 8}, // get_gquest_cap + {ID: 1031, Value: 100}, // get_exchange_rate_guild (GCP) + {ID: 1032, Value: 0}, // isValid_partner + {ID: 1044, Value: 200}, // get_rate_tload_time_out + {ID: 1045, Value: 0}, // get_rate_tower_treasure_preset + {ID: 1046, Value: 99}, // get_hunter_life_cap + {ID: 1048, Value: 0}, // get_rate_tower_hint_sec + {ID: 1049, Value: 10}, // get_rate_tower_gem_max + {ID: 1050, Value: 1}, // get_rate_tower_gem_set + {ID: 1051, Value: 200}, // get_pallone_score_rate_premium {ID: 1052, Value: 200}, // get_trp_rate_premium {ID: 1063, Value: 50000}, // get_nboost_quest_point_from_hrank {ID: 1064, Value: 50000}, // get_nboost_quest_point_from_srank @@ -356,9 +357,11 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { {ID: 1072, Value: 300}, // get_rate_premium_ravi_tama {ID: 1073, Value: 300}, // get_rate_premium_ravi_ax_tama {ID: 1074, Value: 300}, // get_rate_premium_ravi_g_tama - {ID: 1078, Value: 0}, - {ID: 1079, Value: 1}, - {ID: 1080, Value: 1}, + {ID: 1078, Value: 0}, // isCapped_tenrou_irai + {ID: 1079, Value: 1}, // get_add_tower_level_assist + {ID: 1080, Value: 1}, // get_tune_add_tower_level_w_assist_nboost + + // get_tune_secret_book_item {ID: 1081, Value: 1}, {ID: 1082, Value: 4}, {ID: 1083, Value: 2}, @@ -383,14 +386,17 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { {ID: 1102, Value: 5}, {ID: 1103, Value: 2}, {ID: 1104, Value: 10}, - {ID: 1145, Value: 200}, - {ID: 1146, Value: 0}, // isTower_invisible - {ID: 1147, Value: 0}, // isVenom_playable - {ID: 1149, Value: 20}, - {ID: 1152, Value: 1130}, - {ID: 1154, Value: 0}, // isDisabled_object_season - {ID: 1158, Value: 1}, - {ID: 1160, Value: 300}, + + {ID: 1145, Value: 200}, // get_ud_point_rate_premium + {ID: 1146, Value: 0}, // isTower_invisible + {ID: 1147, Value: 0}, // isVenom_playable + {ID: 1149, Value: 20}, // get_ud_break_parts_point + {ID: 1152, Value: 1130}, // unused? + {ID: 1154, Value: 0}, // isDisabled_object_season + {ID: 1158, Value: 1}, // isDelivery_venom_ult_quest + {ID: 1160, Value: 300}, // get_rate_premium_ravi_g_enhance_tama + + // unknown {ID: 1162, Value: 1}, {ID: 1163, Value: 3}, {ID: 1164, Value: 5}, @@ -410,240 +416,6 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { {ID: 1178, Value: 10}, {ID: 1179, Value: 2}, {ID: 1180, Value: 5}, - {ID: 3000, Value: 100}, - {ID: 3001, Value: 100}, - {ID: 3002, Value: 100}, - {ID: 3003, Value: 100}, - {ID: 3004, Value: 100}, - {ID: 3005, Value: 100}, - {ID: 3006, Value: 100}, - {ID: 3007, Value: 100}, - {ID: 3008, Value: 100}, - {ID: 3009, Value: 100}, - {ID: 3010, Value: 100}, - {ID: 3011, Value: 100}, - {ID: 3012, Value: 100}, - {ID: 3013, Value: 100}, - {ID: 3014, Value: 100}, - {ID: 3015, Value: 100}, - {ID: 3016, Value: 100}, - {ID: 3017, Value: 100}, - {ID: 3018, Value: 100}, - {ID: 3019, Value: 100}, - {ID: 3020, Value: 100}, - {ID: 3021, Value: 100}, - {ID: 3022, Value: 100}, - {ID: 3023, Value: 100}, - {ID: 3024, Value: 100}, - {ID: 3025, Value: 100}, - {ID: 3286, Value: 200}, - {ID: 3287, Value: 200}, - {ID: 3288, Value: 200}, - {ID: 3289, Value: 200}, - {ID: 3290, Value: 200}, - {ID: 3291, Value: 200}, - {ID: 3292, Value: 200}, - {ID: 3293, Value: 200}, - {ID: 3294, Value: 200}, - {ID: 3295, Value: 200}, - {ID: 3296, Value: 200}, - {ID: 3297, Value: 200}, - {ID: 3298, Value: 200}, - {ID: 3299, Value: 200}, - {ID: 3300, Value: 200}, - {ID: 3301, Value: 200}, - {ID: 3302, Value: 200}, - {ID: 3303, Value: 200}, - {ID: 3304, Value: 200}, - {ID: 3305, Value: 200}, - {ID: 3306, Value: 200}, - {ID: 3307, Value: 200}, - {ID: 3308, Value: 200}, - {ID: 3309, Value: 200}, - {ID: 3310, Value: 200}, - {ID: 3311, Value: 200}, - {ID: 3312, Value: 300}, - {ID: 3313, Value: 300}, - {ID: 3314, Value: 300}, - {ID: 3315, Value: 300}, - {ID: 3316, Value: 300}, - {ID: 3317, Value: 300}, - {ID: 3318, Value: 300}, - {ID: 3319, Value: 300}, - {ID: 3320, Value: 300}, - {ID: 3321, Value: 300}, - {ID: 3322, Value: 300}, - {ID: 3323, Value: 300}, - {ID: 3324, Value: 300}, - {ID: 3325, Value: 300}, - {ID: 3326, Value: 300}, - {ID: 3327, Value: 300}, - {ID: 3328, Value: 300}, - {ID: 3329, Value: 300}, - {ID: 3330, Value: 300}, - {ID: 3331, Value: 300}, - {ID: 3332, Value: 300}, - {ID: 3333, Value: 300}, - {ID: 3334, Value: 300}, - {ID: 3335, Value: 300}, - {ID: 3336, Value: 300}, - {ID: 3337, Value: 300}, - {ID: 3338, Value: 100}, - {ID: 3339, Value: 100}, - {ID: 3340, Value: 100}, - {ID: 3341, Value: 100}, - {ID: 3342, Value: 100}, - {ID: 3343, Value: 100}, - {ID: 3344, Value: 100}, - {ID: 3345, Value: 100}, - {ID: 3346, Value: 100}, - {ID: 3347, Value: 100}, - {ID: 3348, Value: 100}, - {ID: 3349, Value: 100}, - {ID: 3350, Value: 100}, - {ID: 3351, Value: 100}, - {ID: 3352, Value: 100}, - {ID: 3353, Value: 100}, - {ID: 3354, Value: 100}, - {ID: 3355, Value: 100}, - {ID: 3356, Value: 100}, - {ID: 3357, Value: 100}, - {ID: 3358, Value: 100}, - {ID: 3359, Value: 100}, - {ID: 3360, Value: 100}, - {ID: 3361, Value: 100}, - {ID: 3362, Value: 100}, - {ID: 3363, Value: 100}, - {ID: 3364, Value: 100}, - {ID: 3365, Value: 100}, - {ID: 3366, Value: 100}, - {ID: 3367, Value: 100}, - {ID: 3368, Value: 100}, - {ID: 3369, Value: 100}, - {ID: 3370, Value: 100}, - {ID: 3371, Value: 100}, - {ID: 3372, Value: 100}, - {ID: 3373, Value: 100}, - {ID: 3374, Value: 100}, - {ID: 3375, Value: 100}, - {ID: 3376, Value: 100}, - {ID: 3377, Value: 100}, - {ID: 3378, Value: 100}, - {ID: 3379, Value: 100}, - {ID: 3380, Value: 100}, - {ID: 3381, Value: 100}, - {ID: 3382, Value: 100}, - {ID: 3383, Value: 100}, - {ID: 3384, Value: 100}, - {ID: 3385, Value: 100}, - {ID: 3386, Value: 100}, - {ID: 3387, Value: 100}, - {ID: 3388, Value: 100}, - {ID: 3389, Value: 100}, - {ID: 3390, Value: 100}, - {ID: 3391, Value: 100}, - {ID: 3392, Value: 100}, - {ID: 3393, Value: 100}, - {ID: 3394, Value: 100}, - {ID: 3395, Value: 100}, - {ID: 3396, Value: 100}, - {ID: 3397, Value: 100}, - {ID: 3398, Value: 100}, - {ID: 3399, Value: 100}, - {ID: 3400, Value: 100}, - {ID: 3401, Value: 100}, - {ID: 3402, Value: 100}, - {ID: 3416, Value: 100}, - {ID: 3417, Value: 100}, - {ID: 3418, Value: 100}, - {ID: 3419, Value: 100}, - {ID: 3420, Value: 100}, - {ID: 3421, Value: 100}, - {ID: 3422, Value: 100}, - {ID: 3423, Value: 100}, - {ID: 3424, Value: 100}, - {ID: 3425, Value: 100}, - {ID: 3426, Value: 100}, - {ID: 3427, Value: 100}, - {ID: 3428, Value: 100}, - {ID: 3442, Value: 100}, - {ID: 3443, Value: 100}, - {ID: 3444, Value: 100}, - {ID: 3445, Value: 100}, - {ID: 3446, Value: 100}, - {ID: 3447, Value: 100}, - {ID: 3448, Value: 100}, - {ID: 3449, Value: 100}, - {ID: 3450, Value: 100}, - {ID: 3451, Value: 100}, - {ID: 3452, Value: 100}, - {ID: 3453, Value: 100}, - {ID: 3454, Value: 100}, - {ID: 3468, Value: 100}, - {ID: 3469, Value: 100}, - {ID: 3470, Value: 100}, - {ID: 3471, Value: 100}, - {ID: 3472, Value: 100}, - {ID: 3473, Value: 100}, - {ID: 3474, Value: 100}, - {ID: 3475, Value: 100}, - {ID: 3476, Value: 100}, - {ID: 3477, Value: 100}, - {ID: 3478, Value: 100}, - {ID: 3479, Value: 100}, - {ID: 3480, Value: 100}, - {ID: 3494, Value: 0}, - {ID: 3495, Value: 0}, - {ID: 3496, Value: 0}, - {ID: 3497, Value: 0}, - {ID: 3498, Value: 0}, - {ID: 3499, Value: 0}, - {ID: 3500, Value: 0}, - {ID: 3501, Value: 0}, - {ID: 3502, Value: 0}, - {ID: 3503, Value: 0}, - {ID: 3504, Value: 0}, - {ID: 3505, Value: 0}, - {ID: 3506, Value: 0}, - {ID: 3520, Value: 0}, - {ID: 3521, Value: 0}, - {ID: 3522, Value: 0}, - {ID: 3523, Value: 0}, - {ID: 3524, Value: 0}, - {ID: 3525, Value: 0}, - {ID: 3526, Value: 0}, - {ID: 3527, Value: 0}, - {ID: 3528, Value: 0}, - {ID: 3529, Value: 0}, - {ID: 3530, Value: 0}, - {ID: 3531, Value: 0}, - {ID: 3532, Value: 0}, - {ID: 3546, Value: 0}, - {ID: 3547, Value: 0}, - {ID: 3548, Value: 0}, - {ID: 3549, Value: 0}, - {ID: 3550, Value: 0}, - {ID: 3551, Value: 0}, - {ID: 3552, Value: 0}, - {ID: 3553, Value: 0}, - {ID: 3554, Value: 0}, - {ID: 3555, Value: 0}, - {ID: 3556, Value: 0}, - {ID: 3557, Value: 0}, - {ID: 3558, Value: 0}, - {ID: 3572, Value: 0}, - {ID: 3573, Value: 0}, - {ID: 3574, Value: 0}, - {ID: 3575, Value: 0}, - {ID: 3576, Value: 0}, - {ID: 3577, Value: 0}, - {ID: 3578, Value: 0}, - {ID: 3579, Value: 0}, - {ID: 3580, Value: 0}, - {ID: 3581, Value: 0}, - {ID: 3582, Value: 0}, - {ID: 3583, Value: 0}, - {ID: 3584, Value: 0}, } tuneValues = append(tuneValues, tuneValue{1020, uint16(s.server.erupeConfig.GameplayOptions.GCPMultiplier * 100)}) @@ -656,62 +428,62 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { if s.server.erupeConfig.GameplayOptions.EnableKaijiEvent { tuneValues = append(tuneValues, tuneValue{1106, 1}) - } else { - tuneValues = append(tuneValues, tuneValue{1106, 0}) } if s.server.erupeConfig.GameplayOptions.EnableHiganjimaEvent { tuneValues = append(tuneValues, tuneValue{1144, 1}) - } else { - tuneValues = append(tuneValues, tuneValue{1144, 0}) } if s.server.erupeConfig.GameplayOptions.EnableNierEvent { tuneValues = append(tuneValues, tuneValue{1153, 1}) - } else { - tuneValues = append(tuneValues, tuneValue{1153, 0}) } if s.server.erupeConfig.GameplayOptions.DisableRoad { tuneValues = append(tuneValues, tuneValue{1155, 1}) - } else { - tuneValues = append(tuneValues, tuneValue{1155, 0}) } - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3026, uint16(s.server.erupeConfig.GameplayOptions.GRPMultiplier * 100)}) - } - - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3039, uint16(s.server.erupeConfig.GameplayOptions.GSRPMultiplier * 100)}) - } - - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3052, uint16(s.server.erupeConfig.GameplayOptions.GZennyMultiplier * 100)}) - } - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3078, uint16(s.server.erupeConfig.GameplayOptions.GZennyMultiplier * 100)}) - } - - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3104, uint16(s.server.erupeConfig.GameplayOptions.MaterialMultiplier * 100)}) - } - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3130, uint16(s.server.erupeConfig.GameplayOptions.MaterialMultiplier * 100)}) - } - - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3156, s.server.erupeConfig.GameplayOptions.ExtraCarves}) - } - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3182, s.server.erupeConfig.GameplayOptions.ExtraCarves}) - } - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3208, s.server.erupeConfig.GameplayOptions.ExtraCarves}) - } - for i := uint16(0); i < 13; i++ { - tuneValues = append(tuneValues, tuneValue{i + 3234, s.server.erupeConfig.GameplayOptions.ExtraCarves}) - } + // get_hrp_rate_from_rank + tuneValues = append(tuneValues, getTuneValueRange(3000, uint16(s.server.erupeConfig.GameplayOptions.HRPMultiplier*100))...) + tuneValues = append(tuneValues, getTuneValueRange(3338, uint16(s.server.erupeConfig.GameplayOptions.HRPMultiplierNC*100))...) + // get_srp_rate_from_rank + tuneValues = append(tuneValues, getTuneValueRange(3013, uint16(s.server.erupeConfig.GameplayOptions.SRPMultiplier*100))...) + tuneValues = append(tuneValues, getTuneValueRange(3351, uint16(s.server.erupeConfig.GameplayOptions.SRPMultiplierNC*100))...) + // get_grp_rate_from_rank + tuneValues = append(tuneValues, getTuneValueRange(3026, uint16(s.server.erupeConfig.GameplayOptions.GRPMultiplier*100))...) + tuneValues = append(tuneValues, getTuneValueRange(3364, uint16(s.server.erupeConfig.GameplayOptions.GRPMultiplierNC*100))...) + // get_gsrp_rate_from_rank + tuneValues = append(tuneValues, getTuneValueRange(3039, uint16(s.server.erupeConfig.GameplayOptions.GSRPMultiplier*100))...) + tuneValues = append(tuneValues, getTuneValueRange(3377, uint16(s.server.erupeConfig.GameplayOptions.GSRPMultiplierNC*100))...) + // get_zeny_rate_from_hrank + tuneValues = append(tuneValues, getTuneValueRange(3052, uint16(s.server.erupeConfig.GameplayOptions.ZennyMultiplier*100))...) + tuneValues = append(tuneValues, getTuneValueRange(3390, uint16(s.server.erupeConfig.GameplayOptions.ZennyMultiplierNC*100))...) + // get_zeny_rate_from_grank + tuneValues = append(tuneValues, getTuneValueRange(3078, uint16(s.server.erupeConfig.GameplayOptions.GZennyMultiplier*100))...) + tuneValues = append(tuneValues, getTuneValueRange(3416, uint16(s.server.erupeConfig.GameplayOptions.GZennyMultiplierNC*100))...) + // get_reward_rate_from_hrank + tuneValues = append(tuneValues, getTuneValueRange(3104, uint16(s.server.erupeConfig.GameplayOptions.MaterialMultiplier*100))...) + tuneValues = append(tuneValues, getTuneValueRange(3442, uint16(s.server.erupeConfig.GameplayOptions.MaterialMultiplierNC*100))...) + // get_reward_rate_from_grank + tuneValues = append(tuneValues, getTuneValueRange(3130, uint16(s.server.erupeConfig.GameplayOptions.GMaterialMultiplier*100))...) + tuneValues = append(tuneValues, getTuneValueRange(3468, uint16(s.server.erupeConfig.GameplayOptions.GMaterialMultiplierNC*100))...) + // get_lottery_rate_from_hrank + tuneValues = append(tuneValues, getTuneValueRange(3156, 0)...) + tuneValues = append(tuneValues, getTuneValueRange(3494, 0)...) + // get_lottery_rate_from_grank + tuneValues = append(tuneValues, getTuneValueRange(3182, 0)...) + tuneValues = append(tuneValues, getTuneValueRange(3520, 0)...) + // get_hagi_rate_from_hrank + tuneValues = append(tuneValues, getTuneValueRange(3208, s.server.erupeConfig.GameplayOptions.ExtraCarves)...) + tuneValues = append(tuneValues, getTuneValueRange(3546, s.server.erupeConfig.GameplayOptions.ExtraCarvesNC)...) + // get_hagi_rate_from_grank + tuneValues = append(tuneValues, getTuneValueRange(3234, s.server.erupeConfig.GameplayOptions.GExtraCarves)...) + tuneValues = append(tuneValues, getTuneValueRange(3572, s.server.erupeConfig.GameplayOptions.GExtraCarvesNC)...) + // get_nboost_transcend_rate_from_hrank + tuneValues = append(tuneValues, getTuneValueRange(3286, 200)...) + tuneValues = append(tuneValues, getTuneValueRange(3312, 300)...) + // get_nboost_transcend_rate_from_grank + tuneValues = append(tuneValues, getTuneValueRange(3299, 200)...) + tuneValues = append(tuneValues, getTuneValueRange(3325, 300)...) offset := uint16(time.Now().Unix()) bf.WriteUint16(offset) @@ -781,6 +553,14 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } +func getTuneValueRange(start uint16, value uint16) []tuneValue { + var tv []tuneValue + for i := uint16(0); i < 13; i++ { + tv = append(tv, tuneValue{start + i, value}) + } + return tv +} + func handleMsgMhfEnterTournamentQuest(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfGetUdBonusQuestInfo(s *Session, p mhfpacket.MHFPacket) { From 771f240d13286681ee68bb3144b86d9be85be01d Mon Sep 17 00:00:00 2001 From: wish Date: Sat, 10 Feb 2024 17:45:40 +1100 Subject: [PATCH 51/70] implement course 31 --- common/mhfcourse/mhfcourse.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/common/mhfcourse/mhfcourse.go b/common/mhfcourse/mhfcourse.go index 332c684dc..71ccc0ab7 100644 --- a/common/mhfcourse/mhfcourse.go +++ b/common/mhfcourse/mhfcourse.go @@ -75,7 +75,7 @@ func GetCourseStruct(rights uint32) ([]Course, uint32) { sort.Slice(s, func(i, j int) bool { return s[i].ID > s[j].ID }) - var normalCafeCourseSet, netcafeCourseSet bool + var normalCafeCourseSet, netcafeCourseSet, hidenCourseSet bool for _, course := range s { if rights-course.Value() < 0x80000000 { switch course.ID { @@ -92,6 +92,12 @@ func GetCourseStruct(rights uint32) ([]Course, uint32) { } netcafeCourseSet = true resp = append(resp, Course{ID: 30}) + case 10: + if hidenCourseSet { + break + } + hidenCourseSet = true + resp = append(resp, Course{ID: 31}) } course.Expiry = time.Date(2030, 1, 1, 0, 0, 0, 0, time.FixedZone("UTC+9", 9*60*60)) resp = append(resp, course) From 6ec9d9d8690f1b09c8fa22fdcbeb56976c305fef Mon Sep 17 00:00:00 2001 From: wish Date: Sun, 11 Feb 2024 23:22:42 +1100 Subject: [PATCH 52/70] add AutoBackportQuest DebugOption --- config.json | 1 + config/config.go | 1 + server/channelserver/handlers_quest.go | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/config.json b/config.json index 9bb641f72..c8705a661 100644 --- a/config.json +++ b/config.json @@ -36,6 +36,7 @@ "TournamentOverride": 0, "DisableTokenCheck": false, "QuestTools": false, + "AutoQuestBackport": true, "ProxyPort": 0, "CapLink": { "Values": [51728, 20000, 51729, 1, 20000], diff --git a/config/config.go b/config/config.go index d2038f150..b0094f628 100644 --- a/config/config.go +++ b/config/config.go @@ -118,6 +118,7 @@ type DebugOptions struct { TournamentOverride int // VS Tournament event status DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) QuestTools bool // Enable various quest debug logs + AutoQuestBackport bool // Automatically backport quest files ProxyPort uint16 // Forces the game to connect to a channel server proxy CapLink CapLinkOptions } diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index c8bd41f3b..148908dbf 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -2,6 +2,7 @@ package channelserver import ( "database/sql" + "encoding/binary" "erupe-ce/common/byteframe" "erupe-ce/common/decryption" ps "erupe-ce/common/pascalstring" @@ -21,6 +22,20 @@ type tuneValue struct { Value uint16 } +func BackportQuest(data []byte) []byte { + wp := binary.LittleEndian.Uint32(data[0:4]) + 96 + rp := wp + 4 + for i := uint32(0); i < 6; i++ { + if i != 0 { + wp += 4 + rp += 8 + } + copy(data[wp:wp+4], data[rp:rp+4]) + } + copy(data[wp:wp+180], data[rp:rp+180]) + return data +} + func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysGetFile) @@ -63,6 +78,9 @@ func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, data) return } + if _config.ErupeConfig.RealClientMode <= _config.Z1 && s.server.erupeConfig.DebugOptions.AutoQuestBackport { + data = BackportQuest(decryption.UnpackSimple(data)) + } doAckBufSucceed(s, pkt.AckHandle, data) } } @@ -124,6 +142,9 @@ func loadQuestFile(s *Session, questId int) []byte { } decrypted := decryption.UnpackSimple(file) + if _config.ErupeConfig.RealClientMode <= _config.Z1 && s.server.erupeConfig.DebugOptions.AutoQuestBackport { + decrypted = BackportQuest(decrypted) + } fileBytes := byteframe.NewByteFrameFromBytes(decrypted) fileBytes.SetLE() fileBytes.Seek(int64(fileBytes.ReadUint32()), 0) From 5f370896dff9f01df63fbe50859d4eac20624573 Mon Sep 17 00:00:00 2001 From: wish Date: Wed, 14 Feb 2024 17:28:01 +1100 Subject: [PATCH 53/70] clean up Tower responses --- schemas/patch-schema/02-tower.sql | 4 +-- server/channelserver/handlers_tower.go | 47 ++++++++++++++++---------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/schemas/patch-schema/02-tower.sql b/schemas/patch-schema/02-tower.sql index 0697fc2be..732f46c5e 100644 --- a/schemas/patch-schema/02-tower.sql +++ b/schemas/patch-schema/02-tower.sql @@ -7,8 +7,8 @@ CREATE TABLE IF NOT EXISTS tower ( tsp INT, block1 INT, block2 INT, - skills TEXT DEFAULT '0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0', - gems TEXT DEFAULT '0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0' + skills TEXT, + gems TEXT ); ALTER TABLE IF EXISTS guild_characters diff --git a/server/channelserver/handlers_tower.go b/server/channelserver/handlers_tower.go index 7f6bdb3b9..e075804e4 100644 --- a/server/channelserver/handlers_tower.go +++ b/server/channelserver/handlers_tower.go @@ -1,8 +1,10 @@ package channelserver import ( + _config "erupe-ce/config" "fmt" "go.uber.org/zap" + "strings" "time" "erupe-ce/common/byteframe" @@ -16,8 +18,8 @@ type TowerInfoTRP struct { } type TowerInfoSkill struct { - TSP int32 - Unk1 []int16 // 40 + TSP int32 + Skills []int16 // 64 } type TowerInfoHistory struct { @@ -32,6 +34,14 @@ type TowerInfoLevel struct { Unk3 int32 } +func EmptyTowerCSV(len int) string { + temp := make([]string, len) + for i := range temp { + temp[i] = "0" + } + return strings.Join(temp, ",") +} + func handleMsgMhfGetTowerInfo(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetTowerInfo) var data []*byteframe.ByteFrame @@ -44,21 +54,24 @@ func handleMsgMhfGetTowerInfo(s *Session, p mhfpacket.MHFPacket) { towerInfo := TowerInfo{ TRP: []TowerInfoTRP{{0, 0}}, - Skill: []TowerInfoSkill{{0, make([]int16, 40)}}, + Skill: []TowerInfoSkill{{0, make([]int16, 64)}}, History: []TowerInfoHistory{{make([]int16, 5), make([]int16, 5)}}, Level: []TowerInfoLevel{{0, 0, 0, 0}, {0, 0, 0, 0}}, } - tempSkills := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" - - err := s.server.db.QueryRow(`SELECT COALESCE(tr, 0), COALESCE(trp, 0), COALESCE(tsp, 0), COALESCE(block1, 0), COALESCE(block2, 0), skills FROM tower WHERE char_id=$1 - `, s.charID).Scan(&towerInfo.TRP[0].TR, &towerInfo.TRP[0].TRP, &towerInfo.Skill[0].TSP, &towerInfo.Level[0].Floors, &towerInfo.Level[1].Floors, &tempSkills) + var tempSkills string + err := s.server.db.QueryRow(`SELECT COALESCE(tr, 0), COALESCE(trp, 0), COALESCE(tsp, 0), COALESCE(block1, 0), COALESCE(block2, 0), COALESCE(skills, $1) FROM tower WHERE char_id=$2 + `, EmptyTowerCSV(64), s.charID).Scan(&towerInfo.TRP[0].TR, &towerInfo.TRP[0].TRP, &towerInfo.Skill[0].TSP, &towerInfo.Level[0].Floors, &towerInfo.Level[1].Floors, &tempSkills) if err != nil { s.server.db.Exec(`INSERT INTO tower (char_id) VALUES ($1)`, s.charID) } + if _config.ErupeConfig.RealClientMode <= _config.G7 { + towerInfo.Level = towerInfo.Level[:1] + } + for i, skill := range stringsupport.CSVElems(tempSkills) { - towerInfo.Skill[0].Unk1[i] = int16(skill) + towerInfo.Skill[0].Skills[i] = int16(skill) } switch pkt.InfoType { @@ -73,8 +86,8 @@ func handleMsgMhfGetTowerInfo(s *Session, p mhfpacket.MHFPacket) { for _, skills := range towerInfo.Skill { bf := byteframe.NewByteFrame() bf.WriteInt32(skills.TSP) - for i := range skills.Unk1 { - bf.WriteInt16(skills.Unk1[i]) + for i := range skills.Skills { + bf.WriteInt16(skills.Skills[i]) } data = append(data, bf) } @@ -89,7 +102,7 @@ func handleMsgMhfGetTowerInfo(s *Session, p mhfpacket.MHFPacket) { } data = append(data, bf) } - case 5: + case 3, 5: for _, level := range towerInfo.Level { bf := byteframe.NewByteFrame() bf.WriteInt32(level.Floors) @@ -123,8 +136,8 @@ func handleMsgMhfPostTowerInfo(s *Session, p mhfpacket.MHFPacket) { switch pkt.InfoType { case 2: - skills := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" - s.server.db.QueryRow(`SELECT skills FROM tower WHERE char_id=$1`, s.charID).Scan(&skills) + var skills string + s.server.db.QueryRow(`SELECT COALESCE(skills, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(64), s.charID).Scan(&skills) s.server.db.Exec(`UPDATE tower SET skills=$1, tsp=tsp-$2 WHERE char_id=$3`, stringsupport.CSVSetIndex(skills, int(pkt.Skill), stringsupport.CSVGetIndex(skills, int(pkt.Skill))+1), pkt.Cost, s.charID) case 1, 7: // This might give too much TSP? No idea what the rate is supposed to be @@ -412,8 +425,8 @@ func handleMsgMhfGetGemInfo(s *Session, p mhfpacket.MHFPacket) { gemInfo := []GemInfo{} gemHistory := []GemHistory{} - tempGems := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" - s.server.db.QueryRow(`SELECT gems FROM tower WHERE char_id=$1`, s.charID).Scan(&tempGems) + var tempGems string + s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$1`, EmptyTowerCSV(30), s.charID).Scan(&tempGems) for i, v := range stringsupport.CSVElems(tempGems) { gemInfo = append(gemInfo, GemInfo{uint16(((i / 5) * 256) + ((i % 5) + 1)), uint16(v)}) } @@ -455,8 +468,8 @@ func handleMsgMhfPostGemInfo(s *Session, p mhfpacket.MHFPacket) { ) } - gems := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" - s.server.db.QueryRow(`SELECT gems FROM tower WHERE char_id=$1`, s.charID).Scan(&gems) + var gems string + s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$1`, EmptyTowerCSV(30), s.charID).Scan(&gems) switch pkt.Op { case 1: // Add gem i := int(((pkt.Gem / 256) * 5) + (((pkt.Gem - ((pkt.Gem / 256) * 256)) - 1) % 5)) From 685f51ecb38d8e39ee7f9131b2cd4aa49498879c Mon Sep 17 00:00:00 2001 From: wish Date: Wed, 14 Feb 2024 18:03:56 +1100 Subject: [PATCH 54/70] clean up Tower responses --- server/channelserver/handlers_tower.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/channelserver/handlers_tower.go b/server/channelserver/handlers_tower.go index e075804e4..e6d4d8849 100644 --- a/server/channelserver/handlers_tower.go +++ b/server/channelserver/handlers_tower.go @@ -426,7 +426,7 @@ func handleMsgMhfGetGemInfo(s *Session, p mhfpacket.MHFPacket) { gemHistory := []GemHistory{} var tempGems string - s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$1`, EmptyTowerCSV(30), s.charID).Scan(&tempGems) + s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(30), s.charID).Scan(&tempGems) for i, v := range stringsupport.CSVElems(tempGems) { gemInfo = append(gemInfo, GemInfo{uint16(((i / 5) * 256) + ((i % 5) + 1)), uint16(v)}) } @@ -469,7 +469,7 @@ func handleMsgMhfPostGemInfo(s *Session, p mhfpacket.MHFPacket) { } var gems string - s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$1`, EmptyTowerCSV(30), s.charID).Scan(&gems) + s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(30), s.charID).Scan(&gems) switch pkt.Op { case 1: // Add gem i := int(((pkt.Gem / 256) * 5) + (((pkt.Gem - ((pkt.Gem / 256) * 256)) - 1) % 5)) From 79cdc28a01b52991e0c85c2e80311f2a8cc74214 Mon Sep 17 00:00:00 2001 From: wish Date: Wed, 14 Feb 2024 18:36:06 +1100 Subject: [PATCH 55/70] simplify Gem math --- server/channelserver/handlers_tower.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/channelserver/handlers_tower.go b/server/channelserver/handlers_tower.go index e6d4d8849..8f32a7882 100644 --- a/server/channelserver/handlers_tower.go +++ b/server/channelserver/handlers_tower.go @@ -428,7 +428,7 @@ func handleMsgMhfGetGemInfo(s *Session, p mhfpacket.MHFPacket) { var tempGems string s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(30), s.charID).Scan(&tempGems) for i, v := range stringsupport.CSVElems(tempGems) { - gemInfo = append(gemInfo, GemInfo{uint16(((i / 5) * 256) + ((i % 5) + 1)), uint16(v)}) + gemInfo = append(gemInfo, GemInfo{uint16((i / 5 << 8) + (i%5 + 1)), uint16(v)}) } switch pkt.Unk0 { @@ -472,7 +472,7 @@ func handleMsgMhfPostGemInfo(s *Session, p mhfpacket.MHFPacket) { s.server.db.QueryRow(`SELECT COALESCE(gems, $1) FROM tower WHERE char_id=$2`, EmptyTowerCSV(30), s.charID).Scan(&gems) switch pkt.Op { case 1: // Add gem - i := int(((pkt.Gem / 256) * 5) + (((pkt.Gem - ((pkt.Gem / 256) * 256)) - 1) % 5)) + i := int((pkt.Gem >> 8 * 5) + (pkt.Gem - pkt.Gem&0xFF00 - 1%5)) s.server.db.Exec(`UPDATE tower SET gems=$1 WHERE char_id=$2`, stringsupport.CSVSetIndex(gems, i, stringsupport.CSVGetIndex(gems, i)+int(pkt.Quantity)), s.charID) case 2: // Transfer gem // no way im doing this for now From 18cabd03f1af3cb9532d03c3f08224d905791c21 Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 19 Feb 2024 17:32:32 +1100 Subject: [PATCH 56/70] add version case to FestaInfo --- server/channelserver/handlers_festa.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index 4fc5370e1..86d704545 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -321,7 +321,9 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint16(100) // Normal rate bf.WriteUint16(50) // 50% penalty - ps.Uint16(bf, "", false) + if _config.ErupeConfig.RealClientMode >= _config.G52 { + ps.Uint16(bf, "", false) + } doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } From 377ff14a22980d59f4ed30c83f23d9a7f29cf821 Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 19 Feb 2024 17:34:01 +1100 Subject: [PATCH 57/70] use Monster enum on GetPaperData --- server/channelserver/handlers_data.go | 56 +++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/server/channelserver/handlers_data.go b/server/channelserver/handlers_data.go index 8844dbdd8..0f99b42f3 100644 --- a/server/channelserver/handlers_data.go +++ b/server/channelserver/handlers_data.go @@ -1042,34 +1042,34 @@ func handleMsgMhfGetPaperData(s *Session, p mhfpacket.MHFPacket) { {1105, 1, 10, 500, 0, 0, 0}, {1105, 2, 10, 500, 0, 0, 0}, // setServerBoss - {2001, 1, 17, 58, 0, 6, 700}, - {2001, 1, 20, 58, 0, 3, 200}, - {2001, 1, 22, 58, 0, 7, 250}, - {2001, 1, 27, 58, 0, 1, 100}, - {2001, 1, 53, 58, 0, 8, 1000}, - {2001, 1, 67, 58, 0, 9, 500}, - {2001, 1, 68, 58, 0, 2, 150}, - {2001, 1, 74, 58, 0, 4, 200}, - {2001, 1, 75, 58, 0, 5, 500}, - {2001, 1, 76, 58, 0, 10, 800}, - {2001, 1, 80, 58, 0, 11, 900}, - {2001, 1, 89, 58, 0, 12, 600}, - {2001, 2, 17, 60, 0, 6, 700}, - {2001, 2, 20, 60, 0, 3, 200}, - {2001, 2, 22, 60, 0, 7, 350}, - {2001, 2, 27, 60, 0, 1, 100}, - {2001, 2, 39, 60, 0, 13, 200}, - {2001, 2, 40, 60, 0, 15, 600}, - {2001, 2, 53, 60, 0, 8, 1000}, - {2001, 2, 67, 60, 0, 2, 500}, - {2001, 2, 68, 60, 0, 9, 150}, - {2001, 2, 74, 60, 0, 4, 200}, - {2001, 2, 75, 60, 0, 5, 500}, - {2001, 2, 76, 60, 0, 10, 800}, - {2001, 2, 80, 60, 0, 11, 900}, - {2001, 2, 81, 60, 0, 14, 900}, - {2001, 2, 89, 60, 0, 12, 600}, - {2001, 2, 94, 60, 0, 16, 1000}, + {2001, 1, mhfmon.Gravios, 58, 0, 6, 700}, + {2001, 1, mhfmon.Gypceros, 58, 0, 3, 200}, + {2001, 1, mhfmon.Basarios, 58, 0, 7, 250}, + {2001, 1, mhfmon.Velocidrome, 58, 0, 1, 100}, + {2001, 1, mhfmon.Rajang, 58, 0, 8, 1000}, + {2001, 1, mhfmon.ShogunCeanataur, 58, 0, 9, 500}, + {2001, 1, mhfmon.Bulldrome, 58, 0, 2, 150}, + {2001, 1, mhfmon.Hypnocatrice, 58, 0, 4, 200}, + {2001, 1, mhfmon.Lavasioth, 58, 0, 5, 500}, + {2001, 1, mhfmon.Tigrex, 58, 0, 10, 800}, + {2001, 1, mhfmon.Espinas, 58, 0, 11, 900}, + {2001, 1, mhfmon.Pariapuria, 58, 0, 12, 600}, + {2001, 2, mhfmon.Gravios, 60, 0, 6, 700}, + {2001, 2, mhfmon.Gypceros, 60, 0, 3, 200}, + {2001, 2, mhfmon.Basarios, 60, 0, 7, 350}, + {2001, 2, mhfmon.Velocidrome, 60, 0, 1, 100}, + {2001, 2, mhfmon.PurpleGypceros, 60, 0, 13, 200}, + {2001, 2, mhfmon.YianGaruga, 60, 0, 15, 600}, + {2001, 2, mhfmon.Rajang, 60, 0, 8, 1000}, + {2001, 2, mhfmon.ShogunCeanataur, 60, 0, 2, 500}, + {2001, 2, mhfmon.Bulldrome, 60, 0, 9, 150}, + {2001, 2, mhfmon.Hypnocatrice, 60, 0, 4, 200}, + {2001, 2, mhfmon.Lavasioth, 60, 0, 5, 500}, + {2001, 2, mhfmon.Tigrex, 60, 0, 10, 800}, + {2001, 2, mhfmon.Espinas, 60, 0, 11, 900}, + {2001, 2, mhfmon.BurningEspinas, 60, 0, 14, 900}, + {2001, 2, mhfmon.Pariapuria, 60, 0, 12, 600}, + {2001, 2, mhfmon.Dyuragaua, 60, 0, 16, 1000}, } case 6: paperData = []PaperData{ From 0b3e1f520f37cc9dffa2e23bbc98d569a5c1dfb6 Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 19 Feb 2024 17:34:08 +1100 Subject: [PATCH 58/70] use Monster enum on GetPaperData --- server/channelserver/handlers_data.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/channelserver/handlers_data.go b/server/channelserver/handlers_data.go index 0f99b42f3..fd41e1366 100644 --- a/server/channelserver/handlers_data.go +++ b/server/channelserver/handlers_data.go @@ -1,6 +1,7 @@ package channelserver import ( + "erupe-ce/common/mhfmon" "erupe-ce/common/stringsupport" _config "erupe-ce/config" "fmt" From 7549fe63e68530c3f07089e4b90e3d62567bcd02 Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 19 Feb 2024 17:35:24 +1100 Subject: [PATCH 59/70] conform Event Quest body size & auto change gathering points --- server/channelserver/handlers_quest.go | 73 ++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index 148908dbf..53251f9db 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -22,6 +22,32 @@ type tuneValue struct { Value uint16 } +func findSubSliceIndices(data []byte, sub []byte) []int { + var indices []int + lenSub := len(sub) + for i := 0; i < len(data); i++ { + if i+lenSub > len(data) { + break + } + if equal(data[i:i+lenSub], sub) { + indices = append(indices, i) + } + } + return indices +} + +func equal(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + func BackportQuest(data []byte) []byte { wp := binary.LittleEndian.Uint32(data[0:4]) + 96 rp := wp + 4 @@ -32,7 +58,33 @@ func BackportQuest(data []byte) []byte { } copy(data[wp:wp+4], data[rp:rp+4]) } - copy(data[wp:wp+180], data[rp:rp+180]) + + fillLength := uint32(108) + if _config.ErupeConfig.RealClientMode <= _config.S6 { + fillLength = 44 + } else if _config.ErupeConfig.RealClientMode <= _config.F5 { + fillLength = 52 + } else if _config.ErupeConfig.RealClientMode <= _config.G101 { + fillLength = 76 + } + + copy(data[wp:wp+fillLength], data[rp:rp+fillLength]) + if _config.ErupeConfig.RealClientMode <= _config.G91 { + patterns := [][]byte{ + {0x0A, 0x00, 0x01, 0x33, 0xD7, 0x00}, // 10% Armor Sphere -> Stone + {0x06, 0x00, 0x02, 0x33, 0xD8, 0x00}, // 6% Armor Sphere+ -> Iron Ore + {0x0A, 0x00, 0x03, 0x33, 0xD7, 0x00}, // 10% Adv Armor Sphere -> Stone + {0x06, 0x00, 0x04, 0x33, 0xDB, 0x00}, // 6% Hard Armor Sphere -> Dragonite Ore + {0x0A, 0x00, 0x05, 0x33, 0xD9, 0x00}, // 10% Heaven Armor Sphere -> Earth Crystal + {0x06, 0x00, 0x06, 0x33, 0xDB, 0x00}, // 6% True Armor Sphere -> Dragonite Ore + } + for i := range patterns { + j := findSubSliceIndices(data, patterns[i][0:4]) + for k := range j { + copy(data[j[k]+2:j[k]+4], patterns[i][4:6]) + } + } + } return data } @@ -149,21 +201,32 @@ func loadQuestFile(s *Session, questId int) []byte { fileBytes.SetLE() fileBytes.Seek(int64(fileBytes.ReadUint32()), 0) - // The 320 bytes directly following the data pointer must go directly into the event's body, after the header and before the string pointers. - questBody := byteframe.NewByteFrameFromBytes(fileBytes.ReadBytes(320)) + bodyLength := 320 + if _config.ErupeConfig.RealClientMode <= _config.S6 { + bodyLength = 160 + } else if _config.ErupeConfig.RealClientMode <= _config.F5 { + bodyLength = 168 + } else if _config.ErupeConfig.RealClientMode <= _config.G101 { + bodyLength = 192 + } else if _config.ErupeConfig.RealClientMode <= _config.Z1 { + bodyLength = 224 + } + + // The n bytes directly following the data pointer must go directly into the event's body, after the header and before the string pointers. + questBody := byteframe.NewByteFrameFromBytes(fileBytes.ReadBytes(uint(bodyLength))) questBody.SetLE() // Find the master quest string pointer questBody.Seek(40, 0) fileBytes.Seek(int64(questBody.ReadUint32()), 0) questBody.Seek(40, 0) // Overwrite it - questBody.WriteUint32(320) + questBody.WriteUint32(uint32(bodyLength)) questBody.Seek(0, 2) // Rewrite the quest strings and their pointers var tempString []byte newStrings := byteframe.NewByteFrame() - tempPointer := 352 + tempPointer := bodyLength + 32 for i := 0; i < 8; i++ { questBody.WriteUint32(uint32(tempPointer)) temp := int64(fileBytes.Index()) From 864586a40bec9ddab66817c3f3b5a240049edcdd Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 19 Feb 2024 17:37:40 +1100 Subject: [PATCH 60/70] exclude 0 values on EnumerateQuest --- server/channelserver/handlers_quest.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index 53251f9db..753e59040 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -569,8 +569,13 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { tuneValues = append(tuneValues, getTuneValueRange(3299, 200)...) tuneValues = append(tuneValues, getTuneValueRange(3325, 300)...) - offset := uint16(time.Now().Unix()) - bf.WriteUint16(offset) + var temp []tuneValue + for i := range tuneValues { + if tuneValues[i].Value > 0 { + temp = append(temp, tuneValues[i]) + } + } + tuneValues = temp tuneLimit := 770 if _config.ErupeConfig.RealClientMode <= _config.F5 { @@ -596,6 +601,9 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { tuneValues = tuneValues[:tuneLimit] } + offset := uint16(time.Now().Unix()) + bf.WriteUint16(offset) + bf.WriteUint16(uint16(len(tuneValues))) for i := range tuneValues { bf.WriteUint16(tuneValues[i].ID ^ offset) From 183f88654b528c421519622755145e3dbe23d3d0 Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 19 Feb 2024 18:12:51 +1100 Subject: [PATCH 61/70] fix InfoFesta response on S6.0 --- server/channelserver/handlers_festa.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index 86d704545..c2d6d1a30 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -234,7 +234,9 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint16(trial.Locale) bf.WriteUint16(trial.Reward) bf.WriteInt16(int16(FestivalColourCodes[trial.Monopoly])) - bf.WriteUint16(trial.Unk) + if _config.ErupeConfig.RealClientMode >= _config.F4 { // Not in S6.0 + bf.WriteUint16(trial.Unk) + } } // The Winner and Loser Armor IDs are missing From 9cfbd924546bf32d4c090374a4da4beb2ff4d9a0 Mon Sep 17 00:00:00 2001 From: wish Date: Mon, 19 Feb 2024 18:36:03 +1100 Subject: [PATCH 62/70] add blank Winner entries to InfoFesta --- server/channelserver/handlers_festa.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index c2d6d1a30..e7afcf33d 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -293,22 +293,22 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { } bf.WriteUint16(500) - categoryWinners := uint16(0) // NYI + categoryWinners := uint16(4) // 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 + bf.WriteUint32(0) // Guild ID + bf.WriteUint16(i + 1) // Category ID + bf.WriteInt16(int16(FestivalColourCodes[FestivalColourNone])) // Festa Team + ps.Uint8(bf, "", true) // Guild Name } - dailyWinners := uint16(0) // NYI + dailyWinners := uint16(7) // 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 + bf.WriteUint32(0) // Guild ID + bf.WriteUint16(i + 1) // Category ID + bf.WriteInt16(int16(FestivalColourCodes[FestivalColourNone])) // Festa Team + ps.Uint8(bf, "", true) // Guild Name } bf.WriteUint32(0) // Clan goal From b73f85ef4a346b46d16759c7ca8ec163cd523f2e Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 20 Feb 2024 00:33:38 +1100 Subject: [PATCH 63/70] add Quest timer toggle Chat Command --- config.json | 5 ++++ schemas/patch-schema/timer-toggle.sql | 5 ++++ server/channelserver/handlers_cast_binary.go | 25 ++++++++++++++++---- server/channelserver/sys_language.go | 10 ++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 schemas/patch-schema/timer-toggle.sql diff --git a/config.json b/config.json index c8705a661..48e166824 100644 --- a/config.json +++ b/config.json @@ -157,6 +157,11 @@ "Enabled": false, "Description": "Ban/Temp Ban a user", "Prefix": "ban" + }, { + "Name": "Timer", + "Enabled": true, + "Description": "Toggle the Quest timer", + "Prefix": "timer" } ], "Courses": [ diff --git a/schemas/patch-schema/timer-toggle.sql b/schemas/patch-schema/timer-toggle.sql new file mode 100644 index 000000000..c2bff008f --- /dev/null +++ b/schemas/patch-schema/timer-toggle.sql @@ -0,0 +1,5 @@ +BEGIN; + +ALTER TABLE users ADD COLUMN IF NOT EXISTS timer bool; + +END; \ No newline at end of file diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index c219e8a42..67e6e3a3a 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -144,6 +144,19 @@ func parseChatCommand(s *Session, command string) { } else { sendServerChatMessage(s, s.server.i18n.commands.noOp) } + case commands["Timer"].Prefix: + if commands["Timer"].Enabled || s.isOp() { + var state bool + s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&state) + s.server.db.Exec(`UPDATE users u SET timer=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, !state, s.charID) + if state { + sendServerChatMessage(s, s.server.i18n.commands.timer.disabled) + } else { + sendServerChatMessage(s, s.server.i18n.commands.timer.enabled) + } + } else { + sendDisabledCommandMessage(s, commands["Timer"]) + } case commands["PSN"].Prefix: if commands["PSN"].Enabled || s.isOp() { if len(args) > 1 { @@ -413,10 +426,14 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { if pkt.BroadcastType == 0x03 && pkt.MessageType == 0x03 && len(pkt.RawDataPayload) == 0x10 { if tmp.ReadUint16() == 0x0002 && tmp.ReadUint8() == 0x18 { - _ = tmp.ReadBytes(9) - tmp.SetLE() - frame := tmp.ReadUint32() - sendServerChatMessage(s, fmt.Sprintf("TIME : %d'%d.%03d (%dframe)", frame/30/60, frame/30%60, int(math.Round(float64(frame%30*100)/3)), frame)) + var timer bool + s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&timer) + if timer { + _ = tmp.ReadBytes(9) + tmp.SetLE() + frame := tmp.ReadUint32() + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.timer, frame/30/60/60, frame/30/60, frame/30%60, int(math.Round(float64(frame%30*100)/3)), frame)) + } } } diff --git a/server/channelserver/sys_language.go b/server/channelserver/sys_language.go index eae96dc85..aae8706bb 100644 --- a/server/channelserver/sys_language.go +++ b/server/channelserver/sys_language.go @@ -5,6 +5,7 @@ type i18n struct { cafe struct { reset string } + timer string commands struct { noOp string disabled string @@ -46,6 +47,10 @@ type i18n struct { error string length string } + timer struct { + enabled string + disabled string + } ravi struct { noCommand string start struct { @@ -102,6 +107,7 @@ func getLangStrings(s *Server) i18n { case "jp": i.language = "日本語" i.cafe.reset = "%d/%dにリセット" + i.timer = "タイマー:%02d'%02d\"%02d.%03d (%df)" i.commands.noOp = "You don't have permission to use this command" i.commands.disabled = "%sのコマンドは無効です" @@ -164,6 +170,7 @@ func getLangStrings(s *Server) i18n { default: i.language = "English" i.cafe.reset = "Resets on %d/%d" + i.timer = "Time: %02d:%02d:%02d.%03d (%df)" i.commands.noOp = "You don't have permission to use this command" i.commands.disabled = "%s command is disabled" @@ -192,6 +199,9 @@ func getLangStrings(s *Server) i18n { i.commands.ban.error = "Error in command. Format: %s [length]" i.commands.ban.length = " until %s" + i.commands.timer.enabled = "Quest timer enabled" + i.commands.timer.disabled = "Quest timer disabled" + i.commands.ravi.noCommand = "No Raviente command specified!" i.commands.ravi.start.success = "The Great Slaying will begin in a moment" i.commands.ravi.start.error = "The Great Slaying has already begun!" From df062613ebb495c7a43ed4a8cbe61aac58a0c6a6 Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 20 Feb 2024 03:08:48 +1100 Subject: [PATCH 64/70] add Save pointers for S6.0 --- server/channelserver/handlers_character.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server/channelserver/handlers_character.go b/server/channelserver/handlers_character.go index 9d90cc898..310fa0221 100644 --- a/server/channelserver/handlers_character.go +++ b/server/channelserver/handlers_character.go @@ -97,6 +97,17 @@ func getPointers() map[SavePointer]int { pointers[pGalleryData] = 72064 pointers[pGardenData] = 74424 pointers[pRP] = 74614 + case _config.S6: + pointers[pWeaponID] = 12522 + pointers[pWeaponType] = 12789 + pointers[pHouseTier] = 13900 + pointers[pToreData] = 14228 + pointers[pHRP] = 14550 + pointers[pHouseData] = 14561 + pointers[pBookshelfData] = 9118 // Probably same here + pointers[pGalleryData] = 24064 + pointers[pGardenData] = 26424 + pointers[pRP] = 26614 } if _config.ErupeConfig.RealClientMode == _config.G5 { pointers[lBookshelfData] = 5548 @@ -212,7 +223,7 @@ func (save *CharacterSaveData) updateStructWithSaveData() { save.Gender = false } if !save.IsNewCharacter { - if _config.ErupeConfig.RealClientMode >= _config.F4 { + if _config.ErupeConfig.RealClientMode >= _config.S6 { save.RP = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pRP] : save.Pointers[pRP]+2]) save.HouseTier = save.decompSave[save.Pointers[pHouseTier] : save.Pointers[pHouseTier]+5] save.HouseData = save.decompSave[save.Pointers[pHouseData] : save.Pointers[pHouseData]+195] From 1c4370b9299b03c92eb8e171e4015471d2dade89 Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 20 Feb 2024 04:12:22 +1100 Subject: [PATCH 65/70] fix EnumerateFestaMember prior to Z2 --- server/channelserver/handlers_festa.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index e7afcf33d..4b1929169 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -408,7 +408,12 @@ func handleMsgMhfEnumerateFestaMember(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint16(0) // Unk for _, member := range validMembers { bf.WriteUint32(member.CharID) - bf.WriteUint32(member.Souls) + if _config.ErupeConfig.RealClientMode <= _config.Z1 { + bf.WriteUint16(uint16(member.Souls)) + bf.WriteUint16(0) + } else { + bf.WriteUint32(member.Souls) + } } doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } From d22a7c782f2adff174582cbe1ac02d0cfbc9c152 Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 20 Feb 2024 04:16:26 +1100 Subject: [PATCH 66/70] changes to FestivalColor --- server/channelserver/handlers_festa.go | 16 +++---- server/channelserver/handlers_guild.go | 60 +++++++++++++------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index 4b1929169..530f3fc7b 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -141,13 +141,13 @@ func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 { } type FestaTrial struct { - ID uint32 `db:"id"` - Objective uint16 `db:"objective"` - GoalID uint32 `db:"goal_id"` - TimesReq uint16 `db:"times_req"` - Locale uint16 `db:"locale_req"` - Reward uint16 `db:"reward"` - Monopoly FestivalColour `db:"monopoly"` + ID uint32 `db:"id"` + Objective uint16 `db:"objective"` + GoalID uint32 `db:"goal_id"` + TimesReq uint16 `db:"times_req"` + Locale uint16 `db:"locale_req"` + Reward uint16 `db:"reward"` + Monopoly FestivalColor `db:"monopoly"` Unk uint16 } @@ -233,7 +233,7 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint16(trial.TimesReq) bf.WriteUint16(trial.Locale) bf.WriteUint16(trial.Reward) - bf.WriteInt16(int16(FestivalColourCodes[trial.Monopoly])) + bf.WriteInt16(FestivalColorCodes[trial.Monopoly]) if _config.ErupeConfig.RealClientMode >= _config.F4 { // Not in S6.0 bf.WriteUint16(trial.Unk) } diff --git a/server/channelserver/handlers_guild.go b/server/channelserver/handlers_guild.go index 43629e49a..7fc90cdf1 100644 --- a/server/channelserver/handlers_guild.go +++ b/server/channelserver/handlers_guild.go @@ -21,18 +21,18 @@ import ( "go.uber.org/zap" ) -type FestivalColour string +type FestivalColor string const ( - FestivalColourNone FestivalColour = "none" - FestivalColourBlue FestivalColour = "blue" - FestivalColourRed FestivalColour = "red" + FestivalColorNone FestivalColor = "none" + FestivalColorBlue FestivalColor = "blue" + FestivalColorRed FestivalColor = "red" ) -var FestivalColourCodes = map[FestivalColour]int8{ - FestivalColourNone: -1, - FestivalColourBlue: 0, - FestivalColourRed: 1, +var FestivalColorCodes = map[FestivalColor]int16{ + FestivalColorNone: -1, + FestivalColorBlue: 0, + FestivalColorRed: 1, } type GuildApplicationType string @@ -43,27 +43,27 @@ const ( ) type Guild struct { - ID uint32 `db:"id"` - Name string `db:"name"` - MainMotto uint8 `db:"main_motto"` - SubMotto uint8 `db:"sub_motto"` - CreatedAt time.Time `db:"created_at"` - MemberCount uint16 `db:"member_count"` - RankRP uint32 `db:"rank_rp"` - EventRP uint32 `db:"event_rp"` - Comment string `db:"comment"` - PugiName1 string `db:"pugi_name_1"` - PugiName2 string `db:"pugi_name_2"` - PugiName3 string `db:"pugi_name_3"` - PugiOutfit1 uint8 `db:"pugi_outfit_1"` - PugiOutfit2 uint8 `db:"pugi_outfit_2"` - PugiOutfit3 uint8 `db:"pugi_outfit_3"` - PugiOutfits uint32 `db:"pugi_outfits"` - Recruiting bool `db:"recruiting"` - FestivalColour FestivalColour `db:"festival_colour"` - Souls uint32 `db:"souls"` - AllianceID uint32 `db:"alliance_id"` - Icon *GuildIcon `db:"icon"` + ID uint32 `db:"id"` + Name string `db:"name"` + MainMotto uint8 `db:"main_motto"` + SubMotto uint8 `db:"sub_motto"` + CreatedAt time.Time `db:"created_at"` + MemberCount uint16 `db:"member_count"` + RankRP uint32 `db:"rank_rp"` + EventRP uint32 `db:"event_rp"` + Comment string `db:"comment"` + PugiName1 string `db:"pugi_name_1"` + PugiName2 string `db:"pugi_name_2"` + PugiName3 string `db:"pugi_name_3"` + PugiOutfit1 uint8 `db:"pugi_outfit_1"` + PugiOutfit2 uint8 `db:"pugi_outfit_2"` + PugiOutfit3 uint8 `db:"pugi_outfit_3"` + PugiOutfits uint32 `db:"pugi_outfits"` + Recruiting bool `db:"recruiting"` + FestivalColor FestivalColor `db:"festival_colour"` + Souls uint32 `db:"souls"` + AllianceID uint32 `db:"alliance_id"` + Icon *GuildIcon `db:"icon"` GuildLeader } @@ -967,7 +967,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint8(uint8(len(guildLeaderName))) bf.WriteBytes(guildName) bf.WriteBytes(guildComment) - bf.WriteInt8(FestivalColourCodes[guild.FestivalColour]) + bf.WriteInt8(int8(FestivalColorCodes[guild.FestivalColor])) bf.WriteUint32(guild.RankRP) bf.WriteBytes(guildLeaderName) bf.WriteUint32(0) // Unk From 5bcfe25ede474c2f7437f1143af5d61d56b98e30 Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 20 Feb 2024 04:18:16 +1100 Subject: [PATCH 67/70] implement Festa Bonus Categories & Guild Character optimisations --- network/mhfpacket/msg_mhf_charge_festa.go | 32 ++++----- schemas/patch-schema/festa-submissions.sql | 15 +++++ server/channelserver/handlers_festa.go | 65 ++++++++++++++----- server/channelserver/handlers_guild_member.go | 54 +++++++-------- 4 files changed, 103 insertions(+), 63 deletions(-) create mode 100644 schemas/patch-schema/festa-submissions.sql diff --git a/network/mhfpacket/msg_mhf_charge_festa.go b/network/mhfpacket/msg_mhf_charge_festa.go index f5452df73..145c2a3a7 100644 --- a/network/mhfpacket/msg_mhf_charge_festa.go +++ b/network/mhfpacket/msg_mhf_charge_festa.go @@ -1,19 +1,20 @@ package mhfpacket import ( - "errors" + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfChargeFesta represents the MSG_MHF_CHARGE_FESTA type MsgMhfChargeFesta struct { - AckHandle uint32 - FestaID uint32 - GuildID uint32 - Souls int + AckHandle uint32 + FestaID uint32 + GuildID uint32 + Souls []uint16 + Auto bool } // Opcode returns the ID associated with this packet type. @@ -23,15 +24,14 @@ func (m *MsgMhfChargeFesta) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfChargeFesta) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - m.AckHandle = bf.ReadUint32() - m.FestaID = bf.ReadUint32() - m.GuildID = bf.ReadUint32() - m.Souls = 0 - for i := bf.ReadUint16(); i > 0; i-- { - m.Souls += int(bf.ReadUint16()) - } - _ = bf.ReadUint8() // Unk - return nil + m.AckHandle = bf.ReadUint32() + m.FestaID = bf.ReadUint32() + m.GuildID = bf.ReadUint32() + for i := bf.ReadUint16(); i > 0; i-- { + m.Souls = append(m.Souls, bf.ReadUint16()) + } + m.Auto = bf.ReadBool() + return nil } // Build builds a binary packet from the current data. diff --git a/schemas/patch-schema/festa-submissions.sql b/schemas/patch-schema/festa-submissions.sql new file mode 100644 index 000000000..d720c587f --- /dev/null +++ b/schemas/patch-schema/festa-submissions.sql @@ -0,0 +1,15 @@ +BEGIN; + +CREATE TABLE festa_submissions ( + character_id int NOT NULL, + guild_id int NOT NULL, + trial_type int NOT NULL, + souls int NOT NULL, + timestamp timestamp with time zone NOT NULL +); + +ALTER TABLE guild_characters DROP COLUMN souls; + +ALTER TYPE festival_colour RENAME TO festival_color; + +END; \ No newline at end of file diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index 530f3fc7b..afab08dfb 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -95,8 +95,9 @@ func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) { 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_submissions") s.server.db.Exec("DELETE FROM festa_prizes_accepted") - s.server.db.Exec("UPDATE guild_characters SET souls=0, trial_vote=NULL") + s.server.db.Exec("UPDATE guild_characters SET trial_vote=NULL") } func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 { @@ -293,22 +294,45 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { } bf.WriteUint16(500) - categoryWinners := uint16(4) // NYI - bf.WriteUint16(categoryWinners) - for i := uint16(0); i < categoryWinners; i++ { - bf.WriteUint32(0) // Guild ID - bf.WriteUint16(i + 1) // Category ID - bf.WriteInt16(int16(FestivalColourCodes[FestivalColourNone])) // Festa Team - ps.Uint8(bf, "", true) // Guild Name + var temp uint32 + bf.WriteUint16(4) + for i := uint16(0); i < 4; i++ { + var guildID uint32 + var guildName string + var guildTeam = FestivalColorNone + s.server.db.QueryRow(` + SELECT fs.guild_id, g.name, fr.team, SUM(fs.souls) as _ + FROM festa_submissions fs + LEFT JOIN festa_registrations fr ON fs.guild_id = fr.guild_id + LEFT JOIN guilds g ON fs.guild_id = g.id + WHERE fs.trial_type = $1 + GROUP BY fs.guild_id, g.name, fr.team + ORDER BY _ DESC LIMIT 1 + `, i+1).Scan(&guildID, &guildName, &guildTeam, &temp) + bf.WriteUint32(guildID) + bf.WriteUint16(i + 1) + bf.WriteInt16(FestivalColorCodes[guildTeam]) + ps.Uint8(bf, guildName, true) } - - dailyWinners := uint16(7) // NYI - bf.WriteUint16(dailyWinners) - for i := uint16(0); i < dailyWinners; i++ { - bf.WriteUint32(0) // Guild ID - bf.WriteUint16(i + 1) // Category ID - bf.WriteInt16(int16(FestivalColourCodes[FestivalColourNone])) // Festa Team - ps.Uint8(bf, "", true) // Guild Name + bf.WriteUint16(7) + for i := uint16(0); i < 7; i++ { + var guildID uint32 + var guildName string + var guildTeam = FestivalColorNone + offset := 86400 * uint32(i) + s.server.db.QueryRow(` + SELECT fs.guild_id, g.name, fr.team, SUM(fs.souls) as _ + FROM festa_submissions fs + LEFT JOIN festa_registrations fr ON fs.guild_id = fr.guild_id + LEFT JOIN guilds g ON fs.guild_id = g.id + WHERE EXTRACT(EPOCH FROM fs.timestamp)::int > $1 AND EXTRACT(EPOCH FROM fs.timestamp)::int < $2 + GROUP BY fs.guild_id, g.name, fr.team + ORDER BY _ DESC LIMIT 1 + `, timestamps[1]+offset, timestamps[1]+offset+86400).Scan(&guildID, &guildName, &guildTeam, &temp) + bf.WriteUint32(guildID) + bf.WriteUint16(i + 1) + bf.WriteInt16(FestivalColorCodes[guildTeam]) + ps.Uint8(bf, guildName, true) } bf.WriteUint32(0) // Clan goal @@ -445,7 +469,14 @@ func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfChargeFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfChargeFesta) - s.server.db.Exec("UPDATE guild_characters SET souls=souls+$1 WHERE character_id=$2", pkt.Souls, s.charID) + tx, _ := s.server.db.Begin() + for i := range pkt.Souls { + if pkt.Souls[i] == 0 { + continue + } + _, _ = tx.Exec(`INSERT INTO festa_submissions VALUES ($1, $2, $3, $4, now())`, s.charID, pkt.GuildID, i, pkt.Souls[i]) + } + _ = tx.Commit() doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } diff --git a/server/channelserver/handlers_guild_member.go b/server/channelserver/handlers_guild_member.go index e053e2498..ac64e892a 100644 --- a/server/channelserver/handlers_guild_member.go +++ b/server/channelserver/handlers_guild_member.go @@ -61,41 +61,35 @@ func (gm *GuildMember) Save(s *Session) error { } const guildMembersSelectSQL = ` -SELECT - g.id as guild_id, - joined_at, - coalesce(souls, 0) as souls, - COALESCE(rp_today, 0) AS rp_today, - COALESCE(rp_yesterday, 0) AS rp_yesterday, - c.name, - character.character_id, - coalesce(gc.order_index, 0) as order_index, - c.last_login, - coalesce(gc.recruiter, false) as recruiter, - coalesce(gc.avoid_leadership, false) as avoid_leadership, - c.hrp, - c.gr, - c.weapon_id, - c.weapon_type, - character.is_applicant, - CASE WHEN g.leader_id = c.id THEN 1 ELSE 0 END as is_leader - FROM ( - SELECT character_id, true as is_applicant, guild_id - FROM guild_applications ga - WHERE ga.application_type = 'applied' - UNION - SELECT character_id, false as is_applicant, guild_id +SELECT * FROM ( + SELECT + g.id AS guild_id, + joined_at, + COALESCE((SELECT SUM(souls) FROM festa_submissions fs WHERE fs.character_id=c.id), 0) AS souls, + COALESCE(rp_today, 0) AS rp_today, + COALESCE(rp_yesterday, 0) AS rp_yesterday, + c.name, + c.id AS character_id, + COALESCE(order_index, 0) AS order_index, + c.last_login, + COALESCE(recruiter, false) AS recruiter, + COALESCE(avoid_leadership, false) AS avoid_leadership, + c.hrp, + c.gr, + c.weapon_id, + c.weapon_type, + EXISTS(SELECT 1 FROM guild_applications ga WHERE ga.character_id=c.id AND application_type='applied') AS is_applicant, + CASE WHEN g.leader_id = c.id THEN true ELSE false END AS is_leader FROM guild_characters gc - ) character - JOIN characters c on character.character_id = c.id - LEFT JOIN guild_characters gc ON gc.character_id = character.character_id - JOIN guilds g ON g.id = character.guild_id + LEFT JOIN characters c ON c.id = gc.character_id + LEFT JOIN guilds g ON g.id = gc.guild_id +) AS subquery ` func GetGuildMembers(s *Session, guildID uint32, applicants bool) ([]*GuildMember, error) { rows, err := s.server.db.Queryx(fmt.Sprintf(` %s - WHERE character.guild_id = $1 AND is_applicant = $2 + WHERE guild_id = $1 AND is_applicant = $2 `, guildMembersSelectSQL), guildID, applicants) if err != nil { @@ -121,7 +115,7 @@ func GetGuildMembers(s *Session, guildID uint32, applicants bool) ([]*GuildMembe } func GetCharacterGuildData(s *Session, charID uint32) (*GuildMember, error) { - rows, err := s.server.db.Queryx(fmt.Sprintf("%s WHERE character.character_id=$1", guildMembersSelectSQL), charID) + rows, err := s.server.db.Queryx(fmt.Sprintf("%s WHERE character_id=$1", guildMembersSelectSQL), charID) if err != nil { s.logger.Error(fmt.Sprintf("failed to retrieve membership data for character '%d'", charID)) From c5905d74d4d7c2f8b38d8a6f7d471855b38c62ae Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 20 Feb 2024 04:19:43 +1100 Subject: [PATCH 68/70] index Patch Schemas --- schemas/patch-schema/{op-accounts.sql => 17-op-accounts.sql} | 0 schemas/patch-schema/{timer-toggle.sql => 18-timer-toggle.sql} | 0 .../{festa-submissions.sql => 19-festa-submissions.sql} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename schemas/patch-schema/{op-accounts.sql => 17-op-accounts.sql} (100%) rename schemas/patch-schema/{timer-toggle.sql => 18-timer-toggle.sql} (100%) rename schemas/patch-schema/{festa-submissions.sql => 19-festa-submissions.sql} (100%) diff --git a/schemas/patch-schema/op-accounts.sql b/schemas/patch-schema/17-op-accounts.sql similarity index 100% rename from schemas/patch-schema/op-accounts.sql rename to schemas/patch-schema/17-op-accounts.sql diff --git a/schemas/patch-schema/timer-toggle.sql b/schemas/patch-schema/18-timer-toggle.sql similarity index 100% rename from schemas/patch-schema/timer-toggle.sql rename to schemas/patch-schema/18-timer-toggle.sql diff --git a/schemas/patch-schema/festa-submissions.sql b/schemas/patch-schema/19-festa-submissions.sql similarity index 100% rename from schemas/patch-schema/festa-submissions.sql rename to schemas/patch-schema/19-festa-submissions.sql From a968f18438d39907200c9a95cffa7b1b36e81e11 Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 20 Feb 2024 14:53:59 +1100 Subject: [PATCH 69/70] add support for SIGN requests --- server/signserver/session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/signserver/session.go b/server/signserver/session.go index 164ab70e2..e4cbd5537 100644 --- a/server/signserver/session.go +++ b/server/signserver/session.go @@ -54,7 +54,7 @@ func (s *Session) handlePacket(pkt []byte) error { bf := byteframe.NewByteFrameFromBytes(pkt) reqType := string(bf.ReadNullTerminatedBytes()) switch reqType[:len(reqType)-3] { - case "DLTSKEYSIGN:", "DSGN:": + case "DLTSKEYSIGN:", "DSGN:", "SIGN:": s.handleDSGN(bf) case "PS3SGN:": s.client = PS3 From c3409996efdbc4380fa0721ca2a7f36a2412f8b6 Mon Sep 17 00:00:00 2001 From: wish Date: Tue, 20 Feb 2024 15:02:10 +1100 Subject: [PATCH 70/70] rollback unknown Festa values --- server/channelserver/handlers_festa.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index afab08dfb..11a0670ef 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -399,10 +399,10 @@ func handleMsgMhfStateFestaG(s *Session, p mhfpacket.MHFPacket) { return } resp.WriteUint32(guild.Souls) - resp.WriteInt32(0) // unk + resp.WriteInt32(1) // unk resp.WriteInt32(1) // unk, rank? - resp.WriteInt32(0) // unk - resp.WriteInt32(0) // unk + resp.WriteInt32(1) // unk + resp.WriteInt32(1) // unk doAckBufSucceed(s, pkt.AckHandle, resp.Data()) }