panel — open-source game server manager (public release)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:19:43 -07:00
commit 658bda1d24
2160 changed files with 300413 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
package empyrionitems
// Loot group parser for Empyrion's `LootGroups.ecf`.
// Format:
//
// { +LootGroup Name: EscapePodEasy
// Count: all
// Item_0: EmergencyRations, param1: 1
// Item_1: WaterBottle, param1: 5
// ...
// }
//
// param1 = count (or "low-high" range), param2 = optional probability.
// Item names are canonical Item.Name tokens.
import (
"bufio"
"io"
"os"
"strings"
)
type LootEntry struct {
Item string `json:"item"`
Count string `json:"count,omitempty"`
Probability string `json:"probability,omitempty"`
}
type LootGroup struct {
Name string `json:"name"`
Count string `json:"count,omitempty"`
Items []LootEntry `json:"items"`
}
type LootIndex struct {
Groups []LootGroup
byName map[string]*LootGroup
byItem map[string][]string // ingredientLower → group names
}
func LoadLootFromFile(path string) (*LootIndex, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return LoadLootFromReader(f)
}
func LoadLootFromReader(r io.Reader) (*LootIndex, error) {
idx := &LootIndex{byName: map[string]*LootGroup{}, byItem: map[string][]string{}}
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
var current *LootGroup
var depth int
for sc.Scan() {
line := stripComment(sc.Text())
trim := strings.TrimSpace(line)
if trim == "" {
continue
}
if current == nil {
if strings.HasPrefix(trim, "{ +LootGroup ") || strings.HasPrefix(trim, "{+LootGroup ") || strings.HasPrefix(trim, "{ LootGroup ") {
body := strings.TrimPrefix(trim, "{ +LootGroup ")
body = strings.TrimPrefix(body, "{+LootGroup ")
body = strings.TrimPrefix(body, "{ LootGroup ")
current = &LootGroup{}
for _, kv := range splitTopLevel(body, ',') {
parts := strings.SplitN(strings.TrimSpace(kv), ":", 2)
if len(parts) != 2 {
continue
}
k := strings.TrimSpace(parts[0])
v := stripQuotes(strings.TrimSpace(parts[1]))
if strings.EqualFold(k, "name") {
current.Name = v
}
}
depth = 1
}
continue
}
opens := strings.Count(trim, "{")
closes := strings.Count(trim, "}")
depth += opens - closes
if depth <= 0 {
if current.Name != "" {
cp := *current
idx.Groups = append(idx.Groups, cp)
gp := &idx.Groups[len(idx.Groups)-1]
idx.byName[strings.ToLower(cp.Name)] = gp
for _, it := range cp.Items {
low := strings.ToLower(it.Item)
idx.byItem[low] = append(idx.byItem[low], cp.Name)
}
}
current = nil
depth = 0
continue
}
parts := strings.SplitN(trim, ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := stripComment(strings.TrimSpace(parts[1]))
if strings.HasPrefix(strings.ToLower(key), "item_") {
// Item_N: Name, param1: count, param2: probability
le := LootEntry{}
pieces := splitTopLevel(val, ',')
for i, p := range pieces {
p = strings.TrimSpace(p)
if i == 0 {
le.Item = stripQuotes(p)
continue
}
kv := strings.SplitN(p, ":", 2)
if len(kv) != 2 {
continue
}
kk := strings.ToLower(strings.TrimSpace(kv[0]))
vv := strings.TrimSpace(kv[1])
switch kk {
case "param1", "param":
le.Count = vv
case "param2", "xdata":
le.Probability = vv
}
}
if le.Item != "" {
current.Items = append(current.Items, le)
}
} else if strings.EqualFold(key, "Count") {
current.Count = strings.TrimSpace(val)
}
}
if err := sc.Err(); err != nil {
return nil, err
}
return idx, nil
}
func (idx *LootIndex) ByName(name string) *LootGroup {
if g, ok := idx.byName[strings.ToLower(name)]; ok {
c := *g
return &c
}
return nil
}
func (idx *LootIndex) DroppedBy(itemName string) []string {
return append([]string{}, idx.byItem[strings.ToLower(itemName)]...)
}