Merge branch 'main' into feature/diva

# Conflicts:
#	bundled-schema/DivaShops.sql
#	bundled-schema/OtherShops.sql
#	patch-schema/shop-db.sql
#	server/channelserver/handlers_guild.go
#	server/channelserver/handlers_tactics.go
This commit is contained in:
wish
2023-04-15 13:57:33 +10:00
43 changed files with 471 additions and 685 deletions

View File

@@ -0,0 +1,102 @@
BEGIN;
-- Start Normal Demo
INSERT INTO gacha_shop (min_gr, min_hr, name, url_banner, url_feature, url_thumbnail, wide, recommended, gacha_type, hidden)
VALUES (0, 0, 'Normal Demo',
'http://img4.imagetitan.com/img4/QeRWNAviFD8UoTx/26/26_template_innerbanner.png',
'http://img4.imagetitan.com/img4/QeRWNAviFD8UoTx/26/26_template_feature.png',
'http://img4.imagetitan.com/img4/small/26/26_template_outerbanner.png',
false, false, 0, false);
-- Create two different 'rolls', the first rolls once for 1z, the second rolls eleven times for 10z
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES
((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 0, 10, 1, 0, 0, 0, 1, 0, 0),
((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 1, 10, 10, 0, 0, 0, 11, 0, 0);
-- Creates a prize of 1z with a weighted chance of 100
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 100, 0, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 1, 0);
-- Creates a prize of 2z with a weighted chance of 70
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 70, 1, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 2, 0);
-- Creates a prize of 3z with a weighted chance of 10
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 10, 2, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 3, 0);
-- End Normal Demo
-- Start Step-Up Demo
INSERT INTO gacha_shop (min_gr, min_hr, name, url_banner, url_feature, url_thumbnail, wide, recommended, gacha_type, hidden)
VALUES (0, 0, 'Step-Up Demo', '', '', '', false, false, 1, false);
-- Create two 'steps', the first costs 1z, the second costs 2z
-- The first step has zero rolls so it will only give the prizes directly linked to the entry ID, being 1z
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 0, 10, 1, 0, 0, 0, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 1, 0);
-- The second step has one roll on the random prize list as will as the direct prize, being 3z
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 1, 10, 2, 0, 0, 0, 1, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 3, 0);
-- Set up two random prizes, the first gives 1z, the second gives 2z
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 100, 0, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 1, 0);
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 90, 1, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 2, 0);
-- End Step-Up Demo
-- Start Box Demo
INSERT INTO gacha_shop (min_gr, min_hr, name, url_banner, url_feature, url_thumbnail, wide, recommended, gacha_type, hidden)
VALUES (0, 0, 'Box Demo', '', '', '', false, false, 4, false);
-- Create two different 'rolls', the first rolls once for 1z, the second rolls twice for 2z
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES
((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 0, 10, 1, 0, 0, 0, 1, 0, 0),
((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 1, 10, 2, 0, 0, 0, 2, 0, 0);
-- Create five different 'Box' items, weight is always 0 for these
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 0, 0, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 1, 0);
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 0, 0, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 1, 0);
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 0, 0, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 1, 0);
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 0, 0, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 2, 0);
INSERT INTO gacha_entries (gacha_id, entry_type, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points)
VALUES ((SELECT id FROM gacha_shop ORDER BY id DESC LIMIT 1), 100, 0, 0, 0, 0, 0, 0, 0, 0);
INSERT INTO gacha_items (entry_id, item_type, item_id, quantity)
VALUES ((SELECT id FROM gacha_entries ORDER BY id DESC LIMIT 1), 10, 3, 0);
-- End Box Demo
END;

View File

@@ -0,0 +1,102 @@
package mhfcourse
import (
"golang.org/x/exp/slices"
"math"
"time"
)
type Course struct {
ID uint16
Expiry time.Time
}
var aliases = map[uint16][]string{
1: {"Trial", "TL"},
2: {"HunterLife", "HL"},
3: {"Extra", "ExtraA", "EX"},
4: {"ExtraB"},
5: {"Mobile"},
6: {"Premium"},
7: {"Pallone", "ExtraC"},
8: {"Assist", "***ist", "Legend", "Rasta"},
9: {"N"},
10: {"Hiden", "Secret"},
11: {"HunterSupport", "HunterAid", "Support", "Aid", "Royal"},
12: {"NBoost", "NetCafeBoost", "Boost"},
// 13-19 show up as (unknown)
20: {"DEBUG"},
21: {"COG_LINK_EXPIRED"},
22: {"360_GOLD"},
23: {"PS3_TROP"},
24: {"COG"},
25: {"CAFE_SP"},
26: {"NetCafe", "Cafe", "OfficialCafe", "Official"},
27: {"HLRenewing", "HLR", "HLRenewal", "HLRenew", "CardHL"},
28: {"EXRenewing", "EXR", "EXRenewal", "EXRenew", "CardEX"},
29: {"Free"},
// 30 = Real NetCafe course
}
func (c Course) Aliases() []string {
return aliases[c.ID]
}
func Courses() []Course {
courses := make([]Course, 32)
for i := range courses {
courses[i].ID = uint16(i)
}
return courses
}
func (c Course) Value() uint32 {
return uint32(math.Pow(2, float64(c.ID)))
}
// CourseExists returns true if the named course exists in the given slice
func CourseExists(ID uint16, c []Course) bool {
for _, course := range c {
if course.ID == ID {
return true
}
}
return false
}
// GetCourseStruct returns a slice of Course(s) from a rights integer
func GetCourseStruct(rights uint32) ([]Course, uint32) {
resp := []Course{{ID: 1}, {ID: 24}}
s := Courses()
slices.SortStableFunc(s, func(i, j Course) bool {
return i.ID > j.ID
})
var normalCafeCourseSet, netcafeCourseSet bool
for _, course := range s {
if rights-course.Value() < 0x80000000 {
switch course.ID {
case 26:
if normalCafeCourseSet {
break
}
normalCafeCourseSet = true
resp = append(resp, Course{ID: 25})
fallthrough
case 9:
if netcafeCourseSet {
break
}
netcafeCourseSet = true
resp = append(resp, Course{ID: 30})
}
course.Expiry = time.Date(2030, 1, 1, 0, 0, 0, 0, time.FixedZone("UTC+9", 9*60*60))
resp = append(resp, course)
rights -= course.Value()
}
}
rights = 0
for _, course := range resp {
rights += course.Value()
}
return resp, rights
}

View File

