feat(scum): rebuild plugin-owned management data

This commit is contained in:
npc0-hue
2026-08-15 12:43:09 +08:00
parent 92b1159cc8
commit 65353cf269
113 changed files with 3116 additions and 8058 deletions
@@ -47,21 +47,59 @@ type StateFieldPatch struct {
After float64
}
// AuthorizedRewardPort accepts only a frozen grant and typed items. It cannot
// receive SQL, a raw database row, a shell command, an RCON command, or secrets.
// AuthorizedRewardPort accepts a frozen grant with typed items and operations.
type AuthorizedRewardPort interface {
DeliverReward(context.Context, RewardGrant) (DeliveryReceipt, error)
}
type RewardGrant struct {
GrantID string
PlayerID string
Items []RewardItem
GrantID string
PlayerID string
Items []RewardItem
Operations []string
}
type RewardItem struct {
CatalogCode string
Quantity int
}
type DeliveryReceipt struct{ Outcome string }
type DeliveryReceipt struct {
Outcome string
DeliveryID string
}
type AuthorizedEventPort interface {
StartEvent(context.Context, EventStartRequest) (EventStartReceipt, error)
}
type EventStartRequest struct {
EventID string
EventType string
Class int
Title string
Placard string
Percent int
NPC int
Item int
Zombie int
Animal int
Produces []EventProduceRequest
DurationSeconds int
MaxParticipants int
Announce bool
}
type EventProduceRequest struct {
TradeGoodsID string
Percent int
Value int
Radius float64
X float64
Y float64
Z float64
}
type EventStartReceipt struct {
Accepted bool
Status string
EventID string
Message string
}
const fixedNotificationType = 4
@@ -102,6 +140,7 @@ type RuntimeAdapter struct {
Config AuthorizedConfigPort
GameData AuthorizedGameDataPort
Rewards AuthorizedRewardPort
Events AuthorizedEventPort
Notification UE4SSNotificationPort
VehicleSpawn UE4SSVehicleSpawnPort
DiagnosticsState map[string]string
@@ -191,12 +230,45 @@ func (adapter RuntimeAdapter) DeliverReward(ctx context.Context, payload map[str
}
receipt, err := adapter.Rewards.DeliverReward(ctx, grant)
if err != nil || receipt.Outcome == "unknown" {
return map[string]any{"outcome": "unknown"}, nil
return map[string]any{"accepted": false, "status": "rejected", "message": "reward delivery result is unknown"}, nil
}
if receipt.Outcome != "delivered" {
return map[string]any{"outcome": "failed"}, nil
return map[string]any{"accepted": false, "status": "rejected"}, nil
}
return map[string]any{"outcome": "delivered"}, nil
result := map[string]any{"accepted": true, "status": "delivered"}
if receipt.DeliveryID != "" {
result["deliveryId"] = receipt.DeliveryID
}
return result, nil
}
func (adapter RuntimeAdapter) StartEvent(ctx context.Context, payload map[string]any) (map[string]any, error) {
if adapter.Events == nil {
return nil, errAdapterUnsupported
}
request, err := eventStartRequest(payload)
if err != nil {
return nil, err
}
receipt, err := adapter.Events.StartEvent(ctx, request)
if err != nil {
return nil, err
}
status := receipt.Status
if status == "" {
status = "rejected"
}
if status != "started" && status != "queued" && status != "rejected" {
return nil, fmt.Errorf("event start receipt is invalid")
}
eventID := receipt.EventID
if eventID == "" {
eventID = request.EventID
}
result := map[string]any{"accepted": receipt.Accepted, "status": status, "eventId": eventID}
if receipt.Message != "" {
result["message"] = receipt.Message
}
return result, nil
}
func (adapter RuntimeAdapter) NotifyPlayer(ctx context.Context, payload map[string]any) (map[string]any, error) {
if adapter.BoundServerID == "" || adapter.Notification == nil {
@@ -342,22 +414,149 @@ func stateApplied(values map[string]float64, fields []StateFieldPatch) bool {
func rewardGrant(payload map[string]any) (RewardGrant, error) {
grantID, grantOK := payload["grantId"].(string)
playerID, playerOK := payload["playerId"].(string)
raw, itemsOK := payload["items"].([]any)
if !grantOK || !playerOK || !itemsOK || len(raw) == 0 || len(raw) > 8 {
rawItems, itemsOK := payload["items"].([]any)
rawOperations, operationsOK := payload["operations"].([]any)
if !grantOK || !playerOK || !itemsOK || !operationsOK || (len(rawItems) == 0 && len(rawOperations) == 0) || len(rawItems) > 8 {
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
}
items := make([]RewardItem, 0, len(raw))
for _, value := range raw {
items := make([]RewardItem, 0, len(rawItems))
for _, value := range rawItems {
item, ok := value.(map[string]any)
if !ok {
if !ok || len(item) != 2 {
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
}
code, codeOK := item["catalogCode"].(string)
quantity, quantityOK := item["quantity"].(float64)
if !codeOK || !quantityOK || quantity < 1 || quantity > 99 {
quantity, quantityOK := integerPayloadValue(item["quantity"])
if !codeOK || !supportedCatalogCode(code) || !quantityOK || quantity < 1 || quantity > 100 {
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
}
items = append(items, RewardItem{CatalogCode: code, Quantity: int(quantity)})
items = append(items, RewardItem{CatalogCode: code, Quantity: quantity})
}
return RewardGrant{GrantID: grantID, PlayerID: playerID, Items: items}, nil
operations := make([]string, 0, len(rawOperations))
for _, value := range rawOperations {
operation, ok := value.(string)
if !ok || operation == "" || !utf8.ValidString(operation) {
return RewardGrant{}, fmt.Errorf("reward payload is invalid")
}
operations = append(operations, operation)
}
return RewardGrant{GrantID: grantID, PlayerID: playerID, Items: items, Operations: operations}, nil
}
func eventStartRequest(payload map[string]any) (EventStartRequest, error) {
eventID, eventIDOK := payload["eventId"].(string)
eventType, eventTypeOK := payload["eventType"].(string)
eventClass, classOK := integerPayloadValue(payload["class"])
title, titleOK := payload["title"].(string)
duration, durationOK := integerPayloadValue(payload["durationSeconds"])
percent, percentOK := integerPayloadValue(payload["percent"])
placard, placardOK := payload["placard"].(string)
produces, producesOK := eventProduceRequests(payload["produces"])
counts := make([]int, 4)
for index, key := range []string{"npc", "item", "zombie", "animal"} {
if value, exists := payload[key]; exists {
count, ok := integerPayloadValue(value)
if !ok || count < 0 || count > 10000 {
return EventStartRequest{}, fmt.Errorf("event start payload is invalid")
}
counts[index] = count
}
}
participants := 0
if value, exists := payload["maxParticipants"]; exists {
var ok bool
participants, ok = integerPayloadValue(value)
if !ok || participants < 1 || participants > 1000 {
return EventStartRequest{}, fmt.Errorf("event start payload is invalid")
}
}
announce := false
if value, exists := payload["announce"]; exists {
var ok bool
announce, ok = value.(bool)
if !ok {
return EventStartRequest{}, fmt.Errorf("event start payload is invalid")
}
}
if !eventIDOK || !eventTypeOK || !classOK || !titleOK || !durationOK || !percentOK || !placardOK || !producesOK || !supportedEventType(eventType) || eventClass < 1 || eventClass > 2 || (eventClass == 1) != (eventType == "range") || strings.TrimSpace(eventID) == "" || strings.TrimSpace(title) == "" || len(placard) > 500 || percent < 0 || percent > 100 || duration < 30 || duration > 86400 {
return EventStartRequest{}, fmt.Errorf("event start payload is invalid")
}
return EventStartRequest{EventID: eventID, EventType: eventType, Class: eventClass, Title: title, Placard: placard, Percent: percent, NPC: counts[0], Item: counts[1], Zombie: counts[2], Animal: counts[3], Produces: produces, DurationSeconds: duration, MaxParticipants: participants, Announce: announce}, nil
}
func eventProduceRequests(value any) ([]EventProduceRequest, bool) {
raw, ok := value.([]any)
if !ok || len(raw) > 100 {
return nil, false
}
result := make([]EventProduceRequest, 0, len(raw))
for _, candidate := range raw {
produce, ok := candidate.(map[string]any)
if !ok || len(produce) != 7 {
return nil, false
}
tradeGoodsID, idOK := produce["tradeGoodsId"].(string)
percent, percentOK := integerPayloadValue(produce["percent"])
quantity, quantityOK := integerPayloadValue(produce["value"])
radius, radiusOK := numberPayloadValue(produce["r"])
x, xOK := numberPayloadValue(produce["x"])
y, yOK := numberPayloadValue(produce["y"])
z, zOK := numberPayloadValue(produce["z"])
if !idOK || strings.TrimSpace(tradeGoodsID) == "" || len(tradeGoodsID) > 128 || !percentOK || percent < 0 || percent > 100 || !quantityOK || quantity < 1 || quantity > 10000 || !radiusOK || radius < 0 || radius > 2000000 || !xOK || !yOK || !zOK || x < -2000000 || x > 2000000 || y < -2000000 || y > 2000000 || z < -2000000 || z > 2000000 {
return nil, false
}
result = append(result, EventProduceRequest{TradeGoodsID: tradeGoodsID, Percent: percent, Value: quantity, Radius: radius, X: x, Y: y, Z: z})
}
return result, true
}
func integerPayloadValue(value any) (int, bool) {
switch number := value.(type) {
case float64:
if number != float64(int(number)) {
return 0, false
}
return int(number), true
case int:
return number, true
case int32:
return int(number), true
case int64:
return int(number), true
default:
return 0, false
}
}
func numberPayloadValue(value any) (float64, bool) {
switch number := value.(type) {
case float64:
return number, true
case float32:
return float64(number), true
case int:
return float64(number), true
case int32:
return float64(number), true
case int64:
return float64(number), true
default:
return 0, false
}
}
func supportedCatalogCode(value string) bool {
if value == "" || len(value) > 128 {
return false
}
for _, character := range value {
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') && (character < '0' || character > '9') && character != '_' && character != '-' && character != '.' {
return false
}
}
return true
}
func supportedEventType(value string) bool {
return value == "range" || value == "fixed"
}