Compare commits

...

4 Commits

Author SHA1 Message Date
7081db42f4 Get SaW maps displaying 2020-06-06 13:50:13 +01:00
0bf8233cd1 Fix binary paths in README 2020-06-06 12:45:10 +01:00
c2cbf1d95d Get the initial copyright notice displaying
This is really awful, but it's nice to check it off.
2020-06-06 12:44:08 +01:00
54fe95239e More README niceness 2020-06-05 22:47:06 +01:00
10 changed files with 214 additions and 95 deletions

1
.gitignore vendored
View File

@@ -5,4 +5,5 @@
/SL /SL
/SaW /SaW
/WoW /WoW
/WoW-CD
/bin /bin

View File

@@ -72,6 +72,10 @@ The menu system seen in Chaos Gate is not present; instead, there is a `BUTTONS`
directory and a lot of `pcx` files under `PIC` that, I suspect, do the job for directory and a lot of `pcx` files under `PIC` that, I suspect, do the job for
this game. this game.
Even with a full installation, Wages of War leaves a lot of data on the CD. It
may be best to run solely from the `WOW` directory on the CD, assuming it's a
strict superset of what gets installed, data-wise.
## Long-term goals ## Long-term goals
Once full playthrough of the official single-player campaign for all four games Once full playthrough of the official single-player campaign for all four games
@@ -96,7 +100,8 @@ Completely new fantasy game using the same engine.
## Building from source ## Building from source
I'm writing code in Go at the moment, so you'll need to have a Go runtime I'm writing code in Go at the moment, so you'll need to have a Go runtime
installed on your system: installed on your system. Dependency management uses `go mod`, so ensure you
have at least Go 1.11.
``` ```
$ go version $ go version
@@ -114,28 +119,25 @@ Debian:
You can then run `make all` in the source tree to get the binaries that are You can then run `make all` in the source tree to get the binaries that are
present at the moment. present at the moment.
Place your WH40K: Chaos Gate installation in `./orig` to benefit from automatic ## Configuring
path defaults. Otherwise, point to it with `-game-path`
The `view-map` binary attempts to render a map, and is the current focus of Since we support multiple games, a fair bit of configuration is required. Copy
effort. Once I can render a whole map, including pre-placed characters (cultist `config.toml.example` to `config.toml` and edit it to your requirements. The
scum), things can start to get more interesting. `data_dir` for the engine(s) you want to use is probably the most important bit,
along with the `default_engine`.
Current status: almost pixel-perfect map rendering. Static objects (four per map The various games all use snapshots of the original engine at different points
coordinate: floor, centre, left, and right) are rendered fine, and each Z level in time, and specify a lot in code that we need to specify in data. That should
looks good. There are a few minor artifacts here and there. all go into the config file, so new games will be able to adapt the engine to
their needs.
Characters and animations aren't touched at all yet. Rendering performance is ## Running
poor. No gameplay, no campaign logic. Interaction with the play area is minimal
and limited to pan, zoom, and click for basic console output.
Still, I'm proud of myself.
To run: To run:
``` ```
$ make view-map $ make view-map
$ ./view-map -map Chapter01 $ ./bin/view-map -map Chapter01
``` ```
Looks like this: Looks like this:
@@ -145,27 +147,29 @@ Looks like this:
Use the arrow keys to scroll around the map, the mouse wheel to zoom, and the Use the arrow keys to scroll around the map, the mouse wheel to zoom, and the
`1` - `7` keys to change Z level. `1` - `7` keys to change Z level.
Dependency management uses `go mod`, so ensure you have at least Go 1.11. Menus / UI widgets have fairly good support now; you can use the `view-menu`
binary to inspect them:
There is the **start** of the menu / campaign flow in a `ordoor` binary:
```
$ cp config.toml.example config.toml
$ make ordoor
$ ./ordoor
```
This plays the introductory videos so far, and nothing else.
Menus are in the process of being rendered; you can use the `view-menu` binary
to inspect them:
``` ```
make view-menu make view-menu
./view-menu -menu ./orig/Menu/Main.mnu ./bin/view-menu -menu Main
``` ```
This menu *displays* OK, including This renders the menus found in Chaos Gate and Soldiers At War. The Squad Leader
format seems basically the same, but has some extra files and aren't 8-bit
colour. They don't display at the moment. Wages of War uses a different format
altogether.
For Chaos Gate, there is the **start** of the game in an `ordoor` binary:
```
$ make ordoor
$ ./bin/ordoor
```
The idea is to hook all the different parts together, and to an abstract game
state (which is called `ship` for ordoor), to make the whole thing playable. It
isn't playable *yet*, but it's heading in that direction.
## Sound ## Sound
@@ -180,4 +184,3 @@ $ ./scripts/convert-wav ./orig/Wav
As with video playback, the ambition is to *eventually* remove this dependency As with video playback, the ambition is to *eventually* remove this dependency
and operate on the unmodified files instead. and operate on the unmodified files instead.

