refactor: extract gametime package, replace fmt.Printf with zap logging

Move time utilities (TimeAdjusted, TimeMidnight, TimeWeekStart, TimeWeekNext,
TimeGameAbsolute) from channelserver into common/gametime to break the
inappropriate dependency where signserver, entranceserver, and api imported
the 38K-line channelserver package just for time functions.

Replace all fmt.Printf debug logging in sys_session.go and handlers_object.go
with structured zap logging for consistent observability.
This commit is contained in:
Houmgaor
2026-02-17 17:54:51 +01:00
parent fb3e86f429
commit d2b5bb72f8
14 changed files with 258 additions and 264 deletions

View File

@@ -0,0 +1,32 @@
package gametime
import (
"time"
)
func Adjusted() time.Time {
baseTime := time.Now().In(time.FixedZone("UTC+9", 9*60*60))
return time.Date(baseTime.Year(), baseTime.Month(), baseTime.Day(), baseTime.Hour(), baseTime.Minute(), baseTime.Second(), baseTime.Nanosecond(), baseTime.Location())
}
func Midnight() time.Time {
baseTime := time.Now().In(time.FixedZone("UTC+9", 9*60*60))
return time.Date(baseTime.Year(), baseTime.Month(), baseTime.Day(), 0, 0, 0, 0, baseTime.Location())
}
func WeekStart() time.Time {
midnight := Midnight()
offset := int(midnight.Weekday()) - int(time.Monday)
if offset < 0 {
offset += 7
}
return midnight.Add(-time.Duration(offset) * 24 * time.Hour)
}
func WeekNext() time.Time {
return WeekStart().Add(time.Hour * 24 * 7)
}
func GameAbsolute() uint32 {
return uint32((Adjusted().Unix() - 2160) % 5760)
}

View File

@@ -0,0 +1,157 @@
package gametime
import (
"testing"
"time"
)
func TestAdjusted(t *testing.T) {
result := Adjusted()
_, offset := result.Zone()
expectedOffset := 9 * 60 * 60
if offset != expectedOffset {
t.Errorf("Adjusted() zone offset = %d, want %d (UTC+9)", offset, expectedOffset)
}
now := time.Now()
diff := result.Sub(now.In(time.FixedZone("UTC+9", 9*60*60)))
if diff < -time.Second || diff > time.Second {
t.Errorf("Adjusted() time differs from expected by %v", diff)
}
}
func TestMidnight(t *testing.T) {
midnight := Midnight()
if midnight.Hour() != 0 {
t.Errorf("Midnight() hour = %d, want 0", midnight.Hour())
}
if midnight.Minute() != 0 {
t.Errorf("Midnight() minute = %d, want 0", midnight.Minute())
}
if midnight.Second() != 0 {
t.Errorf("Midnight() second = %d, want 0", midnight.Second())
}
if midnight.Nanosecond() != 0 {
t.Errorf("Midnight() nanosecond = %d, want 0", midnight.Nanosecond())
}
_, offset := midnight.Zone()
expectedOffset := 9 * 60 * 60
if offset != expectedOffset {
t.Errorf("Midnight() zone offset = %d, want %d (UTC+9)", offset, expectedOffset)
}
}
func TestWeekStart(t *testing.T) {
weekStart := WeekStart()
if weekStart.Weekday() != time.Monday {
t.Errorf("WeekStart() weekday = %v, want Monday", weekStart.Weekday())
}
if weekStart.Hour() != 0 || weekStart.Minute() != 0 || weekStart.Second() != 0 {
t.Errorf("WeekStart() should be at midnight, got %02d:%02d:%02d",
weekStart.Hour(), weekStart.Minute(), weekStart.Second())
}
_, offset := weekStart.Zone()
expectedOffset := 9 * 60 * 60
if offset != expectedOffset {
t.Errorf("WeekStart() zone offset = %d, want %d (UTC+9)", offset, expectedOffset)
}
midnight := Midnight()
if weekStart.After(midnight) {
t.Errorf("WeekStart() %v should be <= current midnight %v", weekStart, midnight)
}
}
func TestWeekNext(t *testing.T) {
weekStart := WeekStart()
weekNext := WeekNext()
expectedNext := weekStart.Add(time.Hour * 24 * 7)
if !weekNext.Equal(expectedNext) {
t.Errorf("WeekNext() = %v, want %v (7 days after WeekStart)", weekNext, expectedNext)
}
if weekNext.Weekday() != time.Monday {
t.Errorf("WeekNext() weekday = %v, want Monday", weekNext.Weekday())
}
if weekNext.Hour() != 0 || weekNext.Minute() != 0 || weekNext.Second() != 0 {
t.Errorf("WeekNext() should be at midnight, got %02d:%02d:%02d",
weekNext.Hour(), weekNext.Minute(), weekNext.Second())
}
if !weekNext.After(weekStart) {
t.Errorf("WeekNext() %v should be after WeekStart() %v", weekNext, weekStart)
}
}
func TestWeekStartSundayEdge(t *testing.T) {
weekStart := WeekStart()
if weekStart.Weekday() != time.Monday {
t.Errorf("WeekStart() on any day should return Monday, got %v", weekStart.Weekday())
}
}
func TestMidnightSameDay(t *testing.T) {
adjusted := Adjusted()
midnight := Midnight()
if midnight.Year() != adjusted.Year() ||
midnight.Month() != adjusted.Month() ||
midnight.Day() != adjusted.Day() {
t.Errorf("Midnight() date = %v, want same day as Adjusted() %v",
midnight.Format("2006-01-02"), adjusted.Format("2006-01-02"))
}
}
func TestWeekDuration(t *testing.T) {
weekStart := WeekStart()
weekNext := WeekNext()
duration := weekNext.Sub(weekStart)
expectedDuration := time.Hour * 24 * 7
if duration != expectedDuration {
t.Errorf("Duration between WeekStart and WeekNext = %v, want %v", duration, expectedDuration)
}
}
func TestTimeZoneConsistency(t *testing.T) {
adjusted := Adjusted()
midnight := Midnight()
weekStart := WeekStart()
weekNext := WeekNext()
times := []struct {
name string
time time.Time
}{
{"Adjusted", adjusted},
{"Midnight", midnight},
{"WeekStart", weekStart},
{"WeekNext", weekNext},
}
expectedOffset := 9 * 60 * 60
for _, tt := range times {
_, offset := tt.time.Zone()
if offset != expectedOffset {
t.Errorf("%s() zone offset = %d, want %d (UTC+9)", tt.name, offset, expectedOffset)
}
}
}
func TestGameAbsolute(t *testing.T) {
result := GameAbsolute()
if result >= 5760 {
t.Errorf("GameAbsolute() = %d, should be < 5760", result)
}
}

