lawless/server/modules/map.go

300 行
8.9 KiB
Go

package modules
import (
"context"
"database/sql"
"encoding/json"
"github.com/heroiclabs/nakama-common/runtime"
)
// RegisterMap 注册地图/副本/事件相关 RPC。
func RegisterMap(initializer runtime.Initializer) error {
rpcs := map[string]func(context.Context, runtime.Logger, *sql.DB, runtime.NakamaModule, string) (string, error){
"MapService/GetRegion": getRegion,
"MapService/GetNearby": getNearby,
"MapService/EnterInstance": enterInstance,
"MapService/ListWorldEvents": listWorldEvents,
"MapService/PublishPlayerEvent": publishPlayerEvent,
}
for name, fn := range rpcs {
if err := initializer.RegisterRpc(name, fn); err != nil {
return err
}
}
return nil
}
type regionReq struct {
RegionID string `json:"region_id"`
}
type nearbyReq struct {
RegionID string `json:"region_id"`
Limit int32 `json:"limit"`
EventTypes []string `json:"event_types"`
}
type enterInstanceReq struct {
InstanceID string `json:"instance_id"`
PartyMembers []string `json:"party_members"`
ConsumableKeyID string `json:"consumable_key_id"`
UseStamina bool `json:"use_stamina"`
}
type worldEventReq struct {
WorldTier int32 `json:"world_tier"`
RegionID string `json:"region_id"`
EventScope string `json:"event_scope"`
Page int32 `json:"page"`
PageSize int32 `json:"page_size"`
}
type playerEventReq struct {
EventType string `json:"event_type"`
RegionID string `json:"region_id"`
CostItems []materialCostReq `json:"cost_items"`
Description string `json:"description"`
}
type regionData struct {
RegionID string `json:"region_id"`
WorldTier int32 `json:"world_tier"`
Name string `json:"name"`
IsSafeZone bool `json:"is_safe_zone"`
PvpRules interface{} `json:"pvp_rules"`
Weather string `json:"weather"`
ResourceDensity int32 `json:"resource_density"`
DangerLevel int32 `json:"danger_level"`
NearbyPlayerCount int32 `json:"nearby_player_count"`
}
type nearbyData struct {
Players interface{} `json:"players"`
Events interface{} `json:"events"`
}
type instanceRunData struct {
RunID string `json:"run_id"`
InstanceID string `json:"instance_id"`
Status string `json:"status"`
DifficultyCoefficient float64 `json:"difficulty_coefficient"`
ConsumedKeys int32 `json:"consumed_keys"`
StartedAt string `json:"started_at"`
}
type worldEventItemData struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"`
WorldTier int32 `json:"world_tier"`
RegionID string `json:"region_id"`
Title string `json:"title"`
Content string `json:"content"`
ExpiresAt string `json:"expires_at"`
}
type worldEventListData struct {
Total int32 `json:"total"`
List []worldEventItemData `json:"list"`
}
type worldEventData struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
func getRegion(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
traceID := newTraceID()
var req regionReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(8001, "invalid payload", traceID)
}
// 查询区域信息
var name string
var worldTier int32
var isSafeZone bool
err := db.QueryRowContext(ctx, `
SELECT name, world_tier, is_safe_zone FROM regions WHERE id = $1
`, req.RegionID).Scan(&name, &worldTier, &isSafeZone)
if err != nil {
return errResp(8002, "region not found", traceID)
}
// 查询区域内的区域
rows, err := db.QueryContext(ctx, `
SELECT id, zone_type, name, terrain, danger_level, rarity
FROM zones WHERE region_id = $1
ORDER BY danger_level ASC
`, req.RegionID)
if err != nil {
logger.Error("get region zones failed: %v", err)
return errResp(9002, "internal error", traceID)
}
defer rows.Close()
var zones []zoneData
for rows.Next() {
var z zoneData
if err := rows.Scan(&z.ZoneID, &z.ZoneType, &z.Name, &z.Terrain, &z.DangerLevel, &z.Rarity); err != nil {
continue
}
zones = append(zones, z)
}
return okResp(regionData{
RegionID: req.RegionID,
Name: name,
WorldTier: worldTier,
IsSafeZone: isSafeZone,
Zones: zones,
}, traceID)
}
func getNearby(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
traceID := newTraceID()
var req nearbyReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(8001, "invalid payload", traceID)
}
if req.Limit <= 0 || req.Limit > 50 {
req.Limit = 20
}
// 查询附近玩家(同区域)
rows, err := db.QueryContext(ctx, `
SELECT c.id, c.name, c.race_id, c.world_tier, c.realm_tier
FROM characters c
JOIN regions r ON c.world_tier = r.world_tier
WHERE r.id = $1 AND c.status = 'active' AND c.id != $2
ORDER BY c.last_online_at DESC
LIMIT $3
`, req.RegionID, req.ExcludeCharacterID, req.Limit)
if err != nil {
logger.Error("get nearby players failed: %v", err)
return errResp(9002, "internal error", traceID)
}
defer rows.Close()
type playerInfo struct {
ID string `json:"id"`
Name string `json:"name"`
RaceID string `json:"race_id"`
WorldTier int32 `json:"world_tier"`
RealmTier int32 `json:"realm_tier"`
}
var players []playerInfo
for rows.Next() {
var p playerInfo
if err := rows.Scan(&p.ID, &p.Name, &p.RaceID, &p.WorldTier, &p.RealmTier); err != nil {
continue
}
players = append(players, p)
}
return okResp(nearbyData{
Players: players,
Events: []interface{}{},
}, traceID)
}
func enterInstance(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
traceID := newTraceID()
var req enterInstanceReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(8003, "invalid payload", traceID)
}
// 校验副本是否存在
var instanceExists bool
var recommendedRealm int32
err := db.QueryRowContext(ctx, `
SELECT EXISTS(SELECT 1 FROM instances WHERE id = $1), recommended_realm_tier
FROM instances WHERE id = $1
`, req.InstanceID).Scan(&instanceExists, &recommendedRealm)
if err != nil || !instanceExists {
return errResp(8004, "instance not found", traceID)
}
// 创建副本运行记录
var runID string
err = db.QueryRowContext(ctx, `
INSERT INTO instance_runs (instance_id, party_leader_id, party_members, difficulty_coefficient, status, consumed_keys, started_at)
VALUES ($1, $2, $3, 1.15, 'in_progress', 1, NOW())
RETURNING id::text
`, req.InstanceID, req.LeaderID, req.PartyMembers).Scan(&runID)
if err != nil {
logger.Error("enter instance failed: %v", err)
return errResp(9002, "internal error", traceID)
}
logger.Info("MapService/EnterInstance success: run_id=%s", runID)
return okResp(instanceRunData{
RunID: runID,
InstanceID: req.InstanceID,
Status: "in_progress",
DifficultyCoefficient: 1.15,
ConsumedKeys: 1,
}, traceID)
}
func listWorldEvents(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
traceID := newTraceID()
var req worldEventReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(8001, "invalid payload", traceID)
}
// 简化版:返回示例事件
events := []worldEventItemData{
{EventID: "evt_001", EventType: "world_boss", Status: "active", CreatedAt: time.Now().Format(time.RFC3339)},
{EventID: "evt_002", EventType: "resource_spawn", Status: "active", CreatedAt: time.Now().Format(time.RFC3339)},
}
return okResp(worldEventListData{
Total: int32(len(events)),
List: events,
}, traceID)
}
func publishPlayerEvent(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
traceID := newTraceID()
uid := userIDFromCtx(ctx)
if uid == "" {
return errResp(1001, "missing token", traceID)
}
var req playerEventReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(8009, "invalid payload", traceID)
}
// 获取角色ID
var charID string
err := db.QueryRowContext(ctx, `
SELECT id FROM characters WHERE player_id = $1 AND status = 'active'
ORDER BY created_at DESC LIMIT 1
`, uid).Scan(&charID)
if err != nil {
return errResp(7003, "character not found", traceID)
}
// 校验事件类型
validTypes := map[string]bool{"pvp": true, "trade": true, "help": true, "event": true}
if !validTypes[req.EventType] {
return errResp(8010, "invalid event type", traceID)
}
// 创建事件记录(简化版)
eventID := genUUID()
logger.Info("MapService/PublishPlayerEvent success: event_id=%s", eventID)
return okResp(worldEventData{
EventID: eventID,
EventType: req.EventType,
Status: "active",
}, traceID)
}