Compare commits
4 Commits
132670507c
...
2a39eee0a1
Author | SHA1 | Date | |
---|---|---|---|
2a39eee0a1 | |||
3866ee07a8 | |||
c1268e8d57 | |||
59baf20c35 |
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,14 +1,8 @@
|
|||||||
/config.toml
|
/config.toml
|
||||||
/loader
|
/loader
|
||||||
/orig
|
/isos
|
||||||
/palette-idx
|
/CG
|
||||||
/view-ani
|
/SL
|
||||||
/view-font
|
/SaW
|
||||||
/view-obj
|
/WoW
|
||||||
/view-map
|
/bin
|
||||||
/view-minimap
|
|
||||||
/view-menu
|
|
||||||
/view-set
|
|
||||||
/ordoor
|
|
||||||
/investigation/Maps
|
|
||||||
/investigation/Obj
|
|
||||||
|
45
Makefile
45
Makefile
@@ -4,37 +4,40 @@ GOBUILD ?= go build -tags ebitengl
|
|||||||
|
|
||||||
all: loader ordoor palette-idx view-ani view-font view-obj view-map view-menu view-minimap view-set
|
all: loader ordoor palette-idx view-ani view-font view-obj view-map view-menu view-minimap view-set
|
||||||
|
|
||||||
loader: $(srcfiles)
|
bin:
|
||||||
$(GOBUILD) -o loader ./cmd/loader
|
mkdir bin
|
||||||
|
|
||||||
palette-idx: $(srcfiles)
|
loader: bin $(srcfiles)
|
||||||
$(GOBUILD) -o palette-idx ./cmd/palette-idx
|
$(GOBUILD) -o bin/loader ./cmd/loader
|
||||||
|
|
||||||
view-ani: $(srcfiles)
|
palette-idx: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-ani ./cmd/view-ani
|
$(GOBUILD) -o bin/palette-idx ./cmd/palette-idx
|
||||||
|
|
||||||
view-font: $(srcfiles)
|
view-ani: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-font ./cmd/view-font
|
$(GOBUILD) -o bin/view-ani ./cmd/view-ani
|
||||||
|
|
||||||
view-obj: $(srcfiles)
|
view-font: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-obj ./cmd/view-obj
|
$(GOBUILD) -o bin/view-font ./cmd/view-font
|
||||||
|
|
||||||
view-map: $(srcfiles)
|
view-obj: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-map ./cmd/view-map
|
$(GOBUILD) -o bin/view-obj ./cmd/view-obj
|
||||||
|
|
||||||
view-menu: $(srcfiles)
|
view-map: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-menu ./cmd/view-menu
|
$(GOBUILD) -o bin/view-map ./cmd/view-map
|
||||||
|
|
||||||
view-minimap: $(srcfiles)
|
view-menu: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-minimap ./cmd/view-minimap
|
$(GOBUILD) -o bin/view-menu ./cmd/view-menu
|
||||||
|
|
||||||
view-set: $(srcfiles)
|
view-minimap: bin $(srcfiles)
|
||||||
$(GOBUILD) -o view-set ./cmd/view-set
|
$(GOBUILD) -o bin/view-minimap ./cmd/view-minimap
|
||||||
|
|
||||||
ordoor: $(srcfiles)
|
view-set: bin $(srcfiles)
|
||||||
$(GOBUILD) -o ordoor ./cmd/ordoor
|
$(GOBUILD) -o bin/view-set ./cmd/view-set
|
||||||
|
|
||||||
|
ordoor: bin $(srcfiles)
|
||||||
|
$(GOBUILD) -o bin/ordoor ./cmd/ordoor
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f loader ordoor view-ani view-obj view-map view-minimap view-set palette-idx view-font
|
rm -rf bin
|
||||||
|
|
||||||
.PHONY: all clean
|
.PHONY: all clean
|
||||||
|
82
README.md
82
README.md
@@ -1,20 +1,34 @@
|
|||||||
# Ordoor
|
# Ordoor
|
||||||
|
|
||||||
Ordoor is an **unofficial** [game engine recreation](https://en.wikipedia.org/wiki/Game_engine_recreation)
|
Ordoor is an **unofficial** [game engine recreation](https://en.wikipedia.org/wiki/Game_engine_recreation)
|
||||||
of the classic game from 1998, [Warhammer 40,000: Chaos Gate](https://en.wikipedia.org/wiki/Warhammer_40,000:_Chaos_Gate)
|
of the Random Games, Inc., [Strategy Engine](https://www.mobygames.com/game-group/game-engine-random-games-1996-2000-strategy-engine),
|
||||||
|
which was in use from 1996 - 2000.
|
||||||
|
|
||||||
**You must have a copy of the original game data to use this project**. GOG is
|
Four games are known to have been published for this engine:
|
||||||
the current publisher of this game; [you can purchase it here](https://www.gog.com/game/warhammer_40000_chaos_gate).
|
|
||||||
|
|
||||||
"Warhammer 40,000" is a trademark of Games Workshop, and the game data used by
|
* [Wages of War: The Business of Battle](https://en.wikipedia.org/wiki/Wages_of_War) (1996)
|
||||||
Ordoor contains Games Workshop intellectual property. I am confident that this
|
* [Soldiers At War](https://en.wikipedia.org/wiki/Soldiers_at_War) (1998)
|
||||||
project uses all those things in accordance with the
|
* [Warhammer 40,000: Chaos Gate](https://en.wikipedia.org/wiki/Warhammer_40,000:_Chaos_Gate) (1998) [GOG](https://www.gog.com/game/warhammer_40000_chaos_gate)
|
||||||
[Intellectual Property Policy](https://www.games-workshop.com/en-GB/Intellectual-Property-Policy)
|
* [Avalon Hill's Squad Leader](https://en.wikipedia.org/wiki/Avalon_Hill%27s_Squad_Leader) (2000)
|
||||||
and the license granted when purchasing a copy of the game in question. Do let
|
|
||||||
me know if you see or suspect any violation, and I'll address it immediately.
|
The aim of Ordoor is to be a complete reimplementation that allows all four
|
||||||
|
of these games to be played on modern hardware. It should also permit new games
|
||||||
|
of the same style to be built.
|
||||||
|
|
||||||
|
For each of the games above, **You must have a copy of the original game data to play**.
|
||||||
|
Links are provided above if we're aware of an active publisher; otherwise, check
|
||||||
|
your back catalogue, or perhaps a local charity shop.
|
||||||
|
|
||||||
|
Trademarks and intellectual property are the property of their respective
|
||||||
|
owners, and the games mentioned above (including the game data) are protected by
|
||||||
|
copyright. As a mere game engine recreation, we're confident that this project
|
||||||
|
operates legally, and that its goal is a noble one. Do get in touch if you
|
||||||
|
believe otherwise!
|
||||||
|
|
||||||
Ordoor is a portmanteau of Order Door, which is, of course, the opposite of a
|
Ordoor is a portmanteau of Order Door, which is, of course, the opposite of a
|
||||||
Chaos Gate.
|
Chaos Gate. The project began with a Chaos Gate recreation, then more games were
|
||||||
|
discovered, so scope expanded. A rename and/or rewrite may be on the cards as a
|
||||||
|
result.
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
@@ -31,6 +45,27 @@ I've just been informed that another game from 1998, [Soldiers At War](https://e
|
|||||||
seems to use the same engine. Maybe at some point Ordoor will be able to play
|
seems to use the same engine. Maybe at some point Ordoor will be able to play
|
||||||
both. Will that need a rename? Hmm. Watch this space.
|
both. Will that need a rename? Hmm. Watch this space.
|
||||||
|
|
||||||
|
## Long-term goals
|
||||||
|
|
||||||
|
Once full playthrough of the official single-player campaign for all four games
|
||||||
|
has been achieved, thoughts turn to other things we could do. Here are some
|
||||||
|
ideas, mostly at random.
|
||||||
|
|
||||||
|
Multi-player support.
|
||||||
|
|
||||||
|
Graphics enhancements - 3D models instead of sprites, high-resolution tile sets,
|
||||||
|
32-bit colour, etc. Hopefully we'd be able to drop these in one at a time.
|
||||||
|
|
||||||
|
Vastly improved AI.
|
||||||
|
|
||||||
|
Mash-ups? How do mercenaries fare against cultists fare against Nazis? Only one
|
||||||
|
way to find out!
|
||||||
|
|
||||||
|
New campaigns with existing assets. Tell new stories, or elaborate on / modify
|
||||||
|
existing ones.
|
||||||
|
|
||||||
|
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
|
||||||
@@ -119,30 +154,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.
|
||||||
|
|
||||||
## Miscellany
|
|
||||||
|
|
||||||
"Mission Setup" includes information about available squad types
|
|
||||||
|
|
||||||
From EquipDef.cpp Dumo: CEquipment we learn the following object types:
|
|
||||||
|
|
||||||
0. DELETED
|
|
||||||
1. WEAPON
|
|
||||||
2. GRENADE
|
|
||||||
3. MEDIPACK
|
|
||||||
4. SCANNER
|
|
||||||
5. GENESEED
|
|
||||||
6. CLIP
|
|
||||||
7. DOOR KEY
|
|
||||||
8. DOOR KEY
|
|
||||||
9. DOOR KEY
|
|
||||||
10. DOOR KEY
|
|
||||||
|
|
||||||
And we learn they can be "on"....
|
|
||||||
|
|
||||||
0. CHARACTER
|
|
||||||
1. VEHICLE
|
|
||||||
2. CANISTER
|
|
||||||
|
|
||||||
I'm starting to see some parallels with [this](https://github.com/shlainn/game-file-formats/wiki/)
|
|
||||||
in the data formats, and the timeline (1997) seems about right. Worth keeping an
|
|
||||||
eye on!
|
|
||||||
|
@@ -3,42 +3,58 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/color"
|
||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"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/fonts"
|
"code.ur.gs/lupine/ordoor/internal/fonts"
|
||||||
"code.ur.gs/lupine/ordoor/internal/idx"
|
"code.ur.gs/lupine/ordoor/internal/idx"
|
||||||
"code.ur.gs/lupine/ordoor/internal/maps"
|
"code.ur.gs/lupine/ordoor/internal/maps"
|
||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/palettes"
|
||||||
"code.ur.gs/lupine/ordoor/internal/sets"
|
"code.ur.gs/lupine/ordoor/internal/sets"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
skipObj = flag.Bool("skip-obj", true, "Skip loading .obj files")
|
skipObj = flag.Bool("skip-obj", true, "Skip loading .obj files")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// FIXME: all these paths are hardcoded with Chaos Gate in mind
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
loadData()
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
if !*skipObj {
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
loadObj()
|
}
|
||||||
|
engine := cfg.DefaultEngine()
|
||||||
|
gamePath := engine.DataDir
|
||||||
|
palette, ok := palettes.Get(engine.Palette)
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("Unknown palette name: %v", engine.Palette)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMapsFrom("Maps")
|
loadData(filepath.Join(gamePath, "Data"))
|
||||||
loadMapsFrom("MultiMaps")
|
|
||||||
loadSets()
|
if !*skipObj {
|
||||||
loadMenus()
|
loadObj(filepath.Join(gamePath, "Obj"))
|
||||||
loadFonts()
|
}
|
||||||
loadIdx()
|
|
||||||
|
loadMapsFrom(filepath.Join(gamePath, "Maps"))
|
||||||
|
loadMapsFrom(filepath.Join(gamePath, "MultiMaps"))
|
||||||
|
loadSets(filepath.Join(gamePath, "Sets"))
|
||||||
|
loadMenus(filepath.Join(gamePath, "Menu"), palette)
|
||||||
|
loadFonts(filepath.Join(gamePath, "Fonts"))
|
||||||
|
loadIdx(filepath.Join(gamePath, "Idx", "WarHammer.idx"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadData() {
|
func loadData(dataPath string) {
|
||||||
dataPath := filepath.Join(*gamePath, "Data")
|
|
||||||
accountingPath := filepath.Join(dataPath, "Accounting.dat")
|
accountingPath := filepath.Join(dataPath, "Accounting.dat")
|
||||||
aniObDefPath := filepath.Join(dataPath, "AniObDef.dat")
|
aniObDefPath := filepath.Join(dataPath, "AniObDef.dat")
|
||||||
genericDataPath := filepath.Join(dataPath, "GenericData.dat")
|
genericDataPath := filepath.Join(dataPath, "GenericData.dat")
|
||||||
@@ -84,9 +100,7 @@ func loadData() {
|
|||||||
ha.Print()
|
ha.Print()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadObj() {
|
func loadObj(objDataPath string) {
|
||||||
objDataPath := filepath.Join(*gamePath, "Obj")
|
|
||||||
|
|
||||||
// TODO: Obj/cpiece.rec isn't loaded by this. Do we need it? How do we know?
|
// TODO: Obj/cpiece.rec isn't loaded by this. Do we need it? How do we know?
|
||||||
log.Printf("Loading %s...", objDataPath)
|
log.Printf("Loading %s...", objDataPath)
|
||||||
objects, err := data.LoadObjects(objDataPath)
|
objects, err := data.LoadObjects(objDataPath)
|
||||||
@@ -116,8 +130,7 @@ func loadObj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMapsFrom(part string) {
|
func loadMapsFrom(mapsPath string) {
|
||||||
mapsPath := filepath.Join(*gamePath, part)
|
|
||||||
log.Printf("Loading maps from %s", mapsPath)
|
log.Printf("Loading maps from %s", mapsPath)
|
||||||
|
|
||||||
gameMaps, err := maps.LoadGameMaps(mapsPath)
|
gameMaps, err := maps.LoadGameMaps(mapsPath)
|
||||||
@@ -140,8 +153,7 @@ func loadMapsFrom(part string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSets() {
|
func loadSets(setsPath string) {
|
||||||
setsPath := filepath.Join(*gamePath, "Sets")
|
|
||||||
log.Printf("Loading sets from %s", setsPath)
|
log.Printf("Loading sets from %s", setsPath)
|
||||||
|
|
||||||
mapSets, err := sets.LoadSets(setsPath)
|
mapSets, err := sets.LoadSets(setsPath)
|
||||||
@@ -157,10 +169,10 @@ func loadSets() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMenus() {
|
func loadMenus(menusPath string, palette color.Palette) {
|
||||||
menusPath := filepath.Join(*gamePath, "Menu")
|
log.Printf("Loading menus from %s", menusPath)
|
||||||
|
|
||||||
menus, err := menus.LoadMenus(menusPath)
|
menus, err := menus.LoadMenus(menusPath, palette)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse %s/*.mnu as menus: %v", menusPath, err)
|
log.Fatalf("Failed to parse %s/*.mnu as menus: %v", menusPath, err)
|
||||||
}
|
}
|
||||||
@@ -187,8 +199,8 @@ func displayRecord(record *menus.Record, depth int) {
|
|||||||
fmt.Printf("%s* %s\n", strings.Repeat(" ", depth), content)
|
fmt.Printf("%s* %s\n", strings.Repeat(" ", depth), content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadFonts() {
|
func loadFonts(fontsPath string) {
|
||||||
fontsPath := filepath.Join(*gamePath, "Fonts")
|
log.Printf("Loading fonts from %s", fontsPath)
|
||||||
|
|
||||||
fonts, err := fonts.LoadFonts(fontsPath)
|
fonts, err := fonts.LoadFonts(fontsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -200,8 +212,9 @@ func loadFonts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadIdx() {
|
func loadIdx(idxPath string) {
|
||||||
idxPath := filepath.Join(*gamePath, "Idx", "WarHammer.idx")
|
log.Printf("Loading idx from %s", idxPath)
|
||||||
|
|
||||||
idx, err := idx.Load(idxPath)
|
idx, err := idx.Load(idxPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to parse %s as idx: %v", idxPath, err)
|
log.Fatalf("Failed to parse %s as idx: %v", idxPath, err)
|
||||||
|
@@ -11,11 +11,14 @@ import (
|
|||||||
"golang.org/x/image/colornames"
|
"golang.org/x/image/colornames"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
groupIdx = flag.Int("group", 1, "Group index to start at")
|
groupIdx = flag.Int("group", 1, "Group index to start at")
|
||||||
recIdx = flag.Int("record", 0, "Record index to start at")
|
recIdx = flag.Int("record", 0, "Record index to start at")
|
||||||
|
|
||||||
@@ -43,12 +46,17 @@ type state struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" {
|
if *configFile == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to set up asset store: %v", err)
|
log.Fatal("Failed to set up asset store: %v", err)
|
||||||
}
|
}
|
||||||
|
@@ -10,11 +10,14 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
fontName = flag.String("font", "", "Name of a font, e.g., basfont12")
|
fontName = flag.String("font", "", "Name of a font, e.g., basfont12")
|
||||||
txt = flag.String("text", "Test string", "Text to render")
|
txt = flag.String("text", "Test string", "Text to render")
|
||||||
|
|
||||||
@@ -37,12 +40,17 @@ type state struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || *fontName == "" {
|
if *configFile == "" || *fontName == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@@ -9,12 +9,15 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/scenario"
|
"code.ur.gs/lupine/ordoor/internal/scenario"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
|
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
|
||||||
|
|
||||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||||
@@ -28,14 +31,19 @@ type env struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || *gameMap == "" {
|
if *configFile == "" || *gameMap == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err)
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to scan root directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scenario, err := scenario.NewScenario(assets, *gameMap)
|
scenario, err := scenario.NewScenario(assets, *gameMap)
|
||||||
|
@@ -8,11 +8,14 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
menuName = flag.String("menu", "", "Name of a menu, e.g. Main")
|
menuName = flag.String("menu", "", "Name of a menu, e.g. Main")
|
||||||
|
|
||||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||||
@@ -28,12 +31,17 @@ type dlg struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || *menuName == "" {
|
if *configFile == "" || *menuName == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@@ -10,11 +10,14 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ")
|
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")
|
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
|
||||||
sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at")
|
sprIdx = flag.Int("spr-idx", 0, "Sprite index to start at")
|
||||||
@@ -42,12 +45,17 @@ type state struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || (*objName == "" && *objFile == "") {
|
if *configFile == "" || (*objName == "" && *objFile == "") {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to set up asset store: %v", err)
|
log.Fatal("Failed to set up asset store: %v", err)
|
||||||
}
|
}
|
||||||
|
@@ -10,11 +10,14 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/config"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
|
configFile = flag.String("config", "config.toml", "Config file")
|
||||||
|
engine = flag.String("engine", "", "Override engine to use")
|
||||||
|
|
||||||
setName = flag.String("set", "", "Name of a set, e.g., map01")
|
setName = flag.String("set", "", "Name of a set, e.g., map01")
|
||||||
|
|
||||||
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
|
||||||
@@ -39,12 +42,17 @@ type state struct {
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *gamePath == "" || *setName == "" {
|
if *configFile == "" || *setName == "" {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(*gamePath)
|
cfg, err := config.Load(*configFile, *engine)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,22 @@
|
|||||||
[ordoor]
|
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
|
||||||
data_dir = "./orig"
|
|
||||||
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
|
default_engine = "ordoor"
|
||||||
|
|
||||||
|
[engines.geas] # Wages of War -> Gifts of Peace -> Geas
|
||||||
|
data_dir = "./WoW"
|
||||||
|
palette = "WagesOfWar"
|
||||||
|
|
||||||
|
[engines.ordoor] # Chaos Gate -> Order Door -> Ordoor
|
||||||
|
data_dir = "./CG"
|
||||||
|
palette = "ChaosGate"
|
||||||
|
|
||||||
|
[engines.baps] # Soldiers At War -> Boys at Play -> Baps
|
||||||
|
data_dir = "./SaW"
|
||||||
|
palette = "SoldiersAtWar"
|
||||||
|
|
||||||
|
# [engines.] Squad Leader -> ??? -> ???
|
||||||
|
# data_dir = "./SL"
|
||||||
|
# palette = "SquadLeader" # may not be relevant?
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
play_movies = true
|
play_movies = true
|
||||||
|
@@ -2,24 +2,23 @@ package assetstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/color"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"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"
|
||||||
)
|
"code.ur.gs/lupine/ordoor/internal/palettes"
|
||||||
|
|
||||||
const (
|
|
||||||
RootDir = "" // Used in the entryMap for entries pertaining to the root dir
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type entryMap map[string]map[string]string
|
type entryMap map[string]map[string]string
|
||||||
|
|
||||||
// type AssetStore is responsible for lazily loading game data when it is
|
// type AssetStore is responsible for lazily loading game data when it is
|
||||||
// required. Applications shouldn't need to do anything except set one of these
|
// required. Applications shouldn't need to do anything except set one of these
|
||||||
// up, pointing at the game dir root, to access all assets.
|
// up, pointing at the game dir root, to access all assets for that game.
|
||||||
//
|
//
|
||||||
// Assets should be loaded on-demand to keep memory costs as low as possible.
|
// Assets should be loaded on-demand to keep memory costs as low as possible.
|
||||||
// Cross-platform differences such as filename case sensitivity are also dealt
|
// Cross-platform differences such as filename case sensitivity are also dealt
|
||||||
@@ -27,8 +26,12 @@ type entryMap map[string]map[string]string
|
|||||||
//
|
//
|
||||||
// We assume the directory is read-only. You can run Refresh() if you make a
|
// We assume the directory is read-only. You can run Refresh() if you make a
|
||||||
// change.
|
// change.
|
||||||
|
//
|
||||||
|
// To mix assets from different games, either construct a synthetic directory
|
||||||
|
// or instantiate two separate asset stores.
|
||||||
type AssetStore struct {
|
type AssetStore struct {
|
||||||
RootDir string
|
RootDir string
|
||||||
|
Palette color.Palette
|
||||||
|
|
||||||
// Case-insensitive file lookup.
|
// Case-insensitive file lookup.
|
||||||
// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
|
// {"":{"anim":"Anim", "obj":"Obj", ...}, "anim":{ "warhammer.ani":"WarHammer.ani" }, ...}
|
||||||
@@ -50,9 +53,19 @@ type AssetStore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new AssetStore
|
// New returns a new AssetStore
|
||||||
func New(dir string) (*AssetStore, error) {
|
func New(engine *config.Engine) (*AssetStore, error) {
|
||||||
|
if engine == nil {
|
||||||
|
return nil, fmt.Errorf("Unconfigured engine passed to assetstore")
|
||||||
|
}
|
||||||
|
|
||||||
|
palette, ok := palettes.Get(engine.Palette)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("Couldn't find palette %q for engine", engine.Palette)
|
||||||
|
}
|
||||||
|
|
||||||
store := &AssetStore{
|
store := &AssetStore{
|
||||||
RootDir: dir,
|
RootDir: engine.DataDir,
|
||||||
|
Palette: palette,
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill entryMap
|
// fill entryMap
|
||||||
@@ -70,7 +83,7 @@ func (a *AssetStore) Refresh() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newEntryMap := make(entryMap, len(rootEntries))
|
newEntryMap := make(entryMap, len(rootEntries))
|
||||||
newEntryMap[RootDir] = rootEntries
|
newEntryMap[""] = rootEntries
|
||||||
|
|
||||||
for lower, natural := range rootEntries {
|
for lower, natural := range rootEntries {
|
||||||
path := filepath.Join(a.RootDir, natural)
|
path := filepath.Join(a.RootDir, natural)
|
||||||
@@ -118,7 +131,7 @@ func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) {
|
|||||||
dir = canonical(dir)
|
dir = canonical(dir)
|
||||||
if base, ok := a.entries[dir]; ok {
|
if base, ok := a.entries[dir]; ok {
|
||||||
if file, ok := base[filename]; ok {
|
if file, ok := base[filename]; ok {
|
||||||
actualDir := a.entries[RootDir][dir]
|
actualDir := a.entries[""][dir]
|
||||||
return filepath.Join(a.RootDir, actualDir, file), nil
|
return filepath.Join(a.RootDir, actualDir, file), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -62,7 +62,7 @@ func (a *AssetStore) Menu(name string) (*Menu, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
raw, err := menus.LoadMenu(filename)
|
raw, err := menus.LoadMenu(filename, a.Palette)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -117,7 +117,7 @@ func (o *Object) Sprite(idx int) (*Sprite, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
raw := o.raw.Sprites[idx]
|
raw := o.raw.Sprites[idx]
|
||||||
img, err := ebiten.NewImageFromImage(raw.ToImage(), ebiten.FilterDefault)
|
img, err := ebiten.NewImageFromImage(raw.ToImage(o.assets.Palette), ebiten.FilterDefault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -2,15 +2,16 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ordoor struct {
|
type Engine struct {
|
||||||
DataDir string `toml:"data_dir"`
|
DataDir string `toml:"data_dir"`
|
||||||
VideoPlayer []string `toml:"video_player"`
|
Palette string `toml:"palette"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Things set in the options hash
|
// Things set in the options hash
|
||||||
@@ -38,12 +39,40 @@ type Options struct {
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
filename string `toml:"-"`
|
filename string `toml:"-"`
|
||||||
|
|
||||||
|
VideoPlayer []string `toml:"video_player"`
|
||||||
|
Engines map[string]Engine `toml:"engines"`
|
||||||
|
|
||||||
|
DefaultEngineName string `toml:"default_engine"`
|
||||||
|
|
||||||
|
// FIXME: options may well end up being per-engine too
|
||||||
Defaults *Options `toml:"-"`
|
Defaults *Options `toml:"-"`
|
||||||
Ordoor `toml:"ordoor"`
|
|
||||||
Options `toml:"options"`
|
Options `toml:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(filename string) (*Config, error) {
|
func (c *Config) Engine(name string) *Engine {
|
||||||
|
engine, ok := c.Engines[name]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) DefaultEngine() *Engine {
|
||||||
|
return c.Engine(c.DefaultEngineName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: case-insensitive lookup
|
||||||
|
func (c *Config) DataFile(engine string, path string) string {
|
||||||
|
cfg, ok := c.Engines[engine]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(cfg.DataDir, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(filename string, overrideDefaultEngine string) (*Config, error) {
|
||||||
var out Config
|
var out Config
|
||||||
|
|
||||||
_, err := toml.DecodeFile(filename, &out)
|
_, err := toml.DecodeFile(filename, &out)
|
||||||
@@ -53,7 +82,15 @@ func Load(filename string) (*Config, error) {
|
|||||||
|
|
||||||
out.filename = filename
|
out.filename = filename
|
||||||
|
|
||||||
return &out, err
|
if overrideDefaultEngine != "" {
|
||||||
|
out.DefaultEngineName = overrideDefaultEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
if out.DefaultEngine() == nil {
|
||||||
|
return nil, fmt.Errorf("Default engine %q not configured", out.DefaultEngineName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) HasUnsetOptions() bool {
|
func (c *Config) HasUnsetOptions() bool {
|
||||||
@@ -72,11 +109,6 @@ func (c *Config) Save() error {
|
|||||||
return toml.NewEncoder(f).Encode(c)
|
return toml.NewEncoder(f).Encode(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: case-insensitive lookup
|
|
||||||
func (c *Config) DataFile(path string) string {
|
|
||||||
return filepath.Join(c.DataDir, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) ResetDefaults() error {
|
func (c *Config) ResetDefaults() error {
|
||||||
if c.Defaults == nil {
|
if c.Defaults == nil {
|
||||||
return errors.New("Defaults not available")
|
return errors.New("Defaults not available")
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
@@ -12,7 +13,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/data/rle"
|
"code.ur.gs/lupine/ordoor/internal/data/rle"
|
||||||
"code.ur.gs/lupine/ordoor/internal/palettes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SpriteHeader struct {
|
type SpriteHeader struct {
|
||||||
@@ -53,12 +53,12 @@ type Sprite struct {
|
|||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sprite) ToImage() *image.Paletted {
|
func (s *Sprite) ToImage(palette color.Palette) *image.Paletted {
|
||||||
return &image.Paletted{
|
return &image.Paletted{
|
||||||
Pix: s.Data,
|
Pix: s.Data,
|
||||||
Stride: int(s.Width),
|
Stride: int(s.Width),
|
||||||
Rect: image.Rect(0, 0, int(s.Width), int(s.Height)),
|
Rect: image.Rect(0, 0, int(s.Width), int(s.Height)),
|
||||||
Palette: palettes.DefaultPalette(),
|
Palette: palette,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
|
||||||
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`
|
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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,7 +9,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/palettes"
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,7 +142,7 @@ func (p *Properties) Point() image.Point {
|
|||||||
return image.Point{}
|
return image.Point{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadMenu(filename string) (*Menu, error) {
|
func LoadMenu(filename string, palette color.Palette) (*Menu, error) {
|
||||||
name := filepath.Base(filename)
|
name := filepath.Base(filename)
|
||||||
name = strings.TrimSuffix(name, filepath.Ext(name))
|
name = strings.TrimSuffix(name, filepath.Ext(name))
|
||||||
name = strings.ToLower(name)
|
name = strings.ToLower(name)
|
||||||
@@ -163,7 +162,7 @@ func LoadMenu(filename string) (*Menu, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := loadProperties(out, scanner); err != nil {
|
if err := loadProperties(out, scanner, palette); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +188,7 @@ func loadObjects(menu *Menu, scanner *asciiscan.Scanner) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
|
func loadProperties(menu *Menu, scanner *asciiscan.Scanner, palette color.Palette) error {
|
||||||
for {
|
for {
|
||||||
ok, err := scanner.PeekProperty()
|
ok, err := scanner.PeekProperty()
|
||||||
|
|
||||||
@@ -218,9 +217,9 @@ func loadProperties(menu *Menu, scanner *asciiscan.Scanner) error {
|
|||||||
|
|
||||||
switch strings.ToUpper(k) {
|
switch strings.ToUpper(k) {
|
||||||
case "BACKGROUND COLOR":
|
case "BACKGROUND COLOR":
|
||||||
menu.BackgroundColor = palettes.DefaultPalette()[vInt]
|
menu.BackgroundColor = palette[vInt]
|
||||||
case "HYPERTEXT COLOR":
|
case "HYPERTEXT COLOR":
|
||||||
menu.HypertextColor = palettes.DefaultPalette()[vInt]
|
menu.HypertextColor = palette[vInt]
|
||||||
case "FONT TYPE":
|
case "FONT TYPE":
|
||||||
menu.FontType = vInt
|
menu.FontType = vInt
|
||||||
default:
|
default:
|
||||||
@@ -319,7 +318,7 @@ func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadMenus(dir string) (map[string]*Menu, error) {
|
func LoadMenus(dir string, palette color.Palette) (map[string]*Menu, error) {
|
||||||
fis, err := ioutil.ReadDir(dir)
|
fis, err := ioutil.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -337,7 +336,7 @@ func LoadMenus(dir string) (map[string]*Menu, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
built, err := LoadMenu(filepath.Join(dir, relname))
|
built, err := LoadMenu(filepath.Join(dir, relname), palette)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err)
|
return nil, fmt.Errorf("%s: %v", filepath.Join(dir, relname), err)
|
||||||
}
|
}
|
||||||
|
@@ -34,12 +34,12 @@ type Ordoor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Run(configFile string, overrideX, overrideY int) error {
|
func Run(configFile string, overrideX, overrideY int) error {
|
||||||
cfg, err := config.Load(configFile)
|
cfg, err := config.Load(configFile, "ordoor")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Couldn't load config file: %v", err)
|
return fmt.Errorf("Couldn't load config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assets, err := assetstore.New(cfg.Ordoor.DataDir)
|
assets, err := assetstore.New(cfg.DefaultEngine())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to initialize asset store: %v", err)
|
return fmt.Errorf("Failed to initialize asset store: %v", err)
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (o *Ordoor) PlayVideo(name string, skippable bool) {
|
func (o *Ordoor) PlayVideo(name string, skippable bool) {
|
||||||
filename := o.config.DataFile("SMK/" + name + ".smk")
|
filename := o.config.DataFile("ordoor", "SMK/"+name+".smk")
|
||||||
|
|
||||||
if len(o.config.VideoPlayer) == 0 {
|
if len(o.config.VideoPlayer) == 0 {
|
||||||
log.Printf("Video player not configured, skipping video %v", filename)
|
log.Printf("Video player not configured, skipping video %v", filename)
|
||||||
|
@@ -5,9 +5,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Override this to change the palette globally
|
|
||||||
const DefaultPaletteName = "ChaosGate"
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
||||||
|
|
||||||
@@ -16,6 +13,8 @@ var (
|
|||||||
initPalettes = sync.Once{}
|
initPalettes = sync.Once{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultPalette() color.Palette {
|
func Get(name string) (color.Palette, bool) {
|
||||||
return Palettes[DefaultPaletteName]
|
p, ok := Palettes[name]
|
||||||
|
|
||||||
|
return p, ok
|
||||||
}
|
}
|
||||||
|
@@ -1,34 +0,0 @@
|
|||||||
#!/usr/bin/env ruby
|
|
||||||
|
|
||||||
lines = File.read("palette.ppm").split("\n")
|
|
||||||
lines = lines.select { |l| l[0] != "#" }
|
|
||||||
|
|
||||||
raise "Not an ASCII ppm" if lines.shift != "P3"
|
|
||||||
raise "Incorrect dimensions" unless lines.shift == "1 256"
|
|
||||||
raise "Incorrect maxval" unless lines.shift == "255"
|
|
||||||
raise "Too many lines left" unless lines.size == 768
|
|
||||||
|
|
||||||
puts <<EOF
|
|
||||||
package data
|
|
||||||
|
|
||||||
import "image/color"
|
|
||||||
|
|
||||||
var (
|
|
||||||
Transparent = color.RGBA{R: 0, G: 0, B: 0, A: 0}
|
|
||||||
|
|
||||||
ColorPalette = color.Palette{
|
|
||||||
Transparent,
|
|
||||||
EOF
|
|
||||||
|
|
||||||
lines.shift(3) # Ignore idx 0
|
|
||||||
|
|
||||||
255.times do
|
|
||||||
r, g, b = lines.shift(3).map(&:to_i)
|
|
||||||
|
|
||||||
puts "\t\tcolor.RGBA{R: #{r}, G: #{g}, B: #{b}, A: 255},"
|
|
||||||
end
|
|
||||||
|
|
||||||
puts <<EOF
|
|
||||||
}
|
|
||||||
)
|
|
||||||
EOF
|
|
@@ -1,12 +0,0 @@
|
|||||||
# jungle floor
|
|
||||||
# jungtil.obj/.asn
|
|
||||||
# /--> d:\warflics\missions\jungtil.flc
|
|
||||||
#
|
|
||||||
|
|
||||||
0:DEF 2;
|
|
||||||
|
|
||||||
0:TYPE 2;
|
|
||||||
|
|
||||||
END OF FILE
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,772 +0,0 @@
|
|||||||
P3
|
|
||||||
# CREATOR: GIMP PNM Filter Version 1.1
|
|
||||||
1 256
|
|
||||||
255
|
|
||||||
255
|
|
||||||
255
|
|
||||||
255
|
|
||||||
128
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
128
|
|
||||||
0
|
|
||||||
128
|
|
||||||
128
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
128
|
|
||||||
128
|
|
||||||
0
|
|
||||||
128
|
|
||||||
0
|
|
||||||
128
|
|
||||||
128
|
|
||||||
192
|
|
||||||
192
|
|
||||||
192
|
|
||||||
192
|
|
||||||
220
|
|
||||||
192
|
|
||||||
166
|
|
||||||
202
|
|
||||||
240
|
|
||||||
255
|
|
||||||
255
|
|
||||||
255
|
|
||||||
240
|
|
||||||
240
|
|
||||||
240
|
|
||||||
221
|
|
||||||
221
|
|
||||||
221
|
|
||||||
203
|
|
||||||
203
|
|
||||||
203
|
|
||||||
187
|
|
||||||
187
|
|
||||||
187
|
|
||||||
178
|
|
||||||
178
|
|
||||||
178
|
|
||||||
168
|
|
||||||
168
|
|
||||||
168
|
|
||||||
157
|
|
||||||
157
|
|
||||||
157
|
|
||||||
147
|
|
||||||
147
|
|
||||||
147
|
|
||||||
137
|
|
||||||
137
|
|
||||||
137
|
|
||||||
127
|
|
||||||
127
|
|
||||||
127
|
|
||||||
117
|
|
||||||
117
|
|
||||||
117
|
|
||||||
106
|
|
||||||
106
|
|
||||||
106
|
|
||||||
96
|
|
||||||
96
|
|
||||||
96
|
|
||||||
86
|
|
||||||
86
|
|
||||||
86
|
|
||||||
76
|
|
||||||
76
|
|
||||||
76
|
|
||||||
61
|
|
||||||
61
|
|
||||||
61
|
|
||||||
49
|
|
||||||
49
|
|
||||||
49
|
|
||||||
36
|
|
||||||
36
|
|
||||||
36
|
|
||||||
24
|
|
||||||
24
|
|
||||||
24
|
|
||||||
12
|
|
||||||
12
|
|
||||||
12
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
134
|
|
||||||
134
|
|
||||||
255
|
|
||||||
113
|
|
||||||
113
|
|
||||||
241
|
|
||||||
93
|
|
||||||
93
|
|
||||||
228
|
|
||||||
72
|
|
||||||
72
|
|
||||||
214
|
|
||||||
63
|
|
||||||
63
|
|
||||||
200
|
|
||||||
55
|
|
||||||
55
|
|
||||||
186
|
|
||||||
46
|
|
||||||
46
|
|
||||||
172
|
|
||||||
38
|
|
||||||
38
|
|
||||||
158
|
|
||||||
29
|
|
||||||
29
|
|
||||||
144
|
|
||||||
20
|
|
||||||
20
|
|
||||||
131
|
|
||||||
12
|
|
||||||
12
|
|
||||||
117
|
|
||||||
3
|
|
||||||
3
|
|
||||||
103
|
|
||||||
3
|
|
||||||
3
|
|
||||||
91
|
|
||||||
3
|
|
||||||
3
|
|
||||||
79
|
|
||||||
3
|
|
||||||
3
|
|
||||||
68
|
|
||||||
3
|
|
||||||
3
|
|
||||||
56
|
|
||||||
255
|
|
||||||
145
|
|
||||||
145
|
|
||||||
242
|
|
||||||
123
|
|
||||||
123
|
|
||||||
230
|
|
||||||
101
|
|
||||||
101
|
|
||||||
217
|
|
||||||
79
|
|
||||||
79
|
|
||||||
205
|
|
||||||
70
|
|
||||||
70
|
|
||||||
193
|
|
||||||
61
|
|
||||||
61
|
|
||||||
181
|
|
||||||
53
|
|
||||||
53
|
|
||||||
169
|
|
||||||
44
|
|
||||||
44
|
|
||||||
157
|
|
||||||
35
|
|
||||||
35
|
|
||||||
144
|
|
||||||
26
|
|
||||||
26
|
|
||||||
132
|
|
||||||
18
|
|
||||||
18
|
|
||||||
120
|
|
||||||
9
|
|
||||||
9
|
|
||||||
108
|
|
||||||
8
|
|
||||||
8
|
|
||||||
94
|
|
||||||
7
|
|
||||||
7
|
|
||||||
79
|
|
||||||
7
|
|
||||||
7
|
|
||||||
65
|
|
||||||
6
|
|
||||||
6
|
|
||||||
147
|
|
||||||
142
|
|
||||||
185
|
|
||||||
132
|
|
||||||
126
|
|
||||||
172
|
|
||||||
117
|
|
||||||
109
|
|
||||||
159
|
|
||||||
102
|
|
||||||
93
|
|
||||||
146
|
|
||||||
95
|
|
||||||
86
|
|
||||||
133
|
|
||||||
88
|
|
||||||
78
|
|
||||||
123
|
|
||||||
82
|
|
||||||
73
|
|
||||||
115
|
|
||||||
77
|
|
||||||
67
|
|
||||||
107
|
|
||||||
72
|
|
||||||
61
|
|
||||||
100
|
|
||||||
67
|
|
||||||
55
|
|
||||||
92
|
|
||||||
61
|
|
||||||
50
|
|
||||||
84
|
|
||||||
56
|
|
||||||
44
|
|
||||||
76
|
|
||||||
51
|
|
||||||
38
|
|
||||||
68
|
|
||||||
46
|
|
||||||
32
|
|
||||||
60
|
|
||||||
40
|
|
||||||
27
|
|
||||||
52
|
|
||||||
35
|
|
||||||
21
|
|
||||||
44
|
|
||||||
200
|
|
||||||
150
|
|
||||||
137
|
|
||||||
187
|
|
||||||
130
|
|
||||||
115
|
|
||||||
175
|
|
||||||
110
|
|
||||||
94
|
|
||||||
164
|
|
||||||
95
|
|
||||||
77
|
|
||||||
154
|
|
||||||
79
|
|
||||||
61
|
|
||||||
143
|
|
||||||
64
|
|
||||||
44
|
|
||||||
137
|
|
||||||
60
|
|
||||||
42
|
|
||||||
132
|
|
||||||
55
|
|
||||||
38
|
|
||||||
125
|
|
||||||
50
|
|
||||||
33
|
|
||||||
120
|
|
||||||
48
|
|
||||||
29
|
|
||||||
111
|
|
||||||
44
|
|
||||||
26
|
|
||||||
103
|
|
||||||
39
|
|
||||||
24
|
|
||||||
94
|
|
||||||
35
|
|
||||||
21
|
|
||||||
83
|
|
||||||
30
|
|
||||||
18
|
|
||||||
72
|
|
||||||
25
|
|
||||||
14
|
|
||||||
61
|
|
||||||
20
|
|
||||||
10
|
|
||||||
121
|
|
||||||
107
|
|
||||||
34
|
|
||||||
109
|
|
||||||
94
|
|
||||||
29
|
|
||||||
96
|
|
||||||
82
|
|
||||||
25
|
|
||||||
84
|
|
||||||
69
|
|
||||||
20
|
|
||||||
77
|
|
||||||
62
|
|
||||||
17
|
|
||||||
70
|
|
||||||
55
|
|
||||||
14
|
|
||||||
63
|
|
||||||
47
|
|
||||||
12
|
|
||||||
56
|
|
||||||
40
|
|
||||||
9
|
|
||||||
93
|
|
||||||
120
|
|
||||||
53
|
|
||||||
80
|
|
||||||
103
|
|
||||||
42
|
|
||||||
66
|
|
||||||
86
|
|
||||||
31
|
|
||||||
53
|
|
||||||
69
|
|
||||||
20
|
|
||||||
49
|
|
||||||
60
|
|
||||||
16
|
|
||||||
45
|
|
||||||
52
|
|
||||||
12
|
|
||||||
43
|
|
||||||
44
|
|
||||||
10
|
|
||||||
43
|
|
||||||
38
|
|
||||||
8
|
|
||||||
136
|
|
||||||
145
|
|
||||||
44
|
|
||||||
118
|
|
||||||
128
|
|
||||||
37
|
|
||||||
101
|
|
||||||
111
|
|
||||||
30
|
|
||||||
83
|
|
||||||
94
|
|
||||||
23
|
|
||||||
70
|
|
||||||
79
|
|
||||||
17
|
|
||||||
56
|
|
||||||
65
|
|
||||||
11
|
|
||||||
42
|
|
||||||
50
|
|
||||||
6
|
|
||||||
28
|
|
||||||
36
|
|
||||||
0
|
|
||||||
57
|
|
||||||
134
|
|
||||||
64
|
|
||||||
48
|
|
||||||
118
|
|
||||||
54
|
|
||||||
38
|
|
||||||
101
|
|
||||||
43
|
|
||||||
29
|
|
||||||
85
|
|
||||||
33
|
|
||||||
22
|
|
||||||
71
|
|
||||||
25
|
|
||||||
15
|
|
||||||
58
|
|
||||||
17
|
|
||||||
7
|
|
||||||
45
|
|
||||||
8
|
|
||||||
0
|
|
||||||
32
|
|
||||||
0
|
|
||||||
143
|
|
||||||
87
|
|
||||||
56
|
|
||||||
126
|
|
||||||
75
|
|
||||||
45
|
|
||||||
110
|
|
||||||
64
|
|
||||||
35
|
|
||||||
93
|
|
||||||
52
|
|
||||||
24
|
|
||||||
85
|
|
||||||
44
|
|
||||||
16
|
|
||||||
72
|
|
||||||
36
|
|
||||||
12
|
|
||||||
64
|
|
||||||
32
|
|
||||||
8
|
|
||||||
56
|
|
||||||
24
|
|
||||||
4
|
|
||||||
127
|
|
||||||
96
|
|
||||||
54
|
|
||||||
115
|
|
||||||
85
|
|
||||||
46
|
|
||||||
102
|
|
||||||
75
|
|
||||||
39
|
|
||||||
90
|
|
||||||
64
|
|
||||||
31
|
|
||||||
82
|
|
||||||
57
|
|
||||||
25
|
|
||||||
75
|
|
||||||
51
|
|
||||||
20
|
|
||||||
68
|
|
||||||
44
|
|
||||||
15
|
|
||||||
61
|
|
||||||
38
|
|
||||||
10
|
|
||||||
141
|
|
||||||
86
|
|
||||||
56
|
|
||||||
126
|
|
||||||
75
|
|
||||||
46
|
|
||||||
110
|
|
||||||
65
|
|
||||||
36
|
|
||||||
95
|
|
||||||
54
|
|
||||||
26
|
|
||||||
88
|
|
||||||
51
|
|
||||||
25
|
|
||||||
73
|
|
||||||
40
|
|
||||||
18
|
|
||||||
57
|
|
||||||
29
|
|
||||||
10
|
|
||||||
42
|
|
||||||
18
|
|
||||||
3
|
|
||||||
172
|
|
||||||
199
|
|
||||||
199
|
|
||||||
138
|
|
||||||
173
|
|
||||||
173
|
|
||||||
104
|
|
||||||
148
|
|
||||||
148
|
|
||||||
71
|
|
||||||
122
|
|
||||||
122
|
|
||||||
37
|
|
||||||
97
|
|
||||||
97
|
|
||||||
3
|
|
||||||
71
|
|
||||||
71
|
|
||||||
4
|
|
||||||
56
|
|
||||||
56
|
|
||||||
4
|
|
||||||
41
|
|
||||||
41
|
|
||||||
217
|
|
||||||
209
|
|
||||||
200
|
|
||||||
202
|
|
||||||
194
|
|
||||||
184
|
|
||||||
188
|
|
||||||
178
|
|
||||||
167
|
|
||||||
173
|
|
||||||
163
|
|
||||||
151
|
|
||||||
158
|
|
||||||
147
|
|
||||||
134
|
|
||||||
148
|
|
||||||
136
|
|
||||||
123
|
|
||||||
137
|
|
||||||
125
|
|
||||||
112
|
|
||||||
126
|
|
||||||
114
|
|
||||||
101
|
|
||||||
116
|
|
||||||
104
|
|
||||||
91
|
|
||||||
105
|
|
||||||
93
|
|
||||||
80
|
|
||||||
94
|
|
||||||
82
|
|
||||||
69
|
|
||||||
84
|
|
||||||
71
|
|
||||||
58
|
|
||||||
73
|
|
||||||
60
|
|
||||||
47
|
|
||||||
62
|
|
||||||
50
|
|
||||||
36
|
|
||||||
52
|
|
||||||
39
|
|
||||||
25
|
|
||||||
41
|
|
||||||
28
|
|
||||||
14
|
|
||||||
231
|
|
||||||
232
|
|
||||||
207
|
|
||||||
219
|
|
||||||
217
|
|
||||||
180
|
|
||||||
208
|
|
||||||
201
|
|
||||||
152
|
|
||||||
196
|
|
||||||
186
|
|
||||||
125
|
|
||||||
184
|
|
||||||
171
|
|
||||||
98
|
|
||||||
173
|
|
||||||
155
|
|
||||||
70
|
|
||||||
161
|
|
||||||
140
|
|
||||||
43
|
|
||||||
150
|
|
||||||
129
|
|
||||||
39
|
|
||||||
139
|
|
||||||
119
|
|
||||||
37
|
|
||||||
127
|
|
||||||
109
|
|
||||||
33
|
|
||||||
117
|
|
||||||
99
|
|
||||||
29
|
|
||||||
105
|
|
||||||
89
|
|
||||||
25
|
|
||||||
90
|
|
||||||
76
|
|
||||||
21
|
|
||||||
75
|
|
||||||
62
|
|
||||||
18
|
|
||||||
60
|
|
||||||
49
|
|
||||||
14
|
|
||||||
45
|
|
||||||
35
|
|
||||||
10
|
|
||||||
128
|
|
||||||
99
|
|
||||||
127
|
|
||||||
113
|
|
||||||
73
|
|
||||||
112
|
|
||||||
97
|
|
||||||
47
|
|
||||||
97
|
|
||||||
82
|
|
||||||
21
|
|
||||||
82
|
|
||||||
75
|
|
||||||
2
|
|
||||||
74
|
|
||||||
68
|
|
||||||
2
|
|
||||||
67
|
|
||||||
52
|
|
||||||
3
|
|
||||||
50
|
|
||||||
35
|
|
||||||
4
|
|
||||||
33
|
|
||||||
247
|
|
||||||
178
|
|
||||||
102
|
|
||||||
229
|
|
||||||
152
|
|
||||||
75
|
|
||||||
212
|
|
||||||
127
|
|
||||||
48
|
|
||||||
194
|
|
||||||
101
|
|
||||||
21
|
|
||||||
179
|
|
||||||
87
|
|
||||||
16
|
|
||||||
161
|
|
||||||
73
|
|
||||||
12
|
|
||||||
142
|
|
||||||
59
|
|
||||||
9
|
|
||||||
124
|
|
||||||
45
|
|
||||||
5
|
|
||||||
255
|
|
||||||
0
|
|
||||||
0
|
|
||||||
194
|
|
||||||
3
|
|
||||||
3
|
|
||||||
161
|
|
||||||
2
|
|
||||||
2
|
|
||||||
255
|
|
||||||
227
|
|
||||||
11
|
|
||||||
209
|
|
||||||
185
|
|
||||||
8
|
|
||||||
169
|
|
||||||
150
|
|
||||||
6
|
|
||||||
103
|
|
||||||
190
|
|
||||||
255
|
|
||||||
2
|
|
||||||
130
|
|
||||||
232
|
|
||||||
4
|
|
||||||
4
|
|
||||||
209
|
|
||||||
0
|
|
||||||
255
|
|
||||||
0
|
|
||||||
7
|
|
||||||
180
|
|
||||||
7
|
|
||||||
3
|
|
||||||
132
|
|
||||||
3
|
|
||||||
255
|
|
||||||
114
|
|
||||||
230
|
|
||||||
255
|
|
||||||
17
|
|
||||||
205
|
|
||||||
203
|
|
||||||
6
|
|
||||||
156
|
|
||||||
246
|
|
||||||
164
|
|
||||||
73
|
|
||||||
221
|
|
||||||
123
|
|
||||||
16
|
|
||||||
177
|
|
||||||
95
|
|
||||||
4
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
132
|
|
||||||
18
|
|
||||||
18
|
|
||||||
113
|
|
||||||
13
|
|
||||||
13
|
|
||||||
94
|
|
||||||
7
|
|
||||||
7
|
|
||||||
107
|
|
||||||
8
|
|
||||||
8
|
|
||||||
120
|
|
||||||
9
|
|
||||||
9
|
|
||||||
193
|
|
||||||
62
|
|
||||||
62
|
|
||||||
181
|
|
||||||
54
|
|
||||||
54
|
|
||||||
168
|
|
||||||
45
|
|
||||||
45
|
|
||||||
157
|
|
||||||
36
|
|
||||||
36
|
|
||||||
144
|
|
||||||
27
|
|
||||||
27
|
|
||||||
62
|
|
||||||
50
|
|
||||||
36
|
|
||||||
57
|
|
||||||
45
|
|
||||||
31
|
|
||||||
52
|
|
||||||
39
|
|
||||||
25
|
|
||||||
46
|
|
||||||
34
|
|
||||||
20
|
|
||||||
41
|
|
||||||
28
|
|
||||||
14
|
|
||||||
255
|
|
||||||
251
|
|
||||||
240
|
|
||||||
160
|
|
||||||
160
|
|
||||||
164
|
|
||||||
128
|
|
||||||
128
|
|
||||||
128
|
|
||||||
255
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
255
|
|
||||||
0
|
|
||||||
255
|
|
||||||
255
|
|
||||||
0
|
|
||||||
0
|
|
||||||
0
|
|
||||||
255
|
|
||||||
255
|
|
||||||
0
|
|
||||||
255
|
|
||||||
0
|
|
||||||
255
|
|
||||||
255
|
|
||||||
255
|
|
||||||
255
|
|
||||||
255
|
|
Binary file not shown.
@@ -1,13 +0,0 @@
|
|||||||
To build this palette, I did the following:
|
|
||||||
|
|
||||||
* Created the .asn + .obj files by hand
|
|
||||||
* Referenced them in a .set file and asked WH40K_TD.exe to render the sprites
|
|
||||||
* Screenshotted the output and used GIMP to create a 1x255 image with them all
|
|
||||||
* Exported that image as an ASCII .ppm file
|
|
||||||
* Used the `generate_palette` script to output Go code \o/
|
|
||||||
|
|
||||||
Note that palette index 0 is ignored, and hardcoded to be transparent black.
|
|
||||||
This is because 0x00 seems to be used as a record separator in .obj files, so
|
|
||||||
can't be used as a palette index anyway.
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
Reference in New Issue
Block a user