Merge pull request #3 from xl3lackout/main

Merge several changes
This commit is contained in:
wish
2022-06-16 11:37:00 +10:00
committed by GitHub
15 changed files with 1567 additions and 955 deletions

View File

@@ -1,153 +1,156 @@
{
"host_ip": "",
"bin_path": "bin",
"devmode": true,
"devmodeoptions": {
"serverName" : "",
"cleandb": false,
"maxlauncherhr": true,
"LogOutboundMessages": false,
"Event": 0,
"OpcodeMessages": false,
"SaveDumps": {
"Enabled": true,
"OutputDir": "savedata"
}
},
"discord": {
"enabled": false,
"bottoken": "",
"channelid": ""
},
"database": {
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "",
"database": "erupe"
},
"launcher": {
"port": 80,
"UseOriginalLauncherFiles": false
},
"sign": {
"port": 53312
},
"channel": {
"port1": 54001,
"port2": 54002,
"port3": 54003,
"port4": 54004
},
"entrance": {
"port": 53310,
"entries": [
{
"name": " Server #1",
"ip": "",
"unk2": 0,
"type": 3,
"season": 3,
"unk6": 0,
"allowedclientflags": "4096",
"channels": [
{
"port": 54001,
"MaxPlayers": 100,
"CurrentPlayers": 0,
"Unk4": 0,
"Unk5": 0,
"Unk6": 0,
"Unk7": 0,
"Unk8": 0,
"Unk9": 0,
"Unk10": 319,
"Unk11": 248,
"Unk12": 159,
"Unk13": 12345
}
]
},
{
"name": " Server #2",
"ip": "",
"unk2": 0,
"type": 1,
"season": 3,
"unk6": 0,
"allowedclientflags": 0,
"channels": [
{
"port": 54002,
"MaxPlayers": 50,
"CurrentPlayers": 0,
"Unk4": 0,
"Unk5": 0,
"Unk6": 0,
"Unk7": 0,
"Unk8": 0,
"Unk9": 0,
"Unk10": 318,
"Unk11": 251,
"Unk12": 155,
"Unk13": 12345
}
]
},
{
"name": " Server #3",
"ip": "",
"unk2": 0,
"type": 2,
"season": 1,
"unk6": 0,
"allowedclientflags": 0,
"channels": [
{
"port": 54003,
"MaxPlayers": 50,
"CurrentPlayers": 0,
"Unk4": 0,
"Unk5": 0,
"Unk6": 0,
"Unk7": 0,
"Unk8": 0,
"Unk9": 0,
"Unk10": 318,
"Unk11": 251,
"Unk12": 155,
"Unk13": 12345
}
]
},
{
"name": " Server #4",
"ip": "",
"unk2": 0,
"type": 4,
"season": 0,
"unk6": 0,
"allowedclientflags": 0,
"channels": [
{
"port": 54004,
"MaxPlayers": 50,
"CurrentPlayers": 0,
"Unk4": 0,
"Unk5": 0,
"Unk6": 0,
"Unk7": 0,
"Unk8": 0,
"Unk9": 0,
"Unk10": 318,
"Unk11": 251,
"Unk12": 155,
"Unk13": 12345
}
]
}
]
}
{
"host_ip": "",
"bin_path": "bin",
"devmode": true,
"devmodeoptions": {
"serverName" : "",
"cleandb": false,
"maxlauncherhr": true,
"LogOutboundMessages": false,
"Event": 0,
"OpcodeMessages": false,
"SaveDumps": {
"Enabled": true,
"OutputDir": "savedata"
}
},
"discord": {
"enabled": false,
"bottoken": "",
"realtimeChannelID": "",
"serverId": "",
"devRoles": [],
"devMode": false
},
"database": {
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "",
"database": "erupe"
},
"launcher": {
"port": 80,
"UseOriginalLauncherFiles": false
},
"sign": {
"port": 53312
},
"channel": {
"port1": 54001,
"port2": 54002,
"port3": 54003,
"port4": 54004
},
"entrance": {
"port": 53310,
"entries": [
{
"name": " Server #1",
"ip": "",
"unk2": 0,
"type": 3,
"season": 3,
"unk6": 0,
"allowedclientflags": "4096",
"channels": [
{
"port": 54001,
"MaxPlayers": 100,
"CurrentPlayers": 0,
"Unk4": 0,
"Unk5": 0,
"Unk6": 0,
"Unk7": 0,
"Unk8": 0,
"Unk9": 0,
"Unk10": 319,
"Unk11": 248,
"Unk12": 159,
"Unk13": 12345
}
]
},
{
"name": " Server #2",
"ip": "",
"unk2": 0,
"type": 1,
"season": 3,
"unk6": 0,
"allowedclientflags": 0,
"channels": [
{
"port": 54002,
"MaxPlayers": 50,
"CurrentPlayers": 0,
"Unk4": 0,
"Unk5": 0,
"Unk6": 0,
"Unk7": 0,
"Unk8": 0,
"Unk9": 0,
"Unk10": 318,
"Unk11": 251,
"Unk12": 155,
"Unk13": 12345
}
]
},
{
"name": " Server #3",
"ip": "",
"unk2": 0,
"type": 2,
"season": 1,
"unk6": 0,
"allowedclientflags": 0,
"channels": [
{
"port": 54003,
"MaxPlayers": 50,
"CurrentPlayers": 0,
"Unk4": 0,
"Unk5": 0,
"Unk6": 0,
"Unk7": 0,
"Unk8": 0,
"Unk9": 0,
"Unk10": 318,
"Unk11": 251,
"Unk12": 155,
"Unk13": 12345
}
]
},
{
"name": " Server #4",
"ip": "",
"unk2": 0,
"type": 4,
"season": 0,
"unk6": 0,
"allowedclientflags": 0,
"channels": [
{
"port": 54004,
"MaxPlayers": 50,
"CurrentPlayers": 0,
"Unk4": 0,
"Unk5": 0,
"Unk6": 0,
"Unk7": 0,
"Unk8": 0,
"Unk9": 0,
"Unk10": 318,
"Unk11": 251,
"Unk12": 155,
"Unk13": 12345
}
]
}
]
}
}

View File

@@ -1,157 +1,159 @@
package config
import (
"log"
"net"
"github.com/spf13/viper"
)
// Config holds the global server-wide config.
type Config struct {
HostIP string `mapstructure:"host_ip"`
BinPath string `mapstructure:"bin_path"`
DevMode bool
DevModeOptions DevModeOptions
Discord Discord
Database Database
Launcher Launcher
Sign Sign
Channel Channel
Entrance Entrance
}
// DevModeOptions holds various debug/temporary options for use while developing Erupe.
type DevModeOptions struct {
ServerName string // To get specific instance server about (Current Players/Event Week)
CleanDB bool // Automatically wipes the DB on server reset.
MaxLauncherHR bool // Sets the HR returned in the launcher to HR9 so that you can join non-beginner worlds.
FixedStageID bool // Causes all move_stage to use the ID sl1Ns200p0a0u0 to get you into all stages
LogOutboundMessages bool // Log all messages sent to the clients
Event int // Changes the current event
OpcodeMessages bool // Get all message for Opcodes
SaveDumps SaveDumpOptions
}
type SaveDumpOptions struct {
Enabled bool
OutputDir string
}
// Discord holds the discord integration config.
type Discord struct {
Enabled bool
BotToken string
ChannelID string
}
// Database holds the postgres database config.
type Database struct {
Host string
Port int
User string
Password string
Database string
}
// Launcher holds the launcher server config.
type Launcher struct {
Port int
UseOriginalLauncherFiles bool
}
// Sign holds the sign server config.
type Sign struct {
Port int
}
// Channel holds the channel server config.
type Channel struct {
Port1 int
Port2 int
Port3 int
Port4 int
}
// Entrance holds the entrance server config.
type Entrance struct {
Port uint16
Entries []EntranceServerInfo
}
// EntranceServerInfo represents an entry in the serverlist.
type EntranceServerInfo struct {
IP string
Unk2 uint16
Type uint8 // Server type. 0=?, 1=open, 2=cities, 3=newbie, 4=bar
Season uint8 // Server activity. 0 = green, 1 = orange, 2 = blue
Unk6 uint8 // Something to do with server recommendation on 0, 3, and 5.
Name string // Server name, 66 byte null terminated Shift-JIS(JP) or Big5(TW).
// 4096(PC, PS3/PS4)?, 8258(PC, PS3/PS4)?, 8192 == nothing?
// THIS ONLY EXISTS IF Binary8Header.type == "SV2", NOT "SVR"!
AllowedClientFlags uint32
Channels []EntranceChannelInfo
}
// EntranceChannelInfo represents an entry in a server's channel list.
type EntranceChannelInfo struct {
Port uint16
MaxPlayers uint16
CurrentPlayers uint16
Unk4 uint16
Unk5 uint16
Unk6 uint16
Unk7 uint16
Unk8 uint16
Unk9 uint16
Unk10 uint16
Unk11 uint16
Unk12 uint16
Unk13 uint16
}
// getOutboundIP4 gets the preferred outbound ip4 of this machine
// From https://stackoverflow.com/a/37382208
func getOutboundIP4() net.IP {
conn, err := net.Dial("udp4", "8.8.8.8:80")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.To4()
}
// LoadConfig loads the given config toml file.
func LoadConfig() (*Config, error) {
viper.SetConfigName("config")
viper.AddConfigPath(".")
viper.SetDefault("DevModeOptions.SaveDumps", SaveDumpOptions{
Enabled: false,
OutputDir: "savedata",
})
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
c := &Config{}
err = viper.Unmarshal(c)
if err != nil {
return nil, err
}
if c.HostIP == "" {
c.HostIP = getOutboundIP4().To4().String()
}
return c, nil
}
package config
import (
"log"
"net"
"github.com/spf13/viper"
)
// Config holds the global server-wide config.
type Config struct {
HostIP string `mapstructure:"host_ip"`
BinPath string `mapstructure:"bin_path"`
DevMode bool
DevModeOptions DevModeOptions
Discord Discord
Database Database
Launcher Launcher
Sign Sign
Channel Channel
Entrance Entrance
}
// DevModeOptions holds various debug/temporary options for use while developing Erupe.
type DevModeOptions struct {
ServerName string // To get specific instance server about (Current Players/Event Week)
CleanDB bool // Automatically wipes the DB on server reset.
MaxLauncherHR bool // Sets the HR returned in the launcher to HR9 so that you can join non-beginner worlds.
FixedStageID bool // Causes all move_stage to use the ID sl1Ns200p0a0u0 to get you into all stages
LogOutboundMessages bool // Log all messages sent to the clients
Event int // Changes the current event
OpcodeMessages bool // Get all message for Opcodes
SaveDumps SaveDumpOptions
}
type SaveDumpOptions struct {
Enabled bool
OutputDir string
}
// Discord holds the discord integration config.
type Discord struct {
Enabled bool
BotToken string
ServerID string
RealtimeChannelID string
DevRoles []string
DevMode bool
}
// Database holds the postgres database config.
type Database struct {
Host string
Port int
User string
Password string
Database string
}
// Launcher holds the launcher server config.
type Launcher struct {
Port int
UseOriginalLauncherFiles bool
}
// Sign holds the sign server config.
type Sign struct {
Port int
}
// Channel holds the channel server config.
type Channel struct {
Port1 int
Port2 int
Port3 int
Port4 int
}
// Entrance holds the entrance server config.
type Entrance struct {
Port uint16
Entries []EntranceServerInfo
}
// EntranceServerInfo represents an entry in the serverlist.
type EntranceServerInfo struct {
IP string
Unk2 uint16
Type uint8 // Server type. 0=?, 1=open, 2=cities, 3=newbie, 4=bar
Season uint8 // Server activity. 0 = green, 1 = orange, 2 = blue
Unk6 uint8 // Something to do with server recommendation on 0, 3, and 5.
Name string // Server name, 66 byte null terminated Shift-JIS(JP) or Big5(TW).
// 4096(PC, PS3/PS4)?, 8258(PC, PS3/PS4)?, 8192 == nothing?
// THIS ONLY EXISTS IF Binary8Header.type == "SV2", NOT "SVR"!
AllowedClientFlags uint32
Channels []EntranceChannelInfo
}
// EntranceChannelInfo represents an entry in a server's channel list.
type EntranceChannelInfo struct {
Port uint16
MaxPlayers uint16
CurrentPlayers uint16
Unk4 uint16
Unk5 uint16
Unk6 uint16
Unk7 uint16
Unk8 uint16
Unk9 uint16
Unk10 uint16
Unk11 uint16
Unk12 uint16
Unk13 uint16
}
// getOutboundIP4 gets the preferred outbound ip4 of this machine
// From https://stackoverflow.com/a/37382208
func getOutboundIP4() net.IP {
conn, err := net.Dial("udp4", "8.8.8.8:80")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.To4()
}
// LoadConfig loads the given config toml file.
func LoadConfig() (*Config, error) {
viper.SetConfigName("config")
viper.AddConfigPath(".")
viper.SetDefault("DevModeOptions.SaveDumps", SaveDumpOptions{
Enabled: false,
OutputDir: "savedata",
})
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
c := &Config{}
err = viper.Unmarshal(c)
if err != nil {
return nil, err
}
if c.HostIP == "" {
c.HostIP = getOutboundIP4().To4().String()
}
return c, nil
}

26
Erupe/distitem.sql Normal file
View File

@@ -0,0 +1,26 @@
BEGIN;
CREATE TABLE public.distribution
(
id serial NOT NULL PRIMARY KEY,
character_id int,
type int NOT NULL,
deadline timestamp without time zone,
event_name text NOT NULL DEFAULT 'GM Gift!',
description text NOT NULL DEFAULT '~C05You received a gift!',
times_acceptable int NOT NULL DEFAULT 1,
min_hr int NOT NULL DEFAULT 65535,
max_hr int NOT NULL DEFAULT 65535,
min_sr int NOT NULL DEFAULT 65535,
max_sr int NOT NULL DEFAULT 65535,
min_gr int NOT NULL DEFAULT 65535,
max_gr int NOT NULL DEFAULT 65535,
data bytea NOT NULL
);
CREATE TABLE public.distributions_accepted
(
distribution_id int,
character_id int
);
END;

View File

@@ -1,177 +1,215 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/Solenataris/Erupe/config"
"github.com/Solenataris/Erupe/server/channelserver"
"github.com/Solenataris/Erupe/server/entranceserver"
"github.com/Solenataris/Erupe/server/launcherserver"
"github.com/Solenataris/Erupe/server/signserver"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"go.uber.org/zap"
)
// Temporary DB auto clean on startup for quick development & testing.
func cleanDB(db *sqlx.DB) {
_ = db.MustExec("DELETE FROM guild_characters")
_ = db.MustExec("DELETE FROM guilds")
_ = db.MustExec("DELETE FROM characters")
_ = db.MustExec("DELETE FROM sign_sessions")
_ = db.MustExec("DELETE FROM users")
}
func main() {
zapLogger, _ := zap.NewDevelopment()
defer zapLogger.Sync()
logger := zapLogger.Named("main")
logger.Info("Starting Erupe")
// Load the configuration.
erupeConfig, err := config.LoadConfig()
if err != nil {
logger.Fatal("Failed to load config", zap.Error(err))
}
// Create the postgres DB pool.
connectString := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname= %s sslmode=disable",
erupeConfig.Database.Host,
erupeConfig.Database.Port,
erupeConfig.Database.User,
erupeConfig.Database.Password,
erupeConfig.Database.Database,
)
db, err := sqlx.Open("postgres", connectString)
if err != nil {
logger.Fatal("Failed to open sql database", zap.Error(err))
}
// Test the DB connection.
err = db.Ping()
if err != nil {
logger.Fatal("Failed to ping database", zap.Error(err))
}
logger.Info("Connected to database")
// Clean the DB if the option is on.
if erupeConfig.DevMode && erupeConfig.DevModeOptions.CleanDB {
logger.Info("Cleaning DB")
cleanDB(db)
logger.Info("Done cleaning DB")
}
// Now start our server(s).
// Launcher HTTP server.
launcherServer := launcherserver.NewServer(
&launcherserver.Config{
Logger: logger.Named("launcher"),
ErupeConfig: erupeConfig,
DB: db,
UseOriginalLauncherFiles: erupeConfig.Launcher.UseOriginalLauncherFiles,
})
err = launcherServer.Start()
if err != nil {
logger.Fatal("Failed to start launcher server", zap.Error(err))
}
logger.Info("Started launcher server.")
// Entrance server.
entranceServer := entranceserver.NewServer(
&entranceserver.Config{
Logger: logger.Named("entrance"),
ErupeConfig: erupeConfig,
DB: db,
})
err = entranceServer.Start()
if err != nil {
logger.Fatal("Failed to start entrance server", zap.Error(err))
}
logger.Info("Started entrance server.")
// Sign server.
signServer := signserver.NewServer(
&signserver.Config{
Logger: logger.Named("sign"),
ErupeConfig: erupeConfig,
DB: db,
})
err = signServer.Start()
if err != nil {
logger.Fatal("Failed to start sign server", zap.Error(err))
}
logger.Info("Started sign server.")
// Channel Server
channelServer1 := channelserver.NewServer(
&channelserver.Config{
Logger: logger.Named("channel"),
ErupeConfig: erupeConfig,
DB: db,
})
err = channelServer1.Start(erupeConfig.Channel.Port1)
if err != nil {
logger.Fatal("Failed to start channel server1", zap.Error(err))
}
logger.Info("Started channel server.")
// Channel Server
channelServer2 := channelserver.NewServer(
&channelserver.Config{
Logger: logger.Named("channel"),
ErupeConfig: erupeConfig,
DB: db,
})
err = channelServer2.Start(erupeConfig.Channel.Port2)
if err != nil {
logger.Fatal("Failed to start channel server2", zap.Error(err))
}
// Channel Server
channelServer3 := channelserver.NewServer(
&channelserver.Config{
Logger: logger.Named("channel"),
ErupeConfig: erupeConfig,
DB: db,
})
err = channelServer3.Start(erupeConfig.Channel.Port3)
if err != nil {
logger.Fatal("Failed to start channel server3", zap.Error(err))
}
// Channel Server
channelServer4 := channelserver.NewServer(
&channelserver.Config{
Logger: logger.Named("channel"),
ErupeConfig: erupeConfig,
DB: db,
})
err = channelServer4.Start(erupeConfig.Channel.Port4)
if err != nil {
logger.Fatal("Failed to start channel server4", zap.Error(err))
}
// Wait for exit or interrupt with ctrl+C.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
logger.Info("Trying to shutdown gracefully.")
channelServer4.Shutdown()
channelServer3.Shutdown()
channelServer2.Shutdown()
channelServer1.Shutdown()
signServer.Shutdown()
entranceServer.Shutdown()
launcherServer.Shutdown()
time.Sleep(1 * time.Second)
}
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/Solenataris/Erupe/config"
"github.com/Solenataris/Erupe/server/channelserver"
"github.com/Solenataris/Erupe/server/discordbot"
"github.com/Solenataris/Erupe/server/entranceserver"
"github.com/Solenataris/Erupe/server/launcherserver"
"github.com/Solenataris/Erupe/server/signserver"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"go.uber.org/zap"
)
// Temporary DB auto clean on startup for quick development & testing.
func cleanDB(db *sqlx.DB) {
_ = db.MustExec("DELETE FROM guild_characters")
_ = db.MustExec("DELETE FROM guilds")
_ = db.MustExec("DELETE FROM characters")
_ = db.MustExec("DELETE FROM sign_sessions")
_ = db.MustExec("DELETE FROM users")
}
func main() {
zapLogger, _ := zap.NewDevelopment()
defer zapLogger.Sync()
logger := zapLogger.Named("main")
logger.Info("Starting Erupe")
// Load the configuration.
erupeConfig, err := config.LoadConfig()
if err != nil {
logger.Fatal("Failed to load config", zap.Error(err))
}
// Discord bot
var discordBot *discordbot.DiscordBot = nil
if erupeConfig.Discord.Enabled {
bot, err := discordbot.NewDiscordBot(discordbot.DiscordBotOptions{
Logger: logger,
Config: erupeConfig,
})
if err != nil {
logger.Fatal("Failed to create discord bot", zap.Error(err))
}
// Discord bot
err = bot.Start()
if err != nil {
logger.Fatal("Failed to starts discord bot", zap.Error(err))
}
discordBot = bot
} else {
logger.Info("Discord bot is disabled")
}
// Create the postgres DB pool.
connectString := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname= %s sslmode=disable",
erupeConfig.Database.Host,
erupeConfig.Database.Port,
erupeConfig.Database.User,
erupeConfig.Database.Password,
erupeConfig.Database.Database,
)
db, err := sqlx.Open("postgres", connectString)
if err != nil {
logger.Fatal("Failed to open sql database", zap.Error(err))
}
// Test the DB connection.
err = db.Ping()
if err != nil {
logger.Fatal("Failed to ping database", zap.Error(err))
}
logger.Info("Connected to database")
// Clean the DB if the option is on.
if erupeConfig.DevMode && erupeConfig.DevModeOptions.CleanDB {
logger.Info("Cleaning DB")
cleanDB(db)
logger.Info("Done cleaning DB")
}
// Now start our server(s).
// Launcher HTTP server.
launcherServer := launcherserver.NewServer(
&launcherserver.Config{
Logger: logger.Named("launcher"),
ErupeConfig: erupeConfig,
DB: db,
UseOriginalLauncherFiles: erupeConfig.Launcher.UseOriginalLauncherFiles,
})
err = launcherServer.Start()
if err != nil {
logger.Fatal("Failed to start launcher server", zap.Error(err))
}
logger.Info("Started launcher server.")
// Entrance server.
entranceServer := entranceserver.NewServer(
&entranceserver.Config{
Logger: logger.Named("entrance"),
ErupeConfig: erupeConfig,
DB: db,
})
err = entranceServer.Start()
if err != nil {
logger.Fatal("Failed to start entrance server", zap.Error(err))
}
logger.Info("Started entrance server.")
// Sign server.
signServer := signserver.NewServer(
&signserver.Config{
Logger: logger.Named("sign"),
ErupeConfig: erupeConfig,
DB: db,
})
err = signServer.Start()
if err != nil {
logger.Fatal("Failed to start sign server", zap.Error(err))
}
logger.Info("Started sign server.")
// Channel Server
channelServer1 := channelserver.NewServer(
&channelserver.Config{
Logger: logger.Named("channel"),
ErupeConfig: erupeConfig,
DB: db,
Name: erupeConfig.Entrance.Entries[0].Name,
Enable: erupeConfig.Entrance.Entries[0].Channels[0].MaxPlayers > 0,
//DiscordBot: discordBot,
})
err = channelServer1.Start(erupeConfig.Channel.Port1)
if err != nil {
logger.Fatal("Failed to start channel server1", zap.Error(err))
}
logger.Info("Started channel server.")
// Channel Server
channelServer2 := channelserver.NewServer(
&channelserver.Config{
Logger: logger.Named("channel"),
ErupeConfig: erupeConfig,
DB: db,
Name: erupeConfig.Entrance.Entries[1].Name,
Enable: erupeConfig.Entrance.Entries[1].Channels[0].MaxPlayers > 0,
DiscordBot: discordBot,
})
err = channelServer2.Start(erupeConfig.Channel.Port2)
if err != nil {
logger.Fatal("Failed to start channel server2", zap.Error(err))
}
// Channel Server
channelServer3 := channelserver.NewServer(
&channelserver.Config{
Logger: logger.Named("channel"),
ErupeConfig: erupeConfig,
DB: db,
Name: erupeConfig.Entrance.Entries[2].Name,
Enable: erupeConfig.Entrance.Entries[2].Channels[0].MaxPlayers > 0,
//DiscordBot: discordBot,
})
err = channelServer3.Start(erupeConfig.Channel.Port3)
if err != nil {
logger.Fatal("Failed to start channel server3", zap.Error(err))
}
// Channel Server
channelServer4 := channelserver.NewServer(
&channelserver.Config{
Logger: logger.Named("channel"),
ErupeConfig: erupeConfig,
DB: db,
Name: erupeConfig.Entrance.Entries[3].Name,
Enable: erupeConfig.Entrance.Entries[3].Channels[0].MaxPlayers > 0,
//DiscordBot: discordBot,
})
err = channelServer4.Start(erupeConfig.Channel.Port4)
if err != nil {
logger.Fatal("Failed to start channel server4", zap.Error(err))
}
// Wait for exit or interrupt with ctrl+C.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
logger.Info("Trying to shutdown gracefully.")
channelServer4.Shutdown()
channelServer3.Shutdown()
channelServer2.Shutdown()
channelServer1.Shutdown()
signServer.Shutdown()
entranceServer.Shutdown()
launcherServer.Shutdown()
time.Sleep(1 * time.Second)
}

