diff --git a/doc/formats/mnu.md b/doc/formats/mnu.md index 02690a8..03f925d 100644 --- a/doc/formats/mnu.md +++ b/doc/formats/mnu.md @@ -245,13 +245,14 @@ observed, suggesting structure. For instance, we have `24`, `240`, `241` and `2410`, but not `2411` or `2409`. Sometimes we have a comma-separated list, e.g.: `400,30,-1,5`. -A listing of currently-known values: +A listing of some currently-known values: | Value | Type | | ----- | ---------------- | | 0 | Static image | | 1 | Menu | | 3 | Button | +| 45 | Thumb | | 50 | Invoke? Button? | | 61 | "Overlay" | | 70 | "Hypertext" | @@ -260,6 +261,13 @@ A listing of currently-known values: | 228 | Main menu button | | 232 | Slider | +Hypothesis: `MENUTYPE` and `SUBMENUTYPE` are actually distinct lists of values. +So far, I've been treating them as the same thing, but, e.g., `MainGame.mnu` has +a `MENUTYPE: 45` which is labelled "MAIN BACKGROUND", while `SUBMENUTYPE: 45` +is tentatively labelled a "thumb" and used in text boxes. There are also a few +cases where I've had to manually override the `MENUTYPE` because it coincides +with `Button`. + ### `ACTIVE` There are only 4 values seen across all menus: `0`, `1`, `1,0`, `102` and `1,1`. diff --git a/internal/flow/bridge.go b/internal/flow/bridge.go index 0a7884b..4a94ed4 100644 --- a/internal/flow/bridge.go +++ b/internal/flow/bridge.go @@ -4,15 +4,16 @@ func (f *Flow) linkBridge() { // FIXME: sometimes these doors are frozen, depending on ship state, but we // don't implement that yet. - f.onClick(bridge, "2.1", f.setDriver(briefing)) // Mission briefing clickable - f.onClick(bridge, "2.2", f.setDriver(choices)) // Options door hotspot - f.onClick(bridge, "2.4", f.playNextScenario()) // Enter combat door hotspot - f.setFreeze(bridge, "2.6", true) // TODO: Vehicle configure door hotspot - f.onClick(bridge, "2.8", f.setDriver(arrange)) // Squads configure door hotspot + f.onClick(bridge, "2.1", f.setDriver(briefing)) // Mission briefing clickable + f.onClick(bridge, "2.2", f.setDriver(choices)) // Options door hotspot + f.onClick(bridge, "2.4", f.playNextScenario(bridge)) // Enter combat door hotspot + f.setFreeze(bridge, "2.6", true) // TODO: Vehicle configure door hotspot + f.onClick(bridge, "2.8", f.setDriver(arrange)) // Squads configure door hotspot // link children f.linkBriefing() f.linkChoices() + f.linkMainGame() f.linkArrange() } diff --git a/internal/flow/drivers.go b/internal/flow/drivers.go new file mode 100644 index 0000000..397f4f2 --- /dev/null +++ b/internal/flow/drivers.go @@ -0,0 +1,74 @@ +package flow + +import ( + "fmt" +) + +type driverName string + +const ( + // Names of all the drivers + main driverName = "Main" + levelPly driverName = "LevelPly" + singles driverName = "Singles" + randomMap driverName = "RandomMap" + newGame driverName = "NewGame" + loadGame driverName = "LoadGame" + options driverName = "Options" + kbd driverName = "Keyboard" + bridge driverName = "Bridge" + briefing driverName = "Briefing" + choices driverName = "Choices" + saveGame driverName = "SaveGame" + credits driverName = "Credits" + arrange driverName = "Arrange" + + configureUltEquip driverName = "Configure_UltEquip" + configureVehiclesUltra driverName = "Configure_Vehicles_Ultra" + + mainGame driverName = "MainGame" +) + +var ( + driverNames = []driverName{ + main, levelPly, singles, randomMap, newGame, loadGame, options, kbd, + bridge, briefing, choices, saveGame, credits, arrange, + configureUltEquip, configureVehiclesUltra, + mainGame, + } +) + +// from is the child menu, to is the parent +func (f *Flow) returnToLastDriverNow(from driverName) error { + to, ok := f.returns[from] + if !ok { + return fmt.Errorf("Couldn't work out where to return to from %v", from) + } + + delete(f.returns, from) + + f.setDriverNow(to) + return nil +} + +func (f *Flow) setDriver(name driverName) func() { + return func() { + f.setDriverNow(name) + } +} + +func (f *Flow) setDriverNow(name driverName) { + f.current = f.drivers[name] +} + +// from is the parent menu, to is the child +func (f *Flow) setReturningDriver(from, to driverName) func() { + return func() { + f.setReturningDriverNow(from, to) + } +} + +func (f *Flow) setReturningDriverNow(from, to driverName) { + f.returns[to] = from + f.setDriverNow(to) +} diff --git a/internal/flow/flow.go b/internal/flow/flow.go index bdeb9c3..b005dc9 100644 --- a/internal/flow/flow.go +++ b/internal/flow/flow.go @@ -40,38 +40,9 @@ type Flow struct { exit error } -type driverName string - -const ( - // Names of all the drivers - main driverName = "Main" - levelPly driverName = "LevelPly" - singles driverName = "Singles" - randomMap driverName = "RandomMap" - newGame driverName = "NewGame" - loadGame driverName = "LoadGame" - options driverName = "Options" - kbd driverName = "Keyboard" - bridge driverName = "Bridge" - briefing driverName = "Briefing" - choices driverName = "Choices" - saveGame driverName = "SaveGame" - credits driverName = "Credits" - arrange driverName = "Arrange" - - configureUltEquip driverName = "Configure_UltEquip" - configureVehiclesUltra driverName = "Configure_Vehicles_Ultra" -) - var ( ErrExit = errors.New("exiting gracefully") - driverNames = []driverName{ - main, levelPly, singles, randomMap, newGame, loadGame, options, kbd, - bridge, briefing, choices, saveGame, credits, arrange, - configureUltEquip, configureVehiclesUltra, - } - // Constants used for sliders h3Slider = map[int]int{1: 8, 2: 56, 3: 110, 4: 120} @@ -245,25 +216,7 @@ func (f *Flow) setValueBool(driver driverName, id string, value bool) { f.exit = f.drivers[driver].SetValueBool(id, value) } -func (f *Flow) setDriver(name driverName) func() { - return func() { - f.setDriverNow(name) - } -} - -func (f *Flow) setDriverNow(name driverName) { - f.current = f.drivers[name] -} - -// from is the parent menu, to is the child -func (f *Flow) setReturningDriver(from, to driverName) func() { - return func() { - f.returns[to] = from - f.setDriverNow(to) - } -} - -func (f *Flow) playNextScenario() func() { +func (f *Flow) playNextScenario(from driverName) func() { return func() { log.Printf("Loading scenario: %v", f.ship.NextScenario) @@ -275,24 +228,11 @@ func (f *Flow) playNextScenario() func() { return } - f.current = nil // TODO: show the UI for a scenario + f.setReturningDriverNow(from, mainGame) f.scenario = scenario } } -// from is the child menu, to is the parent -func (f *Flow) returnToLastDriverNow(from driverName) error { - to, ok := f.returns[from] - if !ok { - return fmt.Errorf("Couldn't work out where to return to from %v", from) - } - - delete(f.returns, from) - - f.setDriverNow(to) - return nil -} - func (f *Flow) setExit() { f.exit = ErrExit } diff --git a/internal/flow/main_game.go b/internal/flow/main_game.go new file mode 100644 index 0000000..3e237ed --- /dev/null +++ b/internal/flow/main_game.go @@ -0,0 +1,55 @@ +package flow + +// TODO: There are Chaos and Ultramarine versions of MainGame. Do we really want +// to duplicate everything for both? + +func (f *Flow) linkMainGame() { + // 3: Action menu + + // 4: Interface options menu + f.onClick(mainGame, "4.1", f.setReturningDriver(mainGame, options)) // Options button + // 4.2: Map button + // 4.3: Mission objectives button + // 4.4: Inventory + // 4.5: Next man + // 4.6: Next enemy + // 4.7: Total enemy text + + // 5: Holding menu + // 6: View menu + // 7: General character menu + // 8: Character stats + // 9: Visible enemy menu + // 10: Friendly squad menu + // 11: Psyker spell dialogue + + // FIXME: lots and lots and lots of wiring up to do. + // For now, just link all the exit buttons to go back to the bridge + f.onClick(mainGame, "11.6", func() { + f.scenario = nil + f.returnToLastDriverNow(mainGame) + }) + + // 12: Inventory dialogue + f.onClick(mainGame, "12.21", func() { + f.scenario = nil + f.returnToLastDriverNow(mainGame) + }) + + // 13: Exchange menu + + f.onClick(mainGame, "13.1", func() { + f.scenario = nil + f.returnToLastDriverNow(mainGame) + }) + + // 14: Map + + // 15: Interface wing left + // 16: Interface wing right + // 17: Grenade dialogue + // 18: Info dialogue + // 19: Turn start dialogue + // 20: Chat menu + // 21: Chat list menu box +} diff --git a/internal/menus/menus.go b/internal/menus/menus.go index a6ac4f3..bd30c1d 100644 --- a/internal/menus/menus.go +++ b/internal/menus/menus.go @@ -65,11 +65,16 @@ var TextOverrides = map[string]string{ // 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, diff --git a/internal/ui/driver.go b/internal/ui/driver.go index 19c3989..d8b7691 100644 --- a/internal/ui/driver.go +++ b/internal/ui/driver.go @@ -327,7 +327,7 @@ func (d *Driver) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) { } func (d *Driver) addRecord(record *menus.Record) error { - //log.Printf("Adding record: %#+v", record) + log.Printf("Adding record %v: %#+v", record.Locator(), record) children := record.Children handler, ok := widgetBuilders[record.Type] diff --git a/internal/ui/list_box.go b/internal/ui/list_box.go index c50c1ce..5abf449 100644 --- a/internal/ui/list_box.go +++ b/internal/ui/list_box.go @@ -45,6 +45,7 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { var downBtn *menus.Record var thumb *menus.Record var items []*menus.Record + var otherChildren []*menus.Record for _, rec := range menu.Children { switch rec.Type { @@ -66,7 +67,8 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { } thumb = rec default: - return nil, fmt.Errorf("Unrecognised child in listbox menu: %v", rec.Locator()) + // e.g. maingame:18.12 includes a button that is not part of the box + otherChildren = append(otherChildren, rec) } } @@ -80,12 +82,21 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { return nil, err } - elemUp, err := registerButton(d, upBtn, upBtn.SpriteId[0]) + upSprId := upBtn.SpriteId[0] + if upSprId == -1 { + upSprId = upBtn.Share + } + + elemUp, err := registerButton(d, upBtn, upSprId) if err != nil { return nil, err } - elemDown, err := registerButton(d, downBtn, downBtn.SpriteId[0]) + dnSprId := downBtn.SpriteId[0] + if dnSprId == -1 { + dnSprId = downBtn.Share + } + elemDown, err := registerButton(d, downBtn, dnSprId) if err != nil { return nil, err } @@ -95,7 +106,12 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { return nil, err } - thumbImgSpr, err := d.menu.Sprite(thumb.SpriteId[0]) + thumbSprId := thumb.SpriteId[0] + if thumbSprId == -1 { + thumbSprId = thumb.Share + } + + thumbImgSpr, err := d.menu.Sprite(thumbSprId) if err != nil { return nil, err } @@ -145,7 +161,7 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { element.refresh() - return nil, nil + return otherChildren, nil } func (l *listBox) SetStrings(to []string) { diff --git a/internal/ui/noninteractive.go b/internal/ui/noninteractive.go index 38021e9..ec5767f 100644 --- a/internal/ui/noninteractive.go +++ b/internal/ui/noninteractive.go @@ -16,7 +16,7 @@ const ( ) func init() { - registerBuilder(menus.TypeStatic, noChildren(registerStatic)) + registerBuilder(menus.TypeStatic, registerStatic) // MainGame has a hypertext child registerBuilder(menus.TypeHypertext, noChildren(registerHypertext)) registerBuilder(menus.TypeOverlay, noChildren(registerOverlay)) registerBuilder(menus.TypeAnimationSample, noChildren(registerAnimation)) @@ -58,9 +58,9 @@ type animationHover struct { closing bool } -func registerStatic(d *Driver, r *menus.Record) error { +func registerStatic(d *Driver, r *menus.Record) ([]*menus.Record, error) { _, err := registerNoninteractive(d, r) - return err + return r.Children, err } func registerNoninteractive(d *Driver, r *menus.Record) (*noninteractive, error) {