package menus import ( "fmt" "image/color" "io/ioutil" "log" "path/filepath" "strconv" "strings" "code.ur.gs/lupine/ordoor/internal/data" "code.ur.gs/lupine/ordoor/internal/util/asciiscan" ) type MenuType int const ( TypeStatic MenuType = 0 TypeMenu MenuType = 1 TypeDragMenu MenuType = 2 // Only seen in Configure_Vehicle_{Chaos,Ultra} TypeSimpleButton MenuType = 3 TypeDoorHotspot MenuType = 30 // Like a button I guess? "FONTTYPE is animation speed" TypeDoorHotspot2 MenuType = 31 // Seems like a duplicate of the above? What's different? TypeLineKbd MenuType = 40 TypeThumb MenuType = 45 // A "thumb" appears to be a vertical slider TypeLineBriefing MenuType = 41 TypeInvokeButton MenuType = 50 TypeDoorHotspot3 MenuType = 60 // Maybe? Appears in Arrange.mnu TypeOverlay MenuType = 61 TypeHypertext MenuType = 70 TypeCheckbox MenuType = 91 TypeEditBox MenuType = 100 TypeInventorySelect MenuType = 110 TypeRadioButton MenuType = 120 TypeDropdownButton MenuType = 200 TypeComboBoxItem MenuType = 205 TypeAnimationSample MenuType = 220 TypeAnimationHover MenuType = 221 // FONTTYPE is animation speed. Only animate when hovered TypeMainButton MenuType = 228 TypeSlider MenuType = 232 TypeStatusBar MenuType = 233 TypeDialogue MenuType = 300 TypeListBoxUp MenuType = 400 // FIXME: these have multiple items in MENUTYPE TypeListBoxDown MenuType = 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", } // FIXME: The menu is specified as type 2 (button) in these cases, which is // weird. Make it a menu for now. // // Hypothesis: MENUTYPE and SUBMENUTYPE are not equivalent? var TypeOverrides = map[string]MenuType{ "levelply:2": TypeMenu, "savegame:2": TypeMenu, "loadgame:2": TypeMenu, // "thumb" is not a background. "maingame:2": TypeStatic, // ??? "configure_ultequip:7.5": TypeListBoxUp, "configure_ultequip:7.6": TypeListBoxDown, } type Record struct { Menu *Menu Parent *Record Children []*Record Id int ObjectIdx int // Can be specified in MENUID, defaults to 0 Type MenuType DrawType int FontType int Active bool SpriteId []int Share int X int Y int // From i18n Text string Help string // FIXME: turn these into first-class data properties map[string]string } 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. Records []*Record } func LoadMenu(filename string) (*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); 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) 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 } switch strings.ToUpper(k) { case "BACKGROUND COLOR 0..255..-1 TRANS": menu.BackgroundColor = data.ColorPalette[vInt] case "HYPERTEXT COLOR 0..255": menu.HypertextColor = data.ColorPalette[vInt] case "FONT TYPE 0..5": 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 { var record *Record // We build records here and add them when complete 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 } switch str { case "*": if record != nil { menu.Records = append(menu.Records, record.Toplevel()) } continue // NEXT RECORD case "~": return nil // THE END } k, v := asciiscan.ConsumeProperty(str) switch k { case "MENUID": record = newRecord(menu, nil) case "SUBMENUID": record = newRecord(menu, record.Toplevel()) } setProperty(record, k, v) } return nil } func LoadMenus(dir string) (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)) if err != nil { return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err) } out[basename] = built } return out, nil } func newRecord(menu *Menu, parent *Record) *Record { out := &Record{ Menu: menu, Parent: parent, properties: map[string]string{}, } if parent != nil { parent.Children = append(parent.Children, out) } return out } func (r *Record) Toplevel() *Record { if r.Parent != nil { return r.Parent.Toplevel() } return r } func setProperty(r *Record, k, v string) { vSplit := strings.Split(v, ",") vInt, _ := strconv.Atoi(v) vSplitInt := make([]int, len(vSplit)) for i, subV := range vSplit { vSplitInt[i], _ = strconv.Atoi(subV) } switch k { case "MENUID": // ObjectIdx can be specified in the MENUID. Only seen for .mni files if strings.Contains(v, ",") && len(vSplitInt) >= 2 { r.Id = vSplitInt[0] r.ObjectIdx = vSplitInt[1] } else { r.Id = vInt } case "SUBMENUID": if strings.Contains(v, ",") { log.Printf("%v has an object index in SUBMENUID - surprising", r.Locator()) r.Id = vSplitInt[0] r.ObjectIdx = vSplitInt[1] } else { r.Id = vInt if r.Parent != nil { // Children seem to inherit from parents? r.ObjectIdx = r.Parent.ObjectIdx } } case "MENUTYPE", "SUBMENUTYPE": if strings.Contains(v, ",") { r.Type = MenuType(vSplitInt[0]) // FIXME: what are the other values in this case? } else { r.Type = MenuType(vInt) } // FIXME: Type override. Note that MENUID is specified first, so this works if override, ok := TypeOverrides[r.Locator()]; ok { r.Type = override } case "ACTIVE": r.Active = (vInt != 0) case "SPRITEID": r.SpriteId = vSplitInt case "X-CORD": r.X = vInt case "Y-CORD": r.Y = vInt case "FONTTYPE": r.FontType = vInt case "DRAW TYPE": r.DrawType = vInt case "SHARE": r.Share = vInt default: r.properties[k] = v } } type Replacer interface { ReplaceText(int, *string) ReplaceHelp(int, *string) } func (r *Record) Internationalize(replacer Replacer) { if override, ok := TextOverrides[r.Locator()]; ok { delete(r.properties, "DESC") r.Text = override } if override, ok := DescOverrides[r.Locator()]; ok { r.properties["DESC"] = strconv.Itoa(override) } id, err := strconv.Atoi(r.properties["DESC"]) if err == nil { delete(r.properties, "DESC") replacer.ReplaceText(id, &r.Text) replacer.ReplaceHelp(id, &r.Help) } for _, child := range r.Children { child.Internationalize(replacer) } } func (m *Menu) Internationalize(replacer Replacer) { for _, record := range m.Records { record.Internationalize(replacer) } } func (r *Record) Path() string { var path []string for rec := r; rec != nil; rec = rec.Parent { path = append([]string{strconv.Itoa(rec.Id)}, path...) } return strings.Join(path, ".") } func (r *Record) Locator() string { return fmt.Sprintf("%v:%v", r.Menu.Name, r.Path()) }