Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
defaultMapXShift = 6
|
||||
defaultMapYShift = 6
|
||||
defaultMapHeightShift = 3
|
||||
)
|
||||
|
||||
// BaseInfo describes the basic parameters of a level map.
|
||||
type BaseInfo struct {
|
||||
// XSize is the horizontal extent of the map (West to East).
|
||||
XSize int32
|
||||
// YSize is the vertical extent of the map (South to North).
|
||||
YSize int32
|
||||
// XShift is the base value of XSize (XSize <= 1 << XShift).
|
||||
XShift int32
|
||||
// YShift is the base value of YSize (YSize <= 1 << YShift).
|
||||
YShift int32
|
||||
// ZShift is the base value of the height of the map.
|
||||
ZShift HeightShift
|
||||
_ [4]byte
|
||||
// Cyberspace indicates whether the level is cyberspace.
|
||||
Cyberspace byte
|
||||
_ [12]byte
|
||||
// Scheduler contains base information about the schedule table of the level.
|
||||
Scheduler SchedulerInfo
|
||||
}
|
||||
|
||||
// DefaultBaseInfo returns an initialized instance.
|
||||
func DefaultBaseInfo(cyberspace bool) BaseInfo {
|
||||
info := BaseInfo{
|
||||
XSize: 1 << defaultMapXShift,
|
||||
YSize: 1 << defaultMapYShift,
|
||||
XShift: defaultMapXShift,
|
||||
YShift: defaultMapYShift,
|
||||
ZShift: defaultMapHeightShift,
|
||||
Scheduler: DefaultSchedulerInfo(),
|
||||
}
|
||||
if cyberspace {
|
||||
info.Cyberspace = 1
|
||||
}
|
||||
return info
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDefaultBaseInfoSerialized(t *testing.T) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
info := level.DefaultBaseInfo(true)
|
||||
_ = binary.Write(buf, binary.LittleEndian, &info)
|
||||
|
||||
data := buf.Bytes()
|
||||
assert.Equal(t, 58, len(data), "length mismatch")
|
||||
assert.Equal(t, []byte{
|
||||
0x40, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00,
|
||||
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}, data, "data mismatch")
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package level
|
||||
|
||||
// BlockPuzzleState describes the state of a block puzzle.
|
||||
type BlockPuzzleState struct {
|
||||
width int
|
||||
height int
|
||||
data []byte
|
||||
}
|
||||
|
||||
// NewBlockPuzzleState returns a new instance of a block puzzle state modifier.
|
||||
// The returned instance works with the passed data slice directly.
|
||||
func NewBlockPuzzleState(data []byte, height, width int) *BlockPuzzleState {
|
||||
return &BlockPuzzleState{
|
||||
data: data,
|
||||
width: width,
|
||||
height: height}
|
||||
}
|
||||
|
||||
// CellValue returns the value of the identified cell.
|
||||
func (state *BlockPuzzleState) CellValue(row, col int) int {
|
||||
scratch := uint16(0)
|
||||
|
||||
if state.positionOk(row, col) {
|
||||
byteOffset, byteShift := state.byteOffset(row, col)
|
||||
bitsAvailable := 8 - byteShift
|
||||
|
||||
scratch = uint16(state.data[state.mappedByteIndex(byteOffset)]) >> byteShift
|
||||
if bitsAvailable < 3 {
|
||||
scratch |= uint16(state.data[state.mappedByteIndex(byteOffset+1)]) << bitsAvailable
|
||||
}
|
||||
}
|
||||
|
||||
return int(scratch & 7)
|
||||
}
|
||||
|
||||
// SetCellValue sets the value of the identified cell.
|
||||
func (state *BlockPuzzleState) SetCellValue(row, col int, value int) {
|
||||
if state.positionOk(row, col) {
|
||||
byteOffset, byteShift := state.byteOffset(row, col)
|
||||
needsSecondByte := (8 - byteShift) < 3
|
||||
scratch := uint16(state.data[state.mappedByteIndex(byteOffset)])
|
||||
mask := uint16(0x0007) << byteShift
|
||||
|
||||
if needsSecondByte {
|
||||
scratch |= uint16(state.data[state.mappedByteIndex(byteOffset+1)]) << 8
|
||||
}
|
||||
scratch = (scratch & ^mask) | (uint16(value) << byteShift)
|
||||
state.data[state.mappedByteIndex(byteOffset)] = byte((scratch >> 0) & 0xFF)
|
||||
if needsSecondByte {
|
||||
state.data[state.mappedByteIndex(byteOffset+1)] = byte((scratch >> 8) & 0xFF)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (state *BlockPuzzleState) bitOffset(row, col int) int {
|
||||
return (row*state.width + col) * 3
|
||||
}
|
||||
|
||||
func (state *BlockPuzzleState) positionOk(row, col int) bool {
|
||||
bitOffset := state.bitOffset(row, col)
|
||||
|
||||
return (row < state.height) && (col < state.width) && (bitOffset <= (128 - 3))
|
||||
}
|
||||
|
||||
func (state *BlockPuzzleState) byteOffset(row, col int) (byteOffset int, byteShift uint) {
|
||||
bitOffset := state.bitOffset(row, col)
|
||||
byteOffset = bitOffset / 8
|
||||
byteShift = uint(bitOffset % 8)
|
||||
return
|
||||
}
|
||||
|
||||
func (state *BlockPuzzleState) mappedByteIndex(index int) int {
|
||||
remainder := index % 4
|
||||
return 15 - (index - remainder + (3 - remainder))
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBlockPuzzleCellValueReturnsTheCellValue_A(t *testing.T) {
|
||||
var data [16]byte
|
||||
state := level.NewBlockPuzzleState(data[:], 1, 1)
|
||||
|
||||
assert.Equal(t, 0, state.CellValue(0, 0))
|
||||
}
|
||||
|
||||
func TestBlockPuzzleCellValueReturnsTheCellValue_B(t *testing.T) {
|
||||
var data [16]byte
|
||||
data[16-4] = 2
|
||||
state := level.NewBlockPuzzleState(data[:], 1, 1)
|
||||
|
||||
assert.Equal(t, 2, state.CellValue(0, 0))
|
||||
}
|
||||
|
||||
func TestBlockPuzzleCellValueReturnsTheCellValue_C(t *testing.T) {
|
||||
var data [16]byte
|
||||
data[16-4] = 0x18
|
||||
state := level.NewBlockPuzzleState(data[:], 2, 1)
|
||||
|
||||
assert.Equal(t, 3, state.CellValue(1, 0))
|
||||
}
|
||||
|
||||
func TestBlockPuzzleCellValueReturnsTheCellValue_D(t *testing.T) {
|
||||
var data [16]byte
|
||||
data[15] = 0x40
|
||||
data[8] = 0x01
|
||||
state := level.NewBlockPuzzleState(data[:], 4, 3)
|
||||
|
||||
assert.Equal(t, 5, state.CellValue(3, 1))
|
||||
}
|
||||
|
||||
func TestBlockPuzzleCellValueReturnsTheCellValue_E(t *testing.T) {
|
||||
var data [16]byte
|
||||
data[3] = 0x38
|
||||
state := level.NewBlockPuzzleState(data[:], 6, 7)
|
||||
|
||||
assert.Equal(t, 7, state.CellValue(5, 6))
|
||||
}
|
||||
|
||||
func TestBlockPuzzleCellValueReturnsZeroOutOfBounds_A(t *testing.T) {
|
||||
var data [16]byte
|
||||
for index := 0; index < len(data); index++ {
|
||||
data[index] = 0xFF
|
||||
}
|
||||
state := level.NewBlockPuzzleState(data[:], 1, 1)
|
||||
|
||||
assert.Equal(t, 0, state.CellValue(0, 1))
|
||||
assert.Equal(t, 0, state.CellValue(1, 0))
|
||||
assert.Equal(t, 0, state.CellValue(5, 5))
|
||||
assert.Equal(t, 0, state.CellValue(8, 8))
|
||||
}
|
||||
|
||||
func TestBlockPuzzleCellValueReturnsZeroOutOfBounds_B(t *testing.T) {
|
||||
var data [16]byte
|
||||
for index := 0; index < len(data); index++ {
|
||||
data[index] = 0xFF
|
||||
}
|
||||
state := level.NewBlockPuzzleState(data[:], 7, 6)
|
||||
|
||||
assert.Equal(t, 0, state.CellValue(6, 7))
|
||||
assert.Equal(t, 0, state.CellValue(10, 10))
|
||||
}
|
||||
|
||||
func TestBlockPuzzleCellValueReturnsZeroOutOfBounds_C(t *testing.T) {
|
||||
var data [16]byte
|
||||
for index := 0; index < len(data); index++ {
|
||||
data[index] = 0xFF
|
||||
}
|
||||
state := level.NewBlockPuzzleState(data[:], 7, 7)
|
||||
|
||||
assert.Equal(t, 0, state.CellValue(6, 0))
|
||||
}
|
||||
|
||||
func TestBlockPuzzleSetCellValue_A(t *testing.T) {
|
||||
var data [16]byte
|
||||
state := level.NewBlockPuzzleState(data[:], 1, 1)
|
||||
|
||||
state.SetCellValue(0, 0, 1)
|
||||
assert.Equal(t, [16]byte{
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00}, data)
|
||||
}
|
||||
|
||||
func TestBlockPuzzleSetCellValue_B(t *testing.T) {
|
||||
var data [16]byte
|
||||
state := level.NewBlockPuzzleState(data[:], 2, 2)
|
||||
|
||||
state.SetCellValue(1, 0, 7)
|
||||
assert.Equal(t, [16]byte{
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0xC0, 0x01, 0x00, 0x00}, data)
|
||||
}
|
||||
|
||||
func TestBlockPuzzleSetCellValue_C(t *testing.T) {
|
||||
var data [16]byte
|
||||
state := level.NewBlockPuzzleState(data[:], 6, 7)
|
||||
|
||||
state.SetCellValue(5, 6, 5)
|
||||
assert.Equal(t, [16]byte{
|
||||
0x00, 0x00, 0x00, 0x28,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}, data)
|
||||
}
|
||||
|
||||
func TestBlockPuzzleSetCellValueIgnoredOutOfBounds(t *testing.T) {
|
||||
var data [16]byte
|
||||
state := level.NewBlockPuzzleState(data[:], 6, 7)
|
||||
|
||||
state.SetCellValue(8, 8, 7)
|
||||
assert.Equal(t, [16]byte{
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00}, data)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
// MapVersion is the version of the map structures.
|
||||
MapVersion int32 = 11
|
||||
// ObjectVersion is the version of object structures.
|
||||
ObjectVersion int32 = 27
|
||||
// SavefileVersion is the version of the save.
|
||||
SavefileVersion int32 = 13
|
||||
|
||||
// InventorySize specifies how many items the inventory could hold.
|
||||
InventorySize int = 20
|
||||
|
||||
// GradesOfShadow is the number of how much shadow (light) is possible.
|
||||
GradesOfShadow = 16
|
||||
)
|
||||
|
||||
var mapVersionValue = MapVersion
|
||||
var objectVersionValue = ObjectVersion
|
||||
var savefileVersionValue = SavefileVersion
|
||||
@@ -0,0 +1,22 @@
|
||||
package level
|
||||
|
||||
// FineCoordinatesPerTileSide is the number of possible positions along an axis within a tile.
|
||||
const FineCoordinatesPerTileSide = 256
|
||||
|
||||
// Coordinate describes a tile position in one dimension.
|
||||
type Coordinate uint16
|
||||
|
||||
// CoordinateAt returns a coordinate with given parameters.
|
||||
func CoordinateAt(tile, fine byte) Coordinate {
|
||||
return Coordinate(uint16(tile)<<8 | uint16(fine))
|
||||
}
|
||||
|
||||
// Tile returns the tile part of the coordinate.
|
||||
func (coord Coordinate) Tile() byte {
|
||||
return byte(coord >> 8)
|
||||
}
|
||||
|
||||
// Fine returns the fine part of the coordinate.
|
||||
func (coord Coordinate) Fine() byte {
|
||||
return byte(coord & 0xFF)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CyberspaceFlightPull describes a directed pull of hacker in cyberspace.
|
||||
type CyberspaceFlightPull byte
|
||||
|
||||
// String returns a textual representation.
|
||||
func (pull CyberspaceFlightPull) String() string {
|
||||
switch pull {
|
||||
case CyberspaceFlightPullNone:
|
||||
return "None"
|
||||
case CyberspaceFlightPullWeakEast:
|
||||
return "Weak East"
|
||||
case CyberspaceFlightPullWeakWest:
|
||||
return "Weak West"
|
||||
case CyberspaceFlightPullWeakNorth:
|
||||
return "Weak North"
|
||||
case CyberspaceFlightPullWeakSouth:
|
||||
return "Weak South"
|
||||
case CyberspaceFlightPullMediumEast:
|
||||
return "Medium East"
|
||||
case CyberspaceFlightPullMediumWest:
|
||||
return "Medium West"
|
||||
case CyberspaceFlightPullMediumNorth:
|
||||
return "Medium North"
|
||||
case CyberspaceFlightPullMediumSouth:
|
||||
return "Medium South"
|
||||
case CyberspaceFlightPullStrongEast:
|
||||
return "Strong East"
|
||||
case CyberspaceFlightPullStrongWest:
|
||||
return "Strong West"
|
||||
case CyberspaceFlightPullStrongNorth:
|
||||
return "Strong North"
|
||||
case CyberspaceFlightPullStrongSouth:
|
||||
return "Strong South"
|
||||
case CyberspaceFlightPullMediumCeiling:
|
||||
return "Medium Ceiling"
|
||||
case CyberspaceFlightPullMediumFloor:
|
||||
return "Medium Floor"
|
||||
case CyberspaceFlightPullStrongCeiling:
|
||||
return "Strong Ceiling"
|
||||
case CyberspaceFlightPullStrongFloor:
|
||||
return "Strong Floor"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%02X", int(pull))
|
||||
}
|
||||
}
|
||||
|
||||
// CyberspaceFlightPull constants are listed below.
|
||||
const (
|
||||
CyberspaceFlightPullNone CyberspaceFlightPull = 0
|
||||
CyberspaceFlightPullWeakEast CyberspaceFlightPull = 1
|
||||
CyberspaceFlightPullWeakWest CyberspaceFlightPull = 2
|
||||
CyberspaceFlightPullWeakNorth CyberspaceFlightPull = 3
|
||||
CyberspaceFlightPullWeakSouth CyberspaceFlightPull = 4
|
||||
CyberspaceFlightPullMediumEast CyberspaceFlightPull = 5
|
||||
CyberspaceFlightPullMediumWest CyberspaceFlightPull = 6
|
||||
CyberspaceFlightPullMediumNorth CyberspaceFlightPull = 7
|
||||
CyberspaceFlightPullMediumSouth CyberspaceFlightPull = 8
|
||||
CyberspaceFlightPullStrongEast CyberspaceFlightPull = 9
|
||||
CyberspaceFlightPullStrongWest CyberspaceFlightPull = 10
|
||||
CyberspaceFlightPullStrongNorth CyberspaceFlightPull = 11
|
||||
CyberspaceFlightPullStrongSouth CyberspaceFlightPull = 12
|
||||
CyberspaceFlightPullMediumCeiling CyberspaceFlightPull = 13
|
||||
CyberspaceFlightPullMediumFloor CyberspaceFlightPull = 14
|
||||
CyberspaceFlightPullStrongCeiling CyberspaceFlightPull = 15
|
||||
CyberspaceFlightPullStrongFloor CyberspaceFlightPull = 16
|
||||
)
|
||||
|
||||
// CyberspaceFlightPulls returns all constants of CyberspaceFlightPull.
|
||||
func CyberspaceFlightPulls() []CyberspaceFlightPull {
|
||||
return []CyberspaceFlightPull{
|
||||
CyberspaceFlightPullNone,
|
||||
CyberspaceFlightPullWeakEast,
|
||||
CyberspaceFlightPullWeakWest,
|
||||
CyberspaceFlightPullWeakNorth,
|
||||
CyberspaceFlightPullWeakSouth,
|
||||
CyberspaceFlightPullMediumEast,
|
||||
CyberspaceFlightPullMediumWest,
|
||||
CyberspaceFlightPullMediumNorth,
|
||||
CyberspaceFlightPullMediumSouth,
|
||||
CyberspaceFlightPullStrongEast,
|
||||
CyberspaceFlightPullStrongWest,
|
||||
CyberspaceFlightPullStrongNorth,
|
||||
CyberspaceFlightPullStrongSouth,
|
||||
CyberspaceFlightPullMediumCeiling,
|
||||
CyberspaceFlightPullMediumFloor,
|
||||
CyberspaceFlightPullStrongCeiling,
|
||||
CyberspaceFlightPullStrongFloor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package level
|
||||
|
||||
// Direction is an enumeration of cardinal and ordinal directions.
|
||||
// Cardinal directions describe sides, ordinal directions describe corners.
|
||||
type Direction byte
|
||||
|
||||
// Plus is a shortcut for dir.AsMask().Plus(other).
|
||||
func (dir Direction) Plus(other Direction) DirectionMask {
|
||||
return dir.AsMask().Plus(other)
|
||||
}
|
||||
|
||||
// AsMask returns the direction as a mask.
|
||||
func (dir Direction) AsMask() DirectionMask {
|
||||
return DirectionMask(1 << dir)
|
||||
}
|
||||
|
||||
// Offset returns a direction that you reach by offsetting this one by the given value.
|
||||
// The direction goes in a circle, and both positive and negative offsets are allowed.
|
||||
func (dir Direction) Offset(offset int) Direction {
|
||||
return Direction((int(dir) + ((8 + (offset % 8)) % 8)) % 8)
|
||||
}
|
||||
|
||||
// DirectionMask is a bitfield combination of directions.
|
||||
type DirectionMask byte
|
||||
|
||||
// Plus adds the mask of the given direction to this mask and returns the result.
|
||||
func (mask DirectionMask) Plus(dir Direction) DirectionMask {
|
||||
return mask | dir.AsMask()
|
||||
}
|
||||
|
||||
// Direction constants are listed below.
|
||||
const (
|
||||
DirNorth Direction = iota
|
||||
DirNorthEast
|
||||
DirEast
|
||||
DirSouthEast
|
||||
DirSouth
|
||||
DirSouthWest
|
||||
DirWest
|
||||
DirNorthWest
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDirectionOffset(t *testing.T) {
|
||||
tt := []struct {
|
||||
start level.Direction
|
||||
offset int
|
||||
expected level.Direction
|
||||
}{
|
||||
{level.DirNorth, 0, level.DirNorth},
|
||||
{level.DirSouth, 0, level.DirSouth},
|
||||
|
||||
{level.DirNorth, 1, level.DirNorthEast},
|
||||
{level.DirNorth, -1, level.DirNorthWest},
|
||||
|
||||
{level.DirEast, 2, level.DirSouth},
|
||||
{level.DirWest, 3, level.DirNorthEast},
|
||||
|
||||
{level.DirNorth, 8, level.DirNorth},
|
||||
{level.DirNorth, -8, level.DirNorth},
|
||||
|
||||
{level.DirEast, 16, level.DirEast},
|
||||
{level.DirSouthEast, 4, level.DirNorthWest},
|
||||
|
||||
{level.DirSouth, -15, level.DirSouthWest},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
result := tc.start.Offset(tc.offset)
|
||||
assert.Equal(t, tc.expected, result, fmt.Sprintf("Wrong for %d+%d", int(tc.start), tc.offset))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/bits"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlids"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
// EmptyLevelParameters contain the values to create an empty level.
|
||||
type EmptyLevelParameters struct {
|
||||
// XSize specifies the expected width, in tiles. 0 (or less) will default.
|
||||
XSize int32
|
||||
// YSize specifies the expected height, in tiles. 0 (or less) will default.
|
||||
YSize int32
|
||||
// Cyberspace indicates whether the level should be marked as cyberspace.
|
||||
Cyberspace bool
|
||||
// MapModifier is called to make initial changes to the map before serializing.
|
||||
// This can be used to empty out starter tile on starter level.
|
||||
MapModifier func(x, y int, entry *TileMapEntry)
|
||||
}
|
||||
|
||||
// EmptyLevelData returns an array of serialized data for an empty level.
|
||||
func EmptyLevelData(param EmptyLevelParameters) [lvlids.PerLevel][]byte {
|
||||
var levelData [lvlids.PerLevel][]byte
|
||||
baseInfo := DefaultBaseInfo(param.Cyberspace)
|
||||
if param.XSize > 0 {
|
||||
baseInfo.XSize = param.XSize
|
||||
baseInfo.XShift = int32(bits.Len32(uint32(param.XSize)))
|
||||
if (1 << (baseInfo.XShift - 1)) == baseInfo.XSize {
|
||||
baseInfo.XShift--
|
||||
}
|
||||
}
|
||||
if param.YSize > 0 {
|
||||
baseInfo.YSize = param.YSize
|
||||
baseInfo.YShift = int32(bits.Len32(uint32(param.YSize)))
|
||||
if (1 << (baseInfo.YShift - 1)) == baseInfo.YSize {
|
||||
baseInfo.YShift--
|
||||
}
|
||||
}
|
||||
|
||||
levelData[lvlids.MapVersionNumber] = encode(mapVersionValue)
|
||||
levelData[lvlids.ObjectVersionNumber] = encode(objectVersionValue)
|
||||
levelData[lvlids.Information] = encode(&baseInfo)
|
||||
|
||||
tileMap := NewTileMap(1<<baseInfo.XShift, 1<<baseInfo.YShift)
|
||||
if param.MapModifier != nil {
|
||||
for y := 0; y < int(baseInfo.YSize); y++ {
|
||||
for x := 0; x < int(baseInfo.XSize); x++ {
|
||||
param.MapModifier(x, y, tileMap.Tile(TilePosition{X: byte(x), Y: byte(y)}, int(baseInfo.XShift)))
|
||||
}
|
||||
}
|
||||
}
|
||||
levelData[lvlids.TileMap] = encode(tileMap)
|
||||
|
||||
levelData[lvlids.Schedules] = encode(make([]byte, baseInfo.Scheduler.ElementSize*1))
|
||||
levelData[lvlids.TextureAtlas] = encode(make(TextureAtlas, DefaultTextureAtlasSize))
|
||||
levelData[lvlids.ObjectMainTable] = encode(DefaultObjectMainTable())
|
||||
levelData[lvlids.ObjectCrossRefTable] = encode(DefaultObjectCrossReferenceTable())
|
||||
for class := object.Class(0); class < object.ClassCount; class++ {
|
||||
levelData[lvlids.ObjectClassTablesStart+int(class)] = encode(DefaultObjectClassTable(class))
|
||||
levelData[lvlids.ObjectDefaultTablesStart+int(class)] = encode(NewObjectClassEntry(ObjectClassInfoFor(class).DataSize))
|
||||
}
|
||||
|
||||
levelData[lvlids.SavefileVersion] = encode(savefileVersionValue)
|
||||
levelData[lvlids.Unused41] = encode(UnknownL41{})
|
||||
|
||||
levelData[lvlids.TextureAnimations] = encode([TextureAnimationCount]TextureAnimationEntry{})
|
||||
levelData[lvlids.SurveillanceSources] = encode(make([]ObjectID, SurveillanceObjectCount))
|
||||
levelData[lvlids.SurveillanceSurrogates] = encode(make([]ObjectID, SurveillanceObjectCount))
|
||||
levelData[lvlids.Parameters] = encode(DefaultParameters())
|
||||
levelData[lvlids.MapNotes] = encode(make([]byte, MapNotesSize))
|
||||
levelData[lvlids.MapNotesPointer] = encode(MapNotesPointer(0))
|
||||
|
||||
levelData[lvlids.Unknown48] = encode(UnknownL48{})
|
||||
levelData[lvlids.Unknown49] = encode(UnknownL49{})
|
||||
levelData[lvlids.Unknown50] = encode(UnknownL50{})
|
||||
|
||||
levelData[lvlids.LoopConfiguration] = encode(make([]byte, LoopConfigEntrySize*LoopConfigEntryCount))
|
||||
|
||||
levelData[lvlids.Unknown52] = encode(UnknownL52{})
|
||||
levelData[lvlids.HeightSemaphores] = encode(HeightSemaphores{})
|
||||
|
||||
return levelData
|
||||
}
|
||||
|
||||
func encode(data interface{}) []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := serial.NewEncoder(buf)
|
||||
encoder.Code(data)
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlids"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEmptyLevelDataSizesForDefault(t *testing.T) {
|
||||
param := level.EmptyLevelParameters{}
|
||||
levelData := level.EmptyLevelData(param)
|
||||
|
||||
// For now all the expected length values are deliberately written as numbers and not from the constants.
|
||||
// This is to better catch any changes against the default (vanilla) behaviour.
|
||||
|
||||
assert.Equal(t, 4, len(levelData[lvlids.MapVersionNumber]), "MapVersionNumber")
|
||||
assert.Equal(t, 4, len(levelData[lvlids.ObjectVersionNumber]), "ObjectVersionNumber")
|
||||
assert.Equal(t, 58, len(levelData[lvlids.Information]), "Information")
|
||||
|
||||
assert.Equal(t, 64*64*16, len(levelData[lvlids.TileMap]), "TileMap")
|
||||
assert.Equal(t, 8*1, len(levelData[lvlids.Schedules]), "Schedules")
|
||||
assert.Equal(t, 2*54, len(levelData[lvlids.TextureAtlas]), "TextureAtlas")
|
||||
assert.Equal(t, 27*872, len(levelData[lvlids.ObjectMainTable]), "ObjectMainTable")
|
||||
assert.Equal(t, 10*1600, len(levelData[lvlids.ObjectCrossRefTable]), "ObjectCrossRefTable")
|
||||
|
||||
// leaving the object class tables
|
||||
|
||||
assert.Equal(t, 4, len(levelData[lvlids.SavefileVersion]), "SavefileVersion")
|
||||
assert.Equal(t, 1, len(levelData[lvlids.Unused41]), "Unused41")
|
||||
|
||||
assert.Equal(t, 7*4, len(levelData[lvlids.TextureAnimations]), "TextureAnimations")
|
||||
assert.Equal(t, 2*8, len(levelData[lvlids.SurveillanceSources]), "SurveillanceSources")
|
||||
assert.Equal(t, 2*8, len(levelData[lvlids.SurveillanceSurrogates]), "SurveillanceSurrogates")
|
||||
assert.Equal(t, 94, len(levelData[lvlids.Parameters]), "Parameters")
|
||||
assert.Equal(t, 0x0800, len(levelData[lvlids.MapNotes]), "MapNotes")
|
||||
assert.Equal(t, 4, len(levelData[lvlids.MapNotesPointer]), "MapNotesPointer")
|
||||
|
||||
assert.Equal(t, 0x30, len(levelData[lvlids.Unknown48]), "Unknown48")
|
||||
assert.Equal(t, 0x1C0, len(levelData[lvlids.Unknown49]), "Unknown49")
|
||||
assert.Equal(t, 2, len(levelData[lvlids.Unknown50]), "Unknown50")
|
||||
|
||||
assert.Equal(t, 15*64, len(levelData[lvlids.LoopConfiguration]), "LoopConfiguration")
|
||||
|
||||
assert.Equal(t, 2, len(levelData[lvlids.Unknown52]), "Unknown52")
|
||||
|
||||
assert.Equal(t, 0x40, len(levelData[lvlids.HeightSemaphores]), "HeightSemaphores")
|
||||
}
|
||||
|
||||
func TestEmptyLevelDataSizeOfMapForDifferentDimension_A(t *testing.T) {
|
||||
param := level.EmptyLevelParameters{
|
||||
XSize: 33, // will scale to 64
|
||||
YSize: 3, // will scale to 4
|
||||
}
|
||||
levelData := level.EmptyLevelData(param)
|
||||
|
||||
assert.Equal(t, 64*4*16, len(levelData[lvlids.TileMap]), "TileMap")
|
||||
}
|
||||
|
||||
func TestEmptyLevelDataSizeOfMapForDifferentDimension_B(t *testing.T) {
|
||||
param := level.EmptyLevelParameters{
|
||||
XSize: 64,
|
||||
YSize: 64,
|
||||
}
|
||||
levelData := level.EmptyLevelData(param)
|
||||
|
||||
assert.Equal(t, 64*64*16, len(levelData[lvlids.TileMap]), "TileMap")
|
||||
}
|
||||
|
||||
func TestEmptyLevelDataSizeOfMapForDifferentDimension_C(t *testing.T) {
|
||||
param := level.EmptyLevelParameters{
|
||||
XSize: 256,
|
||||
YSize: 16,
|
||||
}
|
||||
levelData := level.EmptyLevelData(param)
|
||||
|
||||
assert.Equal(t, 256*16*16, len(levelData[lvlids.TileMap]), "TileMap")
|
||||
}
|
||||
|
||||
func TestEmptyLevelDataSizeOfMapForDifferentDimension_D(t *testing.T) {
|
||||
param := level.EmptyLevelParameters{
|
||||
XSize: 1,
|
||||
YSize: 1,
|
||||
}
|
||||
levelData := level.EmptyLevelData(param)
|
||||
|
||||
assert.Equal(t, 1*1*16, len(levelData[lvlids.TileMap]), "TileMap")
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package level
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1"
|
||||
|
||||
const (
|
||||
errInvalidHeightShift ss1.StringError = "invalid height shift"
|
||||
errInvalidClass ss1.StringError = "invalid class specified"
|
||||
errNoMoreRoomForClass ss1.StringError = "no more room for class"
|
||||
errNoMoreRoomForObjects ss1.StringError = "no more room for objects"
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
package level
|
||||
|
||||
// FloorInfo describes the properties of a floor.
|
||||
type FloorInfo byte
|
||||
|
||||
// AbsoluteHeight returns the floor height in range of [0..TileHeightUnitMax-1].
|
||||
func (info FloorInfo) AbsoluteHeight() TileHeightUnit {
|
||||
return TileHeightUnit(info & 0x1F)
|
||||
}
|
||||
|
||||
// WithAbsoluteHeight returns a floor info that specifies the given absolute height.
|
||||
// The allowed range is [0..TileHeightUnitMax-1].
|
||||
func (info FloorInfo) WithAbsoluteHeight(value TileHeightUnit) FloorInfo {
|
||||
return FloorInfo(byte(info&0xE0) | (byte(value) & 0x1F))
|
||||
}
|
||||
|
||||
// TextureRotations returns the rotation steps to apply for the floor texture. Valid range: [0..3].
|
||||
func (info FloorInfo) TextureRotations() int {
|
||||
return int(info&0x60) >> 5
|
||||
}
|
||||
|
||||
// WithTextureRotations returns a floor info that specifies the given the rotation steps.
|
||||
// The provided value is normalized to the valid range.
|
||||
func (info FloorInfo) WithTextureRotations(value int) FloorInfo {
|
||||
normalized := (4 + (value % 4)) % 4
|
||||
return FloorInfo(byte(info&^0x60) | byte(normalized)<<5)
|
||||
}
|
||||
|
||||
// HasHazard returns true if the hazard flag is set.
|
||||
func (info FloorInfo) HasHazard() bool {
|
||||
return (byte(info) & 0x80) != 0
|
||||
}
|
||||
|
||||
// WithHazard returns a floor info with the hazard flag set according to given value.
|
||||
func (info FloorInfo) WithHazard(value bool) FloorInfo {
|
||||
var flag byte
|
||||
if value {
|
||||
flag = 0x80
|
||||
}
|
||||
return FloorInfo(byte(info&^0x80) | flag)
|
||||
}
|
||||
|
||||
// CeilingInfo describes the properties of a ceiling.
|
||||
type CeilingInfo byte
|
||||
|
||||
// AbsoluteHeight returns the height (from minimum floor height zero) of the ceiling.
|
||||
// Internally, this value is stored "from maximum ceiling height 32" down.
|
||||
// The range is [1..TileHeightUnitMax].
|
||||
func (info CeilingInfo) AbsoluteHeight() TileHeightUnit {
|
||||
value := info & 0x1F
|
||||
return TileHeightUnit(byte(TileHeightUnitMax) - byte(value))
|
||||
}
|
||||
|
||||
// WithAbsoluteHeight returns a ceiling info that specifies the given absolute height.
|
||||
// Internally, this value is stored "from maximum ceiling height 32" down.
|
||||
// The allowed range is [1..TileHeightUnitMax].
|
||||
func (info CeilingInfo) WithAbsoluteHeight(value TileHeightUnit) CeilingInfo {
|
||||
return CeilingInfo(byte(info&0xE0) | (byte(TileHeightUnitMax-value) & 0x1F))
|
||||
}
|
||||
|
||||
// TextureRotations returns the rotation steps to apply for the ceiling texture. Valid range: [0..3].
|
||||
func (info CeilingInfo) TextureRotations() int {
|
||||
return int(info&0x60) >> 5
|
||||
}
|
||||
|
||||
// WithTextureRotations returns a ceiling info that specifies the given the rotation steps.
|
||||
// The provided value is normalized to the valid range.
|
||||
func (info CeilingInfo) WithTextureRotations(value int) CeilingInfo {
|
||||
normalized := (4 + (value % 4)) % 4
|
||||
return CeilingInfo(byte(info&^0x60) | byte(normalized)<<5)
|
||||
}
|
||||
|
||||
// HasHazard returns true if the hazard flag is set.
|
||||
func (info CeilingInfo) HasHazard() bool {
|
||||
return (byte(info) & 0x80) != 0
|
||||
}
|
||||
|
||||
// WithHazard returns a ceiling info with the hazard flag set according to given value.
|
||||
func (info CeilingInfo) WithHazard(value bool) CeilingInfo {
|
||||
var flag byte
|
||||
if value {
|
||||
flag = 0x80
|
||||
}
|
||||
return CeilingInfo(byte(info&^0x80) | flag)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
)
|
||||
|
||||
func TestFloorCeilingInfoPermutations(t *testing.T) {
|
||||
for height := level.TileHeightUnit(0); height < level.TileHeightUnitMax; height++ {
|
||||
for rotations := -4; rotations < 6; rotations++ {
|
||||
for _, hazard := range []bool{false, true} {
|
||||
floor1 := level.FloorInfo(0).WithAbsoluteHeight(height).WithTextureRotations(rotations).WithHazard(hazard)
|
||||
floor2 := level.FloorInfo(0).WithTextureRotations(rotations).WithHazard(hazard).WithAbsoluteHeight(height)
|
||||
floor3 := level.FloorInfo(0).WithHazard(hazard).WithAbsoluteHeight(height).WithTextureRotations(rotations)
|
||||
|
||||
assert.Equal(t, floor1, floor2, "Floor1/2 mismatch")
|
||||
assert.Equal(t, floor1, floor3, "Floor1/3 mismatch")
|
||||
assert.Equal(t, floor2, floor3, "Floor2/3 mismatch")
|
||||
|
||||
assert.Equal(t, height, floor1.AbsoluteHeight(), "floor.AbsoluteHeight mismatch")
|
||||
assert.Equal(t, (4+rotations)%4, floor1.TextureRotations(), "floor.TextureRotations mismatch")
|
||||
assert.Equal(t, hazard, floor1.HasHazard(), "floor.HasHazard mismatch")
|
||||
|
||||
ceiling1 := level.CeilingInfo(0).WithAbsoluteHeight(height + 1).WithTextureRotations(rotations).WithHazard(hazard)
|
||||
ceiling2 := level.CeilingInfo(0).WithTextureRotations(rotations).WithHazard(hazard).WithAbsoluteHeight(height + 1)
|
||||
ceiling3 := level.CeilingInfo(0).WithHazard(hazard).WithAbsoluteHeight(height + 1).WithTextureRotations(rotations)
|
||||
|
||||
assert.Equal(t, ceiling1, ceiling2, "Floor1/2 mismatch")
|
||||
assert.Equal(t, ceiling1, ceiling3, "Floor1/3 mismatch")
|
||||
assert.Equal(t, ceiling2, ceiling3, "Floor2/3 mismatch")
|
||||
|
||||
assert.Equal(t, height+1, ceiling1.AbsoluteHeight(), "ceiling.AbsoluteHeight mismatch")
|
||||
assert.Equal(t, (4+rotations)%4, ceiling1.TextureRotations(), "ceiling.TextureRotations mismatch")
|
||||
assert.Equal(t, hazard, ceiling1.HasHazard(), "ceiling.HasHazard mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package level
|
||||
|
||||
// HeightSemaphores is a placeholder for storing height transitions.
|
||||
type HeightSemaphores [0x40]byte
|
||||
@@ -0,0 +1,39 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
var tileHeights = []float64{32.0, 16.0, 8.0, 4.0, 2.0, 1.0, 0.5, 0.25}
|
||||
|
||||
// HeightShift indicates the vertical scale of a level.
|
||||
type HeightShift int32
|
||||
|
||||
// ValueFromTileHeight returns a floating point value in tiles based on a tile height.
|
||||
func (shift HeightShift) ValueFromTileHeight(raw TileHeightUnit) (float32, error) {
|
||||
return shift.valueFromScale(float32(raw), float64(TileHeightUnitMax))
|
||||
}
|
||||
|
||||
// ValueFromObjectHeight returns a floating point value in tiles based on an object height.
|
||||
func (shift HeightShift) ValueFromObjectHeight(raw HeightUnit) (float32, error) {
|
||||
return shift.valueFromScale(float32(raw), float64(0x100))
|
||||
}
|
||||
|
||||
// ValueToObjectHeight returns an object height value based on given floating point value.
|
||||
func (shift HeightShift) ValueToObjectHeight(value float32) HeightUnit {
|
||||
return HeightUnit(math.Min(0xFF, math.Max(0, shift.valueToScale(value, float64(0x100)))))
|
||||
}
|
||||
|
||||
func (shift HeightShift) valueFromScale(raw float32, scale float64) (float32, error) {
|
||||
if (shift < 0) || (int(shift) >= len(tileHeights)) {
|
||||
return 0.0, errInvalidHeightShift
|
||||
}
|
||||
return float32((float64(raw) * tileHeights[int(shift)]) / scale), nil
|
||||
}
|
||||
|
||||
func (shift HeightShift) valueToScale(value float32, scale float64) float64 {
|
||||
if (shift < 0) || (int(shift) >= len(tileHeights)) {
|
||||
return 0.0
|
||||
}
|
||||
return (float64(value) * scale) / tileHeights[shift]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package level
|
||||
|
||||
// HeightUnit describes a height in a range of 0..255.
|
||||
type HeightUnit byte
|
||||
@@ -0,0 +1,581 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlids"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj"
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
// Level is the complete structure defining all necessary data for a level.
|
||||
type Level struct {
|
||||
id int
|
||||
localizer resource.Localizer
|
||||
|
||||
resStart resource.ID
|
||||
resEnd resource.ID
|
||||
|
||||
baseInfo BaseInfo
|
||||
tileMap TileMap
|
||||
wallHeightsMap WallHeightsMap
|
||||
textureAtlas TextureAtlas
|
||||
|
||||
objectMainTable ObjectMainTable
|
||||
objectCrossRefTable ObjectCrossReferenceTable
|
||||
objectClassTables [object.ClassCount]ObjectClassTable
|
||||
|
||||
textureAnimations []TextureAnimationEntry
|
||||
surveillanceSources [SurveillanceObjectCount]ObjectID
|
||||
surveillanceSurrogates [SurveillanceObjectCount]ObjectID
|
||||
parameters Parameters
|
||||
}
|
||||
|
||||
// NewLevel returns a new instance.
|
||||
func NewLevel(resourceBase resource.ID, id int, localizer resource.Localizer) *Level {
|
||||
lvl := &Level{
|
||||
id: id,
|
||||
|
||||
localizer: localizer,
|
||||
|
||||
resStart: resourceBase.Plus(lvlids.PerLevel * id),
|
||||
}
|
||||
lvl.resEnd = lvl.resStart.Plus(lvlids.PerLevel)
|
||||
|
||||
lvl.reloadBaseInfo()
|
||||
lvl.reloadTileMap()
|
||||
lvl.reloadTextureAtlas()
|
||||
lvl.reloadObjectMainTable()
|
||||
lvl.reloadObjectCrossRefTable()
|
||||
lvl.reloadObjectClassTables()
|
||||
lvl.reloadTextureAnimations()
|
||||
lvl.reloadSurveillanceSources()
|
||||
lvl.reloadSurveillanceSurrogates()
|
||||
lvl.reloadParameters()
|
||||
|
||||
return lvl
|
||||
}
|
||||
|
||||
// ID returns the identifier of the level.
|
||||
func (lvl *Level) ID() int {
|
||||
return lvl.id
|
||||
}
|
||||
|
||||
// InvalidateResources resets all internally cached data.
|
||||
func (lvl *Level) InvalidateResources(ids []resource.ID) {
|
||||
for _, id := range ids {
|
||||
if (id >= lvl.resStart) && (id < lvl.resEnd) {
|
||||
lvl.onLevelResourceDataChanged(int(id.Value() - lvl.resStart.Value()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the dimensions of the level.
|
||||
func (lvl *Level) Size() (x, y int, z HeightShift) {
|
||||
return int(lvl.baseInfo.XSize), int(lvl.baseInfo.YSize), lvl.baseInfo.ZShift
|
||||
}
|
||||
|
||||
// SetHeightShift changes the vertical scale of the level.
|
||||
func (lvl *Level) SetHeightShift(value HeightShift) {
|
||||
lvl.baseInfo.ZShift = value
|
||||
}
|
||||
|
||||
// TextureAnimations returns the list of animations.
|
||||
func (lvl *Level) TextureAnimations() []TextureAnimationEntry {
|
||||
return lvl.textureAnimations
|
||||
}
|
||||
|
||||
// SurveillanceSources returns the current sources of surveillance.
|
||||
func (lvl *Level) SurveillanceSources() [SurveillanceObjectCount]ObjectID {
|
||||
return lvl.surveillanceSources
|
||||
}
|
||||
|
||||
// SetSurveillanceSource sets a surveillance source.
|
||||
func (lvl *Level) SetSurveillanceSource(index int, id ObjectID) {
|
||||
if (index >= 0) && (index < SurveillanceObjectCount) {
|
||||
lvl.surveillanceSources[index] = id
|
||||
}
|
||||
}
|
||||
|
||||
// SurveillanceSurrogates returns the current surrogates of surveillance.
|
||||
func (lvl *Level) SurveillanceSurrogates() [SurveillanceObjectCount]ObjectID {
|
||||
return lvl.surveillanceSurrogates
|
||||
}
|
||||
|
||||
// SetSurveillanceSurrogate sets a surveillance source.
|
||||
func (lvl *Level) SetSurveillanceSurrogate(index int, id ObjectID) {
|
||||
if (index >= 0) && (index < SurveillanceObjectCount) {
|
||||
lvl.surveillanceSurrogates[index] = id
|
||||
}
|
||||
}
|
||||
|
||||
// IsCyberspace returns true if the level describes a cyberspace.
|
||||
func (lvl *Level) IsCyberspace() bool {
|
||||
return lvl.baseInfo.Cyberspace != 0
|
||||
}
|
||||
|
||||
// Parameters returns the extra properties the level has.
|
||||
func (lvl *Level) Parameters() *Parameters {
|
||||
return &lvl.parameters
|
||||
}
|
||||
|
||||
// TextureAtlas returns the atlas for textures.
|
||||
func (lvl *Level) TextureAtlas() TextureAtlas {
|
||||
return lvl.textureAtlas
|
||||
}
|
||||
|
||||
// SetTextureAtlasEntry changes the entry of the atlas.
|
||||
func (lvl *Level) SetTextureAtlasEntry(index int, textureIndex TextureIndex) {
|
||||
if (index >= 0) && (index < len(lvl.textureAtlas)) {
|
||||
lvl.textureAtlas[index] = textureIndex
|
||||
}
|
||||
}
|
||||
|
||||
// Tile returns the tile entry at given position.
|
||||
func (lvl *Level) Tile(pos TilePosition) *TileMapEntry {
|
||||
return lvl.tileMap.Tile(pos, int(lvl.baseInfo.XShift))
|
||||
}
|
||||
|
||||
// MapGridInfo returns the information necessary to draw a 2D map.
|
||||
func (lvl *Level) MapGridInfo(pos TilePosition) (TileType, TileSlopeControl, WallHeights) {
|
||||
tile := lvl.Tile(pos)
|
||||
if tile == nil {
|
||||
return TileTypeSolid, TileSlopeControlCeilingInverted, WallHeights{}
|
||||
}
|
||||
return tile.Type, tile.Flags.SlopeControl(), *lvl.wallHeightsMap.Tile(pos)
|
||||
}
|
||||
|
||||
// ObjectCapacity returns the number of objects that can be stored in this level.
|
||||
func (lvl *Level) ObjectCapacity() int {
|
||||
return lvl.objectMainTable.Capacity()
|
||||
}
|
||||
|
||||
// ObjectClassStats returns the number of used and total possible entries of given class.
|
||||
func (lvl *Level) ObjectClassStats(class object.Class) (active, capacity int) {
|
||||
if int(class) >= len(lvl.objectClassTables) {
|
||||
return 0, 0
|
||||
}
|
||||
objectClassTable := lvl.objectClassTables[class]
|
||||
return objectClassTable.AllocatedCount(), objectClassTable.Capacity()
|
||||
}
|
||||
|
||||
// ForEachObject iterates over all active objects and calls the given handler.
|
||||
func (lvl *Level) ForEachObject(handler func(ObjectID, ObjectMainEntry)) {
|
||||
tableSize := len(lvl.objectMainTable)
|
||||
if tableSize > 0 {
|
||||
id := ObjectID(lvl.objectMainTable[0].CrossReferenceTableIndex)
|
||||
for (id > 0) && (int(id) < tableSize) {
|
||||
entry := lvl.objectMainTable[id]
|
||||
handler(id, entry)
|
||||
id = entry.Next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HasRoomForObjectOf returns true if the level can hold one object of given class.
|
||||
func (lvl *Level) HasRoomForObjectOf(class object.Class) bool {
|
||||
classActive, classCapacity := lvl.ObjectClassStats(class)
|
||||
levelActive := lvl.objectMainTable.AllocatedCount()
|
||||
levelCapacity := lvl.objectMainTable.Capacity()
|
||||
return (levelActive < levelCapacity) && (classActive < classCapacity)
|
||||
}
|
||||
|
||||
// NewObject attempts to allocate a new object for given class.
|
||||
// The created object has no place in the world and must be placed.
|
||||
// Returns 0 and an error if not possible.
|
||||
func (lvl *Level) NewObject(class object.Class) (ObjectID, error) {
|
||||
if int(class) >= len(lvl.objectClassTables) {
|
||||
return 0, errInvalidClass
|
||||
}
|
||||
classTable := lvl.objectClassTables[class]
|
||||
classIndex := classTable.Allocate()
|
||||
if classIndex == 0 {
|
||||
return 0, errNoMoreRoomForClass
|
||||
}
|
||||
id := lvl.objectMainTable.Allocate()
|
||||
if id == 0 {
|
||||
classTable.Release(classIndex)
|
||||
return 0, errNoMoreRoomForObjects
|
||||
}
|
||||
obj := &lvl.objectMainTable[id]
|
||||
classEntry := &classTable[classIndex]
|
||||
classEntry.ObjectID = id
|
||||
obj.ClassTableIndex = int16(classIndex)
|
||||
obj.Class = class
|
||||
|
||||
lvl.addCrossReferenceTo(id, obj, offMapReferencePosition())
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// UpdateObjectLocation updates the reference table between object and tiles based on its current location.
|
||||
func (lvl *Level) UpdateObjectLocation(id ObjectID) {
|
||||
obj := lvl.Object(id)
|
||||
if (obj == nil) || (obj.InUse == 0) {
|
||||
return
|
||||
}
|
||||
lvl.removeCrossReferences(int(obj.CrossReferenceTableIndex), func(entry ObjectCrossReferenceEntry) int { return int(entry.NextTileForObj) })
|
||||
lvl.addCrossReferenceTo(id, obj, obj.TilePosition())
|
||||
}
|
||||
|
||||
// DelObject removes the identified object from the level.
|
||||
func (lvl *Level) DelObject(id ObjectID) {
|
||||
obj := lvl.Object(id)
|
||||
if (obj == nil) || (obj.InUse == 0) {
|
||||
return
|
||||
}
|
||||
lvl.removeCrossReferences(int(obj.CrossReferenceTableIndex), func(entry ObjectCrossReferenceEntry) int { return int(entry.NextTileForObj) })
|
||||
if int(obj.Class) < len(lvl.objectClassTables) {
|
||||
lvl.objectClassTables[obj.Class].Release(int(obj.ClassTableIndex))
|
||||
}
|
||||
lvl.objectMainTable.Release(id)
|
||||
}
|
||||
|
||||
func (lvl *Level) addCrossReferenceTo(id ObjectID, obj *ObjectMainEntry, pos TilePosition) {
|
||||
newIndex := lvl.objectCrossRefTable.Allocate()
|
||||
if newIndex == 0 {
|
||||
return
|
||||
}
|
||||
entry := &lvl.objectCrossRefTable[newIndex]
|
||||
entry.ObjectID = id
|
||||
entry.SetTilePosition(pos)
|
||||
|
||||
tile := lvl.Tile(pos)
|
||||
if tile != nil {
|
||||
entry.NextInTile = tile.FirstObjectIndex
|
||||
tile.FirstObjectIndex = int16(newIndex)
|
||||
}
|
||||
if obj.CrossReferenceTableIndex != 0 {
|
||||
entry.NextTileForObj = obj.CrossReferenceTableIndex
|
||||
endIndex := obj.CrossReferenceTableIndex
|
||||
for lvl.objectCrossRefTable[endIndex].NextTileForObj != obj.CrossReferenceTableIndex {
|
||||
endIndex = lvl.objectCrossRefTable[endIndex].NextTileForObj
|
||||
}
|
||||
lvl.objectCrossRefTable[endIndex].NextTileForObj = int16(newIndex)
|
||||
} else {
|
||||
entry.NextTileForObj = int16(newIndex)
|
||||
}
|
||||
obj.CrossReferenceTableIndex = int16(newIndex)
|
||||
}
|
||||
|
||||
func (lvl *Level) removeCrossReferences(start int, next func(ObjectCrossReferenceEntry) int) {
|
||||
index := start
|
||||
for (index != 0) && (index < len(lvl.objectCrossRefTable)) {
|
||||
entry := lvl.objectCrossRefTable[index]
|
||||
|
||||
tile := lvl.Tile(entry.TilePosition())
|
||||
if tile != nil {
|
||||
if tile.FirstObjectIndex == int16(index) {
|
||||
tile.FirstObjectIndex = entry.NextInTile
|
||||
} else {
|
||||
otherIndex := tile.FirstObjectIndex
|
||||
for otherIndex != 0 {
|
||||
otherEntry := &lvl.objectCrossRefTable[otherIndex]
|
||||
otherIndex = otherEntry.NextInTile
|
||||
if otherEntry.NextInTile == int16(index) {
|
||||
otherEntry.NextInTile = entry.NextInTile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obj := lvl.Object(entry.ObjectID)
|
||||
if obj != nil {
|
||||
switch {
|
||||
case entry.NextTileForObj == int16(start):
|
||||
obj.CrossReferenceTableIndex = 0
|
||||
case obj.CrossReferenceTableIndex == int16(index):
|
||||
obj.CrossReferenceTableIndex = entry.NextTileForObj
|
||||
default:
|
||||
otherIndex := obj.CrossReferenceTableIndex
|
||||
for otherIndex != 0 {
|
||||
otherEntry := &lvl.objectCrossRefTable[otherIndex]
|
||||
otherIndex = otherEntry.NextTileForObj
|
||||
if otherEntry.NextTileForObj == int16(index) {
|
||||
otherEntry.NextTileForObj = entry.NextTileForObj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lvl.objectCrossRefTable.Release(index)
|
||||
|
||||
index = next(entry)
|
||||
if index == start {
|
||||
index = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Object returns the main entry for the identified object.
|
||||
func (lvl *Level) Object(id ObjectID) *ObjectMainEntry {
|
||||
var entry *ObjectMainEntry
|
||||
if (id > 0) && (int(id) < len(lvl.objectMainTable)) {
|
||||
entry = &lvl.objectMainTable[id]
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
// ObjectClassData returns the raw class data for the given object.
|
||||
func (lvl *Level) ObjectClassData(obj *ObjectMainEntry) *interpreters.Instance {
|
||||
if (obj == nil) || (obj.InUse == 0) {
|
||||
return interpreters.New().For(nil)
|
||||
}
|
||||
if int(obj.Class) >= len(lvl.objectClassTables) {
|
||||
return interpreters.New().For(nil)
|
||||
}
|
||||
classTable := lvl.objectClassTables[obj.Class]
|
||||
if (obj.ClassTableIndex < 1) || (int(obj.ClassTableIndex) >= len(classTable)) {
|
||||
return interpreters.New().For(nil)
|
||||
}
|
||||
|
||||
interpreterFactory := lvlobj.ForRealWorld
|
||||
if lvl.IsCyberspace() {
|
||||
interpreterFactory = lvlobj.ForCyberspace
|
||||
}
|
||||
return interpreterFactory(obj.Triple(), classTable[obj.ClassTableIndex].Data)
|
||||
}
|
||||
|
||||
// ObjectExtraData returns the raw extra data for the given object.
|
||||
func (lvl *Level) ObjectExtraData(obj *ObjectMainEntry) *interpreters.Instance {
|
||||
if (obj == nil) || (obj.InUse == 0) {
|
||||
return interpreters.New().For(nil)
|
||||
}
|
||||
interpreterFactory := lvlobj.RealWorldExtra
|
||||
if lvl.IsCyberspace() {
|
||||
interpreterFactory = lvlobj.CyberspaceExtra
|
||||
}
|
||||
return interpreterFactory(obj.Triple(), obj.Extra[:])
|
||||
}
|
||||
|
||||
// EncodeState returns a subset of encoded level data, which only includes
|
||||
// data that is loaded (modified) by the level structure.
|
||||
// For any data block that is not relevant, a zero length slice is returned.
|
||||
func (lvl *Level) EncodeState() [lvlids.PerLevel][]byte {
|
||||
var levelData [lvlids.PerLevel][]byte
|
||||
|
||||
levelData[lvlids.Information] = encode(&lvl.baseInfo)
|
||||
|
||||
levelData[lvlids.TextureAtlas] = encode(lvl.textureAtlas)
|
||||
levelData[lvlids.TileMap] = encode(lvl.tileMap)
|
||||
levelData[lvlids.ObjectMainTable] = encode(lvl.objectMainTable)
|
||||
levelData[lvlids.ObjectCrossRefTable] = encode(lvl.objectCrossRefTable)
|
||||
for class := 0; class < len(lvl.objectClassTables); class++ {
|
||||
levelData[lvlids.ObjectClassTablesStart+class] = encode(lvl.objectClassTables[class])
|
||||
}
|
||||
|
||||
levelData[lvlids.TextureAnimations] = encode(lvl.textureAnimations)
|
||||
levelData[lvlids.SurveillanceSources] = encode(&lvl.surveillanceSources)
|
||||
levelData[lvlids.SurveillanceSurrogates] = encode(&lvl.surveillanceSurrogates)
|
||||
levelData[lvlids.Parameters] = encode(&lvl.parameters)
|
||||
|
||||
return levelData
|
||||
}
|
||||
|
||||
func (lvl *Level) onLevelResourceDataChanged(id int) {
|
||||
switch id {
|
||||
case lvlids.Information:
|
||||
lvl.reloadBaseInfo()
|
||||
case lvlids.TextureAtlas:
|
||||
lvl.reloadTextureAtlas()
|
||||
case lvlids.TileMap:
|
||||
lvl.reloadTileMap()
|
||||
case lvlids.ObjectMainTable:
|
||||
lvl.reloadObjectMainTable()
|
||||
case lvlids.ObjectCrossRefTable:
|
||||
lvl.reloadObjectCrossRefTable()
|
||||
case lvlids.TextureAnimations:
|
||||
lvl.reloadTextureAnimations()
|
||||
case lvlids.SurveillanceSources:
|
||||
lvl.reloadSurveillanceSources()
|
||||
case lvlids.SurveillanceSurrogates:
|
||||
lvl.reloadSurveillanceSurrogates()
|
||||
case lvlids.Parameters:
|
||||
lvl.reloadParameters()
|
||||
}
|
||||
if (id >= lvlids.ObjectClassTablesStart) && (id < (lvlids.ObjectClassTablesStart + len(lvl.objectClassTables))) {
|
||||
lvl.reloadObjectClassTable(object.Class(id - lvlids.ObjectClassTablesStart))
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadBaseInfo() {
|
||||
reader, err := lvl.reader(lvlids.Information)
|
||||
if err != nil {
|
||||
lvl.baseInfo = BaseInfo{}
|
||||
return
|
||||
}
|
||||
err = binary.Read(reader, binary.LittleEndian, &lvl.baseInfo)
|
||||
if err != nil {
|
||||
lvl.baseInfo = BaseInfo{}
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadTextureAtlas() {
|
||||
reader, err := lvl.reader(lvlids.TextureAtlas)
|
||||
if err != nil {
|
||||
lvl.textureAtlas = nil
|
||||
return
|
||||
}
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
lvl.textureAtlas = nil
|
||||
return
|
||||
}
|
||||
lvl.textureAtlas = make([]TextureIndex, len(data)/2)
|
||||
err = binary.Read(bytes.NewReader(data), binary.LittleEndian, lvl.textureAtlas)
|
||||
if err != nil {
|
||||
lvl.textureAtlas = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadTileMap() {
|
||||
lvl.tileMap = NewTileMap(1<<lvl.baseInfo.XShift, 1<<lvl.baseInfo.YShift)
|
||||
reader, err := lvl.reader(lvlids.TileMap)
|
||||
if err == nil {
|
||||
coder := serial.NewDecoder(reader)
|
||||
lvl.tileMap.Code(coder)
|
||||
err = coder.FirstError()
|
||||
}
|
||||
if err != nil {
|
||||
lvl.clearTileMap()
|
||||
}
|
||||
lvl.wallHeightsMap = NewWallHeightsMap(int(lvl.baseInfo.XSize), int(lvl.baseInfo.YSize))
|
||||
lvl.wallHeightsMap.CalculateFrom(lvl)
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadObjectMainTable() {
|
||||
reader, err := lvl.reader(lvlids.ObjectMainTable)
|
||||
if err != nil {
|
||||
lvl.objectMainTable = nil
|
||||
return
|
||||
}
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
lvl.objectMainTable = nil
|
||||
return
|
||||
}
|
||||
lvl.objectMainTable = make([]ObjectMainEntry, len(data)/ObjectMainEntrySize)
|
||||
err = binary.Read(bytes.NewReader(data), binary.LittleEndian, lvl.objectMainTable)
|
||||
if err != nil {
|
||||
lvl.objectMainTable = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadObjectCrossRefTable() {
|
||||
reader, err := lvl.reader(lvlids.ObjectCrossRefTable)
|
||||
if err != nil {
|
||||
lvl.objectCrossRefTable = nil
|
||||
return
|
||||
}
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
lvl.objectCrossRefTable = nil
|
||||
return
|
||||
}
|
||||
lvl.objectCrossRefTable = make([]ObjectCrossReferenceEntry, len(data)/ObjectCrossReferenceEntrySize)
|
||||
err = binary.Read(bytes.NewReader(data), binary.LittleEndian, lvl.objectCrossRefTable)
|
||||
if err != nil {
|
||||
lvl.objectCrossRefTable = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadObjectClassTables() {
|
||||
for class := 0; class < len(lvl.objectClassTables); class++ {
|
||||
lvl.reloadObjectClassTable(object.Class(class))
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadObjectClassTable(class object.Class) {
|
||||
lvl.objectClassTables[class] = nil
|
||||
reader, err := lvl.reader(lvlids.ObjectClassTablesStart + int(class))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
info := ObjectClassInfoFor(class)
|
||||
table := make(ObjectClassTable, len(data)/(ObjectClassEntryHeaderSize+info.DataSize))
|
||||
table.AllocateData(info.DataSize)
|
||||
|
||||
coder := serial.NewDecoder(bytes.NewReader(data))
|
||||
table.Code(coder)
|
||||
if coder.FirstError() != nil {
|
||||
return
|
||||
}
|
||||
lvl.objectClassTables[class] = table
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadTextureAnimations() {
|
||||
reader, err := lvl.reader(lvlids.TextureAnimations)
|
||||
if err != nil {
|
||||
lvl.textureAnimations = nil
|
||||
return
|
||||
}
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
lvl.textureAnimations = nil
|
||||
return
|
||||
}
|
||||
lvl.textureAnimations = make([]TextureAnimationEntry, len(data)/TextureAnimationEntrySize)
|
||||
err = binary.Read(bytes.NewReader(data), binary.LittleEndian, lvl.textureAnimations)
|
||||
if err != nil {
|
||||
lvl.textureAnimations = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadSurveillanceSources() {
|
||||
reader, err := lvl.reader(lvlids.SurveillanceSources)
|
||||
if err == nil {
|
||||
err = binary.Read(reader, binary.LittleEndian, &lvl.surveillanceSources)
|
||||
}
|
||||
if err != nil {
|
||||
lvl.surveillanceSources = [SurveillanceObjectCount]ObjectID{}
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadSurveillanceSurrogates() {
|
||||
reader, err := lvl.reader(lvlids.SurveillanceSurrogates)
|
||||
if err == nil {
|
||||
err = binary.Read(reader, binary.LittleEndian, &lvl.surveillanceSurrogates)
|
||||
}
|
||||
if err != nil {
|
||||
lvl.surveillanceSurrogates = [SurveillanceObjectCount]ObjectID{}
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reloadParameters() {
|
||||
reader, err := lvl.reader(lvlids.Parameters)
|
||||
if err == nil {
|
||||
err = binary.Read(reader, binary.LittleEndian, &lvl.parameters)
|
||||
}
|
||||
if err != nil {
|
||||
lvl.parameters = DefaultParameters()
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) clearTileMap() {
|
||||
for i := 0; i < len(lvl.tileMap.entries); i++ {
|
||||
lvl.tileMap.entries[i].Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (lvl *Level) reader(block int) (io.Reader, error) {
|
||||
resID := lvl.resStart.Plus(block)
|
||||
res, err := lvl.localizer.LocalizedResources(resource.LangAny).Select(resID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if (res.ContentType() != resource.Archive) || (res.BlockCount() != 1) {
|
||||
return nil, resource.ErrWrongType(resource.KeyOf(resID, resource.LangAny, 0), resource.Archive)
|
||||
}
|
||||
return res.Block(0)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package level
|
||||
|
||||
// LightDeltaInfo describes the light offsets for a tile.
|
||||
type LightDeltaInfo byte
|
||||
|
||||
// OfCeiling returns the delta value for the ceiling.
|
||||
func (info LightDeltaInfo) OfCeiling() int {
|
||||
return int(info >> 4)
|
||||
}
|
||||
|
||||
// WithCeiling returns a new instance with the given ceiling value.
|
||||
func (info LightDeltaInfo) WithCeiling(value int) LightDeltaInfo {
|
||||
if (value < 0) || (value > 15) {
|
||||
return info
|
||||
}
|
||||
return (info & 0x0F) | LightDeltaInfo(value<<4)
|
||||
}
|
||||
|
||||
// OfFloor returns the delta value for the floor.
|
||||
func (info LightDeltaInfo) OfFloor() int {
|
||||
return int(info & 0x0F)
|
||||
}
|
||||
|
||||
// WithFloor returns a new instance with the given floor value.
|
||||
func (info LightDeltaInfo) WithFloor(value int) LightDeltaInfo {
|
||||
if (value < 0) || (value > 15) {
|
||||
return info
|
||||
}
|
||||
return (info & 0xF0) | LightDeltaInfo(value)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
// LoopConfigEntryCount describes how many loop entries a level has.
|
||||
LoopConfigEntryCount = 64
|
||||
// LoopConfigEntrySize describes the size, in bytes, of a loop config entry.
|
||||
LoopConfigEntrySize = 15
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
// MapNotesSize is the size, in bytes, of the map notes resource.
|
||||
MapNotesSize = 0x0800
|
||||
)
|
||||
|
||||
// MapNotesPointer is an offset into the map notes resource.
|
||||
type MapNotesPointer uint32
|
||||
@@ -0,0 +1,34 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
// ObjectClassInfo contains meta information about a level object class in an archive.
|
||||
type ObjectClassInfo struct {
|
||||
DataSize int
|
||||
EntryCount int
|
||||
}
|
||||
|
||||
var objectClassInfoList = []ObjectClassInfo{
|
||||
{DataSize: 2, EntryCount: 16},
|
||||
{DataSize: 0, EntryCount: 32},
|
||||
{DataSize: 34, EntryCount: 32},
|
||||
{DataSize: 6, EntryCount: 32},
|
||||
{DataSize: 0, EntryCount: 32},
|
||||
{DataSize: 1, EntryCount: 8},
|
||||
{DataSize: 3, EntryCount: 16},
|
||||
{DataSize: 10, EntryCount: 176},
|
||||
{DataSize: 10, EntryCount: 128},
|
||||
{DataSize: 24, EntryCount: 64},
|
||||
{DataSize: 8, EntryCount: 64},
|
||||
{DataSize: 4, EntryCount: 32},
|
||||
{DataSize: 22, EntryCount: 160},
|
||||
{DataSize: 15, EntryCount: 64},
|
||||
{DataSize: 40, EntryCount: 64},
|
||||
}
|
||||
|
||||
// ObjectClassInfoFor returns the info entry for the corresponding object class.
|
||||
func ObjectClassInfoFor(class object.Class) ObjectClassInfo {
|
||||
return objectClassInfoList[int(class)]
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
const (
|
||||
// ObjectClassEntryHeaderSize is the size, in bytes, of the header prefix for each class entry.
|
||||
ObjectClassEntryHeaderSize = 6
|
||||
)
|
||||
|
||||
// ObjectClassEntry describes an entry in a object-class specific list.
|
||||
type ObjectClassEntry struct {
|
||||
ObjectID ObjectID
|
||||
Next int16
|
||||
Prev int16
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// NewObjectClassEntry returns an instance with the given amount of bytes initialized for data.
|
||||
func NewObjectClassEntry(dataSize int) *ObjectClassEntry {
|
||||
return &ObjectClassEntry{
|
||||
Data: make([]byte, dataSize),
|
||||
}
|
||||
}
|
||||
|
||||
// Code serializes the entry with given coder, including the data array.
|
||||
func (entry *ObjectClassEntry) Code(coder serial.Coder) {
|
||||
coder.Code(&entry.ObjectID)
|
||||
coder.Code(&entry.Next)
|
||||
coder.Code(&entry.Prev)
|
||||
coder.Code(entry.Data)
|
||||
}
|
||||
|
||||
// Reset sets all members of the entry to zero, including all bytes of the data array.
|
||||
func (entry *ObjectClassEntry) Reset() {
|
||||
entry.ObjectID = 0
|
||||
entry.Next = 0
|
||||
entry.Prev = 0
|
||||
for i := 0; i < len(entry.Data); i++ {
|
||||
entry.Data[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// ObjectClassTable is a list of entries.
|
||||
//
|
||||
// The first entry is reserved for internal use. For the reserved entry,
|
||||
// the Next field identifies the head of the single-linked free chain,
|
||||
// and the ObjectID field identifies the head of the double-linked used chain.
|
||||
type ObjectClassTable []ObjectClassEntry
|
||||
|
||||
// DefaultObjectClassTable returns an initialized table for given object class.
|
||||
func DefaultObjectClassTable(class object.Class) ObjectClassTable {
|
||||
info := ObjectClassInfoFor(class)
|
||||
table := make(ObjectClassTable, info.EntryCount)
|
||||
table.AllocateData(info.DataSize)
|
||||
table.Reset()
|
||||
return table
|
||||
}
|
||||
|
||||
// AllocateData prepares each entry to be able to store the given amount of bytes.
|
||||
// This function drops any previously assigned data.
|
||||
func (table ObjectClassTable) AllocateData(size int) {
|
||||
for i := 0; i < len(table); i++ {
|
||||
table[i].Data = make([]byte, size)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset wipes the entire table and initializes all links.
|
||||
// The bytes of the data members of each entry are reset to zero.
|
||||
func (table ObjectClassTable) Reset() {
|
||||
tableLen := len(table)
|
||||
for i := 0; i < tableLen; i++ {
|
||||
entry := &table[i]
|
||||
entry.Reset()
|
||||
entry.Next = int16(i + 1)
|
||||
}
|
||||
if tableLen > 0 {
|
||||
table[tableLen-1].Next = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Code serializes the table with the provided coder.
|
||||
func (table ObjectClassTable) Code(coder serial.Coder) {
|
||||
for i := 0; i < len(table); i++ {
|
||||
table[i].Code(coder)
|
||||
}
|
||||
}
|
||||
|
||||
// Capacity returns the number of how many objects the table can hold.
|
||||
func (table ObjectClassTable) Capacity() int {
|
||||
size := len(table)
|
||||
if size < 2 {
|
||||
return 0
|
||||
}
|
||||
return size - 1
|
||||
}
|
||||
|
||||
// AllocatedCount returns the number of how many objects are currently allocated in the table.
|
||||
func (table ObjectClassTable) AllocatedCount() int {
|
||||
capacity := table.Capacity()
|
||||
if capacity == 0 {
|
||||
return 0
|
||||
}
|
||||
index := int(table[0].ObjectID)
|
||||
count := 0
|
||||
for (index != 0) && (count < capacity) {
|
||||
count++
|
||||
index = int(table[index].Next)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Allocate attempts to reserve an entry in the table and returns the corresponding index.
|
||||
// returns 0 if exhausted.
|
||||
func (table ObjectClassTable) Allocate() int {
|
||||
if len(table) < 2 {
|
||||
return 0
|
||||
}
|
||||
start := &table[0]
|
||||
if start.Next == 0 {
|
||||
return 0
|
||||
}
|
||||
index := start.Next
|
||||
entry := &table[index]
|
||||
start.Next = entry.Next
|
||||
|
||||
entry.Reset()
|
||||
entry.Next = int16(start.ObjectID)
|
||||
table[start.ObjectID].Prev = index
|
||||
entry.Prev = 0
|
||||
start.ObjectID = ObjectID(index)
|
||||
|
||||
return int(index)
|
||||
}
|
||||
|
||||
// Release frees the identified entry.
|
||||
func (table ObjectClassTable) Release(index int) {
|
||||
if (index < 1) || (index >= len(table)) {
|
||||
return
|
||||
}
|
||||
start := &table[0]
|
||||
entry := &table[index]
|
||||
|
||||
if entry.Prev == 0 {
|
||||
start.ObjectID = ObjectID(entry.Next)
|
||||
} else {
|
||||
table[entry.Prev].Next = entry.Next
|
||||
}
|
||||
table[entry.Next].Prev = entry.Prev
|
||||
|
||||
entry.Reset()
|
||||
entry.Next = start.Next
|
||||
start.Next = int16(index)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewObjectClassEntrySetsData(t *testing.T) {
|
||||
entry := level.NewObjectClassEntry(20)
|
||||
assert.Equal(t, 20, len(entry.Data))
|
||||
}
|
||||
|
||||
func TestObjectClassEntryCanBeSerializedWithCoder(t *testing.T) {
|
||||
entry := level.NewObjectClassEntry(10)
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := serial.NewEncoder(buf)
|
||||
encoder.Code(entry)
|
||||
data := buf.Bytes()
|
||||
assert.Equal(t, 10+6, len(data))
|
||||
}
|
||||
|
||||
func TestObjectClassTableCanBeSerializedWithCoder(t *testing.T) {
|
||||
table := level.ObjectClassTable([]level.ObjectClassEntry{*level.NewObjectClassEntry(5), *level.NewObjectClassEntry(5)})
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := serial.NewEncoder(buf)
|
||||
encoder.Code(table)
|
||||
data := buf.Bytes()
|
||||
assert.Equal(t, (5+6)*2, len(data))
|
||||
}
|
||||
|
||||
func TestObjectClassTableCanAllocateData(t *testing.T) {
|
||||
table := level.ObjectClassTable([]level.ObjectClassEntry{{}, {}})
|
||||
table.AllocateData(4)
|
||||
assert.Equal(t, 4, len(table[0].Data))
|
||||
assert.Equal(t, 4, len(table[1].Data))
|
||||
}
|
||||
|
||||
func TestObjectClassTableResetClearsEntryData(t *testing.T) {
|
||||
table := level.ObjectClassTable([]level.ObjectClassEntry{aRandomObjectClassEntry(), aRandomObjectClassEntry()})
|
||||
table.Reset()
|
||||
|
||||
for index, entry := range table {
|
||||
expected := make([]byte, len(entry.Data))
|
||||
assert.Equal(t, expected, entry.Data, fmt.Sprintf("Data not zero for %d", index))
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectClassTableResetInitializesChainPointers(t *testing.T) {
|
||||
table := level.ObjectClassTable([]level.ObjectClassEntry{aRandomObjectClassEntry(), aRandomObjectClassEntry(), aRandomObjectClassEntry()})
|
||||
table.Reset()
|
||||
|
||||
start := table[0]
|
||||
assert.Equal(t, int16(1), start.Next, "start.Next should be 1, the first free entry.")
|
||||
assert.Equal(t, level.ObjectID(0), start.ObjectID, "start.ObjectID should be 0, the list is empty.")
|
||||
free0 := table[1]
|
||||
assert.Equal(t, int16(2), free0.Next, "free0.Next should be 2, the second free entry.")
|
||||
free1 := table[2]
|
||||
assert.Equal(t, int16(0), free1.Next, "free1.Next should be 0, the start entry.")
|
||||
}
|
||||
|
||||
func TestDefaultObjectClassTable(t *testing.T) {
|
||||
table := level.DefaultObjectClassTable(0)
|
||||
assert.Equal(t, 16, len(table), "16 entries for class 0 expected")
|
||||
assert.Equal(t, 2, len(table[0].Data), "2 bytes for class 0 data expected")
|
||||
assert.Equal(t, int16(1), table[0].Next, "free list expected to be initialized")
|
||||
}
|
||||
|
||||
func TestObjectClassTableAllocate(t *testing.T) {
|
||||
tt := []int{0, 1, 2, 3, 100}
|
||||
|
||||
for _, tc := range tt {
|
||||
table := make(level.ObjectClassTable, tc)
|
||||
table.Reset()
|
||||
possible := tc - 1
|
||||
|
||||
if possible < 0 {
|
||||
possible = 0
|
||||
}
|
||||
assert.Equal(t, possible, table.Capacity(), "table should have proper capacity")
|
||||
for attempt := 0; attempt < possible; attempt++ {
|
||||
assert.Equal(t, attempt, table.AllocatedCount(), "table should have proper allocation count")
|
||||
index := table.Allocate()
|
||||
assert.NotEqual(t, 0, index, "could not allocate at attempt %d for size %d", attempt, tc)
|
||||
}
|
||||
assert.Equal(t, possible, table.AllocatedCount(), "table should be full")
|
||||
last := table.Allocate()
|
||||
assert.Equal(t, 0, last, "table was not exhausted although it should be")
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectClassTableRelease(t *testing.T) {
|
||||
stats := func(table level.ObjectClassTable) (used, free int) {
|
||||
for next := int16(table[0].ObjectID); next != 0; next = table[next].Next {
|
||||
used++
|
||||
}
|
||||
for next := table[0].Next; next != 0; next = table[next].Next {
|
||||
free++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
table := make(level.ObjectClassTable, 10)
|
||||
table.Reset()
|
||||
var allocated []int
|
||||
for i := 0; i < 4; i++ {
|
||||
id := table.Allocate()
|
||||
allocated = append(allocated, id)
|
||||
}
|
||||
used, free := stats(table)
|
||||
require.Equal(t, 4, used, "invalid amount of used entries")
|
||||
require.Equal(t, 5, free, "invalid amount of free entries")
|
||||
|
||||
for _, id := range allocated {
|
||||
table.Release(id)
|
||||
}
|
||||
|
||||
used, free = stats(table)
|
||||
assert.Equal(t, 0, used, "invalid amount of used entries after release")
|
||||
assert.Equal(t, 9, free, "invalid amount of free entries after release")
|
||||
|
||||
table.Release(0)
|
||||
table.Release(20)
|
||||
for i := 0; i < len(table)-1; i++ {
|
||||
id := table.Allocate()
|
||||
assert.NotEqual(t, level.ObjectID(0), id, "should have been able to re-allocate")
|
||||
}
|
||||
}
|
||||
|
||||
func aRandomObjectClassEntry() level.ObjectClassEntry {
|
||||
decoder := serial.NewDecoder(rand.Reader)
|
||||
var dataSize byte
|
||||
decoder.Code(&dataSize)
|
||||
entry := level.NewObjectClassEntry(int(dataSize) + 1)
|
||||
decoder.Code(entry)
|
||||
return *entry
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
// ObjectCrossReferenceEntrySize describes the size, in bytes, of a ObjectCrossReferenceEntry.
|
||||
ObjectCrossReferenceEntrySize = 10
|
||||
|
||||
defaultObjectCrossReferenceEntryCount = 1600
|
||||
)
|
||||
|
||||
func offMapReferencePosition() TilePosition {
|
||||
return TilePosition{X: 0xFF, Y: 0}
|
||||
}
|
||||
|
||||
// ObjectCrossReferenceEntry links objects and tiles.
|
||||
type ObjectCrossReferenceEntry struct {
|
||||
TileX int16
|
||||
TileY int16
|
||||
|
||||
ObjectID ObjectID
|
||||
NextInTile int16
|
||||
NextTileForObj int16
|
||||
}
|
||||
|
||||
// TilePosition returns the position for that entry.
|
||||
func (entry ObjectCrossReferenceEntry) TilePosition() TilePosition {
|
||||
return TilePosition{
|
||||
X: byte(entry.TileX),
|
||||
Y: byte(entry.TileY),
|
||||
}
|
||||
}
|
||||
|
||||
// SetTilePosition sets the position for that entry.
|
||||
func (entry *ObjectCrossReferenceEntry) SetTilePosition(pos TilePosition) {
|
||||
if pos == offMapReferencePosition() {
|
||||
entry.TileX = -1
|
||||
entry.TileY = 0
|
||||
} else {
|
||||
entry.TileX = int16(pos.X)
|
||||
entry.TileY = int16(pos.Y)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset clears the members of the entry.
|
||||
func (entry *ObjectCrossReferenceEntry) Reset() {
|
||||
*entry = ObjectCrossReferenceEntry{}
|
||||
}
|
||||
|
||||
// ObjectCrossReferenceTable is a list of entries.
|
||||
// The first entry is reserved for internal use. For the reserved entry,
|
||||
// The NextInTile member refers to the head of the single-linked free chain.
|
||||
type ObjectCrossReferenceTable []ObjectCrossReferenceEntry
|
||||
|
||||
// DefaultObjectCrossReferenceTable returns an initialized table with a default size.
|
||||
func DefaultObjectCrossReferenceTable() ObjectCrossReferenceTable {
|
||||
table := make(ObjectCrossReferenceTable, defaultObjectCrossReferenceEntryCount)
|
||||
table.Reset()
|
||||
return table
|
||||
}
|
||||
|
||||
// Reset wipes the entire table and initializes all links.
|
||||
func (table ObjectCrossReferenceTable) Reset() {
|
||||
tableLen := len(table)
|
||||
for i := 0; i < tableLen; i++ {
|
||||
entry := &table[i]
|
||||
entry.Reset()
|
||||
entry.NextInTile = int16(i + 1)
|
||||
}
|
||||
if tableLen > 0 {
|
||||
table[tableLen-1].NextInTile = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate attempts to reserve a free entry in the table and return its index.
|
||||
// Returns 0 if exhausted.
|
||||
func (table ObjectCrossReferenceTable) Allocate() int {
|
||||
if len(table) < 2 {
|
||||
return 0
|
||||
}
|
||||
start := &table[0]
|
||||
if start.NextInTile == 0 {
|
||||
return 0
|
||||
}
|
||||
index := start.NextInTile
|
||||
entry := &table[index]
|
||||
start.NextInTile = entry.NextInTile
|
||||
|
||||
entry.Reset()
|
||||
|
||||
return int(index)
|
||||
}
|
||||
|
||||
// Release frees the entry with given index.
|
||||
func (table ObjectCrossReferenceTable) Release(index int) {
|
||||
if (index < 1) || (index >= len(table)) {
|
||||
return
|
||||
}
|
||||
start := &table[0]
|
||||
entry := &table[index]
|
||||
|
||||
entry.Reset()
|
||||
entry.NextInTile = start.NextInTile
|
||||
start.NextInTile = int16(index)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestObjectCrossReferenceEntrySerializedSize(t *testing.T) {
|
||||
var entry level.ObjectCrossReferenceEntry
|
||||
size := binary.Size(&entry)
|
||||
assert.Equal(t, level.ObjectCrossReferenceEntrySize, size)
|
||||
}
|
||||
|
||||
func TestObjectCrossReferenceTableCanBeSerializedWithCoder(t *testing.T) {
|
||||
table := level.ObjectCrossReferenceTable([]level.ObjectCrossReferenceEntry{{}, {}})
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := serial.NewEncoder(buf)
|
||||
encoder.Code(table)
|
||||
data := buf.Bytes()
|
||||
assert.Equal(t, level.ObjectCrossReferenceEntrySize*2, len(data))
|
||||
}
|
||||
|
||||
func TestObjectCrossReferenceTableResetInitializesChainPointers(t *testing.T) {
|
||||
table := level.ObjectCrossReferenceTable([]level.ObjectCrossReferenceEntry{
|
||||
aRandomObjectCrossReferenceEntry(), aRandomObjectCrossReferenceEntry(), aRandomObjectCrossReferenceEntry()})
|
||||
table.Reset()
|
||||
|
||||
start := table[0]
|
||||
assert.Equal(t, level.ObjectID(0), start.ObjectID, "start.ObjectID should be 0, it is unused.")
|
||||
assert.Equal(t, int16(1), start.NextInTile, "start.NextInTile should be 1, the first free.")
|
||||
free0 := table[1]
|
||||
assert.Equal(t, level.ObjectID(0), free0.ObjectID, "free0.ObjectID should be 0, it is unused.")
|
||||
assert.Equal(t, int16(2), free0.NextInTile, "free0.NextInTile should be 2, the second free.")
|
||||
free1 := table[2]
|
||||
assert.Equal(t, level.ObjectID(0), free1.ObjectID, "free1.ObjectID should be 0, it is unused.")
|
||||
assert.Equal(t, int16(0), free1.NextInTile, "free1.NextInTile should be 0, the start.")
|
||||
}
|
||||
|
||||
func TestDefaultObjectCrossReferenceTable(t *testing.T) {
|
||||
table := level.DefaultObjectCrossReferenceTable()
|
||||
|
||||
assert.Equal(t, 1600, len(table), "Wrong default size")
|
||||
assert.Equal(t, int16(1), table[0].NextInTile, "start entry should point to first free.")
|
||||
}
|
||||
|
||||
func TestObjectCrossReferenceTableAllocate(t *testing.T) {
|
||||
tt := []int{0, 1, 2, 3, 100}
|
||||
|
||||
for _, tc := range tt {
|
||||
table := make(level.ObjectCrossReferenceTable, tc)
|
||||
table.Reset()
|
||||
possible := tc - 1
|
||||
|
||||
for attempt := 0; attempt < possible; attempt++ {
|
||||
index := table.Allocate()
|
||||
assert.NotEqual(t, 0, index, "could not allocate at attempt %d for size %d", attempt, tc)
|
||||
}
|
||||
last := table.Allocate()
|
||||
assert.Equal(t, 0, last, "table was not exhausted although it should be")
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectCrossReferenceTableRelease(t *testing.T) {
|
||||
stats := func(table level.ObjectCrossReferenceTable) (used, free int) {
|
||||
for next := table[0].NextInTile; next != 0; next = table[next].NextInTile {
|
||||
free++
|
||||
}
|
||||
used = len(table) - 1 - free
|
||||
return
|
||||
}
|
||||
|
||||
table := make(level.ObjectCrossReferenceTable, 10)
|
||||
table.Reset()
|
||||
var allocated []int
|
||||
for i := 0; i < 4; i++ {
|
||||
id := table.Allocate()
|
||||
allocated = append(allocated, id)
|
||||
}
|
||||
used, free := stats(table)
|
||||
require.Equal(t, 4, used, "invalid amount of used entries")
|
||||
require.Equal(t, 5, free, "invalid amount of free entries")
|
||||
|
||||
for _, id := range allocated {
|
||||
table.Release(id)
|
||||
}
|
||||
|
||||
used, free = stats(table)
|
||||
assert.Equal(t, 0, used, "invalid amount of used entries after release")
|
||||
assert.Equal(t, 9, free, "invalid amount of free entries after release")
|
||||
|
||||
table.Release(0)
|
||||
table.Release(20)
|
||||
for i := 0; i < len(table)-1; i++ {
|
||||
id := table.Allocate()
|
||||
assert.NotEqual(t, level.ObjectID(0), id, "should have been able to re-allocate")
|
||||
}
|
||||
}
|
||||
|
||||
func aRandomObjectCrossReferenceEntry() level.ObjectCrossReferenceEntry {
|
||||
decoder := serial.NewDecoder(rand.Reader)
|
||||
var dataSize byte
|
||||
decoder.Code(&dataSize)
|
||||
var entry level.ObjectCrossReferenceEntry
|
||||
decoder.Code(&entry)
|
||||
return entry
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package level
|
||||
|
||||
// ObjectID identifies one object in a level.
|
||||
// It is also the index into the main object table.
|
||||
type ObjectID int16
|
||||
@@ -0,0 +1,160 @@
|
||||
package level
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/content/object"
|
||||
|
||||
const (
|
||||
// ObjectMainEntrySize describes the size, in bytes, of a ObjectMainEntry.
|
||||
ObjectMainEntrySize = 27
|
||||
|
||||
defaultObjectMainEntryCount = 872
|
||||
)
|
||||
|
||||
// ObjectMainEntryModifier is a function that changes properties.
|
||||
type ObjectMainEntryModifier func(*ObjectMainEntry)
|
||||
|
||||
// ObjectMainEntry describes an object in the level.
|
||||
type ObjectMainEntry struct {
|
||||
InUse byte
|
||||
|
||||
Class object.Class
|
||||
Subclass object.Subclass
|
||||
|
||||
ClassTableIndex int16
|
||||
CrossReferenceTableIndex int16
|
||||
Next ObjectID
|
||||
Prev ObjectID
|
||||
|
||||
X Coordinate
|
||||
Y Coordinate
|
||||
Z HeightUnit
|
||||
XRotation RotationUnit
|
||||
ZRotation RotationUnit
|
||||
YRotation RotationUnit
|
||||
|
||||
_ byte
|
||||
|
||||
Type object.Type
|
||||
|
||||
Hitpoints int16
|
||||
|
||||
Extra [4]byte
|
||||
}
|
||||
|
||||
// Triple returns the unique identifier of the entry.
|
||||
func (entry ObjectMainEntry) Triple() object.Triple {
|
||||
return object.TripleFrom(int(entry.Class), int(entry.Subclass), int(entry.Type))
|
||||
}
|
||||
|
||||
// TilePosition returns the tile coordinates of the object.
|
||||
func (entry ObjectMainEntry) TilePosition() TilePosition {
|
||||
return TilePosition{X: entry.X.Tile(), Y: entry.Y.Tile()}
|
||||
}
|
||||
|
||||
// FinePosition returns the coordinates of the object within the tile.
|
||||
func (entry ObjectMainEntry) FinePosition() FinePosition {
|
||||
return FinePosition{X: entry.X.Fine(), Y: entry.Y.Fine()}
|
||||
}
|
||||
|
||||
// Reset clears the entry and resets all members.
|
||||
func (entry *ObjectMainEntry) Reset() {
|
||||
*entry = ObjectMainEntry{}
|
||||
}
|
||||
|
||||
// ObjectMainTable is a list of entries.
|
||||
// The first entry is reserved for internal use. For the reserved entry,
|
||||
// the Next field refers to the head of the single-linked free chain,
|
||||
// and the CrossReferenceTableIndex refers to the head of the double-linked used chain.
|
||||
type ObjectMainTable []ObjectMainEntry
|
||||
|
||||
// DefaultObjectMainTable returns an initialized table with a default size.
|
||||
func DefaultObjectMainTable() ObjectMainTable {
|
||||
table := make(ObjectMainTable, defaultObjectMainEntryCount)
|
||||
table.Reset()
|
||||
return table
|
||||
}
|
||||
|
||||
// Capacity returns the number of how many objects the table can hold.
|
||||
func (table ObjectMainTable) Capacity() int {
|
||||
size := len(table)
|
||||
if size < 2 {
|
||||
return 0
|
||||
}
|
||||
return size - 1
|
||||
}
|
||||
|
||||
// AllocatedCount returns the number of how many objects are currently allocated in the table.
|
||||
func (table ObjectMainTable) AllocatedCount() int {
|
||||
capacity := table.Capacity()
|
||||
if capacity == 0 {
|
||||
return 0
|
||||
}
|
||||
index := int(table[0].CrossReferenceTableIndex)
|
||||
count := 0
|
||||
for (index != 0) && (count < capacity) {
|
||||
count++
|
||||
index = int(table[index].Next)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Reset wipes the entire table and initializes all links.
|
||||
func (table ObjectMainTable) Reset() {
|
||||
tableLen := len(table)
|
||||
for i := 0; i < tableLen; i++ {
|
||||
entry := &table[i]
|
||||
entry.Reset()
|
||||
entry.Next = ObjectID(i + 1)
|
||||
}
|
||||
if tableLen > 0 {
|
||||
table[tableLen-1].Next = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate attempts to find an available entry in the table and activates it.
|
||||
// Returns 0 if not possible (exhausted).
|
||||
func (table ObjectMainTable) Allocate() ObjectID {
|
||||
if len(table) < 2 {
|
||||
return 0
|
||||
}
|
||||
start := &table[0]
|
||||
if start.Next == 0 {
|
||||
return 0
|
||||
}
|
||||
id := start.Next
|
||||
entry := &table[id]
|
||||
start.Next = entry.Next
|
||||
|
||||
entry.Reset()
|
||||
entry.Next = ObjectID(start.CrossReferenceTableIndex)
|
||||
table[start.CrossReferenceTableIndex].Prev = id
|
||||
entry.Prev = 0
|
||||
start.CrossReferenceTableIndex = int16(id)
|
||||
|
||||
entry.InUse = 1
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// Release deactivates the entry with given ID.
|
||||
func (table ObjectMainTable) Release(id ObjectID) {
|
||||
if (id < 1) || (int(id) >= len(table)) {
|
||||
return
|
||||
}
|
||||
start := &table[0]
|
||||
entry := &table[id]
|
||||
|
||||
if entry.InUse == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if entry.Prev == 0 {
|
||||
start.CrossReferenceTableIndex = int16(entry.Next)
|
||||
} else {
|
||||
table[entry.Prev].Next = entry.Next
|
||||
}
|
||||
table[entry.Next].Prev = entry.Prev
|
||||
|
||||
entry.Reset()
|
||||
entry.Next = start.Next
|
||||
start.Next = id
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestObjectMainEntrySerializedSize(t *testing.T) {
|
||||
var entry level.ObjectMainEntry
|
||||
size := binary.Size(&entry)
|
||||
assert.Equal(t, level.ObjectMainEntrySize, size)
|
||||
}
|
||||
|
||||
func TestObjectMainTableCanBeSerializedWithCoder(t *testing.T) {
|
||||
table := level.ObjectMainTable([]level.ObjectMainEntry{{}, {}})
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := serial.NewEncoder(buf)
|
||||
encoder.Code(table)
|
||||
data := buf.Bytes()
|
||||
assert.Equal(t, level.ObjectMainEntrySize*2, len(data))
|
||||
}
|
||||
|
||||
func TestObjectMainTableResetClearsInUseFlags(t *testing.T) {
|
||||
table := level.ObjectMainTable([]level.ObjectMainEntry{{InUse: 1}, {InUse: 1}})
|
||||
table.Reset()
|
||||
|
||||
assert.Equal(t, byte(0), table[0].InUse, "table[0].InUse should be zero.")
|
||||
assert.Equal(t, byte(0), table[1].InUse, "table[1].InUse should be zero.")
|
||||
}
|
||||
|
||||
func TestObjectMainTableInitializesLists(t *testing.T) {
|
||||
table := level.ObjectMainTable([]level.ObjectMainEntry{{Next: 20, CrossReferenceTableIndex: 10}, {Next: 30}, {Next: 40}})
|
||||
table.Reset()
|
||||
|
||||
assert.Equal(t, level.ObjectID(1), table[0].Next, "table[0].Next should be 1, the first free entry.")
|
||||
assert.Equal(t, int16(0), table[0].CrossReferenceTableIndex, "table[0].CrossReferenceTableIndex should be 0, the used list is empty.")
|
||||
assert.Equal(t, level.ObjectID(2), table[1].Next, "table[1].Next should be 2, the second free entry.")
|
||||
assert.Equal(t, level.ObjectID(0), table[2].Next, "table[2].Next should be 0, the start entry.")
|
||||
}
|
||||
|
||||
func TestDefaultObjectMainTable(t *testing.T) {
|
||||
table := level.DefaultObjectMainTable()
|
||||
|
||||
assert.Equal(t, 872, len(table), "Table length mismatch")
|
||||
assert.Equal(t, level.ObjectID(1), table[0].Next, "table[0].Next should be 1, the first free entry.")
|
||||
}
|
||||
|
||||
func TestObjectMainTableAllocate(t *testing.T) {
|
||||
tt := []int{0, 1, 2, 3, 100}
|
||||
|
||||
for _, tc := range tt {
|
||||
table := make(level.ObjectMainTable, tc)
|
||||
table.Reset()
|
||||
possible := tc - 1
|
||||
|
||||
if possible < 0 {
|
||||
possible = 0
|
||||
}
|
||||
assert.Equal(t, possible, table.Capacity(), "table should have proper capacity")
|
||||
for attempt := 0; attempt < possible; attempt++ {
|
||||
assert.Equal(t, attempt, table.AllocatedCount(), "table should have expected allocation count")
|
||||
id := table.Allocate()
|
||||
assert.NotEqual(t, level.ObjectID(0), id, "could not allocate at attempt %d for size %d", attempt, tc)
|
||||
}
|
||||
assert.Equal(t, possible, table.AllocatedCount(), "table should be full")
|
||||
last := table.Allocate()
|
||||
assert.Equal(t, level.ObjectID(0), last, "table was not exhausted although it should be")
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectMainTableRelease(t *testing.T) {
|
||||
stats := func(table level.ObjectMainTable) (used, free int) {
|
||||
for i := 1; i < len(table); i++ {
|
||||
entry := &table[i]
|
||||
if entry.InUse != 0 {
|
||||
used++
|
||||
} else {
|
||||
free++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
table := make(level.ObjectMainTable, 10)
|
||||
table.Reset()
|
||||
var allocated []level.ObjectID
|
||||
for i := 0; i < 4; i++ {
|
||||
id := table.Allocate()
|
||||
allocated = append(allocated, id)
|
||||
}
|
||||
used, free := stats(table)
|
||||
require.Equal(t, 4, used, "invalid amount of used entries")
|
||||
require.Equal(t, 5, free, "invalid amount of free entries")
|
||||
|
||||
for _, id := range allocated {
|
||||
table.Release(id)
|
||||
}
|
||||
|
||||
used, free = stats(table)
|
||||
assert.Equal(t, 0, used, "invalid amount of used entries after release")
|
||||
assert.Equal(t, 9, free, "invalid amount of free entries after release")
|
||||
|
||||
table.Release(0)
|
||||
table.Release(20)
|
||||
for i := 0; i < len(table)-1; i++ {
|
||||
id := table.Allocate()
|
||||
assert.NotEqual(t, level.ObjectID(0), id, "should have been able to re-allocate")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
// ParametersSize is the serialized size, in bytes, of the Parameters structure.
|
||||
ParametersSize = 94
|
||||
)
|
||||
|
||||
// Parameters describes level-global properties.
|
||||
// It also contains information on the auto-maps, which, as per original code, should have been in the game state.
|
||||
type Parameters struct {
|
||||
Size int16
|
||||
_ [2]byte
|
||||
|
||||
CeilingHazardLevel byte
|
||||
FloorHazardLevel byte
|
||||
FloorHazardIsGravity byte
|
||||
BiohazardRegister byte
|
||||
RadiationRegister byte
|
||||
|
||||
_ [4]byte
|
||||
_ [3][27]byte
|
||||
}
|
||||
|
||||
// DefaultParameters returns a new instance of parameters.
|
||||
func DefaultParameters() Parameters {
|
||||
return Parameters{
|
||||
Size: ParametersSize,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDefaultParametersSerialized(t *testing.T) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
param := level.DefaultParameters()
|
||||
_ = binary.Write(buf, binary.LittleEndian, ¶m)
|
||||
|
||||
data := buf.Bytes()
|
||||
assert.Equal(t, level.ParametersSize, len(data), "length mismatch")
|
||||
assert.Equal(t, []byte{
|
||||
0x5e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}, data, "data mismatch")
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package level
|
||||
|
||||
// RotationUnit describes a rotation in the range 0..255.
|
||||
type RotationUnit byte
|
||||
|
||||
// ToDegrees returns the value converted to degrees [0..360).
|
||||
func (unit RotationUnit) ToDegrees() float64 {
|
||||
return (float64(unit) * 360.0) / 256.0
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
schedulerDefaultSize = 64
|
||||
schedulerDefaultElementSize = 8
|
||||
)
|
||||
|
||||
// SchedulerInfo describes the basic parameters of a level scheduler.
|
||||
// Its entries are stored in the Schedules table.
|
||||
type SchedulerInfo struct {
|
||||
// Size describes the maximum amount of schedule entries in the table.
|
||||
Size int32
|
||||
// ScheduleCount is the current amount of active schedules.
|
||||
ScheduleCount int32
|
||||
// ElementSize is the size of an schedule entry, in bytes.
|
||||
ElementSize int32
|
||||
// GrowFlag indicates whether the schedule table can be resized.
|
||||
GrowFlag byte
|
||||
_ [4]byte
|
||||
_ [4]byte
|
||||
}
|
||||
|
||||
// DefaultSchedulerInfo returns a new scheduler info structure with default values.
|
||||
func DefaultSchedulerInfo() SchedulerInfo {
|
||||
return SchedulerInfo{
|
||||
Size: schedulerDefaultSize,
|
||||
ElementSize: schedulerDefaultElementSize,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
// SurveillanceObjectCount describes how many surveillance objects a level can have.
|
||||
SurveillanceObjectCount = 8
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
// TextureAnimationCount describes how many entries of texture animations a level can have.
|
||||
TextureAnimationCount = 4
|
||||
|
||||
// TextureAnimationEntrySize is the size, in bytes, of one animation entry.
|
||||
TextureAnimationEntrySize = 7
|
||||
)
|
||||
|
||||
// TextureAnimationEntry describes one entry of the texture animation table.
|
||||
type TextureAnimationEntry struct {
|
||||
FrameTime uint16
|
||||
CurrentFrameTime uint16
|
||||
CurrentFrameIndex byte
|
||||
FrameCount byte
|
||||
LoopType TextureAnimationLoopType
|
||||
}
|
||||
|
||||
// TextureAnimationLoopType describes how a texture animation loop should advance.
|
||||
type TextureAnimationLoopType byte
|
||||
|
||||
// String returns the textual representation.
|
||||
func (loopType TextureAnimationLoopType) String() string {
|
||||
switch loopType {
|
||||
case TextureAnimationForward:
|
||||
return "Forward"
|
||||
case TextureAnimationForthAndBack:
|
||||
return "Forth-And-Back"
|
||||
case TextureAnimationBackAndForth:
|
||||
return "Back-And-Forth"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%02X", int(loopType))
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// TextureAnimationForward has the texture animation run in a linear loop.
|
||||
TextureAnimationForward = TextureAnimationLoopType(0x00)
|
||||
// TextureAnimationForthAndBack reverses the sequence at every end.
|
||||
TextureAnimationForthAndBack = TextureAnimationLoopType(0x01)
|
||||
// TextureAnimationBackAndForth reverses the sequence at every end.
|
||||
TextureAnimationBackAndForth = TextureAnimationLoopType(0x81)
|
||||
)
|
||||
|
||||
// TextureAnimationLoopTypes returns all constants.
|
||||
func TextureAnimationLoopTypes() []TextureAnimationLoopType {
|
||||
return []TextureAnimationLoopType{
|
||||
TextureAnimationForward,
|
||||
TextureAnimationForthAndBack,
|
||||
TextureAnimationBackAndForth,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
// DefaultTextureAtlasSize is the amount of textures a level can hold.
|
||||
DefaultTextureAtlasSize = 54
|
||||
// FloorCeilingTextureLimit describes the amount of textures available for floors/ceilings.
|
||||
FloorCeilingTextureLimit = 32
|
||||
)
|
||||
|
||||
// TextureIndex identifies one game texture.
|
||||
type TextureIndex int16
|
||||
|
||||
// AtlasIndex is an index into the TextureAtlas.
|
||||
type AtlasIndex byte
|
||||
|
||||
// TextureAtlas is a selection of textures for the level.
|
||||
type TextureAtlas []TextureIndex
|
||||
@@ -0,0 +1,227 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TileFlag describes simple properties of a map tile.
|
||||
type TileFlag uint32
|
||||
|
||||
// RealWorldFlag describes simple properties of a map tile in the real world.
|
||||
type RealWorldFlag TileFlag
|
||||
|
||||
// CyberspaceFlag describes simple properties of a map tile in cyberspace.
|
||||
type CyberspaceFlag TileFlag
|
||||
|
||||
// ForRealWorld interprets the flag value for the real world.
|
||||
func (flag TileFlag) ForRealWorld() RealWorldFlag {
|
||||
return RealWorldFlag(flag)
|
||||
}
|
||||
|
||||
// ForCyberspace interprets the flag value for cyberspace.
|
||||
func (flag TileFlag) ForCyberspace() CyberspaceFlag {
|
||||
return CyberspaceFlag(flag)
|
||||
}
|
||||
|
||||
// MusicZone returns the music zone.
|
||||
func (flag TileFlag) MusicZone() TileMusicZone {
|
||||
return TileMusicZone((flag & 0x0000E000) >> 13)
|
||||
}
|
||||
|
||||
// WithMusicZone returns a new flag value with the given music index set.
|
||||
func (flag TileFlag) WithMusicZone(value TileMusicZone) TileFlag {
|
||||
return TileFlag(uint32(flag&^0x0000E000) | (uint32(value) << 13))
|
||||
}
|
||||
|
||||
// Peril returns whether the tile is marked as peril (may play peril music).
|
||||
func (flag TileFlag) Peril() bool {
|
||||
return (flag & 0x00001000) != 0
|
||||
}
|
||||
|
||||
// WithPeril returns a flag with the given peril set.
|
||||
func (flag TileFlag) WithPeril(value bool) TileFlag {
|
||||
var valueFlag uint32
|
||||
if value {
|
||||
valueFlag = 0x00001000
|
||||
}
|
||||
return TileFlag(uint32(flag&^0x00001000) | valueFlag)
|
||||
}
|
||||
|
||||
// SlopeControl returns the slope control as per flags.
|
||||
func (flag TileFlag) SlopeControl() TileSlopeControl {
|
||||
return TileSlopeControl((flag & 0x00000C00) >> 10)
|
||||
}
|
||||
|
||||
// WithSlopeControl returns a new flag value with the given slope control set.
|
||||
func (flag TileFlag) WithSlopeControl(ctrl TileSlopeControl) TileFlag {
|
||||
return TileFlag(uint32(flag&^0x00000C00) | (uint32(ctrl&0x3) << 10))
|
||||
}
|
||||
|
||||
// AsTileFlag returns the flags as regular tile flag value.
|
||||
func (flag RealWorldFlag) AsTileFlag() TileFlag {
|
||||
return TileFlag(flag)
|
||||
}
|
||||
|
||||
// WallTextureOffset returns the vertical offset (in tile height units) to apply for wall textures.
|
||||
func (flag RealWorldFlag) WallTextureOffset() TileHeightUnit {
|
||||
return TileHeightUnit(flag & 0x0000001F)
|
||||
}
|
||||
|
||||
// WithWallTextureOffset returns a flag value with the given wall texture offset.
|
||||
func (flag RealWorldFlag) WithWallTextureOffset(value TileHeightUnit) RealWorldFlag {
|
||||
return RealWorldFlag(uint32(flag&^0x0000001F) | uint32(value&0x1F))
|
||||
}
|
||||
|
||||
// WallTexturePattern returns the pattern to apply for walls.
|
||||
func (flag RealWorldFlag) WallTexturePattern() WallTexturePattern {
|
||||
return WallTexturePattern(byte(flag&0x00000060) >> 5)
|
||||
}
|
||||
|
||||
// WithWallTexturePattern returns a flag with the given pattern set.
|
||||
func (flag RealWorldFlag) WithWallTexturePattern(value WallTexturePattern) RealWorldFlag {
|
||||
return RealWorldFlag(uint32(flag&^0x00000060) | (uint32(value&0x3) << 5))
|
||||
}
|
||||
|
||||
// UseAdjacentWallTexture returns whether the wall texture from the adjacent tile should be used for each side.
|
||||
func (flag RealWorldFlag) UseAdjacentWallTexture() bool {
|
||||
return (flag & 0x00000100) != 0
|
||||
}
|
||||
|
||||
// WithUseAdjacentWallTexture returns a flag with the given usage set.
|
||||
func (flag RealWorldFlag) WithUseAdjacentWallTexture(value bool) RealWorldFlag {
|
||||
var valueFlag uint32
|
||||
if value {
|
||||
valueFlag = 0x00000100
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x00000100) | valueFlag)
|
||||
}
|
||||
|
||||
// Deconstructed returns whether the tile is marked as heavily deconstructed (should play spooky music).
|
||||
func (flag RealWorldFlag) Deconstructed() bool {
|
||||
return (flag & 0x00000200) != 0
|
||||
}
|
||||
|
||||
// WithDeconstructed returns a flag with the given deconstruction set.
|
||||
func (flag RealWorldFlag) WithDeconstructed(value bool) RealWorldFlag {
|
||||
var valueFlag uint32
|
||||
if value {
|
||||
valueFlag = 0x00000200
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x00000200) | valueFlag)
|
||||
}
|
||||
|
||||
// FloorShadow returns the floor shadow value. Range: [0..GradesOfShadow].
|
||||
func (flag RealWorldFlag) FloorShadow() int {
|
||||
return int((flag & 0x000F0000) >> 16)
|
||||
}
|
||||
|
||||
// WithFloorShadow returns a new flag value with the given floor shadow set. Values beyond allowed range are ignored.
|
||||
func (flag RealWorldFlag) WithFloorShadow(value int) RealWorldFlag {
|
||||
if (value < 0) || (value >= GradesOfShadow) {
|
||||
return flag
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x000F0000) | (uint32(value) << 16))
|
||||
}
|
||||
|
||||
// CeilingShadow returns the ceiling shadow value. Range: [0..GradesOfShadow].
|
||||
func (flag RealWorldFlag) CeilingShadow() int {
|
||||
return int((flag & 0x0F000000) >> 24)
|
||||
}
|
||||
|
||||
// WithCeilingShadow returns a new flag value with the given ceiling shadow set. Values beyond allowed range are ignored.
|
||||
func (flag RealWorldFlag) WithCeilingShadow(value int) RealWorldFlag {
|
||||
if (value < 0) || (value >= GradesOfShadow) {
|
||||
return flag
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x0F000000) | (uint32(value) << 24))
|
||||
}
|
||||
|
||||
// TileVisited returns whether the tile is marked as being visited (seen).
|
||||
func (flag RealWorldFlag) TileVisited() bool {
|
||||
return (flag & 0x80000000) != 0
|
||||
}
|
||||
|
||||
// WithTileVisited returns a flag with the given visited set.
|
||||
func (flag RealWorldFlag) WithTileVisited(value bool) RealWorldFlag {
|
||||
var valueFlag uint32
|
||||
if value {
|
||||
valueFlag = 0x80000000
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x80000000) | valueFlag)
|
||||
}
|
||||
|
||||
// AsTileFlag returns the flags as regular tile flag value.
|
||||
func (flag CyberspaceFlag) AsTileFlag() TileFlag {
|
||||
return TileFlag(flag)
|
||||
}
|
||||
|
||||
// GameOfLifeState returns the current game-of-life state.
|
||||
func (flag CyberspaceFlag) GameOfLifeState() int {
|
||||
return int(byte(flag&0x00000060) >> 5)
|
||||
}
|
||||
|
||||
// WithGameOfLifeState returns a flag with the given game-of-life state set.
|
||||
func (flag CyberspaceFlag) WithGameOfLifeState(value int) CyberspaceFlag {
|
||||
return CyberspaceFlag(uint32(flag&^0x00000060) | (uint32(value&0x3) << 5))
|
||||
}
|
||||
|
||||
// FlightPull returns the pull applying in a tile.
|
||||
func (flag CyberspaceFlag) FlightPull() CyberspaceFlightPull {
|
||||
return CyberspaceFlightPull((uint32(flag&0x01000000) >> 20) | (uint32(flag&0x000F0000) >> 16))
|
||||
}
|
||||
|
||||
// WithFlightPull returns a flag instance with the given pull applied.
|
||||
func (flag CyberspaceFlag) WithFlightPull(value CyberspaceFlightPull) CyberspaceFlag {
|
||||
newFlag := uint32(flag &^ 0x010F0000)
|
||||
newFlag |= uint32(value&0x0F) << 16
|
||||
newFlag |= uint32(value&0x10) << 20
|
||||
return CyberspaceFlag(newFlag)
|
||||
}
|
||||
|
||||
type TileMusicZone byte
|
||||
|
||||
const (
|
||||
TileMusicZoneNoMusic TileMusicZone = 0
|
||||
TileMusicZoneHospital TileMusicZone = 1
|
||||
TileMusicZoneExecutive TileMusicZone = 2
|
||||
TileMusicZoneIndustrial TileMusicZone = 3
|
||||
TileMusicZoneMetal TileMusicZone = 4
|
||||
TileMusicZonePark TileMusicZone = 5
|
||||
TileMusicZoneBridge TileMusicZone = 6
|
||||
TileMusicZoneElevator TileMusicZone = 7
|
||||
)
|
||||
|
||||
// TileSlopeControls returns all control values.
|
||||
func TileMusicZones() []TileMusicZone {
|
||||
return []TileMusicZone{
|
||||
TileMusicZoneNoMusic,
|
||||
TileMusicZoneHospital,
|
||||
TileMusicZoneExecutive,
|
||||
TileMusicZoneIndustrial,
|
||||
TileMusicZoneMetal,
|
||||
TileMusicZonePark,
|
||||
TileMusicZoneBridge,
|
||||
TileMusicZoneElevator,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl TileMusicZone) String() string {
|
||||
switch ctrl {
|
||||
case TileMusicZoneNoMusic:
|
||||
return "No Music"
|
||||
case TileMusicZoneHospital:
|
||||
return "Hospital"
|
||||
case TileMusicZoneExecutive:
|
||||
return "Executive"
|
||||
case TileMusicZoneIndustrial:
|
||||
return "Industrial"
|
||||
case TileMusicZoneMetal:
|
||||
return "Metal"
|
||||
case TileMusicZonePark:
|
||||
return "Park"
|
||||
case TileMusicZoneBridge:
|
||||
return "Bridge"
|
||||
case TileMusicZoneElevator:
|
||||
return "Elevator"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%02X", int(ctrl))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package level
|
||||
|
||||
// TileHeightUnit constants are listed below.
|
||||
const (
|
||||
TileHeightUnitMin TileHeightUnit = 0
|
||||
TileHeightUnitMax TileHeightUnit = 32
|
||||
)
|
||||
|
||||
// TileHeightUnit describes a height in a range of 0..32.
|
||||
type TileHeightUnit byte
|
||||
@@ -0,0 +1,93 @@
|
||||
package level
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
// TileMapEntry describes one tile of the map.
|
||||
type TileMapEntry struct {
|
||||
// Type indicates what kind of tile this is.
|
||||
Type TileType
|
||||
// Floor describes floor properties.
|
||||
Floor FloorInfo
|
||||
// Ceiling describes ceiling properties.
|
||||
Ceiling CeilingInfo
|
||||
// SlopeHeight indicates for non-flat tiles the height offset.
|
||||
SlopeHeight TileHeightUnit
|
||||
|
||||
// FirstObjectIndex points into the level object cross reference table to the first object in this tile.
|
||||
FirstObjectIndex int16
|
||||
// TextureInfo describes tile texturing.
|
||||
TextureInfo TileTextureInfo
|
||||
// Flags contains arbitrary additional information.
|
||||
Flags TileFlag
|
||||
|
||||
// SubClip is always 0xFF.
|
||||
SubClip byte
|
||||
_ [2]byte
|
||||
// LightDelta describes the light offsets of this tile.
|
||||
LightDelta LightDeltaInfo
|
||||
}
|
||||
|
||||
// Reset puts the tile into an initial state.
|
||||
func (tile *TileMapEntry) Reset() {
|
||||
*tile = TileMapEntry{}
|
||||
tile.SubClip = 0xFF
|
||||
}
|
||||
|
||||
// FloorTileHeightAt returns the height of the floor (in tile unit) at the given fine position.
|
||||
func (tile TileMapEntry) FloorTileHeightAt(pos FinePosition, height HeightShift) float32 {
|
||||
floorHeight, _ := height.ValueFromTileHeight(tile.Floor.AbsoluteHeight())
|
||||
if tile.Flags.SlopeControl() == TileSlopeControlFloorFlat {
|
||||
return floorHeight
|
||||
}
|
||||
slopeHeight, _ := height.ValueFromTileHeight(tile.SlopeHeight)
|
||||
fineHeight := slopeHeight * pos.SlopeFactorFor(tile.Type)
|
||||
return floorHeight + fineHeight
|
||||
}
|
||||
|
||||
// CeilingTileHeightAt returns the height of the ceiling (in tile unit) at the given fine position.
|
||||
func (tile TileMapEntry) CeilingTileHeightAt(pos FinePosition, height HeightShift) float32 {
|
||||
ceilingHeight, _ := height.ValueFromTileHeight(tile.Ceiling.AbsoluteHeight())
|
||||
slopeControl := tile.Flags.SlopeControl()
|
||||
if slopeControl == TileSlopeControlCeilingFlat {
|
||||
return ceilingHeight
|
||||
}
|
||||
slopeHeight, _ := height.ValueFromTileHeight(tile.SlopeHeight)
|
||||
var slopeFactor float32
|
||||
if slopeControl == TileSlopeControlCeilingMirrored {
|
||||
slopeFactor = pos.SlopeFactorFor(tile.Type)
|
||||
} else {
|
||||
slopeFactor = pos.SlopeFactorFor(tile.Type.Info().SlopeInvertedType)
|
||||
}
|
||||
fineHeight := slopeHeight * slopeFactor
|
||||
return ceilingHeight - fineHeight
|
||||
}
|
||||
|
||||
// TileMap is a rectangular set of tiles.
|
||||
// The first index is the Y-axis, the second index the X-axis. This way the map can be serialized quicker.
|
||||
type TileMap struct {
|
||||
entries []TileMapEntry
|
||||
}
|
||||
|
||||
// NewTileMap returns a new, initialized map.
|
||||
func NewTileMap(width, height int) TileMap {
|
||||
entries := make([]TileMapEntry, width*height)
|
||||
for i := 0; i < len(entries); i++ {
|
||||
entries[i].Reset()
|
||||
}
|
||||
return TileMap{entries: entries}
|
||||
}
|
||||
|
||||
// Tile returns a pointer to the tile within the map for given position.
|
||||
// Nil is returned for a coordinate outside the boundaries.
|
||||
func (m TileMap) Tile(pos TilePosition, xShift int) *TileMapEntry {
|
||||
index := int(pos.X) + (int(pos.Y) << xShift)
|
||||
if (int(pos.X) >= (1 << xShift)) || (index >= len(m.entries)) {
|
||||
return nil
|
||||
}
|
||||
return &m.entries[index]
|
||||
}
|
||||
|
||||
// Code serializes the map.
|
||||
func (m TileMap) Code(coder serial.Coder) {
|
||||
coder.Code(m.entries)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTileMapEntrySerializedSize(t *testing.T) {
|
||||
var entry level.TileMapEntry
|
||||
size := binary.Size(&entry)
|
||||
assert.Equal(t, 16, size, "Size mismatch")
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package level
|
||||
|
||||
// TilePosition contains the coordinates of a tile in a level.
|
||||
type TilePosition struct {
|
||||
X byte
|
||||
Y byte
|
||||
}
|
||||
|
||||
// FinePosition contains the coordinates of an object within a tile.
|
||||
type FinePosition struct {
|
||||
X byte
|
||||
Y byte
|
||||
}
|
||||
|
||||
// SlopeFactorFor returns the factor of how much the slope of given tile type would apply at the fine position.
|
||||
func (pos FinePosition) SlopeFactorFor(tileType TileType) float32 {
|
||||
southToNorth := float32(pos.Y) / 255
|
||||
westToEast := float32(pos.X) / 255
|
||||
swneDiag := func(northWest, southEast float32) float32 {
|
||||
if pos.X < pos.Y {
|
||||
return northWest
|
||||
}
|
||||
return southEast
|
||||
}
|
||||
nwseDiag := func(southWest, northEast float32) float32 {
|
||||
if (255 - pos.X) < pos.Y {
|
||||
return northEast
|
||||
}
|
||||
return southWest
|
||||
}
|
||||
|
||||
switch tileType {
|
||||
case TileTypeSlopeSouthToNorth:
|
||||
return southToNorth
|
||||
case TileTypeSlopeWestToEast:
|
||||
return westToEast
|
||||
case TileTypeSlopeNorthToSouth:
|
||||
return 1 - southToNorth
|
||||
case TileTypeSlopeEastToWest:
|
||||
return 1 - westToEast
|
||||
|
||||
case TileTypeValleySouthEastToNorthWest:
|
||||
return nwseDiag(1-westToEast, southToNorth)
|
||||
case TileTypeValleySouthWestToNorthEast:
|
||||
return swneDiag(southToNorth, westToEast)
|
||||
case TileTypeValleyNorthWestToSouthEast:
|
||||
return nwseDiag(1-southToNorth, westToEast)
|
||||
case TileTypeValleyNorthEastToSouthWest:
|
||||
return swneDiag(1-westToEast, 1-southToNorth)
|
||||
|
||||
case TileTypeRidgeNorthWestToSouthEast:
|
||||
return nwseDiag(westToEast, 1-southToNorth)
|
||||
case TileTypeRidgeNorthEastToSouthWest:
|
||||
return swneDiag(1-southToNorth, 1-westToEast)
|
||||
case TileTypeRidgeSouthEastToNorthWest:
|
||||
return nwseDiag(southToNorth, 1-westToEast)
|
||||
case TileTypeRidgeSouthWestToNorthEast:
|
||||
return swneDiag(westToEast, southToNorth)
|
||||
}
|
||||
|
||||
return 0.0
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
)
|
||||
|
||||
func TestFinePositionSlopeFactorFor(t *testing.T) {
|
||||
tt := []struct {
|
||||
pos level.FinePosition
|
||||
tileType level.TileType
|
||||
expected float32
|
||||
}{
|
||||
{level.FinePosition{X: 127, Y: 127}, level.TileTypeOpen, 0.0},
|
||||
{level.FinePosition{X: 127, Y: 127}, level.TileTypeSlopeNorthToSouth, 0.5},
|
||||
{level.FinePosition{X: 127, Y: 127}, level.TileTypeRidgeNorthWestToSouthEast, 0.5},
|
||||
{level.FinePosition{X: 0, Y: 127}, level.TileTypeRidgeNorthWestToSouthEast, 0.0},
|
||||
{level.FinePosition{X: 127, Y: 0}, level.TileTypeRidgeNorthWestToSouthEast, 0.5},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
result := tc.pos.SlopeFactorFor(tc.tileType)
|
||||
assert.InDeltaf(t, tc.expected, result, 0.01, "Type: %v, Pos: %v", tc.tileType, tc.pos)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TileSlopeControl defines how the floor and ceiling of a sloped tile should be processed.
|
||||
type TileSlopeControl byte
|
||||
|
||||
// String returns the textual representation of the value.
|
||||
func (ctrl TileSlopeControl) String() string {
|
||||
switch ctrl {
|
||||
case TileSlopeControlCeilingInverted:
|
||||
return "CeilingInverted"
|
||||
case TileSlopeControlCeilingMirrored:
|
||||
return "CeilingMirrored"
|
||||
case TileSlopeControlCeilingFlat:
|
||||
return "CeilingFlat"
|
||||
case TileSlopeControlFloorFlat:
|
||||
return "FloorFlat"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%02X", int(ctrl))
|
||||
}
|
||||
}
|
||||
|
||||
// FloorSlopeFactors returns the slope factors for the given tile type as per control constant.
|
||||
func (ctrl TileSlopeControl) FloorSlopeFactors(tileType TileType) SlopeFactors {
|
||||
if ctrl == TileSlopeControlFloorFlat {
|
||||
return SlopeFactors{}
|
||||
}
|
||||
return tileType.Info().SlopeFloorFactors
|
||||
}
|
||||
|
||||
// CeilingSlopeFactors returns the slope factors for the given tile type as per control constant.
|
||||
func (ctrl TileSlopeControl) CeilingSlopeFactors(tileType TileType) SlopeFactors {
|
||||
if ctrl == TileSlopeControlCeilingFlat {
|
||||
return SlopeFactors{}
|
||||
}
|
||||
if ctrl == TileSlopeControlCeilingMirrored {
|
||||
return tileType.Info().SlopeFloorFactors
|
||||
}
|
||||
return tileType.Info().SlopeInvertedType.Info().SlopeFloorFactors
|
||||
}
|
||||
|
||||
// TileSlopeControl constants.
|
||||
const (
|
||||
TileSlopeControlCeilingInverted TileSlopeControl = 0
|
||||
TileSlopeControlCeilingMirrored TileSlopeControl = 1
|
||||
TileSlopeControlCeilingFlat TileSlopeControl = 2
|
||||
TileSlopeControlFloorFlat TileSlopeControl = 3
|
||||
)
|
||||
|
||||
// TileSlopeControls returns all control values.
|
||||
func TileSlopeControls() []TileSlopeControl {
|
||||
return []TileSlopeControl{
|
||||
TileSlopeControlCeilingInverted,
|
||||
TileSlopeControlCeilingMirrored,
|
||||
TileSlopeControlCeilingFlat,
|
||||
TileSlopeControlFloorFlat,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package level
|
||||
|
||||
// TileTextureInfo describes the textures used for a map tile.
|
||||
type TileTextureInfo uint16
|
||||
|
||||
// WallTextureIndex returns the texture index into the texture atlas for the walls.
|
||||
// Valid range [0..63].
|
||||
// This property is only valid in real world.
|
||||
func (info TileTextureInfo) WallTextureIndex() AtlasIndex {
|
||||
return AtlasIndex(info & 0x003F)
|
||||
}
|
||||
|
||||
// WithWallTextureIndex returns an info with given index set.
|
||||
// Values outside valid range are ignored.
|
||||
func (info TileTextureInfo) WithWallTextureIndex(value AtlasIndex) TileTextureInfo {
|
||||
if value >= 64 {
|
||||
return info
|
||||
}
|
||||
return TileTextureInfo(uint16(info&^0x003F) | uint16(value&0x003F))
|
||||
}
|
||||
|
||||
// CeilingTextureIndex returns the texture index into the texture atlas for the ceiling.
|
||||
// Valid range [0..31].
|
||||
// This property is only valid in real world.
|
||||
func (info TileTextureInfo) CeilingTextureIndex() AtlasIndex {
|
||||
return AtlasIndex((info & 0x07C0) >> 6)
|
||||
}
|
||||
|
||||
// WithCeilingTextureIndex returns an info with given index set.
|
||||
// Values outside valid range are ignored.
|
||||
func (info TileTextureInfo) WithCeilingTextureIndex(value AtlasIndex) TileTextureInfo {
|
||||
if value >= FloorCeilingTextureLimit {
|
||||
return info
|
||||
}
|
||||
return TileTextureInfo(uint16(info&^0x07C0) | (uint16(value&0x001F) << 6))
|
||||
}
|
||||
|
||||
// FloorTextureIndex returns the texture index into the texture atlas for the floor.
|
||||
// Valid range [0..31].
|
||||
// This property is only valid in real world.
|
||||
func (info TileTextureInfo) FloorTextureIndex() AtlasIndex {
|
||||
return AtlasIndex((info & 0xF800) >> 11)
|
||||
}
|
||||
|
||||
// WithFloorTextureIndex returns an info with given index set.
|
||||
// Values outside valid range are ignored.
|
||||
func (info TileTextureInfo) WithFloorTextureIndex(value AtlasIndex) TileTextureInfo {
|
||||
if value >= FloorCeilingTextureLimit {
|
||||
return info
|
||||
}
|
||||
return TileTextureInfo(uint16(info&^0xF800) | (uint16(value&0x001F) << 11))
|
||||
}
|
||||
|
||||
// FloorPaletteIndex returns the palette index for the floor in cyberspace.
|
||||
func (info TileTextureInfo) FloorPaletteIndex() byte {
|
||||
return byte(info)
|
||||
}
|
||||
|
||||
// WithFloorPaletteIndex returns an info with given index set.
|
||||
func (info TileTextureInfo) WithFloorPaletteIndex(value byte) TileTextureInfo {
|
||||
return TileTextureInfo(uint16(info&0xFF00) | uint16(value))
|
||||
}
|
||||
|
||||
// CeilingPaletteIndex returns the palette index for the ceiling in cyberspace.
|
||||
func (info TileTextureInfo) CeilingPaletteIndex() byte {
|
||||
return byte(info >> 8)
|
||||
}
|
||||
|
||||
// WithCeilingPaletteIndex returns an info with given index set.
|
||||
func (info TileTextureInfo) WithCeilingPaletteIndex(value byte) TileTextureInfo {
|
||||
return TileTextureInfo(uint16(info&0x00FF) | (uint16(value) << 8))
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TileType describes the general type of a map tile.
|
||||
type TileType byte
|
||||
|
||||
// Info returns the information associated with the tile type.
|
||||
func (t TileType) Info() TileTypeInfo {
|
||||
if int(t) < len(tileTypeInfoList) {
|
||||
return tileTypeInfoList[t]
|
||||
}
|
||||
info := tileTypeInfoList[TileTypeSolid]
|
||||
info.Name = t.String()
|
||||
info.SlopeInvertedType = t
|
||||
return info
|
||||
}
|
||||
|
||||
// String returns the textual representation of the type.
|
||||
func (t TileType) String() string {
|
||||
if int(t) < len(tileTypeInfoList) {
|
||||
return tileTypeInfoList[t].Name
|
||||
}
|
||||
return fmt.Sprintf("Unknown%02X", int(t))
|
||||
}
|
||||
|
||||
// Tiles come in different forms:
|
||||
// Solid tiles can not be entered, Open tiles are regular tiles with a flat floor and a flat ceiling.
|
||||
// DiagonalOpen tiles are those with flat floors and ceilings, and two walls cut off by one diagonal wall.
|
||||
// Slope tiles have a sloped floor (or ceiling). Valley tiles have one floor vertex lower while Ridge tiles have one
|
||||
// floor vertex higher than the other three.
|
||||
const (
|
||||
TileTypeSolid TileType = 0x00
|
||||
TileTypeOpen TileType = 0x01
|
||||
|
||||
TileTypeDiagonalOpenSouthEast TileType = 0x02
|
||||
TileTypeDiagonalOpenSouthWest TileType = 0x03
|
||||
TileTypeDiagonalOpenNorthWest TileType = 0x04
|
||||
TileTypeDiagonalOpenNorthEast TileType = 0x05
|
||||
|
||||
TileTypeSlopeSouthToNorth TileType = 0x06
|
||||
TileTypeSlopeWestToEast TileType = 0x07
|
||||
TileTypeSlopeNorthToSouth TileType = 0x08
|
||||
TileTypeSlopeEastToWest TileType = 0x09
|
||||
|
||||
TileTypeValleySouthEastToNorthWest TileType = 0x0A
|
||||
TileTypeValleySouthWestToNorthEast TileType = 0x0B
|
||||
TileTypeValleyNorthWestToSouthEast TileType = 0x0C
|
||||
TileTypeValleyNorthEastToSouthWest TileType = 0x0D
|
||||
|
||||
TileTypeRidgeNorthWestToSouthEast TileType = 0x0E
|
||||
TileTypeRidgeNorthEastToSouthWest TileType = 0x0F
|
||||
TileTypeRidgeSouthEastToNorthWest TileType = 0x10
|
||||
TileTypeRidgeSouthWestToNorthEast TileType = 0x11
|
||||
)
|
||||
|
||||
// TileTypes returns a list of all known tile types.
|
||||
func TileTypes() []TileType {
|
||||
return []TileType{
|
||||
TileTypeSolid, TileTypeOpen,
|
||||
TileTypeDiagonalOpenSouthEast, TileTypeDiagonalOpenSouthWest, TileTypeDiagonalOpenNorthWest, TileTypeDiagonalOpenNorthEast,
|
||||
TileTypeSlopeSouthToNorth, TileTypeSlopeWestToEast, TileTypeSlopeNorthToSouth, TileTypeSlopeEastToWest,
|
||||
TileTypeValleySouthEastToNorthWest, TileTypeValleySouthWestToNorthEast, TileTypeValleyNorthWestToSouthEast, TileTypeValleyNorthEastToSouthWest,
|
||||
TileTypeRidgeNorthWestToSouthEast, TileTypeRidgeNorthEastToSouthWest, TileTypeRidgeSouthEastToNorthWest, TileTypeRidgeSouthWestToNorthEast,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package level
|
||||
|
||||
// SlopeFactors is a list of multipliers for each direction of a tile.
|
||||
type SlopeFactors [8]float32
|
||||
|
||||
// Negated returns the factors multiplied by -1.
|
||||
func (factors SlopeFactors) Negated() SlopeFactors {
|
||||
return SlopeFactors{
|
||||
factors[0] * -1, factors[1] * -1, factors[2] * -1, factors[3] * -1,
|
||||
factors[4] * -1, factors[5] * -1, factors[6] * -1, factors[7] * -1,
|
||||
}
|
||||
}
|
||||
|
||||
// TileTypeInfo is the meta information about a tile type.
|
||||
type TileTypeInfo struct {
|
||||
// Name is the textual representation of the tile type.
|
||||
Name string
|
||||
|
||||
// SolidSides is a bitfield of cardinal directions describing solid walls.
|
||||
SolidSides DirectionMask
|
||||
|
||||
// SlopeFloorFactors defines how a slope affects the floor in each direction of a tile [0.0 .. 1.0].
|
||||
SlopeFloorFactors SlopeFactors
|
||||
|
||||
// SlopeInvertedType is the type that inverts the slope to form a solid tile if merged (e.g. floor & ceiling).
|
||||
// Types that have no slope invert themselves.
|
||||
SlopeInvertedType TileType
|
||||
}
|
||||
|
||||
var tileTypeInfoList = []TileTypeInfo{
|
||||
{"Solid", DirNorth.Plus(DirEast).Plus(DirSouth).Plus(DirWest), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeSolid},
|
||||
{"Open", 0, SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeOpen},
|
||||
|
||||
{"DiagonalOpenSouthEast", DirNorth.Plus(DirWest), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeDiagonalOpenSouthEast},
|
||||
{"DiagonalOpenSouthWest", DirNorth.Plus(DirEast), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeDiagonalOpenSouthWest},
|
||||
{"DiagonalOpenNorthWest", DirSouth.Plus(DirEast), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeDiagonalOpenNorthWest},
|
||||
{"DiagonalOpenNorthEast", DirSouth.Plus(DirWest), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeDiagonalOpenNorthEast},
|
||||
|
||||
{"SlopeSouthToNorth", 0, SlopeFactors{1.0, 1.0, 0.5, 0.0, 0.0, 0.0, 0.5, 1.0}, TileTypeSlopeNorthToSouth},
|
||||
{"SlopeWestToEast", 0, SlopeFactors{0.5, 1.0, 1.0, 1.0, 0.5, 0.0, 0.0, 0.0}, TileTypeSlopeEastToWest},
|
||||
{"SlopeNorthToSouth", 0, SlopeFactors{0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.5, 0.0}, TileTypeSlopeSouthToNorth},
|
||||
{"SlopeEastToWest", 0, SlopeFactors{0.5, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0}, TileTypeSlopeWestToEast},
|
||||
|
||||
{"ValleySouthEastToNorthWest", 0, SlopeFactors{1.0, 1.0, 0.5, 0.0, 0.5, 1.0, 1.0, 1.0}, TileTypeRidgeNorthWestToSouthEast},
|
||||
{"ValleySouthWestToNorthEast", 0, SlopeFactors{1.0, 1.0, 1.0, 1.0, 0.5, 0.0, 0.5, 1.0}, TileTypeRidgeNorthEastToSouthWest},
|
||||
{"ValleyNorthWestToSouthEast", 0, SlopeFactors{0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.0}, TileTypeRidgeSouthEastToNorthWest},
|
||||
{"ValleyNorthEastToSouthWest", 0, SlopeFactors{0.5, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0}, TileTypeRidgeSouthWestToNorthEast},
|
||||
|
||||
{"RidgeNorthWestToSouthEast", 0, SlopeFactors{0.0, 0.0, 0.5, 1.0, 0.5, 0.0, 0.0, 0.0}, TileTypeValleySouthEastToNorthWest},
|
||||
{"RidgeNorthEastToSouthWest", 0, SlopeFactors{0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 0.5, 0.0}, TileTypeValleySouthWestToNorthEast},
|
||||
{"RidgeSouthEastToNorthWest", 0, SlopeFactors{0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 1.0}, TileTypeValleyNorthWestToSouthEast},
|
||||
{"RidgeSouthWestToNorthEast", 0, SlopeFactors{0.5, 1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0}, TileTypeValleyNorthEastToSouthWest},
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTileTypeInfoSlopeFactorsSealTileWithInversion(t *testing.T) {
|
||||
tt := []level.TileType{
|
||||
level.TileTypeSlopeSouthToNorth,
|
||||
level.TileTypeSlopeWestToEast,
|
||||
level.TileTypeSlopeNorthToSouth,
|
||||
level.TileTypeSlopeEastToWest,
|
||||
|
||||
level.TileTypeValleySouthEastToNorthWest,
|
||||
level.TileTypeValleySouthWestToNorthEast,
|
||||
level.TileTypeValleyNorthWestToSouthEast,
|
||||
level.TileTypeValleyNorthEastToSouthWest,
|
||||
|
||||
level.TileTypeRidgeNorthWestToSouthEast,
|
||||
level.TileTypeRidgeNorthEastToSouthWest,
|
||||
level.TileTypeRidgeSouthEastToNorthWest,
|
||||
level.TileTypeRidgeSouthWestToNorthEast,
|
||||
}
|
||||
for _, tc := range tt {
|
||||
info := tc.Info()
|
||||
mirroredInfo := info.SlopeInvertedType.Info()
|
||||
var factors level.SlopeFactors
|
||||
for i := 0; i < 8; i++ {
|
||||
factors[i] = info.SlopeFloorFactors[i] + mirroredInfo.SlopeFloorFactors[i]
|
||||
}
|
||||
assert.Equal(t, level.SlopeFactors{1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}, factors, fmt.Sprintf("Factors don't seal tile for type 0x%02X", tc))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTileTypeInfoDefaultsToSolidEquivalent(t *testing.T) {
|
||||
tileType := level.TileType(0x77)
|
||||
expected := level.TileTypeInfo{
|
||||
Name: "Unknown77",
|
||||
SolidSides: level.DirNorth.Plus(level.DirEast).Plus(level.DirSouth).Plus(level.DirWest),
|
||||
SlopeFloorFactors: level.SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0},
|
||||
SlopeInvertedType: tileType,
|
||||
}
|
||||
assert.Equal(t, expected, tileType.Info())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package level
|
||||
|
||||
// UnknownL41 is the unknown data of L41.
|
||||
type UnknownL41 [1]byte
|
||||
|
||||
// UnknownL48 is the unknown data of L48.
|
||||
type UnknownL48 [0x30]byte
|
||||
|
||||
// UnknownL49 is the unknown data of L49.
|
||||
type UnknownL49 [0x1C0]byte
|
||||
|
||||
// UnknownL50 is the unknown data of L50.
|
||||
type UnknownL50 [2]byte
|
||||
|
||||
// UnknownL52 is the unknown data of L52.
|
||||
type UnknownL52 [2]byte
|
||||
@@ -0,0 +1,119 @@
|
||||
package level
|
||||
|
||||
// WallHeights describes the height delta, in HeightUnits, one would take to
|
||||
// cross to the other tile. A value of 0 means equal floor, a negative value a fall down.
|
||||
// A value of 0x20 (= maximum of height units) means a solid wall.
|
||||
//
|
||||
// For each cardinal direction, facing to the specified direction,
|
||||
// the first entry is the left side, the second the center, and the third the right side.
|
||||
type WallHeights struct {
|
||||
North [3]float32
|
||||
East [3]float32
|
||||
South [3]float32
|
||||
West [3]float32
|
||||
}
|
||||
|
||||
// Reset sets all the heights to zero.
|
||||
func (heights *WallHeights) Reset() {
|
||||
*heights = WallHeights{}
|
||||
}
|
||||
|
||||
// WallHeightsMap is rectangular table of wall heights, typically mirroring a tile map.
|
||||
type WallHeightsMap [][]WallHeights
|
||||
|
||||
// NewWallHeightsMap returns a new, initialized map.
|
||||
func NewWallHeightsMap(width, height int) WallHeightsMap {
|
||||
m := make([][]WallHeights, height)
|
||||
for y := 0; y < height; y++ {
|
||||
m[y] = make([]WallHeights, width)
|
||||
for x := 0; x < width; x++ {
|
||||
m[y][x].Reset()
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Tile returns a pointer to the tile within the map for given position.
|
||||
func (m WallHeightsMap) Tile(pos TilePosition) *WallHeights {
|
||||
return &m[pos.Y][pos.X]
|
||||
}
|
||||
|
||||
// CalculateFrom updates all the wall heights according to the specified map.
|
||||
func (m *WallHeightsMap) CalculateFrom(tileMap interface {
|
||||
Tile(pos TilePosition) *TileMapEntry
|
||||
}) {
|
||||
posAt := func(x, y int) TilePosition {
|
||||
return TilePosition{X: byte(x), Y: byte(y)}
|
||||
}
|
||||
for y, row := range *m {
|
||||
for x := 0; x < len(row); x++ {
|
||||
tile := tileMap.Tile(posAt(x, y))
|
||||
if tile == nil {
|
||||
continue
|
||||
}
|
||||
heights := &row[x]
|
||||
heights.North = calculateWallHeight(tile, DirNorth, tileMap.Tile(posAt(x, y+1)), DirSouth)
|
||||
heights.East = calculateWallHeight(tile, DirEast, tileMap.Tile(posAt(x+1, y)), DirWest)
|
||||
heights.South = calculateWallHeight(tile, DirSouth, tileMap.Tile(posAt(x, y-1)), DirNorth)
|
||||
heights.West = calculateWallHeight(tile, DirWest, tileMap.Tile(posAt(x-1, y)), DirEast)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func calculateWallHeight(entry *TileMapEntry, entrySide Direction, other *TileMapEntry, otherSide Direction) [3]float32 {
|
||||
var result [3]float32
|
||||
baseHeights := func(tile *TileMapEntry, side Direction) (floor TileHeightUnit, slope TileHeightUnit, ceiling TileHeightUnit) {
|
||||
floor = TileHeightUnitMax
|
||||
slope = TileHeightUnitMin
|
||||
ceiling = TileHeightUnitMax
|
||||
if (tile != nil) && ((tile.Type.Info().SolidSides & side.AsMask()) == 0) {
|
||||
floor = tile.Floor.AbsoluteHeight()
|
||||
slope = tile.SlopeHeight
|
||||
ceiling = tile.Ceiling.AbsoluteHeight()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
otherFloorHeight, otherSlopeHeight, otherCeilingHeight := baseHeights(other, otherSide)
|
||||
var otherFloorFactors SlopeFactors
|
||||
var otherCeilingFactors SlopeFactors
|
||||
if other != nil {
|
||||
otherSlopeControl := other.Flags.SlopeControl()
|
||||
otherFloorFactors = otherSlopeControl.FloorSlopeFactors(other.Type)
|
||||
otherCeilingFactors = otherSlopeControl.CeilingSlopeFactors(other.Type)
|
||||
}
|
||||
|
||||
entryFloorHeight, entrySlopeHeight, entryCeilingHeight := baseHeights(entry, entrySide)
|
||||
entrySlopeControl := entry.Flags.SlopeControl()
|
||||
entryFloorFactors := entrySlopeControl.FloorSlopeFactors(entry.Type)
|
||||
entryCeilingFactors := entrySlopeControl.CeilingSlopeFactors(entry.Type)
|
||||
|
||||
sideHeights := func(side Direction, sideOffset int, factors SlopeFactors, slopeHeight TileHeightUnit, absoluteHeight TileHeightUnit) [3]float32 {
|
||||
return [3]float32{
|
||||
factors[side.Offset(-sideOffset)]*float32(slopeHeight) + float32(absoluteHeight),
|
||||
factors[side.Offset(0)]*float32(slopeHeight) + float32(absoluteHeight),
|
||||
factors[side.Offset(+sideOffset)]*float32(slopeHeight) + float32(absoluteHeight),
|
||||
}
|
||||
}
|
||||
entryFloorSideHeights := sideHeights(entrySide, 1, entryFloorFactors, entrySlopeHeight, entryFloorHeight)
|
||||
entryCeilingSideHeights := sideHeights(entrySide, 1, entryCeilingFactors.Negated(), entrySlopeHeight, entryCeilingHeight)
|
||||
otherFloorSideHeights := sideHeights(otherSide, -1, otherFloorFactors, otherSlopeHeight, otherFloorHeight)
|
||||
otherCeilingSideHeights := sideHeights(otherSide, -1, otherCeilingFactors.Negated(), otherSlopeHeight, otherCeilingHeight)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
switch {
|
||||
case (otherFloorSideHeights[i] == entryFloorSideHeights[i]) && (entryFloorSideHeights[i] == float32(TileHeightUnitMax)):
|
||||
// shortcut for solid-to-solid - no "wall" to see here
|
||||
result[i] = 0
|
||||
case (otherFloorSideHeights[i] >= otherCeilingSideHeights[i]) || // other ceiling side seals off at its floor
|
||||
(otherCeilingSideHeights[i] <= entryFloorSideHeights[i]) || // other ceiling side seals off at this floor
|
||||
(otherFloorSideHeights[i] >= entryCeilingSideHeights[i]) || // other floor side seals off at this ceiling
|
||||
(entryFloorSideHeights[i] >= entryCeilingSideHeights[i]): // own ceiling side seals off at floor
|
||||
result[i] = float32(TileHeightUnitMax)
|
||||
default:
|
||||
result[i] = otherFloorSideHeights[i] - entryFloorSideHeights[i]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
)
|
||||
|
||||
type WallHeightsSuite struct {
|
||||
suite.Suite
|
||||
|
||||
tileMap level.TileMap
|
||||
heightsMap level.WallHeightsMap
|
||||
|
||||
tile *level.TileMapEntry
|
||||
}
|
||||
|
||||
func TestWallHeightsSuite(t *testing.T) {
|
||||
suite.Run(t, new(WallHeightsSuite))
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) SetupTest() {
|
||||
suite.tileMap = level.NewTileMap(4, 4)
|
||||
suite.heightsMap = level.NewWallHeightsMap(3, 3)
|
||||
suite.tile = nil
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromResetsToZeroForAllSolids() {
|
||||
suite.givenARandomHeightsMap()
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightsShouldBeForTile(1, 1, 0.0)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsToMaximumHeightUnitFromOpenToSolid() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightsShouldBeForTile(1, 1, 32.0)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(1, 0, level.DirNorth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(0, 1, level.DirEast, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(2, 1, level.DirWest, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsToMaximumHeightUnitFromOpenToSolidSide() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen)
|
||||
suite.givenTile(2, 1).typed(level.TileTypeDiagonalOpenNorthEast)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightsShouldBeForTile(1, 1, 32)
|
||||
suite.thenHeightShouldBeForTileAt(2, 1, level.DirWest, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsHeightDifferencesForFloors() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(0)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeOpen).floorAt(10)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 10, 10, 10)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, -10, -10, -10)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsHeightDifferencesForFloorsRespectingSlope() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeSlopeEastToWest).floorAt(0).sloped(level.TileSlopeControlCeilingFlat, 2)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeOpen).floorAt(10)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 8, 9, 10)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, -10, -9, -8)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsHeightDifferencesForFloorsRespectingSlopeSameFloorHeight() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(0)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeSlopeEastToWest).floorAt(0).sloped(level.TileSlopeControlCeilingFlat, 4)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 4, 2, 0)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 0, -2, -4)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromConsidersCeilingHeightsIfWallingOff() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(0)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeSlopeSouthToNorth).floorAt(0).sloped(level.TileSlopeControlFloorFlat, 8).ceilingAt(8)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromConsidersMirroredSlopesWallingOff() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(0)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeSlopeNorthToSouth).floorAt(0).sloped(level.TileSlopeControlCeilingMirrored, 16)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromWithFloorCrossingOtherCeiling() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(20)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeOpen).floorAt(0).ceilingAt(8)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromWithSameSlopes() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeSlopeEastToWest).sloped(level.TileSlopeControlCeilingInverted, 8)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeSlopeEastToWest).sloped(level.TileSlopeControlCeilingInverted, 8)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 0, 0, 0)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 0, 0, 0)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) givenTile(x, y int) *WallHeightsSuite {
|
||||
suite.tile = suite.Tile(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) typed(tileType level.TileType) *WallHeightsSuite {
|
||||
suite.tile.Type = tileType
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) sloped(ctrl level.TileSlopeControl, height level.TileHeightUnit) *WallHeightsSuite {
|
||||
suite.tile.Flags = suite.tile.Flags.WithSlopeControl(ctrl)
|
||||
suite.tile.SlopeHeight = height
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) floorAt(value level.TileHeightUnit) *WallHeightsSuite {
|
||||
suite.tile.Floor = suite.tile.Floor.WithAbsoluteHeight(value)
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) ceilingAt(value level.TileHeightUnit) *WallHeightsSuite {
|
||||
suite.tile.Ceiling = suite.tile.Ceiling.WithAbsoluteHeight(value)
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) givenARandomHeightsMap() {
|
||||
var random float32
|
||||
randomizeSide := func(side *[3]float32) {
|
||||
for i := 0; i < 3; i++ {
|
||||
random += 0.5
|
||||
if random > 32 {
|
||||
random = -32
|
||||
}
|
||||
side[i] = random
|
||||
}
|
||||
}
|
||||
randomizeTile := func(heights *level.WallHeights) {
|
||||
randomizeSide(&heights.North)
|
||||
randomizeSide(&heights.East)
|
||||
randomizeSide(&heights.South)
|
||||
randomizeSide(&heights.West)
|
||||
}
|
||||
|
||||
for _, row := range suite.heightsMap {
|
||||
for i := 0; i < len(row); i++ {
|
||||
heights := &row[i]
|
||||
randomizeTile(heights)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) whenHeightsAreCalculatedFromMap() {
|
||||
suite.heightsMap.CalculateFrom(suite)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) thenHeightsShouldBeForTile(x int, y int, expected float32) {
|
||||
tile := suite.heightsMap.Tile(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
|
||||
verifySide := func(name string, values [3]float32) {
|
||||
for i := 0; i < len(values); i++ {
|
||||
assert.Equal(suite.T(), expected, values[i], fmt.Sprintf("%s[%d] mismatch", name, i))
|
||||
}
|
||||
}
|
||||
verifySide("North", tile.North)
|
||||
verifySide("East", tile.East)
|
||||
verifySide("South", tile.South)
|
||||
verifySide("West", tile.West)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) thenHeightShouldBeForTileAt(x int, y int, side level.Direction,
|
||||
expectedLeft float32, expectedCenter float32, expectedRight float32) {
|
||||
tile := suite.heightsMap.Tile(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
var values [3]float32
|
||||
|
||||
switch side {
|
||||
case level.DirNorth:
|
||||
values = tile.North
|
||||
case level.DirEast:
|
||||
values = tile.East
|
||||
case level.DirSouth:
|
||||
values = tile.South
|
||||
case level.DirWest:
|
||||
values = tile.West
|
||||
default:
|
||||
require.Fail(suite.T(), "Invalid side specified")
|
||||
}
|
||||
|
||||
assert.Equal(suite.T(), expectedLeft, values[0], "left mismatch")
|
||||
assert.Equal(suite.T(), expectedCenter, values[1], "center mismatch")
|
||||
assert.Equal(suite.T(), expectedRight, values[2], "right mismatch")
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) Tile(pos level.TilePosition) *level.TileMapEntry {
|
||||
return suite.tileMap.Tile(pos, 2)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// WallTexturePattern describes how a wall texture should be applied for a tile.
|
||||
type WallTexturePattern byte
|
||||
|
||||
// String returns the textual information.
|
||||
func (pattern WallTexturePattern) String() string {
|
||||
switch pattern {
|
||||
case WallTexturePatternRegular:
|
||||
return "Regular"
|
||||
case WallTexturePatternFlipHorizontal:
|
||||
return "FlipHorizontal"
|
||||
case WallTexturePatternFlipAlternating:
|
||||
return "FlipAlternating"
|
||||
case WallTexturePatternFlipAlternatingInverted:
|
||||
return "FlipAlternatingInverted"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%02X", int(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
// WallTexturePattern constants are listed below.
|
||||
const (
|
||||
WallTexturePatternRegular WallTexturePattern = 0
|
||||
WallTexturePatternFlipHorizontal WallTexturePattern = 1
|
||||
WallTexturePatternFlipAlternating WallTexturePattern = 2
|
||||
WallTexturePatternFlipAlternatingInverted WallTexturePattern = 3
|
||||
)
|
||||
|
||||
// WallTexturePatterns returns all available texture patterns.
|
||||
func WallTexturePatterns() []WallTexturePattern {
|
||||
return []WallTexturePattern{
|
||||
WallTexturePatternRegular,
|
||||
WallTexturePatternFlipHorizontal,
|
||||
WallTexturePatternFlipAlternating,
|
||||
WallTexturePatternFlipAlternatingInverted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package lvlids
|
||||
|
||||
const (
|
||||
// PerLevel is the amount of resources reserved per level.
|
||||
PerLevel = 100
|
||||
|
||||
// FirstUsed identifies the first level-specific resource in a level-range.
|
||||
FirstUsed = 2
|
||||
// FirstUnused identifies the first unused level-specific resource in a level-range.
|
||||
FirstUnused = 54
|
||||
)
|
||||
|
||||
// Level resource identifier are listed below.
|
||||
const (
|
||||
MapVersionNumber = 2
|
||||
ObjectVersionNumber = 3
|
||||
Information = 4
|
||||
|
||||
TileMap = 5
|
||||
Schedules = 6
|
||||
TextureAtlas = 7
|
||||
ObjectMainTable = 8
|
||||
ObjectCrossRefTable = 9
|
||||
ObjectClassTablesStart = 10
|
||||
ObjectDefaultTablesStart = 25
|
||||
|
||||
SavefileVersion = 40
|
||||
Unused41 = 41
|
||||
|
||||
TextureAnimations = 42
|
||||
SurveillanceSources = 43
|
||||
SurveillanceSurrogates = 44
|
||||
Parameters = 45
|
||||
MapNotes = 46
|
||||
MapNotesPointer = 47
|
||||
|
||||
Unknown48 = 48
|
||||
Unknown49 = 49
|
||||
Unknown50 = 50
|
||||
|
||||
LoopConfiguration = 51
|
||||
|
||||
Unknown52 = 52
|
||||
HeightSemaphores = 53
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseAnimation = interpreters.New()
|
||||
@@ -0,0 +1,122 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseBigStuff = interpreters.New()
|
||||
|
||||
var multiAnimation = interpreters.New().
|
||||
With("LoopType", 0, 2).As(interpreters.Bitfield(map[uint32]string{0x01: "Forward/Backward", 0x02: "Backward"})).
|
||||
With("Alternation", 2, 2).As(interpreters.SpecialValue("MultiAnimation")).
|
||||
With("Picture", 4, 2).As(interpreters.SpecialValue("PictureSource")).
|
||||
With("Alternate", 6, 2).As(interpreters.SpecialValue("PictureSource"))
|
||||
|
||||
var displayScenery = baseBigStuff.
|
||||
With("FrameCount", 0, 2).As(interpreters.RangedValue(0, 8)).
|
||||
Refining("", 2, 8, multiAnimation, interpreters.Always)
|
||||
|
||||
var displayControlPedestal = baseBigStuff.
|
||||
With("FrameCount", 0, 2).As(interpreters.RangedValue(0, 8)).
|
||||
With("TriggerObjectID1", 2, 2).As(interpreters.ObjectID()).
|
||||
With("TriggerObjectID2", 4, 2).As(interpreters.ObjectID()).
|
||||
Refining("", 2, 8, multiAnimation, interpreters.Always)
|
||||
|
||||
var cabinetFurniture = baseBigStuff.
|
||||
With("Object1ID", 2, 2).As(interpreters.ObjectID()).
|
||||
With("Object2ID", 4, 2).As(interpreters.ObjectID())
|
||||
|
||||
var texturableFurniture = baseBigStuff.
|
||||
With("Texture", 6, 2).As(interpreters.SpecialValue("PictureSource"))
|
||||
|
||||
var wordScenery = baseBigStuff.
|
||||
With("TextIndex", 0, 2).As(interpreters.RangedValue(0, 511)).
|
||||
With("Font", 2, 1).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x0F: "Face",
|
||||
0xF0: "Size"})).
|
||||
With("Color", 4, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var textureMapScenery = baseBigStuff.
|
||||
With("TextureIndex", 6, 2).As(interpreters.SpecialValue("LevelTexture"))
|
||||
|
||||
var buttonControlPedestal = baseBigStuff.
|
||||
With("TriggerObjectID1", 2, 2).As(interpreters.ObjectID()).
|
||||
With("TriggerObjectID2", 4, 2).As(interpreters.ObjectID())
|
||||
|
||||
var surgicalMachine = baseBigStuff.
|
||||
With("BrokenState", 2, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "OK", 0xE7: "Broken"})).
|
||||
With("BrokenMessageIndex", 5, 1)
|
||||
|
||||
var securityCamera = baseBigStuff.
|
||||
With("PanningSwitch", 2, 1).As(interpreters.EnumValue(map[uint32]string{0: "Stationary", 1: "Panning"}))
|
||||
|
||||
var solidBridge = baseBigStuff.
|
||||
With("Size", 2, 1).As(interpreters.Bitfield(map[uint32]string{0x0F: "X", 0xF0: "Y"})).
|
||||
With("Height", 3, 1).As(interpreters.SpecialValue("ObjectHeight")).
|
||||
With("TopBottomTexture", 4, 1).As(interpreters.SpecialValue("MaterialOrLevelTexture")).
|
||||
With("SideTexture", 5, 1).As(interpreters.SpecialValue("MaterialOrLevelTexture"))
|
||||
|
||||
var forceBridge = baseBigStuff.
|
||||
With("Size", 2, 1).As(interpreters.Bitfield(map[uint32]string{0x0F: "X", 0xF0: "Y"})).
|
||||
With("Height", 3, 1).As(interpreters.SpecialValue("ObjectHeight")).
|
||||
With("Color", 6, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) (result string) {
|
||||
return forceColors[value]
|
||||
}))
|
||||
|
||||
func initBigStuff() interpreterRetriever {
|
||||
displays := newInterpreterLeaf(displayScenery)
|
||||
textureable := newInterpreterLeaf(texturableFurniture)
|
||||
|
||||
electronics := newInterpreterEntry(baseBigStuff)
|
||||
electronics.set(6, displays)
|
||||
electronics.set(7, displays)
|
||||
|
||||
furniture := newInterpreterEntry(baseBigStuff)
|
||||
furniture.set(2, newInterpreterLeaf(cabinetFurniture))
|
||||
furniture.set(5, textureable)
|
||||
furniture.set(7, textureable)
|
||||
furniture.set(8, textureable)
|
||||
|
||||
surfaces := newInterpreterEntry(baseBigStuff)
|
||||
surfaces.set(3, newInterpreterLeaf(wordScenery))
|
||||
surfaces.set(6, displays)
|
||||
surfaces.set(7, newInterpreterLeaf(textureMapScenery))
|
||||
surfaces.set(8, displays)
|
||||
surfaces.set(9, displays)
|
||||
|
||||
lighting := newInterpreterEntry(baseBigStuff)
|
||||
|
||||
medicalEquipment := newInterpreterEntry(baseBigStuff)
|
||||
medicalEquipment.set(0, newInterpreterLeaf(buttonControlPedestal))
|
||||
medicalEquipment.set(3, newInterpreterLeaf(surgicalMachine))
|
||||
medicalEquipment.set(5, textureable)
|
||||
|
||||
scienceSecurityEquipment := newInterpreterEntry(baseBigStuff)
|
||||
scienceSecurityEquipment.set(4, newInterpreterLeaf(securityCamera))
|
||||
scienceSecurityEquipment.set(5, newInterpreterLeaf(buttonControlPedestal))
|
||||
scienceSecurityEquipment.set(6, newInterpreterLeaf(displayControlPedestal))
|
||||
|
||||
gardenScenery := newInterpreterEntry(baseBigStuff)
|
||||
|
||||
bridges := newInterpreterEntry(baseBigStuff)
|
||||
solidBridges := newInterpreterLeaf(solidBridge)
|
||||
forceBridges := newInterpreterLeaf(forceBridge)
|
||||
bridges.set(0, solidBridges)
|
||||
bridges.set(1, solidBridges)
|
||||
bridges.set(7, forceBridges)
|
||||
bridges.set(8, forceBridges)
|
||||
bridges.set(9, forceBridges)
|
||||
|
||||
class := newInterpreterEntry(baseBigStuff)
|
||||
class.set(0, electronics)
|
||||
class.set(1, furniture)
|
||||
class.set(2, surfaces)
|
||||
class.set(3, lighting)
|
||||
class.set(4, medicalEquipment)
|
||||
class.set(5, scienceSecurityEquipment)
|
||||
class.set(6, gardenScenery)
|
||||
class.set(7, bridges)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseContainer = interpreters.New()
|
||||
|
||||
var standardContainer = baseContainer.
|
||||
With("ObjectID1", 0, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 2, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID4", 6, 2).As(interpreters.ObjectID())
|
||||
|
||||
var crate = standardContainer.
|
||||
With("Width", 8, 1).
|
||||
With("Depth", 9, 1).
|
||||
With("Height", 10, 1).
|
||||
With("TopBottomTexture", 11, 1).
|
||||
With("SideTexture", 12, 1)
|
||||
|
||||
func initContainers() interpreterRetriever {
|
||||
standardContainers := newInterpreterLeaf(standardContainer)
|
||||
crates := newInterpreterLeaf(crate)
|
||||
|
||||
class := newInterpreterEntry(baseContainer)
|
||||
class.set(0, crates)
|
||||
class.set(1, standardContainers)
|
||||
class.set(2, standardContainers)
|
||||
class.set(3, standardContainers)
|
||||
class.set(4, standardContainers)
|
||||
class.set(5, standardContainers)
|
||||
class.set(6, standardContainers)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseCritter = interpreters.New().
|
||||
With("PendingZRotation", 0, 4).
|
||||
With("ForwardVelocityFraction", 4, 2).
|
||||
With("ForwardVelocity", 6, 2).
|
||||
With("Hastiness", 0x0A, 2).
|
||||
With("StateTimeout", 0x0C, 2).As(interpreters.RangedValue(0, 300)).
|
||||
With("Unknown000E", 0x0E, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("Unknown0010", 0x10, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("RoamingState", 0x14, 1).
|
||||
With("PrimaryState", 0x15, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "docile",
|
||||
1: "cautious",
|
||||
2: "hostile",
|
||||
3: "cautious (?)",
|
||||
4: "attacking",
|
||||
5: "sleeping",
|
||||
6: "tranquilized",
|
||||
7: "confused"})).
|
||||
With("SecondaryState", 0x16, 1).
|
||||
With("TertiaryState", 0x17, 1).
|
||||
With("AICoordinates0018", 0x18, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("AICoordinates001A", 0x1A, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("AICoordinates001C", 0x1C, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("Always255", 0x1E, 1).As(interpreters.RangedValue(0, 255)).
|
||||
With("AICoordinates001F", 0x1F, 1).As(interpreters.SpecialValue("Unknown")).
|
||||
With("LootObjectID1", 0x20, 2).As(interpreters.ObjectID()).
|
||||
With("LootObjectID2", 0x22, 2).As(interpreters.ObjectID()).
|
||||
With("Unknown0026", 0x26, 2).As(interpreters.SpecialValue("Unknown"))
|
||||
|
||||
func initCritters() interpreterRetriever {
|
||||
class := newInterpreterEntry(baseCritter)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseDoor = interpreters.New().
|
||||
With("LockVariableIndex", 0, 2).As(interpreters.SpecialValue("LockVariable")).
|
||||
With("LockMessageIndex", 2, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) (result string) {
|
||||
return fmt.Sprintf("%d", value+7)
|
||||
})).
|
||||
With("ForceDoorColor", 3, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) (result string) {
|
||||
return forceColors[value]
|
||||
})).
|
||||
With("RequiredAccessLevel", 4, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) (result string) {
|
||||
if value == 255 {
|
||||
result = "SHODAN"
|
||||
} else if accessLevel, known := accessLevelMasks[1<<uint32(value)]; known {
|
||||
result = accessLevel
|
||||
} else {
|
||||
result = "Unknown"
|
||||
}
|
||||
return
|
||||
})).
|
||||
With("AutoCloseTime", 5, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) string {
|
||||
return fmt.Sprintf("%.2f sec", float64(value)*0.5)
|
||||
})).
|
||||
With("OtherObjectID", 6, 2).As(interpreters.ObjectID())
|
||||
|
||||
func initDoors() interpreterRetriever {
|
||||
return newInterpreterLeaf(baseDoor)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj/actions"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj/conditions"
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseFixture = interpreters.New()
|
||||
|
||||
var gameVariablePanel = baseFixture.
|
||||
Refining("Condition", 2, 4, conditions.GameVariable(), interpreters.Always)
|
||||
|
||||
var buttonPanel = gameVariablePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always).
|
||||
With("AccessMask", 22, 2)
|
||||
|
||||
var recepticlePanel = baseFixture
|
||||
|
||||
var standardRecepticle = recepticlePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always).
|
||||
Refining("TypeCondition", 2, 3, conditions.ObjectType(), interpreters.Always).
|
||||
With("FailMessage", 5, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var antennaRelayPanel = recepticlePanel.
|
||||
With("TriggerObjectID1", 6, 2).As(interpreters.ObjectID()).
|
||||
With("TriggerObjectID2", 10, 2).As(interpreters.ObjectID()).
|
||||
With("DestroyObjectID", 14, 2).As(interpreters.ObjectID())
|
||||
|
||||
var retinalIDScanner = recepticlePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always).
|
||||
With("Head", 2, 3).As(interpreters.FormattedRangedValue(0, 21,
|
||||
func(value int) (result string) {
|
||||
if value < 11 {
|
||||
return fmt.Sprintf("(8/2/13 frame: %d)", value)
|
||||
} else {
|
||||
return fmt.Sprintf("(8/2/14 frame: %d)", value-11)
|
||||
}
|
||||
}))
|
||||
|
||||
var cyberspaceTerminal = gameVariablePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always)
|
||||
|
||||
var energyChargeStation = gameVariablePanel.
|
||||
With("EnergyDelta", 6, 4).As(interpreters.RangedValue(0, 255)).
|
||||
With("RechargeTime", 10, 4).As(interpreters.FormattedRangedValue(0, 3600,
|
||||
func(value int) string {
|
||||
return fmt.Sprintf("%d sec", value)
|
||||
})).
|
||||
With("TriggerObjectID", 14, 4).As(interpreters.ObjectID()).
|
||||
With("RechargedTimestamp", 18, 4)
|
||||
|
||||
var vendingMachine = baseFixture.
|
||||
Refining("TypeCondition", 2, 3, conditions.ObjectType(), interpreters.Always).
|
||||
With("FailMessage", 5, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var inputPanel = gameVariablePanel
|
||||
|
||||
var wirePuzzleStateDescription = interpreters.Bitfield(map[uint32]string{
|
||||
0x00000007: "Wire 1 Left",
|
||||
0x00000038: "Wire 1 Right",
|
||||
0x000001C0: "Wire 2 Left",
|
||||
0x00000E00: "Wire 2 Right",
|
||||
|
||||
0x00007000: "Wire 3 Left",
|
||||
0x00038000: "Wire 3 Right",
|
||||
0x001C0000: "Wire 4 Left",
|
||||
0x00E00000: "Wire 4 Right",
|
||||
|
||||
0x07000000: "Wire 5 Left",
|
||||
0x38000000: "Wire 5 Right"})
|
||||
|
||||
var wirePuzzleData = interpreters.New().
|
||||
With("TargetObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("Layout", 4, 1).As(interpreters.Bitfield(map[uint32]string{0x0F: "Wires", 0xF0: "Connectors"})).
|
||||
With("TargetPowerLevel", 5, 1).
|
||||
With("WireProperties", 6, 1).As(interpreters.Bitfield(map[uint32]string{0x01: "Colored", 0xF0: "Solved"})).
|
||||
With("TargetState", 8, 4).As(wirePuzzleStateDescription).
|
||||
With("CurrentState", 12, 4).As(wirePuzzleStateDescription)
|
||||
|
||||
var blockPuzzleData = interpreters.New().
|
||||
With("TargetObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("StateStoreObjectID", 4, 2).As(interpreters.ObjectID()).
|
||||
With("Layout", 8, 4).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x00000001: "PuzzleSolved",
|
||||
0x00000070: "SourceCoordinate",
|
||||
0x00000180: "SourceLocation",
|
||||
0x00007000: "DestCoordinate",
|
||||
0x00018000: "DestLocation",
|
||||
0x00700000: "Width",
|
||||
0x07000000: "Height",
|
||||
0x70000000: "SideEffectType"}))
|
||||
|
||||
var puzzleSpecificData = interpreters.New().
|
||||
With("Type", 7, 1).As(interpreters.EnumValue(map[uint32]string{0: "WirePuzzle", 0x10: "BlockPuzzle"})).
|
||||
Refining("Wire", 0, 18, wirePuzzleData, func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("Type") == 0
|
||||
}).
|
||||
Refining("Block", 0, 18, blockPuzzleData, func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("Type") == 0x10
|
||||
})
|
||||
|
||||
var puzzlePanel = inputPanel.
|
||||
Refining("Puzzle", 6, 18, puzzleSpecificData, interpreters.Always)
|
||||
|
||||
var elevatorPanel = inputPanel.
|
||||
With("DestinationObjectIndex2", 6, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex1", 8, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex4", 10, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex3", 12, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex6", 14, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex5", 16, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("AccessibleBitmask", 18, 2).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x0001: "Level 0",
|
||||
0x0002: "Level 1",
|
||||
0x0004: "Level 2",
|
||||
0x0008: "Level 3",
|
||||
0x0010: "Level 4",
|
||||
0x0020: "Level 5",
|
||||
0x0040: "Level 6",
|
||||
0x0080: "Level 7",
|
||||
0x0100: "Level 8",
|
||||
0x0200: "Level 9",
|
||||
0x0400: "Level 10",
|
||||
0x0800: "Level 11",
|
||||
0x1000: "Level 12",
|
||||
0x2000: "Level 13",
|
||||
0x4000: "Level 14",
|
||||
0x8000: "Level 15"})).
|
||||
With("ElevatorShaftBitmask", 20, 2).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x0001: "Level 0",
|
||||
0x0002: "Level 1",
|
||||
0x0004: "Level 2",
|
||||
0x0008: "Level 3",
|
||||
0x0010: "Level 4",
|
||||
0x0020: "Level 5",
|
||||
0x0040: "Level 6",
|
||||
0x0080: "Level 7",
|
||||
0x0100: "Level 8",
|
||||
0x0200: "Level 9",
|
||||
0x0400: "Level 10",
|
||||
0x0800: "Level 11",
|
||||
0x1000: "Level 12",
|
||||
0x2000: "Level 13",
|
||||
0x4000: "Level 14",
|
||||
0x8000: "Level 15"}))
|
||||
|
||||
var numberPad = inputPanel.
|
||||
With("Combination1", 6, 2).As(interpreters.SpecialValue("BinaryCodedDecimal")).
|
||||
With("TriggerObjectID1", 8, 2).As(interpreters.ObjectID()).
|
||||
With("Combination2", 10, 2).As(interpreters.SpecialValue("BinaryCodedDecimal")).
|
||||
With("TriggerObjectID2", 12, 2).As(interpreters.ObjectID()).
|
||||
With("Combination3", 14, 2).As(interpreters.SpecialValue("BinaryCodedDecimal")).
|
||||
With("TriggerObjectID3", 16, 2).As(interpreters.ObjectID()).
|
||||
With("FailObjectID", 18, 2).As(interpreters.ObjectID())
|
||||
|
||||
var inactiveCyberspaceSwitch = gameVariablePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always)
|
||||
|
||||
func initFixtures() interpreterRetriever {
|
||||
standardRecepticles := newInterpreterLeaf(standardRecepticle)
|
||||
antennaRelays := newInterpreterLeaf(antennaRelayPanel)
|
||||
recepticles := newInterpreterEntry(recepticlePanel)
|
||||
recepticles.set(0, standardRecepticles)
|
||||
recepticles.set(1, standardRecepticles)
|
||||
recepticles.set(2, standardRecepticles)
|
||||
recepticles.set(3, antennaRelays) // standard panel
|
||||
recepticles.set(4, antennaRelays) // plastiqued
|
||||
recepticles.set(6, newInterpreterLeaf(retinalIDScanner))
|
||||
|
||||
stations := newInterpreterEntry(baseFixture)
|
||||
stations.set(0, newInterpreterLeaf(cyberspaceTerminal))
|
||||
stations.set(1, newInterpreterLeaf(energyChargeStation))
|
||||
|
||||
vendingMachines := newInterpreterEntry(vendingMachine)
|
||||
|
||||
puzzles := newInterpreterLeaf(puzzlePanel)
|
||||
elevatorPanels := newInterpreterLeaf(elevatorPanel)
|
||||
numberPads := newInterpreterLeaf(numberPad)
|
||||
inputPanels := newInterpreterEntry(inputPanel)
|
||||
inputPanels.set(0, puzzles)
|
||||
inputPanels.set(1, puzzles)
|
||||
inputPanels.set(2, puzzles)
|
||||
inputPanels.set(3, puzzles)
|
||||
inputPanels.set(4, elevatorPanels)
|
||||
inputPanels.set(5, elevatorPanels)
|
||||
inputPanels.set(6, elevatorPanels)
|
||||
inputPanels.set(7, numberPads)
|
||||
inputPanels.set(8, numberPads)
|
||||
inputPanels.set(9, puzzles)
|
||||
inputPanels.set(10, puzzles)
|
||||
|
||||
cyberspaceSwitches := newInterpreterEntry(baseFixture)
|
||||
cyberspaceSwitches.set(0, newInterpreterLeaf(inactiveCyberspaceSwitch))
|
||||
|
||||
class := newInterpreterEntry(baseFixture)
|
||||
class.set(0, newInterpreterLeaf(buttonPanel))
|
||||
class.set(1, recepticles)
|
||||
class.set(2, stations)
|
||||
class.set(3, inputPanels)
|
||||
class.set(4, vendingMachines)
|
||||
class.set(5, cyberspaceSwitches)
|
||||
|
||||
return class
|
||||
}
|
||||
|
||||
func initCyberspaceFixtures() interpreterRetriever {
|
||||
cyberspaceSwitches := newInterpreterEntry(baseFixture)
|
||||
cyberspaceSwitches.set(0, newInterpreterLeaf(inactiveCyberspaceSwitch))
|
||||
|
||||
class := newInterpreterEntry(baseFixture)
|
||||
class.set(5, cyberspaceSwitches)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
// ForCyberspace returns an interpreter instance that handles the level class
|
||||
// data of the specified object - in cyberspace.
|
||||
func ForCyberspace(triple object.Triple, data []byte) *interpreters.Instance {
|
||||
return cyberspaceEntries.specialize(int(triple.Class)).specialize(int(triple.Subclass)).specialize(int(triple.Type)).instance(data)
|
||||
}
|
||||
|
||||
// CyberspaceExtra returns an interpreter instance that handles the level object extra
|
||||
// data of the specified object - in cybperspace.
|
||||
func CyberspaceExtra(triple object.Triple, data []byte) *interpreters.Instance {
|
||||
return cyberspaceExtras.specialize(int(triple.Class)).specialize(int(triple.Subclass)).specialize(int(triple.Type)).instance(data)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
// ForRealWorld returns an interpreter instance that handles the level class
|
||||
// data of the specified object - in real world.
|
||||
func ForRealWorld(triple object.Triple, data []byte) *interpreters.Instance {
|
||||
return realWorldEntries.specialize(int(triple.Class)).specialize(int(triple.Subclass)).specialize(int(triple.Type)).instance(data)
|
||||
}
|
||||
|
||||
// RealWorldExtra returns an interpreter instance that handles the level object extra
|
||||
// data of the specified object - in real world.
|
||||
func RealWorldExtra(triple object.Triple, data []byte) *interpreters.Instance {
|
||||
return realWorldExtras.specialize(int(triple.Class)).specialize(int(triple.Subclass)).specialize(int(triple.Type)).instance(data)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package lvlobj
|
||||
|
||||
var forceColors = map[int]string{
|
||||
0: "Default (red)",
|
||||
|
||||
247: "Gray, medium",
|
||||
248: "Gray, light",
|
||||
|
||||
249: "Red, light",
|
||||
250: "Green, light",
|
||||
251: "Blue, light",
|
||||
|
||||
252: "Invisible",
|
||||
|
||||
253: "Blue, medium",
|
||||
254: "Green, medium",
|
||||
255: "Red, medium"}
|
||||
@@ -0,0 +1,21 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseGrenade = interpreters.New().
|
||||
With("Unknown0000", 0, 2).
|
||||
With("State", 2, 2).As(interpreters.EnumValue(map[uint32]string{0: "Inert", 1: "Thrown Live", 5: "Landed Live"})).
|
||||
With("TimerTime", 4, 2).As(interpreters.RangedValue(0, 32767))
|
||||
|
||||
func initGrenades() interpreterRetriever {
|
||||
timedExplosives := newInterpreterEntry(baseGrenade)
|
||||
|
||||
timedExplosives.set(2, newInterpreterLeaf(interpreters.New())) // Object explosion - not encountered
|
||||
|
||||
class := newInterpreterEntry(baseGrenade)
|
||||
class.set(1, timedExplosives)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnergyWeaponMaxTemperature is the maximum value possible for heat.
|
||||
EnergyWeaponMaxTemperature = 100
|
||||
// EnergyWeaponMaxEnergy is the maximum value for energy.
|
||||
// The original source does not explicitly state this value, it has been determined by looking at
|
||||
// a savegame with the energy set to max. A closely related number in the source is
|
||||
// in the macro "mfd_charge_units_per_pixel", which scales to 69.
|
||||
EnergyWeaponMaxEnergy = 59
|
||||
)
|
||||
|
||||
var baseGun = interpreters.New()
|
||||
|
||||
var energyWeapon = baseGun.
|
||||
With("Charge", 0, 1).As(interpreters.Bitfield(
|
||||
map[uint32]string{
|
||||
0x80: "Overload",
|
||||
0x7F: "Energy",
|
||||
})).
|
||||
With("Temperature", 1, 1).As(interpreters.RangedValue(0, EnergyWeaponMaxTemperature))
|
||||
|
||||
var projectileWeapon = baseGun.
|
||||
With("AmmoType", 0, 1).As(interpreters.EnumValue(map[uint32]string{0: "Standard", 1: "Special"})).
|
||||
With("AmmoCount", 1, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
func initGuns() interpreterRetriever {
|
||||
projectileWeapons := newInterpreterLeaf(projectileWeapon)
|
||||
energyWeapons := newInterpreterLeaf(energyWeapon)
|
||||
|
||||
class := newInterpreterEntry(baseGun)
|
||||
class.set(0, projectileWeapons)
|
||||
class.set(1, projectileWeapons)
|
||||
class.set(2, projectileWeapons)
|
||||
|
||||
class.set(4, energyWeapons)
|
||||
class.set(5, energyWeapons)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseHardware = interpreters.New().
|
||||
With("Version", 0, 1).As(interpreters.RangedValue(0, 4))
|
||||
@@ -0,0 +1,9 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
// InterpreterFactory returns an interpreter instance according to a specific object triple.
|
||||
type InterpreterFactory func(object.Triple, []byte) *interpreters.Instance
|
||||
@@ -0,0 +1,53 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
type interpreterRetriever interface {
|
||||
specialize(key int) interpreterRetriever
|
||||
instance(data []byte) *interpreters.Instance
|
||||
}
|
||||
|
||||
type interpreterLeaf struct {
|
||||
desc *interpreters.Description
|
||||
}
|
||||
|
||||
func newInterpreterLeaf(desc *interpreters.Description) *interpreterLeaf {
|
||||
return &interpreterLeaf{desc: desc}
|
||||
}
|
||||
|
||||
func (node *interpreterLeaf) specialize(key int) interpreterRetriever {
|
||||
return node
|
||||
}
|
||||
|
||||
func (node *interpreterLeaf) instance(data []byte) *interpreters.Instance {
|
||||
return node.desc.For(data)
|
||||
}
|
||||
|
||||
type interpreterEntry struct {
|
||||
defaultLeaf *interpreterLeaf
|
||||
subEntries map[int]interpreterRetriever
|
||||
}
|
||||
|
||||
func newInterpreterEntry(defaultDesc *interpreters.Description) *interpreterEntry {
|
||||
return &interpreterEntry{
|
||||
defaultLeaf: newInterpreterLeaf(defaultDesc),
|
||||
subEntries: make(map[int]interpreterRetriever)}
|
||||
}
|
||||
|
||||
func (node *interpreterEntry) set(key int, sub interpreterRetriever) {
|
||||
node.subEntries[key] = sub
|
||||
}
|
||||
|
||||
func (node *interpreterEntry) specialize(key int) (retriever interpreterRetriever) {
|
||||
retriever = node.subEntries[key]
|
||||
if retriever == nil {
|
||||
retriever = node.defaultLeaf
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (node *interpreterEntry) instance(data []byte) *interpreters.Instance {
|
||||
return node.defaultLeaf.instance(data)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var basePhysics = interpreters.New()
|
||||
@@ -0,0 +1,128 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseItem = interpreters.New()
|
||||
|
||||
var paperItem = baseItem.
|
||||
With("PaperId", 2, 1)
|
||||
|
||||
var briefcaseItem = baseItem.
|
||||
With("ObjectID1", 2, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 6, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID4", 8, 2).As(interpreters.ObjectID())
|
||||
|
||||
var corpseItem = baseItem.
|
||||
With("Unknown0000", 0, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("ObjectID1", 2, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 6, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID4", 8, 2).As(interpreters.ObjectID())
|
||||
|
||||
var severedHeadItem = baseItem.
|
||||
With("ImageIndex", 2, 1)
|
||||
|
||||
var accessLevelMasks = map[uint32]string{
|
||||
0x00000001: "None",
|
||||
0x00000002: "Generic1",
|
||||
0x00000004: "Generic2",
|
||||
0x00000008: "Generic3",
|
||||
0x00000010: "Generic4",
|
||||
0x00000020: "Generic5",
|
||||
0x00000040: "Generic6",
|
||||
0x00000080: "Generic7",
|
||||
|
||||
0x00000100: "Group1",
|
||||
0x00000200: "Group2",
|
||||
0x00000400: "Group3",
|
||||
0x00000800: "Group4",
|
||||
0x00001000: "Group5",
|
||||
0x00002000: "Group6",
|
||||
0x00004000: "Group7",
|
||||
0x00008000: "Group8",
|
||||
|
||||
0x00010000: "Group9",
|
||||
0x00020000: "Group10",
|
||||
0x00040000: "Group11",
|
||||
0x00080000: "Group12",
|
||||
0x00100000: "Group13",
|
||||
0x00200000: "Group14",
|
||||
0x00400000: "Group15",
|
||||
0x00800000: "Group16",
|
||||
|
||||
0x01000000: "Personal1",
|
||||
0x02000000: "Personal2",
|
||||
0x04000000: "Personal3",
|
||||
0x08000000: "Personal4",
|
||||
0x10000000: "Personal5",
|
||||
0x20000000: "Personal6",
|
||||
0x40000000: "Personal7"}
|
||||
|
||||
var accessMaskDescription = interpreters.Bitfield(accessLevelMasks)
|
||||
|
||||
var accessCardItem = baseItem.
|
||||
With("Ignored0000", 0, 2).As(interpreters.SpecialValue("Ignored")).
|
||||
With("AccessMask", 2, 4).As(accessMaskDescription)
|
||||
|
||||
var securityIDModuleItem = baseItem.
|
||||
With("AccessMask", 2, 4).As(accessMaskDescription)
|
||||
|
||||
var cyberInfoNodeItem = baseItem.
|
||||
With("TextIndex", 2, 1)
|
||||
|
||||
var cyberRestorative = baseItem.
|
||||
With("RestorationAmount", 2, 1)
|
||||
|
||||
var cyberDefenseMine = baseItem.
|
||||
With("DamageAmount", 2, 1)
|
||||
|
||||
var cyberBarricade = baseItem.
|
||||
With("Size", 2, 1).
|
||||
With("Height", 3, 1).
|
||||
With("Color", 6, 1)
|
||||
|
||||
func initSmallStuff() interpreterRetriever {
|
||||
junk := newInterpreterEntry(baseItem)
|
||||
junk.set(2, newInterpreterLeaf(paperItem))
|
||||
junk.set(7, newInterpreterLeaf(briefcaseItem))
|
||||
|
||||
dead := newInterpreterEntry(baseItem)
|
||||
corpses := newInterpreterLeaf(corpseItem)
|
||||
severedHeads := newInterpreterLeaf(severedHeadItem)
|
||||
dead.set(0, corpses)
|
||||
dead.set(1, corpses)
|
||||
dead.set(2, corpses)
|
||||
dead.set(3, corpses)
|
||||
dead.set(4, corpses)
|
||||
dead.set(5, corpses)
|
||||
dead.set(6, corpses)
|
||||
dead.set(7, corpses)
|
||||
dead.set(13, severedHeads)
|
||||
dead.set(14, severedHeads)
|
||||
|
||||
class := newInterpreterEntry(baseItem)
|
||||
class.set(0, junk)
|
||||
class.set(2, dead)
|
||||
class.set(4, newInterpreterLeaf(accessCardItem))
|
||||
|
||||
return class
|
||||
}
|
||||
|
||||
func initCyberspaceSmallStuff() interpreterRetriever {
|
||||
cyberspaceItems := newInterpreterEntry(baseItem)
|
||||
infoNodes := newInterpreterLeaf(cyberInfoNodeItem)
|
||||
cyberspaceItems.set(1, newInterpreterLeaf(cyberRestorative))
|
||||
cyberspaceItems.set(2, newInterpreterLeaf(cyberDefenseMine))
|
||||
cyberspaceItems.set(3, newInterpreterLeaf(securityIDModuleItem))
|
||||
cyberspaceItems.set(6, infoNodes)
|
||||
cyberspaceItems.set(8, infoNodes)
|
||||
cyberspaceItems.set(9, newInterpreterLeaf(cyberBarricade))
|
||||
|
||||
class := newInterpreterEntry(baseItem)
|
||||
class.set(5, cyberspaceItems)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseSoftware = interpreters.New()
|
||||
|
||||
var multimediaFile = baseSoftware.
|
||||
With("ID", 1, 1).As(interpreters.Bitfield(map[uint32]string{0x0F: "Index", 0xF0: "Level"})).
|
||||
With("Type", 2, 1).As(interpreters.EnumValue(map[uint32]string{0: "E-Mail", 1: "Log", 2: "Data"}))
|
||||
|
||||
var cyberspaceProgram = baseSoftware.
|
||||
With("Version", 0, 1).As(interpreters.RangedValue(1, 9))
|
||||
|
||||
var funPack = baseSoftware.
|
||||
With("GameMask", 0, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0x01: "Ping",
|
||||
0x02: "Eel Zapper",
|
||||
0x04: "Road",
|
||||
0x08: "Botbounce",
|
||||
0x10: "15",
|
||||
0x20: "TriopToe",
|
||||
0x80: "Wing 0"}))
|
||||
|
||||
var baseCyberspaceScenery = interpreters.New()
|
||||
|
||||
var scenerySoftware = baseCyberspaceScenery.
|
||||
Refining("FunPack", 0, 2, funPack,
|
||||
func(inst *interpreters.Instance) bool {
|
||||
return (inst.Get("Subclass") == 3) && (inst.Get("Type") == 0)
|
||||
}).
|
||||
Refining("Program", 0, 2, cyberspaceProgram,
|
||||
func(inst *interpreters.Instance) bool {
|
||||
return (inst.Get("Subclass") == 0) || (inst.Get("Subclass") == 1)
|
||||
}).
|
||||
With("Subclass", 2, 4).As(interpreters.RangedValue(0, 7)).
|
||||
With("Type", 6, 4).As(interpreters.RangedValue(0, 16))
|
||||
|
||||
func initSoftware() interpreterRetriever {
|
||||
cyberspacePrograms := newInterpreterLeaf(cyberspaceProgram)
|
||||
|
||||
realWorldTools := newInterpreterEntry(baseSoftware)
|
||||
realWorldTools.set(0, newInterpreterLeaf(funPack))
|
||||
|
||||
multimediaSoftware := newInterpreterEntry(baseSoftware)
|
||||
multimediaSoftware.set(0, newInterpreterLeaf(multimediaFile)) // text
|
||||
multimediaSoftware.set(1, newInterpreterLeaf(multimediaFile)) // email
|
||||
|
||||
class := newInterpreterEntry(baseSoftware)
|
||||
class.set(0, cyberspacePrograms) // aggressive programs
|
||||
class.set(1, cyberspacePrograms) // defensive programs
|
||||
class.set(3, realWorldTools)
|
||||
class.set(4, multimediaSoftware)
|
||||
|
||||
return class
|
||||
}
|
||||
|
||||
func initCyberspaceBigStuff() interpreterRetriever {
|
||||
cyberspaceScenery := newInterpreterEntry(scenerySoftware)
|
||||
|
||||
cyberspaceScenerySubclass2 := newInterpreterEntry(baseCyberspaceScenery)
|
||||
cyberspaceScenerySubclass2.set(0, cyberspaceScenery) // "SIGN"
|
||||
cyberspaceScenerySubclass3 := newInterpreterEntry(baseCyberspaceScenery)
|
||||
cyberspaceScenerySubclass3.set(2, cyberspaceScenery) // "GLOW BULB"
|
||||
|
||||
class := newInterpreterEntry(scenerySoftware)
|
||||
|
||||
class.set(2, cyberspaceScenerySubclass2)
|
||||
class.set(3, cyberspaceScenerySubclass3)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj/actions"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj/conditions"
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseTraps = interpreters.New()
|
||||
|
||||
var repulsor = baseTraps.
|
||||
With("Ignored0000", 0, 1).As(interpreters.SpecialValue("Ignored")).
|
||||
With("StartHeightFraction", 10, 2).As(interpreters.RangedValue(0, 65536)).
|
||||
With("StartHeight", 12, 2).As(interpreters.RangedValue(0, 31)).
|
||||
With("EndHeightFraction", 14, 2).As(interpreters.RangedValue(0, 65536)).
|
||||
With("EndHeight", 16, 2).As(interpreters.RangedValue(0, 32)).
|
||||
With("Flags", 18, 4).As(interpreters.Bitfield(map[uint32]string{0x00000001: "Disabled", 0x00000008: "Strong"}))
|
||||
|
||||
var aiHint = baseTraps.
|
||||
With("Ignored0000", 0, 1).As(interpreters.SpecialValue("Ignored")).
|
||||
With("NextObjectID", 6, 2).As(interpreters.ObjectID()).
|
||||
With("TriggerObjectFlag", 18, 2).As(interpreters.EnumValue(map[uint32]string{0: "Off", 1: "On"})).
|
||||
With("TriggerObjectID", 20, 2).As(interpreters.ObjectID())
|
||||
|
||||
var baseTrigger = baseTraps.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always)
|
||||
|
||||
var gameVariableTrigger = baseTrigger.
|
||||
Refining("Condition", 2, 4, conditions.GameVariable(), interpreters.Always)
|
||||
|
||||
var puzzleData = interpreters.New()
|
||||
|
||||
var nullTrigger = baseTraps.
|
||||
Refining("Action", 0, 22, actions.Unconditional().
|
||||
Refining("PuzzleData", 6, 16, puzzleData, func(inst *interpreters.Instance) bool { return inst.Get("Type") == 0 }),
|
||||
interpreters.Always).
|
||||
Refining("Condition", 2, 4, conditions.GameVariable(), interpreters.Always)
|
||||
|
||||
var deathWatchTrigger = baseTrigger.
|
||||
With("ConditionType", 5, 1).As(interpreters.EnumValue(map[uint32]string{0: "Object Type", 1: "Object ID"})).
|
||||
Refining("TypeCondition", 2, 4, conditions.ObjectType(), func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("ConditionType") == 0
|
||||
}).
|
||||
Refining("IndexCondition", 2, 4, conditions.ObjectID(), func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("ConditionType") == 1
|
||||
})
|
||||
|
||||
var ecologyTrigger = baseTrigger.
|
||||
Refining("TypeCondition", 2, 4, conditions.ObjectType(), interpreters.Always).
|
||||
With("ConditionLimit", 5, 1)
|
||||
|
||||
var mapNote = baseTraps.
|
||||
With("EntryOffset", 18, 4)
|
||||
|
||||
var musicVoodoo = baseTraps.
|
||||
With("MusicFlavour", 6, 1).As(interpreters.RangedValue(0, 4))
|
||||
|
||||
func initTraps() interpreterRetriever {
|
||||
gameVariableTriggers := newInterpreterLeaf(gameVariableTrigger)
|
||||
|
||||
trigger := newInterpreterEntry(baseTraps)
|
||||
trigger.set(0, gameVariableTriggers) // tile entry trigger
|
||||
trigger.set(1, newInterpreterLeaf(nullTrigger))
|
||||
trigger.set(2, gameVariableTriggers) // floor trigger
|
||||
trigger.set(3, gameVariableTriggers) // player death trigger
|
||||
trigger.set(4, newInterpreterLeaf(deathWatchTrigger))
|
||||
trigger.set(7, newInterpreterLeaf(aiHint))
|
||||
trigger.set(8, gameVariableTriggers) // level entry trigger
|
||||
trigger.set(10, newInterpreterLeaf(repulsor))
|
||||
trigger.set(11, newInterpreterLeaf(ecologyTrigger))
|
||||
trigger.set(12, gameVariableTriggers) // shodan trigger
|
||||
|
||||
mapMarker := newInterpreterEntry(baseTraps)
|
||||
mapMarker.set(3, newInterpreterLeaf(mapNote))
|
||||
mapMarker.set(4, newInterpreterLeaf(musicVoodoo))
|
||||
|
||||
class := newInterpreterEntry(baseTraps)
|
||||
class.set(0, trigger)
|
||||
class.set(2, mapMarker)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
func forType(typeID int) func(*interpreters.Instance) bool {
|
||||
return func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("Type") == uint32(typeID)
|
||||
}
|
||||
}
|
||||
|
||||
var transportHackerDetails = interpreters.New().
|
||||
With("TargetX", 0, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetY", 4, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetZ", 8, 1).As(interpreters.RangedValue(0, 255)).
|
||||
With("PreserveHeight", 9, 1).As(interpreters.EnumValue(map[uint32]string{0: "No", 0x40: "Yes"})).
|
||||
With("CrossLevelTransportDestination", 12, 1).As(interpreters.RangedValue(0, 15)).
|
||||
With("CrossLevelTransportFlag", 13, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0x00: "Cross-Level", 0x10: "Same-Level (0x10)", 0x20: "Same-Level (0x20)", 0x22: "Same-Level (0x22)"}))
|
||||
|
||||
var changeHealthDetails = interpreters.New().
|
||||
With("HealthDelta", 4, 1).
|
||||
With("HealthChangeFlag", 6, 2).As(interpreters.EnumValue(map[uint32]string{0: "Remove Delta", 1: "Add Delta"})).
|
||||
With("PowerDelta", 8, 1).
|
||||
With("PowerChangeFlag", 10, 2).As(interpreters.EnumValue(map[uint32]string{0: "Remove Delta", 1: "Add Delta"})).
|
||||
With("FatigueDelta", 12, 2).As(interpreters.RangedValue(0, 255)).
|
||||
With("FatigueChangeFlag", 14, 2).As(interpreters.EnumValue(map[uint32]string{0: "Add Delta", 1: "Remove Delta"}))
|
||||
|
||||
var cloneMoveObjectDetails = interpreters.New().
|
||||
With("ObjectID", 0, 2).As(interpreters.ObjectID()).
|
||||
With("MoveFlag", 2, 2).As(interpreters.EnumValue(map[uint32]string{
|
||||
0x0000: "Clone Object",
|
||||
0x0001: "Move Object (0x0001)",
|
||||
0x0002: "Move Object (0x0002)",
|
||||
0x0FFF: "Move Object (0x0FFF)",
|
||||
0xAAAA: "Move Object (0xAAAA)",
|
||||
0xFFFF: "Move Object (0xFFFF)"})).
|
||||
With("TargetX", 4, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetY", 8, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetHeight", 12, 1).As(interpreters.SpecialValue("ObjectHeight")).
|
||||
With("KeepSourceHeight", 13, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "Set height", 0x40: "Keep height"}))
|
||||
|
||||
var setGameVariableDetails = interpreters.New().
|
||||
With("VariableKey", 0, 4).As(interpreters.SpecialValue("VariableKey")).
|
||||
With("Value", 4, 2).
|
||||
With("Operation", 6, 2).As(interpreters.EnumValue(map[uint32]string{0: "Set", 1: "Add", 2: "Subtract", 3: "Multiply", 4: "Divide", 5: "Modulo"})).
|
||||
With("Message1", 8, 4).As(interpreters.RangedValue(0, 511)).
|
||||
With("Message2", 12, 4).As(interpreters.RangedValue(0, 511))
|
||||
|
||||
var showCutsceneDetails = interpreters.New().
|
||||
With("CutsceneIndex", 0, 4).As(interpreters.EnumValue(map[uint32]string{0: "Death", 1: "Intro", 2: "Ending"})).
|
||||
With("EndGameFlag", 4, 4).As(interpreters.EnumValue(map[uint32]string{0: "No (not working)", 1: "Yes"}))
|
||||
|
||||
func pointOneSecond(value int) string {
|
||||
return fmt.Sprintf("%.1f sec", float64(value)*0.1)
|
||||
}
|
||||
|
||||
var triggerOtherObjectsDetails = interpreters.New().
|
||||
With("Object1ID", 0, 2).As(interpreters.ObjectID()).
|
||||
With("Object1Delay", 2, 2).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond)).
|
||||
With("Object2ID", 4, 2).As(interpreters.ObjectID()).
|
||||
With("Object2Delay", 6, 2).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond)).
|
||||
With("Object3ID", 8, 2).As(interpreters.ObjectID()).
|
||||
With("Object3Delay", 10, 2).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond)).
|
||||
With("Object4ID", 12, 2).As(interpreters.ObjectID()).
|
||||
With("Object4Delay", 14, 2).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond))
|
||||
|
||||
var changeLightingDetails = interpreters.New().
|
||||
Refining("ObjectExtent", 0, 2, interpreters.New().With("Index", 0, 2).As(interpreters.ObjectID()), func(inst *interpreters.Instance) bool {
|
||||
var lightType = inst.Get("LightType")
|
||||
|
||||
return (lightType == 0x00) || (lightType == 0x01)
|
||||
}).
|
||||
Refining("RadiusExtent", 0, 2, interpreters.New().With("Tiles", 0, 2).As(interpreters.RangedValue(0, 31)), func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("LightType") == 0x03
|
||||
}).
|
||||
With("ReferenceObjectID", 2, 2).As(interpreters.ObjectID()).
|
||||
With("TransitionType", 4, 2).As(interpreters.EnumValue(map[uint32]string{0x0000: "immediate", 0x0001: "fade", 0x0100: "flicker"})).
|
||||
With("LightModification", 7, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "light on", 0x10: "light off"})).
|
||||
With("LightType", 8, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "rectangular", 0x03: "circular gradient"})).
|
||||
With("LightSurface", 10, 2).As(interpreters.EnumValue(map[uint32]string{0: "floor", 1: "ceiling", 2: "floor and ceiling"})).
|
||||
Refining("Rectangular", 12, 2, interpreters.New().
|
||||
With("Off light value", 0, 1).As(interpreters.RangedValue(0, 15)).
|
||||
With("On light value", 1, 1).As(interpreters.RangedValue(0, 15)), func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("LightType") == 0x00
|
||||
}).
|
||||
Refining("Gradient", 12, 4, interpreters.New().
|
||||
With("Off light begin intensity", 0, 1).As(interpreters.RangedValue(0, 127)).
|
||||
With("Off light end intensity", 1, 1).As(interpreters.RangedValue(0, 127)).
|
||||
With("On light begin intensity", 2, 1).As(interpreters.RangedValue(0, 127)).
|
||||
With("On light end intensity", 3, 1).As(interpreters.RangedValue(0, 127)), func(inst *interpreters.Instance) bool {
|
||||
var lightType = inst.Get("LightType")
|
||||
|
||||
return (lightType == 0x01) || (lightType == 0x03)
|
||||
})
|
||||
|
||||
var effectDetails = interpreters.New().
|
||||
With("SoundIndex", 0, 2).As(interpreters.RangedValue(0, 512)).
|
||||
With("SoundPlayCount", 2, 2).As(interpreters.RangedValue(0, 100)).
|
||||
With("VisualEffect", 4, 2).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "none",
|
||||
1: "power on",
|
||||
2: "quake",
|
||||
3: "escape pod",
|
||||
4: "red static",
|
||||
5: "interference"})).
|
||||
With("AdditionalVisualEffect", 8, 2).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "none",
|
||||
1: "white flash",
|
||||
2: "pink flash",
|
||||
3: "gray static",
|
||||
4: "vertical panning"})).
|
||||
With("AdditionalEffectTime", 12, 2).As(interpreters.RangedValue(0, 1000))
|
||||
|
||||
var changeTileHeightsDetails = interpreters.New().
|
||||
With("TileX", 0, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TileY", 4, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetFloorHeight", 8, 2).As(interpreters.SpecialValue("MoveTileHeight")).
|
||||
With("TargetCeilingHeight", 10, 2).As(interpreters.SpecialValue("MoveTileHeight")).
|
||||
With("FloorSound", 14, 2).As(interpreters.EnumValue(map[uint32]string{0x0000: "Audible", 0x0001: "Silent"}))
|
||||
|
||||
var changeTerrainDetails = interpreters.New().
|
||||
With("TileX", 0, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TileY", 4, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("Type", 8, 1).As(interpreters.SpecialValue("TileType")).
|
||||
With("TypeControl", 9, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "Set type", 0x40: "Keep type"})).
|
||||
With("SlopeHeight", 12, 1).As(interpreters.SpecialValue("TileHeight")).
|
||||
With("SlopeControl", 13, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "Set height", 0x40: "Keep height"}))
|
||||
|
||||
var scheduledTrapDetails = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("TimeInterval", 4, 4).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond)).
|
||||
With("ActivationValue", 8, 4).As(interpreters.EnumValue(map[uint32]string{0: "Off",
|
||||
0xFFFF: "On (0xFFFF)", 0x10000: "On (0x10000)", 0x11111: "On (0x11111)"})).
|
||||
With("Variance", 12, 2).As(interpreters.RangedValue(0, 512))
|
||||
|
||||
var cycleObjectsDetails = interpreters.New().
|
||||
With("ObjectID1", 0, 4).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 4, 4).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 8, 4).As(interpreters.ObjectID()).
|
||||
With("NextObject", 12, 4).As(interpreters.RangedValue(0, 2))
|
||||
|
||||
var deleteObjectsDetails = interpreters.New().
|
||||
With("ObjectID1", 0, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 8, 2).As(interpreters.ObjectID()).
|
||||
With("MessageIndex", 12, 2).As(interpreters.RangedValue(0, 511))
|
||||
|
||||
var receiveEmailDetails = interpreters.New().
|
||||
With("EmailIndex", 0, 2).As(interpreters.RangedValue(0, 1000)).
|
||||
With("DelaySec", 4, 2).As(interpreters.RangedValue(0, 600))
|
||||
|
||||
var exposeDetails = interpreters.New().
|
||||
With("DeltaValue", 0, 2).As(interpreters.RangedValue(0, 1000)).
|
||||
With("EffectChangeFlag", 2, 2).As(interpreters.EnumValue(map[uint32]string{0: "Add Delta", 1: "Remove Delta"})).
|
||||
With("EffectType", 4, 4).As(interpreters.EnumValue(map[uint32]string{4: "Radiation poisoning", 8: "Bio contamination"}))
|
||||
|
||||
var setObjectParameterDetails = interpreters.New().
|
||||
With("ObjectID1", 0, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 2, 2).As(interpreters.ObjectID()).
|
||||
With("Value1", 4, 4).
|
||||
With("Value2", 8, 4).
|
||||
With("Value3", 12, 4)
|
||||
|
||||
var setScreenPictureDetails = interpreters.New().
|
||||
With("ScreenObjectID1", 0, 2).As(interpreters.ObjectID()).
|
||||
With("ScreenObjectID2", 2, 2).As(interpreters.ObjectID()).
|
||||
With("SingleSequenceSource", 4, 4).
|
||||
With("LoopSequenceSource", 8, 4)
|
||||
|
||||
var setCritterStateDetails = interpreters.New().
|
||||
With("ReferenceObjectID1", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ReferenceObjectID2", 6, 2).As(interpreters.ObjectID()).
|
||||
With("NewState", 8, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "docile",
|
||||
1: "cautious",
|
||||
2: "hostile",
|
||||
3: "cautious (?)",
|
||||
4: "attacking",
|
||||
5: "sleeping",
|
||||
6: "tranquilized",
|
||||
7: "confused"}))
|
||||
|
||||
var trapMessageDetails = interpreters.New().
|
||||
With("BackgroundImageIndex", 0, 4).As(interpreters.RangedValue(-2, 500)).
|
||||
With("MessageIndex", 4, 4).As(interpreters.RangedValue(0, 511)).
|
||||
With("TextColor", 8, 4).As(interpreters.RangedValue(0, 255)).
|
||||
With("MfdSuppressionFlag", 12, 4).As(interpreters.EnumValue(map[uint32]string{0: "Show in MFD", 1: "Show only in HUD"}))
|
||||
|
||||
var spawnObjectsDetails = interpreters.New().
|
||||
With("ObjectType", 0, 4).As(interpreters.SpecialValue("ObjectTriple")).
|
||||
With("ReferenceObject1ID", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ReferenceObject2ID", 6, 2).As(interpreters.ObjectID()).
|
||||
With("NumberOfObjects", 8, 4).As(interpreters.RangedValue(0, 100)).
|
||||
With("Flags", 12, 1).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x00000001: "Avoid spawn within 7 tiles",
|
||||
0x00000002: "Avoid spawn in close view",
|
||||
0x00000004: "Avoid spawn in elevator music",
|
||||
0x00000008: "Force spawn in occupied/close tiles"}))
|
||||
|
||||
var changeObjectTypeDetails = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("NewType", 4, 2).As(interpreters.RangedValue(0, 16)).
|
||||
With("ResetMask", 6, 2).As(interpreters.RangedValue(0, 15))
|
||||
|
||||
// Change state block
|
||||
|
||||
var toggleRepulsorChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("OffTextureIndex", 4, 1).As(interpreters.SpecialValue("LevelTexture")).
|
||||
With("OnTextureIndex", 5, 1).As(interpreters.SpecialValue("LevelTexture")).
|
||||
With("ToggleType", 8, 1).As(interpreters.EnumValue(map[uint32]string{0: "Toggle On/Off", 1: "Toggle On, Stay On", 2: "Toggle Off, Stay Off"}))
|
||||
|
||||
var showGameCodeDigitChange = interpreters.New().
|
||||
With("ScreenObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("DigitNumber", 4, 4).As(interpreters.RangedValue(1, 6))
|
||||
|
||||
var setParameterFromVariableChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("ParameterNumber", 4, 4).As(interpreters.RangedValue(0, 16)).
|
||||
With("VariableIndex", 8, 4).As(interpreters.SpecialValue("VariableKey"))
|
||||
|
||||
var setFrameStateChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("NewState", 4, 4).As(interpreters.EnumValue(map[uint32]string{0: "Off", 1: "On"}))
|
||||
|
||||
var doorControlChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("ControlValue", 4, 4).As(interpreters.EnumValue(map[uint32]string{1: "open door", 2: "close door", 3: "toggle door", 4: "disable auto-close"}))
|
||||
|
||||
var rotateObjectChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("Amount", 4, 1).As(interpreters.RangedValue(0, 255)).
|
||||
With("RotationType", 5, 1).As(interpreters.EnumValue(map[uint32]string{0: "Endless", 1: "Back and forth"})).
|
||||
With("Direction", 6, 1).As(interpreters.EnumValue(map[uint32]string{0: "Forward", 1: "Backward"})).
|
||||
With("Axis", 7, 1).As(interpreters.EnumValue(map[uint32]string{0: "Z (Yaw)", 1: "X (Pitch)", 2: "Y (Roll)"})).
|
||||
With("ForwardLimit", 8, 1).As(interpreters.RangedValue(0, 255)).
|
||||
With("BackwardLimit", 9, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var removeObjectsChange = interpreters.New().
|
||||
With("ObjectType", 0, 4).As(interpreters.SpecialValue("ObjectTriple")).
|
||||
With("Radius", 4, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var setConditionChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("Condition", 4, 4)
|
||||
|
||||
var makeItemRadioactiveChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("WatchedObjectID", 4, 2).As(interpreters.ObjectID()).
|
||||
With("WatchedObjectTriggerState", 6, 2)
|
||||
|
||||
var orientedTriggerObjectChange = interpreters.New().
|
||||
With("HorizontalDirection", 0, 2).As(interpreters.RangedValue(0, 0xFFFF)).
|
||||
With("ObjectID", 4, 2).As(interpreters.ObjectID())
|
||||
|
||||
var closeDataMfdChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID())
|
||||
|
||||
var changeObjectTypeGlobalChange = interpreters.New().
|
||||
With("ObjectType", 0, 4).As(interpreters.SpecialValue("ObjectTriple")).
|
||||
With("NewType", 4, 1).As(interpreters.RangedValue(0, 16))
|
||||
|
||||
var hackDetails = interpreters.New().
|
||||
With("Type", 0, 4).As(interpreters.EnumValue(map[uint32]string{
|
||||
1: "Toggle Repulsor",
|
||||
2: "Show Game Code Digit",
|
||||
3: "Set Parameter from Variable",
|
||||
4: "Set Button State",
|
||||
5: "Door Control",
|
||||
6: "Return to Menu",
|
||||
7: "Rotate Objects",
|
||||
8: "Remove Objects",
|
||||
9: "SHODAN Pixelation",
|
||||
10: "Set Condition",
|
||||
11: "Show System Analyzer",
|
||||
12: "Make Item Radioactive",
|
||||
13: "Oriented Trigger Object",
|
||||
14: "Close Data MFD",
|
||||
15: "Earth Destruction by Laser",
|
||||
16: "Change Objects Type (Level)"})).
|
||||
Refining("ToggleRepulsor", 4, 12, toggleRepulsorChange, forType(1)).
|
||||
Refining("ShowGameCodeDigit", 4, 12, showGameCodeDigitChange, forType(2)).
|
||||
Refining("SetParameterFromVariable", 4, 12, setParameterFromVariableChange, forType(3)).
|
||||
Refining("SetFrameState", 4, 12, setFrameStateChange, forType(4)).
|
||||
Refining("DoorControl", 4, 12, doorControlChange, forType(5)).
|
||||
Refining("ReturnToMenu", 4, 12, interpreters.New(), forType(6)).
|
||||
Refining("RotateObject", 4, 12, rotateObjectChange, forType(7)).
|
||||
Refining("RemoveObjects", 4, 12, removeObjectsChange, forType(8)).
|
||||
Refining("ShodanPixelation", 4, 12, interpreters.New(), forType(9)).
|
||||
Refining("SetCondition", 4, 12, setConditionChange, forType(10)).
|
||||
Refining("ShowSystemAnalyzer", 4, 12, interpreters.New(), forType(11)).
|
||||
Refining("MakeItemRadioactive", 4, 12, makeItemRadioactiveChange, forType(12)).
|
||||
Refining("OrientedTriggerObject", 4, 12, orientedTriggerObjectChange, forType(13)).
|
||||
Refining("CloseDataMfd", 4, 12, closeDataMfdChange, forType(14)).
|
||||
Refining("EarthDestructionByLaser", 4, 12, interpreters.New(), forType(15)).
|
||||
Refining("ChangeObjectsType", 4, 12, changeObjectTypeGlobalChange, forType(16))
|
||||
|
||||
var unconditionalAction = interpreters.New().
|
||||
With("Type", 0, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "Nothing",
|
||||
1: "Transport Hacker",
|
||||
2: "Change Health",
|
||||
3: "Clone/Move Object",
|
||||
4: "Set Game Variable",
|
||||
5: "Show Cutscene",
|
||||
6: "Trigger Other Objects",
|
||||
7: "Change Lighting",
|
||||
8: "Effect",
|
||||
9: "Change Tile Heights",
|
||||
10: "Change Terrain",
|
||||
11: "Random Timer",
|
||||
12: "Cycle Objects",
|
||||
13: "Delete Objects",
|
||||
14: "Unknown (14)",
|
||||
15: "Receive E-Mail",
|
||||
16: "Change Effect",
|
||||
17: "Set Object Parameter",
|
||||
18: "Set Screen Picture",
|
||||
19: "Change State",
|
||||
20: "Unknown (20)",
|
||||
21: "Set Critter State",
|
||||
22: "Trap Message",
|
||||
23: "Spawn Objects",
|
||||
24: "Change Object Type"})).
|
||||
With("UsageQuota", 1, 1).
|
||||
Refining("TransportHacker", 6, 16, transportHackerDetails, forType(1)).
|
||||
Refining("ChangeHealth", 6, 16, changeHealthDetails, forType(2)).
|
||||
Refining("CloneMoveObject", 6, 16, cloneMoveObjectDetails, forType(3)).
|
||||
Refining("SetGameVariable", 6, 16, setGameVariableDetails, forType(4)).
|
||||
Refining("ShowCutscene", 6, 16, showCutsceneDetails, forType(5)).
|
||||
Refining("TriggerOtherObjects", 6, 16, triggerOtherObjectsDetails, forType(6)).
|
||||
Refining("ChangeLighting", 6, 16, changeLightingDetails, forType(7)).
|
||||
Refining("Effect", 6, 16, effectDetails, forType(8)).
|
||||
Refining("ChangeTileHeights", 6, 16, changeTileHeightsDetails, forType(9)).
|
||||
Refining("ChangeTerrain", 6, 16, changeTerrainDetails, forType(10)).
|
||||
Refining("ScheduledTrap", 6, 16, scheduledTrapDetails, forType(11)).
|
||||
Refining("CycleObjects", 6, 16, cycleObjectsDetails, forType(12)).
|
||||
Refining("DeleteObjects", 6, 16, deleteObjectsDetails, forType(13)).
|
||||
// 14 unused
|
||||
Refining("ReceiveEmail", 6, 16, receiveEmailDetails, forType(15)).
|
||||
Refining("Expose", 6, 16, exposeDetails, forType(16)).
|
||||
Refining("SetObjectParameter", 6, 16, setObjectParameterDetails, forType(17)).
|
||||
Refining("SetScreenPicture", 6, 16, setScreenPictureDetails, forType(18)).
|
||||
Refining("Hack", 6, 16, hackDetails, forType(19)).
|
||||
// 20 unknown
|
||||
Refining("SetCritterState", 6, 16, setCritterStateDetails, forType(21)).
|
||||
Refining("TrapMessage", 6, 16, trapMessageDetails, forType(22)).
|
||||
Refining("SpawnObjects", 6, 16, spawnObjectsDetails, forType(23)).
|
||||
Refining("ChangeObjectType", 6, 16, changeObjectTypeDetails, forType(24))
|
||||
|
||||
// Unconditional returns the description of actions without a condition.
|
||||
func Unconditional() *interpreters.Description {
|
||||
return unconditionalAction
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package conditions
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var gameVariable = interpreters.New().
|
||||
With("VariableKey", 0, 2).As(interpreters.SpecialValue("VariableCondition")).
|
||||
With("Value", 2, 1).
|
||||
With("MessageIndex", 3, 1)
|
||||
|
||||
// GameVariable returns the description for a game variable condition.
|
||||
func GameVariable() *interpreters.Description {
|
||||
return gameVariable
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package conditions
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var objectType = interpreters.New().
|
||||
With("ObjectType", 0, 3).As(interpreters.SpecialValue("ObjectTriple"))
|
||||
|
||||
var objectID = interpreters.New().
|
||||
With("ObjectID", 0, 2).As(interpreters.ObjectID())
|
||||
|
||||
// ObjectType returns a condition description for object types.
|
||||
func ObjectType() *interpreters.Description {
|
||||
return objectType
|
||||
}
|
||||
|
||||
// ObjectID returns a condition description for object indices.
|
||||
func ObjectID() *interpreters.Description {
|
||||
return objectID
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
var realWorldEntries *interpreterEntry
|
||||
var cyberspaceEntries *interpreterEntry
|
||||
|
||||
var realWorldExtras *interpreterEntry
|
||||
var cyberspaceExtras *interpreterEntry
|
||||
|
||||
var extraDefault = interpreters.New().
|
||||
With("CurrentFrame", 1, 1).
|
||||
With("TimeRemainder", 2, 1)
|
||||
|
||||
var extraIced = interpreters.New().
|
||||
With("ICE-presence", 1, 1).
|
||||
With("ICE-level", 3, 1)
|
||||
|
||||
var extraIcedFixtures = interpreters.New().
|
||||
With("PanelName", 0, 1).
|
||||
With("ICE-presence", 1, 1).
|
||||
With("ICE-level", 3, 1)
|
||||
|
||||
var extraFixtures = interpreters.New().
|
||||
With("PanelName", 0, 1)
|
||||
|
||||
var extraSurfaces = interpreters.New().
|
||||
With("Index", 1, 1)
|
||||
|
||||
func init() {
|
||||
software := initSoftware()
|
||||
traps := initTraps()
|
||||
critters := initCritters()
|
||||
|
||||
realWorldEntries = newInterpreterEntry(interpreters.New())
|
||||
realWorldEntries.set(int(object.ClassGun), initGuns())
|
||||
realWorldEntries.set(int(object.ClassAmmo), newInterpreterEntry(interpreters.New())) // have no data
|
||||
realWorldEntries.set(int(object.ClassPhysics), newInterpreterEntry(basePhysics))
|
||||
realWorldEntries.set(int(object.ClassGrenade), initGrenades())
|
||||
realWorldEntries.set(int(object.ClassDrug), newInterpreterEntry(interpreters.New())) // have no data
|
||||
realWorldEntries.set(int(object.ClassHardware), newInterpreterEntry(baseHardware))
|
||||
realWorldEntries.set(int(object.ClassSoftware), software)
|
||||
realWorldEntries.set(int(object.ClassBigStuff), initBigStuff())
|
||||
realWorldEntries.set(int(object.ClassSmallStuff), initSmallStuff())
|
||||
realWorldEntries.set(int(object.ClassFixture), initFixtures())
|
||||
realWorldEntries.set(int(object.ClassDoor), initDoors())
|
||||
realWorldEntries.set(int(object.ClassAnimating), newInterpreterEntry(baseAnimation))
|
||||
realWorldEntries.set(int(object.ClassTrap), traps)
|
||||
realWorldEntries.set(int(object.ClassContainer), initContainers())
|
||||
realWorldEntries.set(int(object.ClassCritter), critters)
|
||||
|
||||
cyberspaceEntries = newInterpreterEntry(interpreters.New())
|
||||
cyberspaceEntries.set(int(object.ClassSoftware), software)
|
||||
cyberspaceEntries.set(int(object.ClassBigStuff), initCyberspaceBigStuff())
|
||||
cyberspaceEntries.set(int(object.ClassSmallStuff), initCyberspaceSmallStuff())
|
||||
cyberspaceEntries.set(int(object.ClassFixture), initCyberspaceFixtures())
|
||||
cyberspaceEntries.set(int(object.ClassTrap), traps)
|
||||
cyberspaceEntries.set(int(object.ClassCritter), critters)
|
||||
|
||||
realWorldExtras = newInterpreterEntry(extraDefault)
|
||||
realWorldExtras.set(int(object.ClassFixture), newInterpreterLeaf(extraFixtures))
|
||||
extraBigStuff := newInterpreterEntry(extraDefault)
|
||||
simpleSurfaces := newInterpreterLeaf(extraSurfaces)
|
||||
surfaces := newInterpreterEntry(extraDefault)
|
||||
surfaces.set(1, simpleSurfaces)
|
||||
surfaces.set(4, simpleSurfaces)
|
||||
extraBigStuff.set(2, surfaces)
|
||||
realWorldExtras.set(int(object.ClassBigStuff), extraBigStuff)
|
||||
cyberspaceExtras = newInterpreterEntry(interpreters.New())
|
||||
cyberspaceExtras.set(int(object.ClassSoftware), newInterpreterLeaf(extraIced))
|
||||
cyberspaceExtras.set(int(object.ClassBigStuff), newInterpreterLeaf(extraIced))
|
||||
cyberspaceExtras.set(int(object.ClassSmallStuff), newInterpreterLeaf(extraIced))
|
||||
cyberspaceExtras.set(int(object.ClassFixture), newInterpreterLeaf(extraIcedFixtures))
|
||||
}
|
||||
Reference in New Issue
Block a user