diff --git a/common/mhfcourse/mhfcourse.go b/common/mhfcourse/mhfcourse.go new file mode 100644 index 000000000..62b8d171f --- /dev/null +++ b/common/mhfcourse/mhfcourse.go @@ -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}} + 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 +} diff --git a/network/mhfpacket/msg_sys_update_right.go b/network/mhfpacket/msg_sys_update_right.go index 8364e37bf..c384deece 100644 --- a/network/mhfpacket/msg_sys_update_right.go +++ b/network/mhfpacket/msg_sys_update_right.go @@ -3,50 +3,17 @@ package mhfpacket import ( "errors" "erupe-ce/common/byteframe" + "erupe-ce/common/mhfcourse" ps "erupe-ce/common/pascalstring" "erupe-ce/network" "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 type MsgSysUpdateRight struct { ClientRespAckHandle uint32 // If non-0, requests the client to send back a MSG_SYS_ACK packet with this value. Bitfield uint32 - Rights []ClientRight + Rights []mhfcourse.Course 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) for _, v := range m.Rights { bf.WriteUint16(v.ID) - bf.WriteUint16(v.Unk0) - bf.WriteUint32(v.Timestamp) + bf.WriteUint16(0) + 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 return nil } - -func Courses() []Course { - var courses = []Course{ - {Aliases: []string{"Trial", "TL"}, ID: 1}, - {Aliases: []string{"HunterLife", "HL"}, ID: 2}, // BASIC - {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}, - // 25 = CAFE_SP, active with 26 but useless on it's own? Just use OFFICIAL and set bit - {Aliases: []string{"NetCafe", "Cafe", "OfficialCafe", "Official"}, ID: 26}, - {Aliases: []string{"HLRenewing", "HLR", "HLRenewal", "HLRenew"}, ID: 27}, // CARD - {Aliases: []string{"EXRenewing", "EXR", "EXRenewal", "EXRenew"}, ID: 28}, // CARD_EX - {Aliases: []string{"Free"}, ID: 29}, - // 30 = NETCAFE, what actually gives you any NetCafe gameplay benefits - } - 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 -} diff --git a/patch-schema/rights-v4.sql b/patch-schema/rights-v4.sql new file mode 100644 index 000000000..9732097ed --- /dev/null +++ b/patch-schema/rights-v4.sql @@ -0,0 +1,6 @@ +BEGIN; + +-- Remove Trial Course from all users +UPDATE users SET rights = rights-2; + +END; \ No newline at end of file diff --git a/server/channelserver/handlers.go b/server/channelserver/handlers.go index 343b12094..d22f8be3b 100644 --- a/server/channelserver/handlers.go +++ b/server/channelserver/handlers.go @@ -3,6 +3,7 @@ package channelserver import ( "encoding/binary" "encoding/hex" + "erupe-ce/common/mhfcourse" ps "erupe-ce/common/pascalstring" "erupe-ce/common/stringsupport" "fmt" @@ -74,28 +75,13 @@ func doAckSimpleFail(s *Session, ackHandle uint32, data []byte) { } 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.courses = mhfpacket.GetCourseStruct(rightsInt) - rights := []mhfpacket.ClientRight{{1, 0, 0}} - var normalCafeBitSet, netcafeBitSet bool - for _, course := range s.courses { - if (course.ID == 9 || course.ID == 26) && !netcafeBitSet { - netcafeBitSet = true - rightsInt += 0x40000000 - rights = append(rights, mhfpacket.ClientRight{ID: 30}) - } - if (course.ID == 26) && !normalCafeBitSet { - normalCafeBitSet = true - rightsInt += 0x2000000 - rights = append(rights, mhfpacket.ClientRight{ID: 25}) - } - rights = append(rights, mhfpacket.ClientRight{ID: course.ID, Timestamp: 0x70DB59F0}) - } + s.courses, rightsInt = mhfcourse.GetCourseStruct(rightsInt) update := &mhfpacket.MsgSysUpdateRight{ ClientRespAckHandle: 0, Bitfield: rightsInt, - Rights: rights, + Rights: s.courses, UnkSize: 0, } s.QueueSendMHF(update) @@ -226,7 +212,7 @@ func logoutPlayer(s *Session) { timePlayed += sessionTime var rpGained int - if s.FindCourse("NetCafe").ID != 0 || s.FindCourse("N").ID != 0 { + if mhfcourse.CourseExists(30, s.courses) { rpGained = timePlayed / 900 timePlayed = timePlayed % 900 s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID) diff --git a/server/channelserver/handlers_cafe.go b/server/channelserver/handlers_cafe.go index 2973472de..46a6425a0 100644 --- a/server/channelserver/handlers_cafe.go +++ b/server/channelserver/handlers_cafe.go @@ -2,6 +2,7 @@ package channelserver import ( "erupe-ce/common/byteframe" + "erupe-ce/common/mhfcourse" ps "erupe-ce/common/pascalstring" "erupe-ce/network/mhfpacket" "fmt" @@ -88,7 +89,7 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) { if err != nil { 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 } bf.WriteUint32(cafeTime) // Total cafe time diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 9ef5eec4e..10d15938a 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -3,6 +3,7 @@ package channelserver import ( "encoding/hex" "erupe-ce/common/byteframe" + "erupe-ce/common/mhfcourse" "erupe-ce/config" "erupe-ce/network/binpacket" "erupe-ce/network/mhfpacket" @@ -192,13 +193,14 @@ func parseChatCommand(s *Session, command string) { sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseError"], commands["Course"].Prefix)) } else { name = strings.ToLower(name) - for _, course := range mhfpacket.Courses() { - for _, alias := range course.Aliases { + for _, course := range mhfcourse.Courses() { + for _, alias := range course.Aliases() { if strings.ToLower(name) == strings.ToLower(alias) { - if slices.Contains(s.server.erupeConfig.Courses, config.Course{Name: course.Aliases[0], Enabled: true}) { - if s.FindCourse(name).ID != 0 { - ei := slices.IndexFunc(s.courses, func(c mhfpacket.Course) bool { - for _, alias := range c.Aliases { + if slices.Contains(s.server.erupeConfig.Courses, config.Course{Name: course.Aliases()[0], Enabled: true}) { + var delta, rightsInt uint32 + if mhfcourse.CourseExists(course.ID, s.courses) { + ei := slices.IndexFunc(s.courses, func(c mhfcourse.Course) bool { + for _, alias := range c.Aliases() { if strings.ToLower(name) == strings.ToLower(alias) { return true } @@ -206,25 +208,26 @@ func parseChatCommand(s *Session, command string) { return false }) if ei != -1 { - s.courses = append(s.courses[:ei], s.courses[ei+1:]...) - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseDisabled"], course.Aliases[0])) + delta = uint32(-1 * math.Pow(2, float64(course.ID))) + sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseDisabled"], course.Aliases()[0])) } } else { - s.courses = append(s.courses, course) - sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseEnabled"], course.Aliases[0])) + delta = uint32(math.Pow(2, float64(course.ID))) + sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandCourseEnabled"], course.Aliases()[0])) } - var newInt uint32 - for _, course := range s.courses { - newInt += uint32(math.Pow(2, float64(course.ID))) + 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) + if err == nil { + 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) } 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 { sendDisabledCommandMessage(s, commands["Course"]) diff --git a/server/channelserver/sys_session.go b/server/channelserver/sys_session.go index 0bdf8e335..40d54fdb3 100644 --- a/server/channelserver/sys_session.go +++ b/server/channelserver/sys_session.go @@ -3,10 +3,10 @@ package channelserver import ( "encoding/binary" "encoding/hex" + "erupe-ce/common/mhfcourse" "fmt" "io" "net" - "strings" "sync" "time" @@ -43,7 +43,7 @@ type Session struct { charID uint32 logKey []byte sessionStart int64 - courses []mhfpacket.Course + courses []mhfcourse.Course token string kqf []byte kqfOverride bool @@ -268,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)) } } - -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{} -} diff --git a/server/signserver/dbutils.go b/server/signserver/dbutils.go index 213a74a14..ee4dcc493 100644 --- a/server/signserver/dbutils.go +++ b/server/signserver/dbutils.go @@ -1,6 +1,7 @@ package signserver import ( + "erupe-ce/common/mhfcourse" "strings" "time" @@ -119,8 +120,9 @@ func (s *Server) getLastCID(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) + _, rights = mhfcourse.GetCourseStruct(rights) return rights }