From 46925c09d1073753c1a71e0ebfbdc85221a16acf Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Sat, 21 Mar 2020 18:50:26 +0000 Subject: [PATCH] Make the menu buttons work --- cmd/view-menu/main.go | 127 +++++++++++++++++++--------------- cmd/view-obj/main.go | 14 ++-- doc/formats/mnu.md | 61 ++++++++++++++++ internal/assetstore/object.go | 22 ++++-- internal/menus/menus.go | 36 ++++++++++ 5 files changed, 194 insertions(+), 66 deletions(-) diff --git a/cmd/view-menu/main.go b/cmd/view-menu/main.go index 63d5050..67e8521 100644 --- a/cmd/view-menu/main.go +++ b/cmd/view-menu/main.go @@ -120,7 +120,7 @@ func (e *env) Draw(screen *ebiten.Image) error { cam.Scale(scaleX, scaleY) for _, record := range e.menu.Records { - if err := e.drawRecord(record, screen, cam); err != nil { + if err := e.drawRecordRecursive(record, screen, cam); err != nil { return err } } @@ -128,67 +128,82 @@ func (e *env) Draw(screen *ebiten.Image) error { return nil } -func (e *env) drawRecord(record *menus.Record, screen *ebiten.Image, offset ebiten.GeoM) error { - // Draw this record if it's valid to do so. FIXME: lots to learn - if len(record.SpriteId) >= 0 { - spriteId := record.SpriteId[0] - x := float64(record.X) - y := float64(record.Y) - - // Maybe: we either give spriteid, or y,x,spriteId ? Unsure, doesn't seem - // to be needed for now - if len(record.SpriteId) == 3 { - // x = x + float64(record.SpriteId[1]) - // y = y + float64(record.SpriteId[0]) - spriteId = record.SpriteId[2] - } - - // FIXME: some here are set at -1. Presume that means don't draw. - if spriteId < 0 { - goto out - } - - // FIXME: some are set at -1, -1. No idea why. Origin? - if x < 0.0 { - x = 0.0 - } - if y < 0.0 { - y = 0.0 - } - - // FIXME: Need to handle multiple objects - obj := e.objects[0] - sprite, err := obj.Sprite(spriteId) - if err != nil { - return err - } - - x = x + float64(sprite.XOffset) - y = y + float64(sprite.YOffset) - // Account for scaling - x, y = offset.Apply(x, y) - - log.Printf( - "Drawing id=%v type=%v spriteid=%v x=%v(+%v) y=%v(%+v) desc=%q parent=%p", - record.Id, record.Type, spriteId, record.X, record.Y, sprite.XOffset, sprite.YOffset, record.Desc, record.Parent, - ) - - offset.Translate(x, y) - screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: offset}) - - // FIXME: we probably shouldn't draw everything? - // FIXME: handle multiple fonts - // if len(e.fonts) > 0 && record.Desc != "" { - // e.fonts[0].Output(screen, origOffset, record.Desc) - // } +func (e *env) drawRecordRecursive(record *menus.Record, screen *ebiten.Image, geo ebiten.GeoM) error { + if err := e.drawRecord(record, screen, geo); err != nil { + return err } -out: + // Draw all children of this record for _, child := range record.Children { - if err := e.drawRecord(child, screen, offset); err != nil { + if err := e.drawRecordRecursive(child, screen, geo); err != nil { return err } } return nil } + +// If the record has a "share" type, we can work out whether it's +func (e *env) isFocused(record *menus.Record, geo ebiten.GeoM) bool { + if record.Share < 0 { + return false + } + + sprite, err := e.objects[0].Sprite(record.Share) // FIXME: need to handle multiple objects + if err != nil { + return false + } + + invGeo := geo + invGeo.Invert() + + cX, cY := ebiten.CursorPosition() + cursorX, cursorY := invGeo.Apply(float64(cX), float64(cY)) // Undo screen scaling + cursorPoint := image.Pt(int(cursorX), int(cursorY)) + + return cursorPoint.In(sprite.Rect) +} + +func (e *env) drawRecord(record *menus.Record, screen *ebiten.Image, geo ebiten.GeoM) error { + // Draw this record if it's valid to do so. FIXME: lots to learn + + spriteId := record.SelectSprite( + e.step/2, + ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft), + e.isFocused(record, geo), + ) + + if spriteId < 0 { + return nil + } + + // X-CORD and Y-CORD are universally either 0 or -1, so ignore here. + // TODO: maybe 0 overrides in-sprite offset (set below)? + + // FIXME: Need to handle multiple objects + obj := e.objects[0] + sprite, err := obj.Sprite(spriteId) + if err != nil { + return err + } + + // Account for scaling, draw sprite at its specified offset + x, y := geo.Apply(float64(sprite.XOffset), float64(sprite.YOffset)) + + // log.Printf( + // "Drawing id=%v type=%v spriteid=%v x=%v(+%v) y=%v(%+v) desc=%q parent=%p", + // record.Id, record.Type, spriteId, record.X, record.Y, sprite.XOffset, sprite.YOffset, record.Desc, record.Parent, + // ) + + geo.Translate(x, y) + + screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: geo}) + + // FIXME: we probably shouldn't draw everything? + // FIXME: handle multiple fonts + // if len(e.fonts) > 0 && record.Desc != "" { + // e.fonts[0].Output(screen, origOffset, record.Desc) + // } + + return nil +} diff --git a/cmd/view-obj/main.go b/cmd/view-obj/main.go index d086694..fc9ffb4 100644 --- a/cmd/view-obj/main.go +++ b/cmd/view-obj/main.go @@ -15,7 +15,8 @@ import ( var ( gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation") - objName = flag.String("obj", "", "Name of an .obj file, e.g. TZEENTCH") + objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ") + objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH") ) type env struct { @@ -36,7 +37,7 @@ type state struct { func main() { flag.Parse() - if *gamePath == "" || *objName == "" { + if *gamePath == "" || (*objName == "" && *objFile == "") { flag.Usage() os.Exit(1) } @@ -46,9 +47,14 @@ func main() { log.Fatal("Failed to set up asset store: %v", err) } - obj, err := assets.Object(*objName) + var obj *assetstore.Object + if *objName != "" { + obj, err = assets.Object(*objName) + } else { + obj, err = assets.ObjectByPath(*objFile) + } if err != nil { - log.Fatalf("Failed to load %s: %v", *objName, err) + log.Fatalf("Failed to load %s%s: %v", *objName, *objFile, err) } state := state{ diff --git a/doc/formats/mnu.md b/doc/formats/mnu.md index ea0d0ae..d94a49e 100644 --- a/doc/formats/mnu.md +++ b/doc/formats/mnu.md @@ -201,3 +201,64 @@ This seems to be done by choosing a different sprite to draw every N ticks. They are laid out sequentially, but I don't yet know how to animate them. It's likely to be the same approach as used for other obj files. +Looking at Main.mnu, it points at the object fail Main.obj. This has 118 +sprites, which can be described as follows: + +| Start | Count | Desc | +| ------ | ----- | ---- | +| 0 | 1 | Background image | +| 1 | 3 | New game button: base, pressed, disabled | +| 4 | 3 | Load game button: base, pressed, disabled | +| 7 | 3 | Multiplayer button: base, pressed, disabled | +| 10 | 3 | Settings button: base, pressed, disabled | +| 13 | 3 | Quit button: base, pressed, disabled | +| 16 | 20 | New game button: 20 animation frames | +| 36 | 20 | Load game button: 20 animation frames | +| 56 | 20 | Multiplayer button: 20 animation frames | +| 76 | 20 | Settings button: 20 animation frames | +| 96 | 20 | Quit button: 20 animation frames | +| 116 | 1 | Section of background ("Menu title") | +| 117 | 1 | Version hotspot | + +So we have 5 buttons with very similar characteristics, but at different sprite +offsets, and two distinct ranges per button, plus some others. Here's some +attributes plucked from `Main.mnu`: + +| Name | (SUB)MENUTYPE | "Active" | "SPRITEID" | "DRAW TYPE" | "SHARE" | +| ---------- | ------------- | -------- | ---------- | ----------- | ------- | +| Background | 1 | 1 | 0 | 0 | -1 | +| Start menu | 1 | 1 | -1 | 0 | -1 | +| New game | 228 | 1,0 | 16,-1,1 | 20 | 1 | +| Load game | 228 | 1,0 | 36,-1,4 | 20 | 4 | +| MP game | 228 | 1,0 | 56,-1,7 | 20 | 7 | +| Options | 228 | 1,0 | 76,-1,10 | 20 | 10 | +| Quit | 228 | 1,0 | 96,-1,13 | 20 | 13 | +| Menu title | 61 | 1 | -1 | 0 | 116 | +| V hotspot | 61 | 1 | -1 | 0 | 117 | + +The buttons, menu title and version hotspot are submenus of the start menu. + +### `MENUTYPE` + +This is the only menu where we see a type of 228. ~750 other unique values are +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`. + +### `ACTIVE` + +There are only 4 values seen across all menus: `0`, `1`, `1,0`, `102` and `1,1`. +Perhaps this represents possible states? + +### Sprite selection + +For the background (`MENUTYPE: 1`), this points simply at the sprite index in +the object file. For the start menu, it's `-1` (no sprite, I assume). For the +menu title and version hotspot (`MENUTYPE: 61`, it's `-1` too. + +For the buttons, it's a list pointing to the start of the 20 animated frames, +`-1`, then the start of the 3 static frames. + +`DRAW TYPE` is the number of animated frames. We only use the animated frames +when the button is focused. `SHARE` repeats the start of the static frames, and +is the only place they're found for the menu title and version hotspot. diff --git a/internal/assetstore/object.go b/internal/assetstore/object.go index cb67498..fdc1f24 100644 --- a/internal/assetstore/object.go +++ b/internal/assetstore/object.go @@ -2,6 +2,7 @@ package assetstore import ( "fmt" + "image" "log" "path/filepath" "strings" @@ -23,12 +24,13 @@ type Object struct { type Sprite struct { obj *Object - ID string - XOffset int + XOffset int // TODO: replace these everywhere with Rect YOffset int Width int Height int + ID string + Rect image.Rectangle Image *ebiten.Image } @@ -106,13 +108,21 @@ func (o *Object) Sprite(idx int) (*Sprite, error) { return nil, err } + rect := image.Rect( + int(raw.XOffset), + int(raw.YOffset), + int(raw.XOffset+raw.Width), + int(raw.YOffset+raw.Height), + ) + sprite := &Sprite{ ID: fmt.Sprintf("%v:%v", o.raw.Name, idx), obj: o, - Width: int(raw.Width), - Height: int(raw.Width), - XOffset: int(raw.XOffset), - YOffset: int(raw.YOffset), + Width: rect.Dx(), + Height: rect.Dy(), + XOffset: rect.Min.X, + YOffset: rect.Min.Y, + Rect: rect, Image: img, } diff --git a/internal/menus/menus.go b/internal/menus/menus.go index 0b549ae..94e9a44 100644 --- a/internal/menus/menus.go +++ b/internal/menus/menus.go @@ -10,15 +10,24 @@ import ( "code.ur.gs/lupine/ordoor/internal/util/asciiscan" ) +const ( + TypeStatic = 0 + TypeMenu = 1 + TypeOverlay = 61 + TypeMainButton = 228 +) + type Record struct { Parent *Record Children []*Record Id int Type int + DrawType int FontType int Active bool SpriteId []int + Share int X int Y int Desc string @@ -161,6 +170,29 @@ func (r *Record) Toplevel() *Record { return r } +func (r *Record) SelectSprite(step int, pressed, focused bool) int { + switch r.Type { + case TypeStatic: + return r.SpriteId[0] + case TypeMenu: + return r.SpriteId[0] // Probably -1 + case TypeOverlay: + return r.Share + case TypeMainButton: + // A main button has 4 states: unfocused, focused (animated), mousedown, disabled + if focused && pressed { + return r.Share + 1 + } else if focused { + return r.SpriteId[0] + (step % r.DrawType) + } + + // TODO: disabled + return r.Share + } + + return -1 +} + func setProperty(r *Record, k, v string) { vSplit := strings.Split(v, ",") vInt, _ := strconv.Atoi(v) @@ -187,6 +219,10 @@ func setProperty(r *Record, k, v string) { r.Desc = v case "FONTTYPE": r.FontType = vInt + case "DRAW TYPE": + r.DrawType = vInt + case "SHARE": + r.Share = vInt default: r.properties[k] = v }