Compare commits
4 Commits
6d58d12536
...
7081db42f4
Author | SHA1 | Date | |
---|---|---|---|
7081db42f4 | |||
0bf8233cd1 | |||
c2cbf1d95d | |||
54fe95239e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,4 +5,5 @@
|
||||
/SL
|
||||
/SaW
|
||||
/WoW
|
||||
/WoW-CD
|
||||
/bin
|
||||
|
67
README.md
67
README.md
@@ -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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@@ -114,28 +119,25 @@ Debian:
|
||||
You can then run `make all` in the source tree to get the binaries that are
|
||||
present at the moment.
|
||||
|
||||
Place your WH40K: Chaos Gate installation in `./orig` to benefit from automatic
|
||||
path defaults. Otherwise, point to it with `-game-path`
|
||||
## Configuring
|
||||
|
||||
The `view-map` binary attempts to render a map, and is the current focus of
|
||||
effort. Once I can render a whole map, including pre-placed characters (cultist
|
||||
scum), things can start to get more interesting.
|
||||
Since we support multiple games, a fair bit of configuration is required. Copy
|
||||
`config.toml.example` to `config.toml` and edit it to your requirements. The
|
||||
`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
|
||||
coordinate: floor, centre, left, and right) are rendered fine, and each Z level
|
||||
looks good. There are a few minor artifacts here and there.
|
||||
The various games all use snapshots of the original engine at different points
|
||||
in time, and specify a lot in code that we need to specify in data. That should
|
||||
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
|
||||
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.
|
||||
## Running
|
||||
|
||||
To run:
|
||||
|
||||
```
|
||||
$ make view-map
|
||||
$ ./view-map -map Chapter01
|
||||
$ ./bin/view-map -map Chapter01
|
||||
```
|
||||
|
||||
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
|
||||
`1` - `7` keys to change Z level.
|
||||
|
||||
Dependency management uses `go mod`, so ensure you have at least Go 1.11.
|
||||
|
||||
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:
|
||||
Menus / UI widgets have fairly good support now; you can use the `view-menu`
|
||||
binary to inspect them:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
@@ -180,4 +184,3 @@ $ ./scripts/convert-wav ./orig/Wav
|
||||
|
||||
As with video playback, the ambition is to *eventually* remove this dependency
|
||||
and operate on the unmodified files instead.
|
||||
|
||||
|
@@ -3,7 +3,7 @@ video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--
|
||||
default_engine = "ordoor"
|
||||
|
||||
[engines.geas] # Wages of War -> Gifts of Peace -> Geas
|
||||
data_dir = "./WoW"
|
||||
data_dir = "./WoW-CD"
|
||||
palette = "WagesOfWar"
|
||||
|
||||
[engines.ordoor] # Chaos Gate -> Order Door -> Ordoor
|
||||
@@ -14,9 +14,9 @@ default_engine = "ordoor"
|
||||
data_dir = "./SaW"
|
||||
palette = "SoldiersAtWar"
|
||||
|
||||
# [engines.] Squad Leader -> ??? -> ???
|
||||
# data_dir = "./SL"
|
||||
# palette = "SquadLeader" # may not be relevant?
|
||||
[engines.sl] # Squad Leader -> ??? -> ???
|
||||
data_dir = "./SL"
|
||||
palette = "ChaosGate" # may not be relevant?
|
||||
|
||||
[options]
|
||||
play_movies = true
|
||||
|
1
go.mod
1
go.mod
@@ -9,6 +9,7 @@ require (
|
||||
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
|
||||
github.com/kr/text v0.2.0 // 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
|
||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
|
||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1
|
||||
|
2
go.sum
2
go.sum
@@ -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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
|
@@ -8,6 +8,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
|
||||
"code.ur.gs/lupine/ordoor/internal/config"
|
||||
"code.ur.gs/lupine/ordoor/internal/data"
|
||||
"code.ur.gs/lupine/ordoor/internal/idx"
|
||||
@@ -44,6 +46,7 @@ type AssetStore struct {
|
||||
fonts map[string]*Font
|
||||
generic *data.Generic
|
||||
idx *idx.Idx
|
||||
images map[string]*ebiten.Image
|
||||
maps map[string]*Map
|
||||
menus map[string]*Menu
|
||||
objs map[string]*Object
|
||||
@@ -109,6 +112,7 @@ func (a *AssetStore) Refresh() error {
|
||||
a.entries = newEntryMap
|
||||
a.fonts = make(map[string]*Font)
|
||||
a.idx = nil
|
||||
a.images = make(map[string]*ebiten.Image)
|
||||
a.maps = make(map[string]*Map)
|
||||
a.menus = make(map[string]*Menu)
|
||||
a.objs = make(map[string]*Object)
|
||||
|
42
internal/assetstore/image.go
Normal file
42
internal/assetstore/image.go
Normal 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
|
||||
}
|
@@ -15,8 +15,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
expectedMagic = []byte("\x08\x00WHMAP\x00")
|
||||
expectedSetNameOffset = uint32(0x34)
|
||||
expectedMagic = []byte("\x15\x00AMB_MAP\x00")
|
||||
expectedSetNameOffset = uint32(0x10)
|
||||
notImplemented = fmt.Errorf("Not implemented")
|
||||
)
|
||||
|
||||
@@ -25,35 +25,25 @@ const (
|
||||
MaxLength = 100 // Y coordinate
|
||||
MaxWidth = 130 // X coordinate
|
||||
|
||||
CellSize = 16 // seems to be
|
||||
CellSize = 13 // seems to be
|
||||
|
||||
cellDataOffset = 0x110 // definitely
|
||||
cellDataOffset = 0xc0
|
||||
cellCount = MaxHeight * MaxLength * MaxWidth
|
||||
)
|
||||
|
||||
type Header struct {
|
||||
IsCampaignMap uint32 // Tentatively: 0 = no, 1 = yes
|
||||
MinWidth uint32
|
||||
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`
|
||||
Magic [10]byte // "\x15\x00AMB_MAP\x00"
|
||||
SetName [8]byte // Links to a filename in `/Sets/*.set`
|
||||
// Need to investigate the rest of the header too
|
||||
IsCampaignMap byte
|
||||
}
|
||||
|
||||
func (h Header) Width() int {
|
||||
return int(h.MaxWidth - h.MinWidth)
|
||||
return MaxWidth
|
||||
}
|
||||
|
||||
func (h Header) Length() int {
|
||||
return int(h.MaxLength - h.MinLength)
|
||||
return MaxLength
|
||||
}
|
||||
|
||||
func (h Header) Height() int {
|
||||
@@ -80,7 +70,7 @@ type ObjRef struct {
|
||||
|
||||
// The index into a set palette to retrieve the object
|
||||
func (o ObjRef) Index() int {
|
||||
return int(o.AreaByte)
|
||||
return int(o.AreaByte & 0x7f)
|
||||
}
|
||||
|
||||
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.
|
||||
func (o ObjRef) IsActive() bool {
|
||||
return (o.SpriteAndFlagByte & 0x80) == 0x80
|
||||
}
|
||||
|
||||
} // PARIS is 78 x 60 x 7
|
||||
// 4E 3C 7
|
||||
/*
|
||||
type Cell struct {
|
||||
DoorAndCanisterRelated byte
|
||||
DoorLockAndReactorRelated byte
|
||||
Unknown2 byte
|
||||
// DoorLockAndReactorRelated byte
|
||||
// Unknown2 byte
|
||||
Surface ObjRef
|
||||
Left ObjRef
|
||||
Right ObjRef
|
||||
@@ -105,43 +96,60 @@ type Cell struct {
|
||||
Unknown12 byte
|
||||
Unknown13 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 {
|
||||
switch n {
|
||||
case 0:
|
||||
return c.DoorAndCanisterRelated
|
||||
return c.Unknown1
|
||||
case 1:
|
||||
return c.DoorLockAndReactorRelated
|
||||
case 2:
|
||||
return c.Unknown2
|
||||
case 3:
|
||||
return c.Surface.AreaByte
|
||||
case 4:
|
||||
case 2:
|
||||
return c.Surface.SpriteAndFlagByte
|
||||
case 5:
|
||||
case 3:
|
||||
return c.Left.AreaByte
|
||||
case 6:
|
||||
case 4:
|
||||
return c.Left.SpriteAndFlagByte
|
||||
case 7:
|
||||
case 5:
|
||||
return c.Right.AreaByte
|
||||
case 8:
|
||||
case 6:
|
||||
return c.Right.SpriteAndFlagByte
|
||||
case 9:
|
||||
case 7:
|
||||
return c.Center.AreaByte
|
||||
case 10:
|
||||
case 8:
|
||||
return c.Center.SpriteAndFlagByte
|
||||
case 9:
|
||||
return c.Unknown2[0]
|
||||
case 10:
|
||||
return c.Unknown2[1]
|
||||
case 11:
|
||||
return c.Unknown11
|
||||
return c.Unknown2[2]
|
||||
case 12:
|
||||
return c.Unknown12
|
||||
case 13:
|
||||
return c.Unknown13
|
||||
case 14:
|
||||
return c.Unknown14
|
||||
case 15:
|
||||
return c.SquadRelated
|
||||
return c.Unknown2[3]
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
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 {
|
||||
var out []error
|
||||
if h.IsCampaignMap > 1 {
|
||||
out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
|
||||
}
|
||||
// if h.IsCampaignMap > 1 {
|
||||
// out = append(out, fmt.Errorf("Expected 0 or 1 for IsCampaignMap, got %v", h.IsCampaignMap))
|
||||
// }
|
||||
|
||||
if bytes.Compare(expectedMagic, h.Magic[:]) != 0 {
|
||||
out = append(out, fmt.Errorf("Unexpected magic value: %v", h.Magic))
|
||||
@@ -176,10 +192,10 @@ type GameMap struct {
|
||||
|
||||
func (m *GameMap) Rect() image.Rectangle {
|
||||
return image.Rect(
|
||||
int(m.Header.MinWidth),
|
||||
int(m.Header.MinLength),
|
||||
int(m.Header.MaxWidth),
|
||||
int(m.Header.MaxLength),
|
||||
int(0),
|
||||
int(0),
|
||||
int(m.Width()-1),
|
||||
int(m.Length()-1),
|
||||
)
|
||||
}
|
||||
|
||||
|
20
internal/ordoor/display.go
Normal file
20
internal/ordoor/display.go
Normal 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
|
||||
}
|
@@ -7,6 +7,7 @@ package ordoor
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten"
|
||||
"github.com/hajimehoshi/ebiten/audio"
|
||||
@@ -29,6 +30,10 @@ type Ordoor struct {
|
||||
// Relevant to interface state
|
||||
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
|
||||
ship *ship.Ship
|
||||
}
|
||||
@@ -81,10 +86,6 @@ func Run(configFile string, overrideX, overrideY int) error {
|
||||
|
||||
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 {
|
||||
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 {
|
||||
// FIXME: we're missing a screen about SSI here
|
||||
// FIXME: these should be displayed *after*, not *before*, the copyright
|
||||
if o.config.Options.PlayMovies {
|
||||
o.PlaySkippableVideo("LOGOS")
|
||||
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()
|
||||
if err == flow.ErrExit {
|
||||
log.Printf("Exit requested")
|
||||
@@ -151,6 +156,16 @@ func (o *Ordoor) setupFlow() 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
|
||||
if o.music != nil && o.music.IsPlaying() != 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user