Adding modding folder with a hacked build.

This commit is contained in:
ml
2026-09-26 08:53:08 +02:00
parent cdb960dc71
commit c91080aba7
755 changed files with 50424 additions and 0 deletions
@@ -0,0 +1,42 @@
package text
import "strings"
const (
// textLineLimit is the lowest number found by a cursory search.
// It appears that various texts have different limits.
textLineLimit = 79
)
// Blocked splits the given input string into a series of lines, usable for blocked serialization.
// It returns an array of strings that each are below a maximum line length. The list is terminated with
// one empty line (necessary for compound strings).
func Blocked(input string) []string {
var result []string
textLines := strings.Split(input, "\n")
resultLine := ""
addBlock := func() {
if len(resultLine) > 0 {
result = append(result, resultLine)
resultLine = ""
}
}
for textLineIndex, textLine := range textLines {
words := strings.Split(textLine, " ")
for wordIndex, word := range words {
if wordIndex > 0 {
resultLine += " "
}
if (len(resultLine) + len(word)) > textLineLimit {
addBlock()
}
resultLine += word
}
if textLineIndex < (len(textLines) - 1) {
resultLine += "\n"
}
}
addBlock()
return append(result, "")
}
@@ -0,0 +1,36 @@
package text_test
import (
"testing"
"github.com/inkyblackness/hacked/ss1/content/text"
"github.com/stretchr/testify/assert"
)
func TestSplitText(t *testing.T) {
tt := []struct {
input string
expected []string
}{
{input: "", expected: []string{""}},
{input: "a", expected: []string{"a", ""}},
{input: "a", expected: []string{"a", ""}},
{input: "b\n", expected: []string{"b\n", ""}},
{input: " spacing! ", expected: []string{" spacing! ", ""}},
{input: "terse1\n\n\nterse2", expected: []string{"terse1\n\n\nterse2", ""}},
{
input: "aaaaaaaaa bbbbbbbbb ccccccccc ddddddddd eeeeeeeee fffffffff ggggggggg hhhhhhhhh iiiiiiiii jjjjjjjjj kkkkk",
expected: []string{
"aaaaaaaaa bbbbbbbbb ccccccccc ddddddddd eeeeeeeee fffffffff ggggggggg hhhhhhhhh ",
"iiiiiiiii jjjjjjjjj kkkkk",
"",
},
},
}
for _, tc := range tt {
result := text.Blocked(tc.input)
assert.Equal(t, tc.expected, result, "wrong result for <"+tc.input+">")
}
}
+114
View File
@@ -0,0 +1,114 @@
package text
import (
"io/ioutil"
"github.com/inkyblackness/hacked/ss1/resource"
)
type keyResolver func(resource.Key) resource.Key
type textReader func(resource.Selector, resource.Key, Codepage) (string, error)
// Cache retrieves texts from a localizer and keeps them decoded until they are invalidated.
type Cache struct {
cp Codepage
localizer resource.Localizer
reader textReader
keyResolver keyResolver
texts map[resource.Key]string
}
func newCache(cp Codepage, localizer resource.Localizer, keyResolver keyResolver, reader textReader) *Cache {
cache := &Cache{
cp: cp,
localizer: localizer,
reader: reader,
keyResolver: keyResolver,
texts: make(map[resource.Key]string),
}
return cache
}
// NewLineCache returns a cache for single-block texts.
func NewLineCache(cp Codepage, localizer resource.Localizer) *Cache {
return newCache(cp, localizer, func(key resource.Key) resource.Key { return key }, readLine)
}
// NewPageCache returns a cache for resource-based texts.
func NewPageCache(cp Codepage, localizer resource.Localizer) *Cache {
return newCache(cp, localizer, func(key resource.Key) resource.Key {
return resource.KeyOf(key.ID.Plus(key.Index), key.Lang, 0)
}, readPage)
}
func readLine(selector resource.Selector, key resource.Key, cp Codepage) (string, error) {
view, err := selector.Select(key.ID)
if err != nil {
return "", err
}
if view.ContentType() != resource.Text {
return "", resource.ErrWrongType(key, resource.Text)
}
reader, err := view.Block(key.Index)
if err != nil {
return "", err
}
raw, err := ioutil.ReadAll(reader)
if err != nil {
return "", err
}
return cp.Decode(raw), nil
}
func readPage(selector resource.Selector, key resource.Key, cp Codepage) (string, error) {
view, err := selector.Select(key.ID.Plus(key.Index))
if err != nil {
return "", err
}
if view.ContentType() != resource.Text {
return "", resource.ErrWrongType(key, resource.Text)
}
blockCount := view.BlockCount()
value := ""
for block := 0; block < blockCount; block++ {
reader, err := view.Block(block)
if err != nil {
return "", err
}
raw, err := ioutil.ReadAll(reader)
if err != nil {
return "", err
}
value += cp.Decode(raw)
}
return value, nil
}
// InvalidateResources lets the cache remove any texts from resources that are specified in the given slice.
func (cache *Cache) InvalidateResources(ids []resource.ID) {
for _, id := range ids {
for key := range cache.texts {
if key.ID == id {
delete(cache.texts, key)
}
}
}
}
// Text retrieves and caches the text of given key.
func (cache *Cache) Text(key resource.Key) (string, error) {
cacheKey := cache.keyResolver(key)
value, existing := cache.texts[cacheKey]
if existing {
return value, nil
}
selector := cache.localizer.LocalizedResources(key.Lang)
value, err := cache.reader(selector, key, cache.cp)
if err != nil {
return "", err
}
cache.texts[cacheKey] = value
return value, nil
}
@@ -0,0 +1,198 @@
package text_test
import (
"testing"
"github.com/inkyblackness/hacked/ss1/content/text"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type CacheSuite struct {
suite.Suite
localizedResources resource.LocalizedResourcesList
cp text.Codepage
instance *text.Cache
}
func TestCacheSuite(t *testing.T) {
suite.Run(t, new(CacheSuite))
}
func (suite *CacheSuite) SetupTest() {
suite.cp = text.DefaultCodepage()
suite.instance = nil
}
func (suite *CacheSuite) TestTextReturnsValueIfOKForLineCache() {
suite.givenALineCache()
suite.whenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, "test")))
suite.thenTextShouldReturn("test", resource.KeyOf(0x1000, resource.LangGerman, 0))
}
func (suite *CacheSuite) TestTextReturnsValueIfOKForPageCache() {
suite.givenAPageCache()
suite.whenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, "this is", " separated", " over lines")))
suite.thenTextShouldReturn("this is separated over lines", resource.KeyOf(0x1000, resource.LangGerman, 0))
}
func (suite *CacheSuite) TestTextReturnsErrorIfResourceNotExistingForLineCache() {
suite.givenALineCache()
suite.whenResourcesAre()
suite.thenTextShouldReturnError(resource.KeyOf(0x1000, resource.LangGerman, 0))
}
func (suite *CacheSuite) TestTextReturnsErrorIfResourceNotExistingForPageCache() {
suite.givenAPageCache()
suite.whenResourcesAre()
suite.thenTextShouldReturnError(resource.KeyOf(0x1000, resource.LangGerman, 0))
}
func (suite *CacheSuite) TestTextReturnsErrorIfBlockNotExistingForLineCache() {
suite.givenALineCache()
suite.whenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, "zero", "one")))
suite.thenTextShouldReturnError(resource.KeyOf(0x1000, resource.LangGerman, 2))
}
func (suite *CacheSuite) TestTextReturnsCachedValueIfPreviouslyRetrieved() {
key := resource.KeyOf(0x1000, resource.LangGerman, 0)
suite.givenALineCache()
suite.givenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, "test")))
suite.givenTextWasRetrieved(key)
suite.whenResourcesAre()
suite.thenTextShouldReturn("test", key)
}
func (suite *CacheSuite) TestTextTriesToReloadWhenCacheInvalidated() {
key := resource.KeyOf(0x1000, resource.LangGerman, 0)
suite.givenALineCache()
suite.givenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, "test")))
suite.givenTextWasRetrieved(key)
suite.givenResourcesAre()
suite.whenCacheResourcesAreInvalidated(0x1000)
suite.thenTextShouldReturnError(key)
}
func (suite *CacheSuite) TestInvalidationConsidersOnlyAffectedIDs() {
suite.givenALineCache()
key := resource.KeyOf(0x1000, resource.LangGerman, 0)
suite.givenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, "test"), suite.storing(0x1001, "other")))
suite.givenTextWasRetrieved(key)
suite.givenResourcesAre()
suite.whenCacheResourcesAreInvalidated(0x1001)
suite.thenTextShouldReturn("test", key)
}
func (suite *CacheSuite) TestTextReturnsErrorIfResourceIsNotATextForLineCache() {
suite.givenALineCache()
suite.whenResourcesAre(suite.someLocalizedResources(resource.LangDefault,
suite.storingNonText(0x1000)))
suite.thenTextShouldReturnError(resource.KeyOf(0x1000, resource.LangDefault, 0))
}
func (suite *CacheSuite) TestTextReturnsErrorIfResourceIsNotATextForPageCache() {
suite.givenAPageCache()
suite.whenResourcesAre(suite.someLocalizedResources(resource.LangDefault,
suite.storingNonText(0x1000)))
suite.thenTextShouldReturnError(resource.KeyOf(0x1000, resource.LangDefault, 0))
}
func (suite *CacheSuite) givenALineCache() {
suite.instance = text.NewLineCache(suite.cp, suite)
}
func (suite *CacheSuite) givenAPageCache() {
suite.instance = text.NewPageCache(suite.cp, suite)
}
func (suite *CacheSuite) givenResourcesAre(resources ...resource.LocalizedResources) {
suite.localizedResources = resources
}
func (suite *CacheSuite) givenTextWasRetrieved(key resource.Key) {
_, err := suite.instance.Text(key)
assert.Nil(suite.T(), err, "no error expected")
}
func (suite *CacheSuite) whenResourcesAre(resources ...resource.LocalizedResources) {
suite.localizedResources = resources
}
func (suite *CacheSuite) whenCacheResourcesAreInvalidated(ids ...resource.ID) {
suite.instance.InvalidateResources(ids)
}
func (suite *CacheSuite) thenTextShouldReturn(expected string, key resource.Key) {
result, err := suite.instance.Text(key)
require.Nil(suite.T(), err, "No error expected")
assert.Equal(suite.T(), expected, result, "Texts don't match")
}
func (suite *CacheSuite) thenTextShouldReturnError(key resource.Key) {
_, err := suite.instance.Text(key)
require.NotNil(suite.T(), err, "Error expected")
}
func (suite *CacheSuite) someLocalizedResources(lang resource.Language, modifiers ...func(*resource.Store)) resource.LocalizedResources {
var store resource.Store
for _, modifier := range modifiers {
modifier(&store)
}
return resource.LocalizedResources{
ID: "unnamed",
Language: lang,
Viewer: store,
}
}
func (suite *CacheSuite) storing(id int, lines ...string) func(*resource.Store) {
data := make([][]byte, len(lines))
for i, line := range lines {
data[i] = suite.cp.Encode(line)
}
return func(store *resource.Store) {
_ = store.Put(resource.ID(id), resource.Resource{
Properties: resource.Properties{
ContentType: resource.Text,
Compound: len(data) != 1,
},
Blocks: resource.BlocksFrom(data),
})
}
}
func (suite *CacheSuite) storingNonText(id int) func(*resource.Store) {
return func(store *resource.Store) {
_ = store.Put(resource.ID(id), resource.Resource{
Properties: resource.Properties{
ContentType: resource.Sound,
Compound: false,
},
Blocks: resource.BlocksFrom([][]byte{{}}),
})
}
}
func (suite *CacheSuite) LocalizedResources(lang resource.Language) resource.Selector {
return resource.Selector{
From: suite.localizedResources,
Lang: lang,
}
}
@@ -0,0 +1,10 @@
package text
// Codepage wraps the methods for serializing strings. The encoded byte array is expected
// to be terminated by one 0x00 byte.
type Codepage interface {
// Encode converts the provided string to a byte array. It appends one 0x00 byte at the end.
Encode(value string) []byte
// Decode converts the provided byte slice to a string. It ignores trailing 0x00 bytes.
Decode(data []byte) string
}
@@ -0,0 +1,34 @@
package text
var cp437ToRune = [256]rune{
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F,
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5,
0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192,
0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB,
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F, 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229,
0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0,
}
// DefaultCodepage returns a Codepage instance that represents the one used for the resources.
// It is based on the Code Page 437 ( https://en.wikipedia.org/wiki/Code_page_437 ).
func DefaultCodepage() Codepage {
cp437ToByte := make(map[rune]byte)
for i, r := range &cp437ToRune {
cp437ToByte[r] = byte(i)
}
cp := &tabledCodepage{tableToRune: cp437ToRune[:], tableToByte: cp437ToByte}
return cp
}
@@ -0,0 +1,27 @@
package text_test
import (
"testing"
"github.com/inkyblackness/hacked/ss1/content/text"
"github.com/stretchr/testify/assert"
)
func TestDefaultCodepageEncode(t *testing.T) {
result := text.DefaultCodepage().Encode("ä")
assert.Equal(t, []byte{132, 0x00}, result)
}
func TestDefaultCodepageDecode(t *testing.T) {
result := text.DefaultCodepage().Decode([]byte{144, 225, 0x00})
assert.Equal(t, "Éß", result)
}
func TestDefaultCodepageMapsUnknownCharacterToQuestionMark(t *testing.T) {
result := text.DefaultCodepage().Encode("„quoted”")
assert.Equal(t, []byte{0x3F, 0x71, 0x75, 0x6F, 0x74, 0x65, 0x64, 0x3F, 0x00}, result)
}
@@ -0,0 +1,201 @@
package text
import (
"fmt"
"io/ioutil"
"regexp"
"strconv"
"github.com/inkyblackness/hacked/ss1/resource"
)
const (
metaExpressionInterrupt = "(t)"
metaExpressionNextMessage = "(?:i([0-9a-fA-F]{2}))"
metaExpressionColorIndex = "(?:c([0-9a-fA-F]{2}))"
metaExpressionLeftDisplay = "([0-9]+)"
metaExpressionRightDisplay = "(?:,[ ]*([0-9]+))"
)
var metaExpression = regexp.MustCompile("^[ ]*(?:(?:" +
metaExpressionInterrupt + "|" +
metaExpressionNextMessage + "|" +
metaExpressionColorIndex + "|" +
metaExpressionLeftDisplay + "|" +
metaExpressionRightDisplay + ")[ ]*)*$")
type metaStringError struct {
MetaString string
}
func (err metaStringError) Error() string {
return fmt.Sprintf("failed to parse meta string: '%s'", err.MetaString)
}
type blockError struct {
Action string
Index int
Nested error
}
func (err blockError) Error() string {
return fmt.Sprintf("failed to %s block %d: %v", err.Action, err.Index, err.Nested)
}
func (err blockError) Unwrap() error {
return err.Nested
}
// ElectronicMessage describes one message.
type ElectronicMessage struct {
// NextMessage identifies the message that should follow this one. -1 for none.
NextMessage int
// IsInterrupt marks messages that are the ones following another.
IsInterrupt bool
// ColorIndex is into the palette for the sender text. -1 for default.
ColorIndex int
// LeftDisplay indicates the image to show in the left MFD. -1 for none.
LeftDisplay int
// RightDisplay indicates the image to show in the right MFD. -1 for none.
RightDisplay int
// Title of the message.
Title string
// Sender of the message.
Sender string
// Subject of the message.
Subject string
// VerboseText the long form.
VerboseText string
// TerseText the short form.
TerseText string
}
// EmptyElectronicMessage returns an instance of an empty electronic message.
func EmptyElectronicMessage() ElectronicMessage {
message := ElectronicMessage{
NextMessage: -1,
ColorIndex: -1,
LeftDisplay: -1,
RightDisplay: -1,
}
return message
}
// DecodeElectronicMessage tries to decode a message from given block holder.
func DecodeElectronicMessage(cp Codepage, provider resource.BlockProvider) (message ElectronicMessage, err error) {
blockIndex := 0
nextBlockString := func() (line string) {
if err != nil {
return
}
if blockIndex < provider.BlockCount() {
blockReader, blockErr := provider.Block(blockIndex)
if blockErr != nil {
err = blockError{
Action: "access",
Index: blockIndex,
Nested: blockErr,
}
return
}
data, dataErr := ioutil.ReadAll(blockReader)
if dataErr != nil {
err = blockError{
Action: "read data from",
Index: blockIndex,
Nested: dataErr,
}
}
line = cp.Decode(data)
blockIndex++
}
return
}
nextText := func() (text string) {
for line := nextBlockString(); len(line) > 0; line = nextBlockString() {
text += line
}
return
}
metaString := nextBlockString()
metaData := metaExpression.FindStringSubmatch(metaString)
parseInt := func(metaIndex int, base, bits int) (result int) {
var value uint64
result = -1
if (err == nil) && (len(metaData[metaIndex]) > 0) {
value, err = strconv.ParseUint(metaData[metaIndex], base, bits)
if err == nil {
result = int(value)
}
}
return
}
message = EmptyElectronicMessage()
if (len(metaData) == 0) || (len(metaData[0]) != len(metaString)) {
err = metaStringError{MetaString: metaString}
}
if (err == nil) && (len(metaData[1]) > 0) {
message.IsInterrupt = true
}
message.NextMessage = parseInt(2, 16, 16)
message.ColorIndex = parseInt(3, 16, 8)
message.LeftDisplay = parseInt(4, 10, 16)
message.RightDisplay = parseInt(5, 10, 16)
message.Title = nextBlockString()
message.Sender = nextBlockString()
message.Subject = nextBlockString()
message.VerboseText = nextText()
message.TerseText = nextText()
return
}
// Encode serializes the message into a block holder.
func (message ElectronicMessage) Encode(cp Codepage) [][]byte {
var blocks [][]byte
blocks = append(blocks, cp.Encode(message.metaString()))
blocks = append(blocks, cp.Encode(message.Title))
blocks = append(blocks, cp.Encode(message.Sender))
blocks = append(blocks, cp.Encode(message.Subject))
for _, line := range Blocked(message.VerboseText) {
blocks = append(blocks, cp.Encode(line))
}
for _, line := range Blocked(message.TerseText) {
blocks = append(blocks, cp.Encode(line))
}
return blocks
}
func (message ElectronicMessage) metaString() string {
result := ""
add := func(sep, part string) {
if len(result) > 0 {
result += sep
}
result += part
}
if message.IsInterrupt {
add("", "t")
}
if message.NextMessage >= 0 {
add(" ", fmt.Sprintf("i%02X", message.NextMessage))
}
if message.ColorIndex >= 0 {
add(" ", fmt.Sprintf("c%02X", message.ColorIndex))
}
if message.LeftDisplay >= 0 {
add(" ", fmt.Sprintf("%d", message.LeftDisplay))
}
if message.RightDisplay >= 0 {
add("", fmt.Sprintf(",%d", message.RightDisplay))
}
return result
}
@@ -0,0 +1,58 @@
package text
import (
"github.com/inkyblackness/hacked/ss1/resource"
)
// ElectronicMessageCache retrieves messages from a localizer and keeps them decoded until they are invalidated.
type ElectronicMessageCache struct {
cp Codepage
localizer resource.Localizer
messages map[resource.Key]ElectronicMessage
}
// NewElectronicMessageCache returns a new instance.
func NewElectronicMessageCache(cp Codepage, localizer resource.Localizer) *ElectronicMessageCache {
cache := &ElectronicMessageCache{
cp: cp,
localizer: localizer,
messages: make(map[resource.Key]ElectronicMessage),
}
return cache
}
// InvalidateResources lets the cache remove any texts from resources that are specified in the given slice.
func (cache *ElectronicMessageCache) InvalidateResources(ids []resource.ID) {
for _, id := range ids {
for key := range cache.messages {
if key.ID == id {
delete(cache.messages, key)
}
}
}
}
// Message retrieves and caches the message of given key.
func (cache *ElectronicMessageCache) Message(key resource.Key) (ElectronicMessage, error) {
cacheKey := resource.KeyOf(key.ID.Plus(key.Index), key.Lang, 0)
value, existing := cache.messages[cacheKey]
if existing {
return value, nil
}
selector := cache.localizer.LocalizedResources(key.Lang)
view, err := selector.Select(cacheKey.ID)
if err != nil {
return EmptyElectronicMessage(), err
}
if (view.ContentType() != resource.Text) || !view.Compound() {
return EmptyElectronicMessage(), resource.ErrWrongType(key, resource.Text)
}
value, err = DecodeElectronicMessage(cache.cp, view)
if err != nil {
return EmptyElectronicMessage(), err
}
cache.messages[cacheKey] = value
return value, nil
}
@@ -0,0 +1,189 @@
package text_test
import (
"testing"
"github.com/inkyblackness/hacked/ss1/content/text"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type ElectronicMessageCacheSuite struct {
suite.Suite
localizedResources resource.LocalizedResourcesList
cp text.Codepage
instance *text.ElectronicMessageCache
}
func TestElectronicMessageCacheSuite(t *testing.T) {
suite.Run(t, new(ElectronicMessageCacheSuite))
}
func (suite *ElectronicMessageCacheSuite) SetupTest() {
suite.cp = text.DefaultCodepage()
suite.instance = nil
}
func (suite *ElectronicMessageCacheSuite) TestMessageReturnsValueIfOK() {
suite.givenACache()
suite.whenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, func(msg text.ElectronicMessage) text.ElectronicMessage {
msg.VerboseText = "123"
msg.TerseText = "terse"
return msg
})))
suite.thenMessageShouldReturn(resource.KeyOf(0x1000, resource.LangGerman, 0), func(msg text.ElectronicMessage) {
assert.Equal(suite.T(), "123", msg.VerboseText)
assert.Equal(suite.T(), "terse", msg.TerseText)
})
}
func (suite *ElectronicMessageCacheSuite) TestTextReturnsErrorIfResourceNotExisting() {
suite.givenACache()
suite.whenResourcesAre()
suite.thenMessageShouldReturnError(resource.KeyOf(0x1000, resource.LangGerman, 0))
}
func (suite *ElectronicMessageCacheSuite) TestTextReturnsCachedValueIfPreviouslyRetrieved() {
key := resource.KeyOf(0x1000, resource.LangGerman, 0)
suite.givenACache()
suite.givenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, func(msg text.ElectronicMessage) text.ElectronicMessage {
msg.VerboseText = "older text"
msg.TerseText = "terse text"
return msg
})))
suite.givenMessageWasRetrieved(key)
suite.whenResourcesAre()
suite.thenMessageShouldReturn(key, func(msg text.ElectronicMessage) {
assert.Equal(suite.T(), "older text", msg.VerboseText)
assert.Equal(suite.T(), "terse text", msg.TerseText)
})
}
func (suite *ElectronicMessageCacheSuite) TestTextTriesToReloadWhenCacheInvalidated() {
key := resource.KeyOf(0x1000, resource.LangGerman, 0)
suite.givenACache()
suite.givenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, func(msg text.ElectronicMessage) text.ElectronicMessage {
msg.VerboseText = "12"
msg.TerseText = "20"
return msg
})))
suite.givenMessageWasRetrieved(key)
suite.givenResourcesAre()
suite.whenCacheResourcesAreInvalidated(0x1000)
suite.thenMessageShouldReturnError(key)
}
func (suite *ElectronicMessageCacheSuite) TestInvalidationConsidersOnlyAffectedIDs() {
suite.givenACache()
key := resource.KeyOf(0x1000, resource.LangGerman, 0)
suite.givenResourcesAre(
suite.someLocalizedResources(resource.LangGerman,
suite.storing(0x1000, func(msg text.ElectronicMessage) text.ElectronicMessage {
msg.VerboseText = "A"
return msg
}),
suite.storing(0x1001, func(msg text.ElectronicMessage) text.ElectronicMessage {
msg.VerboseText = "B"
return msg
})))
suite.givenMessageWasRetrieved(key)
suite.givenResourcesAre()
suite.whenCacheResourcesAreInvalidated(0x1001)
suite.thenMessageShouldReturn(key, func(msg text.ElectronicMessage) {
assert.Equal(suite.T(), "A", msg.VerboseText)
})
}
func (suite *ElectronicMessageCacheSuite) TestTextReturnsErrorIfResourceIsNotATextForLineCache() {
suite.givenACache()
suite.whenResourcesAre(suite.someLocalizedResources(resource.LangDefault,
suite.storingNonText(0x1000)))
suite.thenMessageShouldReturnError(resource.KeyOf(0x1000, resource.LangDefault, 0))
}
func (suite *ElectronicMessageCacheSuite) givenACache() {
suite.instance = text.NewElectronicMessageCache(suite.cp, suite)
}
func (suite *ElectronicMessageCacheSuite) givenResourcesAre(resources ...resource.LocalizedResources) {
suite.localizedResources = resources
}
func (suite *ElectronicMessageCacheSuite) givenMessageWasRetrieved(key resource.Key) {
_, err := suite.instance.Message(key)
assert.Nil(suite.T(), err, "no error expected")
}
func (suite *ElectronicMessageCacheSuite) whenResourcesAre(resources ...resource.LocalizedResources) {
suite.localizedResources = resources
}
func (suite *ElectronicMessageCacheSuite) whenCacheResourcesAreInvalidated(ids ...resource.ID) {
suite.instance.InvalidateResources(ids)
}
func (suite *ElectronicMessageCacheSuite) thenMessageShouldReturn(key resource.Key, validator func(msg text.ElectronicMessage)) {
result, err := suite.instance.Message(key)
require.Nil(suite.T(), err, "No error expected")
validator(result)
}
func (suite *ElectronicMessageCacheSuite) thenMessageShouldReturnError(key resource.Key) {
_, err := suite.instance.Message(key)
require.NotNil(suite.T(), err, "Error expected")
}
func (suite *ElectronicMessageCacheSuite) someLocalizedResources(lang resource.Language, modifiers ...func(*resource.Store)) resource.LocalizedResources {
var store resource.Store
for _, modifier := range modifiers {
modifier(&store)
}
return resource.LocalizedResources{
ID: "unnamed",
Language: lang,
Viewer: store,
}
}
func (suite *ElectronicMessageCacheSuite) storing(id int, modifier func(msg text.ElectronicMessage) text.ElectronicMessage) func(*resource.Store) {
data := modifier(text.EmptyElectronicMessage()).Encode(suite.cp)
return func(store *resource.Store) {
_ = store.Put(resource.ID(id), resource.Resource{
Properties: resource.Properties{
ContentType: resource.Text,
Compound: true,
},
Blocks: resource.BlocksFrom(data),
})
}
}
func (suite *ElectronicMessageCacheSuite) storingNonText(id int) func(*resource.Store) {
return func(store *resource.Store) {
_ = store.Put(resource.ID(id), resource.Resource{
Properties: resource.Properties{
ContentType: resource.Sound,
Compound: false,
},
Blocks: resource.BlocksFrom([][]byte{{}}),
})
}
}
func (suite *ElectronicMessageCacheSuite) LocalizedResources(lang resource.Language) resource.Selector {
return resource.Selector{
From: suite.localizedResources,
Lang: lang,
}
}
@@ -0,0 +1,262 @@
package text_test
import (
"testing"
"github.com/inkyblackness/hacked/ss1/content/text"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type ElectronicMessageSuite struct {
suite.Suite
cp text.Codepage
}
func TestElectronicMessageSuite(t *testing.T) {
suite.Run(t, new(ElectronicMessageSuite))
}
func (suite *ElectronicMessageSuite) SetupTest() {
suite.cp = text.DefaultCodepage()
}
func (suite *ElectronicMessageSuite) TestEncodeBasicMessage() {
message := text.EmptyElectronicMessage()
message.Title = "1"
message.Sender = "2"
message.Subject = "3"
message.VerboseText = "4"
message.TerseText = "5"
encoded := message.Encode(suite.cp)
suite.verifyBlock(0, encoded, []byte{0x00})
suite.verifyBlock(1, encoded, []byte{0x31, 0x00})
suite.verifyBlock(2, encoded, []byte{0x32, 0x00})
suite.verifyBlock(3, encoded, []byte{0x33, 0x00})
suite.verifyBlock(4, encoded, []byte{0x34, 0x00})
suite.verifyBlock(5, encoded, []byte{0x00})
suite.verifyBlock(6, encoded, []byte{0x35, 0x00})
suite.verifyBlock(7, encoded, []byte{0x00})
}
func (suite *ElectronicMessageSuite) TestEncodeMeta_A() {
message := text.EmptyElectronicMessage()
message.NextMessage = 0x20
message.ColorIndex = 0x13
message.LeftDisplay = 30
message.RightDisplay = 40
encoded := message.Encode(suite.cp)
require.NotNil(suite.T(), encoded)
require.Equal(suite.T(), true, len(encoded) > 0)
suite.verifyBlock(0, encoded, suite.cp.Encode("i20 c13 30,40"))
}
func (suite *ElectronicMessageSuite) TestEncodeMeta_B() {
message := text.EmptyElectronicMessage()
message.IsInterrupt = true
message.LeftDisplay = 31
encoded := message.Encode(suite.cp)
require.NotNil(suite.T(), encoded)
require.Equal(suite.T(), true, len(encoded) > 0)
suite.verifyBlock(0, encoded, suite.cp.Encode("t 31"))
}
func (suite *ElectronicMessageSuite) TestEncodeMeta_C() {
message := text.EmptyElectronicMessage()
message.IsInterrupt = true
encoded := message.Encode(suite.cp)
require.NotNil(suite.T(), encoded)
require.Equal(suite.T(), true, len(encoded) > 0)
suite.verifyBlock(0, encoded, suite.cp.Encode("t"))
}
func (suite *ElectronicMessageSuite) TestEncodeCreatesNewBlocksPerNewLine() {
message := text.EmptyElectronicMessage()
message.VerboseText = "line1\n\n\nline2"
message.TerseText = "terse1\n\n\nterse2"
encoded := message.Encode(suite.cp)
require.NotNil(suite.T(), encoded)
require.Equal(suite.T(), true, len(encoded) > 0)
suite.verifyBlock(4, encoded, suite.cp.Encode("line1\n\n\nline2"))
suite.verifyBlock(6, encoded, suite.cp.Encode("terse1\n\n\nterse2"))
}
func (suite *ElectronicMessageSuite) TestEncodeBreaksUpLinesAfterLimitCharacters() {
message := text.EmptyElectronicMessage()
message.VerboseText = "aaaaaaaaa bbbbbbbbb ccccccccc ddddddddd eeeeeeeee fffffffff ggggggggg hhhhhhhhh iiiiiiiii jjjjjjjjj kkkkk"
encoded := message.Encode(suite.cp)
require.NotNil(suite.T(), encoded)
require.Equal(suite.T(), true, len(encoded) > 0)
suite.verifyBlock(4, encoded,
suite.cp.Encode("aaaaaaaaa bbbbbbbbb ccccccccc ddddddddd eeeeeeeee fffffffff ggggggggg hhhhhhhhh "))
suite.verifyBlock(5, encoded,
suite.cp.Encode("iiiiiiiii jjjjjjjjj kkkkk"))
}
func (suite *ElectronicMessageSuite) TestDecodeMeta() {
message, err := text.DecodeElectronicMessage(suite.cp, suite.holderWithMeta("i20 c13 30,40"))
require.Nil(suite.T(), err)
require.NotNil(suite.T(), message)
assert.Equal(suite.T(), 0x20, message.NextMessage)
assert.Equal(suite.T(), 0x13, message.ColorIndex)
assert.Equal(suite.T(), 30, message.LeftDisplay)
assert.Equal(suite.T(), 40, message.RightDisplay)
}
func (suite *ElectronicMessageSuite) TestDecodeMeta_Failure() {
_, err := text.DecodeElectronicMessage(suite.cp, suite.holderWithMeta("i20 c 13 30,40"))
assert.NotNil(suite.T(), err)
}
func (suite *ElectronicMessageSuite) TestDecodeMetaColorIs8BitUnsigned() {
message, err := text.DecodeElectronicMessage(suite.cp, suite.holderWithMeta("cD1"))
require.Nil(suite.T(), err)
require.NotNil(suite.T(), message)
assert.Equal(suite.T(), 0xD1, message.ColorIndex)
}
func (suite *ElectronicMessageSuite) TestDecodeMessage() {
message, err := text.DecodeElectronicMessage(suite.cp, suite.holderWithMeta("10"))
require.Nil(suite.T(), err)
require.NotNil(suite.T(), message)
assert.Equal(suite.T(), "title", message.Title)
assert.Equal(suite.T(), "sender", message.Sender)
assert.Equal(suite.T(), "subject", message.Subject)
assert.Equal(suite.T(), "verbose", message.VerboseText)
assert.Equal(suite.T(), "terse", message.TerseText)
}
func (suite *ElectronicMessageSuite) TestDecodeMessageIsPossibleForVanillaStubMails() {
message, err := text.DecodeElectronicMessage(suite.cp, suite.vanillaStubMail())
require.Nil(suite.T(), err)
require.NotNil(suite.T(), message)
assert.Equal(suite.T(), "", message.Title)
assert.Equal(suite.T(), "", message.Sender)
assert.Equal(suite.T(), "", message.Subject)
assert.Equal(suite.T(), "stub emailstub email", message.VerboseText)
assert.Equal(suite.T(), "", message.TerseText)
}
func (suite *ElectronicMessageSuite) TestDecodeMessageIsPossibleForMissingTerminatingLine() {
message, err := text.DecodeElectronicMessage(suite.cp, suite.holderWithMissingTerminatingLine())
require.Nil(suite.T(), err)
require.NotNil(suite.T(), message)
assert.Equal(suite.T(), "title", message.Title)
assert.Equal(suite.T(), "sender", message.Sender)
assert.Equal(suite.T(), "subject", message.Subject)
assert.Equal(suite.T(), "verbose text", message.VerboseText)
assert.Equal(suite.T(), "terse text", message.TerseText)
}
func (suite *ElectronicMessageSuite) TestRecodeMessage() {
inMessage := text.EmptyElectronicMessage()
inMessage.IsInterrupt = true
inMessage.NextMessage = 0x10
inMessage.ColorIndex = 0x20
inMessage.LeftDisplay = 40
inMessage.RightDisplay = 50
inMessage.VerboseText = "abcd\nefgh\nsome"
inMessage.TerseText = "\n"
data := inMessage.Encode(suite.cp)
outMessage, err := text.DecodeElectronicMessage(suite.cp, resource.BlocksFrom(data))
require.Nil(suite.T(), err)
require.NotNil(suite.T(), outMessage)
assert.Equal(suite.T(), inMessage, outMessage)
}
func (suite *ElectronicMessageSuite) TestRecodeMessageWithMultipleNewLines() {
inMessage := text.EmptyElectronicMessage()
inMessage.IsInterrupt = true
inMessage.NextMessage = 0x10
inMessage.ColorIndex = 0x20
inMessage.LeftDisplay = 40
inMessage.RightDisplay = 50
inMessage.VerboseText = "first\n\n\nsecond"
inMessage.TerseText = "terse\n"
data := inMessage.Encode(suite.cp)
outMessage, err := text.DecodeElectronicMessage(suite.cp, resource.BlocksFrom(data))
require.Nil(suite.T(), err)
require.NotNil(suite.T(), outMessage)
assert.Equal(suite.T(), inMessage, outMessage)
}
func (suite *ElectronicMessageSuite) verifyBlock(index int, blocks [][]byte, expected []byte) {
require.True(suite.T(), len(blocks) > index, "too few blocks")
assert.Equal(suite.T(), expected, blocks[index])
}
func (suite *ElectronicMessageSuite) holderWithMeta(meta string) resource.BlockProvider {
data := [][]byte{
suite.cp.Encode(meta),
suite.cp.Encode("title"),
suite.cp.Encode("sender"),
suite.cp.Encode("subject"),
suite.cp.Encode("verbose"),
suite.cp.Encode(""),
suite.cp.Encode("terse"),
suite.cp.Encode("")}
return resource.BlocksFrom(data)
}
func (suite *ElectronicMessageSuite) vanillaStubMail() resource.BlockProvider {
// The string resources contain a few mails which aren't used.
// They are missing the terminating line for the verbose text.
data := [][]byte{
suite.cp.Encode(""),
suite.cp.Encode(""),
suite.cp.Encode(""),
suite.cp.Encode(""),
suite.cp.Encode("stub email"),
suite.cp.Encode("stub email"),
suite.cp.Encode("")}
return resource.BlocksFrom(data)
}
func (suite *ElectronicMessageSuite) holderWithMissingTerminatingLine() resource.BlockProvider {
// This case is encountered once in gerstrng.res
data := [][]byte{
suite.cp.Encode(""),
suite.cp.Encode("title"),
suite.cp.Encode("sender"),
suite.cp.Encode("subject"),
suite.cp.Encode("verbose "),
suite.cp.Encode("text"),
suite.cp.Encode(""),
suite.cp.Encode("terse "),
suite.cp.Encode("text")}
return resource.BlocksFrom(data)
}
@@ -0,0 +1,33 @@
package text
type tabledCodepage struct {
tableToRune []rune
tableToByte map[rune]byte
}
func (cp *tabledCodepage) Encode(value string) []byte {
result := make([]byte, 0, len(value)+1)
for _, c := range value {
mapped, known := cp.tableToByte[c]
if !known {
mapped = cp.tableToByte['?']
}
result = append(result, mapped)
}
result = append(result, 0x00)
return result
}
func (cp *tabledCodepage) Decode(data []byte) string {
runes := make([]rune, 0, len(data))
for _, value := range data {
if value != 0x00 {
runes = append(runes, cp.tableToRune[value])
}
}
return string(runes)
}