mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-04-03 06:22:33 +02:00
docs: add doc.go files and godoc comments to all packages
Add package-level documentation (doc.go) to all 22 first-party packages and godoc comments to ~150 previously undocumented exported symbols across common/, network/, and server/.
This commit is contained in:
3
common/bfutil/doc.go
Normal file
3
common/bfutil/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package bfutil provides byte-slice utility functions for working with
|
||||||
|
// null-terminated binary data commonly found in MHF network packets.
|
||||||
|
package bfutil
|
||||||
@@ -135,6 +135,7 @@ func (b *ByteFrame) DataFromCurrent() []byte {
|
|||||||
return b.buf[b.index:b.usedSize]
|
return b.buf[b.index:b.usedSize]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Index returns the current read/write position in the buffer.
|
||||||
func (b *ByteFrame) Index() uint {
|
func (b *ByteFrame) Index() uint {
|
||||||
return b.index
|
return b.index
|
||||||
}
|
}
|
||||||
|
|||||||
4
common/byteframe/doc.go
Normal file
4
common/byteframe/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package byteframe provides a seekable, growable byte buffer for reading and
|
||||||
|
// writing binary data in big-endian or little-endian byte order. It is the
|
||||||
|
// primary serialization primitive used throughout the Erupe network layer.
|
||||||
|
package byteframe
|
||||||
4
common/decryption/doc.go
Normal file
4
common/decryption/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package decryption implements the JPK decompression algorithm used by
|
||||||
|
// Monster Hunter Frontier to compress game data files. The format is
|
||||||
|
// identified by the magic bytes 0x1A524B4A ("JKR").
|
||||||
|
package decryption
|
||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
var mShiftIndex = 0
|
var mShiftIndex = 0
|
||||||
var mFlag = byte(0)
|
var mFlag = byte(0)
|
||||||
|
|
||||||
|
// UnpackSimple decompresses a JPK type-3 compressed byte slice. If the data
|
||||||
|
// does not start with the JKR magic header it is returned unchanged.
|
||||||
func UnpackSimple(data []byte) []byte {
|
func UnpackSimple(data []byte) []byte {
|
||||||
mShiftIndex = 0
|
mShiftIndex = 0
|
||||||
mFlag = byte(0)
|
mFlag = byte(0)
|
||||||
@@ -40,6 +42,8 @@ func UnpackSimple(data []byte) []byte {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProcessDecode runs the JPK LZ-style decompression loop, reading compressed
|
||||||
|
// tokens from data and writing decompressed bytes into outBuffer.
|
||||||
func ProcessDecode(data *byteframe.ByteFrame, outBuffer []byte) {
|
func ProcessDecode(data *byteframe.ByteFrame, outBuffer []byte) {
|
||||||
outIndex := 0
|
outIndex := 0
|
||||||
|
|
||||||
@@ -85,6 +89,8 @@ func ProcessDecode(data *byteframe.ByteFrame, outBuffer []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JPKBitShift reads one bit from the compressed stream's flag byte, refilling
|
||||||
|
// the flag from the next byte in data when all 8 bits have been consumed.
|
||||||
func JPKBitShift(data *byteframe.ByteFrame) byte {
|
func JPKBitShift(data *byteframe.ByteFrame) byte {
|
||||||
mShiftIndex--
|
mShiftIndex--
|
||||||
|
|
||||||
@@ -96,6 +102,8 @@ func JPKBitShift(data *byteframe.ByteFrame) byte {
|
|||||||
return (byte)((mFlag >> mShiftIndex) & 1)
|
return (byte)((mFlag >> mShiftIndex) & 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JPKCopy copies length bytes from a previous position in outBuffer (determined
|
||||||
|
// by offset back from the current index) to implement LZ back-references.
|
||||||
func JPKCopy(outBuffer []byte, offset int, length int, index *int) {
|
func JPKCopy(outBuffer []byte, offset int, length int, index *int) {
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
||||||
outBuffer[*index] = outBuffer[*index-offset-1]
|
outBuffer[*index] = outBuffer[*index-offset-1]
|
||||||
@@ -103,6 +111,7 @@ func JPKCopy(outBuffer []byte, offset int, length int, index *int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadByte reads a single byte from the ByteFrame.
|
||||||
func ReadByte(bf *byteframe.ByteFrame) byte {
|
func ReadByte(bf *byteframe.ByteFrame) byte {
|
||||||
value := bf.ReadUint8()
|
value := bf.ReadUint8()
|
||||||
return value
|
return value
|
||||||
|
|||||||
4
common/gametime/doc.go
Normal file
4
common/gametime/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package gametime provides time helpers anchored to the JST (UTC+9) timezone
|
||||||
|
// used by Monster Hunter Frontier's game clock, including weekly reset
|
||||||
|
// boundaries and the in-game absolute time cycle.
|
||||||
|
package gametime
|
||||||
@@ -4,16 +4,19 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Adjusted returns the current time in JST (UTC+9), the timezone used by MHF.
|
||||||
func Adjusted() time.Time {
|
func Adjusted() time.Time {
|
||||||
baseTime := time.Now().In(time.FixedZone("UTC+9", 9*60*60))
|
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())
|
return time.Date(baseTime.Year(), baseTime.Month(), baseTime.Day(), baseTime.Hour(), baseTime.Minute(), baseTime.Second(), baseTime.Nanosecond(), baseTime.Location())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Midnight returns today's midnight (00:00) in JST.
|
||||||
func Midnight() time.Time {
|
func Midnight() time.Time {
|
||||||
baseTime := time.Now().In(time.FixedZone("UTC+9", 9*60*60))
|
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())
|
return time.Date(baseTime.Year(), baseTime.Month(), baseTime.Day(), 0, 0, 0, 0, baseTime.Location())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WeekStart returns the most recent Monday at midnight in JST.
|
||||||
func WeekStart() time.Time {
|
func WeekStart() time.Time {
|
||||||
midnight := Midnight()
|
midnight := Midnight()
|
||||||
offset := int(midnight.Weekday()) - int(time.Monday)
|
offset := int(midnight.Weekday()) - int(time.Monday)
|
||||||
@@ -23,10 +26,13 @@ func WeekStart() time.Time {
|
|||||||
return midnight.Add(-time.Duration(offset) * 24 * time.Hour)
|
return midnight.Add(-time.Duration(offset) * 24 * time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WeekNext returns the next Monday at midnight in JST.
|
||||||
func WeekNext() time.Time {
|
func WeekNext() time.Time {
|
||||||
return WeekStart().Add(time.Hour * 24 * 7)
|
return WeekStart().Add(time.Hour * 24 * 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GameAbsolute returns the current position within the 5760-second (96-minute)
|
||||||
|
// in-game day/night cycle, offset by 2160 seconds.
|
||||||
func GameAbsolute() uint32 {
|
func GameAbsolute() uint32 {
|
||||||
return uint32((Adjusted().Unix() - 2160) % 5760)
|
return uint32((Adjusted().Unix() - 2160) % 5760)
|
||||||
}
|
}
|
||||||
|
|||||||
3
common/mhfcid/doc.go
Normal file
3
common/mhfcid/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package mhfcid converts MHF Character ID strings (a base-32 encoding that
|
||||||
|
// omits the ambiguous characters 0, I, O, and S) to their numeric equivalents.
|
||||||
|
package mhfcid
|
||||||
5
common/mhfcourse/doc.go
Normal file
5
common/mhfcourse/doc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Package mhfcourse models the subscription course system used by Monster
|
||||||
|
// Hunter Frontier. Courses (Trial, HunterLife, Extra, Premium, etc.) are
|
||||||
|
// represented as bit flags in a uint32 rights field and control which game
|
||||||
|
// features a player can access.
|
||||||
|
package mhfcourse
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Course represents an active subscription course with its ID and expiry time.
|
||||||
type Course struct {
|
type Course struct {
|
||||||
ID uint16
|
ID uint16
|
||||||
Expiry time.Time
|
Expiry time.Time
|
||||||
@@ -39,10 +40,12 @@ var aliases = map[uint16][]string{
|
|||||||
// 30 = Real NetCafe course
|
// 30 = Real NetCafe course
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aliases returns the human-readable names for this course (e.g. "HunterLife", "HL").
|
||||||
func (c Course) Aliases() []string {
|
func (c Course) Aliases() []string {
|
||||||
return aliases[c.ID]
|
return aliases[c.ID]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Courses returns all 32 possible course slots with zero-value expiry times.
|
||||||
func Courses() []Course {
|
func Courses() []Course {
|
||||||
courses := make([]Course, 32)
|
courses := make([]Course, 32)
|
||||||
for i := range courses {
|
for i := range courses {
|
||||||
@@ -51,6 +54,7 @@ func Courses() []Course {
|
|||||||
return courses
|
return courses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Value returns the bitmask value for this course (2^ID).
|
||||||
func (c Course) Value() uint32 {
|
func (c Course) Value() uint32 {
|
||||||
return uint32(math.Pow(2, float64(c.ID)))
|
return uint32(math.Pow(2, float64(c.ID)))
|
||||||
}
|
}
|
||||||
|
|||||||
4
common/mhfitem/doc.go
Normal file
4
common/mhfitem/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package mhfitem defines item, equipment, and sigil data structures as they
|
||||||
|
// appear in the MHF binary protocol, and provides serialization helpers for
|
||||||
|
// warehouse (box/storage) operations.
|
||||||
|
package mhfitem
|
||||||
@@ -6,15 +6,18 @@ import (
|
|||||||
_config "erupe-ce/config"
|
_config "erupe-ce/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MHFItem represents a single item identified by its in-game item ID.
|
||||||
type MHFItem struct {
|
type MHFItem struct {
|
||||||
ItemID uint16
|
ItemID uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MHFSigilEffect represents a single effect slot on a sigil with an ID and level.
|
||||||
type MHFSigilEffect struct {
|
type MHFSigilEffect struct {
|
||||||
ID uint16
|
ID uint16
|
||||||
Level uint16
|
Level uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MHFSigil represents a weapon sigil containing up to three effects.
|
||||||
type MHFSigil struct {
|
type MHFSigil struct {
|
||||||
Effects []MHFSigilEffect
|
Effects []MHFSigilEffect
|
||||||
Unk0 uint8
|
Unk0 uint8
|
||||||
@@ -23,6 +26,8 @@ type MHFSigil struct {
|
|||||||
Unk3 uint8
|
Unk3 uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MHFEquipment represents an equipment piece (weapon or armor) with its
|
||||||
|
// decorations and sigils as stored in the player's warehouse.
|
||||||
type MHFEquipment struct {
|
type MHFEquipment struct {
|
||||||
WarehouseID uint32
|
WarehouseID uint32
|
||||||
ItemType uint8
|
ItemType uint8
|
||||||
@@ -34,6 +39,7 @@ type MHFEquipment struct {
|
|||||||
Unk1 uint16
|
Unk1 uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MHFItemStack represents a stacked item slot in the warehouse with a quantity.
|
||||||
type MHFItemStack struct {
|
type MHFItemStack struct {
|
||||||
WarehouseID uint32
|
WarehouseID uint32
|
||||||
Item MHFItem
|
Item MHFItem
|
||||||
@@ -41,6 +47,8 @@ type MHFItemStack struct {
|
|||||||
Unk0 uint32
|
Unk0 uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadWarehouseItem deserializes an MHFItemStack from a ByteFrame, assigning a
|
||||||
|
// random warehouse ID if the encoded ID is zero.
|
||||||
func ReadWarehouseItem(bf *byteframe.ByteFrame) MHFItemStack {
|
func ReadWarehouseItem(bf *byteframe.ByteFrame) MHFItemStack {
|
||||||
var item MHFItemStack
|
var item MHFItemStack
|
||||||
item.WarehouseID = bf.ReadUint32()
|
item.WarehouseID = bf.ReadUint32()
|
||||||
@@ -53,6 +61,9 @@ func ReadWarehouseItem(bf *byteframe.ByteFrame) MHFItemStack {
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DiffItemStacks merges an updated item stack list into an existing one,
|
||||||
|
// matching by warehouse ID. New items receive a random ID; items with zero
|
||||||
|
// quantity in the old list are removed.
|
||||||
func DiffItemStacks(o []MHFItemStack, u []MHFItemStack) []MHFItemStack {
|
func DiffItemStacks(o []MHFItemStack, u []MHFItemStack) []MHFItemStack {
|
||||||
// o = old, u = update, f = final
|
// o = old, u = update, f = final
|
||||||
var f []MHFItemStack
|
var f []MHFItemStack
|
||||||
@@ -77,6 +88,7 @@ func DiffItemStacks(o []MHFItemStack, u []MHFItemStack) []MHFItemStack {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToBytes serializes the item stack to its binary protocol representation.
|
||||||
func (is MHFItemStack) ToBytes() []byte {
|
func (is MHFItemStack) ToBytes() []byte {
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
bf.WriteUint32(is.WarehouseID)
|
bf.WriteUint32(is.WarehouseID)
|
||||||
@@ -86,6 +98,8 @@ func (is MHFItemStack) ToBytes() []byte {
|
|||||||
return bf.Data()
|
return bf.Data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SerializeWarehouseItems serializes a slice of item stacks with a uint16
|
||||||
|
// count header for transmission in warehouse response packets.
|
||||||
func SerializeWarehouseItems(i []MHFItemStack) []byte {
|
func SerializeWarehouseItems(i []MHFItemStack) []byte {
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
bf.WriteUint16(uint16(len(i)))
|
bf.WriteUint16(uint16(len(i)))
|
||||||
@@ -96,6 +110,9 @@ func SerializeWarehouseItems(i []MHFItemStack) []byte {
|
|||||||
return bf.Data()
|
return bf.Data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadWarehouseEquipment deserializes an MHFEquipment from a ByteFrame. The
|
||||||
|
// binary layout varies by game version: sigils are present from G1 onward and
|
||||||
|
// an additional field is present from Z1 onward.
|
||||||
func ReadWarehouseEquipment(bf *byteframe.ByteFrame) MHFEquipment {
|
func ReadWarehouseEquipment(bf *byteframe.ByteFrame) MHFEquipment {
|
||||||
var equipment MHFEquipment
|
var equipment MHFEquipment
|
||||||
equipment.Decorations = make([]MHFItem, 3)
|
equipment.Decorations = make([]MHFItem, 3)
|
||||||
@@ -134,6 +151,7 @@ func ReadWarehouseEquipment(bf *byteframe.ByteFrame) MHFEquipment {
|
|||||||
return equipment
|
return equipment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToBytes serializes the equipment to its binary protocol representation.
|
||||||
func (e MHFEquipment) ToBytes() []byte {
|
func (e MHFEquipment) ToBytes() []byte {
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
bf.WriteUint32(e.WarehouseID)
|
bf.WriteUint32(e.WarehouseID)
|
||||||
@@ -164,6 +182,8 @@ func (e MHFEquipment) ToBytes() []byte {
|
|||||||
return bf.Data()
|
return bf.Data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SerializeWarehouseEquipment serializes a slice of equipment with a uint16
|
||||||
|
// count header for transmission in warehouse response packets.
|
||||||
func SerializeWarehouseEquipment(i []MHFEquipment) []byte {
|
func SerializeWarehouseEquipment(i []MHFEquipment) []byte {
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
bf.WriteUint16(uint16(len(i)))
|
bf.WriteUint16(uint16(len(i)))
|
||||||
|
|||||||
4
common/mhfmon/doc.go
Normal file
4
common/mhfmon/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package mhfmon enumerates every monster in Monster Hunter Frontier by its
|
||||||
|
// internal enemy ID (em001–em176) and provides metadata such as display name
|
||||||
|
// and large/small classification.
|
||||||
|
package mhfmon
|
||||||
@@ -180,11 +180,13 @@ const (
|
|||||||
KingShakalaka
|
KingShakalaka
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Monster holds display metadata for a single monster species.
|
||||||
type Monster struct {
|
type Monster struct {
|
||||||
Name string
|
Name string
|
||||||
Large bool
|
Large bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Monsters is an ordered table of all MHF monsters, indexed by enemy ID.
|
||||||
var Monsters = []Monster{
|
var Monsters = []Monster{
|
||||||
{"Mon0", false},
|
{"Mon0", false},
|
||||||
{"Rathian", true},
|
{"Rathian", true},
|
||||||
|
|||||||
4
common/pascalstring/doc.go
Normal file
4
common/pascalstring/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package pascalstring writes length-prefixed, null-terminated strings into a
|
||||||
|
// ByteFrame. The prefix width is selectable (uint8, uint16, or uint32) and
|
||||||
|
// strings are optionally encoded to Shift-JIS before writing.
|
||||||
|
package pascalstring
|
||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Uint8 writes x as a null-terminated string with a uint8 length prefix. If t
|
||||||
|
// is true the string is first encoded to Shift-JIS.
|
||||||
func Uint8(bf *byteframe.ByteFrame, x string, t bool) {
|
func Uint8(bf *byteframe.ByteFrame, x string, t bool) {
|
||||||
if t {
|
if t {
|
||||||
e := japanese.ShiftJIS.NewEncoder()
|
e := japanese.ShiftJIS.NewEncoder()
|
||||||
@@ -20,6 +22,8 @@ func Uint8(bf *byteframe.ByteFrame, x string, t bool) {
|
|||||||
bf.WriteNullTerminatedBytes([]byte(x))
|
bf.WriteNullTerminatedBytes([]byte(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uint16 writes x as a null-terminated string with a uint16 length prefix. If
|
||||||
|
// t is true the string is first encoded to Shift-JIS.
|
||||||
func Uint16(bf *byteframe.ByteFrame, x string, t bool) {
|
func Uint16(bf *byteframe.ByteFrame, x string, t bool) {
|
||||||
if t {
|
if t {
|
||||||
e := japanese.ShiftJIS.NewEncoder()
|
e := japanese.ShiftJIS.NewEncoder()
|
||||||
@@ -34,6 +38,8 @@ func Uint16(bf *byteframe.ByteFrame, x string, t bool) {
|
|||||||
bf.WriteNullTerminatedBytes([]byte(x))
|
bf.WriteNullTerminatedBytes([]byte(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uint32 writes x as a null-terminated string with a uint32 length prefix. If
|
||||||
|
// t is true the string is first encoded to Shift-JIS.
|
||||||
func Uint32(bf *byteframe.ByteFrame, x string, t bool) {
|
func Uint32(bf *byteframe.ByteFrame, x string, t bool) {
|
||||||
if t {
|
if t {
|
||||||
e := japanese.ShiftJIS.NewEncoder()
|
e := japanese.ShiftJIS.NewEncoder()
|
||||||
|
|||||||
3
common/stringstack/doc.go
Normal file
3
common/stringstack/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package stringstack provides a minimal LIFO stack for strings, used
|
||||||
|
// internally to track hierarchical state such as nested stage paths.
|
||||||
|
package stringstack
|
||||||
5
common/stringsupport/doc.go
Normal file
5
common/stringsupport/doc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Package stringsupport provides string conversion utilities for the MHF
|
||||||
|
// protocol, including UTF-8 ↔ Shift-JIS transcoding, padded fixed-width
|
||||||
|
// string encoding, NG-word conversion, and comma-separated integer list
|
||||||
|
// manipulation used for database storage.
|
||||||
|
package stringsupport
|
||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"golang.org/x/text/transform"
|
"golang.org/x/text/transform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UTF8ToSJIS encodes a UTF-8 string to Shift-JIS bytes, silently dropping any
|
||||||
|
// runes that cannot be represented in Shift-JIS.
|
||||||
func UTF8ToSJIS(x string) []byte {
|
func UTF8ToSJIS(x string) []byte {
|
||||||
e := japanese.ShiftJIS.NewEncoder()
|
e := japanese.ShiftJIS.NewEncoder()
|
||||||
xt, _, err := transform.String(e, x)
|
xt, _, err := transform.String(e, x)
|
||||||
@@ -28,6 +30,7 @@ func UTF8ToSJIS(x string) []byte {
|
|||||||
return []byte(xt)
|
return []byte(xt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SJISToUTF8 decodes Shift-JIS bytes to a UTF-8 string.
|
||||||
func SJISToUTF8(b []byte) string {
|
func SJISToUTF8(b []byte) string {
|
||||||
d := japanese.ShiftJIS.NewDecoder()
|
d := japanese.ShiftJIS.NewDecoder()
|
||||||
result, err := io.ReadAll(transform.NewReader(bytes.NewReader(b), d))
|
result, err := io.ReadAll(transform.NewReader(bytes.NewReader(b), d))
|
||||||
@@ -37,6 +40,8 @@ func SJISToUTF8(b []byte) string {
|
|||||||
return string(result)
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToNGWord converts a UTF-8 string into a slice of uint16 values in the
|
||||||
|
// Shift-JIS byte-swapped format used by the MHF NG-word (chat filter) system.
|
||||||
func ToNGWord(x string) []uint16 {
|
func ToNGWord(x string) []uint16 {
|
||||||
var w []uint16
|
var w []uint16
|
||||||
for _, r := range x {
|
for _, r := range x {
|
||||||
@@ -55,6 +60,8 @@ func ToNGWord(x string) []uint16 {
|
|||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaddedString returns a fixed-width null-terminated byte slice of the given
|
||||||
|
// size. If t is true the string is first encoded to Shift-JIS.
|
||||||
func PaddedString(x string, size uint, t bool) []byte {
|
func PaddedString(x string, size uint, t bool) []byte {
|
||||||
if t {
|
if t {
|
||||||
e := japanese.ShiftJIS.NewEncoder()
|
e := japanese.ShiftJIS.NewEncoder()
|
||||||
@@ -70,6 +77,7 @@ func PaddedString(x string, size uint, t bool) []byte {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSVAdd appends v to the comma-separated integer list if not already present.
|
||||||
func CSVAdd(csv string, v int) string {
|
func CSVAdd(csv string, v int) string {
|
||||||
if len(csv) == 0 {
|
if len(csv) == 0 {
|
||||||
return strconv.Itoa(v)
|
return strconv.Itoa(v)
|
||||||
@@ -81,6 +89,7 @@ func CSVAdd(csv string, v int) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSVRemove removes v from the comma-separated integer list.
|
||||||
func CSVRemove(csv string, v int) string {
|
func CSVRemove(csv string, v int) string {
|
||||||
s := strings.Split(csv, ",")
|
s := strings.Split(csv, ",")
|
||||||
for i, e := range s {
|
for i, e := range s {
|
||||||
@@ -92,6 +101,7 @@ func CSVRemove(csv string, v int) string {
|
|||||||
return strings.Join(s, ",")
|
return strings.Join(s, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSVContains reports whether v is present in the comma-separated integer list.
|
||||||
func CSVContains(csv string, v int) bool {
|
func CSVContains(csv string, v int) bool {
|
||||||
s := strings.Split(csv, ",")
|
s := strings.Split(csv, ",")
|
||||||
for i := 0; i < len(s); i++ {
|
for i := 0; i < len(s); i++ {
|
||||||
@@ -103,6 +113,7 @@ func CSVContains(csv string, v int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSVLength returns the number of elements in the comma-separated list.
|
||||||
func CSVLength(csv string) int {
|
func CSVLength(csv string) int {
|
||||||
if csv == "" {
|
if csv == "" {
|
||||||
return 0
|
return 0
|
||||||
@@ -111,6 +122,7 @@ func CSVLength(csv string) int {
|
|||||||
return len(s)
|
return len(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSVElems parses the comma-separated integer list into an int slice.
|
||||||
func CSVElems(csv string) []int {
|
func CSVElems(csv string) []int {
|
||||||
var r []int
|
var r []int
|
||||||
if csv == "" {
|
if csv == "" {
|
||||||
@@ -124,6 +136,8 @@ func CSVElems(csv string) []int {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSVGetIndex returns the integer at position i in the comma-separated list,
|
||||||
|
// or 0 if i is out of range.
|
||||||
func CSVGetIndex(csv string, i int) int {
|
func CSVGetIndex(csv string, i int) int {
|
||||||
s := CSVElems(csv)
|
s := CSVElems(csv)
|
||||||
if i < len(s) {
|
if i < len(s) {
|
||||||
@@ -132,6 +146,8 @@ func CSVGetIndex(csv string, i int) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSVSetIndex replaces the integer at position i in the comma-separated list
|
||||||
|
// with v. If i is out of range the list is returned unchanged.
|
||||||
func CSVSetIndex(csv string, i int, v int) string {
|
func CSVSetIndex(csv string, i int, v int) string {
|
||||||
s := CSVElems(csv)
|
s := CSVElems(csv)
|
||||||
if i < len(s) {
|
if i < len(s) {
|
||||||
|
|||||||
3
common/token/doc.go
Normal file
3
common/token/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package token provides concurrency-safe random number generation and
|
||||||
|
// alphanumeric token generation for session tokens and warehouse IDs.
|
||||||
|
package token
|
||||||
@@ -12,12 +12,15 @@ type SafeRand struct {
|
|||||||
rng *rand.Rand
|
rng *rand.Rand
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSafeRand creates a SafeRand seeded with the current time.
|
||||||
func NewSafeRand() *SafeRand {
|
func NewSafeRand() *SafeRand {
|
||||||
return &SafeRand{
|
return &SafeRand{
|
||||||
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
|
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intn returns a non-negative pseudo-random int in [0,n). It is safe for
|
||||||
|
// concurrent use.
|
||||||
func (sr *SafeRand) Intn(n int) int {
|
func (sr *SafeRand) Intn(n int) int {
|
||||||
sr.mu.Lock()
|
sr.mu.Lock()
|
||||||
v := sr.rng.Intn(n)
|
v := sr.rng.Intn(n)
|
||||||
@@ -25,6 +28,7 @@ func (sr *SafeRand) Intn(n int) int {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uint32 returns a pseudo-random uint32. It is safe for concurrent use.
|
||||||
func (sr *SafeRand) Uint32() uint32 {
|
func (sr *SafeRand) Uint32() uint32 {
|
||||||
sr.mu.Lock()
|
sr.mu.Lock()
|
||||||
v := sr.rng.Uint32()
|
v := sr.rng.Uint32()
|
||||||
@@ -32,6 +36,8 @@ func (sr *SafeRand) Uint32() uint32 {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RNG is the global concurrency-safe random number generator used throughout
|
||||||
|
// the server for generating warehouse IDs, session tokens, and other values.
|
||||||
var RNG = NewSafeRand()
|
var RNG = NewSafeRand()
|
||||||
|
|
||||||
// Generate returns an alphanumeric token of specified length
|
// Generate returns an alphanumeric token of specified length
|
||||||
|
|||||||
4
network/binpacket/doc.go
Normal file
4
network/binpacket/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package binpacket defines higher-level binary message types that are carried
|
||||||
|
// inside MSG_SYS_CAST_BINARY / MSG_SYS_CASTED_BINARY packets. These include
|
||||||
|
// chat messages, mail notifications, and targeted player broadcasts.
|
||||||
|
package binpacket
|
||||||
@@ -6,20 +6,24 @@ import (
|
|||||||
"erupe-ce/network"
|
"erupe-ce/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MsgBinMailNotify is a binpacket broadcast to notify a player of new mail.
|
||||||
type MsgBinMailNotify struct {
|
type MsgBinMailNotify struct {
|
||||||
SenderName string
|
SenderName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse parses the packet from binary.
|
||||||
func (m MsgBinMailNotify) Parse(bf *byteframe.ByteFrame) error {
|
func (m MsgBinMailNotify) Parse(bf *byteframe.ByteFrame) error {
|
||||||
panic("implement me")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build builds a binary packet from the current data.
|
||||||
func (m MsgBinMailNotify) Build(bf *byteframe.ByteFrame) error {
|
func (m MsgBinMailNotify) Build(bf *byteframe.ByteFrame) error {
|
||||||
bf.WriteUint8(0x01) // Unk
|
bf.WriteUint8(0x01) // Unk
|
||||||
bf.WriteBytes(stringsupport.PaddedString(m.SenderName, 21, true))
|
bf.WriteBytes(stringsupport.PaddedString(m.SenderName, 21, true))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Opcode returns the ID associated with this packet type.
|
||||||
func (m MsgBinMailNotify) Opcode() network.PacketID {
|
func (m MsgBinMailNotify) Opcode() network.PacketID {
|
||||||
return network.MSG_SYS_CASTED_BINARY
|
return network.MSG_SYS_CASTED_BINARY
|
||||||
}
|
}
|
||||||
|
|||||||
3
network/clientctx/doc.go
Normal file
3
network/clientctx/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package clientctx provides per-connection context passed to packet
|
||||||
|
// Parse/Build methods, allowing version-dependent encoding decisions.
|
||||||
|
package clientctx
|
||||||
5
network/crypto/doc.go
Normal file
5
network/crypto/doc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Package crypto implements the symmetric substitution-cipher used by Monster
|
||||||
|
// Hunter Frontier to encrypt and decrypt TCP packet bodies. The algorithm uses
|
||||||
|
// a 256-byte S-box with a rolling derived key and produces three integrity
|
||||||
|
// checksums alongside the ciphertext.
|
||||||
|
package crypto
|
||||||
5
network/doc.go
Normal file
5
network/doc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Package network defines the encrypted TCP transport layer for MHF client
|
||||||
|
// connections. It provides Blowfish-based packet encryption/decryption via
|
||||||
|
// [CryptConn], packet header parsing, and the [PacketID] enumeration of all
|
||||||
|
// ~400 message types in the MHF protocol.
|
||||||
|
package network
|
||||||
4
network/mhfpacket/doc.go
Normal file
4
network/mhfpacket/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package mhfpacket defines the struct representations and binary
|
||||||
|
// serialization for every MHF network packet (~400 message types). Each
|
||||||
|
// packet implements the [MHFPacket] interface (Parse, Build, Opcode).
|
||||||
|
package mhfpacket
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"erupe-ce/network/clientctx"
|
"erupe-ce/network/clientctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// EnumerateGuildType specifies the search/sort criteria for guild enumeration.
|
||||||
type EnumerateGuildType uint8
|
type EnumerateGuildType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"erupe-ce/network/clientctx"
|
"erupe-ce/network/clientctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OperateGuildAction identifies the guild management action to perform.
|
||||||
type OperateGuildAction uint8
|
type OperateGuildAction uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"erupe-ce/network/clientctx"
|
"erupe-ce/network/clientctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OperateGuildMemberAction identifies the guild member management action.
|
||||||
type OperateGuildMemberAction uint8
|
type OperateGuildMemberAction uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"erupe-ce/network/clientctx"
|
"erupe-ce/network/clientctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OperateJointAction identifies the alliance (joint) operation to perform.
|
||||||
type OperateJointAction uint8
|
type OperateJointAction uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"erupe-ce/network/clientctx"
|
"erupe-ce/network/clientctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// OperateMailOperation identifies the mail operation to perform.
|
||||||
type OperateMailOperation uint8
|
type OperateMailOperation uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"erupe-ce/network/clientctx"
|
"erupe-ce/network/clientctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CaAchievementHist is a single entry in the CA achievement history packet.
|
||||||
type CaAchievementHist struct {
|
type CaAchievementHist struct {
|
||||||
Unk0 uint32
|
Unk0 uint32
|
||||||
Unk1 uint8
|
Unk1 uint8
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"erupe-ce/network/clientctx"
|
"erupe-ce/network/clientctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Goocoo represents a single Goocoo (guacot) companion entry in an update packet.
|
||||||
type Goocoo struct {
|
type Goocoo struct {
|
||||||
Index uint32
|
Index uint32
|
||||||
Data1 []int16
|
Data1 []int16
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"erupe-ce/network/clientctx"
|
"erupe-ce/network/clientctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GuildIconMsgPart represents one graphical part of a guild icon (emblem).
|
||||||
type GuildIconMsgPart struct {
|
type GuildIconMsgPart struct {
|
||||||
Index uint16
|
Index uint16
|
||||||
ID uint16
|
ID uint16
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
//revive:disable
|
//revive:disable
|
||||||
|
|
||||||
|
// PacketID identifies an MHF network message type.
|
||||||
type PacketID uint16
|
type PacketID uint16
|
||||||
|
|
||||||
//go:generate stringer -type=PacketID
|
//go:generate stringer -type=PacketID
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config holds the dependencies required to initialize an APIServer.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Logger *zap.Logger
|
Logger *zap.Logger
|
||||||
DB *sqlx.DB
|
DB *sqlx.DB
|
||||||
|
|||||||
5
server/api/doc.go
Normal file
5
server/api/doc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Package api provides an HTTP REST API server (port 8080) for the V2
|
||||||
|
// sign/patch flow and administrative endpoints. It handles user
|
||||||
|
// authentication, character management, launcher configuration, and
|
||||||
|
// screenshot uploads via JSON and XML over HTTP.
|
||||||
|
package api
|
||||||
@@ -24,23 +24,30 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Notification type constants for launcher messages.
|
||||||
const (
|
const (
|
||||||
|
// NotificationDefault represents a standard notification.
|
||||||
NotificationDefault = iota
|
NotificationDefault = iota
|
||||||
|
// NotificationNew represents a new/unread notification.
|
||||||
NotificationNew
|
NotificationNew
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LauncherResponse is the JSON payload returned by the /launcher endpoint,
|
||||||
|
// containing banners, messages, and links for the game launcher UI.
|
||||||
type LauncherResponse struct {
|
type LauncherResponse struct {
|
||||||
Banners []_config.APISignBanner `json:"banners"`
|
Banners []_config.APISignBanner `json:"banners"`
|
||||||
Messages []_config.APISignMessage `json:"messages"`
|
Messages []_config.APISignMessage `json:"messages"`
|
||||||
Links []_config.APISignLink `json:"links"`
|
Links []_config.APISignLink `json:"links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User represents an authenticated user's session credentials and permissions.
|
||||||
type User struct {
|
type User struct {
|
||||||
TokenID uint32 `json:"tokenId"`
|
TokenID uint32 `json:"tokenId"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Rights uint32 `json:"rights"`
|
Rights uint32 `json:"rights"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Character represents a player character's summary data as returned by the API.
|
||||||
type Character struct {
|
type Character struct {
|
||||||
ID uint32 `json:"id"`
|
ID uint32 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -51,6 +58,7 @@ type Character struct {
|
|||||||
LastLogin int32 `json:"lastLogin" db:"last_login"`
|
LastLogin int32 `json:"lastLogin" db:"last_login"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MezFes represents the current Mezeporta Festival event schedule and ticket configuration.
|
||||||
type MezFes struct {
|
type MezFes struct {
|
||||||
ID uint32 `json:"id"`
|
ID uint32 `json:"id"`
|
||||||
Start uint32 `json:"start"`
|
Start uint32 `json:"start"`
|
||||||
@@ -60,6 +68,8 @@ type MezFes struct {
|
|||||||
Stalls []uint32 `json:"stalls"`
|
Stalls []uint32 `json:"stalls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthData is the JSON payload returned after successful login or registration,
|
||||||
|
// containing session info, character list, event data, and server notices.
|
||||||
type AuthData struct {
|
type AuthData struct {
|
||||||
CurrentTS uint32 `json:"currentTs"`
|
CurrentTS uint32 `json:"currentTs"`
|
||||||
ExpiryTS uint32 `json:"expiryTs"`
|
ExpiryTS uint32 `json:"expiryTs"`
|
||||||
@@ -71,6 +81,7 @@ type AuthData struct {
|
|||||||
PatchServer string `json:"patchServer"`
|
PatchServer string `json:"patchServer"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportData wraps a character's full database row for save export.
|
||||||
type ExportData struct {
|
type ExportData struct {
|
||||||
Character map[string]interface{} `json:"character"`
|
Character map[string]interface{} `json:"character"`
|
||||||
}
|
}
|
||||||
@@ -112,6 +123,7 @@ func (s *APIServer) newAuthData(userID uint32, userRights uint32, userTokenID ui
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Launcher handles GET /launcher and returns banners, messages, and links for the launcher UI.
|
||||||
func (s *APIServer) Launcher(w http.ResponseWriter, r *http.Request) {
|
func (s *APIServer) Launcher(w http.ResponseWriter, r *http.Request) {
|
||||||
var respData LauncherResponse
|
var respData LauncherResponse
|
||||||
respData.Banners = s.erupeConfig.API.Banners
|
respData.Banners = s.erupeConfig.API.Banners
|
||||||
@@ -121,6 +133,8 @@ func (s *APIServer) Launcher(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = json.NewEncoder(w).Encode(respData)
|
_ = json.NewEncoder(w).Encode(respData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Login handles POST /login, authenticating a user by username and password
|
||||||
|
// and returning a session token with character data.
|
||||||
func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) {
|
func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
var reqData struct {
|
var reqData struct {
|
||||||
@@ -173,6 +187,8 @@ func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = json.NewEncoder(w).Encode(respData)
|
_ = json.NewEncoder(w).Encode(respData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register handles POST /register, creating a new user account and returning
|
||||||
|
// a session token.
|
||||||
func (s *APIServer) Register(w http.ResponseWriter, r *http.Request) {
|
func (s *APIServer) Register(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
var reqData struct {
|
var reqData struct {
|
||||||
@@ -213,6 +229,8 @@ func (s *APIServer) Register(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = json.NewEncoder(w).Encode(respData)
|
_ = json.NewEncoder(w).Encode(respData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateCharacter handles POST /character/create, creating a new character
|
||||||
|
// slot for the authenticated user.
|
||||||
func (s *APIServer) CreateCharacter(w http.ResponseWriter, r *http.Request) {
|
func (s *APIServer) CreateCharacter(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
var reqData struct {
|
var reqData struct {
|
||||||
@@ -242,6 +260,8 @@ func (s *APIServer) CreateCharacter(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = json.NewEncoder(w).Encode(character)
|
_ = json.NewEncoder(w).Encode(character)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteCharacter handles POST /character/delete, soft-deleting an existing
|
||||||
|
// character or removing an unfinished one.
|
||||||
func (s *APIServer) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
|
func (s *APIServer) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
var reqData struct {
|
var reqData struct {
|
||||||
@@ -267,6 +287,8 @@ func (s *APIServer) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = json.NewEncoder(w).Encode(struct{}{})
|
_ = json.NewEncoder(w).Encode(struct{}{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportSave handles POST /character/export, returning the full character
|
||||||
|
// database row as JSON for backup purposes.
|
||||||
func (s *APIServer) ExportSave(w http.ResponseWriter, r *http.Request) {
|
func (s *APIServer) ExportSave(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
var reqData struct {
|
var reqData struct {
|
||||||
@@ -295,6 +317,8 @@ func (s *APIServer) ExportSave(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(save)
|
_ = json.NewEncoder(w).Encode(save)
|
||||||
}
|
}
|
||||||
|
// ScreenShotGet handles GET /api/ss/bbs/{id}, serving a previously uploaded
|
||||||
|
// screenshot image by its token ID.
|
||||||
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
|
||||||
token := mux.Vars(r)["id"]
|
token := mux.Vars(r)["id"]
|
||||||
@@ -329,6 +353,8 @@ func (s *APIServer) ScreenShotGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ScreenShot handles POST /api/ss/bbs/upload.php, accepting a JPEG image
|
||||||
|
// upload from the game client and saving it to the configured output directory.
|
||||||
func (s *APIServer) ScreenShot(w http.ResponseWriter, r *http.Request) {
|
func (s *APIServer) ScreenShot(w http.ResponseWriter, r *http.Request) {
|
||||||
type Result struct {
|
type Result struct {
|
||||||
XMLName xml.Name `xml:"result"`
|
XMLName xml.Name `xml:"result"`
|
||||||
|
|||||||
3
server/channelserver/compression/deltacomp/doc.go
Normal file
3
server/channelserver/compression/deltacomp/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package deltacomp implements delta-diff decompression for incremental save
|
||||||
|
// data updates sent by the MHF client.
|
||||||
|
package deltacomp
|
||||||
4
server/channelserver/compression/nullcomp/doc.go
Normal file
4
server/channelserver/compression/nullcomp/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package nullcomp implements null-byte run-length compression used by the MHF
|
||||||
|
// client for save data. The format uses a "cmp 20110113" header and encodes
|
||||||
|
// runs of zero bytes as a (0x00, count) pair.
|
||||||
|
package nullcomp
|
||||||
11
server/channelserver/doc.go
Normal file
11
server/channelserver/doc.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Package channelserver implements the gameplay channel server (TCP port
|
||||||
|
// 54001+) that handles all in-game multiplayer functionality. It manages
|
||||||
|
// player sessions, stage (lobby/quest room) state, guild operations, item
|
||||||
|
// management, event systems, and binary state relay between clients.
|
||||||
|
//
|
||||||
|
// Packet handlers are organized by game system into separate files
|
||||||
|
// (handlers_quest.go, handlers_guild.go, etc.) and registered in
|
||||||
|
// handlers_table.go. Each handler has the signature:
|
||||||
|
//
|
||||||
|
// func(s *Session, p mhfpacket.MHFPacket)
|
||||||
|
package channelserver
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FestivalColor is a festival color identifier string.
|
||||||
type FestivalColor string
|
type FestivalColor string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,12 +24,14 @@ const (
|
|||||||
FestivalColorRed FestivalColor = "red"
|
FestivalColorRed FestivalColor = "red"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FestivalColorCodes maps festival colors to their numeric codes.
|
||||||
var FestivalColorCodes = map[FestivalColor]int16{
|
var FestivalColorCodes = map[FestivalColor]int16{
|
||||||
FestivalColorNone: -1,
|
FestivalColorNone: -1,
|
||||||
FestivalColorBlue: 0,
|
FestivalColorBlue: 0,
|
||||||
FestivalColorRed: 1,
|
FestivalColorRed: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GuildApplicationType is the type of a guild application (applied or invited).
|
||||||
type GuildApplicationType string
|
type GuildApplicationType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -36,6 +39,7 @@ const (
|
|||||||
GuildApplicationTypeInvited GuildApplicationType = "invited"
|
GuildApplicationTypeInvited GuildApplicationType = "invited"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Guild represents a guild with all its metadata.
|
||||||
type Guild struct {
|
type Guild struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
@@ -64,11 +68,13 @@ type Guild struct {
|
|||||||
GuildLeader
|
GuildLeader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GuildLeader holds the character ID and name of a guild's leader.
|
||||||
type GuildLeader struct {
|
type GuildLeader struct {
|
||||||
LeaderCharID uint32 `db:"leader_id"`
|
LeaderCharID uint32 `db:"leader_id"`
|
||||||
LeaderName string `db:"leader_name"`
|
LeaderName string `db:"leader_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GuildIconPart represents one graphical part of a guild icon.
|
||||||
type GuildIconPart struct {
|
type GuildIconPart struct {
|
||||||
Index uint16
|
Index uint16
|
||||||
ID uint16
|
ID uint16
|
||||||
@@ -82,6 +88,7 @@ type GuildIconPart struct {
|
|||||||
PosY uint16
|
PosY uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GuildApplication represents a pending guild application or invitation.
|
||||||
type GuildApplication struct {
|
type GuildApplication struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
GuildID uint32 `db:"guild_id"`
|
GuildID uint32 `db:"guild_id"`
|
||||||
@@ -91,6 +98,7 @@ type GuildApplication struct {
|
|||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GuildIcon is a composite guild icon made up of multiple parts.
|
||||||
type GuildIcon struct {
|
type GuildIcon struct {
|
||||||
Parts []GuildIconPart
|
Parts []GuildIconPart
|
||||||
}
|
}
|
||||||
@@ -467,6 +475,7 @@ func (guild *Guild) HasApplicationForCharID(s *Session, charID uint32) (bool, er
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateGuild creates a new guild in the database and adds the session's character as its leader.
|
||||||
func CreateGuild(s *Session, guildName string) (int32, error) {
|
func CreateGuild(s *Session, guildName string) (int32, error) {
|
||||||
transaction, err := s.server.db.Begin()
|
transaction, err := s.server.db.Begin()
|
||||||
|
|
||||||
@@ -539,6 +548,7 @@ func rollbackTransaction(s *Session, transaction *sql.Tx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGuildInfoByID retrieves guild info by guild ID, returning nil if not found.
|
||||||
func GetGuildInfoByID(s *Session, guildID uint32) (*Guild, error) {
|
func GetGuildInfoByID(s *Session, guildID uint32) (*Guild, error) {
|
||||||
rows, err := s.server.db.Queryx(fmt.Sprintf(`
|
rows, err := s.server.db.Queryx(fmt.Sprintf(`
|
||||||
%s
|
%s
|
||||||
@@ -562,6 +572,7 @@ func GetGuildInfoByID(s *Session, guildID uint32) (*Guild, error) {
|
|||||||
return buildGuildObjectFromDbResult(rows, err, s)
|
return buildGuildObjectFromDbResult(rows, err, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGuildInfoByCharacterId retrieves guild info for a character, including applied guilds.
|
||||||
func GetGuildInfoByCharacterId(s *Session, charID uint32) (*Guild, error) {
|
func GetGuildInfoByCharacterId(s *Session, charID uint32) (*Guild, error) {
|
||||||
rows, err := s.server.db.Queryx(fmt.Sprintf(`
|
rows, err := s.server.db.Queryx(fmt.Sprintf(`
|
||||||
%s
|
%s
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ var achievementCurveMap = map[uint8][]int32{
|
|||||||
32: achievementCurves[3],
|
32: achievementCurves[3],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Achievement represents computed achievement data for a character.
|
||||||
type Achievement struct {
|
type Achievement struct {
|
||||||
Level uint8
|
Level uint8
|
||||||
Value uint32
|
Value uint32
|
||||||
@@ -42,6 +43,7 @@ type Achievement struct {
|
|||||||
Trophy uint8
|
Trophy uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAchData computes achievement level and progress from a raw score.
|
||||||
func GetAchData(id uint8, score int32) Achievement {
|
func GetAchData(id uint8, score int32) Achievement {
|
||||||
curve := achievementCurveMap[id]
|
curve := achievementCurveMap[id]
|
||||||
var ach Achievement
|
var ach Achievement
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CafeBonus represents a cafe duration bonus reward entry.
|
||||||
type CafeBonus struct {
|
type CafeBonus struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
TimeReq uint32 `db:"time_req"`
|
TimeReq uint32 `db:"time_req"`
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CampaignEvent represents a promotional campaign event.
|
||||||
type CampaignEvent struct {
|
type CampaignEvent struct {
|
||||||
ID uint32
|
ID uint32
|
||||||
Unk0 uint32
|
Unk0 uint32
|
||||||
@@ -35,6 +36,7 @@ type CampaignEvent struct {
|
|||||||
Categories []uint16
|
Categories []uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CampaignCategory represents a category grouping for campaign events.
|
||||||
type CampaignCategory struct {
|
type CampaignCategory struct {
|
||||||
ID uint16
|
ID uint16
|
||||||
Type uint8
|
Type uint8
|
||||||
@@ -42,6 +44,7 @@ type CampaignCategory struct {
|
|||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CampaignLink links a campaign event to its items/rewards.
|
||||||
type CampaignLink struct {
|
type CampaignLink struct {
|
||||||
CategoryID uint16
|
CategoryID uint16
|
||||||
CampaignID uint32
|
CampaignID uint32
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RyoudamaReward represents a caravan (Ryoudama) reward entry.
|
||||||
type RyoudamaReward struct {
|
type RyoudamaReward struct {
|
||||||
Unk0 uint8
|
Unk0 uint8
|
||||||
Unk1 uint8
|
Unk1 uint8
|
||||||
@@ -16,22 +17,26 @@ type RyoudamaReward struct {
|
|||||||
Unk5 uint16
|
Unk5 uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RyoudamaKeyScore represents a caravan key score entry.
|
||||||
type RyoudamaKeyScore struct {
|
type RyoudamaKeyScore struct {
|
||||||
Unk0 uint8
|
Unk0 uint8
|
||||||
Unk1 int32
|
Unk1 int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RyoudamaCharInfo represents per-character caravan info.
|
||||||
type RyoudamaCharInfo struct {
|
type RyoudamaCharInfo struct {
|
||||||
CID uint32
|
CID uint32
|
||||||
Unk0 int32
|
Unk0 int32
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RyoudamaBoostInfo represents caravan boost status.
|
||||||
type RyoudamaBoostInfo struct {
|
type RyoudamaBoostInfo struct {
|
||||||
Start time.Time
|
Start time.Time
|
||||||
End time.Time
|
End time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ryoudama represents complete caravan data.
|
||||||
type Ryoudama struct {
|
type Ryoudama struct {
|
||||||
Reward []RyoudamaReward
|
Reward []RyoudamaReward
|
||||||
KeyScore []RyoudamaKeyScore
|
KeyScore []RyoudamaKeyScore
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetCharacterSaveData loads a character's save data from the database.
|
||||||
func GetCharacterSaveData(s *Session, charID uint32) (*CharacterSaveData, error) {
|
func GetCharacterSaveData(s *Session, charID uint32) (*CharacterSaveData, error) {
|
||||||
result, err := s.server.db.Query("SELECT id, savedata, is_new_character, name FROM characters WHERE id = $1", charID)
|
result, err := s.server.db.Query("SELECT id, savedata, is_new_character, name FROM characters WHERE id = $1", charID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PaperMissionTimetable represents a daily mission schedule entry.
|
||||||
type PaperMissionTimetable struct {
|
type PaperMissionTimetable struct {
|
||||||
Start time.Time
|
Start time.Time
|
||||||
End time.Time
|
End time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaperMissionData represents daily mission details.
|
||||||
type PaperMissionData struct {
|
type PaperMissionData struct {
|
||||||
Unk0 uint8
|
Unk0 uint8
|
||||||
Unk1 uint8
|
Unk1 uint8
|
||||||
@@ -25,11 +27,13 @@ type PaperMissionData struct {
|
|||||||
Reward2Quantity uint8
|
Reward2Quantity uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaperMission represents a daily mission wrapper.
|
||||||
type PaperMission struct {
|
type PaperMission struct {
|
||||||
Timetables []PaperMissionTimetable
|
Timetables []PaperMissionTimetable
|
||||||
Data []PaperMissionData
|
Data []PaperMissionData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaperData represents complete daily paper data.
|
||||||
type PaperData struct {
|
type PaperData struct {
|
||||||
Unk0 uint16
|
Unk0 uint16
|
||||||
Unk1 int16
|
Unk1 int16
|
||||||
@@ -40,6 +44,7 @@ type PaperData struct {
|
|||||||
Unk6 int16
|
Unk6 int16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaperGift represents a paper gift reward entry.
|
||||||
type PaperGift struct {
|
type PaperGift struct {
|
||||||
Unk0 uint16
|
Unk0 uint16
|
||||||
Unk1 uint8
|
Unk1 uint8
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Distribution represents an item distribution event.
|
||||||
type Distribution struct {
|
type Distribution struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
Deadline time.Time `db:"deadline"`
|
Deadline time.Time `db:"deadline"`
|
||||||
@@ -119,6 +120,7 @@ func handleMsgMhfEnumerateDistItem(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DistributionItem represents a single item in a distribution.
|
||||||
type DistributionItem struct {
|
type DistributionItem struct {
|
||||||
ItemType uint8 `db:"item_type"`
|
ItemType uint8 `db:"item_type"`
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Event represents an in-game event entry.
|
||||||
type Event struct {
|
type Event struct {
|
||||||
EventType uint16
|
EventType uint16
|
||||||
Unk1 uint16
|
Unk1 uint16
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Gacha represents a gacha lottery definition.
|
||||||
type Gacha struct {
|
type Gacha struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
MinGR uint32 `db:"min_gr"`
|
MinGR uint32 `db:"min_gr"`
|
||||||
@@ -22,6 +23,7 @@ type Gacha struct {
|
|||||||
Hidden bool `db:"hidden"`
|
Hidden bool `db:"hidden"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GachaEntry represents a gacha entry (step/box).
|
||||||
type GachaEntry struct {
|
type GachaEntry struct {
|
||||||
EntryType uint8 `db:"entry_type"`
|
EntryType uint8 `db:"entry_type"`
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
@@ -36,6 +38,7 @@ type GachaEntry struct {
|
|||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GachaItem represents a single item in a gacha pool.
|
||||||
type GachaItem struct {
|
type GachaItem struct {
|
||||||
ItemType uint8 `db:"item_type"`
|
ItemType uint8 `db:"item_type"`
|
||||||
ItemID uint16 `db:"item_id"`
|
ItemID uint16 `db:"item_id"`
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GuildAdventure represents a guild adventure expedition.
|
||||||
type GuildAdventure struct {
|
type GuildAdventure struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
Destination uint32 `db:"destination"`
|
Destination uint32 `db:"destination"`
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ END
|
|||||||
FROM guild_alliances ga
|
FROM guild_alliances ga
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// GuildAlliance represents a multi-guild alliance.
|
||||||
type GuildAlliance struct {
|
type GuildAlliance struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
@@ -43,6 +44,7 @@ type GuildAlliance struct {
|
|||||||
SubGuild2 Guild
|
SubGuild2 Guild
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllianceData loads alliance data from the database.
|
||||||
func GetAllianceData(s *Session, AllianceID uint32) (*GuildAlliance, error) {
|
func GetAllianceData(s *Session, AllianceID uint32) (*GuildAlliance, error) {
|
||||||
rows, err := s.server.db.Queryx(fmt.Sprintf(`
|
rows, err := s.server.db.Queryx(fmt.Sprintf(`
|
||||||
%s
|
%s
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MessageBoardPost represents a guild message board post.
|
||||||
type MessageBoardPost struct {
|
type MessageBoardPost struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
StampID uint32 `db:"stamp_id"`
|
StampID uint32 `db:"stamp_id"`
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GuildMeal represents a guild cooking meal entry.
|
||||||
type GuildMeal struct {
|
type GuildMeal struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
MealID uint32 `db:"meal_id"`
|
MealID uint32 `db:"meal_id"`
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GuildMember represents a guild member with role and stats.
|
||||||
type GuildMember struct {
|
type GuildMember struct {
|
||||||
GuildID uint32 `db:"guild_id"`
|
GuildID uint32 `db:"guild_id"`
|
||||||
CharID uint32 `db:"character_id"`
|
CharID uint32 `db:"character_id"`
|
||||||
@@ -92,6 +93,7 @@ SELECT
|
|||||||
LEFT JOIN guilds g ON g.id = gc.guild_id
|
LEFT JOIN guilds g ON g.id = gc.guild_id
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// GetGuildMembers loads all members of a guild.
|
||||||
func GetGuildMembers(s *Session, guildID uint32, applicants bool) ([]*GuildMember, error) {
|
func GetGuildMembers(s *Session, guildID uint32, applicants bool) ([]*GuildMember, error) {
|
||||||
rows, err := s.server.db.Queryx(fmt.Sprintf(`
|
rows, err := s.server.db.Queryx(fmt.Sprintf(`
|
||||||
%s
|
%s
|
||||||
@@ -120,6 +122,7 @@ func GetGuildMembers(s *Session, guildID uint32, applicants bool) ([]*GuildMembe
|
|||||||
return members, nil
|
return members, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCharacterGuildData loads a character's guild membership.
|
||||||
func GetCharacterGuildData(s *Session, charID uint32) (*GuildMember, error) {
|
func GetCharacterGuildData(s *Session, charID uint32) (*GuildMember, error) {
|
||||||
rows, err := s.server.db.Queryx(fmt.Sprintf("%s WHERE character.character_id=$1", guildMembersSelectSQL), charID)
|
rows, err := s.server.db.Queryx(fmt.Sprintf("%s WHERE character.character_id=$1", guildMembersSelectSQL), charID)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"erupe-ce/network/mhfpacket"
|
"erupe-ce/network/mhfpacket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GuildMission represents a guild mission entry.
|
||||||
type GuildMission struct {
|
type GuildMission struct {
|
||||||
ID uint32
|
ID uint32
|
||||||
Unk uint32
|
Unk uint32
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TreasureHunt represents a guild treasure hunt entry.
|
||||||
type TreasureHunt struct {
|
type TreasureHunt struct {
|
||||||
HuntID uint32 `db:"id"`
|
HuntID uint32 `db:"id"`
|
||||||
HostID uint32 `db:"host_id"`
|
HostID uint32 `db:"host_id"`
|
||||||
@@ -147,6 +148,7 @@ func handleMsgMhfOperateGuildTresureReport(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TreasureSouvenir represents a guild treasure souvenir entry.
|
||||||
type TreasureSouvenir struct {
|
type TreasureSouvenir struct {
|
||||||
Destination uint32
|
Destination uint32
|
||||||
Quantity uint32
|
Quantity uint32
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func handleMsgMhfUpdateInterior(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HouseData represents player house/my house data.
|
||||||
type HouseData struct {
|
type HouseData struct {
|
||||||
CharID uint32 `db:"id"`
|
CharID uint32 `db:"id"`
|
||||||
HR uint16 `db:"hr"`
|
HR uint16 `db:"hr"`
|
||||||
@@ -329,6 +330,7 @@ func handleMsgMhfSaveDecoMyset(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title represents a hunter title entry.
|
||||||
type Title struct {
|
type Title struct {
|
||||||
ID uint16 `db:"id"`
|
ID uint16 `db:"id"`
|
||||||
Acquired time.Time `db:"unlocked_at"`
|
Acquired time.Time `db:"unlocked_at"`
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Mail represents an in-game mail message.
|
||||||
type Mail struct {
|
type Mail struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
SenderID uint32 `db:"sender_id"`
|
SenderID uint32 `db:"sender_id"`
|
||||||
@@ -79,6 +80,7 @@ func (m *Mail) MarkRead(s *Session) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMailListForCharacter loads all mail for a character.
|
||||||
func GetMailListForCharacter(s *Session, charID uint32) ([]Mail, error) {
|
func GetMailListForCharacter(s *Session, charID uint32) ([]Mail, error) {
|
||||||
rows, err := s.server.db.Queryx(`
|
rows, err := s.server.db.Queryx(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -127,6 +129,7 @@ func GetMailListForCharacter(s *Session, charID uint32) ([]Mail, error) {
|
|||||||
return allMail, nil
|
return allMail, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMailByID loads a single mail by ID.
|
||||||
func GetMailByID(s *Session, ID int) (*Mail, error) {
|
func GetMailByID(s *Session, ID int) (*Mail, error) {
|
||||||
row := s.server.db.QueryRowx(`
|
row := s.server.db.QueryRowx(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -167,6 +170,7 @@ func GetMailByID(s *Session, ID int) (*Mail, error) {
|
|||||||
return mail, nil
|
return mail, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMailNotification sends a new mail notification to a player.
|
||||||
func SendMailNotification(s *Session, m *Mail, recipient *Session) {
|
func SendMailNotification(s *Session, m *Mail, recipient *Session) {
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
|
|
||||||
|
|||||||
@@ -370,6 +370,7 @@ func handleMsgMhfEnumerateAiroulist(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
|
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Airou represents Airou (felyne companion) data.
|
||||||
type Airou struct {
|
type Airou struct {
|
||||||
ID uint32
|
ID uint32
|
||||||
Name []byte
|
Name []byte
|
||||||
@@ -445,6 +446,7 @@ func getGuildAirouList(s *Session) []Airou {
|
|||||||
return guildCats
|
return guildCats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAirouDetails parses Airou data from a ByteFrame.
|
||||||
func GetAirouDetails(bf *byteframe.ByteFrame) []Airou {
|
func GetAirouDetails(bf *byteframe.ByteFrame) []Airou {
|
||||||
catCount := bf.ReadUint8()
|
catCount := bf.ReadUint8()
|
||||||
cats := make([]Airou, catCount)
|
cats := make([]Airou, catCount)
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ func handleMsgMhfGetLobbyCrowd(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0x320))
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0x320))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TrendWeapon represents trending weapon usage data.
|
||||||
type TrendWeapon struct {
|
type TrendWeapon struct {
|
||||||
WeaponType uint8
|
WeaponType uint8
|
||||||
WeaponID uint16
|
WeaponID uint16
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func equal(a, b []byte) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackportQuest converts a quest binary to an older format.
|
||||||
func BackportQuest(data []byte) []byte {
|
func BackportQuest(data []byte) []byte {
|
||||||
wp := binary.LittleEndian.Uint32(data[0:4]) + 96
|
wp := binary.LittleEndian.Uint32(data[0:4]) + 96
|
||||||
rp := wp + 4
|
rp := wp + 4
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func handleMsgMhfReleaseEvent(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RaviUpdate represents a Raviente register update entry.
|
||||||
type RaviUpdate struct {
|
type RaviUpdate struct {
|
||||||
Op uint8
|
Op uint8
|
||||||
Dest uint8
|
Dest uint8
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ const rengokuScoreQuery = `, c.name FROM rengoku_score rs
|
|||||||
LEFT JOIN characters c ON c.id = rs.character_id
|
LEFT JOIN characters c ON c.id = rs.character_id
|
||||||
LEFT JOIN guild_characters gc ON gc.character_id = rs.character_id `
|
LEFT JOIN guild_characters gc ON gc.character_id = rs.character_id `
|
||||||
|
|
||||||
|
// RengokuScore represents a Rengoku (Hunting Road) ranking score.
|
||||||
type RengokuScore struct {
|
type RengokuScore struct {
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Score uint32 `db:"score"`
|
Score uint32 `db:"score"`
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Scenario represents scenario counter data.
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
MainID uint32
|
MainID uint32
|
||||||
// 0 = Basic
|
// 0 = Basic
|
||||||
|
|||||||
@@ -6,27 +6,32 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SeibattleTimetable represents a seibattle schedule entry.
|
||||||
type SeibattleTimetable struct {
|
type SeibattleTimetable struct {
|
||||||
Start time.Time
|
Start time.Time
|
||||||
End time.Time
|
End time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SeibattleKeyScore represents a seibattle key score.
|
||||||
type SeibattleKeyScore struct {
|
type SeibattleKeyScore struct {
|
||||||
Unk0 uint8
|
Unk0 uint8
|
||||||
Unk1 int32
|
Unk1 int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SeibattleCareer represents seibattle career stats.
|
||||||
type SeibattleCareer struct {
|
type SeibattleCareer struct {
|
||||||
Unk0 uint16
|
Unk0 uint16
|
||||||
Unk1 uint16
|
Unk1 uint16
|
||||||
Unk2 uint16
|
Unk2 uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SeibattleOpponent represents seibattle opponent data.
|
||||||
type SeibattleOpponent struct {
|
type SeibattleOpponent struct {
|
||||||
Unk0 int32
|
Unk0 int32
|
||||||
Unk1 int8
|
Unk1 int8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SeibattleConventionResult represents a seibattle convention result.
|
||||||
type SeibattleConventionResult struct {
|
type SeibattleConventionResult struct {
|
||||||
Unk0 uint32
|
Unk0 uint32
|
||||||
Unk1 uint16
|
Unk1 uint16
|
||||||
@@ -35,10 +40,12 @@ type SeibattleConventionResult struct {
|
|||||||
Unk4 uint16
|
Unk4 uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SeibattleCharScore represents a seibattle per-character score.
|
||||||
type SeibattleCharScore struct {
|
type SeibattleCharScore struct {
|
||||||
Unk0 uint32
|
Unk0 uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SeibattleCurResult represents a seibattle current result.
|
||||||
type SeibattleCurResult struct {
|
type SeibattleCurResult struct {
|
||||||
Unk0 uint32
|
Unk0 uint32
|
||||||
Unk1 uint16
|
Unk1 uint16
|
||||||
@@ -46,6 +53,7 @@ type SeibattleCurResult struct {
|
|||||||
Unk3 uint16
|
Unk3 uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seibattle represents complete seibattle data.
|
||||||
type Seibattle struct {
|
type Seibattle struct {
|
||||||
Timetable []SeibattleTimetable
|
Timetable []SeibattleTimetable
|
||||||
KeyScore []SeibattleKeyScore
|
KeyScore []SeibattleKeyScore
|
||||||
@@ -159,6 +167,7 @@ func handleMsgMhfGetBreakSeibatuLevelReward(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WeeklySeibatuRankingReward represents a weekly seibattle ranking reward.
|
||||||
type WeeklySeibatuRankingReward struct {
|
type WeeklySeibatuRankingReward struct {
|
||||||
Unk0 int32
|
Unk0 int32
|
||||||
Unk1 int32
|
Unk1 int32
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ShopItem represents a shop item listing.
|
||||||
type ShopItem struct {
|
type ShopItem struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
ItemID uint32 `db:"item_id"`
|
ItemID uint32 `db:"item_id"`
|
||||||
@@ -248,6 +249,7 @@ 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})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FPointExchange represents a frontier point exchange entry.
|
||||||
type FPointExchange struct {
|
type FPointExchange struct {
|
||||||
ID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
ItemType uint8 `db:"item_type"`
|
ItemType uint8 `db:"item_type"`
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TournamentInfo0 represents tournament information (type 0).
|
||||||
type TournamentInfo0 struct {
|
type TournamentInfo0 struct {
|
||||||
ID uint32
|
ID uint32
|
||||||
MaxPlayers uint32
|
MaxPlayers uint32
|
||||||
@@ -28,6 +29,7 @@ type TournamentInfo0 struct {
|
|||||||
Unk6 string
|
Unk6 string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TournamentInfo21 represents tournament information (type 21).
|
||||||
type TournamentInfo21 struct {
|
type TournamentInfo21 struct {
|
||||||
Unk0 uint32
|
Unk0 uint32
|
||||||
Unk1 uint32
|
Unk1 uint32
|
||||||
@@ -35,6 +37,7 @@ type TournamentInfo21 struct {
|
|||||||
Unk3 uint8
|
Unk3 uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TournamentInfo22 represents tournament information (type 22).
|
||||||
type TournamentInfo22 struct {
|
type TournamentInfo22 struct {
|
||||||
Unk0 uint32
|
Unk0 uint32
|
||||||
Unk1 uint32
|
Unk1 uint32
|
||||||
@@ -110,6 +113,7 @@ func handleMsgMhfEntryTournament(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TournamentReward represents a tournament reward entry.
|
||||||
type TournamentReward struct {
|
type TournamentReward struct {
|
||||||
Unk0 uint16
|
Unk0 uint16
|
||||||
Unk1 uint16
|
Unk1 uint16
|
||||||
|
|||||||
@@ -14,21 +14,25 @@ import (
|
|||||||
"erupe-ce/network/mhfpacket"
|
"erupe-ce/network/mhfpacket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TowerInfoTRP represents tower RP (points) info.
|
||||||
type TowerInfoTRP struct {
|
type TowerInfoTRP struct {
|
||||||
TR int32
|
TR int32
|
||||||
TRP int32
|
TRP int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TowerInfoSkill represents tower skill info.
|
||||||
type TowerInfoSkill struct {
|
type TowerInfoSkill struct {
|
||||||
TSP int32
|
TSP int32
|
||||||
Skills []int16 // 64
|
Skills []int16 // 64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TowerInfoHistory represents tower clear history.
|
||||||
type TowerInfoHistory struct {
|
type TowerInfoHistory struct {
|
||||||
Unk0 []int16 // 5
|
Unk0 []int16 // 5
|
||||||
Unk1 []int16 // 5
|
Unk1 []int16 // 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TowerInfoLevel represents tower level info.
|
||||||
type TowerInfoLevel struct {
|
type TowerInfoLevel struct {
|
||||||
Floors int32
|
Floors int32
|
||||||
Unk1 int32
|
Unk1 int32
|
||||||
@@ -36,6 +40,7 @@ type TowerInfoLevel struct {
|
|||||||
Unk3 int32
|
Unk3 int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EmptyTowerCSV creates an empty CSV string of the given length.
|
||||||
func EmptyTowerCSV(len int) string {
|
func EmptyTowerCSV(len int) string {
|
||||||
temp := make([]string, len)
|
temp := make([]string, len)
|
||||||
for i := range temp {
|
for i := range temp {
|
||||||
@@ -194,6 +199,7 @@ var tenrouiraiData = []TenrouiraiData{
|
|||||||
{2, 6, 40, 0, 3, 1, 0, 0, 1, 1},
|
{2, 6, 40, 0, 3, 1, 0, 0, 1, 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TenrouiraiProgress represents Tenrouirai (sky corridor) progress.
|
||||||
type TenrouiraiProgress struct {
|
type TenrouiraiProgress struct {
|
||||||
Page uint8
|
Page uint8
|
||||||
Mission1 uint16
|
Mission1 uint16
|
||||||
@@ -201,17 +207,20 @@ type TenrouiraiProgress struct {
|
|||||||
Mission3 uint16
|
Mission3 uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TenrouiraiReward represents a Tenrouirai reward.
|
||||||
type TenrouiraiReward struct {
|
type TenrouiraiReward struct {
|
||||||
Index uint8
|
Index uint8
|
||||||
Item []uint16 // 5
|
Item []uint16 // 5
|
||||||
Quantity []uint8 // 5
|
Quantity []uint8 // 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TenrouiraiKeyScore represents a Tenrouirai key score.
|
||||||
type TenrouiraiKeyScore struct {
|
type TenrouiraiKeyScore struct {
|
||||||
Unk0 uint8
|
Unk0 uint8
|
||||||
Unk1 int32
|
Unk1 int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TenrouiraiData represents Tenrouirai data.
|
||||||
type TenrouiraiData struct {
|
type TenrouiraiData struct {
|
||||||
Block uint8
|
Block uint8
|
||||||
Mission uint8
|
Mission uint8
|
||||||
@@ -231,17 +240,20 @@ type TenrouiraiData struct {
|
|||||||
Skill6 uint8 // 50
|
Skill6 uint8 // 50
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TenrouiraiCharScore represents a Tenrouirai per-character score.
|
||||||
type TenrouiraiCharScore struct {
|
type TenrouiraiCharScore struct {
|
||||||
Score int32
|
Score int32
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TenrouiraiTicket represents a Tenrouirai ticket entry.
|
||||||
type TenrouiraiTicket struct {
|
type TenrouiraiTicket struct {
|
||||||
Unk0 uint8
|
Unk0 uint8
|
||||||
RP uint32
|
RP uint32
|
||||||
Unk2 uint32
|
Unk2 uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tenrouirai represents complete Tenrouirai data.
|
||||||
type Tenrouirai struct {
|
type Tenrouirai struct {
|
||||||
Progress []TenrouiraiProgress
|
Progress []TenrouiraiProgress
|
||||||
Reward []TenrouiraiReward
|
Reward []TenrouiraiReward
|
||||||
@@ -429,11 +441,13 @@ func handleMsgMhfPresentBox(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckEarthSucceed(s, pkt.AckHandle, data)
|
doAckEarthSucceed(s, pkt.AckHandle, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GemInfo represents gem (decoration) info.
|
||||||
type GemInfo struct {
|
type GemInfo struct {
|
||||||
Gem uint16
|
Gem uint16
|
||||||
Quantity uint16
|
Quantity uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GemHistory represents gem usage history.
|
||||||
type GemHistory struct {
|
type GemHistory struct {
|
||||||
Gem uint16
|
Gem uint16
|
||||||
Message uint16
|
Message uint16
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"erupe-ce/server/channelserver/compression/nullcomp"
|
"erupe-ce/server/channelserver/compression/nullcomp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SavePointer identifies a section within the character save data blob.
|
||||||
type SavePointer int
|
type SavePointer int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -29,6 +30,7 @@ const (
|
|||||||
lBookshelfData
|
lBookshelfData
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CharacterSaveData holds a character's save data and its parsed fields.
|
||||||
type CharacterSaveData struct {
|
type CharacterSaveData struct {
|
||||||
CharID uint32
|
CharID uint32
|
||||||
Name string
|
Name string
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Raviente holds shared state for the Raviente siege event.
|
||||||
type Raviente struct {
|
type Raviente struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
id uint16
|
id uint16
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ func (s *Server) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WorldcastMHF broadcasts a packet to all sessions across all channel servers.
|
||||||
func (s *Server) WorldcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session, ignoredChannel *Server) {
|
func (s *Server) WorldcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session, ignoredChannel *Server) {
|
||||||
for _, c := range s.Channels {
|
for _, c := range s.Channels {
|
||||||
if c == ignoredChannel {
|
if c == ignoredChannel {
|
||||||
@@ -292,6 +293,7 @@ func (s *Server) BroadcastChatMessage(message string) {
|
|||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DiscordChannelSend sends a chat message to the configured Discord channel.
|
||||||
func (s *Server) DiscordChannelSend(charName string, content string) {
|
func (s *Server) DiscordChannelSend(charName string, content string) {
|
||||||
if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
|
if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
|
||||||
message := fmt.Sprintf("**%s**: %s", charName, content)
|
message := fmt.Sprintf("**%s**: %s", charName, content)
|
||||||
@@ -299,6 +301,7 @@ func (s *Server) DiscordChannelSend(charName string, content string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DiscordScreenShotSend sends a screenshot link to the configured Discord channel.
|
||||||
func (s *Server) DiscordScreenShotSend(charName string, title string, description string, articleToken string) {
|
func (s *Server) DiscordScreenShotSend(charName string, title string, description string, articleToken string) {
|
||||||
if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
|
if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
|
||||||
imageUrl := fmt.Sprintf("%s:%d/api/ss/bbs/%s", s.erupeConfig.Screenshots.Host, s.erupeConfig.Screenshots.Port, articleToken)
|
imageUrl := fmt.Sprintf("%s:%d/api/ss/bbs/%s", s.erupeConfig.Screenshots.Host, s.erupeConfig.Screenshots.Port, articleToken)
|
||||||
@@ -307,6 +310,7 @@ func (s *Server) DiscordScreenShotSend(charName string, title string, descriptio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindSessionByCharID looks up a session by character ID across all channels.
|
||||||
func (s *Server) FindSessionByCharID(charID uint32) *Session {
|
func (s *Server) FindSessionByCharID(charID uint32) *Session {
|
||||||
for _, c := range s.Channels {
|
for _, c := range s.Channels {
|
||||||
for _, session := range c.sessions {
|
for _, session := range c.sessions {
|
||||||
@@ -318,6 +322,7 @@ func (s *Server) FindSessionByCharID(charID uint32) *Session {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisconnectUser disconnects all sessions belonging to the given user ID.
|
||||||
func (s *Server) DisconnectUser(uid uint32) {
|
func (s *Server) DisconnectUser(uid uint32) {
|
||||||
var cid uint32
|
var cid uint32
|
||||||
var cids []uint32
|
var cids []uint32
|
||||||
@@ -343,6 +348,7 @@ func (s *Server) DisconnectUser(uid uint32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindObjectByChar finds a stage object owned by the given character ID.
|
||||||
func (s *Server) FindObjectByChar(charID uint32) *Object {
|
func (s *Server) FindObjectByChar(charID uint32) *Object {
|
||||||
s.stagesLock.RLock()
|
s.stagesLock.RLock()
|
||||||
defer s.stagesLock.RUnlock()
|
defer s.stagesLock.RUnlock()
|
||||||
@@ -361,6 +367,7 @@ func (s *Server) FindObjectByChar(charID uint32) *Object {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasSemaphore checks if the given session is hosting any semaphore.
|
||||||
func (s *Server) HasSemaphore(ses *Session) bool {
|
func (s *Server) HasSemaphore(ses *Session) bool {
|
||||||
for _, semaphore := range s.semaphore {
|
for _, semaphore := range s.semaphore {
|
||||||
if semaphore.host == ses {
|
if semaphore.host == ses {
|
||||||
@@ -370,6 +377,7 @@ func (s *Server) HasSemaphore(ses *Session) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Season returns the current in-game season (0-2) based on server ID and time.
|
||||||
func (s *Server) Season() uint8 {
|
func (s *Server) Season() uint8 {
|
||||||
sid := int64(((s.ID & 0xFF00) - 4096) / 256)
|
sid := int64(((s.ID & 0xFF00) - 4096) / 256)
|
||||||
return uint8(((TimeAdjusted().Unix() / 86400) + sid) % 3)
|
return uint8(((TimeAdjusted().Unix() / 86400) + sid) % 3)
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ func (s *Session) getObjectId() uint32 {
|
|||||||
return uint32(s.objectID)<<16 | uint32(s.objectIndex)
|
return uint32(s.objectID)<<16 | uint32(s.objectIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSemaphoreID returns the semaphore ID held by the session, varying by semaphore mode.
|
||||||
func (s *Session) GetSemaphoreID() uint32 {
|
func (s *Session) GetSemaphoreID() uint32 {
|
||||||
if s.semaphoreMode {
|
if s.semaphoreMode {
|
||||||
return 0x000E0000 + uint32(s.semaphoreID[1])
|
return 0x000E0000 + uint32(s.semaphoreID[1])
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TimeAdjusted, TimeMidnight, TimeWeekStart, TimeWeekNext, and TimeGameAbsolute
|
||||||
|
// are package-level wrappers around the gametime utility functions, providing
|
||||||
|
// convenient access to adjusted server time, daily/weekly boundaries, and the
|
||||||
|
// absolute game timestamp used by the MHF client.
|
||||||
|
|
||||||
func TimeAdjusted() time.Time { return gametime.Adjusted() }
|
func TimeAdjusted() time.Time { return gametime.Adjusted() }
|
||||||
func TimeMidnight() time.Time { return gametime.Midnight() }
|
func TimeMidnight() time.Time { return gametime.Midnight() }
|
||||||
func TimeWeekStart() time.Time { return gametime.WeekStart() }
|
func TimeWeekStart() time.Time { return gametime.WeekStart() }
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Commands defines the slash commands registered with Discord, including
|
||||||
|
// account linking and password management.
|
||||||
var Commands = []*discordgo.ApplicationCommand{
|
var Commands = []*discordgo.ApplicationCommand{
|
||||||
{
|
{
|
||||||
Name: "link",
|
Name: "link",
|
||||||
@@ -35,6 +37,8 @@ var Commands = []*discordgo.ApplicationCommand{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DiscordBot manages a Discord session and provides methods for relaying
|
||||||
|
// messages between the game server and a configured Discord channel.
|
||||||
type DiscordBot struct {
|
type DiscordBot struct {
|
||||||
Session *discordgo.Session
|
Session *discordgo.Session
|
||||||
config *_config.Config
|
config *_config.Config
|
||||||
@@ -43,11 +47,14 @@ type DiscordBot struct {
|
|||||||
RelayChannel *discordgo.Channel
|
RelayChannel *discordgo.Channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options holds the configuration and logger required to create a DiscordBot.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Config *_config.Config
|
Config *_config.Config
|
||||||
Logger *zap.Logger
|
Logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDiscordBot creates a DiscordBot using the provided options, establishing
|
||||||
|
// a Discord session and optionally resolving the relay channel.
|
||||||
func NewDiscordBot(options Options) (discordBot *DiscordBot, err error) {
|
func NewDiscordBot(options Options) (discordBot *DiscordBot, err error) {
|
||||||
session, err := discordgo.New("Bot " + options.Config.Discord.BotToken)
|
session, err := discordgo.New("Bot " + options.Config.Discord.BotToken)
|
||||||
|
|
||||||
@@ -77,6 +84,7 @@ func NewDiscordBot(options Options) (discordBot *DiscordBot, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start opens the websocket connection to Discord.
|
||||||
func (bot *DiscordBot) Start() (err error) {
|
func (bot *DiscordBot) Start() (err error) {
|
||||||
err = bot.Session.Open()
|
err = bot.Session.Open()
|
||||||
|
|
||||||
@@ -105,6 +113,8 @@ func (bot *DiscordBot) NormalizeDiscordMessage(message string) string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RealtimeChannelSend sends a message to the configured relay channel. If no
|
||||||
|
// relay channel is configured, the call is a no-op.
|
||||||
func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) {
|
func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) {
|
||||||
if bot.RelayChannel == nil {
|
if bot.RelayChannel == nil {
|
||||||
return
|
return
|
||||||
@@ -114,6 +124,8 @@ func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) {
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// ReplaceTextAll replaces every match of regex in text by calling handler with
|
||||||
|
// the first capture group of each match and substituting the result.
|
||||||
func ReplaceTextAll(text string, regex *regexp.Regexp, handler func(input string) string) string {
|
func ReplaceTextAll(text string, regex *regexp.Regexp, handler func(input string) string) string {
|
||||||
result := regex.ReplaceAllFunc([]byte(text), func(s []byte) []byte {
|
result := regex.ReplaceAllFunc([]byte(text), func(s []byte) []byte {
|
||||||
input := regex.ReplaceAllString(string(s), `$1`)
|
input := regex.ReplaceAllString(string(s), `$1`)
|
||||||
|
|||||||
4
server/discordbot/doc.go
Normal file
4
server/discordbot/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package discordbot provides an optional Discord bot integration that relays
|
||||||
|
// in-game chat to Discord channels and supports slash commands for server
|
||||||
|
// management.
|
||||||
|
package discordbot
|
||||||
12
server/entranceserver/doc.go
Normal file
12
server/entranceserver/doc.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Package entranceserver implements the MHF entrance server, which listens on
|
||||||
|
// TCP port 53310 and acts as the gateway between authentication (sign server)
|
||||||
|
// and gameplay (channel servers). It presents the server list to authenticated
|
||||||
|
// clients, handles character selection, and directs players to the appropriate
|
||||||
|
// channel server.
|
||||||
|
//
|
||||||
|
// The entrance server uses MHF's custom "binary8" encryption and "sum32"
|
||||||
|
// checksum for all client-server communication. Each client connection is
|
||||||
|
// short-lived: the server sends a single response containing the server list
|
||||||
|
// (SV2/SVR) and optionally user session data (USR), then closes the
|
||||||
|
// connection.
|
||||||
|
package entranceserver
|
||||||
5
server/signserver/doc.go
Normal file
5
server/signserver/doc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Package signserver implements the MHF sign server, which handles client
|
||||||
|
// authentication, session creation, and character management. It listens
|
||||||
|
// on TCP port 53312 and is the first server a client connects to in the
|
||||||
|
// three-server network model (sign, entrance, channel).
|
||||||
|
package signserver
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package signserver
|
package signserver
|
||||||
|
|
||||||
|
// RespID represents a sign server response code sent to the client
|
||||||
|
// to indicate the result of an authentication or session operation.
|
||||||
type RespID uint8
|
type RespID uint8
|
||||||
|
|
||||||
|
// Sign server response codes. These values are sent as the first byte of
|
||||||
|
// a sign response and map to client-side error messages.
|
||||||
const (
|
const (
|
||||||
SIGN_UNKNOWN RespID = iota
|
SIGN_UNKNOWN RespID = iota
|
||||||
SIGN_SUCCESS
|
SIGN_SUCCESS
|
||||||
|
|||||||
Reference in New Issue
Block a user