Adding modding folder with a hacked build.

This commit is contained in:
ml
2026-09-26 08:53:08 +02:00
parent cdb960dc71
commit c91080aba7
755 changed files with 50424 additions and 0 deletions
@@ -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, &param)
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))
}