mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
Add zero-dependency SQLite mode so users can run Erupe without
PostgreSQL. A transparent db.DB wrapper auto-translates PostgreSQL
SQL ($N placeholders, now(), ::casts, ILIKE, public. prefix,
TRUNCATE) for SQLite at runtime — all 28 repo files use the wrapper
with no per-query changes needed.
Setup wizard gains two new steps: quest file detection with download
link, and gameplay presets (solo/small/community/rebalanced). The API
server gets a /dashboard endpoint with auto-refreshing stats.
CI release workflow now builds and pushes Docker images to GHCR
alongside binary artifacts on tag push.
Key changes:
- common/db: DB/Tx wrapper with 6 SQL translation rules
- server/migrations/sqlite: full SQLite schema (0001-0005)
- config: Database.Driver field ("postgres" or "sqlite")
- main.go: SQLite connection with WAL mode, single writer
- server/setup: quest check + preset selection steps
- server/api: /dashboard with live stats
- .github/workflows: Docker in release, deduplicate docker.yml
259 lines
8.6 KiB
Go
259 lines
8.6 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"erupe-ce/common/byteframe"
|
|
"erupe-ce/common/mhfcourse"
|
|
ps "erupe-ce/common/pascalstring"
|
|
cfg "erupe-ce/config"
|
|
"erupe-ce/network/mhfpacket"
|
|
"fmt"
|
|
"go.uber.org/zap"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
func handleMsgMhfAcquireCafeItem(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfAcquireCafeItem)
|
|
netcafePoints, err := adjustCharacterInt(s, "netcafe_points", -int(pkt.PointCost))
|
|
if err != nil {
|
|
s.logger.Error("Failed to deduct netcafe points", zap.Error(err))
|
|
}
|
|
resp := byteframe.NewByteFrame()
|
|
resp.WriteUint32(uint32(netcafePoints))
|
|
doAckSimpleSucceed(s, pkt.AckHandle, resp.Data())
|
|
}
|
|
|
|
func handleMsgMhfUpdateCafepoint(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfUpdateCafepoint)
|
|
netcafePoints, err := readCharacterInt(s, "netcafe_points")
|
|
if err != nil {
|
|
s.logger.Error("Failed to get netcafe points", zap.Error(err))
|
|
}
|
|
resp := byteframe.NewByteFrame()
|
|
resp.WriteUint32(uint32(netcafePoints))
|
|
doAckSimpleSucceed(s, pkt.AckHandle, resp.Data())
|
|
}
|
|
|
|
func handleMsgMhfCheckDailyCafepoint(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfCheckDailyCafepoint)
|
|
|
|
midday := TimeMidnight().Add(12 * time.Hour)
|
|
if TimeAdjusted().After(midday) {
|
|
midday = midday.Add(24 * time.Hour)
|
|
}
|
|
|
|
// get time after which daily claiming would be valid from db
|
|
dailyTime, err := s.server.charRepo.ReadTime(s.charID, "daily_time", time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
if err != nil {
|
|
s.logger.Error("Failed to get daily_time savedata from db", zap.Error(err))
|
|
}
|
|
|
|
var bondBonus, bonusQuests, dailyQuests uint32
|
|
bf := byteframe.NewByteFrame()
|
|
if midday.After(dailyTime) {
|
|
if err := addPointNetcafe(s, 5); err != nil {
|
|
s.logger.Error("Failed to add daily netcafe points", zap.Error(err))
|
|
}
|
|
bondBonus = 5 // Bond point bonus quests
|
|
bonusQuests = s.server.erupeConfig.GameplayOptions.BonusQuestAllowance
|
|
dailyQuests = s.server.erupeConfig.GameplayOptions.DailyQuestAllowance
|
|
if err := s.server.charRepo.UpdateDailyCafe(s.charID, midday, bonusQuests, dailyQuests); err != nil {
|
|
s.logger.Error("Failed to update daily cafe data", zap.Error(err))
|
|
}
|
|
bf.WriteBool(true) // Success?
|
|
} else {
|
|
bf.WriteBool(false)
|
|
}
|
|
bf.WriteUint32(bondBonus)
|
|
bf.WriteUint32(bonusQuests)
|
|
bf.WriteUint32(dailyQuests)
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetCafeDuration)
|
|
bf := byteframe.NewByteFrame()
|
|
|
|
cafeReset, err := s.server.charRepo.ReadTime(s.charID, "cafe_reset", time.Time{})
|
|
if err != nil {
|
|
cafeReset = TimeWeekNext()
|
|
if err := s.server.charRepo.SaveTime(s.charID, "cafe_reset", cafeReset); err != nil {
|
|
s.logger.Error("Failed to set cafe reset time", zap.Error(err))
|
|
}
|
|
}
|
|
if TimeAdjusted().After(cafeReset) {
|
|
cafeReset = TimeWeekNext()
|
|
if err := s.server.charRepo.ResetCafeTime(s.charID, cafeReset); err != nil {
|
|
s.logger.Error("Failed to reset cafe time", zap.Error(err))
|
|
}
|
|
if err := s.server.cafeRepo.ResetAccepted(s.charID); err != nil {
|
|
s.logger.Error("Failed to delete accepted cafe bonuses", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
cafeTime, err := readCharacterInt(s, "cafe_time")
|
|
if err != nil {
|
|
s.logger.Error("Failed to get cafe time", zap.Error(err))
|
|
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
|
return
|
|
}
|
|
if mhfcourse.CourseExists(30, s.courses) {
|
|
cafeTime = int(TimeAdjusted().Unix()) - int(s.sessionStart) + cafeTime
|
|
}
|
|
bf.WriteUint32(uint32(cafeTime))
|
|
if s.server.erupeConfig.RealClientMode >= cfg.ZZ {
|
|
bf.WriteUint16(0)
|
|
ps.Uint16(bf, fmt.Sprintf(s.server.i18n.cafe.reset, int(cafeReset.Month()), cafeReset.Day()), true)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
// CafeBonus represents a cafe duration bonus reward entry.
|
|
type CafeBonus struct {
|
|
ID uint32 `db:"id"`
|
|
TimeReq uint32 `db:"time_req"`
|
|
ItemType uint32 `db:"item_type"`
|
|
ItemID uint32 `db:"item_id"`
|
|
Quantity uint32 `db:"quantity"`
|
|
Claimed bool `db:"claimed"`
|
|
}
|
|
|
|
func handleMsgMhfGetCafeDurationBonusInfo(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetCafeDurationBonusInfo)
|
|
|
|
bonuses, err := s.server.cafeRepo.GetBonuses(s.charID)
|
|
if err != nil {
|
|
s.logger.Error("Error getting cafebonus", zap.Error(err))
|
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
return
|
|
}
|
|
bf := byteframe.NewByteFrame()
|
|
for _, cb := range bonuses {
|
|
bf.WriteUint32(cb.TimeReq)
|
|
bf.WriteUint32(cb.ItemType)
|
|
bf.WriteUint32(cb.ItemID)
|
|
bf.WriteUint32(cb.Quantity)
|
|
bf.WriteBool(cb.Claimed)
|
|
}
|
|
resp := byteframe.NewByteFrame()
|
|
resp.WriteUint32(0)
|
|
resp.WriteUint32(uint32(TimeAdjusted().Unix()))
|
|
resp.WriteUint32(uint32(len(bonuses)))
|
|
resp.WriteBytes(bf.Data())
|
|
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
|
|
}
|
|
|
|
func handleMsgMhfReceiveCafeDurationBonus(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfReceiveCafeDurationBonus)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(0)
|
|
claimable, err := s.server.cafeRepo.GetClaimable(s.charID, TimeAdjusted().Unix()-s.sessionStart)
|
|
if err != nil || !mhfcourse.CourseExists(30, s.courses) {
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
} else {
|
|
for _, cb := range claimable {
|
|
bf.WriteUint32(cb.ID)
|
|
bf.WriteUint32(cb.ItemType)
|
|
bf.WriteUint32(cb.ItemID)
|
|
bf.WriteUint32(cb.Quantity)
|
|
}
|
|
_, _ = bf.Seek(0, io.SeekStart)
|
|
bf.WriteUint32(uint32(len(claimable)))
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
}
|
|
|
|
func handleMsgMhfPostCafeDurationBonusReceived(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfPostCafeDurationBonusReceived)
|
|
for _, cbID := range pkt.CafeBonusID {
|
|
itemType, quantity, err := s.server.cafeRepo.GetBonusItem(cbID)
|
|
if err == nil {
|
|
if itemType == 17 {
|
|
if err := addPointNetcafe(s, int(quantity)); err != nil {
|
|
s.logger.Error("Failed to add cafe bonus netcafe points", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
if err := s.server.cafeRepo.AcceptBonus(cbID, s.charID); err != nil {
|
|
s.logger.Error("Failed to insert accepted cafe bonus", zap.Error(err))
|
|
}
|
|
}
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
func addPointNetcafe(s *Session, p int) error {
|
|
points, err := readCharacterInt(s, "netcafe_points")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
points = min(points+p, s.server.erupeConfig.GameplayOptions.MaximumNP)
|
|
if err := s.server.charRepo.SaveInt(s.charID, "netcafe_points", points); err != nil {
|
|
s.logger.Error("Failed to update netcafe points", zap.Error(err))
|
|
return fmt.Errorf("save netcafe points: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleMsgMhfStartBoostTime(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfStartBoostTime)
|
|
bf := byteframe.NewByteFrame()
|
|
boostLimit := TimeAdjusted().Add(time.Duration(s.server.erupeConfig.GameplayOptions.BoostTimeDuration) * time.Second)
|
|
if s.server.erupeConfig.GameplayOptions.DisableBoostTime {
|
|
bf.WriteUint32(0)
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
return
|
|
}
|
|
if err := s.server.charRepo.SaveTime(s.charID, "boost_time", boostLimit); err != nil {
|
|
s.logger.Error("Failed to update boost time", zap.Error(err))
|
|
}
|
|
bf.WriteUint32(uint32(boostLimit.Unix()))
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfGetBoostTime(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetBoostTime)
|
|
doAckBufSucceed(s, pkt.AckHandle, []byte{})
|
|
}
|
|
|
|
func handleMsgMhfGetBoostTimeLimit(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetBoostTimeLimit)
|
|
bf := byteframe.NewByteFrame()
|
|
boostLimit, err := s.server.charRepo.ReadTime(s.charID, "boost_time", time.Time{})
|
|
if err != nil {
|
|
bf.WriteUint32(0)
|
|
} else {
|
|
bf.WriteUint32(uint32(boostLimit.Unix()))
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
func handleMsgMhfGetBoostRight(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetBoostRight)
|
|
boostLimit, err := s.server.charRepo.ReadTime(s.charID, "boost_time", time.Time{})
|
|
if err != nil {
|
|
doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
|
return
|
|
}
|
|
if boostLimit.After(TimeAdjusted()) {
|
|
doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x01})
|
|
} else {
|
|
doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x02})
|
|
}
|
|
}
|
|
|
|
func handleMsgMhfPostBoostTimeQuestReturn(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfPostBoostTimeQuestReturn)
|
|
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
|
}
|
|
|
|
func handleMsgMhfPostBoostTime(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfPostBoostTime)
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
func handleMsgMhfPostBoostTimeLimit(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfPostBoostTimeLimit)
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|