package menus import ( "fmt" "image" "image/color" "io/ioutil" "path/filepath" "strconv" "strings" "code.ur.gs/lupine/ordoor/internal/util/asciiscan" ) // MenuType tells us what sort of Group we have type MenuType int // SubMenuType tells us what sort of Record we have type SubMenuType int const ( TypeStatic MenuType = 0 TypeMenu MenuType = 1 TypeDragMenu MenuType = 2 // Only seen in Configure_Vehicle_{Chaos,Ultra} TypeRadioMenu MenuType = 3 // ??? TypeMainBackground MenuType = 45 // ??? TypeDialogue MenuType = 300 SubTypeSimpleButton SubMenuType = 3 SubTypeDoorHotspot1 SubMenuType = 30 // Like a button I guess? "FONTTYPE is animation speed" SubTypeDoorHotspot2 SubMenuType = 31 // Seems like a duplicate of the above? What's different? SubTypeLineKbd SubMenuType = 40 SubTypeLineBriefing SubMenuType = 41 SubTypeThumb SubMenuType = 45 // A "thumb" appears to be a vertical slider SubTypeInvokeButton SubMenuType = 50 SubTypeClickText SubMenuType = 60 SubTypeOverlay SubMenuType = 61 SubTypeHypertext SubMenuType = 70 SubTypeCheckbox SubMenuType = 91 SubTypeEditBox SubMenuType = 100 SubTypeInventorySelect SubMenuType = 110 SubTypeRadioButton SubMenuType = 120 SubTypeDropdownButton SubMenuType = 200 SubTypeComboBoxItem SubMenuType = 205 SubTypeAnimationSample SubMenuType = 220 SubTypeAnimationHover SubMenuType = 221 // FONTTYPE is animation speed. Only animate when hovered SubTypeMainButton SubMenuType = 228 SubTypeSlider SubMenuType = 232 SubTypeStatusBar SubMenuType = 233 SubTypeListBoxUp SubMenuType = 400 // FIXME: these have multiple items in SUBMENUTYPE SubTypeListBoxDown SubMenuType = 405 ) // FIXME: certain elements - especially overlays - don't have a DESC specified // in the .mnu file, but display text specified with a number in i18n. The only // conclusion I can draw is that they're hardcoded in the binary and set from // outside. So, do that here. var DescOverrides = map[string]int{ "main:2.6": 50992, "newgame:2.5": 50993, "keyboard:3.3": 50995, "levelply:2.6": 50996, } // FIXME: Same idea with text overrides, only these aren't mentioned in the .dta // file at all! var TextOverrides = map[string]string{ "main:2.7": "0.1-ordoor", } var TypeOverrides = map[string]SubMenuType{ // FIXME: These are put down as simple buttons, but it's a *lot* easier to // understand them as list box buttons. "configure_ultequip:7.5": SubTypeListBoxUp, "configure_ultequip:7.6": SubTypeListBoxDown, } type Menu struct { Name string // TODO: load these ObjectFiles []string FontNames []string // These are properties set in the menu header. We don't know what they're // all for. BackgroundColor color.Color HypertextColor color.Color FontType int // The actual menu records. There are multiple top-level items. Submenus are // only ever nested one deep. Groups []*Group } // Group represents an element with a MENUTYPE. It is part of a Menu and may // have children. type Group struct { Menu *Menu Records []*Record Properties Type MenuType } type Record struct { Menu *Menu Group *Group Properties Type SubMenuType } type Properties struct { Locator string // Not strictly a property. Set for tracking. ID int ObjectIdx int // Can be specified in MENUID, defaults to 0 Accelerator int Active bool Desc string DrawType int FontType int Moveable bool Share int SoundType int SpriteId []int X int Y int // From i18n Text string Help string } func (p *Properties) Point() image.Point { if p.X > 0 || p.Y > 0 { return image.Pt(p.X, p.Y) } return image.Point{} } func LoadMenu(filename string, palette color.Palette) (*Menu, error) { name := filepath.Base(filename) name = strings.TrimSuffix(name, filepath.Ext(name)) name = strings.ToLower(name) scanner, err := asciiscan.New(filename) if err != nil { return nil, err } defer scanner.Close() out := &Menu{ Name: name, } if err := loadObjects(out, scanner); err != nil { return nil, err } if err := loadProperties(out, scanner, palette); err != nil { return nil, err } if err := loadFonts(out, scanner); err != nil { return nil, err } if err := loadRecords(filepath.Dir(filename), out, scanner); err != nil { return nil, err } return out, nil } func loadObjects(menu *Menu, scanner *asciiscan.Scanner) error { strs, err := scanner.ConsumeStringList() if err != nil { return err } menu.ObjectFiles = strs return nil } func loadProperties(menu *Menu, scanner *asciiscan.Scanner, palette color.Palette) error { for { ok, err := scanner.PeekProperty() if err != nil { return err } if !ok { break } k, v, err := scanner.ConsumeProperty() if err != nil { return err } vInt, err := strconv.Atoi(v) // All properties have been int if err != nil { return err } // DeBrief.mnu misspells these parts := strings.SplitN(strings.ToUpper(k), " ", 3) if len(parts) > 2 { k = strings.Join(parts[0:2], " ") } switch strings.ToUpper(k) { case "BACKGROUND COLOR": menu.BackgroundColor = palette[vInt] case "HYPERTEXT COLOR": menu.HypertextColor = palette[vInt] case "FONT TYPE": menu.FontType = vInt default: return fmt.Errorf("Unhandled menu property in %v: %q=%q", menu.Name, k, v) } } return nil } func loadFonts(menu *Menu, scanner *asciiscan.Scanner) error { // FIXME: Can we just ignore NULL, or does the index matter? strs, err := scanner.ConsumeStringList("NULL") if err != nil { return err } menu.FontNames = strs return nil } func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error { // We build things up line by line in these variables var group *Group var record *Record var properties *Properties for { str, err := scanner.ConsumeString() if err != nil { return err } if strings.HasPrefix(str, "$") { subScanner, err := asciiscan.New(filepath.Join(baseDir, str[1:])) if err != nil { return err } err = loadRecords(baseDir, menu, subScanner) subScanner.Close() // Don't keep this around for all of loadRecords if err != nil { return fmt.Errorf("Processing child %q: %v", str, err) } continue } if str == "*" { if record != nil { group.Records = append(group.Records, record) record = nil } if group != nil { menu.Groups = append(menu.Groups, group) group = nil } continue // New group } if str == "~" { break // THE END } k, v := asciiscan.ConsumeProperty(str) switch strings.ToUpper(k) { case "MENUID": if group != nil { menu.Groups = append(menu.Groups, group) } group = newGroup(menu, v) properties = &group.Properties case "SUBMENUID": if record != nil { group.Records = append(group.Records, record) } record = newRecord(group, v) properties = &record.Properties case "MENUTYPE": group.setMenuType(v) case "SUBMENUTYPE": record.setSubMenuType(v) default: if err := properties.setProperty(k, v); err != nil { return err } } } return nil } func LoadMenus(dir string, palette color.Palette) (map[string]*Menu, error) { fis, err := ioutil.ReadDir(dir) if err != nil { return nil, err } out := make(map[string]*Menu, len(fis)) for _, fi := range fis { relname := fi.Name() basename := filepath.Base(relname) extname := filepath.Ext(relname) // Skip anything that isn't a .mnu file if !strings.EqualFold(extname, ".mnu") { continue } built, err := LoadMenu(filepath.Join(dir, relname), palette) if err != nil { return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err) } out[basename] = built } return out, nil } func listOfInts(s string) []int { vSplit := strings.Split(s, ",") vSplitInt := make([]int, len(vSplit)) for i, subV := range vSplit { vSplitInt[i], _ = strconv.Atoi(subV) } return vSplitInt } func newGroup(menu *Menu, idStr string) *Group { out := &Group{Menu: menu} // ObjectIdx can be specified in the MENUID. Only seen for .mni files ints := listOfInts(idStr) out.ID = ints[0] if len(ints) > 1 { out.ObjectIdx = ints[1] } out.Locator = fmt.Sprintf("%v:%v", menu.Name, out.ID) return out } func newRecord(group *Group, idStr string) *Record { out := &Record{Group: group} out.ID, _ = strconv.Atoi(idStr) // FIXME: we're ignoring conversion errors here out.ObjectIdx = group.ObjectIdx // FIXME: we shouldn't *copy* this out.Locator = fmt.Sprintf("%v.%v", group.Locator, out.ID) return out } func (g *Group) setMenuType(s string) { v, _ := strconv.Atoi(s) // FIXME: conversion errors g.Type = MenuType(v) } func (r *Record) setSubMenuType(s string) { // FIXME: Type overrides shouldn't be necessary! if override, ok := TypeOverrides[r.Locator]; ok { r.Type = override return } // FIXME: what are the other types here? Related to list boxes? ints := listOfInts(s) r.Type = SubMenuType(ints[0]) } func (p *Properties) setProperty(k, v string) error { ints := listOfInts(v) vInt := ints[0] asBool := (vInt != 0) switch strings.ToUpper(k) { case "ACCELERATOR": p.Accelerator = vInt case "ACTIVE": p.Active = asBool case "DESC": p.Desc = v // Usually int, occasionally string case "DRAW TYPE": p.DrawType = vInt case "FONTTYPE": p.FontType = vInt case "MOVEABLE": p.Moveable = asBool case "SOUNDTYPE": p.SoundType = vInt case "SPRITEID": p.SpriteId = ints case "X-CORD": p.X = vInt case "Y-CORD": p.Y = vInt case "SHARE": p.Share = vInt default: return fmt.Errorf("Unknown property for %v: %v=%v", p.Locator, k, v) } return nil } type Replacer interface { ReplaceText(int, *string) ReplaceHelp(int, *string) } func (r *Record) Internationalize(replacer Replacer) { if override, ok := TextOverrides[r.Locator]; ok { r.Text = override return } if override, ok := DescOverrides[r.Locator]; ok { r.Desc = strconv.Itoa(override) } id, err := strconv.Atoi(r.Desc) if err == nil { replacer.ReplaceText(id, &r.Text) replacer.ReplaceHelp(id, &r.Help) } else { r.Text = r.Desc // Sometimes it's a string like "EQUIPMENT" } } func (m *Menu) Internationalize(replacer Replacer) { for _, group := range m.Groups { for _, record := range group.Records { record.Internationalize(replacer) } } } func (g *Group) Props() *Properties { return &g.Properties } func (r *Record) Props() *Properties { return &r.Properties } func (p *Properties) BaseSpriteID() int { base := p.Share // SpriteId takes precedence if present if len(p.SpriteId) > 0 && p.SpriteId[0] >= 0 { base = p.SpriteId[0] } return base }