mirror of
https://github.com/CPunch/gopenfusion.git
synced 2025-10-04 16:50:18 +00:00
moved 'core' to 'internal'
This commit is contained in:
70
internal/db/account.go
Normal file
70
internal/db/account.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
||||
"github.com/georgysavva/scany/v2/sqlscan"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
AccountID int `db:"accountid"`
|
||||
Login string `db:"login"`
|
||||
Password string `db:"password"`
|
||||
Selected int `db:"selected"`
|
||||
AccountLevel int `db:"accountlevel"`
|
||||
Created int `db:"created"`
|
||||
LastLogin int `db:"lastlogin"`
|
||||
BannedUntil int `db:"banneduntil"`
|
||||
BannedSince int `db:"bannedsince"`
|
||||
BanReason string `db:"banreason"`
|
||||
}
|
||||
|
||||
func (db *DBHandler) NewAccount(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($1, $2, $3) RETURNING *", Login, hash, protocol.CN_ACCOUNT_LEVEL__USER)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var account Account
|
||||
row.Next()
|
||||
if err := sqlscan.ScanRow(&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 (db *DBHandler) TryLogin(Login, Password string) (*Account, error) {
|
||||
row, err := db.Query("SELECT * FROM Accounts WHERE Login=$1", Login)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var account Account
|
||||
row.Next()
|
||||
if err := sqlscan.ScanRow(&account, row); err != nil {
|
||||
log.Printf("Error scanning row: %v", err)
|
||||
return nil, LoginErrorInvalidID
|
||||
}
|
||||
|
||||
if bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(Password)) != nil {
|
||||
return nil, LoginErrorInvalidPassword
|
||||
}
|
||||
|
||||
// else, login was a success
|
||||
return &account, nil
|
||||
}
|
62
internal/db/inventory.go
Normal file
62
internal/db/inventory.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
||||
)
|
||||
|
||||
type Inventory struct {
|
||||
PlayerID int `db:"playerid"`
|
||||
Slot int `db:"slot"`
|
||||
ID int `db:"id"`
|
||||
Type int `db:"type"`
|
||||
Opt int `db:"opt"`
|
||||
TimeLimit int `db:"timelimit"`
|
||||
}
|
||||
|
||||
// start && end are both inclusive
|
||||
func (db *DBHandler) GetPlayerInventorySlots(PlayerID int, start int, end int) ([]protocol.SItemBase, error) {
|
||||
rows, err := db.Query("SELECT Slot, Type, ID, Opt, TimeLimit FROM Inventory WHERE Slot BETWEEN $1 AND $2 AND PlayerID = $3", start, end, PlayerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]protocol.SItemBase, end-start)
|
||||
for rows.Next() {
|
||||
var slot int
|
||||
item := protocol.SItemBase{}
|
||||
|
||||
if err := rows.Scan(
|
||||
&slot, &item.IType, &item.IID, &item.IOpt, &item.ITimeLimit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items[slot-start] = item
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// start is inclusive
|
||||
func (db *DBHandler) SetPlayerInventorySlots(PlayerID int, start int, items []protocol.SItemBase) error {
|
||||
return db.Transaction(func(tx *sql.Tx) error {
|
||||
// delete inventory slots
|
||||
_, err := db.Exec("DELETE FROM Inventory WHERE Slot BETWEEN $1 AND $2 AND PlayerID = $3", start, start+len(items)-1, PlayerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert inventory
|
||||
for i, item := range items {
|
||||
if item.IID != 0 {
|
||||
_, err := db.Exec("INSERT INTO Inventory (PlayerID, Slot, ID, Type, Opt, TimeLimit) VALUES ($1, $2, $3, $4, $5, $6)", PlayerID, start+i, item.IID, item.IType, item.IOpt, item.ITimeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
166
internal/db/migrations/new.sql
Normal file
166
internal/db/migrations/new.sql
Normal file
@@ -0,0 +1,166 @@
|
||||
-- this file has been lifted from https://github.com/OpenFusionProject/OpenFusion/bytea/master/sql/tables.sql
|
||||
-- all credit to original contributors!
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS citext;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Accounts (
|
||||
AccountID SERIAL NOT NULL,
|
||||
Login CITEXT NOT NULL UNIQUE,
|
||||
Password TEXT NOT NULL,
|
||||
Selected INTEGER DEFAULT 1 NOT NULL,
|
||||
AccountLevel INTEGER NOT NULL,
|
||||
Created INTEGER DEFAULT (extract(EPOCH FROM current_time)) NOT NULL,
|
||||
LastLogin INTEGER DEFAULT (extract(EPOCH FROM current_time)) NOT NULL,
|
||||
BannedUntil INTEGER DEFAULT 0 NOT NULL,
|
||||
BannedSince INTEGER DEFAULT 0 NOT NULL,
|
||||
BanReason TEXT DEFAULT '' NOT NULL,
|
||||
PRIMARY KEY(AccountID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS Players (
|
||||
PlayerID SERIAL NOT NULL,
|
||||
AccountID INTEGER NOT NULL,
|
||||
FirstName CITEXT NOT NULL,
|
||||
LastName CITEXT NOT NULL,
|
||||
NameCheck INTEGER NOT NULL,
|
||||
Slot INTEGER NOT NULL,
|
||||
Created INTEGER DEFAULT (extract(EPOCH FROM current_time)) NOT NULL,
|
||||
LastLogin INTEGER DEFAULT (extract(EPOCH FROM current_time)) 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 bytea NOT NULL,
|
||||
FirstUseFlag bytea NOT NULL,
|
||||
Quests bytea NOT NULL,
|
||||
PRIMARY KEY(PlayerID),
|
||||
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 CITEXT NOT NULL,
|
||||
SenderLastName CITEXT NOT NULL,
|
||||
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)
|
||||
)
|
199
internal/db/players.go
Normal file
199
internal/db/players.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/entity"
|
||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
||||
"github.com/blockloop/scan"
|
||||
)
|
||||
|
||||
// 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
|
||||
rows, err := tx.Query(
|
||||
"INSERT INTO Players (AccountID, Slot, FirstName, LastName, XCoordinate, YCoordinate, ZCoordinate, Angle, HP, NameCheck, Quests, SkywayLocationFlag, FirstUseFlag) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) 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, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create appearance
|
||||
if _, err := tx.Exec("INSERT INTO Appearances (PlayerID) VALUES ($1)", 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 = $1 AND AccountID = $2 AND AppearanceFlag = 0", character.PCStyle.IPC_UID, AccountId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update Appearance
|
||||
_, err = tx.Exec("UPDATE Appearances SET Body = $1, EyeColor = $2, FaceStyle = $3, Gender = $4, HairColor = $5, HairStyle = $6, Height = $7, SkinColor = $8 WHERE PlayerID = $9",
|
||||
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, $2, $3, $4, 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 = $1 AND AccountID = $2 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 Slot number
|
||||
func (db *DBHandler) DeletePlayer(PlayerID, AccountID int) (int, error) {
|
||||
row, err := db.Query("DELETE FROM Players WHERE AccountID = $1 AND PlayerID = $2 RETURNING Slot")
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
var slot int
|
||||
if err := row.Scan(&slot); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return slot, nil
|
||||
}
|
||||
|
||||
const (
|
||||
QUERY_PLAYERS = `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, acc.AccountLevel
|
||||
FROM Players as p
|
||||
INNER JOIN Appearances as a ON p.PlayerID = a.PlayerID
|
||||
INNER JOIN Accounts as acc ON p.AccountID = acc.AccountID `
|
||||
)
|
||||
|
||||
func (db *DBHandler) readPlayer(rows *sql.Rows) (*entity.Player, error) {
|
||||
plr := entity.Player{ActiveNanoSlotNum: 0}
|
||||
|
||||
if err := rows.Scan(
|
||||
&plr.PlayerID, &plr.AccountID, &plr.Slot, &plr.PCStyle.SzFirstName, &plr.PCStyle.SzLastName,
|
||||
&plr.Level, &plr.EquippedNanos[0], &plr.EquippedNanos[1], &plr.EquippedNanos[2],
|
||||
&plr.PCStyle2.IAppearanceFlag, &plr.PCStyle2.ITutorialFlag, &plr.PCStyle2.IPayzoneFlag,
|
||||
&plr.X, &plr.Y, &plr.Z, &plr.PCStyle.INameCheck,
|
||||
&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.PCStyle.IBody, &plr.PCStyle.IEyeColor, &plr.PCStyle.IFaceStyle, &plr.PCStyle.IGender, &plr.PCStyle.IHairColor, &plr.PCStyle.IHairStyle,
|
||||
&plr.PCStyle.IHeight, &plr.PCStyle.ISkinColor, &plr.AccountLevel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plr.PCStyle.IPC_UID = int64(plr.PlayerID)
|
||||
|
||||
inven, err := db.GetPlayerInventorySlots(plr.PlayerID, 0, config.AEQUIP_COUNT+config.AINVEN_COUNT+config.ABANK_COUNT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// populate player inven
|
||||
for i, item := range inven {
|
||||
if item.IID == 0 { // skip empty slots
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case i < config.AEQUIP_COUNT:
|
||||
plr.Equip[i] = item
|
||||
case i < config.AEQUIP_COUNT+config.AINVEN_COUNT:
|
||||
plr.Inven[i-config.AEQUIP_COUNT] = item
|
||||
default:
|
||||
plr.Bank[i-config.AEQUIP_COUNT-config.AINVEN_COUNT] = item
|
||||
}
|
||||
}
|
||||
|
||||
return &plr, nil
|
||||
}
|
||||
|
||||
func (db *DBHandler) GetPlayer(PlayerID int) (*entity.Player, error) {
|
||||
rows, err := db.Query(QUERY_PLAYERS+"WHERE p.PlayerID = $1", PlayerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plr *entity.Player
|
||||
for rows.Next() {
|
||||
plr, err = db.readPlayer(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return plr, nil
|
||||
}
|
||||
|
||||
func (db *DBHandler) GetPlayers(AccountID int) ([]entity.Player, error) {
|
||||
rows, err := db.Query(QUERY_PLAYERS+"WHERE p.AccountID = $1", AccountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var plrs []entity.Player
|
||||
for rows.Next() {
|
||||
plr, err := db.readPlayer(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plrs = append(plrs, *plr)
|
||||
}
|
||||
|
||||
return plrs, nil
|
||||
}
|
73
internal/db/schema.go
Normal file
73
internal/db/schema.go
Normal file
@@ -0,0 +1,73 @@
|
||||
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/CPunch/gopenfusion/config"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type DBHandler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
//go:embed migrations/new.sql
|
||||
var createDBQuery string
|
||||
|
||||
func OpenPostgresDB(dbAddr string) (*DBHandler, error) {
|
||||
fmt := fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", config.GetDBUser(), config.GetDBPass(), dbAddr, config.GetDBName())
|
||||
|
||||
db, err := sql.Open("postgres", fmt)
|
||||
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
|
||||
}
|
86
internal/entity/chunk.go
Normal file
86
internal/entity/chunk.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Chunk struct {
|
||||
Position ChunkPosition
|
||||
Entities map[Entity]struct{}
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewChunk(position ChunkPosition) *Chunk {
|
||||
return &Chunk{
|
||||
Position: position,
|
||||
Entities: make(map[Entity]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Chunk) AddEntity(entity Entity) {
|
||||
c.Entities[entity] = struct{}{}
|
||||
}
|
||||
|
||||
func (c *Chunk) RemoveEntity(entity Entity) {
|
||||
delete(c.Entities, entity)
|
||||
}
|
||||
|
||||
// send packet to all peers in this chunk and kill each peer if error
|
||||
func (c *Chunk) SendPacket(typeID uint32, pkt ...interface{}) {
|
||||
c.SendPacketExclude(nil, typeID, pkt...)
|
||||
}
|
||||
|
||||
func (c *Chunk) SendPacketExclude(exclude Entity, typeID uint32, pkt ...interface{}) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
for entity := range c.Entities {
|
||||
// only send to players, and exclude the player that sent the packet
|
||||
if entity.GetKind() != ENTITY_KIND_PLAYER || entity == exclude {
|
||||
continue
|
||||
}
|
||||
|
||||
plr, ok := entity.(*Player)
|
||||
if !ok {
|
||||
log.Panic("Chunk.SendPacket: entity kind was player, but is not a *Player")
|
||||
}
|
||||
peer := plr.Peer
|
||||
|
||||
if err := peer.Send(typeID, pkt...); err != nil {
|
||||
log.Printf("Error sending packet to peer %p: %v", peer, err)
|
||||
peer.Kill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Chunk) GetAdjacentPositions() []ChunkPosition {
|
||||
return []ChunkPosition{
|
||||
{c.Position.X - 1, c.Position.Y - 1},
|
||||
{c.Position.X - 1, c.Position.Y},
|
||||
{c.Position.X - 1, c.Position.Y + 1},
|
||||
{c.Position.X, c.Position.Y - 1},
|
||||
{c.Position.X, c.Position.Y},
|
||||
{c.Position.X, c.Position.Y + 1},
|
||||
{c.Position.X + 1, c.Position.Y - 1},
|
||||
{c.Position.X + 1, c.Position.Y},
|
||||
{c.Position.X + 1, c.Position.Y + 1},
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/45428032 lol
|
||||
func ChunkSliceDifference(a, b []*Chunk) []*Chunk {
|
||||
m := make(map[*Chunk]struct{})
|
||||
for _, item := range b {
|
||||
m[item] = struct{}{}
|
||||
}
|
||||
|
||||
var diff []*Chunk
|
||||
for _, item := range a {
|
||||
if _, ok := m[item]; !ok {
|
||||
diff = append(diff, item)
|
||||
}
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
15
internal/entity/chunkposition.go
Normal file
15
internal/entity/chunkposition.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package entity
|
||||
|
||||
import "github.com/CPunch/gopenfusion/config"
|
||||
|
||||
type ChunkPosition struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
func MakeChunkPosition(x, y int) ChunkPosition {
|
||||
return ChunkPosition{
|
||||
X: x / (config.VIEW_DISTANCE / 3),
|
||||
Y: y / (config.VIEW_DISTANCE / 3),
|
||||
}
|
||||
}
|
25
internal/entity/entity.go
Normal file
25
internal/entity/entity.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package entity
|
||||
|
||||
import "github.com/CPunch/gopenfusion/internal/protocol"
|
||||
|
||||
type EntityKind int
|
||||
|
||||
const (
|
||||
ENTITY_KIND_PLAYER EntityKind = iota
|
||||
ENTITY_KIND_NPC
|
||||
)
|
||||
|
||||
type Entity interface {
|
||||
GetKind() EntityKind
|
||||
|
||||
GetChunk() ChunkPosition
|
||||
GetPosition() (x int, y int, z int)
|
||||
GetAngle() int
|
||||
|
||||
SetChunk(chunk ChunkPosition)
|
||||
SetPosition(x, y, z int)
|
||||
SetAngle(angle int)
|
||||
|
||||
DisappearFromViewOf(peer *protocol.CNPeer)
|
||||
EnterIntoViewOf(peer *protocol.CNPeer)
|
||||
}
|
87
internal/entity/npc.go
Normal file
87
internal/entity/npc.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
||||
)
|
||||
|
||||
type NPC struct {
|
||||
ID int
|
||||
X int `json:"iX"`
|
||||
Y int `json:"iY"`
|
||||
Z int `json:"iZ"`
|
||||
Angle int `json:"iAngle"`
|
||||
NPCType int `json:"iNPCType"`
|
||||
Chunk ChunkPosition
|
||||
}
|
||||
|
||||
var nextNPCID = &atomic.Int32{}
|
||||
|
||||
func NewNPC(X, Y, Z, Angle int, npcType int) *NPC {
|
||||
return &NPC{
|
||||
ID: int(nextNPCID.Add(1)),
|
||||
X: X,
|
||||
Y: Y,
|
||||
Z: Z,
|
||||
Angle: Angle,
|
||||
NPCType: npcType,
|
||||
Chunk: MakeChunkPosition(X, Y),
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Entity interface ====================
|
||||
|
||||
func (npc *NPC) GetKind() EntityKind {
|
||||
return ENTITY_KIND_NPC
|
||||
}
|
||||
|
||||
func (npc *NPC) GetChunk() ChunkPosition {
|
||||
return npc.Chunk
|
||||
}
|
||||
|
||||
func (npc *NPC) GetPosition() (x int, y int, z int) {
|
||||
return npc.X, npc.Y, npc.Z
|
||||
}
|
||||
|
||||
func (npc *NPC) GetAngle() int {
|
||||
return npc.Angle
|
||||
}
|
||||
|
||||
func (npc *NPC) SetChunk(chunk ChunkPosition) {
|
||||
npc.Chunk = chunk
|
||||
}
|
||||
|
||||
func (npc *NPC) SetPosition(x, y, z int) {
|
||||
npc.X = x
|
||||
npc.Y = y
|
||||
npc.Z = z
|
||||
}
|
||||
|
||||
func (npc *NPC) SetAngle(angle int) {
|
||||
npc.Angle = angle
|
||||
}
|
||||
|
||||
func (npc *NPC) DisappearFromViewOf(peer *protocol.CNPeer) {
|
||||
peer.Send(protocol.P_FE2CL_NPC_EXIT, protocol.SP_FE2CL_NPC_EXIT{
|
||||
INPC_ID: int32(npc.ID),
|
||||
})
|
||||
}
|
||||
|
||||
func (npc *NPC) EnterIntoViewOf(peer *protocol.CNPeer) {
|
||||
peer.Send(protocol.P_FE2CL_NPC_NEW, protocol.SP_FE2CL_NPC_NEW{
|
||||
NPCAppearanceData: npc.GetAppearanceData(),
|
||||
})
|
||||
}
|
||||
|
||||
func (npc *NPC) GetAppearanceData() protocol.SNPCAppearanceData {
|
||||
return protocol.SNPCAppearanceData{
|
||||
INPC_ID: int32(npc.ID),
|
||||
INPCType: int32(npc.NPCType),
|
||||
IHP: 100,
|
||||
IX: int32(npc.X),
|
||||
IY: int32(npc.Y),
|
||||
IZ: int32(npc.Z),
|
||||
IAngle: int32(npc.Angle),
|
||||
}
|
||||
}
|
127
internal/entity/player.go
Normal file
127
internal/entity/player.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/protocol"
|
||||
)
|
||||
|
||||
type Player struct {
|
||||
Peer *protocol.CNPeer
|
||||
Chunk ChunkPosition
|
||||
PlayerID int
|
||||
AccountID int
|
||||
AccountLevel int
|
||||
Slot int
|
||||
PCStyle protocol.SPCStyle
|
||||
PCStyle2 protocol.SPCStyle2
|
||||
EquippedNanos [3]int
|
||||
Nanos [config.NANO_COUNT]protocol.SNano
|
||||
Equip [config.AEQUIP_COUNT]protocol.SItemBase
|
||||
Inven [config.AINVEN_COUNT]protocol.SItemBase
|
||||
Bank [config.ABANK_COUNT]protocol.SItemBase
|
||||
SkywayLocationFlag []byte
|
||||
FirstUseFlag []byte
|
||||
Quests []byte
|
||||
HP int
|
||||
Level int
|
||||
Taros int
|
||||
FusionMatter int
|
||||
Mentor int
|
||||
X, Y, Z int
|
||||
Angle int
|
||||
BatteryN int
|
||||
BatteryW int
|
||||
WarpLocationFlag int
|
||||
ActiveNanoSlotNum int
|
||||
Fatigue int
|
||||
CurrentMissionID int
|
||||
IPCState int8
|
||||
}
|
||||
|
||||
// ==================== Entity interface ====================
|
||||
|
||||
func (plr *Player) GetKind() EntityKind {
|
||||
return ENTITY_KIND_PLAYER
|
||||
}
|
||||
|
||||
func (plr *Player) GetChunk() ChunkPosition {
|
||||
return plr.Chunk
|
||||
}
|
||||
|
||||
func (plr *Player) GetPosition() (x int, y int, z int) {
|
||||
return plr.X, plr.Y, plr.Z
|
||||
}
|
||||
|
||||
func (plr *Player) GetAngle() int {
|
||||
return plr.Angle
|
||||
}
|
||||
|
||||
func (plr *Player) SetChunk(chunk ChunkPosition) {
|
||||
plr.Chunk = chunk
|
||||
}
|
||||
|
||||
func (plr *Player) SetPosition(x, y, z int) {
|
||||
plr.X = x
|
||||
plr.Y = y
|
||||
plr.Z = z
|
||||
}
|
||||
|
||||
func (plr *Player) SetAngle(angle int) {
|
||||
plr.Angle = angle
|
||||
}
|
||||
|
||||
func (plr *Player) DisappearFromViewOf(peer *protocol.CNPeer) {
|
||||
peer.Send(protocol.P_FE2CL_PC_EXIT, protocol.SP_FE2CL_PC_EXIT{
|
||||
IID: int32(plr.PlayerID),
|
||||
})
|
||||
}
|
||||
|
||||
func (plr *Player) EnterIntoViewOf(peer *protocol.CNPeer) {
|
||||
peer.Send(protocol.P_FE2CL_PC_NEW, protocol.SP_FE2CL_PC_NEW{
|
||||
PCAppearanceData: plr.GetAppearanceData(),
|
||||
})
|
||||
}
|
||||
|
||||
func (plr *Player) ToPCLoadData2CL() protocol.SPCLoadData2CL {
|
||||
return protocol.SPCLoadData2CL{
|
||||
IUserLevel: int16(plr.AccountLevel),
|
||||
PCStyle: plr.PCStyle,
|
||||
PCStyle2: plr.PCStyle2,
|
||||
IMentor: int16(plr.Mentor),
|
||||
IMentorCount: 1,
|
||||
IHP: int32(plr.HP),
|
||||
IBatteryW: int32(plr.BatteryW),
|
||||
IBatteryN: int32(plr.BatteryN),
|
||||
ICandy: int32(plr.Taros),
|
||||
IFusionMatter: int32(plr.FusionMatter),
|
||||
ISpecialState: 0,
|
||||
IMapNum: 0,
|
||||
IX: int32(plr.X),
|
||||
IY: int32(plr.Y),
|
||||
IZ: int32(plr.Z),
|
||||
IAngle: int32(plr.Angle),
|
||||
AEquip: plr.Equip,
|
||||
AInven: plr.Inven,
|
||||
ANanoSlots: [3]int16{int16(plr.EquippedNanos[0]), int16(plr.EquippedNanos[1]), int16(plr.EquippedNanos[2])},
|
||||
IActiveNanoSlotNum: int16(plr.ActiveNanoSlotNum),
|
||||
IWarpLocationFlag: int32(plr.WarpLocationFlag),
|
||||
IBuddyWarpTime: 60,
|
||||
IFatigue: 50,
|
||||
}
|
||||
}
|
||||
|
||||
func (plr *Player) GetAppearanceData() protocol.SPCAppearanceData {
|
||||
return protocol.SPCAppearanceData{
|
||||
IID: int32(plr.PlayerID),
|
||||
IHP: int32(plr.HP),
|
||||
ILv: int16(plr.Level),
|
||||
IX: int32(plr.X),
|
||||
IY: int32(plr.Y),
|
||||
IZ: int32(plr.Z),
|
||||
IAngle: int32(plr.Angle),
|
||||
PCStyle: plr.PCStyle,
|
||||
IPCState: plr.IPCState,
|
||||
ItemEquip: plr.Equip,
|
||||
Nano: plr.Nanos[plr.ActiveNanoSlotNum],
|
||||
}
|
||||
}
|
150
internal/protocol/cnpeer.go
Normal file
150
internal/protocol/cnpeer.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/protocol/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
USE_E = iota
|
||||
USE_FE
|
||||
)
|
||||
|
||||
// CNPeer is a simple wrapper for net.Conn connections to send/recv packets over the Fusionfall packet protocol.
|
||||
type CNPeer struct {
|
||||
conn net.Conn
|
||||
eRecv chan *Event
|
||||
SzID string
|
||||
E_key []byte
|
||||
FE_key []byte
|
||||
AccountID int
|
||||
PlayerID int32
|
||||
whichKey int
|
||||
alive bool
|
||||
}
|
||||
|
||||
func GetTime() uint64 {
|
||||
return uint64(time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
func NewCNPeer(eRecv chan *Event, conn net.Conn) *CNPeer {
|
||||
return &CNPeer{
|
||||
conn: conn,
|
||||
eRecv: eRecv,
|
||||
SzID: "",
|
||||
E_key: []byte(DEFAULT_KEY),
|
||||
FE_key: nil,
|
||||
AccountID: -1,
|
||||
whichKey: USE_E,
|
||||
alive: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (peer *CNPeer) Send(typeID uint32, data ...interface{}) error {
|
||||
// grab buffer from pool
|
||||
buf := pool.Get()
|
||||
defer pool.Put(buf)
|
||||
|
||||
// allocate space for packet size
|
||||
buf.Write(make([]byte, 4))
|
||||
|
||||
// body start
|
||||
pkt := NewPacket(buf)
|
||||
|
||||
// encode type id
|
||||
if err := pkt.Encode(typeID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// encode data
|
||||
for _, trailer := range data {
|
||||
if err := pkt.Encode(trailer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// prepend the packet size
|
||||
binary.LittleEndian.PutUint32(buf.Bytes()[:4], uint32(buf.Len()-4))
|
||||
|
||||
// encrypt body
|
||||
switch peer.whichKey {
|
||||
case USE_E:
|
||||
EncryptData(buf.Bytes()[4:], peer.E_key)
|
||||
case USE_FE:
|
||||
EncryptData(buf.Bytes()[4:], peer.FE_key)
|
||||
}
|
||||
|
||||
// send full packet
|
||||
log.Printf("Sending %#v, sizeof: %d, buffer: %v", data, buf.Len(), buf.Bytes())
|
||||
if _, err := peer.conn.Write(buf.Bytes()); err != nil {
|
||||
return fmt.Errorf("failed to write packet body! %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *CNPeer) SetActiveKey(whichKey int) {
|
||||
peer.whichKey = whichKey
|
||||
}
|
||||
|
||||
func (peer *CNPeer) Kill() {
|
||||
log.Printf("Killing peer %p", peer)
|
||||
if !peer.alive {
|
||||
return
|
||||
}
|
||||
|
||||
peer.alive = false
|
||||
peer.conn.Close()
|
||||
peer.eRecv <- &Event{Type: EVENT_CLIENT_DISCONNECT, Peer: peer}
|
||||
}
|
||||
|
||||
// meant to be invoked as a goroutine
|
||||
func (peer *CNPeer) Handler() {
|
||||
defer peer.Kill()
|
||||
|
||||
for {
|
||||
// read packet size, the goroutine spends most of it's time parked here
|
||||
var sz uint32
|
||||
if err := binary.Read(peer.conn, binary.LittleEndian, &sz); err != nil {
|
||||
log.Printf("[FATAL] failed to read packet size! %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// client should never send a packet size outside of this range
|
||||
if sz > CN_PACKET_BUFFER_SIZE || sz < 4 {
|
||||
log.Printf("[FATAL] malicious packet size received! %d", sz)
|
||||
return
|
||||
}
|
||||
|
||||
// grab buffer && read packet body
|
||||
if err := func() error {
|
||||
buf := pool.Get()
|
||||
if _, err := buf.ReadFrom(io.LimitReader(peer.conn, int64(sz))); err != nil {
|
||||
return fmt.Errorf("failed to read packet body! %v", err)
|
||||
}
|
||||
|
||||
// decrypt
|
||||
DecryptData(buf.Bytes(), peer.E_key)
|
||||
pkt := NewPacket(buf)
|
||||
|
||||
// create packet && read pktID
|
||||
var pktID uint32
|
||||
if err := pkt.Decode(&pktID); err != nil {
|
||||
return fmt.Errorf("failed to read packet type! %v", err)
|
||||
}
|
||||
|
||||
// dispatch packet
|
||||
log.Printf("Got packet ID: %x, with a sizeof: %d\n", pktID, sz)
|
||||
peer.eRecv <- &Event{Type: EVENT_CLIENT_PACKET, Peer: peer, Pkt: buf, PktID: pktID}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
log.Printf("[FATAL] %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
71
internal/protocol/encrypt.go
Normal file
71
internal/protocol/encrypt.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_KEY = "m@rQn~W#"
|
||||
KEY_LENGTH = 8
|
||||
)
|
||||
|
||||
func encrypt_byte_change_A(ERSize int, data []byte) int {
|
||||
var num, num2, num3 int
|
||||
|
||||
for num+ERSize <= len(data) {
|
||||
num4 := num + num3
|
||||
num5 := num + (ERSize - 1 - num3)
|
||||
|
||||
b := data[num4]
|
||||
data[num4] = data[num5]
|
||||
data[num5] = b
|
||||
num += ERSize
|
||||
num3++
|
||||
if num3 > ERSize/2 {
|
||||
num3 = 0
|
||||
}
|
||||
}
|
||||
|
||||
num2 = ERSize - (num + ERSize - len(data))
|
||||
return num + num2
|
||||
}
|
||||
|
||||
func xorData(buff, key []byte, size int) {
|
||||
for i := 0; i < size; i++ {
|
||||
buff[i] ^= key[i%KEY_LENGTH]
|
||||
}
|
||||
}
|
||||
|
||||
func EncryptData(buff, key []byte) {
|
||||
ERSize := len(buff)%(KEY_LENGTH/2+1)*2 + KEY_LENGTH
|
||||
xorData(buff, key, len(buff))
|
||||
encrypt_byte_change_A(ERSize, buff)
|
||||
}
|
||||
|
||||
func DecryptData(buff, key []byte) {
|
||||
ERSize := len(buff)%(KEY_LENGTH/2+1)*2 + KEY_LENGTH
|
||||
size := encrypt_byte_change_A(ERSize, buff)
|
||||
xorData(buff, key, size)
|
||||
}
|
||||
|
||||
func CreateNewKey(uTime uint64, iv1, iv2 uint64) []byte {
|
||||
num := iv1 + 1
|
||||
num2 := iv2 + 1
|
||||
|
||||
dEKey := uint64(binary.LittleEndian.Uint64([]byte(DEFAULT_KEY)))
|
||||
key := dEKey * (uTime * num * num2)
|
||||
buf := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(buf, uint64(key))
|
||||
return buf
|
||||
}
|
||||
|
||||
func GenSerialKey() (int64, error) {
|
||||
tmp := [8]byte{}
|
||||
if _, err := rand.Read(tmp[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// convert to uint64 && return
|
||||
return int64(binary.LittleEndian.Uint64(tmp[:])), nil
|
||||
}
|
15
internal/protocol/event.go
Normal file
15
internal/protocol/event.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package protocol
|
||||
|
||||
import "bytes"
|
||||
|
||||
const (
|
||||
EVENT_CLIENT_DISCONNECT = iota
|
||||
EVENT_CLIENT_PACKET
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Type int
|
||||
Peer *CNPeer
|
||||
Pkt *bytes.Buffer
|
||||
PktID uint32
|
||||
}
|
158
internal/protocol/packet.go
Normal file
158
internal/protocol/packet.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
/*
|
||||
this file handles serializing (and deserializing) structs to alignment-strict c structures generated via `tools/genstructs.py`.
|
||||
see script for details on usage!
|
||||
*/
|
||||
|
||||
type Packet struct {
|
||||
readWriter io.ReadWriter
|
||||
}
|
||||
|
||||
func NewPacket(readWriter io.ReadWriter) Packet {
|
||||
return Packet{
|
||||
readWriter: readWriter,
|
||||
}
|
||||
}
|
||||
|
||||
func (pkt Packet) encodeStructField(field reflect.StructField, value reflect.Value) error {
|
||||
// log.Printf("Encoding '%s'", field.Name)
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.String: // all strings in fusionfall packets are encoded as utf16, we'll need to encode it
|
||||
sz, err := strconv.Atoi(field.Tag.Get("size"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to grab string 'size' tag")
|
||||
}
|
||||
|
||||
buf16 := utf16.Encode([]rune(value.String()))
|
||||
|
||||
// len(buf16) needs to be the same size as sz
|
||||
if len(buf16) > sz {
|
||||
// truncate
|
||||
buf16 = buf16[:sz]
|
||||
} else {
|
||||
// grow
|
||||
// TODO: probably a better way to do this?
|
||||
for len(buf16) < sz {
|
||||
buf16 = append(buf16, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// write
|
||||
if err := binary.Write(pkt.readWriter, binary.LittleEndian, buf16); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := pkt.Encode(value.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// write padding bytes
|
||||
pad, err := strconv.Atoi(field.Tag.Get("pad"))
|
||||
if err == nil {
|
||||
for i := 0; i < pad; i++ {
|
||||
if _, err := pkt.readWriter.Write([]byte{0}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pkt Packet) Encode(data interface{}) error {
|
||||
rv := reflect.Indirect(reflect.ValueOf(data))
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Struct:
|
||||
// walk through each struct fields
|
||||
sz := rv.NumField()
|
||||
for i := 0; i < sz; i++ {
|
||||
if err := pkt.encodeStructField(rv.Type().Field(i), rv.Field(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
// we pass everything else to go's binary package
|
||||
if err := binary.Write(pkt.readWriter, binary.LittleEndian, data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pkt Packet) decodeStructField(field reflect.StructField, value reflect.Value) error {
|
||||
// log.Printf("Decoding '%s'", field.Name)
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.String: // all strings in fusionfall packets are encoded as utf16, we'll need to decode it
|
||||
sz, err := strconv.Atoi(field.Tag.Get("size"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to grab string 'size' tag")
|
||||
}
|
||||
|
||||
buf16 := make([]uint16, sz)
|
||||
if err := binary.Read(pkt.readWriter, binary.LittleEndian, buf16); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find null terminator
|
||||
var realSize int
|
||||
for ; realSize < len(buf16); realSize++ {
|
||||
if buf16[realSize] == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
value.SetString(string(utf16.Decode(buf16[:realSize])))
|
||||
default:
|
||||
if err := pkt.Decode(value.Addr().Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// consume padding bytes
|
||||
pad, err := strconv.Atoi(field.Tag.Get("pad"))
|
||||
if err == nil {
|
||||
for i := 0; i < pad; i++ {
|
||||
if _, err := pkt.readWriter.Read([]byte{0}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pkt Packet) Decode(data interface{}) error {
|
||||
rv := reflect.Indirect(reflect.ValueOf(data))
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Struct:
|
||||
// walk through each struct fields
|
||||
sz := rv.NumField()
|
||||
for i := 0; i < sz; i++ {
|
||||
if err := pkt.decodeStructField(rv.Type().Field(i), rv.Field(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
if err := binary.Read(pkt.readWriter, binary.LittleEndian, data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
19
internal/protocol/pool/pool.go
Normal file
19
internal/protocol/pool/pool.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var allocator = &sync.Pool{
|
||||
New: func() any { return new(bytes.Buffer) },
|
||||
}
|
||||
|
||||
func Get() *bytes.Buffer {
|
||||
return allocator.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
func Put(buf *bytes.Buffer) {
|
||||
buf.Reset()
|
||||
allocator.Put(buf)
|
||||
}
|
4965
internal/protocol/structs.go
Normal file
4965
internal/protocol/structs.go
Normal file
File diff suppressed because it is too large
Load Diff
52
internal/redis/login.go
Normal file
52
internal/redis/login.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
)
|
||||
|
||||
type LoginMetadata struct {
|
||||
FEKey []byte `json:",omitempty"`
|
||||
PlayerID int32 `json:",omitempty"`
|
||||
AccountID int `json:",omitempty"`
|
||||
}
|
||||
|
||||
// we store login queues into redis with the name "loginMetadata_<serialKey>"
|
||||
// set to expire after config.LOGIN_TIMEOUT duration. this way we can easily
|
||||
// have a shared pool of active serial keys & player login data which any
|
||||
// shard can pull from
|
||||
|
||||
func makeLoginMetadataKey(serialKey int64) string {
|
||||
return fmt.Sprintf("loginMetadata_%s", strconv.Itoa(int(serialKey)))
|
||||
}
|
||||
|
||||
func (r *RedisHandler) QueueLogin(serialKey int64, data LoginMetadata) error {
|
||||
value, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add to table
|
||||
return r.client.Set(r.ctx, makeLoginMetadataKey(serialKey), value, config.LOGIN_TIMEOUT).Err()
|
||||
}
|
||||
|
||||
func (r *RedisHandler) GetLogin(serialKey int64) (LoginMetadata, error) {
|
||||
value, err := r.client.Get(r.ctx, makeLoginMetadataKey(serialKey)).Result()
|
||||
if err != nil {
|
||||
return LoginMetadata{}, err
|
||||
}
|
||||
|
||||
var data LoginMetadata
|
||||
if err := json.Unmarshal([]byte(value), &data); err != nil {
|
||||
return LoginMetadata{}, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *RedisHandler) RemoveLogin(serialKey int64) error {
|
||||
return r.client.Del(r.ctx, makeLoginMetadataKey(serialKey)).Err()
|
||||
}
|
36
internal/redis/redis.go
Normal file
36
internal/redis/redis.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package redis
|
||||
|
||||
/*
|
||||
used for state management between shard and login servers
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type RedisHandler struct {
|
||||
client *redis.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
const (
|
||||
SHARD_SET = "shards"
|
||||
)
|
||||
|
||||
func OpenRedis(addr string) (*RedisHandler, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
CredentialsProvider: config.GetRedisCredentials(),
|
||||
})
|
||||
|
||||
_, err := client.Ping(context.Background()).Result()
|
||||
return &RedisHandler{client: client, ctx: context.Background()}, err
|
||||
}
|
||||
|
||||
func (r *RedisHandler) Close() error {
|
||||
return r.client.Close()
|
||||
}
|
34
internal/redis/shard.go
Normal file
34
internal/redis/shard.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package redis
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type ShardMetadata struct {
|
||||
IP string
|
||||
Port int
|
||||
}
|
||||
|
||||
func (r *RedisHandler) RegisterShard(shard ShardMetadata) error {
|
||||
value, err := json.Marshal(shard)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.client.SAdd(r.ctx, SHARD_SET, value).Err()
|
||||
}
|
||||
|
||||
func (r *RedisHandler) GetShards() []ShardMetadata {
|
||||
shardData := r.client.SMembers(r.ctx, SHARD_SET).Val()
|
||||
|
||||
// unmarshal all shards
|
||||
shards := make([]ShardMetadata, 0, len(shardData))
|
||||
for _, data := range shardData {
|
||||
var shard ShardMetadata
|
||||
if err := json.Unmarshal([]byte(data), &shard); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
shards = append(shards, shard)
|
||||
}
|
||||
|
||||
return shards
|
||||
}
|
Reference in New Issue
Block a user