diff --git a/bundled-schema/OtherShops.sql b/bundled-schema/OtherShops.sql index d25e453b0..3c88bb896 100644 --- a/bundled-schema/OtherShops.sql +++ b/bundled-schema/OtherShops.sql @@ -5,6 +5,42 @@ INSERT INTO public.shop_items VALUES (5,5,16516,100,1,0,0,1,0,0,0,0), (5,5,16517,100,1,0,0,1,0,0,0,0), + (6,5,9958,3,3,1,0,0,0,0,0,0), + (6,5,1897,3,1,1,0,0,0,0,0,0), + (6,5,8889,3,1,0,0,1,0,0,0,0), + (6,5,6176,3,6,1,0,0,0,0,0,0), + (6,5,1472,3,10,1,0,0,0,0,0,0), + (6,5,7280,3,3,0,0,1,0,0,0,0), + (6,5,8027,3,30,1,0,0,0,0,0,0), + (6,5,8028,3,30,1,0,0,0,0,0,0), + (6,5,8029,3,30,1,0,0,0,0,0,0), + (6,5,8026,3,30,1,0,0,0,0,0,0), + (6,5,8030,3,30,1,0,0,0,0,0,0), + (6,5,4353,3,30,1,0,0,0,0,0,0), + (6,5,4354,3,30,1,0,0,0,0,0,0), + (6,5,4355,3,30,1,0,0,0,0,0,0), + (6,5,4356,3,30,1,0,0,0,0,0,0), + (6,5,4357,3,30,1,0,0,0,0,0,0), + (6,5,4745,3,30,1,0,0,0,0,0,0), + (6,5,4746,3,30,1,0,0,0,0,0,0), + (6,5,4747,3,30,1,0,0,0,0,0,0), + (6,5,4748,3,30,1,0,0,0,0,0,0), + (6,5,4749,3,30,1,0,0,0,0,0,0), + (6,5,5122,3,30,1,0,0,0,0,0,0), + (6,5,5123,3,30,1,0,0,0,0,0,0), + (6,5,5124,3,30,1,0,0,0,0,0,0), + (6,5,5125,3,30,1,0,0,0,0,0,0), + (6,5,5126,3,30,1,0,0,0,0,0,0), + (6,5,5795,3,30,1,0,0,0,0,0,0), + (6,5,5796,3,30,1,0,0,0,0,0,0), + (6,5,5797,3,30,1,0,0,0,0,0,0), + (6,5,5798,3,30,1,0,0,0,0,0,0), + (6,5,5799,3,30,1,0,0,0,0,0,0), + (6,5,6168,3,30,1,0,0,0,0,0,0), + (6,5,6169,3,30,1,0,0,0,0,0,0), + (6,5,6170,3,30,1,0,0,0,0,0,0), + (6,5,6171,3,30,1,0,0,0,0,0,0), + (6,5,6172,3,30,1,0,0,0,0,0,0), (7,0,13190,10,1,0,0,0,0,0,0,0), (7,0,1662,10,1,0,0,0,0,0,0,0), (7,0,10179,100,1,0,0,0,0,0,0,0); diff --git a/config.json b/config.json index aabc3d62a..cc74da8de 100644 --- a/config.json +++ b/config.json @@ -14,6 +14,7 @@ "ClientMode": "ZZ", "QuestCacheExpiry": 300, "ProxyPort": 0, + "CommandPrefix": "!", "DevMode": true, "DevModeOptions": { "AutoCreateAccount": true, @@ -30,7 +31,7 @@ "QuestDebugTools": false, "EarthStatusOverride": 0, "EarthIDOverride": 0, - "EarthMonsterOverride": 0, + "EarthMonsterOverride": [0, 0, 0, 0], "SaveDumps": { "Enabled": true, "OutputDir": "save-backups" @@ -82,32 +83,44 @@ }, "Commands": [ { + "Name": "Help", + "Enabled": true, + "Description": "Show enabled chat commands", + "Prefix": "help" + }, { "Name": "Rights", "Enabled": false, + "Description": "Overwrite the Rights value on your account", "Prefix": "rights" }, { "Name": "Raviente", "Enabled": true, + "Description": "Various Raviente siege commands", "Prefix": "ravi" }, { "Name": "Teleport", "Enabled": false, + "Description": "Teleport to specified coordinates", "Prefix": "tele" }, { "Name": "Reload", "Enabled": true, + "Description": "Reload all players in your Land", "Prefix": "reload" }, { "Name": "KeyQuest", "Enabled": false, + "Description": "Overwrite your HR Key Quest progress", "Prefix": "kqf" }, { "Name": "Course", "Enabled": true, + "Description": "Toggle Courses on your account", "Prefix": "course" }, { "Name": "PSN", "Enabled": true, + "Description": "Link a PlayStation Network ID to your account", "Prefix": "psn" }, { "Name": "Discord", diff --git a/config/config.go b/config/config.go index 4c1809e40..aea353d25 100644 --- a/config/config.go +++ b/config/config.go @@ -81,6 +81,7 @@ type Config struct { RealClientMode Mode QuestCacheExpiry int // Number of seconds to keep quest data cached ProxyPort uint16 // Forces the game to connect to a channel server proxy + CommandPrefix string // The prefix for commands DevMode bool DevModeOptions DevModeOptions @@ -111,7 +112,7 @@ type DevModeOptions struct { QuestDebugTools bool // Enable various quest debug logs EarthStatusOverride int32 EarthIDOverride int32 - EarthMonsterOverride int32 + EarthMonsterOverride []int32 SaveDumps SaveDumpOptions } @@ -173,9 +174,10 @@ type DiscordRealTime struct { // Command is a channelserver chat command type Command struct { - Name string - Enabled bool - Prefix string + Name string + Enabled bool + Description string + Prefix string } // Course represents a course within MHF diff --git a/network/mhfpacket/msg_mhf_enumerate_shop.go b/network/mhfpacket/msg_mhf_enumerate_shop.go index 153095db4..d57655e98 100644 --- a/network/mhfpacket/msg_mhf_enumerate_shop.go +++ b/network/mhfpacket/msg_mhf_enumerate_shop.go @@ -14,7 +14,7 @@ type MsgMhfEnumerateShop struct { AckHandle uint32 ShopType uint8 // 1 running gachas, 10 normal shop extensions, 8 Diva Defense shop ShopID uint32 - Unk2 uint16 // 00 80 running gachas, 00 20 normal shop + Limit uint16 Unk3 uint8 Unk4 uint8 Unk5 uint32 @@ -30,7 +30,7 @@ func (m *MsgMhfEnumerateShop) Parse(bf *byteframe.ByteFrame, ctx *clientctx.Clie m.AckHandle = bf.ReadUint32() m.ShopType = bf.ReadUint8() m.ShopID = bf.ReadUint32() - m.Unk2 = bf.ReadUint16() + m.Limit = bf.ReadUint16() m.Unk3 = bf.ReadUint8() if _config.ErupeConfig.RealClientMode >= _config.G2 { m.Unk4 = bf.ReadUint8() diff --git a/network/mhfpacket/msg_mhf_post_seibattle.go b/network/mhfpacket/msg_mhf_post_seibattle.go index 7e3e578c4..9c9101747 100644 --- a/network/mhfpacket/msg_mhf_post_seibattle.go +++ b/network/mhfpacket/msg_mhf_post_seibattle.go @@ -1,15 +1,24 @@ package mhfpacket -import ( - "errors" +import ( + "errors" - "erupe-ce/network/clientctx" - "erupe-ce/network" "erupe-ce/common/byteframe" + "erupe-ce/network" + "erupe-ce/network/clientctx" ) // MsgMhfPostSeibattle represents the MSG_MHF_POST_SEIBATTLE -type MsgMhfPostSeibattle struct{} +type MsgMhfPostSeibattle struct { + AckHandle uint32 + Unk0 uint8 + Unk1 uint8 + Unk2 uint32 + Unk3 uint8 + Unk4 uint16 + Unk5 uint16 + Unk6 uint8 +} // Opcode returns the ID associated with this packet type. func (m *MsgMhfPostSeibattle) Opcode() network.PacketID { @@ -18,7 +27,15 @@ func (m *MsgMhfPostSeibattle) Opcode() network.PacketID { // Parse parses the packet from binary func (m *MsgMhfPostSeibattle) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { - return errors.New("NOT IMPLEMENTED") + m.AckHandle = bf.ReadUint32() + m.Unk0 = bf.ReadUint8() + m.Unk1 = bf.ReadUint8() + m.Unk2 = bf.ReadUint32() + m.Unk3 = bf.ReadUint8() + m.Unk4 = bf.ReadUint16() + m.Unk5 = bf.ReadUint16() + m.Unk6 = bf.ReadUint8() + return nil } // Build builds a binary packet from the current data. diff --git a/server/channelserver/handlers.go b/server/channelserver/handlers.go index abba0cfe6..7445d0f2b 100644 --- a/server/channelserver/handlers.go +++ b/server/channelserver/handlers.go @@ -992,10 +992,17 @@ func handleMsgMhfGetEarthStatus(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint32(uint32(TimeWeekNext().Unix())) // End bf.WriteInt32(s.server.erupeConfig.DevModeOptions.EarthStatusOverride) bf.WriteInt32(s.server.erupeConfig.DevModeOptions.EarthIDOverride) - bf.WriteInt32(s.server.erupeConfig.DevModeOptions.EarthMonsterOverride) - bf.WriteInt32(0) - bf.WriteInt32(0) - bf.WriteInt32(0) + for i, m := range s.server.erupeConfig.DevModeOptions.EarthMonsterOverride { + if _config.ErupeConfig.RealClientMode <= _config.G9 { + if i == 3 { + break + } + } + if i == 4 { + break + } + bf.WriteInt32(m) + } doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } @@ -1195,7 +1202,10 @@ func handleMsgMhfGetSeibattle(s *Session, p mhfpacket.MHFPacket) { doAckEarthSucceed(s, pkt.AckHandle, data) } -func handleMsgMhfPostSeibattle(s *Session, p mhfpacket.MHFPacket) {} +func handleMsgMhfPostSeibattle(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfPostSeibattle) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} func handleMsgMhfGetDailyMissionMaster(s *Session, p mhfpacket.MHFPacket) {} diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 08dee353e..b0646fcce 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -85,7 +85,7 @@ func sendServerChatMessage(s *Session, message string) { } func parseChatCommand(s *Session, command string) { - args := strings.Split(command[1:], " ") + args := strings.Split(command[len(s.server.erupeConfig.CommandPrefix):], " ") switch args[0] { case commands["PSN"].Prefix: if commands["PSN"].Enabled { @@ -344,6 +344,15 @@ func parseChatCommand(s *Session, command string) { sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscordSuccess"], discordToken)) } else { sendDisabledCommandMessage(s, commands["Discord"]) + case commands["Help"].Prefix: + if commands["Help"].Enabled { + for _, command := range commands { + if command.Enabled { + sendServerChatMessage(s, fmt.Sprintf("%s%s: %s", s.server.erupeConfig.CommandPrefix, command.Prefix, command.Description)) + } + } + } else { + sendDisabledCommandMessage(s, commands["Help"]) } } } @@ -418,7 +427,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { bf.SetLE() chatMessage := &binpacket.MsgBinChat{} chatMessage.Parse(bf) - if strings.HasPrefix(chatMessage.Message, "!") { + if strings.HasPrefix(chatMessage.Message, s.server.erupeConfig.CommandPrefix) { parseChatCommand(s, chatMessage.Message) return } diff --git a/server/channelserver/handlers_diva.go b/server/channelserver/handlers_diva.go index 1867bfacd..2c60e9767 100644 --- a/server/channelserver/handlers_diva.go +++ b/server/channelserver/handlers_diva.go @@ -72,7 +72,7 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { var timestamps []uint32 if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.DivaEvent >= 0 { if s.server.erupeConfig.DevModeOptions.DivaEvent == 0 { - if s.server.erupeConfig.RealClientMode <= _config.Z1 { + if s.server.erupeConfig.RealClientMode >= _config.Z2 { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 32)) } else { doAckBufSucceed(s, pkt.AckHandle, make([]byte, 36)) @@ -84,7 +84,7 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { timestamps = generateDivaTimestamps(s, start, false) } - if s.server.erupeConfig.RealClientMode <= _config.Z1 { + if s.server.erupeConfig.RealClientMode >= _config.Z2 { bf.WriteUint32(id) } for i := range timestamps { diff --git a/server/channelserver/handlers_house.go b/server/channelserver/handlers_house.go index 35ebfa308..62561c96d 100644 --- a/server/channelserver/handlers_house.go +++ b/server/channelserver/handlers_house.go @@ -262,66 +262,55 @@ func handleMsgMhfLoadDecoMyset(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfSaveDecoMyset(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSaveDecoMyset) - // TODO: Backwards compatibility for DecoMyset - if s.server.erupeConfig.RealClientMode < _config.ZZ { + var temp []byte + err := s.server.db.QueryRow("SELECT decomyset FROM characters WHERE id = $1", s.charID).Scan(&temp) + if err != nil { + s.logger.Error("Failed to load decomyset", zap.Error(err)) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return } - // https://gist.github.com/Andoryuuta/9c524da7285e4b5ca7e52e0fc1ca1daf - var loadData []byte - bf := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload[1:]) // skip first unk byte - err := s.server.db.QueryRow("SELECT decomyset FROM characters WHERE id = $1", s.charID).Scan(&loadData) - if err != nil { - s.logger.Error("Failed to load decomyset", zap.Error(err)) - } else { - numSets := bf.ReadUint8() // sets being written - // empty save - if len(loadData) == 0 { - loadData = []byte{0x01, 0x00} - } - savedSets := loadData[1] // existing saved sets - // no sets, new slice with just first 2 bytes for appends later - if savedSets == 0 { - loadData = []byte{0x01, 0x00} - } - for i := 0; i < int(numSets); i++ { - writeSet := bf.ReadUint16() - dataChunk := bf.ReadBytes(76) - setBytes := append([]byte{uint8(writeSet >> 8), uint8(writeSet & 0xff)}, dataChunk...) - for x := 0; true; x++ { - if x == int(savedSets) { - // appending set - if loadData[len(loadData)-1] == 0x10 { - // sanity check for if there was a messy manual import - loadData = append(loadData[:len(loadData)-2], setBytes...) - } else { - loadData = append(loadData, setBytes...) - } - savedSets++ - break - } - currentSet := loadData[3+(x*78)] - if int(currentSet) == int(writeSet) { - // replacing a set - loadData = append(loadData[:2+(x*78)], append(setBytes, loadData[2+((x+1)*78):]...)...) - break - } else if int(currentSet) > int(writeSet) { - // inserting before current set - loadData = append(loadData[:2+((x)*78)], append(setBytes, loadData[2+((x)*78):]...)...) - savedSets++ - break - } - } - loadData[1] = savedSets // update set count - } - dumpSaveData(s, loadData, "decomyset") - _, err := s.server.db.Exec("UPDATE characters SET decomyset=$1 WHERE id=$2", loadData, s.charID) - if err != nil { - s.logger.Error("Failed to save decomyset", zap.Error(err)) - } + // Version handling + bf := byteframe.NewByteFrame() + var size uint + if s.server.erupeConfig.RealClientMode >= _config.G10 { + size = 76 + bf.WriteUint8(1) + } else { + size = 68 + bf.WriteUint8(0) } - doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) + + // Handle nil data + if len(temp) == 0 { + temp = append(bf.Data(), uint8(0)) + } + + // Build a map of set data + sets := make(map[uint16][]byte) + oldSets := byteframe.NewByteFrameFromBytes(temp[2:]) + for i := uint8(0); i < temp[1]; i++ { + index := oldSets.ReadUint16() + sets[index] = oldSets.ReadBytes(size) + } + + // Overwrite existing sets + newSets := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload[2:]) + for i := uint8(0); i < pkt.RawDataPayload[1]; i++ { + index := newSets.ReadUint16() + sets[index] = newSets.ReadBytes(size) + } + + // Serialise the set data + bf.WriteUint8(uint8(len(sets))) + for u, b := range sets { + bf.WriteUint16(u) + bf.WriteBytes(b) + } + + dumpSaveData(s, bf.Data(), "decomyset") + s.server.db.Exec("UPDATE characters SET decomyset=$1 WHERE id=$2", bf.Data(), s.charID) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } type Title struct { diff --git a/server/channelserver/handlers_shop_gacha.go b/server/channelserver/handlers_shop_gacha.go index c18119c6e..3058fb632 100644 --- a/server/channelserver/handlers_shop_gacha.go +++ b/server/channelserver/handlers_shop_gacha.go @@ -10,13 +10,13 @@ import ( type ShopItem struct { ID uint32 `db:"id"` - ItemID uint16 `db:"item_id"` + ItemID uint32 `db:"item_id"` Cost uint32 `db:"cost"` Quantity uint16 `db:"quantity"` MinHR uint16 `db:"min_hr"` MinSR uint16 `db:"min_sr"` MinGR uint16 `db:"min_gr"` - StoreLevel uint16 `db:"store_level"` + StoreLevel uint8 `db:"store_level"` MaxQuantity uint16 `db:"max_quantity"` UsedQuantity uint16 `db:"used_quantity"` RoadFloors uint16 `db:"road_floors"` @@ -61,19 +61,30 @@ func writeShopItems(bf *byteframe.ByteFrame, items []ShopItem) { bf.WriteUint16(uint16(len(items))) bf.WriteUint16(uint16(len(items))) for _, item := range items { - bf.WriteUint32(item.ID) - bf.WriteUint16(0) - bf.WriteUint16(item.ItemID) + if _config.ErupeConfig.RealClientMode >= _config.Z2 { + bf.WriteUint32(item.ID) + } + bf.WriteUint32(item.ItemID) bf.WriteUint32(item.Cost) bf.WriteUint16(item.Quantity) bf.WriteUint16(item.MinHR) bf.WriteUint16(item.MinSR) - bf.WriteUint16(item.MinGR) - bf.WriteUint16(item.StoreLevel) - bf.WriteUint16(item.MaxQuantity) - bf.WriteUint16(item.UsedQuantity) - bf.WriteUint16(item.RoadFloors) - bf.WriteUint16(item.RoadFatalis) + if _config.ErupeConfig.RealClientMode >= _config.Z2 { + bf.WriteUint16(item.MinGR) + } + bf.WriteUint8(0) // Unk + bf.WriteUint8(item.StoreLevel) + if _config.ErupeConfig.RealClientMode >= _config.Z2 { + bf.WriteUint16(item.MaxQuantity) + bf.WriteUint16(item.UsedQuantity) + } + if _config.ErupeConfig.RealClientMode == _config.Z1 { + bf.WriteUint8(uint8(item.RoadFloors)) + bf.WriteUint8(uint8(item.RoadFatalis)) + } else if _config.ErupeConfig.RealClientMode >= _config.Z2 { + bf.WriteUint16(item.RoadFloors) + bf.WriteUint16(item.RoadFatalis) + } } } @@ -241,6 +252,9 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) { case 10: // Item shop, 0-8 bf := byteframe.NewByteFrame() items := getShopItems(s, pkt.ShopType, pkt.ShopID) + if len(items) > int(pkt.Limit) { + items = items[:pkt.Limit] + } writeShopItems(bf, items) doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } diff --git a/server/channelserver/handlers_stage.go b/server/channelserver/handlers_stage.go index 3092db3fd..abd9d3ba5 100644 --- a/server/channelserver/handlers_stage.go +++ b/server/channelserver/handlers_stage.go @@ -171,8 +171,8 @@ func handleMsgSysBackStage(s *Session, p mhfpacket.MHFPacket) { // Transfer back to the saved stage ID before the previous move or enter. backStage, err := s.stageMoveStack.Pop() - if err != nil { - panic(err) + if backStage == "" || err != nil { + backStage = "sl1Ns200p0a0u0" } if _, exists := s.stage.reservedClientSlots[s.charID]; exists { diff --git a/server/channelserver/handlers_tower.go b/server/channelserver/handlers_tower.go index a9bc1421c..4ce0bcc9f 100644 --- a/server/channelserver/handlers_tower.go +++ b/server/channelserver/handlers_tower.go @@ -126,8 +126,9 @@ func handleMsgMhfPostTowerInfo(s *Session, p mhfpacket.MHFPacket) { skills := "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" s.server.db.QueryRow(`SELECT skills FROM tower WHERE char_id=$1`, s.charID).Scan(&skills) s.server.db.Exec(`UPDATE tower SET skills=$1, tsp=tsp-$2 WHERE char_id=$3`, stringsupport.CSVSetIndex(skills, int(pkt.Skill), stringsupport.CSVGetIndex(skills, int(pkt.Skill))+1), pkt.Cost, s.charID) - case 7: - s.server.db.Exec(`UPDATE tower SET tr=$1, trp=trp+$2, block1=block1+$3 WHERE char_id=$4`, pkt.TR, pkt.TRP, pkt.Block1, s.charID) + case 1, 7: + // This might give too much TSP? No idea what the rate is supposed to be + s.server.db.Exec(`UPDATE tower SET tr=$1, trp=COALESCE(trp, 0)+$2, tsp=COALESCE(tsp, 0)+$3, block1=COALESCE(block1, 0)+$4 WHERE char_id=$5`, pkt.TR, pkt.TRP, pkt.Cost, pkt.Block1, s.charID) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) }