Discord Integration Improvement

How to config the bot integration:

- In the config.json file, find the and do the following in my instruction.

"discord": {
        "enabled": false, 	 // set this value to "true" to enable it
        "bottoken": "",		 // add your bot token, for this you need a bot
        "realtimeChannelID": "", // add the Discord channel ID that will receive and send messages
        "serverId": "", 	 // add your Discord group ID here
        "devRoles": [],		 // add a Discord dev role for a few internal commands, like debug
        "devMode": false


- It is possible to set it to multiple Worlds too, for that just do the following:
By default it will only broadcast messages in the Normal World using the Local and Party chat.

In the main.go file, find the and do the following.

//DiscordBot:  discordBot,	 // just remove the "//" to enable it for that World.


- Use the command !players in the Discord chat to see a list of all online players.

By : Invasores de Fronteiras
https://github.com/Invasor-de-Fronteiras/Erupe
This commit is contained in:
Malckyor
2022-06-15 07:32:45 +09:00
committed by GitHub
parent 642040cb1e
commit 9c6950d200
8 changed files with 1157 additions and 677 deletions

View File

@@ -18,7 +18,10 @@
"discord": { "discord": {
"enabled": false, "enabled": false,
"bottoken": "", "bottoken": "",
"channelid": "" "realtimeChannelID": "",
"serverId": "",
"devRoles": [],
"devMode": false
}, },
"database": { "database": {
"host": "localhost", "host": "localhost",

View File

@@ -43,7 +43,10 @@ type SaveDumpOptions struct {
type Discord struct { type Discord struct {
Enabled bool Enabled bool
BotToken string BotToken string
ChannelID string ServerID string
RealtimeChannelID string
DevRoles []string
DevMode bool
} }
// Database holds the postgres database config. // Database holds the postgres database config.
@@ -154,4 +157,3 @@ func LoadConfig() (*Config, error) {
return c, nil return c, nil
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/Solenataris/Erupe/config" "github.com/Solenataris/Erupe/config"
"github.com/Solenataris/Erupe/server/channelserver" "github.com/Solenataris/Erupe/server/channelserver"
"github.com/Solenataris/Erupe/server/discordbot"
"github.com/Solenataris/Erupe/server/entranceserver" "github.com/Solenataris/Erupe/server/entranceserver"
"github.com/Solenataris/Erupe/server/launcherserver" "github.com/Solenataris/Erupe/server/launcherserver"
"github.com/Solenataris/Erupe/server/signserver" "github.com/Solenataris/Erupe/server/signserver"
@@ -39,6 +40,31 @@ func main() {
logger.Fatal("Failed to load config", zap.Error(err)) 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. // Create the postgres DB pool.
connectString := fmt.Sprintf( connectString := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname= %s sslmode=disable", "host=%s port=%d user=%s password=%s dbname= %s sslmode=disable",
@@ -116,6 +142,9 @@ func main() {
Logger: logger.Named("channel"), Logger: logger.Named("channel"),
ErupeConfig: erupeConfig, ErupeConfig: erupeConfig,
DB: db, 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) err = channelServer1.Start(erupeConfig.Channel.Port1)
@@ -129,6 +158,9 @@ func main() {
Logger: logger.Named("channel"), Logger: logger.Named("channel"),
ErupeConfig: erupeConfig, ErupeConfig: erupeConfig,
DB: db, 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) err = channelServer2.Start(erupeConfig.Channel.Port2)
@@ -141,6 +173,9 @@ func main() {
Logger: logger.Named("channel"), Logger: logger.Named("channel"),
ErupeConfig: erupeConfig, ErupeConfig: erupeConfig,
DB: db, 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) err = channelServer3.Start(erupeConfig.Channel.Port3)
@@ -153,6 +188,9 @@ func main() {
Logger: logger.Named("channel"), Logger: logger.Named("channel"),
ErupeConfig: erupeConfig, ErupeConfig: erupeConfig,
DB: db, 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) err = channelServer4.Start(erupeConfig.Channel.Port4)

View File

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

View File

@@ -2,162 +2,344 @@ package channelserver
import ( import (
"fmt" "fmt"
"os" "sort"
"strings" "strings"
"time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
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"`
}
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>",
}
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 {
status = p.StageName
}
missingSpace := length - len(p.CharName)
whitespaces := strings.Repeat(" ", missingSpace+5)
return fmt.Sprintf("%s %s %s %s", p.WeaponEmoji, p.CharName, whitespaces, status)
}
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:",
}
bigNameLen := 0
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)
}
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. // onDiscordMessage handles receiving messages from discord and forwarding them ingame.
func (s *Server) onDiscordMessage(ds *discordgo.Session, m *discordgo.MessageCreate) { func (s *Server) onDiscordMessage(ds *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore messages from our bot, or ones that are not in the correct channel. // 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 { if m.Author.ID == ds.State.User.ID || !s.enable {
return return
} }
// Broadcast to the game clients. // Ignore other channels in devMode
data := m.Content if s.erupeConfig.Discord.DevMode && m.ChannelID != s.erupeConfig.Discord.RealtimeChannelID {
// 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 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) message := fmt.Sprintf("[DISCORD] %s: %s", m.Author.Username, m.Content)
s.BroadcastChatMessage(message) s.BroadcastChatMessage(s.discordBot.NormalizeDiscordMessage(message))
}
func dayConvert(result string) string {
var replaceDays string
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"
} else {
replaceDays = "NULL"
}
return replaceDays
}
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"
}
return replaceMonth
}
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.")
}
}
// Déconnecter tous les joueurs du serveur.
if timer == 0 {
os.Exit(-1)
}
}
} }
} }

