Initial commit
I can parse some of the data files, and extract individual frames from .obj files. I can't yet convert those frames into something viewable.
This commit is contained in:
52
internal/data/accounting.go
Normal file
52
internal/data/accounting.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Comments are `//`, start of line only, for accounting
|
||||
var (
|
||||
comment = []byte("//")
|
||||
)
|
||||
|
||||
type Accounting map[string]string
|
||||
|
||||
func LoadAccounting(filename string) (Accounting, error) {
|
||||
scanLines, err := fileToScanner(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(Accounting)
|
||||
|
||||
for scanLines.Scan() {
|
||||
if err := parse(out, scanLines.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanLines.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parse(into Accounting, line []byte) error {
|
||||
if len(line) == 0 || bytes.Equal(line[0:2], comment) {
|
||||
return nil
|
||||
}
|
||||
|
||||
idx := bytes.Index(line, []byte("="))
|
||||
key := string(line[0:idx])
|
||||
value := string(bytes.Trim(line[idx+1:len(line)], "\t\r\n"))
|
||||
|
||||
if _, ok := into[key]; ok {
|
||||
return errors.New("Duplicate key: " + key)
|
||||
}
|
||||
|
||||
into[key] = value
|
||||
|
||||
return nil
|
||||
}
|
127
internal/data/animated_object_definitions.go
Normal file
127
internal/data/animated_object_definitions.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
// "fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Cell struct {
|
||||
X int // FIXME: This is just a guess, but I think it's a 3D coordinate system
|
||||
Y int
|
||||
Z int
|
||||
}
|
||||
|
||||
type AnimatedObject struct {
|
||||
CentreCell Cell
|
||||
Direction int
|
||||
Type int
|
||||
AnimationID int
|
||||
AnimationGroup int
|
||||
AnimatedType int
|
||||
AnimationIDs CompassPoints // 8 of these, down to MoveBits. Compass points?
|
||||
InCells CompassPoints
|
||||
VisualBits CompassPoints
|
||||
MoveBits CompassPoints
|
||||
Visibility int
|
||||
Protection int
|
||||
MinDelay int // in milliseconds
|
||||
DelayRange int // in milliseconds
|
||||
}
|
||||
|
||||
func LoadAnimatedObjectDefinitions(filename string) ([]AnimatedObject, error) {
|
||||
var out []AnimatedObject
|
||||
|
||||
scanLines, err := fileToScanner(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
obj, err := consumeAnimatedObjectDefinition(scanLines)
|
||||
if err == io.EOF {
|
||||
return out, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out = append(out, obj)
|
||||
}
|
||||
}
|
||||
|
||||
func consumeAnimatedObjectDefinition(scanner *bufio.Scanner) (AnimatedObject, error) {
|
||||
var out AnimatedObject
|
||||
var err error
|
||||
|
||||
out.CentreCell, err = consumeCell(scanner)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
if err := consumeIntPtrs(
|
||||
scanner,
|
||||
&out.Direction, &out.Type, &out.AnimationID, &out.AnimationGroup, &out.AnimatedType,
|
||||
); err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
// 8-int arrays
|
||||
out.AnimationIDs, err = consumeCompassPoints(scanner)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
out.InCells, err = consumeCompassPoints(scanner)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
out.VisualBits, err = consumeCompassPoints(scanner)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
out.MoveBits, err = consumeCompassPoints(scanner)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
if err := consumeIntPtrs(
|
||||
scanner,
|
||||
&out.Visibility, &out.Protection, &out.MinDelay, &out.DelayRange,
|
||||
); err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func consumeCell(scanner *bufio.Scanner) (Cell, error) {
|
||||
out := Cell{}
|
||||
str, err := consumeString(scanner)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
parts := strings.Split(str, " ")
|
||||
if len(parts) != 3 {
|
||||
return out, errors.New("Malformed cell definition")
|
||||
}
|
||||
|
||||
if out.X, err = strconv.Atoi(parts[0]); err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
if out.Y, err = strconv.Atoi(parts[1]); err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
if out.Z, err = strconv.Atoi(parts[2]); err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
107
internal/data/data.go
Normal file
107
internal/data/data.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var hashComment = []byte("#")
|
||||
|
||||
type CompassPoints struct {
|
||||
N int
|
||||
NE int
|
||||
E int
|
||||
SE int
|
||||
S int
|
||||
SW int
|
||||
W int
|
||||
NW int
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
Start int
|
||||
End int
|
||||
}
|
||||
|
||||
func fileToScanner(filename string) (*bufio.Scanner, error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bufio.NewScanner(bytes.NewReader(data)), nil
|
||||
}
|
||||
|
||||
func consumeString(scanner *bufio.Scanner) (string, error) {
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
|
||||
if len(line) == 0 || line[0] == hashComment[0] { // Most .dat files use # for comments
|
||||
continue
|
||||
}
|
||||
|
||||
comment := bytes.Index(line, hashComment)
|
||||
if comment > 0 {
|
||||
line = line[0:comment]
|
||||
}
|
||||
|
||||
return string(bytes.TrimRight(line, "\r\n\t ")), nil
|
||||
}
|
||||
|
||||
err := scanner.Err()
|
||||
if err == nil {
|
||||
return "", io.EOF
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
func consumeInt(scanner *bufio.Scanner) (int, error) {
|
||||
str, err := consumeString(scanner)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(str)
|
||||
}
|
||||
|
||||
func consumeIntPtr(to *int, scanner *bufio.Scanner) error {
|
||||
val, err := consumeInt(scanner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*to = val
|
||||
return nil
|
||||
}
|
||||
|
||||
func consumeIntPtrs(scanner *bufio.Scanner, ptrs ...*int) error {
|
||||
for _, ptr := range ptrs {
|
||||
if err := consumeIntPtr(ptr, scanner); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func consumeRange(scanner *bufio.Scanner) (Range, error) {
|
||||
var out Range
|
||||
|
||||
err := consumeIntPtrs(scanner, &out.Start, &out.End)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func consumeCompassPoints(scanner *bufio.Scanner) (CompassPoints, error) {
|
||||
var out CompassPoints
|
||||
|
||||
err := consumeIntPtrs(
|
||||
scanner,
|
||||
&out.N, &out.NE, &out.E, &out.SE, &out.S, &out.SW, &out.W, &out.NW,
|
||||
)
|
||||
|
||||
return out, err
|
||||
}
|
339
internal/data/generic.go
Normal file
339
internal/data/generic.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package data
|
||||
|
||||
type ActionPointEnum int
|
||||
type SettingEnum int
|
||||
type TransitionFrameEnum int
|
||||
type SquadTypeEnum int // just for the multiplayer choose units menu
|
||||
type VehicleTypeEnum int // just for the multiplayer choose units screen
|
||||
type OptionEnum int // differs from settings in that these are defaults changeable from the UI
|
||||
type SpellEnum int
|
||||
type AIPriorityEnum int // FIXME: no idea what this is for
|
||||
type RngRangeEnum int
|
||||
|
||||
// VEHICLE_WALK_TRANSITION_FRAME entries
|
||||
|
||||
const (
|
||||
APCastSpell ActionPointEnum = 0
|
||||
APWalk = 1
|
||||
APRun = 2
|
||||
APTurn = 3
|
||||
APCrouch = 4
|
||||
APStand = 5
|
||||
APOpenDoor = 6
|
||||
APDropItem = 7
|
||||
APPickupItem = 8
|
||||
APThrowItem = 9
|
||||
APUseMedikit = 10
|
||||
APUseScanner = 11
|
||||
APJump = 12
|
||||
APThrowGrenade = 13
|
||||
APHalfTurn = 14
|
||||
APToClearJammedWeapon = 15
|
||||
APHandToHandRound = 16
|
||||
APToUseSwitch = 17
|
||||
APToUseKey = 18
|
||||
APToBoardDisembark = 19
|
||||
APWhenOnFire = 20
|
||||
APWhenBroken = 21
|
||||
APMax = 22
|
||||
|
||||
SettingOccasionalSoundPercent SettingEnum = 0
|
||||
SettingOccasionalMovementSound = 1
|
||||
SettingFullJumpFuel = 2
|
||||
SettingMaxJumpDistance = 3
|
||||
SettingFlyFramesPerCell = 4
|
||||
SettingPowerArmorRating = 5
|
||||
SettingTerminatorArmorRating = 6
|
||||
SettingBloodThirsterArmorRating = 7
|
||||
SettingDemonicArmorRating = 8
|
||||
SettingPrimitiveArmorRating = 9
|
||||
SettingHorrorArmorRating = 10
|
||||
SettingArmorDice = 11
|
||||
SettingJumpPixelsPerFrame = 12
|
||||
SettingCharacterAnimationDelay = 13
|
||||
SettingVehicleAnimationDelay = 14
|
||||
SettingLoopingEffectsDelay = 15
|
||||
SettingFireAnimationDelay = 16
|
||||
SettingCharacterFireTransparency = 17
|
||||
SettingMissileDrawOffset = 18 // pixels per missing piece
|
||||
SettingWeaponJamPercent = 19 // to two decimal places
|
||||
SettingScanFrequency = 20
|
||||
SettingCoverSearchRadius = 21
|
||||
SettingDialogAutoCloseTime = 22
|
||||
SettingPointBlankBallisticSkillBonus = 23
|
||||
SettingPointBlankRange = 24
|
||||
SettingMinimumOverwatchShotPercent = 25
|
||||
SettingMultipleSekectDragTolerance = 26
|
||||
SettingStandingGuardPostRadius = 27
|
||||
SettingMaxDemonFlyDistance = 28
|
||||
SettingSpellLoopingVolume = 29 // percentage, 0 decimal places
|
||||
SettingPercentOfChaosInSecondaryQuadrant = 30
|
||||
SettingNumberOfRandomCrates = 31
|
||||
SettingMax = 32
|
||||
|
||||
TFDeletedVehicle TransitionFrameEnum = 0
|
||||
TFUltraRhino = 1
|
||||
TFUltraPredator = 2
|
||||
TFUltraLandSpeeder = 3
|
||||
TFUltraDreadnought = 4
|
||||
TFChaosRhino = 5
|
||||
TFChaosPredator = 6
|
||||
TFChaosDreadnought = 7
|
||||
TFMax = 8
|
||||
|
||||
SquadTerminator SquadTypeEnum = 0
|
||||
SquadTactical = 1
|
||||
SquadAssault = 2
|
||||
SquadDevastator = 3
|
||||
SquadChaos = 4
|
||||
SquadChaosKhorne = 5
|
||||
SquadChaosTerminator = 6
|
||||
SquadChaosCultise = 7
|
||||
SquadLibrarian = 8
|
||||
SquadTechmarine = 9
|
||||
SquadApothecary = 10
|
||||
SquadChaplain = 11
|
||||
SquadCaptain = 12
|
||||
SquadChaosLord = 13
|
||||
SquadChaosChaplain = 14
|
||||
SquadChaosSorcerer = 15
|
||||
SauadChaosBlodThirster = 16
|
||||
SquadChaosBlootLetter = 17
|
||||
SquadChaosFleshHound = 18
|
||||
SquadChaosLordOfChange = 19
|
||||
SquadChaosFlamer = 20
|
||||
SquadChaosPinkHorror = 21
|
||||
SquadChaosBlueHorror = 22
|
||||
SquadMax = 23
|
||||
|
||||
VehicleUltraRhino VehicleTypeEnum = 0
|
||||
VehicleUltraPredator = 1
|
||||
VehicleUltraLandSpeeder = 2
|
||||
VehicleUltraDreadnought = 3
|
||||
VehicleChaosRhino = 4
|
||||
VehicleChaosPredator = 5
|
||||
VehicleChaosDreadnought = 6
|
||||
VehicleMax = 7
|
||||
|
||||
OptionMovies OptionEnum = 0
|
||||
OptionMusic = 1
|
||||
OptionCombatVoices = 2
|
||||
OptionGrid = 3
|
||||
OptionShowPaths = 4
|
||||
OptionPointSave = 5
|
||||
OptionAutoCutLevel = 6
|
||||
OptionShowUnitAnimations = 7
|
||||
OptionCombatResolution = 8
|
||||
OptionMusicVolume = 9 // percentage, 0df
|
||||
OptionSoundEffectsVolume = 10 // percentage, 0df
|
||||
OptionUnitAnimationSpeed = 11 // percentage, 0df, see CHARCTER ANIMATION DELAY / VEHICLE ANIMATION DELAY for worst case value
|
||||
OptionEffectAnimationSpeed = 12 // percentage, 0d, see LOOPING_EFFECTS_DELAY for worst case value
|
||||
OptionMax = 13
|
||||
|
||||
SpellHellfire SpellEnum = 0
|
||||
SpellLightningArc = 1
|
||||
SpellGate = 2
|
||||
SpellAssail = 3
|
||||
SpellDisplacement = 4
|
||||
SpellMachineCurse = 5
|
||||
SpellScan = 6
|
||||
SpellQuickening = 7
|
||||
SpellStrengthOfMind = 8
|
||||
SpellSmite = 9
|
||||
SpellIronArm = 10
|
||||
SpellSalamandar = 11
|
||||
SpellTeleport = 12
|
||||
SpellPsychicShield = 13
|
||||
SpellDestroyDemon = 14
|
||||
SpellHolocaust = 15
|
||||
SpellScourge = 16
|
||||
SpellPurge = 17
|
||||
SpellAuraOfFire = 18
|
||||
SpellVortex = 19
|
||||
SpellStormOfWrath = 20
|
||||
SpellAuraOfFortitude = 21
|
||||
SpellPinkFireOfTzeentch = 22
|
||||
SpellBoltOfChange = 23
|
||||
SpellTzeentchFireStore = 24
|
||||
SpellMax = 25
|
||||
|
||||
AIPrioritySpecialCharacter = 0
|
||||
AIPriorityVehicle = 1
|
||||
AIPriorityTerminator = 2
|
||||
AIPriorityHeavyWeapon = 3
|
||||
AIPriorityRegular = 4
|
||||
AIPriorityCultist = 5
|
||||
AIPriorityMax = 6
|
||||
|
||||
RngRangeVehicleHillHeight RngRangeEnum = 0
|
||||
RngRangeVehicleHillWidth = 1
|
||||
RngRangeVehicleHillDensity = 2
|
||||
RngRangeVehicleLargeObjectDensity = 3
|
||||
RngRangeVehicleSmallObjectDensity = 4
|
||||
RngRangeNoVehicleHillHeight = 5
|
||||
RngRangeNoVehicleHillWidth = 6
|
||||
RngRangeNoVehicleHillDensity = 7
|
||||
RngRangeNoVehicleLargeObjectDensity = 8
|
||||
RngRangeNoVehicleSmallObjectDensity = 9
|
||||
RngRangeMax = 10
|
||||
|
||||
// FIXME: is this really a hardcoded value?
|
||||
NumCampaignScenarios = 15
|
||||
)
|
||||
|
||||
type Generic struct {
|
||||
ActionPoints map[ActionPointEnum]int
|
||||
Settings map[SettingEnum]int
|
||||
VehicleWalkTransitionFrames map[TransitionFrameEnum]CompassPoints
|
||||
MPChooseUnitSquadLimits map[SquadTypeEnum]int
|
||||
|
||||
// I'm assuming this is to do with MP becauses squads are. I could be wrong.
|
||||
MPChooseVehicleLimits map[VehicleTypeEnum]int
|
||||
|
||||
Options map[OptionEnum]int
|
||||
|
||||
CampaignMaps []string
|
||||
CampaignEndMissionWavs []string
|
||||
|
||||
AICoherentForce int // Range that defnines a coherent for for the AI. No idea
|
||||
AISpellDeltaPriorityWeights map[SpellEnum]int // AI delta priority weights for each spell
|
||||
AIPriority map[AIPriorityEnum]int
|
||||
AICommandInfiniteLoopCounterWatermark int // Messing with this can cripple the API...
|
||||
|
||||
RngRanges map[RngRangeEnum]Range
|
||||
}
|
||||
|
||||
// TODO: consume these values from the file
|
||||
func LoadGeneric(filename string) (*Generic, error) {
|
||||
scanLines, err := fileToScanner(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &Generic{
|
||||
ActionPoints: make(map[ActionPointEnum]int),
|
||||
Settings: make(map[SettingEnum]int),
|
||||
VehicleWalkTransitionFrames: make(map[TransitionFrameEnum]CompassPoints),
|
||||
MPChooseUnitSquadLimits: make(map[SquadTypeEnum]int),
|
||||
MPChooseVehicleLimits: make(map[VehicleTypeEnum]int),
|
||||
Options: make(map[OptionEnum]int),
|
||||
AISpellDeltaPriorityWeights: make(map[SpellEnum]int),
|
||||
AIPriority: make(map[AIPriorityEnum]int),
|
||||
RngRanges: make(map[RngRangeEnum]Range),
|
||||
}
|
||||
|
||||
// Various action point values, first to last
|
||||
for ap := ActionPointEnum(0); ap < APMax; ap++ {
|
||||
val, err := consumeInt(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.ActionPoints[ap] = val
|
||||
}
|
||||
|
||||
// Other miscellaneous data fields, first to last
|
||||
for setting := SettingEnum(0); setting < SettingMax; setting++ {
|
||||
val, err := consumeInt(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.Settings[setting] = val
|
||||
}
|
||||
|
||||
// Vehicle walk transition frames. Whatever they are.
|
||||
for tf := TransitionFrameEnum(0); tf < TFMax; tf++ {
|
||||
val, err := consumeCompassPoints(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.VehicleWalkTransitionFrames[tf] = val
|
||||
}
|
||||
|
||||
for st := SquadTypeEnum(0); st < SquadMax; st++ {
|
||||
val, err := consumeInt(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.MPChooseUnitSquadLimits[st] = val
|
||||
}
|
||||
|
||||
for vt := VehicleTypeEnum(0); vt < VehicleMax; vt++ {
|
||||
val, err := consumeInt(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.MPChooseVehicleLimits[vt] = val
|
||||
}
|
||||
|
||||
for op := OptionEnum(0); op < OptionMax; op++ {
|
||||
val, err := consumeInt(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.Options[op] = val
|
||||
}
|
||||
|
||||
for i := 0; i < NumCampaignScenarios; i++ {
|
||||
val, err := consumeString(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.CampaignMaps = append(out.CampaignMaps, val)
|
||||
}
|
||||
|
||||
for i := 0; i < NumCampaignScenarios; i++ {
|
||||
val, err := consumeString(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME: "none" is a special-case value here
|
||||
out.CampaignEndMissionWavs = append(out.CampaignEndMissionWavs, val)
|
||||
}
|
||||
|
||||
out.AICoherentForce, err = consumeInt(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for sp := SpellEnum(0); sp < SpellMax; sp++ {
|
||||
val, err := consumeInt(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.AISpellDeltaPriorityWeights[sp] = val
|
||||
}
|
||||
|
||||
for ai := AIPriorityEnum(0); ai < AIPriorityMax; ai++ {
|
||||
val, err := consumeInt(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.AIPriority[ai] = val
|
||||
}
|
||||
|
||||
out.AICommandInfiniteLoopCounterWatermark, err = consumeInt(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rg := RngRangeEnum(0); rg < RngRangeMax; rg++ {
|
||||
val, err := consumeRange(scanLines)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.RngRanges[rg] = val
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
267
internal/data/object.go
Normal file
267
internal/data/object.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// FIXME: My poor understanding of the .obj format prevents me from
|
||||
// successfully loading these files
|
||||
objBlacklist = []string{
|
||||
/* Lots of unexpected magic values, disable the check for now
|
||||
"15_rocks.obj", // Unexpected magic value: 19726564 (expected 21102801)
|
||||
"2_cath.obj", // Unexpected magic value: 21364973 (expected 21102801)
|
||||
"2nd_flor.obj", // Unexpected magic value: 22151377 (expected 21102801)
|
||||
"3_cath.obj", // Unexpected magic value: 21102809 (expected 21102801)
|
||||
"4_cath.obj", // Unexpected magic value: 21496017 (expected 21102801)
|
||||
"BODIES.obj", // Unexpected magic value: 21627103 (expected 21102801)
|
||||
"BRDG_TIL.OBJ", // Unexpected magic value: 17957074 (expected 21102801)
|
||||
"Cheveron.obj", // Unexpected magic value: 10879118 (expected 21102801)
|
||||
|
||||
"Heavy_Plasma_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Heavy_Plasma_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Heavy_Plasma_Pain_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Heavy_Plasma_Pain_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
|
||||
"Ht_drt.obj", // Unexpected magic value: 22806737 (expected 21102801)
|
||||
"Ht_grs.obj", // Unexpected magic value: 22806737 (expected 21102801)
|
||||
|
||||
"IVY02.OBJ", // Unexpected magic value: 18481399 (expected 21102801)
|
||||
"J_top2.obj", // Unexpected magic value: 22347993 (expected 21102801)
|
||||
|
||||
"Lascannon_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Lascannon_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Lascannon_Pain_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Lascannon_Pain_Mask.obj", // Unexpected magic value: 14811136 (expected 21102801)
|
||||
|
||||
"Man_Shadow.obj", // Unexpected magic value: 22479091 (expected 21102801)
|
||||
|
||||
"Melta_Effect.obj", // Unexpected magic value: 14745777 (expected 21102801)
|
||||
"Melta_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Melta_Pain_Effect.obj", // Unexpected magic value: 14745777 (expected 21102801)
|
||||
"Melta_Pain_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
|
||||
"Multi_Melta_Effect.obj",
|
||||
"Multi_Melta_Mask.obj",
|
||||
"Multi_Melta_Pain_Effect.obj",
|
||||
"Multi_Melta_Pain_Mask.obj",
|
||||
|
||||
"Plasma_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Plasma_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Plasma_Pain_Effect.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
"Plasma_Pain_Mask.obj", // Unexpected magic value: 14811345 (expected 21102801)
|
||||
|
||||
"TZEENTCH.OBJ", // Unexpected magic value: 18088209 (expected 21102801)
|
||||
|
||||
"altar.obj", // Unexpected magic value: 18219222 (expected 21102801)
|
||||
*/
|
||||
|
||||
"j_tree2.obj", // ObjectHeader is completely empty
|
||||
"inven.obj", // Main header padding contains unknown values: [134744072 134744072 134744072]
|
||||
}
|
||||
objFrameMagic = uint32(0x014200D1)
|
||||
)
|
||||
|
||||
func init() {
|
||||
sort.Strings(objBlacklist)
|
||||
}
|
||||
|
||||
type FrameHeader struct {
|
||||
Magic uint32
|
||||
Width uint16 // FIXME: I'm not certain this is what these are. If they are, they may be the wrong way around
|
||||
Height uint16
|
||||
Padding1 uint32 // I don't think this is used. Could be wrong.
|
||||
PixelSize uint32 // Size of PixelData, excluding this frame header
|
||||
Padding2 uint64 // I don't think this is used either. Could be wrong.
|
||||
}
|
||||
|
||||
func (f FrameHeader) Check(expectedSize uint32) error {
|
||||
// There seem to be different frame types, keyed by the magic value?
|
||||
// if f.Magic != objFrameMagic {
|
||||
// return fmt.Errorf("Unexpected magic value: %d (expected %d)", f.Magic, objFrameMagic)
|
||||
// }
|
||||
|
||||
if f.Padding1 != 0 || f.Padding2 != 0 {
|
||||
return fmt.Errorf("Frame header padding contains unknown values: %d %d", f.Padding1, f.Padding2)
|
||||
}
|
||||
|
||||
// Remove 24 bytes from passed-in size to account for the header
|
||||
if f.PixelSize != expectedSize-24 {
|
||||
return fmt.Errorf("Advertised pixel size: %d differs from expected: %v", f.PixelSize, expectedSize-24)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Frame struct {
|
||||
FrameHeader
|
||||
|
||||
PixelData []byte
|
||||
}
|
||||
|
||||
type frameInfoHeader struct {
|
||||
Offset uint32 // Offset of the frame relative to the frame data segment
|
||||
Size uint32 // Size of the frame in bytes, including the header
|
||||
}
|
||||
|
||||
func (f frameInfoHeader) Check() error {
|
||||
if f.Size < 24 {
|
||||
return fmt.Errorf("Unexpected frame size: %d (expected >= 24)", f.Size)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObjectHeader totals 32 bytes on disk
|
||||
type ObjectHeader struct {
|
||||
NumFrames uint32 // How many frames does this object have?
|
||||
MainHeaderSize uint32 // Number of bytes taken by this header. Should always be `32`
|
||||
FrameInfoSize uint32 // Number of bytes taken up by the next block. 8 * NumFrames
|
||||
FrameDataOffset uint32 // The starting point of the frame data
|
||||
FrameDataSize uint32 // frame data should take up this many bytes
|
||||
|
||||
Padding [3]uint32 // Unused, as far as I can see
|
||||
}
|
||||
|
||||
func (h ObjectHeader) ExpectedFrameInfoSize() uint32 {
|
||||
return h.NumFrames * 8
|
||||
}
|
||||
|
||||
func (h ObjectHeader) Check() error {
|
||||
if h.MainHeaderSize != 32 {
|
||||
return fmt.Errorf("Unexpected main header size: %d (expected 32)", h.MainHeaderSize)
|
||||
}
|
||||
|
||||
if h.ExpectedFrameInfoSize() != h.FrameInfoSize {
|
||||
return fmt.Errorf("Unexpected frame info size: %d (expected %d)", h.FrameInfoSize, h.ExpectedFrameInfoSize())
|
||||
}
|
||||
|
||||
if h.Padding[0] != 0 || h.Padding[1] != 0 || h.Padding[2] != 0 {
|
||||
return fmt.Errorf("Main header padding contains unknown values: %+v", h.Padding)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
ObjectHeader
|
||||
|
||||
Filename string
|
||||
Frames []*Frame
|
||||
}
|
||||
|
||||
func LoadObject(filename string) (*Object, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
out := &Object{Filename: filename}
|
||||
if err := binary.Read(f, binary.LittleEndian, &out.ObjectHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := out.ObjectHeader.Check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now load all frames into memory
|
||||
|
||||
framesInfo := make([]frameInfoHeader, out.NumFrames)
|
||||
if err := binary.Read(f, binary.LittleEndian, &framesInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := f.Seek(int64(out.FrameDataOffset), io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME: this might - *might* - load interstitial data we don't really
|
||||
// need, so wasting memory.
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
for i, frameInfo := range framesInfo {
|
||||
fmt.Println("Loading frame %s:%d", filename, i)
|
||||
if err := frameInfo.Check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(int64(frameInfo.Offset), io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
frame := &Frame{}
|
||||
|
||||
if err := binary.Read(buf, binary.LittleEndian, &frame.FrameHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := frame.Check(frameInfo.Size); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// It's safe to assume that a `bytes.Reader` will always satisfy the
|
||||
// requested read size.
|
||||
frame.PixelData = make([]byte, frame.PixelSize)
|
||||
if _, err := buf.Read(frame.PixelData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.Frames = append(out.Frames, frame)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func readObjFrame(f io.Reader, obj *Object) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadObjects(dir string) (map[string]*Object, error) {
|
||||
fis, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(map[string]*Object, len(fis))
|
||||
|
||||
for _, fi := range fis {
|
||||
filename := filepath.Join(dir, fi.Name())
|
||||
basename := filepath.Base(filename)
|
||||
extname := filepath.Ext(filename)
|
||||
|
||||
// Don't try to load non-.obj files
|
||||
if !strings.EqualFold(extname, ".obj") {
|
||||
continue
|
||||
}
|
||||
|
||||
i := sort.SearchStrings(objBlacklist, basename)
|
||||
if i >= len(objBlacklist) || objBlacklist[i] == basename {
|
||||
continue
|
||||
}
|
||||
|
||||
obj, err := LoadObject(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %v", filename, err)
|
||||
}
|
||||
|
||||
out[basename] = obj
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
Reference in New Issue
Block a user