DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,529 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
cryptorand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
var ErrRandomOptionResolverRequired = errors.New("random-option mutation requires a runtime-PVF resolver")
|
||||
|
||||
const (
|
||||
randomOptionCountOffset = 0x47
|
||||
randomOptionTypeOffset = 0x48
|
||||
randomOptionValue1Offset = 0x4B
|
||||
randomOptionValue2Offset = 0x4E
|
||||
randomOptionStateOffset = 0x51
|
||||
randomOptionChangedIndexOffset = 0x52
|
||||
randomOptionCandidateOffset = 0x53
|
||||
randomOptionMaximumCount = 3
|
||||
)
|
||||
|
||||
type RandomOptionValue struct {
|
||||
Type byte
|
||||
Value1 byte
|
||||
Value2 byte
|
||||
}
|
||||
|
||||
type RandomOptionFailure string
|
||||
|
||||
const (
|
||||
RandomOptionFailureNone RandomOptionFailure = ""
|
||||
RandomOptionFailureInsufficientGold RandomOptionFailure = "insufficient_gold"
|
||||
// op455 (seal-grade adjustment) outcomes; each maps to the current
|
||||
// NoPack error code the client's op455 result handler can render.
|
||||
RandomOptionFailureNoSealOption RandomOptionFailure = "no_seal_option"
|
||||
RandomOptionFailureGradeFixed RandomOptionFailure = "grade_fixed"
|
||||
RandomOptionFailureGradesUnavailable RandomOptionFailure = "grades_unavailable"
|
||||
RandomOptionFailureMaterialMissing RandomOptionFailure = "material_missing"
|
||||
RandomOptionFailureMaterialLocked RandomOptionFailure = "material_locked"
|
||||
)
|
||||
|
||||
type RandomOptionMutationResult struct {
|
||||
CharacterID string
|
||||
Success bool
|
||||
Failure RandomOptionFailure
|
||||
Mode string
|
||||
TargetSlotIndex int16
|
||||
TargetItemID int64
|
||||
OptionIndex byte
|
||||
Options []RandomOptionValue
|
||||
GoldCost int64
|
||||
UpdatedGold int64
|
||||
UpdatedStack dnfrepo.ItemStack
|
||||
Changed bool
|
||||
// PercentileFallback is true when the runtime PVF shipped no
|
||||
// [different weight] window for this rarity and the value roll had to fall
|
||||
// back to the whole 0..100 domain. It is surfaced in the bridge log so a
|
||||
// server that keeps unsealing into odd grades is diagnosable.
|
||||
PercentileFallback bool
|
||||
}
|
||||
|
||||
// 【封印属性数值 = 品级百分位,不是绝对数值】
|
||||
//
|
||||
// 90 客户端 X 服同源的 df_game_r(1) 里,random_option 命名空间把这件事写死了:
|
||||
//
|
||||
// - 常量 MAX_OPTION_VALUE = 100(random_option::MAX_OPTION_VALUE,1 字节)。
|
||||
// - RandomOption 结构:+0..+8 三个槽各 {type, value1, value2};+0xA..+0xC 是本次
|
||||
// "变换候选",+0xD 是被变换槽号。RandomOption::set_first_value / set_second_value
|
||||
// 写的两个字节是一对【同值】的百分位(reset_option 里还会 `if (p > 100) p = 100`)。
|
||||
// - 展示值由客户端自己按 PVF 区间算:_get_option_value(min,max) 做的是
|
||||
// min + 量化((max-min) * 百分位),即两个字节是"在这条属性区间里的位置"。
|
||||
//
|
||||
// 所以服务端写词条值时必须掷百分位;把 PVF 的 [level] 区间端点 (min,max) 直接塞进
|
||||
// 这两个字节是错的 —— 那样等于把 min 当成了"min% 的位置",品级会常年落在最低档
|
||||
// (72 号属性 10 级是 (5,39),写进去就是 5% → C 级)。
|
||||
//
|
||||
// 窗口来自 etc/randomoption/randomizedoptionoverall2.etc 的 [different weight]:
|
||||
// 行格式 "稀有度 槽位 下限% 上限%",当前 PVF 是
|
||||
//
|
||||
// 2 1 0 25 2 2 10 30 2 3 30 55 (稀有:第 1 条 0~25%,第 2 条 10~30%,第 3 条 30~55%)
|
||||
// 3 1 10 35 3 2 20 40 3 3 40 65 (史诗:10~35% / 20~40% / 40~65%)
|
||||
//
|
||||
// 与 [postfix grade modification] 的品级区间处在同一个 0~100 百分位空间(稀有第 3
|
||||
// 条上限 55 对 S 级上限 56),也就是说解封掷完之后,客户端自己就能算出 C/B/A/S。
|
||||
//
|
||||
// 若 PVF 没有这张表(自定义 PVF 删段),退化成在整个 0..100 里均匀掷,并把
|
||||
// RandomOptionMutationResult.PercentileFallback 置位,日志里能看到。
|
||||
func sealPercentileForSlot(windows []alignedcmd.RandomOptionPercentWindow, slot int64) (byte, error) {
|
||||
minPct, maxPct := int64(0), currentRandomOptionResetMaxPercent
|
||||
for _, window := range windows {
|
||||
if window.Slot != slot || window.MaxPct < window.MinPct {
|
||||
continue
|
||||
}
|
||||
minPct, maxPct = window.MinPct, window.MaxPct
|
||||
break
|
||||
}
|
||||
if maxPct > currentRandomOptionResetMaxPercent {
|
||||
maxPct = currentRandomOptionResetMaxPercent
|
||||
}
|
||||
if minPct < 0 {
|
||||
minPct = 0
|
||||
}
|
||||
if maxPct < minPct {
|
||||
maxPct = minPct
|
||||
}
|
||||
span := maxPct - minPct
|
||||
offset := int64(0)
|
||||
if span > 0 {
|
||||
var err error
|
||||
offset, err = secureRandomOptionInt(span + 1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return byte(minPct + offset), nil
|
||||
}
|
||||
|
||||
func (o *Owner) UnsealRandomOption(ctx context.Context, cmd Command, resolver alignedcmd.RandomOptionResolver) (RandomOptionMutationResult, error) {
|
||||
return o.mutateRandomOption(ctx, cmd, resolver)
|
||||
}
|
||||
|
||||
func (o *Owner) ChangeRandomOption(ctx context.Context, cmd Command, resolver alignedcmd.RandomOptionResolver) (RandomOptionMutationResult, error) {
|
||||
return o.mutateRandomOption(ctx, cmd, resolver)
|
||||
}
|
||||
|
||||
func (o *Owner) mutateRandomOption(ctx context.Context, cmd Command, resolver alignedcmd.RandomOptionResolver) (RandomOptionMutationResult, error) {
|
||||
base := RandomOptionMutationResult{
|
||||
Mode: cmd.Operation,
|
||||
TargetSlotIndex: cmd.TargetSlotIndex,
|
||||
OptionIndex: cmd.RandomOptionIndex,
|
||||
}
|
||||
if o == nil || o.repo == nil || o.characters == nil {
|
||||
return RandomOptionMutationResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
if cmd.SelectedCharacterID == 0 {
|
||||
return RandomOptionMutationResult{}, ErrCharacterRequired
|
||||
}
|
||||
if resolver == nil {
|
||||
return RandomOptionMutationResult{}, ErrRandomOptionResolverRequired
|
||||
}
|
||||
if cmd.Operation != "unseal_random_option" && cmd.Operation != "change_random_option" {
|
||||
return RandomOptionMutationResult{}, fmt.Errorf("unsupported random-option operation %q", cmd.Operation)
|
||||
}
|
||||
if cmd.TargetSlotIndex < 0 {
|
||||
return base, nil
|
||||
}
|
||||
|
||||
characterID := strconv.FormatUint(uint64(cmd.SelectedCharacterID), 10)
|
||||
base.CharacterID = characterID
|
||||
if !o.inItemTransaction {
|
||||
var result RandomOptionMutationResult
|
||||
err := o.withinCharacterAssetTransaction(ctx, characterID, func(txOwner *Owner) error {
|
||||
var err error
|
||||
result, err = txOwner.mutateRandomOption(ctx, cmd, resolver)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
if result.CharacterID == "" {
|
||||
result = base
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
record, found, err := o.repo.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
if !found {
|
||||
return RandomOptionMutationResult{}, ErrInventoryNotFound
|
||||
}
|
||||
record = dnfrepo.CloneInventory(record)
|
||||
record.CharacterID = characterID
|
||||
if record.Slots == nil {
|
||||
record.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
|
||||
result := base
|
||||
targetKey := slotKey(listTypeMain, cmd.TargetSlotIndex)
|
||||
target, found := record.Slots[targetKey]
|
||||
if !found || target.Count <= 0 || target.ItemID <= 0 || isEquipmentLocked(target) {
|
||||
return result, nil
|
||||
}
|
||||
result.TargetItemID = target.ItemID
|
||||
|
||||
resolution, err := resolver(target.ItemID)
|
||||
if err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
eligible := resolution.Eligible &&
|
||||
strings.EqualFold(strings.TrimSpace(resolution.TargetKind), "equipment") &&
|
||||
strings.TrimSpace(resolution.TargetPVFPath) != "" &&
|
||||
resolution.TargetMinimumLevel >= 0 &&
|
||||
(resolution.TargetRarity == 2 || resolution.TargetRarity == 3)
|
||||
if cmd.Operation == "unseal_random_option" && !eligible {
|
||||
// 客户端右键「解封」走的就是这条 op401 —— 它是通用的撕膜命令,只有稀有 / 神器才
|
||||
// 顺带开随机属性。称号、传说、史诗这类没有随机属性的贴膜物品以前在这里直接被拒,
|
||||
// 客户端本地已经把膜撕了,服务端却一直存着 seal_flag=1:重登、进称号簿都又变回
|
||||
// "封装"。没有随机属性可开就只撕膜,不收金币。
|
||||
// (移植上游 fc3d6d2,2026-09-22)
|
||||
if !stackSealFilmSet(target) {
|
||||
return result, nil
|
||||
}
|
||||
target = cloneStack(target)
|
||||
clearStackSealFilm(&target)
|
||||
record.Slots[targetKey] = target
|
||||
record.UpdatedAt = time.Now()
|
||||
if err := dnfrepo.SaveInventoryFields(ctx, o.repo, record, dnfrepo.InventoryFieldSlots); err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
result.Success = true
|
||||
result.Changed = true
|
||||
result.Mode = "unseal_seal_film"
|
||||
result.UpdatedStack = cloneStack(target)
|
||||
return result, nil
|
||||
}
|
||||
if !eligible {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
raw := currentRawEntryForStack(cmd.TargetSlotIndex, target)
|
||||
if len(raw) != currentItemListEntrySize {
|
||||
return RandomOptionMutationResult{}, fmt.Errorf("random-option target row length %d, want %d", len(raw), currentItemListEntrySize)
|
||||
}
|
||||
current, valid := randomOptionsFromRaw(raw)
|
||||
var next []RandomOptionValue
|
||||
var goldCost int64
|
||||
switch cmd.Operation {
|
||||
case "unseal_random_option":
|
||||
if valid && len(current) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
quantity, err := rollRandomOptionQuantity(resolution.QuantityWeights)
|
||||
if err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
if quantity == 0 || int(quantity) > len(resolution.InitialGroups) || quantity > randomOptionMaximumCount {
|
||||
return result, nil
|
||||
}
|
||||
next, err = rollRandomOptionSet(resolution.InitialGroups[:quantity], nil)
|
||||
if err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
// 解封:每个词条的数值要按"槽位窗口"掷品级百分位(见文件末尾的
|
||||
// sealPercentileForSlot 注释),槽位从 1 开始。
|
||||
for index := range next {
|
||||
percent, percentErr := sealPercentileForSlot(resolution.InitialSlotWindows, int64(index)+1)
|
||||
if percentErr != nil {
|
||||
return RandomOptionMutationResult{}, percentErr
|
||||
}
|
||||
next[index].Value1 = percent
|
||||
next[index].Value2 = percent
|
||||
}
|
||||
result.PercentileFallback = len(resolution.InitialSlotWindows) == 0
|
||||
goldCost = resolution.BreakSealGoldCost
|
||||
case "change_random_option":
|
||||
if !valid || len(current) == 0 || int(cmd.RandomOptionIndex) >= len(current) || int(cmd.RandomOptionIndex) >= len(resolution.ModifiedGroups) {
|
||||
return result, nil
|
||||
}
|
||||
used := make(map[byte]struct{}, len(current)-1)
|
||||
for index, option := range current {
|
||||
if index != int(cmd.RandomOptionIndex) {
|
||||
used[option.Type] = struct{}{}
|
||||
}
|
||||
}
|
||||
candidate, err := rollRandomOptionCandidate(resolution.ModifiedGroups[cmd.RandomOptionIndex], used)
|
||||
if err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
next = append([]RandomOptionValue(nil), current...)
|
||||
next[cmd.RandomOptionIndex] = candidate
|
||||
// 变更是"换类型 + 重掷数值":当前 PVF 没有 [changed postfix],所以沿用
|
||||
// [different weight] 的同槽位窗口(见文件末尾 sealPercentileForSlot)。
|
||||
changeWindows := resolution.ChangeSlotWindows
|
||||
if len(changeWindows) == 0 {
|
||||
changeWindows = resolution.InitialSlotWindows
|
||||
}
|
||||
percent, err := sealPercentileForSlot(changeWindows, int64(cmd.RandomOptionIndex)+1)
|
||||
if err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
next[cmd.RandomOptionIndex].Value1 = percent
|
||||
next[cmd.RandomOptionIndex].Value2 = percent
|
||||
result.PercentileFallback = len(changeWindows) == 0
|
||||
goldCost = resolution.ModificationGoldCost
|
||||
}
|
||||
if goldCost < 0 {
|
||||
return RandomOptionMutationResult{}, fmt.Errorf("random-option negative PVF gold cost %d", goldCost)
|
||||
}
|
||||
|
||||
character, found, err := o.characters.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
if !found {
|
||||
return RandomOptionMutationResult{}, fmt.Errorf("%w: character=%s", ErrWalletTxnRequired, characterID)
|
||||
}
|
||||
if character.Stats == nil {
|
||||
character.Stats = make(map[string]int64, 1)
|
||||
}
|
||||
result.UpdatedGold = character.Stats["gold"]
|
||||
result.GoldCost = goldCost
|
||||
if result.UpdatedGold < goldCost {
|
||||
result.Failure = RandomOptionFailureInsufficientGold
|
||||
return result, nil
|
||||
}
|
||||
|
||||
target = cloneStack(target)
|
||||
setRandomOptions(&target, cmd.TargetSlotIndex, next)
|
||||
record.Slots[targetKey] = target
|
||||
character = dnfrepo.CloneCharacter(character)
|
||||
if character.Stats == nil {
|
||||
character.Stats = make(map[string]int64, 1)
|
||||
}
|
||||
character.Stats["gold"] -= goldCost
|
||||
character.UpdatedAt = time.Now()
|
||||
if err := dnfrepo.SaveCharacterFields(ctx, o.characters, character, dnfrepo.CharacterFieldStats); err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
record.UpdatedAt = time.Now()
|
||||
if err := dnfrepo.SaveInventoryFields(ctx, o.repo, record, dnfrepo.InventoryFieldSlots); err != nil {
|
||||
return RandomOptionMutationResult{}, err
|
||||
}
|
||||
|
||||
result.Success = true
|
||||
result.Options = append([]RandomOptionValue(nil), next...)
|
||||
result.UpdatedGold = character.Stats["gold"]
|
||||
result.UpdatedStack = cloneStack(target)
|
||||
result.Changed = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// stackSealFilmSet 判断物品当前是否贴着膜(封装):持久 seal 别名或 0x77 行 +0x0D 非 0。
|
||||
// 只看 seal 语义的键,不把 bind(绑定)当封装。
|
||||
//
|
||||
// 移植上游 fc3d6d2(2026-09-22)。
|
||||
func stackSealFilmSet(stack dnfrepo.ItemStack) bool {
|
||||
if firstExtraInt(stack.Extra, 0, "seal_flag", "seal") != 0 {
|
||||
return true
|
||||
}
|
||||
return len(stack.RawEntry) > 0x0D && stack.RawEntry[0x0D] != 0
|
||||
}
|
||||
|
||||
// clearStackSealFilm 撕膜:与 equip.removeSealOnFirstEquipmentUse 同一组键与同一个原始行字节。
|
||||
//
|
||||
// 我们 equip 侧(equip/owner.go:1215)用的是同一组键(seal_flag / seal /
|
||||
// equipment_update_seal_flag / update_seal_flag)与同一个 RawEntry[0x0D],所以两处的口径一致。
|
||||
func clearStackSealFilm(stack *dnfrepo.ItemStack) {
|
||||
if stack == nil {
|
||||
return
|
||||
}
|
||||
if len(stack.RawEntry) > 0x0D {
|
||||
stack.RawEntry = append([]byte(nil), stack.RawEntry...)
|
||||
stack.RawEntry[0x0D] = 0
|
||||
}
|
||||
if stack.Extra == nil {
|
||||
stack.Extra = make(map[string]string, 4)
|
||||
}
|
||||
for _, key := range []string{"seal_flag", "seal", "equipment_update_seal_flag", "update_seal_flag"} {
|
||||
delete(stack.Extra, key)
|
||||
}
|
||||
if hexRaw := strings.TrimSpace(stack.Extra["raw_entry_hex"]); len(hexRaw) >= 0x1C {
|
||||
// 0x0D 字节在十六进制串里是第 26、27 个字符;有些路径只读这份副本。
|
||||
stack.Extra["raw_entry_hex"] = hexRaw[:0x1A] + "00" + hexRaw[0x1C:]
|
||||
}
|
||||
stack.Extra["seal_removed_by_unseal"] = "1"
|
||||
}
|
||||
|
||||
func randomOptionsFromRaw(raw []byte) ([]RandomOptionValue, bool) {
|
||||
if len(raw) != currentItemListEntrySize {
|
||||
return nil, false
|
||||
}
|
||||
count := int(raw[randomOptionCountOffset])
|
||||
if count < 0 || count > randomOptionMaximumCount {
|
||||
return nil, false
|
||||
}
|
||||
options := make([]RandomOptionValue, 0, count)
|
||||
seen := make(map[byte]struct{}, count)
|
||||
for index := 0; index < count; index++ {
|
||||
option := RandomOptionValue{
|
||||
Type: raw[randomOptionTypeOffset+index],
|
||||
Value1: raw[randomOptionValue1Offset+index],
|
||||
Value2: raw[randomOptionValue2Offset+index],
|
||||
}
|
||||
if option.Type == 0 {
|
||||
return nil, false
|
||||
}
|
||||
if _, duplicate := seen[option.Type]; duplicate {
|
||||
return nil, false
|
||||
}
|
||||
seen[option.Type] = struct{}{}
|
||||
options = append(options, option)
|
||||
}
|
||||
return options, true
|
||||
}
|
||||
|
||||
func setRandomOptions(stack *dnfrepo.ItemStack, slot int16, options []RandomOptionValue) {
|
||||
if stack == nil || len(options) == 0 || len(options) > randomOptionMaximumCount {
|
||||
return
|
||||
}
|
||||
raw := currentRawEntryForStack(slot, *stack)
|
||||
for index := randomOptionCountOffset; index <= randomOptionCandidateOffset+3; index++ {
|
||||
raw[index] = 0
|
||||
}
|
||||
raw[randomOptionCountOffset] = byte(len(options))
|
||||
for index, option := range options {
|
||||
raw[randomOptionTypeOffset+index] = option.Type
|
||||
raw[randomOptionValue1Offset+index] = option.Value1
|
||||
raw[randomOptionValue2Offset+index] = option.Value2
|
||||
}
|
||||
raw[randomOptionStateOffset] = 0
|
||||
raw[randomOptionChangedIndexOffset] = math.MaxUint8
|
||||
stack.RawEntry = raw
|
||||
if stack.Extra == nil {
|
||||
stack.Extra = make(map[string]string, 16)
|
||||
}
|
||||
stack.Extra["random_option_count"] = strconv.Itoa(len(options))
|
||||
stack.Extra["random_option_state"] = "0"
|
||||
stack.Extra["random_option_changed_index"] = "255"
|
||||
for index := 0; index < randomOptionMaximumCount; index++ {
|
||||
prefix := fmt.Sprintf("random_option_%d_", index+1)
|
||||
if index < len(options) {
|
||||
stack.Extra[prefix+"type"] = strconv.Itoa(int(options[index].Type))
|
||||
stack.Extra[prefix+"value1"] = strconv.Itoa(int(options[index].Value1))
|
||||
stack.Extra[prefix+"value2"] = strconv.Itoa(int(options[index].Value2))
|
||||
} else {
|
||||
stack.Extra[prefix+"type"] = "0"
|
||||
stack.Extra[prefix+"value1"] = "0"
|
||||
stack.Extra[prefix+"value2"] = "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rollRandomOptionQuantity(entries []alignedcmd.RandomOptionWeightedQuantity) (byte, error) {
|
||||
total := int64(0)
|
||||
for _, entry := range entries {
|
||||
if entry.Quantity == 0 || entry.Quantity > randomOptionMaximumCount || entry.Weight <= 0 {
|
||||
continue
|
||||
}
|
||||
if entry.Weight > math.MaxInt64-total {
|
||||
return 0, fmt.Errorf("random-option quantity weights overflow")
|
||||
}
|
||||
total += entry.Weight
|
||||
}
|
||||
roll, err := secureRandomOptionInt(total)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Quantity == 0 || entry.Quantity > randomOptionMaximumCount || entry.Weight <= 0 {
|
||||
continue
|
||||
}
|
||||
if roll < entry.Weight {
|
||||
return entry.Quantity, nil
|
||||
}
|
||||
roll -= entry.Weight
|
||||
}
|
||||
return 0, fmt.Errorf("random-option quantity selection exhausted")
|
||||
}
|
||||
|
||||
func rollRandomOptionSet(groups [][]alignedcmd.RandomOptionCandidate, used map[byte]struct{}) ([]RandomOptionValue, error) {
|
||||
if used == nil {
|
||||
used = make(map[byte]struct{}, len(groups))
|
||||
}
|
||||
options := make([]RandomOptionValue, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
option, err := rollRandomOptionCandidate(group, used)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
used[option.Type] = struct{}{}
|
||||
options = append(options, option)
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func rollRandomOptionCandidate(entries []alignedcmd.RandomOptionCandidate, excluded map[byte]struct{}) (RandomOptionValue, error) {
|
||||
total := int64(0)
|
||||
for _, entry := range entries {
|
||||
if entry.Type == 0 || entry.Weight <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, skip := excluded[entry.Type]; skip {
|
||||
continue
|
||||
}
|
||||
if entry.Weight > math.MaxInt64-total {
|
||||
return RandomOptionValue{}, fmt.Errorf("random-option candidate weights overflow")
|
||||
}
|
||||
total += entry.Weight
|
||||
}
|
||||
roll, err := secureRandomOptionInt(total)
|
||||
if err != nil {
|
||||
return RandomOptionValue{}, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Type == 0 || entry.Weight <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, skip := excluded[entry.Type]; skip {
|
||||
continue
|
||||
}
|
||||
if roll < entry.Weight {
|
||||
return RandomOptionValue{Type: entry.Type, Value1: entry.Value1, Value2: entry.Value2}, nil
|
||||
}
|
||||
roll -= entry.Weight
|
||||
}
|
||||
return RandomOptionValue{}, fmt.Errorf("random-option candidate selection exhausted")
|
||||
}
|
||||
|
||||
func secureRandomOptionInt(upper int64) (int64, error) {
|
||||
if upper <= 0 {
|
||||
return 0, fmt.Errorf("random-option random upper bound %d invalid", upper)
|
||||
}
|
||||
value, err := cryptorand.Int(cryptorand.Reader, big.NewInt(upper))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("random-option random: %w", err)
|
||||
}
|
||||
return value.Int64(), nil
|
||||
}
|
||||
Reference in new issue
Block a user