Adding modding folder with a hacked build.
This commit is contained in:
@@ -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, ¶m)
|
||||
|
||||
data := buf.Bytes()
|
||||
assert.Equal(t, level.ParametersSize, len(data), "length mismatch")
|
||||
assert.Equal(t, []byte{
|
||||
0x5e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}, data, "data mismatch")
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package level
|
||||
|
||||
// RotationUnit describes a rotation in the range 0..255.
|
||||
type RotationUnit byte
|
||||
|
||||
// ToDegrees returns the value converted to degrees [0..360).
|
||||
func (unit RotationUnit) ToDegrees() float64 {
|
||||
return (float64(unit) * 360.0) / 256.0
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
schedulerDefaultSize = 64
|
||||
schedulerDefaultElementSize = 8
|
||||
)
|
||||
|
||||
// SchedulerInfo describes the basic parameters of a level scheduler.
|
||||
// Its entries are stored in the Schedules table.
|
||||
type SchedulerInfo struct {
|
||||
// Size describes the maximum amount of schedule entries in the table.
|
||||
Size int32
|
||||
// ScheduleCount is the current amount of active schedules.
|
||||
ScheduleCount int32
|
||||
// ElementSize is the size of an schedule entry, in bytes.
|
||||
ElementSize int32
|
||||
// GrowFlag indicates whether the schedule table can be resized.
|
||||
GrowFlag byte
|
||||
_ [4]byte
|
||||
_ [4]byte
|
||||
}
|
||||
|
||||
// DefaultSchedulerInfo returns a new scheduler info structure with default values.
|
||||
func DefaultSchedulerInfo() SchedulerInfo {
|
||||
return SchedulerInfo{
|
||||
Size: schedulerDefaultSize,
|
||||
ElementSize: schedulerDefaultElementSize,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
// SurveillanceObjectCount describes how many surveillance objects a level can have.
|
||||
SurveillanceObjectCount = 8
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
// TextureAnimationCount describes how many entries of texture animations a level can have.
|
||||
TextureAnimationCount = 4
|
||||
|
||||
// TextureAnimationEntrySize is the size, in bytes, of one animation entry.
|
||||
TextureAnimationEntrySize = 7
|
||||
)
|
||||
|
||||
// TextureAnimationEntry describes one entry of the texture animation table.
|
||||
type TextureAnimationEntry struct {
|
||||
FrameTime uint16
|
||||
CurrentFrameTime uint16
|
||||
CurrentFrameIndex byte
|
||||
FrameCount byte
|
||||
LoopType TextureAnimationLoopType
|
||||
}
|
||||
|
||||
// TextureAnimationLoopType describes how a texture animation loop should advance.
|
||||
type TextureAnimationLoopType byte
|
||||
|
||||
// String returns the textual representation.
|
||||
func (loopType TextureAnimationLoopType) String() string {
|
||||
switch loopType {
|
||||
case TextureAnimationForward:
|
||||
return "Forward"
|
||||
case TextureAnimationForthAndBack:
|
||||
return "Forth-And-Back"
|
||||
case TextureAnimationBackAndForth:
|
||||
return "Back-And-Forth"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%02X", int(loopType))
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// TextureAnimationForward has the texture animation run in a linear loop.
|
||||
TextureAnimationForward = TextureAnimationLoopType(0x00)
|
||||
// TextureAnimationForthAndBack reverses the sequence at every end.
|
||||
TextureAnimationForthAndBack = TextureAnimationLoopType(0x01)
|
||||
// TextureAnimationBackAndForth reverses the sequence at every end.
|
||||
TextureAnimationBackAndForth = TextureAnimationLoopType(0x81)
|
||||
)
|
||||
|
||||
// TextureAnimationLoopTypes returns all constants.
|
||||
func TextureAnimationLoopTypes() []TextureAnimationLoopType {
|
||||
return []TextureAnimationLoopType{
|
||||
TextureAnimationForward,
|
||||
TextureAnimationForthAndBack,
|
||||
TextureAnimationBackAndForth,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package level
|
||||
|
||||
const (
|
||||
// DefaultTextureAtlasSize is the amount of textures a level can hold.
|
||||
DefaultTextureAtlasSize = 54
|
||||
// FloorCeilingTextureLimit describes the amount of textures available for floors/ceilings.
|
||||
FloorCeilingTextureLimit = 32
|
||||
)
|
||||
|
||||
// TextureIndex identifies one game texture.
|
||||
type TextureIndex int16
|
||||
|
||||
// AtlasIndex is an index into the TextureAtlas.
|
||||
type AtlasIndex byte
|
||||
|
||||
// TextureAtlas is a selection of textures for the level.
|
||||
type TextureAtlas []TextureIndex
|
||||
@@ -0,0 +1,227 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TileFlag describes simple properties of a map tile.
|
||||
type TileFlag uint32
|
||||
|
||||
// RealWorldFlag describes simple properties of a map tile in the real world.
|
||||
type RealWorldFlag TileFlag
|
||||
|
||||
// CyberspaceFlag describes simple properties of a map tile in cyberspace.
|
||||
type CyberspaceFlag TileFlag
|
||||
|
||||
// ForRealWorld interprets the flag value for the real world.
|
||||
func (flag TileFlag) ForRealWorld() RealWorldFlag {
|
||||
return RealWorldFlag(flag)
|
||||
}
|
||||
|
||||
// ForCyberspace interprets the flag value for cyberspace.
|
||||
func (flag TileFlag) ForCyberspace() CyberspaceFlag {
|
||||
return CyberspaceFlag(flag)
|
||||
}
|
||||
|
||||
// MusicZone returns the music zone.
|
||||
func (flag TileFlag) MusicZone() TileMusicZone {
|
||||
return TileMusicZone((flag & 0x0000E000) >> 13)
|
||||
}
|
||||
|
||||
// WithMusicZone returns a new flag value with the given music index set.
|
||||
func (flag TileFlag) WithMusicZone(value TileMusicZone) TileFlag {
|
||||
return TileFlag(uint32(flag&^0x0000E000) | (uint32(value) << 13))
|
||||
}
|
||||
|
||||
// Peril returns whether the tile is marked as peril (may play peril music).
|
||||
func (flag TileFlag) Peril() bool {
|
||||
return (flag & 0x00001000) != 0
|
||||
}
|
||||
|
||||
// WithPeril returns a flag with the given peril set.
|
||||
func (flag TileFlag) WithPeril(value bool) TileFlag {
|
||||
var valueFlag uint32
|
||||
if value {
|
||||
valueFlag = 0x00001000
|
||||
}
|
||||
return TileFlag(uint32(flag&^0x00001000) | valueFlag)
|
||||
}
|
||||
|
||||
// SlopeControl returns the slope control as per flags.
|
||||
func (flag TileFlag) SlopeControl() TileSlopeControl {
|
||||
return TileSlopeControl((flag & 0x00000C00) >> 10)
|
||||
}
|
||||
|
||||
// WithSlopeControl returns a new flag value with the given slope control set.
|
||||
func (flag TileFlag) WithSlopeControl(ctrl TileSlopeControl) TileFlag {
|
||||
return TileFlag(uint32(flag&^0x00000C00) | (uint32(ctrl&0x3) << 10))
|
||||
}
|
||||
|
||||
// AsTileFlag returns the flags as regular tile flag value.
|
||||
func (flag RealWorldFlag) AsTileFlag() TileFlag {
|
||||
return TileFlag(flag)
|
||||
}
|
||||
|
||||
// WallTextureOffset returns the vertical offset (in tile height units) to apply for wall textures.
|
||||
func (flag RealWorldFlag) WallTextureOffset() TileHeightUnit {
|
||||
return TileHeightUnit(flag & 0x0000001F)
|
||||
}
|
||||
|
||||
// WithWallTextureOffset returns a flag value with the given wall texture offset.
|
||||
func (flag RealWorldFlag) WithWallTextureOffset(value TileHeightUnit) RealWorldFlag {
|
||||
return RealWorldFlag(uint32(flag&^0x0000001F) | uint32(value&0x1F))
|
||||
}
|
||||
|
||||
// WallTexturePattern returns the pattern to apply for walls.
|
||||
func (flag RealWorldFlag) WallTexturePattern() WallTexturePattern {
|
||||
return WallTexturePattern(byte(flag&0x00000060) >> 5)
|
||||
}
|
||||
|
||||
// WithWallTexturePattern returns a flag with the given pattern set.
|
||||
func (flag RealWorldFlag) WithWallTexturePattern(value WallTexturePattern) RealWorldFlag {
|
||||
return RealWorldFlag(uint32(flag&^0x00000060) | (uint32(value&0x3) << 5))
|
||||
}
|
||||
|
||||
// UseAdjacentWallTexture returns whether the wall texture from the adjacent tile should be used for each side.
|
||||
func (flag RealWorldFlag) UseAdjacentWallTexture() bool {
|
||||
return (flag & 0x00000100) != 0
|
||||
}
|
||||
|
||||
// WithUseAdjacentWallTexture returns a flag with the given usage set.
|
||||
func (flag RealWorldFlag) WithUseAdjacentWallTexture(value bool) RealWorldFlag {
|
||||
var valueFlag uint32
|
||||
if value {
|
||||
valueFlag = 0x00000100
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x00000100) | valueFlag)
|
||||
}
|
||||
|
||||
// Deconstructed returns whether the tile is marked as heavily deconstructed (should play spooky music).
|
||||
func (flag RealWorldFlag) Deconstructed() bool {
|
||||
return (flag & 0x00000200) != 0
|
||||
}
|
||||
|
||||
// WithDeconstructed returns a flag with the given deconstruction set.
|
||||
func (flag RealWorldFlag) WithDeconstructed(value bool) RealWorldFlag {
|
||||
var valueFlag uint32
|
||||
if value {
|
||||
valueFlag = 0x00000200
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x00000200) | valueFlag)
|
||||
}
|
||||
|
||||
// FloorShadow returns the floor shadow value. Range: [0..GradesOfShadow].
|
||||
func (flag RealWorldFlag) FloorShadow() int {
|
||||
return int((flag & 0x000F0000) >> 16)
|
||||
}
|
||||
|
||||
// WithFloorShadow returns a new flag value with the given floor shadow set. Values beyond allowed range are ignored.
|
||||
func (flag RealWorldFlag) WithFloorShadow(value int) RealWorldFlag {
|
||||
if (value < 0) || (value >= GradesOfShadow) {
|
||||
return flag
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x000F0000) | (uint32(value) << 16))
|
||||
}
|
||||
|
||||
// CeilingShadow returns the ceiling shadow value. Range: [0..GradesOfShadow].
|
||||
func (flag RealWorldFlag) CeilingShadow() int {
|
||||
return int((flag & 0x0F000000) >> 24)
|
||||
}
|
||||
|
||||
// WithCeilingShadow returns a new flag value with the given ceiling shadow set. Values beyond allowed range are ignored.
|
||||
func (flag RealWorldFlag) WithCeilingShadow(value int) RealWorldFlag {
|
||||
if (value < 0) || (value >= GradesOfShadow) {
|
||||
return flag
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x0F000000) | (uint32(value) << 24))
|
||||
}
|
||||
|
||||
// TileVisited returns whether the tile is marked as being visited (seen).
|
||||
func (flag RealWorldFlag) TileVisited() bool {
|
||||
return (flag & 0x80000000) != 0
|
||||
}
|
||||
|
||||
// WithTileVisited returns a flag with the given visited set.
|
||||
func (flag RealWorldFlag) WithTileVisited(value bool) RealWorldFlag {
|
||||
var valueFlag uint32
|
||||
if value {
|
||||
valueFlag = 0x80000000
|
||||
}
|
||||
return RealWorldFlag(uint32(flag&^0x80000000) | valueFlag)
|
||||
}
|
||||
|
||||
// AsTileFlag returns the flags as regular tile flag value.
|
||||
func (flag CyberspaceFlag) AsTileFlag() TileFlag {
|
||||
return TileFlag(flag)
|
||||
}
|
||||
|
||||
// GameOfLifeState returns the current game-of-life state.
|
||||
func (flag CyberspaceFlag) GameOfLifeState() int {
|
||||
return int(byte(flag&0x00000060) >> 5)
|
||||
}
|
||||
|
||||
// WithGameOfLifeState returns a flag with the given game-of-life state set.
|
||||
func (flag CyberspaceFlag) WithGameOfLifeState(value int) CyberspaceFlag {
|
||||
return CyberspaceFlag(uint32(flag&^0x00000060) | (uint32(value&0x3) << 5))
|
||||
}
|
||||
|
||||
// FlightPull returns the pull applying in a tile.
|
||||
func (flag CyberspaceFlag) FlightPull() CyberspaceFlightPull {
|
||||
return CyberspaceFlightPull((uint32(flag&0x01000000) >> 20) | (uint32(flag&0x000F0000) >> 16))
|
||||
}
|
||||
|
||||
// WithFlightPull returns a flag instance with the given pull applied.
|
||||
func (flag CyberspaceFlag) WithFlightPull(value CyberspaceFlightPull) CyberspaceFlag {
|
||||
newFlag := uint32(flag &^ 0x010F0000)
|
||||
newFlag |= uint32(value&0x0F) << 16
|
||||
newFlag |= uint32(value&0x10) << 20
|
||||
return CyberspaceFlag(newFlag)
|
||||
}
|
||||
|
||||
type TileMusicZone byte
|
||||
|
||||
const (
|
||||
TileMusicZoneNoMusic TileMusicZone = 0
|
||||
TileMusicZoneHospital TileMusicZone = 1
|
||||
TileMusicZoneExecutive TileMusicZone = 2
|
||||
TileMusicZoneIndustrial TileMusicZone = 3
|
||||
TileMusicZoneMetal TileMusicZone = 4
|
||||
TileMusicZonePark TileMusicZone = 5
|
||||
TileMusicZoneBridge TileMusicZone = 6
|
||||
TileMusicZoneElevator TileMusicZone = 7
|
||||
)
|
||||
|
||||
// TileSlopeControls returns all control values.
|
||||
func TileMusicZones() []TileMusicZone {
|
||||
return []TileMusicZone{
|
||||
TileMusicZoneNoMusic,
|
||||
TileMusicZoneHospital,
|
||||
TileMusicZoneExecutive,
|
||||
TileMusicZoneIndustrial,
|
||||
TileMusicZoneMetal,
|
||||
TileMusicZonePark,
|
||||
TileMusicZoneBridge,
|
||||
TileMusicZoneElevator,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl TileMusicZone) String() string {
|
||||
switch ctrl {
|
||||
case TileMusicZoneNoMusic:
|
||||
return "No Music"
|
||||
case TileMusicZoneHospital:
|
||||
return "Hospital"
|
||||
case TileMusicZoneExecutive:
|
||||
return "Executive"
|
||||
case TileMusicZoneIndustrial:
|
||||
return "Industrial"
|
||||
case TileMusicZoneMetal:
|
||||
return "Metal"
|
||||
case TileMusicZonePark:
|
||||
return "Park"
|
||||
case TileMusicZoneBridge:
|
||||
return "Bridge"
|
||||
case TileMusicZoneElevator:
|
||||
return "Elevator"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%02X", int(ctrl))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package level
|
||||
|
||||
// TileHeightUnit constants are listed below.
|
||||
const (
|
||||
TileHeightUnitMin TileHeightUnit = 0
|
||||
TileHeightUnitMax TileHeightUnit = 32
|
||||
)
|
||||
|
||||
// TileHeightUnit describes a height in a range of 0..32.
|
||||
type TileHeightUnit byte
|
||||
@@ -0,0 +1,93 @@
|
||||
package level
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
// TileMapEntry describes one tile of the map.
|
||||
type TileMapEntry struct {
|
||||
// Type indicates what kind of tile this is.
|
||||
Type TileType
|
||||
// Floor describes floor properties.
|
||||
Floor FloorInfo
|
||||
// Ceiling describes ceiling properties.
|
||||
Ceiling CeilingInfo
|
||||
// SlopeHeight indicates for non-flat tiles the height offset.
|
||||
SlopeHeight TileHeightUnit
|
||||
|
||||
// FirstObjectIndex points into the level object cross reference table to the first object in this tile.
|
||||
FirstObjectIndex int16
|
||||
// TextureInfo describes tile texturing.
|
||||
TextureInfo TileTextureInfo
|
||||
// Flags contains arbitrary additional information.
|
||||
Flags TileFlag
|
||||
|
||||
// SubClip is always 0xFF.
|
||||
SubClip byte
|
||||
_ [2]byte
|
||||
// LightDelta describes the light offsets of this tile.
|
||||
LightDelta LightDeltaInfo
|
||||
}
|
||||
|
||||
// Reset puts the tile into an initial state.
|
||||
func (tile *TileMapEntry) Reset() {
|
||||
*tile = TileMapEntry{}
|
||||
tile.SubClip = 0xFF
|
||||
}
|
||||
|
||||
// FloorTileHeightAt returns the height of the floor (in tile unit) at the given fine position.
|
||||
func (tile TileMapEntry) FloorTileHeightAt(pos FinePosition, height HeightShift) float32 {
|
||||
floorHeight, _ := height.ValueFromTileHeight(tile.Floor.AbsoluteHeight())
|
||||
if tile.Flags.SlopeControl() == TileSlopeControlFloorFlat {
|
||||
return floorHeight
|
||||
}
|
||||
slopeHeight, _ := height.ValueFromTileHeight(tile.SlopeHeight)
|
||||
fineHeight := slopeHeight * pos.SlopeFactorFor(tile.Type)
|
||||
return floorHeight + fineHeight
|
||||
}
|
||||
|
||||
// CeilingTileHeightAt returns the height of the ceiling (in tile unit) at the given fine position.
|
||||
func (tile TileMapEntry) CeilingTileHeightAt(pos FinePosition, height HeightShift) float32 {
|
||||
ceilingHeight, _ := height.ValueFromTileHeight(tile.Ceiling.AbsoluteHeight())
|
||||
slopeControl := tile.Flags.SlopeControl()
|
||||
if slopeControl == TileSlopeControlCeilingFlat {
|
||||
return ceilingHeight
|
||||
}
|
||||
slopeHeight, _ := height.ValueFromTileHeight(tile.SlopeHeight)
|
||||
var slopeFactor float32
|
||||
if slopeControl == TileSlopeControlCeilingMirrored {
|
||||
slopeFactor = pos.SlopeFactorFor(tile.Type)
|
||||
} else {
|
||||
slopeFactor = pos.SlopeFactorFor(tile.Type.Info().SlopeInvertedType)
|
||||
}
|
||||
fineHeight := slopeHeight * slopeFactor
|
||||
return ceilingHeight - fineHeight
|
||||
}
|
||||
|
||||
// TileMap is a rectangular set of tiles.
|
||||
// The first index is the Y-axis, the second index the X-axis. This way the map can be serialized quicker.
|
||||
type TileMap struct {
|
||||
entries []TileMapEntry
|
||||
}
|
||||
|
||||
// NewTileMap returns a new, initialized map.
|
||||
func NewTileMap(width, height int) TileMap {
|
||||
entries := make([]TileMapEntry, width*height)
|
||||
for i := 0; i < len(entries); i++ {
|
||||
entries[i].Reset()
|
||||
}
|
||||
return TileMap{entries: entries}
|
||||
}
|
||||
|
||||
// Tile returns a pointer to the tile within the map for given position.
|
||||
// Nil is returned for a coordinate outside the boundaries.
|
||||
func (m TileMap) Tile(pos TilePosition, xShift int) *TileMapEntry {
|
||||
index := int(pos.X) + (int(pos.Y) << xShift)
|
||||
if (int(pos.X) >= (1 << xShift)) || (index >= len(m.entries)) {
|
||||
return nil
|
||||
}
|
||||
return &m.entries[index]
|
||||
}
|
||||
|
||||
// Code serializes the map.
|
||||
func (m TileMap) Code(coder serial.Coder) {
|
||||
coder.Code(m.entries)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTileMapEntrySerializedSize(t *testing.T) {
|
||||
var entry level.TileMapEntry
|
||||
size := binary.Size(&entry)
|
||||
assert.Equal(t, 16, size, "Size mismatch")
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package level
|
||||
|
||||
// TilePosition contains the coordinates of a tile in a level.
|
||||
type TilePosition struct {
|
||||
X byte
|
||||
Y byte
|
||||
}
|
||||
|
||||
// FinePosition contains the coordinates of an object within a tile.
|
||||
type FinePosition struct {
|
||||
X byte
|
||||
Y byte
|
||||
}
|
||||
|
||||
// SlopeFactorFor returns the factor of how much the slope of given tile type would apply at the fine position.
|
||||
func (pos FinePosition) SlopeFactorFor(tileType TileType) float32 {
|
||||
southToNorth := float32(pos.Y) / 255
|
||||
westToEast := float32(pos.X) / 255
|
||||
swneDiag := func(northWest, southEast float32) float32 {
|
||||
if pos.X < pos.Y {
|
||||
return northWest
|
||||
}
|
||||
return southEast
|
||||
}
|
||||
nwseDiag := func(southWest, northEast float32) float32 {
|
||||
if (255 - pos.X) < pos.Y {
|
||||
return northEast
|
||||
}
|
||||
return southWest
|
||||
}
|
||||
|
||||
switch tileType {
|
||||
case TileTypeSlopeSouthToNorth:
|
||||
return southToNorth
|
||||
case TileTypeSlopeWestToEast:
|
||||
return westToEast
|
||||
case TileTypeSlopeNorthToSouth:
|
||||
return 1 - southToNorth
|
||||
case TileTypeSlopeEastToWest:
|
||||
return 1 - westToEast
|
||||
|
||||
case TileTypeValleySouthEastToNorthWest:
|
||||
return nwseDiag(1-westToEast, southToNorth)
|
||||
case TileTypeValleySouthWestToNorthEast:
|
||||
return swneDiag(southToNorth, westToEast)
|
||||
case TileTypeValleyNorthWestToSouthEast:
|
||||
return nwseDiag(1-southToNorth, westToEast)
|
||||
case TileTypeValleyNorthEastToSouthWest:
|
||||
return swneDiag(1-westToEast, 1-southToNorth)
|
||||
|
||||
case TileTypeRidgeNorthWestToSouthEast:
|
||||
return nwseDiag(westToEast, 1-southToNorth)
|
||||
case TileTypeRidgeNorthEastToSouthWest:
|
||||
return swneDiag(1-southToNorth, 1-westToEast)
|
||||
case TileTypeRidgeSouthEastToNorthWest:
|
||||
return nwseDiag(southToNorth, 1-westToEast)
|
||||
case TileTypeRidgeSouthWestToNorthEast:
|
||||
return swneDiag(westToEast, southToNorth)
|
||||
}
|
||||
|
||||
return 0.0
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
)
|
||||
|
||||
func TestFinePositionSlopeFactorFor(t *testing.T) {
|
||||
tt := []struct {
|
||||
pos level.FinePosition
|
||||
tileType level.TileType
|
||||
expected float32
|
||||
}{
|
||||
{level.FinePosition{X: 127, Y: 127}, level.TileTypeOpen, 0.0},
|
||||
{level.FinePosition{X: 127, Y: 127}, level.TileTypeSlopeNorthToSouth, 0.5},
|
||||
{level.FinePosition{X: 127, Y: 127}, level.TileTypeRidgeNorthWestToSouthEast, 0.5},
|
||||
{level.FinePosition{X: 0, Y: 127}, level.TileTypeRidgeNorthWestToSouthEast, 0.0},
|
||||
{level.FinePosition{X: 127, Y: 0}, level.TileTypeRidgeNorthWestToSouthEast, 0.5},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
result := tc.pos.SlopeFactorFor(tc.tileType)
|
||||
assert.InDeltaf(t, tc.expected, result, 0.01, "Type: %v, Pos: %v", tc.tileType, tc.pos)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TileSlopeControl defines how the floor and ceiling of a sloped tile should be processed.
|
||||
type TileSlopeControl byte
|
||||
|
||||
// String returns the textual representation of the value.
|
||||
func (ctrl TileSlopeControl) String() string {
|
||||
switch ctrl {
|
||||
case TileSlopeControlCeilingInverted:
|
||||
return "CeilingInverted"
|
||||
case TileSlopeControlCeilingMirrored:
|
||||
return "CeilingMirrored"
|
||||
case TileSlopeControlCeilingFlat:
|
||||
return "CeilingFlat"
|
||||
case TileSlopeControlFloorFlat:
|
||||
return "FloorFlat"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%02X", int(ctrl))
|
||||
}
|
||||
}
|
||||
|
||||
// FloorSlopeFactors returns the slope factors for the given tile type as per control constant.
|
||||
func (ctrl TileSlopeControl) FloorSlopeFactors(tileType TileType) SlopeFactors {
|
||||
if ctrl == TileSlopeControlFloorFlat {
|
||||
return SlopeFactors{}
|
||||
}
|
||||
return tileType.Info().SlopeFloorFactors
|
||||
}
|
||||
|
||||
// CeilingSlopeFactors returns the slope factors for the given tile type as per control constant.
|
||||
func (ctrl TileSlopeControl) CeilingSlopeFactors(tileType TileType) SlopeFactors {
|
||||
if ctrl == TileSlopeControlCeilingFlat {
|
||||
return SlopeFactors{}
|
||||
}
|
||||
if ctrl == TileSlopeControlCeilingMirrored {
|
||||
return tileType.Info().SlopeFloorFactors
|
||||
}
|
||||
return tileType.Info().SlopeInvertedType.Info().SlopeFloorFactors
|
||||
}
|
||||
|
||||
// TileSlopeControl constants.
|
||||
const (
|
||||
TileSlopeControlCeilingInverted TileSlopeControl = 0
|
||||
TileSlopeControlCeilingMirrored TileSlopeControl = 1
|
||||
TileSlopeControlCeilingFlat TileSlopeControl = 2
|
||||
TileSlopeControlFloorFlat TileSlopeControl = 3
|
||||
)
|
||||
|
||||
// TileSlopeControls returns all control values.
|
||||
func TileSlopeControls() []TileSlopeControl {
|
||||
return []TileSlopeControl{
|
||||
TileSlopeControlCeilingInverted,
|
||||
TileSlopeControlCeilingMirrored,
|
||||
TileSlopeControlCeilingFlat,
|
||||
TileSlopeControlFloorFlat,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package level
|
||||
|
||||
// TileTextureInfo describes the textures used for a map tile.
|
||||
type TileTextureInfo uint16
|
||||
|
||||
// WallTextureIndex returns the texture index into the texture atlas for the walls.
|
||||
// Valid range [0..63].
|
||||
// This property is only valid in real world.
|
||||
func (info TileTextureInfo) WallTextureIndex() AtlasIndex {
|
||||
return AtlasIndex(info & 0x003F)
|
||||
}
|
||||
|
||||
// WithWallTextureIndex returns an info with given index set.
|
||||
// Values outside valid range are ignored.
|
||||
func (info TileTextureInfo) WithWallTextureIndex(value AtlasIndex) TileTextureInfo {
|
||||
if value >= 64 {
|
||||
return info
|
||||
}
|
||||
return TileTextureInfo(uint16(info&^0x003F) | uint16(value&0x003F))
|
||||
}
|
||||
|
||||
// CeilingTextureIndex returns the texture index into the texture atlas for the ceiling.
|
||||
// Valid range [0..31].
|
||||
// This property is only valid in real world.
|
||||
func (info TileTextureInfo) CeilingTextureIndex() AtlasIndex {
|
||||
return AtlasIndex((info & 0x07C0) >> 6)
|
||||
}
|
||||
|
||||
// WithCeilingTextureIndex returns an info with given index set.
|
||||
// Values outside valid range are ignored.
|
||||
func (info TileTextureInfo) WithCeilingTextureIndex(value AtlasIndex) TileTextureInfo {
|
||||
if value >= FloorCeilingTextureLimit {
|
||||
return info
|
||||
}
|
||||
return TileTextureInfo(uint16(info&^0x07C0) | (uint16(value&0x001F) << 6))
|
||||
}
|
||||
|
||||
// FloorTextureIndex returns the texture index into the texture atlas for the floor.
|
||||
// Valid range [0..31].
|
||||
// This property is only valid in real world.
|
||||
func (info TileTextureInfo) FloorTextureIndex() AtlasIndex {
|
||||
return AtlasIndex((info & 0xF800) >> 11)
|
||||
}
|
||||
|
||||
// WithFloorTextureIndex returns an info with given index set.
|
||||
// Values outside valid range are ignored.
|
||||
func (info TileTextureInfo) WithFloorTextureIndex(value AtlasIndex) TileTextureInfo {
|
||||
if value >= FloorCeilingTextureLimit {
|
||||
return info
|
||||
}
|
||||
return TileTextureInfo(uint16(info&^0xF800) | (uint16(value&0x001F) << 11))
|
||||
}
|
||||
|
||||
// FloorPaletteIndex returns the palette index for the floor in cyberspace.
|
||||
func (info TileTextureInfo) FloorPaletteIndex() byte {
|
||||
return byte(info)
|
||||
}
|
||||
|
||||
// WithFloorPaletteIndex returns an info with given index set.
|
||||
func (info TileTextureInfo) WithFloorPaletteIndex(value byte) TileTextureInfo {
|
||||
return TileTextureInfo(uint16(info&0xFF00) | uint16(value))
|
||||
}
|
||||
|
||||
// CeilingPaletteIndex returns the palette index for the ceiling in cyberspace.
|
||||
func (info TileTextureInfo) CeilingPaletteIndex() byte {
|
||||
return byte(info >> 8)
|
||||
}
|
||||
|
||||
// WithCeilingPaletteIndex returns an info with given index set.
|
||||
func (info TileTextureInfo) WithCeilingPaletteIndex(value byte) TileTextureInfo {
|
||||
return TileTextureInfo(uint16(info&0x00FF) | (uint16(value) << 8))
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TileType describes the general type of a map tile.
|
||||
type TileType byte
|
||||
|
||||
// Info returns the information associated with the tile type.
|
||||
func (t TileType) Info() TileTypeInfo {
|
||||
if int(t) < len(tileTypeInfoList) {
|
||||
return tileTypeInfoList[t]
|
||||
}
|
||||
info := tileTypeInfoList[TileTypeSolid]
|
||||
info.Name = t.String()
|
||||
info.SlopeInvertedType = t
|
||||
return info
|
||||
}
|
||||
|
||||
// String returns the textual representation of the type.
|
||||
func (t TileType) String() string {
|
||||
if int(t) < len(tileTypeInfoList) {
|
||||
return tileTypeInfoList[t].Name
|
||||
}
|
||||
return fmt.Sprintf("Unknown%02X", int(t))
|
||||
}
|
||||
|
||||
// Tiles come in different forms:
|
||||
// Solid tiles can not be entered, Open tiles are regular tiles with a flat floor and a flat ceiling.
|
||||
// DiagonalOpen tiles are those with flat floors and ceilings, and two walls cut off by one diagonal wall.
|
||||
// Slope tiles have a sloped floor (or ceiling). Valley tiles have one floor vertex lower while Ridge tiles have one
|
||||
// floor vertex higher than the other three.
|
||||
const (
|
||||
TileTypeSolid TileType = 0x00
|
||||
TileTypeOpen TileType = 0x01
|
||||
|
||||
TileTypeDiagonalOpenSouthEast TileType = 0x02
|
||||
TileTypeDiagonalOpenSouthWest TileType = 0x03
|
||||
TileTypeDiagonalOpenNorthWest TileType = 0x04
|
||||
TileTypeDiagonalOpenNorthEast TileType = 0x05
|
||||
|
||||
TileTypeSlopeSouthToNorth TileType = 0x06
|
||||
TileTypeSlopeWestToEast TileType = 0x07
|
||||
TileTypeSlopeNorthToSouth TileType = 0x08
|
||||
TileTypeSlopeEastToWest TileType = 0x09
|
||||
|
||||
TileTypeValleySouthEastToNorthWest TileType = 0x0A
|
||||
TileTypeValleySouthWestToNorthEast TileType = 0x0B
|
||||
TileTypeValleyNorthWestToSouthEast TileType = 0x0C
|
||||
TileTypeValleyNorthEastToSouthWest TileType = 0x0D
|
||||
|
||||
TileTypeRidgeNorthWestToSouthEast TileType = 0x0E
|
||||
TileTypeRidgeNorthEastToSouthWest TileType = 0x0F
|
||||
TileTypeRidgeSouthEastToNorthWest TileType = 0x10
|
||||
TileTypeRidgeSouthWestToNorthEast TileType = 0x11
|
||||
)
|
||||
|
||||
// TileTypes returns a list of all known tile types.
|
||||
func TileTypes() []TileType {
|
||||
return []TileType{
|
||||
TileTypeSolid, TileTypeOpen,
|
||||
TileTypeDiagonalOpenSouthEast, TileTypeDiagonalOpenSouthWest, TileTypeDiagonalOpenNorthWest, TileTypeDiagonalOpenNorthEast,
|
||||
TileTypeSlopeSouthToNorth, TileTypeSlopeWestToEast, TileTypeSlopeNorthToSouth, TileTypeSlopeEastToWest,
|
||||
TileTypeValleySouthEastToNorthWest, TileTypeValleySouthWestToNorthEast, TileTypeValleyNorthWestToSouthEast, TileTypeValleyNorthEastToSouthWest,
|
||||
TileTypeRidgeNorthWestToSouthEast, TileTypeRidgeNorthEastToSouthWest, TileTypeRidgeSouthEastToNorthWest, TileTypeRidgeSouthWestToNorthEast,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package level
|
||||
|
||||
// SlopeFactors is a list of multipliers for each direction of a tile.
|
||||
type SlopeFactors [8]float32
|
||||
|
||||
// Negated returns the factors multiplied by -1.
|
||||
func (factors SlopeFactors) Negated() SlopeFactors {
|
||||
return SlopeFactors{
|
||||
factors[0] * -1, factors[1] * -1, factors[2] * -1, factors[3] * -1,
|
||||
factors[4] * -1, factors[5] * -1, factors[6] * -1, factors[7] * -1,
|
||||
}
|
||||
}
|
||||
|
||||
// TileTypeInfo is the meta information about a tile type.
|
||||
type TileTypeInfo struct {
|
||||
// Name is the textual representation of the tile type.
|
||||
Name string
|
||||
|
||||
// SolidSides is a bitfield of cardinal directions describing solid walls.
|
||||
SolidSides DirectionMask
|
||||
|
||||
// SlopeFloorFactors defines how a slope affects the floor in each direction of a tile [0.0 .. 1.0].
|
||||
SlopeFloorFactors SlopeFactors
|
||||
|
||||
// SlopeInvertedType is the type that inverts the slope to form a solid tile if merged (e.g. floor & ceiling).
|
||||
// Types that have no slope invert themselves.
|
||||
SlopeInvertedType TileType
|
||||
}
|
||||
|
||||
var tileTypeInfoList = []TileTypeInfo{
|
||||
{"Solid", DirNorth.Plus(DirEast).Plus(DirSouth).Plus(DirWest), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeSolid},
|
||||
{"Open", 0, SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeOpen},
|
||||
|
||||
{"DiagonalOpenSouthEast", DirNorth.Plus(DirWest), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeDiagonalOpenSouthEast},
|
||||
{"DiagonalOpenSouthWest", DirNorth.Plus(DirEast), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeDiagonalOpenSouthWest},
|
||||
{"DiagonalOpenNorthWest", DirSouth.Plus(DirEast), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeDiagonalOpenNorthWest},
|
||||
{"DiagonalOpenNorthEast", DirSouth.Plus(DirWest), SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0}, TileTypeDiagonalOpenNorthEast},
|
||||
|
||||
{"SlopeSouthToNorth", 0, SlopeFactors{1.0, 1.0, 0.5, 0.0, 0.0, 0.0, 0.5, 1.0}, TileTypeSlopeNorthToSouth},
|
||||
{"SlopeWestToEast", 0, SlopeFactors{0.5, 1.0, 1.0, 1.0, 0.5, 0.0, 0.0, 0.0}, TileTypeSlopeEastToWest},
|
||||
{"SlopeNorthToSouth", 0, SlopeFactors{0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.5, 0.0}, TileTypeSlopeSouthToNorth},
|
||||
{"SlopeEastToWest", 0, SlopeFactors{0.5, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0}, TileTypeSlopeWestToEast},
|
||||
|
||||
{"ValleySouthEastToNorthWest", 0, SlopeFactors{1.0, 1.0, 0.5, 0.0, 0.5, 1.0, 1.0, 1.0}, TileTypeRidgeNorthWestToSouthEast},
|
||||
{"ValleySouthWestToNorthEast", 0, SlopeFactors{1.0, 1.0, 1.0, 1.0, 0.5, 0.0, 0.5, 1.0}, TileTypeRidgeNorthEastToSouthWest},
|
||||
{"ValleyNorthWestToSouthEast", 0, SlopeFactors{0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.0}, TileTypeRidgeSouthEastToNorthWest},
|
||||
{"ValleyNorthEastToSouthWest", 0, SlopeFactors{0.5, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0}, TileTypeRidgeSouthWestToNorthEast},
|
||||
|
||||
{"RidgeNorthWestToSouthEast", 0, SlopeFactors{0.0, 0.0, 0.5, 1.0, 0.5, 0.0, 0.0, 0.0}, TileTypeValleySouthEastToNorthWest},
|
||||
{"RidgeNorthEastToSouthWest", 0, SlopeFactors{0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 0.5, 0.0}, TileTypeValleySouthWestToNorthEast},
|
||||
{"RidgeSouthEastToNorthWest", 0, SlopeFactors{0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 1.0}, TileTypeValleyNorthWestToSouthEast},
|
||||
{"RidgeSouthWestToNorthEast", 0, SlopeFactors{0.5, 1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0}, TileTypeValleyNorthEastToSouthWest},
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTileTypeInfoSlopeFactorsSealTileWithInversion(t *testing.T) {
|
||||
tt := []level.TileType{
|
||||
level.TileTypeSlopeSouthToNorth,
|
||||
level.TileTypeSlopeWestToEast,
|
||||
level.TileTypeSlopeNorthToSouth,
|
||||
level.TileTypeSlopeEastToWest,
|
||||
|
||||
level.TileTypeValleySouthEastToNorthWest,
|
||||
level.TileTypeValleySouthWestToNorthEast,
|
||||
level.TileTypeValleyNorthWestToSouthEast,
|
||||
level.TileTypeValleyNorthEastToSouthWest,
|
||||
|
||||
level.TileTypeRidgeNorthWestToSouthEast,
|
||||
level.TileTypeRidgeNorthEastToSouthWest,
|
||||
level.TileTypeRidgeSouthEastToNorthWest,
|
||||
level.TileTypeRidgeSouthWestToNorthEast,
|
||||
}
|
||||
for _, tc := range tt {
|
||||
info := tc.Info()
|
||||
mirroredInfo := info.SlopeInvertedType.Info()
|
||||
var factors level.SlopeFactors
|
||||
for i := 0; i < 8; i++ {
|
||||
factors[i] = info.SlopeFloorFactors[i] + mirroredInfo.SlopeFloorFactors[i]
|
||||
}
|
||||
assert.Equal(t, level.SlopeFactors{1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}, factors, fmt.Sprintf("Factors don't seal tile for type 0x%02X", tc))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTileTypeInfoDefaultsToSolidEquivalent(t *testing.T) {
|
||||
tileType := level.TileType(0x77)
|
||||
expected := level.TileTypeInfo{
|
||||
Name: "Unknown77",
|
||||
SolidSides: level.DirNorth.Plus(level.DirEast).Plus(level.DirSouth).Plus(level.DirWest),
|
||||
SlopeFloorFactors: level.SlopeFactors{0, 0, 0, 0, 0, 0, 0, 0},
|
||||
SlopeInvertedType: tileType,
|
||||
}
|
||||
assert.Equal(t, expected, tileType.Info())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package level
|
||||
|
||||
// UnknownL41 is the unknown data of L41.
|
||||
type UnknownL41 [1]byte
|
||||
|
||||
// UnknownL48 is the unknown data of L48.
|
||||
type UnknownL48 [0x30]byte
|
||||
|
||||
// UnknownL49 is the unknown data of L49.
|
||||
type UnknownL49 [0x1C0]byte
|
||||
|
||||
// UnknownL50 is the unknown data of L50.
|
||||
type UnknownL50 [2]byte
|
||||
|
||||
// UnknownL52 is the unknown data of L52.
|
||||
type UnknownL52 [2]byte
|
||||
@@ -0,0 +1,119 @@
|
||||
package level
|
||||
|
||||
// WallHeights describes the height delta, in HeightUnits, one would take to
|
||||
// cross to the other tile. A value of 0 means equal floor, a negative value a fall down.
|
||||
// A value of 0x20 (= maximum of height units) means a solid wall.
|
||||
//
|
||||
// For each cardinal direction, facing to the specified direction,
|
||||
// the first entry is the left side, the second the center, and the third the right side.
|
||||
type WallHeights struct {
|
||||
North [3]float32
|
||||
East [3]float32
|
||||
South [3]float32
|
||||
West [3]float32
|
||||
}
|
||||
|
||||
// Reset sets all the heights to zero.
|
||||
func (heights *WallHeights) Reset() {
|
||||
*heights = WallHeights{}
|
||||
}
|
||||
|
||||
// WallHeightsMap is rectangular table of wall heights, typically mirroring a tile map.
|
||||
type WallHeightsMap [][]WallHeights
|
||||
|
||||
// NewWallHeightsMap returns a new, initialized map.
|
||||
func NewWallHeightsMap(width, height int) WallHeightsMap {
|
||||
m := make([][]WallHeights, height)
|
||||
for y := 0; y < height; y++ {
|
||||
m[y] = make([]WallHeights, width)
|
||||
for x := 0; x < width; x++ {
|
||||
m[y][x].Reset()
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Tile returns a pointer to the tile within the map for given position.
|
||||
func (m WallHeightsMap) Tile(pos TilePosition) *WallHeights {
|
||||
return &m[pos.Y][pos.X]
|
||||
}
|
||||
|
||||
// CalculateFrom updates all the wall heights according to the specified map.
|
||||
func (m *WallHeightsMap) CalculateFrom(tileMap interface {
|
||||
Tile(pos TilePosition) *TileMapEntry
|
||||
}) {
|
||||
posAt := func(x, y int) TilePosition {
|
||||
return TilePosition{X: byte(x), Y: byte(y)}
|
||||
}
|
||||
for y, row := range *m {
|
||||
for x := 0; x < len(row); x++ {
|
||||
tile := tileMap.Tile(posAt(x, y))
|
||||
if tile == nil {
|
||||
continue
|
||||
}
|
||||
heights := &row[x]
|
||||
heights.North = calculateWallHeight(tile, DirNorth, tileMap.Tile(posAt(x, y+1)), DirSouth)
|
||||
heights.East = calculateWallHeight(tile, DirEast, tileMap.Tile(posAt(x+1, y)), DirWest)
|
||||
heights.South = calculateWallHeight(tile, DirSouth, tileMap.Tile(posAt(x, y-1)), DirNorth)
|
||||
heights.West = calculateWallHeight(tile, DirWest, tileMap.Tile(posAt(x-1, y)), DirEast)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func calculateWallHeight(entry *TileMapEntry, entrySide Direction, other *TileMapEntry, otherSide Direction) [3]float32 {
|
||||
var result [3]float32
|
||||
baseHeights := func(tile *TileMapEntry, side Direction) (floor TileHeightUnit, slope TileHeightUnit, ceiling TileHeightUnit) {
|
||||
floor = TileHeightUnitMax
|
||||
slope = TileHeightUnitMin
|
||||
ceiling = TileHeightUnitMax
|
||||
if (tile != nil) && ((tile.Type.Info().SolidSides & side.AsMask()) == 0) {
|
||||
floor = tile.Floor.AbsoluteHeight()
|
||||
slope = tile.SlopeHeight
|
||||
ceiling = tile.Ceiling.AbsoluteHeight()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
otherFloorHeight, otherSlopeHeight, otherCeilingHeight := baseHeights(other, otherSide)
|
||||
var otherFloorFactors SlopeFactors
|
||||
var otherCeilingFactors SlopeFactors
|
||||
if other != nil {
|
||||
otherSlopeControl := other.Flags.SlopeControl()
|
||||
otherFloorFactors = otherSlopeControl.FloorSlopeFactors(other.Type)
|
||||
otherCeilingFactors = otherSlopeControl.CeilingSlopeFactors(other.Type)
|
||||
}
|
||||
|
||||
entryFloorHeight, entrySlopeHeight, entryCeilingHeight := baseHeights(entry, entrySide)
|
||||
entrySlopeControl := entry.Flags.SlopeControl()
|
||||
entryFloorFactors := entrySlopeControl.FloorSlopeFactors(entry.Type)
|
||||
entryCeilingFactors := entrySlopeControl.CeilingSlopeFactors(entry.Type)
|
||||
|
||||
sideHeights := func(side Direction, sideOffset int, factors SlopeFactors, slopeHeight TileHeightUnit, absoluteHeight TileHeightUnit) [3]float32 {
|
||||
return [3]float32{
|
||||
factors[side.Offset(-sideOffset)]*float32(slopeHeight) + float32(absoluteHeight),
|
||||
factors[side.Offset(0)]*float32(slopeHeight) + float32(absoluteHeight),
|
||||
factors[side.Offset(+sideOffset)]*float32(slopeHeight) + float32(absoluteHeight),
|
||||
}
|
||||
}
|
||||
entryFloorSideHeights := sideHeights(entrySide, 1, entryFloorFactors, entrySlopeHeight, entryFloorHeight)
|
||||
entryCeilingSideHeights := sideHeights(entrySide, 1, entryCeilingFactors.Negated(), entrySlopeHeight, entryCeilingHeight)
|
||||
otherFloorSideHeights := sideHeights(otherSide, -1, otherFloorFactors, otherSlopeHeight, otherFloorHeight)
|
||||
otherCeilingSideHeights := sideHeights(otherSide, -1, otherCeilingFactors.Negated(), otherSlopeHeight, otherCeilingHeight)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
switch {
|
||||
case (otherFloorSideHeights[i] == entryFloorSideHeights[i]) && (entryFloorSideHeights[i] == float32(TileHeightUnitMax)):
|
||||
// shortcut for solid-to-solid - no "wall" to see here
|
||||
result[i] = 0
|
||||
case (otherFloorSideHeights[i] >= otherCeilingSideHeights[i]) || // other ceiling side seals off at its floor
|
||||
(otherCeilingSideHeights[i] <= entryFloorSideHeights[i]) || // other ceiling side seals off at this floor
|
||||
(otherFloorSideHeights[i] >= entryCeilingSideHeights[i]) || // other floor side seals off at this ceiling
|
||||
(entryFloorSideHeights[i] >= entryCeilingSideHeights[i]): // own ceiling side seals off at floor
|
||||
result[i] = float32(TileHeightUnitMax)
|
||||
default:
|
||||
result[i] = otherFloorSideHeights[i] - entryFloorSideHeights[i]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package level_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
)
|
||||
|
||||
type WallHeightsSuite struct {
|
||||
suite.Suite
|
||||
|
||||
tileMap level.TileMap
|
||||
heightsMap level.WallHeightsMap
|
||||
|
||||
tile *level.TileMapEntry
|
||||
}
|
||||
|
||||
func TestWallHeightsSuite(t *testing.T) {
|
||||
suite.Run(t, new(WallHeightsSuite))
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) SetupTest() {
|
||||
suite.tileMap = level.NewTileMap(4, 4)
|
||||
suite.heightsMap = level.NewWallHeightsMap(3, 3)
|
||||
suite.tile = nil
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromResetsToZeroForAllSolids() {
|
||||
suite.givenARandomHeightsMap()
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightsShouldBeForTile(1, 1, 0.0)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsToMaximumHeightUnitFromOpenToSolid() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightsShouldBeForTile(1, 1, 32.0)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(1, 0, level.DirNorth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(0, 1, level.DirEast, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(2, 1, level.DirWest, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsToMaximumHeightUnitFromOpenToSolidSide() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen)
|
||||
suite.givenTile(2, 1).typed(level.TileTypeDiagonalOpenNorthEast)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightsShouldBeForTile(1, 1, 32)
|
||||
suite.thenHeightShouldBeForTileAt(2, 1, level.DirWest, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsHeightDifferencesForFloors() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(0)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeOpen).floorAt(10)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 10, 10, 10)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, -10, -10, -10)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsHeightDifferencesForFloorsRespectingSlope() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeSlopeEastToWest).floorAt(0).sloped(level.TileSlopeControlCeilingFlat, 2)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeOpen).floorAt(10)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 8, 9, 10)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, -10, -9, -8)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromSetsHeightDifferencesForFloorsRespectingSlopeSameFloorHeight() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(0)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeSlopeEastToWest).floorAt(0).sloped(level.TileSlopeControlCeilingFlat, 4)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 4, 2, 0)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 0, -2, -4)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromConsidersCeilingHeightsIfWallingOff() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(0)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeSlopeSouthToNorth).floorAt(0).sloped(level.TileSlopeControlFloorFlat, 8).ceilingAt(8)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromConsidersMirroredSlopesWallingOff() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(0)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeSlopeNorthToSouth).floorAt(0).sloped(level.TileSlopeControlCeilingMirrored, 16)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromWithFloorCrossingOtherCeiling() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeOpen).floorAt(20)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeOpen).floorAt(0).ceilingAt(8)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 32, 32, 32)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 32, 32, 32)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) TestCalculateFromWithSameSlopes() {
|
||||
suite.givenTile(1, 1).typed(level.TileTypeSlopeEastToWest).sloped(level.TileSlopeControlCeilingInverted, 8)
|
||||
suite.givenTile(1, 2).typed(level.TileTypeSlopeEastToWest).sloped(level.TileSlopeControlCeilingInverted, 8)
|
||||
suite.whenHeightsAreCalculatedFromMap()
|
||||
suite.thenHeightShouldBeForTileAt(1, 1, level.DirNorth, 0, 0, 0)
|
||||
suite.thenHeightShouldBeForTileAt(1, 2, level.DirSouth, 0, 0, 0)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) givenTile(x, y int) *WallHeightsSuite {
|
||||
suite.tile = suite.Tile(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) typed(tileType level.TileType) *WallHeightsSuite {
|
||||
suite.tile.Type = tileType
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) sloped(ctrl level.TileSlopeControl, height level.TileHeightUnit) *WallHeightsSuite {
|
||||
suite.tile.Flags = suite.tile.Flags.WithSlopeControl(ctrl)
|
||||
suite.tile.SlopeHeight = height
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) floorAt(value level.TileHeightUnit) *WallHeightsSuite {
|
||||
suite.tile.Floor = suite.tile.Floor.WithAbsoluteHeight(value)
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) ceilingAt(value level.TileHeightUnit) *WallHeightsSuite {
|
||||
suite.tile.Ceiling = suite.tile.Ceiling.WithAbsoluteHeight(value)
|
||||
return suite
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) givenARandomHeightsMap() {
|
||||
var random float32
|
||||
randomizeSide := func(side *[3]float32) {
|
||||
for i := 0; i < 3; i++ {
|
||||
random += 0.5
|
||||
if random > 32 {
|
||||
random = -32
|
||||
}
|
||||
side[i] = random
|
||||
}
|
||||
}
|
||||
randomizeTile := func(heights *level.WallHeights) {
|
||||
randomizeSide(&heights.North)
|
||||
randomizeSide(&heights.East)
|
||||
randomizeSide(&heights.South)
|
||||
randomizeSide(&heights.West)
|
||||
}
|
||||
|
||||
for _, row := range suite.heightsMap {
|
||||
for i := 0; i < len(row); i++ {
|
||||
heights := &row[i]
|
||||
randomizeTile(heights)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) whenHeightsAreCalculatedFromMap() {
|
||||
suite.heightsMap.CalculateFrom(suite)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) thenHeightsShouldBeForTile(x int, y int, expected float32) {
|
||||
tile := suite.heightsMap.Tile(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
|
||||
verifySide := func(name string, values [3]float32) {
|
||||
for i := 0; i < len(values); i++ {
|
||||
assert.Equal(suite.T(), expected, values[i], fmt.Sprintf("%s[%d] mismatch", name, i))
|
||||
}
|
||||
}
|
||||
verifySide("North", tile.North)
|
||||
verifySide("East", tile.East)
|
||||
verifySide("South", tile.South)
|
||||
verifySide("West", tile.West)
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) thenHeightShouldBeForTileAt(x int, y int, side level.Direction,
|
||||
expectedLeft float32, expectedCenter float32, expectedRight float32) {
|
||||
tile := suite.heightsMap.Tile(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
var values [3]float32
|
||||
|
||||
switch side {
|
||||
case level.DirNorth:
|
||||
values = tile.North
|
||||
case level.DirEast:
|
||||
values = tile.East
|
||||
case level.DirSouth:
|
||||
values = tile.South
|
||||
case level.DirWest:
|
||||
values = tile.West
|
||||
default:
|
||||
require.Fail(suite.T(), "Invalid side specified")
|
||||
}
|
||||
|
||||
assert.Equal(suite.T(), expectedLeft, values[0], "left mismatch")
|
||||
assert.Equal(suite.T(), expectedCenter, values[1], "center mismatch")
|
||||
assert.Equal(suite.T(), expectedRight, values[2], "right mismatch")
|
||||
}
|
||||
|
||||
func (suite *WallHeightsSuite) Tile(pos level.TilePosition) *level.TileMapEntry {
|
||||
return suite.tileMap.Tile(pos, 2)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package level
|
||||
|
||||
import "fmt"
|
||||
|
||||
// WallTexturePattern describes how a wall texture should be applied for a tile.
|
||||
type WallTexturePattern byte
|
||||
|
||||
// String returns the textual information.
|
||||
func (pattern WallTexturePattern) String() string {
|
||||
switch pattern {
|
||||
case WallTexturePatternRegular:
|
||||
return "Regular"
|
||||
case WallTexturePatternFlipHorizontal:
|
||||
return "FlipHorizontal"
|
||||
case WallTexturePatternFlipAlternating:
|
||||
return "FlipAlternating"
|
||||
case WallTexturePatternFlipAlternatingInverted:
|
||||
return "FlipAlternatingInverted"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%02X", int(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
// WallTexturePattern constants are listed below.
|
||||
const (
|
||||
WallTexturePatternRegular WallTexturePattern = 0
|
||||
WallTexturePatternFlipHorizontal WallTexturePattern = 1
|
||||
WallTexturePatternFlipAlternating WallTexturePattern = 2
|
||||
WallTexturePatternFlipAlternatingInverted WallTexturePattern = 3
|
||||
)
|
||||
|
||||
// WallTexturePatterns returns all available texture patterns.
|
||||
func WallTexturePatterns() []WallTexturePattern {
|
||||
return []WallTexturePattern{
|
||||
WallTexturePatternRegular,
|
||||
WallTexturePatternFlipHorizontal,
|
||||
WallTexturePatternFlipAlternating,
|
||||
WallTexturePatternFlipAlternatingInverted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package lvlids
|
||||
|
||||
const (
|
||||
// PerLevel is the amount of resources reserved per level.
|
||||
PerLevel = 100
|
||||
|
||||
// FirstUsed identifies the first level-specific resource in a level-range.
|
||||
FirstUsed = 2
|
||||
// FirstUnused identifies the first unused level-specific resource in a level-range.
|
||||
FirstUnused = 54
|
||||
)
|
||||
|
||||
// Level resource identifier are listed below.
|
||||
const (
|
||||
MapVersionNumber = 2
|
||||
ObjectVersionNumber = 3
|
||||
Information = 4
|
||||
|
||||
TileMap = 5
|
||||
Schedules = 6
|
||||
TextureAtlas = 7
|
||||
ObjectMainTable = 8
|
||||
ObjectCrossRefTable = 9
|
||||
ObjectClassTablesStart = 10
|
||||
ObjectDefaultTablesStart = 25
|
||||
|
||||
SavefileVersion = 40
|
||||
Unused41 = 41
|
||||
|
||||
TextureAnimations = 42
|
||||
SurveillanceSources = 43
|
||||
SurveillanceSurrogates = 44
|
||||
Parameters = 45
|
||||
MapNotes = 46
|
||||
MapNotesPointer = 47
|
||||
|
||||
Unknown48 = 48
|
||||
Unknown49 = 49
|
||||
Unknown50 = 50
|
||||
|
||||
LoopConfiguration = 51
|
||||
|
||||
Unknown52 = 52
|
||||
HeightSemaphores = 53
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseAnimation = interpreters.New()
|
||||
@@ -0,0 +1,122 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseBigStuff = interpreters.New()
|
||||
|
||||
var multiAnimation = interpreters.New().
|
||||
With("LoopType", 0, 2).As(interpreters.Bitfield(map[uint32]string{0x01: "Forward/Backward", 0x02: "Backward"})).
|
||||
With("Alternation", 2, 2).As(interpreters.SpecialValue("MultiAnimation")).
|
||||
With("Picture", 4, 2).As(interpreters.SpecialValue("PictureSource")).
|
||||
With("Alternate", 6, 2).As(interpreters.SpecialValue("PictureSource"))
|
||||
|
||||
var displayScenery = baseBigStuff.
|
||||
With("FrameCount", 0, 2).As(interpreters.RangedValue(0, 8)).
|
||||
Refining("", 2, 8, multiAnimation, interpreters.Always)
|
||||
|
||||
var displayControlPedestal = baseBigStuff.
|
||||
With("FrameCount", 0, 2).As(interpreters.RangedValue(0, 8)).
|
||||
With("TriggerObjectID1", 2, 2).As(interpreters.ObjectID()).
|
||||
With("TriggerObjectID2", 4, 2).As(interpreters.ObjectID()).
|
||||
Refining("", 2, 8, multiAnimation, interpreters.Always)
|
||||
|
||||
var cabinetFurniture = baseBigStuff.
|
||||
With("Object1ID", 2, 2).As(interpreters.ObjectID()).
|
||||
With("Object2ID", 4, 2).As(interpreters.ObjectID())
|
||||
|
||||
var texturableFurniture = baseBigStuff.
|
||||
With("Texture", 6, 2).As(interpreters.SpecialValue("PictureSource"))
|
||||
|
||||
var wordScenery = baseBigStuff.
|
||||
With("TextIndex", 0, 2).As(interpreters.RangedValue(0, 511)).
|
||||
With("Font", 2, 1).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x0F: "Face",
|
||||
0xF0: "Size"})).
|
||||
With("Color", 4, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var textureMapScenery = baseBigStuff.
|
||||
With("TextureIndex", 6, 2).As(interpreters.SpecialValue("LevelTexture"))
|
||||
|
||||
var buttonControlPedestal = baseBigStuff.
|
||||
With("TriggerObjectID1", 2, 2).As(interpreters.ObjectID()).
|
||||
With("TriggerObjectID2", 4, 2).As(interpreters.ObjectID())
|
||||
|
||||
var surgicalMachine = baseBigStuff.
|
||||
With("BrokenState", 2, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "OK", 0xE7: "Broken"})).
|
||||
With("BrokenMessageIndex", 5, 1)
|
||||
|
||||
var securityCamera = baseBigStuff.
|
||||
With("PanningSwitch", 2, 1).As(interpreters.EnumValue(map[uint32]string{0: "Stationary", 1: "Panning"}))
|
||||
|
||||
var solidBridge = baseBigStuff.
|
||||
With("Size", 2, 1).As(interpreters.Bitfield(map[uint32]string{0x0F: "X", 0xF0: "Y"})).
|
||||
With("Height", 3, 1).As(interpreters.SpecialValue("ObjectHeight")).
|
||||
With("TopBottomTexture", 4, 1).As(interpreters.SpecialValue("MaterialOrLevelTexture")).
|
||||
With("SideTexture", 5, 1).As(interpreters.SpecialValue("MaterialOrLevelTexture"))
|
||||
|
||||
var forceBridge = baseBigStuff.
|
||||
With("Size", 2, 1).As(interpreters.Bitfield(map[uint32]string{0x0F: "X", 0xF0: "Y"})).
|
||||
With("Height", 3, 1).As(interpreters.SpecialValue("ObjectHeight")).
|
||||
With("Color", 6, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) (result string) {
|
||||
return forceColors[value]
|
||||
}))
|
||||
|
||||
func initBigStuff() interpreterRetriever {
|
||||
displays := newInterpreterLeaf(displayScenery)
|
||||
textureable := newInterpreterLeaf(texturableFurniture)
|
||||
|
||||
electronics := newInterpreterEntry(baseBigStuff)
|
||||
electronics.set(6, displays)
|
||||
electronics.set(7, displays)
|
||||
|
||||
furniture := newInterpreterEntry(baseBigStuff)
|
||||
furniture.set(2, newInterpreterLeaf(cabinetFurniture))
|
||||
furniture.set(5, textureable)
|
||||
furniture.set(7, textureable)
|
||||
furniture.set(8, textureable)
|
||||
|
||||
surfaces := newInterpreterEntry(baseBigStuff)
|
||||
surfaces.set(3, newInterpreterLeaf(wordScenery))
|
||||
surfaces.set(6, displays)
|
||||
surfaces.set(7, newInterpreterLeaf(textureMapScenery))
|
||||
surfaces.set(8, displays)
|
||||
surfaces.set(9, displays)
|
||||
|
||||
lighting := newInterpreterEntry(baseBigStuff)
|
||||
|
||||
medicalEquipment := newInterpreterEntry(baseBigStuff)
|
||||
medicalEquipment.set(0, newInterpreterLeaf(buttonControlPedestal))
|
||||
medicalEquipment.set(3, newInterpreterLeaf(surgicalMachine))
|
||||
medicalEquipment.set(5, textureable)
|
||||
|
||||
scienceSecurityEquipment := newInterpreterEntry(baseBigStuff)
|
||||
scienceSecurityEquipment.set(4, newInterpreterLeaf(securityCamera))
|
||||
scienceSecurityEquipment.set(5, newInterpreterLeaf(buttonControlPedestal))
|
||||
scienceSecurityEquipment.set(6, newInterpreterLeaf(displayControlPedestal))
|
||||
|
||||
gardenScenery := newInterpreterEntry(baseBigStuff)
|
||||
|
||||
bridges := newInterpreterEntry(baseBigStuff)
|
||||
solidBridges := newInterpreterLeaf(solidBridge)
|
||||
forceBridges := newInterpreterLeaf(forceBridge)
|
||||
bridges.set(0, solidBridges)
|
||||
bridges.set(1, solidBridges)
|
||||
bridges.set(7, forceBridges)
|
||||
bridges.set(8, forceBridges)
|
||||
bridges.set(9, forceBridges)
|
||||
|
||||
class := newInterpreterEntry(baseBigStuff)
|
||||
class.set(0, electronics)
|
||||
class.set(1, furniture)
|
||||
class.set(2, surfaces)
|
||||
class.set(3, lighting)
|
||||
class.set(4, medicalEquipment)
|
||||
class.set(5, scienceSecurityEquipment)
|
||||
class.set(6, gardenScenery)
|
||||
class.set(7, bridges)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseContainer = interpreters.New()
|
||||
|
||||
var standardContainer = baseContainer.
|
||||
With("ObjectID1", 0, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 2, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID4", 6, 2).As(interpreters.ObjectID())
|
||||
|
||||
var crate = standardContainer.
|
||||
With("Width", 8, 1).
|
||||
With("Depth", 9, 1).
|
||||
With("Height", 10, 1).
|
||||
With("TopBottomTexture", 11, 1).
|
||||
With("SideTexture", 12, 1)
|
||||
|
||||
func initContainers() interpreterRetriever {
|
||||
standardContainers := newInterpreterLeaf(standardContainer)
|
||||
crates := newInterpreterLeaf(crate)
|
||||
|
||||
class := newInterpreterEntry(baseContainer)
|
||||
class.set(0, crates)
|
||||
class.set(1, standardContainers)
|
||||
class.set(2, standardContainers)
|
||||
class.set(3, standardContainers)
|
||||
class.set(4, standardContainers)
|
||||
class.set(5, standardContainers)
|
||||
class.set(6, standardContainers)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseCritter = interpreters.New().
|
||||
With("PendingZRotation", 0, 4).
|
||||
With("ForwardVelocityFraction", 4, 2).
|
||||
With("ForwardVelocity", 6, 2).
|
||||
With("Hastiness", 0x0A, 2).
|
||||
With("StateTimeout", 0x0C, 2).As(interpreters.RangedValue(0, 300)).
|
||||
With("Unknown000E", 0x0E, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("Unknown0010", 0x10, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("RoamingState", 0x14, 1).
|
||||
With("PrimaryState", 0x15, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "docile",
|
||||
1: "cautious",
|
||||
2: "hostile",
|
||||
3: "cautious (?)",
|
||||
4: "attacking",
|
||||
5: "sleeping",
|
||||
6: "tranquilized",
|
||||
7: "confused"})).
|
||||
With("SecondaryState", 0x16, 1).
|
||||
With("TertiaryState", 0x17, 1).
|
||||
With("AICoordinates0018", 0x18, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("AICoordinates001A", 0x1A, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("AICoordinates001C", 0x1C, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("Always255", 0x1E, 1).As(interpreters.RangedValue(0, 255)).
|
||||
With("AICoordinates001F", 0x1F, 1).As(interpreters.SpecialValue("Unknown")).
|
||||
With("LootObjectID1", 0x20, 2).As(interpreters.ObjectID()).
|
||||
With("LootObjectID2", 0x22, 2).As(interpreters.ObjectID()).
|
||||
With("Unknown0026", 0x26, 2).As(interpreters.SpecialValue("Unknown"))
|
||||
|
||||
func initCritters() interpreterRetriever {
|
||||
class := newInterpreterEntry(baseCritter)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseDoor = interpreters.New().
|
||||
With("LockVariableIndex", 0, 2).As(interpreters.SpecialValue("LockVariable")).
|
||||
With("LockMessageIndex", 2, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) (result string) {
|
||||
return fmt.Sprintf("%d", value+7)
|
||||
})).
|
||||
With("ForceDoorColor", 3, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) (result string) {
|
||||
return forceColors[value]
|
||||
})).
|
||||
With("RequiredAccessLevel", 4, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) (result string) {
|
||||
if value == 255 {
|
||||
result = "SHODAN"
|
||||
} else if accessLevel, known := accessLevelMasks[1<<uint32(value)]; known {
|
||||
result = accessLevel
|
||||
} else {
|
||||
result = "Unknown"
|
||||
}
|
||||
return
|
||||
})).
|
||||
With("AutoCloseTime", 5, 1).As(interpreters.FormattedRangedValue(0, 255,
|
||||
func(value int) string {
|
||||
return fmt.Sprintf("%.2f sec", float64(value)*0.5)
|
||||
})).
|
||||
With("OtherObjectID", 6, 2).As(interpreters.ObjectID())
|
||||
|
||||
func initDoors() interpreterRetriever {
|
||||
return newInterpreterLeaf(baseDoor)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj/actions"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj/conditions"
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseFixture = interpreters.New()
|
||||
|
||||
var gameVariablePanel = baseFixture.
|
||||
Refining("Condition", 2, 4, conditions.GameVariable(), interpreters.Always)
|
||||
|
||||
var buttonPanel = gameVariablePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always).
|
||||
With("AccessMask", 22, 2)
|
||||
|
||||
var recepticlePanel = baseFixture
|
||||
|
||||
var standardRecepticle = recepticlePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always).
|
||||
Refining("TypeCondition", 2, 3, conditions.ObjectType(), interpreters.Always).
|
||||
With("FailMessage", 5, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var antennaRelayPanel = recepticlePanel.
|
||||
With("TriggerObjectID1", 6, 2).As(interpreters.ObjectID()).
|
||||
With("TriggerObjectID2", 10, 2).As(interpreters.ObjectID()).
|
||||
With("DestroyObjectID", 14, 2).As(interpreters.ObjectID())
|
||||
|
||||
var retinalIDScanner = recepticlePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always).
|
||||
With("Head", 2, 3).As(interpreters.FormattedRangedValue(0, 21,
|
||||
func(value int) (result string) {
|
||||
if value < 11 {
|
||||
return fmt.Sprintf("(8/2/13 frame: %d)", value)
|
||||
} else {
|
||||
return fmt.Sprintf("(8/2/14 frame: %d)", value-11)
|
||||
}
|
||||
}))
|
||||
|
||||
var cyberspaceTerminal = gameVariablePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always)
|
||||
|
||||
var energyChargeStation = gameVariablePanel.
|
||||
With("EnergyDelta", 6, 4).As(interpreters.RangedValue(0, 255)).
|
||||
With("RechargeTime", 10, 4).As(interpreters.FormattedRangedValue(0, 3600,
|
||||
func(value int) string {
|
||||
return fmt.Sprintf("%d sec", value)
|
||||
})).
|
||||
With("TriggerObjectID", 14, 4).As(interpreters.ObjectID()).
|
||||
With("RechargedTimestamp", 18, 4)
|
||||
|
||||
var vendingMachine = baseFixture.
|
||||
Refining("TypeCondition", 2, 3, conditions.ObjectType(), interpreters.Always).
|
||||
With("FailMessage", 5, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var inputPanel = gameVariablePanel
|
||||
|
||||
var wirePuzzleStateDescription = interpreters.Bitfield(map[uint32]string{
|
||||
0x00000007: "Wire 1 Left",
|
||||
0x00000038: "Wire 1 Right",
|
||||
0x000001C0: "Wire 2 Left",
|
||||
0x00000E00: "Wire 2 Right",
|
||||
|
||||
0x00007000: "Wire 3 Left",
|
||||
0x00038000: "Wire 3 Right",
|
||||
0x001C0000: "Wire 4 Left",
|
||||
0x00E00000: "Wire 4 Right",
|
||||
|
||||
0x07000000: "Wire 5 Left",
|
||||
0x38000000: "Wire 5 Right"})
|
||||
|
||||
var wirePuzzleData = interpreters.New().
|
||||
With("TargetObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("Layout", 4, 1).As(interpreters.Bitfield(map[uint32]string{0x0F: "Wires", 0xF0: "Connectors"})).
|
||||
With("TargetPowerLevel", 5, 1).
|
||||
With("WireProperties", 6, 1).As(interpreters.Bitfield(map[uint32]string{0x01: "Colored", 0xF0: "Solved"})).
|
||||
With("TargetState", 8, 4).As(wirePuzzleStateDescription).
|
||||
With("CurrentState", 12, 4).As(wirePuzzleStateDescription)
|
||||
|
||||
var blockPuzzleData = interpreters.New().
|
||||
With("TargetObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("StateStoreObjectID", 4, 2).As(interpreters.ObjectID()).
|
||||
With("Layout", 8, 4).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x00000001: "PuzzleSolved",
|
||||
0x00000070: "SourceCoordinate",
|
||||
0x00000180: "SourceLocation",
|
||||
0x00007000: "DestCoordinate",
|
||||
0x00018000: "DestLocation",
|
||||
0x00700000: "Width",
|
||||
0x07000000: "Height",
|
||||
0x70000000: "SideEffectType"}))
|
||||
|
||||
var puzzleSpecificData = interpreters.New().
|
||||
With("Type", 7, 1).As(interpreters.EnumValue(map[uint32]string{0: "WirePuzzle", 0x10: "BlockPuzzle"})).
|
||||
Refining("Wire", 0, 18, wirePuzzleData, func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("Type") == 0
|
||||
}).
|
||||
Refining("Block", 0, 18, blockPuzzleData, func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("Type") == 0x10
|
||||
})
|
||||
|
||||
var puzzlePanel = inputPanel.
|
||||
Refining("Puzzle", 6, 18, puzzleSpecificData, interpreters.Always)
|
||||
|
||||
var elevatorPanel = inputPanel.
|
||||
With("DestinationObjectIndex2", 6, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex1", 8, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex4", 10, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex3", 12, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex6", 14, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("DestinationObjectIndex5", 16, 2).As(interpreters.RangedValue(0, 871)).
|
||||
With("AccessibleBitmask", 18, 2).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x0001: "Level 0",
|
||||
0x0002: "Level 1",
|
||||
0x0004: "Level 2",
|
||||
0x0008: "Level 3",
|
||||
0x0010: "Level 4",
|
||||
0x0020: "Level 5",
|
||||
0x0040: "Level 6",
|
||||
0x0080: "Level 7",
|
||||
0x0100: "Level 8",
|
||||
0x0200: "Level 9",
|
||||
0x0400: "Level 10",
|
||||
0x0800: "Level 11",
|
||||
0x1000: "Level 12",
|
||||
0x2000: "Level 13",
|
||||
0x4000: "Level 14",
|
||||
0x8000: "Level 15"})).
|
||||
With("ElevatorShaftBitmask", 20, 2).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x0001: "Level 0",
|
||||
0x0002: "Level 1",
|
||||
0x0004: "Level 2",
|
||||
0x0008: "Level 3",
|
||||
0x0010: "Level 4",
|
||||
0x0020: "Level 5",
|
||||
0x0040: "Level 6",
|
||||
0x0080: "Level 7",
|
||||
0x0100: "Level 8",
|
||||
0x0200: "Level 9",
|
||||
0x0400: "Level 10",
|
||||
0x0800: "Level 11",
|
||||
0x1000: "Level 12",
|
||||
0x2000: "Level 13",
|
||||
0x4000: "Level 14",
|
||||
0x8000: "Level 15"}))
|
||||
|
||||
var numberPad = inputPanel.
|
||||
With("Combination1", 6, 2).As(interpreters.SpecialValue("BinaryCodedDecimal")).
|
||||
With("TriggerObjectID1", 8, 2).As(interpreters.ObjectID()).
|
||||
With("Combination2", 10, 2).As(interpreters.SpecialValue("BinaryCodedDecimal")).
|
||||
With("TriggerObjectID2", 12, 2).As(interpreters.ObjectID()).
|
||||
With("Combination3", 14, 2).As(interpreters.SpecialValue("BinaryCodedDecimal")).
|
||||
With("TriggerObjectID3", 16, 2).As(interpreters.ObjectID()).
|
||||
With("FailObjectID", 18, 2).As(interpreters.ObjectID())
|
||||
|
||||
var inactiveCyberspaceSwitch = gameVariablePanel.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always)
|
||||
|
||||
func initFixtures() interpreterRetriever {
|
||||
standardRecepticles := newInterpreterLeaf(standardRecepticle)
|
||||
antennaRelays := newInterpreterLeaf(antennaRelayPanel)
|
||||
recepticles := newInterpreterEntry(recepticlePanel)
|
||||
recepticles.set(0, standardRecepticles)
|
||||
recepticles.set(1, standardRecepticles)
|
||||
recepticles.set(2, standardRecepticles)
|
||||
recepticles.set(3, antennaRelays) // standard panel
|
||||
recepticles.set(4, antennaRelays) // plastiqued
|
||||
recepticles.set(6, newInterpreterLeaf(retinalIDScanner))
|
||||
|
||||
stations := newInterpreterEntry(baseFixture)
|
||||
stations.set(0, newInterpreterLeaf(cyberspaceTerminal))
|
||||
stations.set(1, newInterpreterLeaf(energyChargeStation))
|
||||
|
||||
vendingMachines := newInterpreterEntry(vendingMachine)
|
||||
|
||||
puzzles := newInterpreterLeaf(puzzlePanel)
|
||||
elevatorPanels := newInterpreterLeaf(elevatorPanel)
|
||||
numberPads := newInterpreterLeaf(numberPad)
|
||||
inputPanels := newInterpreterEntry(inputPanel)
|
||||
inputPanels.set(0, puzzles)
|
||||
inputPanels.set(1, puzzles)
|
||||
inputPanels.set(2, puzzles)
|
||||
inputPanels.set(3, puzzles)
|
||||
inputPanels.set(4, elevatorPanels)
|
||||
inputPanels.set(5, elevatorPanels)
|
||||
inputPanels.set(6, elevatorPanels)
|
||||
inputPanels.set(7, numberPads)
|
||||
inputPanels.set(8, numberPads)
|
||||
inputPanels.set(9, puzzles)
|
||||
inputPanels.set(10, puzzles)
|
||||
|
||||
cyberspaceSwitches := newInterpreterEntry(baseFixture)
|
||||
cyberspaceSwitches.set(0, newInterpreterLeaf(inactiveCyberspaceSwitch))
|
||||
|
||||
class := newInterpreterEntry(baseFixture)
|
||||
class.set(0, newInterpreterLeaf(buttonPanel))
|
||||
class.set(1, recepticles)
|
||||
class.set(2, stations)
|
||||
class.set(3, inputPanels)
|
||||
class.set(4, vendingMachines)
|
||||
class.set(5, cyberspaceSwitches)
|
||||
|
||||
return class
|
||||
}
|
||||
|
||||
func initCyberspaceFixtures() interpreterRetriever {
|
||||
cyberspaceSwitches := newInterpreterEntry(baseFixture)
|
||||
cyberspaceSwitches.set(0, newInterpreterLeaf(inactiveCyberspaceSwitch))
|
||||
|
||||
class := newInterpreterEntry(baseFixture)
|
||||
class.set(5, cyberspaceSwitches)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
// ForCyberspace returns an interpreter instance that handles the level class
|
||||
// data of the specified object - in cyberspace.
|
||||
func ForCyberspace(triple object.Triple, data []byte) *interpreters.Instance {
|
||||
return cyberspaceEntries.specialize(int(triple.Class)).specialize(int(triple.Subclass)).specialize(int(triple.Type)).instance(data)
|
||||
}
|
||||
|
||||
// CyberspaceExtra returns an interpreter instance that handles the level object extra
|
||||
// data of the specified object - in cybperspace.
|
||||
func CyberspaceExtra(triple object.Triple, data []byte) *interpreters.Instance {
|
||||
return cyberspaceExtras.specialize(int(triple.Class)).specialize(int(triple.Subclass)).specialize(int(triple.Type)).instance(data)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
// ForRealWorld returns an interpreter instance that handles the level class
|
||||
// data of the specified object - in real world.
|
||||
func ForRealWorld(triple object.Triple, data []byte) *interpreters.Instance {
|
||||
return realWorldEntries.specialize(int(triple.Class)).specialize(int(triple.Subclass)).specialize(int(triple.Type)).instance(data)
|
||||
}
|
||||
|
||||
// RealWorldExtra returns an interpreter instance that handles the level object extra
|
||||
// data of the specified object - in real world.
|
||||
func RealWorldExtra(triple object.Triple, data []byte) *interpreters.Instance {
|
||||
return realWorldExtras.specialize(int(triple.Class)).specialize(int(triple.Subclass)).specialize(int(triple.Type)).instance(data)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package lvlobj
|
||||
|
||||
var forceColors = map[int]string{
|
||||
0: "Default (red)",
|
||||
|
||||
247: "Gray, medium",
|
||||
248: "Gray, light",
|
||||
|
||||
249: "Red, light",
|
||||
250: "Green, light",
|
||||
251: "Blue, light",
|
||||
|
||||
252: "Invisible",
|
||||
|
||||
253: "Blue, medium",
|
||||
254: "Green, medium",
|
||||
255: "Red, medium"}
|
||||
@@ -0,0 +1,21 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseGrenade = interpreters.New().
|
||||
With("Unknown0000", 0, 2).
|
||||
With("State", 2, 2).As(interpreters.EnumValue(map[uint32]string{0: "Inert", 1: "Thrown Live", 5: "Landed Live"})).
|
||||
With("TimerTime", 4, 2).As(interpreters.RangedValue(0, 32767))
|
||||
|
||||
func initGrenades() interpreterRetriever {
|
||||
timedExplosives := newInterpreterEntry(baseGrenade)
|
||||
|
||||
timedExplosives.set(2, newInterpreterLeaf(interpreters.New())) // Object explosion - not encountered
|
||||
|
||||
class := newInterpreterEntry(baseGrenade)
|
||||
class.set(1, timedExplosives)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnergyWeaponMaxTemperature is the maximum value possible for heat.
|
||||
EnergyWeaponMaxTemperature = 100
|
||||
// EnergyWeaponMaxEnergy is the maximum value for energy.
|
||||
// The original source does not explicitly state this value, it has been determined by looking at
|
||||
// a savegame with the energy set to max. A closely related number in the source is
|
||||
// in the macro "mfd_charge_units_per_pixel", which scales to 69.
|
||||
EnergyWeaponMaxEnergy = 59
|
||||
)
|
||||
|
||||
var baseGun = interpreters.New()
|
||||
|
||||
var energyWeapon = baseGun.
|
||||
With("Charge", 0, 1).As(interpreters.Bitfield(
|
||||
map[uint32]string{
|
||||
0x80: "Overload",
|
||||
0x7F: "Energy",
|
||||
})).
|
||||
With("Temperature", 1, 1).As(interpreters.RangedValue(0, EnergyWeaponMaxTemperature))
|
||||
|
||||
var projectileWeapon = baseGun.
|
||||
With("AmmoType", 0, 1).As(interpreters.EnumValue(map[uint32]string{0: "Standard", 1: "Special"})).
|
||||
With("AmmoCount", 1, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
func initGuns() interpreterRetriever {
|
||||
projectileWeapons := newInterpreterLeaf(projectileWeapon)
|
||||
energyWeapons := newInterpreterLeaf(energyWeapon)
|
||||
|
||||
class := newInterpreterEntry(baseGun)
|
||||
class.set(0, projectileWeapons)
|
||||
class.set(1, projectileWeapons)
|
||||
class.set(2, projectileWeapons)
|
||||
|
||||
class.set(4, energyWeapons)
|
||||
class.set(5, energyWeapons)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseHardware = interpreters.New().
|
||||
With("Version", 0, 1).As(interpreters.RangedValue(0, 4))
|
||||
@@ -0,0 +1,9 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
// InterpreterFactory returns an interpreter instance according to a specific object triple.
|
||||
type InterpreterFactory func(object.Triple, []byte) *interpreters.Instance
|
||||
@@ -0,0 +1,53 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
type interpreterRetriever interface {
|
||||
specialize(key int) interpreterRetriever
|
||||
instance(data []byte) *interpreters.Instance
|
||||
}
|
||||
|
||||
type interpreterLeaf struct {
|
||||
desc *interpreters.Description
|
||||
}
|
||||
|
||||
func newInterpreterLeaf(desc *interpreters.Description) *interpreterLeaf {
|
||||
return &interpreterLeaf{desc: desc}
|
||||
}
|
||||
|
||||
func (node *interpreterLeaf) specialize(key int) interpreterRetriever {
|
||||
return node
|
||||
}
|
||||
|
||||
func (node *interpreterLeaf) instance(data []byte) *interpreters.Instance {
|
||||
return node.desc.For(data)
|
||||
}
|
||||
|
||||
type interpreterEntry struct {
|
||||
defaultLeaf *interpreterLeaf
|
||||
subEntries map[int]interpreterRetriever
|
||||
}
|
||||
|
||||
func newInterpreterEntry(defaultDesc *interpreters.Description) *interpreterEntry {
|
||||
return &interpreterEntry{
|
||||
defaultLeaf: newInterpreterLeaf(defaultDesc),
|
||||
subEntries: make(map[int]interpreterRetriever)}
|
||||
}
|
||||
|
||||
func (node *interpreterEntry) set(key int, sub interpreterRetriever) {
|
||||
node.subEntries[key] = sub
|
||||
}
|
||||
|
||||
func (node *interpreterEntry) specialize(key int) (retriever interpreterRetriever) {
|
||||
retriever = node.subEntries[key]
|
||||
if retriever == nil {
|
||||
retriever = node.defaultLeaf
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (node *interpreterEntry) instance(data []byte) *interpreters.Instance {
|
||||
return node.defaultLeaf.instance(data)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var basePhysics = interpreters.New()
|
||||
@@ -0,0 +1,128 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseItem = interpreters.New()
|
||||
|
||||
var paperItem = baseItem.
|
||||
With("PaperId", 2, 1)
|
||||
|
||||
var briefcaseItem = baseItem.
|
||||
With("ObjectID1", 2, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 6, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID4", 8, 2).As(interpreters.ObjectID())
|
||||
|
||||
var corpseItem = baseItem.
|
||||
With("Unknown0000", 0, 2).As(interpreters.SpecialValue("Unknown")).
|
||||
With("ObjectID1", 2, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 6, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID4", 8, 2).As(interpreters.ObjectID())
|
||||
|
||||
var severedHeadItem = baseItem.
|
||||
With("ImageIndex", 2, 1)
|
||||
|
||||
var accessLevelMasks = map[uint32]string{
|
||||
0x00000001: "None",
|
||||
0x00000002: "Generic1",
|
||||
0x00000004: "Generic2",
|
||||
0x00000008: "Generic3",
|
||||
0x00000010: "Generic4",
|
||||
0x00000020: "Generic5",
|
||||
0x00000040: "Generic6",
|
||||
0x00000080: "Generic7",
|
||||
|
||||
0x00000100: "Group1",
|
||||
0x00000200: "Group2",
|
||||
0x00000400: "Group3",
|
||||
0x00000800: "Group4",
|
||||
0x00001000: "Group5",
|
||||
0x00002000: "Group6",
|
||||
0x00004000: "Group7",
|
||||
0x00008000: "Group8",
|
||||
|
||||
0x00010000: "Group9",
|
||||
0x00020000: "Group10",
|
||||
0x00040000: "Group11",
|
||||
0x00080000: "Group12",
|
||||
0x00100000: "Group13",
|
||||
0x00200000: "Group14",
|
||||
0x00400000: "Group15",
|
||||
0x00800000: "Group16",
|
||||
|
||||
0x01000000: "Personal1",
|
||||
0x02000000: "Personal2",
|
||||
0x04000000: "Personal3",
|
||||
0x08000000: "Personal4",
|
||||
0x10000000: "Personal5",
|
||||
0x20000000: "Personal6",
|
||||
0x40000000: "Personal7"}
|
||||
|
||||
var accessMaskDescription = interpreters.Bitfield(accessLevelMasks)
|
||||
|
||||
var accessCardItem = baseItem.
|
||||
With("Ignored0000", 0, 2).As(interpreters.SpecialValue("Ignored")).
|
||||
With("AccessMask", 2, 4).As(accessMaskDescription)
|
||||
|
||||
var securityIDModuleItem = baseItem.
|
||||
With("AccessMask", 2, 4).As(accessMaskDescription)
|
||||
|
||||
var cyberInfoNodeItem = baseItem.
|
||||
With("TextIndex", 2, 1)
|
||||
|
||||
var cyberRestorative = baseItem.
|
||||
With("RestorationAmount", 2, 1)
|
||||
|
||||
var cyberDefenseMine = baseItem.
|
||||
With("DamageAmount", 2, 1)
|
||||
|
||||
var cyberBarricade = baseItem.
|
||||
With("Size", 2, 1).
|
||||
With("Height", 3, 1).
|
||||
With("Color", 6, 1)
|
||||
|
||||
func initSmallStuff() interpreterRetriever {
|
||||
junk := newInterpreterEntry(baseItem)
|
||||
junk.set(2, newInterpreterLeaf(paperItem))
|
||||
junk.set(7, newInterpreterLeaf(briefcaseItem))
|
||||
|
||||
dead := newInterpreterEntry(baseItem)
|
||||
corpses := newInterpreterLeaf(corpseItem)
|
||||
severedHeads := newInterpreterLeaf(severedHeadItem)
|
||||
dead.set(0, corpses)
|
||||
dead.set(1, corpses)
|
||||
dead.set(2, corpses)
|
||||
dead.set(3, corpses)
|
||||
dead.set(4, corpses)
|
||||
dead.set(5, corpses)
|
||||
dead.set(6, corpses)
|
||||
dead.set(7, corpses)
|
||||
dead.set(13, severedHeads)
|
||||
dead.set(14, severedHeads)
|
||||
|
||||
class := newInterpreterEntry(baseItem)
|
||||
class.set(0, junk)
|
||||
class.set(2, dead)
|
||||
class.set(4, newInterpreterLeaf(accessCardItem))
|
||||
|
||||
return class
|
||||
}
|
||||
|
||||
func initCyberspaceSmallStuff() interpreterRetriever {
|
||||
cyberspaceItems := newInterpreterEntry(baseItem)
|
||||
infoNodes := newInterpreterLeaf(cyberInfoNodeItem)
|
||||
cyberspaceItems.set(1, newInterpreterLeaf(cyberRestorative))
|
||||
cyberspaceItems.set(2, newInterpreterLeaf(cyberDefenseMine))
|
||||
cyberspaceItems.set(3, newInterpreterLeaf(securityIDModuleItem))
|
||||
cyberspaceItems.set(6, infoNodes)
|
||||
cyberspaceItems.set(8, infoNodes)
|
||||
cyberspaceItems.set(9, newInterpreterLeaf(cyberBarricade))
|
||||
|
||||
class := newInterpreterEntry(baseItem)
|
||||
class.set(5, cyberspaceItems)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseSoftware = interpreters.New()
|
||||
|
||||
var multimediaFile = baseSoftware.
|
||||
With("ID", 1, 1).As(interpreters.Bitfield(map[uint32]string{0x0F: "Index", 0xF0: "Level"})).
|
||||
With("Type", 2, 1).As(interpreters.EnumValue(map[uint32]string{0: "E-Mail", 1: "Log", 2: "Data"}))
|
||||
|
||||
var cyberspaceProgram = baseSoftware.
|
||||
With("Version", 0, 1).As(interpreters.RangedValue(1, 9))
|
||||
|
||||
var funPack = baseSoftware.
|
||||
With("GameMask", 0, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0x01: "Ping",
|
||||
0x02: "Eel Zapper",
|
||||
0x04: "Road",
|
||||
0x08: "Botbounce",
|
||||
0x10: "15",
|
||||
0x20: "TriopToe",
|
||||
0x80: "Wing 0"}))
|
||||
|
||||
var baseCyberspaceScenery = interpreters.New()
|
||||
|
||||
var scenerySoftware = baseCyberspaceScenery.
|
||||
Refining("FunPack", 0, 2, funPack,
|
||||
func(inst *interpreters.Instance) bool {
|
||||
return (inst.Get("Subclass") == 3) && (inst.Get("Type") == 0)
|
||||
}).
|
||||
Refining("Program", 0, 2, cyberspaceProgram,
|
||||
func(inst *interpreters.Instance) bool {
|
||||
return (inst.Get("Subclass") == 0) || (inst.Get("Subclass") == 1)
|
||||
}).
|
||||
With("Subclass", 2, 4).As(interpreters.RangedValue(0, 7)).
|
||||
With("Type", 6, 4).As(interpreters.RangedValue(0, 16))
|
||||
|
||||
func initSoftware() interpreterRetriever {
|
||||
cyberspacePrograms := newInterpreterLeaf(cyberspaceProgram)
|
||||
|
||||
realWorldTools := newInterpreterEntry(baseSoftware)
|
||||
realWorldTools.set(0, newInterpreterLeaf(funPack))
|
||||
|
||||
multimediaSoftware := newInterpreterEntry(baseSoftware)
|
||||
multimediaSoftware.set(0, newInterpreterLeaf(multimediaFile)) // text
|
||||
multimediaSoftware.set(1, newInterpreterLeaf(multimediaFile)) // email
|
||||
|
||||
class := newInterpreterEntry(baseSoftware)
|
||||
class.set(0, cyberspacePrograms) // aggressive programs
|
||||
class.set(1, cyberspacePrograms) // defensive programs
|
||||
class.set(3, realWorldTools)
|
||||
class.set(4, multimediaSoftware)
|
||||
|
||||
return class
|
||||
}
|
||||
|
||||
func initCyberspaceBigStuff() interpreterRetriever {
|
||||
cyberspaceScenery := newInterpreterEntry(scenerySoftware)
|
||||
|
||||
cyberspaceScenerySubclass2 := newInterpreterEntry(baseCyberspaceScenery)
|
||||
cyberspaceScenerySubclass2.set(0, cyberspaceScenery) // "SIGN"
|
||||
cyberspaceScenerySubclass3 := newInterpreterEntry(baseCyberspaceScenery)
|
||||
cyberspaceScenerySubclass3.set(2, cyberspaceScenery) // "GLOW BULB"
|
||||
|
||||
class := newInterpreterEntry(scenerySoftware)
|
||||
|
||||
class.set(2, cyberspaceScenerySubclass2)
|
||||
class.set(3, cyberspaceScenerySubclass3)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj/actions"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj/conditions"
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var baseTraps = interpreters.New()
|
||||
|
||||
var repulsor = baseTraps.
|
||||
With("Ignored0000", 0, 1).As(interpreters.SpecialValue("Ignored")).
|
||||
With("StartHeightFraction", 10, 2).As(interpreters.RangedValue(0, 65536)).
|
||||
With("StartHeight", 12, 2).As(interpreters.RangedValue(0, 31)).
|
||||
With("EndHeightFraction", 14, 2).As(interpreters.RangedValue(0, 65536)).
|
||||
With("EndHeight", 16, 2).As(interpreters.RangedValue(0, 32)).
|
||||
With("Flags", 18, 4).As(interpreters.Bitfield(map[uint32]string{0x00000001: "Disabled", 0x00000008: "Strong"}))
|
||||
|
||||
var aiHint = baseTraps.
|
||||
With("Ignored0000", 0, 1).As(interpreters.SpecialValue("Ignored")).
|
||||
With("NextObjectID", 6, 2).As(interpreters.ObjectID()).
|
||||
With("TriggerObjectFlag", 18, 2).As(interpreters.EnumValue(map[uint32]string{0: "Off", 1: "On"})).
|
||||
With("TriggerObjectID", 20, 2).As(interpreters.ObjectID())
|
||||
|
||||
var baseTrigger = baseTraps.
|
||||
Refining("Action", 0, 22, actions.Unconditional(), interpreters.Always)
|
||||
|
||||
var gameVariableTrigger = baseTrigger.
|
||||
Refining("Condition", 2, 4, conditions.GameVariable(), interpreters.Always)
|
||||
|
||||
var puzzleData = interpreters.New()
|
||||
|
||||
var nullTrigger = baseTraps.
|
||||
Refining("Action", 0, 22, actions.Unconditional().
|
||||
Refining("PuzzleData", 6, 16, puzzleData, func(inst *interpreters.Instance) bool { return inst.Get("Type") == 0 }),
|
||||
interpreters.Always).
|
||||
Refining("Condition", 2, 4, conditions.GameVariable(), interpreters.Always)
|
||||
|
||||
var deathWatchTrigger = baseTrigger.
|
||||
With("ConditionType", 5, 1).As(interpreters.EnumValue(map[uint32]string{0: "Object Type", 1: "Object ID"})).
|
||||
Refining("TypeCondition", 2, 4, conditions.ObjectType(), func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("ConditionType") == 0
|
||||
}).
|
||||
Refining("IndexCondition", 2, 4, conditions.ObjectID(), func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("ConditionType") == 1
|
||||
})
|
||||
|
||||
var ecologyTrigger = baseTrigger.
|
||||
Refining("TypeCondition", 2, 4, conditions.ObjectType(), interpreters.Always).
|
||||
With("ConditionLimit", 5, 1)
|
||||
|
||||
var mapNote = baseTraps.
|
||||
With("EntryOffset", 18, 4)
|
||||
|
||||
var musicVoodoo = baseTraps.
|
||||
With("MusicFlavour", 6, 1).As(interpreters.RangedValue(0, 4))
|
||||
|
||||
func initTraps() interpreterRetriever {
|
||||
gameVariableTriggers := newInterpreterLeaf(gameVariableTrigger)
|
||||
|
||||
trigger := newInterpreterEntry(baseTraps)
|
||||
trigger.set(0, gameVariableTriggers) // tile entry trigger
|
||||
trigger.set(1, newInterpreterLeaf(nullTrigger))
|
||||
trigger.set(2, gameVariableTriggers) // floor trigger
|
||||
trigger.set(3, gameVariableTriggers) // player death trigger
|
||||
trigger.set(4, newInterpreterLeaf(deathWatchTrigger))
|
||||
trigger.set(7, newInterpreterLeaf(aiHint))
|
||||
trigger.set(8, gameVariableTriggers) // level entry trigger
|
||||
trigger.set(10, newInterpreterLeaf(repulsor))
|
||||
trigger.set(11, newInterpreterLeaf(ecologyTrigger))
|
||||
trigger.set(12, gameVariableTriggers) // shodan trigger
|
||||
|
||||
mapMarker := newInterpreterEntry(baseTraps)
|
||||
mapMarker.set(3, newInterpreterLeaf(mapNote))
|
||||
mapMarker.set(4, newInterpreterLeaf(musicVoodoo))
|
||||
|
||||
class := newInterpreterEntry(baseTraps)
|
||||
class.set(0, trigger)
|
||||
class.set(2, mapMarker)
|
||||
|
||||
return class
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
func forType(typeID int) func(*interpreters.Instance) bool {
|
||||
return func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("Type") == uint32(typeID)
|
||||
}
|
||||
}
|
||||
|
||||
var transportHackerDetails = interpreters.New().
|
||||
With("TargetX", 0, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetY", 4, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetZ", 8, 1).As(interpreters.RangedValue(0, 255)).
|
||||
With("PreserveHeight", 9, 1).As(interpreters.EnumValue(map[uint32]string{0: "No", 0x40: "Yes"})).
|
||||
With("CrossLevelTransportDestination", 12, 1).As(interpreters.RangedValue(0, 15)).
|
||||
With("CrossLevelTransportFlag", 13, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0x00: "Cross-Level", 0x10: "Same-Level (0x10)", 0x20: "Same-Level (0x20)", 0x22: "Same-Level (0x22)"}))
|
||||
|
||||
var changeHealthDetails = interpreters.New().
|
||||
With("HealthDelta", 4, 1).
|
||||
With("HealthChangeFlag", 6, 2).As(interpreters.EnumValue(map[uint32]string{0: "Remove Delta", 1: "Add Delta"})).
|
||||
With("PowerDelta", 8, 1).
|
||||
With("PowerChangeFlag", 10, 2).As(interpreters.EnumValue(map[uint32]string{0: "Remove Delta", 1: "Add Delta"})).
|
||||
With("FatigueDelta", 12, 2).As(interpreters.RangedValue(0, 255)).
|
||||
With("FatigueChangeFlag", 14, 2).As(interpreters.EnumValue(map[uint32]string{0: "Add Delta", 1: "Remove Delta"}))
|
||||
|
||||
var cloneMoveObjectDetails = interpreters.New().
|
||||
With("ObjectID", 0, 2).As(interpreters.ObjectID()).
|
||||
With("MoveFlag", 2, 2).As(interpreters.EnumValue(map[uint32]string{
|
||||
0x0000: "Clone Object",
|
||||
0x0001: "Move Object (0x0001)",
|
||||
0x0002: "Move Object (0x0002)",
|
||||
0x0FFF: "Move Object (0x0FFF)",
|
||||
0xAAAA: "Move Object (0xAAAA)",
|
||||
0xFFFF: "Move Object (0xFFFF)"})).
|
||||
With("TargetX", 4, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetY", 8, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetHeight", 12, 1).As(interpreters.SpecialValue("ObjectHeight")).
|
||||
With("KeepSourceHeight", 13, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "Set height", 0x40: "Keep height"}))
|
||||
|
||||
var setGameVariableDetails = interpreters.New().
|
||||
With("VariableKey", 0, 4).As(interpreters.SpecialValue("VariableKey")).
|
||||
With("Value", 4, 2).
|
||||
With("Operation", 6, 2).As(interpreters.EnumValue(map[uint32]string{0: "Set", 1: "Add", 2: "Subtract", 3: "Multiply", 4: "Divide", 5: "Modulo"})).
|
||||
With("Message1", 8, 4).As(interpreters.RangedValue(0, 511)).
|
||||
With("Message2", 12, 4).As(interpreters.RangedValue(0, 511))
|
||||
|
||||
var showCutsceneDetails = interpreters.New().
|
||||
With("CutsceneIndex", 0, 4).As(interpreters.EnumValue(map[uint32]string{0: "Death", 1: "Intro", 2: "Ending"})).
|
||||
With("EndGameFlag", 4, 4).As(interpreters.EnumValue(map[uint32]string{0: "No (not working)", 1: "Yes"}))
|
||||
|
||||
func pointOneSecond(value int) string {
|
||||
return fmt.Sprintf("%.1f sec", float64(value)*0.1)
|
||||
}
|
||||
|
||||
var triggerOtherObjectsDetails = interpreters.New().
|
||||
With("Object1ID", 0, 2).As(interpreters.ObjectID()).
|
||||
With("Object1Delay", 2, 2).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond)).
|
||||
With("Object2ID", 4, 2).As(interpreters.ObjectID()).
|
||||
With("Object2Delay", 6, 2).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond)).
|
||||
With("Object3ID", 8, 2).As(interpreters.ObjectID()).
|
||||
With("Object3Delay", 10, 2).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond)).
|
||||
With("Object4ID", 12, 2).As(interpreters.ObjectID()).
|
||||
With("Object4Delay", 14, 2).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond))
|
||||
|
||||
var changeLightingDetails = interpreters.New().
|
||||
Refining("ObjectExtent", 0, 2, interpreters.New().With("Index", 0, 2).As(interpreters.ObjectID()), func(inst *interpreters.Instance) bool {
|
||||
var lightType = inst.Get("LightType")
|
||||
|
||||
return (lightType == 0x00) || (lightType == 0x01)
|
||||
}).
|
||||
Refining("RadiusExtent", 0, 2, interpreters.New().With("Tiles", 0, 2).As(interpreters.RangedValue(0, 31)), func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("LightType") == 0x03
|
||||
}).
|
||||
With("ReferenceObjectID", 2, 2).As(interpreters.ObjectID()).
|
||||
With("TransitionType", 4, 2).As(interpreters.EnumValue(map[uint32]string{0x0000: "immediate", 0x0001: "fade", 0x0100: "flicker"})).
|
||||
With("LightModification", 7, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "light on", 0x10: "light off"})).
|
||||
With("LightType", 8, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "rectangular", 0x03: "circular gradient"})).
|
||||
With("LightSurface", 10, 2).As(interpreters.EnumValue(map[uint32]string{0: "floor", 1: "ceiling", 2: "floor and ceiling"})).
|
||||
Refining("Rectangular", 12, 2, interpreters.New().
|
||||
With("Off light value", 0, 1).As(interpreters.RangedValue(0, 15)).
|
||||
With("On light value", 1, 1).As(interpreters.RangedValue(0, 15)), func(inst *interpreters.Instance) bool {
|
||||
return inst.Get("LightType") == 0x00
|
||||
}).
|
||||
Refining("Gradient", 12, 4, interpreters.New().
|
||||
With("Off light begin intensity", 0, 1).As(interpreters.RangedValue(0, 127)).
|
||||
With("Off light end intensity", 1, 1).As(interpreters.RangedValue(0, 127)).
|
||||
With("On light begin intensity", 2, 1).As(interpreters.RangedValue(0, 127)).
|
||||
With("On light end intensity", 3, 1).As(interpreters.RangedValue(0, 127)), func(inst *interpreters.Instance) bool {
|
||||
var lightType = inst.Get("LightType")
|
||||
|
||||
return (lightType == 0x01) || (lightType == 0x03)
|
||||
})
|
||||
|
||||
var effectDetails = interpreters.New().
|
||||
With("SoundIndex", 0, 2).As(interpreters.RangedValue(0, 512)).
|
||||
With("SoundPlayCount", 2, 2).As(interpreters.RangedValue(0, 100)).
|
||||
With("VisualEffect", 4, 2).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "none",
|
||||
1: "power on",
|
||||
2: "quake",
|
||||
3: "escape pod",
|
||||
4: "red static",
|
||||
5: "interference"})).
|
||||
With("AdditionalVisualEffect", 8, 2).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "none",
|
||||
1: "white flash",
|
||||
2: "pink flash",
|
||||
3: "gray static",
|
||||
4: "vertical panning"})).
|
||||
With("AdditionalEffectTime", 12, 2).As(interpreters.RangedValue(0, 1000))
|
||||
|
||||
var changeTileHeightsDetails = interpreters.New().
|
||||
With("TileX", 0, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TileY", 4, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TargetFloorHeight", 8, 2).As(interpreters.SpecialValue("MoveTileHeight")).
|
||||
With("TargetCeilingHeight", 10, 2).As(interpreters.SpecialValue("MoveTileHeight")).
|
||||
With("FloorSound", 14, 2).As(interpreters.EnumValue(map[uint32]string{0x0000: "Audible", 0x0001: "Silent"}))
|
||||
|
||||
var changeTerrainDetails = interpreters.New().
|
||||
With("TileX", 0, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("TileY", 4, 4).As(interpreters.RangedValue(1, 63)).
|
||||
With("Type", 8, 1).As(interpreters.SpecialValue("TileType")).
|
||||
With("TypeControl", 9, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "Set type", 0x40: "Keep type"})).
|
||||
With("SlopeHeight", 12, 1).As(interpreters.SpecialValue("TileHeight")).
|
||||
With("SlopeControl", 13, 1).As(interpreters.EnumValue(map[uint32]string{0x00: "Set height", 0x40: "Keep height"}))
|
||||
|
||||
var scheduledTrapDetails = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("TimeInterval", 4, 4).As(interpreters.FormattedRangedValue(0, 6000, pointOneSecond)).
|
||||
With("ActivationValue", 8, 4).As(interpreters.EnumValue(map[uint32]string{0: "Off",
|
||||
0xFFFF: "On (0xFFFF)", 0x10000: "On (0x10000)", 0x11111: "On (0x11111)"})).
|
||||
With("Variance", 12, 2).As(interpreters.RangedValue(0, 512))
|
||||
|
||||
var cycleObjectsDetails = interpreters.New().
|
||||
With("ObjectID1", 0, 4).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 4, 4).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 8, 4).As(interpreters.ObjectID()).
|
||||
With("NextObject", 12, 4).As(interpreters.RangedValue(0, 2))
|
||||
|
||||
var deleteObjectsDetails = interpreters.New().
|
||||
With("ObjectID1", 0, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID3", 8, 2).As(interpreters.ObjectID()).
|
||||
With("MessageIndex", 12, 2).As(interpreters.RangedValue(0, 511))
|
||||
|
||||
var receiveEmailDetails = interpreters.New().
|
||||
With("EmailIndex", 0, 2).As(interpreters.RangedValue(0, 1000)).
|
||||
With("DelaySec", 4, 2).As(interpreters.RangedValue(0, 600))
|
||||
|
||||
var exposeDetails = interpreters.New().
|
||||
With("DeltaValue", 0, 2).As(interpreters.RangedValue(0, 1000)).
|
||||
With("EffectChangeFlag", 2, 2).As(interpreters.EnumValue(map[uint32]string{0: "Add Delta", 1: "Remove Delta"})).
|
||||
With("EffectType", 4, 4).As(interpreters.EnumValue(map[uint32]string{4: "Radiation poisoning", 8: "Bio contamination"}))
|
||||
|
||||
var setObjectParameterDetails = interpreters.New().
|
||||
With("ObjectID1", 0, 2).As(interpreters.ObjectID()).
|
||||
With("ObjectID2", 2, 2).As(interpreters.ObjectID()).
|
||||
With("Value1", 4, 4).
|
||||
With("Value2", 8, 4).
|
||||
With("Value3", 12, 4)
|
||||
|
||||
var setScreenPictureDetails = interpreters.New().
|
||||
With("ScreenObjectID1", 0, 2).As(interpreters.ObjectID()).
|
||||
With("ScreenObjectID2", 2, 2).As(interpreters.ObjectID()).
|
||||
With("SingleSequenceSource", 4, 4).
|
||||
With("LoopSequenceSource", 8, 4)
|
||||
|
||||
var setCritterStateDetails = interpreters.New().
|
||||
With("ReferenceObjectID1", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ReferenceObjectID2", 6, 2).As(interpreters.ObjectID()).
|
||||
With("NewState", 8, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "docile",
|
||||
1: "cautious",
|
||||
2: "hostile",
|
||||
3: "cautious (?)",
|
||||
4: "attacking",
|
||||
5: "sleeping",
|
||||
6: "tranquilized",
|
||||
7: "confused"}))
|
||||
|
||||
var trapMessageDetails = interpreters.New().
|
||||
With("BackgroundImageIndex", 0, 4).As(interpreters.RangedValue(-2, 500)).
|
||||
With("MessageIndex", 4, 4).As(interpreters.RangedValue(0, 511)).
|
||||
With("TextColor", 8, 4).As(interpreters.RangedValue(0, 255)).
|
||||
With("MfdSuppressionFlag", 12, 4).As(interpreters.EnumValue(map[uint32]string{0: "Show in MFD", 1: "Show only in HUD"}))
|
||||
|
||||
var spawnObjectsDetails = interpreters.New().
|
||||
With("ObjectType", 0, 4).As(interpreters.SpecialValue("ObjectTriple")).
|
||||
With("ReferenceObject1ID", 4, 2).As(interpreters.ObjectID()).
|
||||
With("ReferenceObject2ID", 6, 2).As(interpreters.ObjectID()).
|
||||
With("NumberOfObjects", 8, 4).As(interpreters.RangedValue(0, 100)).
|
||||
With("Flags", 12, 1).As(interpreters.Bitfield(map[uint32]string{
|
||||
0x00000001: "Avoid spawn within 7 tiles",
|
||||
0x00000002: "Avoid spawn in close view",
|
||||
0x00000004: "Avoid spawn in elevator music",
|
||||
0x00000008: "Force spawn in occupied/close tiles"}))
|
||||
|
||||
var changeObjectTypeDetails = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("NewType", 4, 2).As(interpreters.RangedValue(0, 16)).
|
||||
With("ResetMask", 6, 2).As(interpreters.RangedValue(0, 15))
|
||||
|
||||
// Change state block
|
||||
|
||||
var toggleRepulsorChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("OffTextureIndex", 4, 1).As(interpreters.SpecialValue("LevelTexture")).
|
||||
With("OnTextureIndex", 5, 1).As(interpreters.SpecialValue("LevelTexture")).
|
||||
With("ToggleType", 8, 1).As(interpreters.EnumValue(map[uint32]string{0: "Toggle On/Off", 1: "Toggle On, Stay On", 2: "Toggle Off, Stay Off"}))
|
||||
|
||||
var showGameCodeDigitChange = interpreters.New().
|
||||
With("ScreenObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("DigitNumber", 4, 4).As(interpreters.RangedValue(1, 6))
|
||||
|
||||
var setParameterFromVariableChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("ParameterNumber", 4, 4).As(interpreters.RangedValue(0, 16)).
|
||||
With("VariableIndex", 8, 4).As(interpreters.SpecialValue("VariableKey"))
|
||||
|
||||
var setFrameStateChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("NewState", 4, 4).As(interpreters.EnumValue(map[uint32]string{0: "Off", 1: "On"}))
|
||||
|
||||
var doorControlChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("ControlValue", 4, 4).As(interpreters.EnumValue(map[uint32]string{1: "open door", 2: "close door", 3: "toggle door", 4: "disable auto-close"}))
|
||||
|
||||
var rotateObjectChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("Amount", 4, 1).As(interpreters.RangedValue(0, 255)).
|
||||
With("RotationType", 5, 1).As(interpreters.EnumValue(map[uint32]string{0: "Endless", 1: "Back and forth"})).
|
||||
With("Direction", 6, 1).As(interpreters.EnumValue(map[uint32]string{0: "Forward", 1: "Backward"})).
|
||||
With("Axis", 7, 1).As(interpreters.EnumValue(map[uint32]string{0: "Z (Yaw)", 1: "X (Pitch)", 2: "Y (Roll)"})).
|
||||
With("ForwardLimit", 8, 1).As(interpreters.RangedValue(0, 255)).
|
||||
With("BackwardLimit", 9, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var removeObjectsChange = interpreters.New().
|
||||
With("ObjectType", 0, 4).As(interpreters.SpecialValue("ObjectTriple")).
|
||||
With("Radius", 4, 1).As(interpreters.RangedValue(0, 255))
|
||||
|
||||
var setConditionChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("Condition", 4, 4)
|
||||
|
||||
var makeItemRadioactiveChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID()).
|
||||
With("WatchedObjectID", 4, 2).As(interpreters.ObjectID()).
|
||||
With("WatchedObjectTriggerState", 6, 2)
|
||||
|
||||
var orientedTriggerObjectChange = interpreters.New().
|
||||
With("HorizontalDirection", 0, 2).As(interpreters.RangedValue(0, 0xFFFF)).
|
||||
With("ObjectID", 4, 2).As(interpreters.ObjectID())
|
||||
|
||||
var closeDataMfdChange = interpreters.New().
|
||||
With("ObjectID", 0, 4).As(interpreters.ObjectID())
|
||||
|
||||
var changeObjectTypeGlobalChange = interpreters.New().
|
||||
With("ObjectType", 0, 4).As(interpreters.SpecialValue("ObjectTriple")).
|
||||
With("NewType", 4, 1).As(interpreters.RangedValue(0, 16))
|
||||
|
||||
var hackDetails = interpreters.New().
|
||||
With("Type", 0, 4).As(interpreters.EnumValue(map[uint32]string{
|
||||
1: "Toggle Repulsor",
|
||||
2: "Show Game Code Digit",
|
||||
3: "Set Parameter from Variable",
|
||||
4: "Set Button State",
|
||||
5: "Door Control",
|
||||
6: "Return to Menu",
|
||||
7: "Rotate Objects",
|
||||
8: "Remove Objects",
|
||||
9: "SHODAN Pixelation",
|
||||
10: "Set Condition",
|
||||
11: "Show System Analyzer",
|
||||
12: "Make Item Radioactive",
|
||||
13: "Oriented Trigger Object",
|
||||
14: "Close Data MFD",
|
||||
15: "Earth Destruction by Laser",
|
||||
16: "Change Objects Type (Level)"})).
|
||||
Refining("ToggleRepulsor", 4, 12, toggleRepulsorChange, forType(1)).
|
||||
Refining("ShowGameCodeDigit", 4, 12, showGameCodeDigitChange, forType(2)).
|
||||
Refining("SetParameterFromVariable", 4, 12, setParameterFromVariableChange, forType(3)).
|
||||
Refining("SetFrameState", 4, 12, setFrameStateChange, forType(4)).
|
||||
Refining("DoorControl", 4, 12, doorControlChange, forType(5)).
|
||||
Refining("ReturnToMenu", 4, 12, interpreters.New(), forType(6)).
|
||||
Refining("RotateObject", 4, 12, rotateObjectChange, forType(7)).
|
||||
Refining("RemoveObjects", 4, 12, removeObjectsChange, forType(8)).
|
||||
Refining("ShodanPixelation", 4, 12, interpreters.New(), forType(9)).
|
||||
Refining("SetCondition", 4, 12, setConditionChange, forType(10)).
|
||||
Refining("ShowSystemAnalyzer", 4, 12, interpreters.New(), forType(11)).
|
||||
Refining("MakeItemRadioactive", 4, 12, makeItemRadioactiveChange, forType(12)).
|
||||
Refining("OrientedTriggerObject", 4, 12, orientedTriggerObjectChange, forType(13)).
|
||||
Refining("CloseDataMfd", 4, 12, closeDataMfdChange, forType(14)).
|
||||
Refining("EarthDestructionByLaser", 4, 12, interpreters.New(), forType(15)).
|
||||
Refining("ChangeObjectsType", 4, 12, changeObjectTypeGlobalChange, forType(16))
|
||||
|
||||
var unconditionalAction = interpreters.New().
|
||||
With("Type", 0, 1).As(interpreters.EnumValue(map[uint32]string{
|
||||
0: "Nothing",
|
||||
1: "Transport Hacker",
|
||||
2: "Change Health",
|
||||
3: "Clone/Move Object",
|
||||
4: "Set Game Variable",
|
||||
5: "Show Cutscene",
|
||||
6: "Trigger Other Objects",
|
||||
7: "Change Lighting",
|
||||
8: "Effect",
|
||||
9: "Change Tile Heights",
|
||||
10: "Change Terrain",
|
||||
11: "Random Timer",
|
||||
12: "Cycle Objects",
|
||||
13: "Delete Objects",
|
||||
14: "Unknown (14)",
|
||||
15: "Receive E-Mail",
|
||||
16: "Change Effect",
|
||||
17: "Set Object Parameter",
|
||||
18: "Set Screen Picture",
|
||||
19: "Change State",
|
||||
20: "Unknown (20)",
|
||||
21: "Set Critter State",
|
||||
22: "Trap Message",
|
||||
23: "Spawn Objects",
|
||||
24: "Change Object Type"})).
|
||||
With("UsageQuota", 1, 1).
|
||||
Refining("TransportHacker", 6, 16, transportHackerDetails, forType(1)).
|
||||
Refining("ChangeHealth", 6, 16, changeHealthDetails, forType(2)).
|
||||
Refining("CloneMoveObject", 6, 16, cloneMoveObjectDetails, forType(3)).
|
||||
Refining("SetGameVariable", 6, 16, setGameVariableDetails, forType(4)).
|
||||
Refining("ShowCutscene", 6, 16, showCutsceneDetails, forType(5)).
|
||||
Refining("TriggerOtherObjects", 6, 16, triggerOtherObjectsDetails, forType(6)).
|
||||
Refining("ChangeLighting", 6, 16, changeLightingDetails, forType(7)).
|
||||
Refining("Effect", 6, 16, effectDetails, forType(8)).
|
||||
Refining("ChangeTileHeights", 6, 16, changeTileHeightsDetails, forType(9)).
|
||||
Refining("ChangeTerrain", 6, 16, changeTerrainDetails, forType(10)).
|
||||
Refining("ScheduledTrap", 6, 16, scheduledTrapDetails, forType(11)).
|
||||
Refining("CycleObjects", 6, 16, cycleObjectsDetails, forType(12)).
|
||||
Refining("DeleteObjects", 6, 16, deleteObjectsDetails, forType(13)).
|
||||
// 14 unused
|
||||
Refining("ReceiveEmail", 6, 16, receiveEmailDetails, forType(15)).
|
||||
Refining("Expose", 6, 16, exposeDetails, forType(16)).
|
||||
Refining("SetObjectParameter", 6, 16, setObjectParameterDetails, forType(17)).
|
||||
Refining("SetScreenPicture", 6, 16, setScreenPictureDetails, forType(18)).
|
||||
Refining("Hack", 6, 16, hackDetails, forType(19)).
|
||||
// 20 unknown
|
||||
Refining("SetCritterState", 6, 16, setCritterStateDetails, forType(21)).
|
||||
Refining("TrapMessage", 6, 16, trapMessageDetails, forType(22)).
|
||||
Refining("SpawnObjects", 6, 16, spawnObjectsDetails, forType(23)).
|
||||
Refining("ChangeObjectType", 6, 16, changeObjectTypeDetails, forType(24))
|
||||
|
||||
// Unconditional returns the description of actions without a condition.
|
||||
func Unconditional() *interpreters.Description {
|
||||
return unconditionalAction
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package conditions
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var gameVariable = interpreters.New().
|
||||
With("VariableKey", 0, 2).As(interpreters.SpecialValue("VariableCondition")).
|
||||
With("Value", 2, 1).
|
||||
With("MessageIndex", 3, 1)
|
||||
|
||||
// GameVariable returns the description for a game variable condition.
|
||||
func GameVariable() *interpreters.Description {
|
||||
return gameVariable
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package conditions
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
)
|
||||
|
||||
var objectType = interpreters.New().
|
||||
With("ObjectType", 0, 3).As(interpreters.SpecialValue("ObjectTriple"))
|
||||
|
||||
var objectID = interpreters.New().
|
||||
With("ObjectID", 0, 2).As(interpreters.ObjectID())
|
||||
|
||||
// ObjectType returns a condition description for object types.
|
||||
func ObjectType() *interpreters.Description {
|
||||
return objectType
|
||||
}
|
||||
|
||||
// ObjectID returns a condition description for object indices.
|
||||
func ObjectID() *interpreters.Description {
|
||||
return objectID
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package lvlobj
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
var realWorldEntries *interpreterEntry
|
||||
var cyberspaceEntries *interpreterEntry
|
||||
|
||||
var realWorldExtras *interpreterEntry
|
||||
var cyberspaceExtras *interpreterEntry
|
||||
|
||||
var extraDefault = interpreters.New().
|
||||
With("CurrentFrame", 1, 1).
|
||||
With("TimeRemainder", 2, 1)
|
||||
|
||||
var extraIced = interpreters.New().
|
||||
With("ICE-presence", 1, 1).
|
||||
With("ICE-level", 3, 1)
|
||||
|
||||
var extraIcedFixtures = interpreters.New().
|
||||
With("PanelName", 0, 1).
|
||||
With("ICE-presence", 1, 1).
|
||||
With("ICE-level", 3, 1)
|
||||
|
||||
var extraFixtures = interpreters.New().
|
||||
With("PanelName", 0, 1)
|
||||
|
||||
var extraSurfaces = interpreters.New().
|
||||
With("Index", 1, 1)
|
||||
|
||||
func init() {
|
||||
software := initSoftware()
|
||||
traps := initTraps()
|
||||
critters := initCritters()
|
||||
|
||||
realWorldEntries = newInterpreterEntry(interpreters.New())
|
||||
realWorldEntries.set(int(object.ClassGun), initGuns())
|
||||
realWorldEntries.set(int(object.ClassAmmo), newInterpreterEntry(interpreters.New())) // have no data
|
||||
realWorldEntries.set(int(object.ClassPhysics), newInterpreterEntry(basePhysics))
|
||||
realWorldEntries.set(int(object.ClassGrenade), initGrenades())
|
||||
realWorldEntries.set(int(object.ClassDrug), newInterpreterEntry(interpreters.New())) // have no data
|
||||
realWorldEntries.set(int(object.ClassHardware), newInterpreterEntry(baseHardware))
|
||||
realWorldEntries.set(int(object.ClassSoftware), software)
|
||||
realWorldEntries.set(int(object.ClassBigStuff), initBigStuff())
|
||||
realWorldEntries.set(int(object.ClassSmallStuff), initSmallStuff())
|
||||
realWorldEntries.set(int(object.ClassFixture), initFixtures())
|
||||
realWorldEntries.set(int(object.ClassDoor), initDoors())
|
||||
realWorldEntries.set(int(object.ClassAnimating), newInterpreterEntry(baseAnimation))
|
||||
realWorldEntries.set(int(object.ClassTrap), traps)
|
||||
realWorldEntries.set(int(object.ClassContainer), initContainers())
|
||||
realWorldEntries.set(int(object.ClassCritter), critters)
|
||||
|
||||
cyberspaceEntries = newInterpreterEntry(interpreters.New())
|
||||
cyberspaceEntries.set(int(object.ClassSoftware), software)
|
||||
cyberspaceEntries.set(int(object.ClassBigStuff), initCyberspaceBigStuff())
|
||||
cyberspaceEntries.set(int(object.ClassSmallStuff), initCyberspaceSmallStuff())
|
||||
cyberspaceEntries.set(int(object.ClassFixture), initCyberspaceFixtures())
|
||||
cyberspaceEntries.set(int(object.ClassTrap), traps)
|
||||
cyberspaceEntries.set(int(object.ClassCritter), critters)
|
||||
|
||||
realWorldExtras = newInterpreterEntry(extraDefault)
|
||||
realWorldExtras.set(int(object.ClassFixture), newInterpreterLeaf(extraFixtures))
|
||||
extraBigStuff := newInterpreterEntry(extraDefault)
|
||||
simpleSurfaces := newInterpreterLeaf(extraSurfaces)
|
||||
surfaces := newInterpreterEntry(extraDefault)
|
||||
surfaces.set(1, simpleSurfaces)
|
||||
surfaces.set(4, simpleSurfaces)
|
||||
extraBigStuff.set(2, surfaces)
|
||||
realWorldExtras.set(int(object.ClassBigStuff), extraBigStuff)
|
||||
cyberspaceExtras = newInterpreterEntry(interpreters.New())
|
||||
cyberspaceExtras.set(int(object.ClassSoftware), newInterpreterLeaf(extraIced))
|
||||
cyberspaceExtras.set(int(object.ClassBigStuff), newInterpreterLeaf(extraIced))
|
||||
cyberspaceExtras.set(int(object.ClassSmallStuff), newInterpreterLeaf(extraIced))
|
||||
cyberspaceExtras.set(int(object.ClassFixture), newInterpreterLeaf(extraIcedFixtures))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package audio
|
||||
|
||||
// L8 is a raw, linear 8-bit sound snippet.
|
||||
type L8 struct {
|
||||
SampleRate float32
|
||||
Samples []byte
|
||||
}
|
||||
|
||||
// Empty returns true if there are no samples to play.
|
||||
func (sound L8) Empty() bool {
|
||||
return len(sound.Samples) == 0
|
||||
}
|
||||
|
||||
// Duration returns the length of the sound in seconds.
|
||||
func (sound L8) Duration() float32 {
|
||||
if sound.SampleRate <= 0 {
|
||||
return 0.0
|
||||
}
|
||||
return float32(len(sound.Samples)) / sound.SampleRate
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package voc
|
||||
|
||||
type blockType byte
|
||||
|
||||
const (
|
||||
terminator = blockType(0x00)
|
||||
soundData = blockType(0x01)
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
package voc
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1"
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||
)
|
||||
|
||||
const (
|
||||
errSourceIsNil ss1.StringError = "source is nil"
|
||||
errNoAudioFound ss1.StringError = "no audio found"
|
||||
errInvalidVersionValidity ss1.StringError = "version validity check failed"
|
||||
)
|
||||
|
||||
// Load reads from the provided source a Creative Voice Sound and returns the data.
|
||||
func Load(source io.Reader) (data audio.L8, err error) {
|
||||
if source == nil {
|
||||
return data, errSourceIsNil
|
||||
}
|
||||
|
||||
err = readAndVerifyHeader(source)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return readSoundData(source)
|
||||
}
|
||||
|
||||
func readAndVerifyHeader(source io.Reader) error {
|
||||
start := make([]byte, len(FileHeader))
|
||||
headerSize := uint16(0)
|
||||
version := uint16(0)
|
||||
versionValidity := uint16(0)
|
||||
|
||||
_, err := source.Read(start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(source, binary.LittleEndian, &headerSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(source, binary.LittleEndian, &version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(source, binary.LittleEndian, &versionValidity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
calculated := ^version + versionCheckValue
|
||||
if calculated != versionValidity {
|
||||
return errInvalidVersionValidity
|
||||
}
|
||||
|
||||
skip := make([]byte, headerSize-standardHeaderSize)
|
||||
_, err = source.Read(skip)
|
||||
return err
|
||||
}
|
||||
|
||||
func readSoundData(source io.Reader) (data audio.L8, err error) {
|
||||
sampleRate := float32(0.0)
|
||||
var samples []byte
|
||||
done := false
|
||||
|
||||
for !done {
|
||||
blockStart := make([]byte, 4)
|
||||
|
||||
_, err = source.Read(blockStart)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch blockType(blockStart[0]) {
|
||||
case soundData:
|
||||
{
|
||||
meta := make([]byte, 2)
|
||||
_, err = source.Read(meta)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sampleRate = divisorToSampleRate(meta[0])
|
||||
|
||||
newCount := lengthFromBlockStart(blockStart) - len(meta)
|
||||
buf := make([]byte, newCount)
|
||||
_, err = source.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
oldCount := len(samples)
|
||||
newSamples := make([]byte, oldCount+newCount)
|
||||
copy(newSamples, samples)
|
||||
copy(newSamples[oldCount:], buf)
|
||||
samples = newSamples
|
||||
}
|
||||
case terminator:
|
||||
{
|
||||
done = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(samples) == 0 {
|
||||
return data, errNoAudioFound
|
||||
}
|
||||
|
||||
return audio.L8{
|
||||
SampleRate: sampleRate,
|
||||
Samples: samples,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package voc_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio/voc"
|
||||
)
|
||||
|
||||
func TestLoadReturnsErrorOnNil(t *testing.T) {
|
||||
_, err := voc.Load(nil)
|
||||
|
||||
assert.Errorf(t, err, "source is nil")
|
||||
}
|
||||
|
||||
func newHeader() *bytes.Buffer {
|
||||
writer := bytes.NewBufferString(voc.FileHeader)
|
||||
version := uint16(0x010A)
|
||||
headerSize := uint16(0x001A)
|
||||
|
||||
_ = binary.Write(writer, binary.LittleEndian, headerSize)
|
||||
_ = binary.Write(writer, binary.LittleEndian, version)
|
||||
versionValidity := ^version + uint16(0x1234)
|
||||
_ = binary.Write(writer, binary.LittleEndian, versionValidity)
|
||||
|
||||
return writer
|
||||
}
|
||||
|
||||
func TestLoadReturnsErrorOnInvalidVersion(t *testing.T) {
|
||||
writer := newHeader()
|
||||
|
||||
writer.Write([]byte{0x00}) // Terminator
|
||||
|
||||
data := writer.Bytes()
|
||||
data[24] = 0x00
|
||||
source := bytes.NewReader(data)
|
||||
_, err := voc.Load(source)
|
||||
|
||||
assert.Errorf(t, err, "Version validity failed: 0x1129 != 0x1100")
|
||||
}
|
||||
|
||||
func TestLoadReturnsErrorOnValidButEmptyFile(t *testing.T) {
|
||||
writer := newHeader()
|
||||
|
||||
writer.Write([]byte{0x00}) // Terminator
|
||||
|
||||
source := bytes.NewReader(writer.Bytes())
|
||||
_, err := voc.Load(source)
|
||||
|
||||
assert.Errorf(t, err, "No audio found")
|
||||
}
|
||||
|
||||
func TestLoadReturnsSoundDataOnSampleData(t *testing.T) {
|
||||
writer := newHeader()
|
||||
|
||||
writer.Write([]byte{0x01}) // sound data
|
||||
writer.Write([]byte{0x03, 0x00, 0x00}) // block size
|
||||
writer.Write([]byte{0x64, 0x00}) // divisor, sound type
|
||||
writer.Write([]byte{0x80}) // one sample
|
||||
|
||||
writer.Write([]byte{0x00}) // Terminator
|
||||
|
||||
source := bytes.NewReader(writer.Bytes())
|
||||
data, err := voc.Load(source)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.NotNil(t, data)
|
||||
}
|
||||
|
||||
func TestLoadReturnsSoundDataWithSampleRate(t *testing.T) {
|
||||
writer := newHeader()
|
||||
|
||||
writer.Write([]byte{0x01}) // sound data
|
||||
writer.Write([]byte{0x03, 0x00, 0x00}) // block size
|
||||
writer.Write([]byte{0x9C, 0x00}) // divisor, sound type
|
||||
writer.Write([]byte{0x80}) // one sample
|
||||
|
||||
writer.Write([]byte{0x00}) // Terminator
|
||||
|
||||
source := bytes.NewReader(writer.Bytes())
|
||||
data, err := voc.Load(source)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, float32(10000.0), data.SampleRate)
|
||||
}
|
||||
|
||||
func TestLoadReturnsSoundDataWithSamples(t *testing.T) {
|
||||
writer := newHeader()
|
||||
samples := []byte{0x80, 0xFF, 0x00, 0xC0, 0x40, 0x7F, 0x81}
|
||||
|
||||
writer.Write([]byte{0x01}) // sound data
|
||||
writer.Write([]byte{0x09, 0x00, 0x00}) // block size
|
||||
writer.Write([]byte{0x9C, 0x00}) // divisor, sound type
|
||||
writer.Write(samples) // samples
|
||||
|
||||
writer.Write([]byte{0x00}) // Terminator
|
||||
|
||||
source := bytes.NewReader(writer.Bytes())
|
||||
data, err := voc.Load(source)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, samples, data.Samples)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package voc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Save encodes the provided samples into the given writer.
|
||||
func Save(writer io.Writer, sampleRate float32, samples []byte) error {
|
||||
err := writeHeader(writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeBasicSoundData(writer, sampleRate, samples)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeEndOfFile(writer)
|
||||
return err
|
||||
}
|
||||
|
||||
func writeHeader(writer io.Writer) error {
|
||||
version := baseVersion
|
||||
|
||||
_, err := writer.Write(bytes.NewBufferString(FileHeader).Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.LittleEndian, standardHeaderSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.LittleEndian, baseVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return binary.Write(writer, binary.LittleEndian, (^version)+versionCheckValue)
|
||||
}
|
||||
|
||||
func writeBlockHeader(writer io.Writer, block blockType, dataBytes int) error {
|
||||
_, err := writer.Write([]byte{byte(block), byte(dataBytes), byte(dataBytes >> 8), byte(dataBytes >> 16)})
|
||||
return err
|
||||
}
|
||||
|
||||
func writeBasicSoundData(writer io.Writer, sampleRate float32, samples []byte) error {
|
||||
sampleType := byte(0)
|
||||
|
||||
err := writeBlockHeader(writer, soundData, len(samples)+2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write([]byte{sampleRateToDivisor(sampleRate), sampleType})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(samples)
|
||||
return err
|
||||
}
|
||||
|
||||
func writeEndOfFile(writer io.Writer) error {
|
||||
_, err := writer.Write([]byte{byte(terminator)})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package voc
|
||||
|
||||
const (
|
||||
// FileHeader identifies VOC files.
|
||||
FileHeader string = "Creative Voice File\u001A"
|
||||
standardHeaderSize uint16 = 0x1A
|
||||
baseVersion uint16 = 0x010A
|
||||
versionCheckValue uint16 = 0x1234
|
||||
rateBase float32 = 1000000.0
|
||||
)
|
||||
|
||||
func lengthFromBlockStart(blockStart []byte) int {
|
||||
return int(blockStart[3])<<16 + int(blockStart[2])<<8 + int(blockStart[1])
|
||||
}
|
||||
|
||||
func divisorToSampleRate(divisor byte) float32 {
|
||||
return rateBase / float32(256-int(divisor))
|
||||
}
|
||||
|
||||
func sampleRateToDivisor(sampleRate float32) byte {
|
||||
return byte(256 - int(rateBase/sampleRate))
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package voc // nolint: testpackage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFrequencyDivisorFor11111(t *testing.T) {
|
||||
divisor := byte(0xA6)
|
||||
sampleRate := divisorToSampleRate(divisor)
|
||||
result := sampleRateToDivisor(sampleRate)
|
||||
|
||||
assert.Equal(t, divisor, result)
|
||||
}
|
||||
|
||||
func TestFrequencyDivisorFor222222(t *testing.T) {
|
||||
divisor := byte(0x2D)
|
||||
sampleRate := divisorToSampleRate(divisor)
|
||||
result := sampleRateToDivisor(sampleRate)
|
||||
|
||||
assert.Equal(t, divisor, result)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package wav
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1"
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||
)
|
||||
|
||||
const (
|
||||
errSourceIsNil ss1.StringError = "source is nil"
|
||||
errNotASupportedWave ss1.StringError = "not a supported WAV"
|
||||
)
|
||||
|
||||
// Load reads from the provided source and returns the data.
|
||||
func Load(source io.Reader) (data audio.L8, err error) {
|
||||
if source == nil {
|
||||
return data, errSourceIsNil
|
||||
}
|
||||
|
||||
var loader waveLoader
|
||||
|
||||
loader.load(source)
|
||||
if loader.err != nil {
|
||||
return data, loader.err
|
||||
}
|
||||
|
||||
data.SampleRate = loader.sampleRate
|
||||
data.Samples = loader.samples
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package wav_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio/wav"
|
||||
)
|
||||
|
||||
func TestLoadReturnsErrorOnNil(t *testing.T) {
|
||||
_, err := wav.Load(nil)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestLoadExtractsDataOfL8(t *testing.T) {
|
||||
input := []byte{
|
||||
0x52, 0x49, 0x46, 0x46, // "RIFF"
|
||||
0x29, 0x00, 0x00, 0x00, // len(RIFF)
|
||||
0x57, 0x41, 0x56, 0x45, // "WAVE"
|
||||
0x66, 0x6d, 0x74, 0x20, // "fmt "
|
||||
0x10, 0x00, 0x00, 0x00, // len(fmt)
|
||||
0x01, 0x00, // fmt:type
|
||||
0x01, 0x00, // fmt:channels
|
||||
0x22, 0x56, 0x00, 0x00, // fmt:samples/sec
|
||||
0x22, 0x56, 0x00, 0x00, // fmt:avgBytes/sec
|
||||
0x01, 0x00, // fmt:blockAlign
|
||||
0x08, 0x00, // fmt:bits/sample
|
||||
0x64, 0x61, 0x74, 0x61, // "data"
|
||||
0x05, 0x00, 0x00, 0x00, // len(data)
|
||||
0x00, 0x40, 0x80, 0xC0, 0xFF} // data
|
||||
|
||||
data, err := wav.Load(bytes.NewReader(input))
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, float32(22050), data.SampleRate)
|
||||
assert.Equal(t, []byte{0x00, 0x40, 0x80, 0xC0, 0xFF}, data.Samples)
|
||||
}
|
||||
|
||||
func TestLoadExtractsDataOfL16(t *testing.T) {
|
||||
input := []byte{
|
||||
0x52, 0x49, 0x46, 0x46, // "RIFF"
|
||||
0x2E, 0x00, 0x00, 0x00, // len(RIFF)
|
||||
0x57, 0x41, 0x56, 0x45, // "WAVE"
|
||||
0x66, 0x6d, 0x74, 0x20, // "fmt "
|
||||
0x10, 0x00, 0x00, 0x00, // len(fmt)
|
||||
0x01, 0x00, // fmt:type
|
||||
0x01, 0x00, // fmt:channels
|
||||
0x22, 0x56, 0x00, 0x00, // fmt:samples/sec
|
||||
0x44, 0xAC, 0x00, 0x00, // fmt:avgBytes/sec
|
||||
0x02, 0x00, // fmt:blockAlign
|
||||
0x10, 0x00, // fmt:bits/sample
|
||||
0x64, 0x61, 0x74, 0x61, // "data"
|
||||
0x0A, 0x00, 0x00, 0x00, // len(data)
|
||||
0xAA, 0x00, 0xAA, 0x40, 0xAA, 0x7F, 0xAA, 0xC0, 0xAA, 0xFF} // data
|
||||
|
||||
data, err := wav.Load(bytes.NewReader(input))
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, float32(22050), data.SampleRate)
|
||||
assert.Equal(t, []byte{0x80, 0xC0, 0xFF, 0x40, 0x7F}, data.Samples)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package wav
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Save encodes the provided samples into the given writer.
|
||||
func Save(writer io.Writer, sampleRate float32, samples []byte) error {
|
||||
dataSize := uint32(len(samples))
|
||||
var fmt formatHeader
|
||||
fmtSize := fmt.size()
|
||||
contentType := riffContentTypeWave
|
||||
contentTypeSize := uint32(4)
|
||||
tagSize := uint32(8)
|
||||
tagSizes := tagSize * 2
|
||||
|
||||
fmt.base.FormatType = waveFormatTypePcm
|
||||
fmt.base.Channels = 1
|
||||
fmt.base.SamplesPerSec = uint32(sampleRate)
|
||||
fmt.base.AvgBytesPerSec = fmt.base.SamplesPerSec
|
||||
fmt.base.BlockAlign = 1
|
||||
fmt.extension.BitsPerSample = 8
|
||||
|
||||
riffTag := riffChunkTag{ChunkType: riffChunkTypeRiff, Size: tagSizes + contentTypeSize + fmtSize + dataSize}
|
||||
|
||||
_ = binary.Write(writer, binary.LittleEndian, &riffTag)
|
||||
_ = binary.Write(writer, binary.LittleEndian, &contentType)
|
||||
_ = binary.Write(writer, binary.LittleEndian, &riffChunkTag{ChunkType: riffChunkTypeFmt, Size: fmtSize})
|
||||
_ = binary.Write(writer, binary.LittleEndian, &fmt.base)
|
||||
_ = binary.Write(writer, binary.LittleEndian, &fmt.extension)
|
||||
_ = binary.Write(writer, binary.LittleEndian, &riffChunkTag{ChunkType: riffChunkTypeData, Size: dataSize})
|
||||
_, err := writer.Write(samples)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package wav_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio/wav"
|
||||
)
|
||||
|
||||
func TestSaveStoresBytes(t *testing.T) {
|
||||
sampleRate := float32(22050.0)
|
||||
samples := []byte{0x00, 0x40, 0x80, 0xC0, 0xFF}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
err := wav.Save(buf, sampleRate, samples)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, []byte{
|
||||
0x52, 0x49, 0x46, 0x46, // "RIFF"
|
||||
0x2B, 0x00, 0x00, 0x00, // len(RIFF)
|
||||
0x57, 0x41, 0x56, 0x45, // "WAVE"
|
||||
0x66, 0x6d, 0x74, 0x20, // "fmt "
|
||||
0x12, 0x00, 0x00, 0x00, // len(fmt)
|
||||
0x01, 0x00, // fmt:type
|
||||
0x01, 0x00, // fmt:channels
|
||||
0x22, 0x56, 0x00, 0x00, // fmt:samples/sec
|
||||
0x22, 0x56, 0x00, 0x00, // fmt:avgBytes/sec
|
||||
0x01, 0x00, // fmt:blockAlign
|
||||
0x08, 0x00, // fmt:bits/sample
|
||||
0x00, 0x00, // fmt:extensionSize
|
||||
0x64, 0x61, 0x74, 0x61, // "data"
|
||||
0x05, 0x00, 0x00, 0x00, // len(data)
|
||||
0x00, 0x40, 0x80, 0xC0, 0xFF}, buf.Bytes())
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package wav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1"
|
||||
)
|
||||
|
||||
const (
|
||||
errUnsupportedFormat ss1.StringError = "unsupported WAVE format"
|
||||
)
|
||||
|
||||
func l8FromL8(input []byte) []byte {
|
||||
return input
|
||||
}
|
||||
|
||||
func l8FromL16(input []byte) []byte {
|
||||
samples := len(input) / 2
|
||||
output := make([]byte, samples)
|
||||
|
||||
for i := 0; i < samples; i++ {
|
||||
output[i] = byte(int(input[i*2+1]) + 0x80)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
type waveLoader struct {
|
||||
dataRead bool
|
||||
formatRead bool
|
||||
samples []byte
|
||||
sampleRate float32
|
||||
|
||||
reader io.Reader
|
||||
err error
|
||||
|
||||
dataConverter func([]byte) []byte
|
||||
}
|
||||
|
||||
func (loader *waveLoader) load(reader io.Reader) {
|
||||
loader.reader = reader
|
||||
loader.dataConverter = l8FromL8
|
||||
loader.loadRiff()
|
||||
}
|
||||
|
||||
func (loader *waveLoader) read(data interface{}) bool {
|
||||
if loader.err == nil {
|
||||
loader.err = binary.Read(loader.reader, binary.LittleEndian, data)
|
||||
}
|
||||
return loader.err == nil
|
||||
}
|
||||
|
||||
func (loader *waveLoader) readBytes(size uint32) (data []byte) {
|
||||
data = make([]byte, int(size))
|
||||
_, loader.err = loader.reader.Read(data)
|
||||
return
|
||||
}
|
||||
|
||||
func (loader *waveLoader) loadChunk(handler func(riffChunkType, uint32)) {
|
||||
var tag riffChunkTag
|
||||
|
||||
if loader.read(&tag) {
|
||||
handler(tag.ChunkType, tag.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func (loader *waveLoader) loadRiff() {
|
||||
loader.loadChunk(loader.handleRiff)
|
||||
}
|
||||
|
||||
func (loader *waveLoader) handleRiff(chunkType riffChunkType, size uint32) {
|
||||
if chunkType == riffChunkTypeRiff {
|
||||
var contentType riffContentType
|
||||
if loader.read(&contentType) && (contentType == riffContentTypeWave) {
|
||||
loader.loadWave()
|
||||
} else if loader.err == nil {
|
||||
loader.err = errNotASupportedWave
|
||||
}
|
||||
} else {
|
||||
loader.err = errNotASupportedWave
|
||||
}
|
||||
}
|
||||
|
||||
func (loader *waveLoader) loadWave() {
|
||||
for !loader.isDone() {
|
||||
loader.loadFormatOrData()
|
||||
}
|
||||
loader.samples = loader.dataConverter(loader.samples)
|
||||
}
|
||||
|
||||
func (loader *waveLoader) loadFormatOrData() {
|
||||
loader.loadChunk(loader.handleFormatOrData)
|
||||
}
|
||||
|
||||
func (loader *waveLoader) handleFormatOrData(chunkType riffChunkType, size uint32) {
|
||||
if chunkType == riffChunkTypeFmt {
|
||||
loader.loadFormat(size)
|
||||
} else if chunkType == riffChunkTypeData {
|
||||
loader.loadData(size)
|
||||
}
|
||||
}
|
||||
|
||||
func (loader *waveLoader) loadFormat(size uint32) {
|
||||
headerData := loader.readBytes(size)
|
||||
headerReader := bytes.NewReader(headerData)
|
||||
var header formatHeader
|
||||
|
||||
loader.formatRead = true
|
||||
loader.err = binary.Read(headerReader, binary.LittleEndian, &header.base)
|
||||
loader.err = binary.Read(headerReader, binary.LittleEndian, &header.extension.BitsPerSample)
|
||||
loader.sampleRate = float32(header.base.SamplesPerSec)
|
||||
|
||||
if header.extension.BitsPerSample == 16 {
|
||||
loader.dataConverter = l8FromL16
|
||||
}
|
||||
|
||||
if (header.base.FormatType != waveFormatTypePcm) ||
|
||||
(header.base.Channels != 1) ||
|
||||
((header.extension.BitsPerSample != 8) && (header.extension.BitsPerSample != 16)) {
|
||||
loader.err = errUnsupportedFormat
|
||||
}
|
||||
}
|
||||
|
||||
func (loader *waveLoader) loadData(size uint32) {
|
||||
loader.dataRead = true
|
||||
loader.samples = loader.readBytes(size)
|
||||
}
|
||||
|
||||
func (loader *waveLoader) isDone() bool {
|
||||
return (loader.err != nil) || (loader.dataRead && loader.formatRead)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package wav
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
type waveFormatType uint16
|
||||
|
||||
const (
|
||||
waveFormatTypePcm = 1
|
||||
)
|
||||
|
||||
type waveFormat struct {
|
||||
FormatType waveFormatType
|
||||
Channels uint16
|
||||
SamplesPerSec uint32
|
||||
AvgBytesPerSec uint32
|
||||
BlockAlign uint16
|
||||
}
|
||||
|
||||
type waveFormatExtension struct {
|
||||
BitsPerSample uint16
|
||||
ExtensionSize uint16
|
||||
}
|
||||
|
||||
type formatHeader struct {
|
||||
base waveFormat
|
||||
extension waveFormatExtension
|
||||
}
|
||||
|
||||
func (header formatHeader) size() uint32 {
|
||||
return uint32(binary.Size(&header.base) + binary.Size(&header.extension))
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package wav
|
||||
|
||||
type riffChunkType uint32
|
||||
type riffContentType uint32
|
||||
|
||||
const (
|
||||
riffChunkTypeRiff = riffChunkType(0x46464952)
|
||||
riffChunkTypeFmt = riffChunkType(0x20746d66)
|
||||
riffChunkTypeData = riffChunkType(0x61746164)
|
||||
)
|
||||
|
||||
const (
|
||||
riffContentTypeWave = riffContentType(0x45564157)
|
||||
)
|
||||
|
||||
type riffChunkTag struct {
|
||||
ChunkType riffChunkType
|
||||
Size uint32
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package bitmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
const (
|
||||
animationEndTag = 0x0C
|
||||
animationEndTagData = 0x01
|
||||
|
||||
animationEntryTag = 0x04
|
||||
|
||||
errEndTagDataWrong ss1.StringError = "end tag data wrong"
|
||||
)
|
||||
|
||||
type unknownEntryTagError struct {
|
||||
tag byte
|
||||
}
|
||||
|
||||
func (err unknownEntryTagError) Error() string {
|
||||
return fmt.Sprintf("unknown entry tag 0x%02X", int(err.tag))
|
||||
}
|
||||
|
||||
// Animation describes a sequence of bitmaps to form a small movie.
|
||||
type Animation struct {
|
||||
Width int16
|
||||
Height int16
|
||||
ResourceID resource.ID
|
||||
IntroFlag uint16
|
||||
Entries []AnimationEntry
|
||||
}
|
||||
|
||||
// AnimationEntry describes one part of an animation with common frame time.
|
||||
type AnimationEntry struct {
|
||||
FirstFrame byte
|
||||
LastFrame byte
|
||||
FrameTime int16
|
||||
}
|
||||
|
||||
// ReadAnimation extracts an animation from a serialized stream.
|
||||
func ReadAnimation(reader io.Reader) (anim Animation, err error) {
|
||||
decoder := serial.NewDecoder(reader)
|
||||
codeAnimationBase(decoder, &anim)
|
||||
done := false
|
||||
err = decoder.FirstError()
|
||||
for !done && (err == nil) {
|
||||
var tag byte
|
||||
decoder.Code(&tag)
|
||||
err = decoder.FirstError()
|
||||
switch tag {
|
||||
case animationEntryTag:
|
||||
var entry AnimationEntry
|
||||
decoder.Code(&entry)
|
||||
anim.Entries = append(anim.Entries, entry)
|
||||
err = decoder.FirstError()
|
||||
case animationEndTag:
|
||||
var endTagData byte
|
||||
decoder.Code(&endTagData)
|
||||
if endTagData != animationEndTagData {
|
||||
err = errEndTagDataWrong
|
||||
}
|
||||
done = true
|
||||
default:
|
||||
if err == nil {
|
||||
err = unknownEntryTagError{tag: tag}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// WriteAnimation serializes an animation to a writer.
|
||||
func WriteAnimation(writer io.Writer, anim Animation) error {
|
||||
encoder := serial.NewEncoder(writer)
|
||||
codeAnimationBase(encoder, &anim)
|
||||
for _, entry := range anim.Entries {
|
||||
encoder.Code([]byte{animationEntryTag})
|
||||
encoder.Code(&entry) // nolint: scopelint
|
||||
}
|
||||
encoder.Code([]byte{animationEndTag, animationEndTagData})
|
||||
|
||||
return encoder.FirstError()
|
||||
}
|
||||
|
||||
func codeAnimationBase(coder serial.Coder, anim *Animation) {
|
||||
coder.Code(&anim.Width)
|
||||
coder.Code(&anim.Height)
|
||||
coder.Code(&anim.ResourceID)
|
||||
var unknown [6]byte
|
||||
coder.Code(&unknown)
|
||||
coder.Code(&anim.IntroFlag)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package bitmap
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
// AnimationCache retrieves animations from a localizer and keeps them decoded until they are invalidated.
|
||||
type AnimationCache struct {
|
||||
localizer resource.Localizer
|
||||
|
||||
animations map[resource.Key]Animation
|
||||
}
|
||||
|
||||
// NewAnimationCache returns a new instance.
|
||||
func NewAnimationCache(localizer resource.Localizer) *AnimationCache {
|
||||
cache := &AnimationCache{
|
||||
localizer: localizer,
|
||||
animations: make(map[resource.Key]Animation),
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// InvalidateResources lets the cache remove any animations from resources that are specified in the given slice.
|
||||
func (cache *AnimationCache) InvalidateResources(ids []resource.ID) {
|
||||
for _, id := range ids {
|
||||
for key := range cache.animations {
|
||||
if key.ID == id {
|
||||
delete(cache.animations, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animation tries to look up given animation.
|
||||
func (cache *AnimationCache) Animation(key resource.Key) (anim Animation, err error) {
|
||||
anim, existing := cache.animations[key]
|
||||
if existing {
|
||||
return anim, nil
|
||||
}
|
||||
selector := cache.localizer.LocalizedResources(key.Lang)
|
||||
view, err := selector.Select(key.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if (view.ContentType() != resource.Animation) || (view.BlockCount() != 1) {
|
||||
return anim, resource.ErrWrongType(key, resource.Animation)
|
||||
}
|
||||
reader, err := view.Block(0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
anim, err = ReadAnimation(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cache.animations[key] = anim
|
||||
return anim, nil
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package bitmap_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
const (
|
||||
baseAnimationResourceID = 0x0A4C
|
||||
)
|
||||
|
||||
type AnimationCacheSuite struct {
|
||||
suite.Suite
|
||||
|
||||
localizedResources resource.LocalizedResourcesList
|
||||
|
||||
instance *bitmap.AnimationCache
|
||||
}
|
||||
|
||||
func TestAnimationCacheSuite(t *testing.T) {
|
||||
suite.Run(t, new(AnimationCacheSuite))
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) SetupTest() {
|
||||
suite.instance = nil
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) TestAnimationReturnsValueIfOK() {
|
||||
anim := suite.someAnimation(0)
|
||||
suite.givenAnInstance()
|
||||
suite.whenResourcesAre(
|
||||
suite.someLocalizedResources(
|
||||
suite.storing(0, anim)))
|
||||
suite.thenAnimationShouldReturn(anim, 0)
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) TestAnimationReturnsErrorIfResourceNotExisting() {
|
||||
suite.givenAnInstance()
|
||||
suite.whenResourcesAre()
|
||||
suite.thenAnimationShouldReturnError(0)
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) TestTextTriesToReloadWhenCacheInvalidated() {
|
||||
suite.givenAnInstance()
|
||||
suite.givenResourcesAre(
|
||||
suite.someLocalizedResources(
|
||||
suite.storing(0, suite.someAnimation(0))))
|
||||
suite.givenAnimationWasRetrieved(0)
|
||||
suite.givenResourcesAre()
|
||||
suite.whenCacheResourcesAreInvalidated(baseAnimationResourceID)
|
||||
suite.thenAnimationShouldReturnError(0)
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) TestInvalidationConsidersOnlyAffectedIDs() {
|
||||
suite.givenAnInstance()
|
||||
suite.givenResourcesAre(
|
||||
suite.someLocalizedResources(
|
||||
suite.storing(0, suite.someAnimation(0)), suite.storing(1, suite.someAnimation(1))))
|
||||
suite.givenAnimationWasRetrieved(0)
|
||||
suite.givenResourcesAre()
|
||||
suite.whenCacheResourcesAreInvalidated(0x02BD)
|
||||
suite.thenAnimationShouldReturn(suite.someAnimation(0), 0)
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) TestAnimationReturnsErrorIfResourceIsNotAnAnimation() {
|
||||
suite.givenAnInstance()
|
||||
suite.whenResourcesAre(suite.someLocalizedResources(suite.storingNonAnimation(0)))
|
||||
suite.thenAnimationShouldReturnError(0)
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) givenAnInstance() {
|
||||
suite.instance = bitmap.NewAnimationCache(suite)
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) givenResourcesAre(resources ...resource.LocalizedResources) {
|
||||
suite.localizedResources = resources
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) givenAnimationWasRetrieved(index int) {
|
||||
_, err := suite.instance.Animation(suite.keyed(index))
|
||||
require.Nil(suite.T(), err, "No error expected setting up")
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) whenResourcesAre(resources ...resource.LocalizedResources) {
|
||||
suite.localizedResources = resources
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) whenCacheResourcesAreInvalidated(ids ...resource.ID) {
|
||||
suite.instance.InvalidateResources(ids)
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) thenAnimationShouldReturn(expected bitmap.Animation, index int) {
|
||||
result, err := suite.instance.Animation(suite.keyed(index))
|
||||
require.Nil(suite.T(), err, "No error expected")
|
||||
assert.Equal(suite.T(), expected, result, "Animations don't match")
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) thenAnimationShouldReturnError(index int) {
|
||||
_, err := suite.instance.Animation(suite.keyed(index))
|
||||
require.NotNil(suite.T(), err, "Error expected")
|
||||
}
|
||||
|
||||
func (suite AnimationCacheSuite) keyed(index int) resource.Key {
|
||||
return resource.KeyOf(baseAnimationResourceID, resource.LangAny, index)
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) someLocalizedResources(modifiers ...func(*resource.Store)) resource.LocalizedResources {
|
||||
var store resource.Store
|
||||
for _, modifier := range modifiers {
|
||||
modifier(&store)
|
||||
}
|
||||
return resource.LocalizedResources{
|
||||
ID: "unnamed",
|
||||
Language: resource.LangAny,
|
||||
Viewer: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) storing(id int, anim bitmap.Animation) func(*resource.Store) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_ = bitmap.WriteAnimation(buf, anim)
|
||||
return func(store *resource.Store) {
|
||||
_ = store.Put(resource.ID(baseAnimationResourceID).Plus(id), resource.Resource{
|
||||
Properties: resource.Properties{
|
||||
ContentType: resource.Animation,
|
||||
Compound: true,
|
||||
},
|
||||
Blocks: resource.BlocksFrom([][]byte{buf.Bytes()}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) someAnimation(seed uint8) bitmap.Animation {
|
||||
anim := bitmap.Animation{
|
||||
Width: int16(seed) * 4,
|
||||
Height: int16(seed) * 3,
|
||||
IntroFlag: uint16(seed % 2),
|
||||
ResourceID: resource.ID(seed),
|
||||
Entries: make([]bitmap.AnimationEntry, seed+1),
|
||||
}
|
||||
|
||||
for i := 0; i < len(anim.Entries); i++ {
|
||||
anim.Entries[i] = bitmap.AnimationEntry{
|
||||
FirstFrame: byte(i),
|
||||
LastFrame: byte(i),
|
||||
FrameTime: 100,
|
||||
}
|
||||
}
|
||||
return anim
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) storingNonAnimation(id int) func(*resource.Store) {
|
||||
return func(store *resource.Store) {
|
||||
_ = store.Put(resource.ID(baseAnimationResourceID).Plus(id), resource.Resource{
|
||||
Properties: resource.Properties{
|
||||
ContentType: resource.Sound,
|
||||
Compound: true,
|
||||
},
|
||||
Blocks: resource.BlocksFrom([][]byte{{}}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *AnimationCacheSuite) LocalizedResources(lang resource.Language) resource.Selector {
|
||||
return resource.Selector{
|
||||
From: suite.localizedResources,
|
||||
Lang: lang,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package bitmap_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
func TestReadAnimationErrorsLength(t *testing.T) {
|
||||
for i := 0; i < 20; i++ {
|
||||
data := make([]byte, i)
|
||||
_, err := bitmap.ReadAnimation(bytes.NewReader(data))
|
||||
assert.Error(t, err, fmt.Sprintf("Error expected for %d bytes", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAnimationErrorsTags(t *testing.T) {
|
||||
tt := []struct {
|
||||
info string
|
||||
data []byte
|
||||
}{
|
||||
{
|
||||
info: "end tag data missing",
|
||||
data: []byte{
|
||||
0x00, 0x00,
|
||||
0x02, 0x02,
|
||||
0x04, 0x04,
|
||||
0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
|
||||
0x0C, 0x0C,
|
||||
0x0C,
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "end tag data wrong",
|
||||
data: []byte{
|
||||
0x00, 0x00,
|
||||
0x02, 0x02,
|
||||
0x04, 0x04,
|
||||
0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
|
||||
0x0C, 0x0C,
|
||||
0x0C, 0x00,
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "unknown tag",
|
||||
data: []byte{
|
||||
0x00, 0x00,
|
||||
0x02, 0x02,
|
||||
0x04, 0x04,
|
||||
0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
|
||||
0x0C, 0x0C,
|
||||
0x0B,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
_, err := bitmap.ReadAnimation(bytes.NewReader(tc.data))
|
||||
assert.Error(t, err, fmt.Sprintf("Error expected for %s", tc.info))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadWriteAnimationEntries(t *testing.T) {
|
||||
tt := []struct {
|
||||
info string
|
||||
data []byte
|
||||
expected bitmap.Animation
|
||||
}{
|
||||
{
|
||||
info: "empty list of entries",
|
||||
data: []byte{
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xAA, 0xBB,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xCC, 0xDD,
|
||||
0x0C, 0x01,
|
||||
},
|
||||
expected: bitmap.Animation{
|
||||
Width: 0x3412,
|
||||
Height: 0x7856,
|
||||
ResourceID: resource.ID(0xBBAA),
|
||||
IntroFlag: 0xDDCC,
|
||||
Entries: nil,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
info: "two entries",
|
||||
data: []byte{
|
||||
0x12, 0x34,
|
||||
0x56, 0x78,
|
||||
0xAA, 0xBB,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xCC, 0xDD,
|
||||
0x04, 0x00, 0x0A, 0x10, 0x20,
|
||||
0x04, 0x0B, 0x10, 0x30, 0x40,
|
||||
0x0C, 0x01,
|
||||
},
|
||||
expected: bitmap.Animation{
|
||||
Width: 0x3412,
|
||||
Height: 0x7856,
|
||||
ResourceID: resource.ID(0xBBAA),
|
||||
IntroFlag: 0xDDCC,
|
||||
Entries: []bitmap.AnimationEntry{
|
||||
{FirstFrame: 0x00, LastFrame: 0x0A, FrameTime: 0x2010},
|
||||
{FirstFrame: 0x0B, LastFrame: 0x10, FrameTime: 0x4030},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
td := tc
|
||||
t.Run(tc.info, func(t *testing.T) {
|
||||
anim, err := bitmap.ReadAnimation(bytes.NewReader(td.data))
|
||||
require.Nil(t, err, "No error expected")
|
||||
assert.Equal(t, td.expected, anim, "Animation data not as expected after reading")
|
||||
|
||||
output := bytes.NewBuffer(nil)
|
||||
err = bitmap.WriteAnimation(output, anim)
|
||||
require.Nil(t, err, "No error expected")
|
||||
assert.Equal(t, td.data, output.Bytes(), "Data mismatch after re-encoding")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package bitmap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1"
|
||||
"github.com/inkyblackness/hacked/ss1/serial/rle"
|
||||
)
|
||||
|
||||
const (
|
||||
errReaderIsNil ss1.StringError = "reader is nil"
|
||||
errStrideNotValid ss1.StringError = "stride not equal to width for compressed bitmap"
|
||||
errDataCouldNotBeRead ss1.StringError = "data could not be read"
|
||||
errPaletteCouldNotBeRead ss1.StringError = "palette could not be read"
|
||||
)
|
||||
|
||||
// Bitmap describes a palette based image.
|
||||
type Bitmap struct {
|
||||
Header Header
|
||||
Pixels []byte
|
||||
Palette *Palette
|
||||
}
|
||||
|
||||
// Decode tries to read a bitmap from given reader.
|
||||
// Should the bitmap be a compressed bitmap, then a reference image with pixel data all 0x00 is assumed.
|
||||
func Decode(reader io.Reader) (*Bitmap, error) {
|
||||
return DecodeReferenced(reader, func(width, height int16) ([]byte, error) {
|
||||
return make([]byte, int(width)*int(height)), nil
|
||||
})
|
||||
}
|
||||
|
||||
// DecodeReferenced tries to read a bitmap from given reader.
|
||||
// If the serialized bitmap describes a compressed bitmap, then the pixels from the reference are used as a basis for the result.
|
||||
// The returned byte array from the provider will be used as pixel buffer for the new bitmap.
|
||||
func DecodeReferenced(reader io.Reader, provider func(width, height int16) ([]byte, error)) (*Bitmap, error) {
|
||||
if reader == nil {
|
||||
return nil, errReaderIsNil
|
||||
}
|
||||
|
||||
var bmp Bitmap
|
||||
|
||||
err := binary.Read(reader, binary.LittleEndian, &bmp.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bmp.Header.Type == TypeCompressed8Bit {
|
||||
if bmp.Header.Stride != uint16(bmp.Header.Width) {
|
||||
return nil, errStrideNotValid
|
||||
}
|
||||
bmp.Pixels, err = provider(bmp.Header.Width, bmp.Header.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = rle.Decompress(reader, bmp.Pixels)
|
||||
} else {
|
||||
bmp.Pixels = make([]byte, int(bmp.Header.Height)*int(bmp.Header.Stride))
|
||||
_, err = reader.Read(bmp.Pixels)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errDataCouldNotBeRead
|
||||
}
|
||||
|
||||
if bmp.Header.PaletteOffset != 0 {
|
||||
paletteFlag := uint32(0)
|
||||
_ = binary.Read(reader, binary.LittleEndian, &paletteFlag)
|
||||
bmp.Palette = new(Palette)
|
||||
err = binary.Read(reader, binary.LittleEndian, bmp.Palette)
|
||||
if err != nil {
|
||||
return nil, errPaletteCouldNotBeRead
|
||||
}
|
||||
}
|
||||
|
||||
return &bmp, nil
|
||||
}
|
||||
|
||||
// Encode writes the bitmap to a byte array and returns it.
|
||||
// Compressed bitmaps will be compressed with a reference image with pixel data all 0x00.
|
||||
func Encode(bmp *Bitmap, offsetBase int) []byte {
|
||||
rawData := bmp.Pixels
|
||||
if bmp.Header.Type == TypeCompressed8Bit {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_ = rle.Compress(buf, rawData, nil)
|
||||
rawData = buf.Bytes()
|
||||
}
|
||||
header := bmp.Header
|
||||
if bmp.Palette != nil {
|
||||
header.PaletteOffset = int32(offsetBase + HeaderSize + len(rawData))
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_ = binary.Write(buf, binary.LittleEndian, &header)
|
||||
_ = binary.Write(buf, binary.LittleEndian, rawData)
|
||||
if bmp.Palette != nil {
|
||||
_ = binary.Write(buf, binary.LittleEndian, paletteMarker)
|
||||
_ = binary.Write(buf, binary.LittleEndian, bmp.Palette)
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package bitmap_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDecodeReturnsErrorOnNilSource(t *testing.T) {
|
||||
_, err := bitmap.Decode(nil)
|
||||
|
||||
assert.Error(t, err, "error expected")
|
||||
}
|
||||
|
||||
func TestDecodeOfUncompressedDataReturnsBitmap(t *testing.T) {
|
||||
data := getTestData(bitmap.TypeFlat8Bit, []byte{0xAA}, false)
|
||||
bmp, err := bitmap.Decode(bytes.NewReader(data))
|
||||
|
||||
require.Nil(t, err, "no error expected")
|
||||
require.NotNil(t, bmp, "bitmap expected")
|
||||
assert.Equal(t, int16(1), bmp.Header.Width)
|
||||
assert.Equal(t, int16(1), bmp.Header.Height)
|
||||
assert.Equal(t, []byte{0xAA}, bmp.Pixels)
|
||||
}
|
||||
|
||||
func TestDecodeOfCompressedDataReturnsBitmap(t *testing.T) {
|
||||
data := getTestData(bitmap.TypeCompressed8Bit, []byte{0x00, 0x01, 0xBB, 0x80, 0x00, 0x00}, false)
|
||||
bmp, err := bitmap.Decode(bytes.NewReader(data))
|
||||
|
||||
require.Nil(t, err, "no error expected")
|
||||
require.NotNil(t, bmp, "bitmap expected")
|
||||
assert.Equal(t, int16(1), bmp.Header.Width)
|
||||
assert.Equal(t, int16(1), bmp.Header.Height)
|
||||
assert.Equal(t, []byte{0xBB}, bmp.Pixels)
|
||||
}
|
||||
|
||||
func TestDecodeWithPrivatePaletteReturnsPalette(t *testing.T) {
|
||||
data := getTestData(bitmap.TypeFlat8Bit, []byte{0xAA}, true)
|
||||
bmp, err := bitmap.Decode(bytes.NewReader(data))
|
||||
|
||||
require.Nil(t, err, "no error expected")
|
||||
require.NotNil(t, bmp, "bitmap expected")
|
||||
assert.NotNil(t, bmp.Palette, "palette expected")
|
||||
}
|
||||
|
||||
func TestWriteUncompressedWithoutPalette(t *testing.T) {
|
||||
sourceData := getTestData(bitmap.TypeFlat8Bit, []byte{0xAA}, false)
|
||||
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
|
||||
|
||||
result := bitmap.Encode(bmp, 0)
|
||||
assert.Equal(t, sourceData, result)
|
||||
}
|
||||
|
||||
func TestWriteCompressedWithoutPalette(t *testing.T) {
|
||||
sourceData := getTestData(bitmap.TypeCompressed8Bit, []byte{0x01, 0xBB, 0x80, 0x00, 0x00}, false)
|
||||
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
|
||||
|
||||
result := bitmap.Encode(bmp, 0)
|
||||
assert.Equal(t, sourceData, result)
|
||||
}
|
||||
|
||||
func TestWriteUncompressedWithPalette(t *testing.T) {
|
||||
sourceData := getTestData(bitmap.TypeFlat8Bit, []byte{0xCC}, true)
|
||||
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
|
||||
|
||||
result := bitmap.Encode(bmp, 0)
|
||||
assert.Equal(t, sourceData, result)
|
||||
}
|
||||
|
||||
func TestWriteCompressedWithPalette(t *testing.T) {
|
||||
sourceData := getTestData(bitmap.TypeCompressed8Bit, []byte{0x01, 0xEE, 0x80, 0x00, 0x00}, true)
|
||||
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
|
||||
|
||||
result := bitmap.Encode(bmp, 0)
|
||||
assert.Equal(t, sourceData, result)
|
||||
}
|
||||
|
||||
func TestWriteWithForceTransparency(t *testing.T) {
|
||||
sourceData := getTestData(bitmap.TypeFlat8Bit, []byte{0xAA}, false)
|
||||
sourceData[6] = 1
|
||||
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
|
||||
|
||||
result := bitmap.Encode(bmp, 0)
|
||||
assert.Equal(t, sourceData, result)
|
||||
}
|
||||
|
||||
func getTestData(bmpType bitmap.Type, data []byte, withPalette bool) []byte {
|
||||
var header bitmap.Header
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
header.Type = bmpType
|
||||
header.Width = 1
|
||||
header.Stride = 1
|
||||
header.Height = 1
|
||||
header.WidthFactor = 1
|
||||
header.HeightFactor = 1
|
||||
if withPalette {
|
||||
header.PaletteOffset = int32(binary.Size(header) + len(data))
|
||||
}
|
||||
_ = binary.Write(buf, binary.LittleEndian, &header)
|
||||
buf.Write(data)
|
||||
if withPalette {
|
||||
var pal bitmap.Palette
|
||||
_ = binary.Write(buf, binary.LittleEndian, uint32(0x01000000))
|
||||
_ = binary.Write(buf, binary.LittleEndian, pal)
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user