View File

@@ -3,7 +3,7 @@ video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--
default_engine = "ordoor" default_engine = "ordoor"
[engines.geas] # Wages of War -> Gifts of Peace -> Geas [engines.geas] # Wages of War -> Gifts of Peace -> Geas
data_dir = "./WoW" data_dir = "./WoW-CD"
palette = "WagesOfWar" palette = "WagesOfWar"
[engines.ordoor] # Chaos Gate -> Order Door -> Ordoor [engines.ordoor] # Chaos Gate -> Order Door -> Ordoor
@@ -14,9 +14,9 @@ default_engine = "ordoor"
data_dir = "./SaW" data_dir = "./SaW"
palette = "SoldiersAtWar" palette = "SoldiersAtWar"
# [engines.] Squad Leader -> ??? -> ??? [engines.sl] # Squad Leader -> ??? -> ???
# data_dir = "./SL" data_dir = "./SL"
# palette = "SquadLeader" # may not be relevant? palette = "ChaosGate" # may not be relevant?
[options] [options]
play_movies = true play_movies = true

1
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/jfreymuth/oggvorbis v1.0.1 // indirect github.com/jfreymuth/oggvorbis v1.0.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 golang.org/x/image v0.0.0-20200119044424-58c23975cae1

2
go.sum
View File

@@ -45,6 +45,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4 h1:Y/KOCu+ZLB730PudefxfsKVjtI0m0RhvFk9a0l4O1+c=
github.com/samuel/go-pcx v0.0.0-20180426214139-d9c017170db4/go.mod h1:qxuIawynlRhuaHowuXvd1xjyFWx87Ro4gkZlKRXtHnQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=

View File

