diff --git a/cmd/view-map/main.go b/cmd/view-map/main.go index b1239e8..d076e49 100644 --- a/cmd/view-map/main.go +++ b/cmd/view-map/main.go @@ -156,7 +156,7 @@ func (e *env) Draw(screen *ebiten.Image) error { } func (e *env) renderCell(x, y, z int, screen *ebiten.Image) error { - images, err := e.area.ImagesForCell(x, y, z) + sprites, err := e.area.SpritesForCell(x, y, z) if err != nil { return err } @@ -177,10 +177,12 @@ func (e *env) renderCell(x, y, z int, screen *ebiten.Image) error { // TODO: iso.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor - for _, img := range images { - if err := screen.DrawImage(img, &ebiten.DrawImageOptions{GeoM: iso}); err != nil { + for _, spr := range sprites { + iso.Translate(float64(spr.XOffset), float64(spr.YOffset)) + if err := screen.DrawImage(spr.Image, &ebiten.DrawImageOptions{GeoM: iso}); err != nil { return err } + iso.Translate(float64(-spr.XOffset), float64(-spr.YOffset)) } return nil diff --git a/cmd/view-menu/main.go b/cmd/view-menu/main.go index 81d9704..6d6d0eb 100644 --- a/cmd/view-menu/main.go +++ b/cmd/view-menu/main.go @@ -164,10 +164,11 @@ func (e *env) drawRecord(record *menus.Record, screen *ebiten.Image, offset ebit x := float64(record.X) y := float64(record.Y) - // Theory: we either give spriteid, or y,x,spriteId + // 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]*2) // FIXME: *2 works, no idea + // x = x + float64(record.SpriteId[1]) + // y = y + float64(record.SpriteId[0]) spriteId = record.SpriteId[2] } @@ -184,15 +185,21 @@ func (e *env) drawRecord(record *menus.Record, screen *ebiten.Image, offset ebit y = 0.0 } - log.Printf( - "Drawing id=%v type=%v spriteid=%v x=%v y=%v desc=%q parent=%p", - record.Id, record.Type, spriteId, record.X, record.Y, record.Desc, record.Parent, - ) - // FIXME: Need to handle multiple objects obj := e.objects[0] sprite := obj.Sprites[spriteId] + + 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}) diff --git a/doc/formats/img/Options.mnu.png b/doc/formats/img/Options.mnu.png new file mode 100644 index 0000000..9d893be Binary files /dev/null and b/doc/formats/img/Options.mnu.png differ diff --git a/doc/formats/mnu.md b/doc/formats/mnu.md index 07d351f..ea0d0ae 100644 --- a/doc/formats/mnu.md +++ b/doc/formats/mnu.md @@ -183,3 +183,21 @@ values: | 405,0,0,{8, 16} | ? | | 405,22,22,{2, 4, 5, 6, 7, 8, 9, 10, 13, 16} | ? | | 405,30,-1,5 | ? | + +## Positioning + +The X-CORD and Y-CORD values would seem to be related, to this, but they are +universally set to 0 or -1. + +Far more important are the XOffset and YOffset values for each sprite in the +associated .obj files. Taking these into account is enough to draw `Options.mnu` +successfully, for instance: + +![](img/Options.mnu.png) + +## Animation + +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. + diff --git a/doc/formats/obj.md b/doc/formats/obj.md index 4948cdd..dc5ab6c 100644 --- a/doc/formats/obj.md +++ b/doc/formats/obj.md @@ -202,15 +202,13 @@ in the CENTER position. Interesting. | Offset | Purpose | | ------ | ------- | -| 0x0000 | ? +| 0x0000 | x,y offset (16 bits each) | | 0x0004 | x,y size (16 bits each) | | 0x0008 | ? (blank in all cases so far) | 0x000c | Size of remaining pixeldata | | 0x0010 | Padding? | | 0x0014 | Padding? | -We still don't know what the first 32 bits are all about. - The volume represented by a cell is a little odd. We see three faces of a fake 3D volume of size 64x64x32(ish). This is presented in an isomorphic fashion, so the height is 32px at the leftmost and rightmost extents and 96 in the centre. @@ -222,8 +220,9 @@ for instance. Loading a constructed `palette.obj` containing 1x63 pixels, with 0x0000 for the first 32 bits, caused WH40K_TD.exe to render the pixels far to the left and above -the cell the palette objects were being placed into. This suggests they -represent an x,y offset to draw to. Need to experiment more. +the cell the palette objects were being placed into. Menus have a `.obj` file +associated with them too, and the elements for `Main.mnu` draw in the correct +places if we treat it as such. Considering sprites with a 1,1 x,y. diff --git a/internal/assetstore/map.go b/internal/assetstore/map.go index 0df3023..b607590 100644 --- a/internal/assetstore/map.go +++ b/internal/assetstore/map.go @@ -4,8 +4,6 @@ import ( "image" "log" - "github.com/hajimehoshi/ebiten" - "code.ur.gs/lupine/ordoor/internal/maps" ) @@ -64,11 +62,11 @@ func (a *AssetStore) Map(name string) (*Map, error) { return m, nil } -// ImagesForCell returns the sprites needed to correctly render this cell. +// SpritesForCell returns the sprites needed to correctly render this cell. // They should be rendered from first to last to get the correct ordering -func (m *Map) ImagesForCell(x, y, z int) ([]*ebiten.Image, error) { +func (m *Map) SpritesForCell(x, y, z int) ([]*Sprite, error) { cell := m.raw.At(x, y, z) - images := make([]*ebiten.Image, 0, 4) + sprites := make([]*Sprite, 0, 4) for _, ref := range []maps.ObjRef{cell.Surface, cell.Right, cell.Left, cell.Center} { if !ref.IsActive() { @@ -80,13 +78,13 @@ func (m *Map) ImagesForCell(x, y, z int) ([]*ebiten.Image, error) { return nil, err } - img, err := obj.Image(ref.Sprite()) + sprite, err := obj.Sprite(ref.Sprite()) if err != nil { return nil, err } - images = append(images, img) + sprites = append(sprites, sprite) } - return images, nil + return sprites, nil } diff --git a/internal/assetstore/object.go b/internal/assetstore/object.go index b894805..7de1108 100644 --- a/internal/assetstore/object.go +++ b/internal/assetstore/object.go @@ -9,12 +9,23 @@ import ( ) type Object struct { - assets *AssetStore - images []*ebiten.Image + assets *AssetStore + sprites []*Sprite raw *data.Object } +type Sprite struct { + obj *Object + + XOffset int + YOffset int + Width int + Height int + + Image *ebiten.Image +} + func (a *AssetStore) Object(name string) (*Object, error) { name = canonical(name) @@ -35,9 +46,9 @@ func (a *AssetStore) Object(name string) (*Object, error) { raw.Name = name obj := &Object{ - assets: a, - images: make([]*ebiten.Image, int(raw.NumSprites)), - raw: raw, + assets: a, + sprites: make([]*Sprite, int(raw.NumSprites)), + raw: raw, } a.objs[name] = obj @@ -45,11 +56,11 @@ func (a *AssetStore) Object(name string) (*Object, error) { } // Filled lazily -func (o *Object) Image(idx int) (*ebiten.Image, error) { - if img := o.images[idx]; img != nil { - return img, nil +func (o *Object) Sprite(idx int) (*Sprite, error) { + if sprite := o.sprites[idx]; sprite != nil { + return sprite, nil } - log.Printf("Loading sprite %v %v", o.raw.Name, idx) + log.Printf("Loading sprite %v:%v", o.raw.Name, idx) if o.raw.Sprites[idx] == nil { if err := o.raw.LoadSprite(idx); err != nil { @@ -57,13 +68,22 @@ func (o *Object) Image(idx int) (*ebiten.Image, error) { } } - stdImg := o.raw.Sprites[idx].ToImage() - img, err := ebiten.NewImageFromImage(stdImg, ebiten.FilterDefault) + raw := o.raw.Sprites[idx] + img, err := ebiten.NewImageFromImage(raw.ToImage(), ebiten.FilterDefault) if err != nil { return nil, err } - o.images[idx] = img + sprite := &Sprite{ + obj: o, + Width: int(raw.Width), + Height: int(raw.Width), + XOffset: int(raw.XOffset), + YOffset: int(raw.YOffset), + Image: img, + } - return img, nil + o.sprites[idx] = sprite + + return sprite, nil } diff --git a/internal/conv/object.go b/internal/conv/object.go index 0a04e68..7e04330 100644 --- a/internal/conv/object.go +++ b/internal/conv/object.go @@ -11,8 +11,10 @@ import ( // * Width & height now stored using int // * Colour data is now 32-bit rather than using a palette type Sprite struct { - Width int - Height int + XOffset int + YOffset int + Width int + Height int Image *ebiten.Image } @@ -39,14 +41,18 @@ func ConvertObject(rawObj *data.Object, name string) (*Object, error) { } for i, rawSpr := range rawObj.Sprites { - w := int(rawSpr.Width) - h := int(rawSpr.Height) ebitenImage, err := ebiten.NewImageFromImage(rawSpr.ToImage(), ebiten.FilterDefault) if err != nil { return nil, err } - out.Sprites[i] = &Sprite{Width: w, Height: h, Image: ebitenImage} + out.Sprites[i] = &Sprite{ + XOffset: int(rawSpr.XOffset), + YOffset: int(rawSpr.YOffset), + Width: int(rawSpr.Width), + Height: int(rawSpr.Height), + Image: ebitenImage, + } } return out, nil diff --git a/internal/data/object.go b/internal/data/object.go index 9b730be..b5b77a3 100644 --- a/internal/data/object.go +++ b/internal/data/object.go @@ -14,7 +14,8 @@ import ( ) type SpriteHeader struct { - Unknown0 uint32 + XOffset uint16 + YOffset uint16 Width uint16 Height uint16 Padding1 uint32 // I don't think this is used. Could be wrong. diff --git a/internal/ui/window.go b/internal/ui/window.go index f519787..378a221 100644 --- a/internal/ui/window.go +++ b/internal/ui/window.go @@ -15,6 +15,9 @@ import ( var ( screenScale = flag.Float64("screen-scale", 1.0, "Scale the window by this factor") + winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension") + winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension") + cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") ) @@ -27,7 +30,7 @@ type Window struct { updateFn func() error drawFn func(*ebiten.Image) error - debug bool + debug bool firstRun bool } @@ -103,16 +106,16 @@ func (w *Window) Run(updateFn func() error, drawFn func(*ebiten.Image) error) er w.drawFn = drawFn if *cpuprofile != "" { - f, err := os.Create(*cpuprofile) - if err != nil { - log.Fatal("could not create CPU profile: ", err) - } - defer f.Close() // error handling omitted for example - if err := pprof.StartCPUProfile(f); err != nil { - log.Fatal("could not start CPU profile: ", err) - } - defer pprof.StopCPUProfile() - } + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal("could not create CPU profile: ", err) + } + defer f.Close() // error handling omitted for example + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal("could not start CPU profile: ", err) + } + defer pprof.StopCPUProfile() + } - return ebiten.Run(w.run, 640, 480, 1, w.Title) // Native game resolution: 640x480 + return ebiten.Run(w.run, *winX, *winY, 1, w.Title) // Native game resolution: 640x480 }