View File

@@ -38,4 +38,4 @@ func (m *MsgMhfApplyDistItem) Build(bf *byteframe.ByteFrame, ctx *clientctx.Clie
bf.WriteUint32(m.Unk2)
bf.WriteUint32(m.Unk3)
return nil
}
}

View File

@@ -30,4 +30,4 @@ func (m *MsgMhfGetDistDescription) Parse(bf *byteframe.ByteFrame, ctx *clientctx
// Build builds a binary packet from the current data.
func (m *MsgMhfGetDistDescription) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
return errors.New("NOT IMPLEMENTED")
}
}

View File

@@ -0,0 +1,39 @@
BEGIN;
CREATE TABLE IF NOT EXISTS public.normal_shop_items
(
shoptype integer,
shopid integer,
itemhash integer not null,
itemid integer,
points integer,
tradequantity integer,
rankreqlow integer,
rankreqhigh integer,
rankreqg integer,
storelevelreq integer,
maximumquantity integer,
boughtquantity integer,
roadfloorsrequired integer,
weeklyfataliskills integer,
enable_weeks character varying(8)
);
ALTER TABLE IF EXISTS public.normal_shop_items
(
ADD COLUMN enable_weeks character varying(8)
);
CREATE TABLE IF NOT EXISTS public.shop_item_state
(
char_id bigint REFERENCES characters (id),
itemhash int UNIQUE NOT NULL,
usedquantity int,
week int
);
ALTER TABLE IF EXISTS public.shop_item_state
(
ADD COLUMN week int
);
END;

