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,6 @@
package archive
const (
// MaxLevels describes how many levels the world can hold.
MaxLevels = 16
)
@@ -0,0 +1,825 @@
package archive
import (
"fmt"
"math"
"github.com/inkyblackness/hacked/ss1/content/archive/level"
"github.com/inkyblackness/hacked/ss1/content/interpreters"
"github.com/inkyblackness/hacked/ss1/content/object"
"github.com/inkyblackness/hacked/ss1/content/text"
)
// GameStateSize specifies the byte count of a the GameState in an archive.
// Note that in the original archive.dat file, the resource only has 0x054D bytes.
// This does not matter much, as in the original engine the resource was ignored.
const GameStateSize = 0x0595
const (
stateHackerNameSize = 20
// InventoryWeaponSlots is the number of weapons available to the character.
InventoryWeaponSlots = 7
weaponSlotsStartOffset = 0x048B
weaponSlotSize = 5
// GrenadeTypeCount is the number of grenades (explosives) available to the character.
GrenadeTypeCount = 7
grenadeCountStartOffset = 0x0350
grenadeTimerStartOffset = 0x04FF
grenadeTimerDefault = 70
// GrenadeTimerMaximum is the maximum time that can be set for a grenade in game.
GrenadeTimerMaximum = 571
// PatchTypeCount is the number of patches available to the character.
PatchTypeCount = 7
patchCountStartOffset = 0x0349
// AmmoTypeCount is the number of types of ammo available to the character.
AmmoTypeCount = 15
ammoFullClipCountStartOffset = 0x032B
ammoExtraRoundsCountStartOffset = 0x033A
// GeneralInventorySlotCount is the number of general inventory slots available.
GeneralInventorySlotCount = 14
generalInventorySlotStartOffset = 0x007A
// HardwareTypeCount is the number of available hardware.
HardwareTypeCount = 15
hardwareVersionStartOffset = 0x0309
hardwareStatusStartOffset = 0x04AE
// SoftwareOffenseTypeCount is the number of available offense software.
SoftwareOffenseTypeCount = 7
// SoftwareDefenseTypeCount is the number of available defense software.
SoftwareDefenseTypeCount = 3
// SoftwareOneshotTypeCount is the number of available one-shot software.
SoftwareOneshotTypeCount = 4
installedOffenseSoftwareStartOffset = 0x0318
installedDefenseSoftwareStartOffset = 0x031F
installedOneshotSoftwareStartOffset = 0x0322
engineTicksPerSecond = 280
secondsPerMinute = 60
engineTicksPerMinute = secondsPerMinute * engineTicksPerSecond
cyberspaceMinTime = 90 * engineTicksPerSecond
cyberspaceMaxTime = 30 * secondsPerMinute * engineTicksPerSecond
// EMailCount is the number of available EMail status slots.
EMailCount = 47
// FragmentCount is the number of available fragment (data) status slots.
FragmentCount = 23
// PossibleLogsPerLevel is the number of logs slots available per level.
PossibleLogsPerLevel = 16
// LevelsWithLogs is the number of levels that can hold logs.
LevelsWithLogs = 14
// LogCount is the total number of logs available.
LogCount = PossibleLogsPerLevel * LevelsWithLogs
messageStatusReceived byte = 0x80
messageStatusRead byte = 0x40
messageStatusStartOffset = 0x0357
emailStatusStartOffset = messageStatusStartOffset
logStatusStartOffset = emailStatusStartOffset + EMailCount
fragmentStatusStartOffset = logStatusStartOffset + LogCount
// BooleanVarCount is the number of available boolean variables.
BooleanVarCount = 512
booleanVarStartOffset = 0x00B6
// IntegerVarCount is the number of available integer variables.
IntegerVarCount = 64
integerVarStartOffset = 0x00F6
)
// GameState contains all the necessary information for the current progress of the game.
// It is a combination of world information, character information, and configuration values.
type GameState struct {
*interpreters.Instance
}
// InventoryWeaponSlot describes one entry for weapons in the inventory.
type InventoryWeaponSlot struct {
Index int
State *GameState
}
func freeWeaponSlot() []byte {
return []byte{0xFF, 0x00, 0x00, 0x00, 0x00}
}
func (slot InventoryWeaponSlot) rawData() []byte {
if (slot.Index < 0) || (slot.Index >= InventoryWeaponSlots) {
return freeWeaponSlot()
}
startIndex := weaponSlotsStartOffset + (weaponSlotSize * slot.Index)
return slot.State.Raw()[startIndex : startIndex+weaponSlotSize]
}
// IsInUse returns true if the slot is currently holding a weapon.
func (slot InventoryWeaponSlot) IsInUse() bool {
return slot.rawData()[0] != 0xFF
}
// SetFree clears the weapon slot.
func (slot InventoryWeaponSlot) SetFree() {
rawData := slot.rawData()
copy(rawData, freeWeaponSlot())
}
// SetInUse inserts the given weapon in this slot. Other fields are reset to zero.
func (slot InventoryWeaponSlot) SetInUse(subclass object.Subclass, objType object.Type) {
rawData := slot.rawData()
copy(rawData, freeWeaponSlot())
rawData[0] = byte(subclass)
rawData[1] = byte(objType)
}
// Triple returns the type information of the weapon in this slot.
func (slot InventoryWeaponSlot) Triple() object.Triple {
rawData := slot.rawData()
return object.TripleFrom(int(object.ClassGun), int(rawData[0]), int(rawData[1]))
}
// WeaponState returns the two state values. Their interpretation is subclass specific.
func (slot InventoryWeaponSlot) WeaponState() (byte, byte) {
rawData := slot.rawData()
return rawData[2], rawData[3]
}
// SetWeaponState updates the two state values.
func (slot InventoryWeaponSlot) SetWeaponState(a, b byte) {
rawData := slot.rawData()
rawData[2] = a
rawData[3] = b
}
// InventoryGrenade describes properties of grenades by type.
type InventoryGrenade struct {
Index int
State *GameState
}
func (grenade InventoryGrenade) isValid() bool {
return (grenade.Index >= 0) && (grenade.Index < GrenadeTypeCount) && (grenade.State != nil)
}
// Count returns how many explosives the character is carrying.
func (grenade InventoryGrenade) Count() int {
if !grenade.isValid() {
return 0
}
return int(grenade.State.Raw()[grenadeCountStartOffset+grenade.Index])
}
// SetCount updates the amount of explosives the character is carrying. Invalid values are clamped.
func (grenade InventoryGrenade) SetCount(value int) {
if !grenade.isValid() {
return
}
count := byte(value)
if value < 0 {
count = 0
} else if value > math.MaxUint8 {
count = math.MaxUint8
}
grenade.State.Raw()[grenadeCountStartOffset+grenade.Index] = count
}
// GrenadeTimerSetting is the value for explosive timers.
type GrenadeTimerSetting uint16
// InSeconds returns the amount in unit of seconds.
func (setting GrenadeTimerSetting) InSeconds() float32 {
return float32(setting) / 10.0
}
func (grenade InventoryGrenade) rawTimer() []byte {
if !grenade.isValid() {
return []byte{0x00, 0x00}
}
startIndex := grenadeTimerStartOffset + (2 * grenade.Index)
return grenade.State.Raw()[startIndex : startIndex+2]
}
// TimerSetting returns the generic timer used for this type of grenade.
func (grenade InventoryGrenade) TimerSetting() GrenadeTimerSetting {
raw := grenade.rawTimer()
return GrenadeTimerSetting((uint16(raw[1]) << 8) | (uint16(raw[0]) << 0))
}
// SetTimerSetting updates the generic timer used for this type of grenade.
func (grenade InventoryGrenade) SetTimerSetting(value GrenadeTimerSetting) {
raw := grenade.rawTimer()
raw[0] = byte(value >> 0)
raw[1] = byte(value >> 8)
}
// PatchState describes properties of patches by type.
type PatchState struct {
Index int
State *GameState
}
func (patch PatchState) isValid() bool {
return (patch.Index >= 0) && (patch.Index < PatchTypeCount) && (patch.State != nil)
}
// Count returns how many patches the character is carrying.
func (patch PatchState) Count() int {
if !patch.isValid() {
return 0
}
return int(patch.State.Raw()[patchCountStartOffset+patch.Index])
}
// SetCount updates the amount of patches the character is carrying. Invalid values are clamped.
func (patch PatchState) SetCount(value int) {
if !patch.isValid() {
return
}
count := byte(value)
if value < 0 {
count = 0
} else if value > math.MaxUint8 {
count = math.MaxUint8
}
patch.State.Raw()[patchCountStartOffset+patch.Index] = count
}
// InventoryAmmo describes properties of ammo by type.
type InventoryAmmo struct {
Index int
State *GameState
}
func (ammo InventoryAmmo) isValid() bool {
return (ammo.Index >= 0) && (ammo.Index < AmmoTypeCount) && (ammo.State != nil)
}
// FullClipCount returns how many full clips the character is carrying.
func (ammo InventoryAmmo) FullClipCount() int {
if !ammo.isValid() {
return 0
}
return int(ammo.State.Raw()[ammoFullClipCountStartOffset+ammo.Index])
}
// SetFullClipCount updates the amount of full clips the character is carrying. Invalid values are clamped.
func (ammo InventoryAmmo) SetFullClipCount(value int) {
if !ammo.isValid() {
return
}
count := byte(value)
if value < 0 {
count = 0
} else if value > math.MaxUint8 {
count = math.MaxUint8
}
ammo.State.Raw()[ammoFullClipCountStartOffset+ammo.Index] = count
}
// ExtraRoundsCount returns how many extra rounds the character is carrying.
func (ammo InventoryAmmo) ExtraRoundsCount() int {
if !ammo.isValid() {
return 0
}
return int(ammo.State.Raw()[ammoExtraRoundsCountStartOffset+ammo.Index])
}
// SetExtraRoundsCount updates the amount of extra rounds the character is carrying. Invalid values are clamped.
func (ammo InventoryAmmo) SetExtraRoundsCount(value int) {
if !ammo.isValid() {
return
}
count := byte(value)
if value < 0 {
count = 0
} else if value > math.MaxUint8 {
count = math.MaxUint8
}
ammo.State.Raw()[ammoExtraRoundsCountStartOffset+ammo.Index] = count
}
// GeneralInventorySlot describes properties of a general inventory slot.
type GeneralInventorySlot struct {
Index int
State *GameState
}
func (slot GeneralInventorySlot) isValid() bool {
return (slot.Index >= 0) && (slot.Index < GeneralInventorySlotCount) && (slot.State != nil)
}
func (slot GeneralInventorySlot) rawID() []byte {
if !slot.isValid() {
return []byte{0x00, 0x00}
}
startIndex := generalInventorySlotStartOffset + (2 * slot.Index)
return slot.State.Raw()[startIndex : startIndex+2]
}
// ObjectID returns the ID of the object in this inventory slot.
func (slot GeneralInventorySlot) ObjectID() level.ObjectID {
raw := slot.rawID()
return level.ObjectID((uint16(raw[1]) << 8) | (uint16(raw[0]) << 0))
}
// SetObjectID updates the ID in this inventory slot.
func (slot GeneralInventorySlot) SetObjectID(id level.ObjectID) {
raw := slot.rawID()
raw[0] = byte(id >> 0)
raw[1] = byte(id >> 8)
}
// HardwareState describes properties of hardware by type.
type HardwareState struct {
Index int
State *GameState
}
func (hardware HardwareState) isValid() bool {
return (hardware.Index >= 0) && (hardware.Index < HardwareTypeCount) && (hardware.State != nil)
}
// Version returns which version of the hardware is installed. Zero means not installed.
func (hardware HardwareState) Version() int {
if !hardware.isValid() {
return 0
}
return int(hardware.State.Raw()[hardwareVersionStartOffset+hardware.Index])
}
// SetVersion updates the version the character has installed.
func (hardware HardwareState) SetVersion(value int) {
if !hardware.isValid() {
return
}
count := byte(value)
if value < 0 {
count = 0
} else if value > math.MaxUint8 {
count = math.MaxUint8
}
hardware.State.Raw()[hardwareVersionStartOffset+hardware.Index] = count
}
// IsActive returns whether the hardware is currently active.
func (hardware HardwareState) IsActive() bool {
if !hardware.isValid() {
return false
}
return hardware.State.Raw()[hardwareStatusStartOffset+hardware.Index] != 0
}
// SetActive updates the active state of a hardware.
func (hardware HardwareState) SetActive(on bool) {
if !hardware.isValid() {
return
}
var newValue byte
if on {
newValue = 1
}
hardware.State.Raw()[hardwareStatusStartOffset+hardware.Index] = newValue
}
// VersionedSoftwareState describes software that is upgradeable (exists only once).
type VersionedSoftwareState struct {
startOffset int
limit int
Index int
State *GameState
}
func (sw VersionedSoftwareState) isValid() bool {
return (sw.Index >= 0) && (sw.Index < sw.limit) && (sw.State != nil)
}
// Version returns the installed version number of the software. Zero means not installed.
func (sw VersionedSoftwareState) Version() int {
if !sw.isValid() {
return 0
}
return int(sw.State.Raw()[sw.startOffset+sw.Index])
}
// SetVersion sets the installed version number of the software.
func (sw VersionedSoftwareState) SetVersion(value int) {
if !sw.isValid() {
return
}
count := byte(value)
if value < 0 {
count = 0
} else if value > math.MaxUint8 {
count = math.MaxUint8
}
sw.State.Raw()[sw.startOffset+sw.Index] = count
}
// CountedSoftwareState describes software that is expended upon use.
type CountedSoftwareState struct {
startOffset int
limit int
Index int
State *GameState
}
func (sw CountedSoftwareState) isValid() bool {
return (sw.Index >= 0) && (sw.Index < sw.limit) && (sw.State != nil)
}
// Count returns the available amount of the software.
func (sw CountedSoftwareState) Count() int {
if !sw.isValid() {
return 0
}
return int(sw.State.Raw()[sw.startOffset+sw.Index])
}
// SetCount sets the installed amount of the software.
func (sw CountedSoftwareState) SetCount(value int) {
if !sw.isValid() {
return
}
count := byte(value)
if value < 0 {
count = 0
} else if value > math.MaxUint8 {
count = math.MaxUint8
}
sw.State.Raw()[sw.startOffset+sw.Index] = count
}
// MessageState describes properties of message by index.
type MessageState struct {
startOffset int
limit int
Index int
State *GameState
}
func (message MessageState) isValid() bool {
return (message.Index >= 0) && (message.Index < message.limit) && (message.State != nil)
}
// Received returns whether the given message was received.
func (message MessageState) Received() bool {
return message.hasFlag(messageStatusReceived)
}
// SetReceived marks the receive status of the message.
func (message MessageState) SetReceived(value bool) {
message.setFlag(messageStatusReceived, value)
}
// Read returns whether the given message was read.
func (message MessageState) Read() bool {
return message.hasFlag(messageStatusRead)
}
// SetRead marks the read status of the message.
func (message MessageState) SetRead(value bool) {
message.setFlag(messageStatusRead, value)
}
func (message MessageState) hasFlag(flag byte) bool {
if !message.isValid() {
return false
}
return (message.State.Raw()[message.startOffset+message.Index] & flag) != 0
}
func (message MessageState) setFlag(flag byte, on bool) {
if !message.isValid() {
return
}
offset := message.startOffset + message.Index
newState := message.State.Raw()[offset]
newState &= ^flag
if on {
newState |= flag
}
message.State.Raw()[offset] = newState
}
var gameStateDesc = interpreters.New().
With("Difficulty: Combat", 0x0015, 1).As(interpreters.RangedValue(0, 3)).
With("Difficulty: Mission", 0x0016, 1).As(interpreters.RangedValue(0, 3)).
With("Difficulty: Puzzle", 0x0017, 1).As(interpreters.RangedValue(0, 3)).
With("Difficulty: Cyber", 0x0018, 1).As(interpreters.RangedValue(0, 3)).
With("Game time", 0x001C, 4).As(interpreters.SpecialValue("Internal")).
With("Current Level", 0x0039, 1).As(interpreters.RangedValue(0, 15)).
With("Health", 0x009C, 1).As(interpreters.FormattedRangedValue(0, 255,
func(value int) string {
return fmt.Sprintf("%.2f%%%%", float64(value*100)/255.0)
})).
With("Cyberspace integrity", 0x009D, 1).As(interpreters.FormattedRangedValue(0, 255,
func(value int) string {
return fmt.Sprintf("%.2f%%%%", float64(value*100)/255.0)
})).
With("Health regeneration per minute", 0x009E, 2).As(interpreters.RangedValue(0, 1000)).
With("Power", 0x00AC, 1).As(interpreters.FormattedRangedValue(0, 255,
func(value int) string {
return fmt.Sprintf("%.2f%%%%", float64(value*100)/255.0)
})).
With("Power Usage", 0x00AD, 1).As(interpreters.RangedValue(0, 0xFF)).
With("Out-of-power flag", 0x00AF, 1).As(interpreters.EnumValue(
map[uint32]string{
0: "Powered",
1: "Out-of-power",
})).
With("Cyberspace base time", 0x00B2, 4).As(interpreters.FormattedRangedValue(cyberspaceMinTime, cyberspaceMaxTime,
func(value int) string {
minutes := value / engineTicksPerMinute
return fmt.Sprintf("%2dm %2.2fs", minutes, float64(value-(minutes*engineTicksPerMinute))/(engineTicksPerSecond))
})).
With("Fatigue regeneration", 0x0181, 2).As(interpreters.RangedValue(0, 400)).
With("Fatigue regeneration base", 0x0183, 2).As(interpreters.RangedValue(0, 400)).
With("Fatigue regeneration max", 0x0185, 2).As(interpreters.RangedValue(0, 400)).
With("Accuracy", 0x0187, 1).As(interpreters.RangedValue(0, 255)).
With("Shield absorb rate", 0x0188, 1).As(interpreters.RangedValue(0, 255)).
With("Hacker Position X", 0x053F, 4).As(interpreters.FormattedRangedValue(0x010000, 0x3FFFFF,
func(value int) string {
return fmt.Sprintf("%2.03f", float32(value)/0x010000)
})).
With("Hacker Position Y", 0x0543, 4).As(interpreters.FormattedRangedValue(0x010000, 0x3FFFFF,
func(value int) string {
return fmt.Sprintf("%2.03f", float32(value)/0x010000)
})).
With("Hacker Position Z", 0x0547, 4).As(interpreters.FormattedRangedValue(0x010000, 0x1FFFFF,
func(value int) string {
return fmt.Sprintf("%2.03f tile(s)", float32(value)/0x010000)
})).
With("Hacker Yaw", 0x054B, 4).As(interpreters.RotationValue(0, edmsFullCircle()))
func edmsFullCircle() int64 {
full := math.Pi * 2 * float64(0x10000)
return int64(full) - 1
}
// NewGameState returns a GameState instance for given raw data.
func NewGameState(raw []byte) *GameState {
return &GameState{Instance: gameStateDesc.For(raw)}
}
// IsSavegame returns true if the state describes one during a running game.
func (state *GameState) IsSavegame() bool {
// Picking the right property is tricky. Most properties could be pre-initialized
// by an archive as well.
// For now, assume the "game time" will not be touched by initial archives.
// A possible other approach would be to take the "version" field, which
// is always set when a savegame is created, but is zero in the original. Yet,
// it is not clear whether or not a (pedantic) engine would respect this field and
// ignore data when it's zero.
return state.Get("Game time") > 0
}
// IsDefaulting returns true if the state describes an archive from which the engine
// should initialize the state for a new game.
func (state *GameState) IsDefaulting() bool {
return state.Get("Hacker Position X") == 0
}
// HackerName returns the name interpreted with given codepage.
func (state *GameState) HackerName(cp text.Codepage) string {
return cp.Decode(state.Raw()[:stateHackerNameSize])
}
// SetHackerName stores the given name using given codepage, up to the internal available amount of bytes.
func (state *GameState) SetHackerName(name string, cp text.Codepage) {
raw := state.Raw()
var zeroName [stateHackerNameSize]byte
copy(raw[:stateHackerNameSize], zeroName[:])
copy(raw[:stateHackerNameSize], cp.Encode(name))
raw[stateHackerNameSize-1] = 0
}
// CurrentLevel returns the number of the level hacker is currently in.
func (state GameState) CurrentLevel() int {
return int(state.Get("Current Level"))
}
// HackerMapPosition returns the rough X/Y location on the map.
func (state GameState) HackerMapPosition() (level.Coordinate, level.Coordinate) {
x := state.Get("Hacker Position X")
y := state.Get("Hacker Position Y")
return level.CoordinateAt(byte(x>>16), byte(x>>8)), level.CoordinateAt(byte(y>>16), byte(y>>8))
}
// BooleanVar returns the state of the boolean variable at given index. Unsupported indices return 0.
func (state GameState) BooleanVar(index int) bool {
if (index < 0) || (index >= BooleanVarCount) {
return false
}
return state.Raw()[booleanVarStartOffset+(index/8)]&(0x01<<(uint(index%8))) != 0
}
// SetBooleanVar sets the state of the boolean variable at given index. Unsupported indices are ignored.
func (state *GameState) SetBooleanVar(index int, set bool) {
if (index < 0) || (index >= BooleanVarCount) {
return
}
byteIndex := booleanVarStartOffset + (index / 8)
bitMask := byte(0x01 << uint(index%8))
temp := state.Raw()[byteIndex]
temp &= ^bitMask
if set {
temp |= bitMask
}
state.Raw()[byteIndex] = temp
}
// IntegerVar returns the value of the integer variable at given index. Unsupported indices return 0.
func (state *GameState) IntegerVar(index int) int16 {
if (index < 0) || (index >= IntegerVarCount) {
return 0
}
startOffset := integerVarStartOffset + (2 * index)
val := uint16(state.Raw()[startOffset+1])<<8 | uint16(state.Raw()[startOffset+0])
return int16(val)
}
// SetIntegerVar sets the value of the integer variable at given index. Unsupported indices are ignored.
func (state *GameState) SetIntegerVar(index int, value int16) {
if (index < 0) || (index >= IntegerVarCount) {
return
}
startOffset := integerVarStartOffset + (2 * index)
state.Raw()[startOffset+0] = byte((value >> 0) & 0xFF)
state.Raw()[startOffset+1] = byte((value >> 8) & 0xFF)
}
// InventoryWeaponSlot returns an accessor for the identified weapon slot.
// Index should be within the range of [0..InventoryWeaponSlots[.
func (state *GameState) InventoryWeaponSlot(index int) InventoryWeaponSlot {
return InventoryWeaponSlot{
Index: index,
State: state,
}
}
// InventoryGrenade returns an accessor for the identified grenade type index.
// Index should be within the range of [0..GrenadeTypeCount[ .
func (state *GameState) InventoryGrenade(index int) InventoryGrenade {
return InventoryGrenade{
Index: index,
State: state,
}
}
// PatchState returns an accessor for the identified patch type index.
// Index should be within the range of [0..PatchTypeCount[ .
func (state *GameState) PatchState(index int) PatchState {
return PatchState{
Index: index,
State: state,
}
}
// InventoryAmmo returns an accessor for the identified ammo type index.
// Index should be within the range of [0..AmmoTypeCount[ .
func (state *GameState) InventoryAmmo(index int) InventoryAmmo {
return InventoryAmmo{
Index: index,
State: state,
}
}
// GeneralInventorySlot returns an accessor for the identified general slot.
// Index should be within the range of [0..GeneralInventorySlotCount[ .
func (state *GameState) GeneralInventorySlot(index int) GeneralInventorySlot {
return GeneralInventorySlot{
Index: index,
State: state,
}
}
// HardwareState returns an accessor for the identified hardware type index.
// Index should be within the range of [0..HardwareTypeCount[ .
func (state *GameState) HardwareState(index int) HardwareState {
return HardwareState{
Index: index,
State: state,
}
}
// VersionedOffenseSoftwareState returns an accessor for the identified versioned software status.
// Index should be within the range of [0..SoftwareOffenseTypeCount[ .
func (state *GameState) VersionedOffenseSoftwareState(index int) VersionedSoftwareState {
return VersionedSoftwareState{
startOffset: installedOffenseSoftwareStartOffset,
limit: SoftwareOffenseTypeCount,
Index: index,
State: state,
}
}
// VersionedDefenseSoftwareState returns an accessor for the identified versioned software status.
// Index should be within the range of [0..SoftwareDefenseTypeCount[ .
func (state *GameState) VersionedDefenseSoftwareState(index int) VersionedSoftwareState {
return VersionedSoftwareState{
startOffset: installedDefenseSoftwareStartOffset,
limit: SoftwareDefenseTypeCount,
Index: index,
State: state,
}
}
// OneshotSoftwareState returns an accessor for the identified software status.
// Index should be within the range of [0..SoftwareOneshotTypeCount[ .
func (state *GameState) OneshotSoftwareState(index int) CountedSoftwareState {
return CountedSoftwareState{
startOffset: installedOneshotSoftwareStartOffset,
limit: SoftwareOneshotTypeCount,
Index: index,
State: state,
}
}
// EMailState returns an accessor for the identified EMail status.
// Index should be within the range of [0..EMailCount[ .
func (state *GameState) EMailState(index int) MessageState {
return MessageState{
startOffset: emailStatusStartOffset,
limit: EMailCount,
Index: index,
State: state,
}
}
// LogState returns an accessor for the identified log status.
// Index should be within the range of [0..LogCount[ .
func (state *GameState) LogState(index int) MessageState {
return MessageState{
startOffset: logStatusStartOffset,
limit: LogCount,
Index: index,
State: state,
}
}
// FragmentState returns an accessor for the identified fragment status.
// Index should be within the range of [0..FragmentCount[ .
func (state *GameState) FragmentState(index int) MessageState {
return MessageState{
startOffset: fragmentStatusStartOffset,
limit: FragmentCount,
Index: index,
State: state,
}
}
// ZeroGameStateData returns the state block reset for default engine behaviour.
func ZeroGameStateData() []byte {
return make([]byte, GameStateSize)
}
// DefaultGameState returns a GameState that can be used for any mission.
func DefaultGameState() *GameState {
data := ZeroGameStateData()
state := NewGameState(data)
state.Set("Health", 212)
state.Set("Cyberspace integrity", 255)
state.Set("Health regeneration per minute", 0)
state.Set("Power", 255)
state.Set("Cyberspace base time", 30*60*engineTicksPerSecond)
state.Set("Shield absorb rate", 0)
state.Set("Fatigue regeneration", 0)
state.Set("Fatigue regeneration base", 100)
state.Set("Fatigue regeneration max", 400)
state.Set("Accuracy", 100)
for i := 0; i < InventoryWeaponSlots; i++ {
state.InventoryWeaponSlot(i).SetFree()
}
for i := 0; i < GrenadeTypeCount; i++ {
state.InventoryGrenade(i).SetTimerSetting(grenadeTimerDefault)
}
for index, info := range engineBooleanVariables {
if info.InitValue == nil {
continue
}
state.SetBooleanVar(index, *info.InitValue != 0)
}
for index, info := range engineIntegerVariables {
if info.InitValue == nil {
continue
}
state.SetIntegerVar(index, *info.InitValue)
}
return state
}
@@ -0,0 +1,28 @@
package archive
import (
"io/ioutil"
"github.com/inkyblackness/hacked/ss1/resource"
)
// IsSavegame returns true for resources that most likely identify a savegame.
// This function must be called with a resource from ID 0x0FA1.
func IsSavegame(res resource.BlockProvider) bool {
if res == nil {
return false
}
if res.BlockCount() < 1 {
return false
}
dataReader, err := res.Block(0)
if err != nil {
return false
}
data, err := ioutil.ReadAll(dataReader)
if err != nil {
return false
}
state := NewGameState(data)
return state.IsSavegame()
}
@@ -0,0 +1,69 @@
package archive_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/inkyblackness/hacked/ss1/content/archive"
"github.com/inkyblackness/hacked/ss1/resource"
)
func TestIsSavegameTrueForActualSavegame(t *testing.T) {
stateData := make([]byte, archive.GameStateSize)
stateData[0x001C+2] = 0x10
res := resource.Resource{
Properties: resource.Properties{
Compressed: false,
ContentType: resource.Archive,
Compound: false,
},
Blocks: resource.BlocksFrom([][]byte{stateData}),
}
result := archive.IsSavegame(res)
assert.True(t, result)
}
func TestIsSavegameFalseForWrongResourceContent(t *testing.T) {
res := resource.Resource{
Properties: resource.Properties{
Compressed: false,
ContentType: resource.Archive,
Compound: true,
},
Blocks: resource.BlocksFrom([][]byte{}),
}
result := archive.IsSavegame(res)
assert.False(t, result)
}
func TestIsSavegameFalseForTooShortData(t *testing.T) {
res := resource.Resource{
Properties: resource.Properties{
Compressed: false,
ContentType: resource.Archive,
Compound: true,
},
Blocks: resource.BlocksFrom([][]byte{make([]byte, 0x10)}),
}
result := archive.IsSavegame(res)
assert.False(t, result)
}
func TestIsSavegameFalseForZeroData(t *testing.T) {
stateData := make([]byte, archive.GameStateSize)
res := resource.Resource{
Properties: resource.Properties{
Compressed: false,
ContentType: resource.Archive,
Compound: true,
},
Blocks: resource.BlocksFrom([][]byte{stateData}),
}
result := archive.IsSavegame(res)
assert.False(t, result)
}
@@ -0,0 +1,253 @@
package archive
import "math"
// GameVariableLimits describes limits of a game variable.
type GameVariableLimits struct {
Minimum int16
Maximum int16
}
// GameVariableInfo describes a variable in the game state of the archive.
type GameVariableInfo struct {
// InitValue is nil if the initial value is system dependent.
InitValue *int16
// Name is the short identifier for the variable.
Name string
// Description is an optional text with further information on the variable.
Description string
// Limits may be provided to describe the range of possible values.
Limits *GameVariableLimits
// ValueNames may be set for enumerated values.
ValueNames map[int16]string
// Hardcoded variables are fixed for a new game.
Hardcoded bool
}
// ResetValueInt returns the expected reset value for integer variables.
func (info GameVariableInfo) ResetValueInt() int16 {
if info.InitValue == nil {
return 0
}
return *info.InitValue
}
// ResetValueBool returns the expected reset value for boolean variables.
func (info GameVariableInfo) ResetValueBool() bool {
if info.InitValue == nil {
return false
}
return *info.InitValue != 0
}
// GameVariableInfoProvider provides lookup functionality for integer and boolean variables.
type GameVariableInfoProvider interface {
IntegerVariable(index int) GameVariableInfo
BooleanVariable(index int) GameVariableInfo
}
// GameVariableInfoFor returns a new instance with given name.
func GameVariableInfoFor(name string) GameVariableInfo {
return GameVariableInfo{Name: name}
}
// At returns an information based on the current one, with the given initial value.
func (info GameVariableInfo) At(value int16) GameVariableInfo {
info.InitValue = &value
return info
}
// HardcodedConfig returns an information that is marked as being a hardcoded initialized configuration value.
func (info GameVariableInfo) HardcodedConfig() GameVariableInfo {
info.Hardcoded = true
return info
}
// HardcodedAt returns an information that is marked as being a hardcoded variable with given initial value.
func (info GameVariableInfo) HardcodedAt(value int16) GameVariableInfo {
info.InitValue = &value
info.Hardcoded = true
return info
}
// Enumerated returns an information with given value names.
func (info GameVariableInfo) Enumerated(names map[int16]string) GameVariableInfo {
info.ValueNames = names
return info
}
// Boolean returns an enumerated information with No/Yes as possible values.
func (info GameVariableInfo) Boolean() GameVariableInfo {
return info.Enumerated(map[int16]string{
0: "No",
1: "Yes",
})
}
// LimitedBy returns an information with given minimum and maximum values.
func (info GameVariableInfo) LimitedBy(min, max int16) GameVariableInfo {
info.Limits = &GameVariableLimits{
Minimum: min,
Maximum: max,
}
return info
}
// DescribedAs returns an information with given text as description.
func (info GameVariableInfo) DescribedAs(text string) GameVariableInfo {
info.Description = text
return info
}
// GameVariables is a lookup map for information on game variables.
type GameVariables map[int]GameVariableInfo
// Lookup returns the information for given index. If the index is not known, nil is returned.
func (vars GameVariables) Lookup(index int) *GameVariableInfo {
info, known := vars[index]
if !known {
return nil
}
return &info
}
const (
securityValueDescription = "The current security value is re-calculated whenever a level is loaded up."
randomCodeDescription = "If both codes are equal at the start of a new game, they will be randomized."
highscoreCodeDescription = "The highscore values are combined as one 32-bit integer, used for the MFD game MCOM."
)
var engineIntegerVariables = GameVariables{
2: GameVariableInfoFor("Plastique explosion counter").At(0).LimitedBy(0, 100).
DescribedAs("This value is incremented for each exploding plastique."),
9: GameVariableInfoFor("Plot counter").At(0),
13: GameVariableInfoFor("Difficulty: Mission").HardcodedConfig().LimitedBy(0, 3),
14: GameVariableInfoFor("Difficulty: Cyber").HardcodedConfig().LimitedBy(0, 3),
15: GameVariableInfoFor("Difficulty: Combat").HardcodedConfig().LimitedBy(0, 3),
16: GameVariableInfoFor("Security Value: Level 0").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
17: GameVariableInfoFor("Security Value: Level 1").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
18: GameVariableInfoFor("Security Value: Level 2").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
19: GameVariableInfoFor("Security Value: Level 3").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
20: GameVariableInfoFor("Security Value: Level 4").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
21: GameVariableInfoFor("Security Value: Level 5").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
22: GameVariableInfoFor("Security Value: Level 6").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
23: GameVariableInfoFor("Security Value: Level 7").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
24: GameVariableInfoFor("Security Value: Level 8").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
25: GameVariableInfoFor("Security Value: Level 9").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
26: GameVariableInfoFor("Security Value: Level 10").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
27: GameVariableInfoFor("Security Value: Level 11").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
28: GameVariableInfoFor("Security Value: Level 12").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
29: GameVariableInfoFor("Security Value: Level 13").HardcodedAt(0).LimitedBy(0, 1000).DescribedAs(securityValueDescription),
30: GameVariableInfoFor("Difficulty: Puzzle").HardcodedConfig().LimitedBy(0, 3),
31: GameVariableInfoFor("Random Code 1").At(0).DescribedAs(randomCodeDescription),
32: GameVariableInfoFor("Random Code 2").At(0).DescribedAs(randomCodeDescription),
41: GameVariableInfoFor("Music Volume").HardcodedConfig().LimitedBy(0, 100),
42: GameVariableInfoFor("Video Gamma").HardcodedConfig().LimitedBy(0, math.MaxInt16),
43: GameVariableInfoFor("SFX Volume").HardcodedConfig().LimitedBy(0, 100),
44: GameVariableInfoFor("Mouse Handedness").HardcodedConfig().Enumerated(
map[int16]string{
0: "Right Handed",
1: "Left Handed",
}),
45: GameVariableInfoFor("Game: Highscore (pt1)").At(0).DescribedAs(highscoreCodeDescription),
46: GameVariableInfoFor("Game: Highscore (pt2)").At(0).DescribedAs(highscoreCodeDescription),
47: GameVariableInfoFor("Double-Click Speed").HardcodedConfig().LimitedBy(0, math.MaxInt16),
48: GameVariableInfoFor("Language").HardcodedConfig().Enumerated(map[int16]string{
0: "Default",
1: "French",
2: "German",
}),
49: GameVariableInfoFor("Audiolog Volume").HardcodedConfig().LimitedBy(0, 100),
50: GameVariableInfoFor("Screen Mode").HardcodedConfig(),
51: GameVariableInfoFor("Joystick Sensitivity").At(0x100).LimitedBy(0, 0x100),
52: GameVariableInfoFor("Show Fullscreen Icons").HardcodedConfig().Boolean(),
53: GameVariableInfoFor("Audio Messages").HardcodedConfig().Enumerated(map[int16]string{
0: "Text Only",
1: "Speech Only",
2: "Text and Speech",
}),
54: GameVariableInfoFor("Show Fullscreen Vitals").HardcodedConfig().Boolean(),
55: GameVariableInfoFor("Show Map Notes").HardcodedConfig().Boolean(),
56: GameVariableInfoFor("Game: Wing Level").At(0).LimitedBy(0, 100),
57: GameVariableInfoFor("HUD Color Bank").HardcodedConfig().LimitedBy(0, 2),
58: GameVariableInfoFor("Audio Channels").HardcodedConfig().Enumerated(map[int16]string{
0: "2",
1: "4",
2: "8",
}),
}
var engineBooleanVariables = GameVariables{
0: GameVariableInfoFor("Always False").At(0).
DescribedAs("This variable should always stay at 'False'.\n" +
"Default conditions assume boolean var 0 is zero to not block.\n" +
"For example, default doors would all become locked if it were 'True'."),
10: GameVariableInfoFor("Status HW: Delta Launch Enable").At(0),
11: GameVariableInfoFor("Status HW: Alpha Launch Enable").At(0),
12: GameVariableInfoFor("Status HW: Beta Launch Enable").At(0),
15: GameVariableInfoFor("Status HW: Beta Launched").At(0),
20: GameVariableInfoFor("Reactor on Destruct").At(0).
DescribedAs("Note: Rumble will stop if boolean var 152 is 'True'."),
94: GameVariableInfoFor("Unopenable door").
DescribedAs("Doors that are 'broken beyond repair' or such."),
145: GameVariableInfoFor("On-Line Help"),
152: GameVariableInfoFor("Self-Destruct Rumble Stop").At(0).
DescribedAs("Disables rumble caused by boolean var 20."),
153: GameVariableInfoFor("Four or more plastique exploded").At(0).
DescribedAs("Set to 'True' if integer variable 2 reached 4 (or higher)."),
231: GameVariableInfoFor("Unopenable door").
DescribedAs("Doors that are 'broken beyond repair' or such."),
300: GameVariableInfoFor("New Message Flag").At(0),
}
// EngineIntegerVariable returns a variable info for integer variables.
// If the given index is not used by the engine, nil is returned.
func EngineIntegerVariable(index int) *GameVariableInfo {
return engineIntegerVariables.Lookup(index)
}
// EngineBooleanVariable returns a variable info for boolean variables.
// If the given index is not used by the engine, nil is returned.
func EngineBooleanVariable(index int) *GameVariableInfo {
return engineBooleanVariables.Lookup(index)
}
// IsRandomIntegerVariable returns true for the special variables that are randomized.
func IsRandomIntegerVariable(index int) bool {
return (index == 31) || (index == 32)
}
// EngineVariables is a collector of engine-specific variable accessors.
type EngineVariables struct{}
var unusedVar = GameVariableInfoFor("(unused)").At(0)
// IntegerVariable returns a variable info for given index.
func (vars EngineVariables) IntegerVariable(index int) GameVariableInfo {
varInfo := EngineIntegerVariable(index)
if varInfo == nil {
return unusedVar
}
return *varInfo
}
// BooleanVariable returns a variable info for given index.
func (vars EngineVariables) BooleanVariable(index int) GameVariableInfo {
varInfo := EngineBooleanVariable(index)
if varInfo == nil {
return unusedVar
}
return *varInfo
}
@@ -0,0 +1,2 @@
// Package archive contains all the information that is stored in an archive file.
package archive
@@ -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))
}