300 行
8.9 KiB
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)
|
|
}
|