View File

@@ -9,16 +9,36 @@ import (
"github.com/Solenataris/Erupe/config" "github.com/Solenataris/Erupe/config"
"github.com/Solenataris/Erupe/network/binpacket" "github.com/Solenataris/Erupe/network/binpacket"
"github.com/Solenataris/Erupe/network/mhfpacket" "github.com/Solenataris/Erupe/network/mhfpacket"
"github.com/bwmarrin/discordgo" "github.com/Solenataris/Erupe/server/discordbot"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"go.uber.org/zap" "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. // Config struct allows configuring the server.
type Config struct { type Config struct {
Logger *zap.Logger Logger *zap.Logger
DB *sqlx.DB DB *sqlx.DB
DiscordBot *discordbot.DiscordBot
ErupeConfig *config.Config ErupeConfig *config.Config
Name string
Enable bool
} }
// Map key type for a user binary part. // Map key type for a user binary part.
@@ -37,7 +57,6 @@ type Server struct {
deleteConns chan net.Conn deleteConns chan net.Conn
sessions map[net.Conn]*Session sessions map[net.Conn]*Session
listener net.Listener // Listener that is created when Server.Start is called. listener net.Listener // Listener that is created when Server.Start is called.
isShuttingDown bool isShuttingDown bool
stagesLock sync.RWMutex stagesLock sync.RWMutex
@@ -52,7 +71,67 @@ type Server struct {
semaphore map[string]*Semaphore semaphore map[string]*Semaphore
// Discord chat integration // Discord chat integration
discordSession *discordgo.Session discordBot *discordbot.DiscordBot
name string
enable bool
raviente *Raviente
}
type Raviente struct {
sync.Mutex
register *RavienteRegister
state *RavienteState
support *RavienteSupport
}
type RavienteRegister struct {
nextTime uint32
startTime uint32
postTime uint32
killedTime uint32
ravienteType uint32
maxPlayers uint32
carveQuest uint32
register []uint32
}
type RavienteState struct {
damageMultiplier uint32
stateData []uint32
}
type RavienteSupport struct {
supportData []uint32
}
// Set up the Raviente variables for the server
func NewRaviente() *Raviente {
ravienteRegister := &RavienteRegister {
nextTime: 0,
startTime: 0,
killedTime: 0,
postTime: 0,
ravienteType: 0,
maxPlayers: 0,
carveQuest: 0,
}
ravienteState := &RavienteState {
damageMultiplier: 1,
}
ravienteSupport := &RavienteSupport { }
ravienteRegister.register = []uint32{0, 0, 0, 0, 0}
ravienteState.stateData = []uint32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
ravienteSupport.supportData = []uint32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
raviente := &Raviente {
register: ravienteRegister,
state: ravienteState,
support: ravienteSupport,
}
return raviente
} }
// NewServer creates a new Server type. // NewServer creates a new Server type.
@@ -67,7 +146,10 @@ func NewServer(config *Config) *Server {
stages: make(map[string]*Stage), stages: make(map[string]*Stage),
userBinaryParts: make(map[userBinaryPartID][]byte), userBinaryParts: make(map[userBinaryPartID][]byte),
semaphore: make(map[string]*Semaphore), semaphore: make(map[string]*Semaphore),
discordSession: nil, discordBot: config.DiscordBot,
name: config.Name,
enable: config.Enable,
raviente: NewRaviente(),
} }
// Mezeporta // Mezeporta
@@ -88,7 +170,7 @@ func NewServer(config *Config) *Server {
// Rasta bar stage // Rasta bar stage
s.stages["sl1Ns211p0a0u0"] = NewStage("sl1Ns211p0a0u0") s.stages["sl1Ns211p0a0u0"] = NewStage("sl1Ns211p0a0u0")
// Carvane // Pallone Carvan
s.stages["sl1Ns260p0a0u0"] = NewStage("sl1Ns260p0a0u0") s.stages["sl1Ns260p0a0u0"] = NewStage("sl1Ns260p0a0u0")
// Gook Farm // Gook Farm
@@ -103,16 +185,6 @@ func NewServer(config *Config) *Server {
// MezFes // MezFes
s.stages["sl1Ns462p0a0u0"] = NewStage("sl1Ns462p0a0u0") 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 return s
} }
@@ -128,12 +200,8 @@ func (s *Server) Start(port int) error {
go s.manageSessions() go s.manageSessions()
// Start the discord bot for chat integration. // Start the discord bot for chat integration.
if s.erupeConfig.Discord.Enabled { if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
err = s.discordSession.Open() s.discordBot.Session.AddHandler(s.onDiscordMessage)
if err != nil {
s.logger.Warn("Error opening Discord session.", zap.Error(err))
return err
}
} }
return nil return nil
@@ -146,11 +214,8 @@ func (s *Server) Shutdown() {
s.Unlock() s.Unlock()
s.listener.Close() s.listener.Close()
close(s.acceptConns)
if s.erupeConfig.Discord.Enabled { close(s.acceptConns)
s.discordSession.Close()
}
} }
func (s *Server) acceptClients() { func (s *Server) acceptClients() {
@@ -232,7 +297,7 @@ func (s *Server) BroadcastChatMessage(message string) {
Type: 5, Type: 5,
Flags: 0x80, Flags: 0x80,
Message: message, Message: message,
SenderName: "Erupe", SenderName: s.name,
} }
msgBinChat.Build(bf) msgBinChat.Build(bf)
@@ -243,6 +308,13 @@ func (s *Server) BroadcastChatMessage(message string) {
}, nil) }, 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 { func (s *Server) FindSessionByCharID(charID uint32) *Session {
s.stagesLock.RLock() s.stagesLock.RLock()
defer s.stagesLock.RUnlock() defer s.stagesLock.RUnlock()
@@ -259,3 +331,21 @@ func (s *Server) FindSessionByCharID(charID uint32) *Session {
return nil 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 package channelserver
import ( import (
"sync"
"time"
"github.com/Andoryuuta/byteframe" "github.com/Andoryuuta/byteframe"
"github.com/Solenataris/Erupe/network/mhfpacket" "github.com/Solenataris/Erupe/network/mhfpacket"
"sync"
) )
// StageObject holds infomation about a specific stage object. // StageObject holds infomation about a specific stage object.
@@ -55,6 +58,7 @@ type Stage struct {
maxPlayers uint16 maxPlayers uint16
hasDeparted bool hasDeparted bool
password string password string
createdAt string
} }
// NewStage creates a new stage with intialized values. // NewStage creates a new stage with intialized values.
@@ -68,6 +72,7 @@ func NewStage(ID string) *Stage {
maxPlayers: 4, maxPlayers: 4,
gameObjectCount: 1, gameObjectCount: 1,
objectList: make(map[uint8]*ObjectMap), objectList: make(map[uint8]*ObjectMap),
createdAt: time.Now().Format("01-02-2006 15:04:05"),
} }
s.InitObjectList() s.InitObjectList()
return s return s
@@ -104,6 +109,47 @@ func (s *Stage) InitObjectList() {
} }
} }
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 { func (s *Stage) GetNewObjectID(CharID uint32) uint32 {
ObjId := uint8(0) ObjId := uint8(0)
for seq := uint8(0x7f); seq > uint8(0); seq-- { for seq := uint8(0x7f); seq > uint8(0); seq-- {

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)
}