View File

@@ -143,9 +143,8 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
fmt.Printf("Got chat message: %+v\n", chatMessage)
// Discord integration
if s.server.erupeConfig.Discord.Enabled {
message := fmt.Sprintf("%s: %s", chatMessage.SenderName, chatMessage.Message)
s.server.discordSession.ChannelMessageSend(s.server.erupeConfig.Discord.ChannelID, message)
if chatMessage.Type == binpacket.ChatTypeLocal || chatMessage.Type == binpacket.ChatTypeParty {
s.server.DiscordChannelSend(chatMessage.SenderName, chatMessage.Message)
}
// RAVI COMMANDS V2

View File

@@ -2,162 +2,344 @@ package channelserver
import (
"fmt"
"os"
"sort"
"strings"
"time"
"github.com/bwmarrin/discordgo"
)
// onDiscordMessage handles receiving messages from discord and forwarding them ingame.
func (s *Server) onDiscordMessage(ds *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore messages from our bot, or ones that are not in the correct channel.
if m.Author.ID == ds.State.User.ID || m.ChannelID != s.erupeConfig.Discord.ChannelID {
return
}
// Broadcast to the game clients.
data := m.Content
// Split on comma.
result := strings.Split(data, " ")
if result[0] == "!maintenancedate" && m.Author.ID == "836027554628370492" {
replaceDays := dayConvert(result[1])
replaceMonth := MonthConvert(result[3])
s.BroadcastChatMessage("MAINTENANCE EXCEPTIONNELLE :")
s.BroadcastChatMessage("Les serveurs seront temporairement inaccessibles le")
s.BroadcastChatMessage(fmt.Sprintf("%s %s %s %s a partir de %s heures et %s minutes.", replaceDays, result[2], replaceMonth, result[4], result[5], result[6])) // Jour Mois Année Heures Minutes
s.BroadcastChatMessage("Evitez de vous connecter durant cette periode. Veuillez nous")
s.BroadcastChatMessage("excuser pour la gene occasionee. Merci de votre cooperation.")
return
} else if result[0] == "!maintenance" && m.Author.ID == "836027554628370492" {
s.BroadcastChatMessage("RAPPEL DE MAINTENANCE DU MARDI (18H-22H): Les serveurs seront")
s.BroadcastChatMessage("temporairement inaccessibles dans 15 minutes. Veuillez ne pas")
s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas")
s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous")
s.BroadcastChatMessage("excuser pour la gene occasionnee. Merci de votre cooperation.")
s.TimerUpdate(15, 0, true)
return
} else if result[0] == "!Rmaintenancedate" && m.Author.ID == "836027554628370492" {
s.BroadcastChatMessage("RAPPEL DE MAINTENANCE EXCEPTIONNELLE: Les serveurs seront")
s.BroadcastChatMessage("temporairement inaccessibles dans 15 minutes. Veuillez ne pas")
s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas")
s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous")
s.BroadcastChatMessage("excuser pour la gene occasionnee. Merci de votre cooperation.")
s.TimerUpdate(15, 1, true)
return
} else if result[0] == "!maintenanceStop" && m.Author.ID == "836027554628370492" {
s.BroadcastChatMessage("INFORMATION: A titre exceptionnel, il n'y aura pas de")
s.BroadcastChatMessage("maintenance de 18h a 22h, vous pouvez continuer de jouer")
s.BroadcastChatMessage("librement jusqu'a la prochaine annonce de maintenance !")
s.BroadcastChatMessage("Bonne chasse !")
s.TimerUpdate(0, 0, false)
return
} else if result[0] == "!maintenanceStopExec" && m.Author.ID == "836027554628370492" {
replaceDays := dayConvert(result[1])
replaceMonth := MonthConvert(result[3])
s.BroadcastChatMessage("INFORMATION: A titre exceptionnel, il n'y aura pas de")
s.BroadcastChatMessage(fmt.Sprintf("maintenance le %s %s %s %s a partir de", replaceDays, result[2], replaceMonth, result[4])) // Jour Mois Année
s.BroadcastChatMessage(fmt.Sprintf("%s heures et %s minutes, vous pouvez continuer de jouer", result[5], result[6])) // Heures Minutes
s.BroadcastChatMessage("librement jusqu'a la prochaine annonce de maintenance !")
s.BroadcastChatMessage("Bonne chasse !")
s.TimerUpdate(0, 1, false)
return
}
message := fmt.Sprintf("[DISCORD] %s: %s", m.Author.Username, m.Content)
s.BroadcastChatMessage(message)
type Character struct {
ID uint32 `db:"id"`
IsFemale bool `db:"is_female"`
IsNewCharacter bool `db:"is_new_character"`
Name string `db:"name"`
UnkDescString string `db:"unk_desc_string"`
HRP uint16 `db:"hrp"`
GR uint16 `db:"gr"`
WeaponType uint16 `db:"weapon_type"`
LastLogin uint32 `db:"last_login"`
}
func dayConvert(result string) string {
var replaceDays string
var weapons = []string{
"<:gs:970861408227049477>",
"<:hbg:970861408281563206>",
"<:hm:970861408239628308>",
"<:lc:970861408298352660>",
"<:sns:970861408319315988>",
"<:lbg:970861408327725137>",
"<:ds:970861408277368883>",
"<:ls:970861408319311872>",
"<:hh:970861408222863400>",
"<:gl:970861408327720980>",
"<:bw:970861408294174780>",
"<:tf:970861408424177744>",
"<:sw:970861408340283472>",
"<:ms:970861408411594842>",
}
if result == "1" {
replaceDays = "Lundi"
} else if result == "2" {
replaceDays = "Mardi"
} else if result == "3" {
replaceDays = "Mercredi"
} else if result == "4" {
replaceDays = "Jeudi"
} else if result == "5" {
replaceDays = "Vendredi"
} else if result == "6" {
replaceDays = "Samedi"
} else if result == "7" {
replaceDays = "Dimanche"
func (s *Server) getCharacterForUser(uid int) (*Character, error) {
character := Character{}
err := s.db.Get(&character, "SELECT id, is_female, is_new_character, name, unk_desc_string, hrp, gr, weapon_type, last_login FROM characters WHERE id = $1", uid)
if err != nil {
return nil, err
}
return &character, nil
}
func CountChars(s *Server) string {
count := 0
for _, stage := range s.stages {
count += len(stage.clients)
}
message := fmt.Sprintf("Server [%s]: %d players;", s.name, count)
return message
}
type ListPlayer struct {
CharName string
InQuest bool
WeaponEmoji string
QuestEmoji string
StageName string
}
func (p *ListPlayer) toString(length int) string {
status := ""
if p.InQuest {
status = "(in quest " + p.QuestEmoji + ")"
} else {
replaceDays = "NULL"
status = p.StageName
}
return replaceDays
missingSpace := length - len(p.CharName)
whitespaces := strings.Repeat(" ", missingSpace+5)
return fmt.Sprintf("%s %s %s %s", p.WeaponEmoji, p.CharName, whitespaces, status)
}
func MonthConvert(result string) string {
var replaceMonth string
if result == "01" {
replaceMonth = "Janvier"
} else if result == "02" {
replaceMonth = "Fevrier"
} else if result == "03" {
replaceMonth = "Mars"
} else if result == "04" {
replaceMonth = "Avril"
} else if result == "05" {
replaceMonth = "Mai"
} else if result == "06" {
replaceMonth = "Juin"
} else if result == "07" {
replaceMonth = "Juillet"
} else if result == "08" {
replaceMonth = "Aout"
} else if result == "09" {
replaceMonth = "Septembre"
} else if result == "10" {
replaceMonth = "Octobre"
} else if result == "11" {
replaceMonth = "Novembre"
} else if result == "12" {
replaceMonth = "Decembre"
} else {
replaceMonth = "NULL"
func getPlayerList(s *Server) ([]ListPlayer, int) {
list := []ListPlayer{}
questEmojis := []string{
":white_circle:",
":red_circle:",
":blue_circle:",
":brown_circle:",
":green_circle:",
":purple_circle:",
":yellow_circle:",
":orange_circle:",
}
return replaceMonth
}
bigNameLen := 0
func (s *Server) TimerUpdate(timer int, typeStop int, disableAutoOff bool) {
timertotal := 0
for timer > 0 {
time.Sleep(1 * time.Minute)
timer -= 1
timertotal += 1
if disableAutoOff {
// Un message s'affiche toutes les 10 minutes pour prévenir de la maintenance.
if timertotal == 10 {
timertotal = 0
if typeStop == 0 {
s.BroadcastChatMessage("RAPPEL DE MAINTENANCE DU MARDI (18H-22H): Les serveurs seront")
s.BroadcastChatMessage(fmt.Sprintf("temporairement inaccessibles dans %d minutes. Veuillez ne pas", timer))
s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas")
s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous excuser")
s.BroadcastChatMessage("pour la gene occasionnee. Merci de votre cooperation.")
} else {
s.BroadcastChatMessage("RAPPEL DE MAINTENANCE EXCEPTIONNELLE: Les serveurs seront")
s.BroadcastChatMessage(fmt.Sprintf("temporairement inaccessibles dans %d minutes. Veuillez ne pas", timer))
s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas")
s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous excuser")
s.BroadcastChatMessage("pour la gene occasionnee. Merci de votre cooperation.")
for _, stage := range s.stages {
if len(stage.clients) == 0 {
continue
}
questEmoji := ":black_circle:"
if len(questEmojis) > 0 {
questEmoji = questEmojis[len(questEmojis)-1]
questEmojis = questEmojis[:len(questEmojis)-1]
}
isQuest := stage.isQuest()
for client := range stage.clients {
char, err := s.getCharacterForUser(int(client.charID))
if err == nil {
if len(char.Name) > bigNameLen {
bigNameLen = len(char.Name)
}
}
// Déconnecter tous les joueurs du serveur.
if timer == 0 {
os.Exit(-1)
list = append(list, ListPlayer{
CharName: char.Name,
InQuest: isQuest,
QuestEmoji: questEmoji,
WeaponEmoji: weapons[char.WeaponType],
StageName: stage.GetName(),
})
}
}
}
return list, bigNameLen
}
func PlayerList(s *Server) string {
list := ""
count := 0
listPlayers, bigNameLen := getPlayerList(s)
sort.SliceStable(listPlayers, func(i, j int) bool {
return listPlayers[i].CharName < listPlayers[j].CharName
})
for _, lp := range listPlayers {
list += lp.toString(bigNameLen) + "\n"
count++
}
message := fmt.Sprintf("<:SnS:822963937360347148> Frontier Hunters Online: [%s ] <:switcha:822963906401533992> \n============== Total %d =============\n", s.name, count)
message += list
return message
}
func debug(s *Server) string {
list := ""
for _, stage := range s.stages {
if !stage.isQuest() && len(stage.objects) == 0 {
continue
}
list += fmt.Sprintf(" -> Stage: %s StageId: %s\n", stage.GetName(), stage.id)
isQuest := "false"
hasDeparted := "false"
if stage.isQuest() {
isQuest = "true"
}
list += fmt.Sprintf(" '-> isQuest: %s\n", isQuest)
if stage.isQuest() {
if stage.hasDeparted {
hasDeparted = "true"
}
list += fmt.Sprintf(" '-> isDeparted: %s\n", hasDeparted)
list += fmt.Sprintf(" '-> reserveSlots (%d/%d)\n", len(stage.reservedClientSlots), stage.maxPlayers)
for charid, _ := range stage.reservedClientSlots {
char, err := s.getCharacterForUser(int(charid))
if err == nil {
list += fmt.Sprintf(" '-> %s\n", char.Name)
}
}
}
list += " '-> objects: \n"
for _, obj := range stage.objects {
objInfo := fmt.Sprintf("X,Y,Z: %f %f %f", obj.x, obj.y, obj.z)
list += fmt.Sprintf(" '-> ObjectId: %d - %s\n", obj.id, objInfo)
}
}
message := fmt.Sprintf("Objects in Server: [%s ]\n", s.name)
message += list
return message
}
func questlist(s *Server) string {
list := ""
for _, stage := range s.stages {
if !stage.isQuest() {
continue
}
hasDeparted := ""
if stage.hasDeparted {
hasDeparted = " - departed"
}
list += fmt.Sprintf(" '-> StageId: %s (%d/%d) %s - %s\n", stage.id, len(stage.reservedClientSlots), stage.maxPlayers, hasDeparted, stage.createdAt)
for charid, _ := range stage.reservedClientSlots {
char, err := s.getCharacterForUser(int(charid))
if err == nil {
list += fmt.Sprintf(" '-> %s\n", char.Name)
}
}
}
message := fmt.Sprintf("Quests in Server: [%s ]\n", s.name)
message += list
return message
}
func removeStageById(s *Server, stageId string) string {
if s.stages[stageId] != nil {
delete(s.stages, stageId)
return "Stage deleted!"
}
return "Stage not found!"
}
func cleanStr(str string) string {
return strings.ToLower(strings.Trim(str, " "))
}
func getCharInfo(server *Server, charName string) string {
var s *Stage
var c *Session
for _, stage := range server.stages {
for client := range stage.clients {
if client.Name == "" {
continue
}
if cleanStr(client.Name) == cleanStr(charName) {
s = stage
c = client
}
}
}
if s == nil {
return "Character not found"
}
objInfo := ""
obj := server.FindStageObjectByChar(c.charID)
// server.logger.Info("Found object: %+v", zap.Object("obj", obj))
if obj != nil {
objInfo = fmt.Sprintf("X,Y,Z: %f %f %f", obj.x, obj.y, obj.z)
}
return fmt.Sprintf("Character: %s\nStage: %s\nStageId: %s\n%s", c.Name, s.GetName(), s.id, objInfo)
}
func (s *Server) isDiscordAdmin(ds *discordgo.Session, m *discordgo.MessageCreate) bool {
for _, role := range m.Member.Roles {
for _, id := range s.erupeConfig.Discord.DevRoles {
if id == role {
return true
}
}
}
return false
}
// onDiscordMessage handles receiving messages from discord and forwarding them ingame.
func (s *Server) onDiscordMessage(ds *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore messages from our bot, or ones that are not in the correct channel.
if m.Author.ID == ds.State.User.ID || !s.enable {
return
}
// Ignore other channels in devMode
if s.erupeConfig.Discord.DevMode && m.ChannelID != s.erupeConfig.Discord.RealtimeChannelID {
return
}
args := strings.Split(m.Content, " ")
commandName := args[0]
// Move to slash commadns
if commandName == "!players" {
ds.ChannelMessageSend(m.ChannelID, PlayerList(s))
return
}
if commandName == "-char" {
if len(args) < 2 {
ds.ChannelMessageSend(m.ChannelID, "Usage: !char <char name>")
return
}
charName := strings.Join(args[1:], " ")
ds.ChannelMessageSend(m.ChannelID, getCharInfo(s, charName))
return
}
if commandName == "!debug" && s.isDiscordAdmin(ds, m) {
ds.ChannelMessageSend(m.ChannelID, debug(s))
return
}
if commandName == "!questlist" && s.isDiscordAdmin(ds, m) {
ds.ChannelMessageSend(m.ChannelID, questlist(s))
return
}
if commandName == "!remove-stage" && s.isDiscordAdmin(ds, m) {
if len(args) < 2 {
ds.ChannelMessageSend(m.ChannelID, "Usage: !remove-stage <stage id>")
return
}
stageId := strings.Join(args[1:], " ")
ds.ChannelMessageSend(m.ChannelID, removeStageById(s, stageId))
return
}
if m.ChannelID == s.erupeConfig.Discord.RealtimeChannelID {
message := fmt.Sprintf("[DISCORD] %s: %s", m.Author.Username, m.Content)
s.BroadcastChatMessage(s.discordBot.NormalizeDiscordMessage(message))
}
}

View File

@@ -26,7 +26,6 @@ type ItemDist struct {
func handleMsgMhfEnumerateDistItem(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateDistItem)
bf := byteframe.NewByteFrame()
distCount := 0
dists, err := s.server.db.Queryx(`
SELECT d.id, event_name, description, times_acceptable,
@@ -55,7 +54,6 @@ func handleMsgMhfEnumerateDistItem(s *Session, p mhfpacket.MHFPacket) {
if err != nil {
s.logger.Error("Error parsing item distribution data", zap.Error(err))
}
bf.WriteUint32(distData.ID)
bf.WriteUint32(distData.Deadline)
bf.WriteUint32(0) // Unk
@@ -71,8 +69,8 @@ func handleMsgMhfEnumerateDistItem(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint32(0) // Unk
bf.WriteUint32(0) // Unk
eventName, _ := stringsupport.ConvertUTF8ToShiftJIS(distData.EventName)
bf.WriteUint16(uint16(len(eventName)))
bf.WriteBytes(eventName)
bf.WriteUint16(uint16(len(eventName)+1))
bf.WriteNullTerminatedBytes(eventName)
bf.WriteBytes(make([]byte, 391))
}
resp := byteframe.NewByteFrame()

View File

@@ -1,6 +1,7 @@
package channelserver
import (
"bytes"
"database/sql"
"database/sql/driver"
"encoding/binary"
@@ -8,10 +9,13 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"sort"
"time"
"strings"
"strconv"
"strings"
"time"
"github.com/Andoryuuta/byteframe"
"github.com/Solenataris/Erupe/common/bfutil"
@@ -19,6 +23,8 @@ import (
"github.com/Solenataris/Erupe/network/mhfpacket"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
"golang.org/x/text/encoding/japanese"
"golang.org/x/text/transform"
)
type FestivalColour string
@@ -611,124 +617,124 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
bf := byteframe.NewByteFrame()
switch pkt.Action {
case mhfpacket.OPERATE_GUILD_DISBAND:
if guild.LeaderCharID != s.charID {
s.logger.Warn(fmt.Sprintf("character '%d' is attempting to manage guild '%d' without permission", s.charID, guild.ID))
return
}
err = guild.Disband(s)
response := 0x01
if err != nil {
// All successful acks return 0x01, assuming 0x00 is failure
response = 0x00
}
bf.WriteUint32(uint32(response))
case mhfpacket.OPERATE_GUILD_APPLY:
err = guild.CreateApplication(s, s.charID, GuildApplicationTypeApplied, nil)
if err != nil {
// All successful acks return 0x01, assuming 0x00 is failure
bf.WriteUint32(0x00)
} else {
bf.WriteUint32(guild.LeaderCharID)
}
case mhfpacket.OPERATE_GUILD_LEAVE:
var err error
if characterGuildInfo.IsApplicant {
err = guild.RejectApplication(s, s.charID)
} else {
err = guild.RemoveCharacter(s, s.charID)
}
response := 0x01
if err != nil {
// All successful acks return 0x01, assuming 0x00 is failure
response = 0x00
}
bf.WriteUint32(uint32(response))
case mhfpacket.OPERATE_GUILD_DONATE_RANK:
handleDonateRP(s, pkt, bf, guild, false)
case mhfpacket.OPERATE_GUILD_SET_APPLICATION_DENY:
// TODO: close applications for guild
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
case mhfpacket.OPERATE_GUILD_DISBAND:
if guild.LeaderCharID != s.charID {
s.logger.Warn(fmt.Sprintf("character '%d' is attempting to manage guild '%d' without permission", s.charID, guild.ID))
return
case mhfpacket.OPERATE_GUILD_SET_APPLICATION_ALLOW:
// TODO: open applications for guild
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_SET_AVOID_LEADERSHIP_TRUE:
handleAvoidLeadershipUpdate(s, pkt, true)
case mhfpacket.OPERATE_GUILD_SET_AVOID_LEADERSHIP_FALSE:
handleAvoidLeadershipUpdate(s, pkt, false)
case mhfpacket.OPERATE_GUILD_UPDATE_COMMENT:
pbf := byteframe.NewByteFrameFromBytes(pkt.UnkData)
}
if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
err = guild.Disband(s)
response := 0x01
commentLength := pbf.ReadUint8()
_ = pbf.ReadUint32()
if err != nil {
// All successful acks return 0x01, assuming 0x00 is failure
response = 0x00
}
guild.Comment, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(pbf.ReadBytes(uint(commentLength))))
if err != nil {
s.logger.Warn("failed to convert guild comment to UTF8", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
break
}
err = guild.Save(s)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
bf.WriteUint32(uint32(response))
case mhfpacket.OPERATE_GUILD_APPLY:
err = guild.CreateApplication(s, s.charID, GuildApplicationTypeApplied, nil)
if err != nil {
// All successful acks return 0x01, assuming 0x00 is failure
bf.WriteUint32(0x00)
case mhfpacket.OPERATE_GUILD_UPDATE_MOTTO:
if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
} else {
bf.WriteUint32(guild.LeaderCharID)
}
case mhfpacket.OPERATE_GUILD_LEAVE:
var err error
guild.SubMotto = pkt.UnkData[3]
guild.MainMotto = pkt.UnkData[4]
if characterGuildInfo.IsApplicant {
err = guild.RejectApplication(s, s.charID)
} else {
err = guild.RemoveCharacter(s, s.charID)
}
err := guild.Save(s)
response := 0x01
if err != nil {
// All successful acks return 0x01, assuming 0x00 is failure
response = 0x00
}
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
case mhfpacket.OPERATE_GUILD_RENAME_PUGI_1:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
bf.WriteUint32(uint32(response))
case mhfpacket.OPERATE_GUILD_DONATE_RANK:
handleDonateRP(s, pkt, bf, guild, false)
case mhfpacket.OPERATE_GUILD_SET_APPLICATION_DENY:
// TODO: close applications for guild
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_SET_APPLICATION_ALLOW:
// TODO: open applications for guild
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_SET_AVOID_LEADERSHIP_TRUE:
handleAvoidLeadershipUpdate(s, pkt, true)
case mhfpacket.OPERATE_GUILD_SET_AVOID_LEADERSHIP_FALSE:
handleAvoidLeadershipUpdate(s, pkt, false)
case mhfpacket.OPERATE_GUILD_UPDATE_COMMENT:
pbf := byteframe.NewByteFrameFromBytes(pkt.UnkData)
if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_RENAME_PUGI_2:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
commentLength := pbf.ReadUint8()
_ = pbf.ReadUint32()
guild.Comment, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(pbf.ReadBytes(uint(commentLength))))
if err != nil {
s.logger.Warn("failed to convert guild comment to UTF8", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
break
}
err = guild.Save(s)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_RENAME_PUGI_3:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
bf.WriteUint32(0x00)
case mhfpacket.OPERATE_GUILD_UPDATE_MOTTO:
if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_1:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
guild.SubMotto = pkt.UnkData[3]
guild.MainMotto = pkt.UnkData[4]
err := guild.Save(s)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_2:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_3:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_DONATE_EVENT:
handleDonateRP(s, pkt, bf, guild, true)
default:
panic(fmt.Sprintf("unhandled operate guild action '%d'", pkt.Action))
}
case mhfpacket.OPERATE_GUILD_RENAME_PUGI_1:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_RENAME_PUGI_2:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_RENAME_PUGI_3:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_1:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_2:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_CHANGE_PUGI_3:
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
case mhfpacket.OPERATE_GUILD_DONATE_EVENT:
handleDonateRP(s, pkt, bf, guild, true)
default:
panic(fmt.Sprintf("unhandled operate guild action '%d'", pkt.Action))
}
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
@@ -914,8 +920,8 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint32(guild.RankRP)
bf.WriteNullTerminatedBytes(leaderName)
bf.WriteBytes([]byte{0x00, 0x00, 0x00, 0x00}) // Unk
bf.WriteBool(false) // isReturnGuild
bf.WriteBytes([]byte{0x01, 0x02, 0x02}) // Unk
bf.WriteBool(false) // isReturnGuild
bf.WriteBytes([]byte{0x01, 0x02, 0x02}) // Unk
bf.WriteUint32(guild.EventRP)
// Pugi's names, probably expected as null until you have them with levels? Null gives them a default japanese name
@@ -1001,120 +1007,120 @@ func handleMsgMhfEnumerateGuild(s *Session, p mhfpacket.MHFPacket) {
bf := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload)
switch pkt.Type {
case mhfpacket.ENUMERATE_GUILD_TYPE_GUILD_NAME:
bf.ReadBytes(8)
searchTermLength := bf.ReadUint16()
bf.ReadBytes(1)
searchTerm := bf.ReadBytes(uint(searchTermLength))
var searchTermSafe string
searchTermSafe, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(searchTerm))
if err != nil {
panic(err)
case mhfpacket.ENUMERATE_GUILD_TYPE_GUILD_NAME:
bf.ReadBytes(8)
searchTermLength := bf.ReadUint16()
bf.ReadBytes(1)
searchTerm := bf.ReadBytes(uint(searchTermLength))
var searchTermSafe string
searchTermSafe, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(searchTerm))
if err != nil {
panic(err)
}
guilds, err = FindGuildsByName(s, searchTermSafe)
case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_NAME:
bf.ReadBytes(8)
searchTermLength := bf.ReadUint16()
bf.ReadBytes(1)
searchTerm := bf.ReadBytes(uint(searchTermLength))
var searchTermSafe string
searchTermSafe, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(searchTerm))
if err != nil {
panic(err)
}
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE lc.name ILIKE $1`, guildInfoSelectQuery), searchTermSafe)
if err != nil {
s.logger.Error("Failed to retrieve guild by leader name", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
guilds, err = FindGuildsByName(s, searchTermSafe)
case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_NAME:
bf.ReadBytes(8)
searchTermLength := bf.ReadUint16()
bf.ReadBytes(1)
searchTerm := bf.ReadBytes(uint(searchTermLength))
var searchTermSafe string
searchTermSafe, err = s.clientContext.StrConv.Decode(bfutil.UpToNull(searchTerm))
if err != nil {
panic(err)
}
case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_ID:
bf.ReadBytes(3)
ID := bf.ReadUint32()
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE leader_id = $1`, guildInfoSelectQuery), ID)
if err != nil {
s.logger.Error("Failed to retrieve guild by leader ID", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE lc.name ILIKE $1`, guildInfoSelectQuery), searchTermSafe)
if err != nil {
s.logger.Error("Failed to retrieve guild by leader name", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_MEMBERS:
sorting := bf.ReadUint16()
if sorting == 1 {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY member_count DESC`, guildInfoSelectQuery))
} else {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY member_count ASC`, guildInfoSelectQuery))
}
if err != nil {
s.logger.Error("Failed to retrieve guild by member count", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_ID:
bf.ReadBytes(3)
ID := bf.ReadUint32()
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE leader_id = $1`, guildInfoSelectQuery), ID)
if err != nil {
s.logger.Error("Failed to retrieve guild by leader ID", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_REGISTRATION:
sorting := bf.ReadUint16()
if sorting == 1 {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY id DESC`, guildInfoSelectQuery))
} else {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY id ASC`, guildInfoSelectQuery))
}
if err != nil {
s.logger.Error("Failed to retrieve guild by registration date", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_MEMBERS:
sorting := bf.ReadUint16()
if sorting == 1 {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY member_count DESC`, guildInfoSelectQuery))
} else {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY member_count ASC`, guildInfoSelectQuery))
}
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_RANK:
sorting := bf.ReadUint16()
if sorting == 1 {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY rank_rp DESC`, guildInfoSelectQuery))
} else {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY rank_rp ASC`, guildInfoSelectQuery))
}
if err != nil {
s.logger.Error("Failed to retrieve guild by rank", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
if err != nil {
s.logger.Error("Failed to retrieve guild by member count", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_MOTTO:
bf.ReadBytes(3)
mainMotto := bf.ReadUint16()
subMotto := bf.ReadUint16()
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE main_motto = $1 AND sub_motto = $2`, guildInfoSelectQuery), mainMotto, subMotto)
if err != nil {
s.logger.Error("Failed to retrieve guild by motto", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_REGISTRATION:
sorting := bf.ReadUint16()
if sorting == 1 {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY id DESC`, guildInfoSelectQuery))
} else {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY id ASC`, guildInfoSelectQuery))
}
if err != nil {
s.logger.Error("Failed to retrieve guild by registration date", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_RANK:
sorting := bf.ReadUint16()
if sorting == 1 {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY rank_rp DESC`, guildInfoSelectQuery))
} else {
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s ORDER BY rank_rp ASC`, guildInfoSelectQuery))
}
if err != nil {
s.logger.Error("Failed to retrieve guild by rank", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_MOTTO:
bf.ReadBytes(3)
mainMotto := bf.ReadUint16()
subMotto := bf.ReadUint16()
rows, err = s.server.db.Queryx(fmt.Sprintf(`%s WHERE main_motto = $1 AND sub_motto = $2`, guildInfoSelectQuery), mainMotto, subMotto)
if err != nil {
s.logger.Error("Failed to retrieve guild by motto", zap.Error(err))
} else {
for rows.Next() {
guild, _ := buildGuildObjectFromDbResult(rows, err, s)
guilds = append(guilds, guild)
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_RECRUITING:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ALLIANCE_NAME:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_LEADER_NAME:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_LEADER_ID:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ORDER_MEMBERS:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ORDER_REGISTRATION:
//
default:
panic(fmt.Sprintf("no handler for guild search type '%d'", pkt.Type))
}
case mhfpacket.ENUMERATE_GUILD_TYPE_RECRUITING:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ALLIANCE_NAME:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_LEADER_NAME:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_LEADER_ID:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ORDER_MEMBERS:
//
case mhfpacket.ENUMERATE_ALLIANCE_TYPE_ORDER_REGISTRATION:
//
default:
panic(fmt.Sprintf("no handler for guild search type '%d'", pkt.Type))
}
if err != nil || guilds == nil {
@@ -1133,13 +1139,13 @@ func handleMsgMhfEnumerateGuild(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint32(guild.ID)
bf.WriteUint32(guild.LeaderCharID)
bf.WriteUint16(guild.MemberCount)
bf.WriteUint8(0x00) // Unk
bf.WriteUint8(0x00) // Unk
bf.WriteUint8(0x00) // Unk
bf.WriteUint8(0x00) // Unk
bf.WriteUint16(guild.Rank)
bf.WriteUint32(uint32(guild.CreatedAt.Unix()))
bf.WriteUint8(uint8(len(guildName)+1))
bf.WriteUint8(uint8(len(guildName) + 1))
bf.WriteNullTerminatedBytes(guildName)
bf.WriteUint8(uint8(len(leaderName)+1))
bf.WriteUint8(uint8(len(leaderName) + 1))
bf.WriteNullTerminatedBytes(leaderName)
bf.WriteUint8(0x01) // Unk
}
@@ -1336,8 +1342,8 @@ func handleMsgMhfEnumerateGuildItem(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint32(0x00)
bf.WriteUint16(0x00)
for i := 0; i < amount; i++ {
bf.WriteUint32(binary.BigEndian.Uint32(boxContents[i*4:i*4+4]))
if i + 1 != amount {
bf.WriteUint32(binary.BigEndian.Uint32(boxContents[i*4 : i*4+4]))
if i+1 != amount {
bf.WriteUint64(0x00)
}
}
@@ -1346,7 +1352,7 @@ func handleMsgMhfEnumerateGuildItem(s *Session, p mhfpacket.MHFPacket) {
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
type Item struct{
type Item struct {
ItemId uint16
Amount uint16
}
@@ -1364,8 +1370,8 @@ func handleMsgMhfUpdateGuildItem(s *Session, p mhfpacket.MHFPacket) {
amount := len(boxContents) / 4
oldItems = make([]Item, amount)
for i := 0; i < amount; i++ {
oldItems[i].ItemId = binary.BigEndian.Uint16(boxContents[i*4:i*4+2])
oldItems[i].Amount = binary.BigEndian.Uint16(boxContents[i*4+2:i*4+4])
oldItems[i].ItemId = binary.BigEndian.Uint16(boxContents[i*4 : i*4+2])
oldItems[i].Amount = binary.BigEndian.Uint16(boxContents[i*4+2 : i*4+4])
}
}
@@ -1391,9 +1397,9 @@ func handleMsgMhfUpdateGuildItem(s *Session, p mhfpacket.MHFPacket) {
// Delete empty item stacks
for i := len(newItems) - 1; i >= 0; i-- {
if int(newItems[i].Amount) == 0 {
copy(newItems[i:], newItems[i + 1:])
newItems[len(newItems) - 1] = make([]Item, 1)[0]
newItems = newItems[:len(newItems) - 1]
copy(newItems[i:], newItems[i+1:])
newItems[len(newItems)-1] = make([]Item, 1)[0]
newItems = newItems[:len(newItems)-1]
}
}
@@ -1515,7 +1521,13 @@ func handleMsgMhfLoadGuildCooking(s *Session, p mhfpacket.MHFPacket) {
// uint32 expiration timestamp
// encourage food
data := []byte{0x00, 0x01, 0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0xC4, 0x00, 0x00, 0x00, 0x03, 0x5F, 0xFC, 0x0B, 0x51}
data := []byte{0x00, 0x06,
0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0xc4, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x0B, 0x51,
0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0x9c, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x0B, 0x52,
0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x0B, 0x51,
0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x01, 0x8b, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFD, 0x0B, 0x51,
0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x02, 0x54, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFC, 0x0B, 0x51,
0x0F, 0x51, 0x97, 0xFF, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x03, 0xF3, 0xFC, 0x0B, 0x51}
doAckBufSucceed(s, pkt.AckHandle, data)
//data := []byte{0x00, 0x01, 0x1C, 0x72, 0x54, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x5F, 0xF8, 0x2F, 0xE1}
//doAckBufSucceed(s, pkt.AckHandle, data)
@@ -1566,7 +1578,7 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
msgs, err := s.server.db.Queryx("SELECT post_type, stamp_id, title, body, author_id, (EXTRACT(epoch FROM created_at)::int) as created_at, liked_by FROM guild_posts WHERE guild_id = $1 AND post_type = $2 ORDER BY created_at DESC", guild.ID, int(pkt.BoardType))
if err != nil {
s.logger.Fatal("Failed to get guild messages from db", zap.Error(err))
log.Println("Failed to get guild messages from db", zap.Error(err))
}
bf := byteframe.NewByteFrame()
@@ -1575,22 +1587,38 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
for msgs.Next() {
noMsgs = false
postCount++
var str_title, str_body string
var timestampst, timecomp uint64
timestampst = 32400
postData := &MessageBoardPost{}
err = msgs.StructScan(&postData)
if err != nil {
s.logger.Error("Failed to read guild message board post")
log.Println("Failed to get guild messages from db", zap.Error(err))
}
t := japanese.ShiftJIS.NewEncoder()
str_title, _, err := transform.String(t, postData.Title)
if err != nil {
log.Println(err)
}
str_body, _, err = transform.String(t, postData.Body)
if err != nil {
log.Println(err)
}
timecomp = postData.Timestamp - timestampst
bf.WriteUint32(postData.Type)
bf.WriteUint32(postData.AuthorID)
bf.WriteUint64(postData.Timestamp)
bf.WriteUint64(timecomp)
liked := false
likedBySlice := strings.Split(postData.LikedBy, ",")
for i := 0; i < len(likedBySlice); i++ {
j, _ := strconv.ParseInt(likedBySlice[i], 10, 64)
if int(j) == int(s.charID) {
liked = true; break
liked = true
break
}
}
if likedBySlice[0] == "" {
@@ -1598,12 +1626,13 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
} else {
bf.WriteUint32(uint32(len(likedBySlice)))
}
bf.WriteBool(liked)
bf.WriteUint32(postData.StampID)
bf.WriteUint32(uint32(len(postData.Title)))
bf.WriteBytes([]byte(postData.Title))
bf.WriteUint32(uint32(len(postData.Body)))
bf.WriteBytes([]byte(postData.Body))
bf.WriteUint32(uint32(len(str_title)))
bf.WriteBytes([]byte(str_title))
bf.WriteUint32(uint32(len(str_body)))
bf.WriteBytes([]byte(str_body))
}
if noMsgs {
doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
@@ -1615,6 +1644,19 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
}
}
func transformEncoding(rawReader io.Reader, trans transform.Transformer) (string, error) {
ret, err := ioutil.ReadAll(transform.NewReader(rawReader, trans))
if err == nil {
return string(ret), nil
} else {
return "", err
}
}
func BytesFromShiftJIS(b []byte) (string, error) {
return transformEncoding(bytes.NewReader(b), japanese.ShiftJIS.NewDecoder())
}
func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfUpdateGuildMessageBoard)
bf := byteframe.NewByteFrameFromBytes(pkt.Request)
@@ -1623,57 +1665,109 @@ func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
return
}
switch pkt.MessageOp {
case 0: // Create message
var title_str, body_str string
postType := bf.ReadUint32() // 0 = message, 1 = news
stampId := bf.ReadUint32()
titleLength := bf.ReadUint32()
bodyLength := bf.ReadUint32()
title := bf.ReadBytes(uint(titleLength))
body := bf.ReadBytes(uint(bodyLength))
_, err := s.server.db.Exec("INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)", guild.ID, s.charID, int(stampId), int(postType), string(title), string(body))
title_str, err := BytesFromShiftJIS(title)
if err != nil {
s.logger.Fatal("Failed to add new guild message to db", zap.Error(err))
log.Println(err)
}
fmt.Println(title_str)
body_str, err = BytesFromShiftJIS(body)
if err != nil {
log.Println(err)
}
fmt.Println(body_str)
_, err = s.server.db.Exec("INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)", guild.ID, s.charID, int(stampId), int(postType), string(title_str), string(body_str))
if err != nil {
log.Println("Failed to add new guild message to db", zap.Error(err))
}
// TODO: if there are too many messages, purge excess
_, err = s.server.db.Exec("")
if err != nil {
s.logger.Fatal("Failed to remove excess guild messages from db", zap.Error(err))
log.Println("Failed to remove excess guild messages from db", zap.Error(err))
}
case 1: // Delete message
var timestampst, timecomp uint64
timestampst = 32400
postType := bf.ReadUint32()
timestamp := bf.ReadUint64()
_, err := s.server.db.Exec("DELETE FROM guild_posts WHERE post_type = $1 AND (EXTRACT(epoch FROM created_at)::int) = $2 AND guild_id = $3", int(postType), int(timestamp), guild.ID)
timecomp = timestamp + timestampst
_, err := s.server.db.Exec("DELETE FROM guild_posts WHERE post_type = $1 AND (EXTRACT(epoch FROM created_at)::int) = $2 AND guild_id = $3", int(postType), int(timecomp), guild.ID)
if err != nil {
s.logger.Fatal("Failed to delete guild message from db", zap.Error(err))
log.Println("Failed to delete guild message from db", zap.Error(err))
}
case 2: // Update message
var title_str, body_str string
var timestampst, timecomp uint64
timestampst = 32400
postType := bf.ReadUint32()
timestamp := bf.ReadUint64()
titleLength := bf.ReadUint32()
bodyLength := bf.ReadUint32()
title := bf.ReadBytes(uint(titleLength))
body := bf.ReadBytes(uint(bodyLength))
_, err := s.server.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE post_type = $3 AND (EXTRACT(epoch FROM created_at)::int) = $4 AND guild_id = $5", string(title), string(body), int(postType), int(timestamp), guild.ID)
title_str, err := BytesFromShiftJIS(title)
if err != nil {
s.logger.Fatal("Failed to update guild message in db", zap.Error(err))
log.Println(err)
}
fmt.Println(title_str)
body_str, err = BytesFromShiftJIS(body)
if err != nil {
log.Println(err)
}
fmt.Println(body_str)
timecomp = timestamp + timestampst
_, err = s.server.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE post_type = $3 AND (EXTRACT(epoch FROM created_at)::int) = $4 AND guild_id = $5", string(title_str), string(body_str), int(postType), int(timecomp), guild.ID)
if err != nil {
log.Println("Failed to update guild message in db", zap.Error(err))
}
case 3: // Update stamp
var timestampst, timecomp uint64
timestampst = 32400
postType := bf.ReadUint32()
timestamp := bf.ReadUint64()
stampId := bf.ReadUint32()
_, err := s.server.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", int(stampId), int(postType), int(timestamp), guild.ID)
timecomp = timestamp + timestampst
_, err := s.server.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", int(stampId), int(postType), int(timecomp), guild.ID)
if err != nil {
s.logger.Fatal("Failed to update guild message stamp in db", zap.Error(err))
log.Println("Failed to update guild message stamp in db", zap.Error(err))
}
case 4: // Like message
var timestampst, timecomp uint64
timestampst = 32400
postType := bf.ReadUint32()
timestamp := bf.ReadUint64()
likeState := bf.ReadBool()
timecomp = timestamp + timestampst
var likedBy string
err := s.server.db.QueryRow("SELECT liked_by FROM guild_posts WHERE post_type = $1 AND (EXTRACT(epoch FROM created_at)::int) = $2 AND guild_id = $3", int(postType), int(timestamp), guild.ID).Scan(&likedBy)
err := s.server.db.QueryRow("SELECT liked_by FROM guild_posts WHERE post_type = $1 AND (EXTRACT(epoch FROM created_at)::int) = $2 AND guild_id = $3", int(postType), int(timecomp), guild.ID).Scan(&likedBy)
if err != nil {
s.logger.Fatal("Failed to get guild message like data from db", zap.Error(err))
log.Println("Failed to get guild message like data from db", zap.Error(err))
} else {
if likeState {
if len(likedBy) == 0 {
@@ -1681,22 +1775,22 @@ func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
} else {
likedBy += "," + strconv.Itoa(int(s.charID))
}
_, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", likedBy, int(postType), int(timestamp), guild.ID)
_, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", likedBy, int(postType), int(timecomp), guild.ID)
if err != nil {
s.logger.Fatal("Failed to like guild message in db", zap.Error(err))
log.Println("Failed to like guild message in db", zap.Error(err))
}
} else {
likedBySlice := strings.Split(likedBy, ",")
for i, e := range likedBySlice {
if e == strconv.Itoa(int(s.charID)) {
likedBySlice[i] = likedBySlice[len(likedBySlice) - 1]
likedBySlice = likedBySlice[:len(likedBySlice) - 1]
likedBySlice[i] = likedBySlice[len(likedBySlice)-1]
likedBySlice = likedBySlice[:len(likedBySlice)-1]
}
}
likedBy = strings.Join(likedBySlice, ",")
_, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", likedBy, int(postType), int(timestamp), guild.ID)
_, err := s.server.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE post_type = $2 AND (EXTRACT(epoch FROM created_at)::int) = $3 AND guild_id = $4", likedBy, int(postType), int(timecomp), guild.ID)
if err != nil {
s.logger.Fatal("Failed to unlike guild message in db", zap.Error(err))
log.Println("Failed to unlike guild message in db", zap.Error(err))
}
}
}
@@ -1705,15 +1799,15 @@ func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
var newPosts int
err := s.server.db.QueryRow("SELECT (EXTRACT(epoch FROM guild_post_checked)::int) FROM characters WHERE id = $1", s.charID).Scan(&timeChecked)
if err != nil {
s.logger.Fatal("Failed to get last guild post check timestamp from db", zap.Error(err))
log.Println("Failed to get last guild post check timestamp from db", zap.Error(err))
} else {
_, err = s.server.db.Exec("UPDATE characters SET guild_post_checked = $1 WHERE id = $2", time.Now(), s.charID)
if err != nil {
s.logger.Fatal("Failed to update guild post check timestamp in db", zap.Error(err))
log.Println("Failed to update guild post check timestamp in db", zap.Error(err))
} else {
err = s.server.db.QueryRow("SELECT COUNT(*) FROM guild_posts WHERE guild_id = $1 AND (EXTRACT(epoch FROM created_at)::int) > $2", guild.ID, timeChecked).Scan(&newPosts)
if err != nil {
s.logger.Fatal("Failed to check for new guild posts in db", zap.Error(err))
log.Println("Failed to check for new guild posts in db", zap.Error(err))
} else {
if newPosts > 0 {
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x01})
@@ -1764,8 +1858,8 @@ func handleMsgMhfOperationInvGuild(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfUpdateGuildcard(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfCreateJoint(s *Session, p mhfpacket.MHFPacket) { }
func handleMsgMhfCreateJoint(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) { }
func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfInfoJoint(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfInfoJoint(s *Session, p mhfpacket.MHFPacket) {}

View File

@@ -2,16 +2,28 @@ package channelserver
import (
"encoding/hex"
"fmt"
"strings"
"time"
//"github.com/Solenataris/Erupe/common/stringsupport"
"github.com/Solenataris/Erupe/network/mhfpacket"
"github.com/Andoryuuta/byteframe"
"github.com/Solenataris/Erupe/network/mhfpacket"
"github.com/lib/pq"
"github.com/sachaos/lottery"
"go.uber.org/zap"
)
func contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
}
func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateShop)
// SHOP TYPES:
@@ -149,20 +161,28 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) {
} else {
doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}
} else {
shopEntries, err := s.server.db.Query("SELECT itemhash,itemID,Points,TradeQuantity,rankReqLow,rankReqHigh,rankReqG,storeLevelReq,maximumQuantity,boughtQuantity,roadFloorsRequired,weeklyFatalisKills FROM normal_shop_items WHERE shoptype=$1 AND shopid=$2", pkt.ShopType, pkt.ShopID)
} else {
_, week := time.Now().ISOWeek()
season := fmt.Sprintf("%d", week%4)
shopEntries, err := s.server.db.Query("SELECT itemhash,itemID,Points,TradeQuantity,rankReqLow,rankReqHigh,rankReqG,storeLevelReq,maximumQuantity,boughtQuantity,roadFloorsRequired,weeklyFatalisKills, COALESCE(enable_weeks, '') FROM normal_shop_items WHERE shoptype=$1 AND shopid=$2", pkt.ShopType, pkt.ShopID)
if err != nil {
panic(err)
}
var ItemHash, entryCount int
var itemID, Points, TradeQuantity, rankReqLow, rankReqHigh, rankReqG, storeLevelReq, maximumQuantity, boughtQuantity, roadFloorsRequired, weeklyFatalisKills, charQuantity uint16
var itemWeeks string
resp := byteframe.NewByteFrame()
resp.WriteUint32(0) // total defs
for shopEntries.Next() {
err = shopEntries.Scan(&ItemHash, &itemID, &Points, &TradeQuantity, &rankReqLow, &rankReqHigh, &rankReqG, &storeLevelReq, &maximumQuantity, &boughtQuantity, &roadFloorsRequired, &weeklyFatalisKills)
err = shopEntries.Scan(&ItemHash, &itemID, &Points, &TradeQuantity, &rankReqLow, &rankReqHigh, &rankReqG, &storeLevelReq, &maximumQuantity, &boughtQuantity, &roadFloorsRequired, &weeklyFatalisKills, &itemWeeks)
if err != nil {
panic(err)
}
if len(itemWeeks) > 0 && !contains(strings.Split(itemWeeks, ","), season) {
continue
}
resp.WriteUint32(uint32(ItemHash))
resp.WriteUint16(0) // unk, always 0 in existing packets
resp.WriteUint16(itemID)
@@ -175,9 +195,13 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) {
resp.WriteUint16(storeLevelReq)
resp.WriteUint16(maximumQuantity)
if maximumQuantity > 0 {
err = s.server.db.QueryRow("SELECT COALESCE(usedquantity,0) FROM shop_item_state WHERE itemhash=$1 AND char_id=$2", ItemHash, s.charID).Scan(&charQuantity)
var itemWeek int
err = s.server.db.QueryRow("SELECT COALESCE(usedquantity,0), COALESCE(week,-1) FROM shop_item_state WHERE itemhash=$1 AND char_id=$2", ItemHash, s.charID).Scan(&charQuantity, &itemWeek)
if err != nil {
resp.WriteUint16(0)
} else if pkt.ShopID == 7 && itemWeek >= 0 && itemWeek != week {
clearShopItemState(s, s.charID, uint32(ItemHash))
resp.WriteUint16(0)
} else {
resp.WriteUint16(charQuantity)
}
@@ -200,6 +224,7 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) {
}
func handleMsgMhfAcquireExchangeShop(s *Session, p mhfpacket.MHFPacket) {
_, week := time.Now().ISOWeek()
// writing out to an editable shop enumeration
pkt := p.(*mhfpacket.MsgMhfAcquireExchangeShop)
if pkt.DataSize == 10 {
@@ -207,10 +232,10 @@ func handleMsgMhfAcquireExchangeShop(s *Session, p mhfpacket.MHFPacket) {
_ = bf.ReadUint16() // unk, always 1 in examples
itemHash := bf.ReadUint32()
buyCount := bf.ReadUint32()
_, err := s.server.db.Exec(`INSERT INTO shop_item_state (char_id, itemhash, usedquantity)
VALUES ($1,$2,$3) ON CONFLICT (char_id, itemhash)
_, err := s.server.db.Exec(`INSERT INTO shop_item_state (char_id, itemhash, usedquantity, week)
VALUES ($1,$2,$3,$4) ON CONFLICT (char_id, itemhash)
DO UPDATE SET usedquantity = shop_item_state.usedquantity + $3
WHERE EXCLUDED.char_id=$1 AND EXCLUDED.itemhash=$2`, s.charID, itemHash, buyCount)
WHERE EXCLUDED.char_id=$1 AND EXCLUDED.itemhash=$2`, s.charID, itemHash, buyCount, week)
if err != nil {
s.logger.Fatal("Failed to update shop_item_state in db", zap.Error(err))
}
@@ -218,6 +243,13 @@ func handleMsgMhfAcquireExchangeShop(s *Session, p mhfpacket.MHFPacket) {
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}
func clearShopItemState(s *Session, charId uint32, itemHash uint32) {
_, err := s.server.db.Exec(`DELETE FROM shop_item_state WHERE char_id=$1 AND itemhash=$2`, charId, itemHash)
if err != nil {
s.logger.Fatal("Failed to delete shop_item_state in db", zap.Error(err))
}
}
func handleMsgMhfGetGachaPlayHistory(s *Session, p mhfpacket.MHFPacket) {
// returns number of times the gacha was played, will need persistent db stuff
pkt := p.(*mhfpacket.MsgMhfGetGachaPlayHistory)
@@ -703,4 +735,4 @@ func handleMsgMhfResetBoxGachaInfo(s *Session, p mhfpacket.MHFPacket) {
panic(err)
}
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}
}

View File

@@ -9,16 +9,36 @@ import (
"github.com/Solenataris/Erupe/config"
"github.com/Solenataris/Erupe/network/binpacket"
"github.com/Solenataris/Erupe/network/mhfpacket"
"github.com/bwmarrin/discordgo"
"github.com/Solenataris/Erupe/server/discordbot"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
type StageIdType = string
const (
// GlobalStage is the stage that is used for all users.
MezeportaStageId StageIdType = "sl1Ns200p0a0u0"
GuildHallLv1StageId StageIdType = "sl1Ns202p0a0u0"
GuildHallLv2StageId StageIdType = "sl1Ns203p0a0u0"
GuildHallLv3StageId StageIdType = "sl1Ns204p0a0u0"
PugiFarmStageId StageIdType = "sl1Ns205p0a0u0"
RastaBarStageId StageIdType = "sl1Ns211p0a0u0"
PalloneCaravanStageId StageIdType = "sl1Ns260p0a0u0"
GookFarmStageId StageIdType = "sl1Ns265p0a0u0"
DivaFountainStageId StageIdType = "sl2Ns379p0a0u0"
DivaHallStageId StageIdType = "sl1Ns445p0a0u0"
MezFesStageId StageIdType = "sl1Ns462p0a0u0"
)
// Config struct allows configuring the server.
type Config struct {
Logger *zap.Logger
DB *sqlx.DB
DiscordBot *discordbot.DiscordBot
ErupeConfig *config.Config
Name string
Enable bool
}
// Map key type for a user binary part.
@@ -30,14 +50,13 @@ type userBinaryPartID struct {
// Server is a MHF channel server.
type Server struct {
sync.Mutex
logger *zap.Logger
db *sqlx.DB
erupeConfig *config.Config
acceptConns chan net.Conn
deleteConns chan net.Conn
sessions map[net.Conn]*Session
listener net.Listener // Listener that is created when Server.Start is called.
logger *zap.Logger
db *sqlx.DB
erupeConfig *config.Config
acceptConns chan net.Conn
deleteConns chan net.Conn
sessions map[net.Conn]*Session
listener net.Listener // Listener that is created when Server.Start is called.
isShuttingDown bool
stagesLock sync.RWMutex
@@ -52,7 +71,10 @@ type Server struct {
semaphore map[string]*Semaphore
// Discord chat integration
discordSession *discordgo.Session
discordBot *discordbot.DiscordBot
name string
enable bool
raviente *Raviente
}
@@ -61,8 +83,8 @@ type Raviente struct {
sync.Mutex
register *RavienteRegister
state *RavienteState
support *RavienteSupport
state *RavienteState
support *RavienteSupport
}
type RavienteRegister struct {
@@ -114,7 +136,7 @@ func NewRaviente() *Raviente {
// NewServer creates a new Server type.
func NewServer(config *Config) *Server {
s := &Server{
s := &Server {
logger: config.Logger,
db: config.DB,
erupeConfig: config.ErupeConfig,
@@ -124,7 +146,10 @@ func NewServer(config *Config) *Server {
stages: make(map[string]*Stage),
userBinaryParts: make(map[userBinaryPartID][]byte),
semaphore: make(map[string]*Semaphore),
discordSession: nil,
discordBot: config.DiscordBot,
name: config.Name,
enable: config.Enable,
raviente: NewRaviente(),
}
// Mezeporta
@@ -145,7 +170,7 @@ func NewServer(config *Config) *Server {
// Rasta bar stage
s.stages["sl1Ns211p0a0u0"] = NewStage("sl1Ns211p0a0u0")
// Carvane
// Pallone Carvan
s.stages["sl1Ns260p0a0u0"] = NewStage("sl1Ns260p0a0u0")
// Gook Farm
@@ -160,16 +185,6 @@ func NewServer(config *Config) *Server {
// MezFes
s.stages["sl1Ns462p0a0u0"] = NewStage("sl1Ns462p0a0u0")
// Create the discord session, (not actually connecting to discord servers yet).
if s.erupeConfig.Discord.Enabled {
ds, err := discordgo.New("Bot " + s.erupeConfig.Discord.BotToken)
if err != nil {
s.logger.Fatal("Error creating Discord session.", zap.Error(err))
}
ds.AddHandler(s.onDiscordMessage)
s.discordSession = ds
}
return s
}
@@ -185,12 +200,8 @@ func (s *Server) Start(port int) error {
go s.manageSessions()
// Start the discord bot for chat integration.
if s.erupeConfig.Discord.Enabled {
err = s.discordSession.Open()
if err != nil {
s.logger.Warn("Error opening Discord session.", zap.Error(err))
return err
}
if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
s.discordBot.Session.AddHandler(s.onDiscordMessage)
}
return nil
@@ -203,11 +214,8 @@ func (s *Server) Shutdown() {
s.Unlock()
s.listener.Close()
close(s.acceptConns)
if s.erupeConfig.Discord.Enabled {
s.discordSession.Close()
}
close(s.acceptConns)
}
func (s *Server) acceptClients() {
@@ -289,7 +297,7 @@ func (s *Server) BroadcastChatMessage(message string) {
Type: 5,
Flags: 0x80,
Message: message,
SenderName: "Erupe",
SenderName: s.name,
}
msgBinChat.Build(bf)
@@ -300,6 +308,13 @@ func (s *Server) BroadcastChatMessage(message string) {
}, nil)
}
func (s *Server) DiscordChannelSend(charName string, content string) {
if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
message := fmt.Sprintf("**%s** : %s", charName, content)
s.discordBot.RealtimeChannelSend(message)
}
}
func (s *Server) FindSessionByCharID(charID uint32) *Session {
s.stagesLock.RLock()
defer s.stagesLock.RUnlock()
@@ -316,3 +331,21 @@ func (s *Server) FindSessionByCharID(charID uint32) *Session {
return nil
}
func (s *Server) FindStageObjectByChar(charID uint32) *StageObject {
s.stagesLock.RLock()
defer s.stagesLock.RUnlock()
for _, stage := range s.stages {
stage.RLock()
for objId := range stage.objects {
obj := stage.objects[objId]
if obj.ownerCharID == charID {
stage.RUnlock()
return obj
}
}
stage.RUnlock()
}
return nil
}

View File

@@ -1,9 +1,12 @@
package channelserver
import (
"sync"
"time"
"github.com/Andoryuuta/byteframe"
"github.com/Solenataris/Erupe/network/mhfpacket"
"sync"
)
// StageObject holds infomation about a specific stage object.
@@ -15,7 +18,7 @@ type StageObject struct {
}
type ObjectMap struct {
id uint8
id uint8
charid uint32
status bool
}
@@ -55,6 +58,7 @@ type Stage struct {
maxPlayers uint16
hasDeparted bool
password string
createdAt string
}
// NewStage creates a new stage with intialized values.
@@ -68,6 +72,7 @@ func NewStage(ID string) *Stage {
maxPlayers: 4,
gameObjectCount: 1,
objectList: make(map[uint8]*ObjectMap),
createdAt: time.Now().Format("01-02-2006 15:04:05"),
}
s.InitObjectList()
return s
@@ -94,30 +99,71 @@ func (s *Stage) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session) {
}
func (s *Stage) InitObjectList() {
for seq:=uint8(0x7f);seq>uint8(0);seq-- {
newObj := &ObjectMap{
id: seq,
charid: uint32(0),
status: false,
}
s.objectList[seq] = newObj
for seq := uint8(0x7f); seq > uint8(0); seq-- {
newObj := &ObjectMap{
id: seq,
charid: uint32(0),
status: false,
}
s.objectList[seq] = newObj
}
}
func (s *Stage) isCharInQuestByID(charID uint32) bool {
if _, exists := s.reservedClientSlots[charID]; exists {
return exists
}
return false
}
func (s *Stage) isQuest() bool {
return len(s.reservedClientSlots) > 0
}
func (stage *Stage) GetName() string {
switch stage.id {
case MezeportaStageId:
return "Mezeporta"
case GuildHallLv1StageId:
return "Guild Hall Lv1"
case GuildHallLv2StageId:
return "Guild Hall Lv2"
case GuildHallLv3StageId:
return "Guild Hall Lv3"
case PugiFarmStageId:
return "Pugi Farm"
case RastaBarStageId:
return "Rasta Bar"
case PalloneCaravanStageId:
return "Pallone Caravan"
case GookFarmStageId:
return "Gook Farm"
case DivaFountainStageId:
return "Diva Fountain"
case DivaHallStageId:
return "Diva Hall"
case MezFesStageId:
return "Mez Fes"
default:
return ""
}
}
func (s *Stage) GetNewObjectID(CharID uint32) uint32 {
ObjId:=uint8(0)
for seq:=uint8(0x7f);seq>uint8(0);seq--{
ObjId := uint8(0)
for seq := uint8(0x7f); seq > uint8(0); seq-- {
if s.objectList[seq].status == false {
ObjId=seq
ObjId = seq
break
}
}
s.objectList[ObjId].status=true
s.objectList[ObjId].charid=CharID
s.objectList[ObjId].status = true
s.objectList[ObjId].charid = CharID
bf := byteframe.NewByteFrame()
bf.WriteUint8(uint8(0))
bf.WriteUint8(ObjId)
bf.WriteUint16(uint16(0))
obj :=uint32(bf.Data()[3]) | uint32(bf.Data()[2])<<8 | uint32(bf.Data()[1])<<16 | uint32(bf.Data()[0])<<32
obj := uint32(bf.Data()[3]) | uint32(bf.Data()[2])<<8 | uint32(bf.Data()[1])<<16 | uint32(bf.Data()[0])<<32
return obj
}
}

View File

@@ -0,0 +1,120 @@
package discordbot
import (
"regexp"
"github.com/Solenataris/Erupe/config"
"github.com/bwmarrin/discordgo"
"go.uber.org/zap"
)
type DiscordBot struct {
Session *discordgo.Session
config *config.Config
logger *zap.Logger
MainGuild *discordgo.Guild
RealtimeChannel *discordgo.Channel
}
type DiscordBotOptions struct {
Config *config.Config
Logger *zap.Logger
}
func NewDiscordBot(options DiscordBotOptions) (discordBot *DiscordBot, err error) {
session, err := discordgo.New("Bot " + options.Config.Discord.BotToken)
if err != nil {
options.Logger.Fatal("Discord failed", zap.Error(err))
return nil, err
}
mainGuild, err := session.Guild(options.Config.Discord.ServerID)
if err != nil {
options.Logger.Fatal("Discord failed to get main guild", zap.Error(err))
return nil, err
}
realtimeChannel, err := session.Channel(options.Config.Discord.RealtimeChannelID)
if err != nil {
options.Logger.Fatal("Discord failed to create realtimeChannel", zap.Error(err))
return nil, err
}
discordBot = &DiscordBot{
config: options.Config,
logger: options.Logger,
Session: session,
MainGuild: mainGuild,
RealtimeChannel: realtimeChannel,
}
return
}
func (bot *DiscordBot) Start() (err error) {
err = bot.Session.Open()
return
}
func (bot *DiscordBot) FindRoleByID(id string) *discordgo.Role {
for _, role := range bot.MainGuild.Roles {
if role.ID == id {
return role
}
}
return nil
}
// Replace all mentions to real name from the message.
func (bot *DiscordBot) NormalizeDiscordMessage(message string) string {
userRegex := regexp.MustCompile(`<@!?(\d{17,19})>`)
emojiRegex := regexp.MustCompile(`(?:<a?)?:(\w+):(?:\d{18}>)?`)
roleRegex := regexp.MustCompile(`<@&(\d{17,19})>`)
result := ReplaceTextAll(message, userRegex, func(userId string) string {
user, err := bot.Session.User(userId)
if err != nil {
return "@unknown" // @Unknown
}
return "@" + user.Username
})
result = ReplaceTextAll(result, emojiRegex, func(emojiName string) string {
return ":" + emojiName + ":"
})
result = ReplaceTextAll(result, roleRegex, func(roleId string) string {
role := bot.FindRoleByID(roleId)
if role != nil {
return "@!" + role.Name
}
return "@!unknown"
})
return string(result)
}
func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) {
_, err = bot.Session.ChannelMessageSend(bot.RealtimeChannel.ID, message)
return
}
func ReplaceTextAll(text string, regex *regexp.Regexp, handler func(input string) string) string {
result := regex.ReplaceAllFunc([]byte(text), func(s []byte) []byte {
input := regex.ReplaceAllString(string(s), `$1`)
return []byte(handler(input))
})
return string(result)
}