mirror of
https://github.com/Mezeporta/Erupe.git
synced 2025-12-15 00:15:08 +01:00
More saves, launcher tweaks
Added config at char select screen Transmog saving and loading (unlocks are not yet tackled and it is global rn) Sigil saving (can break, initial pass of save) Null compression and decompression functions. First pass of the diff save function. Disabled stamp cards
This commit is contained in:
@@ -6,7 +6,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MsgMhfAddAchievement represents the MSG_MHF_ADD_ACHIEVEMENT
|
// MsgMhfAddAchievement represents the MSG_MHF_ADD_ACHIEVEMENT
|
||||||
type MsgMhfAddAchievement struct{}
|
type MsgMhfAddAchievement struct{
|
||||||
|
Unk0 []byte
|
||||||
|
}
|
||||||
|
|
||||||
// Opcode returns the ID associated with this packet type.
|
// Opcode returns the ID associated with this packet type.
|
||||||
func (m *MsgMhfAddAchievement) Opcode() network.PacketID {
|
func (m *MsgMhfAddAchievement) Opcode() network.PacketID {
|
||||||
@@ -15,10 +17,12 @@ func (m *MsgMhfAddAchievement) Opcode() network.PacketID {
|
|||||||
|
|
||||||
// Parse parses the packet from binary
|
// Parse parses the packet from binary
|
||||||
func (m *MsgMhfAddAchievement) Parse(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfAddAchievement) Parse(bf *byteframe.ByteFrame) error {
|
||||||
panic("Not implemented")
|
m.Unk0 = bf.ReadBytes(5)
|
||||||
|
// doesn't expect a response
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build builds a binary packet from the current data.
|
// Build builds a binary packet from the current data.
|
||||||
func (m *MsgMhfAddAchievement) Build(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfAddAchievement) Build(bf *byteframe.ByteFrame) error {
|
||||||
panic("Not implemented")
|
panic("Not implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
type MsgMhfSaveHunterNavi struct {
|
type MsgMhfSaveHunterNavi struct {
|
||||||
AckHandle uint32
|
AckHandle uint32
|
||||||
DataSize uint32
|
DataSize uint32
|
||||||
Unk0 bool
|
IsDataDiff bool
|
||||||
RawDataPayload []byte
|
RawDataPayload []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ func (m *MsgMhfSaveHunterNavi) Opcode() network.PacketID {
|
|||||||
func (m *MsgMhfSaveHunterNavi) Parse(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfSaveHunterNavi) Parse(bf *byteframe.ByteFrame) error {
|
||||||
m.AckHandle = bf.ReadUint32()
|
m.AckHandle = bf.ReadUint32()
|
||||||
m.DataSize = bf.ReadUint32()
|
m.DataSize = bf.ReadUint32()
|
||||||
m.Unk0 = bf.ReadBool()
|
m.IsDataDiff = bf.ReadBool()
|
||||||
m.RawDataPayload = bf.ReadBytes(uint(m.DataSize))
|
m.RawDataPayload = bf.ReadBytes(uint(m.DataSize))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MsgMhfSavePlateBox represents the MSG_MHF_SAVE_PLATE_BOX
|
// MsgMhfSavePlateBox represents the MSG_MHF_SAVE_PLATE_BOX
|
||||||
type MsgMhfSavePlateBox struct{}
|
type MsgMhfSavePlateBox struct{
|
||||||
|
AckHandle uint32
|
||||||
|
DataSize uint32
|
||||||
|
IsDataDiff bool
|
||||||
|
RawDataPayload []byte
|
||||||
|
}
|
||||||
|
|
||||||
// Opcode returns the ID associated with this packet type.
|
// Opcode returns the ID associated with this packet type.
|
||||||
func (m *MsgMhfSavePlateBox) Opcode() network.PacketID {
|
func (m *MsgMhfSavePlateBox) Opcode() network.PacketID {
|
||||||
@@ -15,10 +20,14 @@ func (m *MsgMhfSavePlateBox) Opcode() network.PacketID {
|
|||||||
|
|
||||||
// Parse parses the packet from binary
|
// Parse parses the packet from binary
|
||||||
func (m *MsgMhfSavePlateBox) Parse(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfSavePlateBox) Parse(bf *byteframe.ByteFrame) error {
|
||||||
panic("Not implemented")
|
m.AckHandle = bf.ReadUint32()
|
||||||
|
m.DataSize = bf.ReadUint32()
|
||||||
|
m.IsDataDiff = bf.ReadBool()
|
||||||
|
m.RawDataPayload = bf.ReadBytes(uint(m.DataSize))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build builds a binary packet from the current data.
|
// Build builds a binary packet from the current data.
|
||||||
func (m *MsgMhfSavePlateBox) Build(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfSavePlateBox) Build(bf *byteframe.ByteFrame) error {
|
||||||
panic("Not implemented")
|
panic("Not implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MsgMhfSavePlateData represents the MSG_MHF_SAVE_PLATE_DATA
|
// MsgMhfSavePlateData represents the MSG_MHF_SAVE_PLATE_DATA
|
||||||
type MsgMhfSavePlateData struct{}
|
type MsgMhfSavePlateData struct{
|
||||||
|
AckHandle uint32
|
||||||
|
DataSize uint32
|
||||||
|
IsDataDiff bool
|
||||||
|
RawDataPayload []byte
|
||||||
|
}
|
||||||
|
|
||||||
// Opcode returns the ID associated with this packet type.
|
// Opcode returns the ID associated with this packet type.
|
||||||
func (m *MsgMhfSavePlateData) Opcode() network.PacketID {
|
func (m *MsgMhfSavePlateData) Opcode() network.PacketID {
|
||||||
@@ -15,10 +20,14 @@ func (m *MsgMhfSavePlateData) Opcode() network.PacketID {
|
|||||||
|
|
||||||
// Parse parses the packet from binary
|
// Parse parses the packet from binary
|
||||||
func (m *MsgMhfSavePlateData) Parse(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfSavePlateData) Parse(bf *byteframe.ByteFrame) error {
|
||||||
panic("Not implemented")
|
m.AckHandle = bf.ReadUint32()
|
||||||
|
m.DataSize = bf.ReadUint32()
|
||||||
|
m.IsDataDiff = bf.ReadBool()
|
||||||
|
m.RawDataPayload = bf.ReadBytes(uint(m.DataSize))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build builds a binary packet from the current data.
|
// Build builds a binary packet from the current data.
|
||||||
func (m *MsgMhfSavePlateData) Build(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfSavePlateData) Build(bf *byteframe.ByteFrame) error {
|
||||||
panic("Not implemented")
|
panic("Not implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
type MsgMhfSavedata struct {
|
type MsgMhfSavedata struct {
|
||||||
AckHandle uint32
|
AckHandle uint32
|
||||||
AllocMemSize uint32
|
AllocMemSize uint32
|
||||||
Unk0 uint8 // Either 1 or 2, representing a true or false value for some reason.
|
SaveType uint8 // Either 1 or 2, representing a true or false value for some reason.
|
||||||
Unk1 uint32
|
Unk1 uint32
|
||||||
DataSize uint32
|
DataSize uint32
|
||||||
RawDataPayload []byte
|
RawDataPayload []byte
|
||||||
@@ -24,10 +24,14 @@ func (m *MsgMhfSavedata) Opcode() network.PacketID {
|
|||||||
func (m *MsgMhfSavedata) Parse(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfSavedata) Parse(bf *byteframe.ByteFrame) error {
|
||||||
m.AckHandle = bf.ReadUint32()
|
m.AckHandle = bf.ReadUint32()
|
||||||
m.AllocMemSize = bf.ReadUint32()
|
m.AllocMemSize = bf.ReadUint32()
|
||||||
m.Unk0 = bf.ReadUint8()
|
m.SaveType = bf.ReadUint8()
|
||||||
m.Unk1 = bf.ReadUint32()
|
m.Unk1 = bf.ReadUint32()
|
||||||
m.DataSize = bf.ReadUint32()
|
m.DataSize = bf.ReadUint32()
|
||||||
m.RawDataPayload = bf.ReadBytes(uint(m.DataSize))
|
if m.SaveType == 1 {
|
||||||
|
m.RawDataPayload = bf.ReadBytes(uint(m.AllocMemSize))
|
||||||
|
} else if m.SaveType == 2 {
|
||||||
|
m.RawDataPayload = bf.ReadBytes(uint(m.DataSize))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MsgMhfUpdateEquipSkinHist represents the MSG_MHF_UPDATE_EQUIP_SKIN_HIST
|
// MsgMhfUpdateEquipSkinHist represents the MSG_MHF_UPDATE_EQUIP_SKIN_HIST
|
||||||
type MsgMhfUpdateEquipSkinHist struct{}
|
type MsgMhfUpdateEquipSkinHist struct{
|
||||||
|
AckHandle uint32
|
||||||
|
}
|
||||||
|
|
||||||
// Opcode returns the ID associated with this packet type.
|
// Opcode returns the ID associated with this packet type.
|
||||||
func (m *MsgMhfUpdateEquipSkinHist) Opcode() network.PacketID {
|
func (m *MsgMhfUpdateEquipSkinHist) Opcode() network.PacketID {
|
||||||
@@ -15,10 +17,11 @@ func (m *MsgMhfUpdateEquipSkinHist) Opcode() network.PacketID {
|
|||||||
|
|
||||||
// Parse parses the packet from binary
|
// Parse parses the packet from binary
|
||||||
func (m *MsgMhfUpdateEquipSkinHist) Parse(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfUpdateEquipSkinHist) Parse(bf *byteframe.ByteFrame) error {
|
||||||
panic("Not implemented")
|
m.AckHandle = bf.ReadUint32()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build builds a binary packet from the current data.
|
// Build builds a binary packet from the current data.
|
||||||
func (m *MsgMhfUpdateEquipSkinHist) Build(bf *byteframe.ByteFrame) error {
|
func (m *MsgMhfUpdateEquipSkinHist) Build(bf *byteframe.ByteFrame) error {
|
||||||
panic("Not implemented")
|
panic("Not implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -52,6 +53,128 @@ func doSizedAckResp(s *Session, ackHandle uint32, data []byte) {
|
|||||||
s.QueueAck(ackHandle, bfw.Data())
|
s.QueueAck(ackHandle, bfw.Data())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// process a datadiff save for platebox and platedata
|
||||||
|
func saveDataDiff(b []byte, save []byte) []byte {
|
||||||
|
// there are a bunch of extra variations on this method in use which this does not handle yet
|
||||||
|
// specifically this is for diffs with seek amounts trailed by 02 followed by bytes to be written
|
||||||
|
var seekBytes []byte
|
||||||
|
seekOperation := 0
|
||||||
|
write := byte(0)
|
||||||
|
for(len(b) > 2){
|
||||||
|
if bytes.IndexRune(b, 2) != 0 {
|
||||||
|
seekBytes = b[:bytes.IndexRune(b, 2)+1]
|
||||||
|
} else {
|
||||||
|
seekBytes = b[:bytes.IndexRune(b[1:], 2)+2]
|
||||||
|
}
|
||||||
|
if len(seekBytes) == 1{
|
||||||
|
seekBytes = b[:bytes.IndexRune(b, 2)+2]
|
||||||
|
//fmt.Printf("Seek: %d SeekBytes: %X Write: %X\n", seekBytes[0], seekBytes, b[len(seekBytes)] )
|
||||||
|
seekOperation += int(seekBytes[0])
|
||||||
|
write = b[len(seekBytes)]
|
||||||
|
b = b[3:]
|
||||||
|
} else {
|
||||||
|
seek := int32(0)
|
||||||
|
for _, b := range seekBytes[:len(seekBytes)-1] {
|
||||||
|
seek = (seek << 8) | int32(b)
|
||||||
|
}
|
||||||
|
//fmt.Printf("Seek: %d SeekBytes: %X Write: %X\n", seek, seekBytes, b[len(seekBytes)] )
|
||||||
|
seekOperation += int(seek)
|
||||||
|
write = b[len(seekBytes)]
|
||||||
|
b = b[len(seekBytes)+1:]
|
||||||
|
}
|
||||||
|
save[seekOperation-1] = write
|
||||||
|
}
|
||||||
|
|
||||||
|
return save
|
||||||
|
}
|
||||||
|
|
||||||
|
// decompress save data
|
||||||
|
func saveDecompress(compData []byte) ([]byte, error) {
|
||||||
|
r := bytes.NewReader(compData)
|
||||||
|
|
||||||
|
header := make([]byte, 16)
|
||||||
|
n, err := r.Read(header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if n != len(header) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(header, []byte("cmp\x2020110113\x20\x20\x20\x00")) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var output []byte
|
||||||
|
for {
|
||||||
|
b, err := r.ReadByte()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == 0 {
|
||||||
|
// If it's a null byte, then the next byte is how many nulls to add.
|
||||||
|
nullCount, err := r.ReadByte()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
output = append(output, make([]byte, int(nullCount))...)
|
||||||
|
} else {
|
||||||
|
output = append(output, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null compresses a save
|
||||||
|
func saveCompress(rawData []byte) ([]byte, error) {
|
||||||
|
r := bytes.NewReader(rawData)
|
||||||
|
var output []byte
|
||||||
|
output = append(output, []byte("cmp\x2020110113\x20\x20\x20\x00")...)
|
||||||
|
for {
|
||||||
|
b, err := r.ReadByte()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if b == 0 {
|
||||||
|
output = append(output, []byte{0x00}...)
|
||||||
|
// read to get null count
|
||||||
|
nullCount := 1
|
||||||
|
for {
|
||||||
|
i, err := r.ReadByte()
|
||||||
|
if err == io.EOF {
|
||||||
|
output = append(output, []byte{byte(nullCount)}...)
|
||||||
|
break
|
||||||
|
} else if i != 0 {
|
||||||
|
r.UnreadByte()
|
||||||
|
output = append(output, []byte{byte(nullCount)}...)
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nullCount++
|
||||||
|
|
||||||
|
if(nullCount == 255){
|
||||||
|
output = append(output, []byte{0xFF, 0x00}...)
|
||||||
|
nullCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//output = append(output, []byte{byte(nullCount)}...)
|
||||||
|
} else {
|
||||||
|
output = append(output, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func updateRights(s *Session) {
|
func updateRights(s *Session) {
|
||||||
update := &mhfpacket.MsgSysUpdateRight{
|
update := &mhfpacket.MsgSysUpdateRight{
|
||||||
Unk0: 0,
|
Unk0: 0,
|
||||||
@@ -899,9 +1022,13 @@ func handleMsgMhfSavedata(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
s.logger.Fatal("Error dumping savedata", zap.Error(err))
|
s.logger.Fatal("Error dumping savedata", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.server.db.Exec("UPDATE characters SET is_new_character=false, savedata=$1 WHERE id=$2", pkt.RawDataPayload, s.charID)
|
if pkt.SaveType == 2{
|
||||||
if err != nil {
|
_, err = s.server.db.Exec("UPDATE characters SET is_new_character=false, savedata=$1 WHERE id=$2", pkt.RawDataPayload, s.charID)
|
||||||
s.logger.Fatal("Failed to update savedata in db", zap.Error(err))
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to update savedata in db", zap.Error(err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Got savedata packet of type 1, not saving.")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.QueueAck(pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
s.QueueAck(pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
||||||
@@ -1446,7 +1573,45 @@ func handleMsgMhfLoadPlateData(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMsgMhfSavePlateData(s *Session, p mhfpacket.MHFPacket) {}
|
func handleMsgMhfSavePlateData(s *Session, p mhfpacket.MHFPacket) {
|
||||||
|
pkt := p.(*mhfpacket.MsgMhfSavePlateData)
|
||||||
|
err := ioutil.WriteFile(fmt.Sprintf("savedata\\%d_platedata.bin", time.Now().Unix()), pkt.RawDataPayload, 0644)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Error dumping platedata", zap.Error(err))
|
||||||
|
}
|
||||||
|
if pkt.IsDataDiff {
|
||||||
|
// https://gist.github.com/Andoryuuta/9c524da7285e4b5ca7e52e0fc1ca1daf
|
||||||
|
var data []byte
|
||||||
|
//load existing save
|
||||||
|
err := s.server.db.QueryRow("SELECT platedata FROM characters WHERE id = $1", s.charID).Scan(&data)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to get platedata savedata from db", zap.Error(err))
|
||||||
|
}
|
||||||
|
//decompress
|
||||||
|
fmt.Println("Decompressing...")
|
||||||
|
data, err = saveDecompress(data)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to decompress platedata from db", zap.Error(err))
|
||||||
|
}
|
||||||
|
// perform diff and compress it to write back to db
|
||||||
|
fmt.Println("Diffing...")
|
||||||
|
saveOutput, err := saveCompress(saveDataDiff(pkt.RawDataPayload, data))
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to diff and compress platedata savedata", zap.Error(err))
|
||||||
|
}
|
||||||
|
_, err = s.server.db.Exec("UPDATE characters SET platedata=$1 WHERE id=$2", saveOutput, s.charID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to update platedata savedata in db", zap.Error(err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// simply update database, no extra processing
|
||||||
|
_, err := s.server.db.Exec("UPDATE characters SET platedata=$1 WHERE id=$2", pkt.RawDataPayload, s.charID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to update platedata savedata in db", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.QueueAck(pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
||||||
|
}
|
||||||
|
|
||||||
func handleMsgMhfLoadPlateBox(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfLoadPlateBox(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfLoadPlateBox)
|
pkt := p.(*mhfpacket.MsgMhfLoadPlateBox)
|
||||||
@@ -1463,7 +1628,47 @@ func handleMsgMhfLoadPlateBox(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMsgMhfSavePlateBox(s *Session, p mhfpacket.MHFPacket) {}
|
func handleMsgMhfSavePlateBox(s *Session, p mhfpacket.MHFPacket) {
|
||||||
|
pkt := p.(*mhfpacket.MsgMhfSavePlateBox)
|
||||||
|
err := ioutil.WriteFile(fmt.Sprintf("savedata\\%d_platebox.bin", time.Now().Unix()), pkt.RawDataPayload, 0644)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Error dumping hunter platebox savedata", zap.Error(err))
|
||||||
|
}
|
||||||
|
if pkt.IsDataDiff {
|
||||||
|
var data []byte
|
||||||
|
//load existing save
|
||||||
|
err := s.server.db.QueryRow("SELECT platebox FROM characters WHERE id = $1", s.charID).Scan(&data)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to get sigil box savedata from db", zap.Error(err))
|
||||||
|
}
|
||||||
|
//decompress
|
||||||
|
fmt.Println("Decompressing...")
|
||||||
|
data, err = saveDecompress(data)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to decompress savedata from db", zap.Error(err))
|
||||||
|
}
|
||||||
|
// perform diff and compress it to write back to db
|
||||||
|
fmt.Println("Diffing...")
|
||||||
|
saveOutput, err := saveCompress(saveDataDiff(pkt.RawDataPayload, data))
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to diff and compress savedata", zap.Error(err))
|
||||||
|
}
|
||||||
|
_, err = s.server.db.Exec("UPDATE characters SET platebox=$1 WHERE id=$2", saveOutput, s.charID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to update platebox savedata in db", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
fmt.Println("Wrote recompressed save back to DB.")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// simply update database, no extra processing
|
||||||
|
_, err := s.server.db.Exec("UPDATE characters SET platebox=$1 WHERE id=$2", pkt.RawDataPayload, s.charID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to update platedata savedata in db", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.QueueAck(pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
||||||
|
}
|
||||||
|
|
||||||
func handleMsgMhfReadGuildcard(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfReadGuildcard(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfReadGuildcard)
|
pkt := p.(*mhfpacket.MsgMhfReadGuildcard)
|
||||||
@@ -1678,7 +1883,26 @@ func handleMsgMhfLoadHunterNavi(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) {}
|
func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) {
|
||||||
|
pkt := p.(*mhfpacket.MsgMhfSaveHunterNavi)
|
||||||
|
err := ioutil.WriteFile(fmt.Sprintf("savedata\\%d_hunternavi.bin", time.Now().Unix()), pkt.RawDataPayload, 0644)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Error dumping hunter navigation savedata", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkt.IsDataDiff {
|
||||||
|
// https://gist.github.com/Andoryuuta/9c524da7285e4b5ca7e52e0fc1ca1daf
|
||||||
|
// doesn't seem fully consistent with platedata?
|
||||||
|
//
|
||||||
|
} else {
|
||||||
|
// simply update database, no extra processing
|
||||||
|
_, err := s.server.db.Exec("UPDATE characters SET hunternavi=$1 WHERE id=$2", pkt.RawDataPayload, s.charID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Fatal("Failed to update hunternavi savedata in db", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.QueueAck(pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
||||||
|
}
|
||||||
|
|
||||||
func handleMsgMhfRegistSpabiTime(s *Session, p mhfpacket.MHFPacket) {}
|
func handleMsgMhfRegistSpabiTime(s *Session, p mhfpacket.MHFPacket) {}
|
||||||
|
|
||||||
@@ -2109,122 +2333,122 @@ func handleMsgMhfGetUdMonsterPoint(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
pkt := p.(*mhfpacket.MsgMhfGetUdMonsterPoint)
|
pkt := p.(*mhfpacket.MsgMhfGetUdMonsterPoint)
|
||||||
|
|
||||||
monsterPoints := []struct {
|
monsterPoints := []struct {
|
||||||
MID uint8 // Monster ID ?
|
MID uint8
|
||||||
Points uint16
|
Points uint16
|
||||||
}{
|
}{
|
||||||
{MID: 0x01, Points: 0x3C},
|
{MID: 0x01, Points: 0x3C}, // em1 Rathian
|
||||||
{MID: 0x02, Points: 0x5A},
|
{MID: 0x02, Points: 0x5A}, // em2 Fatalis
|
||||||
{MID: 0x06, Points: 0x14},
|
{MID: 0x06, Points: 0x14}, // em6 Yian Kut-Ku
|
||||||
{MID: 0x07, Points: 0x50},
|
{MID: 0x07, Points: 0x50}, // em7 Lao-Shan Lung
|
||||||
{MID: 0x08, Points: 0x28},
|
{MID: 0x08, Points: 0x28}, // em8 Cephadrome
|
||||||
{MID: 0x0B, Points: 0x3C},
|
{MID: 0x0B, Points: 0x3C}, // em11 Rathalos
|
||||||
{MID: 0x0E, Points: 0x3C},
|
{MID: 0x0E, Points: 0x3C}, // em14 Diablos
|
||||||
{MID: 0x0F, Points: 0x46},
|
{MID: 0x0F, Points: 0x46}, // em15 Khezu
|
||||||
{MID: 0x11, Points: 0x46},
|
{MID: 0x11, Points: 0x46}, // em17 Gravios
|
||||||
{MID: 0x14, Points: 0x28},
|
{MID: 0x14, Points: 0x28}, // em20 Gypceros
|
||||||
{MID: 0x15, Points: 0x3C},
|
{MID: 0x15, Points: 0x3C}, // em21 Plesioth
|
||||||
{MID: 0x16, Points: 0x32},
|
{MID: 0x16, Points: 0x32}, // em22 Basarios
|
||||||
{MID: 0x1A, Points: 0x32},
|
{MID: 0x1A, Points: 0x32}, // em26 Monoblos
|
||||||
{MID: 0x1B, Points: 0x0A},
|
{MID: 0x1B, Points: 0x0A}, // em27 Velocidrome
|
||||||
{MID: 0x1C, Points: 0x0A},
|
{MID: 0x1C, Points: 0x0A}, // em28 Gendrome
|
||||||
{MID: 0x1F, Points: 0x0A},
|
{MID: 0x1F, Points: 0x0A}, // em31 Iodrome
|
||||||
{MID: 0x21, Points: 0x50},
|
{MID: 0x21, Points: 0x50}, // em33 Kirin
|
||||||
{MID: 0x24, Points: 0x64},
|
{MID: 0x24, Points: 0x64}, // em36 Crimson Fatalis
|
||||||
{MID: 0x25, Points: 0x3C},
|
{MID: 0x25, Points: 0x3C}, // em37 Pink Rathian
|
||||||
{MID: 0x26, Points: 0x1E},
|
{MID: 0x26, Points: 0x1E}, // em38 Blue Yian Kut-Ku
|
||||||
{MID: 0x27, Points: 0x28},
|
{MID: 0x27, Points: 0x28}, // em39 Purple Gypceros
|
||||||
{MID: 0x28, Points: 0x50},
|
{MID: 0x28, Points: 0x50}, // em40 Yian Garuga
|
||||||
{MID: 0x29, Points: 0x5A},
|
{MID: 0x29, Points: 0x5A}, // em41 Silver Rathalos
|
||||||
{MID: 0x2A, Points: 0x50},
|
{MID: 0x2A, Points: 0x50}, // em42 Gold Rathian
|
||||||
{MID: 0x2B, Points: 0x3C},
|
{MID: 0x2B, Points: 0x3C}, // em43 Black Diablos
|
||||||
{MID: 0x2C, Points: 0x3C},
|
{MID: 0x2C, Points: 0x3C}, // em44 White Monoblos
|
||||||
{MID: 0x2D, Points: 0x46},
|
{MID: 0x2D, Points: 0x46}, // em45 Red Khezu
|
||||||
{MID: 0x2E, Points: 0x3C},
|
{MID: 0x2E, Points: 0x3C}, // em46 Green Plesioth
|
||||||
{MID: 0x2F, Points: 0x50},
|
{MID: 0x2F, Points: 0x50}, // em47 Black Gravios
|
||||||
{MID: 0x30, Points: 0x1E},
|
{MID: 0x30, Points: 0x1E}, // em48 Daimyo Hermitaur
|
||||||
{MID: 0x31, Points: 0x3C},
|
{MID: 0x31, Points: 0x3C}, // em49 Azure Rathalos
|
||||||
{MID: 0x32, Points: 0x50},
|
{MID: 0x32, Points: 0x50}, // em50 Ashen Lao-Shan Lung
|
||||||
{MID: 0x33, Points: 0x3C},
|
{MID: 0x33, Points: 0x3C}, // em51 Blangonga
|
||||||
{MID: 0x34, Points: 0x28},
|
{MID: 0x34, Points: 0x28}, // em52 Congalala
|
||||||
{MID: 0x35, Points: 0x50},
|
{MID: 0x35, Points: 0x50}, // em53 Rajang
|
||||||
{MID: 0x36, Points: 0x6E},
|
{MID: 0x36, Points: 0x6E}, // em54 Kushala Daora
|
||||||
{MID: 0x37, Points: 0x50},
|
{MID: 0x37, Points: 0x50}, // em55 Shen Gaoren
|
||||||
{MID: 0x3A, Points: 0x50},
|
{MID: 0x3A, Points: 0x50}, // em58 Yama Tsukami
|
||||||
{MID: 0x3B, Points: 0x6E},
|
{MID: 0x3B, Points: 0x6E}, // em59 Chameleos
|
||||||
{MID: 0x40, Points: 0x64},
|
{MID: 0x40, Points: 0x64}, // em64 Lunastra
|
||||||
{MID: 0x41, Points: 0x6E},
|
{MID: 0x41, Points: 0x6E}, // em65 Teostra
|
||||||
{MID: 0x43, Points: 0x28},
|
{MID: 0x43, Points: 0x28}, // em67 Shogun Ceanataur
|
||||||
{MID: 0x44, Points: 0x0A},
|
{MID: 0x44, Points: 0x0A}, // em68 Bulldrome
|
||||||
{MID: 0x47, Points: 0x6E},
|
{MID: 0x47, Points: 0x6E}, // em71 White Fatalis
|
||||||
{MID: 0x4A, Points: 0xFA},
|
{MID: 0x4A, Points: 0xFA}, // em74 Hypnocatrice
|
||||||
{MID: 0x4B, Points: 0xFA},
|
{MID: 0x4B, Points: 0xFA}, // em75 Lavasioth
|
||||||
{MID: 0x4C, Points: 0x46},
|
{MID: 0x4C, Points: 0x46}, // em76 Tigrex
|
||||||
{MID: 0x4D, Points: 0x64},
|
{MID: 0x4D, Points: 0x64}, // em77 Akantor
|
||||||
{MID: 0x4E, Points: 0xFA},
|
{MID: 0x4E, Points: 0xFA}, // em78 Bright Hypnoc
|
||||||
{MID: 0x4F, Points: 0xFA},
|
{MID: 0x4F, Points: 0xFA}, // em79 Lavasioth Subspecies
|
||||||
{MID: 0x50, Points: 0xFA},
|
{MID: 0x50, Points: 0xFA}, // em80 Espinas
|
||||||
{MID: 0x51, Points: 0xFA},
|
{MID: 0x51, Points: 0xFA}, // em81 Orange Espinas
|
||||||
{MID: 0x52, Points: 0xFA},
|
{MID: 0x52, Points: 0xFA}, // em82 White Hypnoc
|
||||||
{MID: 0x53, Points: 0xFA},
|
{MID: 0x53, Points: 0xFA}, // em83 Akura Vashimu
|
||||||
{MID: 0x54, Points: 0xFA},
|
{MID: 0x54, Points: 0xFA}, // em84 Akura Jebia
|
||||||
{MID: 0x55, Points: 0xFA},
|
{MID: 0x55, Points: 0xFA}, // em85 Berukyurosu
|
||||||
{MID: 0x59, Points: 0xFA},
|
{MID: 0x59, Points: 0xFA}, // em89 Pariapuria
|
||||||
{MID: 0x5A, Points: 0xFA},
|
{MID: 0x5A, Points: 0xFA}, // em90 White Espinas
|
||||||
{MID: 0x5B, Points: 0xFA},
|
{MID: 0x5B, Points: 0xFA}, // em91 Kamu Orugaron
|
||||||
{MID: 0x5C, Points: 0xFA},
|
{MID: 0x5C, Points: 0xFA}, // em92 Nono Orugaron
|
||||||
{MID: 0x5E, Points: 0xFA},
|
{MID: 0x5E, Points: 0xFA}, // em94 Dyuragaua
|
||||||
{MID: 0x5F, Points: 0xFA},
|
{MID: 0x5F, Points: 0xFA}, // em95 Doragyurosu
|
||||||
{MID: 0x60, Points: 0xFA},
|
{MID: 0x60, Points: 0xFA}, // em96 Gurenzeburu
|
||||||
{MID: 0x63, Points: 0xFA},
|
{MID: 0x63, Points: 0xFA}, // em99 Rukodiora
|
||||||
{MID: 0x65, Points: 0xFA},
|
{MID: 0x65, Points: 0xFA}, // em101 Gogomoa
|
||||||
{MID: 0x67, Points: 0xFA},
|
{MID: 0x67, Points: 0xFA}, // em103 Taikun Zamuza
|
||||||
{MID: 0x68, Points: 0xFA},
|
{MID: 0x68, Points: 0xFA}, // em104 Abiorugu
|
||||||
{MID: 0x69, Points: 0xFA},
|
{MID: 0x69, Points: 0xFA}, // em105 Kuarusepusu
|
||||||
{MID: 0x6A, Points: 0xFA},
|
{MID: 0x6A, Points: 0xFA}, // em106 Odibatorasu
|
||||||
{MID: 0x6B, Points: 0xFA},
|
{MID: 0x6B, Points: 0xFA}, // em107 Disufiroa
|
||||||
{MID: 0x6C, Points: 0xFA},
|
{MID: 0x6C, Points: 0xFA}, // em108 Rebidiora
|
||||||
{MID: 0x6D, Points: 0xFA},
|
{MID: 0x6D, Points: 0xFA}, // em109 Anorupatisu
|
||||||
{MID: 0x6E, Points: 0xFA},
|
{MID: 0x6E, Points: 0xFA}, // em110 Hyujikiki
|
||||||
{MID: 0x6F, Points: 0xFA},
|
{MID: 0x6F, Points: 0xFA}, // em111 Midogaron
|
||||||
{MID: 0x70, Points: 0xFA},
|
{MID: 0x70, Points: 0xFA}, // em112 Giaorugu
|
||||||
{MID: 0x72, Points: 0xFA},
|
{MID: 0x72, Points: 0xFA}, // em114 Farunokku
|
||||||
{MID: 0x73, Points: 0xFA},
|
{MID: 0x73, Points: 0xFA}, // em115 Pokaradon
|
||||||
{MID: 0x74, Points: 0xFA},
|
{MID: 0x74, Points: 0xFA}, // em116 Shantien
|
||||||
{MID: 0x77, Points: 0xFA},
|
{MID: 0x77, Points: 0xFA}, // em119 Goruganosu
|
||||||
{MID: 0x78, Points: 0xFA},
|
{MID: 0x78, Points: 0xFA}, // em120 Aruganosu
|
||||||
{MID: 0x79, Points: 0xFA},
|
{MID: 0x79, Points: 0xFA}, // em121 Baruragaru
|
||||||
{MID: 0x7A, Points: 0xFA},
|
{MID: 0x7A, Points: 0xFA}, // em122 Zerureusu
|
||||||
{MID: 0x7B, Points: 0xFA},
|
{MID: 0x7B, Points: 0xFA}, // em123 Gougarf
|
||||||
{MID: 0x7D, Points: 0xFA},
|
{MID: 0x7D, Points: 0xFA}, // em125 Forokururu
|
||||||
{MID: 0x7E, Points: 0xFA},
|
{MID: 0x7E, Points: 0xFA}, // em126 Meraginasu
|
||||||
{MID: 0x7F, Points: 0xFA},
|
{MID: 0x7F, Points: 0xFA}, // em127 Diorekkusu
|
||||||
{MID: 0x80, Points: 0xFA},
|
{MID: 0x80, Points: 0xFA}, // em128 Garuba Daora
|
||||||
{MID: 0x81, Points: 0xFA},
|
{MID: 0x81, Points: 0xFA}, // em129 Inagami
|
||||||
{MID: 0x82, Points: 0xFA},
|
{MID: 0x82, Points: 0xFA}, // em130 Varusaburosu
|
||||||
{MID: 0x83, Points: 0xFA},
|
{MID: 0x83, Points: 0xFA}, // em131 Poborubarumu
|
||||||
{MID: 0x8B, Points: 0xFA},
|
{MID: 0x8B, Points: 0xFA}, // em139 Gureadomosu
|
||||||
{MID: 0x8C, Points: 0xFA},
|
{MID: 0x8C, Points: 0xFA}, // em140 Harudomerugu
|
||||||
{MID: 0x8D, Points: 0xFA},
|
{MID: 0x8D, Points: 0xFA}, // em141 Toridcless
|
||||||
{MID: 0x8E, Points: 0xFA},
|
{MID: 0x8E, Points: 0xFA}, // em142 Gasurabazura
|
||||||
{MID: 0x90, Points: 0xFA},
|
{MID: 0x90, Points: 0xFA}, // em144 Yama Kurai
|
||||||
{MID: 0x92, Points: 0x78},
|
{MID: 0x92, Points: 0x78}, // em146 Zinogre
|
||||||
{MID: 0x93, Points: 0x78},
|
{MID: 0x93, Points: 0x78}, // em147 Deviljho
|
||||||
{MID: 0x94, Points: 0x78},
|
{MID: 0x94, Points: 0x78}, // em148 Brachydios
|
||||||
{MID: 0x96, Points: 0xFA},
|
{MID: 0x96, Points: 0xFA}, // em150 Toa Tesukatora
|
||||||
{MID: 0x97, Points: 0x78},
|
{MID: 0x97, Points: 0x78}, // em151 Barioth
|
||||||
{MID: 0x98, Points: 0x78},
|
{MID: 0x98, Points: 0x78}, // em152 Uragaan
|
||||||
{MID: 0x99, Points: 0x78},
|
{MID: 0x99, Points: 0x78}, // em153 Stygian Zinogre
|
||||||
{MID: 0x9A, Points: 0xFA},
|
{MID: 0x9A, Points: 0xFA}, // em154 Guanzorumu
|
||||||
{MID: 0x9E, Points: 0xFA},
|
{MID: 0x9E, Points: 0xFA}, // em158 Voljang
|
||||||
{MID: 0x9F, Points: 0x78},
|
{MID: 0x9F, Points: 0x78}, // em159 Nargacuga
|
||||||
{MID: 0xA0, Points: 0xFA},
|
{MID: 0xA0, Points: 0xFA}, // em160 Keoaruboru
|
||||||
{MID: 0xA1, Points: 0xFA},
|
{MID: 0xA1, Points: 0xFA}, // em161 Zenaserisu
|
||||||
{MID: 0xA2, Points: 0x78},
|
{MID: 0xA2, Points: 0x78}, // em162 Gore Magala
|
||||||
{MID: 0xA4, Points: 0x78},
|
{MID: 0xA4, Points: 0x78}, // em164 Shagaru Magala
|
||||||
{MID: 0xA5, Points: 0x78},
|
{MID: 0xA5, Points: 0x78}, // em165 Amatsu
|
||||||
{MID: 0xA6, Points: 0xFA},
|
{MID: 0xA6, Points: 0xFA}, // em166 Elzelion
|
||||||
{MID: 0xA9, Points: 0x78},
|
{MID: 0xA9, Points: 0x78}, // em169 Seregios
|
||||||
{MID: 0xAA, Points: 0xFA},
|
{MID: 0xAA, Points: 0xFA}, // em170 Bogabadorumu
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := byteframe.NewByteFrame()
|
resp := byteframe.NewByteFrame()
|
||||||
@@ -2278,9 +2502,20 @@ func handleMsgMhfGetUdTacticsRewardList(s *Session, p mhfpacket.MHFPacket) {}
|
|||||||
|
|
||||||
func handleMsgMhfGetUdTacticsLog(s *Session, p mhfpacket.MHFPacket) {}
|
func handleMsgMhfGetUdTacticsLog(s *Session, p mhfpacket.MHFPacket) {}
|
||||||
|
|
||||||
func handleMsgMhfGetEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {}
|
func handleMsgMhfGetEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {
|
||||||
|
pkt := p.(*mhfpacket.MsgMhfGetEquipSkinHist)
|
||||||
|
// Transmog / reskin system, bitmask of 3200 bytes length
|
||||||
|
// presumably divided by 5 sections for 5120 armour IDs covered
|
||||||
|
// +10,000 for actual ID to be unlocked by each bit
|
||||||
|
// Returning 3200 bytes of FF just unlocks everything for now
|
||||||
|
doSizedAckResp(s, pkt.AckHandle, bytes.Repeat([]byte{0xFF}, 0xC80))
|
||||||
|
}
|
||||||
|
|
||||||
func handleMsgMhfUpdateEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {}
|
func handleMsgMhfUpdateEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {
|
||||||
|
pkt := p.(*mhfpacket.MsgMhfUpdateEquipSkinHist)
|
||||||
|
// sends a raw armour ID back that needs to be mapped into the persistent bitmask above (-10,000)
|
||||||
|
s.QueueAck(pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
||||||
|
}
|
||||||
|
|
||||||
func handleMsgMhfGetUdTacticsFollower(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfGetUdTacticsFollower(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfGetUdTacticsFollower)
|
pkt := p.(*mhfpacket.MsgMhfGetUdTacticsFollower)
|
||||||
|
|||||||
@@ -66,7 +66,9 @@
|
|||||||
<button id="newButton" class="btn btn-primary">New</button>
|
<button id="newButton" class="btn btn-primary">New</button>
|
||||||
<button id="deleteButton" class="btn btn-primary">Delete</button>
|
<button id="deleteButton" class="btn btn-primary">Delete</button>
|
||||||
-->
|
-->
|
||||||
|
<button id="btn_config" class="btn btn-primary">Config</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,5 +94,13 @@ $(function() {
|
|||||||
window.external.exitLauncher();
|
window.external.exitLauncher();
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#btn_config").click(function() {
|
||||||
|
try{
|
||||||
|
window.external.openMhlConfig();
|
||||||
|
} catch(e){
|
||||||
|
createErrorAlert("Error on openMhlConfig: " + e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,13 +36,10 @@ $(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#btn_config").click(function() {
|
$("#btn_config").click(function() {
|
||||||
DoOpenMhlConfig()
|
try{
|
||||||
|
window.external.openMhlConfig();
|
||||||
|
} catch(e){
|
||||||
|
createErrorAlert("Error on openMhlConfig: " + e);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
function DoOpenMhlConfig() {
|
|
||||||
"use strict";
|
|
||||||
try {
|
|
||||||
window.external.openMhlConfig()
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user