feat(scum): rebuild plugin-owned management data
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user