diff --git a/bin/questlists/.gitkeep b/bin/questlists/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/common/token/token.go b/common/token/token.go new file mode 100644 index 000000000..73568bcbc --- /dev/null +++ b/common/token/token.go @@ -0,0 +1,13 @@ +package token + +import "math/rand" + +// Generate returns an alphanumeric token of specified length +func Generate(length int) string { + var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + b := make([]rune, length) + for i := range b { + b[i] = chars[rand.Intn(len(chars))] + } + return string(b) +} diff --git a/config.json b/config.json index 31ecd8416..b6967be68 100644 --- a/config.json +++ b/config.json @@ -1,16 +1,17 @@ { "Host": "127.0.0.1", "BinPath": "bin", + "Language": "en", "DisableSoftCrash": false, "FeaturedWeapons": 1, + "HideLoginNotice": true, + "LoginNotice": "
Welcome to Erupe SU9.1!
Erupe is experimental software, we are not liable for any
issues caused by installing the software!

■Report bugs on Discord!

■Test everything!

■Don't talk to softlocking NPCs!

■Fork the code on GitHub!

Thank you to all of the contributors,

this wouldn't exist without you.", + "PatchServerManifest": "", + "PatchServerFile": "", + "ScreenshotAPIURL": "", "DevMode": true, "DevModeOptions": { - "PatchServerManifest": "", - "PatchServerFile": "", "AutoCreateAccount": true, - "EnableLauncherServer": false, - "HideLoginNotice": false, - "LoginNotice": "
Welcome to Erupe SU9.1 Beta!
Erupe is experimental software, we are not liable for any
issues caused by installing the software!

■Report bugs on Discord!

■Test everything!

■Don't talk to softlocking NPCs!

■Fork the code on GitHub!

Thank you to all of the contributors,

