panel v0.9.2 — public release

Self-hostable game server control panel: Go controller + agent, 26 game
modules, embedded web UI. One-line install via install.sh / install.ps1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 00:43:35 -07:00
commit 4ccccc6fe2
2164 changed files with 301480 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
// Code generated from 7DTD V3.0 SandboxOptions enum (Assembly-CSharp,
// buildid 23705258). The integer value of each member IS the 2-letter code in a
// SandboxCode (via IndexToAlpha2). Order is authoritative — transcribed 1:1 from
// rb-decompile/3.0/src/SandboxOptions/SandboxOptions.cs, INCLUDING the UNUSED1
// (116) / UNUSED2 (117) gap. Do not reorder.
package sandbox
// sandboxEnumNames is the SandboxOptions enum in declaration order; the slice
// index equals the enum integer value. UNUSED1/UNUSED2 are kept so subsequent
// members get their correct integer. "Max" terminates the enum (not an option).
var sandboxEnumNames = []string{
"RangedDamage", // 0
"MeleeDamage", // 1
"BlockDamage", // 2
"TerrainDamage", // 3
"HeadshotMultiplier", // 4
"CrouchSpeed", // 5
"WalkSpeed", // 6
"RunSpeed", // 7
"JumpStrength", // 8
"StaminaUsage", // 9
"StaminaRegen", // 10
"PlayerLevelBonusApplied", // 11
"JarRefund", // 12
"ShowHealthBars", // 13
"ShowEnemyDamage", // 14
"NewbieCoat", // 15
"HeadshotMode", // 16
"IncomingDamage", // 17
"XPMultiplier", // 18
"ShowXP", // 19
"EncumbranceModifier", // 20
"ItemDegradation", // 21
"LoseItemsOnDeathType", // 22
"LoseItemsOnDeathCount", // 23
"DegradeItemsOnDeath", // 24
"DegradeAmountOnDeath", // 25
"DeathPenalty", // 26
"DropOnDeath", // 27
"DropOnQuit", // 28
"InfectionRate", // 29
"EnemySpawnMode", // 30
"EntityDamage", // 31
"BlockDamageAI", // 32
"BlockDamageAIBM", // 33
"ZombieMove", // 34
"ZombieMoveNight", // 35
"ZombieFeralMove", // 36
"ZombieBMMove", // 37
"ZombieFeralSense", // 38
"AISmellMode", // 39
"AllowZombieDigging", // 40
"ZombieRageChance", // 41
"EntityIncomingDamage", // 42
"MaxEnemyTier", // 43
"BiomeZombieRespawn", // 44
"BiomeAnimalRespawn", // 45
"BiomeEnemyDensity", // 46
"ZombiesEatAnimals", // 47
"BloodMoonFrequency", // 48
"BloodMoonRange", // 49
"BloodMoonWarning", // 50
"BloodMoonEnemyCount", // 51
"AirDropFrequency", // 52
"AirDropMarker", // 53
"AirDropRandomTime", // 54
"BiomeProgression", // 55
"TemperatureSurvival", // 56
"StormFreq", // 57
"StormWarning", // 58
"HeatMapSensitivity", // 59
"GlobalGSModifier", // 60
"BiomeGSModifier", // 61
"GlobalLSModifier", // 62
"BiomeLSModifier", // 63
"POITierLSModifier", // 64
"GlobalTSModifier", // 65
"DayNightLength", // 66
"DayLightLength", // 67
"AllowMap", // 68
"AllowCompass", // 69
"AllowScreenMarkers", // 70
"ShowLocationInfo", // 71
"ShowDayTime", // 72
"WorkstationsInTheWild", // 73
"MaxTechType", // 74
"LootRespawnDays", // 75
"LootTimer", // 76
"LootMaxTier", // 77
"GlobalLootCount", // 78
"FoodLootCount", // 79
"DrinkLootCount", // 80
"MedicalLootCount", // 81
"AmmoLootCount", // 82
"ResourceLootCount", // 83
"ArmorLootCount", // 84
"MeleeLootCount", // 85
"RangedLootCount", // 86
"DukesLootCount", // 87
"CraftingMagazinesLootCount", // 88
"TreasureMapChance", // 89
"LootBagChance", // 90
"CropOutput", // 91
"SeedDropOutput", // 92
"CropGrowthSpeed", // 93
"BackpackCrafting", // 94
"WorkstationCrafting", // 95
"CraftingProgression", // 96
"CraftingTime", // 97
"CraftingInput", // 98
"CraftingOutput", // 99
"CraftingMaxTier", // 100
"MiningOutput", // 101
"HarvestingOutput", // 102
"ScrappingOutput", // 103
"SmeltingType", // 104
"DewCollectorTime", // 105
"DewCollectorOutput", // 106
"DewCollectorInput", // 107
"ApiaryTime", // 108
"ApiaryOutput", // 109
"ApiaryInput", // 110
"UNUSED1", // 111
"UNUSED2", // 112
"RepairTypes", // 113
"MaxDegradationAmount", // 114
"PointsPerMagazine", // 115
"SkillGainRate", // 116
"SkillPointsPerLevel", // 117
"QuestsEnabled", // 118
"IntroQuestEnabled", // 119
"TraderToTraderQuestsEnabled", // 120
"StarterSkillPoints", // 121
"QuestsPerTier", // 122
"QuestProgressionDailyLimit", // 123
"BuriedQuestsEnabled", // 124
"POIQuestsEnabled", // 125
"TraderDialog", // 126
"TraderHours", // 127
"TradersEnabled", // 128
"VendingEnabled", // 129
"TraderSellPrices", // 130
"TraderBuyPrices", // 131
"TraderProtection", // 132
"TraderResetInterval", // 133
"TraderItemAbundance", // 134
"TraderBuyLimit", // 135
"TraderMaxTier", // 136
"VendingResetInterval", // 137
"VendingItemAbundance", // 138
"ChallengesEnabled", // 139
"IntroChallengesEnabled", // 140
"VehicleFuelUsage", // 141
"VehicleEntityDamage", // 142
"VehicleBlockDamage", // 143
"VehicleSelfDamage", // 144
"ElectricalOutput", // 145
"SillyCelebrate", // 146
"SillyBigHeads", // 147
"SillyTinyZombies", // 148
"SillySounds", // 149
"SillyLowGravity", // 150
"SillyBlackandWhite", // 151
// "Max" (152) — enum terminator, not an option.
}
// enumByName maps a SandboxOptions member name to its integer value, built once
// from sandboxEnumNames.
var enumByName = func() map[string]int {
m := make(map[string]int, len(sandboxEnumNames))
for i, n := range sandboxEnumNames {
m[n] = i
}
return m
}()
+208
View File
@@ -0,0 +1,208 @@
// Code generated from 7DTD V3.0 Assembly-CSharp SandboxOptionManager.SetupOptions
// + the SandboxOptions enum (buildid 23705258, experimental 2026-06-15).
// The Enum field is the integer value of the SandboxOptions enum member — it IS
// the 2-letter code in a SandboxCode (IndexToAlpha2((int)enum)). DO NOT renumber.
// CrackedFrom: rb-decompile/3.0/src/SandboxOptions/{SandboxOptions.cs,SandboxOptionManager.cs}
package sandbox
// Category is the 8-way grouping the in-game Sandbox Options window uses. The
// decompile's CategoryName strings are: General, Entities, World, Resources,
// Crafting, Traders, Tasks, Misc. We surface friendlier titles in the UI but
// keep the raw key here.
type optionType int
const (
optFloat optionType = iota
optInt
optBool
)
// option mirrors one AddSandboxOption(...) registration. Enum + set are resolved
// at init from the authoritative SandboxOptions enum (enum.go) and valueSets
// table — never hand-typed here (the Name is the only stable key authored).
type option struct {
Name string // SandboxOptions enum member name (stable key)
Display string // human label from the registration
Category string // General|Entities|World|Resources|Crafting|Traders|Tasks|Misc
Type optionType // float/int/bool — which value-set kind
ValueSet string // key into valueSets
// Default is the option's default value (float for optFloat, int for optInt,
// bool for optBool). Resolved to a default INDEX in the value-set at init.
DefaultFloat float64
DefaultInt int
DefaultBool bool
// Resolved at init() — do not set in literals.
enum int // SandboxOptions integer value (the 2-char code), from enumByName
set valueSet // resolved value-set, from valueSets[ValueSet]
}
// options is the full ordered registration list (insertion order from
// SetupOptions). Enum is set explicitly to survive the UNUSED1/UNUSED2 gap.
var options = []option{
// ---- General / Player ----
{Name: "RangedDamage", Display: "Ranged Damage", Category: "General", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "MeleeDamage", Display: "Melee Damage", Category: "General", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "BlockDamage", Display: "Block Damage", Category: "General", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "TerrainDamage", Display: "Terrain Damage", Category: "General", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "HeadshotMultiplier", Display: "Headshot Multiplier", Category: "General", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "IncomingDamage", Display: "Incoming Damage", Category: "General", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "WalkSpeed", Display: "Walk Speed", Category: "General", Type: optFloat, ValueSet: "PlayerSpeedValuesWithNone", DefaultFloat: 1},
{Name: "RunSpeed", Display: "Run Speed", Category: "General", Type: optFloat, ValueSet: "PlayerSpeedValues", DefaultFloat: 1},
{Name: "CrouchSpeed", Display: "Crouch Speed", Category: "General", Type: optFloat, ValueSet: "PlayerSpeedValues", DefaultFloat: 1},
{Name: "JumpStrength", Display: "Jump Height", Category: "General", Type: optFloat, ValueSet: "JumpStrength", DefaultFloat: 1},
{Name: "StaminaRegen", Display: "Stamina Regen", Category: "General", Type: optFloat, ValueSet: "StaminaRegen", DefaultFloat: 1},
{Name: "StaminaUsage", Display: "Stamina Usage", Category: "General", Type: optFloat, ValueSet: "StaminaUsage", DefaultFloat: 1},
{Name: "XPMultiplier", Display: "XP Multiplier", Category: "General", Type: optFloat, ValueSet: "XPGain", DefaultFloat: 1},
{Name: "ShowXP", Display: "Show XP", Category: "General", Type: optInt, ValueSet: "ShowXP", DefaultInt: 0},
{Name: "PlayerLevelBonusApplied", Display: "Level Health/Stam Bonus", Category: "General", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "SkillGainRate", Display: "Skill Gain Rate", Category: "General", Type: optInt, ValueSet: "SkillGainRate", DefaultInt: 1},
{Name: "SkillPointsPerLevel", Display: "Skill Gain Amount", Category: "General", Type: optInt, ValueSet: "PointsPer", DefaultInt: 1},
{Name: "DeathPenalty", Display: "Death Penalty", Category: "General", Type: optInt, ValueSet: "DeathPenalty", DefaultInt: 1},
{Name: "LoseItemsOnDeathType", Display: "Lose Items Death Type", Category: "General", Type: optInt, ValueSet: "LoseItemsOnDeathType", DefaultInt: 0},
{Name: "LoseItemsOnDeathCount", Display: "Lose Items Death Count", Category: "General", Type: optInt, ValueSet: "LoseItemCount", DefaultInt: 1},
{Name: "DegradeItemsOnDeath", Display: "Degrade Items On Death", Category: "General", Type: optInt, ValueSet: "DegradeItemsOnDeath", DefaultInt: 0},
{Name: "DegradeAmountOnDeath", Display: "Degrade Amount On Death", Category: "General", Type: optFloat, ValueSet: "MaxDegradationAmounts", DefaultFloat: 0.1},
{Name: "DropOnDeath", Display: "Drop On Death", Category: "General", Type: optInt, ValueSet: "DropOnDeath", DefaultInt: 1},
{Name: "DropOnQuit", Display: "Drop On Quit", Category: "General", Type: optInt, ValueSet: "DropOnQuit", DefaultInt: 0},
{Name: "InfectionRate", Display: "Infection Rate", Category: "General", Type: optFloat, ValueSet: "SpeedValues", DefaultFloat: 1},
{Name: "NewbieCoat", Display: "Allow Newbie Coat", Category: "General", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "EncumbranceModifier", Display: "Encumbrance Modifier", Category: "General", Type: optFloat, ValueSet: "Encumbrance", DefaultFloat: 1},
{Name: "JarRefund", Display: "Jar Refund", Category: "General", Type: optFloat, ValueSet: "JarRefund", DefaultFloat: 0.6},
// ---- Entities ----
{Name: "EnemySpawnMode", Display: "Enemy Spawn", Category: "Entities", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "MaxEnemyTier", Display: "Max Enemy Type", Category: "Entities", Type: optInt, ValueSet: "MaxEnemyType", DefaultInt: 5},
{Name: "BiomeEnemyDensity", Display: "Biome Enemy Density", Category: "Entities", Type: optInt, ValueSet: "BiomeEnemyDensity", DefaultInt: 0},
{Name: "BiomeZombieRespawn", Display: "Biome Enemy Respawn", Category: "Entities", Type: optInt, ValueSet: "SlowToFast", DefaultInt: 0},
{Name: "BiomeAnimalRespawn", Display: "Biome Animal Respawn", Category: "Entities", Type: optInt, ValueSet: "SlowToFast", DefaultInt: 0},
{Name: "EntityDamage", Display: "Entity Damage", Category: "Entities", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "EntityIncomingDamage", Display: "Entity Incoming Damage", Category: "Entities", Type: optFloat, ValueSet: "DamageValuesNoNone", DefaultFloat: 1},
{Name: "BlockDamageAI", Display: "Entity Block Damage", Category: "Entities", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "BlockDamageAIBM", Display: "Blood Moon Block Damage", Category: "Entities", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "HeadshotMode", Display: "Headshot Mode", Category: "Entities", Type: optInt, ValueSet: "HeadshotMode", DefaultInt: 0},
{Name: "ShowHealthBars", Display: "Entity Health Bars", Category: "Entities", Type: optBool, ValueSet: "YesNo", DefaultBool: false},
{Name: "ShowEnemyDamage", Display: "Show Entity Damage", Category: "Entities", Type: optBool, ValueSet: "YesNo", DefaultBool: false},
{Name: "ZombieMove", Display: "Zombie Day Speed", Category: "Entities", Type: optInt, ValueSet: "ZombieSpeeds", DefaultInt: 0},
{Name: "ZombieMoveNight", Display: "Zombie Night Speed", Category: "Entities", Type: optInt, ValueSet: "ZombieSpeeds", DefaultInt: 3},
{Name: "ZombieFeralMove", Display: "Zombie Feral Speed", Category: "Entities", Type: optInt, ValueSet: "ZombieSpeeds", DefaultInt: 3},
{Name: "ZombieBMMove", Display: "Zombie Blood Moon Speed", Category: "Entities", Type: optInt, ValueSet: "ZombieSpeeds", DefaultInt: 3},
{Name: "ZombieFeralSense", Display: "Zombie Feral Sense", Category: "Entities", Type: optInt, ValueSet: "ZombieFeralSense", DefaultInt: 0},
{Name: "AISmellMode", Display: "Zombie AI Smell Mode", Category: "Entities", Type: optInt, ValueSet: "AISmellMode", DefaultInt: 3},
{Name: "ZombieRageChance", Display: "Zombie Rage Chance", Category: "Entities", Type: optFloat, ValueSet: "ZombieRageChance", DefaultFloat: 0.15},
{Name: "AllowZombieDigging", Display: "Allow Zombie Digging", Category: "Entities", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "ZombiesEatAnimals", Display: "Zombies Eat Animals", Category: "Entities", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
// ---- World ----
{Name: "GlobalGSModifier", Display: "Global GameStage", Category: "World", Type: optFloat, ValueSet: "LowDefaultHigh", DefaultFloat: 1},
{Name: "BiomeGSModifier", Display: "Biome GameStage", Category: "World", Type: optFloat, ValueSet: "LowDefaultHigh", DefaultFloat: 1},
{Name: "BiomeProgression", Display: "Biome Progression", Category: "World", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "TemperatureSurvival", Display: "Temperature Survival", Category: "World", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "MaxTechType", Display: "Max Tech Type", Category: "World", Type: optInt, ValueSet: "MaxTechType", DefaultInt: 4},
{Name: "WorkstationsInTheWild", Display: "Workstations in the Wild", Category: "World", Type: optFloat, ValueSet: "JarRefund", DefaultFloat: 0},
{Name: "BloodMoonFrequency", Display: "Blood Moon Frequency", Category: "World", Type: optInt, ValueSet: "BloodMoonFrequency", DefaultInt: 7},
{Name: "BloodMoonRange", Display: "Blood Moon Range", Category: "World", Type: optInt, ValueSet: "BloodMoonRange", DefaultInt: 0},
{Name: "BloodMoonEnemyCount", Display: "Blood Moon Count", Category: "World", Type: optInt, ValueSet: "BloodMoonCount", DefaultInt: 8},
{Name: "BloodMoonWarning", Display: "Blood Moon Warning", Category: "World", Type: optInt, ValueSet: "BloodMoonWarning", DefaultInt: 1}, // enum 50? see note — BloodMoonWarning=49 in enum order
{Name: "AirDropFrequency", Display: "Air Drops", Category: "World", Type: optInt, ValueSet: "AirDrops", DefaultInt: 3},
{Name: "AirDropRandomTime", Display: "Air Drop Random Time", Category: "World", Type: optInt, ValueSet: "AirDropRandomTime", DefaultInt: 0},
{Name: "StormFreq", Display: "Storm Frequency", Category: "World", Type: optFloat, ValueSet: "StormFrequency", DefaultFloat: 1},
{Name: "StormWarning", Display: "Storm Warning", Category: "World", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "HeatMapSensitivity", Display: "Heatmap Sensitivity", Category: "World", Type: optFloat, ValueSet: "DisabledLowDefaultHigh", DefaultFloat: 1},
{Name: "DayNightLength", Display: "24 Day Cycle", Category: "World", Type: optInt, ValueSet: "DayNightLength", DefaultInt: 60},
{Name: "DayLightLength", Display: "Day Light Length", Category: "World", Type: optInt, ValueSet: "DayLightLength", DefaultInt: 18}, // enum 72? see note — DayLightLength=72 actually
{Name: "AirDropMarker", Display: "Mark Air Drops", Category: "World", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "AllowMap", Display: "Allow Map", Category: "World", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "AllowCompass", Display: "Allow Compass", Category: "World", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "AllowScreenMarkers", Display: "Allow Screen Markers", Category: "World", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "ShowLocationInfo", Display: "Show Location Info", Category: "World", Type: optBool, ValueSet: "YesNo", DefaultBool: true}, // ShowLocationInfo=71? see note
{Name: "ShowDayTime", Display: "Show Day/Time", Category: "World", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
// ---- Resources ----
{Name: "LootMaxTier", Display: "Loot Max Tier", Category: "Resources", Type: optInt, ValueSet: "ItemTierOptions", DefaultInt: 6},
{Name: "GlobalLSModifier", Display: "Global LootStage", Category: "Resources", Type: optFloat, ValueSet: "LowDefaultHigh", DefaultFloat: 1},
{Name: "BiomeLSModifier", Display: "Biome LootStage", Category: "Resources", Type: optFloat, ValueSet: "LowDefaultHigh", DefaultFloat: 1}, // BiomeLSModifier=68? see note
{Name: "POITierLSModifier", Display: "POI Tier LootStage", Category: "Resources", Type: optFloat, ValueSet: "LowDefaultHigh", DefaultFloat: 1}, // =69? see note
{Name: "LootRespawnDays", Display: "Loot Respawn Days", Category: "Resources", Type: optInt, ValueSet: "LootRespawnDays", DefaultInt: 7},
{Name: "LootTimer", Display: "Loot Time", Category: "Resources", Type: optFloat, ValueSet: "SpeedValues", DefaultFloat: 1},
{Name: "LootBagChance", Display: "Loot Bag Chance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "GlobalLootCount", Display: "Global Loot Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "FoodLootCount", Display: "Food Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "DrinkLootCount", Display: "Drink Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "MedicalLootCount", Display: "Medical Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "AmmoLootCount", Display: "Ammo Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1}, // AmmoLootCount=81? see note
{Name: "ResourceLootCount", Display: "Resource Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "ArmorLootCount", Display: "Armor Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "MeleeLootCount", Display: "Melee Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "RangedLootCount", Display: "Ranged Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "DukesLootCount", Display: "Dukes Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "CraftingMagazinesLootCount", Display: "Magazines Abundance", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "TreasureMapChance", Display: "Treasure Map Chance", Category: "Resources", Type: optFloat, ValueSet: "PlayerSpeedValues", DefaultFloat: 1},
{Name: "MiningOutput", Display: "Mining Output", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "CropOutput", Display: "Crop Output", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "SeedDropOutput", Display: "Seed Drop Output", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "HarvestingOutput", Display: "Harvesting Output", Category: "Resources", Type: optFloat, ValueSet: "LootAbundanceValues", DefaultFloat: 1},
{Name: "CropGrowthSpeed", Display: "Crop Growth", Category: "Resources", Type: optFloat, ValueSet: "CropGrowthSpeed", DefaultFloat: 1},
// ---- Crafting ----
{Name: "CraftingProgression", Display: "Crafting Progression", Category: "Crafting", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "CraftingMaxTier", Display: "Crafting Max Tier", Category: "Crafting", Type: optInt, ValueSet: "ItemTierOptions", DefaultInt: 6},
{Name: "PointsPerMagazine", Display: "Magazine Points", Category: "Crafting", Type: optInt, ValueSet: "PointsPer", DefaultInt: 1},
{Name: "BackpackCrafting", Display: "Backpack Crafting", Category: "Crafting", Type: optInt, ValueSet: "BackpackCrafting", DefaultInt: 1},
{Name: "WorkstationCrafting", Display: "Workstation Crafting", Category: "Crafting", Type: optBool, ValueSet: "YesNo", DefaultBool: true}, // WorkstationCrafting=100? see note
{Name: "SmeltingType", Display: "Smelter Type", Category: "Crafting", Type: optBool, ValueSet: "SmeltingType", DefaultBool: false},
{Name: "CraftingTime", Display: "Crafting Time", Category: "Crafting", Type: optFloat, ValueSet: "SpeedValues", DefaultFloat: 1},
{Name: "CraftingInput", Display: "Crafting Input", Category: "Crafting", Type: optFloat, ValueSet: "SpeedValues", DefaultFloat: 1},
{Name: "CraftingOutput", Display: "Crafting Output", Category: "Crafting", Type: optFloat, ValueSet: "StaminaRegen", DefaultFloat: 1}, // CraftingOutput=104? see note
{Name: "ScrappingOutput", Display: "Scrapping Output", Category: "Crafting", Type: optFloat, ValueSet: "SpeedValues", DefaultFloat: 1},
{Name: "DewCollectorTime", Display: "Dew Collector Time", Category: "Crafting", Type: optFloat, ValueSet: "SpeedValues", DefaultFloat: 1},
{Name: "DewCollectorOutput", Display: "Dew Collector Output", Category: "Crafting", Type: optFloat, ValueSet: "CollectorOutput", DefaultFloat: 1},
{Name: "DewCollectorInput", Display: "Dew Collector Input", Category: "Crafting", Type: optFloat, ValueSet: "DewCollectorInput", DefaultFloat: 1},
{Name: "ApiaryTime", Display: "Apiary Time", Category: "Crafting", Type: optFloat, ValueSet: "SpeedValues", DefaultFloat: 1},
{Name: "ApiaryOutput", Display: "Apiary Output", Category: "Crafting", Type: optFloat, ValueSet: "CollectorOutput", DefaultFloat: 1},
{Name: "ApiaryInput", Display: "Apiary Input", Category: "Crafting", Type: optFloat, ValueSet: "ApiaryInput", DefaultFloat: 1},
{Name: "ItemDegradation", Display: "Item Degradation", Category: "Crafting", Type: optFloat, ValueSet: "DisabledLowDefaultHigh", DefaultFloat: 1},
{Name: "RepairTypes", Display: "Item Repair Types", Category: "Crafting", Type: optInt, ValueSet: "RepairTypes", DefaultInt: 3},
{Name: "MaxDegradationAmount", Display: "Max Degrade Amount", Category: "Crafting", Type: optFloat, ValueSet: "MaxDegradationAmounts", DefaultFloat: 0},
// ---- Traders ----
{Name: "TradersEnabled", Display: "Trading Enabled", Category: "Traders", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "VendingEnabled", Display: "Vending Machines Enabled", Category: "Traders", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "TraderHours", Display: "Trader Hours", Category: "Traders", Type: optInt, ValueSet: "TraderHourPresets", DefaultInt: 0},
{Name: "TraderProtection", Display: "Trader Protection", Category: "Traders", Type: optInt, ValueSet: "TraderArea", DefaultInt: 0},
{Name: "TraderDialog", Display: "Trading Dialog", Category: "Traders", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "GlobalTSModifier", Display: "Global TraderStage", Category: "Traders", Type: optFloat, ValueSet: "LowDefaultHigh", DefaultFloat: 1}, // GlobalTSModifier=70? see note
{Name: "TraderMaxTier", Display: "Trader Max Tier", Category: "Traders", Type: optInt, ValueSet: "ItemTierOptions", DefaultInt: 6},
{Name: "TraderItemAbundance", Display: "Trader Item Abundance", Category: "Traders", Type: optFloat, ValueSet: "LowDefaultHigh", DefaultFloat: 1},
{Name: "VendingItemAbundance", Display: "Vending Item Abundance", Category: "Traders", Type: optFloat, ValueSet: "LowDefaultHigh", DefaultFloat: 1},
{Name: "TraderResetInterval", Display: "Trader Reset Interval", Category: "Traders", Type: optInt, ValueSet: "TraderResetInterval", DefaultInt: -1},
{Name: "VendingResetInterval", Display: "Vending Reset Interval", Category: "Traders", Type: optInt, ValueSet: "TraderResetInterval", DefaultInt: -1},
{Name: "TraderSellPrices", Display: "Traders Sell Price", Category: "Traders", Type: optFloat, ValueSet: "BarterValues", DefaultFloat: 1},
{Name: "TraderBuyPrices", Display: "Traders Buy Price", Category: "Traders", Type: optFloat, ValueSet: "BarterValues", DefaultFloat: 1},
{Name: "TraderBuyLimit", Display: "Trader Buy Limit", Category: "Traders", Type: optInt, ValueSet: "StarterSkillPoints", DefaultInt: 3},
// ---- Tasks ----
{Name: "ChallengesEnabled", Display: "Challenges Enabled", Category: "Tasks", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "IntroChallengesEnabled", Display: "Intro Challenges Enabled", Category: "Tasks", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "QuestsEnabled", Display: "Quests Enabled", Category: "Tasks", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "IntroQuestEnabled", Display: "Intro Quest Enabled", Category: "Tasks", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "TraderToTraderQuestsEnabled", Display: "Trader to Trader Quests", Category: "Tasks", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "BuriedQuestsEnabled", Display: "Buried Quests Enabled", Category: "Tasks", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "POIQuestsEnabled", Display: "POI Quests Enabled", Category: "Tasks", Type: optBool, ValueSet: "YesNo", DefaultBool: true},
{Name: "QuestsPerTier", Display: "Quests per Tier", Category: "Tasks", Type: optInt, ValueSet: "QuestPerTier", DefaultInt: 10},
{Name: "QuestProgressionDailyLimit", Display: "Quests per Day", Category: "Tasks", Type: optInt, ValueSet: "QuestPerDay", DefaultInt: 4},
{Name: "StarterSkillPoints", Display: "Starter Skill Points", Category: "Tasks", Type: optInt, ValueSet: "StarterSkillPoints", DefaultInt: 4},
// ---- Misc ----
{Name: "VehicleFuelUsage", Display: "Vehicle Fuel Usage", Category: "Misc", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "VehicleEntityDamage", Display: "Vehicle Entity Damage", Category: "Misc", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "VehicleBlockDamage", Display: "Vehicle Block Damage", Category: "Misc", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "VehicleSelfDamage", Display: "Vehicle Self Damage", Category: "Misc", Type: optFloat, ValueSet: "DamageValues", DefaultFloat: 1},
{Name: "ElectricalOutput", Display: "Electrical Output", Category: "Misc", Type: optFloat, ValueSet: "StaminaRegen", DefaultFloat: 1},
{Name: "SillyCelebrate", Display: "Celebrate Kills", Category: "Misc", Type: optInt, ValueSet: "Celebrate", DefaultInt: 0},
{Name: "SillyBigHeads", Display: "Big Heads", Category: "Misc", Type: optBool, ValueSet: "YesNo", DefaultBool: false},
{Name: "SillyTinyZombies", Display: "Tiny Zombies", Category: "Misc", Type: optBool, ValueSet: "YesNo", DefaultBool: false},
{Name: "SillyLowGravity", Display: "Gravity", Category: "Misc", Type: optFloat, ValueSet: "Gravity", DefaultFloat: 1},
{Name: "SillySounds", Display: "Silly Sounds", Category: "Misc", Type: optBool, ValueSet: "YesNo", DefaultBool: false},
{Name: "SillyBlackandWhite", Display: "Black and White", Category: "Misc", Type: optBool, ValueSet: "YesNo", DefaultBool: false},
}
+363
View File
@@ -0,0 +1,363 @@
// 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 AZ")
}
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 AZ")
}
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
}
+154
View File
@@ -0,0 +1,154 @@
package sandbox
import (
"testing"
)
// TestInitConsistency: every option resolves an enum + a default index, and no
// two options share an enum (the 2-char code must be unique per option).
func TestInitConsistency(t *testing.T) {
if len(options) != 150 {
t.Fatalf("expected 150 sandbox options, got %d", len(options))
}
seen := map[int]string{}
for i := range options {
o := &options[i]
if o.enum == 0 && o.Name != "RangedDamage" {
t.Errorf("%s: enum unresolved (0)", o.Name)
}
if prev, ok := seen[o.enum]; ok {
t.Errorf("enum %d shared by %s and %s", o.enum, prev, o.Name)
}
seen[o.enum] = o.Name
if _, ok := defaultIdx[o.Name]; !ok {
t.Errorf("%s: no default index", o.Name)
}
}
}
// TestGameDefaultCode decodes the game's own default difficulty (Adventurer)
// code from serverconfig.xml and checks the documented meaning, then re-encodes
// and confirms it round-trips byte-for-byte.
func TestGameDefaultCode(t *testing.T) {
const code = "AAAJABJACJADJARFBNC" // shipped in 3.0 serverconfig.xml comment
c := V3Codec{}
vals, err := c.Decode(code)
if err != nil {
t.Fatalf("decode: %v", err)
}
// Triples: AA J / AB J / AC J / AD J / AR F / BN C
// AA(0)=RangedDamage idx J=9 -> DamageValues[9]=1.5
// AB(1)=MeleeDamage idx 9 -> 1.5
// AC(2)=BlockDamage idx 9 -> 1.5
// AD(3)=TerrainDamage idx 9 -> 1.5
// AR(17)=IncomingDamage idx F=5 -> DamageValues[5]=0.75
// BN(39)=AISmellMode idx C=2 -> AISmellMode set Ints[2]=2
checks := map[string]string{
"RangedDamage": "1.5",
"MeleeDamage": "1.5",
"BlockDamage": "1.5",
"TerrainDamage": "1.5",
"IncomingDamage": "0.75",
"AISmellMode": "2",
}
for name, want := range checks {
if got := vals[name]; got != want {
t.Errorf("%s = %q, want %q", name, got, want)
}
}
// Re-encode: only the 6 non-default options should appear, in enum order →
// must reproduce the original code exactly.
round, err := c.Encode(vals)
if err != nil {
t.Fatalf("encode: %v", err)
}
if round != code {
t.Errorf("round-trip mismatch:\n have %q\n want %q", round, code)
}
}
// TestEmptyAndDefaultsEncodeEmpty: the all-defaults map encodes to just the
// version char (no triples), and an empty code decodes to all defaults.
func TestEmptyAndDefaultsEncodeEmpty(t *testing.T) {
c := V3Codec{}
def := DefaultsMap()
code, err := c.Encode(def)
if err != nil {
t.Fatalf("encode defaults: %v", err)
}
if code != "A" {
t.Errorf("defaults should encode to bare version char %q, got %q", "A", code)
}
vals, err := c.Decode("")
if err != nil {
t.Fatalf("decode empty: %v", err)
}
if len(vals) != 150 {
t.Errorf("empty decode should yield 150 defaults, got %d", len(vals))
}
for name, v := range vals {
if def[name] != v {
t.Errorf("%s: empty-decode %q != defaults %q", name, v, def[name])
}
}
}
// TestRoundTripNonDefault sets a spread of options across categories/types and
// confirms encode→decode preserves them.
func TestRoundTripNonDefault(t *testing.T) {
c := V3Codec{}
in := DefaultsMap()
in["BloodMoonFrequency"] = "14" // int set, non-default
in["XPMultiplier"] = "2" // float set
in["AllowZombieDigging"] = "false"
in["TraderResetInterval"] = "7"
in["SillyBigHeads"] = "true"
code, err := c.Encode(in)
if err != nil {
t.Fatalf("encode: %v", err)
}
out, err := c.Decode(code)
if err != nil {
t.Fatalf("decode: %v", err)
}
for _, k := range []string{"BloodMoonFrequency", "XPMultiplier", "AllowZombieDigging", "TraderResetInterval", "SillyBigHeads"} {
if out[k] != in[k] {
t.Errorf("%s: round-trip %q != %q", k, out[k], in[k])
}
}
}
// TestAlphaHelpers spot-checks the index<->alpha conversions against the C#.
func TestAlphaHelpers(t *testing.T) {
if v, _ := alpha2ToIndex("BN"); v != 39 {
t.Errorf("BN -> %d, want 39", v)
}
if v, _ := alpha2ToIndex("AA"); v != 0 {
t.Errorf("AA -> %d, want 0", v)
}
if s, _ := indexToAlpha2(39); s != "BN" {
t.Errorf("39 -> %q, want BN", s)
}
if v, _ := alphaToIndex('J'); v != 9 {
t.Errorf("J -> %d, want 9", v)
}
if b, _ := indexToAlpha(9); b != 'J' {
t.Errorf("9 -> %c, want J", b)
}
}
// TestValidate rejects malformed codes and accepts good ones.
func TestValidate(t *testing.T) {
if err := Validate(""); err != nil {
t.Errorf("empty should be valid: %v", err)
}
if err := Validate("AAAJABJACJADJARFBNC"); err != nil {
t.Errorf("game default should be valid: %v", err)
}
if err := Validate("ZZZ"); err == nil {
t.Error("wrong version char should fail")
}
if err := Validate("AAB"); err == nil {
t.Error("non-multiple-of-3 body should fail")
}
}
+94
View File
@@ -0,0 +1,94 @@
package sandbox
import (
"sort"
"strconv"
)
// SchemaOption is one sandbox option as the UI needs it: identity, type, the
// selectable values (with display labels), and the default value.
type SchemaOption struct {
Name string `json:"name"` // SandboxOptions enum member (stable key + config_values suffix)
Display string `json:"display"` // human label
Category string `json:"category"` // raw category key (General/Entities/...)
Type string `json:"type"` // "float" | "int" | "bool"
Values []string `json:"values"` // selectable values in index order (the code stores the index)
Labels []string `json:"labels"` // optional UI labels parallel to Values (may be empty)
Default string `json:"default"` // default value (one of Values)
}
// SchemaCategory groups options under a UI-friendly title.
type SchemaCategory struct {
Key string `json:"key"` // raw category key
Title string `json:"title"` // UI title
Icon string `json:"icon"` // emoji for the section
Options []SchemaOption `json:"options"` // in registration order
}
// categoryMeta maps the decompile's raw category keys to UI titles + icons,
// in display order.
var categoryMeta = []struct{ Key, Title, Icon string }{
{"General", "Player", "🧍"},
{"Entities", "Entities & Zombies", "🧟"},
{"World", "World & Blood Moon", "🌍"},
{"Resources", "Loot & Resources", "💰"},
{"Crafting", "Crafting", "🔨"},
{"Traders", "Traders", "🪙"},
{"Tasks", "Quests & Tasks", "📋"},
{"Misc", "Miscellaneous", "🎛️"},
}
// Schema returns the full 150-option sandbox schema grouped into the 8 UI
// categories, in display order. The UI renders per-option controls from this and
// sends back a name→value map for Encode.
func Schema() []SchemaCategory {
byCat := map[string][]SchemaOption{}
for i := range options {
o := &options[i]
so := SchemaOption{
Name: o.Name,
Display: o.Display,
Category: o.Category,
Default: o.valueAtIndex(defaultIdx[o.Name]),
}
switch o.Type {
case optFloat:
so.Type = "float"
for _, f := range o.set.Floats {
so.Values = append(so.Values, strconv.FormatFloat(f, 'f', -1, 64))
}
case optInt:
so.Type = "int"
for _, n := range o.set.Ints {
so.Values = append(so.Values, strconv.Itoa(n))
}
case optBool:
so.Type = "bool"
for _, b := range o.set.Bools {
so.Values = append(so.Values, strconv.FormatBool(b))
}
}
so.Labels = append(so.Labels, o.set.Display...)
byCat[o.Category] = append(byCat[o.Category], so)
}
var out []SchemaCategory
for _, cm := range categoryMeta {
out = append(out, SchemaCategory{Key: cm.Key, Title: cm.Title, Icon: cm.Icon, Options: byCat[cm.Key]})
}
// Any category not in categoryMeta (forward-compat) appended at the end.
known := map[string]bool{}
for _, cm := range categoryMeta {
known[cm.Key] = true
}
var extra []string
for k := range byCat {
if !known[k] {
extra = append(extra, k)
}
}
sort.Strings(extra)
for _, k := range extra {
out = append(out, SchemaCategory{Key: k, Title: k, Icon: "⚙️", Options: byCat[k]})
}
return out
}
+13
View File
@@ -0,0 +1,13 @@
package sandbox
import "testing"
func TestSchemaShape(t *testing.T){
s := Schema()
if len(s) < 8 { t.Fatalf("want >=8 categories, got %d", len(s)) }
n := 0
for _, c := range s { n += len(c.Options); for _, o := range c.Options {
if len(o.Values)==0 { t.Errorf("%s has no values", o.Name) }
found:=false; for _,v:=range o.Values { if v==o.Default {found=true} }
if !found { t.Errorf("%s default %q not in values %v", o.Name, o.Default, o.Values) }
}}
if n != 150 { t.Fatalf("want 150 options across categories, got %d", n) }
}
+114
View File
@@ -0,0 +1,114 @@
// Code generated from 7DTD V3.0 Assembly-CSharp SandboxOptionManager.SetupOptions
// (buildid 23705258, experimental 2026-06-15). DO NOT hand-edit casually — these
// tables ARE the SandboxCode encoding. A wrong value silently corrupts a
// server's sandbox settings. Re-derive from the decompiled SetupOptions if the
// encoding changes in a future 3.0 build (see CrackedFrom below).
//
// CrackedFrom: rb-decompile/3.0/src/SandboxOptions/SandboxOptionManager.cs
package sandbox
// valueKind distinguishes how a value-set's entries map to a serverconfig-style
// scalar. The SandboxCode itself only stores the INDEX into the set; these are
// for the panel UI / display + for emitting a human value.
type valueKind int
const (
kindFloat valueKind = iota // FloatValues
kindInt // IntValues
kindBool // BoolValues
)
// valueSet mirrors a SandboxOptionValueSet{Float,Int,Bool}. Exactly one of
// Floats/Ints/Bools is populated (per Kind). The index stored in a SandboxCode
// is a position into that slice. Display* are the localization-key labels (kept
// for the UI; not needed for encode/decode).
type valueSet struct {
Kind valueKind
Floats []float64
Ints []int
Bools []bool
Display []string // optional UI labels, parallel-ish to values (may be shorter)
}
// Len returns the number of selectable indices in the set.
func (v valueSet) Len() int {
switch v.Kind {
case kindFloat:
return len(v.Floats)
case kindInt:
return len(v.Ints)
case kindBool:
return len(v.Bools)
}
return 0
}
// IsValidIndex mirrors SandboxOptionValueSet.IsValidIndex.
func (v valueSet) IsValidIndex(i int) bool { return i >= 0 && i < v.Len() }
// valueSets is the ValueSets dictionary from SetupOptions(). Keyed by the same
// string the game uses so the option table can reference them by name.
var valueSets = map[string]valueSet{
"DamageValues": {Kind: kindFloat, Floats: []float64{0, 0.25, 0.35, 0.5, 0.65, 0.75, 0.85, 1, 1.25, 1.5, 2, 2.5, 3}, Display: []string{"none"}},
"DamageValuesNoNone": {Kind: kindFloat, Floats: []float64{0.25, 0.35, 0.5, 0.65, 0.75, 0.85, 1, 1.25, 1.5, 2, 2.5, 3}},
"PlayerSpeedValues": {Kind: kindFloat, Floats: []float64{0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 2, 3}, Display: []string{"none"}},
"PlayerSpeedValuesWithNone": {Kind: kindFloat, Floats: []float64{0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 2, 3}},
"SpeedValues": {Kind: kindFloat, Floats: []float64{0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3}, Display: []string{"none"}},
"StaminaUsage": {Kind: kindFloat, Floats: []float64{0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2}, Display: []string{"none"}},
"LootAbundanceValues": {Kind: kindFloat, Floats: []float64{0, 0.25, 0.35, 0.5, 0.65, 0.75, 0.85, 1, 1.25, 1.5, 2, 3, 4, 5}, Display: []string{"none"}},
"ZombieRageChance": {Kind: kindFloat, Floats: []float64{0, 0.15, 0.3, 0.35, 0.4, 0.5, 0.6, 0.75, 0.9, 1}, Display: []string{"none"}},
"ZombieSpeeds": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4}, Display: []string{"goZMWalk", "goZMJog", "goZMRun", "goZMSprint", "goZMNightmare"}},
"AISmellMode": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5}, Display: []string{"none", "goZMWalk", "goZMJog", "goZMRun", "goZMSprint", "goZMNightmare"}},
"JumpStrength": {Kind: kindFloat, Floats: []float64{0, 0.5, 1, 1.25, 1.5, 2, 3}, Display: []string{"none"}},
"StaminaRegen": {Kind: kindFloat, Floats: []float64{0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3}},
"XPGain": {Kind: kindFloat, Floats: []float64{0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 5}, Display: []string{"none"}},
"JarRefund": {Kind: kindFloat, Floats: []float64{0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1}, Display: []string{"none"}},
"BarterValues": {Kind: kindFloat, Floats: []float64{0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4}, Display: []string{"none"}},
"DisabledLowDefaultHigh": {Kind: kindFloat, Floats: []float64{0, 0.25, 0.5, 1, 1.5, 2}, Display: []string{"goDisabled", "goVeryLow", "goLow", "goDefault", "goHigh", "goVeryHigh"}},
"LowDefaultHigh": {Kind: kindFloat, Floats: []float64{0.25, 0.5, 1, 1.5, 2}, Display: []string{"goVeryLow", "goLow", "goDefault", "goHigh", "goVeryHigh"}},
"Encumbrance": {Kind: kindFloat, Floats: []float64{10, 1.35, 1, 0.7, 0.35, 0}, Display: []string{"goDisabled", "goLow", "goDefault", "goHigh", "goVeryHigh", "xuiOptionsVideoTexQualityFull"}},
"SkillGainRate": {Kind: kindInt, Ints: []int{1, 2, 3, 4, 5}},
"PointsPer": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5, 6, 7}, Display: []string{"none"}},
"StarterSkillPoints": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, Display: []string{"none"}},
"BloodMoonFrequency": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 20, 30}, Display: []string{"goDisabled", "goDay"}},
"BloodMoonRange": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 7, 10, 14, 20}, Display: []string{"goDays", "goDay"}},
"BloodMoonWarning": {Kind: kindInt, Ints: []int{0, 1, 2}, Display: []string{"goDisabled", "goMorning", "goEvening"}},
"BloodMoonCount": {Kind: kindInt, Ints: []int{4, 6, 8, 10, 12, 16, 24, 32, 64}},
"AirDrops": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5, 6}, Display: []string{"goDisabled", "goAirDropValue"}},
"AirDropRandomTime": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5, 6}, Display: []string{"none", "goMorning", "goMidDayOnly", "goEvening", "goNightOnly", "goAllDay", "goAnyValue"}},
"StormFrequency": {Kind: kindFloat, Floats: []float64{0, 0.5, 1, 1.5, 2, 3, 4, 5}, Display: []string{"none"}},
"QuestPerTier": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, Display: []string{"none"}},
"QuestPerDay": {Kind: kindInt, Ints: []int{-1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, Display: []string{"goUnlimited"}},
"TraderArea": {Kind: kindInt, Ints: []int{0, 1, 2}, Display: []string{"xuiYes", "goClaimable", "goNotClaimable"}},
"TraderResetInterval": {Kind: kindInt, Ints: []int{-1, 1, 2, 3, 4, 5, 6, 7, 14}, Display: []string{"xuiDefault", "goDay"}},
"ItemTierOptions": {Kind: kindInt, Ints: []int{1, 2, 3, 4, 5, 6}},
"DewCollectorInput": {Kind: kindFloat, Floats: []float64{0, 1, 2, 3}, Display: []string{"none"}},
"ApiaryInput": {Kind: kindFloat, Floats: []float64{0, 0.2, 0.4, 0.6, 0.8, 1, 1.5, 2, 3}, Display: []string{"none"}},
"CollectorOutput": {Kind: kindFloat, Floats: []float64{1, 2, 3, 4, 5}},
"BackpackCrafting": {Kind: kindInt, Ints: []int{0, 1, 2, 3}, Display: []string{"xuiNo", "xuiYes", "goLimited", "goWorkbenchOnly"}},
"DeathPenalty": {Kind: kindInt, Ints: []int{0, 1, 2, 3}, Display: []string{"none", "goXPOnly", "goInjured", "goPermaDeath"}},
"DropOnDeath": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4}, Display: []string{"none", "lblAll", "goToolbelt", "goBackpack", "goDeleteAll"}},
"DropOnQuit": {Kind: kindInt, Ints: []int{0, 1, 2, 3}, Display: []string{"none", "lblAll", "goToolbelt", "goBackpack"}},
"LoseItemsOnDeathType": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5}, Display: []string{"none", "lblAll", "goToolbelt", "goBackpack", "goEquipment", "goCarriedOnly"}},
"DegradeItemsOnDeath": {Kind: kindInt, Ints: []int{0, 1, 2, 3}, Display: []string{"none", "xuiDurability", "xuiMaxDurability", "xuiBoth"}},
"TraderHourPresets": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5, 6}, Display: []string{"xuiDefault", "goMorning", "goMidDayOnly", "goEvening", "goNightOnly", "goOnlyClosedOnBM", "goAlwaysOpen"}},
"YesNo": {Kind: kindBool, Bools: []bool{false, true}, Display: []string{"xuiNo", "xuiYes"}},
"Celebrate": {Kind: kindInt, Ints: []int{0, 1, 2}, Display: []string{"xuiNo", "xuiYes", "goHeadshotOnly"}},
"ShowXP": {Kind: kindInt, Ints: []int{0, 1, 2, 3}, Display: []string{"lblAll", "goBarOnly", "goNotificationsOnly", "none"}},
"HeadshotMode": {Kind: kindInt, Ints: []int{0, 1, 2}, Display: []string{"none", "goHeadshotOnly", "goHeadshotFinisher"}},
"MaxEnemyType": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5}, Display: []string{"goNormals", "goStrongs", "goSpecials", "goFerals", "goRadiated", "goElites"}},
"MaxTechType": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4}, Display: []string{"none", "goTech0", "goTech1", "goTech2", "goTech3"}},
"LoseItemCount": {Kind: kindInt, Ints: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, Display: []string{"1-3", "1-5", "1-10", "1-20", "3-5", "5-7", "5-10", "7-10", "10-15", "15-20"}},
"DayNightLength": {Kind: kindInt, Ints: []int{10, 20, 30, 40, 50, 60, 90, 120}},
"DayLightLength": {Kind: kindInt, Ints: []int{0, 4, 6, 8, 10, 12, 14, 16, 18, 20, 24}, Display: []string{"goAlwaysNight", "4", "6", "8", "10", "12", "14", "16", "18", "20", "goAlwaysDay"}},
"LootRespawnDays": {Kind: kindInt, Ints: []int{-1, 5, 7, 10, 15, 20, 30, 40, 50}, Display: []string{"goDisabled"}},
"MaxChunkAge": {Kind: kindInt, Ints: []int{-1, 1, 3, 5, 7, 10, 20, 30, 40, 50, 75, 100}, Display: []string{"goDisabled"}},
"Gravity": {Kind: kindFloat, Floats: []float64{0.5, 0.6, 0.7, 0.8, 0.9, 1}},
"SlowToFast": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5}, Display: []string{"xuiDefault", "goVerySlow", "goSlow", "goNormal", "goFast", "goVeryFast"}},
"BiomeEnemyDensity": {Kind: kindInt, Ints: []int{0, 1, 2, 3, 4, 5}, Display: []string{"xuiDefault", "goVeryLow", "goLow", "goMedium", "goHigh", "goVeryHigh"}},
"SmeltingType": {Kind: kindBool, Bools: []bool{false, true}, Display: []string{"goSmelter", "lblContextActionRecipes"}},
"RepairTypes": {Kind: kindInt, Ints: []int{0, 1, 2, 3}, Display: []string{"none", "goRepairOnly", "goCombineOnly", "xuiBoth"}},
"MaxDegradationAmounts": {Kind: kindFloat, Floats: []float64{0, 0.05, 0.1, 0.15, 0.2, 0.25}, Display: []string{"none"}},
"CropGrowthSpeed": {Kind: kindFloat, Floats: []float64{1000, 0, 0.2, 0.5, 0.75, 1, 1.5, 2, 3}, Display: []string{"none", "xuiInstant"}},
"ZombieFeralSense": {Kind: kindInt, Ints: []int{0, 1, 2, 3}, Display: []string{"goDisabled", "goZMDay", "goZMNight", "goZMAll"}},
}