Merge pull request #28 from Malckyor/main

Discord Integration Improvement/Road Shop Rotation/Item Distribution
This commit is contained in:
Malckyor
2022-06-15 08:17:20 +09:00
committed by GitHub
14 changed files with 1394 additions and 747 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

@@ -41,9 +41,12 @@ type SaveDumpOptions struct {
// Discord holds the discord integration config. // Discord holds the discord integration config.
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
} }

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

@@ -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

@@ -9,8 +9,8 @@ import (
// MsgMhfApplyDistItem represents the MSG_MHF_APPLY_DIST_ITEM // MsgMhfApplyDistItem represents the MSG_MHF_APPLY_DIST_ITEM
type MsgMhfApplyDistItem struct { type MsgMhfApplyDistItem struct {
AckHandle uint32 AckHandle uint32
Unk0 uint8 DistributionType uint8
RequestType uint32 DistributionID uint32
Unk2 uint32 Unk2 uint32
Unk3 uint32 Unk3 uint32
} }
@@ -23,8 +23,8 @@ func (m *MsgMhfApplyDistItem) Opcode() network.PacketID {
// Parse parses the packet from binary // Parse parses the packet from binary
func (m *MsgMhfApplyDistItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { func (m *MsgMhfApplyDistItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32() m.AckHandle = bf.ReadUint32()
m.Unk0 = bf.ReadUint8() m.DistributionType = bf.ReadUint8()
m.RequestType = bf.ReadUint32() m.DistributionID = bf.ReadUint32()
m.Unk2 = bf.ReadUint32() m.Unk2 = bf.ReadUint32()
m.Unk3 = bf.ReadUint32() m.Unk3 = bf.ReadUint32()
return nil return nil
@@ -33,8 +33,8 @@ func (m *MsgMhfApplyDistItem) Parse(bf *byteframe.ByteFrame, ctx *clientctx.Clie
// Build builds a binary packet from the current data. // Build builds a binary packet from the current data.
func (m *MsgMhfApplyDistItem) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { func (m *MsgMhfApplyDistItem) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
bf.WriteUint32(m.AckHandle) bf.WriteUint32(m.AckHandle)
bf.WriteUint8(m.Unk0) bf.WriteUint8(m.DistributionType)
bf.WriteUint32(m.RequestType) bf.WriteUint32(m.DistributionID)
bf.WriteUint32(m.Unk2) bf.WriteUint32(m.Unk2)
bf.WriteUint32(m.Unk3) bf.WriteUint32(m.Unk3)
return nil return nil

View File

@@ -12,7 +12,7 @@ import (
type MsgMhfGetDistDescription struct{ type MsgMhfGetDistDescription struct{
AckHandle uint32 AckHandle uint32
Unk0 uint8 Unk0 uint8
EntryID uint32 DistributionID uint32
} }
// Opcode returns the ID associated with this packet type. // Opcode returns the ID associated with this packet type.
@@ -24,7 +24,7 @@ func (m *MsgMhfGetDistDescription) Opcode() network.PacketID {
func (m *MsgMhfGetDistDescription) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { func (m *MsgMhfGetDistDescription) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32() m.AckHandle = bf.ReadUint32()
m.Unk0 = bf.ReadUint8() m.Unk0 = bf.ReadUint8()
m.EntryID = bf.ReadUint32() m.DistributionID = bf.ReadUint32()
return nil return nil
} }
// Build builds a binary packet from the current data. // Build builds a binary packet from the current data.

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

@@ -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"
) )
// onDiscordMessage handles receiving messages from discord and forwarding them ingame. type Character struct {
func (s *Server) onDiscordMessage(ds *discordgo.Session, m *discordgo.MessageCreate) { ID uint32 `db:"id"`
// Ignore messages from our bot, or ones that are not in the correct channel. IsFemale bool `db:"is_female"`
if m.Author.ID == ds.State.User.ID || m.ChannelID != s.erupeConfig.Discord.ChannelID { IsNewCharacter bool `db:"is_new_character"`
return Name string `db:"name"`
} UnkDescString string `db:"unk_desc_string"`
HRP uint16 `db:"hrp"`
// Broadcast to the game clients. GR uint16 `db:"gr"`
data := m.Content WeaponType uint16 `db:"weapon_type"`
LastLogin uint32 `db:"last_login"`
// 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)
} }
func dayConvert(result string) string { var weapons = []string{
var replaceDays 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" { func (s *Server) getCharacterForUser(uid int) (*Character, error) {
replaceDays = "Lundi" character := Character{}
} else if result == "2" { 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)
replaceDays = "Mardi" if err != nil {
} else if result == "3" { return nil, err
replaceDays = "Mercredi" }
} else if result == "4" {
replaceDays = "Jeudi" return &character, nil
} else if result == "5" { }
replaceDays = "Vendredi"
} else if result == "6" { func CountChars(s *Server) string {
replaceDays = "Samedi" count := 0
} else if result == "7" { for _, stage := range s.stages {
replaceDays = "Dimanche" 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 { } 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 { func getPlayerList(s *Server) ([]ListPlayer, int) {
var replaceMonth string list := []ListPlayer{}
questEmojis := []string{
if result == "01" { ":white_circle:",
replaceMonth = "Janvier" ":red_circle:",
} else if result == "02" { ":blue_circle:",
replaceMonth = "Fevrier" ":brown_circle:",
} else if result == "03" { ":green_circle:",
replaceMonth = "Mars" ":purple_circle:",
} else if result == "04" { ":yellow_circle:",
replaceMonth = "Avril" ":orange_circle:",
} 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 bigNameLen := 0
}
func (s *Server) TimerUpdate(timer int, typeStop int, disableAutoOff bool) { for _, stage := range s.stages {
timertotal := 0 if len(stage.clients) == 0 {
for timer > 0 { continue
time.Sleep(1 * time.Minute) }
timer -= 1
timertotal += 1 questEmoji := ":black_circle:"
if disableAutoOff {
// Un message s'affiche toutes les 10 minutes pour prévenir de la maintenance. if len(questEmojis) > 0 {
if timertotal == 10 { questEmoji = questEmojis[len(questEmojis)-1]
timertotal = 0 questEmojis = questEmojis[:len(questEmojis)-1]
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)) isQuest := stage.isQuest()
s.BroadcastChatMessage("vous connecter ou deconnectez-vous maintenant, afin de ne pas") for client := range stage.clients {
s.BroadcastChatMessage("perturber les operations de maintenance. Veuillez nous excuser") char, err := s.getCharacterForUser(int(client.charID))
s.BroadcastChatMessage("pour la gene occasionnee. Merci de votre cooperation.") if err == nil {
} else { if len(char.Name) > bigNameLen {
s.BroadcastChatMessage("RAPPEL DE MAINTENANCE EXCEPTIONNELLE: Les serveurs seront") bigNameLen = len(char.Name)
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. list = append(list, ListPlayer{
if timer == 0 { CharName: char.Name,
os.Exit(-1) 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

@@ -1,67 +1,137 @@
package channelserver package channelserver
import ( import (
"encoding/hex"
// "io/ioutil"
// "path/filepath"
"github.com/Solenataris/Erupe/network/mhfpacket" "github.com/Solenataris/Erupe/network/mhfpacket"
"github.com/Solenataris/Erupe/common/stringsupport"
"github.com/Andoryuuta/byteframe"
"go.uber.org/zap"
) )
func handleMsgMhfEnumerateDistItem(s *Session, p mhfpacket.MHFPacket) { type ItemDist struct {
pkt := p.(*mhfpacket.MsgMhfEnumerateDistItem) ID uint32 `db:"id"`
// uint16 number of entries Deadline uint32 `db:"deadline"`
// 446 entry block TimesAcceptable uint16 `db:"times_acceptable"`
// uint32 claimID TimesAccepted uint16 `db:"times_accepted"`
// 00 00 00 00 00 00 MinHR uint16 `db:"min_hr"`
// uint16 timesClaimable MaxHR uint16 `db:"max_hr"`
// 00 00 00 00 FF FF FF FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 00 00 00 MinSR uint16 `db:"min_sr"`
// uint8 stringLength MaxSR uint16 `db:"max_sr"`
// string nullTermString MinGR uint16 `db:"min_gr"`
data, _ := hex.DecodeString("0001000000010000000000000000002000000000FFFFFFFFFFFFFFFFFFFFFFFF0000000000000000002F323020426F7820457870616E73696F6E73000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") MaxGR uint16 `db:"max_gr"`
doAckBufSucceed(s, pkt.AckHandle, data) EventName string `db:"event_name"`
Description string `db:"description"`
Data []byte `db:"data"`
}
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,
min_hr, max_hr, min_sr, max_sr, min_gr, max_gr,
(
SELECT count(*)
FROM distributions_accepted da
WHERE d.id = da.distribution_id
AND da.character_id = $1
) AS times_accepted,
CASE
WHEN (EXTRACT(epoch FROM deadline)::int) IS NULL THEN 0
ELSE (EXTRACT(epoch FROM deadline)::int)
END deadline
FROM distribution d
WHERE character_id = $1 AND type = $2 OR character_id IS NULL AND type = $2 ORDER BY id DESC;
`, s.charID, pkt.Unk0)
if err != nil {
s.logger.Error("Error getting distribution data from db", zap.Error(err))
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
} else {
for dists.Next() {
distCount++
distData := &ItemDist{}
err = dists.StructScan(&distData)
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
bf.WriteUint16(distData.TimesAcceptable)
bf.WriteUint16(distData.TimesAccepted)
bf.WriteUint16(0) // Unk
bf.WriteUint16(distData.MinHR)
bf.WriteUint16(distData.MaxHR)
bf.WriteUint16(distData.MinSR)
bf.WriteUint16(distData.MaxSR)
bf.WriteUint16(distData.MinGR)
bf.WriteUint16(distData.MaxGR)
bf.WriteUint32(0) // Unk
bf.WriteUint32(0) // Unk
eventName, _ := stringsupport.ConvertUTF8ToShiftJIS(distData.EventName)
bf.WriteUint16(uint16(len(eventName)+1))
bf.WriteNullTerminatedBytes(eventName)
bf.WriteBytes(make([]byte, 391))
}
resp := byteframe.NewByteFrame()
resp.WriteUint16(uint16(distCount))
resp.WriteBytes(bf.Data())
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
}
} }
func handleMsgMhfApplyDistItem(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfApplyDistItem(s *Session, p mhfpacket.MHFPacket) {
// 0052a49100011f00000000000000010274db99 equipment box page pkt := p.(*mhfpacket.MsgMhfApplyDistItem)
// 0052a48f00011e0000000000000001195dda5c item box page
// 0052a49400010700003ae30000000132d3a4d6 Item ID 3AE3 if pkt.DistributionID == 0 {
// HEADER: doAckBufSucceed(s, pkt.AckHandle, make([]byte, 6))
// int32: Unique item hash for tracking server side purchases? Swapping across items didn't change image/cost/function etc. } else {
// int16: Number of distributed item types row := s.server.db.QueryRowx("SELECT data FROM distribution WHERE id = $1", pkt.DistributionID)
// ITEM ENTRY dist := &ItemDist{}
// int8: distribution type err := row.StructScan(dist)
// 00 = legs, 01 = Head, 02 = Chest, 03 = Arms, 04 = Waist, 05 = Melee, 06 = Ranged, 07 = Item, 08 == furniture if err != nil {
// ids are wrong shop displays in random order s.logger.Error("Error parsing item distribution data", zap.Error(err))
// 09 = Nothing, 10 = Null Point, 11 = Festi Point, 12 = Zeny, 13 = Null, 14 = Null Points, 15 = My Tore points doAckBufSucceed(s, pkt.AckHandle, make([]byte, 6))
// 16 = Restyle Point, 17 = N Points, 18 = Nothing, 19 = Gacha Coins, 20 = Trial Gacha Coins, 21 = Frontier points return
// 22 = ?, 23 = Guild Points, 30 = Item Box Page, 31 = Equipment Box Page }
// int16: Unk
// int16: Item when type 07 bf := byteframe.NewByteFrame()
// int16: Unk bf.WriteUint32(0)
// int16: Number delivered in batch bf.WriteBytes(dist.Data)
// int32: Unique item hash for tracking server side purchases? Swapping across items didn't change image/cost/function etc. doAckBufSucceed(s, pkt.AckHandle, bf.Data())
pkt := p.(*mhfpacket.MsgMhfApplyDistItem)
if pkt.RequestType == 0 { _, err = s.server.db.Exec(`
doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) INSERT INTO public.distributions_accepted
} else if pkt.RequestType == 0x00000001 { VALUES ($1, $2)
data, _ := hex.DecodeString("0052a494001e0f000000010000000132d3a4d60f000000020000000132d3a4d60f000000030000000132d3a4d60f000000040000000132d3a4d60f000000050000000132d3a4d60f000000060000000132d3a4d60f000000070000000132d3a4d60f000000080000000132d3a4d60f000000090000000132d3a4d60f0000000a0000000132d3a4d60f0000000b0000000132d3a4d60f0000000c0000000132d3a4d60f0000000d0000000132d3a4d60f0000000e0000000132d3a4d60f0000000f0000000132d3a4d60f000000100000000132d3a4d60f000000110000000132d3a4d60f000000120000000132d3a4d60f000000130000000132d3a4d60f000000140000000132d3a4d60f000000150000000132d3a4d60f000000160000000132d3a4d60f000000170000000132d3a4d60f000000180000000132d3a4d60f000000190000000132d3a4d60f0000001a0000000132d3a4d60f0000001b0000000132d3a4d60f0000001c0000000132d3a4d60f0000001d0000000132d3a4d60f0000001e0000000132d3a4d6") `, pkt.DistributionID, s.charID)
doAckBufSucceed(s, pkt.AckHandle, data) if err != nil {
} else { s.logger.Error("Error updating accepted dist count", zap.Error(err))
doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) }
} }
} }
func handleMsgMhfAcquireDistItem(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAcquireDistItem(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfAcquireDistItem) pkt := p.(*mhfpacket.MsgMhfAcquireDistItem)
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
} }
func handleMsgMhfGetDistDescription(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetDistDescription(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetDistDescription) pkt := p.(*mhfpacket.MsgMhfGetDistDescription)
// string for the associated message
data, _ := hex.DecodeString("007E43303547656E65726963204974656D20436C61696D204D6573736167657E4330300D0A596F752067657420736F6D65206B696E64206F66206974656D732070726F6261626C792E00000100") var itemDesc string
//data, _ := hex.DecodeString("0075b750c1c2b17ac1cab652a1757e433035b8cbb3c6bd63c258b169aa41b0c87e433030a1760a0aa175b8cbb3c6bd63c258b169aa41b0c8a176a843c1caa44a31a6b8a141a569c258b169a2b0add30aa8a4a6e2aabaa175b8cbb3c6bd63a176a2b0adb6a143b3cca668a569c258b169a2b4adb6a14300000100") err := s.server.db.QueryRow("SELECT description FROM distribution WHERE id = $1", pkt.DistributionID).Scan(&itemDesc)
doAckBufSucceed(s, pkt.AckHandle, data)
if err != nil {
s.logger.Error("Error parsing item distribution description", zap.Error(err))
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
bf := byteframe.NewByteFrame()
description, _ := stringsupport.ConvertUTF8ToShiftJIS(itemDesc)
bf.WriteUint16(uint16(len(description)+1))
bf.WriteNullTerminatedBytes(description)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
} }

View File

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

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.
@@ -30,14 +50,13 @@ type userBinaryPartID struct {
// Server is a MHF channel server. // Server is a MHF channel server.
type Server struct { type Server struct {
sync.Mutex sync.Mutex
logger *zap.Logger logger *zap.Logger
db *sqlx.DB db *sqlx.DB
erupeConfig *config.Config erupeConfig *config.Config
acceptConns chan net.Conn acceptConns chan net.Conn
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,12 +71,72 @@ 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.
func NewServer(config *Config) *Server { func NewServer(config *Config) *Server {
s := &Server{ s := &Server {
logger: config.Logger, logger: config.Logger,
db: config.DB, db: config.DB,
erupeConfig: config.ErupeConfig, erupeConfig: config.ErupeConfig,
@@ -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.
@@ -15,7 +18,7 @@ type StageObject struct {
} }
type ObjectMap struct { type ObjectMap struct {
id uint8 id uint8
charid uint32 charid uint32
status bool status bool
} }
@@ -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
@@ -94,30 +99,71 @@ func (s *Stage) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session) {
} }
func (s *Stage) InitObjectList() { func (s *Stage) InitObjectList() {
for seq:=uint8(0x7f);seq>uint8(0);seq-- { for seq := uint8(0x7f); seq > uint8(0); seq-- {
newObj := &ObjectMap{ newObj := &ObjectMap{
id: seq, id: seq,
charid: uint32(0), charid: uint32(0),
status: false, status: false,
}
s.objectList[seq] = newObj
} }
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 { 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-- {
if s.objectList[seq].status == false { if s.objectList[seq].status == false {
ObjId=seq ObjId = seq
break break
} }
} }
s.objectList[ObjId].status=true s.objectList[ObjId].status = true
s.objectList[ObjId].charid=CharID s.objectList[ObjId].charid = CharID
bf := byteframe.NewByteFrame() bf := byteframe.NewByteFrame()
bf.WriteUint8(uint8(0)) bf.WriteUint8(uint8(0))
bf.WriteUint8(ObjId) bf.WriteUint8(ObjId)
bf.WriteUint16(uint16(0)) 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 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)
}