major refactoring; started DB

- DB work has started in db. this will be a direct port of the OpenFusion DB format.
- LoginServer is now less of a dummy. You can create and login to accounts, and create a character to go through the tutorial with.
- config.go will host some commonly changed variables.
- protocol: fixed a bug relating to arrays being ignored while encoding packets
This commit is contained in:
CPunch 2023-03-09 01:48:13 -06:00
parent 1fff485f93
commit 8569225ec7
16 changed files with 947 additions and 89 deletions

View File

@ -4,4 +4,4 @@ A toy implementation of the [Fusionfall Packet Protocol](https://openpunk.com/pa
## Generating structures
Dump and decompile the `Assembly - CSharp.dll` assembly from the fusionfall main.unity3d, using a tool like [ilspycmd](https://www.nuget.org/packages/ilspycmd/). The structures can then be stripped from the source and passed to the `genstructs.py` script located in `tools/`. See the script for details on usage.
Dump and decompile the `Assembly - CSharp.dll` assembly from the fusionfall main.unity3d, using a tool like [ilspycmd](https://www.nuget.org/packages/ilspycmd/). The full output source can then be passed to `genstructs.py` script located in `tools/`, which will handle scraping constants and calculating structure padding. See the script for details on usage.

15
config/config.go Normal file
View File

@ -0,0 +1,15 @@
package config
var (
SPAWN_X = 632032
SPAWN_Y = 187177
SPAWN_Z = -5500
AEQUIP_COUNT = 9
AINVEN_COUNT = 50
ABANK_COUNT = 119
)
func GetMaxHP(level int) int {
return (925 + 75*(level))
}

66
db/account.go Normal file
View File

@ -0,0 +1,66 @@
package db
import (
"fmt"
"golang.org/x/crypto/bcrypt"
"github.com/CPunch/GopenFusion/protocol"
"github.com/blockloop/scan"
)
type Account struct {
AccountID int
Login string
Password string
Selected int
AccountLevel int
Created int
LastLogin int
BannedUntil int
BannedSince int
BanReason string
}
func NewAccount(db DBQuery, Login, Password string) (*Account, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(Password), 12)
if err != nil {
return nil, err
}
row, err := db.Query("INSERT INTO Accounts (Login, Password, AccountLevel) VALUES(?, ?, ?) RETURNING *", Login, hash, protocol.CN_ACCOUNT_LEVEL__USER)
if err != nil {
return nil, err
}
var account Account
if err := scan.Row(&account, row); err != nil {
return nil, err
}
return &account, nil
}
var (
LoginErrorInvalidID = fmt.Errorf("Invalid Login ID!")
LoginErrorInvalidPassword = fmt.Errorf("Invalid ID && Password combo!")
)
func TryLogin(db DBQuery, Login, Password string) (*Account, error) {
row, err := db.Query("SELECT * FROM Accounts WHERE Login=?", Login)
if err != nil {
return nil, err
}
var account Account
if err := scan.Row(&account, row); err != nil {
return nil, LoginErrorInvalidID
}
if bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(Password)) != nil {
return nil, LoginErrorInvalidPassword
}
// else, login was a success
return &account, nil
}

61
db/inventory.go Normal file
View File

@ -0,0 +1,61 @@
package db
import (
"github.com/CPunch/GopenFusion/protocol"
"github.com/blockloop/scan"
)
type Inventory struct {
PlayerID int
Slot int
ID int
Type int
Opt int
TimeLimit int
}
// start && end are both inclusive
func (db *DBHandler) GetPlayerInventorySlots(PlayerID int, start int, end int) ([]protocol.SItemBase, error) {
rows, err := db.Query("SELECT * FROM Inventory WHERE Slot BETWEEN ? AND ? AND PlayerID = ?", start, end, PlayerID)
if err != nil {
return nil, err
}
var inven []Inventory
if err := scan.Row(&inven, rows); err != nil {
return make([]protocol.SItemBase, end-start), nil
}
// populate an SItemBase array
items := make([]protocol.SItemBase, end-start)
for _, item := range inven {
items[item.Slot-start] = protocol.SItemBase{
IID: int16(item.ID),
IType: int16(item.Type),
IOpt: int32(item.Opt),
ITimeLimit: int32(item.TimeLimit),
}
}
return items, nil
}
// start is inclusive TODO: needs to be a transaction !!
func (db *DBHandler) SetPlayerInventorySlots(PlayerID int, start int, items []protocol.SItemBase) error {
// delete inventory slots
_, err := db.Query("DELETE FROM Inventory WHERE Slot BETWEEN ? AND ? AND PlayerID = ?", start, start+len(items)-1, PlayerID)
if err != nil {
return err
}
// insert inventory
for i, item := range items {
if item.IID != 0 {
_, err := db.Query("INSERT INTO Inventory (PlayerID, Slot, ID, Type, Opt, TimeLimit) VALUES (?, ?, ?, ?, ?, ?)", PlayerID, start+i, item.IID, item.IType, item.IOpt, item.ITimeLimit)
if err != nil {
return err
}
}
}
return nil
}

164
db/migrations/new.sql Normal file
View File

@ -0,0 +1,164 @@
-- this file has been lifted from https://github.com/OpenFusionProject/OpenFusion/blob/master/sql/tables.sql
-- all credit to original contributors!
CREATE TABLE IF NOT EXISTS Accounts (
AccountID INTEGER NOT NULL,
Login TEXT NOT NULL UNIQUE COLLATE NOCASE,
Password TEXT NOT NULL,
Selected INTEGER DEFAULT 1 NOT NULL,
AccountLevel INTEGER NOT NULL,
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
BannedUntil INTEGER DEFAULT 0 NOT NULL,
BannedSince INTEGER DEFAULT 0 NOT NULL,
BanReason TEXT DEFAULT '' NOT NULL,
PRIMARY KEY(AccountID AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS Players (
PlayerID INTEGER NOT NULL,
AccountID INTEGER NOT NULL,
FirstName TEXT NOT NULL COLLATE NOCASE,
LastName TEXT NOT NULL COLLATE NOCASE,
NameCheck INTEGER NOT NULL,
Slot INTEGER NOT NULL,
Created INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
LastLogin INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
Level INTEGER DEFAULT 1 NOT NULL,
Nano1 INTEGER DEFAULT 0 NOT NULL,
Nano2 INTEGER DEFAULT 0 NOT NULL,
Nano3 INTEGER DEFAULT 0 NOT NULL,
AppearanceFlag INTEGER DEFAULT 0 NOT NULL,
TutorialFlag INTEGER DEFAULT 0 NOT NULL,
PayZoneFlag INTEGER DEFAULT 0 NOT NULL,
XCoordinate INTEGER NOT NULL,
YCoordinate INTEGER NOT NULL,
ZCoordinate INTEGER NOT NULL,
Angle INTEGER NOT NULL,
HP INTEGER NOT NULL,
FusionMatter INTEGER DEFAULT 0 NOT NULL,
Taros INTEGER DEFAULT 0 NOT NULL,
BatteryW INTEGER DEFAULT 0 NOT NULL,
BatteryN INTEGER DEFAULT 0 NOT NULL,
Mentor INTEGER DEFAULT 5 NOT NULL,
CurrentMissionID INTEGER DEFAULT 0 NOT NULL,
WarpLocationFlag INTEGER DEFAULT 0 NOT NULL,
SkywayLocationFlag BLOB NOT NULL,
FirstUseFlag BLOB NOT NULL,
Quests BLOB NOT NULL,
PRIMARY KEY(PlayerID AUTOINCREMENT),
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID, Slot),
UNIQUE (FirstName, LastName)
);
CREATE TABLE IF NOT EXISTS Appearances (
PlayerID INTEGER UNIQUE NOT NULL,
Body INTEGER DEFAULT 0 NOT NULL,
EyeColor INTEGER DEFAULT 1 NOT NULL,
FaceStyle INTEGER DEFAULT 1 NOT NULL,
Gender INTEGER DEFAULT 1 NOT NULL,
HairColor INTEGER DEFAULT 1 NOT NULL,
HairStyle INTEGER DEFAULT 1 NOT NULL,
Height INTEGER DEFAULT 0 NOT NULL,
SkinColor INTEGER DEFAULT 1 NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS Inventory (
PlayerID INTEGER NOT NULL,
Slot INTEGER NOT NULL,
ID INTEGER NOT NULL,
Type INTEGER NOT NULL,
Opt INTEGER NOT NULL,
TimeLimit INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Slot)
);
CREATE TABLE IF NOT EXISTS QuestItems (
PlayerID INTEGER NOT NULL,
Slot INTEGER NOT NULL,
ID INTEGER NOT NULL,
Opt INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Slot)
);
CREATE TABLE IF NOT EXISTS Nanos (
PlayerID INTEGER NOT NULL,
ID INTEGER NOT NULL,
Skill INTEGER NOT NULL,
Stamina INTEGER DEFAULT 150 NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, ID)
);
CREATE TABLE IF NOT EXISTS RunningQuests (
PlayerID INTEGER NOT NULL,
TaskID INTEGER NOT NULL,
RemainingNPCCount1 INTEGER NOT NULL,
RemainingNPCCount2 INTEGER NOT NULL,
RemainingNPCCount3 INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS Buddyships (
PlayerAID INTEGER NOT NULL,
PlayerBID INTEGER NOT NULL,
FOREIGN KEY(PlayerAID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
FOREIGN KEY(PlayerBID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS Blocks (
PlayerID INTEGER NOT NULL,
BlockedPlayerID INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
FOREIGN KEY(BlockedPlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS EmailData (
PlayerID INTEGER NOT NULL,
MsgIndex INTEGER NOT NULL,
ReadFlag INTEGER NOT NULL,
ItemFlag INTEGER NOT NULL,
SenderID INTEGER NOT NULL,
SenderFirstName TEXT NOT NULL COLLATE NOCASE,
SenderLastName TEXT NOT NULL COLLATE NOCASE,
SubjectLine TEXT NOT NULL,
MsgBody TEXT NOT NULL,
Taros INTEGER NOT NULL,
SendTime INTEGER NOT NULL,
DeleteTime INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE(PlayerID, MsgIndex)
);
CREATE TABLE IF NOT EXISTS EmailItems (
PlayerID INTEGER NOT NULL,
MsgIndex INTEGER NOT NULL,
Slot INTEGER NOT NULL,
ID INTEGER NOT NULL,
Type INTEGER NOT NULL,
Opt INTEGER NOT NULL,
TimeLimit INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, MsgIndex, Slot)
);
CREATE TABLE IF NOT EXISTS RaceResults(
EPID INTEGER NOT NULL,
PlayerID INTEGER NOT NULL,
Score INTEGER NOT NULL,
RingCount INTEGER NOT NULL,
Time INTEGER NOT NULL,
Timestamp INTEGER NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS RedeemedCodes(
PlayerID INTEGER NOT NULL,
Code TEXT NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Code)
)

227
db/players.go Normal file
View File

@ -0,0 +1,227 @@
package db
import (
"database/sql"
"fmt"
"github.com/CPunch/GopenFusion/config"
"github.com/CPunch/GopenFusion/protocol"
"github.com/blockloop/scan"
)
type Player struct {
PlayerID int
AccountID int
FirstName string
LastName string
NameCheck int
Slot int
Created int
LastLogin int
Level int
Nano1 int
Nano2 int
Nano3 int
AppearanceFlag int
TutorialFlag int
PayZoneFlag int
XCoordinate int
YCoordinate int
ZCoordinate int
Angle int
HP int
FusionMatter int
Taros int
BatteryW int
BatteryN int
Mentor int
CurrentMissionID int
WarpLocationFlag int
SkywayLocationFlag []byte
FirstUseFlag []byte
Quests []byte
/* Appearance tbl */
Body int `db:"Body"`
EyeColor int `db:"EyeColor"`
FaceStyle int `db:"FaceStyle"`
Gender int `db:"Gender"`
HairColor int `db:"HairColor"`
HairStyle int `db:"HairStyle"`
Height int `db:"Height"`
SkinColor int `db:"SkinColor"`
}
// returns PlayerID, error
func (db *DBHandler) NewPlayer(AccountID int, FirstName, LastName string, slot int) (int, error) {
nameCheck := 1 // for now, we approve all names
QuestFlag := make([]byte, 128)
SkywayLocationFlag := make([]byte, 16)
FirstUseFlag := make([]byte, 16)
var PlayerID int
if err := db.Transaction(func(tx *sql.Tx) error {
// create player
row, err := tx.Query(
"INSERT INTO Players (AccountID, Slot, FirstName, LastName, XCoordinate, YCoordinate, ZCoordinate, Angle, HP, NameCheck, Quests, SkywayLocationFlag, FirstUseFlag) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING PlayerID",
AccountID, slot, FirstName, LastName, config.SPAWN_X, config.SPAWN_Y, config.SPAWN_Z, 0, config.GetMaxHP(1), nameCheck, QuestFlag, SkywayLocationFlag, FirstUseFlag)
if err != nil {
return err
}
if err := scan.Row(&PlayerID, row); err != nil {
return err
}
// create appearance
if _, err := tx.Exec("INSERT INTO Appearances (PlayerID) VALUES (?)", PlayerID); err != nil {
return err
}
return nil
}); err != nil {
return -1, nil
}
return PlayerID, nil
}
// TODO: should this operate on the raw packet? should we do validation here or prior?
func (db *DBHandler) FinishPlayer(character *protocol.SP_CL2LS_REQ_CHAR_CREATE, AccountId int) error {
return db.Transaction(func(tx *sql.Tx) error {
// update AppearanceFlag
_, err := tx.Exec("UPDATE Players SET AppearanceFlag = 1 WHERE PlayerID = ? AND AccountID = ? AND AppearanceFlag = 0", character.PCStyle.IPC_UID, AccountId)
if err != nil {
return err
}
// update Appearance
_, err = tx.Exec("UPDATE Appearances SET Body = ?, EyeColor = ?, FaceStyle = ?, Gender = ?, HairColor = ?, HairStyle = ?, Height = ?, SkinColor = ? WHERE PlayerID = ?",
character.PCStyle.IBody,
character.PCStyle.IEyeColor,
character.PCStyle.IFaceStyle,
character.PCStyle.IGender,
character.PCStyle.IHairColor,
character.PCStyle.IHairStyle,
character.PCStyle.IHeight,
character.PCStyle.ISkinColor,
character.PCStyle.IPC_UID)
if err != nil {
return err
}
// update Inventory
items := [3]int16{character.SOn_Item.IEquipUBID, character.SOn_Item.IEquipLBID, character.SOn_Item.IEquipFootID}
for i := 0; i < len(items); i++ {
_, err = tx.Exec("INSERT INTO Inventory (PlayerID, Slot, ID, Type, Opt) VALUES (?, ?, ?, ?, 1)", character.PCStyle.IPC_UID, i+1, items[i], i+1)
if err != nil {
return err
}
}
return nil
})
}
func (db *DBHandler) FinishTutorial(PlayerID, AccountID int) error {
_, err := db.Exec("UPDATE Players SET TutorialFlag = 1 WHERE PlayerID = ? AND AccountID = ? AND TutorialFlag = 0", PlayerID, AccountID)
if err != nil {
return err
}
// TODO: reference openfusion's finishTutorial for their academy specific patches
return nil
}
// returns the deleted player row
func (db *DBHandler) DeletePlayer(PlayerID, AccountID int) (*Player, error) {
row, err := db.Query("DELETE FROM Players WHERE AccountID = ? AND PlayerID = ? RETURNING *")
if err != nil {
return nil, err
}
var plr Player
if err := scan.Row(&plr, row); err != nil {
return nil, err
}
return &plr, nil
}
func (db *DBHandler) GetPlayer(PlayerID int) (*Player, error) {
rows, err := db.Query(`SELECT
p.PlayerID, p.AccountID, p.Slot, p.FirstName, p.LastName,
p.Level, p.Nano1, p.Nano2, p.Nano3,
p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
p.Angle, p.HP, p.FusionMatter, p.Taros, p.Quests,
p.BatteryW, p.BatteryN, p.Mentor, p.WarpLocationFlag,
p.SkywayLocationFlag, p.CurrentMissionID, p.FirstUseFlag,
a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle,
a.Height, a.SkinColor
FROM Players as p
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
WHERE p.PlayerID = ?`, PlayerID)
if err != nil {
return nil, err
}
fmt.Print(PlayerID)
var plr Player
for rows.Next() {
if err := rows.Scan(
&plr.PlayerID, &plr.AccountID, &plr.Slot, &plr.FirstName, &plr.LastName,
&plr.Level, &plr.Nano1, &plr.Nano2, &plr.Nano3,
&plr.AppearanceFlag, &plr.TutorialFlag, &plr.PayZoneFlag,
&plr.XCoordinate, &plr.YCoordinate, &plr.ZCoordinate, &plr.NameCheck,
&plr.Angle, &plr.HP, &plr.FusionMatter, &plr.Taros, &plr.Quests,
&plr.BatteryW, &plr.BatteryN, &plr.Mentor, &plr.WarpLocationFlag,
&plr.SkywayLocationFlag, &plr.CurrentMissionID, &plr.FirstUseFlag,
&plr.Body, &plr.EyeColor, &plr.FaceStyle, &plr.Gender, &plr.HairColor, &plr.HairStyle,
&plr.Height, &plr.SkinColor); err != nil {
panic(err)
}
}
return &plr, nil
}
func (db *DBHandler) GetPlayers(AccountID int) ([]Player, error) {
rows, err := db.Query(`SELECT
p.PlayerID, p.AccountID, p.Slot, p.FirstName, p.LastName,
p.Level, p.Nano1, p.Nano2, p.Nano3,
p.AppearanceFlag, p.TutorialFlag, p.PayZoneFlag,
p.XCoordinate, p.YCoordinate, p.ZCoordinate, p.NameCheck,
p.Angle, p.HP, p.FusionMatter, p.Taros, p.Quests,
p.BatteryW, p.BatteryN, p.Mentor, p.WarpLocationFlag,
p.SkywayLocationFlag, p.CurrentMissionID, p.FirstUseFlag,
a.Body, a.EyeColor, a.FaceStyle, a.Gender, a.HairColor, a.HairStyle,
a.Height, a.SkinColor
FROM Players as p
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
WHERE p.AccountID = ?`, AccountID)
if err != nil {
return nil, err
}
var plrs []Player
for rows.Next() {
plr := Player{}
if err := rows.Scan(
&plr.PlayerID, &plr.AccountID, &plr.Slot, &plr.FirstName, &plr.LastName,
&plr.Level, &plr.Nano1, &plr.Nano2, &plr.Nano3,
&plr.AppearanceFlag, &plr.TutorialFlag, &plr.PayZoneFlag,
&plr.XCoordinate, &plr.YCoordinate, &plr.ZCoordinate, &plr.NameCheck,
&plr.Angle, &plr.HP, &plr.FusionMatter, &plr.Taros, &plr.Quests,
&plr.BatteryW, &plr.BatteryN, &plr.Mentor, &plr.WarpLocationFlag,
&plr.SkywayLocationFlag, &plr.CurrentMissionID, &plr.FirstUseFlag,
&plr.Body, &plr.EyeColor, &plr.FaceStyle, &plr.Gender, &plr.HairColor, &plr.HairStyle,
&plr.Height, &plr.SkinColor); err != nil {
return nil, err
}
plrs = append(plrs, plr)
}
return plrs, nil
}

81
db/schema.go Normal file
View File

@ -0,0 +1,81 @@
package db
/*
This database has been based off of openfusion's. Databases should be completely interchangable between
openfusion and gopenfusion.
*/
import (
"database/sql"
_ "embed"
"fmt"
_ "github.com/glebarez/go-sqlite"
)
type DBHandler struct {
db *sql.DB
}
type DBQuery interface {
Query(query string, args ...any) (*sql.Rows, error)
Exec(query string, args ...any) (sql.Result, error)
}
//go:embed migrations/new.sql
var createDBQuery string
var (
DefaultDB *DBHandler
)
func OpenLiteDB(dbPath string) (*DBHandler, error) {
sqliteFmt := fmt.Sprintf("%s", dbPath)
db, err := sql.Open("sqlite", sqliteFmt)
if err != nil {
return nil, err
}
return &DBHandler{db}, nil
}
func (db *DBHandler) Query(query string, args ...any) (*sql.Rows, error) {
return db.db.Query(query, args...)
}
func (db *DBHandler) Exec(query string, args ...any) (sql.Result, error) {
return db.db.Exec(query, args...)
}
func (db *DBHandler) Close() error {
return db.db.Close()
}
func (db *DBHandler) Setup() error {
// create db tables
_, err := db.db.Exec(createDBQuery)
return err
}
// calls transaction, if transaction returns a non-nil error the transaction is rolled back. otherwise the transaction is committed
func (db *DBHandler) Transaction(transaction func(*sql.Tx) error) (err error) {
tx, err := db.db.Begin()
if err != nil {
return
}
defer func() {
if p := recover(); p != nil {
// we panic'd ??? rollback and rethrow
tx.Rollback()
panic(p)
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit()
}
}()
err = transaction(tx)
return
}

18
go.mod
View File

@ -1,3 +1,21 @@
module github.com/CPunch/GopenFusion
go 1.19
require (
github.com/blockloop/scan v1.3.0
github.com/glebarez/go-sqlite v1.21.0
golang.org/x/crypto v0.7.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
golang.org/x/sys v0.6.0 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.20.4 // indirect
)

36
go.sum Normal file
View File

@ -0,0 +1,36 @@
github.com/blockloop/scan v1.3.0 h1:p8xnajpGA3d/V6o23IBFdQ764+JnNJ+PQj+OwT+rkdg=
github.com/blockloop/scan v1.3.0/go.mod h1:qd+3w68+o7m5Xhj9X5SlJH2rbFyK8w0WT47Rkuer010=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.21.0 h1:b8MHPtBagkSD2gntImZPsG3o3QEXgMDxguW/GLUonHQ=
github.com/glebarez/go-sqlite v1.21.0/go.mod h1:GodsA6yGSa3eKbvpr7dS+JaqazzVfMcjIXvx6KHhW/c=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=

View File

@ -1,8 +1,14 @@
package main
import "github.com/CPunch/GopenFusion/server"
import (
"github.com/CPunch/GopenFusion/db"
"github.com/CPunch/GopenFusion/server"
)
func main() {
db.DefaultDB, _ = db.OpenLiteDB("test.db")
db.DefaultDB.Setup()
server := server.NewLoginServer()
server.Start()
}

View File

@ -125,12 +125,12 @@ func (pkt *Packet) Encode(data interface{}) {
for i := 0; i < sz; i++ {
pkt.encodeStructField(rv.Type().Field(i), rv.Field(i))
}
case reflect.Slice: // (untested)
case reflect.Array:
sz := rv.Len()
// encode data
for i := 0; i < sz; i++ {
elem := rv.Index(i).Addr()
elem := rv.Index(i)
pkt.Encode(elem.Addr().Interface())
}
case reflect.Uint8:
@ -215,7 +215,7 @@ func (pkt *Packet) Decode(data interface{}) {
for i := 0; i < sz; i++ {
pkt.decodeStructField(rv.Type().Field(i), rv.Field(i))
}
case reflect.Array: // (untested)
case reflect.Array:
sz := rv.Len()
// decode data

View File

@ -1,11 +1,13 @@
package protocol
package server
import (
"encoding/binary"
"fmt"
"log"
"net"
"time"
"github.com/CPunch/GopenFusion/db"
"github.com/CPunch/GopenFusion/protocol"
)
const (
@ -14,30 +16,40 @@ const (
)
type ClientHandler interface {
HandlePacket(client *Client, typeID uint32, pkt *Packet)
HandlePacket(client *Client, typeID uint32, pkt *protocol.Packet)
Connect(client *Client)
Disconnect(client *Client)
}
type Client struct {
handler ClientHandler
conn net.Conn
e_key []byte
fe_key []byte
whichKey int
E_key []byte
FE_key []byte
SzID string
AccountID int
Player *db.Player
handler ClientHandler
conn net.Conn
alive bool
whichKey int
}
func NewClient(handler ClientHandler, conn net.Conn) *Client {
return &Client{
handler: handler,
conn: conn,
e_key: []byte(DEFAULT_KEY),
whichKey: USE_E,
E_key: []byte(protocol.DEFAULT_KEY),
FE_key: nil,
SzID: "",
AccountID: -1,
Player: nil,
handler: handler,
conn: conn,
alive: true,
whichKey: USE_E,
}
}
func (client *Client) Send(data interface{}, typeID uint32) {
// encode
pkt := NewPacket(make([]byte, 0))
pkt := protocol.NewPacket(make([]byte, 0))
pkt.Encode(data)
log.Printf("Sending %#v, sizeof: %d", data, len(pkt.Buf))
@ -55,9 +67,9 @@ func (client *Client) Send(data interface{}, typeID uint32) {
// encrypt typeID & body
switch client.whichKey {
case USE_E:
EncryptData(tmp, client.e_key)
protocol.EncryptData(tmp, client.E_key)
case USE_FE:
EncryptData(tmp, client.fe_key)
protocol.EncryptData(tmp, client.FE_key)
}
// write packet body
@ -66,32 +78,14 @@ func (client *Client) Send(data interface{}, typeID uint32) {
}
}
func (client *Client) AcceptLogin(SZID string, IClientVerC int32, ISlotNum int8, data []SP_LS2CL_REP_CHAR_INFO) {
resp := &SP_LS2CL_REP_LOGIN_SUCC{
SzID: SZID,
ICharCount: int8(len(data)),
ISlotNum: ISlotNum,
IPaymentFlag: 1,
IOpenBetaFlag: 0,
UiSvrTime: uint64(time.Now().Unix()),
func (client *Client) Kill() {
if !client.alive {
return
}
client.Send(resp, P_LS2CL_REP_LOGIN_SUCC)
client.e_key = CreateNewKey(
resp.UiSvrTime,
uint64(resp.ICharCount+1),
uint64(resp.ISlotNum+1),
)
client.fe_key = CreateNewKey(
binary.LittleEndian.Uint64([]byte(DEFAULT_KEY)),
uint64(IClientVerC),
1,
)
// send characters (if any)
for i := 0; i < len(data); i++ {
client.Send(data[i], P_LS2CL_REP_CHAR_INFO)
}
client.alive = false
client.conn.Close()
client.handler.Disconnect(client)
}
func (client *Client) ClientHandler() {
@ -99,11 +93,10 @@ func (client *Client) ClientHandler() {
if err := recover(); err != nil {
log.Printf("Client %p panic'd! %v", client, err)
}
client.conn.Close()
client.handler.Disconnect(client)
client.Kill()
}()
tmp := make([]byte, 4, CN_PACKET_BUFFER_SIZE)
tmp := make([]byte, 4, protocol.CN_PACKET_BUFFER_SIZE)
for {
// read packet size
if _, err := client.conn.Read(tmp); err != nil {
@ -112,7 +105,7 @@ func (client *Client) ClientHandler() {
sz := int(binary.LittleEndian.Uint32(tmp))
// client should never send a packet size outside of this range
if sz > CN_PACKET_BUFFER_SIZE || sz < 4 {
if sz > protocol.CN_PACKET_BUFFER_SIZE || sz < 4 {
panic(fmt.Errorf("[FATAL] malicious packet size received! %d", sz))
}
@ -122,12 +115,12 @@ func (client *Client) ClientHandler() {
}
// decrypt && grab typeID
DecryptData(tmp[:sz], client.e_key)
protocol.DecryptData(tmp[:sz], client.E_key)
typeID := uint32(binary.LittleEndian.Uint32(tmp[:4]))
// dispatch packet
log.Printf("Got packet ID: %x, with a sizeof: %d\n", typeID, sz)
pkt := NewPacket(tmp[4:sz])
pkt := protocol.NewPacket(tmp[4:sz])
client.handler.HandlePacket(client, typeID, pkt)
// reset tmp

171
server/login.go Normal file
View File

@ -0,0 +1,171 @@
package server
import (
"encoding/binary"
"fmt"
"time"
"github.com/CPunch/GopenFusion/config"
"github.com/CPunch/GopenFusion/db"
"github.com/CPunch/GopenFusion/protocol"
"github.com/CPunch/GopenFusion/util"
)
const (
LOGIN_DATABASE_ERROR = 0
LOGIN_ID_DOESNT_EXIST = 1
LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH = 2
LOGIN_ID_ALREADY_IN_USE = 3
LOGIN_ERROR = 4
LOGIN_CLIENT_VERSION_OUTDATED = 6
LOGIN_YOU_ARE_NOT_AN_AUTHORIZED_BETA_TESTER = 7
LOGIN_AUTHENTICATION_CONNECTION_ERROR = 8
LOGIN_UPDATED_EUALA_REQUIRED = 9
)
func (server *LoginServer) AcceptLogin(client *Client, SzID string, IClientVerC int32, ISlotNum int8, data []protocol.SP_LS2CL_REP_CHAR_INFO) {
client.SzID = SzID
resp := &protocol.SP_LS2CL_REP_LOGIN_SUCC{
SzID: SzID,
ICharCount: int8(len(data)),
ISlotNum: ISlotNum,
IPaymentFlag: 1,
IOpenBetaFlag: 0,
UiSvrTime: uint64(time.Now().Unix()),
}
client.Send(resp, protocol.P_LS2CL_REP_LOGIN_SUCC)
client.E_key = protocol.CreateNewKey(
resp.UiSvrTime,
uint64(resp.ICharCount+1),
uint64(resp.ISlotNum+1),
)
client.FE_key = protocol.CreateNewKey(
binary.LittleEndian.Uint64([]byte(protocol.DEFAULT_KEY)),
uint64(IClientVerC),
1,
)
// send characters (if any)
for i := 0; i < len(data); i++ {
client.Send(&data[i], protocol.P_LS2CL_REP_CHAR_INFO)
}
}
func (server *LoginServer) Login(client *Client, pkt *protocol.Packet) {
var loginPkt protocol.SP_CL2LS_REQ_LOGIN
pkt.Decode(&loginPkt)
SendError := func(e int32) {
client.Send(&protocol.SP_LS2CL_REP_LOGIN_FAIL{
IErrorCode: e,
SzID: loginPkt.SzID,
}, protocol.P_LS2CL_REP_LOGIN_FAIL)
}
// client is resending a login packet??
if client.AccountID != -1 {
SendError(LOGIN_ERROR)
panic(fmt.Errorf("Out of order P_CL2LS_REQ_LOGIN!"))
}
// attempt login
account, err := db.TryLogin(db.DefaultDB, loginPkt.SzID, loginPkt.SzPassword)
if err == db.LoginErrorInvalidID {
// this is the default behavior, auto create the account if the ID isn't in use
account, err = db.NewAccount(db.DefaultDB, loginPkt.SzID, loginPkt.SzPassword)
if err != nil {
// respond with a dummy login error packet so the client doesn't get softlocked
SendError(LOGIN_DATABASE_ERROR)
panic(err)
}
} else if err == db.LoginErrorInvalidPassword {
// respond with invalid password
SendError(LOGIN_ID_AND_PASSWORD_DO_NOT_MATCH)
return
} else if err != nil { // wtf?
SendError(LOGIN_DATABASE_ERROR)
panic(err)
}
// grab player data
client.AccountID = account.AccountID
plrs, err := db.DefaultDB.GetPlayers(account.AccountID)
if err != nil {
SendError(LOGIN_DATABASE_ERROR)
panic(err)
}
// build character list
charInfo := make([]protocol.SP_LS2CL_REP_CHAR_INFO, 0)
for _, plr := range plrs {
PCStyle, PCStyle2 := util.Player2PCStyle(&plr)
info := protocol.SP_LS2CL_REP_CHAR_INFO{
ISlot: int8(plr.Slot),
ILevel: int16(plr.Level),
SPC_Style: PCStyle,
SPC_Style2: PCStyle2,
IX: int32(plr.XCoordinate),
IY: int32(plr.YCoordinate),
IZ: int32(plr.ZCoordinate),
}
AEquip, err := db.DefaultDB.GetPlayerInventorySlots(plr.PlayerID, 0, config.AEQUIP_COUNT-1)
if err != nil {
SendError(LOGIN_DATABASE_ERROR)
panic(err)
}
copy(info.AEquip[:], AEquip)
charInfo = append(charInfo, info)
}
server.AcceptLogin(client, loginPkt.SzID, loginPkt.IClientVerC, 1, charInfo)
}
func (server *LoginServer) SaveCharacterName(client *Client, pkt *protocol.Packet) {
var charPkt protocol.SP_CL2LS_REQ_SAVE_CHAR_NAME
pkt.Decode(&charPkt)
if client.AccountID == -1 {
client.Send(&protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_FAIL{}, protocol.P_LS2CL_REP_SAVE_CHAR_NAME_FAIL)
panic(fmt.Errorf("Out of order P_LS2CL_REP_SAVE_CHAR_NAME_FAIL!"))
}
PlayerID, err := db.DefaultDB.NewPlayer(client.AccountID, charPkt.SzFirstName, charPkt.SzLastName, int(charPkt.ISlotNum))
if err != nil {
client.Send(&protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_FAIL{}, protocol.P_LS2CL_REP_SAVE_CHAR_NAME_FAIL)
panic(err)
}
client.Send(&protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_SUCC{
IPC_UID: int64(PlayerID),
ISlotNum: charPkt.ISlotNum,
IGender: charPkt.IGender,
SzFirstName: charPkt.SzFirstName,
SzLastName: charPkt.SzLastName,
}, protocol.P_LS2CL_REP_SAVE_CHAR_NAME_SUCC)
}
func (server *LoginServer) CharacterCreate(client *Client, pkt *protocol.Packet) {
var charPkt protocol.SP_CL2LS_REQ_CHAR_CREATE
pkt.Decode(&charPkt)
if err := db.DefaultDB.FinishPlayer(&charPkt, client.AccountID); err != nil {
panic(err)
}
plr, err := db.DefaultDB.GetPlayer(int(charPkt.PCStyle.IPC_UID))
if err != nil {
panic(err)
}
PCStyle, PCStyle2 := util.Player2PCStyle(plr)
client.Send(&protocol.SP_LS2CL_REP_CHAR_CREATE_SUCC{
ILevel: int16(plr.Level),
SPC_Style: PCStyle,
SPC_Style2: PCStyle2,
SOn_Item: protocol.SOnItem{ /*TODO*/ },
}, protocol.P_LS2CL_REP_CHAR_CREATE_SUCC)
}

View File

@ -4,14 +4,15 @@ import (
"fmt"
"log"
"net"
"sync"
"github.com/CPunch/GopenFusion/protocol"
)
type LoginServer struct {
listener net.Listener
clients map[*protocol.Client]bool
unregister chan *protocol.Client
listener net.Listener
clients map[*Client]bool
lock sync.Mutex
}
func NewLoginServer() *LoginServer {
@ -21,9 +22,8 @@ func NewLoginServer() *LoginServer {
}
return &LoginServer{
listener: listener,
clients: make(map[*protocol.Client]bool),
unregister: make(chan *protocol.Client),
listener: listener,
clients: make(map[*Client]bool),
}
}
@ -31,33 +31,22 @@ func (server *LoginServer) Start() {
log.Print("Server hosted on 127.0.0.1:23000")
for {
select {
case client := <-server.unregister:
delete(server.clients, client)
fmt.Printf("Client %p disconnected\n", client)
default:
conn, err := server.listener.Accept()
if err != nil {
log.Println("Connection error: ", err)
return
}
client := protocol.NewClient(server, conn)
server.clients[client] = true
fmt.Printf("Client %p connected\n", client)
go client.ClientHandler()
conn, err := server.listener.Accept()
if err != nil {
log.Println("Connection error: ", err)
return
}
client := NewClient(server, conn)
server.Connect(client)
go client.ClientHandler()
}
}
func (server *LoginServer) HandlePacket(client *protocol.Client, typeID uint32, pkt *protocol.Packet) {
func (server *LoginServer) HandlePacket(client *Client, typeID uint32, pkt *protocol.Packet) {
switch typeID {
case protocol.P_CL2LS_REQ_LOGIN:
var loginPkt protocol.SP_CL2LS_REQ_LOGIN
pkt.Decode(&loginPkt)
// TODO: for now, we're a dummy server
client.AcceptLogin(loginPkt.SzID, loginPkt.IClientVerC, 1, []protocol.SP_LS2CL_REP_CHAR_INFO{})
server.Login(client, pkt)
case protocol.P_CL2LS_REQ_CHECK_CHAR_NAME:
var charPkt protocol.SP_CL2LS_REQ_CHECK_CHAR_NAME
pkt.Decode(&charPkt)
@ -67,21 +56,24 @@ func (server *LoginServer) HandlePacket(client *protocol.Client, typeID uint32,
SzLastName: charPkt.SzLastName,
}, protocol.P_LS2CL_REP_CHECK_CHAR_NAME_SUCC)
case protocol.P_CL2LS_REQ_SAVE_CHAR_NAME:
var charPkt protocol.SP_CL2LS_REQ_SAVE_CHAR_NAME
pkt.Decode(&charPkt)
client.Send(&protocol.SP_LS2CL_REP_SAVE_CHAR_NAME_SUCC{
IPC_UID: 1,
ISlotNum: charPkt.ISlotNum,
IGender: charPkt.IGender,
SzFirstName: charPkt.SzFirstName,
SzLastName: charPkt.SzLastName,
}, protocol.P_LS2CL_REP_SAVE_CHAR_NAME_SUCC)
server.SaveCharacterName(client, pkt)
case protocol.P_CL2LS_REQ_CHAR_CREATE:
server.CharacterCreate(client, pkt)
default:
log.Printf("[WARN] unsupported packet ID: %x\n", typeID)
}
}
func (server *LoginServer) Disconnect(client *protocol.Client) {
server.unregister <- client
func (server *LoginServer) Disconnect(client *Client) {
server.lock.Lock()
delete(server.clients, client)
fmt.Printf("Client %p disconnected\n", client)
server.lock.Unlock()
}
func (server *LoginServer) Connect(client *Client) {
server.lock.Lock()
server.clients[client] = true
fmt.Printf("Client %p connected\n", client)
server.lock.Unlock()
}

BIN
test.db Normal file

Binary file not shown.

28
util/player.go Normal file
View File

@ -0,0 +1,28 @@
package util
import (
"github.com/CPunch/GopenFusion/db"
"github.com/CPunch/GopenFusion/protocol"
)
func Player2PCStyle(plr *db.Player) (protocol.SPCStyle, protocol.SPCStyle2) {
return protocol.SPCStyle{
IPC_UID: int64(plr.PlayerID),
INameCheck: int8(plr.NameCheck),
SzFirstName: plr.FirstName,
SzLastName: plr.LastName,
IGender: int8(plr.Gender),
IFaceStyle: int8(plr.FaceStyle),
IHairStyle: int8(plr.HairStyle),
IHairColor: int8(plr.HairColor),
ISkinColor: int8(plr.SkinColor),
IEyeColor: int8(plr.EyeColor),
IHeight: int8(plr.Height),
IBody: int8(plr.Body),
},
protocol.SPCStyle2{
IAppearanceFlag: int8(plr.AppearanceFlag),
ITutorialFlag: int8(plr.TutorialFlag),
IPayzoneFlag: int8(plr.PayZoneFlag),
}
}