Adding modding folder with a hacked build.
This commit is contained in:
@@ -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+">")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user