this wouldn't exist without you.", "CleanDB": false, "MaxLauncherHR": false, "LogInboundMessages": false, @@ -63,13 +64,16 @@ ], "Courses": [ {"Name": "HunterLife", "Enabled": true}, - {"Name": "ExtraA", "Enabled": true}, + {"Name": "Extra", "Enabled": true}, {"Name": "Premium", "Enabled": true}, {"Name": "Assist", "Enabled": false}, - {"Name": "Netcafe", "Enabled": false}, + {"Name": "N", "Enabled": false}, {"Name": "Hiden", "Enabled": false}, {"Name": "HunterSupport", "Enabled": false}, - {"Name": "NetcafeBoost", "Enabled": false} + {"Name": "NBoost", "Enabled": false}, + {"Name": "NetCafe", "Enabled": true}, + {"Name": "HLRenewing", "Enabled": true}, + {"Name": "EXRenewing", "Enabled": true} ], "Database": { "Host": "localhost", @@ -87,6 +91,10 @@ "Enabled": true, "Port": 53312 }, + "SignV2": { + "Enabled": false, + "Port": 8080 + }, "Channel": { "Enabled": true }, diff --git a/config/config.go b/config/config.go index d2804b9fc..c2c66f925 100644 --- a/config/config.go +++ b/config/config.go @@ -12,11 +12,17 @@ import ( // Config holds the global server-wide config. type Config struct { - Host string `mapstructure:"Host"` - BinPath string `mapstructure:"BinPath"` - DisableSoftCrash bool // Disables the 'Press Return to exit' dialog allowing scripts to reboot the server automatically - FeaturedWeapons int // Number of Active Feature weapons to generate daily - DevMode bool + Host string `mapstructure:"Host"` + BinPath string `mapstructure:"BinPath"` + Language string + DisableSoftCrash bool // Disables the 'Press Return to exit' dialog allowing scripts to reboot the server automatically + FeaturedWeapons int // Number of Active Feature weapons to generate daily + HideLoginNotice bool // Hide the Erupe notice on login + LoginNotice string // MHFML string of the login notice displayed + PatchServerManifest string // Manifest patch server override + PatchServerFile string // File patch server override + ScreenshotAPIURL string // Destination for screenshots uploaded to BBS + DevMode bool DevModeOptions DevModeOptions Discord Discord @@ -25,30 +31,27 @@ type Config struct { Database Database Launcher Launcher Sign Sign + SignV2 SignV2 Channel Channel Entrance Entrance } // DevModeOptions holds various debug/temporary options for use while developing Erupe. type DevModeOptions struct { - PatchServerManifest string // Manifest patch server override - PatchServerFile string // File patch server override - AutoCreateAccount bool // Automatically create accounts if they don't exist - HideLoginNotice bool // Hide the Erupe notice on login - LoginNotice string // MHFML string of the login notice displayed - CleanDB bool // Automatically wipes the DB on server reset. - MaxLauncherHR bool // Sets the HR returned in the launcher to 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 - MaxHexdumpLength int // Maximum number of bytes printed when logs are enabled - DivaEvent int // Diva Defense event status - FestaEvent int // Hunter's Festa event status - TournamentEvent int // VS Tournament event status - MezFesEvent bool // MezFes status - MezFesAlt bool // Swaps out Volpakkun for Tokotoko - DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) - DisableMailItems bool // Hack to prevent english versions of MHF from crashing - QuestDebugTools bool // Enable various quest debug logs + 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 + MaxHexdumpLength int // Maximum number of bytes printed when logs are enabled + DivaEvent int // Diva Defense event status + FestaEvent int // Hunter's Festa event status + TournamentEvent int // VS Tournament event status + MezFesEvent bool // MezFes status + MezFesAlt bool // Swaps out Volpakkun for Tokotoko + DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) + DisableMailItems bool // Hack to prevent english versions of MHF from crashing + QuestDebugTools bool // Enable various quest debug logs SaveDumps SaveDumpOptions } @@ -99,6 +102,12 @@ type Sign struct { Port int } +// SignV2 holds the new sign server config +type SignV2 struct { + Enabled bool + Port int +} + type Channel struct { Enabled bool } diff --git a/main.go b/main.go index dcb3a14b4..d347f9859 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "erupe-ce/server/entranceserver" "erupe-ce/server/launcherserver" "erupe-ce/server/signserver" + "erupe-ce/server/signv2server" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -35,7 +36,7 @@ func main() { defer zapLogger.Sync() logger := zapLogger.Named("main") - logger.Info("Starting Erupe (9.1b)") + logger.Info("Starting Erupe (9.2b)") if config.ErupeConfig.Database.Password == "" { preventClose("Database password is blank") @@ -166,6 +167,22 @@ func main() { logger.Info("Started sign server") } + // New Sign server + var newSignServer *signv2server.Server + if config.ErupeConfig.SignV2.Enabled { + newSignServer = signv2server.NewServer( + &signv2server.Config{ + Logger: logger.Named("sign"), + ErupeConfig: config.ErupeConfig, + DB: db, + }) + err = newSignServer.Start() + if err != nil { + preventClose(fmt.Sprintf("Failed to start sign-v2 server: %s", err.Error())) + } + logger.Info("Started new sign server") + } + var channels []*channelserver.Server if config.ErupeConfig.Channel.Enabled { @@ -229,6 +246,10 @@ func main() { signServer.Shutdown() } + if config.ErupeConfig.SignV2.Enabled { + newSignServer.Shutdown() + } + if config.ErupeConfig.Entrance.Enabled { entranceServer.Shutdown() } diff --git a/network/mhfpacket/msg_mhf_apply_bbs_article.go b/network/mhfpacket/msg_mhf_apply_bbs_article.go index df0218ce0..d2b9c803b 100644 --- a/network/mhfpacket/msg_mhf_apply_bbs_article.go +++ b/network/mhfpacket/msg_mhf_apply_bbs_article.go @@ -1,15 +1,24 @@ package mhfpacket -import ( - "errors" +import ( + "errors" + "erupe-ce/common/bfutil" + "erupe-ce/common/stringsupport" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfApplyBbsArticle represents the MSG_MHF_APPLY_BBS_ARTICLE -type MsgMhfApplyBbsArticle struct{} +type MsgMhfApplyBbsArticle struct { + AckHandle uint32 + Unk0 uint32 + Unk1 []byte + Name string + Title string + Description string +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfApplyBbsArticle) Opcode() network.PacketID { @@ -18,7 +27,13 @@ func (m *MsgMhfApplyBbsArticle) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfApplyBbsArticle) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Unk0 = bf.ReadUint32() + m.Unk1 = bf.ReadBytes(16) + m.Name = stringsupport.SJISToUTF8(bfutil.UpToNull(bf.ReadBytes(32))) + m.Title = stringsupport.SJISToUTF8(bfutil.UpToNull(bf.ReadBytes(128))) + m.Description = stringsupport.SJISToUTF8(bfutil.UpToNull(bf.ReadBytes(256))) + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_check_monthly_item.go b/network/mhfpacket/msg_mhf_check_monthly_item.go index d79c65240..860725aa4 100644 --- a/network/mhfpacket/msg_mhf_check_monthly_item.go +++ b/network/mhfpacket/msg_mhf_check_monthly_item.go @@ -11,7 +11,8 @@ import ( // MsgMhfCheckMonthlyItem represents the MSG_MHF_CHECK_MONTHLY_ITEM type MsgMhfCheckMonthlyItem struct { AckHandle uint32 - Unk uint32 + Type uint8 + Unk []byte } // Opcode returns the ID associated with this packet type. @@ -22,7 +23,8 @@ func (m *MsgMhfCheckMonthlyItem) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfCheckMonthlyItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { m.AckHandle = bf.ReadUint32() - m.Unk = bf.ReadUint32() + m.Type = bf.ReadUint8() + m.Unk = bf.ReadBytes(3) return nil } diff --git a/network/mhfpacket/msg_mhf_get_bbs_sns_status.go b/network/mhfpacket/msg_mhf_get_bbs_sns_status.go index 07ab9e485..0c50f67e8 100644 --- a/network/mhfpacket/msg_mhf_get_bbs_sns_status.go +++ b/network/mhfpacket/msg_mhf_get_bbs_sns_status.go @@ -1,15 +1,18 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfGetBbsSnsStatus represents the MSG_MHF_GET_BBS_SNS_STATUS -type MsgMhfGetBbsSnsStatus struct{} +type MsgMhfGetBbsSnsStatus struct { + AckHandle uint32 + Unk []byte +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfGetBbsSnsStatus) Opcode() network.PacketID { @@ -18,7 +21,9 @@ func (m *MsgMhfGetBbsSnsStatus) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfGetBbsSnsStatus) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Unk = bf.ReadBytes(12) + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_mhf_get_bbs_user_status.go b/network/mhfpacket/msg_mhf_get_bbs_user_status.go index 69bcef671..9c44faa76 100644 --- a/network/mhfpacket/msg_mhf_get_bbs_user_status.go +++ b/network/mhfpacket/msg_mhf_get_bbs_user_status.go @@ -1,15 +1,18 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfGetBbsUserStatus represents the MSG_MHF_GET_BBS_USER_STATUS -type MsgMhfGetBbsUserStatus struct{} +type MsgMhfGetBbsUserStatus struct { + AckHandle uint32 + Unk []byte +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfGetBbsUserStatus) Opcode() network.PacketID { @@ -18,7 +21,9 @@ func (m *MsgMhfGetBbsUserStatus) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfGetBbsUserStatus) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Unk = bf.ReadBytes(12) + return nil } // Build builds a binary packet from the current data. diff --git a/network/mhfpacket/msg_sys_update_right.go b/network/mhfpacket/msg_sys_update_right.go index 0702a5b3e..99f0da4ae 100644 --- a/network/mhfpacket/msg_sys_update_right.go +++ b/network/mhfpacket/msg_sys_update_right.go @@ -2,12 +2,12 @@ package mhfpacket import ( "errors" - ps "erupe-ce/common/pascalstring" - "golang.org/x/exp/slices" - "erupe-ce/common/byteframe" + ps "erupe-ce/common/pascalstring" "erupe-ce/network" "erupe-ce/network/clientctx" + "golang.org/x/exp/slices" + "math" ) /* @@ -77,18 +77,27 @@ func (m *MsgSysUpdateRight) Build(bf *byteframe.ByteFrame, ctx *clientctx.Client func Courses() []Course { var courses = []Course{ - {[]string{"Trial", "TL"}, 1, 0x00000002}, - {[]string{"HunterLife", "HL"}, 2, 0x00000004}, - {[]string{"ExtraA", "Extra", "EX"}, 3, 0x00000008}, - {[]string{"ExtraB"}, 4, 0x00000010}, - {[]string{"Mobile"}, 5, 0x00000020}, - {[]string{"Premium"}, 6, 0x00000040}, - {[]string{"Pallone"}, 7, 0x00000080}, - {[]string{"Assist", "Legend", "Rasta"}, 8, 0x00000100}, // Legend - {[]string{"Netcafe", "N", "Cafe"}, 9, 0x00000200}, - {[]string{"Hiden", "Secret"}, 10, 0x00000400}, // Secret - {[]string{"HunterSupport", "HunterAid", "Support", "Royal", "Aid"}, 11, 0x00000800}, // Royal - {[]string{"NetcafeBoost", "NBoost", "Boost"}, 12, 0x00001000}, + {Aliases: []string{"Trial", "TL"}, ID: 1}, + {Aliases: []string{"HunterLife", "HL"}, ID: 2}, + {Aliases: []string{"Extra", "ExtraA", "EX"}, ID: 3}, + {Aliases: []string{"ExtraB"}, ID: 4}, + {Aliases: []string{"Mobile"}, ID: 5}, + {Aliases: []string{"Premium"}, ID: 6}, + {Aliases: []string{"Pallone", "ExtraC"}, ID: 7}, + {Aliases: []string{"Assist", "Legend", "Rasta"}, ID: 8}, // Legend + {Aliases: []string{"N"}, ID: 9}, + {Aliases: []string{"Hiden", "Secret"}, ID: 10}, // Secret + {Aliases: []string{"HunterSupport", "HunterAid", "Support", "Aid", "Royal"}, ID: 11}, // Royal + {Aliases: []string{"NBoost", "NetCafeBoost", "Boost"}, ID: 12}, + // 13-25 do nothing + {Aliases: []string{"NetCafe", "Cafe", "InternetCafe"}, ID: 26}, + {Aliases: []string{"HLRenewing", "HLR", "HLRenewal", "HLRenew"}, ID: 27}, + {Aliases: []string{"EXRenewing", "EXR", "EXRenewal", "EXRenew"}, ID: 28}, + {Aliases: []string{"Free"}, ID: 29}, + // 30 = real netcafe bit + } + for i := range courses { + courses[i].Value = uint32(math.Pow(2, float64(courses[i].ID))) } return courses } diff --git a/patch-schema/achievements.sql b/patch-schema/achievements.sql deleted file mode 100644 index 333ae5ed6..000000000 --- a/patch-schema/achievements.sql +++ /dev/null @@ -1,41 +0,0 @@ -BEGIN; - -CREATE TABLE IF NOT EXISTS public.achievements -( - id int NOT NULL PRIMARY KEY , - ach0 int DEFAULT 0, - ach1 int DEFAULT 0, - ach2 int DEFAULT 0, - ach3 int DEFAULT 0, - ach4 int DEFAULT 0, - ach5 int DEFAULT 0, - ach6 int DEFAULT 0, - ach7 int DEFAULT 0, - ach8 int DEFAULT 0, - ach9 int DEFAULT 0, - ach10 int DEFAULT 0, - ach11 int DEFAULT 0, - ach12 int DEFAULT 0, - ach13 int DEFAULT 0, - ach14 int DEFAULT 0, - ach15 int DEFAULT 0, - ach16 int DEFAULT 0, - ach17 int DEFAULT 0, - ach18 int DEFAULT 0, - ach19 int DEFAULT 0, - ach20 int DEFAULT 0, - ach21 int DEFAULT 0, - ach22 int DEFAULT 0, - ach23 int DEFAULT 0, - ach24 int DEFAULT 0, - ach25 int DEFAULT 0, - ach26 int DEFAULT 0, - ach27 int DEFAULT 0, - ach28 int DEFAULT 0, - ach29 int DEFAULT 0, - ach30 int DEFAULT 0, - ach31 int DEFAULT 0, - ach32 int DEFAULT 0 -); - -END; \ No newline at end of file diff --git a/patch-schema/active-feature.sql b/patch-schema/active-feature.sql deleted file mode 100644 index f8b835100..000000000 --- a/patch-schema/active-feature.sql +++ /dev/null @@ -1,9 +0,0 @@ -BEGIN; - -CREATE TABLE IF NOT EXISTS public.feature_weapon -( - start_time timestamp without time zone NOT NULL, - featured integer NOT NULL -); - -END; \ No newline at end of file diff --git a/patch-schema/festa.sql b/patch-schema/festa.sql deleted file mode 100644 index ce7d03594..000000000 --- a/patch-schema/festa.sql +++ /dev/null @@ -1,311 +0,0 @@ -BEGIN; - -CREATE TYPE event_type AS ENUM ('festa', 'diva', 'vs', 'mezfes'); - -DROP TABLE IF EXISTS public.event_week; - -ALTER TABLE IF EXISTS public.guild_characters - ADD COLUMN IF NOT EXISTS souls int DEFAULT 0; - -ALTER TABLE IF EXISTS public.guilds - DROP COLUMN IF EXISTS festival_colour; - -CREATE TABLE IF NOT EXISTS public.events -( - id serial NOT NULL PRIMARY KEY, - event_type event_type NOT NULL, - start_time timestamp without time zone NOT NULL DEFAULT now() -); - -CREATE TABLE IF NOT EXISTS public.festa_registrations -( - guild_id int NOT NULL, - team festival_colour NOT NULL -); - -CREATE TABLE IF NOT EXISTS public.festa_trials -( - id serial NOT NULL PRIMARY KEY, - objective int NOT NULL, - goal_id int NOT NULL, - times_req int NOT NULL, - locale_req int NOT NULL DEFAULT 0, - reward int NOT NULL -); - -CREATE TYPE prize_type AS ENUM ('personal', 'guild'); - -CREATE TABLE IF NOT EXISTS public.festa_prizes -( - id serial NOT NULL PRIMARY KEY, - type prize_type NOT NULL, - tier int NOT NULL, - souls_req int NOT NULL, - item_id int NOT NULL, - num_item int NOT NULL -); - -CREATE TABLE IF NOT EXISTS public.festa_prizes_accepted -( - prize_id int NOT NULL, - character_id int NOT NULL -); - --- Ripped prizes -INSERT INTO public.festa_prizes - (type, tier, souls_req, item_id, num_item) -VALUES - ('personal', 1, 1, 9647, 7), - ('personal', 2, 1, 9647, 7), - ('personal', 3, 1, 9647, 7), - ('personal', 1, 200, 11284, 4), - ('personal', 2, 200, 11284, 4), - ('personal', 3, 200, 11284, 4), - ('personal', 1, 400, 11381, 3), - ('personal', 2, 400, 11381, 3), - ('personal', 3, 400, 11381, 3), - ('personal', 1, 600, 11284, 8), - ('personal', 2, 600, 11284, 8), - ('personal', 3, 600, 11284, 8), - ('personal', 1, 800, 11384, 3), - ('personal', 2, 800, 11384, 3), - ('personal', 3, 800, 11384, 3), - ('personal', 1, 1000, 11284, 12), - ('personal', 2, 1000, 11284, 12), - ('personal', 3, 1000, 11284, 12), - ('personal', 1, 1200, 11381, 5), - ('personal', 2, 1200, 11381, 5), - ('personal', 3, 1200, 11381, 5), - ('personal', 1, 1400, 11284, 16), - ('personal', 2, 1400, 11284, 16), - ('personal', 3, 1400, 11284, 16), - ('personal', 1, 1700, 11384, 5), - ('personal', 2, 1700, 11384, 5), - ('personal', 3, 1700, 11384, 5), - ('personal', 1, 2000, 11284, 16), - ('personal', 2, 2000, 11284, 16), - ('personal', 3, 2000, 11284, 16), - ('personal', 1, 2500, 11382, 4), - ('personal', 2, 2500, 11382, 4), - ('personal', 3, 2500, 11382, 4), - ('personal', 1, 3000, 11284, 24), - ('personal', 2, 3000, 11284, 24), - ('personal', 3, 3000, 11284, 24), - ('personal', 1, 4000, 11385, 4), - ('personal', 2, 4000, 11385, 4), - ('personal', 3, 4000, 11385, 4), - ('personal', 1, 5000, 11381, 11), - ('personal', 2, 5000, 11381, 11), - ('personal', 3, 5000, 11381, 11), - ('personal', 1, 6000, 5177, 5), - ('personal', 2, 6000, 5177, 5), - ('personal', 3, 6000, 5177, 5), - ('personal', 1, 7000, 11384, 11), - ('personal', 2, 7000, 11384, 11), - ('personal', 3, 7000, 11384, 11), - ('personal', 1, 10000, 11382, 8), - ('personal', 2, 10000, 11382, 8), - ('personal', 3, 10000, 11382, 8), - ('personal', 1, 15000, 11385, 4), - ('personal', 2, 15000, 11385, 4), - ('personal', 3, 15000, 11385, 4), - ('personal', 1, 20000, 11381, 13), - ('personal', 2, 20000, 11381, 13), - ('personal', 3, 20000, 11381, 13), - ('personal', 1, 25000, 11385, 4), - ('personal', 2, 25000, 11385, 4), - ('personal', 3, 25000, 11385, 4), - ('personal', 1, 30000, 11383, 1), - ('personal', 2, 30000, 11383, 1), - ('personal', 3, 30000, 11383, 1); - -INSERT INTO public.festa_prizes -(type, tier, souls_req, item_id, num_item) -VALUES - ('guild', 1, 100, 7468, 5), - ('guild', 2, 100, 7468, 5), - ('guild', 3, 100, 7465, 5), - ('guild', 1, 300, 7469, 5), - ('guild', 2, 300, 7469, 5), - ('guild', 3, 300, 7466, 5), - ('guild', 1, 700, 7470, 5), - ('guild', 2, 700, 7470, 5), - ('guild', 3, 700, 7467, 5), - ('guild', 1, 1500, 13405, 14), - ('guild', 1, 1500, 1520, 3), - ('guild', 2, 1500, 13405, 14), - ('guild', 2, 1500, 1520, 3), - ('guild', 3, 1500, 7011, 3), - ('guild', 3, 1500, 13405, 14), - ('guild', 1, 3000, 10201, 10), - ('guild', 2, 3000, 10201, 10), - ('guild', 3, 3000, 10201, 10), - ('guild', 1, 6000, 13895, 14), - ('guild', 1, 6000, 1520, 6), - ('guild', 2, 6000, 13895, 14), - ('guild', 2, 6000, 1520, 6), - ('guild', 3, 6000, 13895, 14), - ('guild', 3, 6000, 7011, 4), - ('guild', 1, 12000, 13406, 14), - ('guild', 1, 12000, 1520, 9), - ('guild', 2, 12000, 13406, 14), - ('guild', 2, 12000, 1520, 9), - ('guild', 3, 12000, 13406, 14), - ('guild', 3, 12000, 7011, 5), - ('guild', 1, 25000, 10207, 10), - ('guild', 2, 25000, 10207, 10), - ('guild', 3, 25000, 10207, 10), - ('guild', 1, 50000, 1520, 12), - ('guild', 1, 50000, 13896, 14), - ('guild', 2, 50000, 1520, 12), - ('guild', 2, 50000, 13896, 14), - ('guild', 3, 50000, 7011, 6), - ('guild', 3, 50000, 13896, 14), - ('guild', 1, 100000, 10201, 10), - ('guild', 2, 100000, 10201, 10), - ('guild', 3, 100000, 10201, 10), - ('guild', 1, 200000, 13406, 16), - ('guild', 2, 200000, 13406, 16), - ('guild', 3, 200000, 13406, 16), - ('guild', 1, 300000, 13896, 16), - ('guild', 2, 300000, 13896, 16), - ('guild', 3, 300000, 13896, 16), - ('guild', 1, 400000, 10207, 10), - ('guild', 2, 400000, 10207, 10), - ('guild', 3, 400000, 10207, 10), - ('guild', 1, 500000, 13407, 6), - ('guild', 1, 500000, 13897, 6), - ('guild', 2, 500000, 13407, 6), - ('guild', 2, 500000, 13897, 6), - ('guild', 3, 500000, 13407, 6), - ('guild', 3, 500000, 13897, 6); - --- Ripped trials -INSERT INTO public.festa_trials - (objective, goal_id, times_req, locale_req, reward) -VALUES - (1,27,1,0,1), - (5,53034,0,0,400), - (5,22042,0,0,89), - (5,23397,0,0,89), - (1,28,1,0,1), - (1,68,1,0,1), - (1,6,1,0,2), - (1,38,1,0,2), - (1,20,1,0,3), - (1,39,1,0,4), - (1,48,1,0,4), - (1,67,1,0,4), - (1,93,1,0,4), - (1,22,1,0,5), - (1,52,1,0,5), - (1,101,1,0,5), - (1,1,1,0,5), - (1,37,1,0,5), - (1,15,1,0,5), - (1,45,1,0,5), - (1,74,1,0,5), - (1,78,1,0,5), - (1,103,1,0,5), - (1,51,1,0,6), - (1,17,1,0,6), - (1,21,1,0,6), - (1,92,1,0,6), - (1,47,1,0,7), - (1,46,1,0,7), - (1,26,1,0,7), - (1,14,1,0,7), - (1,11,1,0,7), - (1,44,1,0,8), - (1,43,1,0,8), - (1,49,1,0,8), - (1,40,1,0,8), - (1,76,1,0,8), - (1,89,1,0,8), - (1,94,1,0,8), - (1,96,1,0,8), - (1,75,1,0,8), - (1,91,1,0,8), - (1,53,1,0,9), - (1,80,1,0,9), - (1,42,1,0,9), - (1,79,1,0,9), - (1,81,1,0,10), - (1,41,1,0,10), - (1,82,1,0,10), - (1,90,1,0,10), - (1,149,1,0,10), - (1,85,1,0,11), - (1,95,1,0,11), - (1,121,1,0,11), - (1,142,1,0,11), - (1,141,1,0,11), - (1,146,1,0,12), - (1,147,1,0,12), - (1,148,1,0,12), - (1,151,1,0,12), - (1,152,1,0,12), - (1,159,1,0,12), - (1,153,1,0,12), - (1,162,1,0,12), - (1,111,1,0,13), - (1,110,1,0,13), - (1,112,1,0,13), - (1,109,1,0,14), - (1,169,1,0,15), - (2,33,1,0,6), - (2,104,1,0,8), - (2,119,1,0,8), - (2,120,1,0,8), - (2,54,1,0,8), - (2,59,1,0,8), - (2,64,1,0,8), - (2,65,1,0,8), - (2,99,1,0,9), - (2,83,1,0,9), - (2,84,1,0,10), - (2,77,1,0,10), - (2,106,1,0,10), - (2,55,1,0,10), - (2,58,1,0,10), - (2,7,1,0,10), - (2,50,1,0,11), - (2,131,1,0,11), - (2,129,1,0,11), - (2,140,1,0,11), - (2,122,1,0,11), - (2,126,1,0,11), - (2,127,1,0,11), - (2,128,1,0,11), - (2,130,1,0,11), - (2,139,1,0,11), - (2,144,1,0,11), - (2,150,1,0,11), - (2,158,1,0,11), - (2,164,1,0,15), - (2,165,1,0,15), - (2,2,1,7,15), - (2,36,1,0,15), - (2,71,1,0,15), - (2,108,1,0,15), - (2,116,1,0,15), - (2,107,1,0,15), - (2,154,1,0,17), - (2,166,1,0,17), - (2,170,1,0,18), - (3,31,1,0,1), - (3,8,1,0,3), - (3,123,1,0,8), - (3,105,1,0,9), - (3,125,1,0,11), - (3,115,1,0,12), - (3,114,1,0,12), - (3,161,1,0,12), - (4,670,1,0,1), - (4,671,1,0,1), - (4,672,1,0,1), - (4,675,1,0,1), - (4,673,1,0,1), - (4,674,1,0,1); - -END; \ No newline at end of file diff --git a/patch-schema/guild-enumeration.sql b/patch-schema/guild-enumeration.sql deleted file mode 100644 index 1e7e522a3..000000000 --- a/patch-schema/guild-enumeration.sql +++ /dev/null @@ -1,9 +0,0 @@ -BEGIN; - -ALTER TABLE IF EXISTS public.guilds - ADD COLUMN IF NOT EXISTS recruiting bool NOT NULL DEFAULT true; - -ALTER TABLE IF EXISTS public.guild_characters - ADD COLUMN IF NOT EXISTS recruiter bool NOT NULL DEFAULT false; - -END; \ No newline at end of file diff --git a/patch-schema/guild-poogie-outfits.sql b/patch-schema/guild-poogie-outfits.sql deleted file mode 100644 index b6ce2e4fa..000000000 --- a/patch-schema/guild-poogie-outfits.sql +++ /dev/null @@ -1,15 +0,0 @@ -BEGIN; - -ALTER TABLE IF EXISTS public.guilds - ADD COLUMN IF NOT EXISTS pugi_outfit_1 int NOT NULL DEFAULT 0; - -ALTER TABLE IF EXISTS public.guilds - ADD COLUMN IF NOT EXISTS pugi_outfit_2 int NOT NULL DEFAULT 0; - -ALTER TABLE IF EXISTS public.guilds - ADD COLUMN IF NOT EXISTS pugi_outfit_3 int NOT NULL DEFAULT 0; - -ALTER TABLE IF EXISTS public.guilds - ADD COLUMN IF NOT EXISTS pugi_outfits int NOT NULL DEFAULT 0; - -END; \ No newline at end of file diff --git a/patch-schema/mail-system-messages.sql b/patch-schema/mail-system-messages.sql deleted file mode 100644 index 4ce8dfaf6..000000000 --- a/patch-schema/mail-system-messages.sql +++ /dev/null @@ -1,13 +0,0 @@ -BEGIN; - -ALTER TABLE IF EXISTS public.mail - ADD COLUMN IF NOT EXISTS is_sys_message bool DEFAULT false; - -UPDATE mail SET is_sys_message=false; - -ALTER TABLE IF EXISTS public.mail - DROP CONSTRAINT IF EXISTS mail_sender_id_fkey; - -INSERT INTO public.characters (id, name) VALUES (0, ''); - -END; \ No newline at end of file diff --git a/patch-schema/mercenary.sql b/patch-schema/mercenary.sql deleted file mode 100644 index 9000db9f3..000000000 --- a/patch-schema/mercenary.sql +++ /dev/null @@ -1,7 +0,0 @@ -BEGIN; - -CREATE SEQUENCE IF NOT EXISTS public.rasta_id_seq; - -UPDATE characters SET savemercenary=NULL; - -END; \ No newline at end of file diff --git a/patch-schema/missing-mail.sql b/patch-schema/missing-mail.sql deleted file mode 100644 index 601c77d4f..000000000 --- a/patch-schema/missing-mail.sql +++ /dev/null @@ -1,6 +0,0 @@ -BEGIN; - -ALTER TABLE IF EXISTS public.mail - ADD COLUMN IF NOT EXISTS locked boolean NOT NULL DEFAULT false; - -END; \ No newline at end of file diff --git a/patch-schema/netcafe-2.sql b/patch-schema/netcafe-2.sql deleted file mode 100644 index d2a1f0763..000000000 --- a/patch-schema/netcafe-2.sql +++ /dev/null @@ -1,6 +0,0 @@ -BEGIN; - -ALTER TABLE IF EXISTS public.characters - ADD COLUMN cafe_reset timestamp without time zone; - -END; \ No newline at end of file diff --git a/patch-schema/netcafe.sql b/patch-schema/netcafe.sql deleted file mode 100644 index 563e10fb0..000000000 --- a/patch-schema/netcafe.sql +++ /dev/null @@ -1,30 +0,0 @@ -BEGIN; - -ALTER TABLE IF EXISTS public.characters - ADD COLUMN IF NOT EXISTS cafe_time integer DEFAULT 0; - -ALTER TABLE IF EXISTS public.characters - DROP COLUMN IF EXISTS netcafe_points; - -ALTER TABLE IF EXISTS public.characters - ADD COLUMN IF NOT EXISTS netcafe_points int DEFAULT 0; - -ALTER TABLE IF EXISTS public.characters - ADD COLUMN IF NOT EXISTS boost_time timestamp without time zone; - -CREATE TABLE IF NOT EXISTS public.cafebonus -( - id serial NOT NULL PRIMARY KEY, - time_req integer NOT NULL, - item_type integer NOT NULL, - item_id integer NOT NULL, - quantity integer NOT NULL -); - -CREATE TABLE IF NOT EXISTS public.cafe_accepted -( - cafe_id integer NOT NULL, - character_id integer NOT NULL -); - -END; \ No newline at end of file diff --git a/patch-schema/persistent-house.sql b/patch-schema/persistent-house.sql deleted file mode 100644 index 43a02da91..000000000 --- a/patch-schema/persistent-house.sql +++ /dev/null @@ -1,37 +0,0 @@ -BEGIN; - -CREATE TABLE IF NOT EXISTS public.user_binary -( - id serial NOT NULL PRIMARY KEY, - type2 bytea, - type3 bytea, - house_tier bytea, - house_state int, - house_password text, - house_data bytea, - house_furniture bytea, - bookshelf bytea, - gallery bytea, - tore bytea, - garden bytea, - mission bytea -); - --- Create entries for existing users -INSERT INTO public.user_binary (id) SELECT c.id FROM characters c; - --- Copy existing data -UPDATE public.user_binary - SET house_furniture = (SELECT house FROM characters WHERE user_binary.id = characters.id); - -UPDATE public.user_binary - SET mission = (SELECT trophy FROM characters WHERE user_binary.id = characters.id); - --- Drop old data location -ALTER TABLE public.characters - DROP COLUMN house; - -ALTER TABLE public.characters - DROP COLUMN trophy; - -END; \ No newline at end of file diff --git a/patch-schema/return.sql b/patch-schema/return.sql deleted file mode 100644 index 4e09d7e47..000000000 --- a/patch-schema/return.sql +++ /dev/null @@ -1,9 +0,0 @@ -BEGIN; - -ALTER TABLE IF EXISTS public.users - ADD COLUMN IF NOT EXISTS last_login timestamp without time zone; - -ALTER TABLE IF EXISTS public.users - ADD COLUMN IF NOT EXISTS return_expires timestamp without time zone; - -END; \ No newline at end of file diff --git a/patch-schema/revert-road-shop.sql b/patch-schema/revert-road-shop.sql deleted file mode 100644 index 830f3aa0f..000000000 --- a/patch-schema/revert-road-shop.sql +++ /dev/null @@ -1,9 +0,0 @@ -BEGIN; - -ALTER TABLE IF EXISTS public.normal_shop_items - DROP COLUMN IF EXISTS enable_weeks; - -ALTER TABLE IF EXISTS public.shop_item_state - DROP COLUMN IF EXISTS week; - -END; \ No newline at end of file diff --git a/patch-schema/road-leaderboard.sql b/patch-schema/road-leaderboard.sql deleted file mode 100644 index 8dfea2875..000000000 --- a/patch-schema/road-leaderboard.sql +++ /dev/null @@ -1,11 +0,0 @@ -BEGIN; - -CREATE TABLE IF NOT EXISTS rengoku_score ( - character_id integer PRIMARY KEY, - max_stages_mp integer, - max_points_mp integer, - max_stages_sp integer, - max_points_sp integer -); - -END; \ No newline at end of file diff --git a/patch-schema/servers_info.sql b/patch-schema/servers_info.sql deleted file mode 100644 index 06389b6ed..000000000 --- a/patch-schema/servers_info.sql +++ /dev/null @@ -1,23 +0,0 @@ ---adds world_name and land columns -BEGIN; - -CREATE TABLE IF NOT EXISTS public.servers -( - server_id integer NOT NULL, - season integer NOT NULL, - current_players integer NOT NULL, - world_name text COLLATE pg_catalog."default", - world_description text, - land integer -); - -ALTER TABLE public.servers - ADD COLUMN IF NOT EXISTS land integer; - -ALTER TABLE public.servers - ADD COLUMN IF NOT EXISTS world_name text COLLATE pg_catalog."default"; - -ALTER TABLE public.servers - ADD COLUMN IF NOT EXISTS world_description text; - -END; \ No newline at end of file diff --git a/patch-schema/stamps.sql b/patch-schema/stamps.sql deleted file mode 100644 index 2b940fa8c..000000000 --- a/patch-schema/stamps.sql +++ /dev/null @@ -1,13 +0,0 @@ -BEGIN; - -CREATE TABLE IF NOT EXISTS public.stamps ( - character_id integer PRIMARY KEY, - hl_total integer DEFAULT 0, - hl_redeemed integer DEFAULT 0, - hl_next timestamp without time zone, - ex_total integer DEFAULT 0, - ex_redeemed integer DEFAULT 0, - ex_next timestamp without time zone -); - -END; \ No newline at end of file diff --git a/patch-schema/titles.sql b/patch-schema/titles.sql deleted file mode 100644 index e4a87dc86..000000000 --- a/patch-schema/titles.sql +++ /dev/null @@ -1,11 +0,0 @@ -BEGIN; - -CREATE TABLE IF NOT EXISTS public.titles -( - id int NOT NULL, - char_id int NOT NULL, - unlocked_at timestamp without time zone, - updated_at timestamp without time zone -); - -END; \ No newline at end of file diff --git a/patch-schema/warehouse.sql b/patch-schema/warehouse.sql deleted file mode 100644 index 2f2a5adde..000000000 --- a/patch-schema/warehouse.sql +++ /dev/null @@ -1,49 +0,0 @@ -BEGIN; - -CREATE TABLE IF NOT EXISTS public.warehouse ( - character_id integer PRIMARY KEY, - item0 bytea, - item1 bytea, - item2 bytea, - item3 bytea, - item4 bytea, - item5 bytea, - item6 bytea, - item7 bytea, - item8 bytea, - item9 bytea, - item10 bytea, - item0name text, - item1name text, - item2name text, - item3name text, - item4name text, - item5name text, - item6name text, - item7name text, - item8name text, - item9name text, - equip0 bytea, - equip1 bytea, - equip2 bytea, - equip3 bytea, - equip4 bytea, - equip5 bytea, - equip6 bytea, - equip7 bytea, - equip8 bytea, - equip9 bytea, - equip10 bytea, - equip0name text, - equip1name text, - equip2name text, - equip3name text, - equip4name text, - equip5name text, - equip6name text, - equip7name text, - equip8name text, - equip9name text -); - -END; \ No newline at end of file diff --git a/server/channelserver/handlers.go b/server/channelserver/handlers.go index 5d19d93f4..d2ea7b678 100644 --- a/server/channelserver/handlers.go +++ b/server/channelserver/handlers.go @@ -77,7 +77,13 @@ func updateRights(s *Session) { 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) s.courses = mhfpacket.GetCourseStruct(rightsInt) rights := []mhfpacket.ClientRight{{1, 0, 0}} + var netcafeBitSet bool for _, course := range s.courses { + if (course.ID == 9 || course.ID == 26) && !netcafeBitSet { + netcafeBitSet = true + rightsInt += 0x40000000 // set netcafe bit + rights = append(rights, mhfpacket.ClientRight{ID: 30}) + } rights = append(rights, mhfpacket.ClientRight{ID: course.ID, Timestamp: 0x70DB59F0}) } update := &mhfpacket.MsgSysUpdateRight{ @@ -135,6 +141,7 @@ func handleMsgSysLogin(s *Session, p mhfpacket.MHFPacket) { s.charID = pkt.CharID0 s.token = pkt.LoginTokenString s.Unlock() + updateRights(s) bf := byteframe.NewByteFrame() bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // Unix timestamp @@ -213,16 +220,16 @@ func logoutPlayer(s *Session) { timePlayed += sessionTime var rpGained int - if s.FindCourse("Netcafe").ID != 0 { + if s.FindCourse("NetCafe").ID != 0 || s.FindCourse("N").ID != 0 { rpGained = timePlayed / 900 timePlayed = timePlayed % 900 + s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID) } else { rpGained = timePlayed / 1800 timePlayed = timePlayed % 1800 } s.server.db.Exec("UPDATE characters SET time_played = $1 WHERE id = $2", timePlayed, s.charID) - s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID) treasureHuntUnregister(s) @@ -1489,10 +1496,6 @@ func handleMsgMhfInfoScenarioCounter(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } -func handleMsgMhfGetBbsSnsStatus(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfApplyBbsArticle(s *Session, p mhfpacket.MHFPacket) {} - func handleMsgMhfGetEtcPoints(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetEtcPoints) diff --git a/server/channelserver/handlers_bbs.go b/server/channelserver/handlers_bbs.go new file mode 100644 index 000000000..222a8eadc --- /dev/null +++ b/server/channelserver/handlers_bbs.go @@ -0,0 +1,41 @@ +package channelserver + +import ( + "erupe-ce/common/byteframe" + "erupe-ce/common/stringsupport" + "erupe-ce/common/token" + "erupe-ce/network/mhfpacket" +) + +func handleMsgMhfGetBbsUserStatus(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetBbsUserStatus) + bf := byteframe.NewByteFrame() + bf.WriteUint32(200) + bf.WriteUint32(0) + bf.WriteUint32(0) + bf.WriteUint32(0) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfGetBbsSnsStatus(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetBbsSnsStatus) + bf := byteframe.NewByteFrame() + bf.WriteUint32(200) + bf.WriteUint32(401) + bf.WriteUint32(401) + bf.WriteUint32(0) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfApplyBbsArticle(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfApplyBbsArticle) + bf := byteframe.NewByteFrame() + articleToken := token.Generate(40) + bf.WriteUint32(200) + bf.WriteUint32(80) + bf.WriteUint32(0) + bf.WriteUint32(0) + bf.WriteBytes(stringsupport.PaddedString(articleToken, 64, false)) + bf.WriteBytes(stringsupport.PaddedString(s.server.erupeConfig.ScreenshotAPIURL, 64, false)) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} diff --git a/server/channelserver/handlers_cafe.go b/server/channelserver/handlers_cafe.go index 15b01f510..596bb5b33 100644 --- a/server/channelserver/handlers_cafe.go +++ b/server/channelserver/handlers_cafe.go @@ -74,9 +74,14 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) { var cafeReset time.Time err := s.server.db.QueryRow(`SELECT cafe_reset FROM characters WHERE id=$1`, s.charID).Scan(&cafeReset) + if err != nil { + cafeReset = TimeWeekNext() + s.server.db.Exec(`UPDATE characters SET cafe_reset=$1 WHERE id=$2`, cafeReset, s.charID) + } if Time_Current_Adjusted().After(cafeReset) { cafeReset = TimeWeekNext() - s.server.db.Exec(`UPDATE characters SET cafe_time=0, cafe_reset=$1 WHERE id=$2; DELETE FROM cafe_accepted WHERE character_id=$2`, cafeReset, s.charID) + s.server.db.Exec(`UPDATE characters SET cafe_time=0, cafe_reset=$1 WHERE id=$2`, cafeReset, s.charID) + s.server.db.Exec(`DELETE FROM cafe_accepted WHERE character_id=$1`, s.charID) } var cafeTime uint32 @@ -84,10 +89,13 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) { if err != nil { panic(err) } - cafeTime = uint32(Time_Current_Adjusted().Unix()) - uint32(s.sessionStart) + cafeTime + if s.FindCourse("NetCafe").ID != 0 || s.FindCourse("N").ID != 0 { + cafeTime = uint32(Time_Current_Adjusted().Unix()) - uint32(s.sessionStart) + cafeTime + } bf.WriteUint32(cafeTime) // Total cafe time bf.WriteUint16(0) - ps.Uint16(bf, fmt.Sprintf("Resets on %s %d", cafeReset.Month().String(), cafeReset.Day()), true) + ps.Uint16(bf, fmt.Sprintf(s.server.dict["cafeReset"], 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 64666d9af..17a1d7858 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -324,7 +324,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { for _, alias := range course.Aliases { if strings.ToLower(name) == strings.ToLower(alias) { if slices.Contains(s.server.erupeConfig.Courses, config.Course{Name: course.Aliases[0], Enabled: true}) { - if s.FindCourse(name).Value != 0 { + if s.FindCourse(name).ID != 0 { ei := slices.IndexFunc(s.courses, func(c mhfpacket.Course) bool { for _, alias := range c.Aliases { if strings.ToLower(name) == strings.ToLower(alias) { @@ -335,20 +335,20 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { }) if ei != -1 { s.courses = append(s.courses[:ei], s.courses[ei+1:]...) - sendServerChatMessage(s, fmt.Sprintf(`%s Course disabled.`, course.Aliases[0])) + sendServerChatMessage(s, fmt.Sprintf(`%s Course disabled`, course.Aliases[0])) } } else { s.courses = append(s.courses, course) - sendServerChatMessage(s, fmt.Sprintf(`%s Course enabled.`, course.Aliases[0])) + sendServerChatMessage(s, fmt.Sprintf(`%s Course enabled`, course.Aliases[0])) } var newInt uint32 for _, course := range s.courses { - newInt += course.Value + newInt += uint32(math.Pow(2, float64(course.ID))) } s.server.db.Exec("UPDATE users u SET rights=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", newInt, s.charID) updateRights(s) } else { - sendServerChatMessage(s, fmt.Sprintf(`%s Course is locked.`, course.Aliases[0])) + sendServerChatMessage(s, fmt.Sprintf(`%s Course is locked`, course.Aliases[0])) } } } diff --git a/server/channelserver/handlers_guild.go b/server/channelserver/handlers_guild.go index a6129bd1e..0e670bd14 100644 --- a/server/channelserver/handlers_guild.go +++ b/server/channelserver/handlers_guild.go @@ -2005,6 +2005,7 @@ func handleMsgMhfCheckMonthlyItem(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfCheckMonthlyItem) doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x01}) // TODO: Implement month-by-month tracker, 0 = Not claimed, 1 = Claimed + // Also handles HLC and EXC items, IDs = 064D, 076B } func handleMsgMhfAcquireMonthlyItem(s *Session, p mhfpacket.MHFPacket) { diff --git a/server/channelserver/handlers_guild_scout.go b/server/channelserver/handlers_guild_scout.go index f47c28bc6..4b45cc93b 100644 --- a/server/channelserver/handlers_guild_scout.go +++ b/server/channelserver/handlers_guild_scout.go @@ -62,10 +62,9 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { mail := &Mail{ SenderID: s.charID, RecipientID: pkt.CharID, - Subject: "Invitation!", + Subject: s.server.dict["guildInviteName"], Body: fmt.Sprintf( - "%s has invited you to join 「%s」\nDo you want to accept?", - getCharacterName(s, s.charID), + s.server.dict["guildInvite"], guildInfo.Name, ), IsGuildInvite: true, @@ -149,30 +148,30 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) { err = guild.AcceptApplication(s, s.charID) mail = append(mail, Mail{ RecipientID: s.charID, - Subject: "Success!", - Body: fmt.Sprintf("You successfully joined 「%s」.", guild.Name), + Subject: s.server.dict["guildInviteSuccessName"], + Body: fmt.Sprintf(s.server.dict["guildInviteSuccess"], guild.Name), IsSystemMessage: true, }) mail = append(mail, Mail{ SenderID: s.charID, RecipientID: pkt.LeaderID, - Subject: "Accepted", - Body: fmt.Sprintf("%s accepted your invitation to join 「%s」.", s.Name, guild.Name), + Subject: s.server.dict["guildInviteAcceptedName"], + Body: fmt.Sprintf(s.server.dict["guildInviteAccepted"], guild.Name), IsSystemMessage: true, }) } else { err = guild.RejectApplication(s, s.charID) mail = append(mail, Mail{ RecipientID: s.charID, - Subject: "Declined", - Body: fmt.Sprintf("You declined the invitation to join 「%s」.", guild.Name), + Subject: s.server.dict["guildInviteRejectName"], + Body: fmt.Sprintf(s.server.dict["guildInviteReject"], guild.Name), IsSystemMessage: true, }) mail = append(mail, Mail{ SenderID: s.charID, RecipientID: pkt.LeaderID, - Subject: "Declined", - Body: fmt.Sprintf("%s declined your invitation to join 「%s」.", s.Name, guild.Name), + Subject: s.server.dict["guildInviteDeclined"], + Body: fmt.Sprintf(s.server.dict["guildInviteDeclined"], guild.Name), IsSystemMessage: true, }) } diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index 347b17ca8..e88262cbd 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -2,13 +2,13 @@ package channelserver import ( "fmt" - "go.uber.org/zap" "io" "os" "path/filepath" "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" + "go.uber.org/zap" ) func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) { diff --git a/server/channelserver/handlers_users.go b/server/channelserver/handlers_users.go index c360b82a6..fc8b47a8d 100644 --- a/server/channelserver/handlers_users.go +++ b/server/channelserver/handlers_users.go @@ -54,5 +54,3 @@ func handleMsgSysGetUserBinary(s *Session, p mhfpacket.MHFPacket) { } func handleMsgSysNotifyUserBinary(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfGetBbsUserStatus(s *Session, p mhfpacket.MHFPacket) {} diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 82f29d03a..7c7447dfa 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -52,6 +52,9 @@ type Server struct { stagesLock sync.RWMutex stages map[string]*Stage + // Used to map different languages + dict map[string]string + // UserBinary userBinaryPartsLock sync.RWMutex userBinaryParts map[userBinaryPartID][]byte @@ -164,6 +167,8 @@ func NewServer(config *Config) *Server { // MezFes s.stages["sl1Ns462p0a0u0"] = NewStage("sl1Ns462p0a0u0") + s.dict = getLangStrings(s) + return s } @@ -279,6 +284,7 @@ func (s *Server) WorldcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session, bf := byteframe.NewByteFrame() bf.WriteUint16(uint16(pkt.Opcode())) pkt.Build(bf, session.clientContext) + bf.WriteUint16(0x0010) session.QueueSendNonBlocking(bf.Data()) } } @@ -313,15 +319,17 @@ func (s *Server) BroadcastRaviente(ip uint32, port uint16, stage []byte, _type u var text string switch _type { case 2: - text = " is being held!" + text = s.dict["ravienteBerserk"] + case 3: + text = s.dict["ravienteExtreme"] case 4: - text = " is being held!" + text = s.dict["ravienteExtremeLimited"] case 5: - text = " is being held!" + text = s.dict["ravienteBerserkSmall"] default: s.logger.Error("Unk raviente type", zap.Uint8("_type", _type)) } - ps.Uint16(bf, text, false) + ps.Uint16(bf, text, true) bf.WriteBytes([]byte{0x5F, 0x53, 0x00}) bf.WriteUint32(ip) // IP address bf.WriteUint16(port) // Port diff --git a/server/channelserver/sys_language.go b/server/channelserver/sys_language.go new file mode 100644 index 000000000..1dc917559 --- /dev/null +++ b/server/channelserver/sys_language.go @@ -0,0 +1,52 @@ +package channelserver + +func getLangStrings(s *Server) map[string]string { + strings := make(map[string]string) + switch s.erupeConfig.Language { + case "jp": + strings["language"] = "日本語" + strings["cafeReset"] = "%d/%dにリセット" + strings["ravienteBerserk"] = "<大討伐:猛狂期>が開催されました!" + strings["ravienteExtreme"] = "<大討伐:猛狂期【極】>が開催されました!" + strings["ravienteExtremeLimited"] = "<大討伐:猛狂期【極】(制限付)>が開催されました!" + strings["ravienteBerserkSmall"] = "<大討伐:猛狂期(小数)>が開催されました!" + + strings["guildInviteName"] = "猟団勧誘のご案内" + strings["guildInvite"] = "猟団「%s」からの勧誘通知です。\n「勧誘に返答」より、返答を行ってください。" + + strings["guildInviteSuccessName"] = "成功" + strings["guildInviteSuccess"] = "あなたは「%s」に参加できました。" + + strings["guildInviteAcceptedName"] = "承諾されました" + strings["guildInviteAccepted"] = "招待した狩人が「%s」への招待を承諾しました。" + + strings["guildInviteRejectName"] = "却下しました" + strings["guildInviteReject"] = "あなたは「%s」への参加を却下しました。" + + strings["guildInviteDeclinedName"] = "辞退しました" + strings["guildInviteDeclined"] = "招待した狩人が「%s」への招待を辞退しました。" + default: + strings["language"] = "English" + strings["cafeReset"] = "Resets on %d/%d" + strings["ravienteBerserk"] = " is being held!" + strings["ravienteExtreme"] = " is being held!" + strings["ravienteExtremeLimited"] = " is being held!" + strings["ravienteBerserkSmall"] = " is being held!" + + strings["guildInviteName"] = "Invitation!" + strings["guildInvite"] = "You have been invited to join\n「%s」\nDo you want to accept?" + + strings["guildInviteSuccessName"] = "Success!" + strings["guildInviteSuccess"] = "You have successfully joined\n「%s」." + + strings["guildInviteAcceptedName"] = "Accepted" + strings["guildInviteAccepted"] = "The recipient accepted your invitation to join\n「%s」." + + strings["guildInviteRejectName"] = "Rejected" + strings["guildInviteReject"] = "You rejected the invitation to join\n「%s」." + + strings["guildInviteDeclinedName"] = "Declined" + strings["guildInviteDeclined"] = "The recipient declined your invitation to join\n「%s」." + } + return strings +} diff --git a/server/channelserver/sys_session.go b/server/channelserver/sys_session.go index 39a96f40f..2ebc6d080 100644 --- a/server/channelserver/sys_session.go +++ b/server/channelserver/sys_session.go @@ -116,10 +116,9 @@ func (s *Session) QueueSend(data []byte) { func (s *Session) QueueSendNonBlocking(data []byte) { select { case s.sendPackets <- packet{data, true}: - // Enqueued data + s.logMessage(binary.BigEndian.Uint16(data[0:2]), data, "Server", s.Name) default: s.logger.Warn("Packet queue too full, dropping!") - // Queue too full } } diff --git a/server/signserver/dsgn_resp.go b/server/signserver/dsgn_resp.go index 3966af8d6..9335de0c0 100644 --- a/server/signserver/dsgn_resp.go +++ b/server/signserver/dsgn_resp.go @@ -4,6 +4,7 @@ import ( "erupe-ce/common/byteframe" ps "erupe-ce/common/pascalstring" "erupe-ce/common/stringsupport" + "erupe-ce/common/token" "erupe-ce/server/channelserver" "fmt" "math/rand" @@ -18,15 +19,6 @@ func makeSignInFailureResp(respID RespID) []byte { return bf.Data() } -func randSeq(n int) string { - var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } - return string(b) -} - func (s *Session) makeSignInResp(uid int) []byte { returnExpiry := s.server.getReturnExpiry(uid) @@ -37,13 +29,13 @@ func (s *Session) makeSignInResp(uid int) []byte { } rand.Seed(time.Now().UnixNano()) - token := randSeq(16) - s.server.registerToken(uid, token) + sessToken := token.Generate(16) + s.server.registerToken(uid, sessToken) bf := byteframe.NewByteFrame() bf.WriteUint8(1) // resp_code - if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.PatchServerManifest != "" && s.server.erupeConfig.DevModeOptions.PatchServerFile != "" { + if s.server.erupeConfig.DevMode && s.server.erupeConfig.PatchServerManifest != "" && s.server.erupeConfig.PatchServerFile != "" { bf.WriteUint8(2) } else { bf.WriteUint8(0) @@ -51,12 +43,12 @@ func (s *Session) makeSignInResp(uid int) []byte { bf.WriteUint8(1) // entrance server count bf.WriteUint8(uint8(len(chars))) // character count bf.WriteUint32(0xFFFFFFFF) // login_token_number - bf.WriteBytes([]byte(token)) // login_token + bf.WriteBytes([]byte(sessToken)) // login_token bf.WriteUint32(uint32(time.Now().Unix())) // current time if s.server.erupeConfig.DevMode { - if s.server.erupeConfig.DevModeOptions.PatchServerManifest != "" && s.server.erupeConfig.DevModeOptions.PatchServerFile != "" { - ps.Uint8(bf, s.server.erupeConfig.DevModeOptions.PatchServerManifest, false) - ps.Uint8(bf, s.server.erupeConfig.DevModeOptions.PatchServerFile, false) + if s.server.erupeConfig.PatchServerManifest != "" && s.server.erupeConfig.PatchServerFile != "" { + ps.Uint8(bf, s.server.erupeConfig.PatchServerManifest, false) + ps.Uint8(bf, s.server.erupeConfig.PatchServerFile, false) } } ps.Uint8(bf, fmt.Sprintf("%s:%d", s.server.erupeConfig.Host, s.server.erupeConfig.Entrance.Port), false) @@ -111,11 +103,11 @@ func (s *Session) makeSignInResp(uid int) []byte { } } - if s.server.erupeConfig.DevModeOptions.HideLoginNotice { + if s.server.erupeConfig.HideLoginNotice { bf.WriteUint8(0) } else { bf.WriteUint8(1) // Notice count - noticeText := s.server.erupeConfig.DevModeOptions.LoginNotice + noticeText := s.server.erupeConfig.LoginNotice ps.Uint32(bf, noticeText, true) } diff --git a/server/signv2server/dbutils.go b/server/signv2server/dbutils.go new file mode 100644 index 000000000..3c8494c95 --- /dev/null +++ b/server/signv2server/dbutils.go @@ -0,0 +1,122 @@ +package signv2server + +import ( + "context" + "database/sql" + "erupe-ce/common/token" + "fmt" + "time" + + "golang.org/x/crypto/bcrypt" +) + +func (s *Server) createNewUser(ctx context.Context, username string, password string) (int, error) { + // Create salted hash of user password + passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return 0, err + } + + var id int + err = s.db.QueryRowContext( + ctx, ` + INSERT INTO users (username, password, return_expires) + VALUES ($1, $2, $3) + RETURNING id + `, + username, string(passwordHash), time.Now().Add(time.Hour*24*30), + ).Scan(&id) + return id, err +} + +func (s *Server) createLoginToken(ctx context.Context, uid int) (string, error) { + loginToken := token.Generate(16) + _, err := s.db.ExecContext(ctx, "INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2)", uid, loginToken) + if err != nil { + return "", err + } + return loginToken, nil +} + +func (s *Server) userIDFromToken(ctx context.Context, token string) (int, error) { + var userID int + err := s.db.QueryRowContext(ctx, "SELECT user_id FROM sign_sessions WHERE token = $1", token).Scan(&userID) + if err == sql.ErrNoRows { + return 0, fmt.Errorf("invalid login token") + } else if err != nil { + return 0, err + } + return userID, nil +} + +func (s *Server) createCharacter(ctx context.Context, userID int) (int, error) { + var charID int + err := s.db.QueryRowContext(ctx, + "SELECT id FROM characters WHERE is_new_character = true AND user_id = $1", + userID, + ).Scan(&charID) + if err == sql.ErrNoRows { + err = s.db.QueryRowContext(ctx, ` + INSERT INTO characters ( + user_id, is_female, is_new_character, name, unk_desc_string, + hrp, gr, weapon_type, last_login + ) + VALUES ($1, false, true, '', '', 0, 0, 0, $2) + RETURNING id`, + userID, uint32(time.Now().Unix()), + ).Scan(&charID) + } + return charID, err +} + +func (s *Server) deleteCharacter(ctx context.Context, userID int, charID int) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + _, err = tx.ExecContext( + ctx, ` + DELETE FROM login_boost_state + WHERE char_id = $1`, + charID, + ) + if err != nil { + return err + } + _, err = tx.ExecContext( + ctx, ` + DELETE FROM guild_characters + WHERE character_id = $1`, + charID, + ) + if err != nil { + return err + } + _, err = tx.ExecContext( + ctx, ` + DELETE FROM characters + WHERE user_id = $1 AND id = $2`, + userID, charID, + ) + if err != nil { + return err + } + return tx.Commit() +} + +func (s *Server) getCharactersForUser(ctx context.Context, uid int) ([]Character, error) { + characters := make([]Character, 0) + err := s.db.SelectContext( + ctx, &characters, ` + SELECT id, name, is_female, weapon_type, hrp, gr, last_login + FROM characters + WHERE user_id = $1 AND deleted = false AND is_new_character = false ORDER BY id ASC`, + uid, + ) + if err != nil { + return nil, err + } + return characters, nil +} diff --git a/server/signv2server/endpoints.go b/server/signv2server/endpoints.go new file mode 100644 index 000000000..eeb7442de --- /dev/null +++ b/server/signv2server/endpoints.go @@ -0,0 +1,208 @@ +package signv2server + +import ( + "database/sql" + "encoding/json" + "errors" + "net/http" + "time" + + "github.com/lib/pq" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" +) + +type LauncherMessage struct { + Message string `json:"message"` + Date int64 `json:"date"` + Link string `json:"link"` +} + +type Character struct { + ID int `json:"id"` + Name string `json:"name"` + IsFemale bool `json:"isFemale" db:"is_female"` + Weapon int `json:"weapon" db:"weapon_type"` + HR int `json:"hr" db:"hrp"` + GR int `json:"gr"` + LastLogin int64 `json:"lastLogin" db:"last_login"` +} + +func (s *Server) Launcher(w http.ResponseWriter, r *http.Request) { + var respData struct { + Important []LauncherMessage `json:"important"` + Normal []LauncherMessage `json:"normal"` + } + respData.Important = []LauncherMessage{ + { + Message: "Server Update 9 Released!", + Date: time.Date(2022, 8, 2, 0, 0, 0, 0, time.UTC).Unix(), + Link: "https://discord.com/channels/368424389416583169/929509970624532511/1003985850255818762", + }, + { + Message: "Eng 2.0 & Ravi Patch Released!", + Date: time.Date(2022, 5, 3, 0, 0, 0, 0, time.UTC).Unix(), + Link: "https://discord.com/channels/368424389416583169/929509970624532511/969305400795078656", + }, + { + Message: "Launcher Patch V1.0 Released!", + Date: time.Date(2022, 4, 24, 0, 0, 0, 0, time.UTC).Unix(), + Link: "https://discord.com/channels/368424389416583169/929509970624532511/969286397301248050", + }, + } + respData.Normal = []LauncherMessage{ + { + Message: "Join the community Discord for updates!", + Date: time.Date(2022, 4, 24, 0, 0, 0, 0, time.UTC).Unix(), + Link: "https://discord.gg/CFnzbhQ", + }, + } + w.WriteHeader(200) + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(respData) +} + +func (s *Server) Login(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var reqData struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil { + s.logger.Error("JSON decode error", zap.Error(err)) + w.WriteHeader(400) + w.Write([]byte("Invalid data received")) + return + } + var ( + userID int + password string + ) + err := s.db.QueryRow("SELECT id, password FROM users WHERE username = $1", reqData.Username).Scan(&userID, &password) + if err == sql.ErrNoRows { + w.WriteHeader(400) + w.Write([]byte("Username does not exist")) + return + } else if err != nil { + s.logger.Warn("SQL query error", zap.Error(err)) + w.WriteHeader(500) + return + } + if bcrypt.CompareHashAndPassword([]byte(password), []byte(reqData.Password)) != nil { + w.WriteHeader(400) + w.Write([]byte("Your password is incorrect")) + return + } + + var respData struct { + Token string `json:"token"` + Characters []Character `json:"characters"` + } + respData.Token, err = s.createLoginToken(ctx, userID) + if err != nil { + s.logger.Warn("Error registering login token", zap.Error(err)) + w.WriteHeader(500) + return + } + respData.Characters, err = s.getCharactersForUser(ctx, userID) + if err != nil { + s.logger.Warn("Error getting characters from DB", zap.Error(err)) + w.WriteHeader(500) + return + } + w.WriteHeader(200) + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(respData) +} + +func (s *Server) Register(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var reqData struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil { + s.logger.Error("JSON decode error", zap.Error(err)) + w.WriteHeader(400) + w.Write([]byte("Invalid data received")) + return + } + s.logger.Info("Creating account", zap.String("username", reqData.Username)) + userID, err := s.createNewUser(ctx, reqData.Username, reqData.Password) + if err != nil { + var pqErr *pq.Error + if errors.As(err, &pqErr) && pqErr.Constraint == "users_username_key" { + w.WriteHeader(400) + w.Write([]byte("User already exists")) + return + } + s.logger.Error("Error checking user", zap.Error(err), zap.String("username", reqData.Username)) + w.WriteHeader(500) + return + } + + var respData struct { + Token string `json:"token"` + } + respData.Token, err = s.createLoginToken(ctx, userID) + if err != nil { + s.logger.Error("Error registering login token", zap.Error(err)) + w.WriteHeader(500) + return + } + json.NewEncoder(w).Encode(respData) +} + +func (s *Server) CreateCharacter(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var reqData struct { + Token string `json:"token"` + } + if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil { + s.logger.Error("JSON decode error", zap.Error(err)) + w.WriteHeader(400) + w.Write([]byte("Invalid data received")) + return + } + + var respData struct { + CharID int `json:"id"` + } + userID, err := s.userIDFromToken(ctx, reqData.Token) + if err != nil { + w.WriteHeader(401) + return + } + respData.CharID, err = s.createCharacter(ctx, userID) + if err != nil { + s.logger.Error("Failed to create character", zap.Error(err), zap.String("token", reqData.Token)) + w.WriteHeader(500) + return + } + json.NewEncoder(w).Encode(respData) +} + +func (s *Server) DeleteCharacter(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var reqData struct { + Token string `json:"token"` + CharID int `json:"id"` + } + if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil { + s.logger.Error("JSON decode error", zap.Error(err)) + w.WriteHeader(400) + w.Write([]byte("Invalid data received")) + return + } + userID, err := s.userIDFromToken(ctx, reqData.Token) + if err != nil { + w.WriteHeader(401) + return + } + if err := s.deleteCharacter(ctx, userID, reqData.CharID); err != nil { + s.logger.Error("Failed to delete character", zap.Error(err), zap.String("token", reqData.Token), zap.Int("charID", reqData.CharID)) + w.WriteHeader(500) + return + } + json.NewEncoder(w).Encode(struct{}{}) +} diff --git a/server/signv2server/signv2_server.go b/server/signv2server/signv2_server.go new file mode 100644 index 000000000..c00a4a641 --- /dev/null +++ b/server/signv2server/signv2_server.go @@ -0,0 +1,89 @@ +package signv2server + +import ( + "context" + "erupe-ce/config" + "fmt" + "net/http" + "os" + "sync" + "time" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" +) + +type Config struct { + Logger *zap.Logger + DB *sqlx.DB + ErupeConfig *config.Config +} + +// Server is the MHF custom launcher sign server. +type Server struct { + sync.Mutex + logger *zap.Logger + erupeConfig *config.Config + db *sqlx.DB + httpServer *http.Server + isShuttingDown bool +} + +// NewServer creates a new Server type. +func NewServer(config *Config) *Server { + s := &Server{ + logger: config.Logger, + erupeConfig: config.ErupeConfig, + db: config.DB, + httpServer: &http.Server{}, + } + return s +} + +// Start starts the server in a new goroutine. +func (s *Server) Start() error { + // Set up the routes responsible for serving the launcher HTML, serverlist, unique name check, and JP auth. + r := mux.NewRouter() + r.HandleFunc("/launcher", s.Launcher) + r.HandleFunc("/login", s.Login) + r.HandleFunc("/register", s.Register) + r.HandleFunc("/character/create", s.CreateCharacter) + r.HandleFunc("/character/delete", s.DeleteCharacter) + handler := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}))(r) + s.httpServer.Handler = handlers.LoggingHandler(os.Stdout, handler) + s.httpServer.Addr = fmt.Sprintf(":%d", s.erupeConfig.SignV2.Port) + + serveError := make(chan error, 1) + go func() { + if err := s.httpServer.ListenAndServe(); err != nil { + // Send error if any. + serveError <- err + } + }() + + // Get the error from calling ListenAndServe, otherwise assume it's good after 250 milliseconds. + select { + case err := <-serveError: + return err + case <-time.After(250 * time.Millisecond): + return nil + } +} + +// Shutdown exits the server gracefully. +func (s *Server) Shutdown() { + s.logger.Debug("Shutting down") + + s.Lock() + s.isShuttingDown = true + s.Unlock() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := s.httpServer.Shutdown(ctx); err != nil { + // Just warn because we are shutting down the server anyway. + s.logger.Warn("Got error on httpServer shutdown", zap.Error(err)) + } +}