diff --git a/config.json b/config.json index c035f60c0..6df68d8c2 100644 --- a/config.json +++ b/config.json @@ -13,9 +13,8 @@ "LogInboundMessages": false, "LogOutboundMessages": false, "MaxHexdumpLength": 256, - "Event": 0, "DivaEvent": 0, - "FestaEvent": 0, + "FestaEvent": -1, "TournamentEvent": 0, "MezFesEvent": true, "MezFesAlt": false, diff --git a/network/binpacket/msg_bin_mail_notify.go b/network/binpacket/msg_bin_mail_notify.go index 5e1687512..125dc57ef 100644 --- a/network/binpacket/msg_bin_mail_notify.go +++ b/network/binpacket/msg_bin_mail_notify.go @@ -16,11 +16,7 @@ func (m MsgBinMailNotify) Parse(bf *byteframe.ByteFrame) error { func (m MsgBinMailNotify) Build(bf *byteframe.ByteFrame) error { bf.WriteUint8(0x01) // Unk - byteName, _ := stringsupport.ConvertUTF8ToShiftJIS(m.SenderName) - - bf.WriteBytes(byteName) - bf.WriteBytes(make([]byte, 21-len(byteName))) - + bf.WriteBytes(stringsupport.PaddedString(m.SenderName, 21, true)) return nil } diff --git a/patch-schema/festa.sql b/patch-schema/festa.sql new file mode 100644 index 000000000..ce7d03594 --- /dev/null +++ b/patch-schema/festa.sql @@ -0,0 +1,311 @@ +BEGIN; + +CREATE TYPE event_type AS ENUM ('festa', 'diva', 'vs', 'mezfes'); + +DROP TABLE IF EXISTS public.event_week; + +ALTER TABLE IF EXISTS public.guild_characters + ADD COLUMN IF NOT EXISTS souls int DEFAULT 0; + +ALTER TABLE IF EXISTS public.guilds + DROP COLUMN IF EXISTS festival_colour; + +CREATE TABLE IF NOT EXISTS public.events +( + id serial NOT NULL PRIMARY KEY, + event_type event_type NOT NULL, + start_time timestamp without time zone NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS public.festa_registrations +( + guild_id int NOT NULL, + team festival_colour NOT NULL +); + +CREATE TABLE IF NOT EXISTS public.festa_trials +( + id serial NOT NULL PRIMARY KEY, + objective int NOT NULL, + goal_id int NOT NULL, + times_req int NOT NULL, + locale_req int NOT NULL DEFAULT 0, + reward int NOT NULL +); + +CREATE TYPE prize_type AS ENUM ('personal', 'guild'); + +CREATE TABLE IF NOT EXISTS public.festa_prizes +( + id serial NOT NULL PRIMARY KEY, + type prize_type NOT NULL, + tier int NOT NULL, + souls_req int NOT NULL, + item_id int NOT NULL, + num_item int NOT NULL +); + +CREATE TABLE IF NOT EXISTS public.festa_prizes_accepted +( + prize_id int NOT NULL, + character_id int NOT NULL +); + +-- Ripped prizes +INSERT INTO public.festa_prizes + (type, tier, souls_req, item_id, num_item) +VALUES + ('personal', 1, 1, 9647, 7), + ('personal', 2, 1, 9647, 7), + ('personal', 3, 1, 9647, 7), + ('personal', 1, 200, 11284, 4), + ('personal', 2, 200, 11284, 4), + ('personal', 3, 200, 11284, 4), + ('personal', 1, 400, 11381, 3), + ('personal', 2, 400, 11381, 3), + ('personal', 3, 400, 11381, 3), + ('personal', 1, 600, 11284, 8), + ('personal', 2, 600, 11284, 8), + ('personal', 3, 600, 11284, 8), + ('personal', 1, 800, 11384, 3), + ('personal', 2, 800, 11384, 3), + ('personal', 3, 800, 11384, 3), + ('personal', 1, 1000, 11284, 12), + ('personal', 2, 1000, 11284, 12), + ('personal', 3, 1000, 11284, 12), + ('personal', 1, 1200, 11381, 5), + ('personal', 2, 1200, 11381, 5), + ('personal', 3, 1200, 11381, 5), + ('personal', 1, 1400, 11284, 16), + ('personal', 2, 1400, 11284, 16), + ('personal', 3, 1400, 11284, 16), + ('personal', 1, 1700, 11384, 5), + ('personal', 2, 1700, 11384, 5), + ('personal', 3, 1700, 11384, 5), + ('personal', 1, 2000, 11284, 16), + ('personal', 2, 2000, 11284, 16), + ('personal', 3, 2000, 11284, 16), + ('personal', 1, 2500, 11382, 4), + ('personal', 2, 2500, 11382, 4), + ('personal', 3, 2500, 11382, 4), + ('personal', 1, 3000, 11284, 24), + ('personal', 2, 3000, 11284, 24), + ('personal', 3, 3000, 11284, 24), + ('personal', 1, 4000, 11385, 4), + ('personal', 2, 4000, 11385, 4), + ('personal', 3, 4000, 11385, 4), + ('personal', 1, 5000, 11381, 11), + ('personal', 2, 5000, 11381, 11), + ('personal', 3, 5000, 11381, 11), + ('personal', 1, 6000, 5177, 5), + ('personal', 2, 6000, 5177, 5), + ('personal', 3, 6000, 5177, 5), + ('personal', 1, 7000, 11384, 11), + ('personal', 2, 7000, 11384, 11), + ('personal', 3, 7000, 11384, 11), + ('personal', 1, 10000, 11382, 8), + ('personal', 2, 10000, 11382, 8), + ('personal', 3, 10000, 11382, 8), + ('personal', 1, 15000, 11385, 4), + ('personal', 2, 15000, 11385, 4), + ('personal', 3, 15000, 11385, 4), + ('personal', 1, 20000, 11381, 13), + ('personal', 2, 20000, 11381, 13), + ('personal', 3, 20000, 11381, 13), + ('personal', 1, 25000, 11385, 4), + ('personal', 2, 25000, 11385, 4), + ('personal', 3, 25000, 11385, 4), + ('personal', 1, 30000, 11383, 1), + ('personal', 2, 30000, 11383, 1), + ('personal', 3, 30000, 11383, 1); + +INSERT INTO public.festa_prizes +(type, tier, souls_req, item_id, num_item) +VALUES + ('guild', 1, 100, 7468, 5), + ('guild', 2, 100, 7468, 5), + ('guild', 3, 100, 7465, 5), + ('guild', 1, 300, 7469, 5), + ('guild', 2, 300, 7469, 5), + ('guild', 3, 300, 7466, 5), + ('guild', 1, 700, 7470, 5), + ('guild', 2, 700, 7470, 5), + ('guild', 3, 700, 7467, 5), + ('guild', 1, 1500, 13405, 14), + ('guild', 1, 1500, 1520, 3), + ('guild', 2, 1500, 13405, 14), + ('guild', 2, 1500, 1520, 3), + ('guild', 3, 1500, 7011, 3), + ('guild', 3, 1500, 13405, 14), + ('guild', 1, 3000, 10201, 10), + ('guild', 2, 3000, 10201, 10), + ('guild', 3, 3000, 10201, 10), + ('guild', 1, 6000, 13895, 14), + ('guild', 1, 6000, 1520, 6), + ('guild', 2, 6000, 13895, 14), + ('guild', 2, 6000, 1520, 6), + ('guild', 3, 6000, 13895, 14), + ('guild', 3, 6000, 7011, 4), + ('guild', 1, 12000, 13406, 14), + ('guild', 1, 12000, 1520, 9), + ('guild', 2, 12000, 13406, 14), + ('guild', 2, 12000, 1520, 9), + ('guild', 3, 12000, 13406, 14), + ('guild', 3, 12000, 7011, 5), + ('guild', 1, 25000, 10207, 10), + ('guild', 2, 25000, 10207, 10), + ('guild', 3, 25000, 10207, 10), + ('guild', 1, 50000, 1520, 12), + ('guild', 1, 50000, 13896, 14), + ('guild', 2, 50000, 1520, 12), + ('guild', 2, 50000, 13896, 14), + ('guild', 3, 50000, 7011, 6), + ('guild', 3, 50000, 13896, 14), + ('guild', 1, 100000, 10201, 10), + ('guild', 2, 100000, 10201, 10), + ('guild', 3, 100000, 10201, 10), + ('guild', 1, 200000, 13406, 16), + ('guild', 2, 200000, 13406, 16), + ('guild', 3, 200000, 13406, 16), + ('guild', 1, 300000, 13896, 16), + ('guild', 2, 300000, 13896, 16), + ('guild', 3, 300000, 13896, 16), + ('guild', 1, 400000, 10207, 10), + ('guild', 2, 400000, 10207, 10), + ('guild', 3, 400000, 10207, 10), + ('guild', 1, 500000, 13407, 6), + ('guild', 1, 500000, 13897, 6), + ('guild', 2, 500000, 13407, 6), + ('guild', 2, 500000, 13897, 6), + ('guild', 3, 500000, 13407, 6), + ('guild', 3, 500000, 13897, 6); + +-- Ripped trials +INSERT INTO public.festa_trials + (objective, goal_id, times_req, locale_req, reward) +VALUES + (1,27,1,0,1), + (5,53034,0,0,400), + (5,22042,0,0,89), + (5,23397,0,0,89), + (1,28,1,0,1), + (1,68,1,0,1), + (1,6,1,0,2), + (1,38,1,0,2), + (1,20,1,0,3), + (1,39,1,0,4), + (1,48,1,0,4), + (1,67,1,0,4), + (1,93,1,0,4), + (1,22,1,0,5), + (1,52,1,0,5), + (1,101,1,0,5), + (1,1,1,0,5), + (1,37,1,0,5), + (1,15,1,0,5), + (1,45,1,0,5), + (1,74,1,0,5), + (1,78,1,0,5), + (1,103,1,0,5), + (1,51,1,0,6), + (1,17,1,0,6), + (1,21,1,0,6), + (1,92,1,0,6), + (1,47,1,0,7), + (1,46,1,0,7), + (1,26,1,0,7), + (1,14,1,0,7), + (1,11,1,0,7), + (1,44,1,0,8), + (1,43,1,0,8), + (1,49,1,0,8), + (1,40,1,0,8), + (1,76,1,0,8), + (1,89,1,0,8), + (1,94,1,0,8), + (1,96,1,0,8), + (1,75,1,0,8), + (1,91,1,0,8), + (1,53,1,0,9), + (1,80,1,0,9), + (1,42,1,0,9), + (1,79,1,0,9), + (1,81,1,0,10), + (1,41,1,0,10), + (1,82,1,0,10), + (1,90,1,0,10), + (1,149,1,0,10), + (1,85,1,0,11), + (1,95,1,0,11), + (1,121,1,0,11), + (1,142,1,0,11), + (1,141,1,0,11), + (1,146,1,0,12), + (1,147,1,0,12), + (1,148,1,0,12), + (1,151,1,0,12), + (1,152,1,0,12), + (1,159,1,0,12), + (1,153,1,0,12), + (1,162,1,0,12), + (1,111,1,0,13), + (1,110,1,0,13), + (1,112,1,0,13), + (1,109,1,0,14), + (1,169,1,0,15), + (2,33,1,0,6), + (2,104,1,0,8), + (2,119,1,0,8), + (2,120,1,0,8), + (2,54,1,0,8), + (2,59,1,0,8), + (2,64,1,0,8), + (2,65,1,0,8), + (2,99,1,0,9), + (2,83,1,0,9), + (2,84,1,0,10), + (2,77,1,0,10), + (2,106,1,0,10), + (2,55,1,0,10), + (2,58,1,0,10), + (2,7,1,0,10), + (2,50,1,0,11), + (2,131,1,0,11), + (2,129,1,0,11), + (2,140,1,0,11), + (2,122,1,0,11), + (2,126,1,0,11), + (2,127,1,0,11), + (2,128,1,0,11), + (2,130,1,0,11), + (2,139,1,0,11), + (2,144,1,0,11), + (2,150,1,0,11), + (2,158,1,0,11), + (2,164,1,0,15), + (2,165,1,0,15), + (2,2,1,7,15), + (2,36,1,0,15), + (2,71,1,0,15), + (2,108,1,0,15), + (2,116,1,0,15), + (2,107,1,0,15), + (2,154,1,0,17), + (2,166,1,0,17), + (2,170,1,0,18), + (3,31,1,0,1), + (3,8,1,0,3), + (3,123,1,0,8), + (3,105,1,0,9), + (3,125,1,0,11), + (3,115,1,0,12), + (3,114,1,0,12), + (3,161,1,0,12), + (4,670,1,0,1), + (4,671,1,0,1), + (4,672,1,0,1), + (4,675,1,0,1), + (4,673,1,0,1), + (4,674,1,0,1); + +END; \ No newline at end of file diff --git a/patch-schema/mail-system-messages.sql b/patch-schema/mail-system-messages.sql new file mode 100644 index 000000000..4ce8dfaf6 --- /dev/null +++ b/patch-schema/mail-system-messages.sql @@ -0,0 +1,13 @@ +BEGIN; + +ALTER TABLE IF EXISTS public.mail + ADD COLUMN IF NOT EXISTS is_sys_message bool DEFAULT false; + +UPDATE mail SET is_sys_message=false; + +ALTER TABLE IF EXISTS public.mail + DROP CONSTRAINT IF EXISTS mail_sender_id_fkey; + +INSERT INTO public.characters (id, name) VALUES (0, ''); + +END; \ No newline at end of file diff --git a/server/channelserver/handlers_diva.go b/server/channelserver/handlers_diva.go index 8927cd792..48e6dcecf 100644 --- a/server/channelserver/handlers_diva.go +++ b/server/channelserver/handlers_diva.go @@ -2,11 +2,116 @@ package channelserver import ( "encoding/hex" + "erupe-ce/common/stringsupport" + "time" "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" ) +func cleanupDiva(s *Session) { + s.server.db.Exec("DELETE FROM events WHERE event_type='diva'") +} + +func generateDivaTimestamps(s *Session, start uint32, debug bool) []uint32 { + timestamps := make([]uint32, 6) + midnight := Time_Current_Midnight() + if debug && start <= 3 { + midnight := uint32(midnight.Unix()) + switch start { + case 1: + timestamps[0] = midnight + timestamps[1] = timestamps[0] + 601200 + timestamps[2] = timestamps[1] + 3900 + timestamps[3] = timestamps[1] + 604800 + timestamps[4] = timestamps[3] + 3900 + timestamps[5] = timestamps[3] + 604800 + case 2: + timestamps[0] = midnight - 605100 + timestamps[1] = midnight - 3900 + timestamps[2] = midnight + timestamps[3] = timestamps[1] + 604800 + timestamps[4] = timestamps[3] + 3900 + timestamps[5] = timestamps[3] + 604800 + case 3: + timestamps[0] = midnight - 1213800 + timestamps[1] = midnight - 608700 + timestamps[2] = midnight - 604800 + timestamps[3] = midnight - 3900 + timestamps[4] = midnight + timestamps[5] = timestamps[3] + 604800 + } + return timestamps + } + if start == 0 || Time_Current_Adjusted().Unix() > int64(start)+2977200 { + cleanupDiva(s) + // Generate a new diva defense, starting midnight tomorrow + start = uint32(midnight.Add(24 * time.Hour).Unix()) + s.server.db.Exec("INSERT INTO events (event_type, start_time) VALUES ('diva', to_timestamp($1)::timestamp without time zone)", start) + } + timestamps[0] = start + timestamps[1] = timestamps[0] + 601200 + timestamps[2] = timestamps[1] + 3900 + timestamps[3] = timestamps[1] + 604800 + timestamps[4] = timestamps[3] + 3900 + timestamps[5] = timestamps[3] + 604800 + return timestamps +} + +func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetUdSchedule) + bf := byteframe.NewByteFrame() + + id, start := uint32(0xCAFEBEEF), uint32(0) + rows, _ := s.server.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='diva'") + for rows.Next() { + rows.Scan(&id, &start) + } + + var timestamps []uint32 + if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.DivaEvent >= 0 { + if s.server.erupeConfig.DevModeOptions.DivaEvent == 0 { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 36)) + return + } + timestamps = generateDivaTimestamps(s, uint32(s.server.erupeConfig.DevModeOptions.DivaEvent), true) + } else { + timestamps = generateDivaTimestamps(s, start, false) + } + + bf.WriteUint32(id) + for _, timestamp := range timestamps { + bf.WriteUint32(timestamp) + } + + bf.WriteUint16(0x19) // Unk 00011001 + bf.WriteUint16(0x2D) // Unk 00101101 + bf.WriteUint16(0x02) // Unk 00000010 + bf.WriteUint16(0x02) // Unk 00000010 + + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfGetUdInfo(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetUdInfo) + // Message that appears on the Diva Defense NPC and triggers the green exclamation mark + udInfos := []struct { + Text string + StartTime time.Time + EndTime time.Time + }{} + + resp := byteframe.NewByteFrame() + resp.WriteUint8(uint8(len(udInfos))) + for _, udInfo := range udInfos { + resp.WriteBytes(stringsupport.PaddedString(udInfo.Text, 1024, true)) + resp.WriteUint32(uint32(udInfo.StartTime.Unix())) + resp.WriteUint32(uint32(udInfo.EndTime.Unix())) + } + + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) +} + func handleMsgMhfGetKijuInfo(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetKijuInfo) // Temporary canned response diff --git a/server/channelserver/handlers_event.go b/server/channelserver/handlers_event.go index 7ec7c30e7..b6765f998 100644 --- a/server/channelserver/handlers_event.go +++ b/server/channelserver/handlers_event.go @@ -7,7 +7,6 @@ import ( "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" - timeServerFix "erupe-ce/server/channelserver/timeserver" ) func handleMsgMhfRegisterEvent(s *Session, p mhfpacket.MHFPacket) { @@ -234,75 +233,6 @@ func handleMsgMhfUseKeepLoginBoost(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } -func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfGetUdSchedule) - var t = timeServerFix.Tstatic_midnight() - var event int = s.server.erupeConfig.DevModeOptions.DivaEvent - - year, month, day := t.Date() - midnight := time.Date(year, month, day, 0, 0, 0, 0, t.Location()) - // Events with time limits are Festival with Sign up, Soul Week and Winners Weeks - // Diva Defense with Prayer, Interception and Song weeks - // Mezeporta Festival with simply 'available' being a weekend thing - resp := byteframe.NewByteFrame() - resp.WriteUint32(0x1d5fda5c) // Unk (1d5fda5c, 0b5397df) - - if event == 1 { - resp.WriteUint32(uint32(midnight.Add(24 * 21 * time.Hour).Unix())) // Week 1 Timestamp, Festi start? - } else { - resp.WriteUint32(uint32(midnight.Add(-24 * 21 * time.Hour).Unix())) // Week 1 Timestamp, Festi start? - } - - if event == 2 { - resp.WriteUint32(uint32(midnight.Add(24 * 14 * time.Hour).Unix())) // Week 2 Timestamp - resp.WriteUint32(uint32(midnight.Add(24 * 14 * time.Hour).Unix())) // Week 2 Timestamp - } else { - resp.WriteUint32(uint32(midnight.Add(-24 * 14 * time.Hour).Unix())) // Week 2 Timestamp - resp.WriteUint32(uint32(midnight.Add(-24 * 14 * time.Hour).Unix())) // Week 2 Timestamp - } - - if event == 3 { - resp.WriteUint32(uint32(midnight.Add((24) * 7 * time.Hour).Unix())) // Diva Defense Interception - resp.WriteUint32(uint32(midnight.Add((24) * 14 * time.Hour).Unix())) // Diva Defense Greeting Song - } else { - resp.WriteUint32(uint32(midnight.Add((-24) * 7 * time.Hour).Unix())) // Diva Defense Interception - resp.WriteUint32(uint32(midnight.Add((-24) * 14 * time.Hour).Unix())) // Diva Defense Greeting Song - } - - resp.WriteUint16(0x19) // Unk 00011001 - resp.WriteUint16(0x2d) // Unk 00101101 - resp.WriteUint16(0x02) // Unk 00000010 - resp.WriteUint16(0x02) // Unk 00000010 - - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) -} - -func handleMsgMhfGetUdInfo(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfGetUdInfo) - // Message that appears on the Diva Defense NPC and triggers the green exclamation mark - udInfos := []struct { - Text string - StartTime time.Time - EndTime time.Time - }{ - /*{ - Text: " ~C17【Erupe】 is dead event!\n\n■Features\n~C18 Dont bother walking around!\n~C17 Take down your DB by doing \n~C17 nearly anything!", - StartTime: Time_static().Add(time.Duration(-5) * time.Minute), // Event started 5 minutes ago, - EndTime: Time_static().Add(time.Duration(24) * time.Hour), // Event ends in 5 minutes, - }, */ - } - - resp := byteframe.NewByteFrame() - resp.WriteUint8(uint8(len(udInfos))) - for _, udInfo := range udInfos { - resp.WriteBytes(fixedSizeShiftJIS(udInfo.Text, 1024)) - resp.WriteUint32(uint32(udInfo.StartTime.Unix())) - resp.WriteUint32(uint32(udInfo.EndTime.Unix())) - } - - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) -} - func handleMsgMhfGetRestrictionEvent(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfSetRestrictionEvent(s *Session, p mhfpacket.MHFPacket) { diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index c1f3cecfb..6e5080ac3 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -2,12 +2,11 @@ package channelserver import ( "encoding/hex" - "math/rand" - "time" - "erupe-ce/common/byteframe" ps "erupe-ce/common/pascalstring" "erupe-ce/network/mhfpacket" + "math/rand" + "time" ) func handleMsgMhfSaveMezfesData(s *Session, p mhfpacket.MHFPacket) { @@ -43,99 +42,200 @@ func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) { case 1: bf.WriteUint32(uint32(midnight.Unix())) bf.WriteUint32(uint32(midnight.Add(3 * 24 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(12 * 24 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(21 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(13 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(20 * 24 * time.Hour).Unix())) case 2: bf.WriteUint32(uint32(midnight.Add(-3 * 24 * time.Hour).Unix())) bf.WriteUint32(uint32(midnight.Unix())) - bf.WriteUint32(uint32(midnight.Add(9 * 24 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(16 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(10 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(17 * 24 * time.Hour).Unix())) case 3: - bf.WriteUint32(uint32(midnight.Add(-12 * 24 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(-9 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(-13 * 24 * time.Hour).Unix())) + bf.WriteUint32(uint32(midnight.Add(-10 * 24 * time.Hour).Unix())) bf.WriteUint32(uint32(midnight.Unix())) bf.WriteUint32(uint32(midnight.Add(7 * 24 * time.Hour).Unix())) default: bf.WriteBytes(make([]byte, 16)) bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // TS Current Time - bf.WriteUint16(1) - bf.WriteUint32(0) + bf.WriteUint8(3) + bf.WriteBytes(make([]byte, 4)) doAckBufSucceed(s, pkt.AckHandle, bf.Data()) return } bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // TS Current Time - d, _ := hex.DecodeString("031491E631353089F18CF68EAE8EEB97C291E589EF00001200000A54001000000000ED130D949A96B697B393A294B081490000000A55001000010000ED130D949A96B697B393A294B081490000000A56001000020000ED130D949A96B697B393A294B081490000000A57001000030000ED130D949A96B697B393A294B081490000000A58001000040000ED130D949A96B697B393A294B081490000000A59001000050000ED130D949A96B697B393A294B081490000000A5A001000060000ED130D949A96B697B393A294B081490000000A5B001000070000ED130D949A96B697B393A294B081490000000A5C001000080000ED130D949A96B697B393A294B081490000000A5D001000090000ED130D949A96B697B393A294B081490000000A5E0010000A0000ED130D949A96B697B393A294B081490000000A5F0010000B0000ED130D949A96B697B393A294B081490000000A600010000C0000ED130D949A96B697B393A294B081490000000A610010000D0000ED130D949A96B697B393A294B081490000000A620011FFFF0000ED121582DD82F182C882C5949A96B697B393A294B081490000000A63000600EA0000000009834C838C834183570000000A64000600ED000000000B836E838A837D834F838D0000000A65000600EF0000000011834A834E8354839383668381834C83930003000002390006000600000E8CC2906C208B9091E58B9B94740001617E43303581798BA38B5A93E09765817A0A7E433030834E83478358836782C592DE82C182BD8B9B82CC83548343835982F08BA382A40A7E433034817991CE8FDB8B9B817A0A7E433030834C838C8341835781410A836E838A837D834F838D8141834A834E8354839383668381834C83930A7E433037817993FC8FDC8FDC9569817A0A7E4330308B9B947482CC82B582E982B58141835E838B836C835290B68E598C9481410A834F815B834E90B68E598C948141834F815B834E91AB90B68E598C9481410A834F815B834E89F095FA8C94283181603388CA290A2F97C29263837C8343839383672831816031303088CA290A2F8FA08360835083628367817B836E815B8374836083508362836794920A2831816035303088CA290A7E43303381798A4A8DC38AFA8AD4817A0A7E43303032303139944E31318C8E323293FA2031343A303082A982E70A32303139944E31318C8E323593FA2031343A303082DC82C5000000023A0011000700001297C292632082668B89E8E891CA935694740000ED7E43303581798BA38B5A93E09765817A0A7E43303081E182DD82F182C882C5949A96B697B393A294B0814981E282F00A93AF82B697C2926382C98F8A91AE82B782E934906C82DC82C582CC0A97C2926388F582C582A282A982C9918182AD834E838A834182B782E982A90A82F08BA382A40A0A7E433037817993FC8FDC8FDC9569817A0A7E43303091E631343789F18EEB906C8DD582CC8DB02831816032303088CA290A0A7E43303381798A4A8DC38AFA8AD4817A0A7E43303032303139944E31318C8E323293FA2031343A303082A982E70A32303139944E31318C8E323593FA2031343A303082DC82C50A000000023B001000070000128CC2906C2082668B89E8E891CA935694740001497E43303581798BA38B5A93E09765817A0A7E43303081E1949A96B697B393A294B0814981E282F00A82A282A982C9918182AD834E838A834182B782E982A982F08BA382A40A0A7E433037817993FC8FDC8FDC9569817A0A7E43303089A48ED282CC8381835F838B283188CA290A2F8CF68EAE82CC82B582E982B58141835E838B836C835290B68E598C9481410A834F815B834E90B68E598C948141834F815B834E91AB90B68E598C9481410A834F815B834E89F095FA8C94283181603388CA290A2F97C29263837C8343839383672831816031303088CA290A2F8FA08360835083628367817B836E815B8374836083508362836794920A2831816035303088CA290A7E43303381798A4A8DC38AFA8AD4817A0A7E43303032303139944E31318C8E323293FA2031343A303082A982E70A32303139944E31318C8E323593FA2031343A303082DC82C500") - bf.WriteBytes(d) + bf.WriteUint8(3) + ps.Uint8(bf, "", false) + bf.WriteUint16(0) // numEvents + bf.WriteUint8(0) // numCups + + /* + struct event + uint32 eventID + uint16 unk + uint16 unk + uint32 unk + psUint8 name + + struct cup + uint32 cupID + uint16 unk + uint16 unk + uint16 unk + psUint8 name + psUint16 desc + */ + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } +func cleanupFesta(s *Session) { + s.server.db.Exec("DELETE FROM events WHERE event_type='festa'") + s.server.db.Exec("DELETE FROM festa_registrations") + s.server.db.Exec("DELETE FROM festa_prizes_accepted") + s.server.db.Exec("UPDATE guild_characters SET souls=0") +} + +func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 { + timestamps := make([]uint32, 5) + midnight := Time_Current_Midnight() + if debug && start <= 3 { + midnight := uint32(midnight.Unix()) + switch start { + case 1: + timestamps[0] = midnight + timestamps[1] = timestamps[0] + 604800 + timestamps[2] = timestamps[1] + 604800 + timestamps[3] = timestamps[2] + 9000 + timestamps[4] = timestamps[3] + 1240200 + case 2: + timestamps[0] = midnight - 604800 + timestamps[1] = midnight + timestamps[2] = timestamps[1] + 604800 + timestamps[3] = timestamps[2] + 9000 + timestamps[4] = timestamps[3] + 1240200 + case 3: + timestamps[0] = midnight - 1209600 + timestamps[1] = midnight - 604800 + timestamps[2] = midnight + timestamps[3] = timestamps[2] + 9000 + timestamps[4] = timestamps[3] + 1240200 + } + return timestamps + } + if start == 0 || Time_Current_Adjusted().Unix() > int64(start)+2977200 { + cleanupFesta(s) + // Generate a new festa, starting midnight tomorrow + start = uint32(midnight.Add(24 * time.Hour).Unix()) + s.server.db.Exec("INSERT INTO events (event_type, start_time) VALUES ('festa', to_timestamp($1)::timestamp without time zone)", start) + } + timestamps[0] = start + timestamps[1] = timestamps[0] + 604800 + timestamps[2] = timestamps[1] + 604800 + timestamps[3] = timestamps[2] + 9000 + timestamps[4] = timestamps[3] + 1240200 + return timestamps +} + +type Trial struct { + ID uint32 `db:"id"` + Objective uint8 `db:"objective"` + GoalID uint32 `db:"goal_id"` + TimesReq uint16 `db:"times_req"` + Locale uint16 `db:"locale_req"` + Reward uint16 `db:"reward"` +} + func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfInfoFesta) bf := byteframe.NewByteFrame() - state := s.server.erupeConfig.DevModeOptions.FestaEvent - bf.WriteUint32(0xdeadbeef) // festaID - // Registration Week Start - // Introductory Week Start - // Totalling Time - // Reward Festival Start (2.5hrs after totalling) - // 2 weeks after RewardFes (next fes?) - midnight := Time_Current_Midnight() - switch state { - case 1: - bf.WriteUint32(uint32(midnight.Unix())) - bf.WriteUint32(uint32(midnight.Add(24 * 7 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(24 * 14 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(24*14*time.Hour + 150*time.Minute).Unix())) - bf.WriteUint32(uint32(midnight.Add(24*28*time.Hour + 11*time.Hour).Unix())) - case 2: - bf.WriteUint32(uint32(midnight.Add(-24 * 7 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Unix())) - bf.WriteUint32(uint32(midnight.Add(24 * 7 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(24*7*time.Hour + 150*time.Minute).Unix())) - bf.WriteUint32(uint32(midnight.Add(24 * 21 * time.Hour).Add(11 * time.Hour).Unix())) - case 3: - bf.WriteUint32(uint32(midnight.Add(-24 * 14 * time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Add(-24*7*time.Hour + 11*time.Hour).Unix())) - bf.WriteUint32(uint32(midnight.Unix())) - bf.WriteUint32(uint32(midnight.Add(150 * time.Minute).Unix())) - bf.WriteUint32(uint32(midnight.Add(24*14*time.Hour + 11*time.Hour).Unix())) - default: + + id, start := uint32(0xDEADBEEF), uint32(0) + rows, _ := s.server.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='festa'") + for rows.Next() { + rows.Scan(&id, &start) + } + + var timestamps []uint32 + if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.FestaEvent >= 0 { + if s.server.erupeConfig.DevModeOptions.FestaEvent == 0 { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } + timestamps = generateFestaTimestamps(s, uint32(s.server.erupeConfig.DevModeOptions.FestaEvent), true) + } else { + timestamps = generateFestaTimestamps(s, start, false) + } + + if timestamps[0] > uint32(Time_Current_Adjusted().Unix()) { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) return } - bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // TS Current Time + + var blueSouls, redSouls uint32 + s.server.db.QueryRow("SELECT SUM(gc.souls) FROM guild_characters gc INNER JOIN festa_registrations fr ON fr.guild_id = gc.guild_id WHERE fr.team = 'blue'").Scan(&blueSouls) + s.server.db.QueryRow("SELECT SUM(gc.souls) FROM guild_characters gc INNER JOIN festa_registrations fr ON fr.guild_id = gc.guild_id WHERE fr.team = 'red'").Scan(&redSouls) + + bf.WriteUint32(id) + for _, timestamp := range timestamps { + bf.WriteUint32(timestamp) + } + bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) bf.WriteUint8(4) ps.Uint8(bf, "", false) bf.WriteUint32(0) - bf.WriteUint32(0) // Blue souls - bf.WriteUint32(0) // Red souls + bf.WriteUint32(blueSouls) + bf.WriteUint32(redSouls) - trials := 0 - bf.WriteUint16(uint16(trials)) - for i := 0; i < trials; i++ { - bf.WriteUint32(uint32(i + 1)) // trialID - bf.WriteUint8(0xFF) // unk - bf.WriteUint8(uint8(i)) // objective - bf.WriteUint32(0x1B) // monID, itemID if deliver - bf.WriteUint16(1) // huntsRemain? - bf.WriteUint16(0) // location - bf.WriteUint16(1) // numSoulsReward - bf.WriteUint8(0xFF) // unk - bf.WriteUint8(0xFF) // monopolised - bf.WriteUint16(0) // unk + rows, _ = s.server.db.Queryx("SELECT * FROM festa_trials") + trialData := byteframe.NewByteFrame() + var count uint16 + for rows.Next() { + trial := &Trial{} + err := rows.StructScan(&trial) + if err != nil { + continue + } + count++ + trialData.WriteUint32(trial.ID) + trialData.WriteUint8(0) // Unk + trialData.WriteUint8(trial.Objective) + trialData.WriteUint32(trial.GoalID) + trialData.WriteUint16(trial.TimesReq) + trialData.WriteUint16(trial.Locale) + trialData.WriteUint16(trial.Reward) + trialData.WriteUint8(0xFF) // Unk + trialData.WriteUint8(0xFF) // MonopolyState + trialData.WriteUint16(0) // Unk + } + bf.WriteUint16(count) + bf.WriteBytes(trialData.Data()) + + // Static bonus rewards + rewards, _ := hex.DecodeString("001901000007015E05F000000000000100000703E81B6300000000010100000C03E8000000000000000100000D0000000000000000000100000100000000000000000002000007015E05F000000000000200000703E81B6300000000010200000C03E8000000000000000200000D0000000000000000000200000400000000000000000003000007015E05F000000000000300000703E81B6300000000010300000C03E8000000000000000300000D0000000000000000000300000100000000000000000004000007015E05F000000000000400000703E81B6300000000010400000C03E8000000000000000400000D0000000000000000000400000400000000000000000005000007015E05F000000000000500000703E81B6300000000010500000C03E8000000000000000500000D00000000000000000005000001000000000000000000") + bf.WriteBytes(rewards) + + bf.WriteUint16(0x0001) + bf.WriteUint32(0xD4C001F4) + + categoryWinners := uint16(0) // NYI + bf.WriteUint16(categoryWinners) + for i := uint16(0); i < categoryWinners; i++ { + bf.WriteUint32(0) // Guild ID + bf.WriteUint16(i + 1) // Category ID + bf.WriteUint16(0) // Festa Team + ps.Uint8(bf, "", true) // Guild Name } - unk := 0 // static rewards? - bf.WriteUint16(uint16(unk)) - for i := 0; i < unk; i++ { - bf.WriteUint32(0) - bf.WriteUint16(0) - bf.WriteUint16(0) - bf.WriteUint32(0) - bf.WriteBool(false) + dailyWinners := uint16(0) // NYI + bf.WriteUint16(dailyWinners) + for i := uint16(0); i < dailyWinners; i++ { + bf.WriteUint32(0) // Guild ID + bf.WriteUint16(i + 1) // Category ID + bf.WriteUint16(0) // Festa Team + ps.Uint8(bf, "", true) // Guild Name } - d, _ := hex.DecodeString("0001D4C001F4000411B6648100010001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100020001152A81F589A497A793C196B18B528E6D926381F52A000C952CE10003000109E54BE54E89B38F970029FDCE04000400001381818D84836C8352819993A294B091D1818100000811B6648100010001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100020001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100030001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100040001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100050001152A81F589A497A793C196B18B528E6D926381F52A0011B6648100060001152A81F589A497A793C196B18B528E6D926381F52A000C952CE10007000109E54BE54E89B38F9700000000000008000001000000000100001388000007D0000003E800000064012C00C8009600640032") + d, _ := hex.DecodeString("000000000000000100001388000007D0000003E800000064012C00C8009600640032") bf.WriteBytes(d) ps.Uint16(bf, "", false) doAckBufSucceed(s, pkt.AckHandle, bf.Data()) @@ -144,8 +244,19 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { // state festa (U)ser func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfStateFestaU) + guild, err := GetGuildInfoByCharacterId(s, s.charID) + applicant := false + if guild != nil { + applicant, _ = guild.HasApplicationForCharID(s, s.charID) + } + if err != nil || guild == nil || applicant { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } + var souls uint32 + s.server.db.QueryRow("SELECT souls FROM guild_characters WHERE character_id=$1", s.charID).Scan(&souls) bf := byteframe.NewByteFrame() - bf.WriteUint32(0) // souls + bf.WriteUint32(souls) bf.WriteUint32(0) // unk doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } @@ -153,80 +264,156 @@ func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) { // state festa (G)uild func handleMsgMhfStateFestaG(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfStateFestaG) + guild, err := GetGuildInfoByCharacterId(s, s.charID) + applicant := false + if guild != nil { + applicant, _ = guild.HasApplicationForCharID(s, s.charID) + } resp := byteframe.NewByteFrame() - resp.WriteUint32(0) // souls - resp.WriteUint32(1) // unk - resp.WriteUint32(1) // unk - resp.WriteUint32(1) // unk, rank? - resp.WriteUint32(1) // unk + if err != nil || guild == nil || applicant { + resp.WriteUint32(0) + resp.WriteUint32(0) + resp.WriteUint32(0xFFFFFFFF) + resp.WriteUint32(0) + resp.WriteUint32(0) + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) + return + } + resp.WriteUint32(guild.Souls) + resp.WriteUint32(0) // unk + resp.WriteUint32(0) // unk, rank? + resp.WriteUint32(0) // unk + resp.WriteUint32(0) // unk doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } func handleMsgMhfEnumerateFestaMember(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateFestaMember) + guild, err := GetGuildInfoByCharacterId(s, s.charID) + if err != nil || guild == nil { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } + members, err := GetGuildMembers(s, guild.ID, false) + if err != nil { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } bf := byteframe.NewByteFrame() - bf.WriteUint16(0) // numMembers - // uint16 unk - // uint32 charID - // uint32 souls + bf.WriteUint16(uint16(len(members))) + bf.WriteUint16(0) // Unk + for _, member := range members { + bf.WriteUint32(member.CharID) + bf.WriteUint32(member.Souls) + } doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } func handleMsgMhfVoteFesta(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfEntryFesta) + pkt := p.(*mhfpacket.MsgMhfVoteFesta) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEntryFesta) - bf := byteframe.NewByteFrame() + guild, err := GetGuildInfoByCharacterId(s, s.charID) + if err != nil || guild == nil { + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) + return + } rand.Seed(time.Now().UnixNano()) - bf.WriteUint32(uint32(rand.Intn(2))) - // Update guild table + team := uint32(rand.Intn(2)) + switch team { + case 0: + s.server.db.Exec("INSERT INTO festa_registrations VALUES ($1, 'blue')", guild.ID) + case 1: + s.server.db.Exec("INSERT INTO festa_registrations VALUES ($1, 'red')", guild.ID) + } + bf := byteframe.NewByteFrame() + bf.WriteUint32(team) doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) } func handleMsgMhfChargeFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfChargeFesta) - // Update festa state table + s.server.db.Exec("UPDATE guild_characters SET souls=souls+$1 WHERE character_id=$2", pkt.Souls, s.charID) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfAcquireFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireFesta) - // Mark festa as claimed - // Update guild table? doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfAcquireFestaPersonalPrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireFestaPersonalPrize) - // Set prize as claimed + s.server.db.Exec("INSERT INTO public.festa_prizes_accepted VALUES ($1, $2)", pkt.PrizeID, s.charID) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } func handleMsgMhfAcquireFestaIntermediatePrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireFestaIntermediatePrize) - // Set prize as claimed + s.server.db.Exec("INSERT INTO public.festa_prizes_accepted VALUES ($1, $2)", pkt.PrizeID, s.charID) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } -// uint32 numPrizes -// struct festaPrize -// uint32 prizeID -// uint32 prizeTier (1/2/3, 3 = GR) -// uint32 soulsReq -// uint32 unk (00 00 00 07) -// uint32 itemID -// uint32 numItem -// bool claimed +type Prize struct { + ID uint32 `db:"id"` + Tier uint32 `db:"tier"` + SoulsReq uint32 `db:"souls_req"` + ItemID uint32 `db:"item_id"` + NumItem uint32 `db:"num_item"` + Claimed int `db:"claimed"` +} func handleMsgMhfEnumerateFestaPersonalPrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateFestaPersonalPrize) - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + rows, _ := s.server.db.Queryx("SELECT id, tier, souls_req, item_id, num_item, (SELECT count(*) FROM festa_prizes_accepted fpa WHERE fp.id = fpa.prize_id AND fpa.character_id = 4) AS claimed FROM festa_prizes fp WHERE type='personal'") + var count uint32 + prizeData := byteframe.NewByteFrame() + for rows.Next() { + prize := &Prize{} + err := rows.StructScan(&prize) + if err != nil { + continue + } + count++ + prizeData.WriteUint32(prize.ID) + prizeData.WriteUint32(prize.Tier) + prizeData.WriteUint32(prize.SoulsReq) + prizeData.WriteUint32(7) // Unk + prizeData.WriteUint32(prize.ItemID) + prizeData.WriteUint32(prize.NumItem) + prizeData.WriteBool(prize.Claimed > 0) + } + bf := byteframe.NewByteFrame() + bf.WriteUint32(count) + bf.WriteBytes(prizeData.Data()) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } func handleMsgMhfEnumerateFestaIntermediatePrize(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateFestaIntermediatePrize) - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + rows, _ := s.server.db.Queryx("SELECT id, tier, souls_req, item_id, num_item, (SELECT count(*) FROM festa_prizes_accepted fpa WHERE fp.id = fpa.prize_id AND fpa.character_id = 4) AS claimed FROM festa_prizes fp WHERE type='guild'") + var count uint32 + prizeData := byteframe.NewByteFrame() + for rows.Next() { + prize := &Prize{} + err := rows.StructScan(&prize) + if err != nil { + continue + } + count++ + prizeData.WriteUint32(prize.ID) + prizeData.WriteUint32(prize.Tier) + prizeData.WriteUint32(prize.SoulsReq) + prizeData.WriteUint32(7) // Unk + prizeData.WriteUint32(prize.ItemID) + prizeData.WriteUint32(prize.NumItem) + prizeData.WriteBool(prize.Claimed > 0) + } + bf := byteframe.NewByteFrame() + bf.WriteUint32(count) + bf.WriteBytes(prizeData.Data()) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } diff --git a/server/channelserver/handlers_guild.go b/server/channelserver/handlers_guild.go index 0bcfdfcbd..2a0ec1256 100644 --- a/server/channelserver/handlers_guild.go +++ b/server/channelserver/handlers_guild.go @@ -56,6 +56,7 @@ type Guild struct { PugiName3 string `db:"pugi_name_3"` Recruiting bool `db:"recruiting"` FestivalColour FestivalColour `db:"festival_colour"` + Souls uint32 `db:"souls"` Rank uint16 `db:"rank"` AllianceID uint32 `db:"alliance_id"` Icon *GuildIcon `db:"icon"` @@ -121,11 +122,12 @@ SELECT leader_id, lc.name as leader_name, comment, - pugi_name_1, - pugi_name_2, - pugi_name_3, + COALESCE(pugi_name_1, '') AS pugi_name_1, + COALESCE(pugi_name_2, '') AS pugi_name_2, + COALESCE(pugi_name_3, '') AS pugi_name_3, recruiting, - festival_colour, + COALESCE((SELECT team FROM festa_registrations fr WHERE fr.guild_id = g.id), 'none') AS festival_colour, + (SELECT SUM(souls) FROM guild_characters gc WHERE gc.guild_id = g.id) AS souls, CASE WHEN rank_rp <= 48 THEN rank_rp/24 WHEN rank_rp <= 288 THEN rank_rp/48+1 @@ -134,23 +136,14 @@ SELECT WHEN rank_rp < 1200 THEN 16 ELSE 17 END rank, - CASE WHEN ( + COALESCE(( SELECT id FROM guild_alliances ga WHERE ga.parent_id = g.id OR ga.sub1_id = g.id OR ga.sub2_id = g.id - ) IS NULL THEN 0 - ELSE ( - SELECT id FROM guild_alliances ga WHERE - ga.parent_id = g.id OR - ga.sub1_id = g.id OR - ga.sub2_id = g.id - ) - END alliance_id, + ), 0) AS alliance_id, icon, - ( - SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id - ) AS member_count + (SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count FROM guilds g JOIN guild_characters lgc ON lgc.character_id = leader_id JOIN characters lc on leader_id = lc.id @@ -158,8 +151,8 @@ SELECT func (guild *Guild) Save(s *Session) error { _, err := s.server.db.Exec(` - UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7, festival_colour=$8, icon=$9 WHERE id=$1 - `, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3, guild.FestivalColour, guild.Icon) + UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7, icon=$8, leader_id=$9 WHERE id=$1 + `, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3, guild.Icon, guild.GuildLeader.LeaderCharID) if err != nil { s.logger.Error("failed to update guild data", zap.Error(err), zap.Uint32("guildID", guild.ID)) @@ -643,6 +636,33 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint32(uint32(response)) doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) return + case mhfpacket.OPERATE_GUILD_RESIGN: + guildMembers, err := GetGuildMembers(s, guild.ID, false) + success := false + if err == nil { + sort.Slice(guildMembers[:], func(i, j int) bool { + return guildMembers[i].OrderIndex < guildMembers[j].OrderIndex + }) + for i := 1; i < len(guildMembers); i++ { + if !guildMembers[i].AvoidLeadership { + guild.LeaderCharID = guildMembers[i].CharID + guildMembers[0].OrderIndex = guildMembers[i].OrderIndex + guildMembers[i].OrderIndex = 1 + guildMembers[0].Save(s) + guildMembers[i].Save(s) + bf.WriteUint32(guildMembers[i].CharID) + success = true + break + } + } + guild.Save(s) + doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) + } + if !success { + bf.WriteUint32(0) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + } + return case mhfpacket.OPERATE_GUILD_APPLY: err = guild.CreateApplication(s, s.charID, GuildApplicationTypeApplied, nil) @@ -667,6 +687,14 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) { if err != nil { // All successful acks return 0x01, assuming 0x00 is failure response = 0x00 + } else { + mail := Mail{ + RecipientID: s.charID, + Subject: "Withdrawal", + Body: fmt.Sprintf("You have withdrawn from 「%s」.", guild.Name), + IsSystemMessage: true, + } + mail.Send(s, nil) } bf.WriteUint32(uint32(response)) @@ -790,14 +818,14 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) { guild, err := GetGuildInfoByCharacterId(s, pkt.CharID) if err != nil || guild == nil { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } actorCharacter, err := GetCharacterGuildData(s, s.charID) if err != nil || (!actorCharacter.IsSubLeader() && guild.LeaderCharID != s.charID) { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) return } @@ -806,41 +834,45 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) { case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT: err = guild.AcceptApplication(s, pkt.CharID) mail = Mail{ - SenderID: s.charID, - RecipientID: pkt.CharID, - Subject: "Accepted!", - Body: fmt.Sprintf("Your application to join 「%s」 was accepted.", guild.Name), - IsGuildInvite: false, + RecipientID: pkt.CharID, + Subject: "Accepted!", + Body: fmt.Sprintf("Your application to join 「%s」 was accepted.", guild.Name), + IsSystemMessage: true, } case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_REJECT: err = guild.RejectApplication(s, pkt.CharID) mail = Mail{ - SenderID: s.charID, - RecipientID: pkt.CharID, - Subject: "Rejected", - Body: fmt.Sprintf("Your application to join 「%s」 was rejected.", guild.Name), - IsGuildInvite: false, + RecipientID: pkt.CharID, + Subject: "Rejected", + Body: fmt.Sprintf("Your application to join 「%s」 was rejected.", guild.Name), + IsSystemMessage: true, } case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_KICK: err = guild.RemoveCharacter(s, pkt.CharID) mail = Mail{ - SenderID: s.charID, - RecipientID: pkt.CharID, - Subject: "Kicked", - Body: fmt.Sprintf("You were kicked from 「%s」.", guild.Name), - IsGuildInvite: false, + RecipientID: pkt.CharID, + Subject: "Kicked", + Body: fmt.Sprintf("You were kicked from 「%s」.", guild.Name), + IsSystemMessage: true, } default: - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) s.logger.Warn(fmt.Sprintf("unhandled operateGuildMember action '%d'", pkt.Action)) } if err != nil { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) } else { mail.Send(s, nil) + for _, channel := range s.server.Channels { + for _, session := range channel.sessions { + if session.charID == pkt.CharID { + SendMailNotification(s, &mail, session) + } + } + } + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } - doAckSimpleSucceed(s, pkt.AckHandle, nil) } func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { @@ -914,24 +946,12 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { bf.WriteBytes(guildLeaderName) bf.WriteBytes([]byte{0x00, 0x00, 0x00, 0x00}) // Unk bf.WriteBool(false) // isReturnGuild - bf.WriteBytes([]byte{0x01, 0x02, 0x02}) // Unk + bf.WriteBool(false) // earnedSpecialHall + bf.WriteBytes([]byte{0x02, 0x02}) // Unk bf.WriteUint32(guild.EventRP) - - if guild.PugiName1 == "" { - bf.WriteUint16(0x0100) - } else { - ps.Uint8(bf, guild.PugiName1, true) - } - if guild.PugiName2 == "" { - bf.WriteUint16(0x0100) - } else { - ps.Uint8(bf, guild.PugiName2, true) - } - if guild.PugiName3 == "" { - bf.WriteUint16(0x0100) - } else { - ps.Uint8(bf, guild.PugiName3, true) - } + ps.Uint8(bf, guild.PugiName1, true) + ps.Uint8(bf, guild.PugiName2, true) + ps.Uint8(bf, guild.PugiName3, true) // probably guild pugi properties, should be status, stamina and luck outfits bf.WriteBytes([]byte{ @@ -952,7 +972,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { } else { bf.WriteUint32(alliance.ID) bf.WriteUint32(uint32(alliance.CreatedAt.Unix())) - bf.WriteUint16(uint16(alliance.TotalMembers)) + bf.WriteUint16(alliance.TotalMembers) bf.WriteUint16(0) // Unk0 ps.Uint16(bf, alliance.Name, true) if alliance.SubGuild1ID > 0 { @@ -1015,7 +1035,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } - if err != nil { + if err != nil || characterGuildData.IsApplicant { bf.WriteUint16(0) } else { bf.WriteUint16(uint16(len(applicants))) @@ -1063,15 +1083,11 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { } else { bf.WriteUint8(0x00) } + bf.WriteUint8(0) // Unk doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } else { - //// REALLY large/complex format... stubbing it out here for simplicity. - //resp := byteframe.NewByteFrame() - //resp.WriteUint32(0) // Count - //resp.WriteUint8(0) // Unk, read if count == 0. - - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 8)) + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 5)) } } @@ -1260,6 +1276,14 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { guild, err = GetGuildInfoByCharacterId(s, s.charID) } + if guild != nil { + isApplicant, _ := guild.HasApplicationForCharID(s, s.charID) + if isApplicant { + doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + return + } + } + if guild == nil && s.prevGuildID > 0 { guild, err = GetGuildInfoByID(s, s.prevGuildID) } @@ -1305,7 +1329,8 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint16(0x0600) } bf.WriteUint8(member.OrderIndex) - ps.Uint16(bf, member.Name, true) + bf.WriteBool(member.AvoidLeadership) + ps.Uint8(bf, member.Name, true) } for _, member := range guildMembers { @@ -1414,12 +1439,8 @@ func handleMsgMhfGetGuildTargetMemberNum(s *Session, p mhfpacket.MHFPacket) { guild, err = GetGuildInfoByID(s, pkt.GuildID) } - if err != nil { - s.logger.Warn("failed to find guild", zap.Error(err), zap.Uint32("guildID", pkt.GuildID)) - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) - return - } else if guild == nil { - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4)) + if err != nil || guild == nil { + doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x02}) return } diff --git a/server/channelserver/handlers_guild_member.go b/server/channelserver/handlers_guild_member.go index 5fa83fb6b..2c3f0cd1a 100644 --- a/server/channelserver/handlers_guild_member.go +++ b/server/channelserver/handlers_guild_member.go @@ -12,6 +12,7 @@ type GuildMember struct { GuildID uint32 `db:"guild_id"` CharID uint32 `db:"character_id"` JoinedAt *time.Time `db:"joined_at"` + Souls uint32 `db:"souls"` Name string `db:"name"` IsApplicant bool `db:"is_applicant"` OrderIndex uint8 `db:"order_index"` @@ -25,12 +26,25 @@ type GuildMember struct { WeaponType uint16 `db:"weapon_type"` } +func (gm *GuildMember) CanRecruit() bool { + if gm.Recruiter { + return true + } + if gm.OrderIndex <= 3 { + return true + } + if gm.IsLeader { + return true + } + return false +} + func (gm *GuildMember) IsSubLeader() bool { - return gm.OrderIndex <= 3 && !gm.AvoidLeadership + return gm.OrderIndex <= 3 } func (gm *GuildMember) Save(s *Session) error { - _, err := s.server.db.Exec("UPDATE guild_characters SET avoid_leadership=$1 WHERE character_id=$2", gm.AvoidLeadership, gm.CharID) + _, err := s.server.db.Exec("UPDATE guild_characters SET avoid_leadership=$1, order_index=$2 WHERE character_id=$3", gm.AvoidLeadership, gm.OrderIndex, gm.CharID) if err != nil { s.logger.Error( @@ -48,6 +62,7 @@ const guildMembersSelectSQL = ` SELECT g.id as guild_id, joined_at, + coalesce(souls, 0) as souls, c.name, character.character_id, coalesce(gc.order_index, 0) as order_index, diff --git a/server/channelserver/handlers_guild_scout.go b/server/channelserver/handlers_guild_scout.go index 4bcbb6b91..f47c28bc6 100644 --- a/server/channelserver/handlers_guild_scout.go +++ b/server/channelserver/handlers_guild_scout.go @@ -21,7 +21,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { panic(err) } - if actorCharGuildData == nil || !actorCharGuildData.Recruiter { + if actorCharGuildData == nil || !actorCharGuildData.CanRecruit() { doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) return } @@ -59,19 +59,13 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { panic(err) } - senderName, err := getCharacterName(s, s.charID) - - if err != nil { - panic(err) - } - mail := &Mail{ SenderID: s.charID, RecipientID: pkt.CharID, - Subject: "Guild! ヽ(・∀・)ノ", + Subject: "Invitation!", Body: fmt.Sprintf( - "%s has invited you to join the wonderful guild %s, do you accept this challenge?", - senderName, + "%s has invited you to join 「%s」\nDo you want to accept?", + getCharacterName(s, s.charID), guildInfo.Name, ), IsGuildInvite: true, @@ -104,7 +98,7 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) { panic(err) } - if guildCharData == nil || !guildCharData.Recruiter { + if guildCharData == nil || !guildCharData.CanRecruit() { doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) return } @@ -128,61 +122,72 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAnswerGuildScout) - + bf := byteframe.NewByteFrame() guild, err := GetGuildInfoByCharacterId(s, pkt.LeaderID) if err != nil { panic(err) } - _, err = guild.GetApplicationForCharID(s, s.charID, GuildApplicationTypeInvited) + app, err := guild.GetApplicationForCharID(s, s.charID, GuildApplicationTypeInvited) - if err != nil { + if app == nil || err != nil { s.logger.Warn( - "could not retrieve guild invitation", + "Guild invite missing, deleted?", zap.Error(err), zap.Uint32("guildID", guild.ID), zap.Uint32("charID", s.charID), ) - doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) + bf.WriteUint32(7) + bf.WriteUint32(guild.ID) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) return } + var mail []Mail if pkt.Answer { err = guild.AcceptApplication(s, s.charID) + mail = append(mail, Mail{ + RecipientID: s.charID, + Subject: "Success!", + Body: fmt.Sprintf("You successfully joined 「%s」.", guild.Name), + IsSystemMessage: true, + }) + mail = append(mail, Mail{ + SenderID: s.charID, + RecipientID: pkt.LeaderID, + Subject: "Accepted", + Body: fmt.Sprintf("%s accepted your invitation to join 「%s」.", s.Name, guild.Name), + IsSystemMessage: true, + }) } else { err = guild.RejectApplication(s, s.charID) + mail = append(mail, Mail{ + RecipientID: s.charID, + Subject: "Declined", + Body: fmt.Sprintf("You declined the invitation to join 「%s」.", guild.Name), + IsSystemMessage: true, + }) + mail = append(mail, Mail{ + SenderID: s.charID, + RecipientID: pkt.LeaderID, + Subject: "Declined", + Body: fmt.Sprintf("%s declined your invitation to join 「%s」.", s.Name, guild.Name), + IsSystemMessage: true, + }) } - if err != nil { - doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) - return + bf.WriteUint32(7) + bf.WriteUint32(guild.ID) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + } else { + bf.WriteUint32(0) + bf.WriteUint32(guild.ID) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + for _, m := range mail { + m.Send(s, nil) + } } - - senderName, err := getCharacterName(s, pkt.LeaderID) - - if err != nil { - doAckSimpleFail(s, pkt.AckHandle, nil) - panic(err) - } - - successMail := Mail{ - SenderID: pkt.LeaderID, - RecipientID: s.charID, - Subject: "Happy days!", - Body: fmt.Sprintf("You successfully joined %s and should be proud of all you have accomplished.", guild.Name), - IsGuildInvite: false, - SenderName: senderName, - } - - err = successMail.Send(s, nil) - - if err != nil { - doAckSimpleFail(s, pkt.AckHandle, nil) - panic(err) - } - - doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x81, 0x7e}) } func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) { @@ -191,12 +196,12 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) { guildInfo, err := GetGuildInfoByCharacterId(s, s.charID) if guildInfo == nil && s.prevGuildID == 0 { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } else { guildInfo, err = GetGuildInfoByID(s, s.prevGuildID) if guildInfo == nil || err != nil { - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } } @@ -210,7 +215,7 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) { if err != nil { s.logger.Error("failed to retrieve scouted characters", zap.Error(err)) - doAckSimpleFail(s, pkt.AckHandle, nil) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } diff --git a/server/channelserver/handlers_mail.go b/server/channelserver/handlers_mail.go index 1e060b245..e31e90eed 100644 --- a/server/channelserver/handlers_mail.go +++ b/server/channelserver/handlers_mail.go @@ -25,21 +25,22 @@ type Mail struct { AttachedItemAmount uint16 `db:"attached_item_amount"` CreatedAt time.Time `db:"created_at"` IsGuildInvite bool `db:"is_guild_invite"` + IsSystemMessage bool `db:"is_sys_message"` SenderName string `db:"sender_name"` } func (m *Mail) Send(s *Session, transaction *sql.Tx) error { query := ` - INSERT INTO mail (sender_id, recipient_id, subject, body, attached_item, attached_item_amount, is_guild_invite) - VALUES ($1, $2, $3, $4, $5, $6, $7) + INSERT INTO mail (sender_id, recipient_id, subject, body, attached_item, attached_item_amount, is_guild_invite, is_sys_message) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ` var err error if transaction == nil { - _, err = s.server.db.Exec(query, m.SenderID, m.RecipientID, m.Subject, m.Body, m.AttachedItemID, m.AttachedItemAmount, m.IsGuildInvite) + _, err = s.server.db.Exec(query, m.SenderID, m.RecipientID, m.Subject, m.Body, m.AttachedItemID, m.AttachedItemAmount, m.IsGuildInvite, m.IsSystemMessage) } else { - _, err = transaction.Exec(query, m.SenderID, m.RecipientID, m.Subject, m.Body, m.AttachedItemID, m.AttachedItemAmount, m.IsGuildInvite) + _, err = transaction.Exec(query, m.SenderID, m.RecipientID, m.Subject, m.Body, m.AttachedItemID, m.AttachedItemAmount, m.IsGuildInvite, m.IsSystemMessage) } if err != nil { @@ -53,6 +54,7 @@ func (m *Mail) Send(s *Session, transaction *sql.Tx) error { zap.Uint16("itemID", m.AttachedItemID), zap.Uint16("itemAmount", m.AttachedItemAmount), zap.Bool("isGuildInvite", m.IsGuildInvite), + zap.Bool("isSystemMessage", m.IsSystemMessage), ) return err } @@ -141,6 +143,7 @@ func GetMailListForCharacter(s *Session, charID uint32) ([]Mail, error) { m.attached_item_amount, m.created_at, m.is_guild_invite, + m.is_sys_message, m.deleted, m.locked, c.name as sender_name @@ -189,6 +192,7 @@ func GetMailByID(s *Session, ID int) (*Mail, error) { m.attached_item_amount, m.created_at, m.is_guild_invite, + m.is_sys_message, m.deleted, m.locked, c.name as sender_name @@ -215,16 +219,10 @@ func GetMailByID(s *Session, ID int) (*Mail, error) { } func SendMailNotification(s *Session, m *Mail, recipient *Session) { - senderName, err := getCharacterName(s, m.SenderID) - - if err != nil { - panic(err) - } - bf := byteframe.NewByteFrame() notification := &binpacket.MsgBinMailNotify{ - SenderName: senderName, + SenderName: getCharacterName(s, m.SenderID), } notification.Build(bf) @@ -241,7 +239,7 @@ func SendMailNotification(s *Session, m *Mail, recipient *Session) { recipient.QueueSendMHF(castedBinary) } -func getCharacterName(s *Session, charID uint32) (string, error) { +func getCharacterName(s *Session, charID uint32) string { row := s.server.db.QueryRow("SELECT name FROM characters WHERE id = $1", charID) charName := "" @@ -249,10 +247,9 @@ func getCharacterName(s *Session, charID uint32) (string, error) { err := row.Scan(&charName) if err != nil { - return "", err + return "" } - - return charName, nil + return charName } func handleMsgMhfReadMail(s *Session, p mhfpacket.MHFPacket) { @@ -325,8 +322,9 @@ func handleMsgMhfListMail(s *Session, p mhfpacket.MHFPacket) { flags |= 0x02 } - // System message, hides ID - // flags |= 0x04 + if m.IsSystemMessage { + flags |= 0x04 + } // Workaround until EN mail items are patched if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.DisableMailItems {