View File

@@ -10,6 +10,7 @@ import (
"syscall" "syscall"
"time" "time"
"erupe-ce/common/gametime"
"erupe-ce/server/api" "erupe-ce/server/api"
"erupe-ce/server/channelserver" "erupe-ce/server/channelserver"
"erupe-ce/server/discordbot" "erupe-ce/server/discordbot"
@@ -142,7 +143,7 @@ func main() {
logger.Info("Database: Finished clearing") logger.Info("Database: Finished clearing")
} }
logger.Info(fmt.Sprintf("Server Time: %s", channelserver.TimeAdjusted().String())) logger.Info(fmt.Sprintf("Server Time: %s", gametime.Adjusted().String()))
// Now start our server(s). // Now start our server(s).

View File

@@ -1,4 +1,4 @@
package clientctx package clientctx
// ClientContext holds contextual data required for packet encoding/decoding. // ClientContext holds contextual data required for packet encoding/decoding.
type ClientContext struct{} // Unused type ClientContext struct{}

View File

@@ -5,27 +5,8 @@ import (
) )
// TestClientContext_Exists verifies that the ClientContext type exists // TestClientContext_Exists verifies that the ClientContext type exists
// and can be instantiated, even though it's currently unused. // and can be instantiated.
func TestClientContext_Exists(t *testing.T) { func TestClientContext_Exists(t *testing.T) {
// This test documents that ClientContext is currently an empty struct
// and is marked as unused in the codebase.
var ctx ClientContext
// Verify it's a zero-size struct
_ = ctx
// Just verify we can create it
ctx2 := ClientContext{}
_ = ctx2
}
// TestClientContext_IsEmpty verifies that ClientContext has no fields
func TestClientContext_IsEmpty(t *testing.T) {
// The struct should be empty as marked by the comment "// Unused"
// This test documents the current state of the struct
ctx := ClientContext{} ctx := ClientContext{}
_ = ctx _ = ctx
// If fields are added in the future, this test will need to be updated
// Currently it's just a placeholder/documentation test
} }

View File

