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 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 ) // 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]map[string]int{ "main": { "2.6": 50992, }, "newgame": { "2.5": 50993, }, "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]map[string]string{ "main": { "2.7": "0.1-ordoor", }, } type Record struct { Menu *Menu Parent *Record Children []*Record Id int 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 BackgroundColor color.Color HypertextColor color.Color // FIXME: turn these into first-class data Properties map[string]string // 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.Replace(name, filepath.Ext(name), "", -1) name = strings.ToLower(name) // FIXME: this needs turning into a real parser sometime scanner, err := asciiscan.New(filename) if err != nil { return nil, err } defer scanner.Close() var str string var record *Record section := 0 isProp := false out := &Menu{ Name: name, Properties: map[string]string{}, } for { str, err = scanner.ConsumeString() if err != nil { return nil, err } // Whether the lines are properties or not alternate with each section, // except the records use `*` as a separator if section < 3 && isProp != asciiscan.IsProperty(str) { section += 1 isProp = !isProp } if str == "~" { break } switch section { case 0: // List of object files out.ObjectFiles = append(out.ObjectFiles, str) case 1: // List of properties k, v := asciiscan.ConsumeProperty(str) vInt, err := strconv.Atoi(v) // FIXME: switch k { case "BACKGROUND COLOR 0..255..-1 trans": if err != nil { return nil, err } out.BackgroundColor = data.ColorPalette[vInt] case "HYPERTEXT COLOR 0..255": if err != nil { return nil, err } out.HypertextColor = data.ColorPalette[vInt] default: out.Properties[k] = v } case 2: // list of fonts // FIXME: do we need to do something cleverer here? if str == "NULL" { continue } out.FontNames = append(out.FontNames, str) case 3: // Menu records if str == "*" { // NEXT RECORD out.Records = append(out.Records, record.Toplevel()) continue } k, v := asciiscan.ConsumeProperty(str) switch k { case "MENUID": record = newRecord(out, nil) case "SUBMENUID": record = newRecord(out, record.Toplevel()) } setProperty(record, k, v) } } log.Printf("Menu properties: %#+v", out.Properties) return out, 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", "SUBMENUID": r.Id = vInt case "MENUTYPE", "SUBMENUTYPE": r.Type = MenuType(vInt) 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 overrides, ok := TextOverrides[r.Menu.Name]; ok { if override, ok := overrides[r.Path()]; ok { delete(r.properties, "DESC") r.Text = override } } if overrides, ok := DescOverrides[r.Menu.Name]; ok { if override, ok := overrides[r.Path()]; 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, ".") }