@@ -2,87 +2,14 @@ package stringsupport
import ( import (
"bytes" "bytes"
"io/ioutil" "io"
"strconv" "strconv"
"strings" "strings"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/japanese" "golang.org/x/text/encoding/japanese"
"golang.org/x/text/transform" "golang.org/x/text/transform"
) )
// StringConverter is a small helper for encoding/decoding strings.
type StringConverter struct {
Encoding encoding.Encoding
}
// Decode decodes the given bytes as the set encoding.
func (sc *StringConverter) Decode(data []byte) (string, error) {
decoded, err := ioutil.ReadAll(transform.NewReader(bytes.NewBuffer(data), sc.Encoding.NewDecoder()))
if err != nil {
return "", err
}
return string(decoded), nil
}
// MustDecode decodes the given bytes as the set encoding. Panics on decode failure.
func (sc *StringConverter) MustDecode(data []byte) string {
decoded, err := sc.Decode(data)
if err != nil {
panic(err)
}
return decoded
}
// Encode encodes the given string as the set encoding.
func (sc *StringConverter) Encode(data string) ([]byte, error) {
encoded, err := ioutil.ReadAll(transform.NewReader(bytes.NewBuffer([]byte(data)), sc.Encoding.NewEncoder()))
if err != nil {
return nil, err
}
return encoded, nil
}
// MustEncode encodes the given string as the set encoding. Panics on encode failure.
func (sc *StringConverter) MustEncode(data string) []byte {
encoded, err := sc.Encode(data)
if err != nil {
panic(err)
}
return encoded
}
/*
func MustConvertShiftJISToUTF8(text string) string {
result, err := ConvertShiftJISToUTF8(text)
if err != nil {
panic(err)
}
return result
}
func MustConvertUTF8ToShiftJIS(text string) string {
result, err := ConvertUTF8ToShiftJIS(text)
if err != nil {
panic(err)
}
return result
}
func ConvertShiftJISToUTF8(text string) (string, error) {
r := bytes.NewBuffer([]byte(text))
decoded, err := ioutil.ReadAll(transform.NewReader(r, japanese.ShiftJIS.NewDecoder()))
if err != nil {
return "", err
}
return string(decoded), nil
}
*/
func UTF8ToSJIS(x string) []byte { func UTF8ToSJIS(x string) []byte {
e := japanese.ShiftJIS.NewEncoder() e := japanese.ShiftJIS.NewEncoder()
xt, _, err := transform.String(e, x) xt, _, err := transform.String(e, x)
@@ -94,7 +21,7 @@ func UTF8ToSJIS(x string) []byte {
func SJISToUTF8(b []byte) string { func SJISToUTF8(b []byte) string {
d := japanese.ShiftJIS.NewDecoder() d := japanese.ShiftJIS.NewDecoder()
result, err := ioutil.ReadAll(transform.NewReader(bytes.NewReader(b), d)) result, err := io.ReadAll(transform.NewReader(bytes.NewReader(b), d))
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -169,22 +96,3 @@ func CSVElems(csv string) []int {
} }
return r return r
} }
// ConvertUTF8ToShiftJIS converts a UTF8 string to a Shift-JIS []byte.
func ConvertUTF8ToShiftJIS(text string) ([]byte, error) {
r := bytes.NewBuffer([]byte(text))
encoded, err := ioutil.ReadAll(transform.NewReader(r, japanese.ShiftJIS.NewEncoder()))
if err != nil {
return nil, err
}
return encoded, nil
}
func ConvertUTF8ToSJIS(text string) (string, error) {
r, _, err := transform.String(japanese.ShiftJIS.NewEncoder(), text)
if err != nil {
return "", err
}
return r, nil
}

View File

@@ -1,13 +1,22 @@
package token package token
import "math/rand" import (
"math/rand"
"time"
)
// Generate returns an alphanumeric token of specified length // Generate returns an alphanumeric token of specified length
func Generate(length int) string { func Generate(length int) string {
rng := RNG()
var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
b := make([]rune, length) b := make([]rune, length)
for i := range b { for i := range b {
b[i] = chars[rand.Intn(len(chars))] b[i] = chars[rng.Intn(len(chars))]
} }
return string(b) return string(b)
} }
// RNG returns a new RNG generator
func RNG() *rand.Rand {
return rand.New(rand.NewSource(time.Now().UnixNano()))
}

View File

@@ -5,7 +5,7 @@
"DisableSoftCrash": false, "DisableSoftCrash": false,
"HideLoginNotice": true, "HideLoginNotice": true,
"LoginNotices": [ "LoginNotices": [
"<BODY><CENTER><SIZE_3><C_4>Welcome to Erupe SU9.2!<BR><BODY><LEFT><SIZE_2><C_5>Erupe is experimental software<C_7>, we are not liable for any<BR><BODY>issues caused by installing the software!<BR><BODY><BR><BODY><C_4>■Report bugs on Discord!<C_7><BR><BODY><BR><BODY><C_4>■Test everything!<C_7><BR><BODY><BR><BODY><C_4>■Don't talk to softlocking NPCs!<C_7><BR><BODY><BR><BODY><C_4>■Fork the code on GitHub!<C_7><BR><BODY><BR><BODY>Thank you to all of the contributors,<BR><BODY><BR><BODY>this wouldn't exist without you." "<BODY><CENTER><SIZE_3><C_4>Welcome to Erupe SU9.3!<BR><BODY><LEFT><SIZE_2><C_5>Erupe is experimental software<C_7>, we are not liable for any<BR><BODY>issues caused by installing the software!<BR><BODY><BR><BODY><C_4>■Report bugs on Discord!<C_7><BR><BODY><BR><BODY><C_4>■Test everything!<C_7><BR><BODY><BR><BODY><C_4>■Don't talk to softlocking NPCs!<C_7><BR><BODY><BR><BODY><C_4>■Fork the code on GitHub!<C_7><BR><BODY><BR><BODY>Thank you to all of the contributors,<BR><BODY><BR><BODY>this wouldn't exist without you."
], ],
"PatchServerManifest": "", "PatchServerManifest": "",
"PatchServerFile": "", "PatchServerFile": "",
@@ -24,7 +24,6 @@
"TournamentEvent": 0, "TournamentEvent": 0,
"MezFesEvent": true, "MezFesEvent": true,
"MezFesAlt": false, "MezFesAlt": false,
"DisableMailItems": true,
"DisableTokenCheck": false, "DisableTokenCheck": false,
"QuestDebugTools": false, "QuestDebugTools": false,
"SaveDumps": { "SaveDumps": {
@@ -73,6 +72,10 @@
"Name": "Course", "Name": "Course",
"Enabled": true, "Enabled": true,
"Prefix": "!course" "Prefix": "!course"
}, {
"Name": "PSN",
"Enabled": true,
"Prefix": "!psn"
} }
], ],
"Courses": [ "Courses": [
@@ -85,7 +88,6 @@
{"Name": "HunterSupport", "Enabled": false}, {"Name": "HunterSupport", "Enabled": false},
{"Name": "NBoost", "Enabled": false}, {"Name": "NBoost", "Enabled": false},
{"Name": "NetCafe", "Enabled": true}, {"Name": "NetCafe", "Enabled": true},
{"Name": "OfficialCafe", "Enabled": true},
{"Name": "HLRenewing", "Enabled": true}, {"Name": "HLRenewing", "Enabled": true},
{"Name": "EXRenewing", "Enabled": true} {"Name": "EXRenewing", "Enabled": true}
], ],

View File

@@ -50,7 +50,6 @@ type DevModeOptions struct {
MezFesEvent bool // MezFes status MezFesEvent bool // MezFes status
MezFesAlt bool // Swaps out Volpakkun for Tokotoko MezFesAlt bool // Swaps out Volpakkun for Tokotoko
DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!) DisableTokenCheck bool // Disables checking login token exists in the DB (security risk!)
DisableMailItems bool // Hack to prevent english versions of MHF from crashing
QuestDebugTools bool // Enable various quest debug logs QuestDebugTools bool // Enable various quest debug logs
SaveDumps SaveDumpOptions SaveDumps SaveDumpOptions
} }

View File