@@ -6,7 +6,7 @@ import (
"encoding/xml" "encoding/xml"
"errors" "errors"
_config "erupe-ce/config" _config "erupe-ce/config"
"erupe-ce/server/channelserver" "erupe-ce/common/gametime"
"fmt" "fmt"
"image" "image"
"image/jpeg" "image/jpeg"
@@ -77,7 +77,7 @@ type ExportData struct {
func (s *APIServer) newAuthData(userID uint32, userRights uint32, userTokenID uint32, userToken string, characters []Character) AuthData { func (s *APIServer) newAuthData(userID uint32, userRights uint32, userTokenID uint32, userToken string, characters []Character) AuthData {
resp := AuthData{ resp := AuthData{
CurrentTS: uint32(channelserver.TimeAdjusted().Unix()), CurrentTS: uint32(gametime.Adjusted().Unix()),
ExpiryTS: uint32(s.getReturnExpiry(userID).Unix()), ExpiryTS: uint32(s.getReturnExpiry(userID).Unix()),
EntranceCount: 1, EntranceCount: 1,
User: User{ User: User{
@@ -99,9 +99,9 @@ func (s *APIServer) newAuthData(userID uint32, userRights uint32, userTokenID ui
stalls[4] = 2 stalls[4] = 2
} }
resp.MezFes = &MezFes{ resp.MezFes = &MezFes{
ID: uint32(channelserver.TimeWeekStart().Unix()), ID: uint32(gametime.WeekStart().Unix()),
Start: uint32(channelserver.TimeWeekStart().Add(-time.Duration(s.erupeConfig.GameplayOptions.MezFesDuration) * time.Second).Unix()), Start: uint32(gametime.WeekStart().Add(-time.Duration(s.erupeConfig.GameplayOptions.MezFesDuration) * time.Second).Unix()),
End: uint32(channelserver.TimeWeekNext().Unix()), End: uint32(gametime.WeekNext().Unix()),
SoloTickets: s.erupeConfig.GameplayOptions.MezFesSoloTickets, SoloTickets: s.erupeConfig.GameplayOptions.MezFesSoloTickets,
GroupTickets: s.erupeConfig.GameplayOptions.MezFesGroupTickets, GroupTickets: s.erupeConfig.GameplayOptions.MezFesGroupTickets,
Stalls: stalls, Stalls: stalls,
@@ -118,7 +118,7 @@ func (s *APIServer) Launcher(w http.ResponseWriter, r *http.Request) {
respData.Messages = s.erupeConfig.API.Messages respData.Messages = s.erupeConfig.API.Messages
respData.Links = s.erupeConfig.API.Links respData.Links = s.erupeConfig.API.Links
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(respData) _ = json.NewEncoder(w).Encode(respData)
} }
func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) { func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) {
@@ -140,7 +140,7 @@ func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) {
err := s.db.QueryRow("SELECT id, password, rights FROM users WHERE username = $1", reqData.Username).Scan(&userID, &password, &userRights) err := s.db.QueryRow("SELECT id, password, rights FROM users WHERE username = $1", reqData.Username).Scan(&userID, &password, &userRights)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
w.WriteHeader(400) w.WriteHeader(400)
w.Write([]byte("username-error")) _, _ = w.Write([]byte("username-error"))
return return
} else if err != nil { } else if err != nil {
s.logger.Warn("SQL query error", zap.Error(err)) s.logger.Warn("SQL query error", zap.Error(err))
@@ -149,7 +149,7 @@ func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) {
} }
if bcrypt.CompareHashAndPassword([]byte(password), []byte(reqData.Password)) != nil { if bcrypt.CompareHashAndPassword([]byte(password), []byte(reqData.Password)) != nil {
w.WriteHeader(400) w.WriteHeader(400)
w.Write([]byte("password-error")) _, _ = w.Write([]byte("password-error"))
return return
} }
@@ -170,7 +170,7 @@ func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) {
} }
respData := s.newAuthData(userID, userRights, userTokenID, userToken, characters) respData := s.newAuthData(userID, userRights, userTokenID, userToken, characters)
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(respData) _ = json.NewEncoder(w).Encode(respData)
} }
func (s *APIServer) Register(w http.ResponseWriter, r *http.Request) { func (s *APIServer) Register(w http.ResponseWriter, r *http.Request) {
@@ -194,7 +194,7 @@ func (s *APIServer) Register(w http.ResponseWriter, r *http.Request) {
var pqErr *pq.Error var pqErr *pq.Error
if errors.As(err, &pqErr) && pqErr.Constraint == "users_username_key" { if errors.As(err, &pqErr) && pqErr.Constraint == "users_username_key" {
w.WriteHeader(400) w.WriteHeader(400)
w.Write([]byte("username-exists-error")) _, _ = w.Write([]byte("username-exists-error"))
return return
} }
s.logger.Error("Error checking user", zap.Error(err), zap.String("username", reqData.Username)) s.logger.Error("Error checking user", zap.Error(err), zap.String("username", reqData.Username))
@@ -210,7 +210,7 @@ func (s *APIServer) Register(w http.ResponseWriter, r *http.Request) {
} }
respData := s.newAuthData(userID, userRights, userTokenID, userToken, []Character{}) respData := s.newAuthData(userID, userRights, userTokenID, userToken, []Character{})
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(respData) _ = json.NewEncoder(w).Encode(respData)
} }
func (s *APIServer) CreateCharacter(w http.ResponseWriter, r *http.Request) { func (s *APIServer) CreateCharacter(w http.ResponseWriter, r *http.Request) {
@@ -239,7 +239,7 @@ func (s *APIServer) CreateCharacter(w http.ResponseWriter, r *http.Request) {
character.HR = 7 character.HR = 7
} }
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(character) _ = json.NewEncoder(w).Encode(character)
} }
func (s *APIServer) DeleteCharacter(w http.ResponseWriter, r *http.Request) { func (s *APIServer) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
@@ -264,7 +264,7 @@ func (s *APIServer) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(struct{}{}) _ = json.NewEncoder(w).Encode(struct{}{})
} }
func (s *APIServer) ExportSave(w http.ResponseWriter, r *http.Request) { func (s *APIServer) ExportSave(w http.ResponseWriter, r *http.Request) {
@@ -293,7 +293,7 @@ func (s *APIServer) ExportSave(w http.ResponseWriter, r *http.Request) {
Character: character, Character: character,
} }
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(save) _ = json.NewEncoder(w).Encode(save)
} }
func (s *APIServer) ScreenShotGet(w http.ResponseWriter, r *http.Request) { func (s *APIServer) ScreenShotGet(w http.ResponseWriter, r *http.Request) {
// Get the 'id' parameter from the URL // Get the 'id' parameter from the URL

View File

@@ -10,7 +10,7 @@ import (
"testing" "testing"
_config "erupe-ce/config" _config "erupe-ce/config"
"erupe-ce/server/channelserver" "erupe-ce/common/gametime"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -99,7 +99,7 @@ func TestLauncherEndpointEmptyConfig(t *testing.T) {
server.Launcher(recorder, req) server.Launcher(recorder, req)
var respData LauncherResponse var respData LauncherResponse
json.NewDecoder(recorder.Body).Decode(&respData) _ = json.NewDecoder(recorder.Body).Decode(&respData)
if respData.Banners == nil { if respData.Banners == nil {
t.Error("Banners should not be nil, should be empty slice") t.Error("Banners should not be nil, should be empty slice")
@@ -355,7 +355,7 @@ func TestScreenShotEndpointDisabled(t *testing.T) {
XMLName xml.Name `xml:"result"` XMLName xml.Name `xml:"result"`
Code string `xml:"code"` Code string `xml:"code"`
} }
xml.NewDecoder(recorder.Body).Decode(&result) _ = xml.NewDecoder(recorder.Body).Decode(&result)
if result.Code != "400" { if result.Code != "400" {
t.Errorf("Expected code 400, got %s", result.Code) t.Errorf("Expected code 400, got %s", result.Code)
@@ -573,7 +573,7 @@ func TestNewAuthDataTimestamps(t *testing.T) {
authData := server.newAuthData(1, 0, 1, "token", []Character{}) authData := server.newAuthData(1, 0, 1, "token", []Character{})
// Timestamps should be reasonable (within last minute and next 30 days) // Timestamps should be reasonable (within last minute and next 30 days)
now := uint32(channelserver.TimeAdjusted().Unix()) now := uint32(gametime.Adjusted().Unix())
if authData.CurrentTS < now-60 || authData.CurrentTS > now+60 { if authData.CurrentTS < now-60 || authData.CurrentTS > now+60 {
t.Errorf("CurrentTS not within reasonable range: %d vs %d", authData.CurrentTS, now) t.Errorf("CurrentTS not within reasonable range: %d vs %d", authData.CurrentTS, now)
} }

View File

@@ -2,8 +2,8 @@ package deltacomp
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"log"
) )
func checkReadUint8(r *bytes.Reader) (uint8, error) { func checkReadUint8(r *bytes.Reader) (uint8, error) {
@@ -77,7 +77,7 @@ func ApplyDataDiff(diff []byte, baseData []byte) []byte {
// Grow slice if it's required // Grow slice if it's required
if len(baseCopy) < dataOffset { if len(baseCopy) < dataOffset {
fmt.Printf("Slice smaller than data offset, growing slice...") log.Printf("Slice smaller than data offset, growing slice...")
baseCopy = append(baseCopy, make([]byte, (dataOffset+differentCount)-len(baseData))...) baseCopy = append(baseCopy, make([]byte, (dataOffset+differentCount)-len(baseData))...)
} else { } else {
length := len(baseCopy[dataOffset:]) length := len(baseCopy[dataOffset:])

View File

@@ -1,10 +1,10 @@
package channelserver package channelserver
import ( import (
"fmt"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/network/mhfpacket" "erupe-ce/network/mhfpacket"
"go.uber.org/zap"
) )
func handleMsgSysCreateObject(s *Session, p mhfpacket.MHFPacket) { func handleMsgSysCreateObject(s *Session, p mhfpacket.MHFPacket) {
@@ -34,7 +34,7 @@ func handleMsgSysCreateObject(s *Session, p mhfpacket.MHFPacket) {
OwnerCharID: newObj.ownerCharID, OwnerCharID: newObj.ownerCharID,
} }
s.logger.Info(fmt.Sprintf("Broadcasting new object: %s (%d)", s.Name, newObj.id)) s.logger.Info("Broadcasting new object", zap.String("name", s.Name), zap.Uint32("objectID", newObj.id))
s.stage.BroadcastMHF(dupObjUpdate, s) s.stage.BroadcastMHF(dupObjUpdate, s)
} }
@@ -43,7 +43,13 @@ func handleMsgSysDeleteObject(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgSysPositionObject(s *Session, p mhfpacket.MHFPacket) { func handleMsgSysPositionObject(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysPositionObject) pkt := p.(*mhfpacket.MsgSysPositionObject)
if s.server.erupeConfig.DebugOptions.LogInboundMessages { if s.server.erupeConfig.DebugOptions.LogInboundMessages {
fmt.Printf("[%s] with objectID [%d] move to (%f,%f,%f)\n\n", s.Name, pkt.ObjID, pkt.X, pkt.Y, pkt.Z) s.logger.Debug("Object position update",
zap.String("name", s.Name),
zap.Uint32("objectID", pkt.ObjID),
zap.Float32("x", pkt.X),
zap.Float32("y", pkt.Y),
zap.Float32("z", pkt.Z),
)
} }
s.stage.Lock() s.stage.Lock()
object, ok := s.stage.objects[s.charID] object, ok := s.stage.objects[s.charID]

View File

@@ -84,7 +84,7 @@ func NewSession(server *Server, conn net.Conn) *Session {
rawConn: conn, rawConn: conn,
cryptConn: network.NewCryptConn(conn), cryptConn: network.NewCryptConn(conn),
sendPackets: make(chan packet, 20), sendPackets: make(chan packet, 20),
clientContext: &clientctx.ClientContext{}, // Unused clientContext: &clientctx.ClientContext{},
lastPacket: time.Now(), lastPacket: time.Now(),
objectID: server.getObjectId(), objectID: server.getObjectId(),
sessionStart: TimeAdjusted().Unix(), sessionStart: TimeAdjusted().Unix(),
@@ -232,8 +232,7 @@ func (s *Session) handlePacketGroup(pktGroup []byte) {
// This shouldn't be needed, but it's better to recover and let the connection die than to panic the server. // This shouldn't be needed, but it's better to recover and let the connection die than to panic the server.
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
fmt.Printf("[%s]", s.Name) s.logger.Error("Recovered from panic", zap.String("name", s.Name), zap.Any("panic", r))
fmt.Println("Recovered from panic", r)
} }
}() }()
@@ -246,13 +245,16 @@ func (s *Session) handlePacketGroup(pktGroup []byte) {
// Get the packet parser and handler for this opcode. // Get the packet parser and handler for this opcode.
mhfPkt := mhfpacket.FromOpcode(opcode) mhfPkt := mhfpacket.FromOpcode(opcode)
if mhfPkt == nil { if mhfPkt == nil {
fmt.Println("Got opcode which we don't know how to parse, can't parse anymore for this group") s.logger.Warn("Got opcode which we don't know how to parse, can't parse anymore for this group")
return return
} }
// Parse the packet. // Parse the packet.
err := mhfPkt.Parse(bf, s.clientContext) err := mhfPkt.Parse(bf, s.clientContext)
if err != nil { if err != nil {
fmt.Printf("\n!!! [%s] %s NOT IMPLEMENTED !!! \n\n\n", s.Name, opcode) s.logger.Warn("Packet not implemented",
zap.String("name", s.Name),
zap.Stringer("opcode", opcode),
)
return return
} }
// Handle the packet. // Handle the packet.
@@ -297,21 +299,23 @@ func (s *Session) logMessage(opcode uint16, data []byte, sender string, recipien
if len(data) >= 6 { if len(data) >= 6 {
ackHandle = binary.BigEndian.Uint32(data[2:6]) ackHandle = binary.BigEndian.Uint32(data[2:6])
} }
if t, ok := s.ackStart[ackHandle]; ok { fields := []zap.Field{
fmt.Printf("[%s] -> [%s] (%fs)\n", sender, recipient, float64(time.Now().UnixNano()-t.UnixNano())/1000000000) zap.String("sender", sender),
} else { zap.String("recipient", recipient),
fmt.Printf("[%s] -> [%s]\n", sender, recipient) zap.Uint16("opcode_dec", opcode),
zap.String("opcode_hex", fmt.Sprintf("0x%04X", opcode)),
zap.Stringer("opcode_name", opcodePID),
zap.Int("data_bytes", len(data)),
}
if t, ok := s.ackStart[ackHandle]; ok {
fields = append(fields, zap.Duration("ack_latency", time.Since(t)))
} }
fmt.Printf("Opcode: (Dec: %d Hex: 0x%04X Name: %s) \n", opcode, opcode, opcodePID)
if s.server.erupeConfig.DebugOptions.LogMessageData { if s.server.erupeConfig.DebugOptions.LogMessageData {
if len(data) <= s.server.erupeConfig.DebugOptions.MaxHexdumpLength { if len(data) <= s.server.erupeConfig.DebugOptions.MaxHexdumpLength {
fmt.Printf("Data [%d bytes]:\n%s\n", len(data), hex.Dump(data)) fields = append(fields, zap.String("data", hex.Dump(data)))
} else {
fmt.Printf("Data [%d bytes]: (Too long!)\n\n", len(data))
} }
} else {
fmt.Printf("\n")
} }
s.logger.Debug("Packet", fields...)
} }
func (s *Session) getObjectId() uint32 { func (s *Session) getObjectId() uint32 {

View File

@@ -1,32 +1,12 @@
package channelserver package channelserver
import ( import (
"erupe-ce/common/gametime"
"time" "time"
) )
func TimeAdjusted() time.Time { func TimeAdjusted() time.Time { return gametime.Adjusted() }
baseTime := time.Now().In(time.FixedZone("UTC+9", 9*60*60)) func TimeMidnight() time.Time { return gametime.Midnight() }
return time.Date(baseTime.Year(), baseTime.Month(), baseTime.Day(), baseTime.Hour(), baseTime.Minute(), baseTime.Second(), baseTime.Nanosecond(), baseTime.Location()) func TimeWeekStart() time.Time { return gametime.WeekStart() }
} func TimeWeekNext() time.Time { return gametime.WeekNext() }
func TimeGameAbsolute() uint32 { return gametime.GameAbsolute() }
func TimeMidnight() time.Time {
baseTime := time.Now().In(time.FixedZone("UTC+9", 9*60*60))
return time.Date(baseTime.Year(), baseTime.Month(), baseTime.Day(), 0, 0, 0, 0, baseTime.Location())
}
func TimeWeekStart() time.Time {
midnight := TimeMidnight()
offset := int(midnight.Weekday()) - int(time.Monday)
if offset < 0 {
offset += 7
}
return midnight.Add(-time.Duration(offset) * 24 * time.Hour)
}
func TimeWeekNext() time.Time {
return TimeWeekStart().Add(time.Hour * 24 * 7)
}
func TimeGameAbsolute() uint32 {
return uint32((TimeAdjusted().Unix() - 2160) % 5760)
}

View File

@@ -1,167 +0,0 @@
package channelserver
import (
"testing"
"time"
)
func TestTimeAdjusted(t *testing.T) {
result := TimeAdjusted()
// Should return a time in UTC+9 timezone
_, offset := result.Zone()
expectedOffset := 9 * 60 * 60 // 9 hours in seconds
if offset != expectedOffset {
t.Errorf("TimeAdjusted() zone offset = %d, want %d (UTC+9)", offset, expectedOffset)
}
// The time should be close to current time (within a few seconds)
now := time.Now()
diff := result.Sub(now.In(time.FixedZone("UTC+9", 9*60*60)))
if diff < -time.Second || diff > time.Second {
t.Errorf("TimeAdjusted() time differs from expected by %v", diff)
}
}
func TestTimeMidnight(t *testing.T) {
midnight := TimeMidnight()
// Should be at midnight (hour=0, minute=0, second=0, nanosecond=0)
if midnight.Hour() != 0 {
t.Errorf("TimeMidnight() hour = %d, want 0", midnight.Hour())
}
if midnight.Minute() != 0 {
t.Errorf("TimeMidnight() minute = %d, want 0", midnight.Minute())
}
if midnight.Second() != 0 {
t.Errorf("TimeMidnight() second = %d, want 0", midnight.Second())
}
if midnight.Nanosecond() != 0 {
t.Errorf("TimeMidnight() nanosecond = %d, want 0", midnight.Nanosecond())
}
// Should be in UTC+9 timezone
_, offset := midnight.Zone()
expectedOffset := 9 * 60 * 60
if offset != expectedOffset {
t.Errorf("TimeMidnight() zone offset = %d, want %d (UTC+9)", offset, expectedOffset)
}
}
func TestTimeWeekStart(t *testing.T) {
weekStart := TimeWeekStart()
// Should be on Monday (weekday = 1)
if weekStart.Weekday() != time.Monday {
t.Errorf("TimeWeekStart() weekday = %v, want Monday", weekStart.Weekday())
}
// Should be at midnight
if weekStart.Hour() != 0 || weekStart.Minute() != 0 || weekStart.Second() != 0 {
t.Errorf("TimeWeekStart() should be at midnight, got %02d:%02d:%02d",
weekStart.Hour(), weekStart.Minute(), weekStart.Second())
}
// Should be in UTC+9 timezone
_, offset := weekStart.Zone()
expectedOffset := 9 * 60 * 60
if offset != expectedOffset {
t.Errorf("TimeWeekStart() zone offset = %d, want %d (UTC+9)", offset, expectedOffset)
}
// Week start should be before or equal to current midnight
midnight := TimeMidnight()
if weekStart.After(midnight) {
t.Errorf("TimeWeekStart() %v should be <= current midnight %v", weekStart, midnight)
}
}
func TestTimeWeekNext(t *testing.T) {
weekStart := TimeWeekStart()
weekNext := TimeWeekNext()
// TimeWeekNext should be exactly 7 days after TimeWeekStart
expectedNext := weekStart.Add(time.Hour * 24 * 7)
if !weekNext.Equal(expectedNext) {
t.Errorf("TimeWeekNext() = %v, want %v (7 days after WeekStart)", weekNext, expectedNext)
}
// Should also be on Monday
if weekNext.Weekday() != time.Monday {
t.Errorf("TimeWeekNext() weekday = %v, want Monday", weekNext.Weekday())
}
// Should be at midnight
if weekNext.Hour() != 0 || weekNext.Minute() != 0 || weekNext.Second() != 0 {
t.Errorf("TimeWeekNext() should be at midnight, got %02d:%02d:%02d",
weekNext.Hour(), weekNext.Minute(), weekNext.Second())
}
// Should be in the future relative to week start
if !weekNext.After(weekStart) {
t.Errorf("TimeWeekNext() %v should be after TimeWeekStart() %v", weekNext, weekStart)
}
}
func TestTimeWeekStartSundayEdge(t *testing.T) {
// When today is Sunday, the calculation should go back to last Monday
// This is tested indirectly by verifying the weekday is always Monday
weekStart := TimeWeekStart()
// Regardless of what day it is now, week start should be Monday
if weekStart.Weekday() != time.Monday {
t.Errorf("TimeWeekStart() on any day should return Monday, got %v", weekStart.Weekday())
}
}
func TestTimeMidnightSameDay(t *testing.T) {
adjusted := TimeAdjusted()
midnight := TimeMidnight()
// Midnight should be on the same day (year, month, day)
if midnight.Year() != adjusted.Year() ||
midnight.Month() != adjusted.Month() ||
midnight.Day() != adjusted.Day() {
t.Errorf("TimeMidnight() date = %v, want same day as TimeAdjusted() %v",
midnight.Format("2006-01-02"), adjusted.Format("2006-01-02"))
}
}
func TestTimeWeekDuration(t *testing.T) {
weekStart := TimeWeekStart()
weekNext := TimeWeekNext()
// Duration between week boundaries should be exactly 7 days
duration := weekNext.Sub(weekStart)
expectedDuration := time.Hour * 24 * 7
if duration != expectedDuration {
t.Errorf("Duration between WeekStart and WeekNext = %v, want %v", duration, expectedDuration)
}
}
func TestTimeZoneConsistency(t *testing.T) {
adjusted := TimeAdjusted()
midnight := TimeMidnight()
weekStart := TimeWeekStart()
weekNext := TimeWeekNext()
// All times should be in the same timezone (UTC+9)
times := []struct {
name string
time time.Time
}{
{"TimeAdjusted", adjusted},
{"TimeMidnight", midnight},
{"TimeWeekStart", weekStart},
{"TimeWeekNext", weekNext},
}
expectedOffset := 9 * 60 * 60
for _, tt := range times {
_, offset := tt.time.Zone()
if offset != expectedOffset {
t.Errorf("%s() zone offset = %d, want %d (UTC+9)", tt.name, offset, expectedOffset)
}
}
}

View File

@@ -9,7 +9,7 @@ import (
"net" "net"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/server/channelserver" "erupe-ce/common/gametime"
) )
func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte { func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte {
@@ -41,7 +41,7 @@ func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte {
bf.WriteUint16(0) bf.WriteUint16(0)
bf.WriteUint16(uint16(len(si.Channels))) bf.WriteUint16(uint16(len(si.Channels)))
bf.WriteUint8(si.Type) bf.WriteUint8(si.Type)
bf.WriteUint8(uint8(((channelserver.TimeAdjusted().Unix() / 86400) + int64(serverIdx)) % 3)) bf.WriteUint8(uint8(((gametime.Adjusted().Unix() / 86400) + int64(serverIdx)) % 3))
if s.erupeConfig.RealClientMode >= _config.G1 { if s.erupeConfig.RealClientMode >= _config.G1 {
bf.WriteUint8(si.Recommended) bf.WriteUint8(si.Recommended)
} }
@@ -71,7 +71,7 @@ func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte {
bf.WriteUint16(uint16(channelIdx | 16)) bf.WriteUint16(uint16(channelIdx | 16))
bf.WriteUint16(ci.MaxPlayers) bf.WriteUint16(ci.MaxPlayers)
var currentPlayers uint16 var currentPlayers uint16
s.db.QueryRow("SELECT current_players FROM servers WHERE server_id=$1", sid).Scan(&currentPlayers) _ = s.db.QueryRow("SELECT current_players FROM servers WHERE server_id=$1", sid).Scan(&currentPlayers)
bf.WriteUint16(currentPlayers) bf.WriteUint16(currentPlayers)
bf.WriteUint16(0) bf.WriteUint16(0)
bf.WriteUint16(0) bf.WriteUint16(0)
@@ -85,7 +85,7 @@ func encodeServerInfo(config *_config.Config, s *Server, local bool) []byte {
bf.WriteUint16(12345) bf.WriteUint16(12345)
} }
} }
bf.WriteUint32(uint32(channelserver.TimeAdjusted().Unix())) bf.WriteUint32(uint32(gametime.Adjusted().Unix()))
// ClanMemberLimits requires at least 1 element with 2 columns to avoid index out of range panics // ClanMemberLimits requires at least 1 element with 2 columns to avoid index out of range panics
// Use default value (60) if array is empty or last row is too small // Use default value (60) if array is empty or last row is too small

View File

@@ -5,7 +5,7 @@ import (
ps "erupe-ce/common/pascalstring" ps "erupe-ce/common/pascalstring"
"erupe-ce/common/stringsupport" "erupe-ce/common/stringsupport"
_config "erupe-ce/config" _config "erupe-ce/config"
"erupe-ce/server/channelserver" "erupe-ce/common/gametime"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@@ -50,7 +50,7 @@ func (s *Session) makeSignResponse(uid uint32) []byte {
bf.WriteUint8(uint8(len(chars))) bf.WriteUint8(uint8(len(chars)))
bf.WriteUint32(tokenID) bf.WriteUint32(tokenID)
bf.WriteBytes([]byte(sessToken)) bf.WriteBytes([]byte(sessToken))
bf.WriteUint32(uint32(channelserver.TimeAdjusted().Unix())) bf.WriteUint32(uint32(gametime.Adjusted().Unix()))
if s.client == PS3 { if s.client == PS3 {
ps.Uint8(bf, fmt.Sprintf("%s/ps3", s.server.erupeConfig.PatchServerManifest), false) ps.Uint8(bf, fmt.Sprintf("%s/ps3", s.server.erupeConfig.PatchServerManifest), false)
ps.Uint8(bf, fmt.Sprintf("%s/ps3", s.server.erupeConfig.PatchServerFile), false) ps.Uint8(bf, fmt.Sprintf("%s/ps3", s.server.erupeConfig.PatchServerFile), false)
@@ -334,7 +334,7 @@ func (s *Session) makeSignResponse(uid uint32) []byte {
if s.client == VITA || s.client == PS3 || s.client == PS4 { if s.client == VITA || s.client == PS3 || s.client == PS4 {
var psnUser string var psnUser string
s.server.db.QueryRow("SELECT psn_id FROM users WHERE id = $1", uid).Scan(&psnUser) _ = s.server.db.QueryRow("SELECT psn_id FROM users WHERE id = $1", uid).Scan(&psnUser)
bf.WriteBytes(stringsupport.PaddedString(psnUser, 20, true)) bf.WriteBytes(stringsupport.PaddedString(psnUser, 20, true))
} }
@@ -385,11 +385,11 @@ func (s *Session) makeSignResponse(uid uint32) []byte {
} }
// We can just use the start timestamp as the event ID // We can just use the start timestamp as the event ID
bf.WriteUint32(uint32(channelserver.TimeWeekStart().Unix())) bf.WriteUint32(uint32(gametime.WeekStart().Unix()))
// Start time // Start time
bf.WriteUint32(uint32(channelserver.TimeWeekNext().Add(-time.Duration(s.server.erupeConfig.GameplayOptions.MezFesDuration) * time.Second).Unix())) bf.WriteUint32(uint32(gametime.WeekNext().Add(-time.Duration(s.server.erupeConfig.GameplayOptions.MezFesDuration) * time.Second).Unix()))
// End time // End time
bf.WriteUint32(uint32(channelserver.TimeWeekNext().Unix())) bf.WriteUint32(uint32(gametime.WeekNext().Unix()))
bf.WriteUint8(uint8(len(tickets))) bf.WriteUint8(uint8(len(tickets)))
for i := range tickets { for i := range tickets {
bf.WriteUint32(tickets[i]) bf.WriteUint32(tickets[i])