diff --git a/cardsim/stats.go b/cardsim/stats.go index 59b5d22..543699b 100644 --- a/cardsim/stats.go +++ b/cardsim/stats.go @@ -118,8 +118,9 @@ func (s statFunc[T]) Visible() bool { return s.visible } -// ExtractStats pulls all exported stats out of a struct. It puts fields before -// method. +// ExtractStats pulls all exported stats out of a struct. It puts methods before +// fields. If the calculated name of a method conflicts with the calculated +// name of a stat from a field, the method wins. // // A field is a stat if it is of some Stat type or is tagged with `cardsim:"stat"`, // `cardsim:"hidden"` (invisible stat), `cardsim:"round2"` (or any integer, 2 is @@ -148,6 +149,60 @@ func ExtractStats(x any) []Stat { typ := v.Type() var ret []Stat + + known := make(map[string]bool) + for _, vv := range []reflect.Value{v, v.Addr()} { + xt := vv.Type() + lim := xt.NumMethod() + for i := 0; i < lim; i++ { + m := xt.Method(i) + if !m.IsExported() { + continue + } + tm := m.Type + if tm.NumIn() != 1 { + // 1 arg -- receiver + continue + } + if tm.NumOut() != 1 { + continue + } + nameParts := explode(m.Name) + if len(nameParts) < 2 { + continue + } + isHidden := false + if nameParts[0] == "Hidden" { + isHidden = true + nameParts = nameParts[1:] + } + if nameParts[0] != "Stat" { + continue + } + n := strings.Join(nameParts[1:], " ") + if n == "" { + continue + } + if known[n] { + continue + } + known[n] = true + val := vv.Method(i).Call([]reflect.Value{}) + if len(val) != 1 { + // This shouldn't happen - we already checked Out. Weird. + continue + } + if !val[0].CanInterface() { + continue + } + ret = append(ret, &StatLiteral{ + Name: n, + Value: fmt.Sprint(val[0].Interface()), + IsVisible: !isHidden, + }) + } + } + fields := reflect.VisibleFields(typ) for _, sf := range fields { if !sf.IsExported() { @@ -157,7 +212,12 @@ func ExtractStats(x any) []Stat { if !f.CanInterface() { continue } - if s, ok := f.Interface().(Stat); ok { + iface := f.Interface() + if s, ok := iface.(Stat); ok { + if known[s.StatName()] { + continue + } + known[s.StatName()] = true ret = append(ret, s) continue } @@ -181,14 +241,19 @@ func ExtractStats(x any) []Stat { t = t[5:] n, _ := strconv.Atoi(t) fs := fmt.Sprintf("%%.%df", n) - val = fmt.Sprintf(fs, f.Interface()) + val = fmt.Sprintf(fs, iface) } else if isStat { - val = fmt.Sprint(f.Interface()) + val = fmt.Sprint(iface) } else { continue // not identifiably a stat } + nm := strings.Join(explode(sf.Name), " ") + if known[nm] { + continue + } + known[nm] = true ret = append(ret, &StatLiteral{ - Name: strings.Join(explode(sf.Name), " "), + Name: nm, Value: val, IsVisible: !isHidden, }) @@ -197,50 +262,6 @@ func ExtractStats(x any) []Stat { // Else, not a stat. } - lim := typ.NumMethod() - for i := 0; i < lim; i++ { - m := typ.Method(i) - if !m.IsExported() { - continue - } - tm := m.Type - if tm.NumIn() != 1 { - // 1 arg -- receiver - continue - } - if tm.NumOut() != 1 { - continue - } - nameParts := explode(m.Name) - if len(nameParts) < 2 { - continue - } - isHidden := false - if nameParts[0] == "Hidden" { - isHidden = true - nameParts = nameParts[1:] - } - if nameParts[0] != "Stat" { - continue - } - n := strings.Join(nameParts[1:], ": ") - if n == "" { - continue - } - val := v.Method(i).Call([]reflect.Value{}) - if len(val) != 1 { - // This shouldn't happen - we already checked Out. Weird. - continue - } - if !val[0].CanInterface() { - continue - } - ret = append(ret, &StatLiteral{ - Name: n, - Value: fmt.Sprint(val[0].Interface()), - IsVisible: !isHidden, - }) - } return ret } diff --git a/smoketest/collection.go b/smoketest/collection.go index 018721a..995dd3b 100644 --- a/smoketest/collection.go +++ b/smoketest/collection.go @@ -15,6 +15,7 @@ type SmokeTestCollection struct { Things int `cardsim:"stat"` MoreThings int `cardsim:"hidden"` FloatyThings float64 `cardsim:"round1"` + Label string `cardsim:"stat"` } func (c *SmokeTestCollection) Average() float64 { diff --git a/smoketest/main.go b/smoketest/main.go index b004b3b..c51be3f 100644 --- a/smoketest/main.go +++ b/smoketest/main.go @@ -28,6 +28,10 @@ func main() { Name: "Flavor", Value: "Lemon", }, + Things: 5, + MoreThings: 9, + FloatyThings: 123.456, + Label: "whee", }, ) p.Name = "Dave"