// Package sandbox handles the 7 Days to Die V3.0 "SandboxCode" — the single // encoded serverconfig.xml property that carries all 150 in-game Sandbox Options // introduced in V3.0 "Dead Hot Summer". // // V3.0 collapses ~29 legacy gameplay serverconfig properties (GameDifficulty, // BloodMoonFrequency, LootAbundance, …) into one deterministic, OFFLINE-encoded // string. The encode/decode algorithm ships in the game binary; this package is // a faithful Go port of it, cracked from the decompiled 3.0 Assembly-CSharp // (SandboxOptions.SandboxOptionManager + SandboxOptionPreset, buildid 23705258). // // ## The encoding (verified against the game's own default code) // // code[0] = version char ('A', SandboxOptionManager.currentVersion) // then 3-char triples, one per option that differs from its default: // triple[0..1] = the SandboxOptions enum int, base-26: c0*26 + c1 (A=0) // triple[2] = the selected value INDEX in that option's value-set (A=0) // // Decode resets every option to its default, then applies each triple. Encode is // the inverse: emit a triple for every option whose chosen index ≠ default index // (options omitted from the code decode to default — identical result, shorter // code, matching how the game emits difficulty presets like AAAJABJACJADJARFBNC). // // Round-trip + the game's example codes are checked in sandbox_test.go. package sandbox import ( "errors" "fmt" "math" "sort" "strconv" "strings" ) // codeVersion is SandboxOptionManager.currentVersion. A code must start with it. const codeVersion = 'A' // ErrCodecUnavailable is retained for API compatibility with the day-one // passthrough scaffold. The real codec no longer returns it. var ErrCodecUnavailable = errors.New("sandbox: per-option SandboxCode codec unavailable") // optByEnum / optByName index the options table for O(1) lookup. defaultIdx // holds each option's default value's index in its value-set. var ( optByEnum = map[int]*option{} optByName = map[string]*option{} defaultIdx = map[string]int{} ) func init() { for i := range options { o := &options[i] e, ok := enumByName[o.Name] if !ok { panic("sandbox: option " + o.Name + " has no SandboxOptions enum entry") } o.enum = e vs, ok := valueSets[o.ValueSet] if !ok { panic("sandbox: option " + o.Name + " references unknown value-set " + o.ValueSet) } di, ok := indexOfDefault(*o, vs) if !ok { panic(fmt.Sprintf("sandbox: option %s default not present in value-set %s", o.Name, o.ValueSet)) } o.set = vs defaultIdx[o.Name] = di optByEnum[e] = o optByName[o.Name] = o } } // indexOfDefault finds the index of an option's default value in its value-set. func indexOfDefault(o option, vs valueSet) (int, bool) { switch o.Type { case optFloat: for i, f := range vs.Floats { if floatEq(f, o.DefaultFloat) { return i, true } } case optInt: for i, n := range vs.Ints { if n == o.DefaultInt { return i, true } } case optBool: for i, b := range vs.Bools { if b == o.DefaultBool { return i, true } } } return 0, false } func floatEq(a, b float64) bool { return math.Abs(a-b) < 1e-6 } // Codec converts between a SandboxCode string and a per-option value map. // The map is keyed by the SandboxOptions member name (e.g. "BloodMoonFrequency") // with string values (the human value: "7", "1.5", "true"). Encode emits a code // for the options whose value differs from default; Decode returns the FULL set // of 150 options at their effective values (defaults for any omitted from code). type Codec interface { Version() string Encode(options map[string]string) (string, error) Decode(code string) (map[string]string, error) } // V3Codec is the real 3.0 SandboxCode codec. type V3Codec struct{} func (V3Codec) Version() string { return string(codeVersion) } // Encode builds a SandboxCode from a name→value map. Unknown keys are ignored; // a value that doesn't match any entry in its option's value-set is an error // (prevents silently writing an out-of-range setting). Options absent from the // map, or equal to their default, are omitted from the code (decode to default). // Triples are emitted in ascending enum order for a stable, diffable code. func (V3Codec) Encode(vals map[string]string) (string, error) { type triple struct { enum, idx int } var triples []triple for name, raw := range vals { o, ok := optByName[name] if !ok { continue // unknown option key — ignore (forward-compat) } idx, err := o.indexOfValue(raw) if err != nil { return "", fmt.Errorf("sandbox: option %s: %w", name, err) } if idx == defaultIdx[name] { continue // equals default — omit (decodes to same value) } triples = append(triples, triple{o.enum, idx}) } sort.Slice(triples, func(i, j int) bool { return triples[i].enum < triples[j].enum }) var b strings.Builder b.WriteByte(codeVersion) for _, t := range triples { s, err := indexToAlpha2(t.enum) if err != nil { return "", err } c, err := indexToAlpha(t.idx) if err != nil { return "", err } b.WriteString(s) b.WriteByte(c) } return b.String(), nil } // Decode parses a SandboxCode into the FULL effective option set (all 150 at // their resolved values: code-specified or default). An empty code returns all // defaults. A code with a wrong version char, bad length, or unknown enum is an // error (unknown enums could mean a version drift we shouldn't silently accept). func (V3Codec) Decode(code string) (map[string]string, error) { out := DefaultsMap() if code == "" { return out, nil } if rune(code[0]) != codeVersion { return nil, fmt.Errorf("sandbox: code version %q unsupported (want %q)", string(code[0]), string(codeVersion)) } body := code[1:] if len(body)%3 != 0 { return nil, fmt.Errorf("sandbox: code body length %d is not a multiple of 3", len(body)) } for i := 0; i < len(body); i += 3 { enum, err := alpha2ToIndex(body[i : i+2]) if err != nil { return nil, err } idx, err := alphaToIndex(body[i+2]) if err != nil { return nil, err } o, ok := optByEnum[enum] if !ok { return nil, fmt.Errorf("sandbox: code references unknown option enum %d (game version drift?)", enum) } if !o.set.IsValidIndex(idx) { return nil, fmt.Errorf("sandbox: option %s index %d out of range (0..%d)", o.Name, idx, o.set.Len()-1) } out[o.Name] = o.valueAtIndex(idx) } return out, nil } // indexOfValue resolves a human value string to its index in the option's set. func (o option) indexOfValue(raw string) (int, error) { raw = strings.TrimSpace(raw) switch o.Type { case optFloat: f, err := strconv.ParseFloat(raw, 64) if err != nil { return 0, fmt.Errorf("expected a number, got %q", raw) } for i, v := range o.set.Floats { if floatEq(v, f) { return i, nil } } return 0, fmt.Errorf("value %s not an allowed option (set %s)", raw, o.ValueSet) case optInt: n, err := strconv.Atoi(raw) if err != nil { return 0, fmt.Errorf("expected an integer, got %q", raw) } for i, v := range o.set.Ints { if v == n { return i, nil } } return 0, fmt.Errorf("value %d not an allowed option (set %s)", n, o.ValueSet) case optBool: b, err := parseBool(raw) if err != nil { return 0, err } for i, v := range o.set.Bools { if v == b { return i, nil } } return 0, fmt.Errorf("value %v not an allowed option (set %s)", b, o.ValueSet) } return 0, fmt.Errorf("option %s has unknown type", o.Name) } // valueAtIndex returns the human value string for an index in the option's set. func (o option) valueAtIndex(i int) string { switch o.Type { case optFloat: return strconv.FormatFloat(o.set.Floats[i], 'f', -1, 64) case optInt: return strconv.Itoa(o.set.Ints[i]) case optBool: return strconv.FormatBool(o.set.Bools[i]) } return "" } func parseBool(s string) (bool, error) { switch strings.ToLower(strings.TrimSpace(s)) { case "true", "1", "yes", "on": return true, nil case "false", "0", "no", "off": return false, nil } return false, fmt.Errorf("expected a boolean, got %q", s) } // ---- alpha <-> index helpers (ports of SandboxOptionManager.*) ---- func indexToAlpha(index int) (byte, error) { if index < 0 || index > 26 { return 0, fmt.Errorf("sandbox: value index %d out of code range", index) } return byte(65 + index%26), nil } func indexToAlpha2(index int) (string, error) { if index < 0 || index >= 676 { return "", fmt.Errorf("sandbox: enum index %d out of code range", index) } return string([]byte{byte(65 + index/26), byte(65 + index%26)}), nil } func alphaToIndex(value byte) (int, error) { v := upper(value) if v < 'A' || v > 'Z' { return 0, errors.New("sandbox: code value char must be A–Z") } return int(v - 65), nil } func alpha2ToIndex(value string) (int, error) { if len(value) != 2 { return 0, errors.New("sandbox: code option key must be exactly 2 letters") } a, b := upper(value[0]), upper(value[1]) if a < 'A' || a > 'Z' || b < 'A' || b > 'Z' { return 0, errors.New("sandbox: code option key must contain only A–Z") } return int(a-65)*26 + int(b-65), nil } func upper(b byte) byte { if b >= 'a' && b <= 'z' { return b - 32 } return b } // DefaultsMap returns every option at its default value (name → value string). func DefaultsMap() map[string]string { out := make(map[string]string, len(options)) for i := range options { o := &options[i] out[o.Name] = o.valueAtIndex(defaultIdx[o.Name]) } return out } // Active is the codec the panel uses. Now the real V3 codec. var Active Codec = V3Codec{} // Preset is an official (or operator-saved) named SandboxCode. type Preset struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Code string `json:"code"` } // Presets are the V3.0 official presets surfaced in the UI. The default // difficulty (Adventurer) code is shipped from the game's serverconfig comment; // the named-preset codes are captured from the in-game Sandbox Options window // (see scripts/capture-preset-codes). An empty Code == the game's built-in // default sandbox. var Presets = []Preset{ {ID: "default", Name: "Default (Adventurer)", Description: "The game's built-in default sandbox.", Code: "AAAJABJACJADJARFBNC"}, {ID: "undead_matinee", Name: "Undead Matinee", Description: "Official V3.0 preset.", Code: ""}, {ID: "madmoles_mayhem", Name: "Madmole's Mayhem", Description: "Official V3.0 preset.", Code: ""}, {ID: "almost_creative", Name: "Almost Creative Mode", Description: "Official V3.0 preset.", Code: ""}, {ID: "bite_club", Name: "Bite Club", Description: "Official V3.0 preset.", Code: ""}, {ID: "legacy_survival", Name: "Legacy Survival", Description: "Official V3.0 preset.", Code: ""}, } // PresetByID returns the named preset, or ok=false if unknown. func PresetByID(id string) (Preset, bool) { id = strings.TrimSpace(strings.ToLower(id)) for _, p := range Presets { if strings.ToLower(p.ID) == id { return p, true } } return Preset{}, false } // Validate does a cheap structural sanity check on a SandboxCode before it's // written into serverconfig.xml. An empty code is valid (== default sandbox). // A non-empty code must decode cleanly (right version, length, known enums, // in-range indices) and contain no XML-attribute-breaking characters. func Validate(code string) error { if code == "" { return nil } if strings.ContainsAny(code, `"<>&`) { return errors.New("sandbox: code contains characters not valid in an XML attribute value") } if _, err := (V3Codec{}).Decode(code); err != nil { return err } return nil }