Merge pull request #34 from ZeruLight/feature/festa

feature/festa
This commit is contained in:
wish
2022-08-26 20:09:21 +10:00
committed by GitHub
11 changed files with 889 additions and 309 deletions

View File

@@ -13,9 +13,8 @@
"LogInboundMessages": false,
"LogOutboundMessages": false,
"MaxHexdumpLength": 256,
"Event": 0,
"DivaEvent": 0,
"FestaEvent": 0,
"FestaEvent": -1,
"TournamentEvent": 0,
"MezFesEvent": true,
"MezFesAlt": false,

View File

@@ -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
}

311
patch-schema/festa.sql Normal file
View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}
bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // TS Current Time
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
}
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())
}

View File

@@ -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,
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,
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,
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)
}
// 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
}

View File

@@ -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,

View File

@@ -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)
}
if err != nil {
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
return
}
senderName, err := getCharacterName(s, pkt.LeaderID)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, nil)
panic(err)
}
successMail := Mail{
SenderID: pkt.LeaderID,
mail = append(mail, Mail{
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,
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,
})
}
err = successMail.Send(s, nil)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, nil)
panic(err)
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)
}
}
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
}

View File

@@ -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 {