From 7935f78acc17825118a8e1af0346b266157ab403 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 1 Apr 2020 01:38:42 +0100 Subject: [PATCH] Add a partial listbox implementation --- internal/menus/menus.go | 72 ++++++++-------- internal/ui/buttons.go | 14 +-- internal/ui/driver.go | 5 -- internal/ui/list_box.go | 158 ++++++++++++++++++++++++++++++++++ internal/ui/menus.go | 36 +++++++- internal/ui/noninteractive.go | 9 +- 6 files changed, 241 insertions(+), 53 deletions(-) create mode 100644 internal/ui/list_box.go diff --git a/internal/menus/menus.go b/internal/menus/menus.go index 67dd138..a6ac4f3 100644 --- a/internal/menus/menus.go +++ b/internal/menus/menus.go @@ -41,44 +41,38 @@ const ( 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]map[string]int{ - "main": { - "2.6": 50992, - }, - "newgame": { - "2.5": 50993, - }, - "levelply": { - "2.6": 50996, - }, +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]map[string]string{ - "main": { - "2.7": "0.1-ordoor", - }, +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. -var TypeOverrides = map[string]map[string]MenuType{ - "levelply": { - "2": TypeMenu, - }, - "savegame": { - "2": TypeMenu, - }, - "loadgame": { - "2": TypeMenu, - }, +var TypeOverrides = map[string]MenuType{ + "levelply:2": TypeMenu, + "savegame:2": TypeMenu, + "loadgame:2": TypeMenu, + + // ??? + "configure_ultequip:7.5": TypeListBoxUp, + "configure_ultequip:7.6": TypeListBoxDown, } type Record struct { @@ -275,13 +269,15 @@ func setProperty(r *Record, k, v string) { case "MENUID", "SUBMENUID": r.Id = vInt case "MENUTYPE", "SUBMENUTYPE": - r.Type = MenuType(vInt) + 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 overrides, ok := TypeOverrides[r.Menu.Name]; ok { - if newType, ok := overrides[r.Path()]; ok { - r.Type = newType - } + if override, ok := TypeOverrides[r.Locator()]; ok { + r.Type = override } case "ACTIVE": r.Active = (vInt != 0) @@ -308,17 +304,13 @@ type Replacer interface { } 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 override, ok := TextOverrides[r.Locator()]; 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) - } + if override, ok := DescOverrides[r.Locator()]; ok { + r.properties["DESC"] = strconv.Itoa(override) } id, err := strconv.Atoi(r.properties["DESC"]) @@ -348,3 +340,7 @@ func (r *Record) Path() string { return strings.Join(path, ".") } + +func (r *Record) Locator() string { + return fmt.Sprintf("%v:%v", r.Menu.Name, r.Path()) +} diff --git a/internal/ui/buttons.go b/internal/ui/buttons.go index 83ac746..e0c3e5c 100644 --- a/internal/ui/buttons.go +++ b/internal/ui/buttons.go @@ -39,11 +39,15 @@ type mainButton struct { } func registerSimpleButton(d *Driver, r *menus.Record) error { - return registerButton(d, r, r.SpriteId[0]) + _, err := registerButton(d, r, r.SpriteId[0]) + + return err } func registerInvokeButton(d *Driver, r *menus.Record) error { - return registerButton(d, r, r.Share) + _, err := registerButton(d, r, r.Share) + + return err } func registerMainButton(d *Driver, r *menus.Record) error { @@ -99,10 +103,10 @@ func registerDoorHotspot(d *Driver, r *menus.Record) error { } -func registerButton(d *Driver, r *menus.Record, spriteId int) error { +func registerButton(d *Driver, r *menus.Record, spriteId int) (*button, error) { sprites, err := d.menu.Sprites(spriteId, 3) // base, pressed, disabled if err != nil { - return err + return nil, err } btn := &button{ @@ -118,7 +122,7 @@ func registerButton(d *Driver, r *menus.Record, spriteId int) error { d.hoverables = append(d.hoverables, btn) d.paintables = append(d.paintables, btn) - return nil + return btn, nil } func (b *button) id() string { diff --git a/internal/ui/driver.go b/internal/ui/driver.go index 0649e60..c1eb936 100644 --- a/internal/ui/driver.go +++ b/internal/ui/driver.go @@ -18,14 +18,9 @@ func init() { // FIXME: these need implementing // Needed for Keyboard.mnu (main -> options -> keyboard) - registerBuilder(menus.TypeLineKbd, registerDebug("Unimplemented LineKbd", nil)) registerBuilder(menus.TypeDialogue, registerDebug("Unimplemented Dialogue", nil)) - // Needed for Briefing.mnu - registerBuilder(menus.TypeLineBriefing, registerDebug("Unimplemented LineBriefing", nil)) - // Needed for ChaEquip.mnu - registerBuilder(menus.TypeThumb, registerDebug("Unimplemented Thumb", nil)) // Needed for MainGameChaos.mnu registerBuilder(menus.TypeStatusBar, registerDebug("Unimplemented StatusBar", nil)) diff --git a/internal/ui/list_box.go b/internal/ui/list_box.go new file mode 100644 index 0000000..756feb9 --- /dev/null +++ b/internal/ui/list_box.go @@ -0,0 +1,158 @@ +package ui + +import ( + "fmt" + "log" + + "code.ur.gs/lupine/ordoor/internal/menus" +) + +func init() { + registerBuilder(menus.TypeLineKbd, ownedByMenu) + registerBuilder(menus.TypeLineBriefing, ownedByMenu) + + registerBuilder(menus.TypeThumb, ownedByMenu) + registerBuilder(menus.TypeListBoxUp, ownedByMenu) + registerBuilder(menus.TypeListBoxDown, ownedByMenu) +} + +// listBox is a TListBox in VCL terms. It has a number of lines of text, one of +// which may be selected, and a slider with up and down buttons to scroll if the +// options in the box exceed its viewing capacity. +// +// TODO: multi-select functionality? Is it needed? +type listBox struct { + upBtn *button + downBtn *button + + // TODO: can we reuse the slider element for the thumb? + + base *noninteractive // The menu itself has a sprite to display + lines []*noninteractive // We display to these + + // The list box acts as a window onto these + strings []string + + // The start of our window + offset int +} + +func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { + var upBtn *menus.Record + var downBtn *menus.Record + var slider *menus.Record + var items []*menus.Record + + for _, rec := range menu.Children { + switch rec.Type { + case menus.TypeListBoxUp: + if upBtn != nil { + return nil, fmt.Errorf("Duplicate up buttons in menu %v", menu.Locator()) + } + upBtn = rec + case menus.TypeListBoxDown: + if downBtn != nil { + return nil, fmt.Errorf("Duplicate down buttons in menu %v", menu.Locator()) + } + downBtn = rec + case menus.TypeLineKbd, menus.TypeLineBriefing: + items = append(items, rec) + case menus.TypeThumb: + if slider != nil { + return nil, fmt.Errorf("Duplicate thumbs in menu %v", menu.Locator()) + } + slider = rec + default: + return nil, fmt.Errorf("Unrecognised child in listbox menu: %v", rec.Locator()) + } + } + + if len(items) == 0 || slider == nil || upBtn == nil || downBtn == nil { + return nil, fmt.Errorf("Missing items in menu %v", menu.Locator()) + } + + // Now build the wonderful thing + elemUp, err := registerButton(d, upBtn, upBtn.SpriteId[0]) + if err != nil { + return nil, err + } + + elemDown, err := registerButton(d, downBtn, downBtn.SpriteId[0]) + if err != nil { + return nil, err + } + + element := &listBox{ + upBtn: elemUp, + downBtn: elemDown, + } + + for _, rec := range items { + ni, err := registerNoninteractive(d, rec) + if err != nil { + return nil, err + } + + element.lines = append(element.lines, ni) + } + + // Internal wiring-up + elemUp.onClick(element.up) + elemDown.onClick(element.down) + + // FIXME: Test data for now + for i := 0; i < 50; i++ { + element.strings = append(element.strings, fmt.Sprintf("FOO %v", i)) + } + + // Register everything + // FIXME: we should be able to freeze/unfreeze as a group. The buttons register themselves though... + + return nil, nil +} + +func (l *listBox) SetStrings(to []string) { + if len(to) < len(l.strings) { + l.offset = 0 // FIXME: unconditional? Trim to max? + } + + l.strings = to + + l.refresh() +} + +// TODO: Selected returns the index and value of the selected item +func (l *listBox) Selected() (int, string) { + return 0, "" +} + +func (l *listBox) up() { + if l.offset <= 0 { + return + } + + l.offset -= 1 + l.refresh() +} + +func (l *listBox) down() { + if l.offset > len(l.strings)-len(l.lines) { + return + } + + l.offset += 1 + l.refresh() +} + +func (l *listBox) refresh() { + for i, ni := range l.lines { + // FIXME: noninteractive isn't set up for dynamic text yet. Need to + // generate textImg on demand instead of once at start. + ni.hoverImpl.text = "" + if len(l.strings) > l.offset+i { + ni.hoverImpl.text = l.strings[l.offset+i] + } + } + + log.Printf("Listbox offset: %v", l.offset) +} diff --git a/internal/ui/menus.go b/internal/ui/menus.go index 434ae00..f0f1743 100644 --- a/internal/ui/menus.go +++ b/internal/ui/menus.go @@ -11,11 +11,42 @@ func init() { } func registerMenu(d *Driver, r *menus.Record) ([]*menus.Record, error) { - // Group all inventory selects that share a menu together + childrenLeft, err := listBoxFromMenu(d, r, r.Children) + if err != nil { + return nil, err + } + + childrenLeft, err = inventorySelectFromMenu(d, r, childrenLeft) + if err != nil { + return nil, err + } + + // Return all the unhandled children to be processed further + return childrenLeft, nil +} + +func listBoxFromMenu(d *Driver, menu *menus.Record, children []*menus.Record) ([]*menus.Record, error) { + ok := false + for _, rec := range children { + if rec.Type == menus.TypeThumb { // FIXME: we're using this to indicate a listbox + ok = true + break + } + } + + if !ok { + return children, nil + } + + return registerListBox(d, menu) +} + +// Group all inventory selects that share a menu together +func inventorySelectFromMenu(d *Driver, menu *menus.Record, children []*menus.Record) ([]*menus.Record, error) { var childrenLeft []*menus.Record var inventorySelects []*inventorySelect - for _, child := range r.Children { + for _, child := range children { switch child.Type { case menus.TypeInventorySelect: is, err := registerInventorySelect(d, child) @@ -37,6 +68,5 @@ func registerMenu(d *Driver, r *menus.Record) ([]*menus.Record, error) { } } - // Return all the unhandled children to be processed further return childrenLeft, nil } diff --git a/internal/ui/noninteractive.go b/internal/ui/noninteractive.go index 4067e3e..262bdf9 100644 --- a/internal/ui/noninteractive.go +++ b/internal/ui/noninteractive.go @@ -46,6 +46,11 @@ type animationHover struct { } func registerStatic(d *Driver, r *menus.Record) error { + _, err := registerNoninteractive(d, r) + return err +} + +func registerNoninteractive(d *Driver, r *menus.Record) (*noninteractive, error) { // FIXME: SpriteID takes precedence over SHARE if present, but is that right? spriteId := r.Share if len(r.SpriteId) > 0 && r.SpriteId[0] != -1 { @@ -54,7 +59,7 @@ func registerStatic(d *Driver, r *menus.Record) error { sprite, err := d.menu.Sprite(spriteId) if err != nil { - return err + return nil, err } ni := &noninteractive{ @@ -67,7 +72,7 @@ func registerStatic(d *Driver, r *menus.Record) error { d.hoverables = append(d.hoverables, ni) d.paintables = append(d.paintables, ni) - return nil + return ni, nil } func registerHypertext(d *Driver, r *menus.Record) error {