@@ -8,6 +8,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/config" "code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/data" "code.ur.gs/lupine/ordoor/internal/data"
"code.ur.gs/lupine/ordoor/internal/idx" "code.ur.gs/lupine/ordoor/internal/idx"
@@ -44,6 +46,7 @@ type AssetStore struct {
fonts map[string]*Font fonts map[string]*Font
generic *data.Generic generic *data.Generic
idx *idx.Idx idx *idx.Idx
images map[string]*ebiten.Image
maps map[string]*Map maps map[string]*Map
menus map[string]*Menu menus map[string]*Menu
objs map[string]*Object objs map[string]*Object
@@ -109,6 +112,7 @@ func (a *AssetStore) Refresh() error {
a.entries = newEntryMap a.entries = newEntryMap
a.fonts = make(map[string]*Font) a.fonts = make(map[string]*Font)
a.idx = nil a.idx = nil
a.images = make(map[string]*ebiten.Image)
a.maps = make(map[string]*Map) a.maps = make(map[string]*Map)
a.menus = make(map[string]*Menu) a.menus = make(map[string]*Menu)
a.objs = make(map[string]*Object) a.objs = make(map[string]*Object)

View File

@@ -0,0 +1,42 @@
package assetstore
import (
"image"
"os"
"github.com/hajimehoshi/ebiten"
_ "github.com/samuel/go-pcx/pcx" // PCX support
)
func (a *AssetStore) Image(name string) (*ebiten.Image, error) {
name = canonical(name)
if img, ok := a.images[name]; ok {
return img, nil
}
// baps, ordoor, geas store .pcx files in Pic
// TODO: SL stores .bmp files in Res
filename, err := a.lookup(name, "pcx", "Pic")
if err != nil {
return nil, err
}
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
rawImg, _, err := image.Decode(f)
if err != nil {
return nil, err
}
img, err := ebiten.NewImageFromImage(rawImg, ebiten.FilterDefault)
if err != nil {
return nil, err
}
a.images[name] = img
return img, nil
}

View File

@@ -15,8 +15,8 @@ import (
) )
var ( var (
expectedMagic = []byte("\x08\x00WHMAP\x00") expectedMagic = []byte("\x15\x00AMB_MAP\x00")
expectedSetNameOffset = uint32(0x34) expectedSetNameOffset = uint32(0x10)
notImplemented = fmt.Errorf("Not implemented") notImplemented = fmt.Errorf("Not implemented")
) )
@@ -25,35 +25,25 @@ const (
MaxLength = 100 // Y coordinate MaxLength = 100 // Y coordinate
MaxWidth = 130 // X coordinate MaxWidth = 130 // X coordinate
CellSize = 16 // seems to be CellSize = 13 // seems to be
cellDataOffset = 0x110 // definitely cellDataOffset = 0xc0
cellCount = MaxHeight * MaxLength * MaxWidth cellCount = MaxHeight * MaxLength * MaxWidth
) )
type Header struct { type Header struct {
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes Magic [10]byte // "\x15\x00AMB_MAP\x00"
MinWidth uint32 SetName [8]byte // Links to a filename in `/Sets/*.set`
MinLength uint32
MaxWidth uint32
MaxLength uint32
Unknown1 uint32
Unknown2 uint32
Unknown3 uint32
Unknown4 uint32
Magic [8]byte // "\x08\x00WHMAP\x00"
Unknown5 uint32
Unknown6 uint32
SetName [8]byte // Links to a filename in `/Sets/*.set`
// Need to investigate the rest of the header too // Need to investigate the rest of the header too
IsCampaignMap byte
} }
func (h Header) Width() int { func (h Header) Width() int {
return int(h.MaxWidth - h.MinWidth) return MaxWidth
} }
func (h Header) Length() int { func (h Header) Length() int {
return int(h.MaxLength - h.MinLength) return MaxLength
} }
func (h Header) Height() int { func (h Header) Height() int {
@@ -80,7 +70,7 @@ type ObjRef struct {
// The index into a set palette to retrieve the object // The index into a set palette to retrieve the object
func (o ObjRef) Index() int { func (o ObjRef) Index() int {
return int(o.AreaByte) return int(o.AreaByte & 0x7f)
} }
func (o ObjRef) Sprite() int { func (o ObjRef) Sprite() int {
@@ -91,12 +81,13 @@ func (o ObjRef) Sprite() int {
// The top bit seems to say whether we should draw or not. // The top bit seems to say whether we should draw or not.
func (o ObjRef) IsActive() bool { func (o ObjRef) IsActive() bool {
return (o.SpriteAndFlagByte & 0x80) == 0x80 return (o.SpriteAndFlagByte & 0x80) == 0x80
} } // PARIS is 78 x 60 x 7
// 4E 3C 7
/*
type Cell struct { type Cell struct {
DoorAndCanisterRelated byte DoorAndCanisterRelated byte
DoorLockAndReactorRelated byte // DoorLockAndReactorRelated byte
Unknown2 byte // Unknown2 byte
Surface ObjRef Surface ObjRef
Left ObjRef Left ObjRef
Right ObjRef Right ObjRef
@@ -105,43 +96,60 @@ type Cell struct {
Unknown12 byte Unknown12 byte
Unknown13 byte Unknown13 byte
Unknown14 byte Unknown14 byte
SquadRelated byte // SquadRelated byte
}*/
type Cell struct {
Unknown1 byte
Surface ObjRef
Left ObjRef
Right ObjRef
Center ObjRef
Unknown2 [4]byte
/*
DoorAndCanisterRelated byte
// DoorLockAndReactorRelated byte
// Unknown2 byte
Surface ObjRef
Left ObjRef
Right ObjRef
Center ObjRef
Unknown11 byte
Unknown12 byte
Unknown13 byte
Unknown14 byte
SquadRelated byte*/
} }
func (c *Cell) At(n int) byte { func (c *Cell) At(n int) byte {
switch n { switch n {
case 0: case 0:
return c.DoorAndCanisterRelated return c.Unknown1
case 1: case 1:
return c.DoorLockAndReactorRelated
case 2:
return c.Unknown2
case 3:
return c.Surface.AreaByte return c.Surface.AreaByte
case 4: case 2:
return c.Surface.SpriteAndFlagByte return c.Surface.SpriteAndFlagByte
case 5: case 3:
return c.Left.AreaByte return c.Left.AreaByte
case 6: case 4:
return c.Left.SpriteAndFlagByte return c.Left.SpriteAndFlagByte
case 7: case 5:
return c.Right.AreaByte return c.Right.AreaByte
case 8: case 6:
return c.Right.SpriteAndFlagByte return c.Right.SpriteAndFlagByte
case 9: case 7:
return c.Center.AreaByte return c.Center.AreaByte
case 10: case 8:
return c.Center.SpriteAndFlagByte return c.Center.SpriteAndFlagByte
case 9:
return c.Unknown2[0]
case 10:
return c.Unknown2[1]
case 11: case 11:
return c.Unknown11 return c.Unknown2[2]
case 12: case 12:
return c.Unknown12 return c.Unknown2[3]
case 13:
return c.Unknown13
case 14:
return c.Unknown14
case 15:
return c.SquadRelated
} }
return 0 return 0
@@ -150,15 +158,23 @@ func (c *Cell) At(n int) byte {
// Cells is always a fixed size; use At to get a cell according to x,y,z // Cells is always a fixed size; use At to get a cell according to x,y,z
type Cells []Cell type Cells []Cell
// 6 Possibilities for being laid out in memory. Most likely:
// XXYYZZ
// OR
// XYZXYZ
func (c Cells) At(x, y, z int) Cell { func (c Cells) At(x, y, z int) Cell {
return c[(z*MaxLength*MaxWidth)+(y*MaxWidth)+x] // log.Printf("At (%v,%v,%v)=%v", x, y, z, x*y*z)
return c[(z*MaxLength*MaxWidth)+
(y*MaxWidth)+
x]
} }
func (h Header) Check() []error { func (h Header) Check() []error {
var out []error var out []error
if h.IsCampaignMap > 1 { // if h.IsCampaignMap > 1 {
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap)) // out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
} // }
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 { if bytes.Compare(expectedMagic, h.Magic[:]) != 0 {
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic)) out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
@@ -176,10 +192,10 @@ type GameMap struct {
func (m *GameMap) Rect() image.Rectangle { func (m *GameMap) Rect() image.Rectangle {
return image.Rect( return image.Rect(
int(m.Header.MinWidth), int(0),
int(m.Header.MinLength), int(0),
int(m.Header.MaxWidth), int(m.Width()-1),
int(m.Header.MaxLength), int(m.Length()-1),
) )
} }

View File

@@ -0,0 +1,20 @@
package ordoor
import (
"time"
)
func (o *Ordoor) DisplayImageFor(d time.Duration, name string) error {
img, err := o.assets.Image(name)
if err != nil {
return err
}
o.pic = img
go func() {
<-time.After(d)
o.pic = nil // FIXME: this is a race condition and a half
}()
return nil
}

View File

@@ -7,6 +7,7 @@ package ordoor
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/audio" "github.com/hajimehoshi/ebiten/audio"
@@ -29,6 +30,10 @@ type Ordoor struct {
// Relevant to interface state // Relevant to interface state
flow *flow.Flow flow *flow.Flow
// FIXME: should be put inside flow
// If this is set, we display it instead of flow
pic *ebiten.Image
// Relevant to campaign state // Relevant to campaign state
ship *ship.Ship ship *ship.Ship
} }
@@ -81,10 +86,6 @@ func Run(configFile string, overrideX, overrideY int) error {
ordoor.win = win ordoor.win = win
if err := ordoor.setupFlow(); err != nil {
return fmt.Errorf("failed to setup UI flow: %v", err)
}
if err := ordoor.Run(); err != nil { if err := ordoor.Run(); err != nil {
return fmt.Errorf("Run finished with error: %v", err) return fmt.Errorf("Run finished with error: %v", err)
} }
@@ -93,12 +94,16 @@ func Run(configFile string, overrideX, overrideY int) error {
} }
func (o *Ordoor) Run() error { func (o *Ordoor) Run() error {
// FIXME: we're missing a screen about SSI here // FIXME: these should be displayed *after*, not *before*, the copyright
if o.config.Options.PlayMovies { if o.config.Options.PlayMovies {
o.PlaySkippableVideo("LOGOS") o.PlaySkippableVideo("LOGOS")
o.PlaySkippableVideo("movie1") o.PlaySkippableVideo("movie1")
} }
if err := o.DisplayImageFor(time.Second, "copyright"); err != nil {
log.Printf("Failed to display copyright image: %v", err)
}
err := o.win.Run() err := o.win.Run()
if err == flow.ErrExit { if err == flow.ErrExit {
log.Printf("Exit requested") log.Printf("Exit requested")
@@ -151,6 +156,16 @@ func (o *Ordoor) setupFlow() error {
} }
func (o *Ordoor) Update(screenX, screenY int) error { func (o *Ordoor) Update(screenX, screenY int) error {
if pic := o.pic; pic != nil {
return nil // Ignore flow until we don't have a pic any more
}
if o.flow == nil {
if err := o.setupFlow(); err != nil {
return fmt.Errorf("failed to setup UI flow: %v", err)
}
}
// Ensure music is doing the right thing // Ensure music is doing the right thing
if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic { if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic {
if o.config.Options.PlayMusic { if o.config.Options.PlayMusic {
@@ -165,9 +180,24 @@ func (o *Ordoor) Update(screenX, screenY int) error {
} }
func (o *Ordoor) Draw(screen *ebiten.Image) error { func (o *Ordoor) Draw(screen *ebiten.Image) error {
if pic := o.pic; pic != nil {
// Scale the picture to the screen and draw it
scaleX := float64(screen.Bounds().Dx()) / float64(pic.Bounds().Dx())
scaleY := float64(screen.Bounds().Dy()) / float64(pic.Bounds().Dy())
do := &ebiten.DrawImageOptions{}
do.GeoM.Scale(scaleX, scaleY)
return screen.DrawImage(pic, do)
}
return o.flow.Draw(screen) return o.flow.Draw(screen)
} }
func (o *Ordoor) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) { func (o *Ordoor) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
return o.flow.Cursor() if o.flow != nil {
return o.flow.Cursor()
}
return nil, nil, nil
} }