Merge branch 'main' into feature/diva

# Conflicts:
#	server/channelserver/handlers_quest.go
This commit is contained in:
wish
2022-11-12 21:12:30 +11:00
43 changed files with 726 additions and 728 deletions

13
common/token/token.go Normal file
View File

@@ -0,0 +1,13 @@
package token
import "math/rand"
// Generate returns an alphanumeric token of specified length
func Generate(length int) string {
var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
b := make([]rune, length)
for i := range b {
b[i] = chars[rand.Intn(len(chars))]
}
return string(b)
}

View File

@@ -1,16 +1,17 @@
{
"Host": "127.0.0.1",
"BinPath": "bin",
"Language": "en",
"DisableSoftCrash": false,
"FeaturedWeapons": 1,
"HideLoginNotice": true,
"LoginNotice": "<BODY><CENTER><SIZE_3><C_4>Welcome to Erupe SU9.1!<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": "",
"PatchServerFile": "",
"ScreenshotAPIURL": "",
"DevMode": true,
"DevModeOptions": {
"PatchServerManifest": "",
"PatchServerFile": "",
"AutoCreateAccount": true,
"EnableLauncherServer": false,
"HideLoginNotice": false,
"LoginNotice": "<BODY><CENTER><SIZE_3><C_4>Welcome to Erupe SU9.1 Beta!<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.",
"CleanDB": false,
"MaxLauncherHR": false,
"LogInboundMessages": false,
@@ -63,13 +64,16 @@
],
"Courses": [
{"Name": "HunterLife", "Enabled": true},
{"Name": "ExtraA", "Enabled": true},
{"Name": "Extra", "Enabled": true},
{"Name": "Premium", "Enabled": true},
{"Name": "Assist", "Enabled": false},
{"Name": "Netcafe", "Enabled": false},
{"Name": "N", "Enabled": false},
{"Name": "Hiden", "Enabled": false},
{"Name": "HunterSupport", "Enabled": false},
{"Name": "NetcafeBoost", "Enabled": false}
{"Name": "NBoost", "Enabled": false},
{"Name": "NetCafe", "Enabled": true},
{"Name": "HLRenewing", "Enabled": true},
{"Name": "EXRenewing", "Enabled": true}
],
"Database": {
"Host": "localhost",
@@ -87,6 +91,10 @@
"Enabled": true,
"Port": 53312
},
"SignV2": {
"Enabled": false,
"Port": 8080
},
"Channel": {
"Enabled": true
},

View File

@@ -12,11 +12,17 @@ import (
// Config holds the global server-wide config.
type Config struct {
Host string `mapstructure:"Host"`
BinPath string `mapstructure:"BinPath"`
DisableSoftCrash bool // Disables the 'Press Return to exit' dialog allowing scripts to reboot the server automatically
FeaturedWeapons int // Number of Active Feature weapons to generate daily
DevMode bool
Host string `mapstructure:"Host"`
BinPath string `mapstructure:"BinPath"`
Language string
DisableSoftCrash bool // Disables the 'Press Return to exit' dialog allowing scripts to reboot the server automatically
FeaturedWeapons int // Number of Active Feature weapons to generate daily
HideLoginNotice bool // Hide the Erupe notice on login
LoginNotice string // MHFML string of the login notice displayed
PatchServerManifest string // Manifest patch server override
PatchServerFile string // File patch server override
ScreenshotAPIURL string // Destination for screenshots uploaded to BBS
DevMode bool
DevModeOptions DevModeOptions
Discord Discord
@@ -25,30 +31,27 @@ type Config struct {
Database Database
Launcher Launcher
Sign Sign
SignV2 SignV2
Channel Channel
Entrance Entrance
}
// DevModeOptions holds various debug/temporary options for use while developing Erupe.
type DevModeOptions struct {
PatchServerManifest string // Manifest patch server override
PatchServerFile string // File patch server override
AutoCreateAccount bool // Automatically create accounts if they don't exist
HideLoginNotice bool // Hide the Erupe notice on login
LoginNotice string // MHFML string of the login notice displayed
CleanDB bool // Automatically wipes the DB on server reset.
MaxLauncherHR bool // Sets the HR returned in the launcher to HR7 so that you can join non-beginner worlds.
LogInboundMessages bool // Log all messages sent to the server
LogOutboundMessages bool // Log all messages sent to the clients
MaxHexdumpLength int // Maximum number of bytes printed when logs are enabled
DivaEvent int // Diva Defense event status
FestaEvent int // Hunter's Festa event status
TournamentEvent int // VS Tournament event status
MezFesEvent bool // MezFes status
MezFesAlt bool // Swaps out Volpakkun for Tokotoko
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
AutoCreateAccount bool // Automatically create accounts if they don't exist
CleanDB bool // Automatically wipes the DB on server reset.
MaxLauncherHR bool // Sets the HR returned in the launcher to HR7 so that you can join non-beginner worlds.
LogInboundMessages bool // Log all messages sent to the server
LogOutboundMessages bool // Log all messages sent to the clients
MaxHexdumpLength int // Maximum number of bytes printed when logs are enabled
DivaEvent int // Diva Defense event status
FestaEvent int // Hunter's Festa event status
TournamentEvent int // VS Tournament event status
MezFesEvent bool // MezFes status
MezFesAlt bool // Swaps out Volpakkun for Tokotoko
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
SaveDumps SaveDumpOptions
}
@@ -99,6 +102,12 @@ type Sign struct {
Port int
}
// SignV2 holds the new sign server config
type SignV2 struct {
Enabled bool
Port int
}
type Channel struct {
Enabled bool
}

23
main.go
View File

@@ -14,6 +14,7 @@ import (
"erupe-ce/server/entranceserver"
"erupe-ce/server/launcherserver"
"erupe-ce/server/signserver"
"erupe-ce/server/signv2server"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
@@ -35,7 +36,7 @@ func main() {
defer zapLogger.Sync()
logger := zapLogger.Named("main")
logger.Info("Starting Erupe (9.1b)")
logger.Info("Starting Erupe (9.2b)")
if config.ErupeConfig.Database.Password == "" {
preventClose("Database password is blank")
@@ -166,6 +167,22 @@ func main() {
logger.Info("Started sign server")
}
// New Sign server
var newSignServer *signv2server.Server
if config.ErupeConfig.SignV2.Enabled {
newSignServer = signv2server.NewServer(
&signv2server.Config{
Logger: logger.Named("sign"),
ErupeConfig: config.ErupeConfig,
DB: db,
})
err = newSignServer.Start()
if err != nil {
preventClose(fmt.Sprintf("Failed to start sign-v2 server: %s", err.Error()))
}
logger.Info("Started new sign server")
}
var channels []*channelserver.Server
if config.ErupeConfig.Channel.Enabled {
@@ -229,6 +246,10 @@ func main() {
signServer.Shutdown()
}
if config.ErupeConfig.SignV2.Enabled {
newSignServer.Shutdown()
}
if config.ErupeConfig.Entrance.Enabled {
entranceServer.Shutdown()
}

View File

@@ -1,15 +1,24 @@
package mhfpacket
import (
"errors"
"errors"
"erupe-ce/common/bfutil"
"erupe-ce/common/stringsupport"
"erupe-ce/network/clientctx"
"erupe-ce/network"
"erupe-ce/common/byteframe"
"erupe-ce/network"
"erupe-ce/network/clientctx"
)
// MsgMhfApplyBbsArticle represents the MSG_MHF_APPLY_BBS_ARTICLE
type MsgMhfApplyBbsArticle struct{}
type MsgMhfApplyBbsArticle struct {
AckHandle uint32
Unk0 uint32
Unk1 []byte
Name string
Title string
Description string
}
// Opcode returns the ID associated with this packet type.
func (m *MsgMhfApplyBbsArticle) Opcode() network.PacketID {
@@ -18,7 +27,13 @@ func (m *MsgMhfApplyBbsArticle) Opcode() network.PacketID {
// Parse parses the packet from binary
func (m *MsgMhfApplyBbsArticle) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
return errors.New("NOT IMPLEMENTED")
m.AckHandle = bf.ReadUint32()
m.Unk0 = bf.ReadUint32()
m.Unk1 = bf.ReadBytes(16)
m.Name = stringsupport.SJISToUTF8(bfutil.UpToNull(bf.ReadBytes(32)))
m.Title = stringsupport.SJISToUTF8(bfutil.UpToNull(bf.ReadBytes(128)))
m.Description = stringsupport.SJISToUTF8(bfutil.UpToNull(bf.ReadBytes(256)))
return nil
}
// Build builds a binary packet from the current data.

View File

@@ -11,7 +11,8 @@ import (
// MsgMhfCheckMonthlyItem represents the MSG_MHF_CHECK_MONTHLY_ITEM
type MsgMhfCheckMonthlyItem struct {
AckHandle uint32
Unk uint32
Type uint8
Unk []byte
}
// Opcode returns the ID associated with this packet type.
@@ -22,7 +23,8 @@ func (m *MsgMhfCheckMonthlyItem) Opcode() network.PacketID {
// Parse parses the packet from binary
func (m *MsgMhfCheckMonthlyItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32()
m.Unk = bf.ReadUint32()
m.Type = bf.ReadUint8()
m.Unk = bf.ReadBytes(3)
return nil
}

View File

@@ -1,15 +1,18 @@
package mhfpacket
import (
"errors"
"errors"
"erupe-ce/network/clientctx"
"erupe-ce/network"
"erupe-ce/common/byteframe"
"erupe-ce/network"
"erupe-ce/network/clientctx"
)
// MsgMhfGetBbsSnsStatus represents the MSG_MHF_GET_BBS_SNS_STATUS
type MsgMhfGetBbsSnsStatus struct{}
type MsgMhfGetBbsSnsStatus struct {
AckHandle uint32
Unk []byte
}
// Opcode returns the ID associated with this packet type.
func (m *MsgMhfGetBbsSnsStatus) Opcode() network.PacketID {
@@ -18,7 +21,9 @@ func (m *MsgMhfGetBbsSnsStatus) Opcode() network.PacketID {
// Parse parses the packet from binary
func (m *MsgMhfGetBbsSnsStatus) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
return errors.New("NOT IMPLEMENTED")
m.AckHandle = bf.ReadUint32()
m.Unk = bf.ReadBytes(12)
return nil
}
// Build builds a binary packet from the current data.

View File

@@ -1,15 +1,18 @@
package mhfpacket
import (
"errors"
"errors"
"erupe-ce/network/clientctx"
"erupe-ce/network"
"erupe-ce/common/byteframe"
"erupe-ce/network"
"erupe-ce/network/clientctx"
)
// MsgMhfGetBbsUserStatus represents the MSG_MHF_GET_BBS_USER_STATUS
type MsgMhfGetBbsUserStatus struct{}
type MsgMhfGetBbsUserStatus struct {
AckHandle uint32
Unk []byte
}
// Opcode returns the ID associated with this packet type.
func (m *MsgMhfGetBbsUserStatus) Opcode() network.PacketID {
@@ -18,7 +21,9 @@ func (m *MsgMhfGetBbsUserStatus) Opcode() network.PacketID {
// Parse parses the packet from binary
func (m *MsgMhfGetBbsUserStatus) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
return errors.New("NOT IMPLEMENTED")
m.AckHandle = bf.ReadUint32()
m.Unk = bf.ReadBytes(12)
return nil
}
// Build builds a binary packet from the current data.

View File

@@ -2,12 +2,12 @@ package mhfpacket
import (
"errors"
ps "erupe-ce/common/pascalstring"
"golang.org/x/exp/slices"
"erupe-ce/common/byteframe"
ps "erupe-ce/common/pascalstring"
"erupe-ce/network"
"erupe-ce/network/clientctx"
"golang.org/x/exp/slices"
"math"
)
/*
@@ -77,18 +77,27 @@ func (m *MsgSysUpdateRight) Build(bf *byteframe.ByteFrame, ctx *clientctx.Client
func Courses() []Course {
var courses = []Course{
{[]string{"Trial", "TL"}, 1, 0x00000002},
{[]string{"HunterLife", "HL"}, 2, 0x00000004},
{[]string{"ExtraA", "Extra", "EX"}, 3, 0x00000008},
{[]string{"ExtraB"}, 4, 0x00000010},
{[]string{"Mobile"}, 5, 0x00000020},
{[]string{"Premium"}, 6, 0x00000040},
{[]string{"Pallone"}, 7, 0x00000080},
{[]string{"Assist", "Legend", "Rasta"}, 8, 0x00000100}, // Legend
{[]string{"Netcafe", "N", "Cafe"}, 9, 0x00000200},
{[]string{"Hiden", "Secret"}, 10, 0x00000400}, // Secret
{[]string{"HunterSupport", "HunterAid", "Support", "Royal", "Aid"}, 11, 0x00000800}, // Royal
{[]string{"NetcafeBoost", "NBoost", "Boost"}, 12, 0x00001000},
{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-25 do nothing
{Aliases: []string{"NetCafe", "Cafe", "InternetCafe"}, 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
}

View File

@@ -1,41 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS public.achievements
(
id int NOT NULL PRIMARY KEY ,
ach0 int DEFAULT 0,
ach1 int DEFAULT 0,
ach2 int DEFAULT 0,
ach3 int DEFAULT 0,
ach4 int DEFAULT 0,
ach5 int DEFAULT 0,
ach6 int DEFAULT 0,
ach7 int DEFAULT 0,
ach8 int DEFAULT 0,
ach9 int DEFAULT 0,
ach10 int DEFAULT 0,
ach11 int DEFAULT 0,
ach12 int DEFAULT 0,
ach13 int DEFAULT 0,
ach14 int DEFAULT 0,
ach15 int DEFAULT 0,
ach16 int DEFAULT 0,
ach17 int DEFAULT 0,
ach18 int DEFAULT 0,
ach19 int DEFAULT 0,
ach20 int DEFAULT 0,
ach21 int DEFAULT 0,
ach22 int DEFAULT 0,
ach23 int DEFAULT 0,
ach24 int DEFAULT 0,
ach25 int DEFAULT 0,
ach26 int DEFAULT 0,
ach27 int DEFAULT 0,
ach28 int DEFAULT 0,
ach29 int DEFAULT 0,
ach30 int DEFAULT 0,
ach31 int DEFAULT 0,
ach32 int DEFAULT 0
);
END;

View File

@@ -1,9 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS public.feature_weapon
(
start_time timestamp without time zone NOT NULL,
featured integer NOT NULL
);
END;

View File

@@ -1,311 +0,0 @@
BEGIN;
CREATE TYPE event_type AS ENUM ('festa', 'diva', 'vs', 'mezfes');
DROP TABLE IF EXISTS public.event_week;
ALTER TABLE IF EXISTS public.guild_characters
ADD COLUMN IF NOT EXISTS souls int DEFAULT 0;
ALTER TABLE IF EXISTS public.guilds
DROP COLUMN IF EXISTS festival_colour;
CREATE TABLE IF NOT EXISTS public.events
(
id serial NOT NULL PRIMARY KEY,
event_type event_type NOT NULL,
start_time timestamp without time zone NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS public.festa_registrations
(
guild_id int NOT NULL,
team festival_colour NOT NULL
);
CREATE TABLE IF NOT EXISTS public.festa_trials
(
id serial NOT NULL PRIMARY KEY,
objective int NOT NULL,
goal_id int NOT NULL,
times_req int NOT NULL,
locale_req int NOT NULL DEFAULT 0,
reward int NOT NULL
);
CREATE TYPE prize_type AS ENUM ('personal', 'guild');
CREATE TABLE IF NOT EXISTS public.festa_prizes
(
id serial NOT NULL PRIMARY KEY,
type prize_type NOT NULL,
tier int NOT NULL,
souls_req int NOT NULL,
item_id int NOT NULL,
num_item int NOT NULL
);
CREATE TABLE IF NOT EXISTS public.festa_prizes_accepted
(
prize_id int NOT NULL,
character_id int NOT NULL
);
-- Ripped prizes
INSERT INTO public.festa_prizes
(type, tier, souls_req, item_id, num_item)
VALUES
('personal', 1, 1, 9647, 7),
('personal', 2, 1, 9647, 7),
('personal', 3, 1, 9647, 7),
('personal', 1, 200, 11284, 4),
('personal', 2, 200, 11284, 4),
('personal', 3, 200, 11284, 4),
('personal', 1, 400, 11381, 3),
('personal', 2, 400, 11381, 3),
('personal', 3, 400, 11381, 3),
('personal', 1, 600, 11284, 8),
('personal', 2, 600, 11284, 8),
('personal', 3, 600, 11284, 8),
('personal', 1, 800, 11384, 3),
('personal', 2, 800, 11384, 3),
('personal', 3, 800, 11384, 3),
('personal', 1, 1000, 11284, 12),
('personal', 2, 1000, 11284, 12),
('personal', 3, 1000, 11284, 12),
('personal', 1, 1200, 11381, 5),
('personal', 2, 1200, 11381, 5),
('personal', 3, 1200, 11381, 5),
('personal', 1, 1400, 11284, 16),
('personal', 2, 1400, 11284, 16),
('personal', 3, 1400, 11284, 16),
('personal', 1, 1700, 11384, 5),
('personal', 2, 1700, 11384, 5),
('personal', 3, 1700, 11384, 5),
('personal', 1, 2000, 11284, 16),
('personal', 2, 2000, 11284, 16),
('personal', 3, 2000, 11284, 16),
('personal', 1, 2500, 11382, 4),
('personal', 2, 2500, 11382, 4),
('personal', 3, 2500, 11382, 4),
('personal', 1, 3000, 11284, 24),
('personal', 2, 3000, 11284, 24),
('personal', 3, 3000, 11284, 24),
('personal', 1, 4000, 11385, 4),
('personal', 2, 4000, 11385, 4),
('personal', 3, 4000, 11385, 4),
('personal', 1, 5000, 11381, 11),
('personal', 2, 5000, 11381, 11),
('personal', 3, 5000, 11381, 11),
('personal', 1, 6000, 5177, 5),
('personal', 2, 6000, 5177, 5),
('personal', 3, 6000, 5177, 5),
('personal', 1, 7000, 11384, 11),
('personal', 2, 7000, 11384, 11),
('personal', 3, 7000, 11384, 11),
('personal', 1, 10000, 11382, 8),
('personal', 2, 10000, 11382, 8),
('personal', 3, 10000, 11382, 8),
('personal', 1, 15000, 11385, 4),
('personal', 2, 15000, 11385, 4),
('personal', 3, 15000, 11385, 4),
('personal', 1, 20000, 11381, 13),
('personal', 2, 20000, 11381, 13),
('personal', 3, 20000, 11381, 13),
('personal', 1, 25000, 11385, 4),
('personal', 2, 25000, 11385, 4),
('personal', 3, 25000, 11385, 4),
('personal', 1, 30000, 11383, 1),
('personal', 2, 30000, 11383, 1),
('personal', 3, 30000, 11383, 1);
INSERT INTO public.festa_prizes
(type, tier, souls_req, item_id, num_item)
VALUES
('guild', 1, 100, 7468, 5),
('guild', 2, 100, 7468, 5),
('guild', 3, 100, 7465, 5),
('guild', 1, 300, 7469, 5),
('guild', 2, 300, 7469, 5),
('guild', 3, 300, 7466, 5),
('guild', 1, 700, 7470, 5),
('guild', 2, 700, 7470, 5),
('guild', 3, 700, 7467, 5),
('guild', 1, 1500, 13405, 14),
('guild', 1, 1500, 1520, 3),
('guild', 2, 1500, 13405, 14),
('guild', 2, 1500, 1520, 3),
('guild', 3, 1500, 7011, 3),
('guild', 3, 1500, 13405, 14),
('guild', 1, 3000, 10201, 10),
('guild', 2, 3000, 10201, 10),
('guild', 3, 3000, 10201, 10),
('guild', 1, 6000, 13895, 14),
('guild', 1, 6000, 1520, 6),
('guild', 2, 6000, 13895, 14),
('guild', 2, 6000, 1520, 6),
('guild', 3, 6000, 13895, 14),
('guild', 3, 6000, 7011, 4),
('guild', 1, 12000, 13406, 14),
('guild', 1, 12000, 1520, 9),
('guild', 2, 12000, 13406, 14),
('guild', 2, 12000, 1520, 9),
('guild', 3, 12000, 13406, 14),
('guild', 3, 12000, 7011, 5),
('guild', 1, 25000, 10207, 10),
('guild', 2, 25000, 10207, 10),
('guild', 3, 25000, 10207, 10),
('guild', 1, 50000, 1520, 12),
('guild', 1, 50000, 13896, 14),
('guild', 2, 50000, 1520, 12),
('guild', 2, 50000, 13896, 14),
('guild', 3, 50000, 7011, 6),
('guild', 3, 50000, 13896, 14),
('guild', 1, 100000, 10201, 10),
('guild', 2, 100000, 10201, 10),
('guild', 3, 100000, 10201, 10),
('guild', 1, 200000, 13406, 16),
('guild', 2, 200000, 13406, 16),
('guild', 3, 200000, 13406, 16),
('guild', 1, 300000, 13896, 16),
('guild', 2, 300000, 13896, 16),
('guild', 3, 300000, 13896, 16),
('guild', 1, 400000, 10207, 10),
('guild', 2, 400000, 10207, 10),
('guild', 3, 400000, 10207, 10),
('guild', 1, 500000, 13407, 6),
('guild', 1, 500000, 13897, 6),
('guild', 2, 500000, 13407, 6),
('guild', 2, 500000, 13897, 6),
('guild', 3, 500000, 13407, 6),
('guild', 3, 500000, 13897, 6);
-- Ripped trials
INSERT INTO public.festa_trials
(objective, goal_id, times_req, locale_req, reward)
VALUES
(1,27,1,0,1),
(5,53034,0,0,400),
(5,22042,0,0,89),
(5,23397,0,0,89),
(1,28,1,0,1),
(1,68,1,0,1),
(1,6,1,0,2),
(1,38,1,0,2),
(1,20,1,0,3),
(1,39,1,0,4),
(1,48,1,0,4),
(1,67,1,0,4),
(1,93,1,0,4),
(1,22,1,0,5),
(1,52,1,0,5),
(1,101,1,0,5),
(1,1,1,0,5),
(1,37,1,0,5),
(1,15,1,0,5),
(1,45,1,0,5),
(1,74,1,0,5),
(1,78,1,0,5),
(1,103,1,0,5),
(1,51,1,0,6),
(1,17,1,0,6),
(1,21,1,0,6),
(1,92,1,0,6),
(1,47,1,0,7),
(1,46,1,0,7),
(1,26,1,0,7),
(1,14,1,0,7),
(1,11,1,0,7),
(1,44,1,0,8),
(1,43,1,0,8),
(1,49,1,0,8),
(1,40,1,0,8),
(1,76,1,0,8),
(1,89,1,0,8),
(1,94,1,0,8),
(1,96,1,0,8),
(1,75,1,0,8),
(1,91,1,0,8),
(1,53,1,0,9),
(1,80,1,0,9),
(1,42,1,0,9),
(1,79,1,0,9),
(1,81,1,0,10),
(1,41,1,0,10),
(1,82,1,0,10),
(1,90,1,0,10),
(1,149,1,0,10),
(1,85,1,0,11),
(1,95,1,0,11),
(1,121,1,0,11),
(1,142,1,0,11),
(1,141,1,0,11),
(1,146,1,0,12),
(1,147,1,0,12),
(1,148,1,0,12),
(1,151,1,0,12),
(1,152,1,0,12),
(1,159,1,0,12),
(1,153,1,0,12),
(1,162,1,0,12),
(1,111,1,0,13),
(1,110,1,0,13),
(1,112,1,0,13),
(1,109,1,0,14),
(1,169,1,0,15),
(2,33,1,0,6),
(2,104,1,0,8),
(2,119,1,0,8),
(2,120,1,0,8),
(2,54,1,0,8),
(2,59,1,0,8),
(2,64,1,0,8),
(2,65,1,0,8),
(2,99,1,0,9),
(2,83,1,0,9),
(2,84,1,0,10),
(2,77,1,0,10),
(2,106,1,0,10),
(2,55,1,0,10),
(2,58,1,0,10),
(2,7,1,0,10),
(2,50,1,0,11),
(2,131,1,0,11),
(2,129,1,0,11),
(2,140,1,0,11),
(2,122,1,0,11),
(2,126,1,0,11),
(2,127,1,0,11),
(2,128,1,0,11),
(2,130,1,0,11),
(2,139,1,0,11),
(2,144,1,0,11),
(2,150,1,0,11),
(2,158,1,0,11),
(2,164,1,0,15),
(2,165,1,0,15),
(2,2,1,7,15),
(2,36,1,0,15),
(2,71,1,0,15),
(2,108,1,0,15),
(2,116,1,0,15),
(2,107,1,0,15),
(2,154,1,0,17),
(2,166,1,0,17),
(2,170,1,0,18),
(3,31,1,0,1),
(3,8,1,0,3),
(3,123,1,0,8),
(3,105,1,0,9),
(3,125,1,0,11),
(3,115,1,0,12),
(3,114,1,0,12),
(3,161,1,0,12),
(4,670,1,0,1),
(4,671,1,0,1),
(4,672,1,0,1),
(4,675,1,0,1),
(4,673,1,0,1),
(4,674,1,0,1);
END;

View File

@@ -1,9 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.guilds
ADD COLUMN IF NOT EXISTS recruiting bool NOT NULL DEFAULT true;
ALTER TABLE IF EXISTS public.guild_characters
ADD COLUMN IF NOT EXISTS recruiter bool NOT NULL DEFAULT false;
END;

View File

@@ -1,15 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.guilds
ADD COLUMN IF NOT EXISTS pugi_outfit_1 int NOT NULL DEFAULT 0;
ALTER TABLE IF EXISTS public.guilds
ADD COLUMN IF NOT EXISTS pugi_outfit_2 int NOT NULL DEFAULT 0;
ALTER TABLE IF EXISTS public.guilds
ADD COLUMN IF NOT EXISTS pugi_outfit_3 int NOT NULL DEFAULT 0;
ALTER TABLE IF EXISTS public.guilds
ADD COLUMN IF NOT EXISTS pugi_outfits int NOT NULL DEFAULT 0;
END;

View File

@@ -1,13 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.mail
ADD COLUMN IF NOT EXISTS is_sys_message bool DEFAULT false;
UPDATE mail SET is_sys_message=false;
ALTER TABLE IF EXISTS public.mail
DROP CONSTRAINT IF EXISTS mail_sender_id_fkey;
INSERT INTO public.characters (id, name) VALUES (0, '');
END;

View File

@@ -1,7 +0,0 @@
BEGIN;
CREATE SEQUENCE IF NOT EXISTS public.rasta_id_seq;
UPDATE characters SET savemercenary=NULL;
END;

View File

@@ -1,6 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.mail
ADD COLUMN IF NOT EXISTS locked boolean NOT NULL DEFAULT false;
END;

View File

@@ -1,6 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.characters
ADD COLUMN cafe_reset timestamp without time zone;
END;

View File

@@ -1,30 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.characters
ADD COLUMN IF NOT EXISTS cafe_time integer DEFAULT 0;
ALTER TABLE IF EXISTS public.characters
DROP COLUMN IF EXISTS netcafe_points;
ALTER TABLE IF EXISTS public.characters
ADD COLUMN IF NOT EXISTS netcafe_points int DEFAULT 0;
ALTER TABLE IF EXISTS public.characters
ADD COLUMN IF NOT EXISTS boost_time timestamp without time zone;
CREATE TABLE IF NOT EXISTS public.cafebonus
(
id serial NOT NULL PRIMARY KEY,
time_req integer NOT NULL,
item_type integer NOT NULL,
item_id integer NOT NULL,
quantity integer NOT NULL
);
CREATE TABLE IF NOT EXISTS public.cafe_accepted
(
cafe_id integer NOT NULL,
character_id integer NOT NULL
);
END;

View File

@@ -1,37 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS public.user_binary
(
id serial NOT NULL PRIMARY KEY,
type2 bytea,
type3 bytea,
house_tier bytea,
house_state int,
house_password text,
house_data bytea,
house_furniture bytea,
bookshelf bytea,
gallery bytea,
tore bytea,
garden bytea,
mission bytea
);
-- Create entries for existing users
INSERT INTO public.user_binary (id) SELECT c.id FROM characters c;
-- Copy existing data
UPDATE public.user_binary
SET house_furniture = (SELECT house FROM characters WHERE user_binary.id = characters.id);
UPDATE public.user_binary
SET mission = (SELECT trophy FROM characters WHERE user_binary.id = characters.id);
-- Drop old data location
ALTER TABLE public.characters
DROP COLUMN house;
ALTER TABLE public.characters
DROP COLUMN trophy;
END;

View File

@@ -1,9 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.users
ADD COLUMN IF NOT EXISTS last_login timestamp without time zone;
ALTER TABLE IF EXISTS public.users
ADD COLUMN IF NOT EXISTS return_expires timestamp without time zone;
END;

View File

@@ -1,9 +0,0 @@
BEGIN;
ALTER TABLE IF EXISTS public.normal_shop_items
DROP COLUMN IF EXISTS enable_weeks;
ALTER TABLE IF EXISTS public.shop_item_state
DROP COLUMN IF EXISTS week;
END;

View File

@@ -1,11 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS rengoku_score (
character_id integer PRIMARY KEY,
max_stages_mp integer,
max_points_mp integer,
max_stages_sp integer,
max_points_sp integer
);
END;

View File

@@ -1,23 +0,0 @@
--adds world_name and land columns
BEGIN;
CREATE TABLE IF NOT EXISTS public.servers
(
server_id integer NOT NULL,
season integer NOT NULL,
current_players integer NOT NULL,
world_name text COLLATE pg_catalog."default",
world_description text,
land integer
);
ALTER TABLE public.servers
ADD COLUMN IF NOT EXISTS land integer;
ALTER TABLE public.servers
ADD COLUMN IF NOT EXISTS world_name text COLLATE pg_catalog."default";
ALTER TABLE public.servers
ADD COLUMN IF NOT EXISTS world_description text;
END;

View File

@@ -1,13 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS public.stamps (
character_id integer PRIMARY KEY,
hl_total integer DEFAULT 0,
hl_redeemed integer DEFAULT 0,
hl_next timestamp without time zone,
ex_total integer DEFAULT 0,
ex_redeemed integer DEFAULT 0,
ex_next timestamp without time zone
);
END;

View File

@@ -1,11 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS public.titles
(
id int NOT NULL,
char_id int NOT NULL,
unlocked_at timestamp without time zone,
updated_at timestamp without time zone
);
END;

View File

@@ -1,49 +0,0 @@
BEGIN;
CREATE TABLE IF NOT EXISTS public.warehouse (
character_id integer PRIMARY KEY,
item0 bytea,
item1 bytea,
item2 bytea,
item3 bytea,
item4 bytea,
item5 bytea,
item6 bytea,
item7 bytea,
item8 bytea,
item9 bytea,
item10 bytea,
item0name text,
item1name text,
item2name text,
item3name text,
item4name text,
item5name text,
item6name text,
item7name text,
item8name text,
item9name text,
equip0 bytea,
equip1 bytea,
equip2 bytea,
equip3 bytea,
equip4 bytea,
equip5 bytea,
equip6 bytea,
equip7 bytea,
equip8 bytea,
equip9 bytea,
equip10 bytea,
equip0name text,
equip1name text,
equip2name text,
equip3name text,
equip4name text,
equip5name text,
equip6name text,
equip7name text,
equip8name text,
equip9name text
);
END;

View File

@@ -77,7 +77,13 @@ func updateRights(s *Session) {
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 netcafeBitSet bool
for _, course := range s.courses {
if (course.ID == 9 || 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{
@@ -135,6 +141,7 @@ func handleMsgSysLogin(s *Session, p mhfpacket.MHFPacket) {
s.charID = pkt.CharID0
s.token = pkt.LoginTokenString
s.Unlock()
updateRights(s)
bf := byteframe.NewByteFrame()
bf.WriteUint32(uint32(Time_Current_Adjusted().Unix())) // Unix timestamp
@@ -213,16 +220,16 @@ func logoutPlayer(s *Session) {
timePlayed += sessionTime
var rpGained int
if s.FindCourse("Netcafe").ID != 0 {
if s.FindCourse("NetCafe").ID != 0 || s.FindCourse("N").ID != 0 {
rpGained = timePlayed / 900
timePlayed = timePlayed % 900
s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID)
} else {
rpGained = timePlayed / 1800
timePlayed = timePlayed % 1800
}
s.server.db.Exec("UPDATE characters SET time_played = $1 WHERE id = $2", timePlayed, s.charID)
s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID)
treasureHuntUnregister(s)
@@ -1489,10 +1496,6 @@ func handleMsgMhfInfoScenarioCounter(s *Session, p mhfpacket.MHFPacket) {
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
}
func handleMsgMhfGetBbsSnsStatus(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfApplyBbsArticle(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfGetEtcPoints(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetEtcPoints)

View File

@@ -0,0 +1,41 @@
package channelserver
import (
"erupe-ce/common/byteframe"
"erupe-ce/common/stringsupport"
"erupe-ce/common/token"
"erupe-ce/network/mhfpacket"
)
func handleMsgMhfGetBbsUserStatus(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetBbsUserStatus)
bf := byteframe.NewByteFrame()
bf.WriteUint32(200)
bf.WriteUint32(0)
bf.WriteUint32(0)
bf.WriteUint32(0)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
func handleMsgMhfGetBbsSnsStatus(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetBbsSnsStatus)
bf := byteframe.NewByteFrame()
bf.WriteUint32(200)
bf.WriteUint32(401)
bf.WriteUint32(401)
bf.WriteUint32(0)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
func handleMsgMhfApplyBbsArticle(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfApplyBbsArticle)
bf := byteframe.NewByteFrame()
articleToken := token.Generate(40)
bf.WriteUint32(200)
bf.WriteUint32(80)
bf.WriteUint32(0)
bf.WriteUint32(0)
bf.WriteBytes(stringsupport.PaddedString(articleToken, 64, false))
bf.WriteBytes(stringsupport.PaddedString(s.server.erupeConfig.ScreenshotAPIURL, 64, false))
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}

View File

@@ -74,9 +74,14 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) {
var cafeReset time.Time
err := s.server.db.QueryRow(`SELECT cafe_reset FROM characters WHERE id=$1`, s.charID).Scan(&cafeReset)
if err != nil {
cafeReset = TimeWeekNext()
s.server.db.Exec(`UPDATE characters SET cafe_reset=$1 WHERE id=$2`, cafeReset, s.charID)
}
if Time_Current_Adjusted().After(cafeReset) {
cafeReset = TimeWeekNext()
s.server.db.Exec(`UPDATE characters SET cafe_time=0, cafe_reset=$1 WHERE id=$2; DELETE FROM cafe_accepted WHERE character_id=$2`, cafeReset, s.charID)
s.server.db.Exec(`UPDATE characters SET cafe_time=0, cafe_reset=$1 WHERE id=$2`, cafeReset, s.charID)
s.server.db.Exec(`DELETE FROM cafe_accepted WHERE character_id=$1`, s.charID)
}
var cafeTime uint32
@@ -84,10 +89,13 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) {
if err != nil {
panic(err)
}
cafeTime = uint32(Time_Current_Adjusted().Unix()) - uint32(s.sessionStart) + cafeTime
if s.FindCourse("NetCafe").ID != 0 || s.FindCourse("N").ID != 0 {
cafeTime = uint32(Time_Current_Adjusted().Unix()) - uint32(s.sessionStart) + cafeTime
}
bf.WriteUint32(cafeTime) // Total cafe time
bf.WriteUint16(0)
ps.Uint16(bf, fmt.Sprintf("Resets on %s %d", cafeReset.Month().String(), cafeReset.Day()), true)
ps.Uint16(bf, fmt.Sprintf(s.server.dict["cafeReset"], int(cafeReset.Month()), cafeReset.Day()), true)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}

View File

@@ -324,7 +324,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
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).Value != 0 {
if s.FindCourse(name).ID != 0 {
ei := slices.IndexFunc(s.courses, func(c mhfpacket.Course) bool {
for _, alias := range c.Aliases {
if strings.ToLower(name) == strings.ToLower(alias) {
@@ -335,20 +335,20 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
})
if ei != -1 {
s.courses = append(s.courses[:ei], s.courses[ei+1:]...)
sendServerChatMessage(s, fmt.Sprintf(`%s Course disabled.`, course.Aliases[0]))
sendServerChatMessage(s, fmt.Sprintf(`%s Course disabled`, course.Aliases[0]))
}
} else {
s.courses = append(s.courses, course)
sendServerChatMessage(s, fmt.Sprintf(`%s Course enabled.`, course.Aliases[0]))
sendServerChatMessage(s, fmt.Sprintf(`%s Course enabled`, course.Aliases[0]))
}
var newInt uint32
for _, course := range s.courses {
newInt += course.Value
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)", newInt, s.charID)
updateRights(s)
} else {
sendServerChatMessage(s, fmt.Sprintf(`%s Course is locked.`, course.Aliases[0]))
sendServerChatMessage(s, fmt.Sprintf(`%s Course is locked`, course.Aliases[0]))
}
}
}

View File

@@ -2005,6 +2005,7 @@ func handleMsgMhfCheckMonthlyItem(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfCheckMonthlyItem)
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x01})
// TODO: Implement month-by-month tracker, 0 = Not claimed, 1 = Claimed
// Also handles HLC and EXC items, IDs = 064D, 076B
}
func handleMsgMhfAcquireMonthlyItem(s *Session, p mhfpacket.MHFPacket) {

View File

@@ -62,10 +62,9 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) {
mail := &Mail{
SenderID: s.charID,
RecipientID: pkt.CharID,
Subject: "Invitation!",
Subject: s.server.dict["guildInviteName"],
Body: fmt.Sprintf(
"%s has invited you to join 「%s」\nDo you want to accept?",
getCharacterName(s, s.charID),
s.server.dict["guildInvite"],
guildInfo.Name,
),
IsGuildInvite: true,
@@ -149,30 +148,30 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) {
err = guild.AcceptApplication(s, s.charID)
mail = append(mail, Mail{
RecipientID: s.charID,
Subject: "Success!",
Body: fmt.Sprintf("You successfully joined 「%s」.", guild.Name),
Subject: s.server.dict["guildInviteSuccessName"],
Body: fmt.Sprintf(s.server.dict["guildInviteSuccess"], guild.Name),
IsSystemMessage: true,
})
mail = append(mail, Mail{
SenderID: s.charID,
RecipientID: pkt.LeaderID,
Subject: "Accepted",
Body: fmt.Sprintf("%s accepted your invitation to join 「%s」.", s.Name, guild.Name),
Subject: s.server.dict["guildInviteAcceptedName"],
Body: fmt.Sprintf(s.server.dict["guildInviteAccepted"], guild.Name),
IsSystemMessage: true,
})
} else {
err = guild.RejectApplication(s, s.charID)
mail = append(mail, Mail{
RecipientID: s.charID,
Subject: "Declined",
Body: fmt.Sprintf("You declined the invitation to join 「%s」.", guild.Name),
Subject: s.server.dict["guildInviteRejectName"],
Body: fmt.Sprintf(s.server.dict["guildInviteReject"], guild.Name),
IsSystemMessage: true,
})
mail = append(mail, Mail{
SenderID: s.charID,
RecipientID: pkt.LeaderID,
Subject: "Declined",
Body: fmt.Sprintf("%s declined your invitation to join 「%s」.", s.Name, guild.Name),
Subject: s.server.dict["guildInviteDeclined"],
Body: fmt.Sprintf(s.server.dict["guildInviteDeclined"], guild.Name),
IsSystemMessage: true,
})
}

View File

@@ -2,13 +2,13 @@ package channelserver
import (
"fmt"
"go.uber.org/zap"
"io"
"os"
"path/filepath"
"erupe-ce/common/byteframe"
"erupe-ce/network/mhfpacket"
"go.uber.org/zap"
)
func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) {

View File

@@ -54,5 +54,3 @@ func handleMsgSysGetUserBinary(s *Session, p mhfpacket.MHFPacket) {
}
func handleMsgSysNotifyUserBinary(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfGetBbsUserStatus(s *Session, p mhfpacket.MHFPacket) {}

View File

@@ -52,6 +52,9 @@ type Server struct {
stagesLock sync.RWMutex
stages map[string]*Stage
// Used to map different languages
dict map[string]string
// UserBinary
userBinaryPartsLock sync.RWMutex
userBinaryParts map[userBinaryPartID][]byte
@@ -164,6 +167,8 @@ func NewServer(config *Config) *Server {
// MezFes
s.stages["sl1Ns462p0a0u0"] = NewStage("sl1Ns462p0a0u0")
s.dict = getLangStrings(s)
return s
}
@@ -279,6 +284,7 @@ func (s *Server) WorldcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session,
bf := byteframe.NewByteFrame()
bf.WriteUint16(uint16(pkt.Opcode()))
pkt.Build(bf, session.clientContext)
bf.WriteUint16(0x0010)
session.QueueSendNonBlocking(bf.Data())
}
}
@@ -313,15 +319,17 @@ func (s *Server) BroadcastRaviente(ip uint32, port uint16, stage []byte, _type u
var text string
switch _type {
case 2:
text = "<Great Slaying: Berserk> is being held!"
text = s.dict["ravienteBerserk"]
case 3:
text = s.dict["ravienteExtreme"]
case 4:
text = "<Great Slaying: Extreme> is being held!"
text = s.dict["ravienteExtremeLimited"]
case 5:
text = "<Great Slaying: Berserk Practice> is being held!"
text = s.dict["ravienteBerserkSmall"]
default:
s.logger.Error("Unk raviente type", zap.Uint8("_type", _type))
}
ps.Uint16(bf, text, false)
ps.Uint16(bf, text, true)
bf.WriteBytes([]byte{0x5F, 0x53, 0x00})
bf.WriteUint32(ip) // IP address
bf.WriteUint16(port) // Port

View File

@@ -0,0 +1,52 @@
package channelserver
func getLangStrings(s *Server) map[string]string {
strings := make(map[string]string)
switch s.erupeConfig.Language {
case "jp":
strings["language"] = "日本語"
strings["cafeReset"] = "%d/%dにリセット"
strings["ravienteBerserk"] = "<大討伐:猛狂期>が開催されました!"
strings["ravienteExtreme"] = "<大討伐:猛狂期【極】>が開催されました!"
strings["ravienteExtremeLimited"] = "<大討伐:猛狂期【極】(制限付)>が開催されました!"
strings["ravienteBerserkSmall"] = "<大討伐:猛狂期(小数)>が開催されました!"
strings["guildInviteName"] = "猟団勧誘のご案内"
strings["guildInvite"] = "猟団「%s」からの勧誘通知です。\n「勧誘に返答」より、返答を行ってください。"
strings["guildInviteSuccessName"] = "成功"
strings["guildInviteSuccess"] = "あなたは「%s」に参加できました。"
strings["guildInviteAcceptedName"] = "承諾されました"
strings["guildInviteAccepted"] = "招待した狩人が「%s」への招待を承諾しました。"
strings["guildInviteRejectName"] = "却下しました"
strings["guildInviteReject"] = "あなたは「%s」への参加を却下しました。"
strings["guildInviteDeclinedName"] = "辞退しました"
strings["guildInviteDeclined"] = "招待した狩人が「%s」への招待を辞退しました。"
default:
strings["language"] = "English"
strings["cafeReset"] = "Resets on %d/%d"
strings["ravienteBerserk"] = "<Great Slaying: Berserk> is being held!"
strings["ravienteExtreme"] = "<Great Slaying: Extreme> is being held!"
strings["ravienteExtremeLimited"] = "<Great Slaying: Extreme (Limited)> is being held!"
strings["ravienteBerserkSmall"] = "<Great Slaying: Berserk (Small)> is being held!"
strings["guildInviteName"] = "Invitation!"
strings["guildInvite"] = "You have been invited to join\n「%s」\nDo you want to accept?"
strings["guildInviteSuccessName"] = "Success!"
strings["guildInviteSuccess"] = "You have successfully joined\n「%s」."
strings["guildInviteAcceptedName"] = "Accepted"
strings["guildInviteAccepted"] = "The recipient accepted your invitation to join\n「%s」."
strings["guildInviteRejectName"] = "Rejected"
strings["guildInviteReject"] = "You rejected the invitation to join\n「%s」."
strings["guildInviteDeclinedName"] = "Declined"
strings["guildInviteDeclined"] = "The recipient declined your invitation to join\n「%s」."
}
return strings
}

View File

@@ -116,10 +116,9 @@ func (s *Session) QueueSend(data []byte) {
func (s *Session) QueueSendNonBlocking(data []byte) {
select {
case s.sendPackets <- packet{data, true}:
// Enqueued data
s.logMessage(binary.BigEndian.Uint16(data[0:2]), data, "Server", s.Name)
default:
s.logger.Warn("Packet queue too full, dropping!")
// Queue too full
}
}

View File

@@ -4,6 +4,7 @@ import (
"erupe-ce/common/byteframe"
ps "erupe-ce/common/pascalstring"
"erupe-ce/common/stringsupport"
"erupe-ce/common/token"
"erupe-ce/server/channelserver"
"fmt"
"math/rand"
@@ -18,15 +19,6 @@ func makeSignInFailureResp(respID RespID) []byte {
return bf.Data()
}
func randSeq(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
func (s *Session) makeSignInResp(uid int) []byte {
returnExpiry := s.server.getReturnExpiry(uid)
@@ -37,13 +29,13 @@ func (s *Session) makeSignInResp(uid int) []byte {
}
rand.Seed(time.Now().UnixNano())
token := randSeq(16)
s.server.registerToken(uid, token)
sessToken := token.Generate(16)
s.server.registerToken(uid, sessToken)
bf := byteframe.NewByteFrame()
bf.WriteUint8(1) // resp_code
if s.server.erupeConfig.DevMode && s.server.erupeConfig.DevModeOptions.PatchServerManifest != "" && s.server.erupeConfig.DevModeOptions.PatchServerFile != "" {
if s.server.erupeConfig.DevMode && s.server.erupeConfig.PatchServerManifest != "" && s.server.erupeConfig.PatchServerFile != "" {
bf.WriteUint8(2)
} else {
bf.WriteUint8(0)
@@ -51,12 +43,12 @@ func (s *Session) makeSignInResp(uid int) []byte {
bf.WriteUint8(1) // entrance server count
bf.WriteUint8(uint8(len(chars))) // character count
bf.WriteUint32(0xFFFFFFFF) // login_token_number
bf.WriteBytes([]byte(token)) // login_token
bf.WriteBytes([]byte(sessToken)) // login_token
bf.WriteUint32(uint32(time.Now().Unix())) // current time
if s.server.erupeConfig.DevMode {
if s.server.erupeConfig.DevModeOptions.PatchServerManifest != "" && s.server.erupeConfig.DevModeOptions.PatchServerFile != "" {
ps.Uint8(bf, s.server.erupeConfig.DevModeOptions.PatchServerManifest, false)
ps.Uint8(bf, s.server.erupeConfig.DevModeOptions.PatchServerFile, false)
if s.server.erupeConfig.PatchServerManifest != "" && s.server.erupeConfig.PatchServerFile != "" {
ps.Uint8(bf, s.server.erupeConfig.PatchServerManifest, false)
ps.Uint8(bf, s.server.erupeConfig.PatchServerFile, false)
}
}
ps.Uint8(bf, fmt.Sprintf("%s:%d", s.server.erupeConfig.Host, s.server.erupeConfig.Entrance.Port), false)
@@ -111,11 +103,11 @@ func (s *Session) makeSignInResp(uid int) []byte {
}
}
if s.server.erupeConfig.DevModeOptions.HideLoginNotice {
if s.server.erupeConfig.HideLoginNotice {
bf.WriteUint8(0)
} else {
bf.WriteUint8(1) // Notice count
noticeText := s.server.erupeConfig.DevModeOptions.LoginNotice
noticeText := s.server.erupeConfig.LoginNotice
ps.Uint32(bf, noticeText, true)
}

View File

@@ -0,0 +1,122 @@
package signv2server
import (
"context"
"database/sql"
"erupe-ce/common/token"
"fmt"
"time"
"golang.org/x/crypto/bcrypt"
)
func (s *Server) createNewUser(ctx context.Context, username string, password string) (int, error) {
// Create salted hash of user password
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return 0, err
}
var id int
err = s.db.QueryRowContext(
ctx, `
INSERT INTO users (username, password, return_expires)
VALUES ($1, $2, $3)
RETURNING id
`,
username, string(passwordHash), time.Now().Add(time.Hour*24*30),
).Scan(&id)
return id, err
}
func (s *Server) createLoginToken(ctx context.Context, uid int) (string, error) {
loginToken := token.Generate(16)
_, err := s.db.ExecContext(ctx, "INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2)", uid, loginToken)
if err != nil {
return "", err
}
return loginToken, nil
}
func (s *Server) userIDFromToken(ctx context.Context, token string) (int, error) {
var userID int
err := s.db.QueryRowContext(ctx, "SELECT user_id FROM sign_sessions WHERE token = $1", token).Scan(&userID)
if err == sql.ErrNoRows {
return 0, fmt.Errorf("invalid login token")
} else if err != nil {
return 0, err
}
return userID, nil
}
func (s *Server) createCharacter(ctx context.Context, userID int) (int, error) {
var charID int
err := s.db.QueryRowContext(ctx,
"SELECT id FROM characters WHERE is_new_character = true AND user_id = $1",
userID,
).Scan(&charID)
if err == sql.ErrNoRows {
err = s.db.QueryRowContext(ctx, `
INSERT INTO characters (
user_id, is_female, is_new_character, name, unk_desc_string,
hrp, gr, weapon_type, last_login
)
VALUES ($1, false, true, '', '', 0, 0, 0, $2)
RETURNING id`,
userID, uint32(time.Now().Unix()),
).Scan(&charID)
}
return charID, err
}
func (s *Server) deleteCharacter(ctx context.Context, userID int, charID int) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(
ctx, `
DELETE FROM login_boost_state
WHERE char_id = $1`,
charID,
)
if err != nil {
return err
}
_, err = tx.ExecContext(
ctx, `
DELETE FROM guild_characters
WHERE character_id = $1`,
charID,
)
if err != nil {
return err
}
_, err = tx.ExecContext(
ctx, `
DELETE FROM characters
WHERE user_id = $1 AND id = $2`,
userID, charID,
)
if err != nil {
return err
}
return tx.Commit()
}
func (s *Server) getCharactersForUser(ctx context.Context, uid int) ([]Character, error) {
characters := make([]Character, 0)
err := s.db.SelectContext(
ctx, &characters, `
SELECT id, name, is_female, weapon_type, hrp, gr, last_login
FROM characters
WHERE user_id = $1 AND deleted = false AND is_new_character = false ORDER BY id ASC`,
uid,
)
if err != nil {
return nil, err
}
return characters, nil
}

View File

@@ -0,0 +1,208 @@
package signv2server
import (
"database/sql"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/lib/pq"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
type LauncherMessage struct {
Message string `json:"message"`
Date int64 `json:"date"`
Link string `json:"link"`
}
type Character struct {
ID int `json:"id"`
Name string `json:"name"`
IsFemale bool `json:"isFemale" db:"is_female"`
Weapon int `json:"weapon" db:"weapon_type"`
HR int `json:"hr" db:"hrp"`
GR int `json:"gr"`
LastLogin int64 `json:"lastLogin" db:"last_login"`
}
func (s *Server) Launcher(w http.ResponseWriter, r *http.Request) {
var respData struct {
Important []LauncherMessage `json:"important"`
Normal []LauncherMessage `json:"normal"`
}
respData.Important = []LauncherMessage{
{
Message: "Server Update 9 Released!",
Date: time.Date(2022, 8, 2, 0, 0, 0, 0, time.UTC).Unix(),
Link: "https://discord.com/channels/368424389416583169/929509970624532511/1003985850255818762",
},
{
Message: "Eng 2.0 & Ravi Patch Released!",
Date: time.Date(2022, 5, 3, 0, 0, 0, 0, time.UTC).Unix(),
Link: "https://discord.com/channels/368424389416583169/929509970624532511/969305400795078656",
},
{
Message: "Launcher Patch V1.0 Released!",
Date: time.Date(2022, 4, 24, 0, 0, 0, 0, time.UTC).Unix(),
Link: "https://discord.com/channels/368424389416583169/929509970624532511/969286397301248050",
},
}
respData.Normal = []LauncherMessage{
{
Message: "Join the community Discord for updates!",
Date: time.Date(2022, 4, 24, 0, 0, 0, 0, time.UTC).Unix(),
Link: "https://discord.gg/CFnzbhQ",
},
}
w.WriteHeader(200)
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(respData)
}
func (s *Server) Login(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
s.logger.Error("JSON decode error", zap.Error(err))
w.WriteHeader(400)
w.Write([]byte("Invalid data received"))
return
}
var (
userID int
password string
)
err := s.db.QueryRow("SELECT id, password FROM users WHERE username = $1", reqData.Username).Scan(&userID, &password)
if err == sql.ErrNoRows {
w.WriteHeader(400)
w.Write([]byte("Username does not exist"))
return
} else if err != nil {
s.logger.Warn("SQL query error", zap.Error(err))
w.WriteHeader(500)
return
}
if bcrypt.CompareHashAndPassword([]byte(password), []byte(reqData.Password)) != nil {
w.WriteHeader(400)
w.Write([]byte("Your password is incorrect"))
return
}
var respData struct {
Token string `json:"token"`
Characters []Character `json:"characters"`
}
respData.Token, err = s.createLoginToken(ctx, userID)
if err != nil {
s.logger.Warn("Error registering login token", zap.Error(err))
w.WriteHeader(500)
return
}
respData.Characters, err = s.getCharactersForUser(ctx, userID)
if err != nil {
s.logger.Warn("Error getting characters from DB", zap.Error(err))
w.WriteHeader(500)
return
}
w.WriteHeader(200)
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(respData)
}
func (s *Server) Register(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
s.logger.Error("JSON decode error", zap.Error(err))
w.WriteHeader(400)
w.Write([]byte("Invalid data received"))
return
}
s.logger.Info("Creating account", zap.String("username", reqData.Username))
userID, err := s.createNewUser(ctx, reqData.Username, reqData.Password)
if err != nil {
var pqErr *pq.Error
if errors.As(err, &pqErr) && pqErr.Constraint == "users_username_key" {
w.WriteHeader(400)
w.Write([]byte("User already exists"))
return
}
s.logger.Error("Error checking user", zap.Error(err), zap.String("username", reqData.Username))
w.WriteHeader(500)
return
}
var respData struct {
Token string `json:"token"`
}
respData.Token, err = s.createLoginToken(ctx, userID)
if err != nil {
s.logger.Error("Error registering login token", zap.Error(err))
w.WriteHeader(500)
return
}
json.NewEncoder(w).Encode(respData)
}
func (s *Server) CreateCharacter(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Token string `json:"token"`
}
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
s.logger.Error("JSON decode error", zap.Error(err))
w.WriteHeader(400)
w.Write([]byte("Invalid data received"))
return
}
var respData struct {
CharID int `json:"id"`
}
userID, err := s.userIDFromToken(ctx, reqData.Token)
if err != nil {
w.WriteHeader(401)
return
}
respData.CharID, err = s.createCharacter(ctx, userID)
if err != nil {
s.logger.Error("Failed to create character", zap.Error(err), zap.String("token", reqData.Token))
w.WriteHeader(500)
return
}
json.NewEncoder(w).Encode(respData)
}
func (s *Server) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Token string `json:"token"`
CharID int `json:"id"`
}
if err := json.NewDecoder(r.Body).Decode(&reqData); err != nil {
s.logger.Error("JSON decode error", zap.Error(err))
w.WriteHeader(400)
w.Write([]byte("Invalid data received"))
return
}
userID, err := s.userIDFromToken(ctx, reqData.Token)
if err != nil {
w.WriteHeader(401)
return
}
if err := s.deleteCharacter(ctx, userID, reqData.CharID); err != nil {
s.logger.Error("Failed to delete character", zap.Error(err), zap.String("token", reqData.Token), zap.Int("charID", reqData.CharID))
w.WriteHeader(500)
return
}
json.NewEncoder(w).Encode(struct{}{})
}

View File

@@ -0,0 +1,89 @@
package signv2server
import (
"context"
"erupe-ce/config"
"fmt"
"net/http"
"os"
"sync"
"time"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type Config struct {
Logger *zap.Logger
DB *sqlx.DB
ErupeConfig *config.Config
}
// Server is the MHF custom launcher sign server.
type Server struct {
sync.Mutex
logger *zap.Logger
erupeConfig *config.Config
db *sqlx.DB
httpServer *http.Server
isShuttingDown bool
}
// NewServer creates a new Server type.
func NewServer(config *Config) *Server {
s := &Server{
logger: config.Logger,
erupeConfig: config.ErupeConfig,
db: config.DB,
httpServer: &http.Server{},
}
return s
}
// Start starts the server in a new goroutine.
func (s *Server) Start() error {
// Set up the routes responsible for serving the launcher HTML, serverlist, unique name check, and JP auth.
r := mux.NewRouter()
r.HandleFunc("/launcher", s.Launcher)
r.HandleFunc("/login", s.Login)
r.HandleFunc("/register", s.Register)
r.HandleFunc("/character/create", s.CreateCharacter)
r.HandleFunc("/character/delete", s.DeleteCharacter)
handler := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}))(r)
s.httpServer.Handler = handlers.LoggingHandler(os.Stdout, handler)
s.httpServer.Addr = fmt.Sprintf(":%d", s.erupeConfig.SignV2.Port)
serveError := make(chan error, 1)
go func() {
if err := s.httpServer.ListenAndServe(); err != nil {
// Send error if any.
serveError <- err
}
}()
// Get the error from calling ListenAndServe, otherwise assume it's good after 250 milliseconds.
select {
case err := <-serveError:
return err
case <-time.After(250 * time.Millisecond):
return nil
}
}
// Shutdown exits the server gracefully.
func (s *Server) Shutdown() {
s.logger.Debug("Shutting down")
s.Lock()
s.isShuttingDown = true
s.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.httpServer.Shutdown(ctx); err != nil {
// Just warn because we are shutting down the server anyway.
s.logger.Warn("Got error on httpServer shutdown", zap.Error(err))
}
}