@@ -54,7 +54,7 @@ func main() {
defer zapLogger.Sync() defer zapLogger.Sync()
logger := zapLogger.Named("main") logger := zapLogger.Named("main")
logger.Info(fmt.Sprintf("Starting Erupe (9.2b-%s)", Commit())) logger.Info(fmt.Sprintf("Starting Erupe (9.3b-%s)", Commit()))
if config.ErupeConfig.Database.Password == "" { if config.ErupeConfig.Database.Password == "" {
preventClose("Database password is blank") preventClose("Database password is blank")

View File

@@ -1,19 +1,20 @@
package mhfpacket package mhfpacket
import ( import (
"errors" "errors"
"erupe-ce/network/clientctx"
"erupe-ce/network"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/network"
"erupe-ce/network/clientctx"
) )
// MsgMhfGetRyoudama represents the MSG_MHF_GET_RYOUDAMA // MsgMhfGetRyoudama represents the MSG_MHF_GET_RYOUDAMA
type MsgMhfGetRyoudama struct{ type MsgMhfGetRyoudama struct {
AckHandle uint32 AckHandle uint32
Unk0 uint16 Unk0 uint8
Unk1 uint32 Unk1 uint8
Unk2 uint8 GuildID uint32
Unk3 uint8
} }
// Opcode returns the ID associated with this packet type. // Opcode returns the ID associated with this packet type.
@@ -24,9 +25,10 @@ func (m *MsgMhfGetRyoudama) Opcode() network.PacketID {
// Parse parses the packet from binary // Parse parses the packet from binary
func (m *MsgMhfGetRyoudama) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { func (m *MsgMhfGetRyoudama) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32() m.AckHandle = bf.ReadUint32()
m.Unk0 = bf.ReadUint16() m.Unk0 = bf.ReadUint8()
m.Unk1 = bf.ReadUint32() m.Unk1 = bf.ReadUint8()
m.Unk2 = bf.ReadUint8() m.GuildID = bf.ReadUint32()
m.Unk3 = bf.ReadUint8()
return nil return nil
} }

View File

@@ -11,7 +11,10 @@ import (
// MsgMhfPostTinyBin represents the MSG_MHF_POST_TINY_BIN // MsgMhfPostTinyBin represents the MSG_MHF_POST_TINY_BIN
type MsgMhfPostTinyBin struct { type MsgMhfPostTinyBin struct {
AckHandle uint32 AckHandle uint32
Unk []byte Unk0 uint16
Unk1 uint8
Unk2 uint8
Data []byte
} }
// Opcode returns the ID associated with this packet type. // Opcode returns the ID associated with this packet type.
@@ -22,7 +25,10 @@ func (m *MsgMhfPostTinyBin) Opcode() network.PacketID {
// Parse parses the packet from binary // Parse parses the packet from binary
func (m *MsgMhfPostTinyBin) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { func (m *MsgMhfPostTinyBin) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32() m.AckHandle = bf.ReadUint32()
m.Unk = bf.ReadBytes(14) m.Unk0 = bf.ReadUint16()
m.Unk1 = bf.ReadUint8()
m.Unk2 = bf.ReadUint8()
m.Data = bf.ReadBytes(uint(bf.ReadUint16()))
return nil return nil
} }

View File

@@ -2,6 +2,7 @@ package mhfpacket
import ( import (
"errors" "errors"
"erupe-ce/common/stringsupport"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/network" "erupe-ce/network"
@@ -10,9 +11,16 @@ import (
// MsgMhfUpdateGuildMessageBoard represents the MSG_MHF_UPDATE_GUILD_MESSAGE_BOARD // MsgMhfUpdateGuildMessageBoard represents the MSG_MHF_UPDATE_GUILD_MESSAGE_BOARD
type MsgMhfUpdateGuildMessageBoard struct { type MsgMhfUpdateGuildMessageBoard struct {
AckHandle uint32 AckHandle uint32
MessageOp uint32 MessageOp uint32
Request []byte PostType uint32
StampID uint32
TitleLength uint32
BodyLength uint32
Title string
Body string
PostID uint32
LikeState bool
} }
// Opcode returns the ID associated with this packet type. // Opcode returns the ID associated with this packet type.
@@ -24,9 +32,31 @@ func (m *MsgMhfUpdateGuildMessageBoard) Opcode() network.PacketID {
func (m *MsgMhfUpdateGuildMessageBoard) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { func (m *MsgMhfUpdateGuildMessageBoard) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32() m.AckHandle = bf.ReadUint32()
m.MessageOp = bf.ReadUint32() m.MessageOp = bf.ReadUint32()
if m.MessageOp != 5 { switch m.MessageOp {
m.Request = bf.DataFromCurrent() case 0:
bf.Seek(int64(len(bf.Data())-2), 0) m.PostType = bf.ReadUint32()
m.StampID = bf.ReadUint32()
m.TitleLength = bf.ReadUint32()
m.BodyLength = bf.ReadUint32()
m.Title = stringsupport.SJISToUTF8(bf.ReadBytes(uint(m.TitleLength)))
m.Body = stringsupport.SJISToUTF8(bf.ReadBytes(uint(m.BodyLength)))
case 1:
m.PostID = bf.ReadUint32()
case 2:
m.PostID = bf.ReadUint32()
bf.ReadBytes(8)
m.TitleLength = bf.ReadUint32()
m.BodyLength = bf.ReadUint32()
m.Title = stringsupport.SJISToUTF8(bf.ReadBytes(uint(m.TitleLength)))
m.Body = stringsupport.SJISToUTF8(bf.ReadBytes(uint(m.BodyLength)))
case 3:
m.PostID = bf.ReadUint32()
bf.ReadBytes(8)
m.StampID = bf.ReadUint32()
case 4:
m.PostID = bf.ReadUint32()
bf.ReadBytes(8)
m.LikeState = bf.ReadBool()
} }
return nil return nil
} }

View File

@@ -3,50 +3,17 @@ package mhfpacket
import ( import (
"errors" "errors"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/common/mhfcourse"
ps "erupe-ce/common/pascalstring" ps "erupe-ce/common/pascalstring"
"erupe-ce/network" "erupe-ce/network"
"erupe-ce/network/clientctx" "erupe-ce/network/clientctx"
"golang.org/x/exp/slices"
"math"
) )
/*
00 58 // Opcode
00 00 00 00
00 00 00 4e
00 04 // Count
00 00 // Skipped(padding?)
00 01 00 00 00 00 00 00
00 02 00 00 5d fa 14 c0
00 03 00 00 5d fa 14 c0
00 06 00 00 5d e7 05 10
00 00 // Count of some buf up to 0x800 bytes following it.
00 10 // Trailer
*/
// ClientRight represents a right that the client has.
type ClientRight struct {
ID uint16
Unk0 uint16
Timestamp uint32
}
type Course struct {
Aliases []string
ID uint16
Value uint32
}
// MsgSysUpdateRight represents the MSG_SYS_UPDATE_RIGHT // MsgSysUpdateRight represents the MSG_SYS_UPDATE_RIGHT
type MsgSysUpdateRight struct { type MsgSysUpdateRight struct {
ClientRespAckHandle uint32 // If non-0, requests the client to send back a MSG_SYS_ACK packet with this value. ClientRespAckHandle uint32 // If non-0, requests the client to send back a MSG_SYS_ACK packet with this value.
Bitfield uint32 Bitfield uint32
Rights []ClientRight Rights []mhfcourse.Course
UnkSize uint16 // Count of some buf up to 0x800 bytes following it. UnkSize uint16 // Count of some buf up to 0x800 bytes following it.
} }
@@ -68,54 +35,13 @@ func (m *MsgSysUpdateRight) Build(bf *byteframe.ByteFrame, ctx *clientctx.Client
bf.WriteUint16(0) bf.WriteUint16(0)
for _, v := range m.Rights { for _, v := range m.Rights {
bf.WriteUint16(v.ID) bf.WriteUint16(v.ID)
bf.WriteUint16(v.Unk0) bf.WriteUint16(0)
bf.WriteUint32(v.Timestamp) if v.Expiry.IsZero() {
bf.WriteUint32(0)
} else {
bf.WriteUint32(uint32(v.Expiry.Unix()))
}
} }
ps.Uint16(bf, "", false) // update client login token / password in the game's launcherstate struct ps.Uint16(bf, "", false) // update client login token / password in the game's launcherstate struct
return nil return nil
} }
func Courses() []Course {
var courses = []Course{
{Aliases: []string{"Trial", "TL"}, ID: 1},
{Aliases: []string{"HunterLife", "HL"}, ID: 2},
{Aliases: []string{"Extra", "ExtraA", "EX"}, ID: 3},
{Aliases: []string{"ExtraB"}, ID: 4},
{Aliases: []string{"Mobile"}, ID: 5},
{Aliases: []string{"Premium"}, ID: 6},
{Aliases: []string{"Pallone", "ExtraC"}, ID: 7},
{Aliases: []string{"Assist", "Legend", "Rasta"}, ID: 8}, // Legend
{Aliases: []string{"N"}, ID: 9},
{Aliases: []string{"Hiden", "Secret"}, ID: 10}, // Secret
{Aliases: []string{"HunterSupport", "HunterAid", "Support", "Aid", "Royal"}, ID: 11}, // Royal
{Aliases: []string{"NBoost", "NetCafeBoost", "Boost"}, ID: 12},
// 13-19 = (unknown), 20 = DEBUG, 21 = COG_LINK_EXPIRED, 22 = 360_GOLD, 23 = PS3_TROP
{Aliases: []string{"COG"}, ID: 24},
{Aliases: []string{"NetCafe", "Cafe", "InternetCafe"}, ID: 25},
{Aliases: []string{"OfficialCafe", "Official"}, ID: 26},
{Aliases: []string{"HLRenewing", "HLR", "HLRenewal", "HLRenew"}, ID: 27},
{Aliases: []string{"EXRenewing", "EXR", "EXRenewal", "EXRenew"}, ID: 28},
{Aliases: []string{"Free"}, ID: 29},
// 30 = real netcafe bit
}
for i := range courses {
courses[i].Value = uint32(math.Pow(2, float64(courses[i].ID)))
}
return courses
}
// GetCourseStruct returns a slice of Course(s) from a rights integer
func GetCourseStruct(rights uint32) []Course {
var resp []Course
s := Courses()
slices.SortStableFunc(s, func(i, j Course) bool {
return i.ID > j.ID
})
for _, course := range s {
if rights-course.Value < 0x80000000 {
resp = append(resp, course)
rights -= course.Value
}
}
return resp
}

View File

@@ -1,9 +0,0 @@
BEGIN;
ALTER TABLE characters ADD bonus_quests INT NOT NULL DEFAULT 0;
ALTER TABLE characters ADD daily_quests INT NOT NULL DEFAULT 0;
ALTER TABLE characters ADD promo_points INT NOT NULL DEFAULT 0;
END;

View File

@@ -1,77 +0,0 @@
BEGIN;
ALTER TABLE characters
DROP COLUMN IF EXISTS gacha_prem;
ALTER TABLE characters
DROP COLUMN IF EXISTS gacha_trial;
ALTER TABLE characters
DROP COLUMN IF EXISTS frontier_points;
ALTER TABLE users
ADD IF NOT EXISTS gacha_premium INT;
ALTER TABLE users
ADD IF NOT EXISTS gacha_trial INT;
ALTER TABLE users
ADD IF NOT EXISTS frontier_points INT;
DROP TABLE IF EXISTS public.gacha_shop;
CREATE TABLE IF NOT EXISTS public.gacha_shop (
id SERIAL PRIMARY KEY,
min_gr INTEGER,
min_hr INTEGER,
name TEXT,
url_banner TEXT,
url_feature TEXT,
url_thumbnail TEXT,
wide BOOLEAN,
recommended BOOLEAN,
gacha_type INTEGER,
hidden BOOLEAN
);
DROP TABLE IF EXISTS public.gacha_shop_items;
CREATE TABLE IF NOT EXISTS public.gacha_entries (
id SERIAL PRIMARY KEY,
gacha_id INTEGER,
entry_type INTEGER,
item_type INTEGER,
item_number INTEGER,
item_quantity INTEGER,
weight INTEGER,
rarity INTEGER,
rolls INTEGER,
frontier_points INTEGER,
daily_limit INTEGER
);
CREATE TABLE IF NOT EXISTS public.gacha_items (
id SERIAL PRIMARY KEY,
entry_id INTEGER,
item_type INTEGER,
item_id INTEGER,
quantity INTEGER
);
DROP TABLE IF EXISTS public.stepup_state;
CREATE TABLE IF NOT EXISTS public.gacha_stepup (
gacha_id INTEGER,
step INTEGER,
character_id INTEGER
);
DROP TABLE IF EXISTS public.lucky_box_state;
CREATE TABLE IF NOT EXISTS public.gacha_box (
gacha_id INTEGER,
entry_id INTEGER,
character_id INTEGER
);
END;

View File

@@ -1,29 +0,0 @@
BEGIN;
DROP TABLE IF EXISTS public.gacha_shop;
CREATE TABLE IF NOT EXISTS public.gacha_shop (
id serial PRIMARY KEY,
min_gr integer,
min_hr integer,
name text,
link1 text,
link2 text,
link3 text,
icon integer,
type integer,
hide boolean
);
DROP TABLE IF EXISTS public.fpoint_items;
CREATE TABLE IF NOT EXISTS public.fpoint_items (
id serial PRIMARY KEY,
item_type integer,
item_id integer,
quantity integer,
fpoints integer,
trade_type integer
);
END;

View File

@@ -1,7 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.guild_characters ADD rp_today INT DEFAULT 0;
ALTER TABLE IF EXISTS public.guild_characters ADD rp_yesterday INT DEFAULT 0;
END;

View File

@@ -1,12 +0,0 @@
BEGIN;
DROP TABLE IF EXISTS public.login_boost_state;
CREATE TABLE IF NOT EXISTS public.login_boost (
char_id INTEGER,
week_req INTEGER,
expiration TIMESTAMP WITH TIME ZONE,
reset TIMESTAMP WITH TIME ZONE
);
END;

View File

@@ -1,6 +0,0 @@
BEGIN;
ALTER TABLE public.characters
ADD COLUMN mezfes BYTEA;
END;

5
patch-schema/psn-id.sql Normal file
View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE users ADD COLUMN IF NOT EXISTS psn_id TEXT;
END;

View File

@@ -1,9 +0,0 @@
BEGIN;
UPDATE characters SET savemercenary = NULL;
ALTER TABLE characters ADD rasta_id INT;
ALTER TABLE characters ADD pact_id INT;
END;

View File

@@ -1,29 +0,0 @@
BEGIN;
DROP TABLE IF EXISTS public.normal_shop_items;
CREATE TABLE IF NOT EXISTS public.shop_items (
id SERIAL PRIMARY KEY,
shop_type INTEGER,
shop_id INTEGER,
item_id INTEGER,
cost INTEGER,
quantity INTEGER,
min_hr INTEGER,
min_sr INTEGER,
min_gr INTEGER,
store_level INTEGER,
max_quantity INTEGER,
road_floors INTEGER,
road_fatalis INTEGER
);
DROP TABLE IF EXISTS public.shop_item_state;
CREATE TABLE IF NOT EXISTS public.shop_items_bought (
character_id INTEGER,
shop_item_id INTEGER,
bought INTEGER
);
END;

View File

@@ -1,5 +0,0 @@
BEGIN;
ALTER TABLE characters ADD stampcard INT NOT NULL DEFAULT 0;
END;

View File

@@ -1,69 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.characters
ALTER COLUMN daily_time TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.characters
ALTER COLUMN guild_post_checked TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.characters
ALTER COLUMN boost_time TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.characters
ALTER COLUMN cafe_reset TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.distribution
ALTER COLUMN deadline TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.events
ALTER COLUMN start_time TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.feature_weapon
ALTER COLUMN start_time TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.guild_alliances
ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.guild_applications
ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.guild_characters
ALTER COLUMN joined_at TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.guild_posts
ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.characters
ALTER COLUMN daily_time TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.guilds
ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.mail
ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.stamps
ALTER COLUMN hl_next TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.stamps
ALTER COLUMN ex_next TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.titles
ALTER COLUMN unlocked_at TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.titles
ALTER COLUMN updated_at TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.users
ALTER COLUMN last_login TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.users
ALTER COLUMN return_expires TYPE TIMESTAMP WITH TIME ZONE;
ALTER TABLE IF EXISTS public.guild_meals
DROP COLUMN expires;
ALTER TABLE IF EXISTS public.guild_meals
ADD COLUMN created_at TIMESTAMP WITH TIME ZONE;
END;

View File

@@ -1,17 +0,0 @@
BEGIN;
DROP TABLE IF EXISTS public.account_ban;
DROP TABLE IF EXISTS public.account_history;
DROP TABLE IF EXISTS public.account_moderation;
DROP TABLE IF EXISTS public.account_sub;
DROP TABLE IF EXISTS public.history;
DROP TABLE IF EXISTS public.questlists;
DROP TABLE IF EXISTS public.schema_migrations;
END;

View File

@@ -3,6 +3,7 @@ package channelserver
import ( import (
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"erupe-ce/common/mhfcourse"
ps "erupe-ce/common/pascalstring" ps "erupe-ce/common/pascalstring"
"erupe-ce/common/stringsupport" "erupe-ce/common/stringsupport"
"fmt" "fmt"
@@ -11,11 +12,11 @@ import (
"strings" "strings"
"time" "time"
"crypto/rand"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/network/mhfpacket" "erupe-ce/network/mhfpacket"
"go.uber.org/zap" "go.uber.org/zap"
"math/bits" "math/bits"
"math/rand"
) )
// Temporary function to just return no results for a MSG_MHF_ENUMERATE* packet // Temporary function to just return no results for a MSG_MHF_ENUMERATE* packet
@@ -74,23 +75,13 @@ func doAckSimpleFail(s *Session, ackHandle uint32, data []byte) {
} }
func updateRights(s *Session) { func updateRights(s *Session) {
rightsInt := uint32(0x0E) rightsInt := uint32(2)
s.server.db.QueryRow("SELECT rights FROM users u INNER JOIN characters c ON u.id = c.user_id WHERE c.id = $1", s.charID).Scan(&rightsInt) s.server.db.QueryRow("SELECT rights FROM users u INNER JOIN characters c ON u.id = c.user_id WHERE c.id = $1", s.charID).Scan(&rightsInt)
s.courses = mhfpacket.GetCourseStruct(rightsInt) s.courses, rightsInt = mhfcourse.GetCourseStruct(rightsInt)
rights := []mhfpacket.ClientRight{{1, 0, 0}}
var netcafeBitSet bool
for _, course := range s.courses {
if (course.ID == 9 || course.ID == 25 || course.ID == 26) && !netcafeBitSet {
netcafeBitSet = true
rightsInt += 0x40000000 // set netcafe bit
rights = append(rights, mhfpacket.ClientRight{ID: 30})
}
rights = append(rights, mhfpacket.ClientRight{ID: course.ID, Timestamp: 0x70DB59F0})
}
update := &mhfpacket.MsgSysUpdateRight{ update := &mhfpacket.MsgSysUpdateRight{
ClientRespAckHandle: 0, ClientRespAckHandle: 0,
Bitfield: rightsInt, Bitfield: rightsInt,
Rights: rights, Rights: s.courses,
UnkSize: 0, UnkSize: 0,
} }
s.QueueSendMHF(update) s.QueueSendMHF(update)
@@ -221,7 +212,7 @@ func logoutPlayer(s *Session) {
timePlayed += sessionTime timePlayed += sessionTime
var rpGained int var rpGained int
if s.FindCourse("NetCafe").ID != 0 || s.FindCourse("N").ID != 0 { if mhfcourse.CourseExists(30, s.courses) {
rpGained = timePlayed / 900 rpGained = timePlayed / 900
timePlayed = timePlayed % 900 timePlayed = timePlayed % 900
s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID) s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID)

View File

@@ -2,6 +2,7 @@ package channelserver
import ( import (
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/common/mhfcourse"
ps "erupe-ce/common/pascalstring" ps "erupe-ce/common/pascalstring"
"erupe-ce/network/mhfpacket" "erupe-ce/network/mhfpacket"
"fmt" "fmt"
@@ -88,7 +89,7 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
if s.FindCourse("NetCafe").ID != 0 || s.FindCourse("N").ID != 0 { if mhfcourse.CourseExists(30, s.courses) {
cafeTime = uint32(TimeAdjusted().Unix()) - uint32(s.sessionStart) + cafeTime cafeTime = uint32(TimeAdjusted().Unix()) - uint32(s.sessionStart) + cafeTime
} }
bf.WriteUint32(cafeTime) // Total cafe time bf.WriteUint32(cafeTime) // Total cafe time

View File

@@ -3,13 +3,14 @@ package channelserver
import ( import (
"encoding/hex" "encoding/hex"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/common/mhfcourse"
"erupe-ce/common/token"
"erupe-ce/config" "erupe-ce/config"
"erupe-ce/network/binpacket" "erupe-ce/network/binpacket"
"erupe-ce/network/mhfpacket" "erupe-ce/network/mhfpacket"
"fmt" "fmt"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"math" "math"
"math/rand"
"strings" "strings"
"time" "time"
@@ -81,6 +82,21 @@ func sendServerChatMessage(s *Session, message string) {
} }
func parseChatCommand(s *Session, command string) { func parseChatCommand(s *Session, command string) {
if strings.HasPrefix(command, commands["PSN"].Prefix) {
if commands["PSN"].Enabled {
var id string
n, err := fmt.Sscanf(command, fmt.Sprintf("%s %%s", commands["PSN"].Prefix), &id)
if err != nil || n != 1 {
sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandPSNError"], commands["PSN"].Prefix))
} else {
_, err = s.server.db.Exec(`UPDATE users u SET psn_id=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, id, s.charID)
if err == nil {
sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandPSNSuccess"], id))
}
}
}
}
if strings.HasPrefix(command, commands["Reload"].Prefix) { if strings.HasPrefix(command, commands["Reload"].Prefix) {
// Flush all objects and users and reload // Flush all objects and users and reload
if commands["Reload"].Enabled { if commands["Reload"].Enabled {
@@ -192,13 +208,14 @@ func parseChatCommand(s *Session, command string) {
sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseError"], commands["Course"].Prefix)) sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseError"], commands["Course"].Prefix))
} else { } else {
name = strings.ToLower(name) name = strings.ToLower(name)
for _, course := range mhfpacket.Courses() { for _, course := range mhfcourse.Courses() {
for _, alias := range course.Aliases { for _, alias := range course.Aliases() {
if strings.ToLower(name) == strings.ToLower(alias) { if strings.ToLower(name) == strings.ToLower(alias) {
if slices.Contains(s.server.erupeConfig.Courses, config.Course{Name: course.Aliases[0], Enabled: true}) { if slices.Contains(s.server.erupeConfig.Courses, config.Course{Name: course.Aliases()[0], Enabled: true}) {
if s.FindCourse(name).ID != 0 { var delta, rightsInt uint32
ei := slices.IndexFunc(s.courses, func(c mhfpacket.Course) bool { if mhfcourse.CourseExists(course.ID, s.courses) {
for _, alias := range c.Aliases { ei := slices.IndexFunc(s.courses, func(c mhfcourse.Course) bool {
for _, alias := range c.Aliases() {
if strings.ToLower(name) == strings.ToLower(alias) { if strings.ToLower(name) == strings.ToLower(alias) {
return true return true
} }
@@ -206,25 +223,26 @@ func parseChatCommand(s *Session, command string) {
return false return false
}) })
if ei != -1 { if ei != -1 {
s.courses = append(s.courses[:ei], s.courses[ei+1:]...) delta = uint32(-1 * math.Pow(2, float64(course.ID)))
sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseDisabled"], course.Aliases[0])) sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseDisabled"], course.Aliases()[0]))
} }
} else { } else {
s.courses = append(s.courses, course) delta = uint32(math.Pow(2, float64(course.ID)))
sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseEnabled"], course.Aliases[0])) sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseEnabled"], course.Aliases()[0]))
} }
var newInt uint32 err = s.server.db.QueryRow("SELECT rights FROM users u INNER JOIN characters c ON u.id = c.user_id WHERE c.id = $1", s.charID).Scan(&rightsInt)
for _, course := range s.courses { if err == nil {
newInt += uint32(math.Pow(2, float64(course.ID))) s.server.db.Exec("UPDATE users u SET rights=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", rightsInt+delta, s.charID)
} }
s.server.db.Exec("UPDATE users u SET rights=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", newInt, s.charID)
updateRights(s) updateRights(s)
} else { } else {
sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseLocked"], course.Aliases[0])) sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseLocked"], course.Aliases()[0]))
} }
return
} }
} }
} }
sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseError"], commands["Course"].Prefix))
} }
} else { } else {
sendDisabledCommandMessage(s, commands["Course"]) sendDisabledCommandMessage(s, commands["Course"])
@@ -367,8 +385,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
roll.SetLE() roll.SetLE()
roll.WriteUint16(4) // Unk roll.WriteUint16(4) // Unk
roll.WriteUint16(authorLen) roll.WriteUint16(authorLen)
rand.Seed(time.Now().UnixNano()) dice := fmt.Sprintf("%d", token.RNG().Intn(100)+1)
dice := fmt.Sprintf("%d", rand.Intn(100)+1)
roll.WriteUint16(uint16(len(dice) + 1)) roll.WriteUint16(uint16(len(dice) + 1))
roll.WriteNullTerminatedBytes([]byte(dice)) roll.WriteNullTerminatedBytes([]byte(dice))
roll.WriteNullTerminatedBytes(tmp.ReadNullTerminatedBytes()) roll.WriteNullTerminatedBytes(tmp.ReadNullTerminatedBytes())

View File

@@ -12,7 +12,7 @@ import (
) )
const ( const (
pointerGender = 0x81 // +1 pointerGender = 0x51 // +1
pointerRP = 0x22D16 // +2 pointerRP = 0x22D16 // +2
pointerHouseTier = 0x1FB6C // +5 pointerHouseTier = 0x1FB6C // +5
pointerHouseData = 0x1FE01 // +195 pointerHouseData = 0x1FE01 // +195

View File

@@ -5,7 +5,6 @@ import (
"erupe-ce/common/stringsupport" "erupe-ce/common/stringsupport"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@@ -260,7 +259,7 @@ func dumpSaveData(s *Session, data []byte, suffix string) {
func handleMsgMhfLoaddata(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfLoaddata(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoaddata) pkt := p.(*mhfpacket.MsgMhfLoaddata)
if _, err := os.Stat(filepath.Join(s.server.erupeConfig.BinPath, "save_override.bin")); err == nil { if _, err := os.Stat(filepath.Join(s.server.erupeConfig.BinPath, "save_override.bin")); err == nil {
data, _ := ioutil.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, "save_override.bin")) data, _ := os.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, "save_override.bin"))
doAckBufSucceed(s, pkt.AckHandle, data) doAckBufSucceed(s, pkt.AckHandle, data)
return return
} }

View File

@@ -1,8 +1,8 @@
package channelserver package channelserver
import ( import (
"erupe-ce/common/token"
"math" "math"
"math/rand"
"time" "time"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
@@ -95,9 +95,9 @@ func generateFeatureWeapons(count int) activeFeature {
} }
nums := make([]int, 0) nums := make([]int, 0)
var result int var result int
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for len(nums) < count { for len(nums) < count {
num := r.Intn(14) rng := token.RNG()
num := rng.Intn(14)
exist := false exist := false
for _, v := range nums { for _, v := range nums {
if v == num { if v == num {
@@ -131,6 +131,7 @@ func handleMsgMhfGetKeepLoginBoostStatus(s *Session, p mhfpacket.MHFPacket) {
var loginBoosts []loginBoost var loginBoosts []loginBoost
rows, err := s.server.db.Queryx("SELECT week_req, expiration, reset FROM login_boost WHERE char_id=$1 ORDER BY week_req", s.charID) rows, err := s.server.db.Queryx("SELECT week_req, expiration, reset FROM login_boost WHERE char_id=$1 ORDER BY week_req", s.charID)
if err != nil || s.server.erupeConfig.GameplayOptions.DisableLoginBoost { if err != nil || s.server.erupeConfig.GameplayOptions.DisableLoginBoost {
rows.Close()
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 35)) doAckBufSucceed(s, pkt.AckHandle, make([]byte, 35))
return return
} }

View File

@@ -4,8 +4,8 @@ import (
"encoding/hex" "encoding/hex"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
ps "erupe-ce/common/pascalstring" ps "erupe-ce/common/pascalstring"
"erupe-ce/common/token"
"erupe-ce/network/mhfpacket" "erupe-ce/network/mhfpacket"
"math/rand"
"sort" "sort"
"time" "time"
) )
@@ -336,8 +336,7 @@ func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return return
} }
rand.Seed(time.Now().UnixNano()) team := uint32(token.RNG().Intn(2))
team := uint32(rand.Intn(2))
switch team { switch team {
case 0: case 0:
s.server.db.Exec("INSERT INTO festa_registrations VALUES ($1, 'blue')", guild.ID) s.server.db.Exec("INSERT INTO festa_registrations VALUES ($1, 'blue')", guild.ID)

View File

@@ -1060,7 +1060,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) {
uint32 guild id uint32 guild id
uint32 guild leader id (for mail) uint32 guild leader id (for mail)
uint32 unk (always null in pcap) uint32 unk (always null in pcap)
uint16 unk (always 0001 in pcap) uint16 member count
uint16 len guild name uint16 len guild name
string nullterm guild name string nullterm guild name
uint16 len guild leader name uint16 len guild leader name
@@ -1862,7 +1862,6 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfUpdateGuildMessageBoard) pkt := p.(*mhfpacket.MsgMhfUpdateGuildMessageBoard)
bf := byteframe.NewByteFrameFromBytes(pkt.Request)
guild, err := GetGuildInfoByCharacterId(s, s.charID) guild, err := GetGuildInfoByCharacterId(s, s.charID)
applicant := false applicant := false
if guild != nil { if guild != nil {
@@ -1874,45 +1873,26 @@ func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
} }
switch pkt.MessageOp { switch pkt.MessageOp {
case 0: // Create message case 0: // Create message
postType := bf.ReadUint32() // 0 = message, 1 = news s.server.db.Exec("INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)", guild.ID, s.charID, pkt.StampID, pkt.PostType, pkt.Title, pkt.Body)
stampID := bf.ReadUint32()
titleLength := bf.ReadUint32()
bodyLength := bf.ReadUint32()
title := stringsupport.SJISToUTF8(bf.ReadBytes(uint(titleLength)))
body := stringsupport.SJISToUTF8(bf.ReadBytes(uint(bodyLength)))
s.server.db.Exec("INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)", guild.ID, s.charID, stampID, postType, title, body)
// TODO: if there are too many messages, purge excess // TODO: if there are too many messages, purge excess
case 1: // Delete message case 1: // Delete message
postID := bf.ReadUint32() s.server.db.Exec("DELETE FROM guild_posts WHERE id = $1", pkt.PostID)
s.server.db.Exec("DELETE FROM guild_posts WHERE id = $1", postID)
case 2: // Update message case 2: // Update message
postID := bf.ReadUint32() s.server.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE id = $3", pkt.Title, pkt.Body, pkt.PostID)
bf.ReadBytes(8)
titleLength := bf.ReadUint32()
bodyLength := bf.ReadUint32()
title := stringsupport.SJISToUTF8(bf.ReadBytes(uint(titleLength)))
body := stringsupport.SJISToUTF8(bf.ReadBytes(uint(bodyLength)))
s.server.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE id = $3", title, body, postID)
case 3: // Update stamp case 3: // Update stamp
postID := bf.ReadUint32() s.server.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE id = $2", pkt.StampID, pkt.PostID)
bf.ReadBytes(8)
stampID := bf.ReadUint32()
s.server.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE id = $2", stampID, postID)
case 4: // Like message case 4: // Like message
postID := bf.ReadUint32()
bf.ReadBytes(8)
likeState := bf.ReadBool()
var likedBy string var likedBy string
err := s.server.db.QueryRow("SELECT liked_by FROM guild_posts WHERE id = $1", postID).Scan(&likedBy) err := s.server.db.QueryRow("SELECT liked_by FROM guild_posts WHERE id = $1", pkt.PostID).Scan(&likedBy)
if err != nil { if err != nil {
s.logger.Error("Failed to get guild message like data from db", zap.Error(err)) s.logger.Error("Failed to get guild message like data from db", zap.Error(err))
} else { } else {
if likeState { if pkt.LikeState {
likedBy = stringsupport.CSVAdd(likedBy, int(s.charID)) likedBy = stringsupport.CSVAdd(likedBy, int(s.charID))
s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, postID) s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, pkt.PostID)
} else { } else {
likedBy = stringsupport.CSVRemove(likedBy, int(s.charID)) likedBy = stringsupport.CSVRemove(likedBy, int(s.charID))
s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, postID) s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, pkt.PostID)
} }
} }
case 5: // Check for new messages case 5: // Check for new messages

View File

@@ -1,14 +1,12 @@
package channelserver package channelserver
import ( import (
"fmt"
"io"
"time"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/common/stringsupport" "erupe-ce/common/stringsupport"
"erupe-ce/network/mhfpacket" "erupe-ce/network/mhfpacket"
"fmt"
"go.uber.org/zap" "go.uber.org/zap"
"io"
) )
func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) {
@@ -206,7 +204,7 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) {
} }
rows, err := s.server.db.Queryx(` rows, err := s.server.db.Queryx(`
SELECT c.id, c.name, ga.actor_id SELECT c.id, c.name, c.hrp, c.gr, ga.actor_id
FROM guild_applications ga FROM guild_applications ga
JOIN characters c ON c.id = ga.character_id JOIN characters c ON c.id = ga.character_id
WHERE ga.guild_id = $1 AND ga.application_type = 'invited' WHERE ga.guild_id = $1 AND ga.application_type = 'invited'
@@ -231,14 +229,14 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) {
for rows.Next() { for rows.Next() {
var charName string var charName string
var charID uint32 var charID, actorID uint32
var actorID uint32 var hrp, gr uint16
err = rows.Scan(&charID, &charName, &actorID) err = rows.Scan(&charID, &charName, &hrp, &gr, &actorID)
if err != nil { if err != nil {
doAckSimpleFail(s, pkt.AckHandle, nil) doAckSimpleFail(s, pkt.AckHandle, nil)
panic(err) continue
} }
// This seems to be used as a unique ID for the invitation sent // This seems to be used as a unique ID for the invitation sent
@@ -247,14 +245,10 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint32(charID) bf.WriteUint32(charID)
bf.WriteUint32(actorID) bf.WriteUint32(actorID)
bf.WriteUint32(charID) bf.WriteUint32(charID)
bf.WriteUint32(uint32(time.Now().Unix())) bf.WriteUint32(uint32(TimeAdjusted().Unix()))
bf.WriteUint16(0x00) // HR? bf.WriteUint16(hrp) // HR?
bf.WriteUint16(0x00) // GR? bf.WriteUint16(gr) // GR?
bf.WriteBytes(stringsupport.PaddedString(charName, 32, true))
charNameBytes, _ := stringsupport.ConvertUTF8ToShiftJIS(charName)
bf.WriteBytes(charNameBytes)
bf.WriteBytes(make([]byte, 32-len(charNameBytes))) // Fixed length string
count++ count++
} }

View File

@@ -326,15 +326,8 @@ func handleMsgMhfListMail(s *Session, p mhfpacket.MHFPacket) {
flags |= 0x04 flags |= 0x04
} }
// Workaround until EN mail items are patched if m.AttachedItemReceived {
if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.DisableMailItems { flags |= 0x08
if itemAttached {
flags |= 0x08
}
} else {
if m.AttachedItemReceived {
flags |= 0x08
}
} }
if m.IsGuildInvite { if m.IsGuildInvite {

View File

@@ -8,7 +8,6 @@ import (
"erupe-ce/server/channelserver/compression/nullcomp" "erupe-ce/server/channelserver/compression/nullcomp"
"go.uber.org/zap" "go.uber.org/zap"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@@ -292,7 +291,7 @@ func handleMsgMhfEnumerateAiroulist(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateAiroulist) pkt := p.(*mhfpacket.MsgMhfEnumerateAiroulist)
resp := byteframe.NewByteFrame() resp := byteframe.NewByteFrame()
if _, err := os.Stat(filepath.Join(s.server.erupeConfig.BinPath, "airoulist.bin")); err == nil { if _, err := os.Stat(filepath.Join(s.server.erupeConfig.BinPath, "airoulist.bin")); err == nil {
data, _ := ioutil.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, "airoulist.bin")) data, _ := os.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, "airoulist.bin"))
resp.WriteBytes(data) resp.WriteBytes(data)
doAckBufSucceed(s, pkt.AckHandle, resp.Data()) doAckBufSucceed(s, pkt.AckHandle, resp.Data())
return return

View File

@@ -4,7 +4,7 @@ import (
ps "erupe-ce/common/pascalstring" ps "erupe-ce/common/pascalstring"
"fmt" "fmt"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"io/ioutil" "os"
"path/filepath" "path/filepath"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
@@ -92,7 +92,7 @@ func handleMsgMhfLoadRengokuData(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfGetRengokuBinary(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetRengokuBinary(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetRengokuBinary) pkt := p.(*mhfpacket.MsgMhfGetRengokuBinary)
// a (massively out of date) version resides in the game's /dat/ folder or up to date can be pulled from packets // a (massively out of date) version resides in the game's /dat/ folder or up to date can be pulled from packets
data, err := ioutil.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, "rengoku_data.bin")) data, err := os.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, "rengoku_data.bin"))
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -358,6 +358,9 @@ func getRandomEntries(entries []GachaEntry, rolls int, isBox bool) ([]GachaEntry
totalWeight += entries[i].Weight totalWeight += entries[i].Weight
} }
for { for {
if rolls == len(chosen) {
break
}
if !isBox { if !isBox {
result := rand.Float64() * totalWeight result := rand.Float64() * totalWeight
for _, entry := range entries { for _, entry := range entries {
@@ -373,9 +376,6 @@ func getRandomEntries(entries []GachaEntry, rolls int, isBox bool) ([]GachaEntry
entries[result] = entries[len(entries)-1] entries[result] = entries[len(entries)-1]
entries = entries[:len(entries)-1] entries = entries[:len(entries)-1]
} }
if rolls == len(chosen) {
break
}
} }
return chosen, nil return chosen, nil
} }

View File

@@ -57,6 +57,8 @@ func getLangStrings(s *Server) map[string]string {
strings["commandCourseLocked"] = "%sコースはロックされています" strings["commandCourseLocked"] = "%sコースはロックされています"
strings["commandTeleportError"] = "テレポートコマンドエラー 構文:%s x y" strings["commandTeleportError"] = "テレポートコマンドエラー 構文:%s x y"
strings["commandTeleportSuccess"] = "%d %dにテレポート" strings["commandTeleportSuccess"] = "%d %dにテレポート"
strings["commandPSNError"] = "PSN連携コマンドエラー %s <psn id>"
strings["commandPSNSuccess"] = "PSN「%s」が連携されています"
strings["commandRaviNoCommand"] = "ラヴィコマンドが指定されていません" strings["commandRaviNoCommand"] = "ラヴィコマンドが指定されていません"
strings["commandRaviStartSuccess"] = "大討伐を開始します" strings["commandRaviStartSuccess"] = "大討伐を開始します"
@@ -142,6 +144,8 @@ func getLangStrings(s *Server) map[string]string {
strings["commandCourseLocked"] = "%s Course is locked" strings["commandCourseLocked"] = "%s Course is locked"
strings["commandTeleportError"] = "Error in command. Format: %s x y" strings["commandTeleportError"] = "Error in command. Format: %s x y"
strings["commandTeleportSuccess"] = "Teleporting to %d %d" strings["commandTeleportSuccess"] = "Teleporting to %d %d"
strings["commandPSNError"] = "Error in command. Format: %s <psn id>"
strings["commandPSNSuccess"] = "Connected PSN ID: %s"
strings["commandRaviNoCommand"] = "No Raviente command specified!" strings["commandRaviNoCommand"] = "No Raviente command specified!"
strings["commandRaviStartSuccess"] = "The Great Slaying will begin in a moment" strings["commandRaviStartSuccess"] = "The Great Slaying will begin in a moment"

View File

@@ -3,10 +3,10 @@ package channelserver
import ( import (
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"erupe-ce/common/mhfcourse"
"fmt" "fmt"
"io" "io"
"net" "net"
"strings"
"sync" "sync"
"time" "time"
@@ -32,6 +32,7 @@ type Session struct {
cryptConn *network.CryptConn cryptConn *network.CryptConn
sendPackets chan packet sendPackets chan packet
clientContext *clientctx.ClientContext clientContext *clientctx.ClientContext
lastPacket time.Time
userEnteredStage bool // If the user has entered a stage before userEnteredStage bool // If the user has entered a stage before
stageID string stageID string
@@ -42,7 +43,7 @@ type Session struct {
charID uint32 charID uint32
logKey []byte logKey []byte
sessionStart int64 sessionStart int64
courses []mhfpacket.Course courses []mhfcourse.Course
token string token string
kqf []byte kqf []byte
kqfOverride bool kqfOverride bool
@@ -73,6 +74,7 @@ func NewSession(server *Server, conn net.Conn) *Session {
cryptConn: network.NewCryptConn(conn), cryptConn: network.NewCryptConn(conn),
sendPackets: make(chan packet, 20), sendPackets: make(chan packet, 20),
clientContext: &clientctx.ClientContext{}, // Unused clientContext: &clientctx.ClientContext{}, // Unused
lastPacket: time.Now(),
sessionStart: TimeAdjusted().Unix(), sessionStart: TimeAdjusted().Unix(),
stageMoveStack: stringstack.New(), stageMoveStack: stringstack.New(),
} }
@@ -160,6 +162,10 @@ func (s *Session) sendLoop() {
func (s *Session) recvLoop() { func (s *Session) recvLoop() {
for { for {
if time.Now().Add(-30 * time.Second).After(s.lastPacket) {
logoutPlayer(s)
return
}
if s.closed { if s.closed {
logoutPlayer(s) logoutPlayer(s)
return return
@@ -181,6 +187,7 @@ func (s *Session) recvLoop() {
} }
func (s *Session) handlePacketGroup(pktGroup []byte) { func (s *Session) handlePacketGroup(pktGroup []byte) {
s.lastPacket = time.Now()
bf := byteframe.NewByteFrameFromBytes(pktGroup) bf := byteframe.NewByteFrameFromBytes(pktGroup)
opcodeUint16 := bf.ReadUint16() opcodeUint16 := bf.ReadUint16()
opcode := network.PacketID(opcodeUint16) opcode := network.PacketID(opcodeUint16)
@@ -228,7 +235,6 @@ func ignored(opcode network.PacketID) bool {
network.MSG_SYS_TIME, network.MSG_SYS_TIME,
network.MSG_SYS_EXTEND_THRESHOLD, network.MSG_SYS_EXTEND_THRESHOLD,
network.MSG_SYS_POSITION_OBJECT, network.MSG_SYS_POSITION_OBJECT,
network.MSG_MHF_ENUMERATE_QUEST,
network.MSG_MHF_SAVEDATA, network.MSG_MHF_SAVEDATA,
} }
set := make(map[network.PacketID]struct{}, len(ignoreList)) set := make(map[network.PacketID]struct{}, len(ignoreList))
@@ -262,14 +268,3 @@ func (s *Session) logMessage(opcode uint16, data []byte, sender string, recipien
fmt.Printf("Data [%d bytes]:\n(Too long!)\n\n", len(data)) fmt.Printf("Data [%d bytes]:\n(Too long!)\n\n", len(data))
} }
} }
func (s *Session) FindCourse(name string) mhfpacket.Course {
for _, course := range s.courses {
for _, alias := range course.Aliases {
if strings.ToLower(name) == strings.ToLower(alias) {
return course
}
}
}
return mhfpacket.Course{}
}

View File

@@ -3,8 +3,6 @@ package channelserver
import ( import (
"sync" "sync"
"time"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/network/mhfpacket" "erupe-ce/network/mhfpacket"
) )
@@ -49,7 +47,6 @@ type Stage struct {
host *Session host *Session
maxPlayers uint16 maxPlayers uint16
password string password string
createdAt string
} }
// NewStage creates a new stage with intialized values. // NewStage creates a new stage with intialized values.
@@ -62,7 +59,6 @@ func NewStage(ID string) *Stage {
objectIndex: 0, objectIndex: 0,
rawBinaryData: make(map[stageBinaryKey][]byte), rawBinaryData: make(map[stageBinaryKey][]byte),
maxPlayers: 4, maxPlayers: 4,
createdAt: time.Now().Format("01-02-2006 15:04:05"),
} }
return s return s
} }

View File

@@ -1,6 +1,7 @@
package signserver package signserver
import ( import (
"erupe-ce/common/mhfcourse"
"strings" "strings"
"time" "time"
@@ -119,8 +120,9 @@ func (s *Server) getLastCID(uid int) uint32 {
} }
func (s *Server) getUserRights(uid int) uint32 { func (s *Server) getUserRights(uid int) uint32 {
var rights uint32 rights := uint32(2)
_ = s.db.QueryRow("SELECT rights FROM users WHERE id=$1", uid).Scan(&rights) _ = s.db.QueryRow("SELECT rights FROM users WHERE id=$1", uid).Scan(&rights)
_, rights = mhfcourse.GetCourseStruct(rights)
return rights return rights
} }

View File

@@ -7,31 +7,19 @@ import (
"erupe-ce/common/token" "erupe-ce/common/token"
"erupe-ce/server/channelserver" "erupe-ce/server/channelserver"
"fmt" "fmt"
"math/rand"
"strings"
"time"
"go.uber.org/zap" "go.uber.org/zap"
"strings"
) )
func makeSignInFailureResp(respID RespID) []byte { func (s *Session) makeSignResponse(uid int) []byte {
bf := byteframe.NewByteFrame()
bf.WriteUint8(uint8(respID))
return bf.Data()
}
func (s *Session) makeSignInResp(uid int) []byte {
returnExpiry := s.server.getReturnExpiry(uid)
// Get the characters from the DB. // Get the characters from the DB.
chars, err := s.server.getCharactersForUser(uid) chars, err := s.server.getCharactersForUser(uid)
if err != nil { if err != nil {
s.logger.Warn("Error getting characters from DB", zap.Error(err)) s.logger.Warn("Error getting characters from DB", zap.Error(err))
} }
rand.Seed(time.Now().UnixNano())
sessToken := token.Generate(16) sessToken := token.Generate(16)
s.server.registerToken(uid, sessToken) _ = s.server.registerToken(uid, sessToken)
bf := byteframe.NewByteFrame() bf := byteframe.NewByteFrame()
@@ -41,11 +29,11 @@ func (s *Session) makeSignInResp(uid int) []byte {
} else { } else {
bf.WriteUint8(0) bf.WriteUint8(0)
} }
bf.WriteUint8(1) // entrance server count bf.WriteUint8(1) // entrance server count
bf.WriteUint8(uint8(len(chars))) // character count bf.WriteUint8(uint8(len(chars)))
bf.WriteUint32(0xFFFFFFFF) // login_token_number bf.WriteUint32(0xFFFFFFFF) // login_token_number
bf.WriteBytes([]byte(sessToken)) // login_token bf.WriteBytes([]byte(sessToken))
bf.WriteUint32(uint32(time.Now().Unix())) // current time bf.WriteUint32(uint32(channelserver.TimeAdjusted().Unix()))
if s.server.erupeConfig.DevMode { if s.server.erupeConfig.DevMode {
if s.server.erupeConfig.PatchServerManifest != "" && s.server.erupeConfig.PatchServerFile != "" { if s.server.erupeConfig.PatchServerManifest != "" && s.server.erupeConfig.PatchServerFile != "" {
ps.Uint8(bf, s.server.erupeConfig.PatchServerManifest, false) ps.Uint8(bf, s.server.erupeConfig.PatchServerManifest, false)
@@ -120,6 +108,11 @@ func (s *Session) makeSignInResp(uid int) []byte {
bf.WriteUint32(s.server.getLastCID(uid)) bf.WriteUint32(s.server.getLastCID(uid))
bf.WriteUint32(s.server.getUserRights(uid)) bf.WriteUint32(s.server.getUserRights(uid))
ps.Uint16(bf, "", false) // filters ps.Uint16(bf, "", false) // filters
if s.client == VITA || s.client == PS3 {
var psnUser string
s.server.db.QueryRow("SELECT psn_id FROM users WHERE id = $1", uid).Scan(&psnUser)
bf.WriteBytes(stringsupport.PaddedString(psnUser, 20, true))
}
bf.WriteUint16(0xCA10) bf.WriteUint16(0xCA10)
bf.WriteUint16(0x4E20) bf.WriteUint16(0x4E20)
ps.Uint16(bf, "", false) // unk key ps.Uint16(bf, "", false) // unk key
@@ -128,7 +121,7 @@ func (s *Session) makeSignInResp(uid int) []byte {
bf.WriteUint16(0x0001) bf.WriteUint16(0x0001)
bf.WriteUint16(0x4E20) bf.WriteUint16(0x4E20)
ps.Uint16(bf, "", false) // unk ipv4 ps.Uint16(bf, "", false) // unk ipv4
bf.WriteUint32(uint32(returnExpiry.Unix())) bf.WriteUint32(uint32(s.server.getReturnExpiry(uid).Unix()))
bf.WriteUint32(0x00000000) bf.WriteUint32(0x00000000)
bf.WriteUint32(0x0A5197DF) // unk id bf.WriteUint32(0x0A5197DF) // unk id

View File

@@ -1,10 +1,7 @@
package signserver package signserver
//revive:disable type RespID uint8
type RespID uint16
//go:generate stringer -type=RespID
const ( const (
SIGN_UNKNOWN RespID = iota SIGN_UNKNOWN RespID = iota
SIGN_SUCCESS SIGN_SUCCESS

View File

@@ -13,6 +13,14 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
type Client int
const (
PC100 Client = iota
VITA
PS3
)
// Session holds state for the sign server connection. // Session holds state for the sign server connection.
type Session struct { type Session struct {
sync.Mutex sync.Mutex
@@ -20,6 +28,7 @@ type Session struct {
server *Server server *Server
rawConn net.Conn rawConn net.Conn
cryptConn *network.CryptConn cryptConn *network.CryptConn
client Client
} }
func (s *Session) work() { func (s *Session) work() {
@@ -42,91 +51,68 @@ func (s *Session) handlePacket(pkt []byte) error {
bf := byteframe.NewByteFrameFromBytes(pkt) bf := byteframe.NewByteFrameFromBytes(pkt)
reqType := string(bf.ReadNullTerminatedBytes()) reqType := string(bf.ReadNullTerminatedBytes())
switch reqType { switch reqType {
case "DLTSKEYSIGN:100": case "DLTSKEYSIGN:100", "DSGN:100":
fallthrough s.handleDSGN(bf)
case "DSGN:100": case "PS3SGN:100":
err := s.handleDSGNRequest(bf) s.client = PS3
if err != nil { s.handlePSSGN(bf)
return nil case "VITASGN:100":
} s.client = VITA
s.handlePSSGN(bf)
case "DELETE:100": case "DELETE:100":
loginTokenString := string(bf.ReadNullTerminatedBytes()) loginTokenString := string(bf.ReadNullTerminatedBytes())
characterID := int(bf.ReadUint32()) characterID := int(bf.ReadUint32())
_ = int(bf.ReadUint32()) // login_token_number _ = int(bf.ReadUint32()) // login_token_number
s.server.deleteCharacter(characterID, loginTokenString) err := s.server.deleteCharacter(characterID, loginTokenString)
s.logger.Info("Deleted character", zap.Int("CharacterID", characterID)) if err == nil {
err := s.cryptConn.SendPacket([]byte{0x01}) // DEL_SUCCESS s.logger.Info("Deleted character", zap.Int("CharacterID", characterID))
if err != nil { s.cryptConn.SendPacket([]byte{0x01}) // DEL_SUCCESS
return nil
} }
default: default:
s.logger.Warn("Unknown sign request", zap.String("reqType", reqType)) s.logger.Warn("Unknown request", zap.String("reqType", reqType))
if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogInboundMessages { if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogInboundMessages {
fmt.Printf("\n[Client] -> [Server]\nData [%d bytes]:\n%s\n", len(pkt), hex.Dump(pkt)) fmt.Printf("\n[Client] -> [Server]\nData [%d bytes]:\n%s\n", len(pkt), hex.Dump(pkt))
} }
} }
return nil return nil
} }
func (s *Session) handleDSGNRequest(bf *byteframe.ByteFrame) error { func (s *Session) authenticate(username string, password string) {
reqUsername := string(bf.ReadNullTerminatedBytes())
reqPassword := string(bf.ReadNullTerminatedBytes())
_ = string(bf.ReadNullTerminatedBytes()) // Unk
newCharaReq := false newCharaReq := false
if reqUsername[len(reqUsername)-1] == 43 { // '+' if username[len(username)-1] == 43 { // '+'
reqUsername = reqUsername[:len(reqUsername)-1] username = username[:len(username)-1]
newCharaReq = true newCharaReq = true
} }
var ( var id int
id int var hash string
password string bf := byteframe.NewByteFrame()
)
err := s.server.db.QueryRow("SELECT id, password FROM users WHERE username = $1", reqUsername).Scan(&id, &password) err := s.server.db.QueryRow("SELECT id, password FROM users WHERE username = $1", username).Scan(&id, &hash)
var serverRespBytes []byte
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
s.logger.Info("User not found", zap.String("Username", reqUsername)) s.logger.Info("User not found", zap.String("Username", username))
serverRespBytes = makeSignInFailureResp(SIGN_EAUTH)
if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.AutoCreateAccount { if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.AutoCreateAccount {
s.logger.Info("Creating user", zap.String("Username", reqUsername)) s.logger.Info("Creating user", zap.String("Username", username))
err = s.server.registerDBAccount(reqUsername, reqPassword) err = s.server.registerDBAccount(username, password)
if err != nil { if err == nil {
s.logger.Error("Error registering new user", zap.Error(err)) bf.WriteBytes(s.makeSignResponse(id))
serverRespBytes = makeSignInFailureResp(SIGN_EABORT)
break
} }
} else { } else {
break bf.WriteUint8(uint8(SIGN_EAUTH))
} }
var id int
err = s.server.db.QueryRow("SELECT id FROM users WHERE username = $1", reqUsername).Scan(&id)
if err != nil {
s.logger.Error("Error getting new user ID", zap.Error(err))
serverRespBytes = makeSignInFailureResp(SIGN_EABORT)
break
}
serverRespBytes = s.makeSignInResp(id)
break
case err != nil: case err != nil:
serverRespBytes = makeSignInFailureResp(SIGN_EABORT) bf.WriteUint8(uint8(SIGN_EABORT))
s.logger.Error("Error getting user details", zap.Error(err)) s.logger.Error("Error getting user details", zap.Error(err))
break
default: default:
if bcrypt.CompareHashAndPassword([]byte(password), []byte(reqPassword)) == nil { if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil || s.client == VITA || s.client == PS3 {
s.logger.Debug("Passwords match!") s.logger.Debug("Passwords match!")
if newCharaReq { if newCharaReq {
err = s.server.newUserChara(reqUsername) err = s.server.newUserChara(username)
if err != nil { if err != nil {
s.logger.Error("Error adding new character to user", zap.Error(err)) s.logger.Error("Error adding new character to user", zap.Error(err))
serverRespBytes = makeSignInFailureResp(SIGN_EABORT) bf.WriteUint8(uint8(SIGN_EABORT))
break break
} }
} }
@@ -137,22 +123,39 @@ func (s *Session) handleDSGNRequest(bf *byteframe.ByteFrame) error {
// serverRespBytes = makeSignInFailureResp(SIGN_EABORT) // serverRespBytes = makeSignInFailureResp(SIGN_EABORT)
// break // break
// } // }
serverRespBytes = s.makeSignInResp(id) bf.WriteBytes(s.makeSignResponse(id))
} else { } else {
s.logger.Warn("Incorrect password") s.logger.Warn("Incorrect password")
serverRespBytes = makeSignInFailureResp(SIGN_EPASS) bf.WriteUint8(uint8(SIGN_EPASS))
} }
} }
if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogOutboundMessages { if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.LogOutboundMessages {
fmt.Printf("\n[Server] -> [Client]\nData [%d bytes]:\n%s\n", len(serverRespBytes), hex.Dump(serverRespBytes)) fmt.Printf("\n[Server] -> [Client]\nData [%d bytes]:\n%s\n", len(bf.Data()), hex.Dump(bf.Data()))
} }
err = s.cryptConn.SendPacket(serverRespBytes) err = s.cryptConn.SendPacket(bf.Data())
if err != nil { }
return err
} func (s *Session) handlePSSGN(bf *byteframe.ByteFrame) {
_ = bf.ReadNullTerminatedBytes() // 0000000256
return nil _ = bf.ReadNullTerminatedBytes() // 1
_ = bf.ReadBytes(82)
psnUser := string(bf.ReadNullTerminatedBytes())
var reqUsername string
err := s.server.db.QueryRow(`SELECT username FROM users WHERE psn_id = $1`, psnUser).Scan(&reqUsername)
if err == sql.ErrNoRows {
resp := byteframe.NewByteFrame()
resp.WriteUint8(uint8(SIGN_ECOGLINK))
s.cryptConn.SendPacket(resp.Data())
return
}
s.authenticate(reqUsername, "")
}
func (s *Session) handleDSGN(bf *byteframe.ByteFrame) {
reqUsername := string(bf.ReadNullTerminatedBytes())
reqPassword := string(bf.ReadNullTerminatedBytes())
_ = string(bf.ReadNullTerminatedBytes()) // Unk
s.authenticate(reqUsername, reqPassword)
} }