diff --git a/common/byteframe/byteframe.go b/common/byteframe/byteframe.go index ffeb66dc1..357595fe0 100644 --- a/common/byteframe/byteframe.go +++ b/common/byteframe/byteframe.go @@ -138,6 +138,10 @@ func (b *ByteFrame) DataFromCurrent() []byte { return b.buf[b.index:b.usedSize] } +func (b *ByteFrame) Index() uint { + return b.index +} + // SetLE sets the byte order to litte endian. func (b *ByteFrame) SetLE() { b.byteOrder = binary.LittleEndian diff --git a/common/decryption/jpk.go b/common/decryption/jpk.go new file mode 100644 index 000000000..e40c97a43 --- /dev/null +++ b/common/decryption/jpk.go @@ -0,0 +1,117 @@ +package decryption + +/* + This code is HEAVILY based from + https://github.com/Chakratos/ReFrontier/blob/master/ReFrontier/Unpack.cs +*/ + +import ( + "erupe-ce/common/byteframe" + "io" +) + +var m_shiftIndex int = 0 +var m_flag byte = byte(0) + +func UnpackSimple(data []byte) []byte { + m_shiftIndex = 0 + m_flag = byte(0) + + bf := byteframe.NewByteFrameFromBytes(data) + bf.SetLE() + header := bf.ReadUint32() + + println("Decrypting") + + if header == 0x1A524B4A { + bf.Seek(0x2, io.SeekCurrent) + jpkType := bf.ReadUint16() + println("JPK Type: ", jpkType) + + switch jpkType { + case 3: + startOffset := bf.ReadInt32() + outSize := bf.ReadInt32() + outBuffer := make([]byte, outSize) + bf.Seek(int64(startOffset), io.SeekStart) + ProcessDecode(bf, outBuffer) + + return outBuffer + } + } + + println("Skipping") + + return data +} + +func ProcessDecode(data *byteframe.ByteFrame, outBuffer []byte) { + outIndex := 0 + + for int(data.Index()) < len(data.Data()) && outIndex < len(outBuffer)-1 { + if JPKBitshift(data) == 0 { + outBuffer[outIndex] = ReadByte(data) + outIndex++ + continue + } else { + if JPKBitshift(data) == 0 { + len := (JPKBitshift(data) << 1) | JPKBitshift(data) + off := ReadByte(data) + JPKCopy(outBuffer, int(off), int(len)+3, &outIndex) + continue + } else { + hi := ReadByte(data) + lo := ReadByte(data) + var len int = int((hi & 0xE0)) >> 5 + var off int = ((int(hi) & 0x1F) << 8) | int(lo) + if len != 0 { + JPKCopy(outBuffer, off, len+2, &outIndex) + continue + } else { + if JPKBitshift(data) == 0 { + len := (JPKBitshift(data) << 3) | (JPKBitshift(data) << 2) | (JPKBitshift(data) << 1) | JPKBitshift(data) + JPKCopy(outBuffer, off, int(len)+2+8, &outIndex) + continue + } else { + temp := ReadByte(data) + if temp == 0xFF { + for i := 0; i < off+0x1B; i++ { + outBuffer[outIndex] = ReadByte(data) + outIndex++ + continue + } + } else { + JPKCopy(outBuffer, off, int(temp)+0x1a, &outIndex) + } + } + } + } + } + } +} + +func JPKBitshift(data *byteframe.ByteFrame) byte { + m_shiftIndex-- + + if m_shiftIndex < 0 { + m_shiftIndex = 7 + m_flag = ReadByte(data) + } + + return (byte)((m_flag >> m_shiftIndex) & 1) +} + +func JPKCopy(outBuffer []byte, offset int, length int, index *int) { + for i := 0; i < length; i++ { + outBuffer[*index] = outBuffer[*index-offset-1] + *index++ + } +} + +func ReadByte(bf *byteframe.ByteFrame) byte { + value := bf.ReadUint8() + if value < 0 { + println("Not implemented") + } + return byte(value) +} diff --git a/patch-schema/event_quests.sql b/patch-schema/event_quests.sql new file mode 100644 index 000000000..85e7bccfe --- /dev/null +++ b/patch-schema/event_quests.sql @@ -0,0 +1,11 @@ +BEGIN; + +create table if not exists event_quests +( + id serial, + max_players integer, + quest_type integer, + quest_id uint16 not null +); + +END; \ No newline at end of file diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index 8f40f7539..eb984f028 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -1,14 +1,17 @@ package channelserver import ( + "database/sql" "erupe-ce/common/byteframe" + "erupe-ce/common/decryption" "erupe-ce/network/mhfpacket" "fmt" - "go.uber.org/zap" "io" "os" "path/filepath" "time" + + "go.uber.org/zap" ) func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) { @@ -79,34 +82,170 @@ func handleMsgMhfSaveFavoriteQuest(s *Session, p mhfpacket.MHFPacket) { doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } +func readOriginalPointers(string_pointer int32, quest []byte) []byte { + file_bytes := byteframe.NewByteFrameFromBytes(quest) + file_bytes.SetLE() + file_bytes.Seek(int64(string_pointer), io.SeekStart) + + quest_name_pointer := file_bytes.ReadInt32() + quest_main_pointer := file_bytes.ReadInt32() + quest_a_pointer := file_bytes.ReadInt32() + quest_b_pointer := file_bytes.ReadInt32() + quest_clear_pointer := file_bytes.ReadInt32() + quest_failure_pointer := file_bytes.ReadInt32() + quest_contractor_pointer := file_bytes.ReadInt32() + quest_description_pointer := file_bytes.ReadInt32() + //Use String header pointers to seek to string and then read null terminated string + file_bytes.Seek(int64(quest_name_pointer), io.SeekStart) + quest_name_string := file_bytes.ReadNullTerminatedBytes() + file_bytes.Seek(int64(quest_main_pointer), io.SeekStart) + quest_main_string := file_bytes.ReadNullTerminatedBytes() + file_bytes.Seek(int64(quest_a_pointer), io.SeekStart) + quest_a_string := file_bytes.ReadNullTerminatedBytes() + file_bytes.Seek(int64(quest_b_pointer), io.SeekStart) + quest_b_string := file_bytes.ReadNullTerminatedBytes() + file_bytes.Seek(int64(quest_clear_pointer), io.SeekStart) + quest_clear_string := file_bytes.ReadNullTerminatedBytes() + file_bytes.Seek(int64(quest_failure_pointer), io.SeekStart) + quest_failure_string := file_bytes.ReadNullTerminatedBytes() + file_bytes.Seek(int64(quest_contractor_pointer), io.SeekStart) + quest_contractor_string := file_bytes.ReadNullTerminatedBytes() + file_bytes.Seek(int64(quest_description_pointer), io.SeekStart) + quest_description_string := file_bytes.ReadNullTerminatedBytes() + + new_pointers := byteframe.NewByteFrame() + new_pointers.SetLE() + pointer_start := 352 + + new_pointers.WriteInt32(int32(pointer_start)) + new_pointers.WriteInt32(int32(pointer_start + len(quest_name_string) + 1)) + new_pointers.WriteInt32(int32(pointer_start + len(quest_name_string) + len(quest_main_string) + 2)) + new_pointers.WriteInt32(int32(pointer_start + len(quest_name_string) + len(quest_main_string) + len(quest_a_string) + 3)) + new_pointers.WriteInt32(int32(pointer_start + len(quest_name_string) + len(quest_main_string) + len(quest_a_string) + len(quest_b_string) + 26)) + new_pointers.WriteInt32(int32(pointer_start + len(quest_name_string) + len(quest_main_string) + len(quest_a_string) + len(quest_b_string) + len(quest_clear_string) + 5)) + new_pointers.WriteInt32(int32(pointer_start + len(quest_name_string) + len(quest_main_string) + len(quest_a_string) + len(quest_b_string) + len(quest_clear_string) + len(quest_failure_string) + 6)) + new_pointers.WriteInt32(int32(pointer_start + len(quest_name_string) + len(quest_main_string) + len(quest_a_string) + len(quest_b_string) + len(quest_clear_string) + len(quest_failure_string) + len(quest_contractor_string) + 7)) + + new_pointers.WriteNullTerminatedBytes(quest_name_string) + new_pointers.WriteNullTerminatedBytes(quest_main_string) + new_pointers.WriteNullTerminatedBytes(quest_a_string) + new_pointers.WriteNullTerminatedBytes(quest_b_string) + new_pointers.WriteNullTerminatedBytes(quest_clear_string) + new_pointers.WriteNullTerminatedBytes(quest_failure_string) + new_pointers.WriteNullTerminatedBytes(quest_contractor_string) + new_pointers.WriteNullTerminatedBytes(quest_description_string) + new_pointers.WriteUint8(18) + new_pointers.WriteBytes([]byte{0x83, 0x59, 0x89, 0x5B, 0x83, 0x3A, 0x58, 0xB6, 0x8E, 0x59, 0x82, 0xCC, 0x83, 0x58, 0x83, 0x58, 0x83, 0x81}) + + return new_pointers.Data() +} + +func loadQuestFile(s *Session, quest_id string) []byte { + file, err := os.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, fmt.Sprintf("quests/%s.bin", quest_id))) + + if err != nil { + s.logger.Error(fmt.Sprintf("Failed to open file: %s/quests/%s.bin", s.server.erupeConfig.BinPath, quest_id)) + return nil + } + + println(fmt.Sprintf("quests/%s.bin", quest_id)) + decrypted := decryption.UnpackSimple(file) + os.WriteFile(fmt.Sprintf("%s.bin", quest_id), decrypted, 0644) + + println("Reading quest file " + quest_id) + + file_bytes := byteframe.NewByteFrameFromBytes(decrypted) + file_bytes.SetLE() + file_bytes.Seek(0, io.SeekStart) + + data_pointer := file_bytes.ReadInt32() + file_bytes.Seek(int64(data_pointer), io.SeekStart) + + // Read the quest data. + quest_body := file_bytes.ReadBytes(320) + + quest_pointer := byteframe.NewByteFrameFromBytes(quest_body) + quest_pointer.SetLE() + + quest_pointer.Seek(40, io.SeekStart) + string_pointer := quest_pointer.ReadInt32() + + strings := readOriginalPointers(string_pointer, decrypted) + + body := byteframe.NewByteFrame() + body.WriteBytes(quest_body) + body.Seek(0, io.SeekEnd) + body.WriteBytes(strings) + + return body.Data() +} + +func makeEventQuest(s *Session, rows *sql.Rows) ([]byte, error) { + var id int32 + var max_players, quest_type, quest_id uint16 + rows.Scan(&id, &max_players, &quest_type, &quest_id) + + bf := byteframe.NewByteFrame() + bf.SetLE() + bf.WriteInt32(id) + bf.WriteInt32(0) + bf.WriteBytes([]byte{0x0F, byte(max_players), byte(quest_type), 0x01}) + bf.WriteUint16(0) + bf.WriteUint16(0) + bf.WriteBytes([]byte{0x00, 0x01}) + bf.WriteUint16(0) + bf.WriteBytes([]byte{0x02, 0x00}) + + data := loadQuestFile(s, fmt.Sprintf("%d", quest_id)+"d0") + + if data == nil { + return nil, fmt.Errorf("failed to load quest file") + } + + bf.WriteBytes(data) + + // update checksum + bf.Seek(21, io.SeekStart) + bf.WriteUint8(uint8(len(bf.Data()) - 553)) + + // overwrite bytes at pos 62 + bf.Seek(62, io.SeekStart) + bf.WriteInt16(320) + + return bf.Data(), nil +} + func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateQuest) var totalCount, returnedCount uint16 bf := byteframe.NewByteFrame() bf.WriteUint16(0) - filepath.Walk(fmt.Sprintf("%s/events/", s.server.erupeConfig.BinPath), func(path string, info os.FileInfo, err error) error { + + rows, _ := s.server.db.Query("SELECT id, max_players, quest_type, quest_id FROM event_quests ORDER BY quest_id ASC") + + // Loop through each row and load the quest entry if it exists. + for rows.Next() { + var pointer []byte + var max_players, quest_type, checksum, quest_id uint16 + rows.Scan(&pointer, &max_players, &quest_type, &checksum, &quest_id) + + data, err := makeEventQuest(s, rows) + if err != nil { - return err - } else if info.IsDir() { - return nil - } - data, err := os.ReadFile(path) - if err != nil { - return err + continue } else { if len(data) > 850 || len(data) < 400 { - return nil // Could be more or less strict with size limits + continue } else { totalCount++ if totalCount > pkt.Offset && len(bf.Data()) < 60000 { returnedCount++ bf.WriteBytes(data) - return nil + continue } } } - return nil - }) + } type tuneValue struct { ID uint16 @@ -549,6 +688,7 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint16(pkt.Offset) bf.Seek(0, io.SeekStart) bf.WriteUint16(returnedCount) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) }